@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* H — the cross-process history serialization primitive.
|
|
3
|
+
*
|
|
4
|
+
* Today a native apply or restore performs SQLite rows, the backup manifest and
|
|
5
|
+
* every rollout file on the caller thread, and the manifest and rollout writes
|
|
6
|
+
* sit OUTSIDE the provider's SQLite transaction
|
|
7
|
+
* (`src/codex/history-provider.ts:606-648,656-698`). A SQLite busy timeout
|
|
8
|
+
* therefore serializes one third of a state transition and explains one stall.
|
|
9
|
+
* H is held across the whole unit — DB, manifest and rollouts together — so an
|
|
10
|
+
* opposite-direction process cannot overtake through the unguarded files.
|
|
11
|
+
*
|
|
12
|
+
* Two ways H differs from K, both forced by review:
|
|
13
|
+
*
|
|
14
|
+
* `H -> N` is a real edge, not a violation. The Worker reads the coordinator row
|
|
15
|
+
* and writes its terminal state while holding H, and both take BEGIN IMMEDIATE
|
|
16
|
+
* on N. That is why H must be its own database: sharing N's would make the
|
|
17
|
+
* Worker contend with itself. `N -> H`, `K -> H` and `C -> H` are forbidden, and
|
|
18
|
+
* the resulting `H -> N -> K -> C` order was checked acyclic in review round 3.
|
|
19
|
+
*
|
|
20
|
+
* H is keyed by the canonical state database as well as the canonical home,
|
|
21
|
+
* because one CODEX_HOME can name a different `state_5.sqlite` and those are not
|
|
22
|
+
* the same exclusion (`src/codex/user-identity.ts`).
|
|
23
|
+
*
|
|
24
|
+
* The permit is a runtime registration, not a type. An opaque TypeScript brand
|
|
25
|
+
* proves a permit-bearing call path exists; it cannot prove the callback still
|
|
26
|
+
* holds the lock, and a permit leaked past its callback type-checks perfectly.
|
|
27
|
+
* So every history mutator asks this module at runtime whether the permit it was
|
|
28
|
+
* handed is still live for the state database it is about to write.
|
|
29
|
+
*
|
|
30
|
+
* Design record: devlog/_plan/260804_codex_write_substrate/005_contract.md §6.
|
|
31
|
+
*/
|
|
32
|
+
import { chmodSync, lstatSync, realpathSync } from "node:fs";
|
|
33
|
+
|
|
34
|
+
import { Database } from "bun:sqlite";
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
CodexUserIdentityRefusal,
|
|
38
|
+
resolveCodexHistorySerializationDatabasePath,
|
|
39
|
+
resolveEffectiveUserIdentity,
|
|
40
|
+
} from "./user-identity";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Authorization to perform history mutations inside ONE H acquisition.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately carries no usable field. Holding this object is necessary and
|
|
46
|
+
* never sufficient: `assertHistoryWritePermit` decides, by looking the object up
|
|
47
|
+
* in a registry only this module can write. A forged cast, a prototype copy or a
|
|
48
|
+
* symbol clone produces a value of this type that every writer refuses.
|
|
49
|
+
*/
|
|
50
|
+
export interface HistoryWritePermit {
|
|
51
|
+
readonly [historyWritePermitBrand]: true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare const historyWritePermitBrand: unique symbol;
|
|
55
|
+
|
|
56
|
+
export type HistorySerializationOutcome<T> =
|
|
57
|
+
| { kind: "completed"; value: T }
|
|
58
|
+
| { kind: "unavailable"; reason: "busy" | "database" | "unsafe-path" };
|
|
59
|
+
|
|
60
|
+
export class HistoryWritePermitRefusal extends Error {
|
|
61
|
+
readonly code = "CODEX_HISTORY_WRITE_PERMIT_REFUSED";
|
|
62
|
+
|
|
63
|
+
constructor(message: string) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "HistoryWritePermitRefusal";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface PermitRegistration {
|
|
70
|
+
readonly canonicalStateDbPath: string;
|
|
71
|
+
readonly transactionId: string;
|
|
72
|
+
live: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Registrations are marked dead, never deleted.
|
|
77
|
+
*
|
|
78
|
+
* Deleting would make a leaked permit indistinguishable from a forged one, and
|
|
79
|
+
* those are different bugs: "used after its acquisition released" points at a
|
|
80
|
+
* caller that kept a permit past its callback, while "never minted here" points
|
|
81
|
+
* at a cast or a clone. Both are refused; only the diagnosis differs. The map is
|
|
82
|
+
* weak, so a dead entry costs nothing once the permit is unreachable.
|
|
83
|
+
*/
|
|
84
|
+
const activePermits = new WeakMap<object, PermitRegistration>();
|
|
85
|
+
let acquisitionCounter = 0;
|
|
86
|
+
|
|
87
|
+
function isBusy(error: unknown): boolean {
|
|
88
|
+
const code = error && typeof error === "object" && "code" in error
|
|
89
|
+
? String((error as { code?: unknown }).code)
|
|
90
|
+
: "";
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED"
|
|
93
|
+
|| /database (?:is|table is) locked/i.test(message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Refuse unless this exact permit is live for this exact state database.
|
|
98
|
+
*
|
|
99
|
+
* Every history mutator calls this BEFORE touching the database, the manifest or
|
|
100
|
+
* a rollout file. The state database is named by the caller rather than derived,
|
|
101
|
+
* because a rollout path can point anywhere the manifest recorded.
|
|
102
|
+
*/
|
|
103
|
+
export function assertHistoryWritePermit(
|
|
104
|
+
permit: HistoryWritePermit,
|
|
105
|
+
canonicalStateDbPath: string,
|
|
106
|
+
): void {
|
|
107
|
+
const registration = activePermits.get(permit as unknown as object);
|
|
108
|
+
if (!registration) {
|
|
109
|
+
throw new HistoryWritePermitRefusal(
|
|
110
|
+
"The history write permit was not minted by the serialization owner.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (!registration.live) {
|
|
114
|
+
throw new HistoryWritePermitRefusal(
|
|
115
|
+
"The history write permit belongs to a released acquisition.",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (registration.canonicalStateDbPath !== canonicalStateDbPath) {
|
|
119
|
+
throw new HistoryWritePermitRefusal(
|
|
120
|
+
"The history write permit authorizes a different Codex state database.",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Acquire H for one canonical state database and run `work` while it is held.
|
|
127
|
+
*
|
|
128
|
+
* The callback may enter N — that is the `H -> N` edge the Worker needs to read
|
|
129
|
+
* the coordinator row and publish its terminal state. It must never re-enter H.
|
|
130
|
+
*
|
|
131
|
+
* `busy_timeout = 0` with `BEGIN IMMEDIATE` makes contention fail fast and
|
|
132
|
+
* typed. Blocking here would hold the acquisition across an unbounded wait,
|
|
133
|
+
* which is the stall this phase exists to remove.
|
|
134
|
+
*/
|
|
135
|
+
export function withHistoryWriteSerialization<T>(
|
|
136
|
+
canonicalCodexHome: string,
|
|
137
|
+
canonicalStateDbPath: string,
|
|
138
|
+
work: (permit: HistoryWritePermit) => T,
|
|
139
|
+
): HistorySerializationOutcome<T> {
|
|
140
|
+
let databasePath: string;
|
|
141
|
+
try {
|
|
142
|
+
databasePath = resolveCodexHistorySerializationDatabasePath(
|
|
143
|
+
resolveEffectiveUserIdentity(),
|
|
144
|
+
canonicalCodexHome,
|
|
145
|
+
canonicalStateDbPath,
|
|
146
|
+
);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof CodexUserIdentityRefusal) {
|
|
149
|
+
return { kind: "unavailable", reason: "unsafe-path" };
|
|
150
|
+
}
|
|
151
|
+
return { kind: "unavailable", reason: "database" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let database: Database | undefined;
|
|
155
|
+
let transactionOpen = false;
|
|
156
|
+
let registration: PermitRegistration | undefined;
|
|
157
|
+
let permit: HistoryWritePermit | undefined;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
let databaseWasAbsent = false;
|
|
161
|
+
try {
|
|
162
|
+
const before = lstatSync(databasePath);
|
|
163
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
164
|
+
return { kind: "unavailable", reason: "unsafe-path" };
|
|
165
|
+
}
|
|
166
|
+
if (process.platform !== "win32") {
|
|
167
|
+
const uid = process.getuid?.();
|
|
168
|
+
if (uid === undefined || before.uid !== uid || (before.mode & 0o777) !== 0o600) {
|
|
169
|
+
return { kind: "unavailable", reason: "unsafe-path" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (cause) {
|
|
173
|
+
const code = cause && typeof cause === "object" && "code" in cause
|
|
174
|
+
? String((cause as { code?: unknown }).code)
|
|
175
|
+
: "";
|
|
176
|
+
if (code !== "ENOENT") throw cause;
|
|
177
|
+
databaseWasAbsent = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
database = new Database(databasePath, { create: true });
|
|
181
|
+
if (databaseWasAbsent) {
|
|
182
|
+
try { chmodSync(databasePath, 0o600); } catch { /* Windows applies ACLs in WP11. */ }
|
|
183
|
+
}
|
|
184
|
+
const opened = lstatSync(databasePath);
|
|
185
|
+
if (opened.isSymbolicLink() || !opened.isFile()
|
|
186
|
+
|| realpathSync.native(databasePath) !== databasePath) {
|
|
187
|
+
return { kind: "unavailable", reason: "unsafe-path" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
database.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
|
|
191
|
+
transactionOpen = true;
|
|
192
|
+
|
|
193
|
+
acquisitionCounter += 1;
|
|
194
|
+
registration = {
|
|
195
|
+
canonicalStateDbPath,
|
|
196
|
+
transactionId: `${process.pid}:${acquisitionCounter}`,
|
|
197
|
+
live: true,
|
|
198
|
+
};
|
|
199
|
+
// A bare object: nothing about it is guessable or reconstructable, because
|
|
200
|
+
// the authority lives in the registry entry rather than in the value.
|
|
201
|
+
permit = {} as HistoryWritePermit;
|
|
202
|
+
activePermits.set(permit as unknown as object, registration);
|
|
203
|
+
|
|
204
|
+
let value: T;
|
|
205
|
+
try {
|
|
206
|
+
value = work(permit);
|
|
207
|
+
} finally {
|
|
208
|
+
// Revoke BEFORE releasing the transaction, so a mutator racing the release
|
|
209
|
+
// can never find a live permit without a live lock. This runs on the
|
|
210
|
+
// throwing path too, which is what a `finally`-less version gets wrong.
|
|
211
|
+
registration.live = false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
database.exec("COMMIT");
|
|
215
|
+
transactionOpen = false;
|
|
216
|
+
return { kind: "completed", value };
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (transactionOpen) {
|
|
219
|
+
try { database?.exec("ROLLBACK"); } catch { /* close releases the transaction */ }
|
|
220
|
+
transactionOpen = false;
|
|
221
|
+
}
|
|
222
|
+
if (registration?.live) {
|
|
223
|
+
registration.live = false;
|
|
224
|
+
}
|
|
225
|
+
if (error instanceof CodexUserIdentityRefusal) {
|
|
226
|
+
return { kind: "unavailable", reason: "unsafe-path" };
|
|
227
|
+
}
|
|
228
|
+
if (isBusy(error)) return { kind: "unavailable", reason: "busy" };
|
|
229
|
+
// A callback failure is the caller's error, not a lock outcome: H acquired
|
|
230
|
+
// fine. Reporting it as `unavailable` would tell the caller to retry
|
|
231
|
+
// something that fails identically.
|
|
232
|
+
throw error;
|
|
233
|
+
} finally {
|
|
234
|
+
try { database?.close(); } catch { /* acquisition already finished */ }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Test-only: prove a leaked permit is dead without reaching into the registry. */
|
|
239
|
+
export function isHistoryWritePermitLive(permit: HistoryWritePermit): boolean {
|
|
240
|
+
return activePermits.get(permit as unknown as object)?.live === true;
|
|
241
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { countPendingOpencodexHistory, migrateHistoryToOpenai } from "./history-provider";
|
|
2
|
+
import { resolveCodexHistoryJobTarget, runCodexHistoryJob } from "./history-job";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Daemon-side retry for the one-time Design-B history migration.
|
|
@@ -23,7 +24,8 @@ export interface HistoryMigrationGuardianHandle {
|
|
|
23
24
|
|
|
24
25
|
export interface HistoryMigrationGuardianDeps {
|
|
25
26
|
countFn?: typeof countPendingOpencodexHistory;
|
|
26
|
-
migrateFn?: () => ReturnType<typeof migrateHistoryToOpenai
|
|
27
|
+
migrateFn?: () => ReturnType<typeof migrateHistoryToOpenai>
|
|
28
|
+
| Promise<ReturnType<typeof migrateHistoryToOpenai>>;
|
|
27
29
|
log?: Pick<Console, "log">;
|
|
28
30
|
tickMs?: number;
|
|
29
31
|
maxTicks?: number;
|
|
@@ -42,7 +44,18 @@ function defaultSchedule(fn: () => void, ms: number): { cancel(): void } {
|
|
|
42
44
|
|
|
43
45
|
export function startHistoryMigrationGuardian(deps: HistoryMigrationGuardianDeps = {}): HistoryMigrationGuardianHandle {
|
|
44
46
|
const countFn = deps.countFn ?? countPendingOpencodexHistory;
|
|
45
|
-
|
|
47
|
+
// The default migration goes through the history job, so the guardian's timer
|
|
48
|
+
// thread never performs the transition itself. A background repair that races
|
|
49
|
+
// an apply or a restore is exactly what H exists to order.
|
|
50
|
+
const migrateFn = deps.migrateFn ?? (async () => {
|
|
51
|
+
const outcome = await runCodexHistoryJob({
|
|
52
|
+
...resolveCodexHistoryJobTarget(),
|
|
53
|
+
operation: "migrate-openai",
|
|
54
|
+
});
|
|
55
|
+
return outcome.kind === "converged"
|
|
56
|
+
? { rows: outcome.rows, files: outcome.files }
|
|
57
|
+
: { rows: 0, files: 0, failed: true as const };
|
|
58
|
+
});
|
|
46
59
|
const log = deps.log ?? console;
|
|
47
60
|
const tickMs = deps.tickMs ?? DEFAULT_TICK_MS;
|
|
48
61
|
const maxTicks = deps.maxTicks ?? DEFAULT_MAX_TICKS;
|
|
@@ -56,7 +69,7 @@ export function startHistoryMigrationGuardian(deps: HistoryMigrationGuardianDeps
|
|
|
56
69
|
pending = (deps.scheduleFn ?? defaultSchedule)(tick, tickMs);
|
|
57
70
|
};
|
|
58
71
|
|
|
59
|
-
const tick = () => {
|
|
72
|
+
const tick = async () => {
|
|
60
73
|
if (stopped) return;
|
|
61
74
|
ticks++;
|
|
62
75
|
try {
|
|
@@ -66,9 +79,9 @@ export function startHistoryMigrationGuardian(deps: HistoryMigrationGuardianDeps
|
|
|
66
79
|
return;
|
|
67
80
|
}
|
|
68
81
|
// Locked probe or pending work: attempt one migration pass.
|
|
69
|
-
const result = migrateFn();
|
|
82
|
+
const result = await migrateFn();
|
|
70
83
|
if (!result.failed) {
|
|
71
|
-
const moved = result.rows + (result.ejectedRows ?? 0);
|
|
84
|
+
const moved = result.rows + ((result as { ejectedRows?: number }).ejectedRows ?? 0);
|
|
72
85
|
if (moved > 0) {
|
|
73
86
|
log.log(`🩹 history-migration: ${moved} legacy opencodex thread(s) migrated back to openai.`);
|
|
74
87
|
}
|
|
@@ -14,7 +14,14 @@ import { atomicWriteFile, getConfigDir } from "../config";
|
|
|
14
14
|
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
15
15
|
|
|
16
16
|
const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite");
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The manifest that shadows one state database.
|
|
19
|
+
*
|
|
20
|
+
* Exported because the history job must resolve it at CALL time for a Worker
|
|
21
|
+
* that does not inherit this module's load-time constants — and must resolve it
|
|
22
|
+
* the same way, since a manifest addressed differently is a different manifest.
|
|
23
|
+
*/
|
|
24
|
+
export function historyBackupPathFor(stateDbPath: string): string {
|
|
18
25
|
const normalized = process.platform === "win32" ? resolve(stateDbPath).toLowerCase() : resolve(stateDbPath);
|
|
19
26
|
const id = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
20
27
|
return join(getConfigDir(), `codex-history-backup-${id}.json`);
|
|
@@ -157,7 +164,7 @@ function patchFirstLineProviderInPlace(path: string, expectedId: string, provide
|
|
|
157
164
|
}
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
type CodexHistoryProvider = "openai" | "opencodex";
|
|
167
|
+
export type CodexHistoryProvider = "openai" | "opencodex";
|
|
161
168
|
|
|
162
169
|
export interface CodexHistorySyncResult {
|
|
163
170
|
rows: number;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the transition a history job belongs to.
|
|
3
|
+
*
|
|
4
|
+
* `updateCodexHistoryTransition` had no production caller, so every completed or
|
|
5
|
+
* skipped job left the coordinator row permanently `pending` — a transition was
|
|
6
|
+
* published and never resolved. This is that caller.
|
|
7
|
+
*
|
|
8
|
+
* The classification is `020_history_isolation.md:564-572`, applied rather than
|
|
9
|
+
* reinvented. The two unions do not line up one-to-one, so each outcome is
|
|
10
|
+
* mapped by hand: a cast across them would compile on the words they share and
|
|
11
|
+
* lie about the rest.
|
|
12
|
+
*/
|
|
13
|
+
import type { CodexHistoryJobOutcome } from "./history-job";
|
|
14
|
+
import type { CodexHistoryState, CodexTransitionVersion } from "./convergence-types";
|
|
15
|
+
import { updateCodexHistoryTransition } from "./transition-state";
|
|
16
|
+
|
|
17
|
+
/** The retry budget for a contended terminal CAS. Bounded; never unbounded. */
|
|
18
|
+
const TERMINAL_RETRY_ATTEMPTS = 3;
|
|
19
|
+
const TERMINAL_RETRY_DELAY_MS = 250;
|
|
20
|
+
|
|
21
|
+
function sleep(ms: number): void {
|
|
22
|
+
// The caller path here is synchronous after the awaited job, so a spin is the
|
|
23
|
+
// honest tool — awaiting would be, and this is short.
|
|
24
|
+
const until = Date.now() + ms;
|
|
25
|
+
while (Date.now() < until) { /* bounded */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function classify(outcome: CodexHistoryJobOutcome, txId: string | null): CodexHistoryState {
|
|
29
|
+
switch (outcome.kind) {
|
|
30
|
+
case "converged":
|
|
31
|
+
return {
|
|
32
|
+
status: "converged",
|
|
33
|
+
attempts: 1,
|
|
34
|
+
nextRetryAt: null,
|
|
35
|
+
txId,
|
|
36
|
+
// Mutation counts, not probe counts. The durable counts come from the
|
|
37
|
+
// final probe, which this path does not run — null rather than a
|
|
38
|
+
// manufactured zero.
|
|
39
|
+
pendingRows: null,
|
|
40
|
+
backupEntries: null,
|
|
41
|
+
};
|
|
42
|
+
case "skipped":
|
|
43
|
+
// The user opting out is a completed decision, not a failure — converged
|
|
44
|
+
// with no reason. Marking it blocked would retry what they asked not to do.
|
|
45
|
+
return { status: "converged", attempts: 0, nextRetryAt: null, txId, pendingRows: null, backupEntries: null };
|
|
46
|
+
case "blocked":
|
|
47
|
+
return {
|
|
48
|
+
status: outcome.reason === "busy" ? "pending" : "blocked",
|
|
49
|
+
reason: outcome.reason === "busy" ? "db-busy"
|
|
50
|
+
: outcome.reason === "database" ? "unreadable"
|
|
51
|
+
: "permission",
|
|
52
|
+
attempts: 1,
|
|
53
|
+
// A busy unit retries; the others do not reschedule themselves.
|
|
54
|
+
nextRetryAt: outcome.reason === "busy"
|
|
55
|
+
? new Date(Date.now() + TERMINAL_RETRY_DELAY_MS * 10).toISOString()
|
|
56
|
+
: null,
|
|
57
|
+
txId,
|
|
58
|
+
pendingRows: null,
|
|
59
|
+
backupEntries: null,
|
|
60
|
+
};
|
|
61
|
+
case "failed":
|
|
62
|
+
return {
|
|
63
|
+
status: "unknown",
|
|
64
|
+
reason: outcome.reason === "timeout" ? "timeout"
|
|
65
|
+
: outcome.reason === "worker-died" ? "worker-died"
|
|
66
|
+
: "record-write-failed",
|
|
67
|
+
attempts: 1,
|
|
68
|
+
nextRetryAt: null,
|
|
69
|
+
txId,
|
|
70
|
+
pendingRows: null,
|
|
71
|
+
backupEntries: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Publish the terminal state of a history job against the transition it belongs
|
|
78
|
+
* to. A `conflict` means a newer transition won and is deliberately left alone.
|
|
79
|
+
* A `busy` terminal CAS retries a bounded number of times; when that exhausts,
|
|
80
|
+
* the previously persisted `pending` schedule is left intact — the same update
|
|
81
|
+
* needs the lock that was busy, so it cannot record its own failure
|
|
82
|
+
* (`005_contract.md:277`), and the retry being out of attempts is itself a
|
|
83
|
+
* terminal observation.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveCodexHistoryTransition(
|
|
86
|
+
receipt: CodexTransitionVersion,
|
|
87
|
+
outcome: CodexHistoryJobOutcome,
|
|
88
|
+
): void {
|
|
89
|
+
const state = classify(outcome, receipt.currentTxId);
|
|
90
|
+
for (let attempt = 0; attempt < TERMINAL_RETRY_ATTEMPTS; attempt += 1) {
|
|
91
|
+
const result = updateCodexHistoryTransition(receipt, state);
|
|
92
|
+
if (result.kind === "updated") return;
|
|
93
|
+
if (result.kind === "conflict") {
|
|
94
|
+
// A newer transition already owns the row. Overwriting it would be the
|
|
95
|
+
// overtaken job overwriting the winner.
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (result.reason === "busy") {
|
|
99
|
+
if (attempt + 1 < TERMINAL_RETRY_ATTEMPTS) sleep(TERMINAL_RETRY_DELAY_MS * (attempt + 1));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// database / unsafe-path: retried CAS will not help.
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread entry for the Codex history unit.
|
|
3
|
+
*
|
|
4
|
+
* Everything mutable about history happens here, behind H: the SQLite rows, the
|
|
5
|
+
* backup manifest, and every rollout patch. Those three do not share a
|
|
6
|
+
* transaction — sync writes the manifest before its database transaction, and
|
|
7
|
+
* restore writes rollouts, then the database, then the manifest
|
|
8
|
+
* (`src/codex/history-provider.ts:606-648,656-698`) — so a busy timeout only
|
|
9
|
+
* ever serialized a third of a state transition. Holding H across the whole unit
|
|
10
|
+
* is what stops an opposite-direction process overtaking through the other two.
|
|
11
|
+
*
|
|
12
|
+
* The message is deliberately thin, and it does NOT carry a direction. A caller
|
|
13
|
+
* saying which way history should move is exactly what the durable operation
|
|
14
|
+
* exists to prevent: `syncResumeHistory: false` means leave history alone, apply
|
|
15
|
+
* targets opencodex only in legacy mode, and legacy recovery is a different
|
|
16
|
+
* operation from generic restore. So the Worker is told which JOB to run and
|
|
17
|
+
* reads the operation from the coordinator row itself.
|
|
18
|
+
*
|
|
19
|
+
* Homes are carried explicitly because a Worker is a separate process: it does
|
|
20
|
+
* not inherit the module-load `CODEX_HOME` that `history-provider.ts` resolves
|
|
21
|
+
* at import time, so a request that leaned on those constants would silently
|
|
22
|
+
* address the wrong home.
|
|
23
|
+
*
|
|
24
|
+
* Design record: devlog/_plan/260804_codex_write_substrate/020_history_isolation.md.
|
|
25
|
+
*/
|
|
26
|
+
import { withHistoryWriteSerialization } from "./history-lock";
|
|
27
|
+
import {
|
|
28
|
+
writeHistoryProviderTransition,
|
|
29
|
+
writeLegacyOpenaiHistoryRecovery,
|
|
30
|
+
type HistoryWriteTarget,
|
|
31
|
+
} from "./internal/history-writer";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The durable operation, mirrored into the request for diagnostics only.
|
|
35
|
+
*
|
|
36
|
+
* The Worker validates this against what it reads and refuses a mismatch rather
|
|
37
|
+
* than trusting it — a tampered copy must not be able to turn a restore into an
|
|
38
|
+
* apply. Structured-clone safe by construction: a closed set of string literals.
|
|
39
|
+
*/
|
|
40
|
+
export type CodexHistoryWorkerOperation =
|
|
41
|
+
| "skip"
|
|
42
|
+
| "apply-opencodex"
|
|
43
|
+
| "migrate-openai"
|
|
44
|
+
| "restore-openai"
|
|
45
|
+
| "recover-legacy-openai";
|
|
46
|
+
|
|
47
|
+
export interface HistoryWorkerRunMessage {
|
|
48
|
+
readonly type: "run";
|
|
49
|
+
readonly requestId: string;
|
|
50
|
+
/** Opaque durable job identity; the Worker refuses work that is not current. */
|
|
51
|
+
readonly jobId: string;
|
|
52
|
+
readonly operation: CodexHistoryWorkerOperation;
|
|
53
|
+
readonly canonicalCodexHome: string;
|
|
54
|
+
readonly canonicalStateDbPath: string;
|
|
55
|
+
readonly canonicalBackupPath: string;
|
|
56
|
+
/** Env snapshot: a Worker may not observe parent mutations on every platform. */
|
|
57
|
+
readonly env?: { readonly CODEX_HOME?: string; readonly OPENCODEX_HOME?: string };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type HistoryWorkerResult =
|
|
61
|
+
| { readonly type: "done"; readonly requestId: string; readonly jobId: string;
|
|
62
|
+
readonly outcome: "converged" | "skipped";
|
|
63
|
+
readonly rows: number; readonly files: number }
|
|
64
|
+
| { readonly type: "blocked"; readonly requestId: string; readonly jobId: string;
|
|
65
|
+
readonly reason: "busy" | "database" | "unsafe-path" }
|
|
66
|
+
| { readonly type: "error"; readonly requestId: string; readonly jobId: string;
|
|
67
|
+
readonly message: string };
|
|
68
|
+
|
|
69
|
+
const OPERATIONS: ReadonlySet<string> = new Set<CodexHistoryWorkerOperation>([
|
|
70
|
+
"skip",
|
|
71
|
+
"apply-opencodex",
|
|
72
|
+
"migrate-openai",
|
|
73
|
+
"restore-openai",
|
|
74
|
+
"recover-legacy-openai",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate the message before it can reach a writer.
|
|
79
|
+
*
|
|
80
|
+
* A malformed message is dropped rather than coerced. Every field is required
|
|
81
|
+
* and non-empty: an absent path would otherwise fall back to a module-load
|
|
82
|
+
* constant that points at the wrong home in this process.
|
|
83
|
+
*/
|
|
84
|
+
export function isHistoryWorkerRunMessage(data: unknown): data is HistoryWorkerRunMessage {
|
|
85
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
86
|
+
const message = data as Record<string, unknown>;
|
|
87
|
+
const nonEmpty = (value: unknown): value is string =>
|
|
88
|
+
typeof value === "string" && value.trim().length > 0;
|
|
89
|
+
return message.type === "run"
|
|
90
|
+
&& nonEmpty(message.requestId)
|
|
91
|
+
&& nonEmpty(message.jobId)
|
|
92
|
+
&& typeof message.operation === "string"
|
|
93
|
+
&& OPERATIONS.has(message.operation)
|
|
94
|
+
&& nonEmpty(message.canonicalCodexHome)
|
|
95
|
+
&& nonEmpty(message.canonicalStateDbPath)
|
|
96
|
+
&& nonEmpty(message.canonicalBackupPath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run one history operation under H.
|
|
101
|
+
*
|
|
102
|
+
* Exported so the unit can be exercised in-process; the Worker entry below is a
|
|
103
|
+
* thin adapter over it. `skip` is a real outcome rather than an absence: opting
|
|
104
|
+
* out of history resume must be recorded, not inferred from nothing happening.
|
|
105
|
+
*/
|
|
106
|
+
export function runHistoryUnitUnderLock(
|
|
107
|
+
message: HistoryWorkerRunMessage,
|
|
108
|
+
): HistoryWorkerResult {
|
|
109
|
+
const { requestId, jobId, operation } = message;
|
|
110
|
+
const target: HistoryWriteTarget = {
|
|
111
|
+
canonicalStateDbPath: message.canonicalStateDbPath,
|
|
112
|
+
canonicalBackupPath: message.canonicalBackupPath,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (operation === "skip") {
|
|
116
|
+
return { type: "done", requestId, jobId, outcome: "skipped", rows: 0, files: 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const acquired = withHistoryWriteSerialization(
|
|
120
|
+
message.canonicalCodexHome,
|
|
121
|
+
message.canonicalStateDbPath,
|
|
122
|
+
permit => {
|
|
123
|
+
if (operation === "recover-legacy-openai") {
|
|
124
|
+
return writeLegacyOpenaiHistoryRecovery(permit, target);
|
|
125
|
+
}
|
|
126
|
+
// apply-opencodex routes history to opencodex; migrate/restore return it to
|
|
127
|
+
// native. The provider is derived from the operation, never from a caller.
|
|
128
|
+
const provider = operation === "apply-opencodex" ? "opencodex" : "openai";
|
|
129
|
+
return writeHistoryProviderTransition(permit, target, provider);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (acquired.kind !== "completed") {
|
|
134
|
+
return { type: "blocked", requestId, jobId, reason: acquired.reason };
|
|
135
|
+
}
|
|
136
|
+
const result = acquired.value;
|
|
137
|
+
if (result.failed === true) {
|
|
138
|
+
return { type: "error", requestId, jobId, message: "history_transition_failed" };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
type: "done",
|
|
142
|
+
requestId,
|
|
143
|
+
jobId,
|
|
144
|
+
outcome: "converged",
|
|
145
|
+
rows: result.rows,
|
|
146
|
+
files: result.files,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
declare const self: Worker;
|
|
151
|
+
|
|
152
|
+
// Guarded so the module can be imported directly by tests without a Worker host.
|
|
153
|
+
if (typeof self !== "undefined" && typeof (self as { onmessage?: unknown }) === "object") {
|
|
154
|
+
self.onmessage = (event: MessageEvent<unknown>) => {
|
|
155
|
+
if (!isHistoryWorkerRunMessage(event.data)) return;
|
|
156
|
+
const message = event.data;
|
|
157
|
+
try {
|
|
158
|
+
if (message.env?.CODEX_HOME) process.env.CODEX_HOME = message.env.CODEX_HOME;
|
|
159
|
+
if (message.env?.OPENCODEX_HOME) process.env.OPENCODEX_HOME = message.env.OPENCODEX_HOME;
|
|
160
|
+
self.postMessage(runHistoryUnitUnderLock(message));
|
|
161
|
+
} catch (error) {
|
|
162
|
+
self.postMessage({
|
|
163
|
+
type: "error",
|
|
164
|
+
requestId: message.requestId,
|
|
165
|
+
jobId: message.jobId,
|
|
166
|
+
message: error instanceof Error ? error.message : "history_worker_failed",
|
|
167
|
+
} satisfies HistoryWorkerResult);
|
|
168
|
+
} finally {
|
|
169
|
+
// Close from inside the Worker so the thread begins exiting before the
|
|
170
|
+
// parent's terminate() races isolate realm reclaim on Windows.
|
|
171
|
+
try {
|
|
172
|
+
(self as unknown as { close?: () => void }).close?.();
|
|
173
|
+
} catch { /* already closing */ }
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|