super-dsh 0.1.9 → 0.1.10-e

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.
Files changed (178) hide show
  1. package/README.md +25 -3
  2. package/agent-agy/cordis.patch.yml +7 -4
  3. package/agent-agy/dist/agent.d.ts +12 -11
  4. package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
  5. package/agent-agy/dist/agy-auth-gateway.js +233 -0
  6. package/agent-agy/dist/agy-auth.d.ts +153 -0
  7. package/agent-agy/dist/agy-auth.js +179 -0
  8. package/agent-agy/dist/agy-cli-client.d.ts +39 -9
  9. package/agent-agy/dist/agy-cli-client.js +82 -18
  10. package/agent-agy/dist/agy-store.d.ts +8 -0
  11. package/agent-agy/dist/index.d.ts +46 -16
  12. package/agent-agy/dist/index.js +82 -56
  13. package/agent-agy/dist/models.d.ts +70 -0
  14. package/agent-agy/dist/models.js +102 -3
  15. package/agent-agy/package.json +7 -5
  16. package/agent-agy-sdk/cordis.patch.yml +58 -0
  17. package/agent-agy-sdk/dist/adapter.d.ts +15 -0
  18. package/agent-agy-sdk/dist/adapter.js +42 -0
  19. package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
  20. package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
  21. package/agent-agy-sdk/dist/agent.d.ts +138 -0
  22. package/agent-agy-sdk/dist/agent.js +494 -0
  23. package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
  24. package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
  25. package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
  26. package/agent-agy-sdk/dist/agy-sessions.js +107 -0
  27. package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
  28. package/agent-agy-sdk/dist/agy-store.js +83 -0
  29. package/agent-agy-sdk/dist/inbox.d.ts +26 -0
  30. package/agent-agy-sdk/dist/inbox.js +53 -0
  31. package/agent-agy-sdk/dist/index.d.ts +93 -0
  32. package/agent-agy-sdk/dist/index.js +476 -0
  33. package/agent-agy-sdk/dist/models.d.ts +41 -0
  34. package/agent-agy-sdk/dist/models.js +74 -0
  35. package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
  36. package/agent-agy-sdk/dist/sdk-venv.js +173 -0
  37. package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
  38. package/agent-agy-sdk/dist/world-plugin.js +82 -0
  39. package/agent-agy-sdk/package.json +64 -0
  40. package/agent-claude/dist/adapter.js +106 -31
  41. package/agent-claude/dist/agent.js +154 -17
  42. package/agent-claude/dist/claude-client.js +84 -43
  43. package/agent-claude/dist/claude-launch.js +101 -0
  44. package/agent-claude/dist/claude-native-config.js +169 -0
  45. package/agent-claude/dist/claude-provider-gateway.js +340 -0
  46. package/agent-claude/dist/claude-providers.js +391 -0
  47. package/agent-claude/dist/claude-route.js +152 -0
  48. package/agent-claude/dist/index.js +214 -37
  49. package/agent-claude/dist/models.js +14 -0
  50. package/agent-claude/dist/sdk-runtime.js +320 -0
  51. package/agent-claude/package.json +6 -4
  52. package/agent-codex/README.md +141 -36
  53. package/agent-codex/cordis.patch.yml +14 -10
  54. package/agent-codex/dist/adapter.js +64 -28
  55. package/agent-codex/dist/agent.js +255 -81
  56. package/agent-codex/dist/app-server-approvals.js +165 -0
  57. package/agent-codex/dist/app-server-client.js +723 -0
  58. package/agent-codex/dist/app-server-events.js +432 -0
  59. package/agent-codex/dist/app-server-protocol.js +95 -0
  60. package/agent-codex/dist/app-server-wire.js +299 -0
  61. package/agent-codex/dist/codex-launch.js +78 -0
  62. package/agent-codex/dist/codex-native-config.js +392 -0
  63. package/agent-codex/dist/codex-provider-gateway.js +402 -0
  64. package/agent-codex/dist/codex-providers.js +376 -0
  65. package/agent-codex/dist/codex-route.js +86 -0
  66. package/agent-codex/dist/index.js +256 -115
  67. package/agent-codex/dist/permission.js +75 -1
  68. package/agent-codex/package.json +1 -2
  69. package/agent-codex-sdk/README.md +79 -0
  70. package/agent-codex-sdk/cordis.patch.yml +81 -0
  71. package/agent-codex-sdk/dist/adapter.js +91 -0
  72. package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
  73. package/agent-codex-sdk/dist/agent.js +1085 -0
  74. package/agent-codex-sdk/dist/codex-native-config.js +392 -0
  75. package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
  76. package/agent-codex-sdk/dist/codex-providers.js +376 -0
  77. package/agent-codex-sdk/dist/codex-route.js +86 -0
  78. package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
  79. package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
  80. package/agent-codex-sdk/dist/codex-store.js +180 -0
  81. package/agent-codex-sdk/dist/inbox.js +53 -0
  82. package/agent-codex-sdk/dist/index.js +739 -0
  83. package/agent-codex-sdk/dist/permission.js +71 -0
  84. package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
  85. package/agent-codex-sdk/dist/session-map.js +167 -0
  86. package/agent-codex-sdk/dist/world-plugin.js +93 -0
  87. package/agent-codex-sdk/package.json +79 -0
  88. package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
  89. package/agent-omp/README.md +72 -55
  90. package/agent-omp/dist/agent.js +36 -11
  91. package/agent-omp/dist/index.js +92 -52
  92. package/agent-omp/dist/lazy-rpc.js +11 -4
  93. package/agent-omp/dist/omp-cli.js +114 -97
  94. package/agent-omp/dist/omp-store.js +12 -4
  95. package/agent-omp/dist/permission.js +70 -5
  96. package/agent-omp/dist/rpc-client.js +409 -0
  97. package/agent-omp/dist/world-plugin.js +36 -3
  98. package/agent-omp/package.json +2 -6
  99. package/agent-omp-sdk/README.md +103 -0
  100. package/agent-omp-sdk/cordis.patch.yml +108 -0
  101. package/agent-omp-sdk/dist/adapter.d.ts +21 -0
  102. package/agent-omp-sdk/dist/adapter.js +36 -0
  103. package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
  104. package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
  105. package/agent-omp-sdk/dist/agent.d.ts +64 -0
  106. package/agent-omp-sdk/dist/agent.js +1178 -0
  107. package/agent-omp-sdk/dist/discovery.d.ts +57 -0
  108. package/agent-omp-sdk/dist/discovery.js +116 -0
  109. package/agent-omp-sdk/dist/inbox.d.ts +28 -0
  110. package/agent-omp-sdk/dist/inbox.js +53 -0
  111. package/agent-omp-sdk/dist/index.d.ts +60 -0
  112. package/agent-omp-sdk/dist/index.js +632 -0
  113. package/agent-omp-sdk/dist/knobs.d.ts +34 -0
  114. package/agent-omp-sdk/dist/knobs.js +50 -0
  115. package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
  116. package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
  117. package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
  118. package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
  119. package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
  120. package/agent-omp-sdk/dist/mobile-boot.js +38 -0
  121. package/agent-omp-sdk/dist/models.d.ts +39 -0
  122. package/agent-omp-sdk/dist/models.js +299 -0
  123. package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
  124. package/agent-omp-sdk/dist/omp-cli.js +164 -0
  125. package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
  126. package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
  127. package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
  128. package/agent-omp-sdk/dist/omp-executable.js +30 -0
  129. package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
  130. package/agent-omp-sdk/dist/omp-store.js +526 -0
  131. package/agent-omp-sdk/dist/pairing.d.ts +7 -0
  132. package/agent-omp-sdk/dist/pairing.js +45 -0
  133. package/agent-omp-sdk/dist/permission.d.ts +138 -0
  134. package/agent-omp-sdk/dist/permission.js +161 -0
  135. package/agent-omp-sdk/dist/protocol.d.ts +189 -0
  136. package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
  137. package/agent-omp-sdk/dist/rpc-types.js +1 -0
  138. package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
  139. package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
  140. package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
  141. package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
  142. package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
  143. package/agent-omp-sdk/dist/sidecar-client.js +197 -0
  144. package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
  145. package/agent-omp-sdk/dist/store/adapter.js +47 -0
  146. package/agent-omp-sdk/dist/store/db.d.ts +70 -0
  147. package/agent-omp-sdk/dist/store/db.js +210 -0
  148. package/agent-omp-sdk/dist/store/index.d.ts +23 -0
  149. package/agent-omp-sdk/dist/store/index.js +59 -0
  150. package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
  151. package/agent-omp-sdk/dist/store/reconcile.js +51 -0
  152. package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
  153. package/agent-omp-sdk/dist/store/schema.js +40 -0
  154. package/agent-omp-sdk/dist/transport.d.ts +49 -0
  155. package/agent-omp-sdk/dist/transport.js +35 -0
  156. package/agent-omp-sdk/dist/world-home.d.ts +4 -0
  157. package/agent-omp-sdk/dist/world-home.js +23 -0
  158. package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
  159. package/agent-omp-sdk/dist/world-plugin.js +137 -0
  160. package/agent-omp-sdk/package.json +87 -0
  161. package/client/index.js +1204 -93
  162. package/cordis.patch.yml +64 -0
  163. package/package.json +17 -8
  164. package/superd-hub/dist/client/agent-settings.d.ts +210 -0
  165. package/superd-hub/dist/client/agent-settings.js +300 -0
  166. package/superd-hub/dist/client/roster.js +6 -0
  167. package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
  168. package/superd-hub/dist/client/runtime-settings-api.js +137 -0
  169. package/superd-hub/dist/client/switch.d.ts +165 -0
  170. package/superd-hub/dist/client/switch.js +295 -0
  171. package/superd-hub/lib/client/index.js +1204 -93
  172. package/agent-codex/dist/models.js +0 -152
  173. package/agent-omp/dist/sidecar-client.js +0 -195
  174. /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
  175. /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
  176. /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
  177. /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
  178. /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
