@bitkyc08/opencodex 2.7.41 → 2.7.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
- package/gui/dist/assets/index-DfVGuN88.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/kiro-constants.ts +6 -2
- package/src/adapters/kiro-retry.ts +175 -10
- package/src/adapters/kiro.ts +172 -85
- package/src/adapters/mimo-free.ts +1 -0
- package/src/adapters/openai-chat.ts +30 -4
- package/src/adapters/openai-responses.ts +90 -12
- package/src/bridge.ts +91 -43
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +29 -2
- package/src/cli/access.ts +108 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account.ts +9 -1
- package/src/cli/agent.ts +184 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +20 -8
- package/src/cli/doctor.ts +45 -8
- package/src/cli/help.ts +65 -13
- package/src/cli/index.ts +108 -7
- package/src/cli/integrations.ts +142 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +9 -10
- package/src/cli/observe.ts +117 -0
- package/src/cli/provider-runtime.ts +152 -0
- package/src/cli/provider.ts +23 -1
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +3 -3
- package/src/cli/status.ts +17 -0
- package/src/cli/system-command.ts +112 -0
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/aggregation.ts +113 -18
- package/src/codex/catalog/provider-fetch.ts +24 -13
- package/src/codex/catalog/sync.ts +20 -8
- package/src/codex/catalog.ts +2 -1
- package/src/codex/refresh.ts +10 -3
- package/src/codex/routing.ts +21 -32
- package/src/codex/sync.ts +17 -0
- package/src/config.ts +48 -0
- package/src/generated/jawcode-model-metadata.ts +2 -1
- package/src/grok/inject.ts +184 -4
- package/src/grok/status.ts +33 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/windows-elevation.ts +627 -0
- package/src/providers/openai-sidecar.ts +46 -2
- package/src/providers/registry.ts +52 -0
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/claude-messages.ts +20 -1
- package/src/server/images.ts +14 -7
- package/src/server/management/agent-settings-routes.ts +10 -4
- package/src/server/management/combo-routes.ts +0 -1
- package/src/server/management/config-routes.ts +0 -1
- package/src/server/management/logs-usage-routes.ts +94 -0
- package/src/server/management/model-routes.ts +0 -1
- package/src/server/management/oauth-account-routes.ts +0 -1
- package/src/server/management/provider-routes.ts +0 -1
- package/src/server/management/shared.ts +0 -1
- package/src/server/management/system-routes.ts +27 -15
- package/src/server/management-api.ts +0 -1
- package/src/server/memory-watchdog.ts +54 -10
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +122 -2
- package/src/server/responses/core.ts +76 -13
- package/src/server/responses/passthrough-error.ts +38 -13
- package/src/server/startup-action-control.ts +266 -15
- package/src/service.ts +512 -3
- package/src/storage/cleanup.ts +1538 -0
- package/src/storage/scanner.ts +4 -1
- package/src/types.ts +16 -0
- package/src/update/job.ts +229 -25
- package/src/usage/log.ts +39 -0
- package/src/web-search/loop.ts +8 -1
- package/gui/dist/assets/index-B2J4t3te.css +0 -1
- package/gui/dist/assets/index-BmvM6wRb.js +0 -65
package/src/storage/scanner.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { Database, constants } from "bun:sqlite";
|
|
5
5
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
6
|
+
import { TRASH_DIR } from "./cleanup";
|
|
6
7
|
|
|
7
8
|
// SQLITE_OPEN_READONLY alone is not filesystem-read-only for a WAL-mode DB: Bun's
|
|
8
9
|
// `{ readonly: true }` can still materialize *.sqlite-wal/-shm sidecars the first time a
|
|
@@ -16,7 +17,7 @@ const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLI
|
|
|
16
17
|
/**
|
|
17
18
|
* Read-only CODEX_HOME storage scanner — Phase 1 of the Storage page epic
|
|
18
19
|
* (devlog/_plan/500_storage-page-session-cleanup). Pure measurement: sizes via
|
|
19
|
-
* fs.stat walks, DB row counts via
|
|
20
|
+
* fs.stat walks, DB row counts via immutable readonly opens that degrade to
|
|
20
21
|
* null on lock/corruption. Performs zero writes under CODEX_HOME.
|
|
21
22
|
*/
|
|
22
23
|
|
|
@@ -199,6 +200,8 @@ export function scanStorage(codexHome: string = resolveCodexHomeDir()): StorageR
|
|
|
199
200
|
continue;
|
|
200
201
|
}
|
|
201
202
|
if (stat.isDirectory()) {
|
|
203
|
+
// Quarantine trash (Phase 2) must not inflate "other" or totals.
|
|
204
|
+
if (name === TRASH_DIR) continue;
|
|
202
205
|
walkFiles(full, name, files[DIR_BUCKETS[name] ?? "other"]);
|
|
203
206
|
} else if (stat.isFile()) {
|
|
204
207
|
const key: StorageBucketKey = STATE_DB_FILE.test(name) ? "state_db" : LOGS_DB_FILE.test(name) ? "logs_db" : "other";
|
package/src/types.ts
CHANGED
|
@@ -712,6 +712,8 @@ export interface OcxTokenGuardianConfig {
|
|
|
712
712
|
}
|
|
713
713
|
|
|
714
714
|
export interface OcxImagesConfig {
|
|
715
|
+
/** Optional custom API-key provider for /v1/images relays. Built-in OpenAI tiers remain automatic. */
|
|
716
|
+
provider?: string;
|
|
715
717
|
/** Upstream timeout (ms) for one /v1/images relay. Default 300000 — generation is slow. */
|
|
716
718
|
timeoutMs?: number;
|
|
717
719
|
}
|
|
@@ -898,6 +900,11 @@ export interface OcxProviderConfig {
|
|
|
898
900
|
* Responses backend rejects Codex summary-delivery fields for that model.
|
|
899
901
|
*/
|
|
900
902
|
modelSupportsReasoningSummaries?: Record<string, boolean>;
|
|
903
|
+
/**
|
|
904
|
+
* Per-model wire value for Responses `stream_options.reasoning_summary_delivery`.
|
|
905
|
+
* Presence also advertises reasoning-summary support for that routed model.
|
|
906
|
+
*/
|
|
907
|
+
modelReasoningSummaryDelivery?: Record<string, ReasoningSummaryDelivery>;
|
|
901
908
|
/** Provider-wide mapping from Codex effort labels to upstream `reasoning_effort` values. */
|
|
902
909
|
reasoningEffortMap?: Record<string, string>;
|
|
903
910
|
/** Model-specific mapping from Codex effort labels to upstream `reasoning_effort` values. */
|
|
@@ -1006,6 +1013,15 @@ export interface OcxProviderConfig {
|
|
|
1006
1013
|
nativeLocalExec?: "off" | "codex-sandbox" | "on";
|
|
1007
1014
|
}
|
|
1008
1015
|
|
|
1016
|
+
export const REASONING_SUMMARY_DELIVERY_VALUES = [
|
|
1017
|
+
"sequential",
|
|
1018
|
+
"sequential_cutoff",
|
|
1019
|
+
"concurrent",
|
|
1020
|
+
"concurrent_cutoff",
|
|
1021
|
+
] as const;
|
|
1022
|
+
|
|
1023
|
+
export type ReasoningSummaryDelivery = typeof REASONING_SUMMARY_DELIVERY_VALUES[number];
|
|
1024
|
+
|
|
1009
1025
|
/** Trusted runtime ownership for Codex-account credentials. Never persisted per provider. */
|
|
1010
1026
|
export type CodexAccountMode = "direct" | "pool";
|
|
1011
1027
|
|
package/src/update/job.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { atomicWriteFile, getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
6
|
import { killProxy } from "../lib/process-control";
|
|
7
7
|
import { reclaimListenPort } from "../server/port-reclaim";
|
|
8
|
-
import { proxyIdentityAt } from "../server/proxy-liveness";
|
|
8
|
+
import { isOpencodexHealthz, probeHostname, proxyIdentityAt, type HealthzIdentity } from "../server/proxy-liveness";
|
|
9
9
|
import { isServiceInstalled } from "../service";
|
|
10
10
|
import {
|
|
11
11
|
type Channel,
|
|
@@ -292,12 +292,20 @@ function spawnDetachedStart(job: UpdateJobState, installer: Installer, port?: nu
|
|
|
292
292
|
child.unref();
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/** Identity snapshot used to prove an npm self-update actually replaced the pre-update process. */
|
|
296
|
+
export interface RestartProxyIdentity {
|
|
297
|
+
pid: number | null;
|
|
298
|
+
version?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
295
301
|
/** Test seam: the wait/spawn pair is injectable so the restart path is verifiable. */
|
|
296
302
|
export interface RestartIo {
|
|
297
303
|
waitForPort?: typeof reclaimListenPort;
|
|
298
304
|
spawnStart?: (job: UpdateJobState, installer: Installer, port?: number) => void;
|
|
299
305
|
serviceInstalledFn?: () => boolean;
|
|
300
306
|
probeProxy?: (port: number, hostname?: string) => Promise<boolean>;
|
|
307
|
+
/** Richer /healthz read for update-correlated restart evidence (pid + version). */
|
|
308
|
+
probeProxyIdentity?: (port: number, hostname?: string) => Promise<RestartProxyIdentity | null>;
|
|
301
309
|
sleepMs?: (ms: number) => Promise<void>;
|
|
302
310
|
now?: () => number;
|
|
303
311
|
/** Service-mode install/reinstall command (defaults to spawnSync via runLoggedCommand). */
|
|
@@ -306,6 +314,12 @@ export interface RestartIo {
|
|
|
306
314
|
bin: string,
|
|
307
315
|
args: string[],
|
|
308
316
|
) => { status: number | null; signal?: NodeJS.Signals | null };
|
|
317
|
+
/** Override the explicit restart path (used by finishGuiUpdateRestart tests). */
|
|
318
|
+
restartAfterUpdateFn?: (
|
|
319
|
+
job: UpdateJobState,
|
|
320
|
+
captured?: { port: number; hostname: string; oldPid?: number },
|
|
321
|
+
io?: RestartIo,
|
|
322
|
+
) => Promise<void>;
|
|
309
323
|
}
|
|
310
324
|
|
|
311
325
|
async function restartAfterUpdate(
|
|
@@ -404,25 +418,19 @@ function restartFailureHint(port: number): string {
|
|
|
404
418
|
+ "reinstall with 'npm install -g --allow-scripts=bun @bitkyc08/opencodex'.";
|
|
405
419
|
}
|
|
406
420
|
|
|
421
|
+
type AwaitHealthyResult =
|
|
422
|
+
| { ok: true }
|
|
423
|
+
| { ok: false; reason: "timeout" | "flapped" };
|
|
424
|
+
|
|
407
425
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* where npm left the bundled Bun runtime half-updated and the restarted proxy died seconds
|
|
411
|
-
* later. A healthy /healthz must appear, then remain healthy for one short stability window.
|
|
426
|
+
* Wait for an identity-checked /healthz on the captured listen target, then require a short
|
|
427
|
+
* stability window. Soft: never marks the job failed (callers decide whether to fail or retry).
|
|
412
428
|
*/
|
|
413
|
-
async function
|
|
429
|
+
async function awaitRestartedProxyHealthy(
|
|
414
430
|
job: UpdateJobState,
|
|
415
431
|
captured: { port: number; hostname: string },
|
|
416
432
|
io: RestartIo = {},
|
|
417
|
-
): Promise<
|
|
418
|
-
/* [Decision Log]
|
|
419
|
-
- 목적과 의도: GUI update job이 detached restart 요청만 보고 성공 처리하지 않도록, 실제 프록시 복귀 여부를 확인한다.
|
|
420
|
-
- 기존 구현 및 제약 조건: update-job.json은 spawn/service reinstall 직후 `succeeded`로 끝났고, Windows npm/Bun 교체 실패처럼 몇 초 후 죽는 재시작을 잡지 못했다.
|
|
421
|
-
- 검토한 주요 대안: (1) 포트 점유만 확인 — 외부 프로세스/죽기 직전 프로세스를 성공으로 오인할 수 있다. (2) 무기한 /healthz 폴링 — UX가 느려지고 worker 종료 시점이 불명확하다. (3) 짧은 healthy 등장 + 안정성 창 확인 — 실제 복귀를 확인하면서도 대기 시간을 제한할 수 있다.
|
|
422
|
-
- 선택한 방식: identity-aware /healthz probe가 일정 시간 안에 나타나고, 추가 안정성 창 동안 유지되는지 확인한다.
|
|
423
|
-
- 다른 대안 대신 이 방식을 선택한 이유: GUI는 "업데이트가 설치됐지만 재시작은 실패"를 분리해 알려줘야 하며, 이 방식이 가장 적은 오탐으로 그 경계를 만든다.
|
|
424
|
-
- 장점, 단점 및 영향: 장점은 silent restart failure가 update-job 상태로 드러난다는 점이다. 단점은 성공 판정이 최대 30초 늦어질 수 있다는 점이며, 대신 실제 복귀를 더 정확히 반영한다.
|
|
425
|
-
*/
|
|
433
|
+
): Promise<AwaitHealthyResult> {
|
|
426
434
|
const probe = io.probeProxy ?? (async (port: number, hostname?: string) => (
|
|
427
435
|
!!(await proxyIdentityAt(port, { hostname }))
|
|
428
436
|
));
|
|
@@ -440,25 +448,50 @@ async function confirmRestartedProxy(
|
|
|
440
448
|
const stableUntil = now() + RESTART_STABILITY_WINDOW_MS;
|
|
441
449
|
while (now() < stableUntil) {
|
|
442
450
|
if (!(await probe(port, hostname))) {
|
|
443
|
-
updateJob(job, {
|
|
444
|
-
|
|
445
|
-
restarted: false,
|
|
446
|
-
error: `proxy restart became unhealthy on ${hostname}:${port}`,
|
|
447
|
-
}, restartFailureHint(port));
|
|
448
|
-
return false;
|
|
451
|
+
updateJob(job, {}, `Proxy became unhealthy on ${hostname}:${port} during the stability window.`);
|
|
452
|
+
return { ok: false, reason: "flapped" };
|
|
449
453
|
}
|
|
450
454
|
await sleep(500);
|
|
451
455
|
}
|
|
452
456
|
updateJob(job, {}, `Proxy stayed healthy for ${Math.trunc(RESTART_STABILITY_WINDOW_MS / 1000)}s after restart.`);
|
|
453
|
-
return true;
|
|
457
|
+
return { ok: true };
|
|
454
458
|
}
|
|
455
459
|
await sleep(250);
|
|
456
460
|
}
|
|
457
461
|
|
|
462
|
+
return { ok: false, reason: "timeout" };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Confirm that the detached/service restart really came back and stayed up. The GUI worker
|
|
467
|
+
* used to mark success immediately after spawning the new process, which hid Windows cases
|
|
468
|
+
* where npm left the bundled Bun runtime half-updated and the restarted proxy died seconds
|
|
469
|
+
* later. A healthy /healthz must appear, then remain healthy for one short stability window.
|
|
470
|
+
*/
|
|
471
|
+
async function confirmRestartedProxy(
|
|
472
|
+
job: UpdateJobState,
|
|
473
|
+
captured: { port: number; hostname: string },
|
|
474
|
+
io: RestartIo = {},
|
|
475
|
+
): Promise<boolean> {
|
|
476
|
+
/* [Decision Log]
|
|
477
|
+
- 목적과 의도: GUI update job이 detached restart 요청만 보고 성공 처리하지 않도록, 실제 프록시 복귀 여부를 확인한다.
|
|
478
|
+
- 기존 구현 및 제약 조건: update-job.json은 spawn/service reinstall 직후 `succeeded`로 끝났고, Windows npm/Bun 교체 실패처럼 몇 초 후 죽는 재시작을 잡지 못했다.
|
|
479
|
+
- 검토한 주요 대안: (1) 포트 점유만 확인 — 외부 프로세스/죽기 직전 프로세스를 성공으로 오인할 수 있다. (2) 무기한 /healthz 폴링 — UX가 느려지고 worker 종료 시점이 불명확하다. (3) 짧은 healthy 등장 + 안정성 창 확인 — 실제 복귀를 확인하면서도 대기 시간을 제한할 수 있다.
|
|
480
|
+
- 선택한 방식: identity-aware /healthz probe가 일정 시간 안에 나타나고, 추가 안정성 창 동안 유지되는지 확인한다.
|
|
481
|
+
- 다른 대안 대신 이 방식을 선택한 이유: GUI는 "업데이트가 설치됐지만 재시작은 실패"를 분리해 알려줘야 하며, 이 방식이 가장 적은 오탐으로 그 경계를 만든다.
|
|
482
|
+
- 장점, 단점 및 영향: 장점은 silent restart failure가 update-job 상태로 드러난다는 점이다. 단점은 성공 판정이 최대 30초 늦어질 수 있다는 점이며, 대신 실제 복귀를 더 정확히 반영한다.
|
|
483
|
+
*/
|
|
484
|
+
const result = await awaitRestartedProxyHealthy(job, captured, io);
|
|
485
|
+
if (result.ok) return true;
|
|
486
|
+
const port = captured.port;
|
|
487
|
+
const hostname = captured.hostname;
|
|
488
|
+
const error = result.reason === "flapped"
|
|
489
|
+
? `proxy restart became unhealthy on ${hostname}:${port}`
|
|
490
|
+
: `proxy restart never became healthy on ${hostname}:${port}`;
|
|
458
491
|
updateJob(job, {
|
|
459
492
|
status: "failed",
|
|
460
493
|
restarted: false,
|
|
461
|
-
error
|
|
494
|
+
error,
|
|
462
495
|
}, restartFailureHint(port));
|
|
463
496
|
return false;
|
|
464
497
|
}
|
|
@@ -471,6 +504,178 @@ export function confirmRestartAfterUpdateForTests(
|
|
|
471
504
|
return confirmRestartedProxy(job, captured, io);
|
|
472
505
|
}
|
|
473
506
|
|
|
507
|
+
async function defaultProbeProxyIdentity(
|
|
508
|
+
port: number,
|
|
509
|
+
hostname?: string,
|
|
510
|
+
): Promise<RestartProxyIdentity | null> {
|
|
511
|
+
try {
|
|
512
|
+
const res = await fetch(`http://${probeHostname(hostname)}:${port}/healthz`, {
|
|
513
|
+
signal: AbortSignal.timeout(750),
|
|
514
|
+
});
|
|
515
|
+
if (!res.ok) return null;
|
|
516
|
+
const body = (await res.json().catch(() => null)) as HealthzIdentity | null;
|
|
517
|
+
if (!isOpencodexHealthz(body)) return null;
|
|
518
|
+
return {
|
|
519
|
+
pid: typeof body?.pid === "number" ? body.pid : null,
|
|
520
|
+
...(typeof body?.version === "string" ? { version: body.version } : {}),
|
|
521
|
+
};
|
|
522
|
+
} catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Health alone is not enough to skip the GUI worker restart: a surviving pre-update
|
|
529
|
+
* process is still identity-healthy. Require update-correlated evidence — a new PID
|
|
530
|
+
* when the pre-update PID was captured, and/or /healthz reporting the job's target
|
|
531
|
+
* version when PID evidence is unavailable.
|
|
532
|
+
*/
|
|
533
|
+
export function npmSelfUpdateRestartEvidence(
|
|
534
|
+
job: Pick<UpdateJobState, "latestVersion">,
|
|
535
|
+
captured: { oldPid?: number },
|
|
536
|
+
identity: RestartProxyIdentity | null,
|
|
537
|
+
): { ok: true; detail: string } | { ok: false; reason: string } {
|
|
538
|
+
if (!identity) return { ok: false, reason: "could not read proxy identity" };
|
|
539
|
+
|
|
540
|
+
const oldPid = typeof captured.oldPid === "number" && captured.oldPid > 0
|
|
541
|
+
? captured.oldPid
|
|
542
|
+
: undefined;
|
|
543
|
+
const livePid = typeof identity.pid === "number" && identity.pid > 0 ? identity.pid : null;
|
|
544
|
+
const expected = typeof job.latestVersion === "string" && job.latestVersion.length > 0
|
|
545
|
+
? job.latestVersion
|
|
546
|
+
: null;
|
|
547
|
+
const versionMatches = expected !== null && identity.version === expected;
|
|
548
|
+
|
|
549
|
+
if (oldPid !== undefined) {
|
|
550
|
+
if (livePid === oldPid) {
|
|
551
|
+
return { ok: false, reason: "still the pre-update PID" };
|
|
552
|
+
}
|
|
553
|
+
if (livePid !== null) {
|
|
554
|
+
if (expected !== null && identity.version && identity.version !== expected) {
|
|
555
|
+
return { ok: false, reason: `new pid but version ${identity.version} !== expected ${expected}` };
|
|
556
|
+
}
|
|
557
|
+
return { ok: true, detail: `pid changed ${oldPid}→${livePid}` };
|
|
558
|
+
}
|
|
559
|
+
// Pre-update PID known but healthz omitted pid — only accept matching target version.
|
|
560
|
+
if (versionMatches) return { ok: true, detail: `version ${identity.version}` };
|
|
561
|
+
return { ok: false, reason: "no PID in healthz and version did not match the update target" };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (versionMatches) return { ok: true, detail: `version ${identity.version}` };
|
|
565
|
+
if (expected !== null && identity.version && identity.version !== expected) {
|
|
566
|
+
return { ok: false, reason: `version ${identity.version} !== expected ${expected}` };
|
|
567
|
+
}
|
|
568
|
+
return { ok: false, reason: "no pre-update PID capture and no expected-version match" };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Post-install restart for the GUI worker.
|
|
573
|
+
*
|
|
574
|
+
* npm installs run `node ocx.mjs update`, which already stops the proxy and reinstalls /
|
|
575
|
+
* starts the service (or falls back to a direct start). A second `service install` here
|
|
576
|
+
* calls `stopWindows()` on that healthy listener, then often fails elevation from the
|
|
577
|
+
* non-interactive worker — leaving the captured port (default 10100) dead until a manual
|
|
578
|
+
* restart. Prefer confirming the npm self-update's own restart first; only re-run restart
|
|
579
|
+
* when that probe fails. Bun/source installs still always take the explicit restart path.
|
|
580
|
+
*
|
|
581
|
+
* Probe-first applies only to service-managed npm installs: without a service, `ocx.mjs`
|
|
582
|
+
* only prints `ocx start` and never brings the proxy back, so waiting would always burn
|
|
583
|
+
* the full health timeout. Skipping also requires update-correlated evidence (PID change
|
|
584
|
+
* and/or target version) so a surviving pre-update process cannot look like success.
|
|
585
|
+
* After an explicit npm restart the same evidence is required again — health alone is
|
|
586
|
+
* not enough when a no-op restart or failed port reclaim leaves the old proxy up.
|
|
587
|
+
*/
|
|
588
|
+
export async function finishGuiUpdateRestart(
|
|
589
|
+
job: UpdateJobState,
|
|
590
|
+
captured: { port: number; hostname: string; oldPid?: number },
|
|
591
|
+
installer: Installer,
|
|
592
|
+
io: RestartIo = {},
|
|
593
|
+
): Promise<boolean> {
|
|
594
|
+
if (installer === "npm") {
|
|
595
|
+
const serviceInstalled = (io.serviceInstalledFn ?? isServiceInstalled)();
|
|
596
|
+
if (serviceInstalled) {
|
|
597
|
+
const already = await awaitRestartedProxyHealthy(job, captured, io);
|
|
598
|
+
if (already.ok) {
|
|
599
|
+
const identity = await (io.probeProxyIdentity ?? defaultProbeProxyIdentity)(
|
|
600
|
+
captured.port,
|
|
601
|
+
captured.hostname,
|
|
602
|
+
);
|
|
603
|
+
const evidence = npmSelfUpdateRestartEvidence(job, captured, identity);
|
|
604
|
+
if (evidence.ok) {
|
|
605
|
+
updateJob(
|
|
606
|
+
job,
|
|
607
|
+
{},
|
|
608
|
+
`Proxy already healthy on ${captured.hostname}:${captured.port} after npm self-update (${evidence.detail}); skipping redundant restart.`,
|
|
609
|
+
);
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
updateJob(
|
|
613
|
+
job,
|
|
614
|
+
{},
|
|
615
|
+
`npm self-update left a healthy proxy but ${evidence.reason}; performing explicit restart...`,
|
|
616
|
+
);
|
|
617
|
+
} else {
|
|
618
|
+
updateJob(job, {}, "npm self-update did not leave a healthy proxy; performing explicit restart...");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const restartFn = io.restartAfterUpdateFn ?? restartAfterUpdate;
|
|
623
|
+
await restartFn(job, captured, io);
|
|
624
|
+
if (installer !== "npm") {
|
|
625
|
+
// Bun/source: health alone remains enough unless a richer identity probe is supplied.
|
|
626
|
+
if (!io.probeProxyIdentity) return confirmRestartedProxy(job, captured, io);
|
|
627
|
+
}
|
|
628
|
+
return confirmNpmExplicitRestart(job, captured, io);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* After an explicit npm (or identity-aware) restart, require update-correlated
|
|
633
|
+
* evidence — not merely a healthy OpenCodex listener. A no-op restart or a
|
|
634
|
+
* failed port reclaim can leave the pre-update process on the captured port;
|
|
635
|
+
* `confirmRestartedProxy` alone would treat that as success.
|
|
636
|
+
*/
|
|
637
|
+
async function confirmNpmExplicitRestart(
|
|
638
|
+
job: UpdateJobState,
|
|
639
|
+
captured: { port: number; hostname: string; oldPid?: number },
|
|
640
|
+
io: RestartIo = {},
|
|
641
|
+
): Promise<boolean> {
|
|
642
|
+
const healthy = await awaitRestartedProxyHealthy(job, captured, io);
|
|
643
|
+
if (!healthy.ok) {
|
|
644
|
+
const port = captured.port;
|
|
645
|
+
const hostname = captured.hostname;
|
|
646
|
+
const error = healthy.reason === "flapped"
|
|
647
|
+
? `proxy restart became unhealthy on ${hostname}:${port}`
|
|
648
|
+
: `proxy restart never became healthy on ${hostname}:${port}`;
|
|
649
|
+
updateJob(job, {
|
|
650
|
+
status: "failed",
|
|
651
|
+
restarted: false,
|
|
652
|
+
error,
|
|
653
|
+
}, restartFailureHint(port));
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const identity = await (io.probeProxyIdentity ?? defaultProbeProxyIdentity)(
|
|
658
|
+
captured.port,
|
|
659
|
+
captured.hostname,
|
|
660
|
+
);
|
|
661
|
+
const evidence = npmSelfUpdateRestartEvidence(job, captured, identity);
|
|
662
|
+
if (!evidence.ok) {
|
|
663
|
+
updateJob(job, {
|
|
664
|
+
status: "failed",
|
|
665
|
+
restarted: false,
|
|
666
|
+
error: `proxy restart did not show update-correlated identity (${evidence.reason})`,
|
|
667
|
+
}, restartFailureHint(captured.port));
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
updateJob(
|
|
672
|
+
job,
|
|
673
|
+
{},
|
|
674
|
+
`Proxy restart confirmed on ${captured.hostname}:${captured.port} (${evidence.detail}).`,
|
|
675
|
+
);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
|
|
474
679
|
export async function runGuiUpdateWorker(jobId: string, channel: Channel, restart: boolean): Promise<void> {
|
|
475
680
|
let job = readUpdateJob(jobId);
|
|
476
681
|
const check = checkForUpdate(channel);
|
|
@@ -590,8 +795,7 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
|
|
|
590
795
|
|
|
591
796
|
if (restart) {
|
|
592
797
|
job = updateJob(job, { status: "restarting" }, "Update installed. Restarting proxy...");
|
|
593
|
-
await
|
|
594
|
-
if (!(await confirmRestartedProxy(job, captured))) return;
|
|
798
|
+
if (!(await finishGuiUpdateRestart(job, captured, check.installer))) return;
|
|
595
799
|
updateJob(job, { status: "succeeded", restarted: true }, "Restart requested and proxy is healthy.");
|
|
596
800
|
return;
|
|
597
801
|
}
|
package/src/usage/log.ts
CHANGED
|
@@ -29,6 +29,11 @@ export interface PersistedUsageAttempt {
|
|
|
29
29
|
usage?: OcxUsage;
|
|
30
30
|
totalTokens?: number;
|
|
31
31
|
errorCode?: string;
|
|
32
|
+
/** Target-specific reasoning intent and exact adapter-normalized wire parameter. */
|
|
33
|
+
requestedEffort?: string;
|
|
34
|
+
effectiveEffort?: string;
|
|
35
|
+
reasoningWireField?: string;
|
|
36
|
+
reasoningWireValue?: string | number;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export interface PersistedUsageEntry {
|
|
@@ -37,10 +42,16 @@ export interface PersistedUsageEntry {
|
|
|
37
42
|
provider: string;
|
|
38
43
|
model: string;
|
|
39
44
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
45
|
+
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
46
|
+
conversationId?: string;
|
|
40
47
|
resolvedModel?: string;
|
|
41
48
|
requestedModel?: string;
|
|
42
49
|
/** Reasoning effort / service-tier metadata for GUI Logs after restart. */
|
|
43
50
|
requestedEffort?: string;
|
|
51
|
+
/** Adapter-normalized tier and exact upstream parameter emitted for this request. */
|
|
52
|
+
effectiveEffort?: string;
|
|
53
|
+
reasoningWireField?: string;
|
|
54
|
+
reasoningWireValue?: string | number;
|
|
44
55
|
requestedServiceTier?: string;
|
|
45
56
|
requestedSpeedLabel?: string;
|
|
46
57
|
configuredServiceTier?: string;
|
|
@@ -213,6 +224,20 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
213
224
|
? { totalTokens: attempt.totalTokens }
|
|
214
225
|
: {}),
|
|
215
226
|
...(typeof attempt.errorCode === "string" ? { errorCode: attempt.errorCode } : {}),
|
|
227
|
+
...(typeof attempt.requestedEffort === "string" && attempt.requestedEffort
|
|
228
|
+
? { requestedEffort: capMetadataString(attempt.requestedEffort) }
|
|
229
|
+
: {}),
|
|
230
|
+
...(typeof attempt.effectiveEffort === "string" && attempt.effectiveEffort
|
|
231
|
+
? { effectiveEffort: capMetadataString(attempt.effectiveEffort) }
|
|
232
|
+
: {}),
|
|
233
|
+
...(typeof attempt.reasoningWireField === "string" && attempt.reasoningWireField
|
|
234
|
+
? { reasoningWireField: capMetadataString(attempt.reasoningWireField) }
|
|
235
|
+
: {}),
|
|
236
|
+
...(typeof attempt.reasoningWireValue === "string" && attempt.reasoningWireValue
|
|
237
|
+
? { reasoningWireValue: capMetadataString(attempt.reasoningWireValue) }
|
|
238
|
+
: isNonNegativeFiniteNumber(attempt.reasoningWireValue)
|
|
239
|
+
? { reasoningWireValue: attempt.reasoningWireValue }
|
|
240
|
+
: {}),
|
|
216
241
|
};
|
|
217
242
|
}
|
|
218
243
|
|
|
@@ -235,11 +260,25 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
235
260
|
provider: entry.provider,
|
|
236
261
|
model: entry.model,
|
|
237
262
|
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
263
|
+
...(typeof entry.conversationId === "string" && entry.conversationId.trim()
|
|
264
|
+
? { conversationId: entry.conversationId.trim().slice(0, 128) }
|
|
265
|
+
: {}),
|
|
238
266
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
239
267
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
240
268
|
...(typeof entry.requestedEffort === "string" && entry.requestedEffort
|
|
241
269
|
? { requestedEffort: capMetadataString(entry.requestedEffort) }
|
|
242
270
|
: {}),
|
|
271
|
+
...(typeof entry.effectiveEffort === "string" && entry.effectiveEffort
|
|
272
|
+
? { effectiveEffort: capMetadataString(entry.effectiveEffort) }
|
|
273
|
+
: {}),
|
|
274
|
+
...(typeof entry.reasoningWireField === "string" && entry.reasoningWireField
|
|
275
|
+
? { reasoningWireField: capMetadataString(entry.reasoningWireField) }
|
|
276
|
+
: {}),
|
|
277
|
+
...(typeof entry.reasoningWireValue === "string" && entry.reasoningWireValue
|
|
278
|
+
? { reasoningWireValue: capMetadataString(entry.reasoningWireValue) }
|
|
279
|
+
: isNonNegativeFiniteNumber(entry.reasoningWireValue)
|
|
280
|
+
? { reasoningWireValue: entry.reasoningWireValue }
|
|
281
|
+
: {}),
|
|
243
282
|
...(typeof entry.requestedServiceTier === "string" && entry.requestedServiceTier
|
|
244
283
|
? { requestedServiceTier: capMetadataString(entry.requestedServiceTier) }
|
|
245
284
|
: {}),
|
package/src/web-search/loop.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderAdapter } from "../adapters/base";
|
|
1
|
+
import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
|
|
2
2
|
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage } from "../types";
|
|
3
3
|
import { namespacedToolName } from "../types";
|
|
4
4
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
@@ -191,6 +191,8 @@ export interface WebSearchLoopDeps {
|
|
|
191
191
|
onFirstOutput?: () => void;
|
|
192
192
|
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
193
193
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
194
|
+
/** Observe the exact adapter request selected for each routed-model iteration. */
|
|
195
|
+
onRequestBuilt?: (request: AdapterRequest) => void;
|
|
194
196
|
/**
|
|
195
197
|
* 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
|
|
196
198
|
* or null when the pool is exhausted (same semantics as the normal routed path).
|
|
@@ -271,6 +273,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
271
273
|
headers: selectedForwardHeaders,
|
|
272
274
|
abortSignal: headerDeadline.signal,
|
|
273
275
|
});
|
|
276
|
+
try {
|
|
277
|
+
deps.onRequestBuilt?.(request);
|
|
278
|
+
} catch {
|
|
279
|
+
// Diagnostics are best-effort and must never abort a web-search iteration.
|
|
280
|
+
}
|
|
274
281
|
const response = requestAdapter.fetchResponse
|
|
275
282
|
? await requestAdapter.fetchResponse(request, {
|
|
276
283
|
abortSignal: headerDeadline.signal,
|