@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 +28 -28
- package/build/index.mjs +84 -16
- package/build/index.mjs.map +1 -1
- package/package.json +12 -12
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
|
|
11
|
-
permission system
|
|
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
|
|
28
|
-
`eval_script`
|
|
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
|
|
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**
|
|
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`
|
|
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**
|
|
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**
|
|
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**
|
|
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
|
|
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
|
|
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
|
|
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**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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 `""`
|
|
216
|
-
`" "`
|
|
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`
|
|
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
|
|
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\`
|
|
2928
|
-
if (trust === "authoritative" && !executes) throw new Error(`${kind} '${name}' is declared authoritative but has no \`execute\`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|