Skip to content
This repository was archived by the owner on Oct 13, 2026. It is now read-only.
This repository was archived by the owner on Oct 13, 2026. It is now read-only.

Prism jobs go silent: review changes orphan the controller, stall transfer races interrupt, cancel cannot finish #132

Closed as not planned

Description

@lukemaj

Outcome

Prism jobs never go silent. Every job either keeps advancing or reaches a reported state the planner is woken for. A stalled worker moves to the next route, a review asking for changes goes back to a worker, and cancel works from Prism.

Evidence and causes (2026-09-27, Router 0.37.0, policy 2.10.0, T3 planner; jobs ao-27-t3-attribution-2 and -3)

1. A review asking for changes orphans the job (highest impact; since 0.36.0 / #126, commit 7984bd5)

  • _review_gate returns {"action": "review-changes-requested"} (controller.py ~3057) after persisting the correction envelope. run_controller_process only keeps looping for actions in continuing (controller.py ~4204), and that tuple lacks review-changes-requested. So the loop breaks with the job still running. running is not in TERMINAL_REPORT_STATUSES, so no terminal report is sent, the lease is released, and nothing ever relaunches the controller.
  • Observed on -3: review_verdict request_changes at 19:01:29.896, controller_exited at 19:01:29.905. Status stayed running with owner_pid null, and the planner was never woken. The user found the dispatcher thread idle 13 minutes later.
  • The same gap applies to {"action": "resumed"} from the recovered-envelope path in _step_inner (~3890); not verified live.
  • Every job whose first review requests changes is affected.

2. Stall to route transfer races the interrupt, so the job blocks instead of moving route

  • controller.py ~862-880 dispatches thread.turn.interrupt, then immediately reads one snapshot. T3 applies the interrupt asynchronously. On -2 the interrupt was requested at 18:03:07.781 and the turn became interrupted at 18:03:07.839 (T3 projection_turns). Router's single read saw a live turn, so idle_confirmed=false, then route_transfer_abort_failed at 18:03:08.05, with 4 of 5 attempts unused.
  • Fix: poll the snapshot (for example every 0.5 s, up to about 15 s) until the turn is completed, error or interrupted. Treat a thread whose session is stopped with no active turn as idle.
  • _interrupt_saved_t3_threads (core.py ~1861) has the same interrupt-then-read-once pattern.

3. Cancel cannot finish outside Prism; jobs stick in cancelling and keep their workspace

  • cancel and recover --request-id from a shell record t3_turn_interrupt_failed: T3 client unavailable. discover_token needs T3_SERVER_TOKEN or a t3 CLI; neither exists outside the Prism-launched environment (no t3 on PATH on this Mac). _finalize_stopped then keeps cancelling with the misleading reason cancellation_pending: live child process group remains. Workspace ownership stays, because ACTIVE_WORKSPACE_STATUSES includes cancelling.
  • Prism exposes no cancel tool, so no working cancel path exists for T3 jobs. -2 and -3 are still cancelling now.
  • Fix: add a prism_cancel tool in t3code that runs cancel and recover with Prism's token. The Router should also report "T3 unreachable" rather than "live child process group". Once every saved turn is already terminal in the last known state, releasing the workspace needs no T3 call.

4. The 60 s silence threshold is likely too aggressive for hidden-reasoning models (inconclusive)

  • T3_SILENCE_SECS = 60. Reasoning parts are not T3 activities. In OpenCode's database, the -2 worker's final part was a reasoning part created 20:01:22 local and never updated until the abort at 20:03:07 (MessageAbortedError). It could be a provider hang or long hidden thinking; the evidence cannot tell them apart.
  • The user's measured OpenCode baseline: healthy sessions reach about 160 s of silence, and the watchdog kills at 180 s.
  • Fix: count OpenCode reasoning-part updates as liveness where readable, or raise the silence window to about 180 s for OpenCode routes.

Acceptance criteria

  • review-changes-requested (and resumed) continue the loop. Any other non-terminal, non-blocked exit is marked blocked with a named reason (controller_exit_unhandled: <action>), so the planner is always woken. Regression test: a review with request_changes goes to the correction worker, then re-review, without relaunch.
  • Stall transfer polls until the turn is settled. Regression test using a fake server that settles the interrupt after a delay.
  • A cancelled T3 job reaches cancelled and frees its workspace through a Prism cancel path. The block reason names T3 unreachability accurately.
  • The silence threshold for OpenCode routes is aligned with measured liveness, or reasoning updates count as activity.

Contributes to the Objective

Reliable dispatch without the human on the critical path: jobs no longer vanish or need manual recovery.

Activity

  1. changed the title [-]Stalled T3 worker blocks the job instead of moving route; cancelling a blocked job never releases the workspace[/-] [+]Prism jobs go silent: review changes orphan the controller, stall transfer races interrupt, cancel cannot finish[/+] on Sep 27, 2026
  2. lukemaj commented on Sep 27, 2026

    @lukemaj
    ContributorAuthor

    Elon record (2026-09-27, after the user's requirement: stream liveness including thinking, no fixed timeouts, exact-second detection at 99.999% certainty)

    Wanted result: every Prism job either advances or wakes the planner. A dead worker is detected within seconds of its stream stopping; a working one is never killed. Cancel works.

    New evidence:

    Cut:

    • Fixed silence windows as the decision rule.
    • The continuing allowlist in run_controller_process, the source of bug 1: continuation becomes status-based.
    • The single post-interrupt read.
    • The misleading "live child process group" reason when T3 is unreachable.
    • A separate watchdog process.
    • Router reading provider databases directly: T3 already sees every token.

    Smallest surviving solution:

    1. Controller: loop while the job is not terminal, not blocked and not waiting on the planner. Any unexpected exit blocks with controller_exit_unhandled: <action>, so the planner is always woken.
    2. Interrupt: poll until the turn is completed, error or interrupted (0.5 s steps, capped). A stopped session with no active turn counts as settled. Apply this in the stall path and in _interrupt_saved_t3_threads.
    3. Liveness: silence = T3 now minus lastStreamAt while no tool is open (Stream clock for Prism: exact-second worker liveness from every token, plus prism_cancel chromeria#55). A turn is stale when silence exceeds the route's measured healthy tail and a confirming probe agrees. Thresholds are policy data per route and phase (time to first token versus mid-stream), derived from the per-turn statistics T3 records. Until a route has enough samples, its threshold stays conservative at the largest observed healthy gap times a margin, never lower than the evidence. Every turn records its max gap and time to first token in the ledger, which Observer uses.
    4. Cancel: prism_cancel (t3code#55). Router finalizes cancelled when T3 reports every saved turn terminal, and names T3 unreachability accurately.

    Order: 1, 2 and the Router half of 4 now. 3 after t3code#55 lands and Chromeria is rebuilt; until then, raise the T3 window to the measured evidence instead of 60 s.

    Falsifier: a healthy turn is interrupted as stale (review each stalled against the T3 record), or a dead turn runs past threshold plus one poll.

  3. lukemaj commented on Sep 27, 2026

    @lukemaj
    ContributorAuthor

    Correction to the Elon record above: the -2 worker was probably not dead. pmset -g log shows clamshell sleep from 20:01:26 to 20:03:05 +0200 (99 s), 4 s after its last reasoning segment (20:01:22). Router killed the turn at 20:03:07, 2 s after wake, and counted the sleep as silence. The same cause explains the "45 s late" detection. The fix branch no longer counts sleep time as silence.

  4. added 2 commits that reference this issue on Sep 27, 2026
  5. lukemaj commented on Sep 27, 2026

    @lukemaj
    ContributorAuthor

    Router side delivered in PR #135 (not merged).

    • Branch: fix/132-prism-silent-jobs
    • Head: 25c0e15fc0380c71ea68ab7d7a8ac24bcae9fb16 (one commit on cf2bda5)
    • Version: 0.38.0 (not tagged or released)
    • Proof: python3 scripts/test.py && python3 -m compileall -q runner scripts tests gives 322 tests OK. versionctl release-check OK. The new regression tests in tests/test_issue132.py fail on 0.37.0 the same way the live jobs failed.
    • Review: independent reviewer (Codex gpt-6-luna) gave APPROVE on 25c0e15 with no findings, after two blockers in the settle check were fixed.

    Covered: bug 1 (the controller keeps stepping while the job is active; an unexpected stop blocks as controller_exit_unhandled: <action>), bug 2 (after an interrupt, poll until the turn settles, in the stall path and in cancel), the Router half of bug 3 (cancel finalizes without T3 when every saved turn's last known state is finished; the reason names T3 unreachability), and bug 4 interim (measured windows per driver: Codex 300 s, OpenCode 210 s, Claude 90 s, others 300 s, plus a t3_turn_stream ledger event per turn).

    New finding on bug 4: the "45 s late" detection on -2 was a 99 s macOS clamshell sleep (20:01:26 to 20:03:05 local, from pmset -g log). The worker's last evidence was 4 s before the sleep, and the Router counted the sleep as silence. Another job was interrupted 1 s earlier for the same reason, and T3 wrote no event from any thread during the sleep. So the -2 worker was probably not dead. Host sleep no longer counts as silence.

    Remaining, so the PR says "Refs" and not "Closes": prism_cancel (toolboxmd/chromeria#55), which -2 and -3 still need because they predate the recorded turn states; and Elon item 3, stream-clock liveness from T3's lastStreamAt/openTool.

  6. lukemaj commented on Sep 28, 2026

    @lukemaj
    ContributorAuthor

    Another occurrence, 2026-09-27, job prism-muk1zqbl-09879193 (lukemaj/transcribe-queue#3), route muse-spark-xhigh-free, turn impl_5: blocked with route_transfer_abort_failed: session not confirmed idle after stalled after 101.7 s of silence (last_part: message:system), 1 of 5 attempts used.

    New consequence worth covering here: the stalled worker had already finished an independent Luna review (request_changes, 7 majors) and written it to $TMPDIR/opencode/luna-review-issue3.log at 19:57; it went silent while parsing it at 20:00:30. The job blocked and the verdict never reached the dispatcher or planner. A later reviewer approved a nearby SHA without those findings, and the PR merged. Suggest: on a stalled turn, salvage the worker's latest artifacts and text into the terminal report before blocking or moving route.

    Role and capability causes from the same job are in #138.

  7. lukemaj commented on Sep 28, 2026

    @lukemaj
    ContributorAuthor

    Plan for the remaining Elon item 3 (stream-clock liveness), 2026-09-28.

    Chromeria #56 is merged and installed; /api/prism/liveness is live. Revision to the record above: the stale decision itself moves into T3 (toolboxmd/chromeria#62), because spawn_thread children have no Router and one detector beats two. Router consumes it:

    1. Rebase fix/132-prism-silent-jobs (PR Keep Prism jobs advancing with T3 stream liveness (#132); 0.41.0 #135) onto current main (0.40.0) and bump to the next version.
    2. Controller liveness: poll GET /api/prism/liveness?threadId= every 1 s for the running worker thread. When the response carries stale: true, treat the turn as stalled (existing interrupt, settle, retry-on-next-route path). When the field is absent (older Chromeria) or the route is unreachable, fall back to the interim per-driver windows from Keep Prism jobs advancing with T3 stream liveness (#132); 0.41.0 #135.
    3. Before interrupting a stale turn, salvage the worker's latest text and any artifacts it named into the terminal report, so findings are not lost (the prism-muk1zqbl case above).
    4. Record each stale decision in the ledger with silenceMs, thresholdMs, thresholdSource and reason, for the falsifier review.
    5. The planner thread (planner_t3_thread) is told on every stale event.

    Falsifier unchanged: a healthy turn interrupted as stale, or a dead turn running past threshold plus one poll.

  8. added 2 commits that reference this issue on Sep 28, 2026
  9. lukemaj commented on Sep 28, 2026

    @lukemaj
    ContributorAuthor

    PR #135 updated for the 2026-09-28 liveness plan, ready for the dispatcher's independent review.

    • Base: cecc4b30be7adbe32f50066e6cce8d9e83ada065 (main 0.40.0). Main's Wait for T3 instead of blocking when Prism is unreachable #136 waiting behavior and Start Chromeria when T3 is down instead of only waiting #140 launch recovery are preserved.
    • Branch: fix/132-prism-silent-jobs; existing workspace model-router-worktrees/132-prism-silent-jobs, clean.
    • Head: 0ab66fb7bf52f828cc65788740c2c9fd2eff4192.
    • Commits: 61c239648aa34dc6a6a326005dc26312220fcc0c (rebase checkpoint), 78e131e754e7a6baa3da573668c73213006a9604 (liveness and salvage checkpoint), 0ab66fb7bf52f828cc65788740c2c9fd2eff4192 (final integration and version).
    • Version: 0.41.0, prepared by versionctl with all three plugin mirrors and CHANGELOG updated.

    T3 owns staleness. Router polls every second, acts on stale: true within one poll, and never interrupts for elapsed silence while stale: false. Missing-field/unreachable-route fallback retains #135's driver windows and host-sleep exclusion. Confirmed T3 staleness interrupts, waits for settling, then moves to the next route. No new watcher or Router-owned timeout was added.

    Before interrupting, Router durably salvages the current turn's text and named text artifacts, records silenceMs, thresholdMs, thresholdSource, reason and staleSince, and notifies planner_t3_thread. Salvage survives route moves and later success in the terminal report. Artifact copy/excerpt limits and unavailable references are explicit in RUNNER.md and reports.

    Proof for this candidate's tree 1f4e89fc831a1c9b16f1d6f067ce505d74de9925:

    • python3 scripts/test.py && python3 -m compileall -q runner scripts tests: 360 tests OK; compileall exit 0.
    • versionctl release-check: v0.41.0 candidate OK at 0ab66fb7bf52f828cc65788740c2c9fd2eff4192.
    • GitHub proof: SUCCESS on the pushed candidate.
    • Regression coverage includes true/false authority, both fallbacks, host sleep, delayed interrupt settling, stale predecessors behind queued messages, salvage before interrupt and after retries, ledger evidence, and repeated planner notification. Key liveness regressions were observed failing before their fixes.

    Independent review verdict: pending on this exact head, owned by the dispatcher through Prism's review role per the planner's direction. GitHub currently has no formal reviews or review/independent status for this candidate; the earlier head's approval is not reused. Next step: review this SHA, publish findings and the exact-SHA status, then update this handoff with the reviewer identity and verdict.

    Verified pushed head and open PR against main; GitHub reports MERGEABLE. The PR retains Refs #132. Chromeria #62 is covered with fakes as requested, not live-verified; existing production jobs were not modified. No merge, tag, release or installation. Website/SEO impact: none, because this is runner behavior and operational documentation.

  10. lukemaj commented on Sep 28, 2026

    @lukemaj
    ContributorAuthor

    Live false positive on installed Router 0.40.0, 2026-09-28, job mr-132-stream-liveness: the dispatcher resume turn (gpt-6-luna@max, thread sub.3cecf5f2-….3866cdbc) completed normally at 18:29:44Z (T3 prism.stream-stats: outcome completed, durationMs 248202, maxGapMs 7085, 1287 events). Router blocked the job at 18:30:43Z with t3_resume_failed: stalled, 59 s after the turn had already completed, and the dispatcher's review action was lost. Another case for the stream-clock liveness this PR adds: a completed turn must never read as stalled.

  11. added a commit that references this issue on Sep 28, 2026
  12. lukemaj commented on Oct 1, 2026

    @lukemaj
    ContributorAuthor

    Superseded by toolboxmd/chromeria#92: the controller, stall transfer and runner cancel are deleted. Stopping a child is interrupt_thread (toolboxmd/chromeria#95, #102); report-back survives restarts (#94, #103).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions