Skip to content

Reconcile stuck agent state against transcript ground truth - #156

Merged
TraderSamwise merged 5 commits into
masterfrom
chore/tui-next-11
Jun 16, 2026
Merged

Reconcile stuck agent state against transcript ground truth#156
TraderSamwise merged 5 commits into
masterfrom
chore/tui-next-11

Conversation

@TraderSamwise

@TraderSamwise TraderSamwise commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Problem

The agent state machine is purely event-driven with no reconciliation: derived.activity only moves when a Claude/Codex hook arrives. When the clearing stop hook is dropped — /compact, interrupt, daemon down during the 10s hook window, or resume (no stop re-fires; stale state restored from disk) — the agent is stranded at running and reads WORKING forever, with nothing to recover it. A daemon restart similarly loses the in-memory interaction registry while a persisted needs_response lingers.

This is the root cause behind agents showing WORKING with "output 1h ago" and never settling to ready.

Fix — reconcile against ground truth (no decay, deterministic)

The agent's real turn-state is recorded deterministically in its transcript. Three phases:

1. Transcript turn-state readers (transcript-turn-state.ts, new) — pure helpers. Claude: scan back to the last assistant record, message.stop_reason (end_turn→complete, tool_use→in_progress), skipping the trailing bookkeeping records. Codex: last turn-boundary event_msg (task_complete/turn_complete/turn_aborted), matching cmux's signal set. Fail-open to unknown — never a false complete.

2. TranscriptReconciler (transcript-reconciler.ts, new) — 4s, project-service-scoped. Settles a stuck running/waiting+normal agent to idleready only when the transcript says the turn is complete and the file is quiescent across a tick (a working agent is still appending). Uses setActivity, not task_done, so it never bumps unseen counts or fires a "done" alert. Also clears needs_response stranded by a daemon restart.

3. Resume settle (runtime-state.ts) — eager, deterministic counterpart: a backend-resumed agent's stale running settles to idle immediately. Genuine needs_input (waiting) is preserved.

Covers Claude and Codex. cmux validated the Codex signals + tail-read approach; its own robustness is architectural (it pipes stream-json, which aimux's interactive-tmux model can't adopt), so a reconciler is the right adaptation.

Process

Built via plan-execute: 3 phases, each plan-audited and impl-audited by an independent agent (2 blockers caught + fixed in Phase 2: Codex path-cache pruning, reconciler gated on HTTP endpoint).

Tests

33 new tests across transcript-turn-state.test.ts, transcript-reconciler.test.ts, runtime-state.test.ts. Full suite 1370 passing, typecheck/lint/build clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a periodic transcript reconciliation loop to automatically settle session activity and clear stale attention based on on-disk transcript turn completion.
  • Bug Fixes

    • More reliably clears stale “needs_response” attention when no interaction is pending.
    • Prevents premature activity settling until the transcript is stable across scan cycles.
    • Improves offline resume so “running” sessions transition to “idle” when reattached.
  • Tests

    • Expanded test coverage for transcript turn-state probing, Codex transcript discovery/backoff, and reconciliation timing behavior.

test and others added 3 commits June 16, 2026 15:22
Pure helpers that read an agent's on-disk transcript (Claude/Codex JSONL)
to report whether the current turn is complete vs in-progress — the ground
truth for reconciling a stuck "working" state later.

- claudeTurnState: scans back to the last `assistant` record and reads
  `message.stop_reason` (end_turn => complete, tool_use => in_progress),
  skipping the trailing bookkeeping records Claude appends after a turn.
- codexTurnState: scans back to the last turn-boundary `event_msg`
  (task_complete/turn_complete/turn_aborted), mirroring cmux's signal set.
- readFileTail (256KB positional read), probeTranscript (stat+tail+parse),
  findCodexTranscriptPath (UUID-guarded recursive lookup).

Fail-open by design: truncation/partial/malformed input yields "unknown",
never a false "complete".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agent state machine is event-driven: derived.activity only moves when a
hook arrives, with no fallback. A dropped `stop` hook (compact, interrupt,
daemon down, resume) strands an agent at "running" so it reads "working"
forever, and a daemon restart loses the in-memory interaction registry while
the persisted "needs_response" attention lingers.

Add a periodic TranscriptReconciler (project-service-scoped) that corrects
both against ground truth:

- Stuck working: for an agent at activity running/waiting + attention normal,
  probe its transcript; if the turn is complete AND the file has gone quiescent
  across a tick (a working agent is still appending), settle it to idle so it
  derives "ready". Uses setActivity, not task_done — a correction must not bump
  unseen counts or fire a completion alert.
- Stranded needs_response: clear it once no live interaction remains.