@@ -0,0 +1,1085 @@
1
+ import { agentEvents } from "@deepseek-ai/dsh-agent";
2
+ import { Inbox } from "./inbox.js";
3
+ import { LlmAttemptId, ToolCallId, QUOTA_EXCEEDED_CODE, createAssistantMessage, createSystemMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { createScope } from "@deepseek-ai/dsh-scope";
5
+ import { NATIVE_SDK_LAUNCH } from "./codex-sdk-launch.js";
6
+ import { resolveRoute } from "./codex-route.js";
7
+ import { withSystemHead } from "../../superd-hub/dist/index.js";
8
+ /**
9
+ * v2 live assistant-stream publication for the Codex bridge — the same frame
10
+ * protocol the reference loop's AssistantStreamAttempt speaks (start marker,
11
+ * dense zero-based chunks, terminal settlement) emitted over this agent's own
12
+ * dispatch, so the session-controller folds Codex's stream into its reconnect
13
+ * baseline. Chunks also accumulate as raw durable records embedded in the
14
+ * final assistant/message.
15
+ */
16
+ class AssistantStreamBridge {
17
+ attemptId;
18
+ records = [];
19
+ #nextRevision;
20
+ #emit;
21
+ #index = 0;
22
+ #terminal = false;
23
+ constructor(sessionId, attempt, nextRevision, emit) {
24
+ this.attemptId = LlmAttemptId(`${sessionId}:${attempt}`);
25
+ this.#nextRevision = nextRevision;
26
+ this.#emit = emit;
27
+ }
28
+ /** Whether the terminal frame already fired. */
29
+ get ended() {
30
+ return this.#terminal;
31
+ }
32
+ /** Opening marker before the first delivered chunk. */
33
+ start(turn, step) {
34
+ this.#emit({ type: "start", attemptId: this.attemptId, revision: this.#nextRevision(), turn, step });
35
+ }
36
+ /** One live chunk: durable record plus dense process-local frame. */
37
+ push(chunk) {
38
+ const time = Date.now();
39
+ this.records.push({ type: "chunk", time, chunk });
40
+ this.#emit({ type: "chunk", attemptId: this.attemptId, revision: this.#nextRevision(), index: this.#index++, time, chunk });
41
+ }
42
+ /** Terminal settlement after the durable assistant/message committed. */
43
+ settle(seq) {
44
+ this.#terminal = true;
45
+ this.#emit({ type: "end", attemptId: this.attemptId, revision: this.#nextRevision(), index: this.#index, outcome: { kind: "committed", eventType: "assistant/message", seq } });
46
+ }
47
+ /** No durable attempt event will commit. */
48
+ abandon() {
49
+ this.#terminal = true;
50
+ this.#emit({ type: "end", attemptId: this.attemptId, revision: this.#nextRevision(), index: this.#index, outcome: { kind: "abandoned" } });
51
+ }
52
+ }
53
+ /** Diagnostic trace (set CODEX_TRACE=1 on the dsh process to enable). */
54
+ const TRACE = process.env.CODEX_TRACE === "1";
55
+ const trace = (...parts) => {
56
+ if (TRACE)
57
+ process.stderr.write(`[codex-agent ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
58
+ };
59
+ export function convertContent(blocks) {
60
+ if (blocks === undefined)
61
+ return [];
62
+ const result = [];
63
+ for (const block of blocks) {
64
+ switch (block.type) {
65
+ case "text":
66
+ if (typeof block.text === "string")
67
+ result.push({ type: "text", text: block.text });
68
+ break;
69
+ case "thinking":
70
+ if (typeof block.thinking === "string")
71
+ result.push({ type: "reasoning", text: block.thinking });
72
+ break;
73
+ case "toolCall": {
74
+ const id = typeof block.id === "string" ? block.id : "";
75
+ const name = typeof block.name === "string" ? block.name : "";
76
+ const args = typeof block.arguments === "string" ? block.arguments : JSON.stringify(block.arguments ?? {});
77
+ result.push({ type: "tool-call", id: ToolCallId(id), name, arguments: args });
78
+ break;
79
+ }
80
+ default:
81
+ // images / unknown blocks are not bridged in V1.
82
+ break;
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+ /**
88
+ * Classify the wire's `stopReason: "error"` assistant message into a Dash
89
+ * `failure` (`{ message, code }`). This is the same taxonomy the harness
90
+ * adapters use for `turn/end` reasons — `AUTH`, `RATE_LIMIT`, `QUOTA`,
91
+ * `SERVER` — so the Web UI renders it through its own error path instead of
92
+ * an empty assistant bubble. Quota/usage wording is checked before the
93
+ * generic 403→AUTH because some providers report an exhausted billing quota
94
+ * as HTTP 403.
95
+ */
96
+ export function wireFailure(message) {
97
+ if (message.stopReason !== "error")
98
+ return undefined;
99
+ const status = typeof message.errorStatus === "number" ? message.errorStatus : undefined;
100
+ const text = typeof message.errorMessage === "string" && message.errorMessage.trim() !== ""
101
+ ? message.errorMessage.trim()
102
+ : undefined;
103
+ const detail = text ?? "";
104
+ let code;
105
+ if (/\b(?:usage|rate)\s*limit\b/i.test(detail) || /\bquota\b/i.test(detail) || /\bbilling\b/i.test(detail) || /\binsufficient\b/i.test(detail)) {
106
+ code = QUOTA_EXCEEDED_CODE;
107
+ }
108
+ else if (status === 401 || status === 403) {
109
+ code = "AUTH";
110
+ }
111
+ else if (status === 429) {
112
+ code = "RATE_LIMIT";
113
+ }
114
+ else if (status !== undefined && status >= 500) {
115
+ code = "SERVER";
116
+ }
117
+ else {
118
+ code = "UNKNOWN";
119
+ }
120
+ return { message: text ?? (status !== undefined ? `HTTP ${status}` : "model request failed"), code };
121
+ }
122
+ /**
123
+ * Classify a NATIVE failure (the codex process / SDK stream itself, surfaced
124
+ * through `onFailure`) into the stable turn-error codes the resume surface
125
+ * uses (plan RC-4): a missing or unreadable rollout is `ROLLOUT_MISSING`; any
126
+ * other refusal of the native resume/re-attach is `NATIVE_REJECTED`.
127
+ * Model-side failures that ride the wire as `stopReason: "error"` are
128
+ * classified by {@link wireFailure} instead. Unrecognized shapes stay
129
+ * `UNKNOWN` — only the two stable codes mark the session not resumable.
130
+ */
131
+ export function classifyNativeFailure(error) {
132
+ const text = error instanceof Error ? error.message : String(error);
133
+ if (/rollout|no such (session|thread)|(session|thread)[\w -]{0,24}not[\w -]{0,8}found|unable to (find|locate|load|read|resume)|failed to (load|read|resume)/i.test(text)) {
134
+ return { message: text, code: "ROLLOUT_MISSING" };
135
+ }
136
+ if (/resume|re-?attach|corrupt|invalid[\w -]{0,24}(session|thread)/i.test(text)) {
137
+ return { message: text, code: "NATIVE_REJECTED" };
138
+ }
139
+ return { message: text, code: "UNKNOWN" };
140
+ }
141
+ /**
142
+ * Map Codex's turn usage accounting into Dash `TokenUsage`. Codex reports
143
+ * five disjoint snake_case counters per turn (`turn_end.data.usage`) and its
144
+ * `reasoning_output_tokens` is ADDITIVE to `output_tokens` (the captured
145
+ * fixture shows output 3, reasoning 18), while the DSH turn fold requires
146
+ * reasoning ⊆ output plus an exact `totalTokens` when both cache buckets are
147
+ * present (upstream token-meter `normalizeUsage`). The total is therefore
148
+ * synthesized from Codex's own counters:
149
+ *
150
+ * totalTokens = input + cachedInput + cacheWrite + output + reasoning
151
+ *
152
+ * Emission rules (plan ruling 6 — omp/pi shape, no invented tokens):
153
+ * - a cache bucket rides through only when > 0 (a zero bucket carries no
154
+ * information but would trip the fold's both-buckets exact-prompt
155
+ * equality for nothing);
156
+ * - `reasoningTokens` rides through only when it fits the fold's subset
157
+ * rule (`reasoning <= output`); codex's additive reasoning is otherwise
158
+ * omitted from the breakdown while still counted in the synthesized
159
+ * total (fail-closed fold philosophy: no attempt is inferred from a
160
+ * contradictory sample).
161
+ */
162
+ export function convertUsage(usage) {
163
+ if (usage === null || typeof usage !== "object")
164
+ return undefined;
165
+ const u = usage;
166
+ const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
167
+ const inputTokens = count(u.input_tokens) ?? 0;
168
+ const outputTokens = count(u.output_tokens) ?? 0;
169
+ if (inputTokens === 0 && outputTokens === 0)
170
+ return undefined;
171
+ const cachedInput = count(u.cached_input_tokens);
172
+ const cacheWrite = count(u.cache_write_input_tokens);
173
+ const reasoning = count(u.reasoning_output_tokens);
174
+ const cacheReadTokens = cachedInput !== undefined && cachedInput > 0 ? cachedInput : undefined;
175
+ const cacheWriteTokens = cacheWrite !== undefined && cacheWrite > 0 ? cacheWrite : undefined;
176
+ const reasoningTokens = reasoning !== undefined && reasoning > 0 && reasoning <= outputTokens ? reasoning : undefined;
177
+ return {
178
+ inputTokens,
179
+ outputTokens,
180
+ totalTokens: inputTokens + (cacheReadTokens ?? 0) + (cacheWriteTokens ?? 0) + outputTokens + (reasoning ?? 0),
181
+ ...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
182
+ ...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
183
+ ...(reasoningTokens === undefined ? {} : { reasoningTokens }),
184
+ };
185
+ }
186
+ /** Join the visible text blocks of a Dash user message into the Codex prompt string. */
187
+ function userMessageText(message) {
188
+ const parts = [];
189
+ for (const block of message.content) {
190
+ if (block.type === "text")
191
+ parts.push(block.text);
192
+ }
193
+ return parts.join("\n");
194
+ }
195
+ /**
196
+ * Dash-host plugin narration from the approval service: the policy-change
197
+ * notice `agent.inject` carries when `/permission` switches a live session's
198
+ * approval policy. Codex has no runtime approval control either (the policy
199
+ * is pinned at launch via the preset mapping; no SDK surface changes it
200
+ * mid-session), so forwarding the text would steer it into an in-flight turn
201
+ * (interrupting tool work) and echo it into history as a fake user prompt.
202
+ * Detect and drop it.
203
+ */
204
+ function isApprovalNarration(message) {
205
+ // 0.1.7: the shared `plugin` source kind is gone (MessageSourceMap is
206
+ // merge-extensible; each producer declares its own kind) — user-approval
207
+ // now carries the dedicated `user-approval` kind.
208
+ // Type-only: loads the `user-approval` MessageSourceMap augmentation
209
+ // (0.1.7: producer-owned source kinds; the shared `plugin` kind is gone).
210
+ // noinspection JSValidateTypes
211
+ return message.source.kind === "user-approval";
212
+ }
213
+ /**
214
+ * Reconstruct a Dash user message from a wire `message_start(role=user)`
215
+ * payload. Used only as a defensive fallback when a queued delivery's
216
+ * retained message is missing (the client echoes text, so the visible
217
+ * content is preserved).
218
+ */
219
+ function userMessageFromWire(message) {
220
+ return createUserMessage({
221
+ content: convertContent(message.content).filter((block) => block.type === "text"),
222
+ source: { kind: "user" },
223
+ });
224
+ }
225
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
226
+ /**
227
+ * How long an agent may sit idle before its codex client is torn down. The
228
+ * Dash host keeps resumed agents registered forever (nothing else disposes
229
+ * them), and every idle agent pins a live thread. Tearing the agent down at
230
+ * idle costs nothing observable — the next prompt cold-resumes the session
231
+ * from Codex's rollout through the replay persistence. `0` disables the exit.
232
+ */
233
+ const CODEX_IDLE_EXIT_MS = parseIdleExit(process.env.CODEX_IDLE_EXIT_MS);
234
+ function parseIdleExit(raw) {
235
+ if (raw === undefined || raw.trim() === "")
236
+ return 600_000;
237
+ const value = Number(raw);
238
+ return Number.isFinite(value) && value >= 0 ? value : 600_000;
239
+ }
240
+ export class CodexAgent {
241
+ id;
242
+ options;
243
+ session;
244
+ inbox;
245
+ ctx;
246
+ #client;
247
+ #loopCtx;
248
+ #scope;
249
+ #dispatch;
250
+ #streaming = false;
251
+ #activityDone = Promise.resolve();
252
+ #resolveActivityDone = () => { };
253
+ #dashTurn = 0;
254
+ #step = 0;
255
+ #lastTurn = 0;
256
+ #turnOpen = false;
257
+ #cancelCause = null;
258
+ /** Terminal model failure of the open turn (`message_end` stopReason "error"); consumed by `agent_end`. */
259
+ #pendingFailure = null;
260
+ /** Live v2 assistant-stream publication for the in-flight wire message. */
261
+ #streamBridge;
262
+ #assistantAttemptCounter = 0;
263
+ #assistantStreamRevision = 0;
264
+ /** The current turn's user/message was appended locally and the echo is still awaited. */
265
+ #localUserPending = false;
266
+ /** A `turn_start` arrived; `step/start` is deferred until the step's owning turn is known. */
267
+ #stepStartPending = false;
268
+ /**
269
+ * Deliveries forwarded through the client's own queue, in submission order.
270
+ * The client echoes each as `message_start(role=user)`; the bridge maps it
271
+ * by transport: a `followUp` echo closes the current Dash turn and opens a
272
+ * fresh one (the client queues it and auto-generates the next turn), a
273
+ * `steer` echo appends a `user/message` step to the current turn. `sent`
274
+ * marks entries already dispatched to the client (a parked injection
275
+ * flushes at agent_start).
276
+ */
277
+ #remoteQueue = [];
278
+ #bridgedToolResults = new Set();
279
+ /**
280
+ * The open turn's FINAL assistant message, buffered from `message_end`
281
+ * until `turn_end` so the SAME turn's usage can land on it (the DSH
282
+ * turn-token fold refuses cross-turn samples). Flushed usage-less on
283
+ * failure/abort paths so the turn's visible text is never dropped.
284
+ */
285
+ #deferredAssistant = null;
286
+ /**
287
+ * The route this TURN runs, captured at turn open. Held per turn (not read
288
+ * live) because the projection can change mid-turn: a switch made while a
289
+ * turn is in flight must land on the NEXT one, and the transcript must keep
290
+ * naming the provider that actually produced it.
291
+ */
292
+ #turnRoute = undefined;
293
+ /** Fires when the agent has been idle past CODEX_IDLE_EXIT_MS (tears down the client). */
294
+ #onIdleExit;
295
+ #idleExitTimer = undefined;
296
+ #disposed = false;
297
+ /** One-shot guard for the first-turn session-identity commit (#bootstrapSessionIdentity). */
298
+ #sessionIdentityCommitted = false;
299
+ /** Adapter-owned runtime facts (system prompt / route metadata), when supplied. */
300
+ #runtimeInfo;
301
+ /** One-shot guard for the `system/message` surface node (never re-emitted). */
302
+ #systemMessageDone = false;
303
+ /** Last `request/context` route key (`provider/model`) — dedupes per-turn emits. */
304
+ #lastRouteKey = null;
305
+ constructor(loopCtx, id, options, session, client, onIdleExit, runtimeInfo) {
306
+ this.#loopCtx = loopCtx;
307
+ this.id = id;
308
+ this.options = options;
309
+ this.session = withSystemHead(session, () => createSystemMessage(""));
310
+ this.#client = client;
311
+ this.#onIdleExit = onIdleExit;
312
+ this.#runtimeInfo = runtimeInfo;
313
+ this.#dispatch = agentEvents(loopCtx, this);
314
+ this.inbox = new Inbox();
315
+ this.#scope = createScope(loopCtx, this);
316
+ this.ctx = this.#scope.ctx.extend({ agent: this });
317
+ this.#lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
318
+ client.on((event) => this.#handleEvent(event));
319
+ client.onFailure((error) => this.#fail(error));
320
+ // Session-identity events (the agent-preset stamp) are committed on the
321
+ // FIRST turn by #bootstrapSessionIdentity — only once the client actually
322
+ // exists — so a freshly created session announces completely blank,
323
+ // exactly like the native agent-loop factory's: the backend takes zero
324
+ // part in the UI's browser-local new-session draft.
325
+ // `/permission` cannot work here: Codex pins the approval policy at
326
+ // launch and no SDK surface changes it mid-session. Registering the SAME
327
+ // name on this agent's own scope (a command-injected child of `agent.ctx`)
328
+ // SHADOWS the global permission command for this agent only — the switch
329
+ // then fails cleanly instead of appending approval/policy events Codex
330
+ // cannot honor. (The `inject` narration drop below stays as the backstop.)
331
+ this.ctx.inject(["commands"], (cmdCtx) => cmdCtx.commands.register({
332
+ name: "permission",
333
+ description: "Unavailable for Codex sessions (approval policy is fixed at Codex launch)",
334
+ input: { hint: "<preset>" },
335
+ handler: () => ({
336
+ kind: "error",
337
+ text: "Approval policy is fixed at Codex launch; runtime switching is not supported for Codex sessions.",
338
+ }),
339
+ }));
340
+ // `/compact` parity: the SDK line exposes no compaction surface (V1 gap
341
+ // table: no messages surgery/compact — fail-soft default). Shadowing the
342
+ // loop-owned global name keeps the (disabled) native pipeline from
343
+ // receiving it; the command node renders the clean refusal natively.
344
+ this.ctx.inject(["commands"], (cmdCtx) => cmdCtx.commands.register({
345
+ name: "compact",
346
+ description: "Unavailable for Codex sessions (the SDK line has no compaction surface)",
347
+ input: { hint: "[instructions]" },
348
+ handler: () => ({
349
+ kind: "error",
350
+ text: "Compaction is not available for Codex sessions: the SDK line has no compaction surface.",
351
+ }),
352
+ }));
353
+ }
354
+ get status() {
355
+ return this.#streaming ? "running" : "idle";
356
+ }
357
+ send(message, target, wakeup) {
358
+ this.#deliver(message, target, wakeup);
359
+ }
360
+ followup(message) {
361
+ // The Web UI's `session.prompt` (mode "queue") routes through followup.
362
+ // Idle → `prompt` (opens the Dash turn locally); busy → the client's own
363
+ // queue (delivered as the next turn's user message and auto-generated).
364
+ // Mirrors dsh-agent-loop's `followup = send(input, "next-turn", true)`.
365
+ this.#deliver(message, "next-turn", true);
366
+ }
367
+ steer(message) {
368
+ // Mirrors dsh-agent-loop's `steer = send(input, "next-step", true)`: an
369
+ // idle driver starts a turn; a running driver consumes it at the next
370
+ // step (on the SDK line: queued followUp after the in-flight turn).
371
+ this.#deliver(message, "next-step", true);
372
+ }
373
+ inject(message) {
374
+ // Dash-host approval-policy narrations have no Codex runtime equivalent
375
+ // (the policy is pinned at launch; no SDK surface changes it mid-session).
376
+ // Forwarding the text would steer it into an in-flight turn — interrupting
377
+ // tool work — and echo it into history as a fake user prompt. Drop it; the
378
+ // accompanying `approval/policy` session event stays recorded so the UI's
379
+ // permission state remains truthful.
380
+ if (isApprovalNarration(message)) {
381
+ trace("inject: dropped approval-policy narration (Codex lacks runtime approval control)");
382
+ return;
383
+ }
384
+ // Mirrors dsh-agent-loop's `inject = send(input, "next-step", false)`:
385
+ // queued without waking; idle drivers leave it pending until a later
386
+ // follow-up/steer wakes them (flushed at the next agent_start).
387
+ this.#deliver(message, "next-step", false);
388
+ }
389
+ cancel(cause, _options) {
390
+ // No active run → no-op: arming `#cancelCause` here would misreport a later,
391
+ // unrelated completed turn as aborted. First cause wins: later cancels must
392
+ // not overwrite an earlier cause (dash contract).
393
+ if (!this.#streaming)
394
+ return;
395
+ if (this.#cancelCause === null)
396
+ this.#cancelCause = cause;
397
+ void this.#client.abort().catch(() => { });
398
+ }
399
+ async whenIdle() {
400
+ // Event-driven: wait for the current activity to settle (agent_end), then
401
+ // confirm the client reports quiescence via get_state (the mapping's poll).
402
+ let activity;
403
+ do {
404
+ activity = this.#activityDone;
405
+ await activity;
406
+ } while (activity !== this.#activityDone);
407
+ for (;;) {
408
+ const state = await this.#client.getState().catch(() => null);
409
+ if (state === null || !state.isStreaming)
410
+ return;
411
+ await delay(50);
412
+ }
413
+ }
414
+ runMaintenance(task) {
415
+ // Codex has no maintenance concept; run the task directly.
416
+ return task(new AbortController().signal);
417
+ }
418
+ /** Stop the codex client and unwind the scoped world. Idempotent. */
419
+ async dispose() {
420
+ if (this.#disposed)
421
+ return;
422
+ this.#disposed = true;
423
+ clearTimeout(this.#idleExitTimer);
424
+ this.#idleExitTimer = undefined;
425
+ this.#client.close();
426
+ await this.#scope.dispose();
427
+ }
428
+ // ── internals ────────────────────────────────────────────────────────────
429
+ /**
430
+ * Route one message by the reference loop's (target, wakeup) semantics:
431
+ * an idle waking delivery starts a fresh local turn (`prompt`); a follow-up
432
+ * sent while a turn is open is forwarded to the client's own queue
433
+ * (`followUp`: the client delivers it as the next turn's user message and
434
+ * auto-generates that turn); steering/injection goes through `steer`
435
+ * (consumed at the next step, or parked until agent_start when not yet
436
+ * streaming).
437
+ *
438
+ * Busy is the LOCAL turn state (`#turnOpen`), not the `#streaming` flag:
439
+ * the client needs a moment to emit `agent_start` after a prompt, and in
440
+ * that gap a bare `prompt` is still safe on the SDK line, but keeping the
441
+ * omp queuing semantics makes the echo mapping identical.
442
+ */
443
+ #deliver(message, target, wakeup) {
444
+ if (this.#disposed)
445
+ return;
446
+ const busy = this.#turnOpen || this.#streaming;
447
+ trace(`#deliver target=${target} wakeup=${wakeup} busy=${busy} (turnOpen=${this.#turnOpen} streaming=${this.#streaming})`);
448
+ if (busy && target === "next-turn") {
449
+ this.#queueRemote(message, "followUp");
450
+ return;
451
+ }
452
+ if (busy || !wakeup) {
453
+ this.#queueRemote(message, "steer");
454
+ return;
455
+ }
456
+ this.#deliverPrompt(message);
457
+ }
458
+ #deliverPrompt(message) {
459
+ if (this.#disposed)
460
+ return;
461
+ trace(`#deliverPrompt turn=${this.#dashTurn + 1} text="${userMessageText(message).slice(0, 40)}"`);
462
+ // Reserve the turn synchronously: busy-flag semantics stay identical to
463
+ // the eager line (a second delivery during the cold start queues
464
+ // remotely), but NOTHING is appended yet — the log stays empty until the
465
+ // client exists, so a failed cold start leaves no orphan turn.
466
+ this.#reserveTurn();
467
+ clearTimeout(this.#idleExitTimer);
468
+ this.#idleExitTimer = undefined;
469
+ this.#beginActivity();
470
+ void this.#startTurn(message);
471
+ }
472
+ /**
473
+ * Cold-start path for one reserved turn: confirm the client is started,
474
+ * commit the session-identity events ahead of the turn (the agent-preset
475
+ * stamp), sync the model, then append turn/start + user/message and
476
+ * dispatch. A failed cold start synthesizes the failed turn (turn/end with
477
+ * the error reason) so the UI's error path renders it, then unwinds the
478
+ * reservation.
479
+ */
480
+ async #startTurn(message) {
481
+ try {
482
+ await this.#client.ensureStarted();
483
+ await this.#bootstrapSessionIdentity();
484
+ // The route is resolved and put in force BEFORE the turn commits: the
485
+ // provider is a property of the handle the run is about to use, and a
486
+ // mid-turn switch must land on the NEXT turn (hence the per-turn hold).
487
+ this.#turnRoute = this.#resolveRoute();
488
+ this.#applyRoute(this.#turnRoute);
489
+ }
490
+ catch (error) {
491
+ trace(`#startTurn cold start failed: ${String(error)}`);
492
+ if (this.#turnOpen) {
493
+ this.session.append("turn/start", { turn: this.#dashTurn });
494
+ this.session.append("user/message", message, { surfaceOp: "append" });
495
+ this.#closeTurn({ kind: "error", error: { message: String(error), code: "UNKNOWN" } });
496
+ }
497
+ this.#endActivity();
498
+ return;
499
+ }
500
+ if (this.#disposed)
501
+ return;
502
+ this.session.append("turn/start", { turn: this.#dashTurn });
503
+ this.#emitRequestContext();
504
+ this.session.append("user/message", message, { surfaceOp: "append" });
505
+ void this.#client.prompt(userMessageText(message)).catch((error) => this.#fail(error));
506
+ }
507
+ /**
508
+ * Stamp the Codex system prompt once per session as the step's surface node.
509
+ * `system/message` is a step-scoped surface event, so this runs only after
510
+ * `step/start` committed (inside the first open step of the first turn).
511
+ * Skips when the log already carries one (resume), when the adapter has no
512
+ * runtime bridge, and while the rollout HEAD is not yet readable — the call
513
+ * is retried on later steps until the text is available.
514
+ */
515
+ #emitSystemMessage() {
516
+ if (this.#systemMessageDone)
517
+ return;
518
+ if (this.#runtimeInfo === undefined || !this.#turnOpen)
519
+ return;
520
+ this.#commitStepStart();
521
+ // commitStepStart re-enters #emitSystemMessage (step open → stamp); the
522
+ // outer call must re-check the one-shot flag after the recursion.
523
+ if (this.#systemMessageDone)
524
+ return;
525
+ if (this.#step === 0)
526
+ return;
527
+ const text = this.#runtimeInfo.systemPrompt();
528
+ if (text === undefined || text.trim() === "")
529
+ return;
530
+ // v4 protected-head posture: replace the placeholder head (stamped before
531
+ // the first user message); an append here would corrupt the log for the
532
+ // 0.1.7 reader ("system/message requires a protected first surface head").
533
+ this.session.stampSystemPrompt(this.#dashTurn, this.#step, createSystemMessage(text));
534
+ this.#systemMessageDone = true;
535
+ trace(`system/message stamped (${text.length} chars)`);
536
+ }
537
+ /**
538
+ * Stamp route metadata for the next request (`request/context`): the TURN's
539
+ * route plus the provider's context window when known. Turn-enclosed (no step
540
+ * required) and deduped by route key, so it lands at the start of a turn and
541
+ * again only when the route changes.
542
+ */
543
+ #emitRequestContext() {
544
+ if (!this.#turnOpen)
545
+ return;
546
+ const route = this.#turnRoute;
547
+ if (route === undefined)
548
+ return;
549
+ const key = `${route.provider}/${route.model}`;
550
+ if (key === this.#lastRouteKey)
551
+ return;
552
+ this.#lastRouteKey = key;
553
+ const contextWindow = this.#runtimeInfo?.contextWindowFor?.(route.provider, route.model);
554
+ this.session.append("request/context", {
555
+ provider: route.provider,
556
+ model: route.model,
557
+ ...(contextWindow === undefined ? {} : { contextWindow }),
558
+ });
559
+ trace(`request/context ${key}${contextWindow === undefined ? "" : ` ctx=${contextWindow}`}`);
560
+ }
561
+ /**
562
+ * One-shot first-turn identity commit: the agent-preset stamp (idempotent).
563
+ * The SDK line exposes no system-prompt surface, so — unlike the omp
564
+ * bridge — no `system/message` node is emitted here; a Codex session's
565
+ * system prompt is Codex's own and never crosses the wire. The guard —
566
+ * skip when the seed already carries a `system/message` — stays for shape
567
+ * parity with resumed logs that might carry one.
568
+ */
569
+ async #bootstrapSessionIdentity() {
570
+ if (this.#sessionIdentityCommitted)
571
+ return;
572
+ this.#sessionIdentityCommitted = true;
573
+ if (!this.session.snapshotEvents().some((event) => event.type === "agent-preset/selected" && event.data?.agentPreset === "codex")) {
574
+ this.session.append("agent-preset/selected", { agentPreset: "codex" });
575
+ }
576
+ // The system prompt is stamped separately by #emitSystemMessage once the
577
+ // thread's rollout HEAD is readable (Codex's own prompt is recovered from
578
+ // `session_meta.base_instructions`, never from a wire message).
579
+ }
580
+ /**
581
+ * Queue a message for client-side delivery. `followUp` dispatches
582
+ * immediately (state-safe whether or not the client is streaming); `steer`
583
+ * dispatches now when already streaming, otherwise parks until
584
+ * agent_start.
585
+ */
586
+ #queueRemote(message, transport) {
587
+ if (this.#disposed)
588
+ return;
589
+ clearTimeout(this.#idleExitTimer);
590
+ this.#idleExitTimer = undefined;
591
+ this.#remoteQueue.push({ message, sent: false, transport });
592
+ if (this.#streaming || transport === "followUp")
593
+ this.#flushRemote();
594
+ }
595
+ #flushRemote() {
596
+ // A remote-driven delivery (follow-up / steer) opens no new DSH turn, so
597
+ // the route is re-read here too: it is the only chance to notice that the
598
+ // user moved the session to another provider before this message lands.
599
+ this.#turnRoute = this.#resolveRoute();
600
+ this.#applyRoute(this.#turnRoute);
601
+ void Promise.resolve().finally(() => {
602
+ for (const entry of this.#remoteQueue) {
603
+ if (entry.sent)
604
+ continue;
605
+ entry.sent = true;
606
+ const text = userMessageText(entry.message);
607
+ const sent = entry.transport === "followUp" ? this.#client.followUp(text) : this.#client.steer(text);
608
+ trace(`-> ${entry.transport} "${text.slice(0, 40)}"`);
609
+ void sent.catch((error) => this.#fail(error));
610
+ }
611
+ });
612
+ }
613
+ /**
614
+ * Resolve the route for the NEXT turn by the harness's own precedence
615
+ * (`codex-route.ts`): a `session/selectModel` switch that no turn has consumed
616
+ * yet, else what the session actually ran last, else the deployment default,
617
+ * else the factory's own options.
618
+ *
619
+ * @returns the route, or `undefined` when nothing names a complete pair.
620
+ */
621
+ #resolveRoute() {
622
+ const projections = this.#loopCtx.get("sessionProjections");
623
+ const service = this.ctx.get("agentDefaultModel");
624
+ return resolveRoute({
625
+ pending: projections?.stateOf?.(this.session, "modelSelection")?.pending,
626
+ logged: this.session.requestHeader()?.config,
627
+ preferred: service?.currentSelection(),
628
+ fallback: { provider: this.options.provider, model: this.options.model },
629
+ });
630
+ }
631
+ /**
632
+ * Put the resolved route in force for the next run.
633
+ *
634
+ * The SDK line needs no child surgery here: the client swaps the handle it
635
+ * will ask for the next run and re-attaches the SAME thread id to it (see
636
+ * `CodexSdkClient.applyRoute`), so a provider move costs one config object and
637
+ * never a process. An unknown provider id (a session pinned before the
638
+ * multi-provider rework) maps to "no injection": the user's own `config.toml`
639
+ * is the honest answer, which is also what `NATIVE_SDK_LAUNCH` means.
640
+ *
641
+ * @param route - the resolved route, or `undefined` when none is known.
642
+ */
643
+ #applyRoute(route) {
644
+ const launch = route === undefined ? undefined : this.#runtimeInfo?.launchFor(route.provider);
645
+ const launchSpec = launch ?? NATIVE_SDK_LAUNCH;
646
+ const providerId = launch === undefined ? null : route?.provider ?? null;
647
+ this.#client.applyRoute(launchSpec, providerId, route?.model ?? "");
648
+ }
649
+ /**
650
+ * Reserve a turn WITHOUT appending anything: the prompt path reserves
651
+ * synchronously, then commits turn/start + user/message only after the
652
+ * cold start (#startTurn). #openTurn keeps the eager append for paths that
653
+ * run against an already-live client (#bridgeRemoteUser echoes).
654
+ */
655
+ #reserveTurn() {
656
+ if (this.#turnOpen)
657
+ return;
658
+ this.#dashTurn = ++this.#lastTurn;
659
+ this.#step = 0;
660
+ this.#turnOpen = true;
661
+ this.#localUserPending = true;
662
+ }
663
+ #openTurn(message, localUser = true) {
664
+ trace(`#openTurn turnOpen=${this.#turnOpen} -> turn=${this.#dashTurn + 1} localUser=${localUser}`);
665
+ if (this.#turnOpen)
666
+ return;
667
+ this.#reserveTurn();
668
+ this.#localUserPending = localUser;
669
+ this.session.append("turn/start", { turn: this.#dashTurn });
670
+ this.#emitRequestContext();
671
+ this.session.append("user/message", message, { surfaceOp: "append" });
672
+ }
673
+ #closeTurn(reason) {
674
+ if (!this.#turnOpen)
675
+ return;
676
+ this.#turnOpen = false;
677
+ this.#localUserPending = false;
678
+ this.session.append("turn/end", { turn: this.#dashTurn, reason });
679
+ }
680
+ /**
681
+ * Append the deferred `step/start` now that the step's first message has
682
+ * revealed which Dash turn owns it (a queued follow-up opens a fresh turn).
683
+ */
684
+ #commitStepStart() {
685
+ if (!this.#turnOpen)
686
+ return;
687
+ // On-demand open: the Codex SDK emits `item.completed` without a preceding
688
+ // `item.started` for plain text turns, so a step must open at the first
689
+ // step-scoped use rather than only on a `message_start` that may never
690
+ // arrive. `#stepStartPending` (set by turn_start) still forces a FRESH
691
+ // step when the runtime opens another turn.
692
+ if (!this.#stepStartPending && this.#step > 0)
693
+ return;
694
+ this.#stepStartPending = false;
695
+ this.#step += 1;
696
+ this.session.append("step/start", { turn: this.#dashTurn, step: this.#step });
697
+ trace(`step/start turn=${this.#dashTurn} step=${this.#step}`);
698
+ }
699
+ /**
700
+ * Bridge a wire `message_start(role=user)` that was NOT appended locally.
701
+ * A `followUp` echo closes the current Dash turn and opens a fresh one with
702
+ * the retained message (the client queued it and is now generating its
703
+ * turn); a steering/injection echo adds a `user/message` step to the
704
+ * current turn. Falls back to the wire's own content if no retained
705
+ * message matches.
706
+ */
707
+ #bridgeRemoteUser(wireMessage) {
708
+ const entry = this.#remoteQueue.shift();
709
+ if (entry === undefined) {
710
+ this.#closeTurn({ kind: "completed" });
711
+ this.#openTurn(userMessageFromWire(wireMessage), false);
712
+ return;
713
+ }
714
+ if (entry.transport === "followUp") {
715
+ this.#closeTurn({ kind: "completed" });
716
+ this.#openTurn(entry.message, false);
717
+ return;
718
+ }
719
+ this.session.append("user/message", entry.message, { surfaceOp: "append" });
720
+ }
721
+ #beginActivity() {
722
+ // Settle any prior in-flight activity first: `send()` can inject a prompt
723
+ // mid-stream (streamingBehavior followUp/steer), superseding the current
724
+ // activity promise. Without settling the old one, whenIdle()'s do-while
725
+ // would await an orphaned promise that never resolves.
726
+ this.#resolveActivityDone();
727
+ this.#activityDone = new Promise((resolve) => {
728
+ this.#resolveActivityDone = resolve;
729
+ });
730
+ }
731
+ #endActivity() {
732
+ this.#resolveActivityDone();
733
+ }
734
+ #markRunning() {
735
+ clearTimeout(this.#idleExitTimer);
736
+ this.#idleExitTimer = undefined;
737
+ if (this.#streaming)
738
+ return;
739
+ this.#streaming = true;
740
+ this.#dispatch.emit("agent/status", { status: "running" });
741
+ }
742
+ #markIdle() {
743
+ if (!this.#streaming)
744
+ return;
745
+ this.#streaming = false;
746
+ this.#dispatch.emit("agent/status", { status: "idle" });
747
+ this.#armIdleExit();
748
+ }
749
+ /**
750
+ * Schedule the idle exit. Skipped while deliveries are still queued (an
751
+ * unanswered parked injection means work is pending, not idle) or when the
752
+ * exit is disabled. Any later activity restarts or cancels the timer.
753
+ */
754
+ #armIdleExit() {
755
+ if (this.#disposed || this.#onIdleExit === undefined || CODEX_IDLE_EXIT_MS === 0)
756
+ return;
757
+ if (this.#remoteQueue.some((entry) => !entry.sent))
758
+ return;
759
+ clearTimeout(this.#idleExitTimer);
760
+ this.#idleExitTimer = setTimeout(() => {
761
+ this.#idleExitTimer = undefined;
762
+ void this.#revalidateIdleExit();
763
+ }, CODEX_IDLE_EXIT_MS);
764
+ this.#idleExitTimer.unref?.();
765
+ }
766
+ /**
767
+ * Quiescence re-validation at fire time. The idle timer is only a hint:
768
+ * before teardown, re-confirm no live work via local signals and the
769
+ * client's own self-attestation (get_state streaming). Any busy signal
770
+ * re-arms instead of killing; a query failure fail-softs to the local
771
+ * signals rather than forcing a teardown.
772
+ */
773
+ async #revalidateIdleExit() {
774
+ if (this.#disposed || this.#streaming)
775
+ return this.#armIdleExit();
776
+ if (this.#remoteQueue.some((entry) => !entry.sent))
777
+ return this.#armIdleExit();
778
+ try {
779
+ const state = await this.#client.getState();
780
+ if (this.#streaming)
781
+ return this.#armIdleExit();
782
+ if (state.isStreaming)
783
+ return this.#armIdleExit();
784
+ }
785
+ catch {
786
+ // Self-attestation unavailable: local signals remain authoritative.
787
+ }
788
+ try {
789
+ const subagents = await this.#client.getSubagents();
790
+ if (this.#streaming)
791
+ return this.#armIdleExit();
792
+ if (subagents.length > 0)
793
+ return this.#armIdleExit();
794
+ }
795
+ catch {
796
+ // getSubagents fail-softs already; belt-and-suspenders.
797
+ }
798
+ if (this.#streaming)
799
+ return this.#armIdleExit();
800
+ trace(`idle exit after ${CODEX_IDLE_EXIT_MS}ms — disposing agent ${this.id}`);
801
+ this.#onIdleExit?.();
802
+ }
803
+ #fail(error) {
804
+ this.#abandonStreamBridge();
805
+ if (this.#disposed)
806
+ return;
807
+ const classified = classifyNativeFailure(error);
808
+ trace(`#fail [${classified.code}] ${classified.message.slice(0, 200)}`);
809
+ if (classified.code === "ROLLOUT_MISSING" || classified.code === "NATIVE_REJECTED") {
810
+ // The native runtime rejected this session's thread at turn time.
811
+ // Mark the identity map so the next resume fails fast with the stable
812
+ // code instead of burning another turn on a dead rollout (plan RC-4).
813
+ this.#runtimeInfo?.markNotResumable?.();
814
+ }
815
+ // Flush a buffered final assistant (usage-less — the turn failed) so
816
+ // failure paths never drop the turn's visible text.
817
+ this.#flushDeferredAssistant();
818
+ this.#closeTurn({ kind: "error", error: classified });
819
+ this.#markIdle();
820
+ this.#endActivity();
821
+ }
822
+ #handleEvent(event) {
823
+ switch (event.type) {
824
+ case "agent_start":
825
+ trace("event agent_start");
826
+ this.#markRunning();
827
+ this.#flushRemote();
828
+ break;
829
+ case "turn_start":
830
+ // Defer `step/start`: the step's owning turn is only known once its
831
+ // first message arrives (a queued follow-up opens a fresh turn).
832
+ trace("event turn_start");
833
+ this.#stepStartPending = true;
834
+ this.#pendingFailure = null;
835
+ // Open the step immediately (the SDK may never send message_start)
836
+ // and try the system prompt now that the thread's rollout exists.
837
+ this.#commitStepStart();
838
+ this.#emitSystemMessage();
839
+ break;
840
+ case "message_start": {
841
+ const message = event.message;
842
+ if (message?.role === "user") {
843
+ if (this.#localUserPending) {
844
+ // The client is echoing the user message #openTurn already appended.
845
+ this.#localUserPending = false;
846
+ }
847
+ else {
848
+ // A queued follow-up / steering message delivered by the client.
849
+ this.#bridgeRemoteUser(message);
850
+ }
851
+ }
852
+ else if (message?.role === "assistant") {
853
+ // Reset per-message streaming accumulators.
854
+ this.#abandonStreamBridge();
855
+ }
856
+ // role === "toolResult" is bridged in message_end (deduped vs tool_execution_end).
857
+ this.#commitStepStart();
858
+ // The Codex system prompt is stamped inside the first OPEN step of the
859
+ // first turn (the rollout HEAD only exists once the thread started).
860
+ this.#emitSystemMessage();
861
+ break;
862
+ }
863
+ case "message_update": {
864
+ const delta = event.assistantMessageEvent;
865
+ if (delta !== undefined)
866
+ this.#handleUpdate(delta);
867
+ break;
868
+ }
869
+ case "message_end": {
870
+ const message = event.message;
871
+ if (message === undefined)
872
+ break;
873
+ const role = message.role;
874
+ if (role === "assistant") {
875
+ const failure = wireFailure(message);
876
+ if (failure !== undefined) {
877
+ this.#pendingFailure = failure;
878
+ this.#abandonStreamBridge();
879
+ }
880
+ else {
881
+ // Buffer as the turn's final assistant candidate: flushed at
882
+ // `turn_end` with the SAME turn's usage (or earlier on failure).
883
+ this.#deferAssistant(message);
884
+ }
885
+ }
886
+ else if (role === "toolResult") {
887
+ this.#appendToolResultMessage(message);
888
+ }
889
+ else if (role !== "user") {
890
+ process.stderr.write(`codex wire: unhandled message_end role "${role}"\n`);
891
+ }
892
+ break;
893
+ }
894
+ case "tool_execution_start": {
895
+ const callId = String(event.toolCallId ?? "");
896
+ const name = String(event.toolName ?? "");
897
+ this.#commitStepStart();
898
+ this.session.append("tool/call", {
899
+ turn: this.#dashTurn,
900
+ step: this.#step,
901
+ callId: ToolCallId(callId),
902
+ name,
903
+ // The DSH payload requires the RAW JSON string the model produced;
904
+ // the wire carries it verbatim for MCP calls and renders it from the
905
+ // discrete fields for the other item shapes.
906
+ arguments: String(event.argumentsJson ?? JSON.stringify(event.args ?? {})),
907
+ });
908
+ break;
909
+ }
910
+ case "tool_execution_end":
911
+ this.#appendToolResult(event);
912
+ break;
913
+ case "todo_write":
914
+ // DSH `todo/write` (log-only, latest-wins whole-list snapshot) —
915
+ // projected by the todo_list tool item alongside its tool pair.
916
+ // The locally-resolved dsh-session build does not declare the
917
+ // tool-todo vocabulary, so the append goes through a narrow cast
918
+ // (same seam as the model/selection append in index.ts).
919
+ // The locally-resolved dsh-session build does not declare the
920
+ // tool-todo vocabulary (no dsh-tool-todo dependency), so the append
921
+ // goes through a narrow cast (same seam as model/selection in index.ts).
922
+ this.session.append("todo/write", {
923
+ todos: Array.isArray(event.todos) ? event.todos : [],
924
+ });
925
+ break;
926
+ case "turn_end":
927
+ trace("event turn_end");
928
+ // Last chance for the system prompt: by now the thread's rollout HEAD
929
+ // exists for certain. Appended inside the still-open step (before
930
+ // step/end) so the step-scope invariant holds; a no-op once stamped.
931
+ this.#emitSystemMessage();
932
+ // Land THIS turn's usage on the turn's final assistant message — the
933
+ // DSH turn-token fold requires the sample on a message of the SAME
934
+ // turn. Flushed inside the still-open step, BEFORE step/end.
935
+ this.#flushDeferredAssistant(convertUsage(event.data?.usage));
936
+ if (this.#turnOpen && this.#step > 0) {
937
+ this.session.append("step/end", { turn: this.#dashTurn, step: this.#step });
938
+ }
939
+ break;
940
+ case "agent_end": {
941
+ trace(`event agent_end cancelCause=${String(this.#cancelCause)}`);
942
+ const pendingFailure = this.#pendingFailure;
943
+ this.#pendingFailure = null;
944
+ // Backstop flush: aborted/failed turns never saw `turn_end`, so the
945
+ // buffered final assistant flushes here (usage-less) and the visible
946
+ // text survives — the pre-deferral behavior, re-ordered.
947
+ this.#flushDeferredAssistant();
948
+ this.#closeTurn(this.#cancelCause !== null
949
+ ? { kind: "aborted", reason: this.#cancelCause }
950
+ : pendingFailure !== null
951
+ ? { kind: "error", error: pendingFailure }
952
+ : { kind: "completed" });
953
+ this.#cancelCause = null;
954
+ this.#markIdle();
955
+ this.#endActivity();
956
+ // Live metadata the Dash surface has no projection seam for yet:
957
+ // surface the client's own accounting in the diagnostic trace so it
958
+ // is observable per completed turn.
959
+ void this.#client
960
+ .getSessionStats()
961
+ .then((stats) => {
962
+ trace(`sessionStats tokens=${JSON.stringify(stats.tokens ?? {})}`);
963
+ })
964
+ .catch(() => { });
965
+ break;
966
+ }
967
+ }
968
+ }
969
+ /** The open stream bridge for this message, starting one on first use. */
970
+ #ensureStreamBridge() {
971
+ if (this.#streamBridge === undefined || this.#streamBridge.ended) {
972
+ this.#streamBridge = new AssistantStreamBridge(this.session.id, ++this.#assistantAttemptCounter, () => ++this.#assistantStreamRevision, (frame) => this.#dispatch.emit("agent/assistant-stream", { frame }));
973
+ this.#streamBridge.start(this.#dashTurn, this.#step);
974
+ }
975
+ return this.#streamBridge;
976
+ }
977
+ /** Abandon any open stream bridge — failure paths never settle. */
978
+ #abandonStreamBridge() {
979
+ if (this.#streamBridge !== undefined && !this.#streamBridge.ended)
980
+ this.#streamBridge.abandon();
981
+ this.#streamBridge = undefined;
982
+ }
983
+ #handleUpdate(delta) {
984
+ switch (delta.type) {
985
+ case "text_delta":
986
+ if (typeof delta.delta === "string" && delta.delta !== "") {
987
+ const chunk = { type: "text-delta", index: delta.contentIndex ?? 0, text: delta.delta };
988
+ // v2: live text no longer appends `assistant/chunk` log events — the
989
+ // stream publishes as dense agent frames and embeds durably in the
990
+ // final assistant/message.
991
+ this.#ensureStreamBridge().push(chunk);
992
+ }
993
+ break;
994
+ default:
995
+ // thinking_*/toolcall_* are folded into the final message.
996
+ break;
997
+ }
998
+ }
999
+ /**
1000
+ * Buffer the turn's final assistant message. An already-buffered message is
1001
+ * flushed first (usage-less — it belonged to an earlier codex turn of the
1002
+ * same Dash turn), so the buffer always holds the LATEST candidate.
1003
+ */
1004
+ #deferAssistant(message) {
1005
+ this.#flushDeferredAssistant();
1006
+ this.#deferredAssistant = message;
1007
+ }
1008
+ /** Append the deferred final assistant message (with `usage` when known); no-op when empty. */
1009
+ #flushDeferredAssistant(usage) {
1010
+ const message = this.#deferredAssistant;
1011
+ if (message === null)
1012
+ return;
1013
+ this.#deferredAssistant = null;
1014
+ this.#appendAssistantMessage(message, usage);
1015
+ }
1016
+ #appendAssistantMessage(message, usage) {
1017
+ const content = convertContent(message.content);
1018
+ const assistant = createAssistantMessage({
1019
+ content,
1020
+ source: {
1021
+ // Rollout/wire messages may carry no attribution: fall back to this
1022
+ // agent's options (the omp bridge did the same).
1023
+ provider: String(message.provider ?? this.options.provider ?? ""),
1024
+ model: String(message.model ?? this.options.model ?? ""),
1025
+ },
1026
+ });
1027
+ const bridge = this.#streamBridge;
1028
+ this.#commitStepStart();
1029
+ // v2: the attempt's timed chunks embed in the event itself;
1030
+ // `sourceEventSeqs` is forbidden on assistant/message in v2.
1031
+ const event = this.session.append("assistant/message", {
1032
+ turn: this.#dashTurn,
1033
+ step: this.#step,
1034
+ message: assistant,
1035
+ stream: bridge?.records ?? [],
1036
+ ...(usage === undefined ? {} : { usage }),
1037
+ }, {
1038
+ surfaceOp: "append",
1039
+ });
1040
+ if (bridge !== undefined && !bridge.ended)
1041
+ bridge.settle(event.seq);
1042
+ this.#streamBridge = undefined;
1043
+ }
1044
+ #appendToolResult(event) {
1045
+ const callId = String(event.toolCallId ?? "");
1046
+ this.#bridgedToolResults.add(callId);
1047
+ const result = event.result;
1048
+ const isError = Boolean(event.isError ?? result?.isError ?? false);
1049
+ const message = createToolResultMessage({
1050
+ callId: ToolCallId(callId),
1051
+ content: convertContent(result?.content),
1052
+ isError,
1053
+ });
1054
+ this.session.append("tool/result", {
1055
+ turn: this.#dashTurn,
1056
+ step: this.#step,
1057
+ message,
1058
+ ...(isError ? { error: { name: "ToolExecutionError", code: "TOOL_ERROR" } } : {}),
1059
+ }, {
1060
+ surfaceOp: "append",
1061
+ });
1062
+ }
1063
+ #appendToolResultMessage(message) {
1064
+ const callId = String(message.toolCallId ?? "");
1065
+ // The normal path bridges via `tool_execution_end` first; skip the
1066
+ // `message_end(toolResult)` duplicate the wire always also emits.
1067
+ if (this.#bridgedToolResults.has(callId))
1068
+ return;
1069
+ this.#bridgedToolResults.add(callId);
1070
+ const isError = Boolean(message.isError ?? false);
1071
+ const result = createToolResultMessage({
1072
+ callId: ToolCallId(callId),
1073
+ content: convertContent(message.content),
1074
+ isError,
1075
+ });
1076
+ this.session.append("tool/result", {
1077
+ turn: this.#dashTurn,
1078
+ step: this.#step,
1079
+ message: result,
1080
+ ...(isError ? { error: { name: "ToolExecutionError", code: "TOOL_ERROR" } } : {}),
1081
+ }, {
1082
+ surfaceOp: "append",
1083
+ });
1084
+ }
1085
+ }