@bitkyc08/opencodex 2.25.0 → 2.26.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.
Files changed (99) hide show
  1. package/bin/ocx.mjs +59 -7
  2. package/gui/dist/assets/index-RL6b1bTV.js +102 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/base.ts +18 -0
  6. package/src/adapters/client-fingerprint.ts +0 -2
  7. package/src/adapters/cursor/http1-bidi.ts +361 -0
  8. package/src/adapters/cursor/live-models.ts +117 -30
  9. package/src/adapters/cursor/live-transport.ts +137 -56
  10. package/src/adapters/cursor/native-exec-fs.ts +1 -1
  11. package/src/adapters/cursor/native-exec-network.ts +1 -1
  12. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  13. package/src/adapters/cursor/protobuf-request.ts +89 -21
  14. package/src/adapters/cursor/tool-definitions.ts +18 -7
  15. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  16. package/src/adapters/cursor/transport.ts +7 -0
  17. package/src/adapters/cursor.ts +2 -0
  18. package/src/adapters/google-http.ts +38 -10
  19. package/src/adapters/google.ts +23 -3
  20. package/src/adapters/openai-chat.ts +38 -13
  21. package/src/adapters/openai-responses.ts +129 -9
  22. package/src/adapters/registry.ts +30 -1
  23. package/src/bridge.ts +50 -12
  24. package/src/chat/inbound.ts +1 -0
  25. package/src/claude/agents-inject.ts +3 -2
  26. package/src/cli/dispatch.ts +19 -6
  27. package/src/cli/help.ts +2 -1
  28. package/src/cli/integrations.ts +35 -0
  29. package/src/cli/minimax.ts +8 -2
  30. package/src/cli/registry.ts +13 -2
  31. package/src/clients/config-export.ts +120 -1
  32. package/src/codex/account-store.ts +17 -1
  33. package/src/codex/auth-api.ts +51 -17
  34. package/src/codex/catalog/effort.ts +8 -5
  35. package/src/codex/catalog/provider-fetch.ts +39 -27
  36. package/src/codex/catalog/sync.ts +30 -8
  37. package/src/codex/inject.ts +39 -13
  38. package/src/codex/main-account.ts +29 -1
  39. package/src/codex/plan-from-token.ts +140 -0
  40. package/src/codex/plan.ts +25 -0
  41. package/src/codex/prompt-journal.ts +6 -2
  42. package/src/codex/quota-rejection.ts +21 -7
  43. package/src/codex/refresh.ts +4 -2
  44. package/src/codex/sync.ts +106 -3
  45. package/src/codex/warmup.ts +187 -81
  46. package/src/config.ts +85 -46
  47. package/src/generated/compatibility-version.json +127 -83
  48. package/src/grok/inject.ts +1 -1
  49. package/src/images/loop.ts +1 -0
  50. package/src/integrations/registry.ts +7 -0
  51. package/src/lab/automation/config-persistence.ts +2 -2
  52. package/src/lab/automation/persistence.ts +2 -2
  53. package/src/lab/ledger/purge.ts +2 -2
  54. package/src/lab/subject/behavior-fingerprint.ts +2 -2
  55. package/src/lib/config-ownership.ts +5 -2
  56. package/src/lib/destination-policy.ts +18 -1
  57. package/src/lib/provider-outbound.ts +7 -0
  58. package/src/lib/redact.ts +11 -0
  59. package/src/lib/tool-argument-integers.ts +50 -6
  60. package/src/lib/upstream-http-version.ts +57 -0
  61. package/src/lib/upstream-retry.ts +2 -1
  62. package/src/lib/windows-atomic-replace.ts +155 -0
  63. package/src/lib/windows-service-wrappers.ts +72 -0
  64. package/src/oauth/google-antigravity.ts +2 -2
  65. package/src/oauth/index.ts +60 -8
  66. package/src/providers/antigravity-models.ts +58 -4
  67. package/src/providers/command-code-efforts.ts +36 -9
  68. package/src/providers/context-cap.ts +9 -0
  69. package/src/providers/derive.ts +4 -0
  70. package/src/providers/fastwire.ts +453 -0
  71. package/src/providers/registry.ts +21 -1
  72. package/src/providers/service-tier.ts +173 -89
  73. package/src/responses/parser.ts +25 -16
  74. package/src/responses/reasoning-replay-cache.ts +30 -0
  75. package/src/responses/thought-signature-replay.ts +74 -4
  76. package/src/router.ts +6 -0
  77. package/src/routing/compatibility/behavior.ts +27 -16
  78. package/src/server/index.ts +10 -1
  79. package/src/server/management/oauth-account-routes.ts +10 -2
  80. package/src/server/management/shared.ts +1 -1
  81. package/src/server/management/system-routes.ts +15 -0
  82. package/src/server/request-log.ts +57 -3
  83. package/src/server/responses/agent-task-recovery.ts +6 -1
  84. package/src/server/responses/core.ts +209 -40
  85. package/src/server/responses/fetch-helpers.ts +15 -36
  86. package/src/server/responses/responses-field-backfill.ts +173 -0
  87. package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
  88. package/src/service.ts +38 -36
  89. package/src/storage/cleanup.ts +2 -2
  90. package/src/tray/windows.ts +3 -2
  91. package/src/types.ts +101 -14
  92. package/src/update/job.ts +9 -18
  93. package/src/update/transactional-install.d.mts +22 -0
  94. package/src/update/transactional-install.mjs +259 -0
  95. package/src/usage/cost.ts +34 -5
  96. package/src/usage/log.ts +74 -4
  97. package/src/vision/anthropic-describe.ts +10 -6
  98. package/src/web-search/anthropic-executor.ts +8 -6
  99. package/gui/dist/assets/index-DxJ7kXj9.js +0 -102
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Stateless SSE block rewrite that backfills the `annotations` field on
3
+ * `output_text` content parts in Responses SSE events.
4
+ *
5
+ * The Responses API spec defines `annotations` as a required field on
6
+ * `OutputTextContent` (it is `Vec<Annotation>`, not `Option<Vec>`). Some
7
+ * upstream relays omit it when there are no annotations, which is technically
8
+ * spec-non-compliant. Strict deserializers — any client that follows the
9
+ * schema without `#[serde(default)]` on that field — fail with
10
+ * `missing field `annotations`` when the field is absent.
11
+ *
12
+ * This rewrite scans every SSE event for output_text content parts — whether
13
+ * they appear in item.content[], part, or response.output[].content[] — and
14
+ * adds annotations: [] if missing.
15
+ *
16
+ * Stateless: no retained buffers, no lifecycle tracking, no fail-closed.
17
+ * Existing values are always authoritative; only absent fields are added.
18
+ * The field is always valid on the wire, so adding it when absent is safe
19
+ * for all clients including Codex CLI/App.
20
+ */
21
+
22
+ import {
23
+ replaceSseDataPayload,
24
+ sseDataPayload,
25
+ type SseBlockRewrite,
26
+ } from "../sse-payload-rewrite";
27
+
28
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
29
+ return !!value && typeof value === "object" && !Array.isArray(value);
30
+ }
31
+
32
+ /**
33
+ * Backfill annotations: [] on an output_text content part if missing.
34
+ * Returns the same object reference if no change is needed.
35
+ */
36
+ function backfillOutputTextPart(part: Record<string, unknown>): Record<string, unknown> {
37
+ if (part.type !== "output_text") return part;
38
+ // Only add annotations when the key is entirely absent — preserve any
39
+ // existing value (even null or a malformed type) so we never overwrite
40
+ // what the upstream actually sent.
41
+ if ("annotations" in part) return part;
42
+ return { ...part, annotations: [] };
43
+ }
44
+
45
+ /**
46
+ * Walk a content array and backfill output_text parts.
47
+ * Returns the same array reference if nothing changed.
48
+ */
49
+ function backfillContentArray(content: unknown): unknown {
50
+ if (!Array.isArray(content)) return content;
51
+ let changed = false;
52
+ const repaired = content.map((part) => {
53
+ if (!isPlainObject(part)) return part;
54
+ const next = backfillOutputTextPart(part);
55
+ if (next !== part) changed = true;
56
+ return next;
57
+ });
58
+ return changed ? repaired : content;
59
+ }
60
+
61
+ /**
62
+ * Walk an output item and backfill output_text parts in its content.
63
+ * Returns the same object reference if nothing changed.
64
+ */
65
+ function backfillOutputItem(item: unknown): unknown {
66
+ if (!isPlainObject(item)) return item;
67
+ const content = item.content;
68
+ const repaired = backfillContentArray(content);
69
+ if (repaired === content) return item;
70
+ return { ...item, content: repaired };
71
+ }
72
+
73
+ /**
74
+ * Walk a response object's output[] and backfill output_text parts.
75
+ * Returns the same object reference if nothing changed.
76
+ */
77
+ function backfillResponseOutput(response: unknown): unknown {
78
+ if (!isPlainObject(response)) return response;
79
+ const output = response.output;
80
+ if (!Array.isArray(output)) return response;
81
+ let changed = false;
82
+ const repaired = output.map((item) => {
83
+ if (!isPlainObject(item)) return item;
84
+ const next = backfillOutputItem(item);
85
+ if (next !== item) changed = true;
86
+ return next;
87
+ });
88
+ return changed ? { ...response, output: repaired } : response;
89
+ }
90
+
91
+ /**
92
+ * Statelessly rewrite one SSE event: backfill annotations
93
+ * on any output_text content part found in the event payload.
94
+ */
95
+ function rewriteEvent(event: Record<string, unknown>): Record<string, unknown> {
96
+ const type = typeof event.type === "string" ? event.type : "";
97
+ let next = event;
98
+ let changed = false;
99
+
100
+ // output_item.added / output_item.done: item.content[] -> output_text parts
101
+ if ((type === "response.output_item.added" || type === "response.output_item.done")
102
+ && isPlainObject(event.item)) {
103
+ const item = backfillOutputItem(event.item);
104
+ if (item !== event.item) {
105
+ next = { ...next, item };
106
+ changed = true;
107
+ }
108
+ }
109
+
110
+ // content_part.added / content_part.done: part -> output_text
111
+ if ((type === "response.content_part.added" || type === "response.content_part.done")
112
+ && isPlainObject(event.part)) {
113
+ const part = backfillOutputTextPart(event.part);
114
+ if (part !== event.part) {
115
+ next = { ...next, part };
116
+ changed = true;
117
+ }
118
+ }
119
+
120
+ // response.created / in_progress / completed / incomplete / failed:
121
+ // response.output[].content[] -> output_text parts
122
+ if (isPlainObject(event.response)) {
123
+ const response = backfillResponseOutput(event.response);
124
+ if (response !== event.response) {
125
+ next = { ...next, response };
126
+ changed = true;
127
+ }
128
+ }
129
+
130
+ return changed ? next : event;
131
+ }
132
+
133
+ /**
134
+ * Create a stateless SSE block rewrite that backfills annotations and
135
+ * on output_text content parts. Unconditional: the field is a required
136
+ * canonical Responses field, so adding it when absent is safe for all
137
+ * clients.
138
+ */
139
+ export function createResponsesFieldBackfillBlockRewrite(): SseBlockRewrite {
140
+ const rewrite: SseBlockRewrite = (block: string): readonly string[] => {
141
+ const payload = sseDataPayload(block);
142
+ if (payload === null) return [block];
143
+ let event: unknown;
144
+ try {
145
+ event = JSON.parse(payload);
146
+ } catch {
147
+ return [block];
148
+ }
149
+ if (!isPlainObject(event)) return [block];
150
+ const rewritten = rewriteEvent(event);
151
+ if (rewritten === event) return [block];
152
+ return [replaceSseDataPayload(block, JSON.stringify(rewritten))];
153
+ };
154
+ return rewrite;
155
+ }
156
+
157
+ /**
158
+ * Backfill annotations on a non-streaming Responses JSON
159
+ * object. Mirrors the SSE block rewrite for the bounded-JSON passthrough
160
+ * path. Returns the original string if no change is needed.
161
+ */
162
+ export function backfillResponsesFieldsJson(payload: string): string {
163
+ let response: unknown;
164
+ try {
165
+ response = JSON.parse(payload);
166
+ } catch {
167
+ return payload;
168
+ }
169
+ if (!isPlainObject(response)) return payload;
170
+ const repaired = backfillResponseOutput(response);
171
+ if (repaired === response) return payload;
172
+ return JSON.stringify(repaired);
173
+ }
@@ -0,0 +1,171 @@
1
+ import type { SsePayloadRewrite } from "./sse-payload-rewrite";
2
+
3
+ /**
4
+ * Route content-channel reasoning from native-Responses upstreams through the
5
+ * expandable summary channel (issue #45).
6
+ *
7
+ * Codex renders the expandable reasoning trace from the Responses reasoning
8
+ * item's `summary[]` channel. DeepSeek's native `/responses` endpoint emits
9
+ * raw thinking on the content channel instead (`response.reasoning_text.delta`
10
+ * plus items with `content: [{type: "reasoning_text", text}]` and an empty
11
+ * `summary`), so routed DeepSeek turns showed the "Worked for Xs" timer with
12
+ * nothing to expand. Native OpenAI upstreams already emit summary-channel
13
+ * events; this rewrite is a no-op for them (no reasoning_text events to
14
+ * rewrite) and only engages when the upstream produces content-channel
15
+ * reasoning.
16
+ *
17
+ * Replay compatibility: Codex echoes the reasoning item it received back into
18
+ * the next request's input. DeepSeek's Responses API accepts summary-shaped
19
+ * reasoning input items (verified live), so the rewrite round-trips.
20
+ */
21
+
22
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
23
+ return !!value && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+
26
+ function reasoningTextOf(item: Record<string, unknown>): string {
27
+ if (!Array.isArray(item.content)) return "";
28
+ return item.content
29
+ .filter((part): part is Record<string, unknown> => isPlainObject(part) && part.type === "reasoning_text")
30
+ .map(part => (typeof part.text === "string" ? part.text : ""))
31
+ .join("");
32
+ }
33
+
34
+ /** Move a reasoning item's content channel into the summary channel. */
35
+ function reasoningItemToSummaryShape(item: Record<string, unknown>): Record<string, unknown> {
36
+ if (item.type !== "reasoning") return item;
37
+ const text = reasoningTextOf(item);
38
+ // Items that already use the summary channel (or carry no content text at
39
+ // all) are left untouched: rewriting them could clear a valid summary.
40
+ if (text.length === 0) return item;
41
+ const next: Record<string, unknown> = { ...item };
42
+ delete next.content;
43
+ next.summary = [{ type: "summary_text", text }];
44
+ return next;
45
+ }
46
+
47
+ /**
48
+ * Rewrite one parsed SSE payload in place of the content channel, or return
49
+ * `null` when nothing changed (caller keeps the original payload).
50
+ */
51
+ function rewritePayload(payload: Record<string, unknown>): Record<string, unknown> | null {
52
+ switch (payload.type) {
53
+ case "response.reasoning_text.delta": {
54
+ const next: Record<string, unknown> = {
55
+ type: "response.reasoning_summary_text.delta",
56
+ item_id: payload.item_id,
57
+ output_index: payload.output_index,
58
+ summary_index: 0,
59
+ delta: payload.delta,
60
+ };
61
+ if (payload.sequence_number !== undefined) next.sequence_number = payload.sequence_number;
62
+ return next;
63
+ }
64
+ case "response.reasoning_text.done": {
65
+ const next: Record<string, unknown> = {
66
+ type: "response.reasoning_summary_text.done",
67
+ item_id: payload.item_id,
68
+ output_index: payload.output_index,
69
+ summary_index: 0,
70
+ text: payload.text,
71
+ };
72
+ if (payload.sequence_number !== undefined) next.sequence_number = payload.sequence_number;
73
+ return next;
74
+ }
75
+ default: {
76
+ let changed = false;
77
+ const next: Record<string, unknown> = { ...payload };
78
+ if (isPlainObject(next.item) && next.item.type === "reasoning") {
79
+ const rewritten = reasoningItemToSummaryShape(next.item);
80
+ if (rewritten !== next.item) {
81
+ next.item = rewritten;
82
+ changed = true;
83
+ }
84
+ }
85
+ // SSE event shape: {type: "response.completed", response: {output}}.
86
+ const response = isPlainObject(next.response) ? { ...next.response } : null;
87
+ if (response && Array.isArray(response.output)) {
88
+ const output = response.output.map(item => {
89
+ if (!isPlainObject(item) || item.type !== "reasoning") return item;
90
+ const rewritten = reasoningItemToSummaryShape(item);
91
+ if (rewritten !== item) changed = true;
92
+ return rewritten;
93
+ });
94
+ if (changed) {
95
+ response.output = output;
96
+ next.response = response;
97
+ }
98
+ }
99
+ // Bare response document shape (non-streaming passthrough):
100
+ // {object: "response", output: [...]}.
101
+ if (Array.isArray(next.output)) {
102
+ const output = next.output.map(item => {
103
+ if (!isPlainObject(item) || item.type !== "reasoning") return item;
104
+ const rewritten = reasoningItemToSummaryShape(item);
105
+ if (rewritten !== item) changed = true;
106
+ return rewritten;
107
+ });
108
+ if (changed) next.output = output;
109
+ }
110
+ return changed ? next : null;
111
+ }
112
+ }
113
+ }
114
+
115
+ /** Payload rewrite for passthrough relays whose upstream emits content-channel reasoning. */
116
+ export function createReasoningSummaryChannelPayloadRewrite(): SsePayloadRewrite {
117
+ return (payload: string): string => {
118
+ let parsed: unknown;
119
+ try {
120
+ parsed = JSON.parse(payload);
121
+ } catch {
122
+ return payload;
123
+ }
124
+ if (!isPlainObject(parsed)) return payload;
125
+ const rewritten = rewritePayload(parsed);
126
+ return rewritten !== null ? JSON.stringify(rewritten) : payload;
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Object-level variant for the non-streaming passthrough: the bounded-JSON
132
+ * relay bypasses the SSE payload rewrite, so reasoning items inside a full
133
+ * Responses JSON document need the same normalization before plain JSON
134
+ * serialization or forced JSON-to-SSE reframing. Returns the same reference
135
+ * when nothing changed.
136
+ */
137
+ export function rewriteReasoningSummaryInJson(value: unknown): unknown {
138
+ if (!isPlainObject(value)) return value;
139
+ const rewritten = rewritePayload(value);
140
+ return rewritten !== null ? rewritten : value;
141
+ }
142
+
143
+ /** String-level variant of {@link rewriteReasoningSummaryInJson}. */
144
+ export function rewriteReasoningSummaryInJsonString(json: string): string {
145
+ let parsed: unknown;
146
+ try {
147
+ parsed = JSON.parse(json);
148
+ } catch {
149
+ return json;
150
+ }
151
+ const rewritten = rewriteReasoningSummaryInJson(parsed);
152
+ return rewritten === parsed ? json : JSON.stringify(rewritten);
153
+ }
154
+
155
+ /**
156
+ * True when a routed native-Responses provider emits content-channel reasoning
157
+ * (raw `reasoning_text`) instead of the summary channel. DeepSeek's
158
+ * `/responses` endpoint is the current example: it ships raw thinking with an
159
+ * empty `summary` and keeps `preserveReasoningContentModels` so multi-turn
160
+ * replays round-trip.
161
+ */
162
+ export function routeUsesContentChannelReasoning(
163
+ provider: { statelessResponses?: boolean; preserveReasoningContentModels?: string[] },
164
+ modelId: string,
165
+ ): boolean {
166
+ if (provider.statelessResponses === true) return true;
167
+ const preserved = provider.preserveReasoningContentModels;
168
+ const normalizedModelId = modelId.toLowerCase();
169
+ return Array.isArray(preserved)
170
+ && preserved.some(id => id.toLowerCase() === normalizedModelId);
171
+ }
package/src/service.ts CHANGED
@@ -45,6 +45,7 @@ import {
45
45
  } from "./lib/windows-secret-acl";
46
46
  import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
47
47
  import { recordOwnedConfigPath } from "./lib/config-ownership";
48
+ import { killWindowsSchedulerWrappers } from "./lib/windows-service-wrappers";
48
49
  import { maybeShowStarPrompt } from "./cli/star-prompt";
49
50
 
50
51
  const LABEL = "com.opencodex.proxy";
@@ -1534,6 +1535,9 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
1534
1535
  windowsBatchSet("OCX_SERVICE_LOG", serviceLogPath(), "path"),
1535
1536
  windowsBatchSet("OCX_BUN", bun, "path"),
1536
1537
  windowsBatchSet("OCX_CLI", cli, "path"),
1538
+ // Package root for the transactional-update restore path (#1942): cli is
1539
+ // <pkg>\src\cli\index.ts, so the package dir is three levels up.
1540
+ 'for %%I in ("%OCX_CLI%\\..\\..\\..") do set "OCX_PKG_DIR=%%~fI"',
1537
1541
  'if exist "%OCX_API_TOKEN_FILE%" (',
1538
1542
  ' set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"',
1539
1543
  ")",
@@ -1546,10 +1550,16 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
1546
1550
  '>>"%OCX_SERVICE_LOG%" echo codex_home="%CODEX_HOME%"',
1547
1551
  '>>"%OCX_SERVICE_LOG%" echo token_file="%OCX_API_TOKEN_FILE%"',
1548
1552
  'if not exist "%OCX_BUN%" (',
1553
+ " call :restore_backup",
1554
+ ")",
1555
+ 'if not exist "%OCX_BUN%" (',
1549
1556
  ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: bundled Bun is missing; reinstall opencodex, then run ocx service repair',
1550
1557
  " exit /b 3",
1551
1558
  ")",
1552
1559
  'if not exist "%OCX_CLI%" (',
1560
+ " call :restore_backup",
1561
+ ")",
1562
+ 'if not exist "%OCX_CLI%" (',
1553
1563
  ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: CLI entry is missing; reinstall opencodex, then run ocx service repair',
1554
1564
  " exit /b 3",
1555
1565
  ")",
@@ -1562,6 +1572,26 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
1562
1572
  " goto loop",
1563
1573
  ")",
1564
1574
  "endlocal",
1575
+ "goto :eof",
1576
+ "",
1577
+ // #1942/#1849: a power loss mid-swap leaves the live package dir missing/broken and
1578
+ // a sibling .ocx-backup-* holding the previous version. This wrapper lives OUTSIDE
1579
+ // the package tree, so it can restore when the launcher itself is gone — the exact
1580
+ // window the in-launcher boot probe cannot reach.
1581
+ ":restore_backup",
1582
+ '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] install incomplete - looking for a transactional-update backup to restore',
1583
+ 'for /f "delims=" %%B in (\'dir /b /ad /o-n "%OCX_PKG_DIR%\\..\\.ocx-backup-*" 2^>nul\') do (',
1584
+ ' if exist "%OCX_PKG_DIR%\\..\\%%B\\opencodex\\package.json" (',
1585
+ ' if exist "%OCX_PKG_DIR%" rmdir /s /q "%OCX_PKG_DIR%" 2>nul',
1586
+ ' move "%OCX_PKG_DIR%\\..\\%%B\\opencodex" "%OCX_PKG_DIR%" >nul 2>&1',
1587
+ ' if exist "%OCX_PKG_DIR%\\package.json" (',
1588
+ ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from %%B',
1589
+ " goto :eof",
1590
+ " )",
1591
+ " )",
1592
+ ")",
1593
+ '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] no restorable backup found',
1594
+ "goto :eof",
1565
1595
  ].filter((line): line is string => Boolean(line));
1566
1596
  return `${lines.join("\r\n")}\r\n`;
1567
1597
  }
@@ -2323,45 +2353,17 @@ function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TA
2323
2353
  /**
2324
2354
  * Best-effort termination of surviving Windows scheduler launcher/wrapper processes.
2325
2355
  * `schtasks /end` ends the task instance but often leaves wscript/cmd running the
2326
- * `:loop` batch, which brings the proxy back during a stop or restart. Same killer
2327
- * the update job uses, so both teardown paths share the guarantee.
2356
+ * `:loop` batch, which brings the proxy back during a stop or restart.
2328
2357
  *
2329
- * Matching is scoped to the CANONICAL paths of THIS installation (opencodex-service.cmd
2330
- * and opencodex-service-launcher.vbs under the current config dir), never a bare
2331
- * filename: a wrapper from another OpenCodex home — or an unrelated process whose
2332
- * command line merely contains the filename — must not be force-terminated.
2333
- * The path must appear as a COMPLETE command-line token (wscript.exe spawns the
2334
- * .vbs as an argument; cmd.exe /c runs the .cmd), so a substring-only match is
2335
- * excluded.
2358
+ * The matching rule canonical paths of THIS installation, as complete
2359
+ * command-line tokens — lives in lib/windows-service-wrappers so the update job
2360
+ * cannot drift away from it again.
2336
2361
  */
2337
2362
  function killWindowsServiceWrapperProcesses(): void {
2338
- if (process.platform !== "win32") return;
2339
- try {
2340
- const script = windowsServiceScriptPath();
2341
- const launcher = windowsLauncherVbsPath();
2342
- // Quote for PowerShell: single-quote the value and double any embedded quote.
2343
- const quote = (value: string) => `'${value.replace(/'/g, "''")}'`;
2344
- const ps = [
2345
- `$pats = @(${quote(script)}, ${quote(launcher)});`,
2346
- "Get-CimInstance Win32_Process | Where-Object {",
2347
- " if ($_.ProcessId -eq $PID) { return $false };",
2348
- " $c = $_.CommandLine; if (-not $c) { return $false };",
2349
- " foreach ($p in $pats) {",
2350
- " $i = $c.IndexOf($p, [System.StringComparison]::OrdinalIgnoreCase);",
2351
- " if ($i -lt 0) { continue };",
2352
- " $before = if ($i -gt 0) { $c.Substring($i - 1, 1) } else { ' ' };",
2353
- " $end = $i + $p.Length;",
2354
- " $after = if ($end -lt $c.Length) { $c.Substring($end, 1) } else { ' ' };",
2355
- " if ($before -match '[\\s\"'']' -and $after -match '[\\s\"'']') { return $true };",
2356
- " };",
2357
- " $false",
2358
- "} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
2359
- ].join(" ");
2360
- spawnSync(resolveTrustedWindowsPowerShellExe(), [
2361
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
2362
- "-Command", ps,
2363
- ], { stdio: "ignore", timeout: 5000, windowsHide: true });
2364
- } catch { /* best-effort */ }
2363
+ killWindowsSchedulerWrappers({
2364
+ scriptPath: windowsServiceScriptPath(),
2365
+ launcherPath: windowsLauncherVbsPath(),
2366
+ });
2365
2367
  }
2366
2368
  function uninstallWindows(): void {
2367
2369
  const probe = probeWindowsSchedulerTask(TASK);
@@ -1091,7 +1091,7 @@ function writeSatelliteBackup(
1091
1091
  throw new Error("test_fail_satellite_backup_replace");
1092
1092
  }
1093
1093
  try {
1094
- renameAtomicFile(tmp, dest);
1094
+ renameAtomicFile(tmp, dest, undefined, "storage-cleanup");
1095
1095
  } catch (error) {
1096
1096
  try { unlinkSync(tmp); } catch { /* */ }
1097
1097
  throw error;
@@ -2435,7 +2435,7 @@ function writeRestorePending(
2435
2435
  throw new Error("test_fail_pending_rename");
2436
2436
  }
2437
2437
  try {
2438
- renameSync(tmp, dest);
2438
+ renameAtomicFile(tmp, dest, undefined, "storage-cleanup");
2439
2439
  } catch (error) {
2440
2440
  try { unlinkSync(tmp); } catch { /* */ }
2441
2441
  throw error;
@@ -1,6 +1,6 @@
1
1
  import { execFile, execFileSync, spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { expandUserPath, getConfigDir } from "../config";
@@ -8,6 +8,7 @@ import { durableBunRuntime } from "../lib/bun-runtime";
8
8
  import type { BunRuntimeSource } from "../lib/bun-runtime";
9
9
  import { forgetEphemeralSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
10
10
  import { recordOwnedConfigPath } from "../lib/config-ownership";
11
+ import { renameAtomicFile } from "../lib/windows-atomic-replace";
11
12
 
12
13
  const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
13
14
  const RUN_PARENT_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion";
@@ -242,7 +243,7 @@ export function replaceWindowsTrayOwnedFile(
242
243
  const hardened = hardenSecretPath(target, { required: true, timeoutMemoKey: path });
243
244
  if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state.");
244
245
  },
245
- rename: renameSync,
246
+ rename: (source, destination) => renameAtomicFile(source, destination, undefined, "tray"),
246
247
  unlink: unlinkSync,
247
248
  },
248
249
  ): void {