@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- 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/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- 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/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- 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/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -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 +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- 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 +53 -67
- 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/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -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/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- 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/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- 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/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- 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/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- 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 +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- 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 +152 -28
- 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 +67 -30
- 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 +64 -7
- 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/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- 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
|
@@ -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 };
|
package/src/service/cli.ts
CHANGED
|
@@ -16,7 +16,8 @@ import { resolveServiceListenPort, reportServiceServing } from "./health";
|
|
|
16
16
|
import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
|
|
17
17
|
import { repairService } from "./repair";
|
|
18
18
|
import type { ServiceRepairVerb } from "./repair";
|
|
19
|
-
import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership } from "./state";
|
|
19
|
+
import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership, serviceStatePaths } from "./state";
|
|
20
|
+
import { ownershipMutationLeaseStatusLine } from "./ownership-mutation-lease.mjs";
|
|
20
21
|
import { foreignServiceOwnerRefusal, unknownServiceOwnerRefusal } from "./repair";
|
|
21
22
|
import type { ServiceBackend } from "./state";
|
|
22
23
|
import { unitPath } from "./systemd";
|
|
@@ -419,6 +420,10 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
419
420
|
// subsumes the not-installed case and adds the serving / stale-plist split.
|
|
420
421
|
console.log(await serviceStatusReport());
|
|
421
422
|
}
|
|
423
|
+
// A busy lease is why a supervised start can keep failing while the line above only
|
|
424
|
+
// says "not running"; report its recorded PID with unverified identity and lease age here.
|
|
425
|
+
const lease = ownershipMutationLeaseStatusLine(serviceStatePaths());
|
|
426
|
+
if (lease) console.log(lease);
|
|
422
427
|
console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
|
|
423
428
|
break;
|
|
424
429
|
}
|
|
@@ -5,6 +5,22 @@ export interface OwnershipMutationLeaseOptions {
|
|
|
5
5
|
readonly processAlive?: (pid: number) => boolean;
|
|
6
6
|
readonly beforeRelease?: (lockPath: string) => void;
|
|
7
7
|
readonly joinToken?: string;
|
|
8
|
+
/** Names the current executable at the recorded PID, identity unverified; defaults to `tasklist` or `ps`. */
|
|
9
|
+
readonly processImage?: (pid: number) => string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The recorded lease owner, identity unverified, read for diagnostics; never used to reclaim it. */
|
|
13
|
+
export interface OwnershipMutationLeaseHolder {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
/** The recorded PID; null when the directory holds no parseable owner. */
|
|
16
|
+
readonly pid: number | null;
|
|
17
|
+
/** Liveness of the current process at the recorded PID; identity is not verified. */
|
|
18
|
+
readonly alive: boolean | null;
|
|
19
|
+
/** The current executable at the recorded PID when alive and readable; identity is not verified. */
|
|
20
|
+
readonly image: string | null;
|
|
21
|
+
/** Milliseconds since the owner was written, on the clock stale recovery uses. */
|
|
22
|
+
readonly ageMs: number | null;
|
|
23
|
+
readonly record: "complete" | "incomplete" | "empty" | "unreadable";
|
|
8
24
|
}
|
|
9
25
|
|
|
10
26
|
export interface OwnershipMutationLease { readonly token: string; release(): void }
|
|
@@ -30,3 +46,13 @@ export declare function withOwnershipMutationLease<T>(
|
|
|
30
46
|
run: () => T,
|
|
31
47
|
options?: OwnershipMutationLeaseOptions,
|
|
32
48
|
): T;
|
|
49
|
+
|
|
50
|
+
export declare function inspectOwnershipMutationLease(
|
|
51
|
+
statePaths: readonly string[],
|
|
52
|
+
options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
|
|
53
|
+
): OwnershipMutationLeaseHolder | null;
|
|
54
|
+
|
|
55
|
+
export declare function ownershipMutationLeaseStatusLine(
|
|
56
|
+
statePaths: readonly string[],
|
|
57
|
+
options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
|
|
58
|
+
): string | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
closeSync,
|
|
@@ -108,6 +109,73 @@ function readIncompleteOwner(path) {
|
|
|
108
109
|
} catch { return null; }
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
/** The current executable at the recorded PID, identity unverified; null if unreadable within a second. */
|
|
113
|
+
function processImage(pid) {
|
|
114
|
+
try {
|
|
115
|
+
if (process.platform === "win32") {
|
|
116
|
+
const listed = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
117
|
+
encoding: "utf8", timeout: 1_000, windowsHide: true,
|
|
118
|
+
});
|
|
119
|
+
const match = /^"([^"]+)","(\d+)"/.exec((listed.stdout ?? "").trim());
|
|
120
|
+
return listed.status === 0 && match && Number(match[2]) === pid ? match[1] : null;
|
|
121
|
+
}
|
|
122
|
+
const listed = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", timeout: 1_000 });
|
|
123
|
+
const command = (listed.stdout ?? "").trim();
|
|
124
|
+
return listed.status === 0 && command ? basename(command) : null;
|
|
125
|
+
} catch { return null; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The recorded owner of the lease directory at `path`, read as stale recovery reads it, or null
|
|
130
|
+
* when no lock directory exists. `pid` is null when the directory has no parseable owner; `age`
|
|
131
|
+
* uses the same clock as reclamation (the later of the record's creation and its file mtime).
|
|
132
|
+
* Liveness and image describe the current process at the recorded PID; its identity is not verified.
|
|
133
|
+
*/
|
|
134
|
+
function leaseHolder(path, now, alive, image) {
|
|
135
|
+
if (!existsSync(path)) return null;
|
|
136
|
+
const observed = readOwner(path);
|
|
137
|
+
const incomplete = observed ? null : readIncompleteOwner(path);
|
|
138
|
+
// Neither reader matched: the holder may have released between these reads. Report a free
|
|
139
|
+
// lease then, not an unreadable one.
|
|
140
|
+
if (!observed && !incomplete && !existsSync(path)) return null;
|
|
141
|
+
const pid = observed?.record.pid ?? incomplete?.pid ?? null;
|
|
142
|
+
const since = observed ? Math.max(observed.record.createdAt, observed.mtimeMs) : incomplete?.mtimeMs ?? null;
|
|
143
|
+
const live = pid === null ? null : alive(pid);
|
|
144
|
+
return {
|
|
145
|
+
path,
|
|
146
|
+
pid,
|
|
147
|
+
alive: live,
|
|
148
|
+
image: live ? image(pid) : null,
|
|
149
|
+
ageMs: since === null ? null : Math.max(0, now() - since),
|
|
150
|
+
record: observed ? "complete" : incomplete?.ownerPath ? "incomplete" : incomplete ? "empty" : "unreadable",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function describeHolder(holder) {
|
|
155
|
+
const age = holder.ageMs === null ? "" : `, lease age ${Math.round(holder.ageMs / 1_000)}s`;
|
|
156
|
+
if (holder.record === "unreadable") return "holder unknown: the lock directory does not hold exactly one owner file";
|
|
157
|
+
if (holder.record === "empty") return `no owner file written yet${age}`;
|
|
158
|
+
const state = holder.alive ? ["alive", ...(holder.image ? [holder.image] : [])].join(", ") : "not alive";
|
|
159
|
+
const partial = holder.record === "incomplete" ? ", owner record incomplete" : "";
|
|
160
|
+
return `recorded PID ${holder.pid} [${state}, identity unverified${partial}]${age}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const RECLAIM_HINT = `stale leases are reclaimed after ${STALE_MS / 1_000}s once the recorded PID is no longer alive`;
|
|
164
|
+
|
|
165
|
+
/** The recorded lease owner for `statePaths`, identity unverified, or null if free. Reads only; never reclaims. */
|
|
166
|
+
export function inspectOwnershipMutationLease(statePaths, options = {}) {
|
|
167
|
+
return leaseHolder(leasePath(statePaths), options.now ?? Date.now, options.processAlive ?? processAlive,
|
|
168
|
+
options.processImage ?? processImage);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** One status line naming the recorded PID, identity unverified, or null when the lease is free. */
|
|
172
|
+
export function ownershipMutationLeaseStatusLine(statePaths, options = {}) {
|
|
173
|
+
const holder = inspectOwnershipMutationLease(statePaths, options);
|
|
174
|
+
if (!holder) return null;
|
|
175
|
+
return `Runtime mutation lease busy at ${holder.path} (${describeHolder(holder)}); `
|
|
176
|
+
+ `opencodex waits for it to start or change service state, and ${RECLAIM_HINT}.`;
|
|
177
|
+
}
|
|
178
|
+
|
|
111
179
|
function reclaim(path, now, alive) {
|
|
112
180
|
const observed = readOwner(path);
|
|
113
181
|
const incomplete = observed ? null : readIncompleteOwner(path);
|
|
@@ -182,7 +250,14 @@ export function acquireOwnershipMutationLease(
|
|
|
182
250
|
}
|
|
183
251
|
if (error?.code !== "EEXIST") throw error;
|
|
184
252
|
if (reclaim(path, now, alive)) continue;
|
|
185
|
-
if (now() >= deadline)
|
|
253
|
+
if (now() >= deadline) {
|
|
254
|
+
const holder = leaseHolder(path, now, alive, options.processImage ?? processImage);
|
|
255
|
+
const error = new Error(`another process owns the runtime mutation lease at ${path}`
|
|
256
|
+
+ (holder ? ` (${describeHolder(holder)}; ${RECLAIM_HINT})` : ""));
|
|
257
|
+
error.code = "OWNERSHIP_MUTATION_LEASE_BUSY";
|
|
258
|
+
error.holder = holder;
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
186
261
|
(options.sleep ?? sleep)(POLL_MS);
|
|
187
262
|
}
|
|
188
263
|
}
|
|
@@ -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. */
|
package/src/types/config.ts
CHANGED
|
@@ -219,6 +219,8 @@ export interface OcxClaudeCodeConfig {
|
|
|
219
219
|
* definition. Unset inherits the parent session effort.
|
|
220
220
|
*/
|
|
221
221
|
subagentEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
|
222
|
+
/** Optional roster-style model forced on subagents at the next routed Claude Code launch. */
|
|
223
|
+
subagentModelForce?: string;
|
|
222
224
|
/** Claude-originated web-search override. Unset fields inherit the global sidecar settings. */
|
|
223
225
|
webSearchSidecar?: { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa"; model?: string };
|
|
224
226
|
/** Claude-originated vision override. Unset fields inherit the global sidecar settings. */
|
|
@@ -750,6 +752,8 @@ export interface OcxConfig {
|
|
|
750
752
|
modelPinnedEfforts?: Record<string, string>;
|
|
751
753
|
compactionRouting?: {
|
|
752
754
|
model: string;
|
|
755
|
+
/** Incoming model allowlist: exact selectors or provider/*; omitted means all models. */
|
|
756
|
+
sourceModels?: string[];
|
|
753
757
|
reasoningEffort?: string;
|
|
754
758
|
/** Compaction triggers this override covers; omission means `["manual"]`. */
|
|
755
759
|
triggers?: ("manual" | "auto")[];
|
|
@@ -1214,6 +1218,8 @@ export interface OcxConfig {
|
|
|
1214
1218
|
*/
|
|
1215
1219
|
anthropicAccountPool?: {
|
|
1216
1220
|
enabled?: boolean;
|
|
1221
|
+
/** Preserve native Claude Messages while the pool is enabled. Default true; false selects legacy translation. */
|
|
1222
|
+
nativeMessages?: boolean;
|
|
1217
1223
|
/** Usage % threshold for new-session auto-pick. Default 80. 0 = disabled (affinity/active only). */
|
|
1218
1224
|
autoSwitchThreshold?: number;
|
|
1219
1225
|
/** New-session rotation strategy. Default quota (today's behaviour). */
|