@bitkyc08/opencodex 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,280 @@
1
+ import type { TranslatorBudget } from "../lib/translator-budget";
2
+ import {
3
+ customToolItemId,
4
+ restoreRoutedCustomCalls,
5
+ unwrapRoutedCustomToolArguments,
6
+ } from "../responses/custom-tool-compat";
7
+ import {
8
+ replaceSseDataPayload,
9
+ sseDataPayload,
10
+ type SseBlockRewrite,
11
+ } from "./sse-payload-rewrite";
12
+
13
+ /** Exact compact prefix used by our upstream rewriter; progressive matching also
14
+ * tolerates insignificant JSON whitespace via FREEFORM_WRAP_PREFIX_RE. */
15
+ const FREEFORM_WRAP_PREFIX = '{"input":"';
16
+ const FREEFORM_WRAP_PREFIX_RE = /^\s*\{\s*"input"\s*:\s*"/;
17
+
18
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
19
+ return !!value && typeof value === "object" && !Array.isArray(value);
20
+ }
21
+
22
+ /**
23
+ * Progressive decode of the freeform `{ "input": "…" }` wrapper.
24
+ * Returns null when the accumulated text does not (yet) match that wrapper so
25
+ * callers suppress deltas and rely on `response.custom_tool_call_input.done`.
26
+ */
27
+ function partialCustomToolInput(argumentsText: string): string | null {
28
+ const match = FREEFORM_WRAP_PREFIX_RE.exec(argumentsText);
29
+ if (!match) return null;
30
+ const body = argumentsText.slice(match[0]!.length);
31
+ let output = "";
32
+ for (let index = 0; index < body.length; index++) {
33
+ const char = body[index];
34
+ if (char === '"') break;
35
+ if (char !== "\\") {
36
+ output += char;
37
+ continue;
38
+ }
39
+ const escaped = body[index + 1];
40
+ if (escaped === undefined) break;
41
+ index += 1;
42
+ if (escaped === "n") output += "\n";
43
+ else if (escaped === "t") output += "\t";
44
+ else if (escaped === "r") output += "\r";
45
+ else if (escaped === "b") output += "\b";
46
+ else if (escaped === "f") output += "\f";
47
+ else if (escaped === "u") {
48
+ const hex = body.slice(index + 1, index + 5);
49
+ if (hex.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) break;
50
+ output += String.fromCharCode(Number.parseInt(hex, 16));
51
+ index += 4;
52
+ } else output += escaped;
53
+ }
54
+ return output;
55
+ }
56
+
57
+ function replaceSseEventName(block: string, type: string): string {
58
+ const newline = block.includes("\r\n") ? "\r\n" : "\n";
59
+ const lines = block.split(/\r?\n/);
60
+ let replaced = false;
61
+ const next = lines.map(line => {
62
+ if (!replaced && line.startsWith("event:")) {
63
+ replaced = true;
64
+ return `event: ${type}`;
65
+ }
66
+ return line;
67
+ });
68
+ return next.join(newline);
69
+ }
70
+
71
+ type OpenCustomCall = {
72
+ argumentsText: string;
73
+ emittedInput: string;
74
+ retainedBytes: number;
75
+ };
76
+
77
+ type PendingArgumentBlock = {
78
+ block: string;
79
+ itemId?: string;
80
+ outputIndex?: number;
81
+ retainedBytes: number;
82
+ };
83
+
84
+ export function createRoutedCustomToolRestoreBlockRewrite(
85
+ names: ReadonlySet<string>,
86
+ budget?: TranslatorBudget,
87
+ ): SseBlockRewrite {
88
+ const itemNames = new Map<string, string>();
89
+ const ordinaryItemIds = new Set<string>();
90
+ const openCalls = new Map<string, OpenCustomCall>();
91
+ let pendingArguments: PendingArgumentBlock[] = [];
92
+ let disposed = false;
93
+
94
+ const releaseCall = (itemId: string): void => {
95
+ const open = openCalls.get(itemId);
96
+ if (!open) return;
97
+ if (open.retainedBytes > 0) {
98
+ budget?.releaseRetained(open.retainedBytes, { kind: "retained_collectors" });
99
+ }
100
+ openCalls.delete(itemId);
101
+ };
102
+
103
+ const releaseAll = (): void => {
104
+ if (disposed) return;
105
+ disposed = true;
106
+ for (const itemId of openCalls.keys()) releaseCall(itemId);
107
+ const pendingBytes = pendingArguments.reduce((total, pending) => total + pending.retainedBytes, 0);
108
+ if (pendingBytes > 0) {
109
+ budget?.releaseRetained(pendingBytes, { kind: "retained_collectors" });
110
+ }
111
+ pendingArguments = [];
112
+ itemNames.clear();
113
+ ordinaryItemIds.clear();
114
+ };
115
+
116
+ const retainPendingArgument = (
117
+ block: string,
118
+ itemId: string | undefined,
119
+ outputIndex: number | undefined,
120
+ ): void => {
121
+ const retainedBytes = Buffer.byteLength(block, "utf8");
122
+ if (retainedBytes > 0) budget?.chargeRetained(retainedBytes, { kind: "retained_collectors" });
123
+ pendingArguments.push({ block, itemId, outputIndex, retainedBytes });
124
+ };
125
+
126
+ const takePendingArguments = (
127
+ itemId: string | undefined,
128
+ outputIndex: number | undefined,
129
+ ): string[] => {
130
+ const matched: PendingArgumentBlock[] = [];
131
+ const remaining: PendingArgumentBlock[] = [];
132
+ for (const pending of pendingArguments) {
133
+ const matches = pending.itemId !== undefined
134
+ ? itemId !== undefined && pending.itemId === itemId
135
+ : outputIndex !== undefined && pending.outputIndex === outputIndex;
136
+ (matches ? matched : remaining).push(pending);
137
+ }
138
+ pendingArguments = remaining;
139
+ const retainedBytes = matched.reduce((total, pending) => total + pending.retainedBytes, 0);
140
+ if (retainedBytes > 0) {
141
+ budget?.releaseRetained(retainedBytes, { kind: "retained_collectors" });
142
+ }
143
+ // Index-matched entries carry no item id. Stamp the resolved id so replay
144
+ // classifies the event instead of buffering it a second time.
145
+ return matched.map(pending => {
146
+ if (pending.itemId !== undefined || itemId === undefined) return pending.block;
147
+ const payload = sseDataPayload(pending.block);
148
+ if (payload === null) return pending.block;
149
+ try {
150
+ const parsed: unknown = JSON.parse(payload);
151
+ if (!isPlainObject(parsed)) return pending.block;
152
+ return replaceSseDataPayload(pending.block, JSON.stringify({ ...parsed, item_id: itemId }));
153
+ } catch {
154
+ return pending.block;
155
+ }
156
+ });
157
+ };
158
+
159
+ const rewrite: SseBlockRewrite = (block: string): readonly string[] => {
160
+ if (disposed) return [block];
161
+ const payload = sseDataPayload(block);
162
+ if (payload === null || payload === "[DONE]") return [block];
163
+ let parsed: unknown;
164
+ try {
165
+ parsed = JSON.parse(payload);
166
+ } catch {
167
+ return [block];
168
+ }
169
+ if (!isPlainObject(parsed)) return [block];
170
+
171
+ const type = typeof parsed.type === "string" ? parsed.type : "";
172
+ const outputIndex = typeof parsed.output_index === "number"
173
+ && Number.isInteger(parsed.output_index)
174
+ && parsed.output_index >= 0
175
+ ? parsed.output_index
176
+ : undefined;
177
+ if (
178
+ (type === "response.output_item.added" || type === "response.output_item.done")
179
+ && isPlainObject(parsed.item)
180
+ && parsed.item.type === "function_call"
181
+ && typeof parsed.item.name === "string"
182
+ ) {
183
+ const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
184
+ const routed = names.has(parsed.item.name);
185
+ if (upstreamItemId) {
186
+ if (routed) {
187
+ itemNames.set(upstreamItemId, parsed.item.name);
188
+ ordinaryItemIds.delete(upstreamItemId);
189
+ } else {
190
+ ordinaryItemIds.add(upstreamItemId);
191
+ }
192
+ if (routed && type === "response.output_item.added") {
193
+ openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
194
+ }
195
+ }
196
+ const pending = takePendingArguments(upstreamItemId, outputIndex);
197
+ if (!routed) {
198
+ if (type === "response.output_item.done" && upstreamItemId) ordinaryItemIds.delete(upstreamItemId);
199
+ return [...pending, block];
200
+ }
201
+ if (upstreamItemId && pending.length > 0 && !openCalls.has(upstreamItemId)) {
202
+ openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
203
+ }
204
+ const restored = restoreRoutedCustomCalls(parsed, names);
205
+ const restoredBlock = restored.changed
206
+ ? replaceSseDataPayload(block, JSON.stringify(restored.value))
207
+ : block;
208
+ const replayed = pending.flatMap(pendingBlock => rewrite(pendingBlock));
209
+ if (type === "response.output_item.done" && upstreamItemId) releaseCall(upstreamItemId);
210
+ return type === "response.output_item.added"
211
+ ? [restoredBlock, ...replayed]
212
+ : [...replayed, restoredBlock];
213
+ }
214
+
215
+ const upstreamItemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
216
+ const argumentEvent = type === "response.function_call_arguments.delta"
217
+ || type === "response.function_call_arguments.done";
218
+ if (argumentEvent && (!upstreamItemId || (!itemNames.has(upstreamItemId) && !ordinaryItemIds.has(upstreamItemId)))) {
219
+ retainPendingArgument(block, upstreamItemId, outputIndex);
220
+ return [];
221
+ }
222
+ if (
223
+ type === "response.function_call_arguments.delta"
224
+ && upstreamItemId
225
+ && itemNames.has(upstreamItemId)
226
+ ) {
227
+ const open = openCalls.get(upstreamItemId) ?? { argumentsText: "", emittedInput: "", retainedBytes: 0 };
228
+ const delta = typeof parsed.delta === "string" ? parsed.delta : "";
229
+ const deltaBytes = Buffer.byteLength(delta, "utf8");
230
+ if (deltaBytes > 0) budget?.chargeRetained(deltaBytes, { kind: "retained_collectors" });
231
+ open.argumentsText += delta;
232
+ open.retainedBytes += deltaBytes;
233
+ openCalls.set(upstreamItemId, open);
234
+ // Still accumulating toward the compact wrapper, or an unrecognized shape:
235
+ // suppress progressive emission and let the done event carry input.
236
+ if (FREEFORM_WRAP_PREFIX.startsWith(open.argumentsText)) return [];
237
+ const fullInput = partialCustomToolInput(open.argumentsText);
238
+ if (fullInput === null) return [];
239
+ if (!fullInput.startsWith(open.emittedInput) || fullInput.length === open.emittedInput.length) return [];
240
+ const inputDelta = fullInput.slice(open.emittedInput.length);
241
+ open.emittedInput = fullInput;
242
+ const nextType = "response.custom_tool_call_input.delta";
243
+ const next = {
244
+ ...parsed,
245
+ type: nextType,
246
+ item_id: customToolItemId(upstreamItemId),
247
+ delta: inputDelta,
248
+ };
249
+ return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
250
+ }
251
+
252
+ if (
253
+ type === "response.function_call_arguments.done"
254
+ && upstreamItemId
255
+ && itemNames.has(upstreamItemId)
256
+ ) {
257
+ const nextType = "response.custom_tool_call_input.done";
258
+ const source = typeof parsed.arguments === "string"
259
+ ? parsed.arguments
260
+ : openCalls.get(upstreamItemId)?.argumentsText ?? "";
261
+ const { arguments: _arguments, ...rest } = parsed;
262
+ const next = {
263
+ ...rest,
264
+ type: nextType,
265
+ item_id: customToolItemId(upstreamItemId),
266
+ input: unwrapRoutedCustomToolArguments(source),
267
+ };
268
+ return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
269
+ }
270
+
271
+ const restored = restoreRoutedCustomCalls(parsed, names);
272
+ const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
273
+ if (terminal) releaseAll();
274
+ return restored.changed
275
+ ? [replaceSseDataPayload(block, JSON.stringify(restored.value))]
276
+ : [block];
277
+ };
278
+ rewrite.dispose = releaseAll;
279
+ return rewrite;
280
+ }
@@ -0,0 +1,342 @@
1
+ import type { TranslatorBudget } from "../lib/translator-budget";
2
+ import type { ResponsesTerminalRepairPolicy } from "../providers/registry";
3
+ import { nextSseBlock, sseDataPayload } from "./sse-payload-rewrite";
4
+
5
+ export interface ResponsesTerminalRepairScheduler {
6
+ nowMs(): number;
7
+ schedule(callback: () => void, delayMs: number): unknown;
8
+ cancel(handle: unknown): void;
9
+ }
10
+
11
+ const systemScheduler: ResponsesTerminalRepairScheduler = {
12
+ nowMs: () => Date.now(),
13
+ schedule(callback, delayMs) {
14
+ const handle = setTimeout(callback, delayMs);
15
+ (handle as { unref?: () => void }).unref?.();
16
+ return handle;
17
+ },
18
+ cancel(handle) { clearTimeout(handle as ReturnType<typeof setTimeout>); },
19
+ };
20
+
21
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ function outputIndex(value: unknown): number | null {
26
+ return Number.isInteger(value) && (value as number) >= 0 ? value as number : null;
27
+ }
28
+
29
+ function isCompleteItem(item: Record<string, unknown>): boolean {
30
+ if (item.status !== "completed") return false;
31
+ if (item.type === "reasoning") {
32
+ return typeof item.id === "string" && item.id.length > 0
33
+ && Array.isArray(item.content)
34
+ && item.content.every(part => isPlainRecord(part)
35
+ && part.type === "reasoning_text" && typeof part.text === "string");
36
+ }
37
+ if (item.type === "message") {
38
+ return typeof item.id === "string" && item.id.length > 0
39
+ && item.role === "assistant" && Array.isArray(item.content)
40
+ && item.content.every(part => isPlainRecord(part)
41
+ && part.type === "output_text" && typeof part.text === "string");
42
+ }
43
+ if (item.type === "function_call") {
44
+ if (typeof item.id !== "string" || item.id.length === 0) return false;
45
+ if (typeof item.call_id !== "string" || item.call_id.length === 0) return false;
46
+ if (typeof item.name !== "string" || item.name.length === 0) return false;
47
+ if (typeof item.arguments !== "string") return false;
48
+ try {
49
+ return isPlainRecord(JSON.parse(item.arguments));
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
57
+ /**
58
+ * Relay a native Responses SSE body while repairing the narrow DeepSeek shape where every
59
+ * output item is complete but the protocol terminal is missing or indefinitely delayed.
60
+ */
61
+ export function relayResponsesSseWithTerminalRepair(
62
+ body: ReadableStream<Uint8Array>,
63
+ upstream: AbortController,
64
+ policy: ResponsesTerminalRepairPolicy,
65
+ budget: TranslatorBudget,
66
+ scheduler: ResponsesTerminalRepairScheduler = systemScheduler,
67
+ ): ReadableStream<Uint8Array> {
68
+ const reader = body.getReader();
69
+ const decoder = new TextDecoder();
70
+ const encoder = new TextEncoder();
71
+ const added = new Map<number, { type?: unknown; id?: unknown }>();
72
+ const completed = new Map<number, { item: Record<string, unknown>; bytes: number }>();
73
+ let created: Record<string, unknown> | null = null;
74
+ let createdBytes = 0;
75
+ let maxSequence = -1;
76
+ let buffer = "";
77
+ let bufferBytes = 0;
78
+ let timer: unknown;
79
+ let timerGeneration = 0;
80
+ let realTerminalSeen = false;
81
+ let tainted = false;
82
+ let disposed = false;
83
+ let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
84
+ let activeRead: Promise<void> | null = null;
85
+
86
+ const onUpstreamAbort = (): void => {
87
+ if (disposed) return;
88
+ dispose();
89
+ reader.cancel(upstream.signal.reason).catch(() => {});
90
+ try { controllerRef?.close(); } catch { /* already closed */ }
91
+ };
92
+
93
+ const releaseRetainedState = (): void => {
94
+ if (createdBytes > 0) budget.releaseRetained(createdBytes, { kind: "retained_collectors" });
95
+ createdBytes = 0;
96
+ for (const retained of completed.values()) {
97
+ budget.releaseRetained(retained.bytes, { kind: "retained_collectors" });
98
+ }
99
+ completed.clear();
100
+ created = null;
101
+ };
102
+
103
+ const releaseBuffer = (): void => {
104
+ if (bufferBytes > 0) budget.releaseRetained(bufferBytes, { kind: "live_transient" });
105
+ buffer = "";
106
+ bufferBytes = 0;
107
+ };
108
+
109
+ const cancelTimer = (): void => {
110
+ timerGeneration += 1;
111
+ if (timer !== undefined) scheduler.cancel(timer);
112
+ timer = undefined;
113
+ };
114
+
115
+ const dispose = (): void => {
116
+ if (disposed) return;
117
+ disposed = true;
118
+ upstream.signal.removeEventListener("abort", onUpstreamAbort);
119
+ cancelTimer();
120
+ releaseRetainedState();
121
+ releaseBuffer();
122
+ };
123
+
124
+ const replaceBuffer = (next: string): void => {
125
+ const nextBytes = encoder.encode(next).byteLength;
126
+ const reservation = budget.reserveTransient(nextBytes, { kind: "live_transient" });
127
+ reservation.commitRetained();
128
+ if (bufferBytes > 0) budget.releaseRetained(bufferBytes, { kind: "live_transient" });
129
+ buffer = next;
130
+ bufferBytes = nextBytes;
131
+ };
132
+
133
+ const appendBuffer = (fragment: string): void => {
134
+ if (!fragment) return;
135
+ replaceBuffer(buffer + fragment);
136
+ };
137
+
138
+ const retainCreated = (response: Record<string, unknown>): void => {
139
+ const bytes = encoder.encode(JSON.stringify(response)).byteLength;
140
+ budget.chargeRetained(bytes, { kind: "retained_collectors" });
141
+ if (createdBytes > 0) budget.releaseRetained(createdBytes, { kind: "retained_collectors" });
142
+ created = response;
143
+ createdBytes = bytes;
144
+ };
145
+
146
+ const retainCompleted = (index: number, item: Record<string, unknown>): void => {
147
+ const bytes = encoder.encode(JSON.stringify(item)).byteLength;
148
+ budget.chargeRetained(bytes, { kind: "retained_collectors" });
149
+ const previous = completed.get(index);
150
+ if (previous) budget.releaseRetained(previous.bytes, { kind: "retained_collectors" });
151
+ completed.set(index, { item, bytes });
152
+ };
153
+
154
+ const completeCandidate = (): boolean => {
155
+ if (realTerminalSeen || tainted || !created || completed.size === 0 || added.size !== completed.size) return false;
156
+ for (const index of added.keys()) {
157
+ const retained = completed.get(index);
158
+ if (!retained || !isCompleteItem(retained.item)) return false;
159
+ }
160
+ for (const index of completed.keys()) if (!added.has(index)) return false;
161
+ return true;
162
+ };
163
+
164
+ const syntheticTerminal = (kind: "completed" | "incomplete"): Uint8Array => {
165
+ const output = [...completed.entries()]
166
+ .sort(([left], [right]) => left - right)
167
+ .map(([, retained]) => retained.item);
168
+ const response = {
169
+ ...(created ?? {}),
170
+ status: kind,
171
+ completed_at: Math.floor(scheduler.nowMs() / 1_000),
172
+ output,
173
+ ...(kind === "incomplete"
174
+ ? { incomplete_details: { reason: "missing_terminal_event" } }
175
+ : {}),
176
+ };
177
+ const type = `response.${kind}`;
178
+ return encoder.encode(`event: ${type}\ndata: ${JSON.stringify({
179
+ type,
180
+ response,
181
+ sequence_number: maxSequence + 1,
182
+ })}\n\n`);
183
+ };
184
+
185
+ const emitSynthetic = (
186
+ kind: "completed" | "incomplete",
187
+ controller: ReadableStreamDefaultController<Uint8Array>,
188
+ ): boolean => {
189
+ if (disposed || realTerminalSeen) return false;
190
+ realTerminalSeen = true;
191
+ cancelTimer();
192
+ controller.enqueue(syntheticTerminal(kind));
193
+ releaseRetainedState();
194
+ return true;
195
+ };
196
+
197
+ const commitSynthetic = (generation: number): void => {
198
+ if (disposed || realTerminalSeen || generation !== timerGeneration || !completeCandidate()) return;
199
+ timer = undefined;
200
+ try {
201
+ if (controllerRef && emitSynthetic("completed", controllerRef)) controllerRef.close();
202
+ } catch {
203
+ /* downstream already closed */
204
+ }
205
+ reader.cancel("Responses terminal repaired after complete output").catch(() => {});
206
+ dispose();
207
+ };
208
+
209
+ const maybeArmTimer = (): void => {
210
+ if (!completeCandidate()) return;
211
+ const generation = timerGeneration;
212
+ timer = scheduler.schedule(() => commitSynthetic(generation), policy.graceMs);
213
+ };
214
+
215
+ const inspectPayload = (payload: string | null): "done" | "ordinary" => {
216
+ if (payload === "[DONE]") return "done";
217
+ if (!payload || realTerminalSeen) return "ordinary";
218
+ let parsed: unknown;
219
+ try {
220
+ parsed = JSON.parse(payload);
221
+ } catch {
222
+ tainted = true;
223
+ return "ordinary";
224
+ }
225
+ if (!isPlainRecord(parsed)) {
226
+ tainted = true;
227
+ return "ordinary";
228
+ }
229
+ if (Number.isInteger(parsed.sequence_number)) {
230
+ maxSequence = Math.max(maxSequence, parsed.sequence_number as number);
231
+ }
232
+ const type = parsed.type;
233
+ if (type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
234
+ realTerminalSeen = true;
235
+ cancelTimer();
236
+ releaseRetainedState();
237
+ return "ordinary";
238
+ }
239
+
240
+ cancelTimer();
241
+ if (type === "response.created" && isPlainRecord(parsed.response)) {
242
+ retainCreated(parsed.response);
243
+ } else if (type === "response.output_item.added") {
244
+ const index = outputIndex(parsed.output_index);
245
+ if (index === null || !isPlainRecord(parsed.item) || added.has(index) || completed.has(index)) {
246
+ tainted = true;
247
+ } else {
248
+ added.set(index, { type: parsed.item.type, id: parsed.item.id });
249
+ }
250
+ } else if (type === "response.output_item.done") {
251
+ const index = outputIndex(parsed.output_index);
252
+ if (index === null || !isPlainRecord(parsed.item) || !added.has(index) || completed.has(index)) {
253
+ tainted = true;
254
+ } else {
255
+ const opened = added.get(index)!;
256
+ if (opened.type !== parsed.item.type || opened.id !== parsed.item.id) tainted = true;
257
+ retainCompleted(index, parsed.item);
258
+ }
259
+ }
260
+ maybeArmTimer();
261
+ return "ordinary";
262
+ };
263
+
264
+ const emitBlocks = (
265
+ controller: ReadableStreamDefaultController<Uint8Array>,
266
+ ): { closed: boolean; emitted: boolean } => {
267
+ let next: ReturnType<typeof nextSseBlock>;
268
+ let emitted = false;
269
+ while ((next = nextSseBlock(buffer))) {
270
+ replaceBuffer(next.rest);
271
+ const kind = inspectPayload(sseDataPayload(next.block));
272
+ if (kind === "done" && !realTerminalSeen) {
273
+ emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
274
+ }
275
+ controller.enqueue(encoder.encode(next.block + next.delimiter));
276
+ emitted = true;
277
+ if (kind === "done") {
278
+ reader.cancel("Responses stream ended with DONE").catch(() => {});
279
+ dispose();
280
+ controller.close();
281
+ return { closed: true, emitted };
282
+ }
283
+ }
284
+ return { closed: false, emitted };
285
+ };
286
+
287
+ const readOnce = async (controller: ReadableStreamDefaultController<Uint8Array>): Promise<void> => {
288
+ try {
289
+ for (;;) {
290
+ const { done, value } = await reader.read();
291
+ if (disposed) return;
292
+ if (done) {
293
+ appendBuffer(decoder.decode());
294
+ if (buffer.length > 0) {
295
+ // A delimiter-less suffix is not a complete SSE event. Preserve the
296
+ // upstream bytes for passthrough compatibility, but never let a
297
+ // truncated lifecycle frame establish synthetic success.
298
+ tainted = true;
299
+ controller.enqueue(encoder.encode(buffer));
300
+ }
301
+ if (!realTerminalSeen) {
302
+ emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
303
+ }
304
+ releaseBuffer();
305
+ dispose();
306
+ controller.close();
307
+ return;
308
+ }
309
+ appendBuffer(decoder.decode(value, { stream: true }));
310
+ const result = emitBlocks(controller);
311
+ if (result.closed || result.emitted) return;
312
+ }
313
+ } catch (error) {
314
+ if (disposed) return;
315
+ dispose();
316
+ controller.error(error);
317
+ }
318
+ };
319
+
320
+ return new ReadableStream<Uint8Array>({
321
+ start(controller) {
322
+ controllerRef = controller;
323
+ if (upstream.signal.aborted) {
324
+ onUpstreamAbort();
325
+ return;
326
+ }
327
+ upstream.signal.addEventListener("abort", onUpstreamAbort, { once: true });
328
+ },
329
+ pull(controller) {
330
+ if (disposed) return;
331
+ if (!activeRead) {
332
+ activeRead = readOnce(controller).finally(() => { activeRead = null; });
333
+ }
334
+ return activeRead;
335
+ },
336
+ cancel(reason) {
337
+ dispose();
338
+ upstream.abort(reason);
339
+ return reader.cancel(reason);
340
+ },
341
+ });
342
+ }
@@ -164,10 +164,17 @@ function runCliInstall(
164
164
  const cli = join(import.meta.dir, "..", "cli", "index.ts");
165
165
  const argv = [cli, ...startupInstallArgv(action, options)];
166
166
  return new Promise((resolve, reject) => {
167
+ const timeout = process.platform === "win32" && action === "install-service"
168
+ ? 0
169
+ : 60_000;
167
170
  execFile(bun, argv, {
168
171
  encoding: "utf8",
169
172
  env: process.env,
170
- timeout: 60_000,
173
+ // A fresh Windows scheduler install now owns its UAC prompt inside this CLI
174
+ // transaction. Killing only the CLI at 60s can orphan its elevated schtasks child,
175
+ // which may register the task after the Dashboard has reported failure. Keep the
176
+ // async request/attempt lock alive until Windows returns approval or cancellation.
177
+ timeout,
171
178
  windowsHide: true,
172
179
  maxBuffer: 256 * 1024,
173
180
  }, (error, stdout, stderr) => {