super-dsh 0.1.9 → 0.1.10
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 +25 -3
- package/agent-agy/cordis.patch.yml +7 -4
- package/agent-agy/dist/agent.d.ts +12 -11
- package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
- package/agent-agy/dist/agy-auth-gateway.js +233 -0
- package/agent-agy/dist/agy-auth.d.ts +153 -0
- package/agent-agy/dist/agy-auth.js +179 -0
- package/agent-agy/dist/agy-cli-client.d.ts +39 -9
- package/agent-agy/dist/agy-cli-client.js +82 -18
- package/agent-agy/dist/agy-store.d.ts +8 -0
- package/agent-agy/dist/index.d.ts +46 -16
- package/agent-agy/dist/index.js +82 -56
- package/agent-agy/dist/models.d.ts +70 -0
- package/agent-agy/dist/models.js +102 -3
- package/agent-agy/package.json +7 -5
- package/agent-agy-sdk/cordis.patch.yml +58 -0
- package/agent-agy-sdk/dist/adapter.d.ts +15 -0
- package/agent-agy-sdk/dist/adapter.js +42 -0
- package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
- package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
- package/agent-agy-sdk/dist/agent.d.ts +138 -0
- package/agent-agy-sdk/dist/agent.js +494 -0
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
- package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
- package/agent-agy-sdk/dist/agy-sessions.js +107 -0
- package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
- package/agent-agy-sdk/dist/agy-store.js +83 -0
- package/agent-agy-sdk/dist/inbox.d.ts +26 -0
- package/agent-agy-sdk/dist/inbox.js +53 -0
- package/agent-agy-sdk/dist/index.d.ts +93 -0
- package/agent-agy-sdk/dist/index.js +476 -0
- package/agent-agy-sdk/dist/models.d.ts +41 -0
- package/agent-agy-sdk/dist/models.js +74 -0
- package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
- package/agent-agy-sdk/dist/sdk-venv.js +173 -0
- package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-agy-sdk/dist/world-plugin.js +82 -0
- package/agent-agy-sdk/package.json +64 -0
- package/agent-codex/README.md +141 -36
- package/agent-codex/cordis.patch.yml +14 -10
- package/agent-codex/dist/adapter.js +64 -28
- package/agent-codex/dist/agent.js +255 -81
- package/agent-codex/dist/app-server-approvals.js +165 -0
- package/agent-codex/dist/app-server-client.js +723 -0
- package/agent-codex/dist/app-server-events.js +432 -0
- package/agent-codex/dist/app-server-protocol.js +95 -0
- package/agent-codex/dist/app-server-wire.js +299 -0
- package/agent-codex/dist/codex-launch.js +78 -0
- package/agent-codex/dist/codex-native-config.js +392 -0
- package/agent-codex/dist/codex-provider-gateway.js +402 -0
- package/agent-codex/dist/codex-providers.js +376 -0
- package/agent-codex/dist/codex-route.js +86 -0
- package/agent-codex/dist/index.js +256 -115
- package/agent-codex/dist/permission.js +75 -1
- package/agent-codex/package.json +1 -2
- package/agent-codex-sdk/README.md +79 -0
- package/agent-codex-sdk/cordis.patch.yml +81 -0
- package/agent-codex-sdk/dist/adapter.js +91 -0
- package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
- package/agent-codex-sdk/dist/agent.js +1085 -0
- package/agent-codex-sdk/dist/codex-native-config.js +392 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
- package/agent-codex-sdk/dist/codex-providers.js +376 -0
- package/agent-codex-sdk/dist/codex-route.js +86 -0
- package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
- package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
- package/agent-codex-sdk/dist/codex-store.js +180 -0
- package/agent-codex-sdk/dist/inbox.js +53 -0
- package/agent-codex-sdk/dist/index.js +739 -0
- package/agent-codex-sdk/dist/permission.js +71 -0
- package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
- package/agent-codex-sdk/dist/session-map.js +167 -0
- package/agent-codex-sdk/dist/world-plugin.js +93 -0
- package/agent-codex-sdk/package.json +79 -0
- package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
- package/agent-omp/README.md +72 -55
- package/agent-omp/dist/agent.js +36 -11
- package/agent-omp/dist/index.js +92 -52
- package/agent-omp/dist/lazy-rpc.js +11 -4
- package/agent-omp/dist/omp-cli.js +114 -97
- package/agent-omp/dist/omp-store.js +12 -4
- package/agent-omp/dist/permission.js +70 -5
- package/agent-omp/dist/rpc-client.js +409 -0
- package/agent-omp/dist/world-plugin.js +36 -3
- package/agent-omp/package.json +2 -6
- package/agent-omp-sdk/README.md +103 -0
- package/agent-omp-sdk/cordis.patch.yml +108 -0
- package/agent-omp-sdk/dist/adapter.d.ts +21 -0
- package/agent-omp-sdk/dist/adapter.js +36 -0
- package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
- package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
- package/agent-omp-sdk/dist/agent.d.ts +64 -0
- package/agent-omp-sdk/dist/agent.js +1178 -0
- package/agent-omp-sdk/dist/discovery.d.ts +57 -0
- package/agent-omp-sdk/dist/discovery.js +116 -0
- package/agent-omp-sdk/dist/inbox.d.ts +28 -0
- package/agent-omp-sdk/dist/inbox.js +53 -0
- package/agent-omp-sdk/dist/index.d.ts +60 -0
- package/agent-omp-sdk/dist/index.js +632 -0
- package/agent-omp-sdk/dist/knobs.d.ts +34 -0
- package/agent-omp-sdk/dist/knobs.js +50 -0
- package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
- package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
- package/agent-omp-sdk/dist/mobile-boot.js +38 -0
- package/agent-omp-sdk/dist/models.d.ts +39 -0
- package/agent-omp-sdk/dist/models.js +299 -0
- package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
- package/agent-omp-sdk/dist/omp-cli.js +164 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
- package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
- package/agent-omp-sdk/dist/omp-executable.js +30 -0
- package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
- package/agent-omp-sdk/dist/omp-store.js +526 -0
- package/agent-omp-sdk/dist/pairing.d.ts +7 -0
- package/agent-omp-sdk/dist/pairing.js +45 -0
- package/agent-omp-sdk/dist/permission.d.ts +138 -0
- package/agent-omp-sdk/dist/permission.js +161 -0
- package/agent-omp-sdk/dist/protocol.d.ts +189 -0
- package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
- package/agent-omp-sdk/dist/rpc-types.js +1 -0
- package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
- package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
- package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
- package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
- package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
- package/agent-omp-sdk/dist/sidecar-client.js +197 -0
- package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
- package/agent-omp-sdk/dist/store/adapter.js +47 -0
- package/agent-omp-sdk/dist/store/db.d.ts +70 -0
- package/agent-omp-sdk/dist/store/db.js +210 -0
- package/agent-omp-sdk/dist/store/index.d.ts +23 -0
- package/agent-omp-sdk/dist/store/index.js +59 -0
- package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
- package/agent-omp-sdk/dist/store/reconcile.js +51 -0
- package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
- package/agent-omp-sdk/dist/store/schema.js +40 -0
- package/agent-omp-sdk/dist/transport.d.ts +49 -0
- package/agent-omp-sdk/dist/transport.js +35 -0
- package/agent-omp-sdk/dist/world-home.d.ts +4 -0
- package/agent-omp-sdk/dist/world-home.js +23 -0
- package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-omp-sdk/dist/world-plugin.js +137 -0
- package/agent-omp-sdk/package.json +87 -0
- package/client/index.js +1115 -93
- package/cordis.patch.yml +64 -0
- package/package.json +17 -7
- package/superd-hub/dist/client/agent-settings.d.ts +156 -0
- package/superd-hub/dist/client/agent-settings.js +229 -0
- package/superd-hub/dist/client/roster.js +6 -0
- package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
- package/superd-hub/dist/client/runtime-settings-api.js +137 -0
- package/superd-hub/dist/client/switch.d.ts +165 -0
- package/superd-hub/dist/client/switch.js +295 -0
- package/superd-hub/lib/client/index.js +1115 -93
- package/agent-codex/dist/models.js +0 -152
- package/agent-omp/dist/sidecar-client.js +0 -195
- /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
- /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
- /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
- /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
- /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodexAppServerClient — the app-server-line client seam (2026-10-03).
|
|
3
|
+
*
|
|
4
|
+
* One DSH session ↔ one `codex app-server` child process ↔ one Codex thread.
|
|
5
|
+
* The public surface deliberately mirrors the retired `CodexSdkClient`
|
|
6
|
+
* (`on` / `onFailure` / `getState` / `getMessages` / `getSessionStats` /
|
|
7
|
+
* `getSubagents` / `prompt` / `followUp` / `steer` / `setModel` / `abort` /
|
|
8
|
+
* `newSession` / `close` / `ensureStarted` / `spawned` / `threadId`) so
|
|
9
|
+
* `agent.ts`'s event switch is untouched, while three of those methods now do
|
|
10
|
+
* the REAL thing instead of an approximation:
|
|
11
|
+
*
|
|
12
|
+
* steer → `turn/steer` (injects into the live turn) — the SDK line had
|
|
13
|
+
* no steer at all and queued a follow-up instead;
|
|
14
|
+
* abort → `turn/interrupt`;
|
|
15
|
+
* setModel → per-turn `model` override — live from the NEXT turn, no thread
|
|
16
|
+
* rebuild (the SDK line had to `resumeThread` with a new option).
|
|
17
|
+
*
|
|
18
|
+
* Plus two app-server-only inputs the agent feeds per turn:
|
|
19
|
+
* `setTurnPolicy({approvalPolicy, sandboxPolicy})` — the runtime permission
|
|
20
|
+
* switch (folded from the DSH session log by `permission.ts`);
|
|
21
|
+
* `setServerRequestHandler(handler)` — the approval bridge
|
|
22
|
+
* (`app-server-approvals.ts`).
|
|
23
|
+
*
|
|
24
|
+
* LAZY SPAWN (agent-adapter-dev-rules §14-i, hard gate): `create()` performs
|
|
25
|
+
* NO I/O and spawns NOTHING. `ensureStarted()` is the only path that creates a
|
|
26
|
+
* process, and the agent calls it from the first real prompt. Viewing,
|
|
27
|
+
* creating, or resuming a session therefore costs zero native processes.
|
|
28
|
+
*
|
|
29
|
+
* Native home (rules §5): the child inherits `process.env` verbatim. This
|
|
30
|
+
* client NEVER sets `CODEX_HOME` — tests redirect the runtime home through the
|
|
31
|
+
* ambient env, exactly like the retired SDK line did.
|
|
32
|
+
*
|
|
33
|
+
* Wire events are projected by `projectAppServerNotification` (app-server-events.ts)
|
|
34
|
+
* onto the same omp-derived vocabulary the SDK line used, so every consumer
|
|
35
|
+
* downstream sees an unchanged shape.
|
|
36
|
+
*/
|
|
37
|
+
import { CodexAppServerTransport } from "./app-server-wire.js";
|
|
38
|
+
import { NATIVE_LAUNCH } from "./codex-launch.js";
|
|
39
|
+
import { projectAppServerNotification } from "./app-server-events.js";
|
|
40
|
+
import { failClosedResponse } from "./app-server-approvals.js";
|
|
41
|
+
let transportFactory = (options) => new CodexAppServerTransport(options);
|
|
42
|
+
/** Replace the transport constructor (tests inject a fake). */
|
|
43
|
+
export function setAppServerTransportFactory(factory) {
|
|
44
|
+
transportFactory = factory;
|
|
45
|
+
}
|
|
46
|
+
const ok = (command) => ({ type: "response", command, success: true });
|
|
47
|
+
function asRecord(value) {
|
|
48
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
49
|
+
return null;
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function asString(value) {
|
|
53
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** One text input block in the v2 `UserInput` shape. */
|
|
56
|
+
function textInput(text) {
|
|
57
|
+
return { type: "text", text, text_elements: [] };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Convert the app-server's `TokenUsageBreakdown` into the codex snake_case
|
|
61
|
+
* usage shape `agent.ts`'s `convertUsage` already consumes verbatim, so the
|
|
62
|
+
* Dash-side fold is unchanged by this migration.
|
|
63
|
+
*/
|
|
64
|
+
export function convertTokenUsage(value) {
|
|
65
|
+
const record = asRecord(value);
|
|
66
|
+
if (record === null)
|
|
67
|
+
return null;
|
|
68
|
+
const out = {};
|
|
69
|
+
const map = [
|
|
70
|
+
["inputTokens", "input_tokens"],
|
|
71
|
+
["outputTokens", "output_tokens"],
|
|
72
|
+
["cachedInputTokens", "cached_input_tokens"],
|
|
73
|
+
["cacheWriteInputTokens", "cache_write_input_tokens"],
|
|
74
|
+
["reasoningOutputTokens", "reasoning_output_tokens"],
|
|
75
|
+
];
|
|
76
|
+
for (const [from, to] of map) {
|
|
77
|
+
const raw = record[from];
|
|
78
|
+
if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 0)
|
|
79
|
+
out[to] = raw;
|
|
80
|
+
}
|
|
81
|
+
return Object.keys(out).length === 0 ? null : out;
|
|
82
|
+
}
|
|
83
|
+
/** A single-shot deferred used for turn-id/`turn/completed` rendezvous. */
|
|
84
|
+
function deferred() {
|
|
85
|
+
let resolve = () => { };
|
|
86
|
+
const promise = new Promise((res) => {
|
|
87
|
+
resolve = res;
|
|
88
|
+
});
|
|
89
|
+
return { promise, resolve };
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export class CodexAppServerClient {
|
|
93
|
+
#options;
|
|
94
|
+
#listeners = new Set();
|
|
95
|
+
#failureListeners = new Set();
|
|
96
|
+
#threadListeners = new Set();
|
|
97
|
+
#messages = [];
|
|
98
|
+
#queue = [];
|
|
99
|
+
#transport;
|
|
100
|
+
#starting;
|
|
101
|
+
#threadId = null;
|
|
102
|
+
#pendingResumeId;
|
|
103
|
+
#activeTurnId = null;
|
|
104
|
+
#inFlight = false;
|
|
105
|
+
#closed = false;
|
|
106
|
+
#failure = null;
|
|
107
|
+
#pendingModel;
|
|
108
|
+
#appliedModel = null;
|
|
109
|
+
/** The provider the live child was spawned on; `null` = the native config decides. */
|
|
110
|
+
#appliedProvider;
|
|
111
|
+
/** The argv/env that pins {@link CodexAppServerClient.#appliedProvider}. */
|
|
112
|
+
#launch;
|
|
113
|
+
/**
|
|
114
|
+
* Bumped whenever a transport is orphaned on purpose (a provider reattach)
|
|
115
|
+
* and whenever one is created. A transport's `onExit` carries the generation
|
|
116
|
+
* it was built with, so the child a reattach deliberately kills cannot reach
|
|
117
|
+
* {@link CodexAppServerClient.#onExit} and fail the client permanently —
|
|
118
|
+
* without this, a restart is indistinguishable from a crash.
|
|
119
|
+
*/
|
|
120
|
+
#transportGeneration = 0;
|
|
121
|
+
#turnPolicy = {};
|
|
122
|
+
#lastUsage = null;
|
|
123
|
+
#terminalError = null;
|
|
124
|
+
#serverRequestHandler;
|
|
125
|
+
/** Bumped by newSession()/close() to orphan in-flight work on a discarded thread. */
|
|
126
|
+
#sessionEpoch = 0;
|
|
127
|
+
#turnStarted = deferred();
|
|
128
|
+
#turnSettled = deferred();
|
|
129
|
+
constructor(options) {
|
|
130
|
+
this.#options = options;
|
|
131
|
+
this.#pendingResumeId = options.resumeThreadId ?? null;
|
|
132
|
+
this.#pendingModel = options.model ?? null;
|
|
133
|
+
this.#launch = options.launch ?? NATIVE_LAUNCH;
|
|
134
|
+
this.#appliedProvider = options.providerId ?? null;
|
|
135
|
+
this.#launch = options.launch ?? { argv: [], env: {} };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Construct a client. Performs ZERO I/O: no process, no socket, no config
|
|
139
|
+
* read — the app-server child is created by {@link ensureStarted}, which the
|
|
140
|
+
* agent only reaches on the first real prompt (lazy-spawn gate).
|
|
141
|
+
*/
|
|
142
|
+
static create(options = {}) {
|
|
143
|
+
return new CodexAppServerClient(options);
|
|
144
|
+
}
|
|
145
|
+
// ── surface parity with the retired SDK client ─────────────────────────────
|
|
146
|
+
/** Register a wire event listener; returns the unsubscriber. */
|
|
147
|
+
on(listener) {
|
|
148
|
+
this.#listeners.add(listener);
|
|
149
|
+
return () => {
|
|
150
|
+
this.#listeners.delete(listener);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Register a failure listener; returns the unsubscriber. */
|
|
154
|
+
onFailure(listener) {
|
|
155
|
+
this.#failureListeners.add(listener);
|
|
156
|
+
return () => {
|
|
157
|
+
this.#failureListeners.delete(listener);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Observe thread attachment (the moment the Dash↔Codex pairing becomes
|
|
162
|
+
* persistable). Fires for the first attach and for every later one
|
|
163
|
+
* (`newSession()` re-attaches), which is exactly the re-arming observer the
|
|
164
|
+
* provider's `dsh-sessions.json` map needs.
|
|
165
|
+
*/
|
|
166
|
+
onThreadAttached(listener) {
|
|
167
|
+
this.#threadListeners.add(listener);
|
|
168
|
+
if (this.#threadId !== null)
|
|
169
|
+
listener(this.#threadId);
|
|
170
|
+
return () => {
|
|
171
|
+
this.#threadListeners.delete(listener);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Install the approval bridge (server-request handler). */
|
|
175
|
+
setServerRequestHandler(handler) {
|
|
176
|
+
this.#serverRequestHandler = handler;
|
|
177
|
+
}
|
|
178
|
+
/** Set the per-turn permission override (approval policy + sandbox). */
|
|
179
|
+
setTurnPolicy(policy) {
|
|
180
|
+
this.#turnPolicy = policy;
|
|
181
|
+
}
|
|
182
|
+
/** The current Codex thread id (null until the thread attaches). */
|
|
183
|
+
get threadId() {
|
|
184
|
+
return this.#threadId;
|
|
185
|
+
}
|
|
186
|
+
/** Whether a native process currently exists (lazy-spawn observability). */
|
|
187
|
+
get spawned() {
|
|
188
|
+
return this.#transport?.alive === true;
|
|
189
|
+
}
|
|
190
|
+
/** The id of the turn currently in flight, when one is. */
|
|
191
|
+
get activeTurnId() {
|
|
192
|
+
return this.#activeTurnId;
|
|
193
|
+
}
|
|
194
|
+
/** The provider the live child is pinned to (`null` = the native config decides). */
|
|
195
|
+
get appliedProvider() {
|
|
196
|
+
return this.#appliedProvider;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Pin the NEXT spawn to a provider without touching a live child. The cheap
|
|
200
|
+
* path: a session that has not run a turn yet owns no process, so its first
|
|
201
|
+
* prompt simply spawns under the new provider.
|
|
202
|
+
*
|
|
203
|
+
* @param launch - argv/env pinning the provider (`codex-launch.ts`).
|
|
204
|
+
* @param providerId - the DSH route id `launch` pins; `null` = native config.
|
|
205
|
+
*/
|
|
206
|
+
applyLaunch(launch, providerId) {
|
|
207
|
+
this.#ensureUsable();
|
|
208
|
+
this.#launch = launch;
|
|
209
|
+
this.#appliedProvider = providerId;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Move an ALREADY-ATTACHED session onto another provider.
|
|
213
|
+
*
|
|
214
|
+
* A provider is bound to the CHILD PROCESS, not to the thread: `turn/start`
|
|
215
|
+
* carries a model but no provider field at all, and a `thread/resume` aimed at
|
|
216
|
+
* a thread a live app-server already holds re-joins it instead of reloading
|
|
217
|
+
* it, so the override is accepted and silently ignored (measured 2026-10-03 —
|
|
218
|
+
* asked for `zhipu-cn`, got `deepseek` back). The death of the child is
|
|
219
|
+
* therefore the ONLY boundary at which a provider can change, which is why
|
|
220
|
+
* this runs between turns and why the thread id is carried across: the new
|
|
221
|
+
* child resumes the same Codex thread, so the conversation survives.
|
|
222
|
+
*
|
|
223
|
+
* A session with no attached thread has nothing to restart — it just takes the
|
|
224
|
+
* new launch for its first spawn.
|
|
225
|
+
*
|
|
226
|
+
* @param launch - argv/env pinning the new provider.
|
|
227
|
+
* @param providerId - the DSH route id `launch` pins; `null` = native config.
|
|
228
|
+
* @param model - the model the next turn must run.
|
|
229
|
+
*/
|
|
230
|
+
async reattach(launch, providerId, model) {
|
|
231
|
+
this.#ensureUsable();
|
|
232
|
+
if (model !== undefined)
|
|
233
|
+
this.#pendingModel = model;
|
|
234
|
+
const threadId = this.#threadId;
|
|
235
|
+
if (threadId === null) {
|
|
236
|
+
this.applyLaunch(launch, providerId);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.#trace(`reattach: provider ${this.#appliedProvider ?? "native"} → ${providerId ?? "native"} thread=${threadId}`);
|
|
240
|
+
// Orphan the dying child FIRST: its `exit` must not be read as a crash.
|
|
241
|
+
this.#transportGeneration += 1;
|
|
242
|
+
this.#transport?.close();
|
|
243
|
+
this.#transport = undefined;
|
|
244
|
+
this.#threadId = null;
|
|
245
|
+
this.#pendingResumeId = threadId;
|
|
246
|
+
this.#launch = launch;
|
|
247
|
+
this.#appliedProvider = providerId;
|
|
248
|
+
this.#inFlight = false;
|
|
249
|
+
this.#activeTurnId = null;
|
|
250
|
+
this.#messages.length = 0;
|
|
251
|
+
this.#queue.length = 0;
|
|
252
|
+
this.#turnSettled.resolve();
|
|
253
|
+
this.#turnStarted.resolve("");
|
|
254
|
+
await this.ensureStarted();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Materialize the native side on first use: spawn → `initialize` →
|
|
258
|
+
* `initialized` → `thread/start` (or `thread/resume`). Single-flight and
|
|
259
|
+
* idempotent; concurrent callers share one attempt.
|
|
260
|
+
*/
|
|
261
|
+
async ensureStarted() {
|
|
262
|
+
this.#ensureUsable();
|
|
263
|
+
if (this.#transport !== undefined && this.#threadId !== null)
|
|
264
|
+
return;
|
|
265
|
+
if (this.#starting === undefined) {
|
|
266
|
+
const epoch = this.#sessionEpoch;
|
|
267
|
+
this.#starting = this.#start(epoch).finally(() => {
|
|
268
|
+
this.#starting = undefined;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return this.#starting;
|
|
272
|
+
}
|
|
273
|
+
/** Query session state; returns the `get_state`-shaped payload. */
|
|
274
|
+
async getState() {
|
|
275
|
+
this.#ensureUsable();
|
|
276
|
+
return { isStreaming: this.#inFlight };
|
|
277
|
+
}
|
|
278
|
+
/** Best-effort conversation mirror accumulated from the wire. */
|
|
279
|
+
async getMessages() {
|
|
280
|
+
return [...this.#messages];
|
|
281
|
+
}
|
|
282
|
+
/** Live token accounting from the latest `thread/tokenUsage/updated`. */
|
|
283
|
+
async getSessionStats() {
|
|
284
|
+
try {
|
|
285
|
+
const usage = this.#lastUsage;
|
|
286
|
+
if (usage === null)
|
|
287
|
+
return {};
|
|
288
|
+
const tokens = {};
|
|
289
|
+
const map = [
|
|
290
|
+
["input", "input_tokens"],
|
|
291
|
+
["output", "output_tokens"],
|
|
292
|
+
["cacheRead", "cached_input_tokens"],
|
|
293
|
+
["cacheWrite", "cache_write_input_tokens"],
|
|
294
|
+
];
|
|
295
|
+
for (const [wireKey, codexKey] of map) {
|
|
296
|
+
const value = usage[codexKey];
|
|
297
|
+
if (typeof value === "number")
|
|
298
|
+
tokens[wireKey] = value;
|
|
299
|
+
}
|
|
300
|
+
return { tokens };
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return {};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Codex subagent/collab items are not projected in this version. */
|
|
307
|
+
async getSubagents() {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
/** Start a turn with a user prompt. */
|
|
311
|
+
async prompt(message) {
|
|
312
|
+
this.#ensureUsable();
|
|
313
|
+
await this.ensureStarted();
|
|
314
|
+
await this.#startTurn(message);
|
|
315
|
+
return ok("prompt");
|
|
316
|
+
}
|
|
317
|
+
/** Queue a follow-up turn: runs immediately when idle, else after the live turn. */
|
|
318
|
+
async followUp(message) {
|
|
319
|
+
this.#ensureUsable();
|
|
320
|
+
await this.ensureStarted();
|
|
321
|
+
if (this.#inFlight) {
|
|
322
|
+
this.#queue.push(message);
|
|
323
|
+
return ok("follow_up");
|
|
324
|
+
}
|
|
325
|
+
await this.#startTurn(message);
|
|
326
|
+
return ok("follow_up");
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Steer the live turn (`turn/steer`). Idle → starts a turn. In flight but the
|
|
330
|
+
* server has not named the turn yet → wait briefly for `turn/started`, then
|
|
331
|
+
* fall back to the client queue (never two concurrent turns).
|
|
332
|
+
*/
|
|
333
|
+
async steer(message) {
|
|
334
|
+
this.#ensureUsable();
|
|
335
|
+
await this.ensureStarted();
|
|
336
|
+
if (!this.#inFlight) {
|
|
337
|
+
await this.#startTurn(message);
|
|
338
|
+
return ok("steer");
|
|
339
|
+
}
|
|
340
|
+
const turnId = this.#activeTurnId ?? (await this.#awaitActiveTurn(5_000));
|
|
341
|
+
const transport = this.#transport;
|
|
342
|
+
const threadId = this.#threadId;
|
|
343
|
+
if (turnId === null || transport === undefined || threadId === null) {
|
|
344
|
+
this.#queue.push(message);
|
|
345
|
+
return ok("steer");
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
await transport.request("turn/steer", { threadId, input: [textInput(message)], expectedTurnId: turnId });
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
// A rejected steer must not lose the message: queue it as the next turn.
|
|
352
|
+
this.#trace(`steer rejected (${String(error)}); queued as the next turn`);
|
|
353
|
+
this.#queue.push(message);
|
|
354
|
+
}
|
|
355
|
+
return ok("steer");
|
|
356
|
+
}
|
|
357
|
+
/** Select the active model; applied as the next turn's `model` override. */
|
|
358
|
+
async setModel(provider, modelId) {
|
|
359
|
+
this.#ensureUsable();
|
|
360
|
+
void provider;
|
|
361
|
+
this.#pendingModel = modelId;
|
|
362
|
+
return ok("set_model");
|
|
363
|
+
}
|
|
364
|
+
/** Interrupt the live turn (`turn/interrupt`) and wait for it to settle. */
|
|
365
|
+
async abort() {
|
|
366
|
+
const transport = this.#transport;
|
|
367
|
+
const threadId = this.#threadId;
|
|
368
|
+
const turnId = this.#activeTurnId;
|
|
369
|
+
if (transport === undefined || threadId === null || turnId === null || !this.#inFlight) {
|
|
370
|
+
return ok("abort");
|
|
371
|
+
}
|
|
372
|
+
const settled = this.#turnSettled.promise;
|
|
373
|
+
try {
|
|
374
|
+
await transport.request("turn/interrupt", { threadId, turnId });
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
this.#trace(`turn/interrupt failed: ${String(error)}`);
|
|
378
|
+
return ok("abort");
|
|
379
|
+
}
|
|
380
|
+
// Bounded wait so a lost `turn/completed` cannot wedge the caller.
|
|
381
|
+
await Promise.race([settled, new Promise((resolve) => setTimeout(resolve, 15_000).unref?.())]);
|
|
382
|
+
return ok("abort");
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Discard the current thread and start a fresh one on the next turn. The
|
|
386
|
+
* child process is kept (only the thread is discarded), so a new session
|
|
387
|
+
* costs one `thread/start` rather than a respawn.
|
|
388
|
+
*/
|
|
389
|
+
async newSession() {
|
|
390
|
+
this.#ensureUsable();
|
|
391
|
+
this.#sessionEpoch += 1;
|
|
392
|
+
await this.abort();
|
|
393
|
+
this.#queue.length = 0;
|
|
394
|
+
this.#messages.length = 0;
|
|
395
|
+
this.#threadId = null;
|
|
396
|
+
this.#pendingResumeId = null;
|
|
397
|
+
this.#activeTurnId = null;
|
|
398
|
+
this.#inFlight = false;
|
|
399
|
+
this.#appliedModel = null;
|
|
400
|
+
this.#lastUsage = null;
|
|
401
|
+
this.#terminalError = null;
|
|
402
|
+
return ok("new_session");
|
|
403
|
+
}
|
|
404
|
+
/** Stop the child and fail all future operations. Idempotent. */
|
|
405
|
+
close() {
|
|
406
|
+
if (this.#closed)
|
|
407
|
+
return;
|
|
408
|
+
this.#closed = true;
|
|
409
|
+
this.#sessionEpoch += 1;
|
|
410
|
+
this.#transport?.close();
|
|
411
|
+
this.#transport = undefined;
|
|
412
|
+
this.#inFlight = false;
|
|
413
|
+
this.#activeTurnId = null;
|
|
414
|
+
this.#turnSettled.resolve();
|
|
415
|
+
this.#turnStarted.resolve("");
|
|
416
|
+
this.#fail(new Error("codex app server client closed"));
|
|
417
|
+
}
|
|
418
|
+
// ── internals ─────────────────────────────────────────────────────────────
|
|
419
|
+
#ensureUsable() {
|
|
420
|
+
if (this.#failure !== null)
|
|
421
|
+
throw this.#failure;
|
|
422
|
+
if (this.#closed)
|
|
423
|
+
throw new Error("codex app server client is closed");
|
|
424
|
+
}
|
|
425
|
+
#trace(message) {
|
|
426
|
+
if (process.env.CODEX_TRACE === "1")
|
|
427
|
+
process.stderr.write(`[codex-appserver] ${message}\n`);
|
|
428
|
+
}
|
|
429
|
+
/** Spawn + handshake + thread attach. Runs at most once per thread. */
|
|
430
|
+
async #start(epoch) {
|
|
431
|
+
const existing = this.#transport;
|
|
432
|
+
const first = existing === undefined;
|
|
433
|
+
let transport;
|
|
434
|
+
if (existing !== undefined) {
|
|
435
|
+
transport = existing;
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
// The generation is captured by THIS transport's exit closure, so a child
|
|
439
|
+
// orphaned on purpose (a provider reattach) stays silent while a real
|
|
440
|
+
// crash still fails the client.
|
|
441
|
+
const generation = ++this.#transportGeneration;
|
|
442
|
+
const launch = this.#launch;
|
|
443
|
+
transport = transportFactory({
|
|
444
|
+
...(this.#options.cwd === undefined ? {} : { cwd: this.#options.cwd }),
|
|
445
|
+
...(this.#options.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: this.#options.requestTimeoutMs }),
|
|
446
|
+
args: ["app-server", ...launch.argv],
|
|
447
|
+
env: { ...process.env, ...launch.env },
|
|
448
|
+
onNotification: (notification) => this.#onNotification(notification),
|
|
449
|
+
onServerRequest: (request) => this.#onServerRequest(request),
|
|
450
|
+
onExit: (info) => {
|
|
451
|
+
if (generation !== this.#transportGeneration)
|
|
452
|
+
return;
|
|
453
|
+
this.#onExit(info);
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
this.#transport = transport;
|
|
458
|
+
if (first) {
|
|
459
|
+
transport.start();
|
|
460
|
+
const info = asRecord(await transport.request("initialize", {
|
|
461
|
+
clientInfo: { name: "superd-agent-codex", title: "Super D agent-codex", version: "0.1.0" },
|
|
462
|
+
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
463
|
+
}));
|
|
464
|
+
transport.notify("initialized");
|
|
465
|
+
this.#trace(`initialized codexHome=${String(info?.["codexHome"] ?? "?")} platform=${String(info?.["platformOs"] ?? "?")}`);
|
|
466
|
+
}
|
|
467
|
+
if (this.#threadId !== null)
|
|
468
|
+
return;
|
|
469
|
+
const resumeId = this.#pendingResumeId;
|
|
470
|
+
const shared = {
|
|
471
|
+
...(this.#options.cwd === undefined ? {} : { cwd: this.#options.cwd }),
|
|
472
|
+
personality: "none",
|
|
473
|
+
serviceName: "superd-agent-codex",
|
|
474
|
+
...(this.#pendingModel === null ? {} : { model: this.#pendingModel }),
|
|
475
|
+
// The provider is a property of the CHILD (`-c` overrides), but restating
|
|
476
|
+
// it on the thread keeps `thread/resume` honest when the id was injected
|
|
477
|
+
// through the config object rather than argv.
|
|
478
|
+
...(this.#appliedProvider === null ? {} : { modelProvider: this.#appliedProvider }),
|
|
479
|
+
...(this.#options.approvalPolicy === undefined && this.#turnPolicy.approvalPolicy === undefined
|
|
480
|
+
? {}
|
|
481
|
+
: { approvalPolicy: this.#turnPolicy.approvalPolicy ?? this.#options.approvalPolicy }),
|
|
482
|
+
...(this.#options.sandboxMode === undefined ? {} : { sandbox: this.#options.sandboxMode }),
|
|
483
|
+
};
|
|
484
|
+
const response = asRecord(resumeId === null
|
|
485
|
+
? await transport.request("thread/start", shared)
|
|
486
|
+
: await transport.request("thread/resume", { threadId: resumeId, ...shared }));
|
|
487
|
+
const thread = asRecord(response?.["thread"]);
|
|
488
|
+
const threadId = asString(thread?.["id"]) ?? resumeId;
|
|
489
|
+
if (threadId === null)
|
|
490
|
+
throw new Error("codex app-server did not return a thread id");
|
|
491
|
+
if (epoch !== this.#sessionEpoch) {
|
|
492
|
+
// A newSession() landed while the thread was attaching: keep the process,
|
|
493
|
+
// discard this thread, and let the next turn start a fresh one.
|
|
494
|
+
this.#pendingResumeId = null;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.#pendingResumeId = null;
|
|
498
|
+
this.#attachThread(threadId);
|
|
499
|
+
}
|
|
500
|
+
#attachThread(threadId) {
|
|
501
|
+
this.#threadId = threadId;
|
|
502
|
+
this.#trace(`thread attached ${threadId}`);
|
|
503
|
+
for (const listener of [...this.#threadListeners]) {
|
|
504
|
+
try {
|
|
505
|
+
listener(threadId);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
this.#trace(`thread listener error: ${String(error)}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/** One turn: apply the per-turn model/policy overrides, then `turn/start`. */
|
|
513
|
+
async #startTurn(message) {
|
|
514
|
+
await this.ensureStarted();
|
|
515
|
+
const transport = this.#transport;
|
|
516
|
+
const threadId = this.#threadId;
|
|
517
|
+
if (transport === undefined || threadId === null)
|
|
518
|
+
throw new Error("codex app-server thread is unavailable");
|
|
519
|
+
this.#messages.push({ role: "user", content: [{ type: "text", text: message }] });
|
|
520
|
+
this.#inFlight = true;
|
|
521
|
+
this.#activeTurnId = null;
|
|
522
|
+
this.#terminalError = null;
|
|
523
|
+
this.#turnStarted = deferred();
|
|
524
|
+
this.#turnSettled = deferred();
|
|
525
|
+
const model = this.#pendingModel ?? this.#options.model ?? null;
|
|
526
|
+
const policy = this.#turnPolicy;
|
|
527
|
+
try {
|
|
528
|
+
const response = asRecord(await transport.request("turn/start", {
|
|
529
|
+
threadId,
|
|
530
|
+
input: [textInput(message)],
|
|
531
|
+
...(model === null ? {} : { model }),
|
|
532
|
+
...(policy.approvalPolicy === undefined ? {} : { approvalPolicy: policy.approvalPolicy }),
|
|
533
|
+
...(policy.sandbox === undefined ? {} : { sandboxPolicy: policy.sandbox }),
|
|
534
|
+
}));
|
|
535
|
+
this.#appliedModel = model;
|
|
536
|
+
const turn = asRecord(response?.["turn"]);
|
|
537
|
+
const turnId = asString(turn?.["id"]);
|
|
538
|
+
if (turnId !== undefined && this.#activeTurnId === null)
|
|
539
|
+
this.#activeTurnId = turnId;
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
this.#inFlight = false;
|
|
543
|
+
this.#turnSettled.resolve();
|
|
544
|
+
this.#turnStarted.resolve("");
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/** Wait for `turn/started` to name the active turn (bounded). */
|
|
549
|
+
async #awaitActiveTurn(timeoutMs) {
|
|
550
|
+
if (this.#activeTurnId !== null)
|
|
551
|
+
return this.#activeTurnId;
|
|
552
|
+
const started = this.#turnStarted.promise;
|
|
553
|
+
await Promise.race([started, new Promise((resolve) => setTimeout(resolve, timeoutMs).unref?.())]);
|
|
554
|
+
return this.#activeTurnId;
|
|
555
|
+
}
|
|
556
|
+
#onNotification(notification) {
|
|
557
|
+
const method = notification.method;
|
|
558
|
+
const params = asRecord(notification.params) ?? {};
|
|
559
|
+
try {
|
|
560
|
+
switch (method) {
|
|
561
|
+
case "turn/started": {
|
|
562
|
+
const turn = asRecord(params["turn"]);
|
|
563
|
+
const turnId = asString(turn?.["id"]);
|
|
564
|
+
if (turnId !== undefined) {
|
|
565
|
+
this.#activeTurnId = turnId;
|
|
566
|
+
this.#turnStarted.resolve(turnId);
|
|
567
|
+
}
|
|
568
|
+
this.#inFlight = true;
|
|
569
|
+
// `agent_start` opens the Dash activity; it must precede `turn_start`
|
|
570
|
+
// (the agent flushes parked deliveries on it).
|
|
571
|
+
this.#emit({ type: "agent_start" });
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case "turn/completed": {
|
|
575
|
+
this.#settleTurn(params);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
case "thread/tokenUsage/updated": {
|
|
579
|
+
const usage = convertTokenUsage(params["tokenUsage"] && asRecord(params["tokenUsage"])?.["last"]);
|
|
580
|
+
if (usage !== null)
|
|
581
|
+
this.#lastUsage = usage;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
case "error": {
|
|
585
|
+
const error = asRecord(params["error"]);
|
|
586
|
+
const willRetry = params["willRetry"] === true;
|
|
587
|
+
if (!willRetry) {
|
|
588
|
+
this.#terminalError = asString(error?.["message"]) ?? this.#terminalError;
|
|
589
|
+
this.#trace(`terminal error notification: ${this.#terminalError ?? "?"}`);
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
case "thread/started":
|
|
594
|
+
case "thread/status/changed":
|
|
595
|
+
case "mcpServer/startupStatus/updated":
|
|
596
|
+
case "remoteControl/status/changed":
|
|
597
|
+
case "warning":
|
|
598
|
+
return;
|
|
599
|
+
default:
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
if (this.#isItemNotification(method) && !this.#acceptsItem(params))
|
|
603
|
+
return;
|
|
604
|
+
const projected = projectAppServerNotification(method, notification.params);
|
|
605
|
+
if (projected === null)
|
|
606
|
+
return;
|
|
607
|
+
for (const event of Array.isArray(projected) ? projected : [projected]) {
|
|
608
|
+
this.#emit(event);
|
|
609
|
+
this.#mirror(event);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
this.#trace(`notification handler failed for ${method}: ${String(error)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/** Item-scoped notifications carry a `turnId`; stale ones must not reopen a closed turn. */
|
|
617
|
+
#isItemNotification(method) {
|
|
618
|
+
return method === "item/started" || method === "item/completed" || method.startsWith("item/") || method === "turn/plan/updated";
|
|
619
|
+
}
|
|
620
|
+
#acceptsItem(params) {
|
|
621
|
+
if (this.#activeTurnId === null)
|
|
622
|
+
return false;
|
|
623
|
+
const turnId = asString(params["turnId"]);
|
|
624
|
+
return turnId === undefined || turnId === this.#activeTurnId;
|
|
625
|
+
}
|
|
626
|
+
/** Close the Dash turn: terminal error + `turn_end` (+ usage) + `agent_end`. */
|
|
627
|
+
#settleTurn(params) {
|
|
628
|
+
const turn = asRecord(params["turn"]) ?? {};
|
|
629
|
+
const status = asString(turn["status"]) ?? "completed";
|
|
630
|
+
const epoch = this.#sessionEpoch;
|
|
631
|
+
this.#inFlight = false;
|
|
632
|
+
this.#activeTurnId = null;
|
|
633
|
+
this.#turnSettled.resolve();
|
|
634
|
+
if (status === "failed") {
|
|
635
|
+
const error = asRecord(turn["error"]);
|
|
636
|
+
const message = asString(error?.["message"]) ?? this.#terminalError ?? "codex turn failed";
|
|
637
|
+
this.#emit({
|
|
638
|
+
type: "message_end",
|
|
639
|
+
message: { role: "assistant", content: [], stopReason: "error", errorMessage: message },
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const usage = this.#lastUsage;
|
|
643
|
+
this.#emit({ type: "turn_end", data: { turn, ...(usage === null ? {} : { usage }) } });
|
|
644
|
+
this.#emit({ type: "agent_end" });
|
|
645
|
+
this.#terminalError = null;
|
|
646
|
+
if (epoch !== this.#sessionEpoch || this.#closed)
|
|
647
|
+
return;
|
|
648
|
+
const next = this.#queue.shift();
|
|
649
|
+
if (next === undefined)
|
|
650
|
+
return;
|
|
651
|
+
// Deliver the queued follow-up as its own turn; the agent's echo bridge
|
|
652
|
+
// closes the just-finished Dash turn and opens a fresh one.
|
|
653
|
+
void this.#startTurn(next).catch((error) => this.#fail(error));
|
|
654
|
+
}
|
|
655
|
+
#onServerRequest(request) {
|
|
656
|
+
const handler = this.#serverRequestHandler;
|
|
657
|
+
if (handler === undefined)
|
|
658
|
+
return failClosedResponse(request.method);
|
|
659
|
+
try {
|
|
660
|
+
return handler(request);
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
this.#trace(`server-request handler threw for ${request.method}: ${String(error)}`);
|
|
664
|
+
return failClosedResponse(request.method);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
#onExit(info) {
|
|
668
|
+
if (this.#closed)
|
|
669
|
+
return;
|
|
670
|
+
this.#fail(new Error(`codex app-server exited (code=${String(info.code)} signal=${String(info.signal)})`));
|
|
671
|
+
}
|
|
672
|
+
/** Mirror wire events into the `getMessages` accumulator (best effort). */
|
|
673
|
+
#mirror(wire) {
|
|
674
|
+
if (wire.type === "message_end") {
|
|
675
|
+
const message = asRecord(wire.message);
|
|
676
|
+
if (message !== null && message.role === "assistant" && typeof message.stopReason !== "string") {
|
|
677
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
678
|
+
const reasoningOnly = blocks.length > 0 && blocks.every((block) => block.type === "thinking");
|
|
679
|
+
if (!reasoningOnly)
|
|
680
|
+
this.#messages.push(message);
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (wire.type === "tool_execution_end") {
|
|
685
|
+
const result = asRecord(wire.result);
|
|
686
|
+
this.#messages.push({
|
|
687
|
+
role: "toolResult",
|
|
688
|
+
toolCallId: wire.toolCallId,
|
|
689
|
+
content: Array.isArray(result?.content) ? result.content : [],
|
|
690
|
+
isError: wire.isError === true,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
#emit(event) {
|
|
695
|
+
if (this.#closed && event.type !== "agent_end")
|
|
696
|
+
return;
|
|
697
|
+
for (const listener of [...this.#listeners]) {
|
|
698
|
+
try {
|
|
699
|
+
listener(event);
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
process.stderr.write(`codex app server listener error: ${String(error)}\n`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
#fail(error) {
|
|
707
|
+
if (this.#failure !== null)
|
|
708
|
+
return;
|
|
709
|
+
this.#failure = error;
|
|
710
|
+
this.#inFlight = false;
|
|
711
|
+
this.#activeTurnId = null;
|
|
712
|
+
this.#turnSettled.resolve();
|
|
713
|
+
this.#turnStarted.resolve("");
|
|
714
|
+
for (const listener of [...this.#failureListeners]) {
|
|
715
|
+
try {
|
|
716
|
+
listener(error);
|
|
717
|
+
}
|
|
718
|
+
catch (listenerError) {
|
|
719
|
+
process.stderr.write(`codex app server failure listener error: ${String(listenerError)}\n`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|