From 0dce5e34c6ae85e2ab3103f89bc585dc5b203c97 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 01:05:30 +0000 Subject: [PATCH] fix: validate run correlation after the relay checkpoint --- .changeset/historical-relay-coverage.md | 5 +++++ src/core/relay/intake.js | 7 +++++- src/core/relay/records.js | 5 ----- test/relay-engine.test.js | 29 +++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 .changeset/historical-relay-coverage.md diff --git a/.changeset/historical-relay-coverage.md b/.changeset/historical-relay-coverage.md new file mode 100644 index 0000000..c0128fc --- /dev/null +++ b/.changeset/historical-relay-coverage.md @@ -0,0 +1,5 @@ +--- +"grok-bot-cli": patch +--- + +Allow automatic messaging to establish a checkpoint over older bot outputs without run IDs while continuing to reject new outputs without correlation IDs. diff --git a/src/core/relay/intake.js b/src/core/relay/intake.js index 9bd4a9a..63d2834 100644 --- a/src/core/relay/intake.js +++ b/src/core/relay/intake.js @@ -70,6 +70,12 @@ export function createIntake({ }); return; } + const incoming = page.slice(index + 1); + if (incoming.some(entry => entry.kind === "send-message" && + (typeof entry.requestId !== "string" || !entry.requestId))) { + await change("targets", { ...target, state: "paused", reason: "invalid-coverage" }); + return; + } const local = { ...state.read().records }, changes = []; // Gather every nonce before classifying bot outputs, even if its user row comes later. @@ -139,7 +145,6 @@ export function createIntake({ ["sending", "unknown", "accepted"].includes(r.submission) && !r.requestId, ); - const incoming = page.slice(index + 1); for (const entry of incoming) { if (entry.kind !== "send-message" || own.has(entry.requestId)) continue; const approvalNotice = grokApprovalNotice(entry, targetId); diff --git a/src/core/relay/records.js b/src/core/relay/records.js index 32fef16..898e5d1 100644 --- a/src/core/relay/records.js +++ b/src/core/relay/records.js @@ -35,11 +35,6 @@ export function pageEntries(payload) { if (ids.has(id) || typeof e.kind !== "string" || !e.kind) throw new Error("Invalid transcript coverage"); ids.add(id); - if ( - e.kind === "send-message" && - (typeof e.requestId !== "string" || !e.requestId) - ) - throw new Error("Message coverage lacks requestId"); } return page; } diff --git a/test/relay-engine.test.js b/test/relay-engine.test.js index b42980f..cfa70d8 100644 --- a/test/relay-engine.test.js +++ b/test/relay-engine.test.js @@ -180,6 +180,35 @@ test("Grok approval cards notify Codex once without returning its answer as auth assert.match(text, /explicit user decision/i); assert.equal(f.sent.length, 0); }); +test("historical messages without requestId do not block a new tracked request", async (t) => { + const f = await fixture(t); + f.page.push({ id: "old", kind: "send-message", text: "Historical output" }); + await f.engine.sendToGrok({ grokTarget: "target", codexThreadId: "thread", message: "Ask Grok", requestId: "ask" }); + assert.equal(f.sent.length, 1); + f.page.push( + { id: "user", kind: "user", clientNonce: f.sent[0].clientNonce, requestId: "run" }, + { id: "answer", kind: "send-message", requestId: "run", text: "New answer" }, + ); + await f.engine.tick(); + await f.engine.tick(); + const calls = f.fake.received.filter(r => r.method === "turn/start"); + assert.equal(calls.length, 1); + assert.match(calls[0].params.input[0].text, /New answer/); + assert.doesNotMatch(calls[0].params.input[0].text, /Historical output/); +}); + +test("new messages without requestId pause intake without forwarding or advancing the checkpoint", async (t) => { + const f = await fixture(t); + await f.engine.sendToGrok({ grokTarget: "target", codexThreadId: "thread", message: "Ask Grok", requestId: "ask" }); + f.page.push({ id: "new", kind: "send-message", text: "Uncorrelated output" }); + await f.engine.tick(); + assert.equal(f.fake.received.filter(r => r.method === "turn/start").length, 0); + const status = await f.engine.status(); + assert.equal(status.targets[0].state, "paused"); + assert.equal(status.targets[0].reason, "invalid-coverage"); + assert.equal(status.targets[0].cursor, null); +}); + test("tracked request uses actual requestId and never forwards another thread reply", async (t) => { const f = await fixture(t); const r = await f.engine.sendToGrok({