@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927
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 +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
package/src/storage/scanner.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { readdir, stat as statAsync } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
import { Database, constants } from "bun:sqlite";
|
|
@@ -110,6 +111,81 @@ function walkFiles(dir: string, relPrefix: string, out: FileEntry[]): void {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/** Upper bound on concurrent readdir/stat calls within one async scan. */
|
|
115
|
+
const SCAN_FS_CONCURRENCY = 64;
|
|
116
|
+
|
|
117
|
+
type FsLimit = <T>(op: () => Promise<T>) => Promise<T>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* FIFO limiter for filesystem calls. A finishing call hands its slot straight to the next
|
|
121
|
+
* waiter, so the bound holds exactly. Only leaf fs calls take a slot: a directory walk
|
|
122
|
+
* never holds one while awaiting its children, so recursion cannot deadlock the pool.
|
|
123
|
+
*/
|
|
124
|
+
function createFsLimit(max: number): FsLimit {
|
|
125
|
+
let active = 0;
|
|
126
|
+
const waiting: Array<() => void> = [];
|
|
127
|
+
return async <T>(op: () => Promise<T>): Promise<T> => {
|
|
128
|
+
if (active < max) active += 1;
|
|
129
|
+
else await new Promise<void>(resolve => waiting.push(resolve));
|
|
130
|
+
try {
|
|
131
|
+
return await op();
|
|
132
|
+
} finally {
|
|
133
|
+
const next = waiting.shift();
|
|
134
|
+
if (next) next();
|
|
135
|
+
else active -= 1;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Entries started together at one directory level of an async scan. */
|
|
141
|
+
const SCAN_BATCH_SIZE = 256;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Map `items` through `fn` a batch at a time, keeping input order. A directory with tens of
|
|
145
|
+
* thousands of entries then has at most one batch of pending tasks at its level, instead of
|
|
146
|
+
* one promise per entry queued behind the fs limiter.
|
|
147
|
+
*/
|
|
148
|
+
async function mapInBatches<T, R>(items: readonly T[], fn: (item: T) => Promise<R>): Promise<R[]> {
|
|
149
|
+
const results: R[] = [];
|
|
150
|
+
for (let start = 0; start < items.length; start += SCAN_BATCH_SIZE) {
|
|
151
|
+
const batch = await Promise.all(items.slice(start, start + SCAN_BATCH_SIZE).map(fn));
|
|
152
|
+
for (const result of batch) results.push(result);
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Async twin of {@link walkFiles}: same skip rules, and entries come back in readdir
|
|
159
|
+
* order so the report (including `largest` tie order) matches the synchronous scan.
|
|
160
|
+
*/
|
|
161
|
+
async function walkFilesAsync(dir: string, relPrefix: string, limit: FsLimit): Promise<FileEntry[]> {
|
|
162
|
+
let entries;
|
|
163
|
+
try {
|
|
164
|
+
entries = await limit(() => readdir(dir, { withFileTypes: true }));
|
|
165
|
+
} catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
// Bun can settle small async fs calls on the microtask queue alone, so a walk of cached
|
|
169
|
+
// directories would otherwise finish without ever giving the event loop a turn. One timer
|
|
170
|
+
// turn per directory keeps request handling live during a large scan.
|
|
171
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
172
|
+
const parts = await mapInBatches(entries, async (entry): Promise<FileEntry[]> => {
|
|
173
|
+
const full = join(dir, entry.name);
|
|
174
|
+
const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
175
|
+
try {
|
|
176
|
+
if (entry.isDirectory()) return await walkFilesAsync(full, relPath, limit);
|
|
177
|
+
if (entry.isFile()) {
|
|
178
|
+
const stat = await limit(() => statAsync(full));
|
|
179
|
+
return [{ relPath, bytes: stat.size, mtimeMs: stat.mtimeMs }];
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
/* entry vanished mid-scan — diagnostics tolerate racy trees */
|
|
183
|
+
}
|
|
184
|
+
return [];
|
|
185
|
+
});
|
|
186
|
+
return parts.flat();
|
|
187
|
+
}
|
|
188
|
+
|
|
113
189
|
function buildBucket(key: StorageBucketKey, files: FileEntry[]): StorageBucket {
|
|
114
190
|
const bucket: StorageBucket = {
|
|
115
191
|
key,
|
|
@@ -169,8 +245,8 @@ function newestVersionedDb(names: string[], pattern: RegExp): string | null {
|
|
|
169
245
|
return best;
|
|
170
246
|
}
|
|
171
247
|
|
|
172
|
-
|
|
173
|
-
|
|
248
|
+
function emptyFiles(): Record<StorageBucketKey, FileEntry[]> {
|
|
249
|
+
return {
|
|
174
250
|
sessions: [],
|
|
175
251
|
archived_sessions: [],
|
|
176
252
|
logs_db: [],
|
|
@@ -179,36 +255,27 @@ export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageR
|
|
|
179
255
|
deletion_manifests: [],
|
|
180
256
|
other: [],
|
|
181
257
|
};
|
|
258
|
+
}
|
|
182
259
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
260
|
+
/** A missing home is a normal fresh-machine state; any other readdir failure is rethrown. */
|
|
261
|
+
function rootReadFailure(error: unknown): string[] {
|
|
262
|
+
// A missing home is a normal fresh-machine state — report zeros. Anything else
|
|
263
|
+
// (e.g. ENOTDIR: CODEX_HOME points at a file) is a broken setup the caller
|
|
264
|
+
// must surface as a scan failure, not silently render as an empty home.
|
|
265
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
266
|
+
if (code !== "ENOENT") throw error;
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
193
269
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
stat = statSync(full);
|
|
199
|
-
} catch {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (stat.isDirectory()) {
|
|
203
|
-
// Quarantine trash (Phase 2) must not inflate "other" or totals.
|
|
204
|
-
if (name === TRASH_DIR) continue;
|
|
205
|
-
walkFiles(full, name, files[DIR_BUCKETS[name] ?? "other"]);
|
|
206
|
-
} else if (stat.isFile()) {
|
|
207
|
-
const key: StorageBucketKey = STATE_DB_FILE.test(name) ? "state_db" : LOGS_DB_FILE.test(name) ? "logs_db" : "other";
|
|
208
|
-
files[key].push({ relPath: name, bytes: stat.size, mtimeMs: stat.mtimeMs });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
270
|
+
function rootFileBucket(name: string): StorageBucketKey {
|
|
271
|
+
return STATE_DB_FILE.test(name) ? "state_db" : LOGS_DB_FILE.test(name) ? "logs_db" : "other";
|
|
272
|
+
}
|
|
211
273
|
|
|
274
|
+
function finishReport(
|
|
275
|
+
codexHome: string,
|
|
276
|
+
rootNames: string[],
|
|
277
|
+
files: Record<StorageBucketKey, FileEntry[]>,
|
|
278
|
+
): StorageReport {
|
|
212
279
|
const buckets = (Object.keys(files) as StorageBucketKey[]).map(key => buildBucket(key, files[key]));
|
|
213
280
|
|
|
214
281
|
const stateDbName = newestVersionedDb(rootNames, STATE_DB_FILE);
|
|
@@ -236,3 +303,84 @@ export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageR
|
|
|
236
303
|
buckets,
|
|
237
304
|
};
|
|
238
305
|
}
|
|
306
|
+
|
|
307
|
+
export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageReport {
|
|
308
|
+
const files = emptyFiles();
|
|
309
|
+
|
|
310
|
+
let rootNames: string[] = [];
|
|
311
|
+
try {
|
|
312
|
+
rootNames = readdirSync(codexHome);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
rootNames = rootReadFailure(error);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const name of rootNames) {
|
|
318
|
+
const full = join(codexHome, name);
|
|
319
|
+
let stat;
|
|
320
|
+
try {
|
|
321
|
+
stat = statSync(full);
|
|
322
|
+
} catch {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (stat.isDirectory()) {
|
|
326
|
+
// Quarantine trash (Phase 2) must not inflate "other" or totals.
|
|
327
|
+
if (name === TRASH_DIR) continue;
|
|
328
|
+
walkFiles(full, name, files[DIR_BUCKETS[name] ?? "other"]);
|
|
329
|
+
} else if (stat.isFile()) {
|
|
330
|
+
files[rootFileBucket(name)].push({ relPath: name, bytes: stat.size, mtimeMs: stat.mtimeMs });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return finishReport(codexHome, rootNames, files);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Same report as {@link scanStorage}, but the tree walk uses async fs calls so the server's
|
|
339
|
+
* event loop keeps serving while it runs. The synchronous walk measured ~3.7s for a
|
|
340
|
+
* 40k-file / 7.5GB CODEX_HOME on Windows, during which every listener (proxy traffic
|
|
341
|
+
* included) stalled each time the Storage page loaded. The two sqlite row counts stay
|
|
342
|
+
* synchronous: immutable readonly opens measured ~0.1s on the same home.
|
|
343
|
+
*/
|
|
344
|
+
export async function scanStorageAsync(codexHome: string = resolveCodexHomeDir()): Promise<StorageReport> {
|
|
345
|
+
const files = emptyFiles();
|
|
346
|
+
const limit = createFsLimit(SCAN_FS_CONCURRENCY);
|
|
347
|
+
|
|
348
|
+
let rootNames: string[] = [];
|
|
349
|
+
try {
|
|
350
|
+
rootNames = await readdir(codexHome);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
rootNames = rootReadFailure(error);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const roots = await mapInBatches(rootNames, async name => {
|
|
356
|
+
const full = join(codexHome, name);
|
|
357
|
+
try {
|
|
358
|
+
return { name, full, stat: await limit(() => statAsync(full)) };
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
const walks = await mapInBatches(roots, async root => {
|
|
364
|
+
if (!root) return;
|
|
365
|
+
if (root.stat.isDirectory()) {
|
|
366
|
+
// Quarantine trash (Phase 2) must not inflate "other" or totals.
|
|
367
|
+
if (root.name === TRASH_DIR) return;
|
|
368
|
+
return { key: DIR_BUCKETS[root.name] ?? "other", entries: await walkFilesAsync(root.full, root.name, limit) };
|
|
369
|
+
}
|
|
370
|
+
if (root.stat.isFile()) {
|
|
371
|
+
return {
|
|
372
|
+
key: rootFileBucket(root.name),
|
|
373
|
+
entries: [{ relPath: root.name, bytes: root.stat.size, mtimeMs: root.stat.mtimeMs }],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// Appended in root order so bucket contents match the synchronous scan. One entry at a
|
|
378
|
+
// time: spreading a large bucket into push() can exceed the engine's argument limit.
|
|
379
|
+
for (const walk of walks) {
|
|
380
|
+
if (!walk) continue;
|
|
381
|
+
const bucket = files[walk.key];
|
|
382
|
+
for (const entry of walk.entries) bucket.push(entry);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return finishReport(codexHome, rootNames, files);
|
|
386
|
+
}
|
|
@@ -34,8 +34,24 @@ export const MAX_ACTIVE_STORAGE_HOME_SLOTS = 32;
|
|
|
34
34
|
const slots = new Map<string, ActiveSlot>();
|
|
35
35
|
const slotGate = createAdmissionGate("storage_home_slots", MAX_ACTIVE_STORAGE_HOME_SLOTS);
|
|
36
36
|
let releaseMisses = 0;
|
|
37
|
+
let mutationEpoch = 0;
|
|
37
38
|
let testHooks: StorageMutationCoordinatorTestHooks | null = null;
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Advances whenever a CODEX_HOME storage mutation finishes (coordinated cleanup, restore
|
|
42
|
+
* and policy runs on lease release; Log Guard maintenance through
|
|
43
|
+
* {@link noteStorageMutationCompleted}). An in-flight storage scan that started under an
|
|
44
|
+
* older epoch describes the tree before that mutation and must not answer a later read.
|
|
45
|
+
*/
|
|
46
|
+
export function storageMutationEpoch(): number {
|
|
47
|
+
return mutationEpoch;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Record a CODEX_HOME mutation that does not go through a coordinator slot. */
|
|
51
|
+
export function noteStorageMutationCompleted(): void {
|
|
52
|
+
mutationEpoch += 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
function slotKey(codexHome?: string): string {
|
|
40
56
|
return resolve(codexHome ?? resolveCodexHomeDir());
|
|
41
57
|
}
|
|
@@ -79,6 +95,7 @@ export function tryBeginStorageMutation(
|
|
|
79
95
|
release() {
|
|
80
96
|
if (!active) return;
|
|
81
97
|
active = false;
|
|
98
|
+
mutationEpoch += 1;
|
|
82
99
|
const owner = slots.get(key);
|
|
83
100
|
if (owner?.lease === ownerLease) slots.delete(key);
|
|
84
101
|
lease.release();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
param(
|
|
1
|
+
param(
|
|
2
2
|
[Parameter(Mandatory = $true)][string]$BunPath,
|
|
3
3
|
[Parameter(Mandatory = $true)][string]$CliPath,
|
|
4
4
|
[Parameter(Mandatory = $true)][string]$CodexHome,
|
|
@@ -15,6 +15,19 @@ Add-Type -AssemblyName System.Windows.Forms
|
|
|
15
15
|
Add-Type -AssemblyName System.Drawing
|
|
16
16
|
try { [System.Windows.Forms.Application]::EnableVisualStyles() } catch { $null = $_ }
|
|
17
17
|
|
|
18
|
+
# Chinese UI cultures get Chinese tray text; every other locale keeps the English strings. The
|
|
19
|
+
# judgment is a named function so it can be exercised directly instead of only through a live
|
|
20
|
+
# desktop, where a selector that always answered English would still look correct.
|
|
21
|
+
function Test-TrayChineseCulture([string]$CultureName) {
|
|
22
|
+
return $CultureName -like "zh*"
|
|
23
|
+
}
|
|
24
|
+
$script:isZh = Test-TrayChineseCulture ([System.Globalization.CultureInfo]::CurrentUICulture.Name)
|
|
25
|
+
function Get-TrayText {
|
|
26
|
+
param([string]$En, [string]$Zh)
|
|
27
|
+
if ($script:isZh) { return $Zh }
|
|
28
|
+
return $En
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
# Normalize aliases before deriving singleton/event names. Without this,
|
|
19
32
|
# C:\path and C:\path\. create separate tray instances for the same home.
|
|
20
33
|
function Normalize-HomePath([string]$Value) {
|
|
@@ -127,7 +140,7 @@ function Start-OcxCommand([string[]]$CommandArgs, [switch]$TrackExit) {
|
|
|
127
140
|
return $true
|
|
128
141
|
} catch {
|
|
129
142
|
Write-ActionLog "launch failed: $($_.Exception.GetType().Name)"
|
|
130
|
-
$notify.ShowBalloonTip(5000, "opencodex action failed", "The action could not start. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
143
|
+
$notify.ShowBalloonTip(5000, (Get-TrayText "opencodex action failed" "opencodex 操作失败"), (Get-TrayText "The action could not start. Open the logs folder or run ocx doctor." "操作无法启动。打开日志文件夹或运行 ocx doctor。"), [System.Windows.Forms.ToolTipIcon]::Error)
|
|
131
144
|
return $false
|
|
132
145
|
}
|
|
133
146
|
}
|
|
@@ -396,19 +409,19 @@ $statusItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
|
396
409
|
$statusItem.Enabled = $false
|
|
397
410
|
$safetyItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
398
411
|
$safetyItem.Enabled = $false
|
|
399
|
-
$openItem = $menu.Items.Add("Open Dashboard")
|
|
400
|
-
$updateItem = $menu.Items.Add("Update available")
|
|
412
|
+
$openItem = $menu.Items.Add((Get-TrayText "Open Dashboard" "打开面板"))
|
|
413
|
+
$updateItem = $menu.Items.Add((Get-TrayText "Update available" "有可用更新"))
|
|
401
414
|
$updateItem.Visible = $false
|
|
402
415
|
$updateItem.Enabled = $false
|
|
403
|
-
$startItem = $menu.Items.Add("Start Proxy")
|
|
404
|
-
$stopItem = $menu.Items.Add("Stop Proxy and Restore Native Routing")
|
|
405
|
-
$restartItem = $menu.Items.Add("Restart Proxy")
|
|
416
|
+
$startItem = $menu.Items.Add((Get-TrayText "Start Proxy" "启动代理"))
|
|
417
|
+
$stopItem = $menu.Items.Add((Get-TrayText "Stop Proxy and Restore Native Routing" "停止代理并还原原生路由"))
|
|
418
|
+
$restartItem = $menu.Items.Add((Get-TrayText "Restart Proxy" "重启代理"))
|
|
406
419
|
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
|
|
407
420
|
[void]$menu.Items.Add($statusItem)
|
|
408
421
|
[void]$menu.Items.Add($safetyItem)
|
|
409
|
-
$logsItem = $menu.Items.Add("Open Logs Folder")
|
|
422
|
+
$logsItem = $menu.Items.Add((Get-TrayText "Open Logs Folder" "打开日志文件夹"))
|
|
410
423
|
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
|
|
411
|
-
$exitItem = $menu.Items.Add("Exit Tray")
|
|
424
|
+
$exitItem = $menu.Items.Add((Get-TrayText "Exit Tray" "退出托盘"))
|
|
412
425
|
|
|
413
426
|
$script:online = $false
|
|
414
427
|
$script:port = 10100
|
|
@@ -463,6 +476,14 @@ function Set-PendingAction([string]$Action, [int]$TimeoutSeconds) {
|
|
|
463
476
|
function Complete-PendingAction([bool]$Success) {
|
|
464
477
|
if ($null -eq $script:pendingAction) { return }
|
|
465
478
|
$action = $script:pendingAction
|
|
479
|
+
# The pending value stays English because it is compared against the labels the click
|
|
480
|
+
# handlers set; only the text a user reads is localized.
|
|
481
|
+
$displayAction = switch ($action) {
|
|
482
|
+
"Start Proxy" { Get-TrayText "Start Proxy" "启动代理" }
|
|
483
|
+
"Stop Proxy" { Get-TrayText "Stop Proxy" "停止代理" }
|
|
484
|
+
"Restart Proxy" { Get-TrayText "Restart Proxy" "重启代理" }
|
|
485
|
+
default { $action }
|
|
486
|
+
}
|
|
466
487
|
$script:pendingAction = $null
|
|
467
488
|
if ($null -ne $script:pendingProcess) {
|
|
468
489
|
try {
|
|
@@ -474,10 +495,10 @@ function Complete-PendingAction([bool]$Success) {
|
|
|
474
495
|
}
|
|
475
496
|
if ($Success) {
|
|
476
497
|
Write-ActionLog "$action completed (port=$($script:port), pid=$($script:proxyPid))"
|
|
477
|
-
$notify.ShowBalloonTip(2500, "opencodex", "$
|
|
498
|
+
$notify.ShowBalloonTip(2500, "opencodex", (Get-TrayText "$displayAction completed." "$displayAction 已完成。"), [System.Windows.Forms.ToolTipIcon]::Info)
|
|
478
499
|
} else {
|
|
479
500
|
Write-ActionLog "$action failed to reach the expected state"
|
|
480
|
-
$notify.ShowBalloonTip(5000, "opencodex action failed", "$
|
|
501
|
+
$notify.ShowBalloonTip(5000, (Get-TrayText "opencodex action failed" "opencodex 操作失败"), (Get-TrayText "$displayAction did not reach the expected state. Open the logs folder or run ocx doctor." "$displayAction 未达到预期状态。打开日志文件夹或运行 ocx doctor。"), [System.Windows.Forms.ToolTipIcon]::Error)
|
|
481
502
|
}
|
|
482
503
|
}
|
|
483
504
|
|
|
@@ -536,8 +557,8 @@ function Update-TrayState {
|
|
|
536
557
|
Start-StartupHealthProbe
|
|
537
558
|
}
|
|
538
559
|
if ($script:online) {
|
|
539
|
-
$statusItem.Text = "Proxy: Online (port $($script:port))"
|
|
540
|
-
$notify.Text = "opencodex: Online"
|
|
560
|
+
$statusItem.Text = (Get-TrayText "Proxy: Online (port $($script:port))" "代理: 在线 (端口 $($script:port))")
|
|
561
|
+
$notify.Text = (Get-TrayText "opencodex: Online" "opencodex: 在线")
|
|
541
562
|
$startItem.Enabled = $false
|
|
542
563
|
$stopItem.Enabled = $true
|
|
543
564
|
$restartItem.Enabled = $true
|
|
@@ -547,21 +568,27 @@ function Update-TrayState {
|
|
|
547
568
|
# diagnostic is cleaned up outside the online-only UI branch.
|
|
548
569
|
$startup = $script:startupHealth
|
|
549
570
|
if ($null -ne $startup) {
|
|
550
|
-
$label = if ($startup.status -eq "at-risk") {
|
|
551
|
-
|
|
571
|
+
$label = if ($startup.status -eq "at-risk") {
|
|
572
|
+
Get-TrayText "At risk" "有风险"
|
|
573
|
+
} elseif ($startup.status -eq "protected") {
|
|
574
|
+
Get-TrayText "Protected" "已保护"
|
|
575
|
+
} else {
|
|
576
|
+
Get-TrayText "Native routing" "原生路由"
|
|
577
|
+
}
|
|
578
|
+
$safetyItem.Text = (Get-TrayText "Restart safety: $label" "重启保护: $label")
|
|
552
579
|
$notify.Icon = if ($startup.status -eq "at-risk") {
|
|
553
580
|
if ($script:updateAvailable) { $warningUpdateIcon } else { $warningIcon }
|
|
554
581
|
} else {
|
|
555
582
|
if ($script:updateAvailable) { $onlineUpdateIcon } else { $onlineIcon }
|
|
556
583
|
}
|
|
557
584
|
} else {
|
|
558
|
-
$safetyItem.Text = "Restart safety: unavailable"
|
|
585
|
+
$safetyItem.Text = (Get-TrayText "Restart safety: unavailable" "重启保护: 不可用")
|
|
559
586
|
$notify.Icon = if ($script:updateAvailable) { $warningUpdateIcon } else { $warningIcon }
|
|
560
587
|
}
|
|
561
588
|
} else {
|
|
562
|
-
$statusItem.Text = "Proxy: Offline"
|
|
563
|
-
$safetyItem.Text = "Restart safety: start the proxy to inspect"
|
|
564
|
-
$notify.Text = "opencodex: Offline"
|
|
589
|
+
$statusItem.Text = (Get-TrayText "Proxy: Offline" "代理: 离线")
|
|
590
|
+
$safetyItem.Text = (Get-TrayText "Restart safety: start the proxy to inspect" "重启保护: 启动代理后查看")
|
|
591
|
+
$notify.Text = (Get-TrayText "opencodex: Offline" "opencodex: 离线")
|
|
565
592
|
$notify.Icon = if ($script:updateAvailable) { $offlineUpdateIcon } else { $offlineIcon }
|
|
566
593
|
$startItem.Enabled = $true
|
|
567
594
|
$stopItem.Enabled = $false
|
|
@@ -607,7 +634,7 @@ $openItem.add_Click({ Start-OcxCommand @("gui") })
|
|
|
607
634
|
$updateItem.add_Click({ Start-OcxCommand @("gui") })
|
|
608
635
|
$startItem.add_Click({
|
|
609
636
|
if (-not (Set-PendingAction "Start Proxy" 75)) { return }
|
|
610
|
-
$statusItem.Text = "Proxy: Starting..."
|
|
637
|
+
$statusItem.Text = (Get-TrayText "Proxy: Starting..." "代理: 启动中...")
|
|
611
638
|
# service start can spend 20s and the CLI then observes health for another 40s.
|
|
612
639
|
$startProcess = Start-OcxCommand @("__tray-start") -TrackExit
|
|
613
640
|
if ($startProcess -is [System.Diagnostics.Process]) {
|
|
@@ -618,7 +645,7 @@ $startItem.add_Click({
|
|
|
618
645
|
})
|
|
619
646
|
$stopItem.add_Click({
|
|
620
647
|
if (-not (Set-PendingAction "Stop Proxy" 15)) { return }
|
|
621
|
-
$statusItem.Text = "Proxy: Stopping..."
|
|
648
|
+
$statusItem.Text = (Get-TrayText "Proxy: Stopping..." "代理: 停止中...")
|
|
622
649
|
$stopProcess = Start-OcxCommand @("stop") -TrackExit
|
|
623
650
|
if ($stopProcess -is [System.Diagnostics.Process]) {
|
|
624
651
|
$script:pendingProcess = $stopProcess
|
|
@@ -628,7 +655,7 @@ $stopItem.add_Click({
|
|
|
628
655
|
})
|
|
629
656
|
$restartItem.add_Click({
|
|
630
657
|
if (-not (Set-PendingAction "Restart Proxy" 160)) { return }
|
|
631
|
-
$statusItem.Text = "Proxy: Restarting..."
|
|
658
|
+
$statusItem.Text = (Get-TrayText "Proxy: Restarting..." "代理: 重启中...")
|
|
632
659
|
# /api/system/restart may drain active work for 60s and then spend up to 70s
|
|
633
660
|
# handing off to an identity-verified replacement. The tray observes health/PID
|
|
634
661
|
# rather than the detached CLI exit, so keep a watchdog margin around that shared
|
|
@@ -665,7 +692,7 @@ $timer.add_Tick({
|
|
|
665
692
|
$notify.ContextMenuStrip = $menu
|
|
666
693
|
$notify.Icon = $offlineIcon
|
|
667
694
|
$notify.Visible = $true
|
|
668
|
-
$notify.Text = "opencodex: Checking..."
|
|
695
|
+
$notify.Text = (Get-TrayText "opencodex: Checking..." "opencodex: 检查中...")
|
|
669
696
|
|
|
670
697
|
try {
|
|
671
698
|
Initialize-UpdateBadgeReader
|
package/src/types/config.ts
CHANGED
|
@@ -722,6 +722,8 @@ export interface OcxConfig {
|
|
|
722
722
|
/** Compaction triggers this override covers; omission means `["manual"]`. */
|
|
723
723
|
triggers?: ("manual" | "auto")[];
|
|
724
724
|
};
|
|
725
|
+
/** Opt-in failure-only recovery; never replaces the initial compaction model. */
|
|
726
|
+
compactionRecovery?: { enabled: boolean; model: string; allowDevinInvalidArgument?: boolean };
|
|
725
727
|
/**
|
|
726
728
|
* Models hidden from Codex discovery without blocking direct proxy calls. Routed provider ids
|
|
727
729
|
* are excluded from the catalog + /v1/models entirely. Account-qualified native ids hide only
|
|
@@ -1514,6 +1516,8 @@ export interface OcxWebSearchSidecarConfig {
|
|
|
1514
1516
|
* explicit account selection. Only automatic rotation skips it.
|
|
1515
1517
|
*/
|
|
1516
1518
|
export interface OcxCodexPoolConfig {
|
|
1519
|
+
/** Start idle Codex windows when the pool is initialized. */
|
|
1520
|
+
startIdleWindows?: boolean;
|
|
1517
1521
|
/**
|
|
1518
1522
|
* Plan keys ordinary rotation skips, matched case-insensitively against the plan stored on each
|
|
1519
1523
|
* account. Absent or empty means no policy.
|
package/src/types/provider.ts
CHANGED
|
@@ -180,6 +180,8 @@ export interface RequestPacingRule {
|
|
|
180
180
|
requestsPerMinute?: number;
|
|
181
181
|
/** Minimum delay between request starts. The slower configured value wins. */
|
|
182
182
|
minIntervalMs?: number;
|
|
183
|
+
/** Maximum number of requests concurrently in flight. */
|
|
184
|
+
maxConcurrentRequests?: number;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
export interface ProviderRequestPacingConfig extends RequestPacingRule {
|
|
@@ -660,13 +662,15 @@ export interface OcxProviderConfig {
|
|
|
660
662
|
* Reactive 429 rotation remains available even when proactive routing is disabled.
|
|
661
663
|
*/
|
|
662
664
|
oauthAccountFailover?: {
|
|
665
|
+
/** Kiro OAuth only: active serving requests per account, 1..100. */
|
|
666
|
+
maxConcurrentPerAccount?: number;
|
|
663
667
|
enabled?: boolean;
|
|
664
668
|
/**
|
|
665
669
|
* Generic OAuth pool selection strategy (#695). Persisted through the pool-settings
|
|
666
670
|
* contract. Consumed by the selector only while `pool.kernel` is on; with the flag off
|
|
667
671
|
* it is still merely persisted, so omitted and set behave the same.
|
|
668
672
|
*/
|
|
669
|
-
strategy?: "quota" | "round-robin" | "fill-first";
|
|
673
|
+
strategy?: "quota" | "round-robin" | "fill-first" | "least-loaded";
|
|
670
674
|
/**
|
|
671
675
|
* 0-100 usage percent at which fill-first advances off the active account (#695).
|
|
672
676
|
* Read only under `pool.kernel` with `strategy: "fill-first"`; 80 when unset, matching
|
package/src/types/request.ts
CHANGED
|
@@ -221,8 +221,17 @@ export interface OcxImageContent {
|
|
|
221
221
|
|
|
222
222
|
export interface OcxVideoContent {
|
|
223
223
|
type: "video";
|
|
224
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* A base64 `data:` URL from an OpenAI-compatible `video_url` part, or a URI
|
|
226
|
+
* the upstream can fetch itself (a YouTube watch URL, a Files API uri).
|
|
227
|
+
*/
|
|
225
228
|
videoUrl: string;
|
|
229
|
+
/**
|
|
230
|
+
* Gemini's agentic video mode, carried verbatim from the caller's
|
|
231
|
+
* `video_url.processing` (#3271). Absent for every request that does not ask
|
|
232
|
+
* for it, so no existing traffic gains a field.
|
|
233
|
+
*/
|
|
234
|
+
processing?: string;
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
/**
|
|
@@ -361,7 +370,7 @@ export interface OcxProviderContinuationState {
|
|
|
361
370
|
}
|
|
362
371
|
|
|
363
372
|
export type AdapterEvent =
|
|
364
|
-
| { type: "heartbeat"; replayUnsafe?: true }
|
|
373
|
+
| { type: "heartbeat"; replayUnsafe?: true; preflightReady?: true }
|
|
365
374
|
| { type: "text_delta"; text: string; phase?: OcxMessagePhase }
|
|
366
375
|
| { type: "thinking_delta"; thinking: string }
|
|
367
376
|
// Anthropic extended-thinking round-trip: signature_delta for the current thinking block, and
|
|
@@ -438,6 +447,8 @@ export interface OcxUrlCitation {
|
|
|
438
447
|
* - `totalTokens` = inputTokens + outputTokens. Never re-add cache detail on top.
|
|
439
448
|
*/
|
|
440
449
|
export interface OcxUsage {
|
|
450
|
+
/** Provider-reported credit spend, independent of token estimates and USD pricing. */
|
|
451
|
+
providerCredits?: number;
|
|
441
452
|
inputTokens: number;
|
|
442
453
|
outputTokens: number;
|
|
443
454
|
/**
|
package/src/update/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "./npm-cache-preflight.mjs";
|
|
39
39
|
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
|
|
40
40
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
41
|
+
import { withoutSiblingMarker } from "../codex/sibling-start";
|
|
41
42
|
import { packageVersion } from "../lib/package-version";
|
|
42
43
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
43
44
|
|
|
@@ -529,7 +530,8 @@ export async function runUpdate(): Promise<void> {
|
|
|
529
530
|
};
|
|
530
531
|
const startProxyDirectly = async (): Promise<boolean> => {
|
|
531
532
|
if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) return false;
|
|
532
|
-
|
|
533
|
+
// An ordinary owner: a stray sibling marker would otherwise mark it before any probe.
|
|
534
|
+
const env = mutation.controlEnvironment(withoutSiblingMarker(process.env));
|
|
533
535
|
delete env.OCX_SERVICE;
|
|
534
536
|
const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
|
|
535
537
|
detached: true, stdio: "ignore", windowsHide: true, env: withProcessRuntimeProvenance(env),
|
package/src/update/job.ts
CHANGED
|
@@ -60,6 +60,8 @@ import {
|
|
|
60
60
|
runNpmCachePreflight,
|
|
61
61
|
type NpmCachePreflightReason,
|
|
62
62
|
} from "./npm-cache-preflight.mjs";
|
|
63
|
+
import { guiUpdateWorkerCommand } from "./worker-launch";
|
|
64
|
+
import { withoutSiblingMarker } from "../codex/sibling-start";
|
|
63
65
|
|
|
64
66
|
const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
|
|
65
67
|
const UPDATE_JOB_FILENAME = "update-job.json";
|
|
@@ -574,7 +576,8 @@ export function spawnGuiUpdateWorker(
|
|
|
574
576
|
restart ? "restart" : "no-restart",
|
|
575
577
|
]);
|
|
576
578
|
if (process.platform !== "win32") {
|
|
577
|
-
|
|
579
|
+
const launch = guiUpdateWorkerCommand(process.execPath, args);
|
|
580
|
+
return spawn(launch.command, launch.argv, {
|
|
578
581
|
detached: true,
|
|
579
582
|
stdio: "ignore",
|
|
580
583
|
windowsHide: true,
|
|
@@ -932,7 +935,8 @@ function spawnDetachedStart(
|
|
|
932
935
|
launcher = packageLauncherPath(),
|
|
933
936
|
): ChildProcess {
|
|
934
937
|
const cmd = restartCommand(false, installer, launcher, port);
|
|
935
|
-
|
|
938
|
+
// An ordinary owner: a stray sibling marker would otherwise mark it before any probe.
|
|
939
|
+
const env: NodeJS.ProcessEnv = withoutSiblingMarker(process.env);
|
|
936
940
|
delete env.OCX_SERVICE;
|
|
937
941
|
updateJob(job, {}, `$ ${cmd.display}`);
|
|
938
942
|
let stdio: "ignore" | [ "ignore", number, number ] = "ignore";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
observePackageManifestAt,
|
|
5
|
+
packageManifestPathIn,
|
|
6
|
+
runningPackageRoot,
|
|
7
|
+
} from "../lib/package-tree-integrity";
|
|
8
|
+
import type { LauncherTarget, ResolveLauncherTarget } from "../lib/package-tree-retarget";
|
|
9
|
+
import { readServiceInstallState, SERVICE_MANAGED_ENV } from "../service/state";
|
|
10
|
+
import { detectInstallOwnershipFromPath, type InstallOwnership } from "./install-detection.mjs";
|
|
11
|
+
|
|
12
|
+
const PACKAGE_DIR = join("@bitkyc08", "opencodex");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The package launcher mise's npm backend creates in `<toolRoot>/<selector>/node_modules/.bin`,
|
|
16
|
+
* beside the package it starts. This is the layout `detectInstallOwnershipFromPath` verifies as
|
|
17
|
+
* mise-owned; a launcher in any other layout is not followed.
|
|
18
|
+
*/
|
|
19
|
+
const LAUNCHER_BIN = join("node_modules", ".bin");
|
|
20
|
+
const LAUNCHER_PACKAGE_DIR = join("node_modules", PACKAGE_DIR);
|
|
21
|
+
|
|
22
|
+
export interface MiseLauncherTargetDeps {
|
|
23
|
+
platform?: NodeJS.Platform;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
ownership?: () => InstallOwnership;
|
|
26
|
+
launcherPath?: () => string | undefined;
|
|
27
|
+
runningRoot?: () => string;
|
|
28
|
+
realpath?: (path: string) => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MiseLauncherTargetWatchPlan {
|
|
32
|
+
runningRoot: string;
|
|
33
|
+
resolveTarget: ResolveLauncherTarget;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function canonical(path: string, realpath: (path: string) => string): string | null {
|
|
37
|
+
try {
|
|
38
|
+
return realpath(path);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Map a recorded service launcher to the package directory it starts, when it is one of mise's
|
|
46
|
+
* own package launchers for this tool. A mise shim (`<data>/shims/ocx`) resolves to the mise
|
|
47
|
+
* binary and names no package, and anything outside `<toolRoot>/<selector>/` is not a launcher
|
|
48
|
+
* this install owns, so both are unsupported and return null.
|
|
49
|
+
*/
|
|
50
|
+
function launcherPackageEntry(launcherPath: string, toolRoot: string, realpath: (path: string) => string): string | null {
|
|
51
|
+
if (!isAbsolute(launcherPath)) return null;
|
|
52
|
+
const binDir = dirname(launcherPath);
|
|
53
|
+
const canonicalToolRoot = canonical(toolRoot, realpath);
|
|
54
|
+
if (canonicalToolRoot === null) return null;
|
|
55
|
+
if (!binDir.endsWith(`/${LAUNCHER_BIN}`)) return null;
|
|
56
|
+
const selectorDir = binDir.slice(0, binDir.length - LAUNCHER_BIN.length - 1);
|
|
57
|
+
if (canonical(dirname(selectorDir), realpath) !== canonicalToolRoot) return null;
|
|
58
|
+
return join(selectorDir, LAUNCHER_PACKAGE_DIR);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Decide whether this process should watch its service launcher for a mise upgrade, and build the
|
|
63
|
+
* resolver when it should.
|
|
64
|
+
*
|
|
65
|
+
* Eligible only when every one of these holds:
|
|
66
|
+
* - Linux. launchd services always run pinned package paths (see the stable-launcher contract),
|
|
67
|
+
* and Windows services have no launcher, so neither can follow a moved `latest` link.
|
|
68
|
+
* - The process is the managed service job itself (`OCX_SERVICE_MANAGED=1`). `OCX_SERVICE=1`
|
|
69
|
+
* alone is also set on proxies `ocx claude`/`ocx opencode` spawn, and a foreground proxy must
|
|
70
|
+
* never restart itself because a service record exists.
|
|
71
|
+
* - The running package has a verified mise owner, and the recorded launcher is one of that
|
|
72
|
+
* tool's package launchers.
|
|
73
|
+
* - At boot, the launcher resolves to exactly the running package. That proves the supervisor
|
|
74
|
+
* started this process through it, so a restart through it converges instead of looping.
|
|
75
|
+
*/
|
|
76
|
+
export function planMiseLauncherTargetWatch(deps: MiseLauncherTargetDeps = {}): MiseLauncherTargetWatchPlan | null {
|
|
77
|
+
if ((deps.platform ?? process.platform) !== "linux") return null;
|
|
78
|
+
if ((deps.env ?? process.env)[SERVICE_MANAGED_ENV] !== "1") return null;
|
|
79
|
+
const realpath = deps.realpath ?? realpathSync;
|
|
80
|
+
const runningRoot = canonical((deps.runningRoot ?? runningPackageRoot)(), realpath);
|
|
81
|
+
if (runningRoot === null) return null;
|
|
82
|
+
const ownership = (deps.ownership ?? (() => detectInstallOwnershipFromPath(runningRoot)))();
|
|
83
|
+
if (ownership.installer !== "mise" || !ownership.owner) return null;
|
|
84
|
+
const owner = ownership.owner;
|
|
85
|
+
const launcherPath = (deps.launcherPath ?? (() => readServiceInstallState()?.launcherPath))();
|
|
86
|
+
if (!launcherPath) return null;
|
|
87
|
+
const entry = launcherPackageEntry(launcherPath, owner.toolRoot, realpath);
|
|
88
|
+
const toolRoot = canonical(owner.toolRoot, realpath);
|
|
89
|
+
if (entry === null || toolRoot === null) return null;
|
|
90
|
+
|
|
91
|
+
const resolveTarget = (): LauncherTarget | null => {
|
|
92
|
+
const root = canonical(entry, realpath);
|
|
93
|
+
// A floating link repointed outside this tool's installs is not a mise upgrade of it.
|
|
94
|
+
if (root === null || !root.startsWith(`${toolRoot}/`)) return null;
|
|
95
|
+
const manifest = observePackageManifestAt(packageManifestPathIn(root));
|
|
96
|
+
return manifest === null ? null : { root, manifest };
|
|
97
|
+
};
|
|
98
|
+
if (resolveTarget()?.root !== runningRoot) return null;
|
|
99
|
+
return { runningRoot, resolveTarget };
|
|
100
|
+
}
|