diff --git a/src/agent-engine.ts b/src/agent-engine.ts index 2fdc06c..ffeb9b7 100644 --- a/src/agent-engine.ts +++ b/src/agent-engine.ts @@ -198,6 +198,8 @@ export interface AgentDeliveryReceipt { submission_started_at?: string | null; /** Earliest wall-clock time at which a known pre-mutation rejection may retry. */ next_attempt_at?: string | null; + /** The receiving TUI visibly accepted this into its own queue; never replay it. */ + composer_accepted?: boolean; } /** A known pre-mutation delivery rejection that is safe to retry. */ @@ -210,7 +212,11 @@ export class RetryableDeliveryError extends Error { type DeliverySubmitter = ( receipt: AgentDeliveryReceipt, -) => Promise<{ retry_count: number; submit_verified: boolean | null }>; +) => Promise<{ + retry_count: number; + submit_verified: boolean | null; + delivery?: "submitted" | "queued"; +}>; export interface SpawnAgentParams { repo: string; @@ -5057,7 +5063,8 @@ export class AgentEngine { }; if ( receipt.delivery_state === "queued" && - receipt.submission_started_at + receipt.submission_started_at && + receipt.composer_accepted !== true ) { receipt.delivery_state = "failed"; receipt.terminal = true; @@ -5127,6 +5134,37 @@ export class AgentEngine { return { ...receipt }; } + acceptComposerQueue(input: { + delivery_id: string; + agent_id: string; + text: string; + press_enter: boolean; + source_event: DeliveryEventType; + retry_count: number; + }): AgentDeliveryReceipt { + const acceptedAt = new Date().toISOString(); + const receipt: AgentDeliveryReceipt = { + ...input, + delivery_state: "queued", + terminal: false, + created_at: acceptedAt, + resolved_at: null, + submit_verified: null, + error: null, + submission_started_at: acceptedAt, + next_attempt_at: null, + composer_accepted: true, + }; + this.deliveryReceipts.set(receipt.delivery_id, receipt); + try { + this.persistDeliveryReceipts(); + } catch (error) { + this.deliveryReceipts.delete(receipt.delivery_id); + throw error; + } + return { ...receipt }; + } + resolveDelivery( input: Omit & { created_at?: string; @@ -5157,6 +5195,7 @@ export class AgentEngine { try { for (const receipt of this.deliveryReceipts.values()) { if (receipt.delivery_state !== "queued") continue; + if (receipt.composer_accepted === true) continue; const agent = this.getAgentState(receipt.agent_id); if (!agent) { receipt.delivery_state = "failed"; @@ -5195,13 +5234,21 @@ export class AgentEngine { ]).finally(() => { if (timeout) clearTimeout(timeout); }); - receipt.delivery_state = "submitted"; - receipt.terminal = true; - receipt.resolved_at = new Date().toISOString(); receipt.retry_count += result.retry_count; - receipt.submit_verified = result.submit_verified; receipt.error = null; receipt.next_attempt_at = null; + if (result.delivery === "queued") { + receipt.delivery_state = "queued"; + receipt.terminal = false; + receipt.resolved_at = null; + receipt.submit_verified = null; + receipt.composer_accepted = true; + } else { + receipt.delivery_state = "submitted"; + receipt.terminal = true; + receipt.resolved_at = new Date().toISOString(); + receipt.submit_verified = result.submit_verified; + } } catch (error) { if (error instanceof RetryableDeliveryError) { receipt.submission_started_at = null; diff --git a/src/agent-health.ts b/src/agent-health.ts index 99c7238..09b53a3 100644 --- a/src/agent-health.ts +++ b/src/agent-health.ts @@ -170,6 +170,9 @@ function issueSeverity( ) { return "info"; } + if (code === "inbox_monitor_not_alive" && context.autoDiscovered) { + return "info"; + } if ( code === "inbox_monitor_not_alive" && !context.inboxMonitorWithinBootGrace diff --git a/src/format.ts b/src/format.ts index 4df83c2..294000a 100644 --- a/src/format.ts +++ b/src/format.ts @@ -266,6 +266,8 @@ export function formatDelivery( // instead of the delivered/failed binary, so the line never contradicts a // pending status. pending?: boolean; + typed?: boolean; + submit_attempted?: boolean; submit_verified?: boolean | null; }, ): string { @@ -280,8 +282,15 @@ export function formatDelivery( ); const parens = meta.length > 0 ? ` (${meta.join(" \u00b7 ")})` : ""; let head: string; - if (info.pending) { + if (info.typed) { + head = `typed into ${label}${parens} (not submitted)`; + } else if (info.pending) { head = `delivering to ${label}${parens}`; + } else if ( + info.submit_attempted && + info.submit_verified === null + ) { + head = `submission attempted to ${label}${parens} (not verified)`; } else if (info.delivered) { head = `delivered to ${label}${parens}`; } else { diff --git a/src/server.ts b/src/server.ts index d4d809b..49b1665 100644 --- a/src/server.ts +++ b/src/server.ts @@ -440,9 +440,10 @@ export const SEND_INPUT_MAX_INLINE_CHARS = parseMaxInlineChars( ); const SEND_INPUT_SUBMIT_VERIFY_POLL_MS = 100; // Busy relays are interjections into an already-running UI. Observe several -// repaint frames, but fail quickly when the submitted text remains queued or -// in the composer so fleet fan-out does not inherit the general 5s timeout. -const BUSY_AGENT_SUBMIT_VERIFY_TIMEOUT_MS = 500; +// repaint frames, accept a correlated TUI queue, and bound exact-composer +// recovery so fleet fan-out does not inherit the general 5s timeout. +const BUSY_AGENT_SUBMIT_VERIFY_TIMEOUT_MS = 1_000; +const CODEX_PENDING_COMPOSER_RETRY_OBSERVE_MS = 250; const SEND_INPUT_SAFE_RETRY_OBSERVE_MS = 2500; const SEND_INPUT_POST_RETRY_VERIFY_GRACE_MS = 300; const BOOT_PROMPT_TIMEOUT_MS = 60_000; @@ -473,9 +474,17 @@ const READY_PATTERN_CLIS: CliType[] = [ "kiro", "cursor", ]; +const SEND_TO_WORKING_EXAMPLE = + 'Example: send_to({ mode: "agent", agent_id: "cmuxlayerCodex-1234", text: "hello" })'; const SendToArgsSchema = z.object({ mode: z - .enum(["agent", "surface", "command", "key"]) + .enum(["agent", "surface", "command", "key"], { + errorMap: () => ({ + message: + 'Expected one of "agent" | "surface" | "command" | "key". ' + + SEND_TO_WORKING_EXAMPLE, + }), + }) .optional() .default("agent"), target: z.string().optional(), @@ -536,11 +545,8 @@ export const THIN_CORE_LEGACY_REPLACEMENTS: Readonly> = { broadcast: "send_to(targeting={...})", send_to_agent: "send_to(mode=agent)", send_input: "send_to(mode=surface)", - send_command: "send_to(mode=command)", - send_key: "send_to(mode=key)", new_worktree_split: "spawn_agent(worktree=true, role=worker)", spawn_in_workspace: "spawn_agent(workspace=...)", - new_split: "spawn_agent(role=...)", wait_for_all: "wait_for(ids=[...])", }; @@ -956,7 +962,12 @@ function withDeprecationWarning( result: ToolReturn, legacyName: string, replacement: string, + emittedWarnings: Set, ): ToolReturn { + if (emittedWarnings.has(legacyName)) { + return result; + } + emittedWarnings.add(legacyName); const warning = `${legacyName} is deprecated for one release; use ${replacement}`; console.warn(`[cmuxlayer] ${warning}`); return { @@ -1818,7 +1829,11 @@ function formatToolValidationError( return `${path}: ${issue.message}`; }) .join("; "); - return `${toolName} invalid arguments: ${details}`; + const example = + toolName === "send_to" + ? ` ${SEND_TO_WORKING_EXAMPLE}` + : ""; + return `${toolName} invalid arguments: ${details}.${example}`; } function isSubmitVerifiedStatus( @@ -2717,7 +2732,12 @@ export type LifecycleAgentInputDeliverer = (args: { allow_busy?: boolean; source_event: DeliveryEventType; delivery_id?: string; -}) => Promise; +}) => Promise<{ + bytes: number; + retry_count: number; + submit_verified: boolean | null; + delivery: "submitted" | "queued"; +}>; export interface CmuxServerContext { client: CmuxLayerClient; @@ -3476,6 +3496,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { string, (args: Record, extra: unknown) => Promise >(); + const emittedDeprecationWarnings = new Set(); const palette = createDefaultToolPalette( opts?.defaultPalette ?? process.env[CMUXLAYER_DEFAULT_PALETTE_ENV], ); @@ -3794,6 +3815,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { const worktreeArgSchema = z.union([ z.boolean(), + z.string(), z.object({ create: z.boolean().optional(), reuse: z.boolean().optional(), @@ -4195,6 +4217,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified: boolean | null; submit_verification_reason: SubmitVerificationFailureReason | null; retry_count: number; + delivery: "submitted" | "queued"; }> => { if (!opts.verify_submit) { // null means submit verification was not attempted, usually because the @@ -4203,6 +4226,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified: null, submit_verification_reason: null, retry_count: 0, + delivery: "submitted", }; } @@ -4255,10 +4279,22 @@ export function createServer(opts?: CreateServerOptions): McpServer { opts.text, ); if (hasQueuedAgentInput) { + if ( + opts.source_event !== "send_to" && + opts.source_event !== "dispatch_nudge" + ) { + return { + submit_verified: false, + submit_verification_reason: "input_still_pending", + retry_count: retryCount, + delivery: "submitted", + }; + } return { - submit_verified: false, - submit_verification_reason: "input_still_pending", + submit_verified: null, + submit_verification_reason: null, retry_count: retryCount, + delivery: "queued", }; } const screenCli = inferComposerCli(snapshot.text, snapshot.parsed); @@ -4282,6 +4318,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified: true, submit_verification_reason: null, retry_count: retryCount, + delivery: "submitted", }; } const hasClearedAgentComposer = @@ -4300,6 +4337,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified: true, submit_verification_reason: null, retry_count: retryCount, + delivery: "submitted", }; } } @@ -4308,12 +4346,20 @@ export function createServer(opts?: CreateServerOptions): McpServer { hasPendingInput || (opts.source_event === "spawn_agent" && screenIncludesSubmittedText(snapshot.text)); - const retryEligiblePendingInput = + const spawnRetryEligiblePendingInput = opts.allow_recovery_enter_retry !== false && shouldRetryEnter && !screenHasAnyAgentIdentity(snapshot.text, snapshot.parsed) && opts.source_event === "spawn_agent" && !hasParsedAgentIdentity(snapshot.parsed); + const codexRetryEligiblePendingInput = + opts.allow_recovery_enter_retry !== false && + (opts.source_event === "send_to" || + opts.source_event === "dispatch_nudge") && + hasPendingSubmitEvidence && + screenCli === "codex"; + const retryEligiblePendingInput = + spawnRetryEligiblePendingInput || codexRetryEligiblePendingInput; lastRetryEligiblePendingInput = retryEligiblePendingInput; if (retryEligiblePendingInput) { retryEligiblePendingSince ??= Date.now(); @@ -4321,8 +4367,10 @@ export function createServer(opts?: CreateServerOptions): McpServer { retryEligiblePendingSince = null; } const retryObserveMs = - opts.source_event === "spawn_agent" && - !hasParsedAgentIdentity(snapshot.parsed) + codexRetryEligiblePendingInput + ? Math.min(timeoutMs, CODEX_PENDING_COMPOSER_RETRY_OBSERVE_MS) + : opts.source_event === "spawn_agent" && + !hasParsedAgentIdentity(snapshot.parsed) ? 0 : Math.min(timeoutMs, SEND_INPUT_SAFE_RETRY_OBSERVE_MS); @@ -4367,6 +4415,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified: false, submit_verification_reason: "input_still_pending", retry_count: retryCount, + delivery: "submitted", }; } @@ -4377,6 +4426,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified: true, submit_verification_reason: null, retry_count: retryCount, + delivery: "submitted", }; } @@ -4403,6 +4453,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified: submitVerified, submit_verification_reason: failureReason, retry_count: retryCount, + delivery: "submitted", }; }; @@ -4427,6 +4478,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { bytes: number; retry_count: number; submit_verified: boolean | null; + delivery: "submitted" | "queued"; }> => { await opts.beforeMutation?.(); const deliverySafetySnapshot = await assertDeliveryTargetIsSafe( @@ -4468,6 +4520,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { let submit_verification_reason: SubmitVerificationFailureReason | null = null; let retry_count = 0; + let delivery: "submitted" | "queued" = "submitted"; if (opts.press_enter) { let cursorResponseBaseline: readonly string[] | null = null; @@ -4526,6 +4579,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { submit_verified = verification.submit_verified; submit_verification_reason = verification.submit_verification_reason; retry_count = verification.retry_count; + delivery = verification.delivery; } await maybeRenameTask({ @@ -4549,7 +4603,9 @@ export function createServer(opts?: CreateServerOptions): McpServer { ? { delivery_id: opts.delivery_id, delivery_state: - submit_verified === false + delivery === "queued" + ? ("queued" as const) + : submit_verified === false ? ("failed" as const) : ("submitted" as const), } @@ -4557,7 +4613,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { }); } - if (submit_verified === false) { + if (submit_verified === false && delivery !== "queued") { const timeoutMs = opts.submit_verify_timeout_ms ?? SEND_INPUT_SUBMIT_VERIFY_TIMEOUT_MS; throw new SubmitVerificationError( @@ -4567,7 +4623,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { ); } - return { bytes, retry_count, submit_verified }; + return { bytes, retry_count, submit_verified, delivery }; }; const waitForBootPromptReady = async (opts: { @@ -7056,7 +7112,11 @@ export function createServer(opts?: CreateServerOptions): McpServer { { direction: z .enum(["left", "right", "up", "down"]) - .describe("Split direction"), + .optional() + .default("right") + .describe( + "Split-direction hint for direct placement; role-based placement may override it. Defaults to right.", + ), workspace: z.string().optional().describe("Target workspace ref"), surface: z.string().optional().describe("Target surface ref"), pane: z.string().optional().describe("Target pane ref"), @@ -7668,6 +7728,12 @@ export function createServer(opts?: CreateServerOptions): McpServer { ANNOTATIONS.mutating, async (args) => { try { + const sourceEvent = + ( + args as typeof args & { + _cmuxlayer_source_event?: DeliveryEventType; + } + )._cmuxlayer_source_event ?? "send_input"; assertInlineInputAllowed({ tool: "send_input", arg: "text", @@ -7771,7 +7837,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { press_enter: args.press_enter, rename_to_task: args.rename_to_task, stableSurfaceIdentity: route.stableSurfaceIdentity, - source_event: "send_input", + source_event: sourceEvent, verify_submit: shouldVerifySubmit, beforeMutation: route.assertCurrent, }); @@ -7785,16 +7851,38 @@ export function createServer(opts?: CreateServerOptions): McpServer { ); const identity = resolveTargetIdentity(stateMgr, route.surface); + const queued = delivery.delivery === "queued"; + const submitted = + delivery.delivery === "submitted" && + delivery.submit_verified === true; + const typed = !args.press_enter; + const acceptedDelivery = queued + ? "queued" + : submitted + ? "submitted" + : null; const data = { ...identity, - delivered: true, + delivered: !queued, + ...(acceptedDelivery + ? { + delivery: acceptedDelivery, + delivery_state: acceptedDelivery, + } + : {}), + terminal: submitted, + ...(typed ? { typed: true } : {}), + submit_attempted: args.press_enter, retry_count: delivery.retry_count, submit_verified: delivery.submit_verified, }; return okFormatted( formatDelivery("send_input", { ...identity, - delivered: true, + delivered: !queued, + pending: queued, + typed, + submit_attempted: args.press_enter, submit_verified: delivery.submit_verified, }), data, @@ -8814,7 +8902,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { // directly into the agent's surface, regardless of registry state. server.tool( "dispatch_to_agent", - "Append a task to an agent's inbox FILE (the deterministic write channel). The agent acts on it via a persistent native Monitor on its inbox. The durable envelope automatically carries reply_to= plus optional via: and observed_at metadata; reply_to is the only routing address, via is a stale-able hint, and tab names never enter the contract. The only connector-authored composer wake is `[inbox] — reply_to: [ via: observed_at:] — read `. With nudge='auto' (default), an idle live agent is woken once on enqueue; a stale/absent monitor also gets the same best-effort pointer independent of lifecycle state. A never-armed reader returns a non-retryable error after the durable append; a previously armed but stale reader returns explicit degraded success. Address to:'orc' to flag the orchestrator (own-tag triage). Channel is EPHEMERAL plumbing — set persist:true only for decisions that should be brain_store'd.", + "Append a task to an agent's inbox FILE (the deterministic write channel). The agent acts on it via a persistent native Monitor on its inbox. The durable envelope automatically carries reply_to= plus optional via: and observed_at metadata; reply_to is the only routing address, via is a stale-able hint, and tab names never enter the contract. The only connector-authored composer wake is `[inbox] — reply_to: [ via: observed_at:] — read `. With nudge='auto' (default), an idle live agent is woken once on enqueue; a stale/absent monitor also gets the same best-effort pointer independent of lifecycle state. A never-armed reader is successful when the verified nudge path submits or queues the pointer; otherwise the durable append returns a non-retryable error. A previously armed but stale reader returns explicit degraded success. Address to:'orc' to flag the orchestrator (own-tag triage). Channel is EPHEMERAL plumbing — set persist:true only for decisions that should be brain_store'd.", { agent_id: z .string() @@ -8883,6 +8971,8 @@ export function createServer(opts?: CreateServerOptions): McpServer { sent: boolean; reason: string; error_code?: string; + delivery?: "submitted" | "queued"; + delivery_id?: string; } = { attempted: false, sent: false, reason: "" }; const acceptedRecord = context.lifecycleRegistry?.get(args.agent_id) ?? null; const wakeIdleAgent = @@ -8918,17 +9008,63 @@ export function createServer(opts?: CreateServerOptions): McpServer { msg, inboxPath(args.agent_id, inboxOpts), ); - await lifecycleAgentInputDeliverer({ - agent_id: args.agent_id, - text: pointer, - press_enter: true, - allow_busy: true, - source_event: "dispatch_nudge", - }); + if ( + record.state === "working" && + context.lifecycleSweepEngine + ) { + const queued = context.lifecycleSweepEngine.queueDelivery({ + agent_id: args.agent_id, + text: pointer, + press_enter: true, + source_event: "dispatch_nudge", + }); + nudge.delivery = "queued"; + nudge.delivery_id = queued.delivery_id; + } else { + const deliveryId = context.lifecycleSweepEngine + ? randomUUID() + : undefined; + const delivered = await lifecycleAgentInputDeliverer({ + agent_id: args.agent_id, + text: pointer, + press_enter: true, + allow_busy: true, + source_event: "dispatch_nudge", + delivery_id: deliveryId, + }); + nudge.delivery = delivered.delivery; + if (context.lifecycleSweepEngine && deliveryId) { + const receipt = + delivered.delivery === "queued" + ? context.lifecycleSweepEngine.acceptComposerQueue({ + delivery_id: deliveryId, + agent_id: args.agent_id, + text: pointer, + press_enter: true, + source_event: "dispatch_nudge", + retry_count: delivered.retry_count, + }) + : context.lifecycleSweepEngine.resolveDelivery({ + delivery_id: deliveryId, + agent_id: args.agent_id, + text: pointer, + press_enter: true, + source_event: "dispatch_nudge", + delivery_state: "submitted", + terminal: true, + retry_count: delivered.retry_count, + submit_verified: delivered.submit_verified, + error: null, + }); + nudge.delivery_id = receipt.delivery_id; + } + } nudge.sent = true; nudge.reason = wakeIdleAgent ? `idle live agent — typed inbox pointer into ${record.surface_id}` - : `heartbeat stale/absent — typed inbox pointer into ${record.surface_id} (state: ${record.state})`; + : record.state === "working" + ? `busy agent — queued inbox pointer for verified lifecycle delivery to ${record.surface_id}` + : `heartbeat stale/absent — typed inbox pointer into ${record.surface_id} (state: ${record.state})`; } catch (e) { if (e instanceof DeliverySafetyGateError) { nudge.error_code = e.error_code; @@ -8963,7 +9099,10 @@ export function createServer(opts?: CreateServerOptions): McpServer { health, nudge, }; - if (monitor_state === "never-armed") { + const nudgeAccepted = + nudge.sent && + (nudge.delivery === "submitted" || nudge.delivery === "queued"); + if (monitor_state === "never-armed" && !nudgeAccepted) { return err( "inbox message was queued, but the recipient has never proved that its inbox monitor is armed", { @@ -9636,7 +9775,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { const prepareSpawnWorktree = async ( repo: string, - worktree: boolean | object | undefined, + worktree: boolean | string | object | undefined, mcpProfile: McpProfile | undefined, ) => { if (!worktree) { @@ -9850,14 +9989,19 @@ export function createServer(opts?: CreateServerOptions): McpServer { delivery_id: args.delivery_id, // Verify every submitted agent relay — not just long ones. A short // relay (the common agent-to-agent case) to a frozen terminal must - // be caught, never reported as ok. Busy sends do not retry Return: - // accepted queued input can repaint slowly, but text still proven - // inside the active composer must not receive a success receipt. + // be caught, never reported as ok. Verified agent messages may + // retry Return once only while the exact text remains in a Codex + // composer; accepted TUI queues are nonterminal receipts instead. verify_submit: args.press_enter && (args.allow_busy || INTERACTIVE_AGENT_STATES.has(deliveryRoute.state)), - allow_recovery_enter_retry: !args.allow_busy, + // A single recovery Return is part of verified sends and inbox + // wakeups. Other lifecycle mutations (notably goal supersession) + // retain their stricter no-retry evidence semantics. + allow_recovery_enter_retry: + args.source_event === "send_to" || + args.source_event === "dispatch_nudge", submit_verify_timeout_ms: args.allow_busy ? BUSY_AGENT_SUBMIT_VERIFY_TIMEOUT_MS : undefined, @@ -10000,7 +10144,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { worktree: worktreeArgSchema .optional() .describe( - "When set, create or reuse a git worktree before launch. A repoGolem registration with an absolute path is required; that registry path is the repo root. true uses /.worktrees/ (legacy ~/Gits/.wt read-fallback until ~2026-09). If a later spawn step fails before a recoverable surface exists, a newly created worktree and branch are rolled back. Object fields can set name, path, branch, base, create, and reuse.", + 'When set, create or reuse a git worktree before launch. Pass a string such as "tool-usage" as the worktree name, true for a generated name, or an object with name, path, branch, base, create, and reuse. A repoGolem registration with an absolute path is required; that registry path is the repo root. true uses /.worktrees/ (legacy ~/Gits/.wt read-fallback until ~2026-09). If a later spawn step fails before a recoverable surface exists, a newly created worktree and branch are rolled back.', ), mcp_profile: mcpProfileSchema .optional() @@ -10741,7 +10885,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { worktree: worktreeArgSchema .optional() .describe( - "Worktree options. A repoGolem registration with an absolute path is required. Defaults to true, creating/reusing /.worktrees/; a newly created worktree and branch are rolled back if spawn fails before a recoverable surface exists (legacy ~/Gits/.wt read-fallback until ~2026-09).", + 'Worktree options. Pass a string such as "tool-usage" as the worktree name, true for a generated name, or an options object. A repoGolem registration with an absolute path is required. Defaults to true, creating/reusing /.worktrees/; a newly created worktree and branch are rolled back if spawn fails before a recoverable surface exists (legacy ~/Gits/.wt read-fallback until ~2026-09).', ), mcp_profile: mcpProfileSchema .optional() @@ -12921,6 +13065,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { press_enter: args.press_enter, rename_to_task: args.rename_to_task, allow_long_inline: args.allow_long_inline, + _cmuxlayer_source_event: "send_to", }, {}, ); @@ -13118,7 +13263,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { } const deliveryId = randomUUID(); if (!args.allow_busy && agent.state === "working") { - const queued = engine.queueDelivery({ + const queued = engine.queueDelivery({ agent_id: agent.agent_id, text: args.text, press_enter: args.press_enter, @@ -13128,6 +13273,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { ...resolutionMetadata, agent_id: agent.agent_id, delivery_id: queued.delivery_id, + delivery: "queued", delivery_state: queued.delivery_state, terminal: queued.terminal, submit_verified: queued.submit_verified, @@ -13145,27 +13291,38 @@ export function createServer(opts?: CreateServerOptions): McpServer { source_event: "send_to", delivery_id: deliveryId, }); - const submitted = engine.resolveDelivery({ - delivery_id: deliveryId, - agent_id: agent.agent_id, - text: args.text, - press_enter: args.press_enter, - source_event: "send_to", - delivery_state: "submitted", - terminal: true, - retry_count: delivery.retry_count, - submit_verified: delivery.submit_verified, - error: null, - }); + const accepted = + delivery.delivery === "queued" + ? engine.acceptComposerQueue({ + delivery_id: deliveryId, + agent_id: agent.agent_id, + text: args.text, + press_enter: args.press_enter, + source_event: "send_to", + retry_count: delivery.retry_count, + }) + : engine.resolveDelivery({ + delivery_id: deliveryId, + agent_id: agent.agent_id, + text: args.text, + press_enter: args.press_enter, + source_event: "send_to", + delivery_state: "submitted", + terminal: true, + retry_count: delivery.retry_count, + submit_verified: delivery.submit_verified, + error: null, + }); mutableReceipts.push({ ...resolutionMetadata, agent_id: agent.agent_id, - delivery_id: submitted.delivery_id, - delivery_state: submitted.delivery_state, - terminal: submitted.terminal, - submit_verified: submitted.submit_verified, + delivery_id: accepted.delivery_id, + delivery: accepted.delivery_state, + delivery_state: accepted.delivery_state, + terminal: accepted.terminal, + submit_verified: accepted.submit_verified, accepted: true, - delivered: true, + delivered: accepted.delivery_state === "submitted", }); } catch (error) { const failed = engine.resolveDelivery( @@ -13265,18 +13422,15 @@ export function createServer(opts?: CreateServerOptions): McpServer { accepted: true, agent_id: agentId, delivery_id: receipt.delivery_id, + delivery: "queued", delivery_state: receipt.delivery_state, terminal: receipt.terminal, + submit_verified: receipt.submit_verified, }; - return { - content: [ - { - type: "text", - text: `send_to accepted — delivery ${receipt.delivery_id} queued`, - }, - ], - structuredContent: data, - }; + return okFormatted( + `send_to accepted — delivery ${receipt.delivery_id} queued`, + data, + ); } const deliveryId = randomUUID(); let delivery: Awaited>; @@ -13320,18 +13474,28 @@ export function createServer(opts?: CreateServerOptions): McpServer { }; throw error; } - const receipt = engine.resolveDelivery({ - delivery_id: deliveryId, - agent_id: agentId, - text: args.text, - press_enter: args.press_enter, - source_event: "send_to", - delivery_state: "submitted", - terminal: true, - retry_count: delivery.retry_count, - submit_verified: delivery.submit_verified, - error: null, - }); + const receipt = + delivery.delivery === "queued" + ? engine.acceptComposerQueue({ + delivery_id: deliveryId, + agent_id: agentId, + text: args.text, + press_enter: args.press_enter, + source_event: "send_to", + retry_count: delivery.retry_count, + }) + : engine.resolveDelivery({ + delivery_id: deliveryId, + agent_id: agentId, + text: args.text, + press_enter: args.press_enter, + source_event: "send_to", + delivery_state: "submitted", + terminal: true, + retry_count: delivery.retry_count, + submit_verified: delivery.submit_verified, + error: null, + }); // Preserve the already-terminal receipt if optional evidence // collection fails after the pane mutation has succeeded. failedReceiptPayload = { @@ -13343,6 +13507,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { const data = { agent_id: agentId, delivery_id: receipt.delivery_id, + delivery: receipt.delivery_state, delivery_state: receipt.delivery_state, terminal: receipt.terminal, retry_count: delivery.retry_count, @@ -13406,41 +13571,43 @@ export function createServer(opts?: CreateServerOptions): McpServer { if (!agentId || args.text === undefined) { throw new Error("send_to_agent requires agent_id and text"); } - assertInlineInputAllowed({ - tool: "send_to_agent", - arg: "text", - value: args.text, - allowLongInline: args.allow_long_inline, - }); - assertDenseInlineInputAllowed({ - tool: "send_to_agent", - arg: "text", - value: args.text, - allowLongInline: args.allow_long_inline, - }); - const targetAgent = - engine.getAgentState(agentId) ?? registry.get(agentId); - assertInteractiveMultilineInputAllowed({ - tool: "send_to_agent", - value: args.text, - cli: targetAgent?.cli, - allowLongInline: args.allow_long_inline, - }); - const delivery = await deliverAgentInput({ - agent_id: agentId, - text: args.text, - press_enter: args.press_enter, - allow_busy: args.allow_busy, - source_event: "send_to_agent", - }); - const evidence = await collectDeliveryEvidence(agentId); - const data = { - agent_id: agentId, - retry_count: delivery.retry_count, - submit_verified: delivery.submit_verified, - ...evidence, + const sendToHandler = toolHandlersByName.get("send_to"); + if (!sendToHandler) { + throw new Error("Internal tool handler unavailable: send_to"); + } + const result = await sendToHandler( + { + ...args, + mode: "agent", + agent_id: agentId, + target: undefined, + targeting: undefined, + }, + {}, + ); + if (!result.isError) return result; + + const preserveLegacyToolLabel = (value: string): string => + value.replaceAll("send_to.", "send_to_agent."); + return { + ...result, + content: result.content.map((item) => ({ + ...item, + text: preserveLegacyToolLabel(item.text), + })), + structuredContent: result.structuredContent + ? { + ...result.structuredContent, + ...(typeof result.structuredContent.error === "string" + ? { + error: preserveLegacyToolLabel( + result.structuredContent.error, + ), + } + : {}), + } + : undefined, }; - return okFormatted(formatOk("send_to_agent", data), data); } catch (e) { if (e instanceof DeliverySafetyGateError) { return err(e, { @@ -13520,7 +13687,9 @@ export function createServer(opts?: CreateServerOptions): McpServer { const error = new SubmitVerificationError( `Supersede submission could not be verified for ${args.agent_id}`, delivery.retry_count, - "submit_evidence_absent", + delivery.delivery === "queued" + ? "input_still_pending" + : "submit_evidence_absent", ); return err(error, { error_code: "supersede_submit_unverified", @@ -14182,6 +14351,7 @@ export function createServer(opts?: CreateServerOptions): McpServer { await originalHandler(args, extra), name, replacement, + emittedDeprecationWarnings, ), } : {}), diff --git a/src/worktree.ts b/src/worktree.ts index 132225b..2255507 100644 --- a/src/worktree.ts +++ b/src/worktree.ts @@ -42,7 +42,7 @@ export interface PrepareWorktreeInput { repo: string; repoRoot?: string; homeGitsDir?: string; - worktree?: boolean | WorktreeRequest; + worktree?: boolean | string | WorktreeRequest; exec?: WorktreeExec; } @@ -114,14 +114,18 @@ function assertAllowedWorktreePath( function normalizeWorktreeRequest( repo: string, - request: boolean | WorktreeRequest | undefined, + request: boolean | string | WorktreeRequest | undefined, ): Required> & Omit & { generatedName: boolean; name: string; } { const spec: WorktreeRequest = - request === true || request === false || request === undefined ? {} : request; + typeof request === "string" + ? { name: request } + : request === true || request === false || request === undefined + ? {} + : request; const generatedName = spec.name === undefined; const name = generatedName ? defaultWorkerName(repo) : safeName(spec.name ?? ""); return { diff --git a/tests/agent-engine.test.ts b/tests/agent-engine.test.ts index eecff4e..849aefc 100644 --- a/tests/agent-engine.test.ts +++ b/tests/agent-engine.test.ts @@ -12036,6 +12036,45 @@ Session ID: ${sessionId}`, }); }); + it("persists a composer-accepted queue without replaying it", async () => { + const submitter = vi.fn(); + engine.setDeliverySubmitter(submitter); + + const receipt = engine.acceptComposerQueue({ + delivery_id: "composer-queue-1", + agent_id: "composer-queued-agent", + text: "already accepted by the Codex queue", + press_enter: true, + source_event: "send_to", + retry_count: 0, + }); + + expect(receipt).toMatchObject({ + delivery_state: "queued", + terminal: false, + composer_accepted: true, + submit_verified: null, + }); + await engine.drainDeliveryQueue(); + expect(submitter).not.toHaveBeenCalled(); + + const restartedState = new StateManager(TEST_DIR); + const restarted = new AgentEngine( + restartedState, + new AgentRegistry(restartedState, async () => []), + mockClient, + ); + try { + expect(restarted.getDeliveryReceipt(receipt.delivery_id)).toMatchObject({ + delivery_state: "queued", + terminal: false, + composer_accepted: true, + }); + } finally { + restarted.dispose(); + } + }); + it("makes a hung queued submission terminal-uncertain without replay", async () => { stateMgr.writeState( makeRecord({ diff --git a/tests/agent-health.test.ts b/tests/agent-health.test.ts index e35aa0d..26b6a8b 100644 --- a/tests/agent-health.test.ts +++ b/tests/agent-health.test.ts @@ -153,6 +153,26 @@ describe("agent lifecycle health", () => { }); }); + it("does not degrade an auto-discovered agent solely for an absent inbox monitor after boot grace", () => { + const createdAt = "2026-06-26T20:00:00.000Z"; + const health = evaluateAgentHealth( + makeRecord({ + agent_id: "auto-codex-surface-306", + task_summary: "(auto-discovered)", + cli_session_id: null, + created_at: createdAt, + }), + { monitor_alive: false, screen_status: "idle" }, + { + now: () => + Date.parse(createdAt) + AGENT_HEALTH_INBOX_MONITOR_BOOT_GRACE_MS + 1, + }, + ); + + expect(health.status).toBe("healthy"); + expect(health.issue_severities?.inbox_monitor_not_alive).toBe("info"); + }); + it("distinguishes a deleted inbox channel dir from a never-armed monitor", () => { const deleted = evaluateAgentHealth(makeRecord(), { monitor_alive: false, @@ -187,7 +207,7 @@ describe("agent lifecycle health", () => { ); }); - it("marks ambiguous auto-discovered repo labels as degraded label hygiene", () => { + it("reports ambiguous auto-discovered repo labels without degrading health", () => { const health = evaluateAgentHealth( makeRecord({ agent_id: "auto-codex-surface-999", @@ -198,7 +218,7 @@ describe("agent lifecycle health", () => { { monitor_alive: false }, ); - expect(health.status).toBe("degraded"); + expect(health.status).toBe("healthy"); expect(health.issue_codes).toContain("ambiguous_repo_cwd_label"); expect(health.issue_severities?.ambiguous_repo_cwd_label).toBe("info"); }); diff --git a/tests/enter-reliability.test.ts b/tests/enter-reliability.test.ts index f70433a..5097a04 100644 --- a/tests/enter-reliability.test.ts +++ b/tests/enter-reliability.test.ts @@ -632,10 +632,11 @@ describe("enter reliability", () => { expect(parsed.error).toMatch(/press_enter|allow_busy|boolean/i); }); - it("does not retry Enter for send_to when the agent composer remains ambiguously pending", async () => { + it("retries Enter once when a Codex composer still holds the exact send", async () => { const client = new FakeClaudeSurfaceClient(); + client.cli = "codex"; server = createReliabilityServer(client); - registerAgent(server); + registerAgent(server, { cli: "codex" }); const result = await callTool(server, "send_to", { agent_id: "agent-1", @@ -646,22 +647,21 @@ describe("enter reliability", () => { const parsed = parseResult(result); const events = readEventLog(); - expect(result.isError).toBe(true); - expect(parsed.ok).toBe(false); - expect(parsed.submit_verified).toBe(false); - expect(parsed.submit_verification_reason).toBe("input_still_pending"); - expect(parsed.retry_safe).toBe(false); - expect(parsed.retry_count).toBe(0); + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("submitted"); + expect(parsed.submit_verified).toBe(true); + expect(parsed.retry_count).toBe(1); expect(client.sendCalls.join("")).toHaveLength(2000); expect(client.sendKeyCalls.filter((key) => key === "return")).toHaveLength( - 1, + 2, ); expect( events.some( (event) => event.event_type === "send_to" && - event.submit_verified === false && - event.retry_count === 0, + event.submit_verified === true && + event.retry_count === 1, ), ).toBe(true); expect( @@ -971,7 +971,7 @@ describe("enter reliability", () => { expect(events[0]?.submit_verified).toBeNull(); }); - it("Probe B: rejects an allow_busy Codex receipt when Return leaves the follow-up in the composer", async () => { + it("Probe B: retries once before rejecting a Codex composer that still holds the follow-up", async () => { const client = new FakeClaudeSurfaceClient(); client.requiredReturns = 99; client.cli = "codex"; @@ -993,17 +993,17 @@ describe("enter reliability", () => { expect(result.isError).toBe(true); expect(parsed.ok).toBe(false); expect(parsed.submit_verified).toBe(false); - expect(parsed.retry_count).toBe(0); + expect(parsed.retry_count).toBe(1); expect(client.sendKeyCalls.filter((key) => key === "return")).toHaveLength( - 1, + 2, ); expect(client.sendCalls.join("")).toBe(followUp); expect(events).toHaveLength(1); expect(events[0]?.submit_verified).toBe(false); - expect(events[0]?.retry_count).toBe(0); + expect(events[0]?.retry_count).toBe(1); }, 10_000); - it("rejects the exact PR343 live Codex queue fixture for allow_busy send_to", async () => { + it("accepts the exact PR343 live Codex queue as a nonterminal delivery", async () => { const client = new FakeClaudeSurfaceClient(); client.requiredReturns = 99; client.cli = "codex"; @@ -1032,16 +1032,74 @@ describe("enter reliability", () => { "› Summarize recent commits", ); expect(client.sendCalls.join("")).toBe(PR343_LIVE_QUEUE_PAYLOAD); - expect(result.isError).toBe(true); - expect(parsed.ok).toBe(false); - expect(parsed.submit_verified).toBe(false); + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("queued"); + expect(parsed.delivery_state).toBe("queued"); + expect(parsed.terminal).toBe(false); + expect(parsed.submit_verified).toBeNull(); expect(parsed.retry_count).toBe(0); expect(client.sendKeyCalls.filter((key) => key === "return")).toHaveLength( 1, ); }, 10_000); - it("rejects a correlated live Codex queue on the first verification frame within 600ms", async () => { + it("accepts the exact Codex queue through send_to mode=surface", async () => { + const client = new FakeClaudeSurfaceClient(); + client.requiredReturns = 99; + client.cli = "codex"; + client.keepWorkingStatusWhilePending = true; + client.postReturnPendingScreenText = + CODEX_PR343_LIVE_QUEUED_FOLLOWUP_SCREEN; + server = createReliabilityServer(client); + registerAgent(server, { state: "ready", cli: "codex" }); + + const result = await callToolInTimerSteps(server, "send_to", { + mode: "surface", + surface: client.surface, + text: PR343_LIVE_QUEUE_PAYLOAD, + press_enter: true, + }); + const parsed = parseResult(result); + + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("queued"); + expect(parsed.delivery_state).toBe("queued"); + expect(parsed.terminal).toBe(false); + expect(parsed.submit_verified).toBeNull(); + expect(client.sendKeyCalls.filter((key) => key === "return")).toHaveLength( + 1, + ); + }, 10_000); + + it("routes send_to_agent through the truthful queued receipt path", async () => { + const client = new FakeClaudeSurfaceClient(); + client.requiredReturns = 99; + client.cli = "codex"; + client.keepWorkingStatusWhilePending = true; + client.postReturnPendingScreenText = + CODEX_PR343_LIVE_QUEUED_FOLLOWUP_SCREEN; + server = createReliabilityServer(client); + registerAgent(server, { state: "ready", cli: "codex" }); + + const result = await callToolInTimerSteps(server, "send_to_agent", { + agent_id: "agent-1", + text: PR343_LIVE_QUEUE_PAYLOAD, + press_enter: true, + }); + const parsed = parseResult(result); + + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("queued"); + expect(parsed.delivery_state).toBe("queued"); + expect(parsed.terminal).toBe(false); + expect(parsed.submit_verified).toBeNull(); + expect(parsed.deprecation_warning).toContain("send_to(mode=agent)"); + }, 10_000); + + it("accepts a correlated live Codex queue on the first verification frame within 600ms", async () => { const client = new FakeClaudeSurfaceClient(); client.requiredReturns = 99; client.cli = "codex"; @@ -1078,18 +1136,22 @@ describe("enter reliability", () => { const result = await resultPromise; const parsed = parseResult(result); - expect(result.isError).toBe(true); - expect(parsed.submit_verified).toBe(false); + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("queued"); + expect(parsed.delivery_state).toBe("queued"); + expect(parsed.terminal).toBe(false); + expect(parsed.submit_verified).toBeNull(); expect(parsed.retry_count).toBe(0); expect(settledAt).not.toBeNull(); expect(settledAt! - startedAt).toBeLessThanOrEqual(600); - expect(client.postReturnScreenReadAttempts).toBe(1); + expect(client.postReturnScreenReadAttempts).toBeGreaterThanOrEqual(1); expect(client.sendKeyCalls.filter((key) => key === "return")).toHaveLength( 1, ); }, 10_000); - it("bounds a definitive allow_busy queued/composer failure to 600ms", async () => { + it("bounds a definitive allow_busy composer failure to 1000ms", async () => { const client = new FakeClaudeSurfaceClient(); client.requiredReturns = 99; client.cli = "codex"; @@ -1127,14 +1189,14 @@ describe("enter reliability", () => { expect(result.isError).toBe(true); expect(parsed.submit_verified).toBe(false); expect(settledAt).not.toBeNull(); - expect(settledAt! - startedAt).toBeLessThanOrEqual(600); + expect(settledAt! - startedAt).toBeLessThanOrEqual(1000); expect(client.postReturnScreenReadAttempts).toBeGreaterThanOrEqual(2); expect(client.sendKeyCalls.filter((key) => key === "return")).toHaveLength( - 1, + 2, ); }, 10_000); - it("Probe E: rejects working status across a truncated Codex queue-to-composer transition", async () => { + it("Probe E: accepts when a correlated Codex queue appears before a truncated composer transition", async () => { const client = new FakeClaudeSurfaceClient(); client.requiredReturns = 99; client.cli = "codex"; @@ -1163,9 +1225,12 @@ describe("enter reliability", () => { ); expect(followUp).toHaveLength(541); - expect(result.isError).toBe(true); - expect(parsed.ok).toBe(false); - expect(parsed.submit_verified).toBe(false); + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("queued"); + expect(parsed.delivery_state).toBe("queued"); + expect(parsed.terminal).toBe(false); + expect(parsed.submit_verified).toBeNull(); expect(parsed.retry_count).toBe(0); expect(queuedScreen).toContain("↳ Probe E queued follow-up evidence"); expect(queuedScreen).not.toContain(tail); @@ -1175,7 +1240,7 @@ describe("enter reliability", () => { 1, ); expect(events).toHaveLength(1); - expect(events[0]?.submit_verified).toBe(false); + expect(events[0]?.submit_verified).toBeNull(); expect(events[0]?.retry_count).toBe(0); }, 10_000); @@ -1210,7 +1275,7 @@ describe("enter reliability", () => { ); }, 10_000); - it("detects a wrapped live Codex queue heading before accepting working status", async () => { + it("accepts a wrapped live Codex queue heading as a nonterminal delivery", async () => { const client = new FakeClaudeSurfaceClient(); client.requiredReturns = 99; client.cli = "codex"; @@ -1235,16 +1300,19 @@ describe("enter reliability", () => { "Messages to be submitted after next\n tool call", ); expect(queuedScreen).toContain("↳ narrow-pane queued follow-up"); - expect(result.isError).toBe(true); - expect(parsed.ok).toBe(false); - expect(parsed.submit_verified).toBe(false); + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("queued"); + expect(parsed.delivery_state).toBe("queued"); + expect(parsed.terminal).toBe(false); + expect(parsed.submit_verified).toBeNull(); expect(parsed.retry_count).toBe(0); expect(client.sendKeyCalls.filter((key) => key === "return")).toHaveLength( 1, ); }, 10_000); - it("rejects decorated wrapped Codex queue chrome correlated to this send", async () => { + it("accepts decorated wrapped Codex queue chrome correlated to this send", async () => { const client = new FakeClaudeSurfaceClient(); client.requiredReturns = 99; client.cli = "codex"; @@ -1268,9 +1336,12 @@ describe("enter reliability", () => { expect(queuedScreen).toContain("│ tool call"); expect(queuedScreen).toContain("│ ↳ decorated correlated queue payload"); - expect(result.isError).toBe(true); - expect(parsed.ok).toBe(false); - expect(parsed.submit_verified).toBe(false); + expect(result.isError).not.toBe(true); + expect(parsed.ok).toBe(true); + expect(parsed.delivery).toBe("queued"); + expect(parsed.delivery_state).toBe("queued"); + expect(parsed.terminal).toBe(false); + expect(parsed.submit_verified).toBeNull(); expect(parsed.retry_count).toBe(0); }, 10_000); diff --git a/tests/inbox-nudge.test.ts b/tests/inbox-nudge.test.ts index dd9239f..c1b1685 100644 --- a/tests/inbox-nudge.test.ts +++ b/tests/inbox-nudge.test.ts @@ -249,7 +249,7 @@ describe("dispatch_to_agent nudge (state-independent inbox wake)", () => { rmSync(inboxDir, { recursive: true, force: true }); }); - it("returns a non-success durable receipt when never armed, while still nudging a TERMINAL agent", async () => { + it("returns verified success when never armed but the TERMINAL-agent nudge submits", async () => { const agentId = await spawnTestAgent(server); // Poison-equivalent: force a terminal state. The nudge must still go out. @@ -272,16 +272,22 @@ describe("dispatch_to_agent nudge (state-independent inbox wake)", () => { const parsed = result.structuredContent ?? JSON.parse(result.content[0].text); - expect(parsed.ok).toBe(false); - expect(parsed.error_code).toBe("inbox_monitor_never_armed"); + expect(parsed.ok).toBe(true); expect(parsed.monitor_state).toBe("never-armed"); expect(parsed.delivery_status).toBe("queued_monitor_never_armed"); - expect(parsed.retryable).toBe(false); expect(parsed.durable).toBe(true); expect(parsed.dispatched.task).toBe("GO"); expect(parsed.monitor_alive).toBe(false); expect(parsed.nudge.attempted).toBe(true); expect(parsed.nudge.sent).toBe(true); + expect(parsed.nudge.delivery).toBe("submitted"); + expect(parsed.nudge.delivery_id).toEqual(expect.any(String)); + const engine = server._registeredTools["interact"]._engine; + expect(engine.getDeliveryReceipt(parsed.nudge.delivery_id)).toMatchObject({ + delivery_state: "submitted", + terminal: true, + source_event: "dispatch_nudge", + }); // The pointer was typed into the agent's surface despite terminal state. const after = sendCalls(exec); expect(after.length).toBeGreaterThan(before); @@ -326,6 +332,71 @@ describe("dispatch_to_agent nudge (state-independent inbox wake)", () => { expect(readInbox(agentId, { baseDir: inboxDir })).toHaveLength(1); }); + it("queues a busy-agent inbox wake without typing into its active composer", async () => { + const agentId = await spawnTestAgent(server); + const engine = server._registeredTools["interact"]._engine; + const working = engine.stateMgr.updateRecord(agentId, { state: "working" }); + engine.getRegistry().set(agentId, working); + writeHeartbeat(agentId, { baseDir: inboxDir, now: () => 1 }); + + const before = sendCalls(exec).length; + const result = await server._registeredTools["dispatch_to_agent"].handler( + { + agent_id: agentId, + task: "Read the durable inbox after the current turn", + from: "orc", + nudge: "auto", + }, + {} as any, + ); + const parsed = + result.structuredContent ?? JSON.parse(result.content[0].text); + + expect(parsed.ok).toBe(true); + expect(parsed.nudge).toMatchObject({ + attempted: true, + sent: true, + delivery: "queued", + delivery_id: expect.any(String), + }); + expect(sendCalls(exec)).toHaveLength(before); + expect(engine.getDeliveryReceipt(parsed.nudge.delivery_id)).toMatchObject({ + delivery_state: "queued", + terminal: false, + source_event: "dispatch_nudge", + }); + }); + + it("reports success when a never-armed busy recipient accepts the verified nudge queue", async () => { + const agentId = await spawnTestAgent(server); + const engine = server._registeredTools["interact"]._engine; + const working = engine.stateMgr.updateRecord(agentId, { state: "working" }); + engine.getRegistry().set(agentId, working); + + const before = sendCalls(exec).length; + const result = await server._registeredTools["dispatch_to_agent"].handler( + { + agent_id: agentId, + task: "Live queue acceptance is delivery evidence", + from: "orc", + nudge: "auto", + }, + {} as any, + ); + const parsed = + result.structuredContent ?? JSON.parse(result.content[0].text); + + expect(parsed.ok).toBe(true); + expect(parsed.monitor_state).toBe("never-armed"); + expect(parsed.nudge).toMatchObject({ + attempted: true, + sent: true, + delivery: "queued", + delivery_id: expect.any(String), + }); + expect(sendCalls(exec)).toHaveLength(before); + }); + it("does NOT nudge when the monitor heartbeat is fresh", async () => { const agentId = await spawnTestAgent(server); writeHeartbeat(agentId, { baseDir: inboxDir }); diff --git a/tests/server-agent-tools.test.ts b/tests/server-agent-tools.test.ts index a02eba9..c4ca35e 100644 --- a/tests/server-agent-tools.test.ts +++ b/tests/server-agent-tools.test.ts @@ -9893,10 +9893,13 @@ codex> const queued = parseToolResult(result); expect(result.isError).toBeFalsy(); expect(queued).toMatchObject({ + ok: true, agent_id: agentId, delivery_id: expect.any(String), + delivery: "queued", delivery_state: "queued", terminal: false, + submit_verified: null, }); expect( mockExec.mock.calls.filter(([, args]) => args.includes("send")), diff --git a/tests/server.test.ts b/tests/server.test.ts index cca36f9..169d5a1 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -485,6 +485,51 @@ describe("createServer", () => { rmSync(stateDir, { recursive: true, force: true }); }); + it("includes a working send_to example in SDK-level invalid-mode errors", async () => { + const stateDir = processScopedTmpDir("cmuxlayer-send-to-schema-error"); + rmSync(stateDir, { recursive: true, force: true }); + const server = createServer({ + exec: vi.fn().mockResolvedValue({ stdout: "{}", stderr: "" }), + stateDir, + controlHealthIntervalMs: 0, + defaultPalette: "send_to", + }); + const client = new Client({ + name: "send-to-schema-error", + version: "0.1.0", + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport), + ]); + try { + const result = await client.callTool({ + name: "send_to", + arguments: { + mode: "message", + agent_id: "cmuxlayerCodex-1234", + text: "hello", + }, + }); + expect(result.isError).toBe(true); + const validationText = result.content + .filter((item) => item.type === "text") + .map((item) => item.text) + .join("\n") + .replaceAll('\\"', '"'); + expect(validationText).toContain( + 'Example: send_to({ mode: "agent", agent_id: "cmuxlayerCodex-1234", text: "hello" })', + ); + } finally { + await client.close(); + await server.close(); + rmSync(stateDir, { recursive: true, force: true }); + } + }); + it("registers Claude channel capability when enabled", () => { const server = createServer({ skipAgentLifecycle: true, @@ -3012,7 +3057,8 @@ describe("tool handler integration", () => { expect(data.title).toBe("BL-LEAD"); expect(data.model).toBe("Opus 4.8"); expect(data.agent_type).toBe("claude"); - expect(result.content[0].text).toContain("delivered to BL-LEAD"); + expect(result.content[0].text).toContain("typed into BL-LEAD"); + expect(result.content[0].text).toContain("not submitted"); expect(result.content[0].text).toContain("Opus 4.8"); expect(result.content[0].text).toContain("claude"); rmSync(stateDir, { recursive: true, force: true }); @@ -3032,7 +3078,8 @@ describe("tool handler integration", () => { expect(data.title).toBeUndefined(); expect(data.model).toBeUndefined(); expect(data.agent_type).toBeUndefined(); - expect(result.content[0].text).toContain("delivered to surface:unknown"); + expect(result.content[0].text).toContain("typed into surface:unknown"); + expect(result.content[0].text).toContain("not submitted"); }); it("send_command fails closed when tracked-agent verification has no screen evidence (F8)", async () => { diff --git a/tests/spawn-monitor-boot.test.ts b/tests/spawn-monitor-boot.test.ts index 61f3100..ad11193 100644 --- a/tests/spawn-monitor-boot.test.ts +++ b/tests/spawn-monitor-boot.test.ts @@ -273,7 +273,7 @@ describe("spawn monitor boot", () => { } }); - it("durably nudges but does not report delivery success until the agent heartbeats", async () => { + it("reports success when the verified nudge submits before the agent heartbeats", async () => { const spawn = server._registeredTools["spawn_agent"]; const dispatch = server._registeredTools["dispatch_to_agent"]; @@ -302,11 +302,9 @@ describe("spawn monitor boot", () => { ); const parsed = parseToolResult(result); - expect(parsed.ok).toBe(false); - expect(parsed.error_code).toBe("inbox_monitor_never_armed"); + expect(parsed.ok).toBe(true); expect(parsed.monitor_state).toBe("never-armed"); expect(parsed.delivery_status).toBe("queued_monitor_never_armed"); - expect(parsed.retryable).toBe(false); expect(parsed.durable).toBe(true); expect(parsed.monitor_alive).toBe(false); expect(parsed.health.issue_codes).toContain("inbox_monitor_not_alive"); diff --git a/tests/thin-core-tools.test.ts b/tests/thin-core-tools.test.ts index 3f6eec9..81ec95d 100644 --- a/tests/thin-core-tools.test.ts +++ b/tests/thin-core-tools.test.ts @@ -31,6 +31,12 @@ const LEGACY_TOOL_NAMES = [ "wait_for_all", ] as const; +const REQUIRED_ESCAPE_HATCHES = [ + "send_command", + "send_key", + "new_split", +] as const; + function makeExec(): ExecFn { return vi.fn().mockImplementation(async (_cmd, args: string[]) => { if (args.includes("list-workspaces")) { @@ -151,13 +157,45 @@ describe("thin-core tool palette", () => { { _meta?: Record } >; - for (const name of LEGACY_TOOL_NAMES) { + for (const name of LEGACY_TOOL_NAMES.filter( + (candidate) => !REQUIRED_ESCAPE_HATCHES.includes(candidate as any), + )) { expect(tools[name]?._meta).toMatchObject({ defer_loading: true, deprecated: true, "cmuxlayer/interim": true, }); } + for (const name of REQUIRED_ESCAPE_HATCHES) { + expect(tools[name]?._meta).toMatchObject({ + defer_loading: true, + "cmuxlayer/interim": true, + }); + expect(tools[name]?._meta?.deprecated).not.toBe(true); + } + }); + + it("accepts optional new_split direction and string worktree shorthand in tool schemas", () => { + const server = createServer({ + exec: makeExec(), + disableSpawnPreflight: true, + controlHealthIntervalMs: 0, + }) as any; + + expect( + server._registeredTools.new_split.inputSchema.safeParse({ + type: "terminal", + role: "worker", + }).success, + ).toBe(true); + expect( + server._registeredTools.spawn_agent.inputSchema.safeParse({ + repo: "cmuxlayer", + cli: "codex", + role: "worker", + worktree: "tool-usage", + }).success, + ).toBe(true); }); }); @@ -220,6 +258,71 @@ describe("send_to consolidated modes", () => { ); }); + it("does not claim a terminal submission when surface mode only types text", async () => { + const exec = makeExec(); + const server = createServer({ + exec, + defaultPalette: "send_to", + disableSpawnPreflight: true, + controlHealthIntervalMs: 0, + }) as any; + + const result = await server._registeredTools.send_to.handler( + { + mode: "surface", + target: "surface:1", + text: "typed but not submitted", + press_enter: false, + }, + {}, + ); + const parsed = parseResult(result); + + expect(result.isError).toBeUndefined(); + expect(parsed.ok).toBe(true); + expect(parsed.submit_verified).toBeNull(); + expect(parsed.delivery).not.toBe("submitted"); + expect(parsed.delivery_state).not.toBe("submitted"); + expect(parsed.terminal).not.toBe(true); + expect(parsed.delivered).toBe(true); + expect(parsed.typed).toBe(true); + expect(result.content[0].text).toContain("typed into"); + expect(result.content[0].text).toContain("not submitted"); + }); + + it("does not claim a terminal submission when surface verification was skipped", async () => { + const exec = makeExec(); + const server = createServer({ + exec, + defaultPalette: "send_to", + disableSpawnPreflight: true, + controlHealthIntervalMs: 0, + }) as any; + + const result = await server._registeredTools.send_to.handler( + { + mode: "surface", + target: "surface:1", + text: "return pressed without verification", + press_enter: true, + }, + {}, + ); + const parsed = parseResult(result); + + expect(result.isError).toBeUndefined(); + expect(parsed.ok).toBe(true); + expect(parsed.submit_attempted).toBe(true); + expect(parsed.submit_verified).toBeNull(); + expect(parsed.delivery).not.toBe("submitted"); + expect(parsed.delivery_state).not.toBe("submitted"); + expect(parsed.terminal).not.toBe(true); + expect(parsed.delivered).toBe(true); + expect(parsed.typed).toBeUndefined(); + expect(result.content[0].text).toContain("submission attempted"); + expect(result.content[0].text).toContain("not verified"); + }); + it("routes command mode through atomic raw-surface command delivery", async () => { const exec = makeExec(); const server = createServer({ exec, controlHealthIntervalMs: 0 }) as any; @@ -300,8 +403,31 @@ describe("consolidated compatibility", () => { expect(parseResult(result)).toMatchObject({ deprecation_warning: expect.stringContaining("send_to(mode=surface)"), }); + const second = await server._registeredTools.send_input.handler( + { surface: "surface:1", text: "legacy again", press_enter: false }, + {}, + ); + expect(warn).toHaveBeenCalledTimes(1); + expect(parseResult(second)).not.toHaveProperty("deprecation_warning"); warn.mockRestore(); }); + + it("prints one working send_to example for invalid mode input", async () => { + const server = createServer({ + exec: makeExec(), + disableSpawnPreflight: true, + controlHealthIntervalMs: 0, + }) as any; + + const result = await server._registeredTools.send_to.handler( + { mode: "message", target: "agent-1", text: "hello" }, + {}, + ); + + expect(parseResult(result).error).toMatch( + /Example: send_to\(\{ mode: "agent", agent_id: "cmuxlayerCodex-1234", text: "hello" \}\)/, + ); + }); }); describe("legacy-name drift", () => { diff --git a/tests/worktree.test.ts b/tests/worktree.test.ts index 176fcaf..246df23 100644 --- a/tests/worktree.test.ts +++ b/tests/worktree.test.ts @@ -199,6 +199,26 @@ describe("worktree helpers", () => { ]); }); + it("accepts a worktree name string as shorthand for a named request", async () => { + const repoRoot = join(TEST_ROOT, "repo"); + mkdirSync(repoRoot, { recursive: true }); + const exec = vi.fn().mockResolvedValue({ stdout: "", stderr: "" }); + + const result = await prepareWorktree({ + repo: "cmuxlayer", + repoRoot, + homeGitsDir: TEST_ROOT, + worktree: "tool usage", + exec, + }); + + expect(result).toMatchObject({ + path: join(repoRoot, ".worktrees", "tool-usage"), + name: "tool-usage", + branch: "wt/tool-usage", + }); + }); + it("rejects an empty explicit worktree name", async () => { await expect( prepareWorktree({