diff --git a/README.md b/README.md index 41be2b4..776f994 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,8 @@ git diff | dispatch send --to work:agent --prompt "review this diff" # Type without submitting (leave it in the composer) dispatch send --to work:agent --prompt "draft" --no-submit -# Queue to an active Codewith/Claude pane that proves Tab queued-message support +# Queue to an active Codewith/Claude pane that proves queued-message support +# (Codewith queues with Tab, Claude Code queues with Enter) dispatch send --to open-dispatch:1.1 --prompt "Follow up safely" --queue --dry-run # Explicit submit key. Tab is accepted only when detection proves queue support. @@ -173,11 +174,12 @@ bulk send results include detection metadata when available: Normal prompt delivery uses `Enter` and refuses active agents unless `--force-active` is explicitly passed. `--queue` is the safe active-agent path: when detection proves the target supports queued-message behavior, dispatch types the prompt and presses -`Tab`; otherwise it refuses. Prompt sends wait until the delivered text is visibly +the agent's queue key (`Tab` for Codewith, `Enter` for Claude Code); otherwise it +refuses. Prompt sends wait until the delivered text is visibly parked in the composer before pressing Enter/Tab; if it never parks within -`DISPATCH_SETTLE_TIMEOUT_MS`, dispatch refuses the submit key. Queued Tab delivery +`DISPATCH_SETTLE_TIMEOUT_MS`, dispatch refuses the submit key. Queued delivery is single-shot to avoid duplicate queued follow-up inputs; `--retries` applies to -Enter submission. Detection supports +idle Enter submission. Detection supports direct binaries and compatible `node`/`bun`/`npx`/`bunx`/`pnpm`/`yarn`/`npm exec` launchers, but wrapper panes still need live composer UI proof so arbitrary `node` output and copied transcripts stay fail-closed. diff --git a/package.json b/package.json index 6839f29..46e6697 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hasna/dispatch", - "version": "0.0.24", + "version": "0.0.25", "author": "Andrei Hasna ", "repository": { "type": "git", diff --git a/src/lib/agent-recovery.test.ts b/src/lib/agent-recovery.test.ts index cf67b27..1c90cde 100644 --- a/src/lib/agent-recovery.test.ts +++ b/src/lib/agent-recovery.test.ts @@ -185,6 +185,38 @@ describe("agent triage and recovery", () => { expect(r.argvs().some((a) => a[1] === "send-keys" || a[1] === "paste-buffer")).toBe(false); }); + test("recover plans queued Enter recovery for active Claude Code seats, never Tab", async () => { + const activeClaudeCapture = ` +✶ Wandering… (12m 2s · ↓ 29.6k tokens) + +──────────────────────────────────────────────────────────────────────────────── +❯ +──────────────────────────────────────────────────────────────────────────────── + ⏵⏵ bypass permissions on · 2 monitors · esc to interrupt · ← for agents +`; + const claudeSeatProcessTree = ` +1234272 8038 Ss /bin/bash -l +1301071 1234272 Sl+ \\_ node /home/hasna/.local/bin/accounts launch account005 --tool claude --permissions dangerous +1302384 1301071 Sl+ \\_ claude --dangerously-skip-permissions +`; + const r = recoveryRunner(activeClaudeCapture, claudeSeatProcessTree); + + const result = await performAgentRecovery( + { target: "hq:staff", prompt: "Please post a one-line status.", lines: 20 }, + { tmux: new Tmux(r) }, + ); + + expect(result).toMatchObject({ + status: "planned", + dryRun: true, + // Claude Code queues with plain Enter; Tab is completion there, so a Tab + // recovery would corrupt the composer instead of queueing the prompt. + action: { kind: "queue", submitKey: "Enter", safeToApply: true }, + triage: { detection: { agentKind: "claude", composerState: "active", canQueuePrompt: true } }, + }); + expect(r.argvs().some((a) => a[1] === "send-keys" || a[1] === "paste-buffer")).toBe(false); + }); + test("recover refuses arbitrary node panes even with copied agent-looking text", async () => { const r = recoveryRunner( `${idleCodewithCapture}\nnode server.js\nListening\n`, diff --git a/src/lib/agent-recovery.ts b/src/lib/agent-recovery.ts index 55490ce..0b73280 100644 --- a/src/lib/agent-recovery.ts +++ b/src/lib/agent-recovery.ts @@ -133,18 +133,21 @@ export function recommendRecoveryAction( }; } if (opts.queue !== false && detection.canQueuePrompt) { + // The queue key is agent-specific (Tab for Codewith, Enter for Claude + // Code); detection carries the proven key for this target. + const queueKey = detection.recommendedSubmitKey ?? "Tab"; return { kind: "queue", - submitKey: "Tab", + submitKey: queueKey, safeToApply: true, - reason: "active agent composer advertises queued Tab prompt support", + reason: `active agent composer advertises queued ${queueKey} prompt support`, }; } return { kind: "refuse", safeToApply: false, reason: detection.canQueuePrompt - ? "target is active; pass queue=true to allow queued Tab recovery" + ? "target is active; pass queue=true to allow queued prompt recovery" : detection.reason, }; } diff --git a/src/lib/engine.test.ts b/src/lib/engine.test.ts index 3243e79..1c078ed 100644 --- a/src/lib/engine.test.ts +++ b/src/lib/engine.test.ts @@ -623,6 +623,80 @@ describe("performDispatch", () => { expect(r.argvs().some((a) => a[1] === "send-keys" && a.includes("Enter"))).toBe(false); }); + test("queues active wrapped Claude Code seats with Enter when queue is explicitly requested", async () => { + const activeClaudeCapture = ` +✶ Wandering… (12m 2s · ↓ 29.6k tokens) + +──────────────────────────────────────────────────────────────────────────────── +❯ +──────────────────────────────────────────────────────────────────────────────── + ⏵⏵ bypass permissions on · 2 monitors · esc to interrupt · ← for agents +`; + const claudeSeatProcessTree = ` +1234272 8038 Ss /bin/bash -l +1301071 1234272 Sl+ \\_ node /home/hasna/.local/bin/accounts launch account005 --tool claude --permissions dangerous +1302384 1301071 Sl+ \\_ claude --dangerously-skip-permissions +`; + const r = composerRunner( + "node", + activeClaudeCapture, + "✶ Wandering… (esc to interrupt)\nQueued: Queue this directive", + claudeSeatProcessTree, + ); + + const rec = await performDispatch( + { target: "hq:staff", prompt: "Queue this directive", queue: true, submitDelayMs: 0 }, + { tmux: new Tmux(r), sleep: noSleep }, + ); + + expect(rec.status).toBe("delivered"); + expect(rec.targetState).toBe("active"); + expect(rec.confirm?.queued).toBe(true); + expect(rec.detection).toMatchObject({ agentKind: "claude", canQueuePrompt: true, recommendedSubmitKey: "Enter" }); + expect(r.argvs().some((a) => a[1] === "send-keys" && a.includes("-l"))).toBe(true); + expect(r.argvs().some((a) => a[1] === "send-keys" && a.includes("Enter"))).toBe(true); + expect(r.argvs().some((a) => a[1] === "send-keys" && a.includes("Tab"))).toBe(false); + }); + + test("refuses --queue --submit-key Tab against active Claude Code seats", async () => { + const activeClaudeCapture = ` +✶ Wandering… (12m 2s · ↓ 29.6k tokens) + +──────────────────────────────────────────────────────────────────────────────── +❯ +──────────────────────────────────────────────────────────────────────────────── + ⏵⏵ bypass permissions on · 2 monitors · esc to interrupt · ← for agents +`; + const claudeSeatProcessTree = ` +1234272 8038 Ss /bin/bash -l +1301071 1234272 Sl+ \\_ node /home/hasna/.local/bin/accounts launch account005 --tool claude --permissions dangerous +1302384 1301071 Sl+ \\_ claude --dangerously-skip-permissions +`; + const r = composerRunner("node", activeClaudeCapture, "✶ Wandering… (esc to interrupt)", claudeSeatProcessTree); + + const rec = await performDispatch( + { target: "hq:staff", prompt: "Queue this", queue: true, submitKey: "Tab", submitDelayMs: 0 }, + { tmux: new Tmux(r), sleep: noSleep }, + ); + + expect(rec.status).toBe("skipped"); + expect(rec.detail).toMatch(/does not prove queued Tab prompt support/); + expect(r.argvs().some((a) => a[1] === "send-keys" || a[1] === "paste-buffer")).toBe(false); + }); + + test("refuses --queue --submit-key Enter against active Codewith panes", async () => { + const r = composerRunner("node", activeCodewithCapture, "✶ Working… (esc to interrupt)", codewithProcessTree); + + const rec = await performDispatch( + { target: "open-sessions:2.1", prompt: "Queue this", queue: true, submitKey: "Enter", submitDelayMs: 0 }, + { tmux: new Tmux(r), sleep: noSleep }, + ); + + expect(rec.status).toBe("skipped"); + expect(rec.detail).toMatch(/cannot receive an Enter prompt safely/); + expect(r.argvs().some((a) => a[1] === "send-keys" || a[1] === "paste-buffer")).toBe(false); + }); + test("reports Codewith auth-switch queued stalls as action-needed instead of delivered", async () => { const r = composerRunner( "node", diff --git a/src/lib/engine.ts b/src/lib/engine.ts index 35a0bbe..fd6e2bb 100644 --- a/src/lib/engine.ts +++ b/src/lib/engine.ts @@ -1,4 +1,4 @@ -import type { DispatchOptions, DispatchRecord } from "../types.js"; +import type { AgentTargetInfo, DispatchOptions, DispatchRecord } from "../types.js"; import { Tmux } from "./tmux.js"; import type { Store } from "./store.js"; import { computeSubmitDelay } from "./delay.js"; @@ -40,9 +40,19 @@ export interface DispatchDeps { sleep?: (ms: number) => Promise; } -function resolveSubmitKey(options: DispatchOptions, targetState: string): "Enter" | "Tab" { +function resolveSubmitKey( + options: DispatchOptions, + targetState: string, + detection?: AgentTargetInfo, +): "Enter" | "Tab" { if (options.submitKey === "Enter" || options.submitKey === "Tab") return options.submitKey; - if (options.queue === true && targetState === "active") return "Tab"; + if (options.queue === true && targetState === "active") { + // The queue key is agent-specific: Codewith stages queued messages with + // Tab, Claude Code queues with plain Enter. Detection carries the proven + // key; fall back to Tab so unproven targets hit the Tab safety gate. + if (detection?.canQueuePrompt === true && detection.recommendedSubmitKey) return detection.recommendedSubmitKey; + return "Tab"; + } return "Enter"; } @@ -105,7 +115,17 @@ export async function performDispatch(options: DispatchOptions, deps: DispatchDe } const targetState = target.activity ?? "unknown"; const detection = target.detection; - const submitKey = resolveSubmitKey(options, targetState); + const submitKey = resolveSubmitKey(options, targetState, detection); + // A queued delivery: explicitly requested, the active target proved it can + // queue, and the resolved submit key IS the proven queue key. Lets + // Enter-queueing agents (Claude Code) through the idle-only Enter gates + // below, exactly as Tab does for Codewith — but an explicit --submit-key + // that differs from the proven queue key never rides this exemption. + const queuedDelivery = + options.queue === true && + targetState === "active" && + detection?.canQueuePrompt === true && + submitKey === detection.recommendedSubmitKey; let captureBefore = target.visible && options.captureBeforeLines ? await performCapture({ target: options.target, lines: options.captureBeforeLines }, { tmux }) : undefined; @@ -115,7 +135,10 @@ export async function performDispatch(options: DispatchOptions, deps: DispatchDe const before = tmux.capturePane(options.target, { start: 50 }); record = { ...record, targetState, detection, captureBefore }; - if (submitKey === "Tab" && detection?.canQueuePrompt !== true) { + // Tab submits only where Tab is the proven queue key: an agent may prove + // queueing generally (Claude Code queues with Enter) while Tab remains a + // destructive key in its composer. + if (submitKey === "Tab" && !(detection?.canQueuePrompt === true && detection.recommendedSubmitKey === "Tab")) { return finish({ status: "skipped", detail: `target does not prove queued Tab prompt support (${detection?.reason ?? "no detection available"})`, @@ -126,7 +149,13 @@ export async function performDispatch(options: DispatchOptions, deps: DispatchDe }); } - if (submitEnabled && submitKey === "Enter" && detection?.canReceivePrompt !== true && options.forceActive !== true) { + if ( + submitEnabled && + submitKey === "Enter" && + detection?.canReceivePrompt !== true && + !queuedDelivery && + options.forceActive !== true + ) { return finish({ status: "skipped", detail: `target cannot receive an Enter prompt safely (${detection?.reason ?? "no detection available"}); pass --queue for supported active agents or --force-active to override`, @@ -148,7 +177,7 @@ export async function performDispatch(options: DispatchOptions, deps: DispatchDe }); } - if (options.ifIdle && targetState !== "idle" && submitKey !== "Tab" && options.forceActive !== true) { + if (options.ifIdle && targetState !== "idle" && submitKey !== "Tab" && !queuedDelivery && options.forceActive !== true) { return finish({ status: "skipped", detail: `target is ${targetState}; refusing because --if-idle was requested (pass --queue to let the agent queue it, or --force-active to override)`, @@ -255,7 +284,9 @@ export async function performDispatch(options: DispatchOptions, deps: DispatchDe const submitResult = await submit(tmux, options.target, { delayMs, - maxRetries: submitKey === "Tab" ? 0 : options.maxSubmitRetries, + // Queued deliveries are single-shot: retrying the queue key against a + // busy agent risks double-queueing the same prompt. + maxRetries: submitKey === "Tab" || queuedDelivery ? 0 : options.maxSubmitRetries, submitKey, isPromptParked, isSubmitted: probe, diff --git a/src/lib/exec-policy.test.ts b/src/lib/exec-policy.test.ts index c8e3480..76a1b42 100644 --- a/src/lib/exec-policy.test.ts +++ b/src/lib/exec-policy.test.ts @@ -404,6 +404,114 @@ Goal active Objective: Copied from a real Codewith pane }); }); + // Measured on station01 (2026-07-31): the hq seats run Claude Code behind + // `accounts launch`, so the pane command is bare `node` and the agent proof + // is the `claude` child process plus the live TUI footer/composer chrome. + const claudeSeatProcessTree = ` +1234272 8038 Ss /bin/bash -l +1301071 1234272 Sl+ \\_ node /home/hasna/.local/bin/accounts launch account005 --tool claude --permissions dangerous +1302384 1301071 Sl+ \\_ claude --dangerously-skip-permissions +`; + + const idleClaudeSeatCapture = ` + The previous turn's summary text is still visible in the transcript above + the composer, exactly as a live seat renders it. + +✻ Churned for 1m 43s · 2 monitors still running + +──────────────────────────────────────────────────────────────────────────────── +❯ run the follow-through check now +──────────────────────────────────────────────────────────────────────────────── + account006 · wks_c0ffee1234567890abcdef1 + opus 5 (1m context) (medium) · 7% left · $12.34 + ⏵⏵ bypass permissions on · 2 monitors · ← for agents +`; + + const activeClaudeSeatCapture = ` +✶ Wandering… (12m 2s · ↓ 29.6k tokens) + ⎿ Tip: Use /btw to ask a quick side question without interrupting Claude's current work + +──────────────────────────────────────────────────────────────────────────────── +❯ +──────────────────────────────────────────────────────────────────────────────── + ⏵⏵ bypass permissions on (shift+tab to cycle) · esc to interrupt · ← for agents +`; + + test("recognizes wrapped idle Claude Code seats launched through the accounts wrapper", () => { + expect( + looksLikeWrappedAgentComposer(idleClaudeSeatCapture, { processTree: claudeSeatProcessTree }), + ).toBe(true); + expect( + detectAgentTargetFromSignals({ + paneCommand: "node", + visible: idleClaudeSeatCapture, + processTree: claudeSeatProcessTree, + cwd: "/home/hasna/.hasna/projects/workspaces/wks_c0ffee1234567890abcdef1", + }), + ).toMatchObject({ + targetKind: "agent", + agentKind: "claude", + composerState: "idle", + canReceivePrompt: true, + canQueuePrompt: false, + submitKeys: ["Enter"], + recommendedSubmitKey: "Enter", + }); + }); + + test("recognizes busy wrapped Claude Code seats and offers Enter queueing", () => { + expect( + detectAgentTargetFromSignals({ + paneCommand: "node", + visible: activeClaudeSeatCapture, + processTree: claudeSeatProcessTree, + cwd: "/home/hasna/.hasna/projects/workspaces/wks_c0ffee1234567890abcdef1", + }), + ).toMatchObject({ + targetKind: "agent", + agentKind: "claude", + composerState: "active", + canReceivePrompt: false, + canQueuePrompt: true, + submitKeys: ["Enter"], + recommendedSubmitKey: "Enter", + }); + }); + + test("recognizes the Claude Code composer glyph followed by a non-breaking space", () => { + const capture = ` +❯${"\u00a0"}deliver the directive to the seat +──────────────────────────────────────────────────────────────────────────────── + ⏵⏵ bypass permissions on · 2 monitors · ← for agents +`; + expect( + detectAgentTargetFromSignals({ + paneCommand: "node", + visible: capture, + processTree: claudeSeatProcessTree, + }), + ).toMatchObject({ targetKind: "agent", agentKind: "claude", composerState: "idle", canReceivePrompt: true }); + }); + + test("refuses Claude Code seat content without Claude process evidence", () => { + for (const processTree of [ + "1234 1 Ss /usr/bin/bash\n1240 1234 Sl+ node /srv/transcript-viewer.js\n", + // The accounts launcher alone (no claude child) is not agent evidence: + // the pane could be mid-launch or showing a copied transcript. + "1234 1 Ss /bin/bash -l\n1240 1234 Sl+ \\_ node /home/hasna/.local/bin/accounts launch account005 --tool claude --permissions dangerous\n", + ]) { + expect( + detectAgentTargetFromSignals({ + paneCommand: "node", + visible: idleClaudeSeatCapture, + processTree, + }), + processTree, + ).toMatchObject({ targetKind: "unknown", agentKind: "unknown", canReceivePrompt: false }); + expect(looksLikeWrappedAgentComposer(idleClaudeSeatCapture, { processTree })).toBe(false); + } + }); + test("detects direct Claude Code and OpenCode panes", () => { const claude = detectAgentTargetFromSignals({ paneCommand: "claude", @@ -420,7 +528,10 @@ Goal active Objective: Copied from a real Codewith pane agentKind: "claude", composerState: "idle", canReceivePrompt: true, - submitKeys: ["Enter", "Tab"], + // Claude Code submits and queues with Enter only; Tab is completion, not + // a submit key, so advertising it would let `dispatch key Tab` corrupt + // the composer. + submitKeys: ["Enter"], recommendedSubmitKey: "Enter", }); diff --git a/src/lib/exec-policy.ts b/src/lib/exec-policy.ts index 43895e8..a813aa3 100644 --- a/src/lib/exec-policy.ts +++ b/src/lib/exec-policy.ts @@ -33,7 +33,18 @@ const DIRECT_AGENT_KINDS: Record = { opencode: "opencode", }; -const QUEUE_CAPABLE_AGENTS = new Set(["codewith", "claude"]); +/** + * Key that queues a prompt typed into an ACTIVE composer, per agent family. + * Codewith stages queued messages with Tab; Claude Code queues a message + * submitted with plain Enter while the agent is working. Agents without an + * entry cannot queue. Tab is never advertised to Claude Code panes — there it + * triggers completion, not submission. + */ +const AGENT_QUEUE_SUBMIT_KEYS: Partial> = { + codewith: "Tab", + claude: "Enter", +}; + const DEFAULT_ALLOW_PREFIXES = [ "mailery status", @@ -345,6 +356,36 @@ function hasWrappedAgentLiveUi(text: string, processTree?: string): boolean { return hasGoalActivity && (hasComposer || hasModelStatus); } +/** + * Live-UI proof for Claude Code panes launched through runtime wrappers + * (e.g. `accounts launch … --tool claude` spawning `claude` under `node`). + * The steady-state Claude Code TUI renders no name banner, so recognition + * rests on its composer glyph plus the chrome it always draws: the + * permission-mode footer (`⏵⏵ bypass permissions on`, `⏵ accept edits on`, + * `plan mode on`, or the default `? for shortcuts` hint) or the + * account/model status lines. `esc to interrupt` marks a working agent whose + * composer still accepts queued input. A bare composer glyph with none of + * this chrome stays refused — copied transcripts must not become targets. + */ +function hasWrappedClaudeComposer(text: string, processTree?: string): boolean { + if (detectAgentKindFromProcessTree(processTree) !== "claude") return false; + const normalized = text + .split("\n") + .map(stripTuiLineChrome) + .map((line) => line.replace(/[ \t\u00a0]+/g, " ").trim()) + .join("\n"); + const hasComposer = /^[›❯>](?:\s|$)/m.test(normalized); + const hasBusySignal = /\besc to interrupt\b/i.test(normalized); + const hasModeFooter = + /[⏵⏸]{1,2} ?(?:bypass permissions|accept edits) on\b/i.test(normalized) || + /\bplan mode on\b/i.test(normalized) || + /\? for shortcuts\b/i.test(normalized); + const hasStatusLine = + /^account[\w-]+ · \S+/im.test(normalized) || + /^(?:opus|sonnet|haiku|claude)[\w .()+-]* · [^\n]*% left\b/im.test(normalized); + return (hasComposer || hasBusySignal) && (hasModeFooter || hasStatusLine); +} + function hasWrappedCodexComposer(text: string, processTree?: string): boolean { if (detectAgentKindFromProcessTree(processTree) !== "codex") return false; const normalized = text @@ -388,7 +429,8 @@ export function detectAgentActivity(text: string): AgentActivityState { function submitKeysFor(agentKind: AgentKind): SubmitKey[] { if (agentKind === "unknown") return []; const keys: SubmitKey[] = ["Enter"]; - if (QUEUE_CAPABLE_AGENTS.has(agentKind)) keys.push("Tab"); + const queueKey = AGENT_QUEUE_SUBMIT_KEYS[agentKind]; + if (queueKey && !keys.includes(queueKey)) keys.push(queueKey); return keys; } @@ -405,6 +447,7 @@ function liveUiProofForKind(kind: AgentKind, text: string, processTree?: string) return true; } if (kind === "codex" && hasWrappedCodexComposer(text, processTree)) return true; + if (kind === "claude" && hasWrappedClaudeComposer(text, processTree)) return true; return false; } @@ -468,13 +511,14 @@ export function detectAgentTargetFromSignals(input: { const composerState = detectAgentActivity(visible) as ComposerState; const submitKeys = submitKeysFor(agentKind); const canReceivePrompt = composerState === "idle"; - const canQueuePrompt = composerState === "active" && QUEUE_CAPABLE_AGENTS.has(agentKind); - const recommendedSubmitKey = canReceivePrompt ? "Enter" : canQueuePrompt ? "Tab" : undefined; + const queueKey = AGENT_QUEUE_SUBMIT_KEYS[agentKind]; + const canQueuePrompt = composerState === "active" && queueKey !== undefined; + const recommendedSubmitKey = canReceivePrompt ? "Enter" : canQueuePrompt ? queueKey : undefined; const stateReason = composerState === "idle" ? "idle composer can receive Enter prompt delivery" : canQueuePrompt - ? "active composer supports queued Tab prompt delivery" + ? `active composer supports queued ${queueKey} prompt delivery` : composerState === "active" ? "active composer does not prove queued prompt support" : "composer state is unknown"; diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index dc972fa..458c02e 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -96,7 +96,7 @@ export const TOOLS: ToolDef[] = [ submit: z.boolean().optional().describe("press Enter to submit (default true)"), confirm: z.boolean().optional().describe("verify delivery (default true)"), delayMs: z.number().optional().describe("override the auto-computed pre-Enter delay"), - retries: z.number().optional().describe("max Enter retries if not confirmed; queued Tab delivery is single-shot"), + retries: z.number().optional().describe("max Enter retries if not confirmed; queued delivery is single-shot"), mode: z.enum(["auto", "paste", "literal"]).optional().describe("delivery mode"), goal: z.boolean().optional().describe("prefix prompt with /goal unless it already starts with /goal"), verbose: z.boolean().optional().describe("return full records instead of compact summaries"), @@ -219,7 +219,7 @@ export const TOOLS: ToolDef[] = [ excerptChars: z.number().optional().describe("max redacted transcript chars in output (default 1200, max 4000)"), includeExcerpt: z.boolean().optional().describe("include the bounded excerpt (default true)"), artifactPath: z.string().optional().describe("relative path under the dispatch artifacts directory for the full bounded redacted capture"), - queue: z.boolean().optional().describe("allow queued Tab recovery recommendation for active queue-capable agents"), + queue: z.boolean().optional().describe("allow queued-delivery recovery recommendation for active queue-capable agents"), }, handler: (deps, a) => deps.client.triage({ @@ -249,7 +249,7 @@ export const TOOLS: ToolDef[] = [ excerptChars: z.number().optional().describe("max redacted transcript chars in output (default 1200, max 4000)"), includeExcerpt: z.boolean().optional().describe("include the bounded excerpt (default true)"), artifactPath: z.string().optional().describe("relative path under the dispatch artifacts directory for the full bounded redacted capture"), - queue: z.boolean().optional().describe("allow queued Tab recovery for active queue-capable agents"), + queue: z.boolean().optional().describe("allow queued-delivery recovery for active queue-capable agents"), confirm: z.boolean().optional().describe("verify delivery when apply=true"), delayMs: z.number().optional().describe("override the auto-computed pre-Enter delay"), retries: z.number().optional().describe("max Enter retries if not confirmed"), @@ -400,7 +400,7 @@ export const TOOLS: ToolDef[] = [ cron: z.string().optional().describe("5-field cron expression"), every: z.string().optional().describe("recurring interval, e.g. 5m or 1 hour"), ifIdle: z.boolean().optional().describe("refuse delivery unless target looks idle when fired"), - queue: z.boolean().optional().describe("queue on active agents that prove Tab queued-message support when fired"), + queue: z.boolean().optional().describe("queue on active agents that prove queued-message support when fired"), forceActive: z.boolean().optional().describe("explicitly override active/unknown target refusal when fired"), verbose: z.boolean().optional().describe("return the full schedule instead of a compact summary"), },