@bitkyc08/opencodex 2.52.0 → 2.53.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/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -200,6 +200,45 @@ export const AGENT_MESSAGE_ROUTING_ENVELOPE = /(?:^|\n)Message Type\s*:\s*(?:NEW
|
|
|
200
200
|
// envelope stripper below.
|
|
201
201
|
export const AGENT_MESSAGE_CONTROL_PREAMBLE = /(?:^|\n)\[CXC-[A-Z0-9-]+\][^\n]*(?:\n(?!\n|Message Type\s*:)[^\n]*)*(?=\n{2,}|\nMessage Type\s*:|$)/gi;
|
|
202
202
|
|
|
203
|
+
export const MAX_AGENT_TASK_ENCRYPTED_PARTS = 32;
|
|
204
|
+
export const MAX_AGENT_TASK_CIPHERTEXT_BYTES = 2 * 1024 * 1024;
|
|
205
|
+
|
|
206
|
+
/** Detection only: joining fragments never authorizes recovery or proves authenticity. */
|
|
207
|
+
function splitFernetParts(content: unknown[]): Set<object> {
|
|
208
|
+
const protectedParts = new Set<object>();
|
|
209
|
+
let run: Array<{ part: object; text: string }> = [];
|
|
210
|
+
let bytes = 0;
|
|
211
|
+
let overLimit = false;
|
|
212
|
+
const finish = (): void => {
|
|
213
|
+
if (!overLimit && run.length > 1
|
|
214
|
+
&& run.every(({ text }) => !isStructurallyValidFernetToken(text))
|
|
215
|
+
&& isStructurallyValidFernetToken(run.map(({ text }) => text).join(""))) {
|
|
216
|
+
for (const { part } of run) protectedParts.add(part);
|
|
217
|
+
}
|
|
218
|
+
run = [];
|
|
219
|
+
bytes = 0;
|
|
220
|
+
overLimit = false;
|
|
221
|
+
};
|
|
222
|
+
for (const part of content) {
|
|
223
|
+
if (!part || typeof part !== "object" || (part as { type?: unknown }).type !== "encrypted_content"
|
|
224
|
+
|| typeof (part as { encrypted_content?: unknown }).encrypted_content !== "string") {
|
|
225
|
+
finish();
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (overLimit) continue;
|
|
229
|
+
const text = (part as { encrypted_content: string }).encrypted_content;
|
|
230
|
+
bytes += Buffer.byteLength(text);
|
|
231
|
+
if (run.length >= MAX_AGENT_TASK_ENCRYPTED_PARTS || bytes > MAX_AGENT_TASK_CIPHERTEXT_BYTES) {
|
|
232
|
+
overLimit = true;
|
|
233
|
+
run = [];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
run.push({ part, text });
|
|
237
|
+
}
|
|
238
|
+
finish();
|
|
239
|
+
return protectedParts;
|
|
240
|
+
}
|
|
241
|
+
|
|
203
242
|
export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
|
|
204
243
|
if (!Array.isArray(input)) return false;
|
|
205
244
|
|
|
@@ -222,7 +261,8 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
|
|
|
222
261
|
const content = (item as { content?: unknown }).content;
|
|
223
262
|
if (!Array.isArray(content)) return false;
|
|
224
263
|
|
|
225
|
-
|
|
264
|
+
const fragmentParts = splitFernetParts(content);
|
|
265
|
+
let hasFernetTask = fragmentParts.size > 0;
|
|
226
266
|
const readableParts: string[] = [];
|
|
227
267
|
for (const part of content) {
|
|
228
268
|
if (!part || typeof part !== "object") continue;
|
|
@@ -238,6 +278,7 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
|
|
|
238
278
|
continue;
|
|
239
279
|
}
|
|
240
280
|
|
|
281
|
+
if (fragmentParts.has(part)) continue;
|
|
241
282
|
const runs = fernetTokenRuns(record.encrypted_content);
|
|
242
283
|
if (runs.length > 0) hasFernetTask = true;
|
|
243
284
|
readableParts.push(textWithoutFernetRuns(record.encrypted_content, runs));
|
|
@@ -282,6 +323,7 @@ export function hasEncryptedContentPart(content: unknown): boolean {
|
|
|
282
323
|
export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
283
324
|
if (!Array.isArray(input)) return 0;
|
|
284
325
|
let rewritten = 0;
|
|
326
|
+
const protectedFragments = new WeakSet<object>();
|
|
285
327
|
type VisitFrame =
|
|
286
328
|
| { kind: "visit"; node: unknown }
|
|
287
329
|
| { kind: "array"; node: unknown[]; index: number }
|
|
@@ -293,6 +335,7 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
|
293
335
|
const frame = stack.pop()!;
|
|
294
336
|
if (frame.kind === "visit") {
|
|
295
337
|
if (Array.isArray(frame.node)) {
|
|
338
|
+
for (const part of splitFernetParts(frame.node)) protectedFragments.add(part);
|
|
296
339
|
stack.push({ kind: "array", node: frame.node, index: 0 });
|
|
297
340
|
} else if (frame.node && typeof frame.node === "object") {
|
|
298
341
|
stack.push({ kind: "object", values: Object.values(frame.node), index: 0 });
|
|
@@ -309,7 +352,7 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
|
309
352
|
&& typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
310
353
|
) {
|
|
311
354
|
const payload = (child as { encrypted_content: string }).encrypted_content;
|
|
312
|
-
if (!looksLikeBackendCiphertext(payload)) {
|
|
355
|
+
if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload)) {
|
|
313
356
|
const parts = encryptedSlotParts(payload);
|
|
314
357
|
frame.node.splice(frame.index, 1, ...parts);
|
|
315
358
|
rewritten += 1;
|
|
@@ -179,5 +179,8 @@ export function codexWsUpstreamFetch(
|
|
|
179
179
|
} catch {
|
|
180
180
|
return sseFallback(url, init);
|
|
181
181
|
}
|
|
182
|
-
return codexWsExchange({
|
|
182
|
+
return codexWsExchange({
|
|
183
|
+
session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
|
|
184
|
+
bunVersion: typeof runtime === "string" ? runtime : runtime.version,
|
|
185
|
+
});
|
|
183
186
|
}
|
|
@@ -115,10 +115,8 @@ export function collectSelfNamedNamespaceScrubAuthorization(
|
|
|
115
115
|
* `None | "" | "functions"` as the default namespace; anything else is concatenated into a flat
|
|
116
116
|
* name before routing. A backend answer of `{ name: "exec", namespace: "exec" }` therefore
|
|
117
117
|
* becomes `execexec`, which no client tool matches, and Codex re-issues the same call forever.
|
|
118
|
-
* The malformed
|
|
118
|
+
* The malformed shape is scrubbed only when the current turn authorized a bare custom tool
|
|
119
119
|
* with that name. A genuine namespaced tool may intentionally use the same namespace and name.
|
|
120
|
-
* The adapter fix that stops provoking the answer lives in `stripSparkCompatibility`; this is the
|
|
121
|
-
* belt to that suspender.
|
|
122
120
|
*/
|
|
123
121
|
export function scrubSelfNamedToolCallNamespace(
|
|
124
122
|
value: unknown,
|
|
@@ -106,7 +106,7 @@ function addWireToolName(
|
|
|
106
106
|
}
|
|
107
107
|
// `exec` is the one name that also switches on nested-helper normalization, so a bare alias
|
|
108
108
|
// for a namespaced MCP tool would silently authorize `exec_command`/`shell_command`/
|
|
109
|
-
// `apply_patch` the request never declared. Every other inner name keeps the bare alias.
|
|
109
|
+
// `apply_patch`/`view_image` the request never declared. Every other inner name keeps the bare alias.
|
|
110
110
|
if (name !== CODE_MODE_EXEC_TOOL_NAME) names.add(name);
|
|
111
111
|
}
|
|
112
112
|
|
package/src/server/search.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
cooldownErrorResponse,
|
|
16
16
|
CodexAuthContextError,
|
|
17
17
|
CodexMainProfileDrainingError,
|
|
18
|
+
CodexModelAvailabilityError,
|
|
18
19
|
CodexPoolAuthenticationError,
|
|
19
20
|
CodexThreadAffinityExpiredError,
|
|
20
21
|
} from "../codex/auth-context";
|
|
@@ -39,6 +40,7 @@ import type { RequestLogContext } from "./request-log";
|
|
|
39
40
|
import { codexLogAccountId, decodeRequestErrorResponse } from "./responses";
|
|
40
41
|
import type { AdmissionLease } from "../lib/admission";
|
|
41
42
|
import { codexAccountSelectionForTurn } from "./lifecycle";
|
|
43
|
+
import { codexModelAvailabilityErrorResponse } from "./responses/codex-auth-error";
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Default TOTAL deadline for one search relay. alpha/search is non-streaming JSON — response
|
|
@@ -143,6 +145,7 @@ export async function handleSearch(
|
|
|
143
145
|
console.error(`[search] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
144
146
|
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
145
147
|
}
|
|
148
|
+
if (err instanceof CodexModelAvailabilityError) return codexModelAvailabilityErrorResponse(err);
|
|
146
149
|
if (err instanceof CodexPoolAuthenticationError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
147
150
|
throw err;
|
|
148
151
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Shared client-facing SSE payload rewrite shell.
|
|
@@ -37,18 +38,21 @@ export function payloadRewriteAsBlockRewrite(rewrite: SsePayloadRewrite): SseBlo
|
|
|
37
38
|
export function composeSseBlockRewrites(...rewrites: SseBlockRewrite[]): SseBlockRewrite {
|
|
38
39
|
const active = rewrites.filter(Boolean);
|
|
39
40
|
if (active.length === 0) return Object.assign((block: string) => [block], {});
|
|
41
|
+
let disposed = false;
|
|
40
42
|
const composed: SseBlockRewrite = (block: string) => {
|
|
41
43
|
let blocks: readonly string[] = [block];
|
|
42
44
|
for (const rewrite of active) {
|
|
43
45
|
const next: string[] = [];
|
|
44
|
-
for (const current of blocks)
|
|
46
|
+
for (const current of blocks) {
|
|
47
|
+
if (disposed) return [];
|
|
48
|
+
next.push(...rewrite(current));
|
|
49
|
+
}
|
|
45
50
|
blocks = next;
|
|
46
51
|
}
|
|
47
52
|
return blocks;
|
|
48
53
|
};
|
|
49
54
|
// Child disposal is part of the contract: one idempotent disposer for the
|
|
50
55
|
// whole chain, so relay teardown never leaks a nested collector.
|
|
51
|
-
let disposed = false;
|
|
52
56
|
composed.dispose = () => {
|
|
53
57
|
if (disposed) return;
|
|
54
58
|
disposed = true;
|
|
@@ -70,10 +74,116 @@ export function nextSseBlock(buffer: string): { block: string; delimiter: string
|
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Incremental form of nextSseBlock for bounded relays. The scan cursor visits only new text and
|
|
79
|
+
* consuming a block subtracts its byte length instead of recounting the remaining suffix.
|
|
80
|
+
* Old/new buffer overlap still requires admission before either append or consumption commits.
|
|
81
|
+
* Call compact before yielding to stop retaining an already-consumed prefix across pulls.
|
|
82
|
+
*/
|
|
83
|
+
export function createSseBlockBuffer(
|
|
84
|
+
budget: TranslatorBudget,
|
|
85
|
+
assertAppendSize?: (bytes: number) => void,
|
|
86
|
+
): {
|
|
87
|
+
append(fragment: string): void;
|
|
88
|
+
next(): { block: string; delimiter: string } | null;
|
|
89
|
+
tail(): string;
|
|
90
|
+
compact(): void;
|
|
91
|
+
clear(): void;
|
|
92
|
+
} {
|
|
93
|
+
const scope = { kind: "live_transient" as const };
|
|
94
|
+
let buffer = "";
|
|
95
|
+
let offset = 0;
|
|
96
|
+
let scanOffset = 0;
|
|
97
|
+
let bufferBytes = 0;
|
|
98
|
+
|
|
99
|
+
const compact = (): void => {
|
|
100
|
+
if (offset === 0) return;
|
|
101
|
+
buffer = buffer.slice(offset);
|
|
102
|
+
scanOffset -= offset;
|
|
103
|
+
offset = 0;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
append(fragment) {
|
|
108
|
+
if (!fragment) return;
|
|
109
|
+
let fragmentBytes = Buffer.byteLength(fragment, "utf8");
|
|
110
|
+
// Decoder output never splits a surrogate pair, but keep the helper exact for string callers.
|
|
111
|
+
const last = buffer.charCodeAt(buffer.length - 1);
|
|
112
|
+
const first = fragment.charCodeAt(0);
|
|
113
|
+
if (offset < buffer.length && last >= 0xd800 && last <= 0xdbff && first >= 0xdc00 && first <= 0xdfff) {
|
|
114
|
+
fragmentBytes -= 2;
|
|
115
|
+
}
|
|
116
|
+
const nextBytes = bufferBytes + fragmentBytes;
|
|
117
|
+
assertAppendSize?.(nextBytes);
|
|
118
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
119
|
+
try {
|
|
120
|
+
compact();
|
|
121
|
+
buffer += fragment;
|
|
122
|
+
reservation.commitRetained();
|
|
123
|
+
budget.releaseRetained(bufferBytes, scope);
|
|
124
|
+
bufferBytes = nextBytes;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
reservation.release();
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
next() {
|
|
131
|
+
for (;;) {
|
|
132
|
+
const newline = buffer.indexOf("\n", scanOffset);
|
|
133
|
+
if (newline < 0) {
|
|
134
|
+
scanOffset = buffer.length;
|
|
135
|
+
return null;
|
|
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 };
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
tail: () => buffer.slice(offset),
|
|
168
|
+
compact,
|
|
169
|
+
clear() {
|
|
170
|
+
budget.releaseRetained(bufferBytes, scope);
|
|
171
|
+
buffer = "";
|
|
172
|
+
offset = 0;
|
|
173
|
+
scanOffset = 0;
|
|
174
|
+
bufferBytes = 0;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
73
179
|
/** Join all data lines from one SSE event according to the event-stream field rules. */
|
|
74
180
|
export function sseDataPayload(block: string): string | null {
|
|
75
181
|
const data: string[] = [];
|
|
76
182
|
for (const line of block.split(/\r?\n/)) {
|
|
183
|
+
if (line === "data") {
|
|
184
|
+
data.push("");
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
77
187
|
if (!line.startsWith("data:")) continue;
|
|
78
188
|
const value = line.slice(5);
|
|
79
189
|
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
@@ -88,7 +198,7 @@ export function replaceSseDataPayload(block: string, payload: string): string {
|
|
|
88
198
|
const rewritten: string[] = [];
|
|
89
199
|
let replaced = false;
|
|
90
200
|
for (const line of lines) {
|
|
91
|
-
if (!line.startsWith("data:")) {
|
|
201
|
+
if (line !== "data" && !line.startsWith("data:")) {
|
|
92
202
|
rewritten.push(line);
|
|
93
203
|
continue;
|
|
94
204
|
}
|
|
@@ -137,8 +247,7 @@ export function relaySseWithBlockRewrite(
|
|
|
137
247
|
const reader = body.getReader();
|
|
138
248
|
const decoder = new TextDecoder();
|
|
139
249
|
const encoder = new TextEncoder();
|
|
140
|
-
|
|
141
|
-
let bufferBytes = 0;
|
|
250
|
+
const buffer = createSseBlockBuffer(translatorBudget);
|
|
142
251
|
// Relays have several independent teardown paths; disposal is exactly once.
|
|
143
252
|
let disposed = false;
|
|
144
253
|
let cancelled = false;
|
|
@@ -148,40 +257,16 @@ export function relaySseWithBlockRewrite(
|
|
|
148
257
|
try { rewrite.dispose?.(); } catch { /* teardown must not throw */ }
|
|
149
258
|
};
|
|
150
259
|
|
|
151
|
-
const appendBuffer = (fragment: string): void => {
|
|
152
|
-
if (!fragment) return;
|
|
153
|
-
const nextBytes = bufferBytes + encoder.encode(fragment).byteLength;
|
|
154
|
-
const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "live_transient" });
|
|
155
|
-
try {
|
|
156
|
-
buffer += fragment;
|
|
157
|
-
reservation.commitRetained();
|
|
158
|
-
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
159
|
-
bufferBytes = nextBytes;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
reservation.release();
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const replaceBuffer = (next: string): void => {
|
|
167
|
-
const nextBytes = encoder.encode(next).byteLength;
|
|
168
|
-
const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "live_transient" });
|
|
169
|
-
reservation.commitRetained();
|
|
170
|
-
buffer = next;
|
|
171
|
-
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
172
|
-
bufferBytes = nextBytes;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
260
|
const enqueueText = (
|
|
176
261
|
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
177
262
|
text: string,
|
|
178
263
|
): void => {
|
|
179
|
-
const bytes =
|
|
264
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
180
265
|
const reservation = translatorBudget.reserveTransient(bytes, { kind: "live_transient" });
|
|
181
266
|
try {
|
|
182
267
|
const encoded = encoder.encode(text);
|
|
183
|
-
reservation.commitRetained();
|
|
184
268
|
controller.enqueue(encoded);
|
|
269
|
+
reservation.commitRetained();
|
|
185
270
|
translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
|
|
186
271
|
} catch (error) {
|
|
187
272
|
reservation.release();
|
|
@@ -189,35 +274,33 @@ export function relaySseWithBlockRewrite(
|
|
|
189
274
|
}
|
|
190
275
|
};
|
|
191
276
|
|
|
192
|
-
const releaseBuffer = (): void => {
|
|
193
|
-
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
194
|
-
buffer = "";
|
|
195
|
-
bufferBytes = 0;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
277
|
const emitProcessedBlocks = (
|
|
199
278
|
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
200
279
|
flushFinal = false,
|
|
201
280
|
): number => {
|
|
202
281
|
let emitted = 0;
|
|
203
|
-
let next: { block: string; delimiter: string
|
|
204
|
-
while ((next =
|
|
205
|
-
|
|
206
|
-
|
|
282
|
+
let next: { block: string; delimiter: string } | null;
|
|
283
|
+
while (!cancelled && (next = buffer.next())) {
|
|
284
|
+
const outBlocks = rewrite(next.block);
|
|
285
|
+
if (cancelled) return emitted;
|
|
286
|
+
for (const outBlock of outBlocks) {
|
|
207
287
|
enqueueText(controller, outBlock + next.delimiter);
|
|
208
288
|
emitted += 1;
|
|
209
289
|
}
|
|
210
290
|
}
|
|
211
|
-
|
|
212
|
-
|
|
291
|
+
buffer.compact();
|
|
292
|
+
const tail = flushFinal ? buffer.tail() : "";
|
|
293
|
+
if (tail.length > 0) {
|
|
294
|
+
const tailBlocks = rewrite(tail);
|
|
295
|
+
if (cancelled) return emitted;
|
|
213
296
|
// A trailing fragment has no delimiter of its own; multiple emitted
|
|
214
297
|
// blocks must still be framed as separate events (#893 review).
|
|
215
|
-
const tailDelimiter =
|
|
298
|
+
const tailDelimiter = tail.includes("\r\n") ? "\r\n\r\n" : "\n\n";
|
|
216
299
|
for (let i = 0; i < tailBlocks.length; i++) {
|
|
217
300
|
enqueueText(controller, tailBlocks[i]! + (i < tailBlocks.length - 1 ? tailDelimiter : ""));
|
|
218
301
|
emitted += 1;
|
|
219
302
|
}
|
|
220
|
-
|
|
303
|
+
buffer.clear();
|
|
221
304
|
}
|
|
222
305
|
return emitted;
|
|
223
306
|
};
|
|
@@ -236,18 +319,20 @@ export function relaySseWithBlockRewrite(
|
|
|
236
319
|
// after its disposal (#893 review).
|
|
237
320
|
if (cancelled) return;
|
|
238
321
|
if (done) {
|
|
239
|
-
|
|
322
|
+
buffer.append(decoder.decode());
|
|
240
323
|
emitProcessedBlocks(controller, true);
|
|
241
|
-
|
|
324
|
+
if (cancelled) return;
|
|
325
|
+
buffer.clear();
|
|
242
326
|
disposeRewrite();
|
|
243
327
|
controller.close();
|
|
244
328
|
return;
|
|
245
329
|
}
|
|
246
|
-
|
|
247
|
-
|
|
330
|
+
buffer.append(decoder.decode(value, { stream: true }));
|
|
331
|
+
const emitted = emitProcessedBlocks(controller);
|
|
332
|
+
if (cancelled || emitted > 0) return;
|
|
248
333
|
}
|
|
249
334
|
} catch (error) {
|
|
250
|
-
|
|
335
|
+
buffer.clear();
|
|
251
336
|
disposeRewrite();
|
|
252
337
|
// Cancelling one tee branch waits for its sibling. Surface the failure
|
|
253
338
|
// now so downstream can abort upstream and release the inspection branch.
|
|
@@ -257,7 +342,7 @@ export function relaySseWithBlockRewrite(
|
|
|
257
342
|
},
|
|
258
343
|
cancel(reason) {
|
|
259
344
|
cancelled = true;
|
|
260
|
-
|
|
345
|
+
buffer.clear();
|
|
261
346
|
disposeRewrite();
|
|
262
347
|
reader.cancel(reason).catch(() => {});
|
|
263
348
|
},
|
package/src/server/ws-bridge.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { DataPlaneAdmission } from "./auth-cors";
|
|
|
8
8
|
import type { AdmissionLease, AdmissionReservation } from "../lib/admission";
|
|
9
9
|
import { BoundedSseFrameBuffer } from "./sse-frame-buffer";
|
|
10
10
|
import { safeResponseHeaders } from "./safe-response-headers";
|
|
11
|
+
import type { AudioSocketTarget } from "./audio-dictation";
|
|
11
12
|
|
|
12
13
|
export { safeResponseHeaders } from "./safe-response-headers";
|
|
13
14
|
|
|
@@ -31,14 +32,28 @@ export interface WsData {
|
|
|
31
32
|
/** Fixed-size logical session lane derived at the HTTP upgrade boundary. */
|
|
32
33
|
sessionLaneId?: string;
|
|
33
34
|
/** Discriminator: Responses reframing vs transparent live/realtime sideband relay. */
|
|
34
|
-
kind?: "responses" | "live-sideband";
|
|
35
|
+
kind?: "responses" | "live-sideband" | "remote-workspace-agent";
|
|
36
|
+
remoteWorkspaceConnection?: { receive(raw: string | Uint8Array): void };
|
|
37
|
+
remoteWorkspaceOpen?: (socket: ServerWebSocket<WsData>) => { receive(raw: string | Uint8Array): void };
|
|
38
|
+
remoteWorkspaceClose?: () => void;
|
|
35
39
|
liveUpstream?: WebSocket;
|
|
36
40
|
liveUpstreamUrl?: string;
|
|
37
41
|
liveUpstreamHeaders?: Record<string, string>;
|
|
42
|
+
liveUpstreamProtocols?: string[];
|
|
43
|
+
liveValidateFrame?: AudioSocketTarget["validateFrame"];
|
|
44
|
+
liveFinish?: AudioSocketTarget["finish"];
|
|
45
|
+
liveOutcome?: number | "timeout" | "connect_error";
|
|
46
|
+
liveMaxSessionMs?: number;
|
|
47
|
+
liveConnectTimer?: ReturnType<typeof setTimeout>;
|
|
48
|
+
liveSessionTimer?: ReturnType<typeof setTimeout>;
|
|
49
|
+
liveAbortSignal?: AbortSignal;
|
|
50
|
+
liveAbortListener?: () => void;
|
|
38
51
|
livePending?: Array<string | Buffer>;
|
|
39
52
|
/** Total encoded bytes retained in livePending while the upstream connects. */
|
|
40
53
|
livePendingBytes?: number;
|
|
41
54
|
liveOpened?: boolean;
|
|
55
|
+
/** Owns captured frames and terminal state until the downstream relay attaches. */
|
|
56
|
+
liveUpstreamHandoff?: LiveSidebandUpstreamHandoff;
|
|
42
57
|
/** Once teardown starts, ignore new client frames until the upstream closes. */
|
|
43
58
|
liveClosing?: boolean;
|
|
44
59
|
/** Schedules one bounded close retry without surrendering native-main ownership. */
|
|
@@ -48,6 +63,25 @@ export interface WsData {
|
|
|
48
63
|
admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>;
|
|
49
64
|
}
|
|
50
65
|
|
|
66
|
+
export interface LiveSidebandUpstreamFailure {
|
|
67
|
+
status: number;
|
|
68
|
+
code: string;
|
|
69
|
+
message: string;
|
|
70
|
+
closeCode?: number;
|
|
71
|
+
closeReason?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type LiveSidebandUpstreamTakeover =
|
|
75
|
+
| { ok: true; frames: Array<string | Buffer> }
|
|
76
|
+
| { ok: false; failure: LiveSidebandUpstreamFailure };
|
|
77
|
+
|
|
78
|
+
export interface LiveSidebandUpstreamHandoff {
|
|
79
|
+
/** Observe failure before the downstream upgrade without ending capture. */
|
|
80
|
+
failure(): LiveSidebandUpstreamFailure | undefined;
|
|
81
|
+
/** Atomically ends capture and transfers buffered frames or terminal state. */
|
|
82
|
+
take(): LiveSidebandUpstreamTakeover;
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
/**
|
|
52
86
|
* Build the Responses WebSocket upgrade payload.
|
|
53
87
|
*
|