super-dsh 0.1.10 → 0.1.11

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 (50) hide show
  1. package/README.md +18 -2
  2. package/agent-agy/dist/agent.js +19 -1
  3. package/agent-claude/dist/adapter.js +106 -31
  4. package/agent-claude/dist/agent.js +177 -22
  5. package/agent-claude/dist/claude-client.js +84 -43
  6. package/agent-claude/dist/claude-launch.js +101 -0
  7. package/agent-claude/dist/claude-native-config.js +169 -0
  8. package/agent-claude/dist/claude-provider-gateway.js +340 -0
  9. package/agent-claude/dist/claude-providers.js +391 -0
  10. package/agent-claude/dist/claude-route.js +152 -0
  11. package/agent-claude/dist/index.js +214 -37
  12. package/agent-claude/dist/models.js +14 -0
  13. package/agent-claude/dist/sdk-runtime.js +320 -0
  14. package/agent-claude/package.json +6 -4
  15. package/agent-codex/README.md +18 -6
  16. package/agent-codex/dist/adapter.js +3 -2
  17. package/agent-codex/dist/agent.js +41 -7
  18. package/agent-codex/dist/app-server-client.js +7 -1
  19. package/agent-codex/dist/app-server-wire.js +10 -22
  20. package/agent-codex/dist/codex-launch.js +10 -32
  21. package/agent-codex/dist/codex-native-config.js +9 -3
  22. package/agent-codex/dist/codex-oauth.js +153 -0
  23. package/agent-codex/dist/codex-provider-gateway.js +36 -38
  24. package/agent-codex/dist/codex-providers.js +20 -7
  25. package/agent-codex/dist/codex-runtime.js +120 -0
  26. package/agent-codex/dist/index.js +18 -11
  27. package/agent-codex/package.json +1 -1
  28. package/agent-codex-sdk/dist/adapter.js +3 -2
  29. package/agent-codex-sdk/dist/agent.js +35 -1
  30. package/agent-codex-sdk/dist/codex-native-config.js +9 -3
  31. package/agent-codex-sdk/dist/codex-oauth.js +153 -0
  32. package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
  33. package/agent-codex-sdk/dist/codex-providers.js +20 -7
  34. package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
  35. package/agent-codex-sdk/dist/index.js +18 -11
  36. package/agent-codex-sdk/dist/sdk-runtime.js +2 -2
  37. package/agent-hermes/dist/agent.js +46 -2
  38. package/agent-hermes/dist/hermes-client.js +49 -1
  39. package/agent-omp/dist/agent.js +17 -4
  40. package/agent-omp-sdk/dist/agent.d.ts +11 -0
  41. package/agent-omp-sdk/dist/agent.js +17 -4
  42. package/agent-pi/dist/agent.js +33 -1
  43. package/client/index.js +150 -58
  44. package/package.json +2 -3
  45. package/superd-hub/dist/client/agent-settings.d.ts +66 -12
  46. package/superd-hub/dist/client/agent-settings.js +85 -14
  47. package/superd-hub/dist/client/runtime-settings-api.js +5 -5
  48. package/superd-hub/dist/world-mount.d.ts +6 -5
  49. package/superd-hub/dist/world-mount.js +6 -6
  50. package/superd-hub/lib/client/index.js +150 -58
@@ -25,15 +25,21 @@ import { Service } from "@deepseek-ai/cordis";
25
25
  import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
26
26
  import { ClaudeSdkClient, probeClaudeModels } from "./claude-client.js";
27
27
  import { ClaudeAgent } from "./agent.js";
28
- import { ClaudeLlmAdapter, CLAUDE_PROVIDER_ID } from "./adapter.js";
28
+ import { ClaudeLlmAdapter, CLAUDE_PROVIDER_ID, routeIds } from "./adapter.js";
29
29
  import { resolveClaudeHome } from "./claude-home.js";
30
30
  import { resetProjectionState } from "./claude-events.js";
31
31
  import { claudeSessionIdFromDsh } from "./session-id.js";
32
32
  import { claudePermissionMode, defaultPermissionPreset, modeChangePatch, presetFromEvents, } from "./permission.js";
33
33
  import { sessionRecord, upsertSession } from "./session-map.js";
34
34
  import { makeCanUseTool, answerAskUserQuestion, askPlanReview, } from "./interaction.js";
