@north-light/crouter 0.3.161 → 0.3.163

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 (130) hide show
  1. package/dist/api/client.d.ts +3 -1
  2. package/dist/api/client.js +6 -0
  3. package/dist/api/dto/modelauth.d.ts +32 -0
  4. package/dist/api/dto/modelauth.js +2 -1
  5. package/dist/api/routes.d.ts +1 -0
  6. package/dist/api/routes.js +1 -0
  7. package/dist/builtin-memory/00-runtime-base.md +1 -0
  8. package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
  9. package/dist/builtin-memory/init.md +38 -0
  10. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +88 -396
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +215 -419
  14. package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
  15. package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
  16. package/dist/clients/attach/__tests__/edit-diff.test.js +34 -0
  17. package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
  18. package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
  19. package/dist/clients/attach/render/chat-view.js +8 -3
  20. package/dist/clients/attach/render/crtr-output.js +31 -2
  21. package/dist/clients/attach/render/edit-diff.js +26 -2
  22. package/dist/clients/attach/render/tool-calls.d.ts +7 -2
  23. package/dist/clients/attach/render/tool-calls.js +25 -4
  24. package/dist/clients/attach/session/connection.d.ts +1 -1
  25. package/dist/clients/attach/session/editor-frame.d.ts +5 -1
  26. package/dist/clients/attach/session/editor-frame.js +21 -15
  27. package/dist/clients/attach/session/identity.d.ts +1 -1
  28. package/dist/clients/attach/viewer.js +441 -439
  29. package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
  30. package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
  31. package/dist/commands/__tests__/human.test.js +2 -0
  32. package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
  33. package/dist/commands/__tests__/search-contents.test.js +52 -0
  34. package/dist/commands/api-client.js +1 -0
  35. package/dist/commands/human/prompts.js +2 -2
  36. package/dist/commands/human/shared.d.ts +1 -0
  37. package/dist/commands/human/shared.js +7 -4
  38. package/dist/commands/memory/lint.d.ts +6 -0
  39. package/dist/commands/memory/lint.js +177 -35
  40. package/dist/commands/memory/read.js +22 -1
  41. package/dist/commands/memory/write.js +18 -4
  42. package/dist/commands/memory.js +1 -1
  43. package/dist/commands/search/contents.js +2 -2
  44. package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
  45. package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
  46. package/dist/commands/sys/setup-wizard.d.ts +8 -1
  47. package/dist/commands/sys/setup-wizard.js +209 -58
  48. package/dist/commands/sys/sync-deps.js +14 -18
  49. package/dist/commands/sys/sync-project-guidance.js +16 -19
  50. package/dist/core/__tests__/broker-launch-candidates.test.js +13 -3
  51. package/dist/core/__tests__/broker-sdk-wiring.test.js +53 -25
  52. package/dist/core/__tests__/full/broker-provider-retry.test.js +6 -122
  53. package/dist/core/__tests__/migration.test.js +8 -3
  54. package/dist/core/__tests__/model-pin-durability.test.js +7 -5
  55. package/dist/core/__tests__/model-routes.test.d.ts +1 -0
  56. package/dist/core/__tests__/model-routes.test.js +41 -0
  57. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
  58. package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
  59. package/dist/core/__tests__/on-read-identity.test.js +8 -15
  60. package/dist/core/__tests__/review-model-floor.test.js +2 -2
  61. package/dist/core/__tests__/revive.test.js +2 -2
  62. package/dist/core/__tests__/tmux-surface.test.js +10 -4
  63. package/dist/core/__tests__/worktree.test.js +3 -3
  64. package/dist/core/auth-file.d.ts +3 -2
  65. package/dist/core/auth-file.js +5 -3
  66. package/dist/core/canvas/extensions.d.ts +1 -1
  67. package/dist/core/canvas/extensions.js +18 -11
  68. package/dist/core/canvas/labels.d.ts +4 -5
  69. package/dist/core/canvas/labels.js +9 -9
  70. package/dist/core/canvas/types.d.ts +6 -1
  71. package/dist/core/command-manifests/schema.js +18 -5
  72. package/dist/core/command.js +17 -7
  73. package/dist/core/config.d.ts +3 -1
  74. package/dist/core/config.js +88 -2
  75. package/dist/core/configured-clis/invoker.js +2 -0
  76. package/dist/core/help.d.ts +3 -1
  77. package/dist/core/help.js +5 -2
  78. package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
  79. package/dist/core/keybindings/catalog.d.ts +3 -2
  80. package/dist/core/keybindings/catalog.js +34 -25
  81. package/dist/core/keybindings/index.d.ts +1 -1
  82. package/dist/core/keybindings/resolve.js +8 -0
  83. package/dist/core/keybindings/types.d.ts +6 -1
  84. package/dist/core/memory/doc-link-grammar.d.ts +20 -0
  85. package/dist/core/memory/doc-link-grammar.js +110 -0
  86. package/dist/core/memory-resolver.d.ts +5 -0
  87. package/dist/core/memory-resolver.js +12 -1
  88. package/dist/core/model-routes.d.ts +37 -0
  89. package/dist/core/model-routes.js +140 -0
  90. package/dist/core/runtime/bearings.d.ts +8 -8
  91. package/dist/core/runtime/bearings.js +20 -16
  92. package/dist/core/runtime/broker.d.ts +0 -4
  93. package/dist/core/runtime/broker.js +119 -385
  94. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  95. package/dist/core/runtime/canvas-extensions.js +2 -0
  96. package/dist/core/runtime/launch.d.ts +10 -12
  97. package/dist/core/runtime/launch.js +16 -22
  98. package/dist/core/runtime/managed-provider-cooling.d.ts +0 -14
  99. package/dist/core/runtime/managed-provider-cooling.js +0 -19
  100. package/dist/core/runtime/model-swap.js +7 -1
  101. package/dist/core/runtime/promote.js +1 -0
  102. package/dist/core/runtime/tmux.js +136 -100
  103. package/dist/core/scope.js +4 -5
  104. package/dist/core/subscription-state.d.ts +12 -36
  105. package/dist/core/subscription-state.js +19 -203
  106. package/dist/core/substrate/ceiling.d.ts +3 -6
  107. package/dist/core/substrate/ceiling.js +13 -15
  108. package/dist/core/substrate/index.d.ts +2 -3
  109. package/dist/core/substrate/index.js +2 -2
  110. package/dist/core/substrate/on-read.d.ts +4 -13
  111. package/dist/core/substrate/on-read.js +142 -262
  112. package/dist/core/substrate/render.js +4 -4
  113. package/dist/core/substrate/schema.d.ts +3 -2
  114. package/dist/core/substrate/schema.js +5 -5
  115. package/dist/daemon/api/__tests__/full/api-server.test.js +9 -6
  116. package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +123 -1
  117. package/dist/daemon/api/handlers/modelauth.js +51 -2
  118. package/dist/daemon/api/handlers/nodes.js +2 -0
  119. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
  120. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
  121. package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
  122. package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
  123. package/dist/pi-extensions/canvas-tool-guide.js +74 -0
  124. package/dist/types.d.ts +17 -0
  125. package/dist/web-client/assets/{index-B76ZKfT_.js → index-NIuSCOHM.js} +1 -1
  126. package/dist/web-client/index.html +1 -1
  127. package/dist/web-client/sw.js +1 -1
  128. package/package.json +1 -1
  129. package/runtime.lock.json +2 -2
  130. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
@@ -1,11 +1,13 @@
1
- import type { ThinkingLevel } from "@earendil-works/pi-ai";
2
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
1
  export declare const ANTHROPIC_PROVIDER_ID = "anthropic";
4
2
  export declare const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
5
3
  export declare const INVALID_REFRESH_TOKEN_BACKOFF_MS: number;
6
4
  export type ManagedProviderId = typeof ANTHROPIC_PROVIDER_ID | typeof OPENAI_CODEX_PROVIDER_ID;
7
- export type LadderProviderId = "anthropic" | "openai";
8
5
  export type ModelStrength = "ultra" | "strong" | "medium" | "light";
6
+ export type RotationConfig = {
7
+ defaultFallbackStrength?: ModelStrength;
8
+ revertWhenAvailable?: boolean;
9
+ };
10
+ type RotationConfigUpdate = RotationConfig;
9
11
  export type SubscriptionCredential = {
10
12
  label: string;
11
13
  refresh: string;
@@ -17,27 +19,6 @@ export type SubscriptionCredential = {
17
19
  authFailure?: "invalid_grant";
18
20
  accountId?: string;
19
21
  };
20
- export type ModelRef = {
21
- providerId: ManagedProviderId;
22
- modelId: string;
23
- };
24
- export type RotationConfig = {
25
- defaultFallbackStrength?: ModelStrength;
26
- revertWhenAvailable?: boolean;
27
- preferredModel?: ModelRef;
28
- };
29
- type RotationConfigUpdate = {
30
- defaultFallbackStrength?: ModelStrength;
31
- revertWhenAvailable?: boolean;
32
- preferredModel?: ModelRef | null;
33
- };
34
- export type FallbackTarget = {
35
- providerId: ManagedProviderId;
36
- modelId: string;
37
- label: string;
38
- strength: ModelStrength;
39
- thinkingLevel?: ThinkingLevel;
40
- };
41
22
  /** A concrete provider/model considered for either broker launch or turn-time rotation. */
42
23
  export type ProviderCandidate = {
43
24
  providerId: string;
@@ -49,6 +30,12 @@ export type ProviderCandidate = {
49
30
  coolingUntil?: number;
50
31
  /** False when this candidate may only be selected by an explicit request, never as fallback. */
51
32
  automaticFallback?: boolean;
33
+ /** Route provenance carried through selection diagnostics. */
34
+ routeId?: string;
35
+ credentialSource?: string;
36
+ family?: string;
37
+ strength?: ModelStrength;
38
+ rank?: number;
52
39
  };
53
40
  export type ProviderCandidateRejection = "unregistered" | "non-managed-fallback" | "unauthenticated" | "cooling";
54
41
  /**
@@ -72,12 +59,6 @@ export declare function resolveProviderCandidates(candidates: readonly ProviderC
72
59
  }>;
73
60
  };
74
61
  export declare function isManagedProvider(providerId: string | undefined): providerId is ManagedProviderId;
75
- /** The `modelLadders` CONFIG key for a runtime provider id -- `openai-codex`
76
- * (the runtime/auth id) maps to the `openai` ladder key used in
77
- * ~/.crouter/config.json. Callers building a repair command that a user can
78
- * paste into `crtr sys config set modelLadders.<key>...` must use THIS key,
79
- * not the runtime provider id. */
80
- export declare function getLadderProviderId(providerId: ManagedProviderId): LadderProviderId;
81
62
  export declare function getProviderLabel(providerId: ManagedProviderId): string;
82
63
  export declare function mutateSubscriptionPool(providerId: ManagedProviderId, fn: (pool: SubscriptionCredential[], assertLockHeld: () => void) => SubscriptionCredential[] | undefined | Promise<SubscriptionCredential[] | undefined>): Promise<SubscriptionCredential[]>;
83
64
  export declare function refreshSubscriptionCredential(providerId: ManagedProviderId, label: string, exchange: (refreshToken: string) => Promise<{
@@ -118,6 +99,7 @@ export declare function commitManagedLogin(providerId: ManagedProviderId, label:
118
99
  lastRateLimitedAt: number;
119
100
  autoLabel?: boolean;
120
101
  }): Promise<SubscriptionCredential[]>;
102
+ export declare function removeAllManagedAccounts(providerId: ManagedProviderId): Promise<number>;
121
103
  export declare function removeManagedProvider(providerId: ManagedProviderId, label?: string): Promise<SubscriptionCredential[]>;
122
104
  export declare function removeSubscription(providerId: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
123
105
  export declare function promoteSubscription(providerId: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
@@ -128,11 +110,5 @@ export declare function markSubscriptionSuccess(providerId: ManagedProviderId, r
128
110
  export declare function parseRetryAfterHeader(value: string | undefined, now?: number): number | undefined;
129
111
  export declare function readRotationConfig(): RotationConfig;
130
112
  export declare function writeRotationConfig(next: RotationConfigUpdate): RotationConfig;
131
- export declare function clearPreferredModel(): RotationConfig;
132
- export declare function rememberPreferredModel(model: ModelRef): RotationConfig;
133
- export declare function resolveFallbackTarget(currentProviderId: ManagedProviderId, currentModelId?: string, config?: RotationConfig, currentThinkingLevel?: string): FallbackTarget | undefined;
134
- export declare function shouldRestorePreferredModel(config?: RotationConfig, now?: number): boolean;
135
- export declare function restorePreferredModelIfPossible(piApi: ExtensionAPI, ctx: Pick<ExtensionContext, "model" | "modelRegistry">): Promise<boolean>;
136
- export declare function switchToFallbackIfPossible(piApi: ExtensionAPI, ctx: Pick<ExtensionContext, "model" | "modelRegistry">, preferredModel?: ModelRef | undefined): Promise<FallbackTarget | undefined>;
137
113
  export declare function formatStatusLine(providerId: ManagedProviderId, now?: number): string;
138
114
  export {};
@@ -1,6 +1,5 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
- import { defaultModelLaddersConfig } from "../types.js";
4
3
  import { homedir } from "node:os";
5
4
  import { dirname, join } from "node:path";
6
5
  import { LockedJsonFile } from "./locked-file.js";
@@ -61,19 +60,14 @@ function agentDir() {
61
60
  function rotationConfigFile() {
62
61
  return join(agentDir(), "provider-rotation.json");
63
62
  }
64
- function crouterConfigFile() {
65
- return join(homedir(), ".crouter", "config.json");
66
- }
67
63
  const DEFAULT_FALLBACK_STRENGTH = "strong";
68
64
  const PROVIDERS = {
69
65
  [ANTHROPIC_PROVIDER_ID]: {
70
66
  label: "Claude",
71
- ladderProviderId: "anthropic",
72
67
  poolFileName: "anthropic-subscriptions.json",
73
68
  },
74
69
  [OPENAI_CODEX_PROVIDER_ID]: {
75
70
  label: "OpenAI Codex",
76
- ladderProviderId: "openai",
77
71
  poolFileName: "openai-codex-subscriptions.json",
78
72
  },
79
73
  };
@@ -184,9 +178,6 @@ function traceDroppedRows(providerId, stored, normalized) {
184
178
  return;
185
179
  process.stderr.write(`[subscription-pool] ${getProviderLabel(providerId)}: dropping ${stored.length - normalized.length} stored row(s) with no label or no token pair\n`);
186
180
  }
187
- function getCurrentModelRef(model) {
188
- return isManagedProvider(model?.provider) ? { providerId: model.provider, modelId: model.id } : undefined;
189
- }
190
181
  // A refreshed/modified token set is only storable when it carries BOTH halves and a usable
191
182
  // expiry. Persisting a partial one is silently destructive: `JSON.stringify` drops the missing
192
183
  // key, and `normalizeCredential` (which requires refresh AND access together) then discards the
@@ -247,14 +238,6 @@ function logCredentialSetChange(providerId, base, next) {
247
238
  export function isManagedProvider(providerId) {
248
239
  return providerId === ANTHROPIC_PROVIDER_ID || providerId === OPENAI_CODEX_PROVIDER_ID;
249
240
  }
250
- /** The `modelLadders` CONFIG key for a runtime provider id -- `openai-codex`
251
- * (the runtime/auth id) maps to the `openai` ladder key used in
252
- * ~/.crouter/config.json. Callers building a repair command that a user can
253
- * paste into `crtr sys config set modelLadders.<key>...` must use THIS key,
254
- * not the runtime provider id. */
255
- export function getLadderProviderId(providerId) {
256
- return PROVIDERS[providerId].ladderProviderId;
257
- }
258
241
  export function getProviderLabel(providerId) {
259
242
  return PROVIDERS[providerId].label;
260
243
  }
@@ -603,14 +586,26 @@ export async function commitManagedLogin(providerId, label, credential) {
603
586
  return merged;
604
587
  });
605
588
  }
589
+ // Remove every account for one managed provider through a single locked transform. The
590
+ // returned count belongs to that locked snapshot, so callers do not race a concurrent login.
591
+ export async function removeAllManagedAccounts(providerId) {
592
+ let removed = 0;
593
+ await mutateSubscriptionPool(providerId, (pool) => {
594
+ removed = pool.length;
595
+ return [];
596
+ });
597
+ return removed;
598
+ }
606
599
  // Coordinated removal for the human UI. Removes a single account when `label` is given; the ENTIRE
607
- // provider (every pool account) when `label` is omitted — always through removeManagedAccount. UI
600
+ // provider (every pool account) when `label` is omitted — always through a pool transform. UI
608
601
  // must NEVER reach for a raw auth.json delete on a managed provider: that strips a file pi no
609
602
  // longer reads for managed providers and orphans the whole pool.
610
603
  export async function removeManagedProvider(providerId, label) {
604
+ if (label === undefined) {
605
+ await removeAllManagedAccounts(providerId);
606
+ return [];
607
+ }
611
608
  return mutateSubscriptionPool(providerId, (pool) => {
612
- if (label === undefined)
613
- return [];
614
609
  const index = resolveCredentialIndex(pool, label);
615
610
  if (index === -1)
616
611
  throw new Error(`Unknown ${getProviderLabel(providerId)} subscription: ${label}`);
@@ -701,9 +696,6 @@ export async function markSubscriptionSuccess(providerId, ref, attemptAt = Date.
701
696
  return next;
702
697
  });
703
698
  }
704
- function findAvailableSubscription(providerId, now = Date.now()) {
705
- return readSubscriptionPool(providerId).find((entry) => !entry.rateLimitedUntil || entry.rateLimitedUntil <= now);
706
- }
707
699
  export function parseRetryAfterHeader(value, now = Date.now()) {
708
700
  if (!value)
709
701
  return undefined;
@@ -716,102 +708,11 @@ export function parseRetryAfterHeader(value, now = Date.now()) {
716
708
  const parsed = Date.parse(trimmed);
717
709
  return Number.isFinite(parsed) ? Math.max(0, parsed - now) : undefined;
718
710
  }
719
- function thinkingLevelOfRef(ref) {
720
- const match = ref.trim().match(/:(thinking|off|minimal|low|medium|high|xhigh|max)$/i);
721
- if (!match)
722
- return undefined;
723
- const level = match[1].toLowerCase();
724
- return level === "off" || level === "thinking" ? undefined : level;
725
- }
726
- function normalizeModelRef(ref) {
727
- return ref
728
- .trim()
729
- .replace(/^[^/]+\//, "")
730
- .replace(/:(?:thinking|off|minimal|low|medium|high|xhigh|max)$/i, "")
731
- .toLowerCase();
732
- }
733
- function splitModelRef(ref) {
734
- const trimmed = ref.trim();
735
- const slash = trimmed.indexOf("/");
736
- const providerId = slash === -1 ? OPENAI_CODEX_PROVIDER_ID : trimmed.slice(0, slash).trim();
737
- if (!isManagedProvider(providerId)) {
738
- throw new Error(`Unsupported fallback provider: ${providerId}`);
739
- }
740
- const rawModelId = slash === -1 ? trimmed : trimmed.slice(slash + 1).trim();
741
- const thinkingMatch = rawModelId.match(/^(.*?)(?::(thinking|off|minimal|low|medium|high|xhigh|max))$/i);
742
- return {
743
- providerId,
744
- modelId: thinkingMatch ? thinkingMatch[1] : rawModelId,
745
- label: `${providerId}/${thinkingMatch ? thinkingMatch[1] : rawModelId}${thinkingMatch?.[2] ? `:${thinkingMatch[2].toLowerCase()}` : ""}`,
746
- strength: DEFAULT_FALLBACK_STRENGTH,
747
- thinkingLevel: thinkingMatch?.[2] && thinkingMatch[2].toLowerCase() !== "off" && thinkingMatch[2].toLowerCase() !== "thinking"
748
- ? thinkingMatch[2].toLowerCase()
749
- : undefined,
750
- };
751
- }
752
- // Layers ~/.crouter/config.json's `modelLadders` (if any) OVER crouter's
753
- // builtin ladder (`defaultModelLaddersConfig()`, src/types.ts — imported
754
- // directly: this package's canonical home is the crouter tree, so the
755
- // relative import always resolves, to dist/types.js at runtime), per
756
- // provider/strength — mirroring crouter's own
757
- // `mergeModelLadders` (src/core/config.ts), which is what a node's launcher
758
- // actually resolves models from. Without this fallback, a config-less user
759
- // (the common case: fresh install, no ~/.crouter/config.json) saw an EMPTY
760
- // ladder here while crtr itself launched them on the builtin defaults —
761
- // rotation could then never resolve a strength tier or a cross-provider
762
- // fallback for a model it was already running. Always returns a fully
763
- // populated ladder for both providers; a config.json override only replaces
764
- // the specific strengths it sets.
765
- function readModelLadders() {
766
- const config = readJsonFile(crouterConfigFile());
767
- const overrides = config?.modelLadders;
768
- const builtin = defaultModelLaddersConfig();
769
- const result = {};
770
- for (const provider of ["anthropic", "openai"]) {
771
- const normalized = { ...builtin[provider] };
772
- const override = overrides?.[provider];
773
- if (override) {
774
- for (const strength of ["ultra", "strong", "medium", "light"]) {
775
- const value = typeof override[strength] === "string" ? override[strength]?.trim() : "";
776
- if (value)
777
- normalized[strength] = value;
778
- }
779
- }
780
- result[provider] = normalized;
781
- }
782
- return result;
783
- }
784
- function findStrengthForModelId(providerId, modelId, defaultStrength, ladders = readModelLadders(), currentThinkingLevel) {
785
- const source = ladders?.[PROVIDERS[providerId].ladderProviderId];
786
- const bareId = modelId ? normalizeModelRef(modelId) : undefined;
787
- if (!source || !bareId)
788
- return undefined;
789
- const matches = [];
790
- for (const strength of ["ultra", "strong", "medium", "light"]) {
791
- const ref = source[strength];
792
- if (ref && normalizeModelRef(ref) === bareId)
793
- matches.push(strength);
794
- }
795
- if (matches.length === 0)
796
- return undefined;
797
- // Rungs that share a bare model id (e.g. gpt-5.6-sol:max vs gpt-5.6-sol:high) are
798
- // disambiguated by the live reasoning level so an ultra run doesn't collapse to strong.
799
- if (matches.length > 1 && currentThinkingLevel) {
800
- const exact = matches.find((strength) => thinkingLevelOfRef(source[strength]) === currentThinkingLevel);
801
- if (exact)
802
- return exact;
803
- }
804
- return matches.includes(defaultStrength) ? defaultStrength : matches[0];
805
- }
806
711
  export function readRotationConfig() {
807
712
  const raw = readJsonFile(rotationConfigFile()) ?? {};
808
- const preferredModel = raw.preferredModel && isManagedProvider(raw.preferredModel.providerId) && typeof raw.preferredModel.modelId === "string"
809
- ? { providerId: raw.preferredModel.providerId, modelId: raw.preferredModel.modelId.trim() }
810
- : undefined;
811
713
  return {
812
714
  defaultFallbackStrength: normalizeStrength(raw.defaultFallbackStrength) ?? DEFAULT_FALLBACK_STRENGTH,
813
715
  revertWhenAvailable: raw.revertWhenAvailable ?? true,
814
- preferredModel,
815
716
  };
816
717
  }
817
718
  export function writeRotationConfig(next) {
@@ -819,100 +720,15 @@ export function writeRotationConfig(next) {
819
720
  const merged = {
820
721
  defaultFallbackStrength: normalizeStrength(next.defaultFallbackStrength) ?? current.defaultFallbackStrength ?? DEFAULT_FALLBACK_STRENGTH,
821
722
  revertWhenAvailable: next.revertWhenAvailable ?? current.revertWhenAvailable ?? true,
822
- preferredModel: Object.prototype.hasOwnProperty.call(next, "preferredModel")
823
- ? next.preferredModel || undefined
824
- : current.preferredModel,
825
723
  };
826
724
  writeJsonFile(rotationConfigFile(), merged);
827
725
  return merged;
828
726
  }
829
- export function clearPreferredModel() {
830
- return writeRotationConfig({ preferredModel: null });
831
- }
832
- export function rememberPreferredModel(model) {
833
- return writeRotationConfig({ preferredModel: model });
834
- }
835
- // `readModelLadders()` always fills both providers from the builtin defaults, so this
836
- // only returns undefined (never throws) in the defensive case where a rung is still
837
- // genuinely missing; callers must treat that as a graceful "no fallback available" case
838
- // rather than a config error to surface mid-turn.
839
- export function resolveFallbackTarget(currentProviderId, currentModelId, config = readRotationConfig(), currentThinkingLevel) {
840
- const ladders = readModelLadders();
841
- const defaultStrength = config.defaultFallbackStrength ?? DEFAULT_FALLBACK_STRENGTH;
842
- const sourceStrength = findStrengthForModelId(currentProviderId, currentModelId, defaultStrength, ladders, currentThinkingLevel);
843
- const strength = sourceStrength ?? defaultStrength;
844
- const targetProvider = currentProviderId === ANTHROPIC_PROVIDER_ID ? OPENAI_CODEX_PROVIDER_ID : ANTHROPIC_PROVIDER_ID;
845
- const targetLadder = ladders?.[PROVIDERS[targetProvider].ladderProviderId];
846
- const candidates = strength === defaultStrength ? [strength] : [strength, defaultStrength];
847
- const fallbackRef = candidates.map((candidate) => targetLadder?.[candidate]).find((ref) => typeof ref === "string" && ref.trim().length > 0);
848
- if (!fallbackRef) {
849
- return undefined;
850
- }
851
- const parsed = splitModelRef(fallbackRef);
852
- const resolvedStrength = candidates.find((candidate) => targetLadder?.[candidate] === fallbackRef) ?? strength;
853
- return { ...parsed, strength: resolvedStrength };
854
- }
855
- export function shouldRestorePreferredModel(config = readRotationConfig(), now = Date.now()) {
856
- if (!config.revertWhenAvailable || !config.preferredModel)
857
- return false;
858
- return Boolean(findAvailableSubscription(config.preferredModel.providerId, now));
859
- }
860
- export async function restorePreferredModelIfPossible(piApi, ctx) {
861
- const config = readRotationConfig();
862
- if (!shouldRestorePreferredModel(config, Date.now()))
863
- return false;
864
- const preferred = config.preferredModel;
865
- if (!preferred)
866
- return false;
867
- const current = getCurrentModelRef(ctx.model);
868
- if (current?.providerId === preferred.providerId && current.modelId === preferred.modelId) {
869
- clearPreferredModel();
870
- return true;
871
- }
872
- const model = ctx.modelRegistry.find(preferred.providerId, preferred.modelId);
873
- if (!model)
874
- return false;
875
- const switched = await piApi.setModel(model);
876
- if (switched)
877
- clearPreferredModel();
878
- return switched;
879
- }
880
- export async function switchToFallbackIfPossible(piApi, ctx, preferredModel = getCurrentModelRef(ctx.model)) {
881
- if (!preferredModel)
882
- return undefined;
883
- rememberPreferredModel(preferredModel);
884
- const currentThinkingLevel = piApi.getThinkingLevel?.();
885
- const target = resolveFallbackTarget(preferredModel.providerId, preferredModel.modelId, readRotationConfig(), currentThinkingLevel);
886
- if (!target)
887
- return undefined;
888
- const current = getCurrentModelRef(ctx.model);
889
- const model = ctx.modelRegistry.find(target.providerId, target.modelId);
890
- if (!model)
891
- return undefined;
892
- if (current?.providerId !== target.providerId || current.modelId !== target.modelId) {
893
- const switched = await piApi.setModel(model);
894
- if (!switched) {
895
- // `setModel` returns false when its target lacks configured auth and leaves
896
- // the live model unchanged. A concurrent transition can still independently
897
- // make THIS exact target current while this call is in flight, so re-read
898
- // the live model and accept only when it now equals the target on both provider and
899
- // model id; any other current model is a genuine failed switch.
900
- const afterSwitch = getCurrentModelRef(ctx.model);
901
- if (afterSwitch?.providerId !== target.providerId || afterSwitch.modelId !== target.modelId)
902
- return undefined;
903
- }
904
- }
905
- if (target.thinkingLevel)
906
- piApi.setThinkingLevel(target.thinkingLevel);
907
- return target;
908
- }
909
727
  export function formatStatusLine(providerId, now = Date.now()) {
910
728
  const pool = readSubscriptionPool(providerId);
911
- const ready = pool.filter((entry) => !entry.rateLimitedUntil || entry.rateLimitedUntil <= now);
729
+ const ready = pool.filter((entry) => entry.authFailure === undefined && (!entry.rateLimitedUntil || entry.rateLimitedUntil <= now));
912
730
  const label = getProviderLabel(providerId);
913
- if (ready.length === 0) {
914
- const fallback = resolveFallbackTarget(providerId, undefined, readRotationConfig());
915
- return fallback ? `${label} pool exhausted -> ${fallback.label}` : `${label} pool exhausted (no fallback configured)`;
916
- }
917
- return `${label} ${ready[0]?.label ?? providerId} (${ready.length}/${pool.length} ready)`;
731
+ return ready.length === 0
732
+ ? `${label} pool exhausted`
733
+ : `${label} ${ready[0]?.label ?? providerId} (${ready.length}/${pool.length} ready)`;
918
734
  }
@@ -8,13 +8,10 @@ export declare function isIndexName(name: string): boolean;
8
8
  export declare function indexDirOf(name: string): string;
9
9
  /** Returns the display path corresponding to a canonical logical identifier. */
10
10
  export declare function displayName(name: string): string;
11
- /** The two visibility surfaces an INDEX can cap. */
12
- export type Surface = 'systemPromptVisibility' | 'fileReadVisibility';
13
11
  /** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
14
12
  * callers MUST pass docs precedence-ordered (project > user > builtin) — the
15
13
  * highest-precedence INDEX governs a dir, mirroring the memory resolver. */
16
14
  export declare function buildCeilingIndex(docs: SubstrateDoc[]): Map<string, SubstrateDoc>;
17
- /** A doc's effective rung on `surface` after applying every ancestor INDEX
18
- * ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
19
- * hides the whole subtree (min with none = none). */
20
- export declare function effectiveRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>, surface: Surface): Rung;
15
+ /** A doc's effective system-prompt rung after applying every ancestor INDEX
16
+ * ceiling. A `none` ancestor hides the whole subtree from the boot catalog. */
17
+ export declare function effectiveSystemPromptRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>): Rung;
@@ -1,16 +1,15 @@
1
- // ceiling.ts — canonical INDEX identifiers and the subtree rung ceiling.
1
+ // ceiling.ts — canonical INDEX identifiers and the boot-catalog subtree ceiling.
2
2
  //
3
- // A directory index has the canonical logical identifier `<directory>/INDEX`
4
- // (`INDEX` at the root) and the same substrate frontmatter as any doc. Its rung
5
- // is a ceiling for every document in its subtree.
3
+ // A directory INDEX has the canonical logical identifier `<directory>/INDEX`
4
+ // (`INDEX` at the root). Its system-prompt rung caps every document in that
5
+ // subtree while the boot catalog renders:
6
6
  //
7
7
  // effective rung of a descendant = min(own rung, every ancestor INDEX's rung)
8
8
  //
9
- // So a directory's INDEX at `preview` lets its files surface at most at
10
- // `preview`; an INDEX at `none` hides the whole subtree. This is the single,
11
- // explicit mechanism for collapsing a directory in agent-facing surfaces (boot
12
- // render + on-read) — there is no parallel auto-hiding heuristic. See design
13
- // taste: `crtr memory read taste/document-substrate`.
9
+ // A `preview` INDEX therefore lets its descendants surface at most at preview;
10
+ // a `none` INDEX hides the subtree. File context does not use this ceiling:
11
+ // every workspace/file match is an explicit `applies-to` route and renders at
12
+ // the matched document's own file-read rung.
14
13
  import { rungRank } from './schema.js';
15
14
  /** The reserved basename (no extension) that marks a directory's index doc. */
16
15
  export const INDEX_NAME = 'INDEX';
@@ -55,15 +54,14 @@ function ancestorDirs(name) {
55
54
  out.push(parts.slice(0, i).join('/'));
56
55
  return out;
57
56
  }
58
- /** A doc's effective rung on `surface` after applying every ancestor INDEX
59
- * ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
60
- * hides the whole subtree (min with none = none). */
61
- export function effectiveRung(doc, ceil, surface) {
62
- let rung = doc[surface];
57
+ /** A doc's effective system-prompt rung after applying every ancestor INDEX
58
+ * ceiling. A `none` ancestor hides the whole subtree from the boot catalog. */
59
+ export function effectiveSystemPromptRung(doc, ceil) {
60
+ let rung = doc.systemPromptVisibility;
63
61
  for (const dir of ancestorDirs(doc.name)) {
64
62
  const idx = ceil.get(dir);
65
63
  if (idx)
66
- rung = minRung(rung, idx[surface]);
64
+ rung = minRung(rung, idx.systemPromptVisibility);
67
65
  }
68
66
  return rung;
69
67
  }
@@ -4,8 +4,7 @@ export { scopeForCwd, profileNameFor } from './subject-fields.js';
4
4
  export type { NodeConfigSubject } from './subject-fields.js';
5
5
  export { spineDepth, assembleNodeSubject } from './subject.js';
6
6
  export { gatePasses } from './gate.js';
7
- export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
8
- export type { Surface } from './ceiling.js';
7
+ export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
9
8
  export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
10
9
  export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
11
- export { renderOnReadDocs } from './on-read.js';
10
+ export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
@@ -19,7 +19,7 @@ resolveDocName, } from './schema.js';
19
19
  export { scopeForCwd, profileNameFor } from './subject-fields.js';
20
20
  export { spineDepth, assembleNodeSubject } from './subject.js';
21
21
  export { gatePasses } from './gate.js';
22
- export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
22
+ export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveSystemPromptRung, } from './ceiling.js';
23
23
  export { renderPreferencesForSubject, renderKnowledgeForSubject } from './render.js';
24
24
  export { renderPreferencesSection, renderKnowledgeBlock } from './render-node.js';
25
- export { renderOnReadDocs } from './on-read.js';
25
+ export { renderOnReadDocs, renderWorkspaceOpenDocs } from './on-read.js';
@@ -1,14 +1,5 @@
1
- /**
2
- * Render the substrate docs that should surface alongside a just-read file.
3
- *
4
- * @param nodeId the canvas node whose subject gates the docs.
5
- * @param readFilePath the path the `read` tool returned (absolute or not — it
6
- * is resolved to a realpath internally).
7
- * @param seen the CALLER-owned, per-session set of already-injected
8
- * doc realpaths. Docs already present are skipped; newly
9
- * injected docs are added. Pass the same set across reads
10
- * within a session (clear it on session_start) to get the
11
- * once-per-session dedup; omit it for a standalone render.
12
- * @returns the `<auto-loaded-context>` envelope, or '' when nothing surfaces.
13
- */
1
+ /** Surface docs matched by a successful read tool call. */
14
2
  export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
3
+ /** Surface workspace-wide docs during first-message assembly. `.` is a reserved
4
+ * applies-to target meaning “when this project store is mounted by cwd/profile”. */
5
+ export declare function renderWorkspaceOpenDocs(nodeId: string): string;