@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
import { SHIM_MARKER } from "./shim-templates";
|
|
4
|
+
import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The proof-captured launcher snapshot used to identify the selected candidate.
|
|
8
|
+
* Every field is optional because older launchers and direct Bun/source runs
|
|
9
|
+
* supply no snapshot at all; derivation fails closed rather than reading the
|
|
10
|
+
* ambient environment.
|
|
11
|
+
*/
|
|
12
|
+
export interface CodexCliInstallationSnapshot {
|
|
13
|
+
readonly codexCliPath?: string | null;
|
|
14
|
+
readonly path?: string | null;
|
|
15
|
+
readonly pathExt?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CodexCliInstallationTargetRefusal =
|
|
19
|
+
| "unsupported_platform"
|
|
20
|
+
| "candidate_unavailable"
|
|
21
|
+
| "unsupported_layout"
|
|
22
|
+
| "toolchain_unresolved";
|
|
23
|
+
|
|
24
|
+
export type CodexCliInstallationTargetDerivation =
|
|
25
|
+
| { readonly kind: "derived"; readonly input: CodexCliInstallationIdentityInput }
|
|
26
|
+
| { readonly kind: "unavailable"; readonly reason: CodexCliInstallationTargetRefusal };
|
|
27
|
+
|
|
28
|
+
export interface CodexCliInstallationTargetDeps {
|
|
29
|
+
readonly platform?: NodeJS.Platform;
|
|
30
|
+
readonly exists?: (path: string) => boolean;
|
|
31
|
+
/** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
|
|
32
|
+
readonly fileContains?: (path: string, marker: string) => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
|
|
36
|
+
const SHIM_PROBE_BYTES = 8 * 1024;
|
|
37
|
+
const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";
|
|
38
|
+
|
|
39
|
+
function defaultFileContains(path: string, marker: string): boolean {
|
|
40
|
+
let descriptor: number | undefined;
|
|
41
|
+
let contains = false;
|
|
42
|
+
try {
|
|
43
|
+
descriptor = openSync(path, "r");
|
|
44
|
+
const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
|
|
45
|
+
const count = readSync(descriptor, bytes, 0, bytes.length, 0);
|
|
46
|
+
contains = bytes.subarray(0, count).toString("utf8").includes(marker);
|
|
47
|
+
} catch {
|
|
48
|
+
contains = false;
|
|
49
|
+
} finally {
|
|
50
|
+
if (descriptor !== undefined) {
|
|
51
|
+
try { closeSync(descriptor); } catch { contains = false; }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return contains;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* First match wins, mirroring PATH resolution: directories in order, and within
|
|
59
|
+
* each directory every PATHEXT suffix in order (or the exact name when it
|
|
60
|
+
* already carries an extension). Skipping a hit to keep scanning would attest
|
|
61
|
+
* something other than the launcher that actually resolves.
|
|
62
|
+
*/
|
|
63
|
+
function scanPath(
|
|
64
|
+
name: string,
|
|
65
|
+
pathValue: string | null | undefined,
|
|
66
|
+
pathExt: string | null | undefined,
|
|
67
|
+
exists: (path: string) => boolean,
|
|
68
|
+
): string | null {
|
|
69
|
+
const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
|
|
70
|
+
const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
|
|
71
|
+
for (const entry of (pathValue ?? "").split(";")) {
|
|
72
|
+
const dir = entry.trim().replace(/^"+|"+$/g, "");
|
|
73
|
+
if (!dir) continue;
|
|
74
|
+
for (const candidateName of names) {
|
|
75
|
+
const candidate = win32.join(dir, candidateName);
|
|
76
|
+
if (exists(candidate)) return candidate;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Derive the four attestation inputs from the proof-captured launcher snapshot.
|
|
84
|
+
* Discovery only proposes candidate paths; the held-handle observation remains
|
|
85
|
+
* the authority, so a stale or racing probe can only produce a refusal, never a
|
|
86
|
+
* false identity. No ambient environment is read: without a snapshot the result
|
|
87
|
+
* is unavailable rather than silently trusting the child's environment.
|
|
88
|
+
*/
|
|
89
|
+
export function deriveCodexCliInstallationInput(
|
|
90
|
+
snapshot: CodexCliInstallationSnapshot,
|
|
91
|
+
deps: CodexCliInstallationTargetDeps = {},
|
|
92
|
+
): CodexCliInstallationTargetDerivation {
|
|
93
|
+
if ((deps.platform ?? process.platform) !== "win32") {
|
|
94
|
+
return { kind: "unavailable", reason: "unsupported_platform" };
|
|
95
|
+
}
|
|
96
|
+
const exists = deps.exists ?? existsSync;
|
|
97
|
+
const fileContains = deps.fileContains ?? defaultFileContains;
|
|
98
|
+
const configured = snapshot.codexCliPath;
|
|
99
|
+
|
|
100
|
+
let candidate: string | null;
|
|
101
|
+
if (configured) {
|
|
102
|
+
if (/^[a-z]:[\\/]/i.test(configured)) {
|
|
103
|
+
if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
104
|
+
candidate = win32.normalize(configured);
|
|
105
|
+
} else {
|
|
106
|
+
if (configured.includes("/") || configured.includes("\\")) {
|
|
107
|
+
return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
108
|
+
}
|
|
109
|
+
candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
|
|
110
|
+
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
|
|
114
|
+
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
|
|
118
|
+
// artifact. The renamed original beside it is the file npm wrote.
|
|
119
|
+
if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) {
|
|
120
|
+
const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
|
|
121
|
+
if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" };
|
|
122
|
+
candidate = backing;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const base = win32.basename(candidate).toLowerCase();
|
|
126
|
+
let prefix: string;
|
|
127
|
+
if (base === "codex.cmd" || base === "codex.opencodex-real.cmd") {
|
|
128
|
+
prefix = win32.dirname(candidate);
|
|
129
|
+
} else if (candidate.toLowerCase().endsWith(CODEX_PACKAGE_SUFFIX)) {
|
|
130
|
+
prefix = candidate.slice(0, candidate.length - CODEX_PACKAGE_SUFFIX.length);
|
|
131
|
+
} else {
|
|
132
|
+
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
133
|
+
}
|
|
134
|
+
if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) {
|
|
135
|
+
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
|
|
139
|
+
let node = win32.join(prefix, "node.exe");
|
|
140
|
+
if (!exists(node)) {
|
|
141
|
+
const resolved = scanPath("node.exe", snapshot.path, null, exists);
|
|
142
|
+
if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
143
|
+
node = resolved;
|
|
144
|
+
}
|
|
145
|
+
const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
|
|
146
|
+
if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
kind: "derived",
|
|
150
|
+
input: Object.freeze({
|
|
151
|
+
candidate,
|
|
152
|
+
npmPrefix: prefix,
|
|
153
|
+
npmCli,
|
|
154
|
+
node,
|
|
155
|
+
candidateSource: "selected",
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -7,14 +7,17 @@ export const CONTEXT_BACKEND_PREFIX = "/backend-api/codex";
|
|
|
7
7
|
|
|
8
8
|
/** The single definition of the opt-in, shared by injection and the runtime gate. */
|
|
9
9
|
export function contextExperimentalEnabled(configContent: string): boolean {
|
|
10
|
-
let parsed: { features?: { context_management?: { experimental_mode?: boolean } } };
|
|
10
|
+
let parsed: { features?: { context_management?: boolean | { experimental_mode?: boolean } } };
|
|
11
11
|
try {
|
|
12
12
|
parsed = Bun.TOML.parse(configContent) as typeof parsed;
|
|
13
13
|
} catch {
|
|
14
14
|
// Injection tolerates incomplete user config; malformed TOML is not an opt-in.
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
// Codex FeatureToml accepts either a boolean or the experimental_mode table.
|
|
18
|
+
// Keep strict true checks: malformed values must not activate a credentialed relay.
|
|
19
|
+
const setting = parsed.features?.context_management;
|
|
20
|
+
return setting === true || (typeof setting === "object" && setting?.experimental_mode === true);
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
let activation: { key: string; active: boolean } | undefined;
|
package/src/codex/convergence.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
catalogHasRoutedEntries,
|
|
37
37
|
findSupportedNativeTemplate,
|
|
38
38
|
legacyCatalogBackupPath,
|
|
39
|
+
nativeMultiAgentDefaults,
|
|
39
40
|
parseCatalogJson,
|
|
40
41
|
type RawCatalog,
|
|
41
42
|
type RawEntry,
|
|
@@ -69,6 +70,7 @@ import {
|
|
|
69
70
|
clampCatalogModelsToObservedCodexSupport,
|
|
70
71
|
supportedCodexReasoningEffortsFromObservedCatalog,
|
|
71
72
|
} from "./catalog/effort";
|
|
73
|
+
import { suppressedSyntheticMaxCatalogSlugs } from "./catalog/model-hints";
|
|
72
74
|
import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
|
|
73
75
|
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "./account-namespaces";
|
|
74
76
|
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
@@ -305,6 +307,7 @@ function prepareCatalog(
|
|
|
305
307
|
[catalog.models ?? [], ...nativeRecoverySources],
|
|
306
308
|
);
|
|
307
309
|
const catalogModels = nativeCatalogModels;
|
|
310
|
+
const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(config, ordered, catalogModels);
|
|
308
311
|
const routedEntries = buildCatalogEntriesFromObservedState({
|
|
309
312
|
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
310
313
|
gptSlugs: [],
|
|
@@ -371,8 +374,10 @@ function prepareCatalog(
|
|
|
371
374
|
includeNativeOpenAi,
|
|
372
375
|
accountBoundEntries,
|
|
373
376
|
suppressedBareNativeSlugs,
|
|
377
|
+
suppressedSyntheticMaxSlugs,
|
|
374
378
|
openaiContextCap,
|
|
375
379
|
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
380
|
+
nativeMultiAgentDefaults: nativeMultiAgentDefaults(baselineCatalogModels),
|
|
376
381
|
policy: {
|
|
377
382
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
378
383
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
@@ -260,7 +260,10 @@ export async function syncCodexOnStartIfEnabled(
|
|
|
260
260
|
// The `.catch` is deliberate and stays: a failure to APPLY must not stop the
|
|
261
261
|
// proxy from coming up. A failed sync simply reports no writes. The readiness
|
|
262
262
|
// gate observes the real outcome so /readyz reflects the sync state exactly as
|
|
263
|
-
// the
|
|
263
|
+
// the contract defines: ready on ok=true once the sync has settled. A nonempty
|
|
264
|
+
// `warning` is a local-Codex-artifact degradation the sync chose to continue
|
|
265
|
+
// past, and #5181 is what it cost to treat it as terminal — a healthy
|
|
266
|
+
// multi-provider proxy reported itself permanently unready.
|
|
264
267
|
const outcome = readinessGate
|
|
265
268
|
? await runStartupReadinessSync(readinessGate, async () => (await sync(port)) ?? null)
|
|
266
269
|
: await sync(port).catch(() => undefined);
|
package/src/codex/history-job.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
CodexHistoryWorkerOperation,
|
|
24
24
|
HistoryWorkerResult,
|
|
25
25
|
} from "./history-worker";
|
|
26
|
-
import { currentHistoryDbBusyTimeoutMs,
|
|
26
|
+
import { currentHistoryDbBusyTimeoutMs, resolveExistingHistoryBackupPath } from "./history-provider";
|
|
27
27
|
import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider";
|
|
28
28
|
import { getCodexHome, resolveCodexStateDbPath } from "./paths";
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ import { getCodexHome, resolveCodexStateDbPath } from "./paths";
|
|
|
32
32
|
*
|
|
33
33
|
* The SQLite root can differ from CODEX_HOME and both environment/config inputs
|
|
34
34
|
* can change between invocations. The parent resolves one exact target and hands
|
|
35
|
-
* those
|
|
35
|
+
* those resolved paths to the Worker rather than asking the Worker to infer a
|
|
36
36
|
* possibly different environment.
|
|
37
37
|
*/
|
|
38
38
|
export function resolveCodexHistoryJobTarget(): {
|
|
@@ -45,10 +45,10 @@ export function resolveCodexHistoryJobTarget(): {
|
|
|
45
45
|
return {
|
|
46
46
|
canonicalCodexHome: home,
|
|
47
47
|
canonicalStateDbPath: stateDb,
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
canonicalBackupPath:
|
|
48
|
+
// Resolve through the provider's canonical-first compatibility rule. Passing
|
|
49
|
+
// only the newly normalized name would hide a pre-#4442 Windows manifest from
|
|
50
|
+
// the Worker and make an upgrade look like an empty backup.
|
|
51
|
+
canonicalBackupPath: resolveExistingHistoryBackupPath(stateDb),
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { parseSessionMetaLine, type ParsedSessionMeta } from "./history-rollout-read";
|
|
5
|
+
export { MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES, readThreadFieldsFromRollout, type RolloutThreadFields } from "./history-rollout-read";
|
|
5
6
|
import { Database } from "bun:sqlite";
|
|
6
7
|
import { resolveCodexStateDbPath } from "./paths";
|
|
8
|
+
import { openCodexStateForPreflight } from "./history-state-open";
|
|
7
9
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
8
10
|
import {
|
|
9
11
|
CODEX_HISTORY_RESUMABLE_SOURCES,
|
|
@@ -15,13 +17,6 @@ import {
|
|
|
15
17
|
type CodexHistoryBackupManifest,
|
|
16
18
|
} from "./history-manifest";
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
* Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
|
|
20
|
-
* Bounds peak memory while reconstructing thread rows; never write the decoded
|
|
21
|
-
* JSONL to disk.
|
|
22
|
-
*/
|
|
23
|
-
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
24
|
-
|
|
25
20
|
/**
|
|
26
21
|
* The manifest that shadows one state database.
|
|
27
22
|
*
|
|
@@ -433,23 +428,39 @@ export function preflightCodexHistoryInjection(
|
|
|
433
428
|
if (!existsSync(resolvedPath)) {
|
|
434
429
|
return restoreEntries.length > 0 ? "history_state_database_missing" : null;
|
|
435
430
|
}
|
|
436
|
-
|
|
431
|
+
// Read-only, and narrowed so a cleanly-closed WAL store is inspected rather than refused
|
|
432
|
+
// (#4943). The open order is the safety property; see history-state-open.ts.
|
|
433
|
+
db = openCodexStateForPreflight(resolvedPath);
|
|
437
434
|
const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
|
|
438
435
|
const paginatedColumn = columns.some(column => column.name === "history_mode");
|
|
439
436
|
if (paginatedColumn && restoreEntries.length > 0) return HISTORY_RELABEL_STANDS_DOWN;
|
|
440
437
|
for (const entry of restoreEntries) assertLegacyHistoryWritable(entry.rolloutPath);
|
|
441
|
-
const rows = db.query<{ rollout_path: string; history_mode: string | null }, []>(`
|
|
442
|
-
SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}
|
|
438
|
+
const rows = db.query<{ rollout_path: string; history_mode: string | null; model_provider: string }, []>(`
|
|
439
|
+
SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}, model_provider
|
|
443
440
|
FROM threads
|
|
444
441
|
WHERE ${providerTableMode
|
|
445
442
|
? resumeHistory ? "model_provider IN ('openai', 'opencodex')" : "0"
|
|
446
443
|
: "model_provider = 'opencodex'"}
|
|
447
444
|
`).all();
|
|
445
|
+
// No ORDER BY: a paginated opencodex row can precede a paginated openai one, so the
|
|
446
|
+
// verdict waits for the full scan instead of standing down on the first paginated row.
|
|
447
|
+
let foundPaginatedRow = false;
|
|
448
|
+
let foundPaginatedOpenaiRow = false;
|
|
448
449
|
for (const row of rows) {
|
|
449
|
-
if (paginatedColumn || row.history_mode === "paginated")
|
|
450
|
+
if (paginatedColumn || row.history_mode === "paginated") {
|
|
451
|
+
foundPaginatedRow = true;
|
|
452
|
+
// A provider-table transition removes the root openai_base_url, and a row already
|
|
453
|
+
// paginated cannot be relabeled: standing it down would route the openai-tagged
|
|
454
|
+
// thread to Codex's built-in OpenAI endpoint. A still-legacy row only stands down.
|
|
455
|
+
if (providerTableMode && row.history_mode === "paginated" && row.model_provider === "openai") {
|
|
456
|
+
foundPaginatedOpenaiRow = true;
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
450
460
|
assertLegacyHistoryWritable(row.rollout_path);
|
|
451
461
|
}
|
|
452
|
-
return
|
|
462
|
+
if (foundPaginatedOpenaiRow) return "history_paginated_openai_requires_native_writer";
|
|
463
|
+
return foundPaginatedRow ? HISTORY_RELABEL_STANDS_DOWN : null;
|
|
453
464
|
} catch (error) {
|
|
454
465
|
return error instanceof CodexHistoryIntegrityError
|
|
455
466
|
? error.message
|
|
@@ -1018,24 +1029,6 @@ function assertRestoreReadback(
|
|
|
1018
1029
|
}
|
|
1019
1030
|
}
|
|
1020
1031
|
|
|
1021
|
-
interface ParsedSessionMeta {
|
|
1022
|
-
record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
/** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
|
|
1026
|
-
function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
|
|
1027
|
-
let parsed: unknown;
|
|
1028
|
-
try {
|
|
1029
|
-
parsed = JSON.parse(line);
|
|
1030
|
-
} catch {
|
|
1031
|
-
return null;
|
|
1032
|
-
}
|
|
1033
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
1034
|
-
const record = parsed as ParsedSessionMeta["record"];
|
|
1035
|
-
if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
|
|
1036
|
-
return { record };
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
1032
|
/**
|
|
1040
1033
|
* Find the LAST `session_meta` line in a rollout, mirroring the app's last-writer-wins fold
|
|
1041
1034
|
* (codex-rs `apply_session_meta_from_item`). We base our patch on the most recent metadata so we
|
|
@@ -1104,142 +1097,6 @@ function compensateConcurrentSessionMetaAppend(
|
|
|
1104
1097
|
}
|
|
1105
1098
|
}
|
|
1106
1099
|
|
|
1107
|
-
/**
|
|
1108
|
-
* Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
|
|
1109
|
-
* Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
|
|
1110
|
-
*
|
|
1111
|
-
* Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
|
|
1112
|
-
* first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
|
|
1113
|
-
*/
|
|
1114
|
-
export interface RolloutThreadFields {
|
|
1115
|
-
id: string;
|
|
1116
|
-
modelProvider: string;
|
|
1117
|
-
source: string;
|
|
1118
|
-
firstUserMessage: string;
|
|
1119
|
-
hasUserEvent: number;
|
|
1120
|
-
cwd?: string;
|
|
1121
|
-
historyMode?: string;
|
|
1122
|
-
cliVersion?: string;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function textFromContentParts(content: unknown): string | null {
|
|
1126
|
-
if (typeof content === "string" && content.trim()) return content.trim();
|
|
1127
|
-
if (!Array.isArray(content)) return null;
|
|
1128
|
-
const parts: string[] = [];
|
|
1129
|
-
for (const part of content) {
|
|
1130
|
-
if (!part || typeof part !== "object") continue;
|
|
1131
|
-
const p = part as Record<string, unknown>;
|
|
1132
|
-
if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
|
|
1133
|
-
else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
|
|
1134
|
-
}
|
|
1135
|
-
const joined = parts.join("\n").trim();
|
|
1136
|
-
return joined || null;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
/** Extract the first user-message preview from a rollout line, or null. */
|
|
1140
|
-
function extractUserMessagePreview(line: string): string | null {
|
|
1141
|
-
let parsed: unknown;
|
|
1142
|
-
try {
|
|
1143
|
-
parsed = JSON.parse(line);
|
|
1144
|
-
} catch {
|
|
1145
|
-
return null;
|
|
1146
|
-
}
|
|
1147
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
1148
|
-
const record = parsed as { type?: unknown; payload?: unknown };
|
|
1149
|
-
const payload = record.payload;
|
|
1150
|
-
if (!payload || typeof payload !== "object") return null;
|
|
1151
|
-
const p = payload as Record<string, unknown>;
|
|
1152
|
-
|
|
1153
|
-
if (record.type === "event_msg") {
|
|
1154
|
-
// codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
|
|
1155
|
-
if (p.type === "user_message" || typeof p.message === "string") {
|
|
1156
|
-
if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
|
|
1157
|
-
const fromContent = textFromContentParts(p.content);
|
|
1158
|
-
if (fromContent) return fromContent;
|
|
1159
|
-
}
|
|
1160
|
-
return null;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
if (record.type === "response_item") {
|
|
1164
|
-
if (p.type === "message" && p.role === "user") {
|
|
1165
|
-
return textFromContentParts(p.content);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
return null;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/**
|
|
1172
|
-
* Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
|
|
1173
|
-
* Returns null when the file is missing or has no parseable `session_meta`.
|
|
1174
|
-
*
|
|
1175
|
-
* Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
|
|
1176
|
-
* rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
|
|
1177
|
-
* no decompressed copy is written to disk.
|
|
1178
|
-
*/
|
|
1179
|
-
export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
|
|
1180
|
-
if (!path || !existsSync(path)) return null;
|
|
1181
|
-
let raw: string;
|
|
1182
|
-
try {
|
|
1183
|
-
raw = path.endsWith(".zst")
|
|
1184
|
-
? decompressRolloutZstUtf8(path)
|
|
1185
|
-
: readFileSync(path, "utf8");
|
|
1186
|
-
} catch {
|
|
1187
|
-
return null;
|
|
1188
|
-
}
|
|
1189
|
-
return parseThreadFieldsFromRolloutText(raw);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function decompressRolloutZstUtf8(
|
|
1193
|
-
path: string,
|
|
1194
|
-
maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
|
|
1195
|
-
): string {
|
|
1196
|
-
const compressed = readFileSync(path);
|
|
1197
|
-
const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
|
|
1198
|
-
maxOutputLength: maxBytes,
|
|
1199
|
-
});
|
|
1200
|
-
if (decoded.byteLength > maxBytes) {
|
|
1201
|
-
throw new Error("rollout_zst_too_large");
|
|
1202
|
-
}
|
|
1203
|
-
return new TextDecoder().decode(decoded);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
|
|
1207
|
-
const lines = raw.split("\n");
|
|
1208
|
-
let latest: ParsedSessionMeta | null = null;
|
|
1209
|
-
let firstUserMessage = "";
|
|
1210
|
-
for (const line of lines) {
|
|
1211
|
-
if (!line) continue;
|
|
1212
|
-
if (line.includes("\"session_meta\"")) {
|
|
1213
|
-
const meta = parseSessionMetaLine(line);
|
|
1214
|
-
if (meta) latest = meta;
|
|
1215
|
-
}
|
|
1216
|
-
if (!firstUserMessage) {
|
|
1217
|
-
const preview = extractUserMessagePreview(line);
|
|
1218
|
-
if (preview) firstUserMessage = preview;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
if (!latest) return null;
|
|
1222
|
-
const payload = latest.record.payload;
|
|
1223
|
-
const id = typeof payload.id === "string" ? payload.id : "";
|
|
1224
|
-
if (!id) return null;
|
|
1225
|
-
const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
|
|
1226
|
-
? payload.model_provider
|
|
1227
|
-
: "openai";
|
|
1228
|
-
const source = typeof payload.source === "string" && payload.source
|
|
1229
|
-
? payload.source
|
|
1230
|
-
: "cli";
|
|
1231
|
-
return {
|
|
1232
|
-
id,
|
|
1233
|
-
modelProvider,
|
|
1234
|
-
source,
|
|
1235
|
-
firstUserMessage,
|
|
1236
|
-
hasUserEvent: firstUserMessage.trim() ? 1 : 0,
|
|
1237
|
-
...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
|
|
1238
|
-
...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
|
|
1239
|
-
...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
1100
|
/**
|
|
1244
1101
|
* Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
|
|
1245
1102
|
* rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reading side of Codex rollout JSONL files: the `session_meta` fold and the thread fields a
|
|
6
|
+
* quarantine restore reconstructs from a rollout.
|
|
7
|
+
*
|
|
8
|
+
* Split out of `history-provider.ts` when that file crossed the repository file-size ratchet at
|
|
9
|
+
* 2009 lines. The cut follows a real seam rather than a line count: everything here reads a
|
|
10
|
+
* rollout file and returns plain data, while the module it left keeps the database and manifest
|
|
11
|
+
* writes. Nothing here opens a database or touches a backup manifest.
|
|
12
|
+
*
|
|
13
|
+
* `history-provider.ts` re-exports the public names, so existing importers are unaffected.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
|
|
17
|
+
* Bounds peak memory while reconstructing thread rows; never write the decoded
|
|
18
|
+
* JSONL to disk.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
export interface ParsedSessionMeta {
|
|
23
|
+
record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
|
|
27
|
+
export function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
|
|
28
|
+
let parsed: unknown;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(line);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
35
|
+
const record = parsed as ParsedSessionMeta["record"];
|
|
36
|
+
if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
|
|
37
|
+
return { record };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
|
|
42
|
+
* Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
|
|
43
|
+
*
|
|
44
|
+
* Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
|
|
45
|
+
* first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
|
|
46
|
+
*/
|
|
47
|
+
export interface RolloutThreadFields {
|
|
48
|
+
id: string;
|
|
49
|
+
modelProvider: string;
|
|
50
|
+
source: string;
|
|
51
|
+
firstUserMessage: string;
|
|
52
|
+
hasUserEvent: number;
|
|
53
|
+
cwd?: string;
|
|
54
|
+
historyMode?: string;
|
|
55
|
+
cliVersion?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function textFromContentParts(content: unknown): string | null {
|
|
59
|
+
if (typeof content === "string" && content.trim()) return content.trim();
|
|
60
|
+
if (!Array.isArray(content)) return null;
|
|
61
|
+
const parts: string[] = [];
|
|
62
|
+
for (const part of content) {
|
|
63
|
+
if (!part || typeof part !== "object") continue;
|
|
64
|
+
const p = part as Record<string, unknown>;
|
|
65
|
+
if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
|
|
66
|
+
else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
|
|
67
|
+
}
|
|
68
|
+
const joined = parts.join("\n").trim();
|
|
69
|
+
return joined || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Extract the first user-message preview from a rollout line, or null. */
|
|
73
|
+
function extractUserMessagePreview(line: string): string | null {
|
|
74
|
+
let parsed: unknown;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(line);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
81
|
+
const record = parsed as { type?: unknown; payload?: unknown };
|
|
82
|
+
const payload = record.payload;
|
|
83
|
+
if (!payload || typeof payload !== "object") return null;
|
|
84
|
+
const p = payload as Record<string, unknown>;
|
|
85
|
+
|
|
86
|
+
if (record.type === "event_msg") {
|
|
87
|
+
// codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
|
|
88
|
+
if (p.type === "user_message" || typeof p.message === "string") {
|
|
89
|
+
if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
|
|
90
|
+
const fromContent = textFromContentParts(p.content);
|
|
91
|
+
if (fromContent) return fromContent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (record.type === "response_item") {
|
|
97
|
+
if (p.type === "message" && p.role === "user") {
|
|
98
|
+
return textFromContentParts(p.content);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
|
|
106
|
+
* Returns null when the file is missing or has no parseable `session_meta`.
|
|
107
|
+
*
|
|
108
|
+
* Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
|
|
109
|
+
* rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
|
|
110
|
+
* no decompressed copy is written to disk.
|
|
111
|
+
*/
|
|
112
|
+
export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
|
|
113
|
+
if (!path || !existsSync(path)) return null;
|
|
114
|
+
let raw: string;
|
|
115
|
+
try {
|
|
116
|
+
raw = path.endsWith(".zst")
|
|
117
|
+
? decompressRolloutZstUtf8(path)
|
|
118
|
+
: readFileSync(path, "utf8");
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return parseThreadFieldsFromRolloutText(raw);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decompressRolloutZstUtf8(
|
|
126
|
+
path: string,
|
|
127
|
+
maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
|
|
128
|
+
): string {
|
|
129
|
+
const compressed = readFileSync(path);
|
|
130
|
+
const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
|
|
131
|
+
maxOutputLength: maxBytes,
|
|
132
|
+
});
|
|
133
|
+
if (decoded.byteLength > maxBytes) {
|
|
134
|
+
throw new Error("rollout_zst_too_large");
|
|
135
|
+
}
|
|
136
|
+
return new TextDecoder().decode(decoded);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
|
|
140
|
+
const lines = raw.split("\n");
|
|
141
|
+
let latest: ParsedSessionMeta | null = null;
|
|
142
|
+
let firstUserMessage = "";
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (!line) continue;
|
|
145
|
+
if (line.includes("\"session_meta\"")) {
|
|
146
|
+
const meta = parseSessionMetaLine(line);
|
|
147
|
+
if (meta) latest = meta;
|
|
148
|
+
}
|
|
149
|
+
if (!firstUserMessage) {
|
|
150
|
+
const preview = extractUserMessagePreview(line);
|
|
151
|
+
if (preview) firstUserMessage = preview;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!latest) return null;
|
|
155
|
+
const payload = latest.record.payload;
|
|
156
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
157
|
+
if (!id) return null;
|
|
158
|
+
const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
|
|
159
|
+
? payload.model_provider
|
|
160
|
+
: "openai";
|
|
161
|
+
const source = typeof payload.source === "string" && payload.source
|
|
162
|
+
? payload.source
|
|
163
|
+
: "cli";
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
modelProvider,
|
|
167
|
+
source,
|
|
168
|
+
firstUserMessage,
|
|
169
|
+
hasUserEvent: firstUserMessage.trim() ? 1 : 0,
|
|
170
|
+
...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
|
|
171
|
+
...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
|
|
172
|
+
...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|