@workerdeck/core 2.7.1 → 2.8.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/README.md CHANGED
@@ -7,8 +7,8 @@ attach/replay; `AiSdkRunner` does the same for any provider the AI SDK supports.
7
7
  transport.
8
8
 
9
9
  Part of [WorkerDeck](https://github.com/workerdeck/workerdeck). A `SessionRunner`
10
- behaves like Claude Code launched in the session's directory — same skills, same `CLAUDE.md`, same
11
- permission system — and both runners emit
10
+ behaves like Claude Code launched in the session's directory - same skills, same `CLAUDE.md`, same
11
+ permission system - and both runners emit
12
12
  [`@workerdeck/protocol`](https://www.npmjs.com/package/@workerdeck/protocol) events.
13
13
  [`@workerdeck/server`](https://www.npmjs.com/package/@workerdeck/server) bridges runners to
14
14
  HTTP + WebSocket; use core directly when you want sessions in-process with no server.
@@ -24,8 +24,8 @@ Node ≥ 22 and a real filesystem. WorkerDeck implements no Anthropic auth: the
24
24
  credentials from the operator's environment (`ANTHROPIC_API_KEY`, Bedrock/Vertex, or a personal
25
25
  `claude login`).
26
26
 
27
- The provider engine additionally wants `ai` (AI SDK v7), your provider package, and — for
28
- `eval_script` — [`@workerdeck/sandbox`](https://www.npmjs.com/package/@workerdeck/sandbox);
27
+ The provider engine additionally wants `ai` (AI SDK v7), your provider package, and - for
28
+ `eval_script` - [`@workerdeck/sandbox`](https://www.npmjs.com/package/@workerdeck/sandbox);
29
29
  all optional, and unused if you only run Claude sessions.
30
30
 
31
31
  ## Usage
@@ -63,28 +63,28 @@ await done
63
63
  Other controls: `interrupt()`, `setPermissionMode(mode)`, `setModel(model?)`, `close(reason?)`,
64
64
  `fail(message)` for host-enforced policy, and `info()` for a protocol `SessionInfo` snapshot
65
65
  (status, cost, pending approval count, title). `runner.id` is the server-side id;
66
- `runner.sdkSessionId` is the Agent SDK's — the one you pass back as `resume`.
66
+ `runner.sdkSessionId` is the Agent SDK's - the one you pass back as `resume`.
67
67
 
68
68
  ## Approvals, event log, resume
69
69
 
70
- - **Pending approvals** — the runner's `canUseTool` hook turns each uncovered tool call into a
70
+ - **Pending approvals** - the runner's `canUseTool` hook turns each uncovered tool call into a
71
71
  `permission_requested` event and a `PendingApproval` that blocks the tool until
72
72
  `resolvePermission()` (or the timeout) settles it. Allowing echoes the tool input back as
73
- `updatedInput` — the SDK requires a record even for an unmodified allow. `AskUserQuestion`
73
+ `updatedInput` - the SDK requires a record even for an unmodified allow. `AskUserQuestion`
74
74
  rides the same path; `questionBehavior: 'auto' | 'deny'` policy-resolves it for unattended runs.
75
- - **Event log** — every event gets a monotonic `seq`; `subscribe(listener, afterSeq)` replays the
75
+ - **Event log** - every event gets a monotonic `seq`; `subscribe(listener, afterSeq)` replays the
76
76
  buffer past `afterSeq` before delivering live events, so late attachers always catch up.
77
- - **Resume** — pass `resume: sdkSessionId` (optionally `forkSession`). The SDK only re-streams
77
+ - **Resume** - pass `resume: sdkSessionId` (optionally `forkSession`). The SDK only re-streams
78
78
  user messages, so the runner backfills the full prior transcript from the SDK's on-disk store
79
79
  as `replay: true` events before the query starts (`backfillHistory: false` to skip).
80
- - **Capabilities + usage** — after init (and eagerly for promptless sessions) the runner fetches
80
+ - **Capabilities + usage** - after init (and eagerly for promptless sessions) the runner fetches
81
81
  supported models/slash commands and a context-window snapshot, emitting `capabilities` and
82
82
  `context_usage` events; context usage is re-polled after every turn.
83
83
 
84
84
  ## The second engine
85
85
 
86
86
  `AiSdkRunner` runs the same protocol against any provider the [AI SDK](https://ai-sdk.dev)
87
- supports — no CLI process, no config directory. `createEngineSession()` assembles one: the model,
87
+ supports - no CLI process, no config directory. `createEngineSession()` assembles one: the model,
88
88
  the capability-scoped tool set, and the executor that runs tool calls.
89
89
 
90
90
  ```ts
@@ -93,7 +93,7 @@ import { loadEngine } from '@workerdeck/sandbox'
93
93
  import { createEngineSession, QuickJsExecutor } from '@workerdeck/core'
94
94
 
95
95
  // Server-side, the WASM guest is loaded once for the process and shared by every
96
- // session. The variant package is a peer dependency you install yourself — core
96
+ // session. The variant package is a peer dependency you install yourself - core
97
97
  // does not pick one for you, because the browser build and the server build are
98
98
  // different artifacts and only you know which side this is.
99
99
  const executor = new QuickJsExecutor({ engine: await loadEngine(variant), defaultTimeoutMs: 15_000 })
@@ -118,7 +118,7 @@ Three seams matter here:
118
118
  with this process's authority and must declare `execute`; `sandboxed` means it rides the executor
119
119
  seam and must *not*. Both contradictions are refused at assembly rather than at runtime, because
120
120
  a sandboxed tool that quietly ran in-process would defeat the only thing sandboxing it was for.
121
- - **`ToolExecutor` decides where code runs**, and that is a real architectural choice — see below.
121
+ - **`ToolExecutor` decides where code runs**, and that is a real architectural choice - see below.
122
122
 
123
123
  ### Which executor?
124
124
 
@@ -127,14 +127,14 @@ Three seams matter here:
127
127
  | Runs where | this Node process, WASM guest | the attached client | wherever you send it |
128
128
  | Needs a client attached | no | **yes** | no |
129
129
  | Data locality | data must reach the server | client-held data never leaves the tab | n/a |
130
- | Trust | you own both sides | results are **untrusted input** — the sandboxed party answers | depends |
130
+ | Trust | you own both sides | results are **untrusted input** - the sandboxed party answers | depends |
131
131
  | Latency | in-process | a WS round trip | unbounded (the session parks) |
132
132
 
133
133
  The question to ask is **where the data the loop reasons over already lives**:
134
134
 
135
135
  - In your database or on your disk → in-process. Pushing execution into the tab buys nothing and
136
136
  hands an executor to the party you are sandboxing against.
137
- - In the user's browser — a document they are editing, a file they dropped, something you would
137
+ - In the user's browser - a document they are editing, a file they dropped, something you would
138
138
  rather not receive at all → the bridge. This is the case it exists for.
139
139
  - Somewhere that answers in minutes or hours (a queue, a human, a build) → deferred, and let the
140
140
  session park.
@@ -151,7 +151,7 @@ session mix all three.
151
151
 
152
152
  `DeferredExecutor` dispatches a call and doesn't wait. The runner then **parks**: `park()` returns
153
153
  a `RunnerSnapshot`, the process can tear the runner down, and passing that snapshot back as
154
- `restore` rebuilds the session as itself — same id, same event log, same seq numbering, mid-turn,
154
+ `restore` rebuilds the session as itself - same id, same event log, same seq numbering, mid-turn,
155
155
  scratch filesystem included.
156
156
 
157
157
  ```ts
@@ -162,12 +162,12 @@ selectExecutor: () => new DeferredExecutor({
162
162
  ```
163
163
 
164
164
  [`@workerdeck/server`](https://www.npmjs.com/package/@workerdeck/server) drives both halves
165
- for you — a `SessionStore` plus `POST /executions/:id/result` — but the mechanism is here, and works
165
+ for you - a `SessionStore` plus `POST /executions/:id/result` - but the mechanism is here, and works
166
166
  with no server at all.
167
167
 
168
168
  `snapshot()` is the same value **without** the teardown: the runner stays live, attached and warm.
169
169
  That separation is what makes a provider session survive a process restart, since it has no
170
- engine-side store to resume from the way claude and codex do — the host writes the snapshot through
170
+ engine-side store to resume from the way claude and codex do - the host writes the snapshot through
171
171
  after each turn and rebuilds from the last one. The gate differs from `park()`'s in one direction
172
172
  only: it refuses a turn in flight and pending *in-process* executions (whose results die with the
173
173
  process), and allows the idle case `park()` exists to refuse.
@@ -182,18 +182,18 @@ Things the compiler will not tell you, each of which has cost someone real time:
182
182
  the temptation to truncate into a snapshot: it would break the fetch for exactly the sessions
183
183
  most likely to be read late.
184
184
 
185
- - **Image refs happen there too — and on the live path as well.** `subscribe(..., { imageRefs })`
185
+ - **Image refs happen there too - and on the live path as well.** `subscribe(..., { imageRefs })`
186
186
  replaces a `tool_result`'s base64 `image` parts with `image_ref` addresses, and unlike truncation
187
187
  it applies to live events as well as the replay, because a client's one render path is
188
188
  ref-then-fetch. The same "never at emit" rule holds for the same reason: `#events` keeps every
189
189
  byte, which is what the fetch route serves back. `SubscriberSet` (`src/lib/subscribers.ts`) is
190
- where that per-subscriber decision lives — a subscriber is a listener *plus what it asked for*,
190
+ where that per-subscriber decision lives - a subscriber is a listener *plus what it asked for*,
191
191
  so the three runners no longer each own a copy of the answer. Consumers that subscribe with no
192
- options — parking, notifications, the queue — see everything, as they do for every rule here.
192
+ options - parking, notifications, the queue - see everything, as they do for every rule here.
193
193
 
194
194
  - **A declared MCP server that never connected is refused, not degraded.** If a profile's
195
195
  `session.mcpServers` names a server and it isn't there, `createEngineSession` throws. The old
196
- behaviour — start anyway, minus those tools — produced a session that reported perfectly healthy
196
+ behaviour - start anyway, minus those tools - produced a session that reported perfectly healthy
197
197
  while the agent apologised its way through every request that needed it. Pass
198
198
  `connectMcpTools(servers, { required: true })` to fail at connect time instead, and hand the
199
199
  resulting connection over as `mcp` (not just `mcp.tools`) so the check is exact.
@@ -201,7 +201,7 @@ Things the compiler will not tell you, each of which has cost someone real time:
201
201
  `GET` before it sends anything. Mounted under a framework's default 404, the whole connect fails
202
202
  with an error that names neither the method nor the route.
203
203
  - **Never seed the VFS by hand on a restore.** Use `seedVfs`, which is ignored when
204
- `config.restore` is set. Building `config.vfs` yourself still works and still wins — and then
204
+ `config.restore` is set. Building `config.vfs` yourself still works and still wins - and then
205
205
  overwriting the files the parked turn wrote is yours to avoid.
206
206
  - **Forward the host's `id`.** `createEngineSession({ id })` is how a session comes back as
207
207
  *itself* across a gateway restart. Dropping it strands every client's route and unread mark, and
@@ -209,11 +209,11 @@ Things the compiler will not tell you, each of which has cost someone real time:
209
209
  - **`onClose` runs on park as well as close.** Parking releases the same resources; a disposer that
210
210
  assumes the session is over will close an MCP connection the woken session still needs to rebuild.
211
211
  - **Authoritative tools are never bridged.** `withMcpTools` marks everything authoritative by
212
- construction. If you want a host tool the tab may run, declare it `sandboxed` in `tools` — and
212
+ construction. If you want a host tool the tab may run, declare it `sandboxed` in `tools` - and
213
213
  then treat its results as untrusted input, because the tab produced them.
214
214
  - **Never make a tool's operation depend on a field being absent.** "Create when `id` is missing,
215
- overwrite when it is present" is the shape that breaks: models send `""` — and, observed live,
216
- `" "` — rather than omitting, and some providers mark every property required so the model
215
+ overwrite when it is present" is the shape that breaks: models send `""` - and, observed live,
216
+ `" "` - rather than omitting, and some providers mark every property required so the model
217
217
  *cannot* omit. `z.string().min(1).optional()` does not save it (a space has length 1). Split it
218
218
  into two tools with required arguments, and trim-and-blank-check optional strings inside `run`.
219
219
 
@@ -222,9 +222,9 @@ Things the compiler will not tell you, each of which has cost someone real time:
222
222
  `InputQueue` (the push-based `AsyncIterable` bridging `sendMessage()` into the SDK's streaming
223
223
  prompt), `normalizeSdkMessage`/`toApiMessage` (SDKMessage → protocol event normalization),
224
224
  `connectMcpTools` for live MCP over http/sse, and `createWebFetch` with its SSRF guard
225
- (`isPrivateAddress`). Tests inject a fake `queryFn` — no real CLI spawn needed.
225
+ (`isPrivateAddress`). Tests inject a fake `queryFn` - no real CLI spawn needed.
226
226
 
227
227
  ## License
228
228
 
229
- MIT © Tobias Strebitzer —
229
+ MIT © Tobias Strebitzer -
230
230
  [LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE)
package/build/index.mjs CHANGED
@@ -1449,7 +1449,7 @@ var SessionRunner = class {
1449
1449
  request
1450
1450
  });
1451
1451
  if (mode === "deny") {
1452
- const message = "Interactive questions are disabled for this session — choose the most reasonable option yourself and continue.";
1452
+ const message = "Interactive questions are disabled for this session - choose the most reasonable option yourself and continue.";
1453
1453
  this.#emit({
1454
1454
  type: "permission_resolved",
1455
1455
  requestId: request.id,
@@ -2924,8 +2924,8 @@ function withHostTools(context, hostTools, kind = "host tool") {
2924
2924
  for (const [name, { tool: hostTool, trust, title }] of entries) {
2925
2925
  if (name in tools) throw new Error(`${kind} '${name}' collides with an existing tool of the same name`);
2926
2926
  const executes = typeof hostTool.execute === "function";
2927
- if (trust === "sandboxed" && executes) throw new Error(`${kind} '${name}' is declared sandboxed but has an \`execute\` — it would run in this process with full authority. Drop \`execute\` so it rides the ToolExecutor seam.`);
2928
- if (trust === "authoritative" && !executes) throw new Error(`${kind} '${name}' is declared authoritative but has no \`execute\` — nothing would ever answer its calls and the turn would stall.`);
2927
+ if (trust === "sandboxed" && executes) throw new Error(`${kind} '${name}' is declared sandboxed but has an \`execute\` - it would run in this process with full authority. Drop \`execute\` so it rides the ToolExecutor seam.`);
2928
+ if (trust === "authoritative" && !executes) throw new Error(`${kind} '${name}' is declared authoritative but has no \`execute\` - nothing would ever answer its calls and the turn would stall.`);
2929
2929
  definitions.push({
2930
2930
  name,
2931
2931
  trust,
@@ -3429,7 +3429,7 @@ const claudeAdapter = {
3429
3429
  if (status === "logged_in") return { available: true };
3430
3430
  if (status === "logged_out") return {
3431
3431
  available: false,
3432
- reason: `no usable Claude credentials for this profile's environment — log in under its config dir (CLAUDE_CONFIG_DIR=${profile.configDir ?? "~/.claude"} claude auth login) or set ANTHROPIC_API_KEY`
3432
+ reason: `no usable Claude credentials for this profile's environment - log in under its config dir (CLAUDE_CONFIG_DIR=${profile.configDir ?? "~/.claude"} claude auth login) or set ANTHROPIC_API_KEY`
3433
3433
  };
3434
3434
  return { available: "unknown" };
3435
3435
  },
@@ -3898,7 +3898,7 @@ function untrustedProjectNotice(options) {
3898
3898
  if (ignored.length === 0) return;
3899
3899
  const trustDir = gitRoot ?? cwd;
3900
3900
  const configs = ignored.map((layer) => join(layer, ".codex", "config.toml"));
3901
- 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}.`;
3901
+ 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}.`;
3902
3902
  }
3903
3903
  //#endregion
3904
3904
  //#region src/engines/codex/runner.ts
@@ -4193,6 +4193,10 @@ const APPROVAL_CHANNELS = {
4193
4193
  deny: (_raw, interrupt) => ({ response: { action: interrupt ? "cancel" : "decline" } })
4194
4194
  }
4195
4195
  };
4196
+ function steerUnsupported(error) {
4197
+ if (!(error instanceof JsonRpcError)) return false;
4198
+ return error.code === -32601 || error.code === -32600 && error.message.includes("unknown variant `turn/steer`");
4199
+ }
4196
4200
  function rateLimitWindowName(minutes) {
4197
4201
  if (typeof minutes !== "number" || !Number.isFinite(minutes) || minutes <= 0) return;
4198
4202
  if (minutes === 300) return "five_hour";
@@ -4237,6 +4241,8 @@ var CodexRunner = class {
4237
4241
  #mcpStatus = /* @__PURE__ */ new Map();
4238
4242
  #agents = new CodexAgentTracker();
4239
4243
  #clearedThreads = /* @__PURE__ */ new Set();
4244
+ #cannotSteer = /* @__PURE__ */ new WeakSet();
4245
+ #clearsPending = 0;
4240
4246
  constructor(config, id = randomUUID()) {
4241
4247
  const mode = config.permissionMode ?? "default";
4242
4248
  if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
@@ -4372,9 +4378,52 @@ var CodexRunner = class {
4372
4378
  });
4373
4379
  if (this.#backfillPending) this.#turnChain = this.#turnChain.then(echo);
4374
4380
  else echo();
4381
+ this.#dispatch(input);
4382
+ }
4383
+ #dispatch(input) {
4384
+ const active = this.#activeTurn;
4385
+ if (!active || !this.#steerable(active)) {
4386
+ this.#enqueueTurn(input);
4387
+ return;
4388
+ }
4389
+ active.steerChain = active.steerChain.then(async () => {
4390
+ await active.steerGate;
4391
+ await this.#steer(active, input);
4392
+ });
4393
+ }
4394
+ #steerable(active) {
4395
+ if (active.settled || active.interrupted || this.#clearsPending > 0) return false;
4396
+ return !this.#connection || !this.#cannotSteer.has(this.#connection);
4397
+ }
4398
+ async #steer(active, input) {
4399
+ const connection = this.#connection;
4400
+ const threadId = this.#sdkSessionId;
4401
+ const turnId = active.turnId;
4402
+ if (!connection || !threadId || !turnId || active.settled || active.interrupted || this.#cannotSteer.has(connection)) {
4403
+ this.#enqueueTurn(input);
4404
+ return;
4405
+ }
4406
+ try {
4407
+ await connection.request("turn/steer", {
4408
+ threadId,
4409
+ expectedTurnId: turnId,
4410
+ input
4411
+ });
4412
+ } catch (error) {
4413
+ if (steerUnsupported(error)) this.#cannotSteer.add(connection);
4414
+ this.#enqueueTurn(input);
4415
+ }
4416
+ }
4417
+ #enqueueTurn(input) {
4418
+ if (this.#closed) return;
4375
4419
  this.#queue.push({ input });
4376
4420
  this.#scheduleTurn();
4377
4421
  }
4422
+ #adoptTurnId(active, turnId) {
4423
+ if (active.turnId || typeof turnId !== "string" || !turnId) return;
4424
+ active.turnId = turnId;
4425
+ active.openSteerGate();
4426
+ }
4378
4427
  #buildInput(text, attachments) {
4379
4428
  const parts = [];
4380
4429
  for (const attachment of attachments) {
@@ -4445,7 +4494,10 @@ var CodexRunner = class {
4445
4494
  }
4446
4495
  async clearContext() {
4447
4496
  if (this.#closed) throw new Error("session is closed");
4448
- const run = this.#turnChain.then(() => this.#clearNow());
4497
+ this.#clearsPending += 1;
4498
+ const run = this.#turnChain.then(() => this.#clearNow()).finally(() => {
4499
+ this.#clearsPending -= 1;
4500
+ });
4449
4501
  this.#turnChain = run.then(() => void 0, () => void 0);
4450
4502
  await run;
4451
4503
  }
@@ -4600,7 +4652,7 @@ var CodexRunner = class {
4600
4652
  } catch (error) {
4601
4653
  connection.close();
4602
4654
  if (this.#connection === connection) this.#connection = void 0;
4603
- if (error instanceof JsonRpcError) throw new Error("codex app-server rejected initialize (capabilities.experimentalApi: true — required for the granular approval policy, and WorkerDeck has no non-experimental fallback): " + error.message, { cause: error });
4655
+ if (error instanceof JsonRpcError) throw new Error("codex app-server rejected initialize (capabilities.experimentalApi: true is required for the granular approval policy, and WorkerDeck has no non-experimental fallback): " + error.message, { cause: error });
4604
4656
  throw error;
4605
4657
  }
4606
4658
  connection.notify("initialized");
@@ -4709,7 +4761,7 @@ var CodexRunner = class {
4709
4761
  }
4710
4762
  if (partialReason) this.#emit({
4711
4763
  type: "session_error",
4712
- message: `Resumed thread history is incomplete — older turns could not be loaded (${partialReason})`
4764
+ message: `Resumed thread history is incomplete: older turns could not be loaded (${partialReason})`
4713
4765
  });
4714
4766
  this.#replayTurns(turns);
4715
4767
  } catch {} finally {
@@ -4746,6 +4798,10 @@ var CodexRunner = class {
4746
4798
  }
4747
4799
  }
4748
4800
  #newTurnState() {
4801
+ let openSteerGate;
4802
+ const steerGate = new Promise((resolve) => {
4803
+ openSteerGate = resolve;
4804
+ });
4749
4805
  return {
4750
4806
  nonce: randomUUID(),
4751
4807
  interrupted: false,
@@ -4761,6 +4817,9 @@ var CodexRunner = class {
4761
4817
  toolUseEmitted: /* @__PURE__ */ new Set(),
4762
4818
  sectionIndex: /* @__PURE__ */ new Map(),
4763
4819
  settled: false,
4820
+ steerGate,
4821
+ openSteerGate,
4822
+ steerChain: Promise.resolve(),
4764
4823
  resolve: () => {},
4765
4824
  reject: () => {}
4766
4825
  };
@@ -4776,11 +4835,13 @@ var CodexRunner = class {
4776
4835
  active.resolve = (turnResult) => {
4777
4836
  if (active.settled) return;
4778
4837
  active.settled = true;
4838
+ active.openSteerGate();
4779
4839
  resolve(turnResult);
4780
4840
  };
4781
4841
  active.reject = (error) => {
4782
4842
  if (active.settled) return;
4783
4843
  active.settled = true;
4844
+ active.openSteerGate();
4784
4845
  reject(error);
4785
4846
  };
4786
4847
  });
@@ -4802,7 +4863,7 @@ var CodexRunner = class {
4802
4863
  connection.request("turn/start", params).then((result) => {
4803
4864
  const started = result?.turn;
4804
4865
  if (!started) return;
4805
- active.turnId ??= started.id;
4866
+ this.#adoptTurnId(active, started.id);
4806
4867
  if (started.status && started.status !== "inProgress") active.resolve(started);
4807
4868
  }, (error) => active.reject(error instanceof Error ? error : new Error(String(error))));
4808
4869
  const result = await outcome;
@@ -4896,7 +4957,7 @@ var CodexRunner = class {
4896
4957
  "turn/started": (params) => {
4897
4958
  const active = this.#activeTurn;
4898
4959
  const turn = params?.turn;
4899
- if (active && turn && !active.turnId) active.turnId = turn.id;
4960
+ if (active && turn) this.#adoptTurnId(active, turn.id);
4900
4961
  },
4901
4962
  "turn/completed": (params) => {
4902
4963
  const active = this.#activeTurn;
@@ -5039,7 +5100,7 @@ var CodexRunner = class {
5039
5100
  requestId: request.id,
5040
5101
  behavior: "deny",
5041
5102
  resolvedBy: "policy",
5042
- message: "Interactive questions are disabled for this session — choose the most reasonable option yourself and continue."
5103
+ message: "Interactive questions are disabled for this session: choose the most reasonable option yourself and continue."
5043
5104
  });
5044
5105
  return { answers: {} };
5045
5106
  }
@@ -5068,7 +5129,7 @@ var CodexRunner = class {
5068
5129
  else {
5069
5130
  behavior = "deny";
5070
5131
  resolvedBy = "policy";
5071
- message = "codex offered no plain accept for this request (only broader session/policy grants) — denied instead";
5132
+ message = "codex offered no plain accept for this request (only broader session/policy grants), denied instead";
5072
5133
  sent = pending.channel.deny(pending.params, false, pending.offered);
5073
5134
  }
5074
5135
  } else sent = pending.channel.deny(pending.params, decision.interrupt === true, pending.offered);
@@ -5224,7 +5285,7 @@ var CodexRunner = class {
5224
5285
  agentThreadId: item.agentThreadId,
5225
5286
  ...item.agentPath ? { agentPath: item.agentPath } : {}
5226
5287
  }, agent);
5227
- 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);
5288
+ this.#emitToolResult(id, "(ran in its own thread, so its work is not part of this thread's stored history)", false, void 0, agent?.toolUseId ?? null);
5228
5289
  return;
5229
5290
  }
5230
5291
  const record = this.#agents.get(item.agentThreadId) ?? this.#agents.open(item.agentThreadId, id, void 0, Date.now());
@@ -5246,6 +5307,13 @@ var CodexRunner = class {
5246
5307
  }
5247
5308
  return;
5248
5309
  }
5310
+ if (item.kind === "completed") {
5311
+ if (record.status === "running") {
5312
+ this.#agents.settle(record, "done");
5313
+ this.#emitToolResult(record.toolUseId, "", false);
5314
+ }
5315
+ return;
5316
+ }
5249
5317
  if (item.kind !== "started" && record.status !== "running") this.#agents.revive(record);
5250
5318
  },
5251
5319
  collabAgentToolCall: (item, active, id, agent) => {
@@ -5546,7 +5614,7 @@ function connectAppServer(options) {
5546
5614
  }
5547
5615
  //#endregion
5548
5616
  //#region src/engines/codex/adapter.ts
5549
- const NOT_INSTALLED = "@openai/codex is not installed — add it (an optional peer of @workerdeck/core) to run codex profiles";
5617
+ const NOT_INSTALLED = "@openai/codex is not installed - add it (an optional peer of @workerdeck/core) to run codex profiles";
5550
5618
  function resolveBundledCodexExecutable() {
5551
5619
  const triple = targetTriple();
5552
5620
  if (!triple) return;
@@ -5582,10 +5650,10 @@ async function checkCodexAvailability(profile, env, options = {}) {
5582
5650
  return;
5583
5651
  }
5584
5652
  if (`${stdout}\n${stderr}`.includes("Not logged in")) {
5585
- const hint = childEnv.CODEX_API_KEY ? " CODEX_API_KEY is read only by `codex exec`, never by the app-server — run `codex login --with-api-key` under this profile’s CODEX_HOME to persist it." : childEnv.OPENAI_API_KEY ? " OPENAI_API_KEY is not used by codex — run `codex login --with-api-key` under this profile’s CODEX_HOME." : "";
5653
+ const hint = childEnv.CODEX_API_KEY ? " CODEX_API_KEY is read only by `codex exec`, never by the app-server - run `codex login --with-api-key` under this profile’s CODEX_HOME to persist it." : childEnv.OPENAI_API_KEY ? " OPENAI_API_KEY is not used by codex - run `codex login --with-api-key` under this profile’s CODEX_HOME." : "";
5586
5654
  resolve({
5587
5655
  available: false,
5588
- reason: `codex is not logged in for this profile's environment — run \`codex login\`` + (profile.codexHome ? ` with CODEX_HOME=${profile.codexHome}` : "") + `.${hint}`
5656
+ reason: `codex is not logged in for this profile's environment - run \`codex login\`` + (profile.codexHome ? ` with CODEX_HOME=${profile.codexHome}` : "") + `.${hint}`
5589
5657
  });
5590
5658
  return;
5591
5659
  }