Skip to content

Stream clock for Prism: exact-second worker liveness from every token, plus prism_cancel #55

Closed

Description

@lukemaj

Outcome

Prism knows the exact moment a worker thread's model stops streaming, from every token T3 receives (text, reasoning or thinking, tool output). Prism can also cancel a job. Model Router uses both to detect stale turns with near certainty and never kill a working model (toolboxmd/model-router#132).

Why (evidence, 2026-09-27)

  • T3 receives every provider token delta in memory (OpenCodeAdapter message.part.delta becomes content.delta; Claude stream_event deltas and system/thinking_tokens; Codex item/*Delta). But it exposes them only as ~100-char reasoning or assistant message segments. EventNdjsonLogger deliberately drops deltas.
  • Router therefore measures silence from segment timestamps with a fixed 60 s window. On job ao-27-t3-attribution-2, the healthy Muse worker streamed reasoning segments every 5-15 s (max gap ~18 s). The dead turn's last segment was at 18:01:22 and nothing followed, yet Router only acted at 18:03:07. An 87-char segment that never closed is invisible at segment granularity.
  • Prism has no cancel tool. Router's CLI cancel cannot reach T3 without Prism's token, so cancelled T3 jobs stay cancelling (model-router#132, bug 3).

Elon record

  • Wanted result: exact-second staleness for every provider, measured where the tokens already arrive, plus a working cancel.
  • Cut: a separate watchdog process, Router reading provider databases, fixed timeouts, logging every delta to disk.
  • Smallest surviving solution: an in-memory per-thread stream clock in the fork (new files), exposed through Prism, plus per-turn gap statistics that make thresholds measurable.

Acceptance criteria

  • Per thread, the server keeps lastStreamAt (the timestamp of the latest provider event of any kind, including every token delta, reasoning delta, Claude thinking_tokens, and tool output delta) and openTool (a tool call in progress, with its start time). Both are in memory only. No per-delta persistence.
  • Prism exposes them per thread (for example GET /api/prism/liveness?threadId=, or in the existing Prism snapshot) with the server's now, so the caller computes silence without clock skew.
  • At each turn end, the server records one activity with the turn's stream statistics: time to first token, max inter-event gap while no tool was open, event count, provider, model and outcome (completed, aborted, error). This is the measurement source for thresholds.
  • A prism_cancel MCP tool runs Router cancel and then recover with Prism's token for a job this thread owns, and returns the final state.
  • New files first. Any upstream edit is allowlisted and mapped in docs/fork-features.md.
  • Handler tests with fake provider streams, and a live check on a Chromeria build: lastStreamAt advances during a long reasoning turn and stops when the turn is aborted.

Non-goals

Stall decisions in T3 (Router owns policy and recovery), and changing upstream usage or logging.

Blockers

None. Installing the new Chromeria build on this Mac is the user's step.

Direction

This serves the current Objective, "bring Model Router into it". Reliable T3 jobs are part of Router jobs running as T3 threads.

Activity

  1. lukemaj commented on Sep 27, 2026

    @lukemaj
    ContributorAuthor

    Implementation is in PR #56. It is not merged.

    Design (Elon record applied). No adapter edits and no per-delta persistence. Every adapter already writes each native event (every token, reasoning and tool output delta, Claude thinking_tokens, Codex item/*Delta, OpenCode message.part.delta) to the shared native event logger. A new in-memory StreamClock wraps that logger and the canonical logger through one layer swap in server.ts. GET /api/prism/liveness?threadId= returns now, lastStreamAt, silenceMs, openTool and the running turn. Each turn end records one hidden prism.stream-stats activity. prism_cancel refuses a job whose planner_t3_thread is not the calling thread, then runs Router cancel and recover with a Prism token and returns the final status. Upstream edits: a one-line work-log filter each in web session-logic.ts and mobile threadActivity.ts (allowlisted and mapped), plus wiring in the already-forked server.ts and McpHttpServer.ts.

    Proof

    • Tests for the new and nearby code pass: src/prism, src/mcp, server.test.ts, ProviderService.test.ts and entrypoint.test.ts on the server; web session-logic; mobile threadActivity. Fake provider streams in streamClock.test.ts cover native-only deltas, tool silence left out of the max gap, abort and error outcomes, and logging off. handlers.test.ts covers the cancel sequence and the refusal for another thread's job.
    • vpr typecheck (all 15 packages), vp check (0 errors), knip:check: pass locally.
    • CI on the head: every job passes except Fork Stack Model. The mobile shikiReviewHighlighter test failed once, passed on rerun, and passes locally. It is unrelated to this PR.
    • scripts/fork-check.sh fails only on the stack size: main already has 20 commits on upstream, and 20 is the limit. With --max-commits 21 it prints fork-features: OK (17 features) and fork-check: OK. Any further fork PR hits this limit, so the limit needs raising or the stack needs consolidating. That is a fork-maintenance decision for you.
    • Live Chromeria check: not done. It is your step after installing this build. The exact steps are in the PR body under "Live check".

    Independent review. A reviewer child thread (Codex, gpt-6-luna) reviewed 89dce17 and returned APPROVE with no blockers or majors and one minor: the clock's per-thread map never evicts entries. I left that as is, because each thread's entry is a few numbers, and evicting it would lose the last known state that Router reads. On acceptance, the reviewer marked everything met except the tests-plus-live-check line, which is partial because the live check is pending.

  2. added 4 commits that reference this issue on Sep 28, 2026
    3b3c04e
    c39e2bf
    191cafb
    19a04dc
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