@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.
- package/bin/ocx.mjs +59 -7
- package/gui/dist/assets/index-RL6b1bTV.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +18 -0
- package/src/adapters/client-fingerprint.ts +0 -2
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/live-models.ts +117 -30
- package/src/adapters/cursor/live-transport.ts +137 -56
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/protobuf-request.ts +89 -21
- package/src/adapters/cursor/tool-definitions.ts +18 -7
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +7 -0
- package/src/adapters/cursor.ts +2 -0
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google.ts +23 -3
- package/src/adapters/openai-chat.ts +38 -13
- package/src/adapters/openai-responses.ts +129 -9
- package/src/adapters/registry.ts +30 -1
- package/src/bridge.ts +50 -12
- package/src/chat/inbound.ts +1 -0
- package/src/claude/agents-inject.ts +3 -2
- package/src/cli/dispatch.ts +19 -6
- package/src/cli/help.ts +2 -1
- package/src/cli/integrations.ts +35 -0
- package/src/cli/minimax.ts +8 -2
- package/src/cli/registry.ts +13 -2
- package/src/clients/config-export.ts +120 -1
- package/src/codex/account-store.ts +17 -1
- package/src/codex/auth-api.ts +51 -17
- package/src/codex/catalog/effort.ts +8 -5
- package/src/codex/catalog/provider-fetch.ts +39 -27
- package/src/codex/catalog/sync.ts +30 -8
- package/src/codex/inject.ts +39 -13
- package/src/codex/main-account.ts +29 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +25 -0
- package/src/codex/prompt-journal.ts +6 -2
- package/src/codex/quota-rejection.ts +21 -7
- package/src/codex/refresh.ts +4 -2
- package/src/codex/sync.ts +106 -3
- package/src/codex/warmup.ts +187 -81
- package/src/config.ts +85 -46
- package/src/generated/compatibility-version.json +127 -83
- package/src/grok/inject.ts +1 -1
- package/src/images/loop.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/lab/automation/config-persistence.ts +2 -2
- package/src/lab/automation/persistence.ts +2 -2
- package/src/lab/ledger/purge.ts +2 -2
- package/src/lab/subject/behavior-fingerprint.ts +2 -2
- package/src/lib/config-ownership.ts +5 -2
- package/src/lib/destination-policy.ts +18 -1
- package/src/lib/provider-outbound.ts +7 -0
- package/src/lib/redact.ts +11 -0
- package/src/lib/tool-argument-integers.ts +50 -6
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-retry.ts +2 -1
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/oauth/google-antigravity.ts +2 -2
- package/src/oauth/index.ts +60 -8
- package/src/providers/antigravity-models.ts +58 -4
- package/src/providers/command-code-efforts.ts +36 -9
- package/src/providers/context-cap.ts +9 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fastwire.ts +453 -0
- package/src/providers/registry.ts +21 -1
- package/src/providers/service-tier.ts +173 -89
- package/src/responses/parser.ts +25 -16
- package/src/responses/reasoning-replay-cache.ts +30 -0
- package/src/responses/thought-signature-replay.ts +74 -4
- package/src/router.ts +6 -0
- package/src/routing/compatibility/behavior.ts +27 -16
- package/src/server/index.ts +10 -1
- package/src/server/management/oauth-account-routes.ts +10 -2
- package/src/server/management/shared.ts +1 -1
- package/src/server/management/system-routes.ts +15 -0
- package/src/server/request-log.ts +57 -3
- package/src/server/responses/agent-task-recovery.ts +6 -1
- package/src/server/responses/core.ts +209 -40
- package/src/server/responses/fetch-helpers.ts +15 -36
- package/src/server/responses/responses-field-backfill.ts +173 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
- package/src/service.ts +38 -36
- package/src/storage/cleanup.ts +2 -2
- package/src/tray/windows.ts +3 -2
- package/src/types.ts +101 -14
- package/src/update/job.ts +9 -18
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +34 -5
- package/src/usage/log.ts +74 -4
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/web-search/anthropic-executor.ts +8 -6
- 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.
|
|
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
|
-
*
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
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
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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);
|
package/src/storage/cleanup.ts
CHANGED
|
@@ -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
|
-
|
|
2438
|
+
renameAtomicFile(tmp, dest, undefined, "storage-cleanup");
|
|
2439
2439
|
} catch (error) {
|
|
2440
2440
|
try { unlinkSync(tmp); } catch { /* */ }
|
|
2441
2441
|
throw error;
|
package/src/tray/windows.ts
CHANGED
|
@@ -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,
|
|
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:
|
|
246
|
+
rename: (source, destination) => renameAtomicFile(source, destination, undefined, "tray"),
|
|
246
247
|
unlink: unlinkSync,
|
|
247
248
|
},
|
|
248
249
|
): void {
|