Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#62
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
deriveStateinfers conversational state from only the last content block ofthe main transcript
.jsonl— a guess with no positive "the agent is alive andproducing" signal. This one weakness produces two opposite, already-filed
symptoms:
working→ operator lockout (#38). A frozen / echo tail (after/clear, or an agent that stops after atool_resultwith no finalassistant:text) pinsworkingforever; the composer's Interrupt no-ops on anidle prompt.
background subagents write their own session files; the main transcript ends
with
assistant:text, so the run readsneeds_input/ idle while subagents arestill running.
This is the root-cause umbrella for both. To be designed by the maintainer
— filing now to capture direction; I'll dig in later.
Where it lives
deriveState—internal/provider/claudecode/chat.go:322(last-block → state).(
internal/chat/tailer.go:154), so a frozen tail is never re-evaluated; and itwatches a single file, missing sidecar subagent sessions (#39).
stop_reason/isSidechain/parentUuid/ in-flightTask(internal/provider/claudecode/chat_types.go,compat.md §5).Candidate directions (to weigh, not yet decided)
rendering?) or process CPU — a positive "producing" signal to gate
workingon, instead of / alongside the tail guess.
stop_reason; treat an unresolvedTask/isSidechainsub-task as"working" (#39).
change (heuristic-on-a-heuristic: a legit long tool writes no bytes for minutes
and would false-flip to idle) — recorded here so it is not re-proposed without a
real liveness signal.
Relationships
workinglockout) and #39 (false-idle forbackground workflows) — consider folding #39 in, or closing both against this
once designed.
explicitly does not touch this derivation; the wrong badge / state remains
until this lands.
Triage: ready-for-human
Category: bug
Summary:
deriveStateinfersworking/needs-inputfrom the last transcriptcontent-block type alone, with no positive liveness signal — confirmed still
live in the current code (
deriveState,internal/provider/claudecode/chat.go):a trailing
user:text | tool_use | tool_result | assistant:thinkingreportsStateWorkingunconditionally, andstop_reason/isSidechain/parentUuid/ in-flight
Taskare parsed nowhere in the transcript types. #51 (merged sincethis issue was filed) generalized the provider seam but did not touch this
derivation — the root cause the memory record for the
/clearlockout flags as"still live" is exactly this code, unchanged.
Current behavior:
State is a pure function of the last content-block key. No process/pane
liveness signal, no
stop_reason, no notion of an in-flight subagentTaskfeeds it. Two opposite symptoms follow: a frozen tail after a tool with no
final
assistant:textpinsworkingforever (#38's lockout), and a workflowwhose subagents are still running but whose main transcript ends in
assistant:textreads as idle/needs-input (#39).Why this is
ready-for-human, notready-for-agent: the issue's owncandidate directions are real architectural alternatives, not an implementation
detail — tmux pane-activity probe vs. process CPU probe vs. parsing
stop_reason/isSidechain/in-flightTaskvs. widening the tailer to watchsidecar session files, in any combination. A time-based staleness downgrade was
already considered and explicitly rejected (a legitimate long-running tool that
writes no bytes for minutes would false-flip to idle). Picking the mechanism is
a product/architecture call — the source of truth for "the agent is alive" has
correctness and performance trade-offs (a pane/CPU probe needs host access an
AFK agent may not have the same way; a transcript-only fix may not be able to
distinguish all cases) that only the maintainer should settle before an agent
brief can be written.
Acceptance criteria (for whichever direction is chosen):
assistant:textno longer pinsworkingforever once the agent has actually stopped producing.working, notidle/needs-input, for as long as those subagents run (#39).
tool that writes no bytes for minutes (the staleness-downgrade approach
is out — already rejected for this reason).
Out of scope:
ready-for-agent,independent of this fix).
cause — worth deciding at the same time whether to keep it separate or fold
it in once a direction is picked.
Relabeling
needs-triage→ready-for-human.Design decided — 2026-07-10 grill (maintainer + agent), full decision record
The "to be designed by the maintainer" part is done. This issue becomes the feature itself; two prerequisites were filed alongside (#92 seam refactor, #93 dead-session sweep).
Decisions
Mechanism: hook-edge lifecycle markers. Claude-code arms turn-lifecycle hooks in its existing per-run settings file (
Setup): prompt-submit / pre-tool fires write a working edge,Stopwrites an ended edge, into the run's spool next to the dialog marker. A positive "the agent is producing" signal — no host probes (pane/CPU false-idles during API waits), no transcript-staleness timeouts (already rejected above, stays rejected).Backstop: downgrades only on hard evidence. Driven by the future notification layer's asymmetry: false-working means a late ping, recoverable by just sending a message (composer is always-Send since #61, and the reply re-arms fresh edges); false-idle means a spurious ping, unrecoverable trust damage. So nothing infers a downgrade — no transcript-order checks, no timeouts. The only downgrade evidence is a dead session, enforced by #93's sweep (
EndRun(death, "session gone")→ state becomes ended through the ordinary lifecycle override). A missed Stop on a live agent stays pinned until the next interaction — accepted cost, self-heals.Seam consequence: liveness never becomes a seam concept. Composition moves wholly into the adapter via #92 (
ReadChat(runID, runtimeDir, transcriptPath);PendingDialog/BlockedStateleave the seam; core keeps only theStateEndedoverride). The liveness edges here are then a purely claudecode-private feature: arm inSetup, read inReadChat, fold into the marker'sSpoolSigdigest so the tailer notices a Stop while the transcript is byte-frozen. Core and the seam are untouched by this issue.State table (the semantics #92's conformance scenarios enforce per-adapter):
workingassistant:textneeds_inputtool_result/ otheridle(the #38 fix)question(dialogs dominate)The last row is both the migration story (a session spawned before this lands has no liveness hooks in its settings file — it keeps legacy behavior until it ends; live sessions are never re-armed) and honest degradation (a signal-less adapter never writes edges and keeps tail-guess semantics through the same path). Edges gate working-vs-not; the tail only picks the non-working flavor; adapters may differ on flavor (codex maps complete/aborted → needs_input) — conformance pins working-vs-not strictly, flavor is adapter policy.
Spike first (codex Tier-2 pattern). Before implementation, a live claude-code session driven over tmux pins in compat.md: does
Stopfire while background subagents / a Workflow still run; does subagent tool-use firePreToolUseinto the parent's hooks (refreshing the working edge); is there a usable start-edge for background work (PreToolUseon Task) to pair withSubagentStop; does Esc-interrupt fireStop(if not, the adapter may write the ended edge from its own Interrupt recipe — it drives the Esc); what/clearrotation does to markers. #39's acceptance criterion is scoped to what the spike proves: if background-subagent liveness is not reliably hook-observable, the criterion is renegotiated on the evidence (e.g. "no false working") rather than met with inference — consistent with decision 2.Codex needs no work — existence proof, not a follow-up. The originally planned "codex LiveSignals follow-up" issue is NOT filed: the codex adapter (#87/ADR-0037, merged) already derives state from rollout-native lifecycle events (
task_started/task_complete/turn_aborted) — a real liveness signal through the transcript file itself, visible to the tailer as ordinary writes, including a clean interrupt marker. The residual value of codex hooks (PermissionRequestdialogs if the never-ask posture ever changes; a notification substrate) stays recorded ininternal/compat/codex/compat.md§8 with the re-verification recipe.Plan (this issue, after #92 + #93 merge)
Setup, marker file in the spool, fold into claudecode'sReadChatcomposition per the state table,SpoolSigcovers the marker,SweepSpoolsGCs it.Acceptance (supersedes the triage comment's criteria)
assistant:textreads not-working once the agent stopped (the #38 fix; UI interrupt affordance follows the honest state).Sequencing
Blocked on #92 (adapter needs runID+runtimeDir at read time) and #93 (the backstop) for the implementation phase; the spike phase has no blockers. Relabeling
ready-for-human→needs-triageas the sequencing gate (the #87/#80 precedent): re-triage toready-for-agentwhen #92 and #93 merge — the design decisions above were the human part, now done.