Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/send-accepted-ack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"grok-bot-cli": patch
---

Confirm `{ accepted: true }` send acknowledgements by finding the request's clientNonce in the transcript tail, so delivered messages report `delivery: "accepted"` with a messageId instead of `unknown`.
2 changes: 1 addition & 1 deletion artifact/agent-bundle.compile-evidence.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifact/agent-bundle.manifest.json

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion artifact/bin/gbot-flight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29884,7 +29884,10 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
}
throw err;
}
const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
let messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
if (!messageId && data && typeof data === "object" && data.accepted === true) {
messageId = await transcriptEntryIdForNonce(session, rec.id, body.clientNonce, extra.signal);
}
return {
target: rec,
result: data && typeof data === "object" ? data : {},
Expand All @@ -29894,6 +29897,21 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
} : {}
};
}
async function transcriptEntryIdForNonce(session, agentId, clientNonce, signal) {
try {
const data = await gatewayCall(session, "getAgentTranscriptTail", {
id: agentId,
limit: 20
}, {
signal
});
const entries = data && Array.isArray(data.entries) ? data.entries : [];
const hit = entries.find((e)=>e && e.clientNonce === clientNonce && typeof e.id === "string" && e.id);
return hit ? hit.id : null;
} catch {
return null;
}
}
async function getTranscriptTail(session, ref, limit = 40) {
const rec = await resolveRef(session, ref);
const bounded = Math.min(Math.max(Math.trunc(limit) || 40, 1), 200);
Expand Down
20 changes: 19 additions & 1 deletion artifact/bin/gbot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48313,7 +48313,10 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
}
throw err;
}
const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
let messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
if (!messageId && data && typeof data === "object" && data.accepted === true) {
messageId = await transcriptEntryIdForNonce(session, rec.id, body.clientNonce, extra.signal);
}
return {
target: rec,
result: data && typeof data === "object" ? data : {},
Expand All @@ -48323,6 +48326,21 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
} : {}
};
}
async function transcriptEntryIdForNonce(session, agentId, clientNonce, signal) {
try {
const data = await gatewayCall(session, "getAgentTranscriptTail", {
id: agentId,
limit: 20
}, {
signal
});
const entries = data && Array.isArray(data.entries) ? data.entries : [];
const hit = entries.find((e)=>e && e.clientNonce === clientNonce && typeof e.id === "string" && e.id);
return hit ? hit.id : null;
} catch {
return null;
}
}
async function getTranscriptTail(session, ref, limit = 40) {
const rec = await resolveRef(session, ref);
const bounded = Math.min(Math.max(Math.trunc(limit) || 40, 1), 200);
Expand Down
20 changes: 19 additions & 1 deletion artifact/mcp/mcp-grok-bot-b8c2461e-flight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28498,7 +28498,10 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
}
throw err;
}
const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
let messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
if (!messageId && data && typeof data === "object" && data.accepted === true) {
messageId = await transcriptEntryIdForNonce(session, rec.id, body.clientNonce, extra.signal);
}
return {
target: rec,
result: data && typeof data === "object" ? data : {},
Expand All @@ -28508,6 +28511,21 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
} : {}
};
}
async function transcriptEntryIdForNonce(session, agentId, clientNonce, signal) {
try {
const data = await gatewayCall(session, "getAgentTranscriptTail", {
id: agentId,
limit: 20
}, {
signal
});
const entries = data && Array.isArray(data.entries) ? data.entries : [];
const hit = entries.find((e)=>e && e.clientNonce === clientNonce && typeof e.id === "string" && e.id);
return hit ? hit.id : null;
} catch {
return null;
}
}
async function getTranscriptTail(session, ref, limit = 40) {
const rec = await resolveRef(session, ref);
const bounded = Math.min(Math.max(Math.trunc(limit) || 40, 1), 200);
Expand Down
20 changes: 19 additions & 1 deletion artifact/mcp/mcp-grok-bot-b8c2461e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65713,7 +65713,10 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
}
throw err;
}
const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
let messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
if (!messageId && data && typeof data === "object" && data.accepted === true) {
messageId = await transcriptEntryIdForNonce(session, rec.id, body.clientNonce, extra.signal);
}
return {
target: rec,
result: data && typeof data === "object" ? data : {},
Expand All @@ -65723,6 +65726,21 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
} : {}
};
}
async function transcriptEntryIdForNonce(session, agentId, clientNonce, signal) {
try {
const data = await gatewayCall(session, "getAgentTranscriptTail", {
id: agentId,
limit: 20
}, {
signal
});
const entries = data && Array.isArray(data.entries) ? data.entries : [];
const hit = entries.find((e)=>e && e.clientNonce === clientNonce && typeof e.id === "string" && e.id);
return hit ? hit.id : null;
} catch {
return null;
}
}
async function getTranscriptTail(session, ref, limit = 40) {
const rec = await resolveRef(session, ref);
const bounded = Math.min(Math.max(Math.trunc(limit) || 40, 1), 200);
Expand Down
20 changes: 19 additions & 1 deletion artifact/scripts/gbot-relay.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25544,7 +25544,10 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
}
throw err;
}
const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
let messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
if (!messageId && data && typeof data === "object" && data.accepted === true) {
messageId = await transcriptEntryIdForNonce(session, rec.id, body.clientNonce, extra.signal);
}
return {
target: rec,
result: data && typeof data === "object" ? data : {},
Expand All @@ -25554,6 +25557,21 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
} : {}
};
}
async function transcriptEntryIdForNonce(session, agentId, clientNonce, signal) {
try {
const data = await gatewayCall(session, "getAgentTranscriptTail", {
id: agentId,
limit: 20
}, {
signal
});
const entries = data && Array.isArray(data.entries) ? data.entries : [];
const hit = entries.find((e)=>e && e.clientNonce === clientNonce && typeof e.id === "string" && e.id);
return hit ? hit.id : null;
} catch {
return null;
}
}
async function getTranscriptTail(session, ref, limit = 40) {
const rec = await resolveRef(session, ref);
const bounded = Math.min(Math.max(Math.trunc(limit) || 40, 1), 200);
Expand Down
19 changes: 18 additions & 1 deletion src/core/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,12 +457,29 @@ export async function sendPrompt(session, ref, prompt, extra = {}) {
}
throw err;
}
const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
let messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : null;
// Current Grok Bot gateways answer `{ accepted: true }` without a messageId. A bare flag is not a
// receipt, so look for our clientNonce in the transcript tail and use that entry's id instead.
if (!messageId && data && typeof data === "object" && data.accepted === true) {
messageId = await transcriptEntryIdForNonce(session, rec.id, body.clientNonce, extra.signal);
}
// Only a confirmed receipt counts as accepted; anything else is unknown, never a silent accept.
// ponytail: full send/execution correlation envelope stays in #37.
return { target: rec, result: data && typeof data === "object" ? data : {}, delivery: messageId ? "accepted" : "unknown", ...(messageId ? { messageId } : {}) };
}