Started/stopped with the project service (singleton-guarded, not gated on the
HTTP endpoint since it talks to the metadata server in-process).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A resumed agent is reattached at its prompt, not mid-generation, but
resumeOfflineSession restored the persisted derived state verbatim. A stale
activity:"running" (from a turn whose stop hook was dropped before the session
was shelved) therefore came back reading "working" forever.

On backend resume, settle a "running" activity to "idle" so it derives "ready".
A real prompt-submit hook re-marks it running the moment work resumes. Genuine
needs_input/blocked (activity "waiting") is preserved; "running" always pairs
with attention "normal", so nothing actionable is lost. This is the eager,
deterministic counterpart to the periodic transcript reconciler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jun 16, 2026 8:09am

@TraderSamwise

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdb73b21-d3ae-4ebf-b5b5-6bb0f4d34e2c

📥 Commits

Reviewing files that changed from the base of the PR and between d1d7ec9 and 5c9e535.

📒 Files selected for processing (2)
  • src/multiplexer/transcript-reconciler.test.ts
  • src/multiplexer/transcript-reconciler.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/multiplexer/transcript-reconciler.test.ts
  • src/multiplexer/transcript-reconciler.ts

📝 Walkthrough

Walkthrough

Adds a TranscriptReconciler that periodically probes on-disk transcript files (Claude and Codex) to detect completed turns and settle stuck session activity states. New MetadataServer methods expose reconciliation side effects. An eager settle is also applied during offline session resume when persisted activity is "running". The reconciler is wired into project service start/stop in dashboard-model.ts.

Changes

Transcript Reconciliation Feature

Layer / File(s) Summary
Transcript turn-state probing primitives
src/transcript-turn-state.ts, src/transcript-turn-state.test.ts
New module reads JSONL transcript tails synchronously and derives TurnState (complete/in_progress/unknown) for Claude and Codex backends. Exports probeTranscript (combining stat + tail + parse) and findCodexTranscriptPath (UUID-validated recursive search). Full test coverage including filesystem-backed tail/probe tests and Codex path discovery.
Claude transcript path derivation
src/backend-session-discovery.ts
Adds claudeTranscriptPath helper that deterministically builds the on-disk Claude transcript JSONL filepath from cwd and backendSessionId.
MetadataServer reconciliation hooks
src/metadata-server.ts
Adds reconcileSettleActivity (sets activity to "idle", fires onChange) and reconcileClearResponse (sets attention to "normal", fires onChange) as public methods for the reconciler to call.
Eager activity settle on offline session resume
src/multiplexer/runtime-state.ts, src/multiplexer/runtime-state.test.ts
In resumeOfflineSession, when using backend-resume and persisted derived.activity is "running", rewrites it to "idle" before resuming. Tests assert the runningidle rewrite and that waiting+needs_input attention is preserved.
TranscriptReconciler class and dependency contract
src/multiplexer/transcript-reconciler.ts, src/multiplexer/transcript-reconciler.test.ts
New TranscriptReconcilerDeps interface and TranscriptReconciler class with interval timer, reentrancy guard, two-tick quiescence requirement before settling, Codex path caching with miss backoff, stranded needs_response clearing, and stuck-activity settlement. Tests cover quiescence, path cache lifecycle, stale-response clearing, and codex miss backoff.
Wiring into project services
src/multiplexer/dashboard-model.ts
Imports TranscriptReconciler, constructs and starts it with metadataServer callbacks in startProjectServices, and stops and nulls it in stopProjectServices.

Sequence Diagram(s)

sequenceDiagram
  participant Timer
  participant TranscriptReconciler
  participant probeTranscript
  participant MetadataServer

  rect rgba(70, 130, 180, 0.5)
    note over Timer,MetadataServer: Periodic reconciliation scan
    Timer->>TranscriptReconciler: interval tick → scan()
    TranscriptReconciler->>MetadataServer: hasPendingInteraction(sessionId)
    alt attention=needs_response, no pending interaction
      TranscriptReconciler->>MetadataServer: reconcileClearResponse(sessionId)
    end
    TranscriptReconciler->>probeTranscript: toolConfigKey, resolvedPath
    probeTranscript-->>TranscriptReconciler: {turn, size, mtimeMs} | null
  end

  rect rgba(34, 139, 34, 0.5)
    note over TranscriptReconciler,MetadataServer: Quiescence check (two ticks)
    alt turn=complete AND size+mtime unchanged from prior tick
      TranscriptReconciler->>MetadataServer: reconcileSettleActivity(sessionId)
    else transcript grew or still in_progress
      TranscriptReconciler->>TranscriptReconciler: store probe snapshot for next tick
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TraderSamwise/aimux#100: Both PRs modify src/multiplexer/runtime-state.ts—specifically resumeOfflineSession—adding reconciliation/patching logic around resumed sessions.

