fix(send): submit multiline prompts + never nudge a session paused on a permission dialog#2357
Conversation
…-dialog nudge guard Ports AgentWrapper#2357 (fixes upstream AgentWrapper#2342) onto our fork's post-AgentWrapper#2349 consolidated adapter architecture. Upstream AgentWrapper#2357 was never rebased onto the adapter-layer consolidation (AgentWrapper#2349), so the per-agent activity.go edits it carries do not apply here; this reconstructs the same semantics against the consolidated layout. Three concerns, matching the upstream sub-PRs squashed into AgentWrapper#2357: A. Reintroduce ActivityBlocked (upstream #2) — a decision-pause distinct from waiting_input. permission-request now derives blocked in the shared activitystate deriver (the one consolidated file replacing the 8 per-agent copies); claudecode splits notifications (idle_prompt→waiting_input, permission_prompt/agent_needs_input→blocked) and adds permission-request → blocked; codex/droid derive the conservative blocked. Both states render needs_input, so the dashboard/frontend vocabulary is unchanged. Ingress accepts "blocked"; status uses the NeedsInput family. B. Send confirmation + sessionguard (upstream #5) — tmux pauses 300ms between a non-empty paste and the trailing Enter (detached from caller cancellation), fixing the multiline unsubmitted-draft race at its source; the new sessionguard package centralizes the just-in-time pre-paste blocked re-read; session_manager delivers through Guard.Deliver (409 SESSION_AWAITING_DECISION on a blocked session) and confirms via Guard.Nudge; lifecycle sendOnce routes through the guard. The Enter nudge is gated on ports.ActivitySignaler (EmitsSubmit + EmitsBlocked); goose/opencode/agy opt out of the nudge, copilot opts out entirely. C. Correlated stale-blocked clearing (upstream #7) — the activity signal carries {event, toolName, toolUseId}; lifecycle keeps per-session tool-flight state and clears a sticky blocked only when the exact approved tool's post fires or at a turn boundary, failing closed on ambiguity/restart. Gates: backend go build/vet/test green (the 4 pre-existing cli spawn tests fail on clean main too — sandbox daemon 404, unrelated); openapi.yaml + frontend schema.ts regenerated; frontend typecheck green. Canonical change is upstream AgentWrapper#2357; we carry the delta until it merges. Refs: AgentWrapper#2357, AgentWrapper#2342 Closes #9 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113G9d2yYNGxX37JyJPXFeZ
…-dialog nudge guard Ports AgentWrapper#2357 (fixes upstream AgentWrapper#2342) onto our fork's post-AgentWrapper#2349 consolidated adapter architecture. Upstream AgentWrapper#2357 was never rebased onto the adapter-layer consolidation (AgentWrapper#2349), so the per-agent activity.go edits it carries do not apply here; this reconstructs the same semantics against the consolidated layout. Three concerns, matching the upstream sub-PRs squashed into AgentWrapper#2357: A. Reintroduce ActivityBlocked (upstream #2) — a decision-pause distinct from waiting_input. permission-request now derives blocked in the shared activitystate deriver (the one consolidated file replacing the 8 per-agent copies); claudecode splits notifications (idle_prompt→waiting_input, permission_prompt/agent_needs_input→blocked) and adds permission-request → blocked; codex/droid derive the conservative blocked. Both states render needs_input, so the dashboard/frontend vocabulary is unchanged. Ingress accepts "blocked"; status uses the NeedsInput family. B. Send confirmation + sessionguard (upstream #5) — tmux pauses 300ms between a non-empty paste and the trailing Enter (detached from caller cancellation), fixing the multiline unsubmitted-draft race at its source; the new sessionguard package centralizes the just-in-time pre-paste blocked re-read; session_manager delivers through Guard.Deliver (409 SESSION_AWAITING_DECISION on a blocked session) and confirms via Guard.Nudge; lifecycle sendOnce routes through the guard. The Enter nudge is gated on ports.ActivitySignaler (EmitsSubmit + EmitsBlocked); goose/opencode/agy opt out of the nudge, copilot opts out entirely. C. Correlated stale-blocked clearing (upstream #7) — the activity signal carries {event, toolName, toolUseId}; lifecycle keeps per-session tool-flight state and clears a sticky blocked only when the exact approved tool's post fires or at a turn boundary, failing closed on ambiguity/restart. Gates: backend go build/vet/test green (the 4 pre-existing cli spawn tests fail on clean main too — sandbox daemon 404, unrelated); openapi.yaml + frontend schema.ts regenerated; frontend typecheck green. Canonical change is upstream AgentWrapper#2357; we carry the delta until it merges. Refs: AgentWrapper#2357, AgentWrapper#2342 Closes #9 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0113G9d2yYNGxX37JyJPXFeZ
…nly genuine ones (#93) * docs(orchestrator): triage needs_input — answer simple Qs, escalate only genuine ones Encode the orchestrator standing duty to triage every worker in needs_input and answer the self-resolvable ones directly instead of leaving them stuck for Nick. Expands the thin supervision-list item into a dedicated `needs_input triage` protocol: - inspect the pane first - answer via `ao send` when resolvable from issue/spec, task context, repo conventions, or reasonable engineering judgment (clarifications, yes/no, which-approach, proceed?, defaults) - escalate to Nick (@mention, the #87 escalation path) ONLY for product/judgment calls, unresolvable ambiguity, destructive actions, external credentials, or a genuine blocker - default is unblock, not escalate — a self-answerable stuck worker is a supervision defect Permission/destructive prompts always escalate and are never self-answered, mirroring the send/permission-dialog guard (AgentWrapper#2357); also added as a Hard line for prominence. Closes #90 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxPmZdfsrGTVfbCK5an3MA * docs(orchestrator): disambiguate merge go-ahead and engineering vs product judgment Address Codex review of PR #93: - A worker's "should I merge?" is not a self-answerable "proceed?" — merge go-ahead is governed by CLAUDE.md rule 6, never on the orchestrator's own initiative. - Split "judgment": engineering judgment (which-approach, defaults, conventions) is self-answerable; only product/business-judgment calls escalate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxPmZdfsrGTVfbCK5an3MA * docs(orchestrator): include step 4 in the conservative-escalation rule Address Codex re-review of PR #93: the closing "bias toward escalation ONLY for the categories in step 3" excluded step 4's always-escalate permission/destructive class. Widen to "steps 3 and 4" so permission prompts are unambiguously on the escalate side. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxPmZdfsrGTVfbCK5an3MA --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
b05f433 to
c292d84
Compare
|
Force-pushed: rebased onto current `main` and squashed the three original commits (#2 blocked state, #5 guarded send-confirmation, #7 correlated clearing) plus the later Guard decorator refactor into one commit. One behavior change worth flagging for review: `permission-request` now maps to `blocked` only for claude-code (and its hook-delegators grok/continue/devin). Those are the only harnesses that install the pre/post-tool-use trio, so they are the only ones where lifecycle can correlate the approved tool's post with the dialog and clear `blocked` mid-turn. Every other harness that mapped `permission-request`/`notification` to `blocked` could enter the state but never clear it before the turn boundary, so `ao send` returned 409 for the rest of the turn and automated nudges were suppressed — copilot/cline/kiro/cursor on every tool call (their pre-tool hook maps to permission-request), codex/qwen/autohand after a real dialog, droid on a 60s-idle notification. Those now map to `waiting_input` (still suppresses automated nudges via NeedsInput, still deliverable by user sends), and they opt out of the Enter nudge via `EmitsBlockedActivity()`. |
c292d84 to
a17870d
Compare
|
Rebased onto current Scope narrowed to claude-code only — it is the one harness with the pre/post-tool-use trio that lets lifecycle correlate the approved tool's post and clear |
37a0c96 to
f6d750e
Compare
…e-code) `ao send` returned 200 the moment the runtime paste + Enter exited 0, but a large multiline prompt's trailing Enter could be absorbed by the TUI — claude-code kept it as an unsubmitted draft, user-prompt-submit never fired, and the orchestrator could not tell a hung message from an idle worker (AgentWrapper#2342). Fix (claude-code): a 300ms paste-settle delay before Enter (mirrors conpty), then a bounded post-send confirm loop that re-sends a bare Enter until the durable Activity.State flips to active (3×2s). A new sessionguard package guards every pane write: Deliver refuses a blocked/missing/terminated session and reports why via a typed Outcome, so `ao send` to a session paused on a permission dialog returns 409 SESSION_AWAITING_DECISION instead of pasting Enter into the dialog. blocked is cleared by correlating the approved tool's post-tool-use with the dialog that blocked the session (lifecycle tool-flight). Scoped to claude-code — the only harness that installs the pre/post-tool-use trio enabling correlated clearing. codex keeps the submit signal but opts out of blocked (no tool trio). Every other harness maps its permission signal to waiting_input via the shared deriver and does not implement ActivitySignaler, so it gets the paste settle but no confirm loop. The prior 13-harness mapping set is preserved on fork/archive/blocked-mappings for restoration when a harness is adopted. Notification semantics (merged with upstream PR AgentWrapper#2541): idle_prompt → idle (finished turn, awaiting instruction); agent_needs_input → waiting_input; permission_prompt → blocked; agent_completed → idle. Zero migrations: the sessions CHECK already permits blocked; NeedsInput() covers both sticky states so the derived needs_input status is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019NxfHmtjXz1oVDwnF4R22t
Fixes the 12 golangci-lint findings + gofmt that failed the build-test and lint checks on this branch: - goimports/gofmt: order sessionguard before skillassets in session_manager/manager.go imports. - nilerr: Send's best-effort confirm no longer returns nil on a store error; it logs a warning instead (confirmation never fails the send). - revive: add the missing doc comment for EmitsBlockedActivity; rewrite the five sessionguard.Outcome const comments in the standard "Name ..." form. - staticcheck QF1008: drop the redundant embedded fakeStore selector in blockOnNthGetStore / blockAfterFirstReadStore (lifecycle + session_manager tests). gofmt clean, golangci-lint v2: 0 issues, affected tests pass. Co-Authored-By: Claude <noreply@anthropic.com>
f6d750e to
92166a0
Compare
|
Rebased onto the latest Conflict with the token-based design-system migration ( The earlier CI failures ( |
Address review on AgentWrapper#2357 (sessionguard.Guard.Send): Send folded a suppressed guard outcome into nil, so after-start prompt delivery (Spawn/Restore when the agent requests PromptDeliveryAfterStart) reported success even when the write was actually refused — a session that terminated or hit a permission dialog between readiness and injection was reported as a successful spawn whose prompt was never delivered. deliverAfterStartPrompt now calls Deliver directly and maps non-Sent outcomes to the existing ErrNotFound/ErrTerminated/ErrAwaitingDecision sentinels (same mapping the user-facing Send uses), so suppression fails the spawn and triggers the existing cleanup path. Guard.Send stays (it satisfies ports.AgentMessenger) but its comment no longer claims suppression is impossible at after-start delivery; it now warns that callers whose success contract depends on the write landing must call Deliver. Test: TestSpawn_AfterStartPromptSuppressedTerminationFailsSpawn covers the terminated-before-injection case (prompt not delivered, spawn fails). Co-Authored-By: Claude <noreply@anthropic.com>
illegalcall
left a comment
There was a problem hiding this comment.
Reviewed the latest push. The after-start prompt suppression issue is addressed by using Deliver directly, and the conflict with the frontend design tokens is resolved. Backend test suite passes locally.
Apply the same golangci-lint + gofmt fixes that unblocked PR AgentWrapper#2357, so this branch passes CI when it opens: - gofmt: manager.go imports (sessionguard before skillassets), project.go. - nilerr: Send's best-effort confirm logs the store error instead of returning nil on it. - revive: doc comment for EmitsBlockedActivity; standard "Name ..." form for the five sessionguard.Outcome consts. - staticcheck QF1008: drop redundant embedded fakeStore selector in blockOnNthGetStore / blockAfterFirstReadStore. gofmt clean, golangci-lint v2: 0 issues, full affected test suite passes. Co-Authored-By: Claude <noreply@anthropic.com>
Fixes #2342.
Problem
ao sendreturns HTTP 200 the momenttmux send-keysexits 0, but for a large multiline prompt claude-code's TUI can absorb the trailingEnterand leave the text as an unsubmitted draft (#2342).UserPromptSubmitnever fires, so the orchestrator can't tell the worker started and polls endlessly.Fixing that surfaced a second, sharper problem: a session paused on a permission dialog looked identical to a session waiting at an idle prompt, so an automated re-send (or any reaction nudge) could paste
Enterinto the dialog and answer a decision on the user's behalf — an unrecoverable action.What this does
Three commits, each a self-contained layer:
feat(activity): reintroduce blocked state— splits the overloadedwaiting_inputintowaiting_input(idle at the prompt, safe to message) andblocked(paused on a permission/approval decision, must not be auto-messaged). Zero DB migrations — thesessionsCHECK constraint already permitsblocked(it was removed as dead code without rebuilding the table). Adds aNeedsInput()predicate; both states map to the existingneeds_inputstatus, so the UI and telemetry are unchanged.fix(send): confirm prompt submission via a guarded pane-write primitive— afterSend, a bounded confirm loop (3×2s) re-sendsEnteruntil the session reportsactive, so a big multiline paste actually submits. All pane-writing paths (session_manager.Send/confirmActive,lifecycle.sendOnce) funnel through a newinternal/sessionguardprimitive that re-reads session state immediately before writing and refuses when the session isblocked/terminated (fail-closed).ao sendinto a blocked session now returns 409SESSION_AWAITING_DECISIONinstead of pasting into the dialog. tmux gains a 300ms paste→Enter settle (matching conpty), on a detached context so a cancel mid-pause can't strand a half-submitted draft.feat(activity): clear stale blocked via the approved tool's own post— answering a permission dialog fires no hook, so the stickyblockedwould otherwise persist until turn-end. claude-code installsPreToolUse/PostToolUse/PostToolUseFailure/PermissionRequesthooks; lifecycle keeps per-session tool-flight state and clearsblockedonly when the uniquely-identified approved tool's post arrives (or a turn boundary), correlating by the single in-flight tool of the blocking name — ambiguous same-name batches fail closed. Subagent/sibling tool traffic can never clear a live dialog. The activity signal gains optional{event, toolName, toolUseId}fields (additive; old daemons ignore them).Compatibility
No DB migrations, no changes to
ports.AgentMessenger,lifecycle.New/session_manager.Depssignatures, or the hooks wire protocol beyond three additive optional fields. Every adapter that doesn't tag its signals keeps last-writer-wins behavior.openapi.yaml/schema.tsregenerated.Testing
go build ./... && go vet ./... && go test -race ./...— all packages green; newsessionguardand tool-flight suites pin the guard and precedence invariants.blocked,ao sendreturns 409, and approving in the terminal clears it toactivebefore turn end via the approved tool's post.This work went through three rounds of adversarial review (Claude + Codex) on my fork; the same-name-sibling clear, an ambiguity-reset leak, and a reaper flight-state leak were each caught and fixed there.
🤖 Generated with Claude Code