@workerdeck/core 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.mts +47 -3
- package/build/index.mjs +887 -66
- package/build/index.mjs.map +1 -1
- package/package.json +5 -5
package/build/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
-
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
|
|
4
|
+
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, contextReading, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
|
|
5
5
|
import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
|
-
import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { createVfs, runScript } from "@workerdeck/sandbox";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { lookup } from "node:dns/promises";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
11
|
+
import { homedir, tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
13
13
|
//#region src/lib/attachments.ts
|
|
14
14
|
/** The four the Anthropic API accepts. Notably absent: image/heic — an iPhone's
|
|
15
15
|
* native photo format, which clients must transcode before upload. */
|
|
@@ -1100,6 +1100,13 @@ var SessionRunner = class {
|
|
|
1100
1100
|
#events = [];
|
|
1101
1101
|
#subscribers = new SubscriberSet();
|
|
1102
1102
|
#seq = 0;
|
|
1103
|
+
/**
|
|
1104
|
+
* Latest context-window reading, retained from the last `context_usage` this
|
|
1105
|
+
* runner emitted so `GET /sessions` can answer it without an attach — see
|
|
1106
|
+
* `SessionInfo.contextUsage`. Folded in the emit path, so it is by
|
|
1107
|
+
* construction the same number the transcript last drew.
|
|
1108
|
+
*/
|
|
1109
|
+
#contextUsage;
|
|
1103
1110
|
#activityCount = 0;
|
|
1104
1111
|
/**
|
|
1105
1112
|
* Seq of the latest `conversation_reset` event, 0 when none. The log itself is
|
|
@@ -1194,6 +1201,7 @@ var SessionRunner = class {
|
|
|
1194
1201
|
createdAt: this.createdAt,
|
|
1195
1202
|
lastSeq: this.#seq,
|
|
1196
1203
|
activityCount: this.#activityCount,
|
|
1204
|
+
contextUsage: this.#contextUsage,
|
|
1197
1205
|
pendingPermissionCount: this.#pending.size,
|
|
1198
1206
|
subagents: this.#subagents.list(),
|
|
1199
1207
|
meta: this.#config.meta,
|
|
@@ -1768,7 +1776,11 @@ var SessionRunner = class {
|
|
|
1768
1776
|
};
|
|
1769
1777
|
this.#lastActivityAt = event.ts;
|
|
1770
1778
|
this.#activityCount += transcriptActivity(body);
|
|
1771
|
-
|
|
1779
|
+
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
1780
|
+
if (body.type === "conversation_reset") {
|
|
1781
|
+
this.#resetSeq = event.seq;
|
|
1782
|
+
this.#contextUsage = void 0;
|
|
1783
|
+
}
|
|
1772
1784
|
this.#subagents.observe(body, event.ts);
|
|
1773
1785
|
this.#events.push(event);
|
|
1774
1786
|
this.#subscribers.emit(event);
|
|
@@ -1816,6 +1828,13 @@ var AiSdkRunner = class {
|
|
|
1816
1828
|
#events = [];
|
|
1817
1829
|
#subscribers = new SubscriberSet();
|
|
1818
1830
|
#seq = 0;
|
|
1831
|
+
/**
|
|
1832
|
+
* Latest context-window reading, retained from the last `context_usage` this
|
|
1833
|
+
* runner emitted so `GET /sessions` can answer it without an attach — see
|
|
1834
|
+
* `SessionInfo.contextUsage`. Folded in the emit path, so it is by
|
|
1835
|
+
* construction the same number the transcript last drew.
|
|
1836
|
+
*/
|
|
1837
|
+
#contextUsage;
|
|
1819
1838
|
#activityCount = 0;
|
|
1820
1839
|
#status = "starting";
|
|
1821
1840
|
#permissionMode;
|
|
@@ -1865,7 +1884,12 @@ var AiSdkRunner = class {
|
|
|
1865
1884
|
if (!state || !Array.isArray(state.messages)) throw new Error("session snapshot is missing its provider-engine state");
|
|
1866
1885
|
this.#seq = snapshot.seq;
|
|
1867
1886
|
this.#events = [...snapshot.events];
|
|
1868
|
-
this.#activityCount =
|
|
1887
|
+
this.#activityCount = 0;
|
|
1888
|
+
for (const event of this.#events) {
|
|
1889
|
+
this.#activityCount += transcriptActivity(event);
|
|
1890
|
+
if (event.type === "conversation_reset") this.#contextUsage = void 0;
|
|
1891
|
+
else this.#contextUsage = contextReading(event) ?? this.#contextUsage;
|
|
1892
|
+
}
|
|
1869
1893
|
this.#messages = [...state.messages];
|
|
1870
1894
|
for (const call of state.pendingToolCalls) this.#pendingToolCalls.set(call.toolCallId, call);
|
|
1871
1895
|
this.#dispatched = new Set(state.dispatched);
|
|
@@ -1916,6 +1940,7 @@ var AiSdkRunner = class {
|
|
|
1916
1940
|
createdAt: this.createdAt,
|
|
1917
1941
|
lastSeq: this.#seq,
|
|
1918
1942
|
activityCount: this.#activityCount,
|
|
1943
|
+
contextUsage: this.#contextUsage,
|
|
1919
1944
|
pendingPermissionCount: 0,
|
|
1920
1945
|
meta: this.#config.meta,
|
|
1921
1946
|
scope: this.#config.scope,
|
|
@@ -2602,6 +2627,8 @@ var AiSdkRunner = class {
|
|
|
2602
2627
|
};
|
|
2603
2628
|
this.#lastActivityAt = event.ts;
|
|
2604
2629
|
this.#activityCount += transcriptActivity(body);
|
|
2630
|
+
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
2631
|
+
if (body.type === "conversation_reset") this.#contextUsage = void 0;
|
|
2605
2632
|
this.#events.push(event);
|
|
2606
2633
|
this.#subscribers.emit(event);
|
|
2607
2634
|
}
|
|
@@ -3822,8 +3849,6 @@ const claudeAdapter = {
|
|
|
3822
3849
|
}));
|
|
3823
3850
|
}
|
|
3824
3851
|
};
|
|
3825
|
-
//#endregion
|
|
3826
|
-
//#region src/engines/codex/jsonrpc.ts
|
|
3827
3852
|
/**
|
|
3828
3853
|
* A JSON-RPC error response from the peer, or one we return to it. `code`
|
|
3829
3854
|
* follows the JSON-RPC 2.0 reserved ranges (-32601 = method not found).
|
|
@@ -3855,9 +3880,12 @@ var JsonRpcStdioConnection = class {
|
|
|
3855
3880
|
#buffer = "";
|
|
3856
3881
|
#closed = false;
|
|
3857
3882
|
#notificationHandler;
|
|
3883
|
+
/** Where {@link CODEX_TRACE_ENV} pointed, or undefined — read once. */
|
|
3884
|
+
#trace;
|
|
3858
3885
|
#requestHandler;
|
|
3859
3886
|
constructor(options) {
|
|
3860
3887
|
this.#output = options.output;
|
|
3888
|
+
this.#trace = process.env["WORKERDECK_CODEX_TRACE"] || void 0;
|
|
3861
3889
|
options.input.on("data", (chunk) => this.#feed(String(chunk)));
|
|
3862
3890
|
options.input.on("error", () => {});
|
|
3863
3891
|
options.output.on("error", () => {});
|
|
@@ -3918,9 +3946,28 @@ var JsonRpcStdioConnection = class {
|
|
|
3918
3946
|
} catch {
|
|
3919
3947
|
continue;
|
|
3920
3948
|
}
|
|
3949
|
+
this.#traceLine(message);
|
|
3921
3950
|
this.#dispatch(message);
|
|
3922
3951
|
}
|
|
3923
3952
|
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Append one inbound message to the trace file, when the operator asked for
|
|
3955
|
+
* one. **Notifications and server→client requests only** — a response body is
|
|
3956
|
+
* not needed to answer the questions this exists for, and `account/*` results
|
|
3957
|
+
* are the one place app-server traffic can carry a masked credential
|
|
3958
|
+
* fragment, which nothing of ours writes to disk (see the auth red lines).
|
|
3959
|
+
* Best-effort and synchronous: a debug sink that loses lines proves nothing,
|
|
3960
|
+
* and a debug sink that throws must not take the session with it.
|
|
3961
|
+
*/
|
|
3962
|
+
#traceLine(message) {
|
|
3963
|
+
if (!this.#trace) return;
|
|
3964
|
+
const method = message.method;
|
|
3965
|
+
if (typeof method !== "string") return;
|
|
3966
|
+
if (method.startsWith("account/") || method.startsWith("login")) return;
|
|
3967
|
+
try {
|
|
3968
|
+
appendFileSync(this.#trace, JSON.stringify(message) + "\n");
|
|
3969
|
+
} catch {}
|
|
3970
|
+
}
|
|
3924
3971
|
#dispatch(message) {
|
|
3925
3972
|
const { id, method } = message;
|
|
3926
3973
|
if (typeof method === "string") {
|
|
@@ -3959,6 +4006,441 @@ var JsonRpcStdioConnection = class {
|
|
|
3959
4006
|
}
|
|
3960
4007
|
};
|
|
3961
4008
|
//#endregion
|
|
4009
|
+
//#region src/engines/codex/subagents.ts
|
|
4010
|
+
/**
|
|
4011
|
+
* The codex side of `SessionInfo.subagents` — and the attribution table that
|
|
4012
|
+
* gives every event a spawned agent produces its `parentToolUseId`.
|
|
4013
|
+
*
|
|
4014
|
+
* Codex's signal is stronger than the claude engine's, so this is deliberately
|
|
4015
|
+
* NOT that tracker generalised (`engines/claude/subagents.ts` infers spawns
|
|
4016
|
+
* from tool names and verdicts from result-text sniffing, ~290 lines of module
|
|
4017
|
+
* doc explaining the inference). Here nothing is inferred: `subAgentActivity
|
|
4018
|
+
* {kind: 'started'}` on the owning thread positively announces an agent, names
|
|
4019
|
+
* it (`agentPath`), keys it (`agentThreadId` — the id every one of its later
|
|
4020
|
+
* notifications carries) and hands over the model's own `spawn_agent` call id;
|
|
4021
|
+
* the agent's end is its own thread's `turn/completed`, status included. So a
|
|
4022
|
+
* record is keyed by **thread id** — the wire's handle — while exposing a
|
|
4023
|
+
* **tool-use id** — the protocol's: `parentToolUseId` on nested events must
|
|
4024
|
+
* equal the anchor `tool_use`'s id for `subagentItems` (the frame membership
|
|
4025
|
+
* rule every client shares) to reassemble the sidechain, and this map is where
|
|
4026
|
+
* the two vocabularies meet.
|
|
4027
|
+
*
|
|
4028
|
+
* Two decisions worth their prose:
|
|
4029
|
+
*
|
|
4030
|
+
* **A record survives the runner's turns.** Codex agents are designed to
|
|
4031
|
+
* outlive the root turn that spawned them (`sendInput`/`resumeAgent` address a
|
|
4032
|
+
* thread that kept existing), so — unlike a pending approval — nothing here is
|
|
4033
|
+
* swept when a root turn ends. What does end every agent is the app-server
|
|
4034
|
+
* process itself: the runner calls {@link sweep} when the child dies or the
|
|
4035
|
+
* session closes, because an agent whose host process is gone can never report,
|
|
4036
|
+
* and `running` on a closed session would be a lie a polled list re-renders
|
|
4037
|
+
* forever (the claude tracker's argument, inherited whole).
|
|
4038
|
+
*
|
|
4039
|
+
* **The settled tail is bounded, running records never are** — the same
|
|
4040
|
+
* {@link SUBAGENT_HISTORY} discipline as the claude tracker, and enforced at
|
|
4041
|
+
* settle time for the same reason: a settle happens once per agent, `list()`
|
|
4042
|
+
* once per row of a 1.2s-polled sessions list.
|
|
4043
|
+
*/
|
|
4044
|
+
var CodexAgentTracker = class {
|
|
4045
|
+
#byThread = /* @__PURE__ */ new Map();
|
|
4046
|
+
#settleCounter = 0;
|
|
4047
|
+
/** The record whose thread this is — the attribution lookup. */
|
|
4048
|
+
get(agentThreadId) {
|
|
4049
|
+
return this.#byThread.get(agentThreadId);
|
|
4050
|
+
}
|
|
4051
|
+
/** Open (or return) the record for a thread. Fill-in, never overwrite: a
|
|
4052
|
+
* label-less fallback record keeps its accumulated count and its already
|
|
4053
|
+
* published toolUseId when the announcing item arrives late. */
|
|
4054
|
+
open(agentThreadId, toolUseId, agentType, ts) {
|
|
4055
|
+
let record = this.#byThread.get(agentThreadId);
|
|
4056
|
+
if (!record) {
|
|
4057
|
+
record = {
|
|
4058
|
+
agentThreadId,
|
|
4059
|
+
toolUseId,
|
|
4060
|
+
status: "running",
|
|
4061
|
+
startedAt: ts,
|
|
4062
|
+
toolCount: 0,
|
|
4063
|
+
counted: /* @__PURE__ */ new Set()
|
|
4064
|
+
};
|
|
4065
|
+
this.#byThread.set(agentThreadId, record);
|
|
4066
|
+
}
|
|
4067
|
+
record.agentType ??= agentType;
|
|
4068
|
+
return record;
|
|
4069
|
+
}
|
|
4070
|
+
/** The agent's thread ran again (`kind: 'interacted'`, or a fresh
|
|
4071
|
+
* `turn/started` on its thread): a settled verdict no longer describes it. */
|
|
4072
|
+
revive(record) {
|
|
4073
|
+
record.status = "running";
|
|
4074
|
+
record.settledOrder = void 0;
|
|
4075
|
+
}
|
|
4076
|
+
#settle(record, status) {
|
|
4077
|
+
record.status = status;
|
|
4078
|
+
record.settledOrder = ++this.#settleCounter;
|
|
4079
|
+
let settled = 0;
|
|
4080
|
+
for (const r of this.#byThread.values()) if (r.settledOrder !== void 0) settled++;
|
|
4081
|
+
while (settled > SUBAGENT_HISTORY) {
|
|
4082
|
+
let oldest;
|
|
4083
|
+
for (const r of this.#byThread.values()) {
|
|
4084
|
+
if (r.settledOrder === void 0) continue;
|
|
4085
|
+
if (!oldest || r.settledOrder < oldest.settledOrder) oldest = r;
|
|
4086
|
+
}
|
|
4087
|
+
if (!oldest) break;
|
|
4088
|
+
this.#byThread.delete(oldest.agentThreadId);
|
|
4089
|
+
settled--;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
/** A real verdict for one agent — its thread's `turn/completed`, or the
|
|
4093
|
+
* `interrupted` activity edge. */
|
|
4094
|
+
settle(record, status) {
|
|
4095
|
+
if (record.status === status) return;
|
|
4096
|
+
this.#settle(record, status);
|
|
4097
|
+
}
|
|
4098
|
+
/** The process the agents lived in is gone (child death, session close):
|
|
4099
|
+
* everything still running is settled as failed — the report can never come. */
|
|
4100
|
+
sweep() {
|
|
4101
|
+
for (const record of this.#byThread.values()) if (record.status === "running") this.#settle(record, "failed");
|
|
4102
|
+
}
|
|
4103
|
+
/** The rollup as `SessionInfo.subagents` serves it — spawn order, fresh
|
|
4104
|
+
* objects, and `undefined` when there is nothing to say (absent and empty
|
|
4105
|
+
* mean the same thing to a client, and bytes on a polled list are paid for). */
|
|
4106
|
+
list() {
|
|
4107
|
+
if (this.#byThread.size === 0) return void 0;
|
|
4108
|
+
const out = [];
|
|
4109
|
+
for (const r of this.#byThread.values()) out.push({
|
|
4110
|
+
toolUseId: r.toolUseId,
|
|
4111
|
+
agentType: r.agentType,
|
|
4112
|
+
status: r.status,
|
|
4113
|
+
startedAt: r.startedAt,
|
|
4114
|
+
toolCount: r.toolCount
|
|
4115
|
+
});
|
|
4116
|
+
return out;
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
//#endregion
|
|
4120
|
+
//#region src/engines/codex/trust.ts
|
|
4121
|
+
/**
|
|
4122
|
+
* Codex project trust: will this session's cwd get its `.codex/config.toml`?
|
|
4123
|
+
*
|
|
4124
|
+
* Codex only layers a project's `.codex/config.toml` onto the operator's base
|
|
4125
|
+
* config when the project is *trusted* (a `[projects."<path>"]
|
|
4126
|
+
* trust_level = "trusted"` entry in `$CODEX_HOME/config.toml`), and the
|
|
4127
|
+
* app-server surface has no trust prompt — that lives in the TUI. So under
|
|
4128
|
+
* WorkerDeck an untrusted project's config, MCP servers included, is silently
|
|
4129
|
+
* ignored: no error, no notice, servers just missing. The runner asks this
|
|
4130
|
+
* module at session start whether that is about to happen, so the transcript
|
|
4131
|
+
* can say so.
|
|
4132
|
+
*
|
|
4133
|
+
* Semantics, all measured against both the bundled 0.146.0 and 0.149.0
|
|
4134
|
+
* (2026-08-22, via `codex mcp list` from probe cwds and via `thread/start` +
|
|
4135
|
+
* `mcpServerStatus/list` on the app-server surface — identical answers):
|
|
4136
|
+
*
|
|
4137
|
+
* - **Discovery**: config layers come from the cwd and its ancestors up to and
|
|
4138
|
+
* including the nearest directory containing `.git` (dir or file). With no
|
|
4139
|
+
* git anywhere above, the cwd alone is consulted. Directories above the
|
|
4140
|
+
* nearest git root never contribute, trusted or not.
|
|
4141
|
+
* - **Trust per layer**: an exact entry for the layer's own canonical path
|
|
4142
|
+
* decides (an explicit `"untrusted"` beats inherited trust); without one the
|
|
4143
|
+
* layer inherits from the chain's git root — trusted iff the git root has a
|
|
4144
|
+
* trusted entry, where a linked worktree's root also counts its main
|
|
4145
|
+
* repository's entry (the `.git` file's gitdir names it). A trusted
|
|
4146
|
+
* mid-chain directory does NOT trust its children, and plain path
|
|
4147
|
+
* containment without git confers nothing.
|
|
4148
|
+
* - **Canonical paths**: codex matches entries against the canonicalized cwd —
|
|
4149
|
+
* a macOS `/tmp/...` entry never matches the `/private/tmp/...` it points
|
|
4150
|
+
* at, while the reverse spelling works (and the app-server canonicalizes its
|
|
4151
|
+
* `cwd` param too). Both sides here are realpath'd, which can only err
|
|
4152
|
+
* toward silence.
|
|
4153
|
+
* - **The gate is sandbox-scoped**: `thread/start` under `workspace-write` or
|
|
4154
|
+
* `danger-full-access` (permission modes `acceptEdits`/`bypassPermissions`)
|
|
4155
|
+
* WRITES the trust entry itself and loads the config — only `read-only`
|
|
4156
|
+
* (mode `default`) leaves the project untrusted and the config ignored. A
|
|
4157
|
+
* later `turn/start` with a wider sandboxPolicy does not heal the thread
|
|
4158
|
+
* (measured): the caller probes `default`-mode sessions only, and the notice
|
|
4159
|
+
* stays true for the session it opens.
|
|
4160
|
+
* - `trust_level`'s vocabulary is exactly `trusted`/`untrusted`; any other
|
|
4161
|
+
* value fails codex's bootstrap outright ("unknown variant"), so a config
|
|
4162
|
+
* carrying one probes silent — that session announces its own failure.
|
|
4163
|
+
*
|
|
4164
|
+
* The correctness bar for every degrade path: a FALSE notice — warning about a
|
|
4165
|
+
* project codex actually trusts — is worse than a missed one. The narrow TOML
|
|
4166
|
+
* reader below refuses (→ silence) anything it cannot interpret with
|
|
4167
|
+
* certainty, rather than guessing.
|
|
4168
|
+
*/
|
|
4169
|
+
const BARE_KEY = /[A-Za-z0-9_-]/;
|
|
4170
|
+
function skipWs(text, pos) {
|
|
4171
|
+
let i = pos;
|
|
4172
|
+
while (i < text.length && (text[i] === " " || text[i] === " ")) i++;
|
|
4173
|
+
return i;
|
|
4174
|
+
}
|
|
4175
|
+
/** One-line TOML basic string starting at `pos` (which must be `"`). Undefined
|
|
4176
|
+
* on an escape TOML doesn't define or a close quote that never comes — the
|
|
4177
|
+
* caller refuses the file rather than guessing what codex would read. */
|
|
4178
|
+
function parseBasicString(text, pos) {
|
|
4179
|
+
let out = "";
|
|
4180
|
+
let i = pos + 1;
|
|
4181
|
+
while (i < text.length) {
|
|
4182
|
+
const ch = text[i];
|
|
4183
|
+
if (ch === "\"") return {
|
|
4184
|
+
value: out,
|
|
4185
|
+
end: i + 1
|
|
4186
|
+
};
|
|
4187
|
+
if (ch === "\\") {
|
|
4188
|
+
const esc = text[i + 1];
|
|
4189
|
+
if (esc === "b") out += "\b";
|
|
4190
|
+
else if (esc === "t") out += " ";
|
|
4191
|
+
else if (esc === "n") out += "\n";
|
|
4192
|
+
else if (esc === "f") out += "\f";
|
|
4193
|
+
else if (esc === "r") out += "\r";
|
|
4194
|
+
else if (esc === "\"") out += "\"";
|
|
4195
|
+
else if (esc === "\\") out += "\\";
|
|
4196
|
+
else if (esc === "u" || esc === "U") {
|
|
4197
|
+
const width = esc === "u" ? 4 : 8;
|
|
4198
|
+
const hex = text.slice(i + 2, i + 2 + width);
|
|
4199
|
+
if (hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex)) return void 0;
|
|
4200
|
+
const code = Number.parseInt(hex, 16);
|
|
4201
|
+
if (code > 1114111) return void 0;
|
|
4202
|
+
out += String.fromCodePoint(code);
|
|
4203
|
+
i += width;
|
|
4204
|
+
} else return void 0;
|
|
4205
|
+
i += 2;
|
|
4206
|
+
continue;
|
|
4207
|
+
}
|
|
4208
|
+
out += ch;
|
|
4209
|
+
i++;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
/** One-line TOML literal string starting at `pos` (which must be `'`). */
|
|
4213
|
+
function parseLiteralString(text, pos) {
|
|
4214
|
+
const close = text.indexOf("'", pos + 1);
|
|
4215
|
+
if (close === -1) return void 0;
|
|
4216
|
+
return {
|
|
4217
|
+
value: text.slice(pos + 1, close),
|
|
4218
|
+
end: close + 1
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
/** A dotted key path — bare, `"basic"` and `'literal'` keys, whitespace around
|
|
4222
|
+
* the dots — as found in table headers and on the left of assignments. */
|
|
4223
|
+
function parseKeyPath(text, pos) {
|
|
4224
|
+
const keys = [];
|
|
4225
|
+
let i = pos;
|
|
4226
|
+
for (;;) {
|
|
4227
|
+
i = skipWs(text, i);
|
|
4228
|
+
const ch = text[i];
|
|
4229
|
+
if (ch === "\"" || ch === "'") {
|
|
4230
|
+
const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
|
|
4231
|
+
if (!str) return void 0;
|
|
4232
|
+
keys.push(str.value);
|
|
4233
|
+
i = str.end;
|
|
4234
|
+
} else if (ch !== void 0 && BARE_KEY.test(ch)) {
|
|
4235
|
+
let end = i;
|
|
4236
|
+
while (end < text.length && BARE_KEY.test(text[end])) end++;
|
|
4237
|
+
keys.push(text.slice(i, end));
|
|
4238
|
+
i = end;
|
|
4239
|
+
} else return;
|
|
4240
|
+
i = skipWs(text, i);
|
|
4241
|
+
if (text[i] !== ".") return {
|
|
4242
|
+
value: keys,
|
|
4243
|
+
end: i
|
|
4244
|
+
};
|
|
4245
|
+
i++;
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* Scan an assignment's value (or the continuation line of a multi-line array),
|
|
4250
|
+
* confirming where it ends. Returns the bracket depth carried onto the next
|
|
4251
|
+
* line (0 = the value is complete) plus the string itself when the whole value
|
|
4252
|
+
* was one plain one-line string. Undefined refuses the file: multi-line
|
|
4253
|
+
* strings are where a line reader starts misreading string *content* as
|
|
4254
|
+
* sections and entries — the exact mistake that could flip a real trust entry
|
|
4255
|
+
* — so they are not parsed around, they end the attempt.
|
|
4256
|
+
*/
|
|
4257
|
+
function scanValueLine(text, pos, depth) {
|
|
4258
|
+
let i = skipWs(text, pos);
|
|
4259
|
+
if (depth === 0 && (text[i] === "\"" || text[i] === "'")) {
|
|
4260
|
+
if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
|
|
4261
|
+
const str = text[i] === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
|
|
4262
|
+
if (!str) return void 0;
|
|
4263
|
+
const rest = skipWs(text, str.end);
|
|
4264
|
+
if (rest < text.length && text[rest] !== "#") return void 0;
|
|
4265
|
+
return {
|
|
4266
|
+
depth: 0,
|
|
4267
|
+
value: str.value
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
while (i < text.length) {
|
|
4271
|
+
const ch = text[i];
|
|
4272
|
+
if (ch === "#") break;
|
|
4273
|
+
if (ch === "\"" || ch === "'") {
|
|
4274
|
+
if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
|
|
4275
|
+
const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
|
|
4276
|
+
if (!str) return void 0;
|
|
4277
|
+
i = str.end;
|
|
4278
|
+
continue;
|
|
4279
|
+
}
|
|
4280
|
+
if (ch === "[" || ch === "{") depth++;
|
|
4281
|
+
else if (ch === "]" || ch === "}") {
|
|
4282
|
+
depth--;
|
|
4283
|
+
if (depth < 0) return void 0;
|
|
4284
|
+
}
|
|
4285
|
+
i++;
|
|
4286
|
+
}
|
|
4287
|
+
return { depth };
|
|
4288
|
+
}
|
|
4289
|
+
/**
|
|
4290
|
+
* The `[projects."<path>"] trust_level = "..."` entries of a codex
|
|
4291
|
+
* `config.toml`, by a deliberately narrow reader (core takes no TOML
|
|
4292
|
+
* dependency for this). Handles what codex itself writes plus the reasonable
|
|
4293
|
+
* hand-edits — comments, CRLF, whitespace, quoted keys with escapes, literal
|
|
4294
|
+
* and bare keys, `[projects]`-with-dotted-keys and top-level dotted forms,
|
|
4295
|
+
* single-line inline tables, multi-line arrays — and returns **undefined for
|
|
4296
|
+
* anything else it meets anywhere in the file** (multi-line strings,
|
|
4297
|
+
* `projects` as an inline table, array-of-tables, junk): the caller treats
|
|
4298
|
+
* undefined as "cannot know" and stays silent. Conflicting duplicate entries
|
|
4299
|
+
* also refuse — invalid for TOML, and guessing wrong is a false notice.
|
|
4300
|
+
*/
|
|
4301
|
+
function parseProjectTrustEntries(source) {
|
|
4302
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4303
|
+
let section = [];
|
|
4304
|
+
let carryDepth = 0;
|
|
4305
|
+
for (const rawLine of source.split("\n")) {
|
|
4306
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
4307
|
+
if (carryDepth > 0) {
|
|
4308
|
+
const scanned = scanValueLine(line, 0, carryDepth);
|
|
4309
|
+
if (!scanned) return void 0;
|
|
4310
|
+
carryDepth = scanned.depth;
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
const start = skipWs(line, 0);
|
|
4314
|
+
if (start >= line.length || line[start] === "#") continue;
|
|
4315
|
+
if (line[start] === "[") {
|
|
4316
|
+
const array = line.startsWith("[[", start);
|
|
4317
|
+
const path = parseKeyPath(line, start + (array ? 2 : 1));
|
|
4318
|
+
if (!path) return void 0;
|
|
4319
|
+
const close = array ? "]]" : "]";
|
|
4320
|
+
if (!line.startsWith(close, path.end)) return void 0;
|
|
4321
|
+
const rest = skipWs(line, path.end + close.length);
|
|
4322
|
+
if (rest < line.length && line[rest] !== "#") return void 0;
|
|
4323
|
+
if (array && path.value[0] === "projects") return void 0;
|
|
4324
|
+
section = path.value;
|
|
4325
|
+
continue;
|
|
4326
|
+
}
|
|
4327
|
+
const key = parseKeyPath(line, start);
|
|
4328
|
+
if (!key) return void 0;
|
|
4329
|
+
if (line[key.end] !== "=") return void 0;
|
|
4330
|
+
const scanned = scanValueLine(line, key.end + 1, 0);
|
|
4331
|
+
if (!scanned) return void 0;
|
|
4332
|
+
carryDepth = scanned.depth;
|
|
4333
|
+
const full = [...section, ...key.value];
|
|
4334
|
+
if (full[0] !== "projects") continue;
|
|
4335
|
+
if (full.length < 3) return void 0;
|
|
4336
|
+
if (full.length === 3 && full[2] === "trust_level") {
|
|
4337
|
+
if (carryDepth !== 0 || scanned.value === void 0) return void 0;
|
|
4338
|
+
const project = full[1];
|
|
4339
|
+
const existing = entries.get(project);
|
|
4340
|
+
if (existing !== void 0 && existing !== scanned.value) return void 0;
|
|
4341
|
+
entries.set(project, scanned.value);
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
if (carryDepth > 0) return void 0;
|
|
4345
|
+
return entries;
|
|
4346
|
+
}
|
|
4347
|
+
/**
|
|
4348
|
+
* A linked worktree inherits trust from its main repository's entry (measured:
|
|
4349
|
+
* trusting the main repo path loads the worktree's project config). The
|
|
4350
|
+
* worktree's `.git` is a FILE whose `gitdir:` line names
|
|
4351
|
+
* `<main>/.git/worktrees/<name>`; the directory owning that `.git` is the
|
|
4352
|
+
* anchor to look up. Anything unreadable or shaped differently resolves false
|
|
4353
|
+
* — this route can only ADD trust, i.e. silence, never a false notice.
|
|
4354
|
+
*/
|
|
4355
|
+
function mainRepositoryTrusted(gitRootDir, canonical) {
|
|
4356
|
+
const gitPath = join(gitRootDir, ".git");
|
|
4357
|
+
try {
|
|
4358
|
+
if (!statSync(gitPath).isFile()) return false;
|
|
4359
|
+
const match = /^gitdir:[ \t]*(.+?)[ \t]*$/m.exec(readFileSync(gitPath, "utf8"));
|
|
4360
|
+
if (!match) return false;
|
|
4361
|
+
const gitdir = resolve(gitRootDir, match[1]);
|
|
4362
|
+
const at = gitdir.lastIndexOf(`${sep}.git${sep}`);
|
|
4363
|
+
if (at <= 0) return false;
|
|
4364
|
+
let main = gitdir.slice(0, at);
|
|
4365
|
+
try {
|
|
4366
|
+
main = realpathSync(main);
|
|
4367
|
+
} catch {}
|
|
4368
|
+
return canonical.get(main) === "trusted";
|
|
4369
|
+
} catch {
|
|
4370
|
+
return false;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
/**
|
|
4374
|
+
* The notice for a codex session about to run on a cwd whose
|
|
4375
|
+
* `.codex/config.toml` codex will ignore, or undefined when there is nothing
|
|
4376
|
+
* to say — no project config anywhere codex would look, the project is
|
|
4377
|
+
* trusted, or the situation cannot be established with certainty. Read-only
|
|
4378
|
+
* throughout: WorkerDeck never writes trust entries (adjacent to the auth red
|
|
4379
|
+
* lines — trusting a directory is the operator's decision, made in codex's
|
|
4380
|
+
* own prompt or by their own hand).
|
|
4381
|
+
*/
|
|
4382
|
+
function untrustedProjectNotice(options) {
|
|
4383
|
+
let cwd;
|
|
4384
|
+
try {
|
|
4385
|
+
cwd = realpathSync(options.cwd);
|
|
4386
|
+
} catch {
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
let home = resolve(options.codexHome);
|
|
4390
|
+
try {
|
|
4391
|
+
home = realpathSync(options.codexHome);
|
|
4392
|
+
} catch {}
|
|
4393
|
+
const chain = [];
|
|
4394
|
+
let dir = cwd;
|
|
4395
|
+
for (;;) {
|
|
4396
|
+
chain.push(dir);
|
|
4397
|
+
if (existsSync(join(dir, ".git"))) break;
|
|
4398
|
+
const parent = dirname(dir);
|
|
4399
|
+
if (parent === dir) break;
|
|
4400
|
+
dir = parent;
|
|
4401
|
+
}
|
|
4402
|
+
const anchor = chain[chain.length - 1];
|
|
4403
|
+
const gitRoot = existsSync(join(anchor, ".git")) ? anchor : void 0;
|
|
4404
|
+
const layers = (gitRoot ? chain : [cwd]).filter((layer) => {
|
|
4405
|
+
if (!existsSync(join(layer, ".codex", "config.toml"))) return false;
|
|
4406
|
+
try {
|
|
4407
|
+
return realpathSync(join(layer, ".codex")) !== home;
|
|
4408
|
+
} catch {
|
|
4409
|
+
return false;
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
if (layers.length === 0) return void 0;
|
|
4413
|
+
const homeConfigPath = join(options.codexHome, "config.toml");
|
|
4414
|
+
let source = "";
|
|
4415
|
+
try {
|
|
4416
|
+
source = readFileSync(homeConfigPath, "utf8");
|
|
4417
|
+
} catch (error) {
|
|
4418
|
+
if (error.code !== "ENOENT") return void 0;
|
|
4419
|
+
}
|
|
4420
|
+
const entries = parseProjectTrustEntries(source);
|
|
4421
|
+
if (!entries) return void 0;
|
|
4422
|
+
for (const value of entries.values()) if (value !== "trusted" && value !== "untrusted") return void 0;
|
|
4423
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
4424
|
+
for (const [key, value] of entries) {
|
|
4425
|
+
let path = key;
|
|
4426
|
+
try {
|
|
4427
|
+
path = realpathSync(key);
|
|
4428
|
+
} catch {}
|
|
4429
|
+
if (canonical.get(path) === "trusted") continue;
|
|
4430
|
+
canonical.set(path, value);
|
|
4431
|
+
}
|
|
4432
|
+
const rootTrusted = gitRoot !== void 0 && (canonical.get(gitRoot) === "trusted" || mainRepositoryTrusted(gitRoot, canonical));
|
|
4433
|
+
const ignored = layers.filter((layer) => {
|
|
4434
|
+
const entry = canonical.get(layer);
|
|
4435
|
+
if (entry !== void 0) return entry !== "trusted";
|
|
4436
|
+
return !rootTrusted;
|
|
4437
|
+
});
|
|
4438
|
+
if (ignored.length === 0) return void 0;
|
|
4439
|
+
const trustDir = gitRoot ?? cwd;
|
|
4440
|
+
const configs = ignored.map((layer) => join(layer, ".codex", "config.toml"));
|
|
4441
|
+
return `codex does not trust this directory, so ${configs.length === 1 ? `its project config (${configs[0]}) is` : `its project configs (${configs.join(", ")}) are`} being ignored — MCP servers and settings declared there will be missing from this session. To trust it, run codex once in ${trustDir} and accept the trust prompt, or add [projects."${trustDir}"] with trust_level = "trusted" to ${homeConfigPath}.`;
|
|
4442
|
+
}
|
|
4443
|
+
//#endregion
|
|
3962
4444
|
//#region src/engines/codex/runner.ts
|
|
3963
4445
|
/**
|
|
3964
4446
|
* thread/start's sandbox axis (string form) — our permission modes as codex
|
|
@@ -3966,16 +4448,30 @@ var JsonRpcStdioConnection = class {
|
|
|
3966
4448
|
* the OS sandbox and — with the ask policy below — escalates to a real
|
|
3967
4449
|
* question), `acceptEdits` → workspace-write (in-workspace writes sail
|
|
3968
4450
|
* through, the acceptEdits grant), `bypassPermissions` → danger-full-access.
|
|
4451
|
+
* `auto` rides the SAME sandbox as acceptEdits — it is not a wider grant, it
|
|
4452
|
+
* only moves *who answers* the approvals (see {@link APPROVALS_REVIEWER_BY_MODE}).
|
|
3969
4453
|
*/
|
|
3970
4454
|
const THREAD_SANDBOX_BY_MODE = {
|
|
3971
4455
|
default: "read-only",
|
|
3972
4456
|
acceptEdits: "workspace-write",
|
|
4457
|
+
auto: "workspace-write",
|
|
3973
4458
|
bypassPermissions: "danger-full-access"
|
|
3974
4459
|
};
|
|
3975
|
-
/**
|
|
4460
|
+
/**
|
|
4461
|
+
* turn/start's sandboxPolicy axis (object form — same policy, second shape).
|
|
4462
|
+
*
|
|
4463
|
+
* The `workspaceWrite` entries here are a SHAPE, not the whole policy: every
|
|
4464
|
+
* unstated field of that variant is serde-defaulted by the app-server, so
|
|
4465
|
+
* sending it bare silently overrides the operator's `[sandbox_workspace_write]`
|
|
4466
|
+
* — `network_access` back to false, `writable_roots` back to empty — on every
|
|
4467
|
+
* turn. {@link CodexRunner.#turnSandboxPolicy} restates those fields from
|
|
4468
|
+
* `config/read`; nothing else may send this map's `workspaceWrite` entries
|
|
4469
|
+
* directly.
|
|
4470
|
+
*/
|
|
3976
4471
|
const TURN_SANDBOX_BY_MODE = {
|
|
3977
4472
|
default: { type: "readOnly" },
|
|
3978
4473
|
acceptEdits: { type: "workspaceWrite" },
|
|
4474
|
+
auto: { type: "workspaceWrite" },
|
|
3979
4475
|
bypassPermissions: { type: "dangerFullAccess" }
|
|
3980
4476
|
};
|
|
3981
4477
|
/**
|
|
@@ -3999,16 +4495,52 @@ const GRANULAR_ASK = { granular: {
|
|
|
3999
4495
|
request_permissions: true,
|
|
4000
4496
|
skill_approval: true
|
|
4001
4497
|
} };
|
|
4498
|
+
const GRANULAR_NEVER = { granular: {
|
|
4499
|
+
sandbox_approval: false,
|
|
4500
|
+
rules: false,
|
|
4501
|
+
mcp_elicitations: false,
|
|
4502
|
+
request_permissions: false,
|
|
4503
|
+
skill_approval: false
|
|
4504
|
+
} };
|
|
4505
|
+
/**
|
|
4506
|
+
* Notifications whose meaning is scoped to ONE thread, and which are therefore
|
|
4507
|
+
* only ever read off the session's own. Everything else (items, deltas) is
|
|
4508
|
+
* accepted from any thread on the connection — see `#handleNotification`.
|
|
4509
|
+
*/
|
|
4510
|
+
const THREAD_SCOPED_NOTIFICATIONS = new Set([
|
|
4511
|
+
"turn/started",
|
|
4512
|
+
"turn/completed",
|
|
4513
|
+
"thread/tokenUsage/updated"
|
|
4514
|
+
]);
|
|
4002
4515
|
const APPROVAL_POLICY_BY_MODE = {
|
|
4003
4516
|
default: GRANULAR_ASK,
|
|
4004
4517
|
acceptEdits: GRANULAR_ASK,
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4518
|
+
auto: GRANULAR_ASK,
|
|
4519
|
+
bypassPermissions: GRANULAR_NEVER
|
|
4520
|
+
};
|
|
4521
|
+
/**
|
|
4522
|
+
* The THIRD approval axis — *who reviews*, independent of the sandbox axis and
|
|
4523
|
+
* the ask axis above. Codex's `approvalsReviewer` (thread/start and turn/start,
|
|
4524
|
+
* present since 0.146.0) routes every approval request either to the user
|
|
4525
|
+
* (`'user'`, codex's own default) or to `'auto_review'`: a prompted subagent
|
|
4526
|
+
* that gathers context and applies a risk framework before allowing or denying.
|
|
4527
|
+
* That is codex's "Approve for me" preset, and our `auto` mode is exactly it.
|
|
4528
|
+
*
|
|
4529
|
+
* Sent explicitly for EVERY mode rather than omitted for the default — a thread
|
|
4530
|
+
* inherits `approvalsReviewer` across turns ("this turn and subsequent turns"),
|
|
4531
|
+
* so leaving it unset would let a stale reviewer from an earlier turn survive a
|
|
4532
|
+
* mode switch back to a user-reviewed mode. Stating it every time makes the
|
|
4533
|
+
* mode the single source of truth.
|
|
4534
|
+
*
|
|
4535
|
+
* NOTE the asymmetry with the Claude engine's `auto`: that classifier is
|
|
4536
|
+
* operator-configurable (`autoMode.environment`, allow/soft_deny/hard_deny);
|
|
4537
|
+
* this reviewer has no configuration surface at all.
|
|
4538
|
+
*/
|
|
4539
|
+
const APPROVALS_REVIEWER_BY_MODE = {
|
|
4540
|
+
default: "user",
|
|
4541
|
+
acceptEdits: "user",
|
|
4542
|
+
auto: "auto_review",
|
|
4543
|
+
bypassPermissions: "user"
|
|
4012
4544
|
};
|
|
4013
4545
|
/** Fallback timeout for a pending approval nobody answers — the SessionRunner
|
|
4014
4546
|
* default, so unattended codex sessions land the same way Claude ones do. */
|
|
@@ -4019,6 +4551,47 @@ const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
|
4019
4551
|
* host filesystem, an inline preview) off it.
|
|
4020
4552
|
*/
|
|
4021
4553
|
const CODEX_IMAGE_TOOL = "CodexImageGeneration";
|
|
4554
|
+
/**
|
|
4555
|
+
* Tool name for a spawned agent's anchor `tool_use` — the claude engine's
|
|
4556
|
+
* `Task` in this engine's vocabulary. Codex never sends such a call: the model's
|
|
4557
|
+
* `spawn_agent` surfaces only as the `subAgentActivity` marker item, so the
|
|
4558
|
+
* runner authors the call itself, because everything downstream is built on a
|
|
4559
|
+
* top-level `tool_use` existing — `terminalBlocks` absorbs a sidechain into the
|
|
4560
|
+
* call whose id its events carry as `parentToolUseId`, the takeover frames by
|
|
4561
|
+
* it, and `taskIdentity` labels it from the input's `subagent_type`. Not a new
|
|
4562
|
+
* wire idea, just a row: the same shape every other codex tool card uses.
|
|
4563
|
+
*/
|
|
4564
|
+
const CODEX_AGENT_TOOL = "CodexAgent";
|
|
4565
|
+
/** Tool name for the model's collab-agent calls (`wait`, `sendInput`, …), the
|
|
4566
|
+
* `tool` field carried in the input. One name for the whole open axis rather
|
|
4567
|
+
* than a name per verb, so a future verb renders instead of vanishing. */
|
|
4568
|
+
const CODEX_COLLAB_TOOL = "CodexCollab";
|
|
4569
|
+
/** An agent's name is its path's basename: '/root/date_one' → 'date_one'. */
|
|
4570
|
+
function agentName(agentPath) {
|
|
4571
|
+
if (typeof agentPath !== "string") return void 0;
|
|
4572
|
+
return agentPath.split("/").filter(Boolean).at(-1) || void 0;
|
|
4573
|
+
}
|
|
4574
|
+
/** The collab card's input: the verb always, the rich fields only when codex
|
|
4575
|
+
* actually filled them (measured against 0.146.0 they arrive empty — the card
|
|
4576
|
+
* must not render five null columns to say 'wait'). */
|
|
4577
|
+
function collabInput(item) {
|
|
4578
|
+
return {
|
|
4579
|
+
tool: item.tool,
|
|
4580
|
+
...item.receiverThreadIds?.length ? { receiverThreadIds: item.receiverThreadIds } : {},
|
|
4581
|
+
...item.prompt ? { prompt: item.prompt } : {},
|
|
4582
|
+
...item.model ? { model: item.model } : {}
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
/** A completed turn's answer, from its summary `items` page — the last
|
|
4586
|
+
* `agentMessage` text. For a sub-agent's thread this is the agent's report,
|
|
4587
|
+
* which is exactly what belongs in the anchor's `tool_result`. */
|
|
4588
|
+
function turnReport(turn) {
|
|
4589
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
4590
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
4591
|
+
const item = items[index];
|
|
4592
|
+
if (item?.type === "agentMessage" && typeof item.text === "string" && item.text) return item.text;
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4022
4595
|
/** Longest `result` worth putting in a tool card. The field is free-form and
|
|
4023
4596
|
* undocumented; anything past this is assumed to be an encoded image rather
|
|
4024
4597
|
* than a sentence, and encoded images do not go in the event log. */
|
|
@@ -4362,6 +4935,13 @@ var CodexRunner = class {
|
|
|
4362
4935
|
#events = [];
|
|
4363
4936
|
#subscribers = new SubscriberSet();
|
|
4364
4937
|
#seq = 0;
|
|
4938
|
+
/**
|
|
4939
|
+
* Latest context-window reading, retained from the last `context_usage` this
|
|
4940
|
+
* runner emitted so `GET /sessions` can answer it without an attach — see
|
|
4941
|
+
* `SessionInfo.contextUsage`. Folded in the emit path, so it is by
|
|
4942
|
+
* construction the same number the transcript last drew.
|
|
4943
|
+
*/
|
|
4944
|
+
#contextUsage;
|
|
4365
4945
|
#activityCount = 0;
|
|
4366
4946
|
#status = "starting";
|
|
4367
4947
|
#sdkSessionId;
|
|
@@ -4379,6 +4959,8 @@ var CodexRunner = class {
|
|
|
4379
4959
|
#turnChain = Promise.resolve();
|
|
4380
4960
|
#activeTurn;
|
|
4381
4961
|
#connection;
|
|
4962
|
+
/** Per-child, from `config/read`; undefined = read failed, send the bare shape. */
|
|
4963
|
+
#workspaceWrite;
|
|
4382
4964
|
#threadLoaded = false;
|
|
4383
4965
|
#numTurns = 0;
|
|
4384
4966
|
#totalCostUsd;
|
|
@@ -4417,6 +4999,12 @@ var CodexRunner = class {
|
|
|
4417
4999
|
* `mcpServerStatus/list` does not carry a status field at all, so without
|
|
4418
5000
|
* this every server would read as "configured" and never as up or down. */
|
|
4419
5001
|
#mcpStatus = /* @__PURE__ */ new Map();
|
|
5002
|
+
/** The spawned agents, keyed by their thread ids — the attribution table
|
|
5003
|
+
* behind `parentToolUseId` and the rollup behind `info().subagents`. Runner-
|
|
5004
|
+
* level, not per-turn: an agent's thread outlives the root turn that spawned
|
|
5005
|
+
* it, and only the child process dying (or the session closing) ends them
|
|
5006
|
+
* all — see the module doc in `subagents.ts`. */
|
|
5007
|
+
#agents = new CodexAgentTracker();
|
|
4420
5008
|
constructor(config, id = randomUUID()) {
|
|
4421
5009
|
const mode = config.permissionMode ?? "default";
|
|
4422
5010
|
if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
|
|
@@ -4468,13 +5056,15 @@ var CodexRunner = class {
|
|
|
4468
5056
|
createdAt: this.createdAt,
|
|
4469
5057
|
lastSeq: this.#seq,
|
|
4470
5058
|
activityCount: this.#activityCount,
|
|
5059
|
+
contextUsage: this.#contextUsage,
|
|
4471
5060
|
pendingPermissionCount: this.#approvals.size,
|
|
4472
5061
|
meta: this.#config.meta,
|
|
4473
5062
|
scope: this.#config.scope,
|
|
4474
5063
|
title: this.#title(),
|
|
4475
5064
|
totalCostUsd: this.#totalCostUsd,
|
|
4476
5065
|
numTurns: this.#numTurns || void 0,
|
|
4477
|
-
lastActivityAt: this.#lastActivityAt
|
|
5066
|
+
lastActivityAt: this.#lastActivityAt,
|
|
5067
|
+
subagents: this.#agents.list()
|
|
4478
5068
|
};
|
|
4479
5069
|
}
|
|
4480
5070
|
#title() {
|
|
@@ -4498,6 +5088,7 @@ var CodexRunner = class {
|
|
|
4498
5088
|
start() {
|
|
4499
5089
|
if (this.#started) return this.#turnChain;
|
|
4500
5090
|
this.#started = true;
|
|
5091
|
+
this.#warnUntrustedProject();
|
|
4501
5092
|
if (this.#config.resume && this.#config.backfillHistory !== false) {
|
|
4502
5093
|
this.#backfillPending = true;
|
|
4503
5094
|
this.#turnChain = this.#turnChain.then(() => this.#backfillHistory());
|
|
@@ -4507,6 +5098,37 @@ var CodexRunner = class {
|
|
|
4507
5098
|
return this.#turnChain;
|
|
4508
5099
|
}
|
|
4509
5100
|
/**
|
|
5101
|
+
* One-time transcript notice for the codex trust gap: a `default`-mode
|
|
5102
|
+
* session (read-only sandbox) on an untrusted cwd has its
|
|
5103
|
+
* `.codex/config.toml` — MCP servers included — silently ignored, and the
|
|
5104
|
+
* app-server surface has no trust prompt to say so (the TUI's prompt is
|
|
5105
|
+
* where the entry normally gets written). `acceptEdits`/`bypassPermissions`
|
|
5106
|
+
* sessions are exempt because their `thread/start` (workspace-write /
|
|
5107
|
+
* danger-full-access sandbox) writes the trust entry itself and loads the
|
|
5108
|
+
* config — measured against 0.146.0 and 0.149.0; a notice there would be
|
|
5109
|
+
* false. Emitted as `session_error`, which both clients render as an inline
|
|
5110
|
+
* notice while the session keeps running (the backfill-history precedent),
|
|
5111
|
+
* so nothing new rides the wire. Every degrade path is silence: a false
|
|
5112
|
+
* warning on a trusted project is worse than a missed one.
|
|
5113
|
+
*/
|
|
5114
|
+
#warnUntrustedProject() {
|
|
5115
|
+
if (this.#permissionMode !== "default") return;
|
|
5116
|
+
try {
|
|
5117
|
+
const env = this.#childEnv();
|
|
5118
|
+
const pin = env.CODEX_HOME;
|
|
5119
|
+
if (pin !== void 0 && pin.length === 0) return;
|
|
5120
|
+
const codexHome = pin ?? join(env.HOME ?? homedir(), ".codex");
|
|
5121
|
+
const message = untrustedProjectNotice({
|
|
5122
|
+
cwd: this.#cwd,
|
|
5123
|
+
codexHome
|
|
5124
|
+
});
|
|
5125
|
+
if (message) this.#emit({
|
|
5126
|
+
type: "session_error",
|
|
5127
|
+
message
|
|
5128
|
+
});
|
|
5129
|
+
} catch {}
|
|
5130
|
+
}
|
|
5131
|
+
/**
|
|
4510
5132
|
* List skills over a **throwaway** connection, for a session with nothing else
|
|
4511
5133
|
* to do yet.
|
|
4512
5134
|
*
|
|
@@ -4688,6 +5310,7 @@ var CodexRunner = class {
|
|
|
4688
5310
|
}, "policy");
|
|
4689
5311
|
this.#connection?.close();
|
|
4690
5312
|
this.#connection = void 0;
|
|
5313
|
+
this.#agents.sweep();
|
|
4691
5314
|
this.#activeTurn?.reject(/* @__PURE__ */ new Error("session closed"));
|
|
4692
5315
|
if (this.#imageDir) try {
|
|
4693
5316
|
rmSync(this.#imageDir, {
|
|
@@ -4714,6 +5337,48 @@ var CodexRunner = class {
|
|
|
4714
5337
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
4715
5338
|
}
|
|
4716
5339
|
/**
|
|
5340
|
+
* Read `[sandbox_workspace_write]` as codex resolves it for this session's
|
|
5341
|
+
* cwd, once per child, so {@link CodexRunner.#turnSandboxPolicy} can restate
|
|
5342
|
+
* it verbatim.
|
|
5343
|
+
*
|
|
5344
|
+
* Why this exists at all: `turn/start`'s object-form sandbox policy is
|
|
5345
|
+
* serde-defaulted field by field, so `{type: 'workspaceWrite'}` bare means
|
|
5346
|
+
* `networkAccess: false, writableRoots: []` NO MATTER what the operator
|
|
5347
|
+
* configured — and we must keep sending the object every turn, because
|
|
5348
|
+
* restating it is what makes a between-turns permission-mode switch take
|
|
5349
|
+
* effect. Measured against 0.149.0 with `network_access = true` set: the
|
|
5350
|
+
* bare object produced `curl: (6) Could not resolve host`, the fully-stated
|
|
5351
|
+
* object and an omitted policy both produced `200`. `read-only` is not
|
|
5352
|
+
* affected — the setting is scoped to workspace-write, as its name says, and
|
|
5353
|
+
* a read-only sandbox has no network either way.
|
|
5354
|
+
*
|
|
5355
|
+
* A failure here is not fatal: `#workspaceWrite` stays undefined and we send
|
|
5356
|
+
* the bare shape, which is exactly the behaviour that shipped before.
|
|
5357
|
+
*/
|
|
5358
|
+
async #readWorkspaceWrite(connection) {
|
|
5359
|
+
this.#workspaceWrite = void 0;
|
|
5360
|
+
try {
|
|
5361
|
+
const block = (await connection.request("config/read", { cwd: this.#cwd }))?.config?.sandbox_workspace_write;
|
|
5362
|
+
if (!block) return;
|
|
5363
|
+
const roots = block.writable_roots;
|
|
5364
|
+
this.#workspaceWrite = {
|
|
5365
|
+
writableRoots: Array.isArray(roots) ? roots.filter((r) => typeof r === "string") : [],
|
|
5366
|
+
networkAccess: block.network_access === true,
|
|
5367
|
+
excludeTmpdirEnvVar: block.exclude_tmpdir_env_var === true,
|
|
5368
|
+
excludeSlashTmp: block.exclude_slash_tmp === true
|
|
5369
|
+
};
|
|
5370
|
+
} catch {}
|
|
5371
|
+
}
|
|
5372
|
+
/** The mode's turn-level sandbox policy, with the operator's workspace-write settings intact. */
|
|
5373
|
+
#turnSandboxPolicy() {
|
|
5374
|
+
const policy = TURN_SANDBOX_BY_MODE[this.#permissionMode];
|
|
5375
|
+
if (policy?.type !== "workspaceWrite" || !this.#workspaceWrite) return policy;
|
|
5376
|
+
return {
|
|
5377
|
+
type: "workspaceWrite",
|
|
5378
|
+
...this.#workspaceWrite
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
/**
|
|
4717
5382
|
* The session's live connection with its thread loaded, (re)building both as
|
|
4718
5383
|
* needed: spawn + `initialize`/`initialized` on a fresh child, then
|
|
4719
5384
|
* `thread/start` (new) or `thread/resume` (a create-request `resume`, or a
|
|
@@ -4738,6 +5403,7 @@ var CodexRunner = class {
|
|
|
4738
5403
|
behavior: "deny",
|
|
4739
5404
|
message
|
|
4740
5405
|
}, "policy");
|
|
5406
|
+
this.#agents.sweep();
|
|
4741
5407
|
this.#activeTurn?.reject(new Error(message));
|
|
4742
5408
|
});
|
|
4743
5409
|
try {
|
|
@@ -4756,12 +5422,14 @@ var CodexRunner = class {
|
|
|
4756
5422
|
throw error;
|
|
4757
5423
|
}
|
|
4758
5424
|
connection.notify("initialized");
|
|
5425
|
+
await this.#readWorkspaceWrite(connection);
|
|
4759
5426
|
}
|
|
4760
5427
|
if (!this.#threadLoaded) {
|
|
4761
5428
|
const options = {
|
|
4762
5429
|
cwd: this.#cwd,
|
|
4763
5430
|
approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
|
|
4764
|
-
sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode]
|
|
5431
|
+
sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode],
|
|
5432
|
+
approvalsReviewer: APPROVALS_REVIEWER_BY_MODE[this.#permissionMode]
|
|
4765
5433
|
};
|
|
4766
5434
|
if (this.#model) options.model = this.#model;
|
|
4767
5435
|
const resuming = this.#sdkSessionId !== void 0;
|
|
@@ -5004,7 +5672,8 @@ var CodexRunner = class {
|
|
|
5004
5672
|
input: turn.input,
|
|
5005
5673
|
cwd: this.#cwd,
|
|
5006
5674
|
approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
|
|
5007
|
-
sandboxPolicy:
|
|
5675
|
+
sandboxPolicy: this.#turnSandboxPolicy(),
|
|
5676
|
+
approvalsReviewer: APPROVALS_REVIEWER_BY_MODE[this.#permissionMode]
|
|
5008
5677
|
};
|
|
5009
5678
|
const model = this.#model ?? this.#resolvedModel;
|
|
5010
5679
|
if (model) params.model = model;
|
|
@@ -5033,11 +5702,79 @@ var CodexRunner = class {
|
|
|
5033
5702
|
}
|
|
5034
5703
|
#handleNotification(method, params) {
|
|
5035
5704
|
if (this.#closed) return;
|
|
5705
|
+
if (THREAD_SCOPED_NOTIFICATIONS.has(method) && !this.#isRootThread(params)) {
|
|
5706
|
+
if (method === "turn/completed") this.#settleAgentTurn(params);
|
|
5707
|
+
else if (method === "turn/started") {
|
|
5708
|
+
const threadId = this.#threadIdOf(params);
|
|
5709
|
+
const record = threadId ? this.#agents.get(threadId) : void 0;
|
|
5710
|
+
if (record && record.status !== "running") this.#agents.revive(record);
|
|
5711
|
+
}
|
|
5712
|
+
return;
|
|
5713
|
+
}
|
|
5036
5714
|
this.#notifications[method]?.(params);
|
|
5037
5715
|
}
|
|
5716
|
+
/** Whether a notification is about the session's own thread. A notification
|
|
5717
|
+
* with no `threadId` counts as the root's: every thread-scoped method the
|
|
5718
|
+
* schema defines carries one, so an absent id means an older or narrower
|
|
5719
|
+
* shape, not a sub-agent. */
|
|
5720
|
+
#isRootThread(params) {
|
|
5721
|
+
const threadId = this.#threadIdOf(params);
|
|
5722
|
+
if (threadId === void 0) return true;
|
|
5723
|
+
return threadId === this.#sdkSessionId;
|
|
5724
|
+
}
|
|
5725
|
+
#threadIdOf(params) {
|
|
5726
|
+
const threadId = params?.threadId;
|
|
5727
|
+
return typeof threadId === "string" ? threadId : void 0;
|
|
5728
|
+
}
|
|
5729
|
+
/**
|
|
5730
|
+
* The agent behind a notification's `threadId` — the attribution every item
|
|
5731
|
+
* and delta handler asks before emitting, so two agents streaming
|
|
5732
|
+
* concurrently into this one connection come apart again by the id each
|
|
5733
|
+
* frame carries, never by any mutable "current agent".
|
|
5734
|
+
*
|
|
5735
|
+
* A non-root thread with no record still gets one: a thread emitting items on
|
|
5736
|
+
* this connection *is* an agent, whatever announced it (codex runs threads of
|
|
5737
|
+
* its own for review/compact, and a `subAgentActivity` could in principle be
|
|
5738
|
+
* missed) — the claude tracker's nested-event fallback, on a stronger signal.
|
|
5739
|
+
* The minted record is label-less and its anchor is authored here, because an
|
|
5740
|
+
* attributed event whose parent id matches no top-level `tool_use` would
|
|
5741
|
+
* render inline rather than as a frame; a late `started` edge fills the name
|
|
5742
|
+
* in. Root-thread traffic — and, defensively, the pre-thread shapes with no
|
|
5743
|
+
* id at all — stays unattributed (`undefined`).
|
|
5744
|
+
*/
|
|
5745
|
+
#agentFor(params) {
|
|
5746
|
+
const threadId = this.#threadIdOf(params);
|
|
5747
|
+
if (threadId === void 0 || threadId === this.#sdkSessionId) return void 0;
|
|
5748
|
+
const known = this.#agents.get(threadId);
|
|
5749
|
+
if (known) return known;
|
|
5750
|
+
const nonce = this.#activeTurn?.nonce ?? "codex";
|
|
5751
|
+
const record = this.#agents.open(threadId, `${nonce}:agent:${threadId}`, void 0, Date.now());
|
|
5752
|
+
record.anchored = true;
|
|
5753
|
+
this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, { agentThreadId: threadId });
|
|
5754
|
+
return record;
|
|
5755
|
+
}
|
|
5756
|
+
/**
|
|
5757
|
+
* A child thread's `turn/completed` is that AGENT's completion — the one
|
|
5758
|
+
* codex sends (`subAgentActivity` has no 'completed' kind, verified live).
|
|
5759
|
+
* The verdict is the turn's own status, and the report is the completed
|
|
5760
|
+
* turn's final message, delivered as the anchor's `tool_result` so the row
|
|
5761
|
+
* settles exactly the way a claude `Task`'s does. Deliberately not gated on
|
|
5762
|
+
* `#activeTurn`: an agent finishing between root turns still finished.
|
|
5763
|
+
*/
|
|
5764
|
+
#settleAgentTurn(params) {
|
|
5765
|
+
const threadId = this.#threadIdOf(params);
|
|
5766
|
+
const record = threadId ? this.#agents.get(threadId) : void 0;
|
|
5767
|
+
if (!record || record.status !== "running") return;
|
|
5768
|
+
const turn = params?.turn;
|
|
5769
|
+
const status = turn?.status === "completed" ? "done" : "failed";
|
|
5770
|
+
this.#agents.settle(record, status);
|
|
5771
|
+
const report = (turn ? turnReport(turn) : void 0) ?? turn?.error?.message ?? (status === "done" ? "" : turn?.status ?? "failed");
|
|
5772
|
+
this.#emitToolResult(record.toolUseId, report, status === "failed");
|
|
5773
|
+
}
|
|
5038
5774
|
/** Reasoning deltas arrive on two methods that differ only in which section
|
|
5039
5775
|
* counter they advance; the section key carries the method so the two streams
|
|
5040
|
-
* never share a boundary
|
|
5776
|
+
* never share a boundary (and item ids are per-thread, so two agents' streams
|
|
5777
|
+
* never share one either). Section boundaries (a new summary/content entry)
|
|
5041
5778
|
* render as paragraph breaks — the completed item joins sections with '\n\n'. */
|
|
5042
5779
|
#reasoningDelta(method) {
|
|
5043
5780
|
return (params) => {
|
|
@@ -5053,7 +5790,7 @@ var CodexRunner = class {
|
|
|
5053
5790
|
this.#emitDelta({
|
|
5054
5791
|
type: "thinking_delta",
|
|
5055
5792
|
thinking: separator + payload.delta
|
|
5056
|
-
});
|
|
5793
|
+
}, this.#agentFor(params)?.toolUseId ?? null);
|
|
5057
5794
|
};
|
|
5058
5795
|
}
|
|
5059
5796
|
/** One item-progress handler serves `item/started` and `item/updated`. */
|
|
@@ -5061,7 +5798,7 @@ var CodexRunner = class {
|
|
|
5061
5798
|
const active = this.#activeTurn;
|
|
5062
5799
|
if (!active) return;
|
|
5063
5800
|
const item = params?.item;
|
|
5064
|
-
if (item) this.#handleItemProgress(item, active);
|
|
5801
|
+
if (item) this.#handleItemProgress(item, active, this.#agentFor(params));
|
|
5065
5802
|
};
|
|
5066
5803
|
/** The notification dispatch table — every method the child emits that this
|
|
5067
5804
|
* runner maps, in one place. Handlers read `this.#activeTurn` themselves:
|
|
@@ -5079,7 +5816,9 @@ var CodexRunner = class {
|
|
|
5079
5816
|
"turn/completed": (params) => {
|
|
5080
5817
|
const active = this.#activeTurn;
|
|
5081
5818
|
const turn = params?.turn;
|
|
5082
|
-
if (active
|
|
5819
|
+
if (!active || !turn) return;
|
|
5820
|
+
if (active.turnId && turn.id && turn.id !== active.turnId) return;
|
|
5821
|
+
active.resolve(turn);
|
|
5083
5822
|
},
|
|
5084
5823
|
"item/started": this.#itemProgress,
|
|
5085
5824
|
"item/updated": this.#itemProgress,
|
|
@@ -5087,7 +5826,7 @@ var CodexRunner = class {
|
|
|
5087
5826
|
const active = this.#activeTurn;
|
|
5088
5827
|
if (!active) return;
|
|
5089
5828
|
const item = params?.item;
|
|
5090
|
-
if (item) this.#handleItemCompleted(item, active);
|
|
5829
|
+
if (item) this.#handleItemCompleted(item, active, this.#agentFor(params));
|
|
5091
5830
|
},
|
|
5092
5831
|
"item/agentMessage/delta": (params) => {
|
|
5093
5832
|
if (!this.#activeTurn) return;
|
|
@@ -5095,7 +5834,7 @@ var CodexRunner = class {
|
|
|
5095
5834
|
if (typeof delta === "string" && delta) this.#emitDelta({
|
|
5096
5835
|
type: "text_delta",
|
|
5097
5836
|
text: delta
|
|
5098
|
-
});
|
|
5837
|
+
}, this.#agentFor(params)?.toolUseId ?? null);
|
|
5099
5838
|
},
|
|
5100
5839
|
"item/reasoning/textDelta": this.#reasoningDelta("item/reasoning/textDelta"),
|
|
5101
5840
|
"item/reasoning/summaryTextDelta": this.#reasoningDelta("item/reasoning/summaryTextDelta"),
|
|
@@ -5288,30 +6027,40 @@ var CodexRunner = class {
|
|
|
5288
6027
|
if (!this.#closed && this.#approvals.size === 0 && this.#status === "awaiting_approval") this.#setStatus("running");
|
|
5289
6028
|
}
|
|
5290
6029
|
/** Tool calls surface as tool_use when they start; text and reasoning stream
|
|
5291
|
-
* natively via the delta notifications.
|
|
5292
|
-
|
|
6030
|
+
* natively via the delta notifications. `agent` is the sub-agent whose thread
|
|
6031
|
+
* the item arrived on — undefined for the session's own. */
|
|
6032
|
+
#handleItemProgress(item, active, agent) {
|
|
5293
6033
|
const id = `${active.nonce}:${item.id}`;
|
|
6034
|
+
if (item.type === "subAgentActivity") {
|
|
6035
|
+
this.#itemCompleted.subAgentActivity(item, active, id, agent);
|
|
6036
|
+
return;
|
|
6037
|
+
}
|
|
5294
6038
|
if (item.type === "commandExecution" && !active.toolUseEmitted.has(id)) {
|
|
5295
6039
|
active.toolUseEmitted.add(id);
|
|
5296
|
-
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
6040
|
+
this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
|
|
5297
6041
|
return;
|
|
5298
6042
|
}
|
|
5299
6043
|
if (item.type === "mcpToolCall" && !active.toolUseEmitted.has(id)) {
|
|
5300
6044
|
active.toolUseEmitted.add(id);
|
|
5301
|
-
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
6045
|
+
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
|
|
6046
|
+
return;
|
|
6047
|
+
}
|
|
6048
|
+
if (item.type === "collabAgentToolCall" && !active.toolUseEmitted.has(id)) {
|
|
6049
|
+
active.toolUseEmitted.add(id);
|
|
6050
|
+
this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
|
|
5302
6051
|
return;
|
|
5303
6052
|
}
|
|
5304
6053
|
if (item.type === "imageGeneration" && !active.toolUseEmitted.has(id)) {
|
|
5305
6054
|
active.toolUseEmitted.add(id);
|
|
5306
|
-
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
6055
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
|
|
5307
6056
|
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
5308
6057
|
}
|
|
5309
6058
|
}
|
|
5310
|
-
#handleItemCompleted(item, active) {
|
|
6059
|
+
#handleItemCompleted(item, active, agent) {
|
|
5311
6060
|
const id = `${active.nonce}:${item.id}`;
|
|
5312
6061
|
const handler = this.#itemCompleted[item.type];
|
|
5313
6062
|
if (handler) {
|
|
5314
|
-
handler(item, active, id);
|
|
6063
|
+
handler(item, active, id, agent);
|
|
5315
6064
|
return;
|
|
5316
6065
|
}
|
|
5317
6066
|
const unknown = item;
|
|
@@ -5332,67 +6081,121 @@ var CodexRunner = class {
|
|
|
5332
6081
|
* union has never heard of; those take the passthrough above.)
|
|
5333
6082
|
*/
|
|
5334
6083
|
#itemCompleted = {
|
|
5335
|
-
userMessage: () => {
|
|
5336
|
-
|
|
6084
|
+
userMessage: (item, active, _id, agent) => {
|
|
6085
|
+
if (!agent) return;
|
|
6086
|
+
const text = historyUserText(item);
|
|
6087
|
+
if (!text) return;
|
|
6088
|
+
this.#emit({
|
|
6089
|
+
type: "user_message",
|
|
6090
|
+
message: {
|
|
6091
|
+
role: "user",
|
|
6092
|
+
content: text
|
|
6093
|
+
},
|
|
6094
|
+
parentToolUseId: agent.toolUseId,
|
|
6095
|
+
uuid: `${active.nonce}:${item.id}`
|
|
6096
|
+
});
|
|
6097
|
+
},
|
|
6098
|
+
agentMessage: (item, active, id, agent) => {
|
|
5337
6099
|
const text = typeof item.text === "string" ? item.text : "";
|
|
5338
6100
|
this.#emitAssistant(id, [{
|
|
5339
6101
|
type: "text",
|
|
5340
6102
|
text
|
|
5341
|
-
}]);
|
|
5342
|
-
active.finalText = text;
|
|
6103
|
+
}], agent?.toolUseId ?? null);
|
|
6104
|
+
if (!agent) active.finalText = text;
|
|
5343
6105
|
},
|
|
5344
|
-
reasoning: (item, _active, id) => {
|
|
6106
|
+
reasoning: (item, _active, id, agent) => {
|
|
5345
6107
|
const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
|
|
5346
6108
|
const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
|
|
5347
6109
|
const thinking = (summary.length > 0 ? summary : content).join("\n\n");
|
|
5348
6110
|
if (thinking) this.#emitAssistant(id, [{
|
|
5349
6111
|
type: "thinking",
|
|
5350
6112
|
thinking
|
|
5351
|
-
}]);
|
|
6113
|
+
}], agent?.toolUseId ?? null);
|
|
5352
6114
|
},
|
|
5353
|
-
commandExecution: (item, active, id) => {
|
|
6115
|
+
commandExecution: (item, active, id, agent) => {
|
|
5354
6116
|
if (!active.toolUseEmitted.has(id)) {
|
|
5355
6117
|
active.toolUseEmitted.add(id);
|
|
5356
|
-
this.#emitToolUse(id, "CodexCommand", { command: item.command });
|
|
6118
|
+
this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
|
|
5357
6119
|
}
|
|
5358
6120
|
const exitCode = item.exitCode ?? void 0;
|
|
5359
6121
|
const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
|
|
5360
6122
|
const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
|
|
5361
|
-
this.#emitToolResult(id, output, failed);
|
|
6123
|
+
this.#emitToolResult(id, output, failed, void 0, agent?.toolUseId ?? null);
|
|
5362
6124
|
},
|
|
5363
|
-
fileChange: (item, _active, id) => {
|
|
5364
|
-
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
|
|
6125
|
+
fileChange: (item, _active, id, agent) => {
|
|
6126
|
+
this.#emitToolUse(id, "CodexFileChange", { changes: item.changes }, agent);
|
|
5365
6127
|
const lines = item.changes.map((change) => {
|
|
5366
6128
|
return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
|
|
5367
6129
|
});
|
|
5368
6130
|
const only = item.changes.length === 1 ? item.changes[0] : void 0;
|
|
5369
|
-
this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0);
|
|
6131
|
+
this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0, agent?.toolUseId ?? null);
|
|
5370
6132
|
},
|
|
5371
|
-
mcpToolCall: (item, active, id) => {
|
|
6133
|
+
mcpToolCall: (item, active, id, agent) => {
|
|
5372
6134
|
if (!active.toolUseEmitted.has(id)) {
|
|
5373
6135
|
active.toolUseEmitted.add(id);
|
|
5374
|
-
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
|
|
6136
|
+
this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
|
|
5375
6137
|
}
|
|
5376
6138
|
const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
|
|
5377
|
-
this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
|
|
6139
|
+
this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError, void 0, agent?.toolUseId ?? null);
|
|
5378
6140
|
},
|
|
5379
|
-
webSearch: (item, _active, id) => {
|
|
5380
|
-
this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
|
|
5381
|
-
this.#emitToolResult(id, "", false);
|
|
6141
|
+
webSearch: (item, _active, id, agent) => {
|
|
6142
|
+
this.#emitToolUse(id, "CodexWebSearch", { query: item.query }, agent);
|
|
6143
|
+
this.#emitToolResult(id, "", false, void 0, agent?.toolUseId ?? null);
|
|
5382
6144
|
},
|
|
5383
|
-
imageGeneration: (item, active, id) => {
|
|
6145
|
+
imageGeneration: (item, active, id, agent) => {
|
|
5384
6146
|
active.toolUseEmitted.add(id);
|
|
5385
|
-
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
|
|
6147
|
+
this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
|
|
5386
6148
|
if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
|
|
5387
6149
|
const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
|
|
5388
|
-
this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
|
|
6150
|
+
this.#emitToolResult(id, lines.join("\n"), item.status === "failed", void 0, agent?.toolUseId ?? null);
|
|
6151
|
+
},
|
|
6152
|
+
imageView: (item, _active, id, agent) => {
|
|
6153
|
+
this.#emitToolUse(id, "CodexImageView", { path: item.path }, agent);
|
|
6154
|
+
this.#emitToolResult(id, item.path, false, void 0, agent?.toolUseId ?? null);
|
|
6155
|
+
},
|
|
6156
|
+
subAgentActivity: (item, _active, id, agent) => {
|
|
6157
|
+
if (this.#replayingHistory) {
|
|
6158
|
+
if (item.kind !== "started") return;
|
|
6159
|
+
this.#emitToolUse(id, CODEX_AGENT_TOOL, {
|
|
6160
|
+
...agentName(item.agentPath) ? { subagent_type: agentName(item.agentPath) } : {},
|
|
6161
|
+
agentThreadId: item.agentThreadId,
|
|
6162
|
+
...item.agentPath ? { agentPath: item.agentPath } : {}
|
|
6163
|
+
}, agent);
|
|
6164
|
+
this.#emitToolResult(id, "(ran in its own thread — its work is not part of this thread's stored history)", false, void 0, agent?.toolUseId ?? null);
|
|
6165
|
+
return;
|
|
6166
|
+
}
|
|
6167
|
+
const record = this.#agents.get(item.agentThreadId) ?? this.#agents.open(item.agentThreadId, id, void 0, Date.now());
|
|
6168
|
+
const name = agentName(item.agentPath);
|
|
6169
|
+
const relabel = record.agentType === void 0 && name !== void 0;
|
|
6170
|
+
if (relabel) record.agentType = name;
|
|
6171
|
+
if (!record.anchored || relabel) {
|
|
6172
|
+
record.anchored = true;
|
|
6173
|
+
this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, {
|
|
6174
|
+
...record.agentType ? { subagent_type: record.agentType } : {},
|
|
6175
|
+
agentThreadId: item.agentThreadId,
|
|
6176
|
+
...item.agentPath ? { agentPath: item.agentPath } : {}
|
|
6177
|
+
}, agent);
|
|
6178
|
+
}
|
|
6179
|
+
if (item.kind === "interrupted") {
|
|
6180
|
+
if (record.status === "running") {
|
|
6181
|
+
this.#agents.settle(record, "failed");
|
|
6182
|
+
this.#emitToolResult(record.toolUseId, "interrupted", true);
|
|
6183
|
+
}
|
|
6184
|
+
return;
|
|
6185
|
+
}
|
|
6186
|
+
if (item.kind !== "started" && record.status !== "running") this.#agents.revive(record);
|
|
5389
6187
|
},
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
6188
|
+
collabAgentToolCall: (item, active, id, agent) => {
|
|
6189
|
+
if (!active.toolUseEmitted.has(id)) {
|
|
6190
|
+
active.toolUseEmitted.add(id);
|
|
6191
|
+
this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
|
|
6192
|
+
}
|
|
6193
|
+
if (item.status === "inProgress") return;
|
|
6194
|
+
const failed = item.status === "failed" || item.status === "declined";
|
|
6195
|
+
this.#emitToolResult(id, failed ? item.status : "", failed, void 0, agent?.toolUseId ?? null);
|
|
5393
6196
|
}
|
|
5394
6197
|
};
|
|
5395
|
-
#emitDelta(delta) {
|
|
6198
|
+
#emitDelta(delta, parent) {
|
|
5396
6199
|
if (this.#config.includePartialMessages === false) return;
|
|
5397
6200
|
this.#emit({
|
|
5398
6201
|
type: "stream_delta",
|
|
@@ -5400,11 +6203,11 @@ var CodexRunner = class {
|
|
|
5400
6203
|
type: "content_block_delta",
|
|
5401
6204
|
delta
|
|
5402
6205
|
},
|
|
5403
|
-
parentToolUseId:
|
|
6206
|
+
parentToolUseId: parent,
|
|
5404
6207
|
uuid: randomUUID()
|
|
5405
6208
|
});
|
|
5406
6209
|
}
|
|
5407
|
-
#emitAssistant(uuid, content) {
|
|
6210
|
+
#emitAssistant(uuid, content, parent) {
|
|
5408
6211
|
this.#emit({
|
|
5409
6212
|
type: "assistant_message",
|
|
5410
6213
|
message: {
|
|
@@ -5412,11 +6215,19 @@ var CodexRunner = class {
|
|
|
5412
6215
|
content,
|
|
5413
6216
|
model: this.#model ?? this.#resolvedModel
|
|
5414
6217
|
},
|
|
5415
|
-
parentToolUseId:
|
|
6218
|
+
parentToolUseId: parent,
|
|
5416
6219
|
uuid
|
|
5417
6220
|
});
|
|
5418
6221
|
}
|
|
5419
|
-
|
|
6222
|
+
/** `agent` (rather than a bare parent id) because a nested call is also the
|
|
6223
|
+
* agent's progress reading: `SubagentInfo.toolCount` ticks here, once per
|
|
6224
|
+
* card — the `counted` set is what keeps an upserted re-emission (the
|
|
6225
|
+
* finished imageGeneration input) from counting one picture twice. */
|
|
6226
|
+
#emitToolUse(id, name, input, agent) {
|
|
6227
|
+
if (agent && !agent.counted.has(id)) {
|
|
6228
|
+
agent.counted.add(id);
|
|
6229
|
+
agent.toolCount += 1;
|
|
6230
|
+
}
|
|
5420
6231
|
this.#emit({
|
|
5421
6232
|
type: "assistant_message",
|
|
5422
6233
|
message: {
|
|
@@ -5429,11 +6240,11 @@ var CodexRunner = class {
|
|
|
5429
6240
|
}],
|
|
5430
6241
|
model: this.#model ?? this.#resolvedModel
|
|
5431
6242
|
},
|
|
5432
|
-
parentToolUseId: null,
|
|
6243
|
+
parentToolUseId: agent?.toolUseId ?? null,
|
|
5433
6244
|
uuid: `${id}-use`
|
|
5434
6245
|
});
|
|
5435
6246
|
}
|
|
5436
|
-
#emitToolResult(toolUseId, content, isError, patch) {
|
|
6247
|
+
#emitToolResult(toolUseId, content, isError, patch, parent = null) {
|
|
5437
6248
|
this.#emit({
|
|
5438
6249
|
type: "user_message",
|
|
5439
6250
|
message: {
|
|
@@ -5445,7 +6256,7 @@ var CodexRunner = class {
|
|
|
5445
6256
|
is_error: isError || void 0
|
|
5446
6257
|
}]
|
|
5447
6258
|
},
|
|
5448
|
-
parentToolUseId:
|
|
6259
|
+
parentToolUseId: parent,
|
|
5449
6260
|
synthetic: true,
|
|
5450
6261
|
patch,
|
|
5451
6262
|
uuid: `${toolUseId}-result`
|
|
@@ -5573,6 +6384,8 @@ var CodexRunner = class {
|
|
|
5573
6384
|
};
|
|
5574
6385
|
this.#lastActivityAt = event.ts;
|
|
5575
6386
|
this.#activityCount += transcriptActivity(body);
|
|
6387
|
+
this.#contextUsage = contextReading(body) ?? this.#contextUsage;
|
|
6388
|
+
if (body.type === "conversation_reset") this.#contextUsage = void 0;
|
|
5576
6389
|
this.#events.push(event);
|
|
5577
6390
|
this.#subscribers.emit(event);
|
|
5578
6391
|
}
|
|
@@ -5594,7 +6407,15 @@ var CodexRunner = class {
|
|
|
5594
6407
|
* const c=JSON.parse(d.slice(s,i));
|
|
5595
6408
|
* for(const m of c.models) console.log(m.slug, m.display_name,
|
|
5596
6409
|
* m.visibility, m.supported_reasoning_levels.map(l=>l.effort).join(","))'\
|
|
5597
|
-
* "$(node -p 'require
|
|
6410
|
+
* "$(node -p 'const{createRequire}=require("module");
|
|
6411
|
+
* const w=require.resolve("@openai/codex/package.json");
|
|
6412
|
+
* createRequire(w).resolve("@openai/codex-darwin-arm64/package.json")
|
|
6413
|
+
* .replace("package.json","vendor/aarch64-apple-darwin/bin/codex")')"
|
|
6414
|
+
*
|
|
6415
|
+
* The two-hop resolve is NOT optional: under pnpm's strict layout the platform
|
|
6416
|
+
* package is a dependency of `@openai/codex`, so it resolves only from that
|
|
6417
|
+
* wrapper's location, never from the repo root. Resolving it directly throws
|
|
6418
|
+
* MODULE_NOT_FOUND — the same two hops `resolveBundledCodexExecutable` makes.
|
|
5598
6419
|
*
|
|
5599
6420
|
* Mapping decisions:
|
|
5600
6421
|
* - the internal `codex-auto-review` row is dropped (the codex analogue of
|
|
@@ -5606,7 +6427,7 @@ var CodexRunner = class {
|
|
|
5606
6427
|
* `max`/`ultra` beyond the SDK union; trust the binary, keep strings open.
|
|
5607
6428
|
*/
|
|
5608
6429
|
const CODEX_CATALOG = {
|
|
5609
|
-
provenance: "embedded model presets of @openai/codex@0.
|
|
6430
|
+
provenance: "embedded model presets of @openai/codex@0.149.0 (darwin-arm64 binary), extracted 2026-08-22",
|
|
5610
6431
|
models: [
|
|
5611
6432
|
{
|
|
5612
6433
|
value: "gpt-5.6-sol",
|