Poem

🐇 A rabbit reads the logs so keen,
And checks if agents' turns are clean.
"Still running?" — No, the tail says done!
Two ticks of quiet, then settle — one.
With quiescence confirmed and caches bright,
The multiplexer hops to idle light. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Reconcile stuck agent state against transcript ground truth' directly and clearly summarizes the main objective of the PR: implementing reconciliation of agents stuck in running state by checking transcript completion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/tui-next-11

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/multiplexer/transcript-reconciler.ts`:
- Around line 70-75: The current caching strategy only caches successful lookups
in the codexPathCache, but when findCodexTranscriptPath returns a falsy value
(cache miss), the code does not cache this miss. This causes the same expensive
lookup to re-execute on every tick for unresolved paths. Modify the cache logic
to store both successful paths and cache misses (using a sentinel value such as
null or a special marker) so that repeated lookups for the same session.id that
previously failed do not trigger expensive filesystem scans on every interval.
This pattern should be applied consistently wherever similar Codex path
resolution logic exists in the codebase.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed34d5d1-de6e-4cbe-b492-3c48472e6ab4

📥 Commits

Reviewing files that changed from the base of the PR and between f53bfd3 and 81e1cc0.

📒 Files selected for processing (8)
  • src/metadata-server.ts
  • src/multiplexer/dashboard-model.ts
  • src/multiplexer/runtime-state.test.ts
  • src/multiplexer/runtime-state.ts
  • src/multiplexer/transcript-reconciler.test.ts
  • src/multiplexer/transcript-reconciler.ts
  • src/transcript-turn-state.test.ts
  • src/transcript-turn-state.ts

Comment thread src/multiplexer/transcript-reconciler.ts
…clears

- claudeTurnState: treat any terminal stop_reason (max_tokens, refusal, …) as
  complete, not just end_turn/stop_sequence — a context-maxed agent with a
  dropped stop hook was never reconciled. Also report in_progress when a new
  user prompt already follows the last end_turn, closing a false-settle window
  while the model is thinking before its first token.
- Codex path lookups now back off after a miss (8 ticks) instead of walking the
  ~/.codex/sessions tree every tick; recursion is depth-capped. (CodeRabbit)
- Dedupe the Claude transcript-path encoding into a shared
  claudeTranscriptPath() in backend-session-discovery so it can't drift.
- needs_response is only cleared after staying unbacked for a second tick, so a
  fast daemon restart mid-re-registration can't clear a live interaction.
- probeTranscript stats once and passes the size to readFileTail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TraderSamwise

Copy link
Copy Markdown
Owner Author

Sub-agent reviewer (orphan finding, not posted inline) — fixed in d1d7ec9: claudeTurnState previously returned unknown for terminal stop reasons other than end_turn/stop_sequence (e.g. max_tokens, refusal), so a context-maxed agent whose stop hook was dropped would never be reconciled. It now treats any terminal stop_reason as complete (and pause_turn/tool_use as in-progress), and reports in-progress when a new user prompt already follows the last end_turn — closing a false-settle window while the model thinks before its first token. Covered by new tests in transcript-turn-state.test.ts.

@TraderSamwise

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/multiplexer/transcript-reconciler.ts`:
- Around line 39-42: The cache maps codexPathCache and codexMissUntil are keyed
by session.id, but the actual Codex lookups use session.backendSessionId. If
backendSessionId changes for a live session (around lines 73-83), the old cache
entries keyed by session.id will be stale and point to the wrong transcript.
Detect when backendSessionId has changed for a session and clear the
corresponding entries from both codexPathCache and codexMissUntil maps to
prevent stale cache hits/misses from being used with the new backend ID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1e19d81-3e67-4c1f-b701-45ead1a81c19

📥 Commits

Reviewing files that changed from the base of the PR and between 81e1cc0 and d1d7ec9.

📒 Files selected for processing (5)
  • src/backend-session-discovery.ts
  • src/multiplexer/transcript-reconciler.test.ts
  • src/multiplexer/transcript-reconciler.ts
  • src/transcript-turn-state.test.ts
  • src/transcript-turn-state.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/transcript-turn-state.ts

Comment thread src/multiplexer/transcript-reconciler.ts Outdated
The codex path cache and miss-backoff were keyed only by session id, but the
lookup identity is backendSessionId. If a live session's backend id is rewritten
(e.g. a fresh relaunch that supersedes the old id), the stale cached path/miss
would be reused. Tag both maps with the backendSessionId they were resolved for
and re-resolve on mismatch. (CodeRabbit)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TraderSamwise

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@TraderSamwise
TraderSamwise merged commit 3112040 into master Jun 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant