@bitkyc08/opencodex 2.15.1 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/gui/dist/assets/{index-CMCDkQ7U.js → index-CZwbOse7.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +2 -0
  5. package/src/adapters/google-antigravity-replay.ts +9 -1
  6. package/src/adapters/kiro-thinking.ts +8 -0
  7. package/src/adapters/kiro.ts +45 -42
  8. package/src/adapters/openai-chat.ts +5 -2
  9. package/src/adapters/openai-responses.ts +5 -1
  10. package/src/cli/dispatch.ts +6 -3
  11. package/src/cli/index.ts +1 -0
  12. package/src/generated/compatibility-version.json +48 -24
  13. package/src/integrations/config-io.ts +119 -1
  14. package/src/integrations/omp-yaml-source.ts +6 -1
  15. package/src/integrations/serialize.ts +80 -1
  16. package/src/integrations/state.ts +37 -6
  17. package/src/integrations/writer.ts +11 -3
  18. package/src/lab/automation/orchestrator.ts +19 -0
  19. package/src/lib/lab-activation.ts +161 -0
  20. package/src/lib/lab-passive-linker-registration.ts +26 -0
  21. package/src/lib/optional-shutdown-hooks.ts +57 -0
  22. package/src/lib/translator-budget.ts +34 -0
  23. package/src/providers/antigravity-models.ts +65 -10
  24. package/src/routing/compatibility/assemble.ts +21 -107
  25. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  26. package/src/routing/compatibility/provider-slot.ts +56 -0
  27. package/src/server/index.ts +8 -17
  28. package/src/server/lifecycle.ts +5 -3
  29. package/src/server/management/routing-profile-routes.ts +9 -1
  30. package/src/server/management-api.ts +37 -6
  31. package/src/server/passive-route-linker.ts +66 -0
  32. package/src/server/responses/core.ts +20 -20
  33. package/src/types.ts +10 -0
@@ -6,8 +6,8 @@ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./mode
6
6
  // CLI resolves labels against. The ids below separate CCA wire ids, collapsed picker entries,
7
7
  // and hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
8
8
  // receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
9
- // picker exposes collapsed base models only when CCA returns every known tier; otherwise each
10
- // returned wire id remains visible so an unavailable tier cannot be selected.
9
+ // picker exposes collapsed known base models only when CCA returns every known tier; unknown
10
+ // returned wire ids remain visible so they stay directly routable.
11
11
 
12
12
  // ── Wire IDs (what CCA :fetchAvailableModels returns) ──
13
13
 
@@ -63,6 +63,41 @@ const ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL: Record<string, string[]> = Object.en
63
63
  return out;
64
64
  }, {});
65
65
 
66
+ const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;
67
+
68
+ function pickerModelIdForDiscoveredWireId(
69
+ wireId: string,
70
+ available: ReadonlyMap<string, Record<string, unknown>>,
71
+ ): string {
72
+ const explicitPickerId = Object.hasOwn(ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID, wireId)
73
+ ? ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId]
74
+ : undefined;
75
+ if (explicitPickerId) {
76
+ const requiredWireIds = Object.hasOwn(ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL, explicitPickerId)
77
+ ? ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL[explicitPickerId] ?? []
78
+ : [];
79
+ if (requiredWireIds.every(id => available.has(id))) return explicitPickerId;
80
+ }
81
+
82
+ // CCA uses a single `-tiered` row for models whose effort levels ride on the
83
+ // request's thinkingLevel field. Keep this generic so new tiered models do not
84
+ // require another provider-specific ID mapping.
85
+ if (wireId.endsWith("-tiered")) {
86
+ const baseId = wireId.slice(0, -"-tiered".length);
87
+ if (isKnownAntigravityPickerModelId(baseId)) return baseId;
88
+ }
89
+
90
+ const effortMatch = /^(.*)-(low|medium|high)$/.exec(wireId);
91
+ if (effortMatch) {
92
+ const baseId = effortMatch[1]!;
93
+ if (isKnownAntigravityPickerModelId(baseId)
94
+ && ANTIGRAVITY_DISCOVERY_EFFORTS.every(effort => available.has(`${baseId}-${effort}`))) {
95
+ return baseId;
96
+ }
97
+ }
98
+ return wireId;
99
+ }
100
+
66
101
  // ── Effort ladders per collapsed base model ──
67
102
  // Gemini models: effort → wire model suffix (official agy UI pattern).
68
103
  // Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
@@ -141,6 +176,10 @@ export const ANTIGRAVITY_MODELS = [
141
176
  "gpt-oss-120b-medium",
142
177
  ];
143
178
 
179
+ function isKnownAntigravityPickerModelId(value: string): boolean {
180
+ return isValidModelDiscoveryModelId(value) && ANTIGRAVITY_MODELS.includes(value);
181
+ }
182
+
144
183
  // Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
145
184
  const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
146
185
  "gemini-3.7-flash": 1_048_576,
@@ -222,6 +261,7 @@ export function parseAntigravityAvailableModels(
222
261
  if (!Array.isArray(modelIds)) return null;
223
262
  for (const id of modelIds) {
224
263
  if (!isValidModelDiscoveryModelId(id)
264
+ || !Object.hasOwn(models, id)
225
265
  || !antigravityRecord(models[id])
226
266
  || ids.length >= limit) return null;
227
267
  ids.push(id);
@@ -235,6 +275,19 @@ export function parseAntigravityAvailableModels(
235
275
  if (ids.length >= limit) return null;
236
276
  ids.push("gemini-3.1-flash-image");
237
277
  }
278
+ // Newer CCA responses identify tiered Flash models through this index instead of
279
+ // adding their synthetic wire ids to agentModelSorts.
280
+ const tieredModelIds = antigravityRecord(body.tieredModelIds);
281
+ const flashTieredIds = tieredModelIds?.flash;
282
+ if (Array.isArray(flashTieredIds)) {
283
+ for (const id of flashTieredIds) {
284
+ if (!isValidModelDiscoveryModelId(id)
285
+ || !Object.hasOwn(models, id)
286
+ || !antigravityRecord(models[id])
287
+ || ids.length >= limit) return null;
288
+ ids.push(id);
289
+ }
290
+ }
238
291
 
239
292
  const available = new Map<string, Record<string, unknown>>();
240
293
  for (const wireId of ids) {
@@ -242,17 +295,17 @@ export function parseAntigravityAvailableModels(
242
295
  if (!info || available.has(wireId)) continue;
243
296
  // Legacy compatibility aliases are deliberately routed to newer wire ids for saved
244
297
  // selections. They are not safe as independently discovered picker rows.
245
- if (ANTIGRAVITY_MODEL_ALIASES[wireId] && ANTIGRAVITY_MODEL_ALIASES[wireId] !== wireId) continue;
298
+ const alias = Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, wireId)
299
+ ? ANTIGRAVITY_MODEL_ALIASES[wireId]
300
+ : undefined;
301
+ if (alias && alias !== wireId) continue;
246
302
  available.set(wireId, info);
247
303
  }
248
304
 
249
305
  const out: AntigravityAvailableModel[] = [];
250
306
  const seen = new Set<string>();
251
307
  for (const [wireId, info] of available) {
252
- const pickerId = ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId];
253
- const completePickerSet = pickerId !== undefined
254
- && ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL[pickerId]!.every(id => available.has(id));
255
- const id = completePickerSet ? pickerId! : wireId;
308
+ const id = pickerModelIdForDiscoveredWireId(wireId, available);
256
309
  if (seen.has(id)) continue;
257
310
  seen.add(id);
258
311
  out.push({
@@ -265,7 +318,9 @@ export function parseAntigravityAvailableModels(
265
318
  }
266
319
 
267
320
  export function resolveAntigravityWireModelId(modelId: string): string {
268
- return ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
321
+ return Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, modelId)
322
+ ? ANTIGRAVITY_MODEL_ALIASES[modelId]
323
+ : modelId;
269
324
  }
270
325
 
271
326
  /**
@@ -279,7 +334,7 @@ export function isAntigravitySuffixModelId(modelId: string): boolean {
279
334
 
280
335
  /** The reasoning tier a retired Flash id used to encode, if it is one. */
281
336
  export function retiredAntigravityFlashTier(modelId: string): string | undefined {
282
- return RETIRED_FLASH_TIERS[modelId];
337
+ return Object.hasOwn(RETIRED_FLASH_TIERS, modelId) ? RETIRED_FLASH_TIERS[modelId] : undefined;
283
338
  }
284
339
 
285
340
  /**
@@ -299,7 +354,7 @@ export function resolveAntigravityEffortWireModel(
299
354
  // Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
300
355
  // current generation and carry the tier the retired id encoded. This runs BEFORE the
301
356
  // suffix check because those ids are aliases, and rule 1 would drop the tier.
302
- const retiredTier = RETIRED_FLASH_TIERS[modelId];
357
+ const retiredTier = retiredAntigravityFlashTier(modelId);
303
358
  if (retiredTier) {
304
359
  return {
305
360
  wireModelId: GEMINI_FLASH_CURRENT,
@@ -5,73 +5,22 @@ import type { PolicyCandidateEvidence } from "../evaluator";
5
5
  import { policyCandidateHealthEvidence } from "../health";
6
6
  import type { NormalizedRoutingProfile } from "../profile";
7
7
  import { quotaEvidenceForCandidate } from "../quota";
8
- import {
9
- compatibilitySuiteKey,
10
- loadCompatibilityCatalogSnapshot,
11
- type CompatibilityCatalogSnapshot,
12
- } from "./catalog";
13
- import { findVerdictForSuite, loadCompatibilityEvidenceSnapshot } from "./reader";
14
- import {
15
- resolvePolicyCompatibilitySubjects,
16
- type ResolvedPolicyCompatibilitySubjects,
17
- } from "./subject";
18
- import type { CandidateCompatibilityEvidence } from "./types";
8
+ import { resolveCompatibilityEvidenceProvider, type CoreEvidenceOptions } from "./provider-slot";
19
9
 
20
10
  export type RoutedProviderResolver = (
21
11
  providerName: string,
22
12
  provider: OcxProviderConfig,
23
13
  ) => OcxProviderConfig;
24
14
 
25
- export interface AssemblePolicyEvidenceOptions {
26
- configDir?: string;
27
- routedProviderConfig: RoutedProviderResolver;
28
- resolveSubjects?: typeof resolvePolicyCompatibilitySubjects;
29
- loadEvidenceSnapshot?: typeof loadCompatibilityEvidenceSnapshot;
30
- loadCatalogSnapshot?: typeof loadCompatibilityCatalogSnapshot;
31
- }
32
-
33
- function attachCompatibilityEvidence(
34
- resolved: ResolvedPolicyCompatibilitySubjects | undefined,
35
- snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot>,
36
- catalog: CompatibilityCatalogSnapshot,
37
- profile: NonNullable<NormalizedRoutingProfile["compatibility"]>,
38
- ): CandidateCompatibilityEvidence {
39
- const subjectIds = resolved?.subjectIds ?? {};
40
- const suites: CandidateCompatibilityEvidence["suites"] = [];
41
-
42
- for (const requirement of profile.requiredSuites) {
43
- const subjectId = subjectIds[requirement.evidenceLayer];
44
- if (!subjectId) continue;
45
- const metadata = catalog.get(compatibilitySuiteKey(requirement.evidenceLayer, requirement.suiteId));
46
- if (!metadata) continue;
47
- const row = findVerdictForSuite(
48
- snapshot,
49
- subjectId,
50
- requirement.evidenceLayer,
51
- requirement.suiteId,
52
- metadata.suiteVersion,
53
- metadata.suiteManifestDigest,
54
- );
55
- if (!row) continue;
56
- suites.push({
57
- subjectId,
58
- suiteId: row.suiteId,
59
- evidenceLayer: requirement.evidenceLayer,
60
- suiteVersion: row.suiteVersion,
61
- suiteManifestDigest: row.suiteManifestDigest,
62
- verdict: row.verdict,
63
- asOf: row.asOf,
64
- maxAgeMs: metadata.maxAgeMs,
65
- notes: row.notes,
66
- });
67
- }
68
-
69
- return {
70
- subjectIds: { ...subjectIds },
71
- projectionAvailable: snapshot.projectionAvailable,
72
- suites,
73
- };
74
- }
15
+ /**
16
+ * Options the core assembler needs. Provider-specific test seams (subject resolution,
17
+ * catalog and projection loading) belong to the compatibility provider, not here -- keeping
18
+ * them out is what stops the core assembler from naming Lab-backed contracts.
19
+ *
20
+ * The provider reads its own seams off the same object, so callers may still pass a
21
+ * `LabCompatibilityProviderOptions`; that type extends this one.
22
+ */
23
+ export type AssemblePolicyEvidenceOptions = CoreEvidenceOptions;
75
24
 
76
25
  /**
77
26
  * Assemble production policy candidate evidence including compatibility snapshots.
@@ -89,55 +38,20 @@ export function assemblePolicyCandidateEvidence(
89
38
  const hasCompatibilityRequirements = Boolean(
90
39
  compatibilityPolicy && compatibilityPolicy.requiredSuites.length > 0,
91
40
  );
92
- const resolvedByCandidate = new Map<string, ResolvedPolicyCompatibilitySubjects>();
93
- let catalog: CompatibilityCatalogSnapshot = new Map();
94
- let snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot> = {
95
- projectionAvailable: true,
96
- projectionIncompatible: false,
97
- bySubject: new Map(),
98
- };
99
-
100
- if (hasCompatibilityRequirements && compatibilityPolicy) {
101
- const resolveSubjects = options.resolveSubjects ?? resolvePolicyCompatibilitySubjects;
102
- const loadCatalog = options.loadCatalogSnapshot ?? loadCompatibilityCatalogSnapshot;
103
- const loadEvidence = options.loadEvidenceSnapshot ?? loadCompatibilityEvidenceSnapshot;
104
- catalog = loadCatalog(compatibilityPolicy.requiredSuites);
105
- const subjectIds = new Set<string>();
106
-
107
- for (const candidate of profile.candidates) {
108
- const provider = config.providers[candidate.provider];
109
- if (!provider) continue;
110
- try {
111
- const routed = options.routedProviderConfig(candidate.provider, provider);
112
- const resolved = resolveSubjects(
113
- config,
114
- candidate.provider,
115
- candidate.model,
116
- routed,
117
- options.configDir,
118
- );
119
- resolvedByCandidate.set(`${candidate.provider}/${candidate.model}`, resolved);
120
- for (const subjectId of Object.values(resolved.subjectIds)) {
121
- if (subjectId) subjectIds.add(subjectId);
122
- }
123
- } catch {
124
- // Subject construction failure is handled per required layer as unknown.
125
- }
126
- }
127
-
128
- snapshot = loadEvidence([...subjectIds], options.configDir);
129
- }
41
+ // Compatibility evidence is supplied by an opt-in subsystem. With no provider registered
42
+ // -- every install without compatibility-gated profiles -- the evaluator sees no
43
+ // compatibility evidence and scores on capability, health, quota, and cost exactly as it
44
+ // did before compatibility policy existed.
45
+ const compatibilityProvider = hasCompatibilityRequirements && compatibilityPolicy
46
+ ? resolveCompatibilityEvidenceProvider()
47
+ : null;
48
+ const compatibilityByCandidate = compatibilityProvider && compatibilityPolicy
49
+ ? compatibilityProvider(config, profile, compatibilityPolicy, options)
50
+ : null;
130
51
 
131
52
  return profile.candidates.map(candidate => {
132
53
  const key = `${candidate.provider}/${candidate.model}`;
133
- const compatibility = hasCompatibilityRequirements && compatibilityPolicy
134
- ? attachCompatibilityEvidence(
135
- resolvedByCandidate.get(key),
136
- snapshot,
137
- catalog,
138
- compatibilityPolicy,
139
- )
140
- : undefined;
54
+ const compatibility = compatibilityByCandidate?.get(key);
141
55
 
142
56
  return {
143
57
  provider: candidate.provider,
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Compatibility-evidence provider (Lab-backed).
3
+ *
4
+ * Holds every Lab-reaching part of policy candidate evidence: route/protocol subject
5
+ * construction, the suite catalog snapshot, and the projection read. The core assembler
6
+ * (`assemble.ts`) keeps capability, health, quota, and cost and consults this only through
7
+ * the provider slot, so an install that never activates Lab never loads this module.
8
+ *
9
+ * This is a relocation of previously inline logic, not a rewrite: `attachCompatibilityEvidence`
10
+ * below is the original function, and its state arrives entirely through arguments.
11
+ *
12
+ * @internal registered by the Lab activation path
13
+ */
14
+ import type { OcxConfig } from "../../types";
15
+ import type { NormalizedRoutingProfile } from "../profile";
16
+ import {
17
+ compatibilitySuiteKey,
18
+ loadCompatibilityCatalogSnapshot,
19
+ type CompatibilityCatalogSnapshot,
20
+ } from "./catalog";
21
+ import { findVerdictForSuite, loadCompatibilityEvidenceSnapshot } from "./reader";
22
+ import {
23
+ resolvePolicyCompatibilitySubjects,
24
+ type ResolvedPolicyCompatibilitySubjects,
25
+ } from "./subject";
26
+ import type { CandidateCompatibilityEvidence } from "./types";
27
+ import type { CoreEvidenceOptions, CompatibilityEvidenceProvider } from "./provider-slot";
28
+
29
+ /** Lab-side seams, kept off the core options contract. */
30
+ export interface LabCompatibilityProviderOptions extends CoreEvidenceOptions {
31
+ resolveSubjects?: typeof resolvePolicyCompatibilitySubjects;
32
+ loadEvidenceSnapshot?: typeof loadCompatibilityEvidenceSnapshot;
33
+ loadCatalogSnapshot?: typeof loadCompatibilityCatalogSnapshot;
34
+ }
35
+
36
+ function attachCompatibilityEvidence(
37
+ resolved: ResolvedPolicyCompatibilitySubjects | undefined,
38
+ snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot>,
39
+ catalog: CompatibilityCatalogSnapshot,
40
+ profile: NonNullable<NormalizedRoutingProfile["compatibility"]>,
41
+ ): CandidateCompatibilityEvidence {
42
+ const subjectIds = resolved?.subjectIds ?? {};
43
+ const suites: CandidateCompatibilityEvidence["suites"] = [];
44
+
45
+ for (const requirement of profile.requiredSuites) {
46
+ const subjectId = subjectIds[requirement.evidenceLayer];
47
+ if (!subjectId) continue;
48
+ const metadata = catalog.get(compatibilitySuiteKey(requirement.evidenceLayer, requirement.suiteId));
49
+ if (!metadata) continue;
50
+ const row = findVerdictForSuite(
51
+ snapshot,
52
+ subjectId,
53
+ requirement.evidenceLayer,
54
+ requirement.suiteId,
55
+ metadata.suiteVersion,
56
+ metadata.suiteManifestDigest,
57
+ );
58
+ if (!row) continue;
59
+ suites.push({
60
+ subjectId,
61
+ suiteId: row.suiteId,
62
+ evidenceLayer: requirement.evidenceLayer,
63
+ suiteVersion: row.suiteVersion,
64
+ suiteManifestDigest: row.suiteManifestDigest,
65
+ verdict: row.verdict,
66
+ asOf: row.asOf,
67
+ maxAgeMs: metadata.maxAgeMs,
68
+ notes: row.notes,
69
+ });
70
+ }
71
+
72
+ return {
73
+ subjectIds: { ...subjectIds },
74
+ projectionAvailable: snapshot.projectionAvailable,
75
+ suites,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Build compatibility evidence for every candidate of one profile.
81
+ * Keyed `provider/model`; a candidate absent from the map has no compatibility evidence.
82
+ */
83
+ export const labCompatibilityEvidenceProvider: CompatibilityEvidenceProvider = (
84
+ config: OcxConfig,
85
+ profile: NormalizedRoutingProfile,
86
+ policy: NonNullable<NormalizedRoutingProfile["compatibility"]>,
87
+ options: CoreEvidenceOptions,
88
+ ): Map<string, CandidateCompatibilityEvidence> => {
89
+ const labOptions = options as LabCompatibilityProviderOptions;
90
+ const resolveSubjects = labOptions.resolveSubjects ?? resolvePolicyCompatibilitySubjects;
91
+ const loadCatalog = labOptions.loadCatalogSnapshot ?? loadCompatibilityCatalogSnapshot;
92
+ const loadEvidence = labOptions.loadEvidenceSnapshot ?? loadCompatibilityEvidenceSnapshot;
93
+
94
+ const resolvedByCandidate = new Map<string, ResolvedPolicyCompatibilitySubjects>();
95
+ const catalog: CompatibilityCatalogSnapshot = loadCatalog(policy.requiredSuites);
96
+ const subjectIds = new Set<string>();
97
+
98
+ for (const candidate of profile.candidates) {
99
+ const provider = config.providers[candidate.provider];
100
+ if (!provider) continue;
101
+ try {
102
+ const routed = options.routedProviderConfig(candidate.provider, provider);
103
+ const resolved = resolveSubjects(
104
+ config,
105
+ candidate.provider,
106
+ candidate.model,
107
+ routed,
108
+ options.configDir,
109
+ );
110
+ resolvedByCandidate.set(`${candidate.provider}/${candidate.model}`, resolved);
111
+ for (const subjectId of Object.values(resolved.subjectIds)) {
112
+ if (subjectId) subjectIds.add(subjectId);
113
+ }
114
+ } catch {
115
+ // Subject construction failure is handled per required layer as unknown.
116
+ }
117
+ }
118
+
119
+ const snapshot = loadEvidence([...subjectIds], options.configDir);
120
+
121
+ const byCandidate = new Map<string, CandidateCompatibilityEvidence>();
122
+ for (const candidate of profile.candidates) {
123
+ const key = `${candidate.provider}/${candidate.model}`;
124
+ byCandidate.set(
125
+ key,
126
+ attachCompatibilityEvidence(resolvedByCandidate.get(key), snapshot, catalog, policy),
127
+ );
128
+ }
129
+ return byCandidate;
130
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Slot for the optional compatibility-evidence provider.
3
+ *
4
+ * Routing is synchronous and must stay synchronous: `routeModelInternal` is sync, and so
5
+ * are the subagent-fallback helpers that call `routeModel` (`isNativeModelQuotaExhausted`,
6
+ * `isModelHealthBlocked`, `selectAvailableSubagentModel`, ...). Making the chain async to
7
+ * permit a dynamic import would touch hundreds of call sites and break those APIs, so this
8
+ * is a plain nullable reference rather than an `await import()`.
9
+ *
10
+ * The Lab implementation is installed during activation. Installs without
11
+ * compatibility-gated routing profiles never register one, so the core evidence assembler
12
+ * never reaches the Lab module graph.
13
+ *
14
+ * See devlog/_plan/260814_lab_core_decoupling/030_router_and_startup_activation.md
15
+ */
16
+ import type { OcxConfig } from "../../types";
17
+ import type { NormalizedRoutingProfile } from "../profile";
18
+ import type { CandidateCompatibilityEvidence } from "./types";
19
+
20
+ /** Options the core assembler can supply without knowing anything Lab-specific. */
21
+ export interface CoreEvidenceOptions {
22
+ configDir?: string;
23
+ routedProviderConfig: (providerName: string, provider: import("../../types").OcxProviderConfig)
24
+ => import("../../types").OcxProviderConfig;
25
+ }
26
+
27
+ /**
28
+ * Produce compatibility evidence per candidate, keyed `provider/model`.
29
+ * A candidate absent from the map has no compatibility evidence.
30
+ */
31
+ export type CompatibilityEvidenceProvider = (
32
+ config: OcxConfig,
33
+ profile: NormalizedRoutingProfile,
34
+ policy: NonNullable<NormalizedRoutingProfile["compatibility"]>,
35
+ options: CoreEvidenceOptions,
36
+ ) => Map<string, CandidateCompatibilityEvidence>;
37
+
38
+ let provider: CompatibilityEvidenceProvider | null = null;
39
+
40
+ /** Install the provider. Returns a detach function. */
41
+ export function setCompatibilityEvidenceProvider(next: CompatibilityEvidenceProvider): () => void {
42
+ provider = next;
43
+ return () => {
44
+ if (provider === next) provider = null;
45
+ };
46
+ }
47
+
48
+ /** The installed provider, or null when no optional subsystem is active. */
49
+ export function resolveCompatibilityEvidenceProvider(): CompatibilityEvidenceProvider | null {
50
+ return provider;
51
+ }
52
+
53
+ /** Test-only reset. */
54
+ export function resetCompatibilityEvidenceProviderForTests(): void {
55
+ provider = null;
56
+ }
@@ -45,12 +45,7 @@ import {
45
45
  registerDefaultAppOwnedObservedBuffers,
46
46
  } from "../lib/app-owned-memory-stores";
47
47
  import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
48
- import {
49
- setLabAutomationDispatchDeps,
50
- startLabAutomationScheduler,
51
- } from "../lab/automation/orchestrator";
52
- import { loadLabAutomationPolicy } from "../lab/automation/persistence";
53
- import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";
48
+ import { activateLab, labActivationRequired } from "../lib/lab-activation";
54
49
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
55
50
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
56
51
  import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
@@ -1735,18 +1730,14 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1735
1730
  // Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
1736
1731
  backgroundLifecycle.scheduleStartupRun();
1737
1732
 
1733
+ // Compatibility Lab is optional: wire it only for installs that actually use it -- any
1734
+ // routing profile, or automation enabled on disk. This runs synchronously before
1735
+ // startServer returns, in the same turn as Bun.serve, so a policy route can never be
1736
+ // evaluated before its evidence provider is registered. That ordering is load-bearing:
1737
+ // the subagent-fallback chain routes synchronously and has nowhere to await.
1738
1738
  const labConfigDir = getConfigDir();
1739
- const productionLabRouteExecutor = createProductionLabRouteExecutor({
1740
- configDir: labConfigDir,
1741
- loadConfig: () => config,
1742
- });
1743
- setLabAutomationDispatchDeps({
1744
- configDir: labConfigDir,
1745
- loadConfig: () => config,
1746
- routeExecutor: productionLabRouteExecutor,
1747
- });
1748
- if (loadLabAutomationPolicy(labConfigDir).enabled) {
1749
- startLabAutomationScheduler(labConfigDir);
1739
+ if (labActivationRequired(config, labConfigDir)) {
1740
+ activateLab(config, labConfigDir);
1750
1741
  }
1751
1742
 
1752
1743
  return server;
@@ -7,7 +7,7 @@ import {
7
7
  } from "../storage/policy-job";
8
8
  import { abortRestoreTrashJobAsync } from "../storage/restore-job";
9
9
  import { stopStorageCleanupScheduler } from "../storage/policy-scheduler";
10
- import { stopLabAutomationScheduler, requestLabAutomationShutdown } from "../lab/automation/orchestrator";
10
+ import { runOptionalShutdownHooks } from "../lib/optional-shutdown-hooks";
11
11
  import { stopStateStoreSweeper } from "../lib/state-store-sweeper";
12
12
  import {
13
13
  cancelQueuedStorageWorkerSpawns,
@@ -452,8 +452,10 @@ export async function drainAndShutdown(
452
452
  // Abort each job independently so one wedged join cannot skip the other,
453
453
  // then drain leftovers; failures must not prevent `server.stop`.
454
454
  stopStorageCleanupScheduler();
455
- requestLabAutomationShutdown();
456
- stopLabAutomationScheduler();
455
+ // Optional subsystems (Compatibility Lab today, anything added later) tear themselves
456
+ // down through hooks registered at activation. A process that never activated one runs
457
+ // nothing here and never loads its module graph.
458
+ runOptionalShutdownHooks();
457
459
  stopStateStoreSweeper();
458
460
  // The overlay reconciler is owner-scoped: the startServer stop override
459
461
  // releases THIS server's lease through runListenerShutdown →
@@ -17,9 +17,10 @@ import {
17
17
  } from "../../routing/profile";
18
18
  import { evaluatePolicyProfile, type PolicyCandidateEvidence, type PolicyRequestEvidence } from "../../routing/evaluator";
19
19
  import { assemblePolicyCandidateEvidence } from "../../routing/compatibility/assemble";
20
+ import { activateLab, labActivationRequired } from "../../lib/lab-activation";
20
21
  import { quotaEvidenceForCandidate } from "../../routing/quota";
21
22
  import { routedProviderConfig } from "../../router";
22
- import { saveConfigPreservingClaudeCode } from "../../config";
23
+ import { saveConfigPreservingClaudeCode, getConfigDir } from "../../config";
23
24
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
24
25
  import { isPlainRecord } from "./shared";
25
26
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
@@ -289,6 +290,9 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
289
290
  const nextProfiles = { ...(config.routingProfiles ?? {}) };
290
291
  nextProfiles[id] = storedProfile(id, body.profile as OcxRoutingProfileConfig);
291
292
  config.routingProfiles = nextProfiles;
293
+ // Creating the first profile on a process started profile-less must install the
294
+ // compatibility provider now; activation is synchronous and idempotent per configDir.
295
+ if (labActivationRequired(config, getConfigDir())) activateLab(config, getConfigDir());
292
296
  // An alias change on update renames the public model id; rewrite config
293
297
  // references (disabledModels, subagentModels, injectionModel,
294
298
  // shadowCallIntercept, claudeCode) so they follow the new alias.
@@ -358,6 +362,10 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
358
362
  // One clock read for both assembly and evaluation keeps freshness, health,
359
363
  // and trace timestamps mutually consistent with the production router.
360
364
  const now = Date.now();
365
+ // R3-1: dry-run assembles candidate evidence independently of the startup gate, so an
366
+ // operator preview on a process started without profiles would silently omit
367
+ // compatibility evidence and disagree with production. Activate first.
368
+ if (labActivationRequired(config, getConfigDir())) activateLab(config, getConfigDir());
361
369
  const candidateEvidence = body.candidates === undefined
362
370
  ? assembleCandidateEvidence(config, resolvedProfile, now)
363
371
  : parseCandidateEvidence(body.candidates);
@@ -61,15 +61,12 @@ import { handleConfigRoutes } from "./management/config-routes";
61
61
  import { handleLogsUsageRoutes } from "./management/logs-usage-routes";
62
62
  import { handleRequestHistoryRoutes } from "./management/request-history-routes";
63
63
  import { handleRoutingAnalyticsRoutes } from "./management/routing-analytics-routes";
64
- import { handleRoutingProfileRoutes } from "./management/routing-profile-routes";
65
64
  import { handleProviderRoutes } from "./management/provider-routes";
66
65
  import { handleModelRoutes } from "./management/model-routes";
67
66
  import { handleAgentSettingsRoutes } from "./management/agent-settings-routes";
68
67
  import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
69
68
  import { handleComboRoutes } from "./management/combo-routes";
70
69
  import { handleSystemRoutes } from "./management/system-routes";
71
- import { handleLabRoutes } from "./management/lab-routes";
72
- import { handleLabAutomationRoutes } from "./management/lab-automation-routes";
73
70
  import { handleSidebarRoutes } from "./management/sidebar-routes";
74
71
  import { handleIntegrationRoutes } from "./management/integration-routes";
75
72
  import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
@@ -96,6 +93,41 @@ const managementConvergenceBindings = new WeakMap<object, Readonly<{
96
93
  converge: ConvergeCodex;
97
94
  }>>();
98
95
 
96
+ /**
97
+ * Namespace match for management route prefixes: exact hit or a child path, never a
98
+ * prefix collision (`/api/labfoo` must not match `/api/lab`).
99
+ */
100
+ function pathInManagementNamespace(pathname: string, prefix: string): boolean {
101
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
102
+ }
103
+
104
+ /**
105
+ * Routing-profile and Compatibility Lab handlers statically import the Lab module graph,
106
+ * so mounting them eagerly would pull ~70 `src/lab/` modules into every management
107
+ * request -- including installs that never opted into Lab. Loading them per namespace
108
+ * keeps `management-api.ts` on the same footing as the three protected core files.
109
+ *
110
+ * Cherry-picked from @Wibias's PR #1676, which solved this before the boundary work
111
+ * reached it. See devlog/_plan/260814_lab_core_decoupling/.
112
+ */
113
+ async function handleRoutingProfileRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
114
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/routing-profiles")) return null;
115
+ const { handleRoutingProfileRoutes } = await import("./management/routing-profile-routes");
116
+ return handleRoutingProfileRoutes(ctx);
117
+ }
118
+
119
+ async function handleLabRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
120
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/lab")) return null;
121
+ // Automation is checked first so its narrower namespace keeps its own handler, matching
122
+ // the eager chain's ordering.
123
+ if (pathInManagementNamespace(ctx.url.pathname, "/api/lab/automation")) {
124
+ const { handleLabAutomationRoutes } = await import("./management/lab-automation-routes");
125
+ return handleLabAutomationRoutes(ctx);
126
+ }
127
+ const { handleLabRoutes } = await import("./management/lab-routes");
128
+ return handleLabRoutes(ctx);
129
+ }
130
+
99
131
  export async function handleManagementAPI(
100
132
  req: Request,
101
133
  url: URL,
@@ -180,7 +212,7 @@ export async function handleManagementAPI(
180
212
  ?? (await handleLogsUsageRoutes(ctx))
181
213
  ?? (await handleRequestHistoryRoutes(ctx))
182
214
  ?? (await handleRoutingAnalyticsRoutes(ctx))
183
- ?? (await handleRoutingProfileRoutes(ctx))
215
+ ?? (await handleRoutingProfileRoutesOnDemand(ctx))
184
216
  ?? (await handleProviderRoutes(ctx))
185
217
  ?? (await handleModelRoutes(ctx))
186
218
  ?? (await handleIntegrationRoutes(ctx))
@@ -189,8 +221,7 @@ export async function handleManagementAPI(
189
221
  ?? (await handleOauthAccountRoutes(ctx))
190
222
  ?? (await handleComboRoutes(ctx))
191
223
  ?? (await handleSystemRoutes(ctx))
192
- ?? (await handleLabAutomationRoutes(ctx))
193
- ?? (await handleLabRoutes(ctx))
224
+ ?? (await handleLabRoutesOnDemand(ctx))
194
225
  ?? (await handleSidebarRoutes(ctx));
195
226
  } catch (error) {
196
227
  const tooLarge = managementBodyTooLargeResponse(error, req, config);