@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925

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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -15,6 +15,10 @@ import {
15
15
  } from "../../usage/summary";
16
16
  import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
17
17
  import type { UsageTimeWindow } from "../../usage/time-range";
18
+ import {
19
+ createJevStatsAccumulator,
20
+ type JevStatsAccumulator,
21
+ } from "../../usage/jev-stats";
18
22
 
19
23
  import {
20
24
  cacheApiKeyUsageFromRollup,
@@ -34,6 +38,19 @@ interface RetainedUsageAggregate {
34
38
  retainedAt: number;
35
39
  }
36
40
 
41
+ interface RetainedJevStatsAggregate {
42
+ accumulator: JevStatsAccumulator;
43
+ usageIncomplete: boolean;
44
+ revision: UsageLogRevision | null;
45
+ identityKey: string;
46
+ revisionKey: string;
47
+ processedThroughBytes: number;
48
+ processedThroughDigest: string;
49
+ retainedAt: number;
50
+ }
51
+
52
+ type RetainedAggregateState = RetainedUsageAggregate | RetainedJevStatsAggregate;
53
+
37
54
  export interface UsageAggregateResult {
38
55
  accumulator: UsageSummaryAccumulator;
39
56
  usageIncomplete: boolean;
@@ -50,6 +67,14 @@ export interface UsageAggregateOptions {
50
67
  managementUsageMaxReadBytes?: number;
51
68
  }
52
69
 
70
+ export interface JevStatsAggregateResult {
71
+ accumulator: JevStatsAccumulator;
72
+ usageIncomplete: boolean;
73
+ revision: UsageLogRevision | null;
74
+ processedThroughBytes: number;
75
+ update: "unchanged" | "append" | "rebuild";
76
+ }
77
+
53
78
  export interface UsageAggregateRetainedStats {
54
79
  count: number;
55
80
  bytes: number;
@@ -62,11 +87,15 @@ const MAX_REBUILD_ATTEMPTS = 2;
62
87
  const MAX_RETAINED_FILTERED_AGGREGATES = 4;
63
88
 
64
89
  let retainedAggregate: RetainedUsageAggregate | null = null;
65
- const pinnedAggregates = new Set<RetainedUsageAggregate>();
90
+ const pinnedAggregates = new Set<RetainedAggregateState>();
66
91
  let baseFlight: Promise<UsageAggregateResult> | null = null;
67
92
  const filteredFlights = new Map<string, Promise<UsageAggregateResult>>();
68
93
  const retainedFilteredAggregates = new Map<string, RetainedUsageAggregate>();
69
94
  const MAX_CONCURRENT_FILTERED_AGGREGATES = 4;
95
+ const retainedJevStatsAggregates = new Map<string, RetainedJevStatsAggregate>();
96
+ const jevStatsFlights = new Map<string, Promise<JevStatsAggregateResult>>();
97
+ const MAX_RETAINED_JEV_STATS_AGGREGATES = 4;
98
+ const MAX_CONCURRENT_JEV_STATS_AGGREGATES = 4;
70
99
 
71
100
  function currentTimeZone(): string {
72
101
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -418,10 +447,171 @@ async function refreshFilteredAggregate(
418
447
  return appendFilteredAggregate(key, state, filter, window);
419
448
  }
420
449
 
450
+ function jevStatsResultFrom(
451
+ state: RetainedJevStatsAggregate,
452
+ update: JevStatsAggregateResult["update"],
453
+ ): JevStatsAggregateResult {
454
+ return {
455
+ accumulator: state.accumulator,
456
+ usageIncomplete: state.usageIncomplete,
457
+ revision: state.revision,
458
+ processedThroughBytes: state.processedThroughBytes,
459
+ update,
460
+ };
461
+ }
462
+
463
+ function trimRetainedJevStatsAggregates(): void {
464
+ while (retainedJevStatsAggregates.size > MAX_RETAINED_JEV_STATS_AGGREGATES) {
465
+ const oldest = [...retainedJevStatsAggregates]
466
+ .filter(([, state]) => !pinnedAggregates.has(state))
467
+ .sort(([, left], [, right]) => left.retainedAt - right.retainedAt)[0];
468
+ if (!oldest) return;
469
+ retainedJevStatsAggregates.delete(oldest[0]);
470
+ }
471
+ }
472
+
473
+ function publishJevStatsAggregate(
474
+ key: string,
475
+ state: RetainedJevStatsAggregate,
476
+ update: JevStatsAggregateResult["update"],
477
+ ): JevStatsAggregateResult {
478
+ retainedJevStatsAggregates.set(key, state);
479
+ trimRetainedJevStatsAggregates();
480
+ enforceAppOwnedMemoryBudget();
481
+ return jevStatsResultFrom(state, update);
482
+ }
483
+
484
+ function retainedJevStatsState(
485
+ accumulator: JevStatsAccumulator,
486
+ scan: Awaited<ReturnType<typeof scanUsageLedgerCooperatively>>,
487
+ priorIncomplete = false,
488
+ ): RetainedJevStatsAggregate {
489
+ return {
490
+ accumulator,
491
+ usageIncomplete: priorIncomplete || scan.oversizedRows > 0,
492
+ revision: scan.revision,
493
+ identityKey: usageLogIdentityKey(scan.revision),
494
+ revisionKey: usageLogRevisionKey(scan.revision),
495
+ processedThroughBytes: scan.processedThroughBytes,
496
+ processedThroughDigest: scan.processedThroughDigest,
497
+ retainedAt: Date.now(),
498
+ };
499
+ }
500
+
501
+ async function rebuildJevStatsAggregate(
502
+ key: string,
503
+ comboId: string | null,
504
+ since: number | null,
505
+ ): Promise<JevStatsAggregateResult> {
506
+ let lastError: unknown;
507
+ for (let attempt = 0; attempt < MAX_REBUILD_ATTEMPTS; attempt += 1) {
508
+ // A rebuild-required result means the opened ledger changed underneath the
509
+ // scan. Start from a fresh accumulator so no partially observed row can be
510
+ // returned or retained.
511
+ const accumulator = createJevStatsAccumulator({ comboId, since });
512
+ try {
513
+ const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
514
+ return publishJevStatsAggregate(key, retainedJevStatsState(accumulator, scan), "rebuild");
515
+ } catch (error) {
516
+ lastError = error;
517
+ if (!(error instanceof UsageLedgerRebuildRequiredError) || attempt + 1 >= MAX_REBUILD_ATTEMPTS) {
518
+ throw error;
519
+ }
520
+ }
521
+ }
522
+ throw lastError ?? new Error("JEV stats aggregate rebuild did not settle");
523
+ }
524
+
525
+ function jevStatsRequiresRebuild(
526
+ state: RetainedJevStatsAggregate,
527
+ observed: UsageLogRevision | null,
528
+ ): boolean {
529
+ if (state.identityKey !== usageLogIdentityKey(observed)) return true;
530
+ if (!state.revision || !observed) return state.revision !== observed;
531
+ if (observed.size < state.revision.size) return true;
532
+ return observed.size === state.revision.size && usageLogRevisionKey(observed) !== state.revisionKey;
533
+ }
534
+
535
+ async function appendJevStatsAggregate(
536
+ key: string,
537
+ state: RetainedJevStatsAggregate,
538
+ comboId: string | null,
539
+ since: number | null,
540
+ ): Promise<JevStatsAggregateResult> {
541
+ pinnedAggregates.add(state);
542
+ let rebuildAfterUnpin = false;
543
+ try {
544
+ const candidate = state.accumulator.clone();
545
+ const scan = await scanUsageLedgerCooperatively({
546
+ startAtBytes: state.processedThroughBytes,
547
+ expectedIdentityKey: state.identityKey,
548
+ expectedProcessedThroughDigest: state.processedThroughDigest,
549
+ onEntry: entry => candidate.add(entry),
550
+ });
551
+ const next = retainedJevStatsState(candidate, scan, state.usageIncomplete);
552
+ return publishJevStatsAggregate(key, next, "append");
553
+ } catch (error) {
554
+ if (retainedJevStatsAggregates.get(key) === state) retainedJevStatsAggregates.delete(key);
555
+ if (error instanceof UsageLedgerRebuildRequiredError) rebuildAfterUnpin = true;
556
+ else throw error;
557
+ } finally {
558
+ pinnedAggregates.delete(state);
559
+ trimRetainedJevStatsAggregates();
560
+ }
561
+ if (rebuildAfterUnpin) return rebuildJevStatsAggregate(key, comboId, since);
562
+ throw new Error("JEV stats aggregate append did not settle");
563
+ }
564
+
565
+ async function refreshJevStatsAggregate(
566
+ key: string,
567
+ comboId: string | null,
568
+ since: number | null,
569
+ ): Promise<JevStatsAggregateResult> {
570
+ const state = retainedJevStatsAggregates.get(key);
571
+ if (!state) return rebuildJevStatsAggregate(key, comboId, since);
572
+ const observed = currentUsageLogRevision();
573
+ if (jevStatsRequiresRebuild(state, observed)) {
574
+ retainedJevStatsAggregates.delete(key);
575
+ return rebuildJevStatsAggregate(key, comboId, since);
576
+ }
577
+ if (state.revisionKey === usageLogRevisionKey(observed)) {
578
+ state.retainedAt = Date.now();
579
+ return jevStatsResultFrom(state, "unchanged");
580
+ }
581
+ return appendJevStatsAggregate(key, state, comboId, since);
582
+ }
583
+
584
+ /**
585
+ * Return one checkpointed JEV projection for a combo and fixed calendar range.
586
+ * Concurrent readers share a flight; later polls scan only a verified append
587
+ * suffix, and unchanged ledgers perform no file read.
588
+ */
589
+ export async function getJevStatsAggregate(options: {
590
+ comboId?: string | null;
591
+ since?: number | null;
592
+ } = {}): Promise<JevStatsAggregateResult> {
593
+ const comboId = options.comboId ?? null;
594
+ const since = options.since ?? null;
595
+ const key = JSON.stringify([comboId, since]);
596
+ const existing = jevStatsFlights.get(key);
597
+ if (existing) return existing;
598
+ if (jevStatsFlights.size >= MAX_CONCURRENT_JEV_STATS_AGGREGATES) {
599
+ throw new Error("too many concurrent JEV stats aggregates");
600
+ }
601
+ const flight = refreshJevStatsAggregate(key, comboId, since);
602
+ jevStatsFlights.set(key, flight);
603
+ try {
604
+ return await flight;
605
+ } finally {
606
+ if (jevStatsFlights.get(key) === flight) jevStatsFlights.delete(key);
607
+ }
608
+ }
609
+
421
610
  export function usageAggregateRetainedStats(): UsageAggregateRetainedStats {
422
611
  const states = [
423
612
  ...(retainedAggregate ? [retainedAggregate] : []),
424
613
  ...retainedFilteredAggregates.values(),
614
+ ...retainedJevStatsAggregates.values(),
425
615
  ];
426
616
  if (states.length === 0) {
427
617
  return { count: 0, bytes: 0, evictableBytes: 0, pinnedBytes: 0, oldestAt: null };
@@ -449,19 +639,27 @@ export function usageAggregateRetainedStats(): UsageAggregateRetainedStats {
449
639
  }
450
640
 
451
641
  export function discardRetainedUsageAggregate(): number {
452
- const candidates: Array<{ key: string | null; state: RetainedUsageAggregate }> = [
642
+ const candidates: Array<{
643
+ kind: "base" | "filtered" | "jev";
644
+ key: string | null;
645
+ state: RetainedAggregateState;
646
+ }> = [
453
647
  ...(retainedAggregate && !pinnedAggregates.has(retainedAggregate)
454
- ? [{ key: null, state: retainedAggregate }]
648
+ ? [{ kind: "base" as const, key: null, state: retainedAggregate }]
455
649
  : []),
456
650
  ...[...retainedFilteredAggregates]
457
651
  .filter(([, state]) => !pinnedAggregates.has(state))
458
- .map(([key, state]) => ({ key, state })),
652
+ .map(([key, state]) => ({ kind: "filtered" as const, key, state })),
653
+ ...[...retainedJevStatsAggregates]
654
+ .filter(([, state]) => !pinnedAggregates.has(state))
655
+ .map(([key, state]) => ({ kind: "jev" as const, key, state })),
459
656
  ];
460
657
  const oldest = candidates.sort((left, right) => left.state.retainedAt - right.state.retainedAt)[0];
461
658
  if (!oldest) return 0;
462
659
  const released = oldest.state.accumulator.estimatedBytes;
463
- if (oldest.key === null) retainedAggregate = null;
464
- else retainedFilteredAggregates.delete(oldest.key);
660
+ if (oldest.kind === "base") retainedAggregate = null;
661
+ else if (oldest.kind === "filtered") retainedFilteredAggregates.delete(oldest.key!);
662
+ else retainedJevStatsAggregates.delete(oldest.key!);
465
663
  return released;
466
664
  }
467
665
 
@@ -471,4 +669,6 @@ export function resetUsageAggregateCacheForTests(): void {
471
669
  baseFlight = null;
472
670
  filteredFlights.clear();
473
671
  retainedFilteredAggregates.clear();
672
+ jevStatsFlights.clear();
673
+ retainedJevStatsAggregates.clear();
474
674
  }
@@ -80,6 +80,7 @@ import { handleClaudeDesktopPickerRoutes } from "./management/claude-desktop-pic
80
80
  import { handleCursorIntegrationRoutes } from "./management/cursor-integration-routes";
81
81
  import type { ManagementContext } from "./management/context";
82
82
  import type { ManagementPrincipal, ManagementSessionControl } from "./management-auth";
83
+ import type { ManagementRequestIngress } from "./management/context";
83
84
  export type { ManagementApiDeps } from "./management/context";
84
85
  import { fetchAllModels } from "./management/shared";
85
86
  import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
@@ -155,6 +156,16 @@ async function handleWorkflowBudgetRoutesOnDemand(ctx: ManagementContext): Promi
155
156
  return handleWorkflowBudgetRoutes(ctx);
156
157
  }
157
158
 
159
+ /**
160
+ * Lazy like the Lab and routing-profile handlers: the protocol planner reaches the router and
161
+ * the ingress eligibility rules, which no other dashboard request needs.
162
+ */
163
+ async function handleProtocolRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
164
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/protocols")) return null;
165
+ const { handleProtocolRoutes } = await import("./management/protocol-routes");
166
+ return handleProtocolRoutes(ctx);
167
+ }
168
+
158
169
  async function handleGrokCouponRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
159
170
  if (!pathInManagementNamespace(ctx.url.pathname, "/api/grok/reset-coupons", true)) return null;
160
171
  const { handleGrokCouponRoutes } = await import("./management/grok-coupon-routes");
@@ -184,6 +195,12 @@ async function handleRemoteWorkspaceRoutesOnDemand(ctx: ManagementContext): Prom
184
195
  return handleRemoteWorkspaceRoutes(ctx);
185
196
  }
186
197
 
198
+ async function handleLinkRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
199
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/link")) return null;
200
+ const { handleLinkRoutes } = await import("./management/link-routes");
201
+ return handleLinkRoutes(ctx);
202
+ }
203
+
187
204
  export async function handleManagementAPI(
188
205
  req: Request,
189
206
  url: URL,
@@ -191,6 +208,7 @@ export async function handleManagementAPI(
191
208
  deps: ManagementApiDeps = {},
192
209
  principal?: ManagementPrincipal,
193
210
  sessionControl?: ManagementSessionControl,
211
+ requestIngress: ManagementRequestIngress = { trustedLoopback: false },
194
212
  ): Promise<Response | null> {
195
213
  if (!isAllowedManagementOrigin(req, config)) {
196
214
  return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
@@ -273,10 +291,16 @@ export async function handleManagementAPI(
273
291
  }
274
292
  } catch { /* best-effort */ }
275
293
  }
276
- const ctx: ManagementContext = { req, url, config, deps, version: VERSION, principal, sessionControl, convergeCodexCatalog, syncClaudeAgentDefsBestEffort };
294
+ const ctx: ManagementContext = {
295
+ req, url, config, deps, version: VERSION, principal, sessionControl,
296
+ trustedLoopbackIngress: requestIngress.trustedLoopback,
297
+ guiSessionIssuance: requestIngress.guiSessionIssuance ?? null,
298
+ convergeCodexCatalog, syncClaudeAgentDefsBestEffort,
299
+ };
277
300
  let routed: Response | null | undefined;
278
301
  try {
279
302
  routed = handleSessionRoutes(ctx)
303
+ ?? (await handleLinkRoutesOnDemand(ctx))
280
304
  ?? (await handleRemoteWorkspaceRoutesOnDemand(ctx))
281
305
  ?? (await handleConfigRoutes(ctx))
282
306
  ?? (await handleStorageLogGuardRoutes(ctx))
@@ -284,6 +308,7 @@ export async function handleManagementAPI(
284
308
  ?? (await handleRequestHistoryRoutes(ctx))
285
309
  ?? (await handleQuotaResetRoutesOnDemand(ctx))
286
310
  ?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
311
+ ?? (await handleProtocolRoutesOnDemand(ctx))
287
312
  ?? (await handleGrokCouponRoutesOnDemand(ctx))
288
313
  ?? (await handleAnthropicResetGrantRoutesOnDemand(ctx))
289
314
  ?? handleMetricsRoutes(ctx)
@@ -71,6 +71,7 @@ import {
71
71
  type GuiSessionRecord,
72
72
  type GuiSessionRequestContext,
73
73
  } from "./gui-session";
74
+ import { hasLocalDesktopSnapshotCapability } from "./local-desktop-snapshot-auth";
74
75
  export type { GuiSessionBootstrap, GuiSessionRequestContext } from "./gui-session";
75
76
 
76
77
  const LOCAL_READ_REPLAY_LIMIT = 256;
@@ -260,6 +261,17 @@ export function issueGuiSession(
260
261
  return issueGuiSessionFromState(req, config, state, context);
261
262
  }
262
263
 
264
+ /** Returns the recorded issuance for the session that authorized this request. */
265
+ export function managementSessionIssuance(
266
+ req: Request,
267
+ state: ManagementAuthState,
268
+ ): import("./gui-session").GuiSessionIssuance | null {
269
+ if (!state.available) return null;
270
+ const credential = requestManagementCredential(req);
271
+ if (!credential || equalSecret(credential, state.token)) return null;
272
+ return state.sessions.get(credential)?.issuance ?? null;
273
+ }
274
+
263
275
  export interface ManagementSessionControl {
264
276
  revokeCurrent(req: Request): boolean;
265
277
  /** Revalidate a long-lived request against current authority, without cached admission or renewal. */
@@ -315,8 +327,8 @@ export function createManagementSessionControl(state: ManagementAuthState): Mana
315
327
  * per-session CSRF token match. Consent-bearing routes must key off this value
316
328
  * rather than off request headers, which the token holder can forge freely.
317
329
  * The capability principals are process-scoped HMACs bound to the current process
318
- * PID and listening port. Local reads are accepted only for two exact GET paths;
319
- * restart and provider reload remain separate wire contracts for their exact POSTs.
330
+ * PID and listening port. Local reads are accepted only for allowlisted GET paths;
331
+ * snapshot, restart and provider reload use separate contracts for their exact POSTs.
320
332
  */
321
333
  export type ManagementPrincipal =
322
334
  | "admin-token"
@@ -324,6 +336,7 @@ export type ManagementPrincipal =
324
336
  | "gui-session"
325
337
  | "gui-pair-capability"
326
338
  | "local-read-capability"
339
+ | "local-desktop-snapshot-capability"
327
340
  | "local-provider-reload-capability"
328
341
  | "local-aside-sync-capability"
329
342
  | "system-restart-capability";
@@ -375,8 +388,6 @@ function hasLocalReadCapability(
375
388
  } catch {
376
389
  return false;
377
390
  }
378
- // Do not let a future query-bearing variant silently inherit this narrow grant.
379
- if (url.search !== "") return false;
380
391
  const expectedPid = parseExpectedLocalManagementPid(
381
392
  req.headers.get(LOCAL_MANAGEMENT_EXPECTED_PID_HEADER),
382
393
  );
@@ -391,7 +402,9 @@ function hasLocalReadCapability(
391
402
  local.attestationSecret,
392
403
  req.headers.get(LOCAL_MANAGEMENT_NONCE_HEADER),
393
404
  req.method,
394
- url.pathname,
405
+ // The query is signed into the capability, so a grant for one range cannot be replayed
406
+ // against another — the grant stays exactly as narrow as the request it was minted for.
407
+ url.pathname + url.search,
395
408
  local.pid,
396
409
  local.port,
397
410
  expiresAt,
@@ -550,6 +563,7 @@ function resolveManagementAdmission(
550
563
  if (hasSystemRestartCapability(req, local)) principal = "system-restart-capability";
551
564
  else if (hasLocalAsideSyncCapability(req, local)) principal = "local-aside-sync-capability";
552
565
  else if (hasLocalProviderReloadCapability(req, local)) principal = "local-provider-reload-capability";
566
+ else if (hasLocalDesktopSnapshotCapability(req, local)) principal = "local-desktop-snapshot-capability";
553
567
  else if (hasLocalReadCapability(req, local)) principal = "local-read-capability";
554
568
  else if (hasGuiPairCapability(req, local)) principal = "gui-pair-capability";
555
569
  else if (state.available) {
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Whether a settled Messages route may take the managed native Messages lane (PF-08).
3
+ *
4
+ * PURE: reads the route, the body's structure and config, and nothing else. The Messages ingress,
5
+ * `count_tokens` and the protocol planner (`src/protocols/plan-snapshot.ts`) all ask here, so a
6
+ * preview, a count and the real send cannot disagree about the lane.
7
+ */
8
+ import { anthropicBodyElidesBlockedSkill } from "../claude/inbound";
9
+ import { isClaudeWebSearchToolName } from "../claude/outbound";
10
+ import { isAnthropicAccountPoolEnabled } from "../oauth/anthropic-routing";
11
+ import type { ProtocolReasonCode } from "../protocols/contract";
12
+ import { featuresFromMessagesBody } from "../protocols/features";
13
+ import { credentialDomainFor } from "../protocols/opaque-state";
14
+ import { resolveProtocolSettings } from "../protocols/settings";
15
+ import type { RouteResult } from "../router";
16
+ import type { OcxConfig } from "../types";
17
+ import { requiresVisionPreprocessing } from "../vision";
18
+ import { resolvePinnedEffort } from "./effort-policy";
19
+
20
+ /** Why the managed native Messages lane declines a route, as a protocol reason code. */
21
+ export type NativeMessagesDeclineReason = Extract<
22
+ ProtocolReasonCode,
23
+ | "rollout-disabled"
24
+ | "cross-wire-ir"
25
+ | "auth-mode-not-native"
26
+ | "oauth-account-pool"
27
+ | "combo-or-policy-route"
28
+ | "effort-row"
29
+ | "fast-row"
30
+ | "vision-preprocessing"
31
+ | "bridge-only-policy"
32
+ >;
33
+
34
+ /** Request facts the body cannot carry. */
35
+ export interface NativeMessagesSelector {
36
+ /** A synthetic effort row was requested. */
37
+ effortRow?: boolean;
38
+ /** A synthetic fast row was requested. */
39
+ fastRow?: boolean;
40
+ /** The model id the bridge would parse (the translated body's `model`), for the effort pin. */
41
+ routeSelector?: string;
42
+ /** The Claude settings this ingress reads (intercept bindings applied); defaults to config's. */
43
+ claudeCode?: OcxConfig["claudeCode"];
44
+ /**
45
+ * Runtime fact, supplied by a caller that is about to send: two or more usable Anthropic OAuth
46
+ * accounts are stored, so the bridge would rotate accounts on a 429
47
+ * (`hasAnthropicFailoverQuorum`). It reads the account store, so the planner never supplies it
48
+ * and judges OAuth from config alone.
49
+ */
50
+ oauthFailoverQuorum?: boolean;
51
+ }
52
+
53
+ type Rec = Record<string, unknown>;
54
+ function isRec(value: unknown): value is Rec {
55
+ return value !== null && typeof value === "object" && !Array.isArray(value);
56
+ }
57
+
58
+ /**
59
+ * Whether the bridge could hand this request to the web-search sidecar: a `web_search*` server
60
+ * tool the tool choice does not exclude, with the sidecar not disabled in the Claude replay config
61
+ * (`buildClaudeReplayConfig` merges `claudeCode.webSearchSidecar` over the global block). Whether
62
+ * a backend credential then exists is decided at dispatch and is not read here, so this errs
63
+ * toward the bridge, which is today's behavior for every such request.
64
+ */
65
+ function webSearchSidecarMayEngage(body: Readonly<Rec>, config: OcxConfig): boolean {
66
+ const sidecar = { ...config.webSearchSidecar, ...config.claudeCode?.webSearchSidecar };
67
+ if (sidecar.enabled === false) return false;
68
+ if (!Array.isArray(body.tools)) return false;
69
+ const declared = body.tools.some(tool => isRec(tool) && typeof tool.type === "string" && tool.type.startsWith("web_search"));
70
+ if (!declared) return false;
71
+ const choice = body.tool_choice;
72
+ if (isRec(choice)) {
73
+ if (choice.type === "none") return false;
74
+ if (choice.type === "tool") return typeof choice.name === "string" && isClaudeWebSearchToolName(choice.name);
75
+ }
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Operator policy the translated path applies and the native lane would skip: a pinned
81
+ * reasoning effort for the route, a blocked-skill bundle the translator would elide, or the
82
+ * web-search sidecar. Any of them keeps the request on the bridge.
83
+ */
84
+ function bridgeOnlyPolicyApplies(
85
+ route: RouteResult,
86
+ body: Readonly<Rec>,
87
+ config: OcxConfig,
88
+ selector: NativeMessagesSelector,
89
+ ): boolean {
90
+ if (resolvePinnedEffort(route, selector.routeSelector, config) !== undefined) return true;
91
+ if (anthropicBodyElidesBlockedSkill(body, selector.claudeCode ?? config.claudeCode)) return true;
92
+ return webSearchSidecarMayEngage(body, config);
93
+ }
94
+
95
+ /**
96
+ * The credential rule. A proxy-managed key is native since PF-08. An Anthropic OAuth account
97
+ * is native only with `managedMessagesNativeOAuth` on (itself effective only with
98
+ * `managedMessagesNative`), only for the `anthropic` provider the OAuth store serves, and only
99
+ * to `api.anthropic.com`. A pooled account set declines: rotation, session affinity and quota
100
+ * ranking live in the Responses pipeline's transport and are not replicated here. `forward`
101
+ * belongs to the caller.
102
+ */
103
+ function credentialDeclineReason(
104
+ route: RouteResult,
105
+ config: OcxConfig,
106
+ selector: NativeMessagesSelector,
107
+ ): NativeMessagesDeclineReason | undefined {
108
+ const provider = route.provider;
109
+ if (provider.authMode === undefined || provider.authMode === "key") return undefined;
110
+ if (provider.authMode !== "oauth") return "auth-mode-not-native";
111
+ if (!resolveProtocolSettings(config).rollout.managedMessagesNativeOAuth) return "auth-mode-not-native";
112
+ if (route.providerName !== "anthropic") return "auth-mode-not-native";
113
+ if (!credentialDomainFor(provider)?.firstPartyAnthropic) return "auth-mode-not-native";
114
+ if (isAnthropicAccountPoolEnabled(config) || selector.oauthFailoverQuorum === true) return "oauth-account-pool";
115
+ return undefined;
116
+ }
117
+
118
+ /**
119
+ * The first rule that keeps a Messages request off the managed native lane, or `undefined` when
120
+ * the route is eligible.
121
+ *
122
+ * - the `protocols.rollout.managedMessagesNative` switch is off;
123
+ * - the final adapter is not `anthropic`;
124
+ * - the credential is neither a proxy-managed key nor an eligible Anthropic OAuth account
125
+ * (`credentialDeclineReason`); a pooled OAuth account set is `oauth-account-pool`;
126
+ * - a combo or policy route owns multi-candidate execution in the Responses pipeline;
127
+ * - a synthetic effort or fast row needs the adapter that owns its wire rewrite;
128
+ * - an image would reach a model the operator declared unable to read it;
129
+ * - operator policy only the bridge applies would engage (pinned effort, blocked-skill elision,
130
+ * the web-search sidecar).
131
+ */
132
+ export function nativeMessagesDeclineReason(
133
+ route: RouteResult,
134
+ body: Readonly<Record<string, unknown>>,
135
+ config: OcxConfig,
136
+ selector: NativeMessagesSelector = {},
137
+ ): NativeMessagesDeclineReason | undefined {
138
+ if (!resolveProtocolSettings(config).rollout.managedMessagesNative) return "rollout-disabled";
139
+ const provider = route.provider;
140
+ if (provider.adapter !== "anthropic") return "cross-wire-ir";
141
+ const credentialDecline = credentialDeclineReason(route, config, selector);
142
+ if (credentialDecline) return credentialDecline;
143
+ if (route.combo || route.routeKind === "combo" || route.routeKind === "policy") return "combo-or-policy-route";
144
+ if (selector.effortRow) return "effort-row";
145
+ if (selector.fastRow) return "fast-row";
146
+ if (featuresFromMessagesBody(body).has("request.images")
147
+ && requiresVisionPreprocessing(config, provider, route.modelId, route.providerName)) {
148
+ return "vision-preprocessing";
149
+ }
150
+ if (bridgeOnlyPolicyApplies(route, body, config, selector)) return "bridge-only-policy";
151
+ return undefined;
152
+ }
153
+
154
+ export function isNativeMessagesRouteEligible(
155
+ route: RouteResult,
156
+ body: Readonly<Record<string, unknown>>,
157
+ config: OcxConfig,
158
+ selector?: NativeMessagesSelector,
159
+ ): boolean {
160
+ return nativeMessagesDeclineReason(route, body, config, selector) === undefined;
161
+ }