@bitkyc08/opencodex 2.76.0 → 2.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../responses/custom-tool-compat";
|
|
14
14
|
import {
|
|
15
15
|
replaceSseDataPayload,
|
|
16
|
+
splitSseBlock,
|
|
16
17
|
sseDataPayload,
|
|
17
18
|
type SseBlockRewrite,
|
|
18
19
|
} from "./sse-payload-rewrite";
|
|
@@ -22,8 +23,7 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function replaceSseEventName(block: string, type: string): string {
|
|
25
|
-
const newline = block
|
|
26
|
-
const lines = block.split(/\r?\n/);
|
|
26
|
+
const { newline, lines } = splitSseBlock(block);
|
|
27
27
|
let replaced = false;
|
|
28
28
|
const next = lines.map(line => {
|
|
29
29
|
if (!replaced && line.startsWith("event:")) {
|
|
@@ -2,6 +2,8 @@ import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { join, posix } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
5
|
+
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
6
|
+
import { TRANSLATOR_MAX_TURN_BYTES } from "../lib/translator-budget";
|
|
5
7
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
6
8
|
import {
|
|
7
9
|
chargeImageBudget, createImageBudget, decodeValidatedImageBase64,
|
|
@@ -12,6 +14,16 @@ import { sseDataPayload, replaceSseDataPayload, type SseBlockRewrite } from "./s
|
|
|
12
14
|
type Row = Record<string, any>;
|
|
13
15
|
const object = (v: unknown): v is Row => !!v && typeof v === "object" && !Array.isArray(v);
|
|
14
16
|
const originators = new Set(["codex_cli_rs", "Codex Desktop", "codex_app", "codex_work_desktop"]);
|
|
17
|
+
const MAX_DISPLAY_ITEMS = 128;
|
|
18
|
+
const MAX_ITEM_METADATA_BYTES = 64 * 1024;
|
|
19
|
+
const MAX_RETAINED_METADATA_BYTES = 1024 * 1024;
|
|
20
|
+
const displayKey = (item: Row, index: number) => typeof item.id === "string" ? "id:" + item.id : "index:" + index;
|
|
21
|
+
|
|
22
|
+
function displayBytes(value: unknown, limit: number): number {
|
|
23
|
+
try { return jsonUtf8Bytes(value, limit); }
|
|
24
|
+
catch { throw new RangeError("hosted image result exceeds local display limits"); }
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
const generatedName = /^img-codex-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\.(png|jpg|webp|gif)$/;
|
|
16
28
|
|
|
17
29
|
/** One spelling per local path: file URL or plain, either separator, dot segments, folded case. */
|
|
@@ -71,13 +83,20 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
71
83
|
const states = new Map<string, { message: Row; added: boolean; done: boolean }>();
|
|
72
84
|
let extraSequence = 0;
|
|
73
85
|
let wroteArtifact = false;
|
|
86
|
+
let retainedMetadataBytes = 0;
|
|
74
87
|
|
|
75
88
|
function stateFor(item: Row, index: number) {
|
|
76
|
-
|
|
89
|
+
// Validate every occurrence, including repeats whose state is already complete.
|
|
90
|
+
const metadataBytes = displayBytes({ id: item.id,
|
|
91
|
+
metadata: item.internal_chat_message_metadata_passthrough }, MAX_ITEM_METADATA_BYTES);
|
|
92
|
+
const key = displayKey(item, index);
|
|
77
93
|
let state = states.get(key);
|
|
78
94
|
if (state) return state;
|
|
79
|
-
|
|
80
|
-
if (
|
|
95
|
+
if (states.size >= MAX_DISPLAY_ITEMS) throw new RangeError("hosted image result count exceeds local display limit");
|
|
96
|
+
if (metadataBytes > MAX_RETAINED_METADATA_BYTES - retainedMetadataBytes) {
|
|
97
|
+
throw new RangeError("hosted image result exceeds local display limits");
|
|
98
|
+
}
|
|
99
|
+
retainedMetadataBytes += metadataBytes;
|
|
81
100
|
const id = "msg_ocx_img_" + createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
82
101
|
state = { added: false, done: false, message: {
|
|
83
102
|
// Persist image results outside the client's collapsible progress messages.
|
|
@@ -147,15 +166,51 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
147
166
|
return events;
|
|
148
167
|
}
|
|
149
168
|
|
|
150
|
-
function
|
|
169
|
+
function projectedMessage(message: Row): Row {
|
|
170
|
+
if (message.status !== "in_progress") return message;
|
|
171
|
+
// Longest possible local filename and JSON text size, without decoding or writing an image.
|
|
172
|
+
const pathText = ", "img-codex-" + "0".repeat(36) + ".webp") + ">)";
|
|
173
|
+
const fallback = "The completed image result has no supported image data for local display.";
|
|
174
|
+
const text = displayBytes(pathText, TRANSLATOR_MAX_TURN_BYTES) > displayBytes(fallback, TRANSLATOR_MAX_TURN_BYTES)
|
|
175
|
+
? pathText : fallback;
|
|
176
|
+
return { ...message, status: "incomplete", content: [{ type: "output_text", text, annotations: [] }] };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function lifecycleBytes(message: Row): number {
|
|
180
|
+
// At most two item copies and four text copies, plus fixed event fields, ids and numeric indices.
|
|
181
|
+
return 2 * displayBytes(message, TRANSLATOR_MAX_TURN_BYTES)
|
|
182
|
+
+ 4 * displayBytes(message.content[0].text, TRANSLATOR_MAX_TURN_BYTES) + 2048;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function snapshot(response: Row, terminal: boolean, events?: Row[], envelope?: Row): Row {
|
|
151
186
|
if (!Array.isArray(response.output)) return response;
|
|
152
|
-
//
|
|
153
|
-
const keys = new Set(
|
|
187
|
+
// Preflight the complete snapshot before any artifact writes or expanded serialization.
|
|
188
|
+
const keys = new Set<string>();
|
|
189
|
+
const ids = new Set<string>();
|
|
154
190
|
for (const [index, item] of response.output.entries()) {
|
|
155
191
|
if (!object(item) || item.type !== "image_generation_call") continue;
|
|
156
|
-
|
|
157
|
-
if (keys.
|
|
192
|
+
const key = displayKey(item, index);
|
|
193
|
+
if (keys.has(key)) throw new RangeError("duplicate hosted image identity in local display snapshot");
|
|
194
|
+
keys.add(key);
|
|
195
|
+
if (typeof item.id === "string") ids.add(item.id);
|
|
196
|
+
stateFor(item, index);
|
|
197
|
+
}
|
|
198
|
+
if (keys.size === 0) return response;
|
|
199
|
+
const seen = new Set<string>();
|
|
200
|
+
for (const item of response.output) {
|
|
201
|
+
if (!object(item) || !ids.has(item.id)) continue;
|
|
202
|
+
if (seen.has(item.id)) throw new RangeError("conflicting hosted image identity in local display snapshot");
|
|
203
|
+
seen.add(item.id);
|
|
158
204
|
}
|
|
205
|
+
let extraBytes = 32; // Sequence-number growth on a rewritten SSE envelope.
|
|
206
|
+
const preview = { ...response, output: response.output.map((item: unknown, index: number) => {
|
|
207
|
+
if (!object(item) || item.type !== "image_generation_call") return item;
|
|
208
|
+
const state = stateFor(item, index);
|
|
209
|
+
const message = terminal ? projectedMessage(state.message) : state.message;
|
|
210
|
+
if (events && terminal && !state.done) extraBytes += lifecycleBytes(message);
|
|
211
|
+
return message;
|
|
212
|
+
}) };
|
|
213
|
+
displayBytes(envelope ? { ...envelope, response: preview } : preview, TRANSLATOR_MAX_TURN_BYTES - extraBytes);
|
|
159
214
|
let changed = false;
|
|
160
215
|
const output = response.output.map((item: unknown, index: number) => {
|
|
161
216
|
if (!object(item) || item.type !== "image_generation_call") return item;
|
|
@@ -182,9 +237,14 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
182
237
|
&& (hasIndex || typeof event.item.id === "string")) {
|
|
183
238
|
if (event.type === "response.output_item.added") {
|
|
184
239
|
const state = stateFor(event.item, index);
|
|
240
|
+
if (!state.added) displayBytes({ ...event, item: state.message }, TRANSLATOR_MAX_TURN_BYTES - 32);
|
|
185
241
|
result = state.added ? [] : [{ ...event, item: state.message }];
|
|
186
242
|
state.added = true;
|
|
187
243
|
} else if (event.type === "response.output_item.done") {
|
|
244
|
+
const state = stateFor(event.item, index);
|
|
245
|
+
if (!state.done && lifecycleBytes(projectedMessage(state.message)) > TRANSLATOR_MAX_TURN_BYTES) {
|
|
246
|
+
throw new RangeError("hosted image result exceeds local display limits");
|
|
247
|
+
}
|
|
188
248
|
result = doneEvents(event.item, index);
|
|
189
249
|
}
|
|
190
250
|
} else if (typeof event.type === "string" && event.type.startsWith("response.image_generation_call.")) {
|
|
@@ -193,7 +253,7 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
193
253
|
} else if (object(event.response)) {
|
|
194
254
|
const terminal = ["response.completed", "response.incomplete", "response.failed"].includes(event.type);
|
|
195
255
|
const injected: Row[] = [];
|
|
196
|
-
const response = snapshot(event.response, terminal, injected);
|
|
256
|
+
const response = snapshot(event.response, terminal, injected, event);
|
|
197
257
|
if (response !== event.response) result = [...injected, { ...event, response }];
|
|
198
258
|
}
|
|
199
259
|
const sequence = Number.isSafeInteger(event.sequence_number) ? event.sequence_number + extraSequence : undefined;
|
|
@@ -216,6 +276,7 @@ export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text
|
|
|
216
276
|
};
|
|
217
277
|
rewrite.dispose = () => {
|
|
218
278
|
states.clear();
|
|
279
|
+
retainedMetadataBytes = 0;
|
|
219
280
|
if (wroteArtifact) {
|
|
220
281
|
wroteArtifact = false;
|
|
221
282
|
pruneArtifacts();
|
|
@@ -91,6 +91,7 @@ export function relayResponsesSseWithTerminalRepair(
|
|
|
91
91
|
let maxSequence = -1;
|
|
92
92
|
let buffer = "";
|
|
93
93
|
let bufferBytes = 0;
|
|
94
|
+
let pendingLineFeed = false;
|
|
94
95
|
let timer: unknown;
|
|
95
96
|
let timerGeneration = 0;
|
|
96
97
|
let realTerminalSeen = false;
|
|
@@ -284,6 +285,7 @@ export function relayResponsesSseWithTerminalRepair(
|
|
|
284
285
|
let emitted = false;
|
|
285
286
|
while ((next = nextSseBlock(buffer))) {
|
|
286
287
|
replaceBuffer(next.rest);
|
|
288
|
+
pendingLineFeed = next.delimiter.endsWith("\r") && buffer.length === 0;
|
|
287
289
|
const kind = inspectPayload(sseDataPayload(next.block));
|
|
288
290
|
if (kind === "done" && !realTerminalSeen) {
|
|
289
291
|
emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
|
|
@@ -327,9 +329,20 @@ export function relayResponsesSseWithTerminalRepair(
|
|
|
327
329
|
controller.close();
|
|
328
330
|
return;
|
|
329
331
|
}
|
|
330
|
-
|
|
332
|
+
let fragment = decoder.decode(value, { stream: true });
|
|
333
|
+
let continuedDelimiter = false;
|
|
334
|
+
if (pendingLineFeed && fragment.length > 0) {
|
|
335
|
+
pendingLineFeed = false;
|
|
336
|
+
if (fragment.startsWith("\n")) {
|
|
337
|
+
// This byte completes an already-forwarded CR delimiter, not a new EOF suffix.
|
|
338
|
+
controller.enqueue(encoder.encode("\n"));
|
|
339
|
+
fragment = fragment.slice(1);
|
|
340
|
+
continuedDelimiter = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
appendBuffer(fragment);
|
|
331
344
|
const result = emitBlocks(controller);
|
|
332
|
-
if (result.closed || result.emitted) return;
|
|
345
|
+
if (result.closed || result.emitted || continuedDelimiter) return;
|
|
333
346
|
}
|
|
334
347
|
} catch (error) {
|
|
335
348
|
if (disposed) return;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
|
|
2
|
+
import { sseDataPayload } from "./sse-payload-rewrite";
|
|
2
3
|
|
|
3
4
|
export const MAX_CLIENT_SSE_FRAME_BYTES = 4 * 1024 * 1024;
|
|
4
5
|
|
|
5
6
|
export const EMPTY_BYTES = new Uint8Array(0);
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// Intern every legal pair; a split CRLF may leave a framing-only LF continuation.
|
|
9
|
+
const DELIMITERS = new Map([
|
|
10
|
+
"\n", "\n\n", "\n\r", "\n\r\n", "\r\r", "\r\r\n", "\r\n\n", "\r\n\r", "\r\n\r\n",
|
|
11
|
+
].map(text => [text, Uint8Array.from(text, char => char.charCodeAt(0))]));
|
|
11
12
|
|
|
12
13
|
export class SseFrameTooLargeError extends Error {
|
|
13
14
|
readonly maxBytes: number;
|
|
@@ -32,6 +33,8 @@ export class SseFrameCountLimitError extends Error {
|
|
|
32
33
|
export type BoundedSseFrame = {
|
|
33
34
|
block: Uint8Array;
|
|
34
35
|
delimiter: Uint8Array;
|
|
36
|
+
/** A late LF extending a previously returned CR; no new event. */
|
|
37
|
+
continuation?: true;
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -40,29 +43,22 @@ export type BoundedSseFrame = {
|
|
|
40
43
|
* Returns the delimiter length in bytes, `0` when `index` does not start a
|
|
41
44
|
* delimiter, and `undefined` when more bytes are required to decide.
|
|
42
45
|
*/
|
|
43
|
-
function
|
|
46
|
+
export function sseDelimiterLengthAt(
|
|
44
47
|
index: number,
|
|
45
48
|
length: number,
|
|
46
49
|
byteAt: (index: number) => number,
|
|
47
50
|
): number | undefined {
|
|
48
51
|
const first = byteAt(index);
|
|
49
|
-
if (first
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
if (byteAt(index + 1) !== 10) return 0;
|
|
60
|
-
if (index + 2 >= length) return undefined;
|
|
61
|
-
const third = byteAt(index + 2);
|
|
62
|
-
if (third === 10) return 3;
|
|
63
|
-
if (third !== 13) return 0;
|
|
64
|
-
if (index + 3 >= length) return undefined;
|
|
65
|
-
return byteAt(index + 3) === 10 ? 4 : 0;
|
|
52
|
+
if (first !== 10 && first !== 13) return 0;
|
|
53
|
+
let next = index + 1;
|
|
54
|
+
if (next >= length) return undefined;
|
|
55
|
+
if (first === 13 && byteAt(next) === 10) next += 1;
|
|
56
|
+
if (next >= length) return undefined;
|
|
57
|
+
const second = byteAt(next);
|
|
58
|
+
if (second !== 10 && second !== 13) return 0;
|
|
59
|
+
next += 1;
|
|
60
|
+
if (second === 13 && next < length && byteAt(next) === 10) next += 1;
|
|
61
|
+
return next - index;
|
|
66
62
|
}
|
|
67
63
|
|
|
68
64
|
function delimiterBytesAt(
|
|
@@ -70,9 +66,9 @@ function delimiterBytesAt(
|
|
|
70
66
|
delimiterLength: number,
|
|
71
67
|
byteAt: (index: number) => number,
|
|
72
68
|
): Uint8Array {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
69
|
+
let text = "";
|
|
70
|
+
for (let offset = 0; offset < delimiterLength; offset++) text += String.fromCharCode(byteAt(index + offset));
|
|
71
|
+
return DELIMITERS.get(text)!;
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
function copyRange(
|
|
@@ -100,14 +96,8 @@ function copyRange(
|
|
|
100
96
|
* cost on ordinary framing.
|
|
101
97
|
*/
|
|
102
98
|
function isResponsesTerminalFrame(block: Uint8Array): boolean {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
if (!line.startsWith("data:")) continue;
|
|
106
|
-
const value = line.slice(5);
|
|
107
|
-
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
108
|
-
}
|
|
109
|
-
if (data.length === 0) return false;
|
|
110
|
-
const payload = data.join("\n");
|
|
99
|
+
const payload = sseDataPayload(new TextDecoder().decode(block));
|
|
100
|
+
if (payload === null) return false;
|
|
111
101
|
if (payload === "[DONE]") return false;
|
|
112
102
|
try {
|
|
113
103
|
const parsed = JSON.parse(payload) as {
|
|
@@ -161,6 +151,7 @@ export class BoundedSseFrameBuffer {
|
|
|
161
151
|
private candidate: Uint8Array = EMPTY_BYTES;
|
|
162
152
|
private candidateBytes = 0;
|
|
163
153
|
private disposed = false;
|
|
154
|
+
private pendingLineFeed = false;
|
|
164
155
|
|
|
165
156
|
constructor(maxFrameBytes = MAX_CLIENT_SSE_FRAME_BYTES) {
|
|
166
157
|
if (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0) {
|
|
@@ -177,6 +168,7 @@ export class BoundedSseFrameBuffer {
|
|
|
177
168
|
this.delimiterTail = EMPTY_BYTES;
|
|
178
169
|
this.candidate = EMPTY_BYTES;
|
|
179
170
|
this.candidateBytes = 0;
|
|
171
|
+
this.pendingLineFeed = false;
|
|
180
172
|
}
|
|
181
173
|
|
|
182
174
|
private ensureCapacity(requiredBytes: number): void {
|
|
@@ -226,6 +218,7 @@ export class BoundedSseFrameBuffer {
|
|
|
226
218
|
if (chunk.byteLength === 0) return [];
|
|
227
219
|
|
|
228
220
|
const frames: BoundedSseFrame[] = [];
|
|
221
|
+
let completedFrames = 0;
|
|
229
222
|
const previousTail = this.delimiterTail;
|
|
230
223
|
this.delimiterTail = EMPTY_BYTES;
|
|
231
224
|
const tailLength = previousTail.byteLength;
|
|
@@ -245,12 +238,19 @@ export class BoundedSseFrameBuffer {
|
|
|
245
238
|
|
|
246
239
|
try {
|
|
247
240
|
let index = 0;
|
|
248
|
-
|
|
241
|
+
if (this.pendingLineFeed && byteAt(0) === 10) {
|
|
242
|
+
// The prior event was already dispatched on CR. Preserve its later LF without
|
|
243
|
+
// charging it to the next event or letting it pair with another line ending.
|
|
244
|
+
frames.push({ block: EMPTY_BYTES, delimiter: DELIMITERS.get("\n")!, continuation: true });
|
|
245
|
+
index = 1;
|
|
246
|
+
}
|
|
247
|
+
this.pendingLineFeed = false;
|
|
248
|
+
let retainedThrough = index;
|
|
249
249
|
while (index < totalLength) {
|
|
250
|
-
const delimiterLength =
|
|
250
|
+
const delimiterLength = sseDelimiterLengthAt(index, totalLength, byteAt);
|
|
251
251
|
if (delimiterLength === undefined) break;
|
|
252
252
|
if (delimiterLength > 0) {
|
|
253
|
-
if (
|
|
253
|
+
if (completedFrames >= this.maxFramesPerFeed) {
|
|
254
254
|
this.clear();
|
|
255
255
|
this.disposed = true;
|
|
256
256
|
throw new SseFrameCountLimitError(this.maxFramesPerFeed);
|
|
@@ -259,7 +259,9 @@ export class BoundedSseFrameBuffer {
|
|
|
259
259
|
const block = this.takeCandidate();
|
|
260
260
|
const delimiter = delimiterBytesAt(index, delimiterLength, byteAt);
|
|
261
261
|
frames.push({ block, delimiter });
|
|
262
|
+
completedFrames += 1;
|
|
262
263
|
index += delimiterLength;
|
|
264
|
+
this.pendingLineFeed = index === totalLength && byteAt(index - 1) === 13;
|
|
263
265
|
retainedThrough = index;
|
|
264
266
|
continue;
|
|
265
267
|
}
|
|
@@ -65,7 +65,8 @@ export function composeSseBlockRewrites(...rewrites: SseBlockRewrite[]): SseBloc
|
|
|
65
65
|
|
|
66
66
|
/** Split one complete SSE event block while retaining its original blank-line delimiter. */
|
|
67
67
|
export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
|
|
68
|
-
|
|
68
|
+
// The negative lookahead prevents backtracking from splitting one CRLF into two endings.
|
|
69
|
+
const match = buffer.match(/(?:\r\n|\r(?!\n)|\n){2}/);
|
|
69
70
|
if (!match || match.index === undefined) return null;
|
|
70
71
|
return {
|
|
71
72
|
block: buffer.slice(0, match.index),
|
|
@@ -87,6 +88,7 @@ export function createSseBlockBuffer(
|
|
|
87
88
|
append(fragment: string): void;
|
|
88
89
|
next(): { block: string; delimiter: string } | null;
|
|
89
90
|
tail(): string;
|
|
91
|
+
isEmpty(): boolean;
|
|
90
92
|
compact(): void;
|
|
91
93
|
clear(): void;
|
|
92
94
|
} {
|
|
@@ -95,6 +97,7 @@ export function createSseBlockBuffer(
|
|
|
95
97
|
let offset = 0;
|
|
96
98
|
let scanOffset = 0;
|
|
97
99
|
let bufferBytes = 0;
|
|
100
|
+
const delimiterPattern = /(?:\r\n|\r(?!\n)|\n){2}/g;
|
|
98
101
|
|
|
99
102
|
const compact = (): void => {
|
|
100
103
|
if (offset === 0) return;
|
|
@@ -128,43 +131,33 @@ export function createSseBlockBuffer(
|
|
|
128
131
|
}
|
|
129
132
|
},
|
|
130
133
|
next() {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let end = newline + 1;
|
|
138
|
-
if (buffer[end] === "\r") end += 1;
|
|
139
|
-
if (end === buffer.length) {
|
|
140
|
-
// Keep the candidate first newline until its possible blank-line delimiter arrives.
|
|
141
|
-
scanOffset = newline;
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
if (buffer[end] !== "\n") {
|
|
145
|
-
scanOffset = newline + 1;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
end += 1;
|
|
149
|
-
const start = newline > offset && buffer[newline - 1] === "\r" ? newline - 1 : newline;
|
|
150
|
-
const block = buffer.slice(offset, start);
|
|
151
|
-
const delimiter = buffer.slice(start, end);
|
|
152
|
-
const nextBytes = bufferBytes - Buffer.byteLength(block, "utf8") - delimiter.length;
|
|
153
|
-
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
154
|
-
reservation.commitRetained();
|
|
155
|
-
budget.releaseRetained(bufferBytes, scope);
|
|
156
|
-
bufferBytes = nextBytes;
|
|
157
|
-
offset = end;
|
|
158
|
-
scanOffset = end;
|
|
159
|
-
if (offset === buffer.length) {
|
|
160
|
-
buffer = "";
|
|
161
|
-
offset = 0;
|
|
162
|
-
scanOffset = 0;
|
|
163
|
-
}
|
|
164
|
-
return { block, delimiter };
|
|
134
|
+
delimiterPattern.lastIndex = scanOffset;
|
|
135
|
+
const match = delimiterPattern.exec(buffer);
|
|
136
|
+
if (!match) {
|
|
137
|
+
// A delimiter is at most four code units. Revisit only its possible split prefix.
|
|
138
|
+
scanOffset = Math.max(offset, buffer.length - 3);
|
|
139
|
+
return null;
|
|
165
140
|
}
|
|
141
|
+
const start = match.index;
|
|
142
|
+
const end = start + match[0].length;
|
|
143
|
+
const block = buffer.slice(offset, start);
|
|
144
|
+
const delimiter = buffer.slice(start, end);
|
|
145
|
+
const nextBytes = bufferBytes - Buffer.byteLength(block, "utf8") - delimiter.length;
|
|
146
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
147
|
+
reservation.commitRetained();
|
|
148
|
+
budget.releaseRetained(bufferBytes, scope);
|
|
149
|
+
bufferBytes = nextBytes;
|
|
150
|
+
offset = end;
|
|
151
|
+
scanOffset = end;
|
|
152
|
+
if (offset === buffer.length) {
|
|
153
|
+
buffer = "";
|
|
154
|
+
offset = 0;
|
|
155
|
+
scanOffset = 0;
|
|
156
|
+
}
|
|
157
|
+
return { block, delimiter };
|
|
166
158
|
},
|
|
167
159
|
tail: () => buffer.slice(offset),
|
|
160
|
+
isEmpty: () => offset === buffer.length,
|
|
168
161
|
compact,
|
|
169
162
|
clear() {
|
|
170
163
|
budget.releaseRetained(bufferBytes, scope);
|
|
@@ -184,11 +177,11 @@ export function sseDataPayload(block: string): string | null {
|
|
|
184
177
|
const len = block.length;
|
|
185
178
|
|
|
186
179
|
while (lineStart < len) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
lineEnd
|
|
180
|
+
let lineEnd = lineStart;
|
|
181
|
+
while (lineEnd < len && block.charCodeAt(lineEnd) !== 13 && block.charCodeAt(lineEnd) !== 10) lineEnd += 1;
|
|
182
|
+
let nextStart = lineEnd;
|
|
183
|
+
if (nextStart < len) {
|
|
184
|
+
nextStart += block.charCodeAt(lineEnd) === 13 && block.charCodeAt(lineEnd + 1) === 10 ? 2 : 1;
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
const lineLen = lineEnd - lineStart;
|
|
@@ -218,10 +211,19 @@ export function sseDataPayload(block: string): string | null {
|
|
|
218
211
|
return found ? result : null;
|
|
219
212
|
}
|
|
220
213
|
|
|
214
|
+
/** Shared line rules for field-mutating repairs; identity paths keep the original block. */
|
|
215
|
+
export function sseLineEnding(block: string): "\r\n" | "\r" | "\n" {
|
|
216
|
+
return block.match(/\r\n|\r|\n/)?.[0] as "\r\n" | "\r" | "\n" ?? "\n";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function splitSseBlock(block: string): { newline: string; lines: string[] } {
|
|
220
|
+
return { newline: sseLineEnding(block), lines: block.split(/\r\n|\r|\n/) };
|
|
221
|
+
}
|
|
222
|
+
|
|
221
223
|
/** Replace an SSE event's data field while preserving non-data fields and newline style. */
|
|
222
224
|
export function replaceSseDataPayload(block: string, payload: string): string {
|
|
223
|
-
|
|
224
|
-
const lines = block
|
|
225
|
+
if (sseDataPayload(block) === payload) return block;
|
|
226
|
+
const { newline, lines } = splitSseBlock(block);
|
|
225
227
|
const rewritten: string[] = [];
|
|
226
228
|
let replaced = false;
|
|
227
229
|
for (const line of lines) {
|
|
@@ -230,7 +232,7 @@ export function replaceSseDataPayload(block: string, payload: string): string {
|
|
|
230
232
|
continue;
|
|
231
233
|
}
|
|
232
234
|
if (!replaced) {
|
|
233
|
-
rewritten.push(...payload.split(/\r
|
|
235
|
+
rewritten.push(...payload.split(/\r\n|\r|\n/).map(line => `data: ${line}`));
|
|
234
236
|
replaced = true;
|
|
235
237
|
}
|
|
236
238
|
}
|
|
@@ -278,6 +280,8 @@ export function relaySseWithBlockRewrite(
|
|
|
278
280
|
// Relays have several independent teardown paths; disposal is exactly once.
|
|
279
281
|
let disposed = false;
|
|
280
282
|
let cancelled = false;
|
|
283
|
+
let pendingLineFeed = false;
|
|
284
|
+
let pendingLineFeedVisible = false;
|
|
281
285
|
const disposeRewrite = (): void => {
|
|
282
286
|
if (disposed) return;
|
|
283
287
|
disposed = true;
|
|
@@ -308,10 +312,18 @@ export function relaySseWithBlockRewrite(
|
|
|
308
312
|
let emitted = 0;
|
|
309
313
|
let next: { block: string; delimiter: string } | null;
|
|
310
314
|
while (!cancelled && (next = buffer.next())) {
|
|
311
|
-
const
|
|
315
|
+
const { block, delimiter } = next;
|
|
316
|
+
// A CR followed by buffered text is already settled; only an end-of-buffer CR
|
|
317
|
+
// can still receive the LF that extends its delimiter in the next fragment.
|
|
318
|
+
pendingLineFeed = delimiter.endsWith("\r") && buffer.isEmpty();
|
|
319
|
+
const outBlocks = rewrite(block);
|
|
320
|
+
pendingLineFeedVisible = outBlocks.length > 0;
|
|
312
321
|
if (cancelled) return emitted;
|
|
313
|
-
for (
|
|
314
|
-
|
|
322
|
+
for (let index = 0; index < outBlocks.length; index++) {
|
|
323
|
+
// Synthetic earlier blocks need a settled delimiter; only the final one can
|
|
324
|
+
// inherit an upstream LF in a later chunk. Completing CR as CRLF is equivalent.
|
|
325
|
+
const settled = index < outBlocks.length - 1 && delimiter.endsWith("\r") ? delimiter + "\n" : delimiter;
|
|
326
|
+
enqueueText(controller, outBlocks[index]! + settled);
|
|
315
327
|
emitted += 1;
|
|
316
328
|
}
|
|
317
329
|
}
|
|
@@ -332,6 +344,23 @@ export function relaySseWithBlockRewrite(
|
|
|
332
344
|
return emitted;
|
|
333
345
|
};
|
|
334
346
|
|
|
347
|
+
const appendFragment = (controller: ReadableStreamDefaultController<Uint8Array>, fragment: string): number => {
|
|
348
|
+
let continuation = false;
|
|
349
|
+
if (pendingLineFeed && fragment.length > 0) {
|
|
350
|
+
continuation = fragment.startsWith("\n");
|
|
351
|
+
pendingLineFeed = false;
|
|
352
|
+
if (continuation) fragment = fragment.slice(1);
|
|
353
|
+
}
|
|
354
|
+
// Consume a late LF before scanning, so it cannot pair with a new LF to create
|
|
355
|
+
// a phantom empty callback. Any following line ending stays in the input buffer.
|
|
356
|
+
buffer.append(fragment);
|
|
357
|
+
if (continuation && pendingLineFeedVisible) {
|
|
358
|
+
enqueueText(controller, "\n");
|
|
359
|
+
return 1;
|
|
360
|
+
}
|
|
361
|
+
return 0;
|
|
362
|
+
};
|
|
363
|
+
|
|
335
364
|
return new ReadableStream<Uint8Array>({
|
|
336
365
|
async pull(controller) {
|
|
337
366
|
try {
|
|
@@ -346,7 +375,7 @@ export function relaySseWithBlockRewrite(
|
|
|
346
375
|
// after its disposal (#893 review).
|
|
347
376
|
if (cancelled) return;
|
|
348
377
|
if (done) {
|
|
349
|
-
|
|
378
|
+
appendFragment(controller, decoder.decode());
|
|
350
379
|
emitProcessedBlocks(controller, true);
|
|
351
380
|
if (cancelled) return;
|
|
352
381
|
buffer.clear();
|
|
@@ -354,8 +383,8 @@ export function relaySseWithBlockRewrite(
|
|
|
354
383
|
controller.close();
|
|
355
384
|
return;
|
|
356
385
|
}
|
|
357
|
-
|
|
358
|
-
const emitted = emitProcessedBlocks(controller);
|
|
386
|
+
const continuation = appendFragment(controller, decoder.decode(value, { stream: true }));
|
|
387
|
+
const emitted = continuation + emitProcessedBlocks(controller);
|
|
359
388
|
if (cancelled || emitted > 0) return;
|
|
360
389
|
}
|
|
361
390
|
} catch (error) {
|
package/src/server/ws-bridge.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ResponsesTerminalStatus } from "../bridge";
|
|
|
8
8
|
import type { DataPlaneAdmission } from "./auth-cors";
|
|
9
9
|
import type { AdmissionLease, AdmissionReservation } from "../lib/admission";
|
|
10
10
|
import { BoundedSseFrameBuffer } from "./sse-frame-buffer";
|
|
11
|
+
import { sseDataPayload as parseSseBlock } from "./sse-payload-rewrite";
|
|
11
12
|
import { safeResponseHeaders } from "./safe-response-headers";
|
|
12
13
|
import type { AudioSocketTarget } from "./audio-dictation";
|
|
13
14
|
|
|
@@ -183,17 +184,6 @@ export function buildWsErrorFrame(
|
|
|
183
184
|
};
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
function parseSseBlock(block: string): string | null {
|
|
187
|
-
const data: string[] = [];
|
|
188
|
-
for (const line of block.split(/\r?\n/)) {
|
|
189
|
-
if (line.startsWith("data:")) {
|
|
190
|
-
const value = line.slice(5);
|
|
191
|
-
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return data.length > 0 ? data.join("\n") : null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
187
|
function payloadType(payload: string): string | null {
|
|
198
188
|
try {
|
|
199
189
|
const json = JSON.parse(payload) as { type?: unknown };
|
|
@@ -154,7 +154,6 @@ export function buildWindowsServiceScript(
|
|
|
154
154
|
' if exist "%OCX_PKG_DIR%" rmdir /s /q "%OCX_PKG_DIR%" 2>nul',
|
|
155
155
|
' move "%OCX_PKG_DIR%\\..\\%%B\\opencodex" "%OCX_PKG_DIR%" >nul 2>&1',
|
|
156
156
|
' if exist "%OCX_PKG_DIR%\\package.json" (',
|
|
157
|
-
' set "OCX_RESTORED_BACKUP=%%B"',
|
|
158
157
|
" goto backup_restored",
|
|
159
158
|
" )",
|
|
160
159
|
" )",
|
|
@@ -162,7 +161,7 @@ export function buildWindowsServiceScript(
|
|
|
162
161
|
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] no restorable backup found',
|
|
163
162
|
"goto :eof",
|
|
164
163
|
":backup_restored",
|
|
165
|
-
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from
|
|
164
|
+
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from transactional-update backup',
|
|
166
165
|
"goto :eof",
|
|
167
166
|
].filter((line): line is string => Boolean(line));
|
|
168
167
|
return `${lines.join("\r\n")}\r\n`;
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { decodeWindowsTextBytes } from "./lib/windows-text";
|
|
30
30
|
import { WINSW_SERVICE_ID } from "./lib/winsw";
|
|
31
31
|
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV } from "./lib/bun-runtime";
|
|
32
|
+
import { REAL_BUN_MIN_BYTES } from "./lib/bun-binary-validator.mjs";
|
|
32
33
|
import { WINDOWS_WRAPPER_PROTOCOL_ENV, WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE } from "./service/windows-wrapper-exit";
|
|
33
34
|
import { buildWindowsServiceScript, windowsTaskActionMatches } from "./service/windows-taskxml";
|
|
34
35
|
import { inspectServiceStateEvidence, serviceStatePathsForOpenCodexHome } from "./service/state";
|
|
@@ -613,8 +614,41 @@ function matchesGeneratedStandaloneControlFlow(body: string, port: number): bool
|
|
|
613
614
|
scriptLines.filter((line, index) => index >= end || line === 'set "ERRORLEVEL="' || !line.startsWith('set "'));
|
|
614
615
|
const actualFlow = withoutPrefixSets(lines, boundary);
|
|
615
616
|
const generatedFlow = withoutPrefixSets(expected, expectedBoundary);
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
// Read-only recognition of the exact previous generator output keeps installed
|
|
618
|
+
// standalone services identifiable across the backup-log hardening update.
|
|
619
|
+
// Never emit or execute this legacy variant; every other control line still matches.
|
|
620
|
+
const legacyFlow = generatedFlow.flatMap(line => {
|
|
621
|
+
if (line === " goto backup_restored") return [' set "OCX_RESTORED_BACKUP=%%B"', line];
|
|
622
|
+
if (line === '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from transactional-update backup') {
|
|
623
|
+
return ['>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from %OCX_RESTORED_BACKUP%'];
|
|
624
|
+
}
|
|
625
|
+
return [line];
|
|
626
|
+
});
|
|
627
|
+
// Also recognize the exact pre-placeholder version, not a partial size-gate
|
|
628
|
+
// hybrid. Only remove these complete known blocks from trusted generator output.
|
|
629
|
+
const beforePlaceholderGate = (flow: string[]): string[] | null => {
|
|
630
|
+
const blocks = [
|
|
631
|
+
['set "OCX_BUN_BYTES="', 'for %%F in ("%OCX_BUN%") do set "OCX_BUN_BYTES=%%~zF"',
|
|
632
|
+
'if not defined OCX_BUN_BYTES goto bun_not_ready', `if %OCX_BUN_BYTES% LSS ${REAL_BUN_MIN_BYTES} goto bun_not_ready`],
|
|
633
|
+
[":bun_not_ready",
|
|
634
|
+
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] bundled Bun is not ready (%OCX_BUN_BYTES% bytes, npm placeholder or mid-install); waiting for its postinstall, retrying in 5s - if this persists, reinstall opencodex with bun scripts allowed',
|
|
635
|
+
"ping -n 6 127.0.0.1 >nul", "goto loop"],
|
|
636
|
+
];
|
|
637
|
+
let earlier = flow;
|
|
638
|
+
for (const block of blocks) {
|
|
639
|
+
const at = earlier.indexOf(block[0]!);
|
|
640
|
+
if (at < 0 || !block.every((line, offset) => earlier[at + offset] === line)) return null;
|
|
641
|
+
earlier = [...earlier.slice(0, at), ...earlier.slice(at + block.length)];
|
|
642
|
+
}
|
|
643
|
+
return earlier;
|
|
644
|
+
};
|
|
645
|
+
const recognized = [generatedFlow, legacyFlow];
|
|
646
|
+
for (const flow of [...recognized]) {
|
|
647
|
+
const earlier = beforePlaceholderGate(flow);
|
|
648
|
+
if (earlier) recognized.push(earlier);
|
|
649
|
+
}
|
|
650
|
+
return recognized.some(expectedFlow => actualFlow.length === expectedFlow.length
|
|
651
|
+
&& actualFlow.every((line, index) => line === expectedFlow[index]));
|
|
618
652
|
}
|
|
619
653
|
|
|
620
654
|
/** Validate the generated launch shape before interpreting omitted optional homes. */
|