35
- import { modelEntryFromSdk, readModelCatalog, setModelCatalog, CLAUDE_DEFAULT_MODEL } from "./models.js";
35
+ import { modelEntryFromSdk, setModelCatalog, CLAUDE_DEFAULT_MODEL } from "./models.js";
36
36
  import { registerClaudePreset } from "./agent-preset-claude.js";
37
+ import { claudeLaunchFor } from "./claude-launch.js";
38
+ import { routeSeed } from "./claude-route.js";
39
+ import { endpointKey, readProviderStore } from "./claude-providers.js";
40
+ import { ClaudeProvidersGateway, syncFromNative } from "./claude-provider-gateway.js";
41
+ import { readNativeModel, readNativeProviders } from "./claude-native-config.js";
42
+ import { resolveClaudeBinary } from "./sdk-runtime.js";
37
43
  const CLAUDE_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "auto", "dontAsk"];
38
44
  /** Narrow an untrusted value to a persisted Claude permission mode. */
39
45
  function isClaudeMode(raw) {
@@ -56,6 +62,14 @@ const trace = (...parts) => {
56
62
  if (process.env.CLAUDE_TRACE === "1")
57
63
  process.stderr.write(`[claude-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
58
64
  };
65
+ /**
66
+ * How often the canonical store is reconciled with the user's `settings.json`
67
+ * env (2026-10-04 user ruling: startup + every ten minutes). The file is the
68
+ * user's own and edited with their own tools, so the sync is a poll rather
69
+ * than an event subscription — ten minutes is the granularity the ruling
70
+ * asked for.
71
+ */
72
+ const SYNC_INTERVAL_MS = 600_000;
59
73
  /**
60
74
  * Resume identity guard (exported for unit testing): fail closed when no Claude
61
75
  * session is recorded for the DSH session id. Route B (a mapping file) is V1+
@@ -118,6 +132,11 @@ export class ClaudeProvider extends Service {
118
132
  home;
119
133
  /** Echo/staleness guard for the app-wide default-model push. */
120
134
  defaultModelKey = "";
135
+ /** Epoch ms of the last reconcile with the user's `settings.json` env. */
136
+ syncedAt;
137
+ /** The live `ctx.llm` registration for the claude routes, when one exists.
138
+ * Its `replace()` is how the route SET follows the store (codex twin). */
139
+ routes;
121
140
  constructor(ctx) {
122
141
  super(ctx, "claudeProvider");
123
142
  this.runtime = { ctx };
@@ -127,6 +146,17 @@ export class ClaudeProvider extends Service {
127
146
  trace(`home: worldHome=${this.home} claudeHome=${resolveClaudeHome()}`);
128
147
  ctx.effect(() => ctx.agents.setFactory(this), "claudeProvider.setFactory()");
129
148
  this.#registerModelCatalog();
149
+ // The store is canonical and the native settings.json env is a source
150
+ // (2026-10-04). Reconcile once at startup, then keep reconciling on a
151
+ // timer; the sync is idempotent and write-only-when-moved, so a world
152
+ // with no providers configured pays nothing but a no-op read.
153
+ this.syncNow();
154
+ const timer = setInterval(() => this.syncNow(), SYNC_INTERVAL_MS);
155
+ timer.unref?.();
156
+ // Fiber-scoped cleanup (super-dsh/AGENTS unload contract): the timer must
157
+ // never outlive this row — HMR row unload disposes the fiber, which runs
158
+ // this effect's disposer.
159
+ ctx.effect(() => () => clearInterval(timer), "claudeProvider.syncTimer()");
130
160
  // The roster must be visible to the API gateway's root-level remote
131
161
  // enumeration; nesting it under a child fiber via ctx.plugin hides its
132
162
  // @Remote routes from the gateway and every agentPresets/* call 404s (the
@@ -140,6 +170,50 @@ export class ClaudeProvider extends Service {
140
170
  registerClaudePreset(ctx);
141
171
  this.#registerDefaultModel();
142
172
  this.#probeBootModels();
173
+ // The Hub settings panel's data plane (2026-10-04 multi-provider ruling):
174
+ // an adapter-owned Typert Remote (`claudeProviders/get|save|remove`) on
175
+ // this world's context. The hub's carrier forwards `envelope.method`
176
+ // verbatim, so the panel reaches it through the existing
177
+ // `/hub/api?agent=claude` family — no new hub route, and no provider
178
+ // knowledge leaks out of the adapter (dev-rules §12). Cordis keeps the
179
+ // instance alive as this context's `claudeProviders` service. There is no
180
+ // `select`: which provider a session runs on is DSH's per-session
181
+ // `model/selection`.
182
+ //
183
+ // `resolveClaudeHome` is passed as a THUNK on purpose (the codex twin's
184
+ // rule): the native home is re-resolved per call, so a test's
185
+ // `CLAUDE_CONFIG_DIR` and an operator's override are both honored.
186
+ //
187
+ // A panel save/remove re-points the route SET (one route per provider, plus
188
+ // the fixed `claude` route) through routes.replace — the codex twin's
189
+ // mechanism, wired exactly.
190
+ new ClaudeProvidersGateway(ctx, this.home, () => resolveClaudeHome(), () => this.refreshRoutes());
191
+ }
192
+ /**
193
+ * Reconcile the canonical provider store with the user's `settings.json`
194
+ * env (2026-10-04 user ruling) and stamp the last-reconcile epoch.
195
+ *
196
+ * Called once at startup and every {@link SYNC_INTERVAL_MS}. Mirrors the
197
+ * codex twin's semantics exactly — idempotent, silent, and
198
+ * write-only-when-moved (claude-providers.ts guarantees the store rewrite
199
+ * happens only when something actually changed) — minus codex's
200
+ * `#applyRoutes` step: claude serves every provider under the ONE fixed
201
+ * `claude` route registered at boot, so there is no route set to re-point.
202
+ * Never throws: a provider list is not worth a boot.
203
+ */
204
+ syncNow() {
205
+ try {
206
+ const { store, added, updated } = syncFromNative(this.home, resolveClaudeHome());
207
+ trace(`provider sync: added=${added.join(",") || "none"} updated=${updated.join(",") || "none"}`);
208
+ if (added.length > 0 || updated.length > 0) {
209
+ this.runtime.ctx.logger.info(`claude-provider: settings.json sync — added ${added.join(", ") || "none"}, refreshed ${updated.join(", ") || "none"}`);
210
+ }
211
+ this.syncedAt = Date.now();
212
+ this.#applyRoutes(store);
213
+ }
214
+ catch (error) {
215
+ this.runtime.ctx.logger.warn(`claude-provider: provider sync failed: ${String(error)}`);
216
+ }
143
217
  }
144
218
  /**
145
219
  * RC-3 boot model probe: ONE ephemeral SDK query asks the CLI's
@@ -150,7 +224,7 @@ export class ClaudeProvider extends Service {
150
224
  * probe-failure fallback, and a live session re-observes anyway.
151
225
  */
152
226
  #probeBootModels() {
153
- void probeClaudeModels()
227
+ void probeClaudeModels({ worldHome: this.home })
154
228
  .then((models) => {
155
229
  const entries = models.flatMap((model) => modelEntryFromSdk(model));
156
230
  if (entries.length === 0) {
@@ -222,21 +296,22 @@ export class ClaudeProvider extends Service {
222
296
  // The host-side permission gate is injected now, closing over the injected
223
297
  // approval/questions seams plus a lazy agent holder (set in setupAndPublish).
224
298
  const agentRef = { current: undefined };
225
- const client = new ClaudeSdkClient({
299
+ const canUseTool = makeClaudeCanUseTool({
300
+ approval: loopCtx.get("approval"),
301
+ questions: loopCtx.get("userQuestions"),
302
+ getAgent: () => agentRef.current,
303
+ });
304
+ // One factory per session: the shared wiring (canUseTool, worldHome, cwd,
305
+ // permission posture) plus per-instance overrides. The agent swaps the whole
306
+ // client through it when a provider move needs a different child.
307
+ const clientFactory = (overrides = {}) => new ClaudeSdkClient({
226
308
  cwd: spawnCwd,
227
- claudeSessionId,
228
- canUseTool: makeClaudeCanUseTool({
229
- approval: loopCtx.get("approval"),
230
- questions: loopCtx.get("userQuestions"),
231
- getAgent: () => agentRef.current,
232
- }),
309
+ worldHome: this.home,
310
+ canUseTool,
233
311
  allowDangerouslySkipPermissions: claudeMode === "bypassPermissions",
312
+ ...overrides,
234
313
  });
235
- // Apply the resolved boot/observed default to the CLI (mirrors codex's
236
- // create-time `setModel`); an explicit caller selection is honored by
237
- // `sessionAgentOptions` at publication.
238
- const createCatalog = readModelCatalog();
239
- void client.setModel(createCatalog.defaultModel ?? CLAUDE_DEFAULT_MODEL).catch(() => { });
314
+ const client = clientFactory({ claudeSessionId });
240
315
  const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
241
316
  ...(options.seed === undefined ? {} : { seed: options.seed }),
242
317
  ...(meta === undefined ? {} : { meta }),
@@ -271,7 +346,9 @@ export class ClaudeProvider extends Service {
271
346
  upsertSession(this.home, String(id), { resumable: false });
272
347
  trace(`marked ${id} not resumable (conversation never materialized)`);
273
348
  },
274
- }, agentRef);
349
+ launchFor: (providerId, model) => this.agentLaunchFor(providerId, model),
350
+ contextWindowFor: (providerId, model) => this.agentContextWindowFor(providerId, model),
351
+ }, agentRef, clientFactory);
275
352
  }
276
353
  catch (error) {
277
354
  trace(`create ${id} THREW: ${String(error)}`);
@@ -333,16 +410,19 @@ export class ClaudeProvider extends Service {
333
410
  // first prompt). Re-apply the persisted permission mode up front (buffered
334
411
  // until the query exists), and re-inject the host-side permission gate.
335
412
  const agentRef = { current: undefined };
336
- const client = new ClaudeSdkClient({
413
+ const canUseTool = makeClaudeCanUseTool({
414
+ approval: loopCtx.get("approval"),
415
+ questions: loopCtx.get("userQuestions"),
416
+ getAgent: () => agentRef.current,
417
+ });
418
+ const clientFactory = (overrides = {}) => new ClaudeSdkClient({
337
419
  cwd: spawnCwd,
338
- resumeSessionId: claudeSessionId,
339
- canUseTool: makeClaudeCanUseTool({
340
- approval: loopCtx.get("approval"),
341
- questions: loopCtx.get("userQuestions"),
342
- getAgent: () => agentRef.current,
343
- }),
420
+ worldHome: this.home,
421
+ canUseTool,
344
422
  allowDangerouslySkipPermissions: claudeMode === "bypassPermissions",
423
+ ...overrides,
345
424
  });
425
+ const client = clientFactory({ resumeSessionId: claudeSessionId });
346
426
  void client.setPermissionMode(claudeMode).catch(() => { });
347
427
  let handle;
348
428
  let stored;
@@ -397,7 +477,9 @@ export class ClaudeProvider extends Service {
397
477
  upsertSession(this.home, String(id), { resumable: false });
398
478
  trace(`marked ${id} not resumable (conversation never materialized)`);
399
479
  },
400
- }, agentRef);
480
+ launchFor: (providerId, model) => this.agentLaunchFor(providerId, model),
481
+ contextWindowFor: (providerId, model) => this.agentContextWindowFor(providerId, model),
482
+ }, agentRef, clientFactory);
401
483
  }
402
484
  catch (error) {
403
485
  trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
@@ -412,25 +494,104 @@ export class ClaudeProvider extends Service {
412
494
  }
413
495
  }
414
496
  #registerModelCatalog() {
497
+ this.#applyRoutes(readProviderStore(this.home));
498
+ }
499
+ /** Re-register the `ctx.llm` routes from the current store (panel mutation path). */
500
+ refreshRoutes() {
501
+ try {
502
+ this.#applyRoutes(readProviderStore(this.home));
503
+ }
504
+ catch (error) {
505
+ this.runtime.ctx.logger.warn(`claude-provider: route refresh failed: ${String(error)}`);
506
+ }
507
+ }
508
+ /** Register or atomically re-point the claude routes at the given store
509
+ * (the fixed `claude` route + one route per provider with models). */
510
+ #applyRoutes(store) {
415
511
  const llm = this.runtime.ctx.get("llm");
416
512
  if (llm === undefined)
417
513
  return;
418
- llm.registerAdapter([CLAUDE_PROVIDER_ID], new ClaudeLlmAdapter());
514
+ const ids = routeIds(store);
515
+ if (this.routes === undefined) {
516
+ this.routes = llm.registerAdapter([...ids], new ClaudeLlmAdapter(this.home));
517
+ trace(`routes registered: ${ids.join(", ")}`);
518
+ return;
519
+ }
520
+ // `replace` validates the whole candidate set before swapping; a throw here
521
+ // must not take the caller (a timer, or a panel save) down with it.
522
+ this.routes.replace([...ids]);
523
+ trace(`routes replaced: ${ids.join(", ") || "none"}`);
419
524
  }
420
525
  /**
421
526
  * The factory owns its sessions' route (user directive 2026-09-15): the
422
527
  * ambient `agentDefaultModel` selection is NOT authoritative here — a session
423
- * created by this factory always runs on the `claude` route, with the boot
424
- * default (or an observed default) unless the caller asked for a served
425
- * claude model.
528
+ * created by this factory starts from the reconciled pair below unless the
529
+ * caller asked for a route this world serves (the native `claude` route or a
530
+ * curated provider route).
426
531
  */
427
532
  sessionAgentOptions(requested) {
428
- const catalog = readModelCatalog();
429
- const explicit = requested?.provider === CLAUDE_PROVIDER_ID && typeof requested.model === "string" && requested.model !== ""
430
- ? requested.model
533
+ // 1. A caller-requested route wins when it names a pair this world serves:
534
+ // the fixed native `claude` route (any raw id — launch native, inject
535
+ // nothing) or a curated provider route whose models include the id.
536
+ const wanted = requested?.provider !== undefined && typeof requested.model === "string" && requested.model !== ""
537
+ ? requested
431
538
  : undefined;
432
- const model = explicit ?? catalog.defaultModel ?? CLAUDE_DEFAULT_MODEL;
433
- return { ...(requested ?? {}), provider: CLAUDE_PROVIDER_ID, ...(model === undefined ? {} : { model }) };
539
+ const served = wanted !== undefined && (wanted.provider === CLAUDE_PROVIDER_ID
540
+ || this.providerEntry(wanted.provider)?.models.some((model) => model.id === wanted.model) === true);
541
+ if (wanted !== undefined && served)
542
+ return { ...wanted, provider: wanted.provider, model: wanted.model };
543
+ // 2. Native fold model (the user's own settings.json env).
544
+ const nativeProvider = this.nativeProviderId();
545
+ const nativeModel = readNativeModel(resolveClaudeHome());
546
+ if (nativeProvider !== undefined && nativeModel !== undefined && nativeModel !== "") {
547
+ return { ...(requested ?? {}), provider: nativeProvider, model: nativeModel };
548
+ }
549
+ // 3. First provider's first model.
550
+ for (const entry of readProviderStore(this.home).providers) {
551
+ const model = entry.models[0]?.id;
552
+ if (model !== undefined)
553
+ return { ...(requested ?? {}), provider: entry.id, model };
554
+ }
555
+ // 4. Legacy boot default (claude/sonnet → NATIVE_LAUNCH).
556
+ return { ...(requested ?? {}), provider: CLAUDE_PROVIDER_ID, model: CLAUDE_DEFAULT_MODEL };
557
+ }
558
+ /** The store entry a provider id names, or `undefined`. */
559
+ providerEntry(providerId) {
560
+ return readProviderStore(this.home).providers.find((entry) => entry.id === providerId);
561
+ }
562
+ /** The provider id the user's own settings.json env folds to, or `undefined`. */
563
+ nativeProviderId() {
564
+ const table = readNativeProviders(resolveClaudeHome())[0];
565
+ if (table?.baseUrl === undefined)
566
+ return undefined;
567
+ const key = endpointKey(table.baseUrl);
568
+ return readProviderStore(this.home).providers.find((entry) => endpointKey(entry.baseUrl) === key)?.id;
569
+ }
570
+ /** The launch that pins one provider for a freshly spawned client. */
571
+ agentLaunchFor(providerId, model) {
572
+ const entry = this.providerEntry(providerId);
573
+ if (entry === undefined)
574
+ return undefined;
575
+ return claudeLaunchFor({ provider: providerId, model }, entry, resolveClaudeBinary());
576
+ }
577
+ /** The provider's own context window for one of its models, when known. */
578
+ agentContextWindowFor(providerId, model) {
579
+ const entry = this.providerEntry(providerId);
580
+ return entry?.models.find((candidate) => candidate.id === model)?.contextWindow;
581
+ }
582
+ /** The app-wide default selection (DSH-side `claude/<composite-or-boot>`). */
583
+ defaultModelSelection() {
584
+ const nativeProvider = this.nativeProviderId();
585
+ const nativeModel = readNativeModel(resolveClaudeHome());
586
+ if (nativeProvider !== undefined && nativeModel !== undefined && nativeModel !== "") {
587
+ return { provider: nativeProvider, model: nativeModel };
588
+ }
589
+ for (const entry of readProviderStore(this.home).providers) {
590
+ const model = entry.models[0]?.id;
591
+ if (model !== undefined)
592
+ return { provider: entry.id, model };
593
+ }
594
+ return { provider: CLAUDE_PROVIDER_ID, model: CLAUDE_DEFAULT_MODEL };
434
595
  }
435
596
  /**
436
597
  * App-wide default-model push (mirrors `agent-codex` `#registerDefaultModel`
@@ -447,8 +608,7 @@ export class ClaudeProvider extends Service {
447
608
  const service = mCtx.get("agentDefaultModel");
448
609
  if (service === undefined)
449
610
  return;
450
- const catalog = readModelCatalog();
451
- const target = { provider: CLAUDE_PROVIDER_ID, model: catalog.defaultModel ?? CLAUDE_DEFAULT_MODEL };
611
+ const target = this.defaultModelSelection();
452
612
  const targetKey = `${target.provider}/${target.model}`;
453
613
  const current = service.currentSelection?.();
454
614
  const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
@@ -487,7 +647,7 @@ async function appendUnstoredSuffix(stored, session) {
487
647
  * a module-level function (not a private method) because Cordis exposes the
488
648
  * provider through a tracing proxy, which breaks hard-private receivers.
489
649
  */
490
- async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo, agentRef) {
650
+ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo, agentRef, clientFactory) {
491
651
  let detachSession;
492
652
  let detachAgent;
493
653
  let agent;
@@ -497,8 +657,25 @@ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, sessi
497
657
  // module-level reset is belt-and-suspenders for a re-entry before the
498
658
  // agent exists (e.g. a throwing setup that re-publishes).
499
659
  resetProjectionState();
500
- agent = new ClaudeAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
660
+ agent = new ClaudeAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo, clientFactory);
501
661
  agentRef.current = agent;
