You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#727, #739, and #680 are one family: "waiting on a human", "working quietly", and "wedged" are semantically distinct states that produce identical observables at the orchestrator's keyhole (pane-log (mtime_ns, size) via _log_activity_key, pane silence, window liveness). Every deterministic rule at that spot is lossy at the margins, not merely unfinished — #680's own counter-example is a blocked 59-minute docker run being indistinguishable from a deliberate sleep-poll, and #617/#610 established that pane text is model-authored, so vocabulary-based detection collides with healthy story output by construction.
Structured hook events (#745) are the first line and settle the family deterministically wherever the CLI emits them. This issue covers the residual margin: profiles whose hook maps end at Stop, and states no CLI exposes as an event.
Proposal sketch — LLM as untrusted sensor, never authority
This deliberately touches the "no LLM calls in the control loop" invariant, so the design must sharpen the rule rather than erode it. Proposed restatement: the control loop gains no LLM authority. Concretely:
Consult points: only at deterministic decision points that already exist — stall-grace expiry pre-nudge in BaseGenericAdapter.wait_for_completion, and pre-crashed verdict. Never on the hot path, never on a timer of its own.
Input: the last screenful of the pane (bounded capture), nothing else. No conversation history, no repo access.
Output: a closed enum — waiting_on_human | working | wedged | unknown — schema-validated; any free text goes only to the journal.
Failure direction: a false waiting_on_human on a genuinely dead session parks the run instead of retrying — degraded liveness, not damage — and is bounded: N consecutive parked verdicts force the escalation path.
Threat model: pane bytes are session-authored and therefore attacker-influenced; the classifier is prompt-injectable by construction. The closed enum + act-gate is the boundary, not the prompt. A hostile session can at worst talk itself into a pause — never into an action.
Cost: bounded consults per session (policy knob), off by default.
Open questions (why needs-design)
What runs the consult: a headless one-shot invocation of the run's own adapter binary? A fixed profile? How do the zero-token invariants for live/E2E tests hold (tests must stub it — same posture as the mock adapter)?
Policy surface: field names, defaults in data/settings/core.toml (sync test), per-run cap.
Journal/TUI shape for verdicts, so a withheld nudge is visible rather than a new silent degrade.
No LLM anywhere near merges, teardown, sweep decisions, verify.py, completion detection, or any repair write. Those stay deterministic unconditionally.
Problem
#727, #739, and #680 are one family: "waiting on a human", "working quietly", and "wedged" are semantically distinct states that produce identical observables at the orchestrator's keyhole (pane-log
(mtime_ns, size)via_log_activity_key, pane silence, window liveness). Every deterministic rule at that spot is lossy at the margins, not merely unfinished — #680's own counter-example is a blocked 59-minute docker run being indistinguishable from a deliberate sleep-poll, and #617/#610 established that pane text is model-authored, so vocabulary-based detection collides with healthy story output by construction.Structured hook events (#745) are the first line and settle the family deterministically wherever the CLI emits them. This issue covers the residual margin: profiles whose hook maps end at Stop, and states no CLI exposes as an event.
Proposal sketch — LLM as untrusted sensor, never authority
This deliberately touches the "no LLM calls in the control loop" invariant, so the design must sharpen the rule rather than erode it. Proposed restatement: the control loop gains no LLM authority. Concretely:
BaseGenericAdapter.wait_for_completion, and pre-crashedverdict. Never on the hot path, never on a timer of its own.waiting_on_human | working | wedged | unknown— schema-validated; any free text goes only to the journal.STALL_NUDGE_TEXT, extend the grace, or pause + escalate. It may never authorize keystrokes, a retry, a kill, teardown, or completion. Session completion remains hook-Stop / window-death only (PR fix(adapters): complete dev sessions only on Stop or window death #53 invariant untouched). Awaiting_on_humanverdict pausing instead of verdictingcrashedis what sparesStoryTask.attempt(A dev session waiting on an interactive permission prompt is recorded ascrashed, andretryburns the last attempt #727's burn sites:Engine._dev_phase,Engine._fix_phase).waiting_on_humanon a genuinely dead session parks the run instead of retrying — degraded liveness, not damage — and is bounded: N consecutive parked verdicts force the escalation path.Open questions (why needs-design)
data/settings/core.toml(sync test), per-run cap.sleep#680 remains once the structuredidle_promptand zero-token legs land — this classifier should only be built for whatever margin measurably survives those.Non-goals
No LLM anywhere near merges, teardown, sweep decisions,
verify.py, completion detection, or any repair write. Those stay deterministic unconditionally.