Skip to content
Merged
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/relay-failure-detail-fd-limit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"grok-bot-cli": patch
---

Codex delivery failures are now explained instead of reported as a bare "Delivery rejected": managed-relay receipts carry a `detail` field (the Codex daemon's own error text), `gbot codex send --reply-to-grok` / `codex_send` print the reason, the detail and the next step (busy thread, `systemError`, queue unavailable), a failed Codex turn returns its error to Grok instead of "no final text", and `--when-busy queue` on a managed send says to omit it (guarded steer is the default) or use `steer`/`reject`. The macOS daemon login script (`gbot codex desktop-shim`) now raises the open-file limit before starting the Codex daemon: launchd's 256-file soft limit made a busy daemon fail turns with "Too many open files" and put threads into `systemError`.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ active work; `busyPolicy: "reject"` on a binding refuses busy threads. A managed
`codex_send` can select `whenBusy` for that one delivery; omitting it uses the
binding's stored policy. An override does not change subsequent linked traffic.

**Rejections and failed turns are explained.** A managed receipt that is `rejected` carries `reason` plus `detail` (the daemon's own error text) and the CLI/MCP result adds the next step; a failed Codex turn is returned to Grok as `Codex turn failed with no final text: <Codex error>`. `--when-busy queue` is not available with `--reply-to-grok`/`bindingId` (the experimental queue cannot carry the Grok return route): omit `--when-busy` (an active turn is steered, an idle thread starts a turn), or pass `steer` / `reject`.

Without an identifiable native source or explicit route, `gbot_send` preserves the
ordinary send and returns `replyRoute: {mode: "manual", reason: "source-unavailable"}`;
read with `gbot_thread` later. `replyMode: "manual"` deliberately selects that flow.
Expand Down Expand Up @@ -260,7 +262,7 @@ gbot codex send --correlation-id M --reply-to M --hop 1 <threadId> "ack" # the a

Sends at `hop >= GROK_BOT_MAX_HOPS` (default 4) are refused with `reason: "hop-limit"` before anything reaches the daemon, so two agents cannot acknowledge each other forever; `gbot` never auto-acknowledges. `--envelope` (implied by any envelope flag) prepends a one-line `[gbot msg=… corr=… reply-to=… hop=… from=user@host]` header so the receiving agent can quote the ids back. That header is caller-authored provenance for the reader, not authentication: the daemon authenticates the local user through the socket, nothing else. Private ChatGPT Desktop pipes and arbitrary ChatGPT chats stay out of scope; only Codex threads on a reachable app-server daemon are routes.

**Busy threads.** `send` reads the thread status on resume. Only `idle` and `notLoaded` threads start a turn. An `active` thread (a turn in progress, or waiting on approval / user input) is refused with `reason: "busy"`: in app-server 0.158.0 a `turn/start` on an active thread steers that turn rather than queueing behind it, by default. Explicit guarded steering and managed bridge routes can deliver into active work; they never interrupt a turn. Either wait for `list-threads` to show `idle` and resend, or pass `--when-busy queue` to hand the message to the daemon's own queue through Codex's experimental `thread/queue/add` — that needs `GROK_BOT_CODEX_EXPERIMENTAL=1`, returns `delivery: "queued"` with `queuedSubmissionId`, and `gbot codex queue <threadId>` shows what is still waiting. `systemError` threads are refused with `reason: "thread-error"`, statuses this version does not know with `reason: "unknown-status"`. Receipts distinguish `delivery: "accepted"` (turn started; `turnId`, `turnStatus`), `"queued"`, `"rejected"` (nothing was sent; see `reason`), and `"unknown"` (the request left but no acknowledgment came back — look for `messageId` in the thread or queue before resending). The decision record, with the schema evidence and a live probe of the queue API, is in [`docs/codex-busy-threads.md`](docs/codex-busy-threads.md).
**Busy threads.** `send` reads the thread status on resume. Only `idle` and `notLoaded` threads start a turn. An `active` thread (a turn in progress, or waiting on approval / user input) is refused with `reason: "busy"`: in app-server 0.158.0 a `turn/start` on an active thread steers that turn rather than queueing behind it, by default. Explicit guarded steering and managed bridge routes can deliver into active work; they never interrupt a turn. Either wait for `list-threads` to show `idle` and resend, or pass `--when-busy queue` to hand the message to the daemon's own queue through Codex's experimental `thread/queue/add` — that needs `GROK_BOT_CODEX_EXPERIMENTAL=1`, returns `delivery: "queued"` with `queuedSubmissionId`, and `gbot codex queue <threadId>` shows what is still waiting. `systemError` threads are refused with `reason: "thread-error"` (usually an unhealthy daemon; `gbot codex status`, and on macOS check the daemon's open-file count — the login script now starts it with a raised limit), statuses this version does not know with `reason: "unknown-status"`. Receipts distinguish `delivery: "accepted"` (turn started; `turnId`, `turnStatus`), `"queued"`, `"rejected"` (nothing was sent; see `reason`), and `"unknown"` (the request left but no acknowledgment came back — look for `messageId` in the thread or queue before resending). The decision record, with the schema evidence and a live probe of the queue API, is in [`docs/codex-busy-threads.md`](docs/codex-busy-threads.md).

**Failure modes.** Every `send` and `codex` outcome under `--json` is one document on stdout with `exitCode`; failures include `{ error, delivery, reason, messageId, correlationId, hop, exitCode: 1, … }` and the process exits 1. Framework argument/schema errors remain on stderr and exit 2. `--json` is reserved anywhere before `--`; put `--` before flag-like message text. `reason` values are stable:

Expand Down
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.

32 changes: 30 additions & 2 deletions artifact/bin/gbot-flight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9828,6 +9828,7 @@ var __webpack_modules__ = {
var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js");
var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js");
var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts");
var _core_format_js__rspack_import_6 = __webpack_require__("./src/core/format.js");
var _core_relay_routes_js__rspack_import_2 = __webpack_require__("./src/core/relay/routes.ts");
const resultSchema = zod__rspack_import_3.KCZ([
_core_codex_routes_js__rspack_import_1.FD,
Expand Down Expand Up @@ -9951,7 +9952,9 @@ var __webpack_modules__ = {
exitCode: out.delivery === 'rejected' ? 1 : 0
},
children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, {
children: `Delivery ${out.delivery}; terminal answer returns to Grok automatically.`
children: (0, _core_format_js__rspack_import_6.Ks)(out, {
managed: true
})
})
});
}
Expand Down Expand Up @@ -15763,7 +15766,7 @@ var __webpack_modules__ = {
}
async function codexReturnOperation(input, context) {
assertManagedSendOptions(input);
if (input.whenBusy === "queue") throw Error("Managed relay supports steer or reject, not experimental queue");
if (input.whenBusy === "queue") throw Error("--when-busy queue is not available with --reply-to-grok / bindingId: Codex's thread queue is experimental and cannot carry the managed Grok return route. " + "Omit --when-busy (default: an active turn is steered with a guarded steer, an idle thread starts a new turn), pass --when-busy steer to say so explicitly, " + "or --when-busy reject to fail instead of steering a busy thread.");
return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)("sendToCodex", {
grokTarget: input.replyToGrok,
codexThreadId: input.threadId,
Expand Down Expand Up @@ -28928,6 +28931,11 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
launchctl unsetenv CODEX_APP_SERVER_WS_URL 2>/dev/null || true
echo "$(ts) CODEX_CLI_PATH=$(launchctl getenv CODEX_CLI_PATH)"
if [[ -x "$REAL" ]]; then
# launchd gives login jobs a 256-file soft limit; a busy daemon (many threads, shell pipes,
# sqlite handles) exhausts it ("Too many open files") and turns fail. Raise it before the
# daemon starts: it inherits this limit.
ulimit -n 65536 2>/dev/null || ulimit -n 10240 2>/dev/null || ulimit -n "$(ulimit -Hn)" 2>/dev/null || true
echo "$(ts) open-file limit $(ulimit -n)"
"$REAL" app-server daemon start >/dev/null 2>&1 || true
echo "$(ts) daemon start attempted"
fi
Expand Down Expand Up @@ -29336,7 +29344,27 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
for (const warning of s.warnings ?? [])lines.push("warning: " + String(warning));
return lines.join("\n");
}
const DELIVERY_HINTS = {
busy: "The thread is mid-turn. Wait for it to go idle, or send with --when-busy steer (managed --reply-to-grok sends steer an active turn by default).",
"thread-error": "Codex reports the thread in systemError. That is usually the Codex daemon failing (e.g. out of file descriptors: `gbot codex status`, then `codex app-server daemon restart` when no turn is running) or a broken rollout file; the thread needs a healthy daemon or to be opened in a Codex client.",
"unknown-status": "This gbot does not know the thread status the daemon reported; upgrade gbot or check `gbot codex list-threads`.",
"experimental-disabled": "--when-busy queue needs GROK_BOT_CODEX_EXPERIMENTAL=1 on a daemon with the experimental queue API; otherwise wait for idle or use --when-busy steer.",
"submission-rejected": "The Codex daemon refused the message; see the detail text.",
transport: "The connection to the Codex daemon dropped; delivery is unknown. Check the thread before resending."
};
function describeDelivery(out, { managed = false } = {}) {
const delivery = String(out?.delivery ?? "unknown");
if (delivery === "rejected") {
const reason = typeof out.reason === "string" && out.reason ? out.reason : "rejected";
const detail = typeof out.detail === "string" && out.detail ? out.detail : typeof out.error === "string" ? out.error : "";
const hint = DELIVERY_HINTS[reason] ?? "";
return `Delivery rejected (${stripTerminalControls(reason)})${detail ? `: ${stripTerminalControls(detail)}` : ""}${hint ? `\n${hint}` : ""}`;
}
if (managed) return `Delivery ${delivery}; terminal answer returns to Grok automatically. If the turn fails, the failure text (including the Codex error) is sent back instead of a final answer.`;
return `Delivery ${delivery}`;
}
__webpack_require__.d(__webpack_exports__, {
Ks: ()=>describeDelivery,
P2: ()=>formatCodexStatus,
bM: ()=>formatRecord,
e_: ()=>formatCodexQueue,
Expand Down
32 changes: 30 additions & 2 deletions artifact/bin/gbot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8236,6 +8236,7 @@ var __webpack_modules__ = {
var _agent_bundle_runtime__rspack_import_5 = __webpack_require__("./node_modules/@agent-bundle/runtime/dist/506.js");
var zod__rspack_import_3 = __webpack_require__("./node_modules/zod/v4/classic/schemas.js");
var _core_codex_routes_js__rspack_import_1 = __webpack_require__("./src/core/codex/routes.ts");
var _core_format_js__rspack_import_6 = __webpack_require__("./src/core/format.js");
var _core_relay_routes_js__rspack_import_2 = __webpack_require__("./src/core/relay/routes.ts");
const resultSchema = zod__rspack_import_3.KCZ([
_core_codex_routes_js__rspack_import_1.FD,
Expand Down Expand Up @@ -8359,7 +8360,9 @@ var __webpack_modules__ = {
exitCode: out.delivery === 'rejected' ? 1 : 0
},
children: (0, react_jsx_runtime__rspack_import_0.jsx)(_agent_bundle_runtime__rspack_import_5.g.Text, {
children: `Delivery ${out.delivery}; terminal answer returns to Grok automatically.`
children: (0, _core_format_js__rspack_import_6.Ks)(out, {
managed: true
})
})
});
}
Expand Down Expand Up @@ -14171,7 +14174,7 @@ var __webpack_modules__ = {
}
async function codexReturnOperation(input, context) {
assertManagedSendOptions(input);
if (input.whenBusy === "queue") throw Error("Managed relay supports steer or reject, not experimental queue");
if (input.whenBusy === "queue") throw Error("--when-busy queue is not available with --reply-to-grok / bindingId: Codex's thread queue is experimental and cannot carry the managed Grok return route. " + "Omit --when-busy (default: an active turn is steered with a guarded steer, an idle thread starts a new turn), pass --when-busy steer to say so explicitly, " + "or --when-busy reject to fail instead of steering a busy thread.");
return (0, _gbot_js__rspack_import_1.yF)(()=>(0, _managed_js__rspack_import_0.Bb)("sendToCodex", {
grokTarget: input.replyToGrok,
codexThreadId: input.threadId,
Expand Down Expand Up @@ -47357,6 +47360,11 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
launchctl unsetenv CODEX_APP_SERVER_WS_URL 2>/dev/null || true
echo "$(ts) CODEX_CLI_PATH=$(launchctl getenv CODEX_CLI_PATH)"
if [[ -x "$REAL" ]]; then
# launchd gives login jobs a 256-file soft limit; a busy daemon (many threads, shell pipes,
# sqlite handles) exhausts it ("Too many open files") and turns fail. Raise it before the
# daemon starts: it inherits this limit.
ulimit -n 65536 2>/dev/null || ulimit -n 10240 2>/dev/null || ulimit -n "$(ulimit -Hn)" 2>/dev/null || true
echo "$(ts) open-file limit $(ulimit -n)"
"$REAL" app-server daemon start >/dev/null 2>&1 || true
echo "$(ts) daemon start attempted"
fi
Expand Down Expand Up @@ -47765,7 +47773,27 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
for (const warning of s.warnings ?? [])lines.push("warning: " + String(warning));
return lines.join("\n");
}
const DELIVERY_HINTS = {
busy: "The thread is mid-turn. Wait for it to go idle, or send with --when-busy steer (managed --reply-to-grok sends steer an active turn by default).",
"thread-error": "Codex reports the thread in systemError. That is usually the Codex daemon failing (e.g. out of file descriptors: `gbot codex status`, then `codex app-server daemon restart` when no turn is running) or a broken rollout file; the thread needs a healthy daemon or to be opened in a Codex client.",
"unknown-status": "This gbot does not know the thread status the daemon reported; upgrade gbot or check `gbot codex list-threads`.",
"experimental-disabled": "--when-busy queue needs GROK_BOT_CODEX_EXPERIMENTAL=1 on a daemon with the experimental queue API; otherwise wait for idle or use --when-busy steer.",
"submission-rejected": "The Codex daemon refused the message; see the detail text.",
transport: "The connection to the Codex daemon dropped; delivery is unknown. Check the thread before resending."
};
function describeDelivery(out, { managed = false } = {}) {
const delivery = String(out?.delivery ?? "unknown");
if (delivery === "rejected") {
const reason = typeof out.reason === "string" && out.reason ? out.reason : "rejected";
const detail = typeof out.detail === "string" && out.detail ? out.detail : typeof out.error === "string" ? out.error : "";
const hint = DELIVERY_HINTS[reason] ?? "";
return `Delivery rejected (${stripTerminalControls(reason)})${detail ? `: ${stripTerminalControls(detail)}` : ""}${hint ? `\n${hint}` : ""}`;
}
if (managed) return `Delivery ${delivery}; terminal answer returns to Grok automatically. If the turn fails, the failure text (including the Codex error) is sent back instead of a final answer.`;
return `Delivery ${delivery}`;
}
__webpack_require__.d(__webpack_exports__, {
Ks: ()=>describeDelivery,
P2: ()=>formatCodexStatus,
bM: ()=>formatRecord,
e_: ()=>formatCodexQueue,
Expand Down
Loading