// Best effort: any failure leaves the send `unknown` rather than throwing after the prompt left.
async function transcriptEntryIdForNonce(session, agentId, clientNonce, signal) {
try {
const data = await gatewayCall(session, "getAgentTranscriptTail", { id: agentId, limit: 20 }, { signal });
const entries = data && Array.isArray(data.entries) ? data.entries : [];
const hit = entries.find((e) => e && e.clientNonce === clientNonce && typeof e.id === "string" && e.id);
return hit ? hit.id : null;
} catch {
return null;
}
}

export async function getTranscriptTail(session, ref, limit = 40) {
const rec = await resolveRef(session, ref);
const bounded = Math.min(Math.max(Math.trunc(limit) || 40, 1), 200);
Expand Down
37 changes: 37 additions & 0 deletions test/gateway-send.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,43 @@ test("sendPrompt without a receipt is unknown, not accepted", async (t) => {
assert.equal(out.messageId, undefined);
});

function mockAckGateway(t, transcript) {
let nonce;
t.mock.method(globalThis, "fetch", async (url, init) => {
const u = String(url);
if (u.endsWith("/api/listAgents")) return new Response(JSON.stringify(roster), { status: 200 });
if (u.endsWith("/api/sendPrompt")) {
nonce = JSON.parse(init.body).clientNonce;
return new Response(JSON.stringify({ accepted: true }), { status: 200 });
}
if (u.endsWith("/api/getAgentTranscriptTail")) return transcript(nonce);
return new Response("{}", { status: 404 });
});
}

test("sendPrompt confirms an { accepted: true } ack by finding its clientNonce in the transcript", async (t) => {
mockAckGateway(t, (nonce) => new Response(JSON.stringify({
entries: [{ id: "t7u", role: "user", clientNonce: "someone-else" }, { id: "t8u", role: "user", clientNonce: nonce }],
}), { status: 200 }));
const out = await sendPrompt(session, "General", "hi");
assert.equal(out.delivery, "accepted");
assert.equal(out.messageId, "t8u");
});

test("sendPrompt keeps an { accepted: true } ack unknown when its clientNonce is not in the transcript", async (t) => {
mockAckGateway(t, () => new Response(JSON.stringify({ entries: [{ id: "t7u", role: "user", clientNonce: "someone-else" }] }), { status: 200 }));
const out = await sendPrompt(session, "General", "hi");
assert.equal(out.delivery, "unknown");
assert.equal(out.messageId, undefined);
});

test("sendPrompt keeps an { accepted: true } ack unknown when the transcript read fails", async (t) => {
mockAckGateway(t, () => new Response("boom", { status: 500 }));
const out = await sendPrompt(session, "General", "hi");
assert.equal(out.delivery, "unknown");
assert.equal(out.messageId, undefined);
});

test("sendPrompt rejects on a 4xx and preserves the target id", async (t) => {
mockGateway(t, () => new Response(JSON.stringify({ message: "nope" }), { status: 400 }));
await assert.rejects(sendPrompt(session, "General", "hi"), (e) => {
Expand Down