662
+ // Seed THIS session's route only when its log has none (mirrors codex): the
663
+ // harness's `model/selection` is the durable per-session route, so an
664
+ // existing one is the authority and only a session that never named a route
665
+ // gets the boot default. Log-only — the agent resolves it each turn.
666
+ try {
667
+ const events = session.snapshotEvents();
668
+ const alreadyRouted = events.some((event) => event.type === "model/selection");
669
+ const seed = routeSeed(agentOptions);
670
+ if (!alreadyRouted && seed !== undefined) {
671
+ const selection = { provider: seed.provider, model: seed.model };
672
+ session.append("model/selection", selection);
673
+ trace(`session route seeded: ${selection.provider}/${selection.model}`);
674
+ }
675
+ }
676
+ catch (error) {
677
+ trace(`session route seeding failed: ${String(error)}`);
678
+ }
502
679
  // Composition-only setup on the unpublished agent scope.
503
680
  const commit = await setup?.(agent.ctx, agent);
504
681
  // Publish: flush the unstored suffix, enter + announce in order.
@@ -30,6 +30,7 @@
30
30
  * field: exactly one Claude app per world observes it, and the settings-UI
31
31
  * round trip constructs its own adapter instance.
32
32
  */
33
+ import { compositeModelId } from "./claude-route.js";
33
34
  /** The empty catalog after a legitimate empty observation (`setModelCatalog([])`). */
34
35
  const EMPTY_CATALOG = Object.freeze({ models: Object.freeze([]) });
35
36
  /**
@@ -97,6 +98,19 @@ export function readModelCatalog() {
97
98
  ...(catalog.defaultModel === undefined ? {} : { defaultModel: catalog.defaultModel }),
98
99
  });
99
100
  }
101
+ /**
102
+ * Mint one observed entry's catalog id as `${provider}/${id}` (the composite id
103
+ * the selector picks). Only the LIVE session's authoritative `supportedModels()`
104
+ * roster goes through this — the degraded single-entry catalog and the boot
105
+ * probe stay raw (observed-default stays raw-only-in-memory).
106
+ *
107
+ * @param provider - the CURRENT turn's provider (`#turnRoute?.provider ?? "native"`).
108
+ * @param entries - observed entries whose ids are RAW wire ids.
109
+ * @returns the entries with composite ids.
110
+ */
111
+ export function mintCatalogEntries(provider, entries) {
112
+ return entries.map((entry) => ({ ...entry, id: compositeModelId(provider, entry.id) }));
113
+ }
100
114
  /**
101
115
  * Derive the **degraded** single-entry catalog from a projected `session_init`
102
116
  * (Task 4) while `supportedModels()` has not answered yet.