diff --git a/agent-node/src/cli.ts b/agent-node/src/cli.ts index 3d20af0d4..07f2657ab 100644 --- a/agent-node/src/cli.ts +++ b/agent-node/src/cli.ts @@ -3546,6 +3546,27 @@ let grokCopresenceRuntimeSession: GrokCopresenceSession | null = null; let grokCopresenceRuntimeOpening: Promise | null = null; let grokCopresenceLocalTaskSequence = 0; +async function retireCachedGrokCopresenceRuntime(): Promise { + const stopped = grokCopresenceRuntimeSession; + // ๐Ÿ”ด ๅŠจๆ€ import๏ผŒๅ’Œๆœฌๆ–‡ไปถ้‡Œๅฆๅค–ไธคๅค„ grok-copresence ็š„ๅ–ๆณ•ไธ€่‡ดใ€‚ + // ้™ๆ€ import ไผš่ขซ ESM ๆๅ‡ โ€”โ€” ่ขซๅฏผๅ…ฅๆจกๅ—็š„้กถๅฑ‚ไผšๅœจ cli.ts ็ฌฌไธ€ๆก่ฏญๅฅไน‹ๅ‰ๆ‰ง่กŒ๏ผŒ + // ไบŽๆ˜ฏ policy.ts ้กถๅฑ‚้‚ฃๅฅ readPinnedGrokCopresenceCapabilityProfile() ่ฏปๅˆฐ็š„ๆ˜ฏ + // **ambient ็Žฏๅขƒๅ˜้‡**๏ผŒ่€Œไธๆ˜ฏๅฏๅŠจๆ—ถไปŽ่Š‚็‚น้…็ฝฎ้’‰ไธ‹ๆฅ็š„ๆกฃไฝใ€‚ + // check-copresence-profile-pin.py ๅฐฑๆ˜ฏๅฎˆ่ฟ™ไธ€ๆก็š„๏ผŒๅฎƒๅœจๆœฌๅˆ†ๆ”ฏไธŠๆŠฅ็š„ๆญฃๆ˜ฏ่ฟ™้‡Œใ€‚ + const { retireStoppedGrokCopresenceRuntime } = await import( + "./runtime/grok-copresence/runtime-retirement" + ); + await retireStoppedGrokCopresenceRuntime(stopped, { + warn: (message) => warn(message), + retire: (retired) => { + if (grokCopresenceRuntimeSession !== retired) return; + grokCopresenceRuntimeSession = null; + clearGrokSession("fatal co-presence boundary; next task requires a fresh session"); + warn("[grok-copresence] retired stopped TUI; next task will open a fresh session"); + }, + }); +} + function grokCopresenceTimeoutMs(): number { const raw = process.env.GROK_CLI_TIMEOUT_MS || fileConfig.flags?.grokCliTimeoutMs @@ -3587,6 +3608,7 @@ async function ensureGrokCopresenceRuntime(): Promise { if (process.platform !== "linux") { throw new Error("grok co-presence preview currently requires Linux PTY, /proc, and Unix sockets"); } + await retireCachedGrokCopresenceRuntime(); if (grokCopresenceRuntimeSession) return grokCopresenceRuntimeSession; if (grokCopresenceRuntimeOpening) return grokCopresenceRuntimeOpening; diff --git a/agent-node/src/runtime/grok-copresence/allowlist-near-miss.test.ts b/agent-node/src/runtime/grok-copresence/allowlist-near-miss.test.ts index 06c0adbba..e299148f0 100644 --- a/agent-node/src/runtime/grok-copresence/allowlist-near-miss.test.ts +++ b/agent-node/src/runtime/grok-copresence/allowlist-near-miss.test.ts @@ -27,9 +27,9 @@ import { GROK_COPRESENCE_EFFECTIVE_TOOLS } from "./policy"; function validResolutionFor(tool: string) { return { requestTool: tool, - activeRequestId: `tool:${tool}`, + pendingRequestCount: 1, humanDecisionDispatched: false, - waitingHuman: true, + waitingHuman: false, turnOwner: "network" as const, terminalEventSeen: false, event: { diff --git a/agent-node/src/runtime/grok-copresence/profile-process-probe.ts b/agent-node/src/runtime/grok-copresence/profile-process-probe.ts index 06d58f295..ba97ce7fe 100644 --- a/agent-node/src/runtime/grok-copresence/profile-process-probe.ts +++ b/agent-node/src/runtime/grok-copresence/profile-process-probe.ts @@ -22,9 +22,9 @@ const args = buildGrokCopresenceArgs({ }); const automaticTool = (tool: string, turnOwner: "human" | "network") => isGrokPreviewAutomaticResolution({ requestTool: tool, - activeRequestId: `tool:${tool}`, + pendingRequestCount: 1, humanDecisionDispatched: false, - waitingHuman: true, + waitingHuman: false, turnOwner, terminalEventSeen: false, event: { diff --git a/agent-node/src/runtime/grok-copresence/runtime-retirement.test.ts b/agent-node/src/runtime/grok-copresence/runtime-retirement.test.ts new file mode 100644 index 000000000..ed8161d65 --- /dev/null +++ b/agent-node/src/runtime/grok-copresence/runtime-retirement.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "fs"; +import { join } from "path"; +import { retireStoppedGrokCopresenceRuntime } from "./runtime-retirement"; + +function fixture(input: { running: boolean; phase: string; closeError?: Error }) { + let closeCalls = 0; + return { + runtime: { + isRunning: input.running, + state: { phase: input.phase }, + async close() { + closeCalls += 1; + if (input.closeError) throw input.closeError; + }, + }, + closeCalls: () => closeCalls, + }; +} + +describe("Grok co-presence terminal-runtime retirement", () => { + test("retains live and recovering runtimes", async () => { + for (const state of [ + { running: true, phase: "idle" }, + { running: false, phase: "recovering" }, + ]) { + const probe = fixture(state); + let retired = false; + expect(await retireStoppedGrokCopresenceRuntime(probe.runtime, { + retire: () => { retired = true; }, + })).toBe(false); + expect(probe.closeCalls()).toBe(0); + expect(retired).toBe(false); + } + }); + + test("closes and retires a terminal runtime even when teardown reports an error", async () => { + for (const closeError of [undefined, new Error("already contained")]) { + const probe = fixture({ running: false, phase: "network_turn", closeError }); + const warnings: string[] = []; + let retiredRuntime: unknown; + expect(await retireStoppedGrokCopresenceRuntime(probe.runtime, { + retire: (runtime) => { retiredRuntime = runtime; }, + warn: (message) => warnings.push(message), + })).toBe(true); + expect(probe.closeCalls()).toBe(1); + expect(retiredRuntime).toBe(probe.runtime); + expect(warnings).toHaveLength(closeError ? 1 : 0); + } + }); + + test("wires retirement before returning the cached product runtime", () => { + const cli = readFileSync(join(import.meta.dir, "..", "..", "cli.ts"), "utf8"); + const ensureStart = cli.indexOf("async function ensureGrokCopresenceRuntime()"); + const ensureEnd = cli.indexOf("\nconst GROK_COPRESENCE_FAILURE_CODE_SET", ensureStart); + const ensure = cli.slice(ensureStart, ensureEnd); + const retireAt = ensure.indexOf("await retireCachedGrokCopresenceRuntime()"); + const returnCachedAt = ensure.indexOf("if (grokCopresenceRuntimeSession) return grokCopresenceRuntimeSession"); + expect(retireAt).toBeGreaterThanOrEqual(0); + expect(returnCachedAt).toBeGreaterThan(retireAt); + }); +}); diff --git a/agent-node/src/runtime/grok-copresence/runtime-retirement.ts b/agent-node/src/runtime/grok-copresence/runtime-retirement.ts new file mode 100644 index 000000000..83b23aecc --- /dev/null +++ b/agent-node/src/runtime/grok-copresence/runtime-retirement.ts @@ -0,0 +1,29 @@ +export interface RetireableGrokCopresenceRuntime { + readonly isRunning: boolean; + readonly state: { phase: string }; + close(): Promise; +} + +/** + * Finish a terminal co-presence runtime before its owner replaces the cached + * slot. A runtime doing its ordinary PTY recovery is deliberately retained. + */ +export async function retireStoppedGrokCopresenceRuntime( + runtime: T | null, + hooks: { + retire: (runtime: T) => void; + warn?: (message: string) => void; + }, +): Promise { + if (!runtime || runtime.isRunning || runtime.state.phase === "recovering") return false; + + try { + await runtime.close(); + } catch (error) { + hooks.warn?.( + `[grok-copresence] stopped runtime teardown reported: ${String((error as Error)?.message || error)}`, + ); + } + hooks.retire(runtime); + return true; +} diff --git a/agent-node/src/runtime/grok-copresence/runtime.test.ts b/agent-node/src/runtime/grok-copresence/runtime.test.ts index 7f932253b..f2199bacd 100644 --- a/agent-node/src/runtime/grok-copresence/runtime.test.ts +++ b/agent-node/src/runtime/grok-copresence/runtime.test.ts @@ -52,9 +52,9 @@ describe("Grok copresence launch and injection policy", () => { test("keeps the fixed-tool auto-resolution exception exact and limited to active turns", () => { const exact = { requestTool: "todo_write", - activeRequestId: "tool:todo_write", + pendingRequestCount: 1, humanDecisionDispatched: false, - waitingHuman: true, + waitingHuman: false, turnOwner: "network" as const, terminalEventSeen: false, event: { @@ -72,9 +72,9 @@ describe("Grok copresence launch and injection policy", () => { })).toBe(true); for (const mutation of [ { ...exact, requestTool: null }, - { ...exact, activeRequestId: "tool:read_file" }, + { ...exact, pendingRequestCount: 0 }, { ...exact, humanDecisionDispatched: true }, - { ...exact, waitingHuman: false }, + { ...exact, waitingHuman: true }, { ...exact, turnOwner: null }, { ...exact, terminalEventSeen: true }, { ...exact, event: { ...exact.event, decision: "allow_once" } }, @@ -93,9 +93,9 @@ describe("Grok copresence launch and injection policy", () => { for (const tool of ["todo_write", "search_tool", "use_tool"]) { const exact = { requestTool: tool, - activeRequestId: `tool:${tool}`, + pendingRequestCount: 1, humanDecisionDispatched: false, - waitingHuman: true, + waitingHuman: false, turnOwner: "human" as const, terminalEventSeen: false, event: { @@ -116,9 +116,9 @@ describe("Grok copresence launch and injection policy", () => { for (const tool of ["read_file", "Bash", "commhub_send_task"]) { expect(isGrokPreviewAutomaticResolution({ requestTool: tool, - activeRequestId: `tool:${tool}`, + pendingRequestCount: 1, humanDecisionDispatched: false, - waitingHuman: true, + waitingHuman: false, turnOwner: "human", terminalEventSeen: false, event: { @@ -1370,6 +1370,23 @@ describe("Grok copresence runtime integration", () => { handshakeTimeoutMs: 500, })).rejects.toThrow("already attached"); + input.write("/model\r"); + await waitFor(() => runtime!.state.phase === "idle"); + const afterBlockedSlash = await runtime.submit({ + taskId: "network-after-blocked-slash", + from: "dashboard", + text: "after blocked slash", + timeoutMs: 4_000, + }); + expect(afterBlockedSlash.replyText).toBe("FINAL network-after-blocked-slash"); + expect(fixture.humanPrompts).not.toContain("/model"); + + input.write("AC"); + await waitFor(() => runtime!.state.phase === "human_editing"); + input.write("\x1b[D"); + input.write("B\r"); + await waitFor(() => fixture.humanPrompts.includes("ABC")); + input.write("\x0f"); input.write("\x1b[Z"); input.write("\x1b[111;5u"); @@ -1400,7 +1417,7 @@ describe("Grok copresence runtime integration", () => { await waitFor(() => fixture.humanPrompts.includes("queued")); input.write("lf-human\n"); await waitFor(() => fixture.humanPrompts.includes("lf-human")); - expect(fixture.humanPrompts).toEqual(["first\rsecond", "queued", "lf-human"]); + expect(fixture.humanPrompts).toEqual(["ABC", "first\rsecond", "queued", "lf-human"]); const approvalPromise = runtime.submit({ taskId: "approval-1", @@ -1650,7 +1667,9 @@ describe("Grok copresence runtime integration", () => { from: "reviewer", text: `AUTO_RESOLVE_TODO_${mutation}`, timeoutMs: 3_000, - }), mutation).rejects.toThrow(/permission request|automatically resolved/); + }), mutation).rejects.toThrow( + /permission request|automatically resolved|unmatched automatic|human approval/, + ); await waitFor(() => !runtime!.isRunning); } finally { await runtime?.close(); @@ -1752,6 +1771,27 @@ describe("Grok copresence runtime integration", () => { } }, 8_000); + test("allows a pinned tool batch whose automatic resolutions are not request ordered", async () => { + const fixture = new RuntimeFixture(); + let runtime: GrokCopresenceRuntimeSession | undefined; + try { + runtime = await fixture.open(); + const result = await runtime.submit({ + taskId: "preview-batched-order", + from: "reviewer", + text: "AUTO_RESOLVE_BATCHED", + timeoutMs: 3_000, + }); + expect(result.replyText).toBe("BATCHED preview-batched-order"); + expect(runtime.isRunning).toBe(true); + expect(runtime.state.waitingHuman).toBe(false); + expect(fixture.approvalDecisionCount()).toBe(0); + } finally { + await runtime?.close(); + await fixture.close(); + } + }, 8_000); + test("never replies with a tool-bearing assistant when the final log is delayed past settling", async () => { const fixture = new RuntimeFixture(); let runtime: GrokCopresenceRuntimeSession | undefined; @@ -2355,6 +2395,7 @@ class FakePty implements GrokPtyLike { private dataListeners: Array<(data: string) => void> = []; private exitListeners: Array<(event: { exitCode: number; signal?: number }) => void> = []; private composer = ""; + private composerCursor = 0; private paste = false; private awaitingApprovalTask = ""; private lateCrashTask = ""; @@ -2653,6 +2694,36 @@ class FakePty implements GrokPtyLike { }, 150); return; } + if (message === "AUTO_RESOLVE_BATCHED") { + appendJson(join(this.sessionDir, "chat_history.jsonl"), { + type: "user", + content: `[Agent Network/from=${from}/task=${taskId}] ${message}`, + }); + const eventsPath = join(this.sessionDir, "events.jsonl"); + appendJson(eventsPath, { type: "turn_started", turn_number: 15 }); + for (const tool_name of ["search_tool", "use_tool", "search_tool"]) { + appendJson(eventsPath, { + type: "permission_requested", + tool_name, + ts: `preview-${tool_name}-requested`, + }); + } + for (const tool_name of ["search_tool", "search_tool", "use_tool"]) { + appendJson(eventsPath, { + type: "permission_resolved", + tool_name, + decision: "allow", + ts: `preview-${tool_name}-resolved`, + wait_ms: 0, + }); + } + appendJson(join(this.sessionDir, "chat_history.jsonl"), { + type: "assistant", + content: `BATCHED ${taskId}`, + }); + appendJson(eventsPath, { type: "turn_ended", outcome: "completed" }); + return; + } if ( message === "AUTO_RESOLVE_TODO_COALESCED" || message === "AUTO_RESOLVE_TODO_FRAGMENTED" @@ -2767,6 +2838,23 @@ class FakePty implements GrokPtyLike { ts: "preview-todo-requested-duplicate", }), }); + if (message.endsWith("_DUPLICATE") && !message.endsWith("CHANGED_DUPLICATE")) { + appendJson(join(this.sessionDir, "events.jsonl"), { + type: "permission_resolved", + tool_name: "todo_write", + decision: "allow", + ts: "preview-todo-resolved-once", + wait_ms: 0, + }); + appendJson(join(this.sessionDir, "chat_history.jsonl"), { + type: "assistant", + content: "must not complete with one unresolved automatic request", + }); + appendJson(join(this.sessionDir, "events.jsonl"), { + type: "turn_ended", + outcome: "completed", + }); + } return; } const resolved = message.endsWith("CAMEL_CASE") @@ -2866,6 +2954,16 @@ class FakePty implements GrokPtyLike { index += 6; continue; } + if (!this.paste && data.startsWith("\x1b[D", index)) { + this.composerCursor = Math.max(0, this.composerCursor - 1); + index += 3; + continue; + } + if (!this.paste && data.startsWith("\x1b[C", index)) { + this.composerCursor = Math.min(this.composer.length, this.composerCursor + 1); + index += 3; + continue; + } const char = data[index++]; if (this.awaitingApprovalTask && /^[1-9]$/.test(char)) { this.resolveApproval(); @@ -2873,14 +2971,19 @@ class FakePty implements GrokPtyLike { } if (char === "\x03" && !this.paste) { this.composer = ""; + this.composerCursor = 0; continue; } if ((char !== "\r" && char !== "\n") || this.paste) { - this.composer += char; + this.composer = this.composer.slice(0, this.composerCursor) + + char + + this.composer.slice(this.composerCursor); + this.composerCursor += char.length; continue; } const submitted = this.composer; this.composer = ""; + this.composerCursor = 0; if (this.awaitingApprovalTask) { this.resolveApproval(); } else if (submitted) { diff --git a/agent-node/src/runtime/grok-copresence/runtime.ts b/agent-node/src/runtime/grok-copresence/runtime.ts index fcedd8836..b6149f093 100644 --- a/agent-node/src/runtime/grok-copresence/runtime.ts +++ b/agent-node/src/runtime/grok-copresence/runtime.ts @@ -64,6 +64,7 @@ const MAX_DEFERRED_HUMAN_BYTES = 128 * 1024; const MAX_TUI_READINESS_BUFFER = 128 * 1024; const MAX_TAIL_READ_BYTES = 4 * 1024 * 1024; const MAX_LIFECYCLE_LINE_BYTES = 256 * 1024; +const MAX_PENDING_AUTOMATIC_PERMISSIONS = 64; const MAX_RESUME_AUDIT_BYTES = 64 * 1024 * 1024; const UNIX_SOCKET_PATH_MAX_BYTES = 100; const GROK_TUI_READY_TEXT = "Shift+Tab:mode"; @@ -651,22 +652,36 @@ function firstApprovalInputAction(data: Buffer): ApprovalInputAction | null { return null; } -function knownComposerNavigationLength(data: Buffer): number { +type ComposerNavigation = { + length: number; + kind: "edit" | "history" | "viewport"; +}; + +function knownComposerNavigation(data: Buffer): ComposerNavigation | null { if (data.length >= 3 && data[0] === 0x1b && data[1] === 0x4f) { - return "ABCDHF".includes(String.fromCharCode(data[2])) ? 3 : 0; + const final = String.fromCharCode(data[2]); + if ("AB".includes(final)) return { length: 3, kind: "history" }; + if ("CDHF".includes(final)) return { length: 3, kind: "edit" }; + return null; } - if (data.length < 3 || data[0] !== 0x1b || data[1] !== 0x5b) return 0; + if (data.length < 3 || data[0] !== 0x1b || data[1] !== 0x5b) return null; const limit = Math.min(data.length, 16); for (let index = 2; index < limit; index++) { const byte = data[index]; if (byte < 0x40 || byte > 0x7e) continue; const final = String.fromCharCode(byte); const params = data.subarray(2, index).toString("ascii"); - if ("ABCDHF".includes(final)) return index + 1; - if (final === "~" && /^(?:1|3|4|5|6|7|8)(?:;\d+)*$/.test(params)) return index + 1; - return 0; + if ("AB".includes(final)) return { length: index + 1, kind: "history" }; + if ("CDHF".includes(final)) return { length: index + 1, kind: "edit" }; + if (final === "~" && /^(?:1|3|4|7|8)(?:;\d+)*$/.test(params)) { + return { length: index + 1, kind: "edit" }; + } + if (final === "~" && /^(?:5|6)(?:;\d+)*$/.test(params)) { + return { length: index + 1, kind: "viewport" }; + } + return null; } - return 0; + return null; } class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { @@ -696,6 +711,7 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { private composerPending = Buffer.alloc(0); private humanComposerAudit = ""; private humanComposerAuditTainted = false; + private humanComposerAuditUnsafe = false; private humanComposerAuditOverflow = false; private humanComposerSawSlash = false; private humanComposerLeadingSlash = false; @@ -714,7 +730,8 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { private quarantinedNetworkTaskId = ""; private approvalDecisionDispatched = false; private activePermissionRequestId: string | null = null; - private activePermissionExactPreviewTool: string | null = null; + private pendingAutomaticPermissions = new Map(); + private pendingAutomaticPermissionTotal = 0; private activeTurnTerminalEventSeen = false; private spawnEnv: NodeJS.ProcessEnv; private readonly controlledSpawnEnv: NodeJS.ProcessEnv; @@ -1192,7 +1209,7 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { this.lifecycleBuffer = ""; this.approvalDecisionDispatched = false; this.activePermissionRequestId = null; - this.activePermissionExactPreviewTool = null; + this.clearAutomaticPermissionCorrelation(); this.resetHumanComposerAudit(); this.completionPendingSince = 0; this.lastChatActivityAt = 0; @@ -1523,8 +1540,21 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { continue; } if (!this.humanPasteMode) { - const navigationLength = knownComposerNavigationLength(remainder); - if (navigationLength > 0) { + const navigation = knownComposerNavigation(remainder); + if (navigation) { + if (navigation.kind === "history") { + // History can recall a slash command that never crossed this + // input proxy. Keep it out of the policy-owning TUI entirely. + this.warnBlockedPermissionModeChange("composer history navigation"); + return; + } + if (navigation.kind === "viewport") { + // Scrolling rendered output does not edit or submit the + // composer and must not claim human turn ownership. + this.writeHumanBytes(remainder.subarray(0, navigation.length)); + offset += navigation.length; + continue; + } if (this.humanComposerSawSlash) { this.writeHumanBytes(Buffer.from("\x03", "binary")); this.resetHumanComposerAudit(); @@ -1536,9 +1566,9 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { return; } if (this.arbitration.phase === "idle") this.transition({ type: "human_input_started" }); - this.writeHumanBytes(remainder.subarray(0, navigationLength)); + this.writeHumanBytes(remainder.subarray(0, navigation.length)); this.humanComposerAuditTainted = true; - offset += navigationLength; + offset += navigation.length; continue; } // Unknown CSI/SS3/Alt sequences include enhanced keyboard encodings @@ -1650,6 +1680,7 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { this.humanComposerAudit = this.humanComposerAudit.slice(-8_192); this.humanComposerAuditOverflow = true; this.humanComposerAuditTainted = true; + this.humanComposerAuditUnsafe = true; } } } @@ -1659,15 +1690,16 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { // `/always-approve`, so filtering only the final literal command is not // sufficient. Keep the shared security posture immutable by disabling // slash-command submission on this proxy altogether. - // Navigation/history makes the real editor content unknowable (Up can - // recall an old `/auto`). A tainted or overflowed composer must be cleared - // with Ctrl-U/Ctrl-C and retyped before any submit key is accepted. - return this.humanComposerLeadingSlash || this.humanComposerAuditTainted; + // History never reaches the TUI, and safe cursor edits reject any slash + // before or after cursor divergence. Overflow remains unsafe because the + // complete editor contents can no longer be reconstructed. + return this.humanComposerLeadingSlash || this.humanComposerAuditUnsafe; } private resetHumanComposerAudit(): void { this.humanComposerAudit = ""; this.humanComposerAuditTainted = false; + this.humanComposerAuditUnsafe = false; this.humanComposerAuditOverflow = false; this.humanComposerSawSlash = false; this.humanComposerLeadingSlash = false; @@ -2048,14 +2080,42 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { )); return; } - if (this.activePermissionRequestId) { - if (this.activePermissionExactPreviewTool) { + const exactPreviewTool = exactPreviewAutomaticPermissionRequestTool(event); + if (exactPreviewTool) { + if ( + this.activePermissionRequestId + || this.arbitration.waitingHuman + || (this.arbitration.activeTurn?.owner !== "network" + && this.arbitration.activeTurn?.owner !== "human") + ) { void this.failFatal(new GrokCopresenceFailure( "approval_boundary", - "grok copresence observed a duplicate preview automatic permission request", + "grok copresence could not correlate an automatic permission request to the active turn", )); return; } + if (this.pendingAutomaticPermissionTotal >= MAX_PENDING_AUTOMATIC_PERMISSIONS) { + void this.failFatal(new GrokCopresenceFailure( + "approval_boundary", + "grok copresence observed too many pending automatic permission requests", + )); + return; + } + this.pendingAutomaticPermissions.set( + exactPreviewTool, + (this.pendingAutomaticPermissions.get(exactPreviewTool) ?? 0) + 1, + ); + this.pendingAutomaticPermissionTotal += 1; + continue; + } + if (this.pendingAutomaticPermissionTotal > 0) { + void this.failFatal(new GrokCopresenceFailure( + "approval_boundary", + "grok copresence observed a manual permission request overlapping an automatic batch", + )); + return; + } + if (this.activePermissionRequestId) { if (this.activePermissionRequestId !== requestId) { void this.failFatal(new GrokCopresenceFailure( "approval_boundary", @@ -2067,7 +2127,6 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { // gate after an Enter/Ctrl-C decision has already been dispatched. continue; } - const exactPreviewTool = exactPreviewAutomaticPermissionRequestTool(event); const transition = this.transition({ type: "approval_requested" }); if (!transition.accepted) { void this.failFatal(new GrokCopresenceFailure( @@ -2077,7 +2136,6 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { return; } this.activePermissionRequestId = requestId; - this.activePermissionExactPreviewTool = exactPreviewTool; this.approvalDecisionDispatched = false; if (this.deferredHuman.length) { // Pre-approval keystrokes are never consent. Drop them instead of @@ -2092,9 +2150,12 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { } } else if (event?.type === "permission_resolved") { const requestId = lifecyclePermissionIdentity(event); + const eventTool = lifecyclePermissionTool(event); if (isGrokPreviewAutomaticResolution({ - requestTool: this.activePermissionExactPreviewTool, - activeRequestId: this.activePermissionRequestId, + requestTool: eventTool, + pendingRequestCount: eventTool + ? (this.pendingAutomaticPermissions.get(eventTool) ?? 0) + : 0, humanDecisionDispatched: this.approvalDecisionDispatched, waitingHuman: this.arbitration.waitingHuman, turnOwner: this.arbitration.activeTurn?.owner ?? null, @@ -2117,10 +2178,16 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { // preview-local completion. Do not derive this exception from the // profile array: adding a future tool must never grant it the same // behavior accidentally. - this.clearApprovalCorrelation(); - this.transition({ type: "preview_todo_resolved_automatically" }); + this.resolveAutomaticPermission(eventTool!); continue; } + if (this.pendingAutomaticPermissionTotal > 0) { + void this.failFatal(new GrokCopresenceFailure( + "approval_boundary", + "grok copresence observed an unmatched automatic permission resolution", + )); + return; + } if ( !requestId || requestId !== this.activePermissionRequestId @@ -2136,6 +2203,18 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { this.transition({ type: "approval_resolved_by_human" }); } else if (event?.type === "permission_rejected" || event?.type === "permission_cancelled") { const requestId = lifecyclePermissionIdentity(event); + const eventTool = lifecyclePermissionTool(event); + if (eventTool && (this.pendingAutomaticPermissions.get(eventTool) ?? 0) > 0) { + this.resolveAutomaticPermission(eventTool); + continue; + } + if (this.pendingAutomaticPermissionTotal > 0) { + void this.failFatal(new GrokCopresenceFailure( + "approval_boundary", + "grok copresence observed an unmatched automatic permission rejection", + )); + return; + } // Automatic denial is safe, but it may release this input gate only // when it correlates to the currently visible request. if (requestId && requestId === this.activePermissionRequestId) { @@ -2247,12 +2326,26 @@ class GrokCopresenceRuntime implements GrokCopresenceRuntimeSession { private clearApprovalCorrelation(_clearSettled = false): void { this.activePermissionRequestId = null; - this.activePermissionExactPreviewTool = null; this.approvalDecisionDispatched = false; + if (_clearSettled) this.clearAutomaticPermissionCorrelation(); } private hasUnresolvedApproval(): boolean { - return this.activePermissionRequestId !== null || this.arbitration.waitingHuman; + return this.activePermissionRequestId !== null + || this.arbitration.waitingHuman + || this.pendingAutomaticPermissionTotal > 0; + } + + private resolveAutomaticPermission(tool: string): void { + const count = this.pendingAutomaticPermissions.get(tool) ?? 0; + if (count <= 1) this.pendingAutomaticPermissions.delete(tool); + else this.pendingAutomaticPermissions.set(tool, count - 1); + this.pendingAutomaticPermissionTotal -= 1; + } + + private clearAutomaticPermissionCorrelation(): void { + this.pendingAutomaticPermissions.clear(); + this.pendingAutomaticPermissionTotal = 0; } private broadcastState(): void { @@ -3148,6 +3241,18 @@ function lifecyclePermissionIdentity(event: { return toolName && Buffer.byteLength(toolName, "utf8") <= 512 ? `tool:${toolName}` : null; } +function lifecyclePermissionTool(event: { + tool_name?: unknown; + toolName?: unknown; +}): string | null { + const toolName = typeof event.tool_name === "string" + ? event.tool_name + : typeof event.toolName === "string" + ? event.toolName + : ""; + return toolName && Buffer.byteLength(toolName, "utf8") <= 512 ? toolName : null; +} + function exactPreviewAutomaticPermissionRequestTool(event: { type?: unknown; ts?: unknown; @@ -3195,7 +3300,7 @@ function isExactPreviewAutomaticPermissionResolution(event: { /** Exact preview exception; exported so every rejected dimension has a pure mutation test. */ export function isGrokPreviewAutomaticResolution(input: { requestTool: string | null; - activeRequestId: string | null; + pendingRequestCount: number; humanDecisionDispatched: boolean; waitingHuman: boolean; turnOwner: "human" | "network" | null; @@ -3214,10 +3319,11 @@ export function isGrokPreviewAutomaticResolution(input: { const requestId = lifecyclePermissionIdentity(input.event); return input.requestTool !== null && (GROK_COPRESENCE_EFFECTIVE_TOOLS as readonly string[]).includes(input.requestTool) - && input.activeRequestId === `tool:${input.requestTool}` - && requestId === input.activeRequestId + && Number.isSafeInteger(input.pendingRequestCount) + && input.pendingRequestCount > 0 + && requestId === `tool:${input.requestTool}` && !input.humanDecisionDispatched - && input.waitingHuman + && !input.waitingHuman // The pinned process-level --always-approve mode applies to the shared // Grok process, not to anet's logical turn owner. Human turns therefore // emit the same exact automatic resolution lifecycle. Accepting that exact diff --git a/docs/message-lifecycle.md b/docs/message-lifecycle.md index 472a913a6..14b10fb3e 100644 --- a/docs/message-lifecycle.md +++ b/docs/message-lifecycle.md @@ -196,7 +196,7 @@ if (["new_task", "broadcast"].includes(ev.type)) { |---|------|--------|------| | 1 | sendReply ็”จ send_message | agent-node | โœ… v1.4.2 | | 2 | SSE ๅชๅ“ๅบ” new_task / broadcast | agent-node | โœ… [cli.ts:1102 `["new_task", "broadcast"].includes(ev.type)`](https://github.com/sleep2agi/agent-network/blob/main/agent-node/src/cli.ts#L1102)๏ผ›`new_reply` ๅ•็‹ฌ่ตฐๆ—ฅๅฟ—่ฎฐๅฝ• cli.ts:1106 | -| 3 | ไฝŽไปทๅ€ผๆถˆๆฏ่ฟ‡ๆปค | agent-node | โœ… v1.4.0๏ผ›ๅฝ“ๅ‰ๅœจ [cli.ts:4669 `shouldSkipMessage`](https://github.com/sleep2agi/agent-network/blob/main/agent-node/src/cli.ts#L4669) | +| 3 | ไฝŽไปทๅ€ผๆถˆๆฏ่ฟ‡ๆปค | agent-node | โœ… v1.4.0๏ผ›ๅฝ“ๅ‰ๅœจ [cli.ts:4691 `shouldSkipMessage`](https://github.com/sleep2agi/agent-network/blob/main/agent-node/src/cli.ts#L4691) | | 4 | CLAUDE.md ไธๅฏน message ๅ›žๅค | ๅ„้กน็›ฎ CLAUDE.md | โœ… R195 chain ๆจกๆฟๅทฒๅŠ  | | 5 | developer_instructions ๅฎ‰้™่ง„ๅˆ™ | agent-node | โœ… v1.4.1 | diff --git a/docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt b/docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt new file mode 100644 index 000000000..0cde433a1 --- /dev/null +++ b/docs/tests/report-grok-copresence-fatal-recovery-7914755a.txt @@ -0,0 +1,126 @@ +# Grok co-presence fatal-runtime recovery and repo-read pilot + +date=2026-08-14T16:10:39+08:00 +source_commit=b830403b82a97a98ec004e59c86eb294ddfd1771 +candidate_image=sha256:261ade0fd44147370df56a7377f409c840f3e65a07aa472174e7761184b45ecb +candidate_agent_node_cli_sha256=5c0d5cbcfb0350af1eb27e78de1ccd5902c5ba22be732df415fe26851c269a45 + +## Incident reproduced from production evidence + +The pinned Grok 0.2.93 TUI auto-resolved a `list_dir` permission request even +though the x-search launch argv denied `list_dir`. The co-presence bridge +correctly failed closed, but agent-node retained that terminal runtime in its +process cache. Every later task therefore inherited `runtime_closed` instead +of opening a fresh TUI. + +The subsequent repo-read pilot exposed a second, distinct ordering bug. The +real Grok event stream emitted three automatic permission requests first +(`search_tool`, `list_dir`, `search_tool`) and then resolved them in the order +`search_tool`, `search_tool`, `list_dir`. The bridge kept only one scalar +automatic request and incorrectly treated this valid batched lifecycle as an +approval-boundary violation. Six queued tasks then failed within seconds; no +model turn was allowed to proceed after the first fatal boundary. + +## Product changes + +- A stopped terminal co-presence runtime is closed, retired with a CAS against + the cached instance, and its persisted Grok session is cleared before the + next task opens a fresh TUI. +- A normal PTY recovery (`phase=recovering`) is retained and is not mistaken + for a terminal runtime. +- A blocked human slash command returns arbitration to idle; the integration + test immediately submits and completes a following network task. +- Exact fixed-profile automatic permissions are now correlated as a bounded + multiset by tool name. Multiple same-tool requests and out-of-request-order + resolutions are accepted while manual approval overlap, unmatched + resolutions/rejections, overflow, malformed events, and terminal completion + with unresolved requests still fail closed. +- The pre-spawn MCP audit binds the inspected CommHub target to the exact + already-resolved Bun executable. It still requires one stdio server named + `commhub`, sourced from the isolated `config.toml`, and rejects any other + command or MCP server. + +## Docker evidence + +test725 exact source image, full agent-node unit domain: + + 1287 pass + 0 fail + 4627 expect() calls + Ran 1287 tests across 92 files. [116.74s] + MUTATION_RED readable-attachment-runtime-disconnected rc=1 + RESULT: PASS + +After the exact-command audit correction, the focused runtime layer was rerun +from source commit 7914755a: + + 56 pass + 0 fail + 464 expect() calls + Ran 56 tests across 2 files. [64.01s] + +The batched-permission regression was first witnessed red against the prior +runtime (`duplicate preview automatic permission request`). From exact source +commit b830403b, the focused runtime/profile/near-miss layer then passed: + + 84 pass + 0 fail + 694 expect() calls + Ran 84 tests across 3 files. + +The exact archive image then passed the full test725 unit domain: + + 1288 pass + 0 fail + 4633 expect() calls + Ran 1288 tests across 92 files. + MUTATION_RED readable-attachment-runtime-disconnected rc=1 + RESULT: PASS + +The archive-built test225 package gate passed its packed-source boundary and, +before its unrelated existing L4 failure, passed the real package path: + + PASS: create -> start -> register -> Hub task -> real tmux attach live render -> reply + PASS: Hub session registration reports agent-node:grok-build-cli + PASS: Grok child env is filtered; state is 0700/0600 + +The same test225 L4 failure (`installed candidate Feishu refusal lacked the +fixed explanation`) was reproduced unchanged on parent source a9df8dba. It is +therefore a pre-existing gate failure and is not claimed as green here. + +## Single-node pilot + +- tmux session remains exactly `้€šไฟก็‹—`, with windows `0:node` and `1:tui`. +- runtime remains `grok-build-cli`; pinned Grok remains 0.2.93. +- capability profile changed from x-search to the already reviewed repo-read + profile (`Read`, `Grep`, `Glob`) because this node receives repository review + tasks. The strict sandbox permits only `read_file`, `grep`, and `list_dir` in + addition to fixed runtime tools; shell, write, web, media, scheduler, and + subagent tools remain denied. +- source b830403b was deployed only to this node. Startup reached TUI-ready, + input-ready, CommHub registration, and SSE connection with new Grok session + `4055ed56-09e1-47a2-9666-277763ef6e63`; Hub reported idle, node_id + `n_72be30e0`, and zero pending/in-flight tasks. +- The first start failed closed because the previous generation's interrupted + cleanup had left five empty, owner-held mode-0444 sandbox placeholder files. + Their exact metadata was verified and they were moved intact into the + owner-only rollback directory before the successful retry. No user file or + policy content was removed. +- No task was synthesized, retried, or sent during this pilot. + +Rollback coordinates: + + previous runtime: ~/.commhub/runtime-commdog-grok-7914755a + rollback dir: ~/.commhub/rollback-commdog-b830403b-20260814T161000CST + candidate runtime: ~/.commhub/runtime-commdog-grok-b830403b + +## Honest limits + +- No owner-origin Dashboard task was sent after the pilot because task traffic + was explicitly paused while the approval-boundary incident was investigated. +- `/model` remains intentionally blocked in this preview. It is a model-picker + control turn rather than an ordinary model response, and the current FIFO + ownership protocol cannot yet prove when that interactive control has + returned to the composer. The TUI now reports the block instead of silently + leaving the queue stuck; changing the persisted model still requires a + controlled config update and restart. diff --git a/docs/tests/report-grok-copresence-safe-navigation-a9df8dba.txt b/docs/tests/report-grok-copresence-safe-navigation-a9df8dba.txt new file mode 100644 index 000000000..9f0d960c5 --- /dev/null +++ b/docs/tests/report-grok-copresence-safe-navigation-a9df8dba.txt @@ -0,0 +1,384 @@ +# test219 โ€” Grok co-presence TUI runtime +date: 2026-08-14T06:43:32+00:00 +source_commit=a9df8dba21683324ed9127a15331914aeb3eea36 +[L0] isolated environment +/usr/bin/flock +v22.23.2 +1.3.14 +node-pty ok +PASS: environment + native PTY dependency +[L1] pure reducers and attach protocol +bun test v1.3.14 (0d9b296a) + +src/runtime/grok-child-env.test.ts: +(pass) Grok child environment boundary > builds the exact reviewed key set and drops every unreviewed credential [0.75ms] +(pass) Grok child environment boundary > re-projects a beforeSpawn result instead of trusting arbitrary keys [0.34ms] +(pass) Grok child environment boundary > rejects a beforeSpawn callback that changes a controlled value [1.17ms] +(pass) Grok child environment boundary > keeps the inherited list exact and reviewable [0.07ms] +(pass) Grok child environment boundary > keeps PTY PWD equal and adds only reviewed terminal/sandbox controls [0.84ms] +(pass) Grok child environment boundary > builds the narrower helper environment from an empty object [0.15ms] + +src/runtime/grok-build-cli.test.ts: +(pass) buildGrokCliArgs > rejects an older Grok CLI before it can ignore required safety flags [0.87ms] +(pass) buildGrokCliArgs > uses streaming headless mode and resumes an existing session [0.31ms] +(pass) buildGrokCliArgs > fails closed instead of auto-approving when permission bypass is disabled [0.15ms] +(pass) buildGrokCliArgs > maps an explicit node tool allowlist and keeps MCP unavailable [0.22ms] +(pass) buildGrokCliArgs > intersects explicit tools with the read-only set when auto-approval is off [0.10ms] +(pass) buildGrokCliArgs > rejects unknown node tool names instead of silently widening access [0.07ms] +(pass) buildGrokCliArgs > rejects an explicit empty tool allowlist instead of widening to all tools [0.07ms] +(pass) buildGrokCliArgs > denies model reads of runtime credential and node-state paths [0.09ms] +(pass) runGrokCliTurn > reports spawn submission before first exact JSONL event consumption [84.58ms] +(pass) runGrokCliTurn > reduces streaming JSON text and persists the end-event session [70.23ms] +(pass) runGrokCliTurn > spawns with exactly the projected environment and no ambient credentials [79.13ms] +(pass) runGrokCliTurn > keeps the production-shaped setpriv/sh launcher on the exact PWD-bound env [89.71ms] +(pass) runGrokCliTurn > refuses a shell launcher when PWD is missing from the reviewed env [1.17ms] +(pass) runGrokCliTurn > removes the prompt when spawn rejects a malformed allowed env value [1.64ms] +(pass) runGrokCliTurn > surfaces non-zero exits and stderr [58.61ms] +(pass) runGrokCliTurn > fails fast when headless Grok asks for an interactive login [57.10ms] +(pass) runGrokCliTurn > rejects cancelled turns [82.63ms] +(pass) runGrokCliTurn > rejects a formal error event even if the process exits zero [89.88ms] +(pass) runGrokCliTurn > rejects max-turn truncation instead of reporting a partial reply as success [58.89ms] +(pass) runGrokCliTurn > terminates the process group when the caller aborts [36.25ms] +(pass) runGrokCliTurn > kills a silent child after the idle timeout [39.55ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > passes when the probe succeeds [0.42ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > throws with the real stderr and an actionable next step when uid_map is refused [0.40ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > still throws when the probe fails with no stderr at all [0.25ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > honours a custom unshare binary path [0.11ms] + +src/runtime/grok-build-cli-home.test.ts: +(pass) prepareGrokCliHome > resolves the CommHub MCP command to one canonical executable [1.61ms] +(pass) prepareGrokCliHome > requires a real CommHub MCP doctor handshake and all three tools [0.84ms] +(pass) prepareGrokCliHome > derives an opaque path segment and rejects dot identities [0.51ms] +(pass) prepareGrokCliHome > accepts only the pinned Grok regular-file copy of source agent_id [10.56ms] +(pass) prepareGrokCliHome > isolates config/trust, preserves a shared auth path, and creates stable sandbox profiles [2.82ms] +(pass) prepareGrokCliHome > refuses broad-mode or symlinked source auth without repairing it [1.82ms] +(pass) prepareGrokCliHome > repairs an existing Grok session store to owner-only modes [3.00ms] +(pass) prepareGrokCliHome > does not follow a symlink while repairing an existing session store [1.60ms] +(pass) prepareGrokCliHome > keeps the post-stop cleanup policy exact and reviewable [0.17ms] +(pass) prepareGrokCliHome > removes exact empty read-only project placeholders before resume without admitting executable sources [6.16ms] +(pass) prepareGrokCliHome > validates every exact project placeholder before unlinking any sibling [2.10ms] +(pass) prepareGrokCliHome > does not let a fatal project counterexample starve independent state containment [3.06ms] +(pass) prepareGrokCliHome > preserves nonempty, linked, wrong-mode, and wrong-type project counterexamples [5.68ms] +(pass) prepareGrokCliHome > preserves real project extension directories and still rejects executable contents on resume [2.72ms] +(pass) prepareGrokCliHome > removes only exact transient state and hardens retained post-stop state [9.20ms] +(pass) prepareGrokCliHome > hardens only the native lock derived from the exact leader socket [1.45ms] +(pass) prepareGrokCliHome > retains a non-empty leader log and rejects post-stop link attacks [2.85ms] +(pass) prepareGrokCliHome > refuses a non-empty exact sandbox placeholder [1.34ms] +(pass) prepareGrokCliHome > reclaims an empty mode-000 sandbox marker under a foreign pid without aborting [1.68ms] +(pass) prepareGrokCliHome > keeps a non-empty foreign sandbox marker unreadable so it fails closed [1.91ms] +(pass) prepareGrokCliHome > validates exact TUI process ids before mutation and refuses a placeholder symlink [1.96ms] +(pass) prepareGrokCliHome > enables the single TUI leader only for explicit copresence mode [21.46ms] +(pass) prepareGrokCliHome > admits only canonical owner-held commhub MCP artifacts [6.82ms] +(pass) prepareGrokCliHome > rejects a shared auth path covered by a required sandbox deny before state mutation [0.81ms] +(pass) prepareGrokCliHome > refuses to claim sandbox isolation when no deny target exists [0.86ms] +(pass) prepareGrokCliHome > rejects a source GROK_HOME reached through an ancestor symlink before state mutation [1.03ms] +(pass) prepareGrokCliHome > removes runtime-owned native hooks before every turn [1.75ms] +(pass) prepareGrokCliHome > unlinks a runtime-owned hook symlink without touching its external target [1.64ms] +(pass) prepareGrokCliHome > fails closed when a project native hook path exists [1.05ms] +(pass) prepareGrokCliHome > trusts only the exact canonical nested cwd and atomically replaces stale grants [4.02ms] +(pass) prepareGrokCliHome > rejects broad or symlinked folder-trust targets before writing trust state [1.78ms] +(pass) prepareGrokCliHome > refuses a planted trust-store symlink and leaves its target untouched [2.05ms] +(pass) prepareGrokCliHome > rejects every project executable source before granting folder trust [13.61ms] +(pass) prepareGrokCliHome > does not impose the shared-folder strict policy on legacy headless mode [3.50ms] +(pass) prepareGrokCliHome > rejects repo-root hooks from a nested cwd and dangling hook links [1.27ms] +(pass) prepareGrokCliHome > rejects a symlinked project .grok directory [0.88ms] +(pass) prepareGrokCliHome > rejects symlinked isolated homes and generated state without changing targets [4.02ms] +(pass) prepareGrokCliHome > rejects a state-home path escape before chmod, removal, or writes [1.00ms] +(pass) prepareGrokCliHome > requires a valid zero-hook inspect response [0.73ms] +(pass) prepareGrokCliHome > flocks the canonical project inode across symlink aliases and releases cleanly [157.70ms] +(pass) prepareGrokCliHome > gives the real flock holder only the exact helper environment [75.38ms] + +src/credential-redaction.test.ts: +(pass) credential persistence redactor > removes exact caller-known values regardless of punctuation or context [1.30ms] +(pass) credential persistence redactor > redacts network, GitHub, AWS and provider token shapes in free text [0.27ms] +(pass) credential persistence redactor > redacts credential assignments while preserving keys and valid JSON [0.41ms] +(pass) credential persistence redactor > redacts shell/error assignment forms including quoted values [0.16ms] +(pass) credential persistence redactor > redacts an unlabelled connection URI with embedded userinfo [0.13ms] +(pass) credential persistence redactor > does not over-delete normal prose and non-credential settings [0.12ms] +(pass) credential persistence redactor > deep-redacts JSON-like values without mutating the input [0.60ms] +(pass) credential value collection > collects exact sensitive values and shaped values under unknown keys [1.28ms] +(pass) credential value collection > key classifier is exact enough not to treat ordinary AWS settings as credentials [2.24ms] + +src/private-log.test.ts: +(pass) Grok preview private ordinary logs > scrubs and repairs legacy logs before appending through a 0600 file [5.31ms] +(pass) Grok preview private ordinary logs > rejects a symlinked directory or final log file [1.70ms] +(pass) Grok preview private ordinary logs > rejects a multiply-linked log instead of rewriting another pathname [0.77ms] +(pass) Grok preview private ordinary logs > does not follow a log-directory symlink introduced after preparation [0.80ms] + +src/reply-reliability.test.ts: +(pass) classifyCommHubResponse > returns ok with parsed application payload (the happy path) [0.37ms] +(pass) classifyCommHubResponse > JSON-RPC error envelope โ†’ retryable CommHubError [0.23ms] +(pass) classifyCommHubResponse > MCP result.isError โ†’ retryable CommHubError [0.22ms] +(pass) classifyCommHubResponse > real legacy Hub unknown-tool result preserves the MCP code [0.12ms] +(pass) classifyCommHubResponse > application-level ok:false โ†’ appLevel CommHubError (NON-retryable) [0.14ms] +(pass) classifyCommHubResponse > non-JSON tool text is passed through verbatim [0.22ms] +(pass) classifyCommHubResponse > data with neither error nor result returns ok with the raw data [0.07ms] +(pass) CommHubError > instances are distinguishable from generic Error via instanceof [0.11ms] +(pass) CommHubError > appLevel flag survives the throw/catch round trip [0.08ms] +(pass) PendingReplyQueue > load() returns empty array when file does not exist [0.53ms] +(pass) PendingReplyQueue > persist + load round-trips an entry with attempts=0 [3.64ms] +(pass) PendingReplyQueue > final persistence boundary scrubs known, shaped, assignment and error credentials [3.57ms] +(pass) PendingReplyQueue > direct save cannot bypass scrub and leaves no sibling temp artifact [2.59ms] +(pass) PendingReplyQueue > load migrates an old broad-mode queue without leaving raw credential bytes [3.22ms] +(pass) PendingReplyQueue > load repairs a broad mode even when content needs no rewrite [0.64ms] +(pass) PendingReplyQueue > accepts the same process-wide redactor used by ordinary log call sites [2.42ms] +(pass) PendingReplyQueue > invalid legacy content is securely replaced with an empty 0600 queue [3.40ms] +(pass) PendingReplyQueue > persist is idempotent on (to, taskId) โ€” attempts counter preserved [7.33ms] +(pass) PendingReplyQueue > clear removes only the matching (to, taskId) [11.09ms] +(pass) PendingReplyQueue.drain > delivers every entry on success and persists an empty queue [8.77ms] +(pass) PendingReplyQueue.drain > transient failure requeues with attempts++ and lastError [6.17ms] +(pass) PendingReplyQueue.drain > transient error text is scrubbed before it reaches disk [5.41ms] +(pass) PendingReplyQueue.drain > app-level CommHubError is dropped loud โ€” not retried, not requeued [9.84ms] +(pass) PendingReplyQueue.drain > drain on empty queue is a no-op and does not write the file [0.65ms] +(pass) PendingReplyQueue.drain > file format is stable JSON โ€” readable by an operator after a crash [5.91ms] +(pass) quickHash > is deterministic [0.33ms] +(pass) quickHash > differs across inputs [0.06ms] +(pass) quickHash > returns 32-char hex [0.12ms] + +src/goals/store.test.ts: +(pass) GoalStore โ€” basic lifecycle > fresh store: load with no file โ†’ ok, empty list [2.29ms] +(pass) GoalStore โ€” basic lifecycle > upsert โ†’ get โ†’ list roundtrip [3.03ms] +(pass) GoalStore โ€” basic lifecycle > delete โ†’ flushes to disk [1.57ms] +(pass) GoalStore โ€” basic lifecycle > setStatus โ†’ in-memory + persisted [1.93ms] +(pass) GoalStore โ€” basic lifecycle > setStatus on unknown id โ†’ undefined, no throw [0.59ms] +(pass) GoalStore โ€” basic lifecycle > mutate applies in-place + bumps updated_at [10.60ms] +(pass) GoalStore โ€” basic lifecycle > mutate on unknown id โ†’ undefined, mutator NOT invoked [0.53ms] +(pass) GoalStore โ€” restart persistence > two instances see the same goals (= restart simulation) [1.01ms] +(pass) GoalStore โ€” restart persistence > status change survives reload [1.74ms] +(pass) GoalStore โ€” corruption recovery (#2) > invalid JSON โ†’ ok=false, .corrupt backup, empty store [2.27ms] +(pass) GoalStore โ€” corruption recovery (#2) > unknown schema version โ†’ recovery [0.76ms] +(pass) GoalStore โ€” corruption recovery (#2) > malformed shape (goals not array) โ†’ recovery [0.63ms] +(pass) GoalStore โ€” Grok preview persistence boundary > recursively migrates task/progress/error, final writes, and archives at 0600 [4.17ms] +(pass) GoalStore โ€” Grok preview persistence boundary > scrubs a broad-mode corrupt backup and replaces the live file with an empty safe store [1.76ms] +(pass) GoalStore โ€” Grok preview persistence boundary > recursively scrubs a parseable unsupported-schema backup [1.78ms] +(pass) P0 runtime gate โ€” name resolution > isClaudeRuntime accepts every claude alias [0.21ms] +(pass) P0 runtime gate โ€” name resolution > isClaudeRuntime rejects codex / grok / unknown / empty [0.09ms] +(pass) P0 runtime gate โ€” name resolution > runtimeBucket maps to canonical buckets [0.18ms] +(pass) #144 round-6 โ€” claude runtime gate REMOVED, scheduler is universal > newGoal({runtime: 'claude-agent-sdk'}) succeeds (was the load-bearing bug) [0.12ms] +(pass) #144 round-6 โ€” claude runtime gate REMOVED, scheduler is universal > newGoal succeeds for every recognized runtime alias (no per-bucket carve-out) [0.23ms] +(pass) #144 round-6 โ€” claude runtime gate REMOVED, scheduler is universal > GoalStore.upsert accepts a claude-runtime goal end-to-end [0.74ms] +(pass) #144 round-6 โ€” claude runtime gate REMOVED, scheduler is universal > isClaudeRuntime still classifies (kept for cross-bucket detection, not gating) [0.07ms] +(pass) P0 runtime gate โ€” archiveAndClear > with live goals: backup file created, store emptied, reload sees empty [2.06ms] +(pass) P0 runtime gate โ€” archiveAndClear > with no live file: returns undefined, no throw, store still flushes empty [0.60ms] +(pass) P0 runtime gate โ€” archiveAndClear > backup filenames are unique across rapid calls [18.62ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > claude + empty โ†’ ok (scheduler runs; was 'skip' pre-#144) [0.35ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > claude + only claude-active goals โ†’ ok (scheduler runs) [0.35ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > codex + empty โ†’ ok [0.06ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > codex + only codex goals โ†’ ok [0.13ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > grok + only grok goals โ†’ ok [0.10ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > claude + active codex/grok goals โ†’ archive + runScheduler=true (recover after archive) [0.33ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > codex + grok-active leftover โ†’ archive (NOT fatal exit anymore) [0.12ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > grok + codex-active leftover โ†’ archive [0.09ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > inactive foreign-bucket goals do NOT trigger archive (only `active` counts) [0.15ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > claude with only inactive foreign leftover โ†’ ok (just cleanup pending) [0.15ms] +(pass) #144 round-6 โ€” decideStartupAction (refined-B matrix) > unknown bucket โ†’ skip (no scheduler, no auto-archive) [0.11ms] +(pass) GoalStore โ€” mutex serialisation (#1+#3) > 50 concurrent upserts โ†’ all 50 persist (no torn writes) [24.70ms] +(pass) GoalStore โ€” mutex serialisation (#1+#3) > interleaved upsert + setStatus + delete stays consistent [16.73ms] + +src/runtime/grok-copresence/state.test.ts: +(pass) Grok co-presence arbitration > lets the first human byte win a simultaneous human/network race [2.68ms] +(pass) Grok co-presence arbitration > gives a newly active human composer priority over an existing FIFO [0.37ms] +(pass) Grok co-presence arbitration > dequeues network tasks FIFO and never preempts an active turn [0.85ms] +(pass) Grok co-presence arbitration > cancels only queued timeouts and rejects duplicate task ids [0.41ms] +(pass) Grok co-presence arbitration > retains the active network task and FIFO across disconnect/reconnect [0.99ms] +(pass) Grok co-presence arbitration > marks approvals waiting for the human without emitting a response [0.26ms] +(pass) Grok co-presence arbitration > clears an already-waiting preview todo resolution in either active turn without completing it [0.28ms] + +src/runtime/grok-copresence/jsonl.test.ts: +(pass) Grok copresence envelope and user parsing > parses only an exact, query-anchored Agent Network envelope [0.35ms] +(pass) Grok copresence envelope and user parsing > extracts the first authoritative user_query from string or Grok text-array content [0.40ms] +(pass) Grok copresence envelope and user parsing > does not trust a syntactically valid prefix unless the bridge registered it [1.49ms] +(pass) Grok copresence envelope and user parsing > nested user_query text cannot turn an owned network task into human delegation [0.46ms] +(pass) Grok copresence turn reducer > waits for completion and replies with the last non-empty assistant record [0.57ms] +(pass) Grok copresence turn reducer > keeps the last no-tool assistant when later tool-bearing chatter exists [0.25ms] +(pass) Grok copresence turn reducer > handles completion/chat-history polling order without returning an empty reply [0.43ms] +(pass) Grok copresence turn reducer > does not finalize an intermediate assistant visible before the completion event [0.30ms] +(pass) Grok copresence turn reducer > retains a completion observed before even the network user line [0.40ms] +(pass) Grok copresence turn reducer > retains an event-first human completion only for a trusted PTY submission [0.29ms] +(pass) Grok copresence turn reducer > never carries an unowned idle completion into a later network task [0.37ms] +(pass) Grok copresence turn reducer > binds an event-first completion to the exact registered network task [0.44ms] +(pass) Grok copresence turn reducer > consumes sanitized sample A block content and turn_number boundary [0.44ms] +(pass) Grok copresence turn reducer > consumes sanitized sample B and selects only the 14th no-tool assistant [0.68ms] +(pass) Grok copresence turn reducer > ignores standalone system-reminder user records without abandoning a network turn [0.17ms] +(pass) Grok copresence turn reducer > fails a terminal record without turn_started and never binds it to the next user [0.31ms] +(pass) Grok copresence turn reducer > never maps a human turn or failed network turn to a network reply [0.80ms] +(pass) Grok copresence turn reducer > abandons an unfinished network turn rather than attaching its answer to a human turn [0.41ms] +(pass) Grok copresence turn reducer > pairs events correctly when chat_history leads by two unnumbered turns [0.55ms] +(pass) Grok copresence turn reducer > does not let a new start overtake an abandoned numbered terminal [0.37ms] +(pass) Grok completion compatibility and defensive parsing > recognizes only top-level turn_ended with an exact successful outcome [0.24ms] +(pass) Grok completion compatibility and defensive parsing > binds turn_started turn_number while permission lifecycle remains inert [0.17ms] +(pass) Grok completion compatibility and defensive parsing > fails a started turn when turn_ended has no outcome [0.23ms] +(pass) Grok completion compatibility and defensive parsing > fails closed on an overlapping turn_started epoch [0.40ms] +(pass) Grok completion compatibility and defensive parsing > retains only a bounded tail of raw completion candidates [0.17ms] +(pass) Grok completion compatibility and defensive parsing > contains malformed and overlong lines instead of parsing or retaining them [1.11ms] +(pass) Grok completion compatibility and defensive parsing > incrementally joins split lines and drops a fragmented oversized line once [1.37ms] +(pass) persistent JSONL tail cursor > starts fresh at end by default, with an explicit start override [3.11ms] +(pass) persistent JSONL tail cursor > continues and fails closed on truncate or inode rotation [0.25ms] +(pass) persistent JSONL tail cursor > treats corrupt persisted state as non-replayable and advances JSON-safely [0.27ms] + +src/runtime/grok-copresence/attach.test.ts: +(pass) Grok co-presence local attach server > serves one owner-only client and cleans its socket on close [20.24ms] +(pass) Grok co-presence local attach server > rejects a second client without disturbing the attached human [9.44ms] +(pass) Grok co-presence local attach server > routes input and resize frames only through serialized arbiter callbacks [4.00ms] +(pass) Grok co-presence local attach server > fails closed when an inbound frame exceeds the configured bound [6.14ms] +(pass) Grok co-presence local attach server > refuses symlinks and regular files at the socket path [1.10ms] + +../agent-network/src/grok-attach-client.test.ts: +(pass) validateGrokAttachSocket rejects symlinks, non-sockets, and foreign owners [0.91ms] +(pass) connectGrokAttach bridges base64 terminal I/O, status, resize, and detach [5.82ms] +(pass) connectGrokAttach splits large input so every NDJSON frame stays bounded [0.94ms] +(pass) connectGrokAttach fails closed on an invalid handshake and oversized frame [1.47ms] +(pass) a single-client rejection before hello preserves the server error [0.77ms] +(pass) hello followed by a fatal frame in the same chunk cannot return a dead session [1.18ms] +(pass) detach force-closes a peer that never completes its half-close [14.07ms] +(pass) callback failure and invalid limits fail before returning an attached client [1.39ms] +(pass) remote detach is surfaced and closes without echoing a detach frame [0.92ms] + +../agent-network/src/grok-copresence-profile.test.ts: +(pass) Grok copresence profile defaults > builds the Grok agent-node parent environment from an exact empty allowlist [0.41ms] +(pass) Grok copresence profile defaults > does not mistake an old headless-only agent-node for co-presence support [0.14ms] +(pass) Grok copresence profile defaults > builds the npm resolver environment from an exact empty allowlist [0.63ms] +(pass) Grok copresence profile defaults > prepares two distinct empty owner-only npm config files without following symlinks [2.21ms] +(pass) Grok copresence profile defaults > enables copresence only for non-headless grok-build-cli [0.56ms] +(pass) Grok copresence profile defaults > uses the owner-bound state home even when XDG is owner-only [0.50ms] +(pass) Grok copresence profile defaults > falls back to a bounded owner tmp path when the state home is too long [0.14ms] + +../agent-network/src/owner-env-file.test.ts: +(pass) loadOwnerOnlyEnvFile > loads the isolated commhub credential without overriding explicit identity [1.01ms] +(pass) loadOwnerOnlyEnvFile > rejects relative, broad-mode, and symlinked credential files [0.67ms] + +../agent-network/src/normalize-runtime.test.ts: +(pass) normalizeRuntime โ€” fallback default is claude-agent-sdk (Vincent no-Max) > legacy normalization: unknown string โ†’ claude-agent-sdk [0.17ms] +(pass) normalizeRuntime โ€” fallback default is claude-agent-sdk (Vincent no-Max) > empty string โ†’ claude-agent-sdk [0.02ms] +(pass) normalizeRuntime โ€” fallback default is claude-agent-sdk (Vincent no-Max) > undefined (no arg) โ†’ claude-agent-sdk [0.02ms] +(pass) normalizeRuntime โ€” fallback default is claude-agent-sdk (Vincent no-Max) > undefined profile arg โ†’ claude-agent-sdk [0.02ms] +(pass) normalizeRuntime โ€” fallback default is claude-agent-sdk (Vincent no-Max) > profile with missing runtime field โ†’ claude-agent-sdk [0.03ms] +(pass) normalizeRuntime โ€” fallback default is claude-agent-sdk (Vincent no-Max) > profile with empty-string runtime field โ†’ claude-agent-sdk [0.03ms] +(pass) normalizeRuntimeStrict โ€” execution boundaries fail closed > missing and empty runtime still select the documented default [0.12ms] +(pass) normalizeRuntimeStrict โ€” execution boundaries fail closed > canonical names and supported aliases are accepted [0.05ms] +(pass) normalizeRuntimeStrict โ€” execution boundaries fail closed > a non-empty unknown runtime is rejected [0.16ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > explicit 'claude-code-cli' โ†’ claude-code-cli (operator opt-in still works) [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > explicit 'claude-agent-sdk' โ†’ claude-agent-sdk [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > alias 'claude' โ†’ claude-agent-sdk (existing canonicalization) [0.04ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > alias 'claude-sdk' โ†’ claude-agent-sdk [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > alias 'agent-sdk' (string form) โ†’ claude-agent-sdk [0.02ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > 'codex' / 'codex-sdk' โ†’ codex-sdk [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > 'grok' / 'grok-build' / 'grok-build-acp' โ†’ grok-build-acp [0.06ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > explicit Grok co-presence names โ†’ grok-build-cli [0.05ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > explicit 'opencode-cli' โ†’ opencode-cli (canonical launcher name) [0.02ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > alias 'opencode' โ†’ opencode-cli (short form) [0.02ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > profile with runtime='opencode-cli' โ†’ opencode-cli [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > profile with runtime='opencode' โ†’ opencode-cli [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > explicit 'codex-app-server' โ†’ codex-app-server [0.02ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > alias 'codex-tui' โ†’ codex-app-server [0.07ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > alias 'codex-appserver' โ†’ codex-app-server [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > 'codex-sdk' still โ†’ codex-sdk (not shadowed by the app-server branch) [0.03ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > 'codex' still โ†’ codex-sdk (legacy short alias unchanged) [0.05ms] +(pass) normalizeRuntime โ€” explicit choices are preserved > profile with runtime='codex-app-server' โ†’ codex-app-server [0.04ms] +(pass) normalizeRuntime โ€” profile object paths > profile with runtime='claude-code-cli' โ†’ claude-code-cli (explicit, preserved) [0.04ms] +(pass) normalizeRuntime โ€” profile object paths > profile with runtime='agent-sdk' + codexRuntime='codex' โ†’ codex-sdk (legacy hybrid) [0.04ms] +(pass) normalizeRuntime โ€” profile object paths > profile with runtime='agent-sdk' + no codexRuntime โ†’ claude-agent-sdk [0.03ms] +(pass) normalizeRuntime โ€” profile object paths > legacy profile normalization keeps unknown โ†’ default for display/migration [0.05ms] + + 242 pass + 0 fail + 1048 expect() calls +Ran 242 tests across 14 files. [1.58s] +PASS: state machine + JSONL reducer + local attach protocol +[L2] single-PTY runtime integration +bun test v1.3.14 (0d9b296a) + +src/runtime/grok-copresence/leader-lifecycle.test.ts: +(pass) Grok auto-Leader lifecycle identity > rejects a different kernel executable hidden behind a pinned argv0 [9.57ms] +(pass) Grok auto-Leader lifecycle identity > rejects a live native listener whose argv0 forges the pinned executable [335.65ms] +(pass) Grok auto-Leader lifecycle identity > terminates one exact generation and removes only its stale socket [103.38ms] +(pass) Grok auto-Leader lifecycle identity > does not adopt a listener whose generation marker differs [182.09ms] +(pass) Grok auto-Leader lifecycle identity > does not signal or unlink after the socket pathname is replaced [80.24ms] +(pass) Grok auto-Leader lifecycle identity > revalidates the exact identity before escalating a TERM-resistant Leader [627.24ms] +(pass) Grok auto-Leader lifecycle identity > does not escalate when a TERM-resistant Leader replaces its listener [382.02ms] +(pass) Grok auto-Leader lifecycle identity > does not signal after the configured binary inode is replaced [70.39ms] +(pass) Grok auto-Leader lifecycle identity > retains the stale socket when another process from the generation remains [316.84ms] + +src/runtime/grok-copresence/runtime.test.ts: +(pass) Grok copresence launch and injection policy > keeps the fixed-tool auto-resolution exception exact and limited to active turns [2.79ms] +(pass) Grok copresence launch and injection policy > admits exact automatic lifecycles only for the fixed preview tool boundary [0.28ms] +(pass) Grok copresence launch and injection policy > exposes only reviewed value-free task failure codes and exact JSONL subcodes [0.45ms] +(pass) Grok copresence launch and injection policy > keeps the JSONL subcode allowlist direct, frozen, and actual-path-only [0.22ms] +(pass) Grok copresence launch and injection policy > locks the probed Grok TUI build exactly [0.15ms] +(pass) Grok copresence launch and injection policy > pins one TUI-effective commhub-only agent profile and hard-denies fallback routes [1.21ms] +(pass) Grok copresence launch and injection policy > rejects terminal escape injection and reserved origin markup [0.54ms] +(pass) Grok copresence launch and injection policy > recognizes the pinned TUI composer footer across ANSI fragments [0.28ms] +(pass) Grok copresence launch and injection policy > rejects external permission sources and noninteractive modes [1.44ms] +(pass) Grok copresence runtime integration > terminates the independently persistent auto-Leader and its unchanged stale socket [576.20ms] +(pass) Grok copresence runtime integration > cleans and hardens the exact pinned footprint only after confirmed close [597.01ms] +(pass) Grok copresence runtime integration > cleans each exact sandbox placeholder at its confirmed recovery boundary [879.54ms] +(pass) Grok copresence runtime integration > removes an old placeholder before a recovery generation reuses its PID [1192.83ms] +(pass) Grok copresence runtime integration > queues network input until the pinned TUI composer is ready [1182.83ms] +(pass) Grok copresence runtime integration > maps keyless fake-writer file mutations to exact value-free tail subcodes [3887.88ms] +(pass) Grok copresence runtime integration > continues exactly once across prefix-preserving atomic chat rewrites [2279.06ms] +(pass) Grok copresence runtime integration > rejects an atomic replacement that preserves only the consumed prefix [703.37ms] +(pass) Grok copresence runtime integration > rejects a same-inode shrink below the highest observed size even when offset remains valid [575.72ms] +(pass) Grok copresence runtime integration > does not expose an intermediate atomic generation before its successor preserves it [1079.33ms] +(pass) Grok copresence runtime integration > does not expose a pinned generation unlinked between path check and read [1107.20ms] +(pass) Grok copresence runtime integration > maps chat and events reset callback failures and stops polling after fatal [1518.42ms] +(pass) Grok copresence runtime integration > maps keyless reducer, lifecycle, and combined flush invariants at their boundaries [2496.71ms] +(pass) Grok copresence runtime integration > close waits for and tears down a Leader spawned by in-flight recovery [907.12ms] +(pass) Grok copresence runtime integration > retains containment and lifetime locks when a closing recovery PTY will not stop [2914.65ms] +(pass) Grok copresence runtime integration > excludes a different runtime from the same canonical project for the full TUI lifetime [1136.82ms] +(pass) Grok copresence runtime integration > contains an exited recovery generation before reusing its PID [1801.30ms] +(pass) Grok copresence runtime integration > retains final-cleanup ownership after every failed recovery PID is consumed [901.99ms] +(pass) Grok copresence runtime integration > reports exact submission and trusted consumption, never queued admission [1762.99ms] +(pass) Grok copresence runtime integration > arbitrates a live PTY, settles final JSONL, attaches once, and resumes [4046.03ms] +(pass) Grok copresence runtime integration > fails closed on automatic permission resolution without a human action [593.93ms] +(pass) Grok copresence runtime integration > accepts only the pinned preview todo_write automatic resolution tuple [1876.63ms] +(pass) Grok copresence runtime integration > keeps the shared TUI alive when the pinned preview auto-resolves todo_write in a human turn [1776.94ms] +(pass) Grok copresence runtime integration > keeps the shared TUI alive across exact search_tool then use_tool in a human turn [1637.43ms] +(pass) Grok copresence runtime integration > rejects every mutated preview todo_write automatic resolution tuple [4095.33ms] +(pass) Grok copresence runtime integration > preserves exact permission lifecycle order across coalesced and split event reads [2361.17ms] +(pass) Grok copresence runtime integration > fails closed on malformed or oversized permission lifecycle JSONL [1160.57ms] +(pass) Grok copresence runtime integration > rejects terminal reordering around automatic permission lifecycles [1757.60ms] +(pass) Grok copresence runtime integration > allows repeated fixed-tool automatic permission lifecycles in one network turn [1075.41ms] +(pass) Grok copresence runtime integration > never replies with a tool-bearing assistant when the final log is delayed past settling [1887.44ms] +(pass) Grok copresence runtime integration > rejects a completed turn that never resolved its approval [550.70ms] +(pass) Grok copresence runtime integration > does not resume a TUI that crashed at an approval prompt [602.24ms] +(pass) Grok copresence runtime integration > rejects a permission record that landed just before the crash poll [909.17ms] +(pass) Grok copresence runtime integration > refuses process-level resume with a persisted unresolved approval [192.40ms] +(pass) Grok copresence runtime integration > permits process-level resume after a persisted approval was resolved [565.68ms] +(pass) Grok copresence runtime integration > arms both resume tails before spawn-time permission records can be skipped [294.10ms] +(pass) Grok copresence runtime integration > discards spawn-time orphan completions before accepting the first new network task [1123.36ms] +(pass) Grok copresence runtime integration > drains more than one tail chunk before attach and fully cleans a startup rejection [964.18ms] +(pass) Grok copresence runtime integration > accepts the pinned startup auto-approval transition [549.12ms] +(pass) Grok copresence runtime integration > reruns the spawn audit and refuses recovery when it fails [825.44ms] +(pass) Grok copresence runtime integration > keeps auto-approval across recovery before scheduling [1723.77ms] +(pass) Grok copresence runtime integration > jointly drains chat and events until both recovery cursors are stable [1862.90ms] +(pass) Grok copresence runtime integration > rejects a beforeSpawn callback that widens a controlled child setting [208.86ms] +(pass) Grok copresence runtime integration > gives every real lifetime-lock holder only the exact helper environment [575.98ms] + + 62 pass + 0 fail + 467 expect() calls +Ran 62 tests across 2 files. [62.94s] +PASS: FIFO/human arbitration + final reply + approval + reconnect + single bridge +[L3] package build and CLI integration + +> @sleep2agi/agent-node@2.5.0-preview.31 build +> bun build src/cli.ts --outdir dist --entry-naming cli.js --target node --minify --external @anthropic-ai/claude-agent-sdk --external '@anthropic-ai/claude-agent-sdk-*' --external @openai/codex-sdk --external node-pty && bun build src/upload-file-mcp-stdio.ts --outdir dist --entry-naming upload-file-mcp-stdio.js --target node --minify + +Bundled 271 modules in 72ms + + cli.js 1.15 MB (entry point) + +Bundled 221 modules in 41ms + + upload-file-mcp-stdio.js 0.27 MB (entry point) + + +> @sleep2agi/agent-network@2.3.0-preview.39 typecheck +> tsc --noEmit + +Bundled 209 modules in 73ms + + anet-cli.js 1.83 MB (entry point) + +PASS: agent-node + anet CLI builds +Summary: PASS (304 tests, 0 failures; all validation ran inside Docker) diff --git a/docs/tests/report-grok-copresence-safe-navigation-mutation-a9df8dba.txt b/docs/tests/report-grok-copresence-safe-navigation-mutation-a9df8dba.txt new file mode 100644 index 000000000..367f9af73 --- /dev/null +++ b/docs/tests/report-grok-copresence-safe-navigation-mutation-a9df8dba.txt @@ -0,0 +1,40 @@ +# Grok co-presence safe composer navigation โ€” witnessed red + +source_commit=a9df8dba21683324ed9127a15331914aeb3eea36 +image_id=sha256:ccb2d39104cda9ba9c6ee38aa372e0a98e2bff86de90bbe6abd73de6bbbc84a0 +baseline_report=docs/tests/report-grok-copresence-safe-navigation-a9df8dba.txt +baseline_report_sha256=f706715a7e9f3f7f39cf5fc166aab5fd8d67f2577691c3a17f50e1d61f188b1f + +Baseline, inside Docker: + + Summary: PASS (304 tests, 0 failures; all validation ran inside Docker) + +Mutation, applied only inside an ephemeral container made from the exact image: + + return this.humanComposerLeadingSlash || this.humanComposerAuditUnsafe; + +was changed back to the prior behavior: + + return this.humanComposerLeadingSlash || this.humanComposerAuditTainted; + +The mutation recreates the product regression in which a safe Left-arrow edit +taints the composer and Enter silently refuses the human turn. The complete +runtime test file produced this attributable failure: + + error: condition not met within 3000ms + at runtime.test.ts:1377 + (fail) Grok copresence runtime integration > arbitrates a live PTY, + settles final JSONL, attaches once, and resumes + +Aggregate: + + 52 pass + 1 fail + 418 expect() calls + Ran 53 tests across 1 file. + +MUTATION_RED safe-navigation-submit-regressed + +The mutation changed product source, not test source. The failing assertion is +the named behavior that types `AC`, sends Left, inserts `B`, submits, and waits +for the runtime-owned PTY to record `ABC` as a human prompt.