@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.
Files changed (80) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
  3. package/gui/dist/assets/index-DfVGuN88.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/base.ts +6 -0
  7. package/src/adapters/kiro-constants.ts +6 -2
  8. package/src/adapters/kiro-retry.ts +175 -10
  9. package/src/adapters/kiro.ts +172 -85
  10. package/src/adapters/mimo-free.ts +1 -0
  11. package/src/adapters/openai-chat.ts +30 -4
  12. package/src/adapters/openai-responses.ts +90 -12
  13. package/src/bridge.ts +91 -43
  14. package/src/claude/desktop-3p-paths.ts +84 -0
  15. package/src/claude/desktop-3p.ts +29 -2
  16. package/src/cli/access.ts +108 -0
  17. package/src/cli/account-auth.ts +223 -0
  18. package/src/cli/account.ts +9 -1
  19. package/src/cli/agent.ts +184 -0
  20. package/src/cli/combo.ts +119 -0
  21. package/src/cli/config-command.ts +145 -0
  22. package/src/cli/debug.ts +20 -8
  23. package/src/cli/doctor.ts +45 -8
  24. package/src/cli/help.ts +65 -13
  25. package/src/cli/index.ts +108 -7
  26. package/src/cli/integrations.ts +142 -0
  27. package/src/cli/models-runtime.ts +212 -0
  28. package/src/cli/models.ts +9 -10
  29. package/src/cli/observe.ts +117 -0
  30. package/src/cli/provider-runtime.ts +152 -0
  31. package/src/cli/provider.ts +23 -1
  32. package/src/cli/runtime-api.ts +325 -0
  33. package/src/cli/star-prompt.ts +3 -3
  34. package/src/cli/status.ts +17 -0
  35. package/src/cli/system-command.ts +112 -0
  36. package/src/codex/auth-api.ts +3 -2
  37. package/src/codex/catalog/aggregation.ts +113 -18
  38. package/src/codex/catalog/provider-fetch.ts +24 -13
  39. package/src/codex/catalog/sync.ts +20 -8
  40. package/src/codex/catalog.ts +2 -1
  41. package/src/codex/refresh.ts +10 -3
  42. package/src/codex/routing.ts +21 -32
  43. package/src/codex/sync.ts +17 -0
  44. package/src/config.ts +48 -0
  45. package/src/generated/jawcode-model-metadata.ts +2 -1
  46. package/src/grok/inject.ts +184 -4
  47. package/src/grok/status.ts +33 -0
  48. package/src/lib/retry-after.ts +55 -0
  49. package/src/lib/windows-elevation.ts +627 -0
  50. package/src/providers/openai-sidecar.ts +46 -2
  51. package/src/providers/registry.ts +52 -0
  52. package/src/server/auth-cors.ts +6 -0
  53. package/src/server/chat-completions.ts +6 -1
  54. package/src/server/claude-messages.ts +20 -1
  55. package/src/server/images.ts +14 -7
  56. package/src/server/management/agent-settings-routes.ts +10 -4
  57. package/src/server/management/combo-routes.ts +0 -1
  58. package/src/server/management/config-routes.ts +0 -1
  59. package/src/server/management/logs-usage-routes.ts +94 -0
  60. package/src/server/management/model-routes.ts +0 -1
  61. package/src/server/management/oauth-account-routes.ts +0 -1
  62. package/src/server/management/provider-routes.ts +0 -1
  63. package/src/server/management/shared.ts +0 -1
  64. package/src/server/management/system-routes.ts +27 -15
  65. package/src/server/management-api.ts +0 -1
  66. package/src/server/memory-watchdog.ts +54 -10
  67. package/src/server/request-log-conversation.ts +168 -0
  68. package/src/server/request-log.ts +122 -2
  69. package/src/server/responses/core.ts +76 -13
  70. package/src/server/responses/passthrough-error.ts +38 -13
  71. package/src/server/startup-action-control.ts +266 -15
  72. package/src/service.ts +512 -3
  73. package/src/storage/cleanup.ts +1538 -0
  74. package/src/storage/scanner.ts +4 -1
  75. package/src/types.ts +16 -0
  76. package/src/update/job.ts +229 -25
  77. package/src/usage/log.ts +39 -0
  78. package/src/web-search/loop.ts +8 -1
  79. package/gui/dist/assets/index-B2J4t3te.css +0 -1
  80. package/gui/dist/assets/index-BmvM6wRb.js +0 -65
@@ -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 short-timeout readonly opens that degrade to
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
- * Confirm that the detached/service restart really came back and stayed up. The GUI worker
409
- * used to mark success immediately after spawning the new process, which hid Windows cases
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 confirmRestartedProxy(
429
+ async function awaitRestartedProxyHealthy(
414
430
  job: UpdateJobState,
415
431
  captured: { port: number; hostname: string },
416
432
  io: RestartIo = {},
417
- ): Promise<boolean> {
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
- status: "failed",
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: `proxy restart never became healthy on ${hostname}:${port}`,
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 restartAfterUpdate(job, captured);
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
  : {}),
@@ -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,