pilotswarm-sdk 0.5.41 → 0.5.43

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 (149) hide show
  1. package/README.md +2 -1
  2. package/api/index.d.ts +22 -1
  3. package/api/index.js +5 -0
  4. package/api/src/api-client.js +20 -1
  5. package/api/src/http-api-transport.js +102 -1
  6. package/api/src/protocol.js +39 -4
  7. package/api/src/surface-contract.js +44 -0
  8. package/dist/agent-manager-tools.d.ts +2 -0
  9. package/dist/agent-manager-tools.d.ts.map +1 -1
  10. package/dist/agent-manager-tools.js +24 -15
  11. package/dist/agent-manager-tools.js.map +1 -1
  12. package/dist/agent-package-service.d.ts +9 -0
  13. package/dist/agent-package-service.d.ts.map +1 -1
  14. package/dist/agent-package-service.js +46 -2
  15. package/dist/agent-package-service.js.map +1 -1
  16. package/dist/client.d.ts +2 -0
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +82 -19
  19. package/dist/client.js.map +1 -1
  20. package/dist/cms-migrations.d.ts.map +1 -1
  21. package/dist/cms-migrations.js +3538 -0
  22. package/dist/cms-migrations.js.map +1 -1
  23. package/dist/cms.d.ts +25 -0
  24. package/dist/cms.d.ts.map +1 -1
  25. package/dist/cms.js +43 -4
  26. package/dist/cms.js.map +1 -1
  27. package/dist/index.d.ts +7 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +5 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/managed-session.d.ts +9 -1
  32. package/dist/managed-session.d.ts.map +1 -1
  33. package/dist/managed-session.js +40 -8
  34. package/dist/managed-session.js.map +1 -1
  35. package/dist/management-client.d.ts +410 -1
  36. package/dist/management-client.d.ts.map +1 -1
  37. package/dist/management-client.js +1254 -78
  38. package/dist/management-client.js.map +1 -1
  39. package/dist/model-providers.d.ts +13 -3
  40. package/dist/model-providers.d.ts.map +1 -1
  41. package/dist/model-providers.js +50 -13
  42. package/dist/model-providers.js.map +1 -1
  43. package/dist/orchestration/index.d.ts +2 -2
  44. package/dist/orchestration/index.js +1 -1
  45. package/dist/orchestration/lifecycle.d.ts.map +1 -1
  46. package/dist/orchestration/lifecycle.js +6 -3
  47. package/dist/orchestration/lifecycle.js.map +1 -1
  48. package/dist/orchestration/queue.d.ts.map +1 -1
  49. package/dist/orchestration/queue.js +1 -0
  50. package/dist/orchestration/queue.js.map +1 -1
  51. package/dist/orchestration/runtime.d.ts +1 -1
  52. package/dist/orchestration/state.d.ts +20 -0
  53. package/dist/orchestration/state.d.ts.map +1 -1
  54. package/dist/orchestration/state.js +1 -0
  55. package/dist/orchestration/state.js.map +1 -1
  56. package/dist/orchestration/turn.d.ts.map +1 -1
  57. package/dist/orchestration/turn.js +134 -3
  58. package/dist/orchestration/turn.js.map +1 -1
  59. package/dist/orchestration-registry.d.ts.map +1 -1
  60. package/dist/orchestration-registry.js +4 -2
  61. package/dist/orchestration-registry.js.map +1 -1
  62. package/dist/orchestration-version.d.ts +1 -1
  63. package/dist/orchestration-version.js +1 -1
  64. package/dist/orchestration.d.ts +2 -2
  65. package/dist/orchestration.js +1 -1
  66. package/dist/orchestration_1_0_46.d.ts +1 -1
  67. package/dist/orchestration_1_0_47.d.ts +1 -1
  68. package/dist/orchestration_1_0_48.d.ts +1 -1
  69. package/dist/orchestration_1_0_49.d.ts +1 -1
  70. package/dist/orchestration_1_0_50.d.ts +1 -1
  71. package/dist/orchestration_1_0_68/agents.d.ts +61 -0
  72. package/dist/orchestration_1_0_68/agents.d.ts.map +1 -0
  73. package/dist/orchestration_1_0_68/agents.js +840 -0
  74. package/dist/orchestration_1_0_68/agents.js.map +1 -0
  75. package/dist/orchestration_1_0_68/index.d.ts +30 -0
  76. package/dist/orchestration_1_0_68/index.d.ts.map +1 -0
  77. package/dist/orchestration_1_0_68/index.js +13 -0
  78. package/dist/orchestration_1_0_68/index.js.map +1 -0
  79. package/dist/orchestration_1_0_68/lifecycle.d.ts +49 -0
  80. package/dist/orchestration_1_0_68/lifecycle.d.ts.map +1 -0
  81. package/dist/orchestration_1_0_68/lifecycle.js +944 -0
  82. package/dist/orchestration_1_0_68/lifecycle.js.map +1 -0
  83. package/dist/orchestration_1_0_68/queue.d.ts +7 -0
  84. package/dist/orchestration_1_0_68/queue.d.ts.map +1 -0
  85. package/dist/orchestration_1_0_68/queue.js +804 -0
  86. package/dist/orchestration_1_0_68/queue.js.map +1 -0
  87. package/dist/orchestration_1_0_68/runtime.d.ts +29 -0
  88. package/dist/orchestration_1_0_68/runtime.d.ts.map +1 -0
  89. package/dist/orchestration_1_0_68/runtime.js +228 -0
  90. package/dist/orchestration_1_0_68/runtime.js.map +1 -0
  91. package/dist/orchestration_1_0_68/state.d.ts +176 -0
  92. package/dist/orchestration_1_0_68/state.d.ts.map +1 -0
  93. package/dist/orchestration_1_0_68/state.js +158 -0
  94. package/dist/orchestration_1_0_68/state.js.map +1 -0
  95. package/dist/orchestration_1_0_68/turn.d.ts +34 -0
  96. package/dist/orchestration_1_0_68/turn.d.ts.map +1 -0
  97. package/dist/orchestration_1_0_68/turn.js +1211 -0
  98. package/dist/orchestration_1_0_68/turn.js.map +1 -0
  99. package/dist/orchestration_1_0_68/utils.d.ts +39 -0
  100. package/dist/orchestration_1_0_68/utils.d.ts.map +1 -0
  101. package/dist/orchestration_1_0_68/utils.js +316 -0
  102. package/dist/orchestration_1_0_68/utils.js.map +1 -0
  103. package/dist/provider-budgets.d.ts +127 -0
  104. package/dist/provider-budgets.d.ts.map +1 -0
  105. package/dist/provider-budgets.js +182 -0
  106. package/dist/provider-budgets.js.map +1 -0
  107. package/dist/provider-catalog.d.ts +125 -0
  108. package/dist/provider-catalog.d.ts.map +1 -0
  109. package/dist/provider-catalog.js +299 -0
  110. package/dist/provider-catalog.js.map +1 -0
  111. package/dist/provider-store.d.ts +383 -0
  112. package/dist/provider-store.d.ts.map +1 -0
  113. package/dist/provider-store.js +514 -0
  114. package/dist/provider-store.js.map +1 -0
  115. package/dist/provider-tools.d.ts +47 -0
  116. package/dist/provider-tools.d.ts.map +1 -0
  117. package/dist/provider-tools.js +585 -0
  118. package/dist/provider-tools.js.map +1 -0
  119. package/dist/session-manager.d.ts +23 -2
  120. package/dist/session-manager.d.ts.map +1 -1
  121. package/dist/session-manager.js +144 -6
  122. package/dist/session-manager.js.map +1 -1
  123. package/dist/session-proxy.d.ts +4 -3
  124. package/dist/session-proxy.d.ts.map +1 -1
  125. package/dist/session-proxy.js +202 -11
  126. package/dist/session-proxy.js.map +1 -1
  127. package/dist/system-agents.d.ts +12 -0
  128. package/dist/system-agents.d.ts.map +1 -1
  129. package/dist/system-agents.js +54 -12
  130. package/dist/system-agents.js.map +1 -1
  131. package/dist/types.d.ts +19 -2
  132. package/dist/types.d.ts.map +1 -1
  133. package/dist/types.js.map +1 -1
  134. package/dist/web/generated-op-methods.d.ts +208 -2
  135. package/dist/web/generated-op-methods.d.ts.map +1 -1
  136. package/dist/web/generated-op-methods.js +50 -0
  137. package/dist/web/generated-op-methods.js.map +1 -1
  138. package/dist/web/web-management-client.d.ts +112 -2
  139. package/dist/web/web-management-client.d.ts.map +1 -1
  140. package/dist/web/web-management-client.js +163 -0
  141. package/dist/web/web-management-client.js.map +1 -1
  142. package/dist/worker.d.ts +9 -0
  143. package/dist/worker.d.ts.map +1 -1
  144. package/dist/worker.js +124 -19
  145. package/dist/worker.js.map +1 -1
  146. package/package.json +3 -3
  147. package/plugins/mgmt/agents/token-manager.agent.md +183 -0
  148. package/plugins/system/agents/default.agent.md +21 -7
  149. package/plugins/system/skills/canvas-apps/SKILL.md +158 -0
@@ -12,6 +12,8 @@
12
12
  */
13
13
  import { RESPONSE_LATEST_KEY, commandResponseKey, stopTurnQueueName, sanitizePromptAttachmentRefs, } from "./types.js";
14
14
  import { SYSTEM_USER_PRINCIPAL } from "./cms.js";
15
+ import { ProviderError } from "./provider-store.js";
16
+ import { PROVIDER_BUDGET_WAKE_PROMPT } from "./provider-budgets.js";
15
17
  import { LOCAL_DEFAULT_USER_PRINCIPAL } from "./session-owner-utils.js";
16
18
  import { normalizeMessageSender } from "./message-sender.js";
17
19
  import { isEnhancedFactStore, EnhancedFactsUnsupportedError } from "./facts-store.js";
@@ -19,11 +21,16 @@ import { resolveStorageConfig } from "./storage-config.js";
19
21
  import { getDuroxideStorageProvider, getRuntimeStorageProvider } from "./storage-providers.js";
20
22
  import { SessionDumper } from "./session-dumper.js";
21
23
  import { computeSessionFootprint, FootprintCache } from "./footprint.js";
22
- import { loadModelProviders } from "./model-providers.js";
24
+ import { loadModelProviders, loadModelProviderTypes } from "./model-providers.js";
25
+ import { bootstrapProviders, resolveProviderCredential, resolveRuntimeModelSelection } from "./provider-catalog.js";
23
26
  import { deriveStatusFromCmsAndRuntime, shouldSyncCompletedStatus, shouldSyncFailedStatus } from "./session-status.js";
24
27
  import { assertUnambiguousProvider, isWebOptions } from "./web/api-connection.js";
25
28
  import { WebPilotSwarmManagementClient } from "./web/web-management-client.js";
26
29
  import { loadSystemAgentConfigs, resolveSystemAgentSessionPlans, startSystemAgents, } from "./system-agents.js";
30
+ import { AgentPackageValidationError, deleteAgentPackageEverywhere, fetchAgentPackageTarGz, publishAgentPackageDir, publishPackedAgentPackage, readAgentPackageVersionEntries, resolveAgentPackageVersion, } from "./agent-package-service.js";
31
+ import fs from "node:fs";
32
+ import os from "node:os";
33
+ import path from "node:path";
27
34
  // duroxide is CommonJS — use createRequire for ESM compatibility
28
35
  import { createRequire } from "node:module";
29
36
  const require = createRequire(import.meta.url);
@@ -49,6 +56,22 @@ function assertValidDate(value, label) {
49
56
  throw new Error(`${label} must be a valid Date`);
50
57
  }
51
58
  }
59
+ /**
60
+ * A default is one tuple, saved whole. With no provider there is nothing to
61
+ * run, so the other three go with it — carrying a model for a provider that
62
+ * was cleared would leave a half-default nobody can act on.
63
+ */
64
+ function normalizeDefaultTuple(tuple) {
65
+ const provider = String(tuple?.provider ?? "").trim() || null;
66
+ if (!provider)
67
+ return { provider: null, model: null, reasoning: null, context: null };
68
+ return {
69
+ provider,
70
+ model: String(tuple?.model ?? "").trim() || null,
71
+ reasoning: String(tuple?.reasoning ?? "").trim() || null,
72
+ context: String(tuple?.context ?? "").trim() || null,
73
+ };
74
+ }
52
75
  function isTerminalOrchestrationStatus(status) {
53
76
  return status === "Completed" || status === "Failed" || status === "Terminated";
54
77
  }
@@ -128,7 +151,7 @@ function normalizeSystemRestartDisposition(disposition) {
128
151
  return "terminate";
129
152
  if (disposition === "hard_delete" || disposition === "hardDelete")
130
153
  return "hard_delete";
131
- throw new Error(`Unsupported system session restart disposition: ${String(disposition)}`);
154
+ throw Object.assign(new Error(`Unsupported system session restart disposition: ${String(disposition)}`), { code: "INVALID_REQUEST" });
132
155
  }
133
156
  function sessionViewFromCmsRow(row) {
134
157
  const liveStatus = row.state || "pending";
@@ -152,6 +175,7 @@ function sessionViewFromCmsRow(row) {
152
175
  serviceOf: row.serviceOf ?? undefined,
153
176
  model: row.model ?? undefined,
154
177
  reasoningEffort: row.reasoningEffort ?? undefined,
178
+ contextTier: row.contextTier ?? undefined,
155
179
  shortSummary: row.shortSummary ?? undefined,
156
180
  summaryState: row.summaryState ?? undefined,
157
181
  summaryUpdatedAt: row.summaryUpdatedAt?.getTime(),
@@ -162,6 +186,7 @@ function sessionViewFromCmsRow(row) {
162
186
  rootSessionId: row.rootSessionId ?? row.sessionId,
163
187
  };
164
188
  }
189
+ export const PROVIDER_USAGE_DIMENSIONS = ["session", "user", "provider", "model", "agent"];
165
190
  // ─── Management Client ──────────────────────────────────────────
166
191
  function normalizeShareSlot(slot) {
167
192
  const n = Math.floor(Number(slot));
@@ -177,6 +202,7 @@ export class PilotSwarmManagementClient {
177
202
  _duroxideClient = null;
178
203
  _modelProviders = null;
179
204
  _systemAgents = [];
205
+ _artifactStore = null;
180
206
  _activeStatusWaitControllers = new Set();
181
207
  _activeStatusWaitPromises = new Set();
182
208
  _started = false;
@@ -198,6 +224,7 @@ export class PilotSwarmManagementClient {
198
224
  return new WebPilotSwarmManagementClient(options);
199
225
  }
200
226
  this.config = options;
227
+ this._artifactStore = options.artifactStore ?? null;
201
228
  }
202
229
  // ─── Lifecycle ───────────────────────────────────────────
203
230
  async start() {
@@ -255,7 +282,14 @@ export class PilotSwarmManagementClient {
255
282
  }
256
283
  }
257
284
  // Load model providers
258
- this._modelProviders = loadModelProviders(this.config.modelProvidersPath);
285
+ this._modelProviders = loadModelProviderTypes(this.config.modelProvidersPath);
286
+ const legacyProviders = loadModelProviders(this.config.modelProvidersPath);
287
+ if (this._catalog.providers && legacyProviders) {
288
+ await bootstrapProviders(this._catalog.providers, {
289
+ providers: legacyProviders.allProviders,
290
+ defaultModel: legacyProviders.defaultModel,
291
+ });
292
+ }
259
293
  this._systemAgents = loadSystemAgentConfigs({
260
294
  pluginDirs: this.config.pluginDirs,
261
295
  disableManagementAgents: this.config.disableManagementAgents,
@@ -354,8 +388,8 @@ export class PilotSwarmManagementClient {
354
388
  candidate.sessionId === target ||
355
389
  `session-${candidate.sessionId}` === target);
356
390
  if (!plan) {
357
- throw new Error(`System agent "${target}" is not known to this management client. ` +
358
- `Configure pluginDirs/systemAgents so the client can recreate the system session.`);
391
+ throw Object.assign(new Error(`System agent "${target}" is not known to this management client. ` +
392
+ `Configure pluginDirs/systemAgents so the client can recreate the system session.`), { code: "NOT_FOUND" });
359
393
  }
360
394
  return plan;
361
395
  }
@@ -672,6 +706,7 @@ export class PilotSwarmManagementClient {
672
706
  isSystem: row.isSystem || undefined,
673
707
  model: row.model ?? undefined,
674
708
  reasoningEffort: row.reasoningEffort ?? undefined,
709
+ contextTier: row.contextTier ?? undefined,
675
710
  shortSummary: row.shortSummary ?? undefined,
676
711
  summaryState: row.summaryState ?? undefined,
677
712
  summaryUpdatedAt: row.summaryUpdatedAt?.getTime(),
@@ -945,10 +980,42 @@ export class PilotSwarmManagementClient {
945
980
  const trimmed = String(model || "").trim();
946
981
  if (!trimmed)
947
982
  throw new Error("setSessionModel requires a model id");
948
- const models = this.listModels();
949
- const match = models.find((m) => m.qualifiedName === trimmed);
983
+ const session = await this.getSession(sessionId).catch(() => null);
984
+ if (!session)
985
+ throw new Error(`Session ${sessionId.slice(0, 8)} was not found`);
986
+ const store = this._requireProviders();
987
+ if (!this._modelProviders)
988
+ throw new Error("Provider type catalog is unavailable");
989
+ const actor = session.owner
990
+ ? await store.lookupUserId(session.owner)
991
+ : null;
992
+ const credentials = await store.allCredentials();
993
+ let selection;
994
+ try {
995
+ selection = resolveRuntimeModelSelection(this._modelProviders, credentials, {
996
+ requestedModel: trimmed,
997
+ requestedReasoning: opts?.reasoningEffort,
998
+ requestedContext: opts?.contextTier,
999
+ eligible: session.isSystem
1000
+ ? (provider) => provider.class === "shared" || provider.systemUseEnabled === true
1001
+ : (provider) => provider.class === "shared" || (actor !== null && provider.ownerUserId === actor),
1002
+ });
1003
+ }
1004
+ catch (error) {
1005
+ if (error?.code === "MODEL_AMBIGUOUS")
1006
+ throw error;
1007
+ if (error?.code === "MODEL_UNRESOLVED") {
1008
+ throw Object.assign(new Error(`Unknown model: ${trimmed}. ${error.message}`), { code: "MODEL_UNRESOLVED" });
1009
+ }
1010
+ throw new Error(`Unknown model: ${trimmed}. ${error?.message || String(error)}`);
1011
+ }
1012
+ const credential = credentials.find((provider) => provider.name === selection.provider);
1013
+ const modelName = selection.model.slice(selection.model.indexOf(":") + 1);
1014
+ const match = this._providerTypeModels(credential.typeId)
1015
+ .find((candidate) => candidate.modelName === modelName);
950
1016
  if (!match)
951
- throw new Error(`Unknown model: ${trimmed}`);
1017
+ throw new Error(`Unknown model: ${selection.model}`);
1018
+ const resolvedModel = selection.model;
952
1019
  // Reasoning effort is applied ONLY when the caller asked for one.
953
1020
  // When omitted, the key is left out of the command args entirely so
954
1021
  // the orchestration's set_model handler PRESERVES the session's
@@ -960,7 +1027,7 @@ export class PilotSwarmManagementClient {
960
1027
  if (nextReasoningEffort) {
961
1028
  const supported = match.supportedReasoningEfforts ?? [];
962
1029
  if (!supported.includes(nextReasoningEffort)) {
963
- throw new Error(`Model ${trimmed} does not support reasoning effort '${nextReasoningEffort}'`);
1030
+ throw new Error(`Model ${resolvedModel} does not support reasoning effort '${nextReasoningEffort}'`);
964
1031
  }
965
1032
  }
966
1033
  // Context-window tier follows the same preserve-when-omitted contract.
@@ -969,17 +1036,14 @@ export class PilotSwarmManagementClient {
969
1036
  if (nextContextTier) {
970
1037
  const supported = match.supportedContextTiers ?? [];
971
1038
  if (!supported.includes(nextContextTier)) {
972
- throw new Error(`Model ${trimmed} does not support context tier '${nextContextTier}'`);
1039
+ throw new Error(`Model ${resolvedModel} does not support context tier '${nextContextTier}'`);
973
1040
  }
974
1041
  }
975
- const session = await this.getSession(sessionId).catch(() => null);
976
- if (!session)
977
- throw new Error(`Session ${sessionId.slice(0, 8)} was not found`);
978
1042
  await this.sendCommand(sessionId, {
979
1043
  cmd: "set_model",
980
1044
  id: buildLifecycleCommandId("set-model"),
981
1045
  args: {
982
- model: trimmed,
1046
+ model: resolvedModel,
983
1047
  ...(hasExplicitEffort ? { reasoningEffort: nextReasoningEffort } : {}),
984
1048
  ...(hasExplicitContextTier ? { contextTier: nextContextTier } : {}),
985
1049
  source: opts?.source ?? "user",
@@ -1000,18 +1064,55 @@ export class PilotSwarmManagementClient {
1000
1064
  if (!session)
1001
1065
  throw new Error(`Session ${sessionId.slice(0, 8)} was not found`);
1002
1066
  if (session.isSystem) {
1003
- throw new Error("System sessions are excluded from regeneration (use restartSystemSession)");
1067
+ throw Object.assign(new Error("System sessions are excluded from regeneration (use restartSystemSession)"), { code: "REGENERATE_UNSUPPORTED" });
1004
1068
  }
1005
1069
  if (session.serviceKind) {
1006
- throw new Error("Service sessions are runtime machinery and are excluded from regeneration");
1070
+ throw Object.assign(new Error("Service sessions are runtime machinery and are excluded from regeneration"), { code: "REGENERATE_UNSUPPORTED" });
1007
1071
  }
1008
- const models = this.listModels();
1009
- const isKnown = (m) => !!m && models.some((x) => x.qualifiedName === m);
1010
- if (opts?.distillerModel && !isKnown(opts.distillerModel)) {
1011
- throw new Error(`Unknown distiller model: ${opts.distillerModel}`);
1072
+ const store = this._requireProviders();
1073
+ if (!this._modelProviders)
1074
+ throw new Error("Provider type catalog is unavailable");
1075
+ const actor = session.owner ? await store.lookupUserId(session.owner) : null;
1076
+ const credentials = await store.allCredentials();
1077
+ const eligible = (provider) => provider.class === "shared" || (actor !== null && provider.ownerUserId === actor);
1078
+ const resolveRequested = (model, reasoning, context) => resolveRuntimeModelSelection(this._modelProviders, credentials, {
1079
+ requestedModel: model,
1080
+ requestedReasoning: reasoning,
1081
+ requestedContext: context,
1082
+ eligible,
1083
+ });
1084
+ let targetModel = opts?.model;
1085
+ let distillerModel = opts?.distillerModel;
1086
+ let distillerReasoningEffort = opts?.distillerReasoningEffort;
1087
+ let distillerContextTier = opts?.distillerContextTier;
1088
+ if (targetModel) {
1089
+ try {
1090
+ targetModel = resolveRequested(targetModel).model;
1091
+ }
1092
+ catch (error) {
1093
+ if (error?.code === "MODEL_AMBIGUOUS")
1094
+ throw error;
1095
+ if (error?.code === "MODEL_UNRESOLVED") {
1096
+ throw Object.assign(new Error(`Unknown model: ${opts?.model}. ${error.message}`), { code: "MODEL_UNRESOLVED" });
1097
+ }
1098
+ throw new Error(`Unknown model: ${opts?.model}. ${error?.message || String(error)}`);
1099
+ }
1012
1100
  }
1013
- if (opts?.model && !isKnown(opts.model)) {
1014
- throw new Error(`Unknown model: ${opts.model}`);
1101
+ if (distillerModel) {
1102
+ try {
1103
+ const resolved = resolveRequested(distillerModel, distillerReasoningEffort, distillerContextTier);
1104
+ distillerModel = resolved.model;
1105
+ distillerReasoningEffort = resolved.reasoning ?? undefined;
1106
+ distillerContextTier = resolved.context ?? undefined;
1107
+ }
1108
+ catch (error) {
1109
+ if (error?.code === "MODEL_AMBIGUOUS")
1110
+ throw error;
1111
+ if (error?.code === "MODEL_UNRESOLVED") {
1112
+ throw Object.assign(new Error(`Unknown distiller model: ${opts?.distillerModel}. ${error.message}`), { code: "MODEL_UNRESOLVED" });
1113
+ }
1114
+ throw new Error(`Unknown distiller model: ${opts?.distillerModel}. ${error?.message || String(error)}`);
1115
+ }
1015
1116
  }
1016
1117
  // Dead-model guard: the reborn session's grounding turn runs on the
1017
1118
  // session's model. If that model no longer resolves (a common regen
@@ -1019,8 +1120,16 @@ export class PilotSwarmManagementClient {
1019
1120
  // discard the transcript and then wedge on the dead model. Require a
1020
1121
  // live replacement rather than making a broken session strictly worse.
1021
1122
  const currentModel = session.model;
1022
- if (!opts?.model && currentModel && !isKnown(currentModel)) {
1023
- throw new Error(`Session model '${currentModel}' is no longer available; pass a replacement model to regenerate this session.`);
1123
+ if (!targetModel && currentModel) {
1124
+ try {
1125
+ resolveRequested(currentModel);
1126
+ }
1127
+ catch (error) {
1128
+ if (error?.code === "MODEL_AMBIGUOUS")
1129
+ throw error;
1130
+ throw Object.assign(new Error(`Session model '${currentModel}' is no longer available; pass a replacement model to regenerate this session. `
1131
+ + `${error?.message || String(error)}`), { code: "MODEL_UNRESOLVED" });
1132
+ }
1024
1133
  }
1025
1134
  const attemptId = buildLifecycleCommandId("regenerate");
1026
1135
  await this.sendCommand(sessionId, {
@@ -1030,10 +1139,10 @@ export class PilotSwarmManagementClient {
1030
1139
  ...(opts?.handoff ? { handoff: String(opts.handoff).slice(0, 4_000) } : {}),
1031
1140
  ...(opts?.instructions ? { instructions: String(opts.instructions).slice(0, 4_000) } : {}),
1032
1141
  ...(opts?.distillMode === "deterministic" ? { distill_mode: "deterministic" } : {}),
1033
- ...(opts?.distillerModel ? { distillerModel: opts.distillerModel } : {}),
1034
- ...(opts?.distillerReasoningEffort ? { distillerReasoningEffort: opts.distillerReasoningEffort } : {}),
1035
- ...(opts?.distillerContextTier ? { distillerContextTier: opts.distillerContextTier } : {}),
1036
- ...(opts?.model ? { model: opts.model } : {}),
1142
+ ...(distillerModel ? { distillerModel } : {}),
1143
+ ...(distillerReasoningEffort ? { distillerReasoningEffort } : {}),
1144
+ ...(distillerContextTier ? { distillerContextTier } : {}),
1145
+ ...(targetModel ? { model: targetModel } : {}),
1037
1146
  ...(opts?.force ? { force: true } : {}),
1038
1147
  source: opts?.source ?? "operator",
1039
1148
  },
@@ -1107,7 +1216,8 @@ export class PilotSwarmManagementClient {
1107
1216
  async restartSystemSession(agentIdOrSessionId, options) {
1108
1217
  this._ensureStarted();
1109
1218
  if (!options?.disposition) {
1110
- throw new Error("restartSystemSession requires a disposition: complete, terminate, or hard_delete");
1219
+ throw Object.assign(new Error("restartSystemSession requires a disposition: complete, terminate, or hard_delete "
1220
+ + "(HTTP callers send it inside the options envelope: {\"options\":{\"disposition\":\"complete\"}})"), { code: "INVALID_REQUEST" });
1111
1221
  }
1112
1222
  const disposition = normalizeSystemRestartDisposition(options.disposition);
1113
1223
  const plan = this._resolveSystemAgentPlan(agentIdOrSessionId);
@@ -1115,64 +1225,153 @@ export class PilotSwarmManagementClient {
1115
1225
  const reason = options.reason ?? `Restarting system session ${plan.agent.id}`;
1116
1226
  const existingRow = await this._catalog.getSession(sessionId);
1117
1227
  const previousSessionExisted = Boolean(existingRow);
1118
- if (existingRow && !existingRow.isSystem) {
1119
- throw new Error(`Session ${sessionId.slice(0, 8)} exists but is not a system session`);
1120
- }
1121
- if (existingRow) {
1122
- if (disposition === "complete") {
1123
- const view = await this.getSession(sessionId).catch(() => null);
1124
- if (view && view.status !== "completed" && view.status !== "failed" && view.status !== "cancelled") {
1125
- try {
1126
- await this.sendCommand(sessionId, {
1127
- cmd: "done",
1128
- id: buildLifecycleCommandId("done-system-restart"),
1129
- args: { reason },
1130
- });
1131
- await this._waitForSession(sessionId, (current) => current != null && (current.status === "completed" || current.status === "failed" || current.status === "cancelled"), options.timeoutMs ?? SESSION_COMMAND_SETTLE_TIMEOUT_MS);
1132
- }
1133
- catch (err) {
1134
- if (!isIgnorableRestartCommandError(err))
1135
- throw err;
1136
- }
1137
- }
1138
- await this._deleteSystemOrchestrationInstance(sessionId);
1139
- await this._archiveSystemSessionForRestart(sessionId, "completed", reason);
1228
+ const providerStore = this._catalog.providers;
1229
+ let persistedRoute = null;
1230
+ let persistedSource = null;
1231
+ if (providerStore) {
1232
+ const [defaults, overrides, credentials] = await Promise.all([
1233
+ providerStore.getDefaults(null),
1234
+ providerStore.listSystemAgentModels(),
1235
+ providerStore.allCredentials(),
1236
+ ]);
1237
+ const override = overrides.find((candidate) => candidate.agentId === plan.agent.id);
1238
+ if (override) {
1239
+ persistedRoute = override;
1240
+ persistedSource = "agent_override";
1140
1241
  }
1141
- else if (disposition === "terminate") {
1142
- await this._terminateSystemOrchestrationInstance(sessionId, reason);
1143
- await this._archiveSystemSessionForRestart(sessionId, "cancelled", `Terminated for restart: ${reason}`);
1242
+ else if (defaults.system.provider && defaults.system.model) {
1243
+ persistedRoute = defaults.system;
1244
+ persistedSource = "system_default";
1144
1245
  }
1145
1246
  else {
1146
- await this._deleteSystemOrchestrationInstance(sessionId);
1147
- await this._archiveSystemSessionForRestart(sessionId, "failed", `Hard-deleted for restart: ${reason}`);
1247
+ persistedRoute = this._firstAvailableTuple(credentials, (provider) => provider.class === "shared" || provider.systemUseEnabled === true);
1248
+ persistedSource = persistedRoute ? "first_available" : null;
1148
1249
  }
1149
1250
  }
1150
- const defaultModel = options.model ?? this._modelProviders?.defaultModel ?? existingRow?.model;
1251
+ let defaultModel = options.model
1252
+ ?? persistedRoute?.model
1253
+ ?? existingRow?.model
1254
+ ?? this._modelProviders?.defaultModel;
1151
1255
  if (!defaultModel) {
1152
1256
  throw new Error("Cannot restart system session without a configured default model");
1153
1257
  }
1154
- const startResults = await startSystemAgents({
1155
- catalog: this._catalog,
1156
- duroxideClient: this._duroxideClient,
1157
- agents: this._systemAgents,
1158
- defaultModel,
1159
- ...(options.reasoningEffort ? { defaultReasoningEffort: options.reasoningEffort } : {}),
1160
- blobEnabled: this.config.blobEnabled,
1161
- dehydrateThreshold: this.config.waitThreshold ?? DEFAULT_SYSTEM_AGENT_DEHYDRATE_THRESHOLD,
1162
- agentId: plan.agent.id,
1163
- });
1164
- const newSession = await this.getSession(sessionId).catch(() => null);
1165
- if (!newSession) {
1166
- throw new Error(`System session ${plan.agent.id} was not recreated`);
1258
+ let restartReasoning = Object.prototype.hasOwnProperty.call(options, "reasoningEffort")
1259
+ ? options.reasoningEffort ?? null
1260
+ : persistedRoute?.reasoning ?? existingRow?.reasoningEffort ?? null;
1261
+ let restartContext = Object.prototype.hasOwnProperty.call(options, "contextTier")
1262
+ ? options.contextTier ?? null
1263
+ : persistedRoute?.context ?? existingRow?.contextTier ?? null;
1264
+ let restartSource = options.modelResolutionSource ?? persistedSource ?? existingRow?.modelResolutionSource ?? "manual_restart";
1265
+ if (providerStore) {
1266
+ if (!this._modelProviders) {
1267
+ throw new Error("Cannot restart system session without a provider type catalog");
1268
+ }
1269
+ const credentials = await providerStore.allCredentials();
1270
+ const validated = resolveRuntimeModelSelection(this._modelProviders, credentials, {
1271
+ requestedModel: defaultModel,
1272
+ requestedReasoning: restartReasoning,
1273
+ requestedContext: restartContext,
1274
+ eligible: (provider) => provider.class === "shared" || provider.systemUseEnabled === true,
1275
+ });
1276
+ defaultModel = validated.model;
1277
+ restartReasoning = validated.reasoning;
1278
+ restartContext = validated.context;
1279
+ if (!options.modelResolutionSource && !persistedSource)
1280
+ restartSource = validated.source;
1281
+ }
1282
+ const operationId = options.operationId ?? `manual:${buildLifecycleCommandId("system-restart")}`;
1283
+ const claimId = buildLifecycleCommandId("system-restart-claim");
1284
+ if (providerStore) {
1285
+ const claim = await providerStore.claimSystemRestart({
1286
+ agentId: plan.agent.id,
1287
+ operationId,
1288
+ claimId,
1289
+ model: defaultModel,
1290
+ reasoning: restartReasoning,
1291
+ context: restartContext,
1292
+ disposition,
1293
+ });
1294
+ if (claim !== "claimed") {
1295
+ return {
1296
+ agentId: plan.agent.id,
1297
+ agentName: plan.agent.name,
1298
+ sessionId,
1299
+ disposition,
1300
+ previousSessionExisted,
1301
+ startResults: [],
1302
+ skippedReason: claim,
1303
+ };
1304
+ }
1305
+ }
1306
+ if (existingRow && !existingRow.isSystem) {
1307
+ throw new Error(`Session ${sessionId.slice(0, 8)} exists but is not a system session`);
1308
+ }
1309
+ try {
1310
+ if (existingRow) {
1311
+ if (disposition === "complete") {
1312
+ const view = await this.getSession(sessionId).catch(() => null);
1313
+ if (view && view.status !== "completed" && view.status !== "failed" && view.status !== "cancelled") {
1314
+ try {
1315
+ await this.sendCommand(sessionId, {
1316
+ cmd: "done",
1317
+ id: buildLifecycleCommandId("done-system-restart"),
1318
+ args: { reason },
1319
+ });
1320
+ await this._waitForSession(sessionId, (current) => current != null && (current.status === "completed" || current.status === "failed" || current.status === "cancelled"), options.timeoutMs ?? SESSION_COMMAND_SETTLE_TIMEOUT_MS);
1321
+ }
1322
+ catch (err) {
1323
+ if (!isIgnorableRestartCommandError(err))
1324
+ throw err;
1325
+ }
1326
+ }
1327
+ await this._deleteSystemOrchestrationInstance(sessionId);
1328
+ await this._archiveSystemSessionForRestart(sessionId, "completed", reason);
1329
+ }
1330
+ else if (disposition === "terminate") {
1331
+ await this._terminateSystemOrchestrationInstance(sessionId, reason);
1332
+ await this._archiveSystemSessionForRestart(sessionId, "cancelled", `Terminated for restart: ${reason}`);
1333
+ }
1334
+ else {
1335
+ await this._deleteSystemOrchestrationInstance(sessionId);
1336
+ await this._archiveSystemSessionForRestart(sessionId, "failed", `Hard-deleted for restart: ${reason}`);
1337
+ }
1338
+ }
1339
+ const startResults = await startSystemAgents({
1340
+ catalog: this._catalog,
1341
+ duroxideClient: this._duroxideClient,
1342
+ agents: this._systemAgents,
1343
+ defaultModel,
1344
+ defaultReasoningEffort: restartReasoning,
1345
+ defaultContextTier: restartContext,
1346
+ modelResolutionSource: restartSource,
1347
+ blobEnabled: this.config.blobEnabled,
1348
+ dehydrateThreshold: this.config.waitThreshold ?? DEFAULT_SYSTEM_AGENT_DEHYDRATE_THRESHOLD,
1349
+ agentId: plan.agent.id,
1350
+ // The retained session id already has ledger rows from earlier
1351
+ // lifetimes; advance its ledger base so the new lifetime's
1352
+ // settles land on fresh keys instead of silently colliding.
1353
+ ...(providerStore ? {
1354
+ prepareRetainedSessionLedger: async (sid) => { await providerStore.bumpLedgerBase(sid); },
1355
+ } : {}),
1356
+ });
1357
+ const newSession = await this.getSession(sessionId).catch(() => null);
1358
+ if (!newSession) {
1359
+ throw new Error(`System session ${plan.agent.id} was not recreated`);
1360
+ }
1361
+ await providerStore?.finishSystemRestart(plan.agent.id, claimId, null);
1362
+ return {
1363
+ agentId: plan.agent.id,
1364
+ agentName: plan.agent.name,
1365
+ sessionId,
1366
+ disposition,
1367
+ previousSessionExisted,
1368
+ startResults,
1369
+ };
1370
+ }
1371
+ catch (error) {
1372
+ await providerStore?.finishSystemRestart(plan.agent.id, claimId, error?.message || String(error));
1373
+ throw error;
1167
1374
  }
1168
- return {
1169
- agentId: plan.agent.id,
1170
- agentName: plan.agent.name,
1171
- sessionId,
1172
- disposition,
1173
- previousSessionExisted,
1174
- startResults,
1175
- };
1176
1375
  }
1177
1376
  // ─── Session Events ──────────────────────────────────────
1178
1377
  /**
@@ -2035,6 +2234,956 @@ export class PilotSwarmManagementClient {
2035
2234
  await this._assertOrchestrationLive(orchId, sessionId, "sendCommand");
2036
2235
  await this._duroxideClient.enqueueEvent(orchId, "messages", JSON.stringify({ type: "cmd", ...command }));
2037
2236
  }
2237
+ // ─── Provider budgets ────────────────────────────────────
2238
+ //
2239
+ // The capabilities of docs/proposals/providers-and-budgets-surface.md.
2240
+ // Each one resolves the caller to a user id and hands that number, with
2241
+ // the role they signed in as, to a `cms_provider_*` procedure. Nothing
2242
+ // below asks whether the caller is allowed to do the thing: the procedure
2243
+ // raises PROVIDER_FORBIDDEN / PROVIDER_NOT_FOUND when they are not, and
2244
+ // that refusal is the same on every surface because it is made in one
2245
+ // place. See provider-store.ts.
2246
+ _requireProviders() {
2247
+ this._ensureStarted();
2248
+ const providers = this._catalog.providers;
2249
+ if (!providers)
2250
+ throw new Error("Provider budgets are not available on this deployment.");
2251
+ return providers;
2252
+ }
2253
+ /**
2254
+ * The caller as the procedures want them: a numeric user id and a role.
2255
+ * The id lookup never creates a row, so an unknown caller stays `null`
2256
+ * rather than being minted by a read.
2257
+ */
2258
+ async _providerActor(viewer) {
2259
+ const store = this._requireProviders();
2260
+ return {
2261
+ store,
2262
+ actor: await store.lookupUserId(viewer?.principal ?? null),
2263
+ isAdmin: viewer?.isAdmin === true,
2264
+ };
2265
+ }
2266
+ async _auditProviderMutation(viewer, action, target, details) {
2267
+ await this._catalog.recordAuthzAudit({
2268
+ actor: viewer.principal ? {
2269
+ provider: viewer.principal.provider,
2270
+ subject: viewer.principal.subject,
2271
+ display: viewer.principal.email ?? viewer.principal.displayName ?? null,
2272
+ } : null,
2273
+ action,
2274
+ target,
2275
+ decision: "provider_change",
2276
+ reason: null,
2277
+ details: details ?? null,
2278
+ }).catch(() => { });
2279
+ }
2280
+ _providerTypeModels(typeId) {
2281
+ return this._modelProviders?.getModelsByProvider()
2282
+ .find((group) => group.providerId === typeId)?.models ?? [];
2283
+ }
2284
+ _normalizeProviderTuple(row, tuple) {
2285
+ const provider = String(tuple.provider || "").trim();
2286
+ const requested = String(tuple.model || "").trim();
2287
+ if (!provider || !requested) {
2288
+ throw new ProviderError("PROVIDER_INVALID", "A default needs both a provider and model.");
2289
+ }
2290
+ const prefix = `${provider}:`;
2291
+ const modelName = requested.startsWith(prefix)
2292
+ ? requested.slice(prefix.length)
2293
+ : (requested.includes(":") ? "" : requested);
2294
+ if (!modelName) {
2295
+ throw new ProviderError("PROVIDER_INVALID", `The model must belong to "${provider}": write it as "${provider}:<model>".`);
2296
+ }
2297
+ const descriptor = this._providerTypeModels(row.typeId)
2298
+ .find((model) => model.modelName === modelName);
2299
+ if (this._modelProviders && !descriptor) {
2300
+ throw new ProviderError("PROVIDER_INVALID", `Provider type "${row.typeId}" does not offer model "${modelName}".`);
2301
+ }
2302
+ const reasoning = tuple.reasoning ?? descriptor?.defaultReasoningEffort ?? null;
2303
+ if (reasoning && descriptor?.supportedReasoningEfforts?.length
2304
+ && !descriptor.supportedReasoningEfforts.includes(reasoning)) {
2305
+ throw new ProviderError("PROVIDER_INVALID", `Model ${provider}:${modelName} does not support reasoning effort '${reasoning}'.`);
2306
+ }
2307
+ const context = tuple.context ?? descriptor?.defaultContextTier ?? null;
2308
+ if (context && descriptor?.supportedContextTiers?.length
2309
+ && !descriptor.supportedContextTiers.includes(context)) {
2310
+ throw new ProviderError("PROVIDER_INVALID", `Model ${provider}:${modelName} does not support context tier '${context}'.`);
2311
+ }
2312
+ return { provider, model: `${provider}:${modelName}`, reasoning, context };
2313
+ }
2314
+ async _validatedDefaultTuple(store, actor, isAdmin, tuple, scope) {
2315
+ if (!tuple.provider)
2316
+ return normalizeDefaultTuple(null);
2317
+ const rows = await store.listProviders(actor, isAdmin);
2318
+ const row = rows.find((candidate) => candidate.name === tuple.provider);
2319
+ if (!row)
2320
+ throw new ProviderError("PROVIDER_NOT_FOUND", `There is no provider named "${tuple.provider}".`);
2321
+ if (scope === "user" && !row.usableByMe) {
2322
+ throw new ProviderError("PROVIDER_NOT_FOUND", `There is no provider named "${tuple.provider}".`);
2323
+ }
2324
+ if (scope === "cluster" && row.class !== "shared") {
2325
+ throw new ProviderError("PROVIDER_INVALID", "The cluster default must be a shared provider.");
2326
+ }
2327
+ if (scope === "system" && !row.systemEligible) {
2328
+ throw new ProviderError("PROVIDER_INVALID", `Provider "${row.name}" is not enabled for system sessions.`);
2329
+ }
2330
+ const normalized = this._normalizeProviderTuple(row, tuple);
2331
+ const credential = scope === "system"
2332
+ ? await store.getSystemCredential(row.name)
2333
+ : await store.getCredential(row.name, actor);
2334
+ const modelName = normalized.model.slice(normalized.model.indexOf(":") + 1);
2335
+ if (!credential || !this._modelProviders
2336
+ || !resolveProviderCredential(this._modelProviders, credential, modelName)) {
2337
+ throw new ProviderError("PROVIDER_INVALID", `Provider "${row.name}" does not have a usable credential and endpoint.`);
2338
+ }
2339
+ return normalized;
2340
+ }
2341
+ _firstAvailableTuple(credentials, eligible) {
2342
+ if (!this._modelProviders)
2343
+ return null;
2344
+ for (const credential of credentials) {
2345
+ if (!eligible(credential))
2346
+ continue;
2347
+ const first = this._providerTypeModels(credential.typeId)[0];
2348
+ if (!first)
2349
+ continue;
2350
+ if (!resolveProviderCredential(this._modelProviders, credential, first.modelName))
2351
+ continue;
2352
+ return {
2353
+ provider: credential.name,
2354
+ model: `${credential.name}:${first.modelName}`,
2355
+ reasoning: first.defaultReasoningEffort ?? null,
2356
+ context: first.defaultContextTier ?? null,
2357
+ };
2358
+ }
2359
+ return null;
2360
+ }
2361
+ _resolvedDefault(tuple, source) {
2362
+ if (!tuple?.provider || !tuple.model)
2363
+ return null;
2364
+ return {
2365
+ provider: tuple.provider,
2366
+ model: tuple.model,
2367
+ reasoningEffort: tuple.reasoning,
2368
+ contextTier: tuple.context,
2369
+ source,
2370
+ };
2371
+ }
2372
+ /** Every shared provider plus the caller's own. Admins also see other people's, unusable. */
2373
+ async listProviders(viewer) {
2374
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2375
+ return { providers: await store.listProviders(actor, isAdmin) };
2376
+ }
2377
+ /** Limits, what has been spent against them, reset times, and the caller's own ceiling. */
2378
+ async getProviderStatus(viewer, names) {
2379
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2380
+ return { providers: await store.providerStatus(actor, isAdmin, names ?? null) };
2381
+ }
2382
+ /**
2383
+ * Everything the provider table draws, in one read.
2384
+ *
2385
+ * Rows come back in render order — shared providers first, then the
2386
+ * caller's own, each immediately followed by its model-scoped limits — so
2387
+ * a table draws the array as it arrives and never asks a second time per
2388
+ * provider.
2389
+ *
2390
+ * Every period reports a used figure whether or not a limit caps it. That
2391
+ * is the fact `getProviderStatus` cannot carry: it lists limits, and a
2392
+ * period with no limit has no limit to list.
2393
+ */
2394
+ async getProviderUsageGrid(viewer) {
2395
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2396
+ return { rows: await store.usageGrid(actor, isAdmin) };
2397
+ }
2398
+ /** Create a shared provider: anyone in the cluster may spend from it. */
2399
+ /**
2400
+ * Wake every session waiting on this provider.
2401
+ *
2402
+ * The durable timer a paused session sleeps on is only the BACKSTOP. What
2403
+ * should actually release it is the change itself — a limit raised or
2404
+ * removed, an allowance widened, a hold released, a missing name created
2405
+ * again — and without this that promise was never kept: a session paused
2406
+ * on a monthly limit would sleep out the backstop no matter what an
2407
+ * administrator did.
2408
+ *
2409
+ * The wake is a nudge, not a decision. Each woken session re-enters the
2410
+ * gate on its next turn and pauses again if the reason still holds, so a
2411
+ * raise that is still not enough wakes nobody in the end — and waking
2412
+ * after a change that releases no one costs one cheap re-check, because
2413
+ * the gate runs before the model.
2414
+ *
2415
+ * Best-effort throughout. A failed wake leaves the backstop, which is
2416
+ * exactly what it is for.
2417
+ */
2418
+ async _wakeProvidersPaused(providerName) {
2419
+ const store = this._catalog?.providers;
2420
+ if (!store || !this._duroxideClient)
2421
+ return;
2422
+ try {
2423
+ const sessionIds = await store.pausedFor(providerName);
2424
+ for (const sessionId of sessionIds) {
2425
+ try {
2426
+ // The orchestration id is DERIVED, not stored: getSession
2427
+ // builds this same string at its top and then returns a
2428
+ // view that does not carry it. Reading it back off that
2429
+ // view gave undefined every time, so `if (!orchId)
2430
+ // continue` skipped every session and this whole function
2431
+ // has never woken anything. Releasing a hold, raising a
2432
+ // limit, widening an allowance, removing a limit and
2433
+ // creating a provider were all silent no-ops behind the
2434
+ // catch below, leaving the 6-hour backstop as the only
2435
+ // real release.
2436
+ const orchId = `session-${sessionId}`;
2437
+ await this._catalog.updateSession(sessionId, {
2438
+ state: "running", waitReason: null, lastActiveAt: new Date(),
2439
+ }).catch(() => { });
2440
+ await this._duroxideClient.enqueueEvent(orchId, "messages", JSON.stringify({
2441
+ prompt: PROVIDER_BUDGET_WAKE_PROMPT,
2442
+ }));
2443
+ }
2444
+ catch {
2445
+ // One unreachable session must not stop the others.
2446
+ }
2447
+ }
2448
+ }
2449
+ catch {
2450
+ // The backstop timer still covers every one of them.
2451
+ }
2452
+ }
2453
+ /**
2454
+ * Refuse a provider type this deployment does not describe.
2455
+ *
2456
+ * The database cannot check it: the types live in the model-providers
2457
+ * FILE, which SQL has never read. Without this, a typo created a
2458
+ * provider that listed as an ordinary usable row and could never run a
2459
+ * turn — buildRuntimeRegistry drops an instance whose type it does not
2460
+ * know, silently, so the failure surfaced only as a session that waited
2461
+ * for ever on a name that was right there in the table.
2462
+ */
2463
+ _assertKnownProviderType(typeId) {
2464
+ const wanted = String(typeId ?? "").trim();
2465
+ const known = this.getModelsByProvider().map((g) => g.providerId);
2466
+ // No catalog at all is not evidence of a bad type — a deployment that
2467
+ // has not loaded one yet would refuse everything.
2468
+ if (known.length === 0)
2469
+ return;
2470
+ if (known.includes(wanted))
2471
+ return;
2472
+ throw new ProviderError("PROVIDER_INVALID", `No provider type named "${wanted}" on this deployment. `
2473
+ + `It has ${known.slice(0, 6).join(", ")}${known.length > 6 ? ", …" : ""}.`);
2474
+ }
2475
+ async createProvider(viewer, input) {
2476
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2477
+ this._assertKnownProviderType(input.type);
2478
+ const created = await store.createProvider({
2479
+ name: input.name,
2480
+ typeId: input.type,
2481
+ class: "shared",
2482
+ secretRef: input.credentials ?? null,
2483
+ baseUrl: input.baseUrl ?? null,
2484
+ }, actor, isAdmin);
2485
+ await this._auditProviderMutation(viewer, "createProvider", created.name, { class: "shared", type: created.typeId });
2486
+ // A name that did not resolve a moment ago now does.
2487
+ await this._wakeProvidersPaused(input.name);
2488
+ return created;
2489
+ }
2490
+ /** Create a provider of the caller's own, on their own credentials. Nobody else sees it. */
2491
+ async createMyProvider(viewer, input) {
2492
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2493
+ if (actor === null) {
2494
+ throw new ProviderError("PROVIDER_FORBIDDEN", "Sign in to add a personal provider.");
2495
+ }
2496
+ this._assertKnownProviderType(input.type);
2497
+ const created = await store.createProvider({
2498
+ name: input.name,
2499
+ typeId: input.type,
2500
+ class: "personal",
2501
+ ownerUserId: actor,
2502
+ secretRef: input.credentials ?? null,
2503
+ baseUrl: input.baseUrl ?? null,
2504
+ }, actor, isAdmin);
2505
+ await this._wakeProvidersPaused(input.name);
2506
+ await this._auditProviderMutation(viewer, "createMyProvider", created.name, { class: "personal", type: created.typeId });
2507
+ return created;
2508
+ }
2509
+ /**
2510
+ * Remove a shared provider. Its sessions are not moved anywhere: they
2511
+ * wait on a name that no longer resolves, and the count says how many.
2512
+ */
2513
+ async deleteProvider(viewer, name) {
2514
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2515
+ const result = { name, waitingSessions: await store.deleteProvider(name, actor, isAdmin) };
2516
+ await this._auditProviderMutation(viewer, "deleteProvider", name, { waitingSessions: result.waitingSessions });
2517
+ return result;
2518
+ }
2519
+ /**
2520
+ * Remove one of the caller's own providers. The same call as
2521
+ * `deleteProvider`: `cms_provider_delete` refuses a name the caller may
2522
+ * not touch, so the two differ only in which door offers them.
2523
+ */
2524
+ async deleteMyProvider(viewer, name) {
2525
+ // The /me namespace holds ONLY the caller's own personal providers.
2526
+ // Delegating straight to deleteProvider let an admin delete a SHARED
2527
+ // provider through this route, and answered "forbidden" for shared
2528
+ // names — an existence oracle. Anything that is not the caller's own
2529
+ // personal provider is a name this namespace has never heard of.
2530
+ const { store, actor } = await this._providerActor(viewer);
2531
+ const rows = await store.listProviders(actor, false);
2532
+ const mine = rows.find((row) => row.name === name && row.class === "personal" && row.ownerUserId === actor);
2533
+ if (!mine) {
2534
+ throw new ProviderError("PROVIDER_NOT_FOUND", `there is no provider named "${name}"`);
2535
+ }
2536
+ const result = { name, waitingSessions: await store.deleteProvider(name, actor, false) };
2537
+ await this._auditProviderMutation(viewer, "deleteMyProvider", name, { waitingSessions: result.waitingSessions });
2538
+ return result;
2539
+ }
2540
+ async clearProviderRoutingDependencies(viewer, name) {
2541
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2542
+ const result = await store.clearRoutingDependencies(name, actor, isAdmin);
2543
+ await this._auditProviderMutation(viewer, "clearProviderRoutingDependencies", name, result);
2544
+ return { name, ...result };
2545
+ }
2546
+ /**
2547
+ * Save one limit. The same (period, scope) replaces what was there.
2548
+ * `seededTokens` is what this period had already spent when the limit
2549
+ * landed — a limit counts from the current window, it never resets it.
2550
+ */
2551
+ async setProviderLimit(viewer, input) {
2552
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2553
+ // Validate here, not in Postgres: an unparsable or fractional value
2554
+ // used to reach the proc and come back as a raw 22P02 dressed up as
2555
+ // an internal server error.
2556
+ const tokens = Number(input.tokens);
2557
+ if (!Number.isSafeInteger(tokens) || tokens < 0) {
2558
+ throw new ProviderError("PROVIDER_INVALID", "A limit is a whole number of tokens, zero or more.");
2559
+ }
2560
+ const saved = await store.setLimit({
2561
+ name: input.provider,
2562
+ period: input.period,
2563
+ modelQualified: input.model ?? null,
2564
+ limitTokens: tokens,
2565
+ }, actor, isAdmin);
2566
+ await this._wakeProvidersPaused(input.provider);
2567
+ return saved;
2568
+ }
2569
+ /** Drop one limit. `removed` is false when that (period, scope) had none. */
2570
+ async removeProviderLimit(viewer, input) {
2571
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2572
+ const removed = await store.removeLimit(input.provider, input.period, input.model ?? null, actor, isAdmin);
2573
+ await this._wakeProvidersPaused(input.provider);
2574
+ return { removed };
2575
+ }
2576
+ /** The share of each of a shared provider's limits that one person may use. 100 is full. */
2577
+ async setProviderAllowance(viewer, input) {
2578
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2579
+ const allowancePct = await store.setAllowance(input.provider, Number(input.pct), actor, isAdmin);
2580
+ await this._wakeProvidersPaused(input.provider);
2581
+ return { name: input.provider, allowancePct };
2582
+ }
2583
+ /**
2584
+ * Pause new turns against a provider, independently of any limit. With
2585
+ * neither an end time nor a release the hold has no end, and only
2586
+ * another call lifts it.
2587
+ */
2588
+ async setProviderHold(viewer, input) {
2589
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2590
+ const release = input.release === true;
2591
+ const untilUtc = release ? null : (input.untilUtc ?? null);
2592
+ if (untilUtc !== null) {
2593
+ const parsed = Date.parse(untilUtc);
2594
+ if (Number.isNaN(parsed)) {
2595
+ throw new ProviderError("PROVIDER_INVALID", `"${untilUtc}" is not a timestamp.`);
2596
+ }
2597
+ // A hold that has already ended holds nothing; accepting one and
2598
+ // echoing it back as set is the write-path lie the 2026-08-21
2599
+ // audit flagged. The Edit sheet validates too, but the API must
2600
+ // not trust the sheet.
2601
+ if (parsed <= Date.now()) {
2602
+ throw new ProviderError("PROVIDER_INVALID", "A hold must end in the future.");
2603
+ }
2604
+ }
2605
+ const indefinite = !release && !untilUtc;
2606
+ await store.setHold(input.provider, { untilUtc, indefinite }, actor, isAdmin);
2607
+ await this._wakeProvidersPaused(input.provider);
2608
+ return { name: input.provider, holdUntilUtc: untilUtc, holdIndefinite: indefinite };
2609
+ }
2610
+ /** Compatibility read: configured ordinary defaults plus system routing. */
2611
+ async getDefaults(viewer) {
2612
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2613
+ return store.getDefaults(actor);
2614
+ }
2615
+ /** @deprecated Use setModelDefault({ scope: "cluster", ... }). */
2616
+ async setClusterDefault(viewer, tuple) {
2617
+ return this.setModelDefault(viewer, {
2618
+ scope: "cluster",
2619
+ provider: tuple.provider,
2620
+ model: tuple.model,
2621
+ reasoningEffort: tuple.reasoning,
2622
+ contextTier: tuple.context,
2623
+ });
2624
+ }
2625
+ /** @deprecated Use setModelDefault({ scope: "user", ... }). */
2626
+ async setMyDefault(viewer, tuple) {
2627
+ return this.setModelDefault(viewer, {
2628
+ scope: "user",
2629
+ provider: tuple?.provider ?? null,
2630
+ model: tuple?.model ?? null,
2631
+ reasoningEffort: tuple?.reasoning,
2632
+ contextTier: tuple?.context,
2633
+ });
2634
+ }
2635
+ async setModelDefault(viewer, input) {
2636
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2637
+ if (input.scope !== "user" && input.scope !== "cluster") {
2638
+ throw new ProviderError("PROVIDER_INVALID", "scope must be user or cluster.");
2639
+ }
2640
+ if (input.scope === "cluster" && !isAdmin) {
2641
+ throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can set the cluster default.");
2642
+ }
2643
+ const requested = normalizeDefaultTuple({
2644
+ provider: input.provider,
2645
+ model: input.model,
2646
+ reasoning: input.reasoningEffort ?? null,
2647
+ context: input.contextTier ?? null,
2648
+ });
2649
+ const next = await this._validatedDefaultTuple(store, actor, isAdmin, requested, input.scope);
2650
+ if (input.scope === "cluster")
2651
+ await store.setClusterDefault(next, isAdmin);
2652
+ else
2653
+ await store.setUserDefault(actor, next);
2654
+ await this._auditProviderMutation(viewer, "setModelDefault", next.provider, {
2655
+ scope: input.scope, model: next.model, cleared: next.provider === null,
2656
+ });
2657
+ return next;
2658
+ }
2659
+ async setProviderSystemUse(viewer, input) {
2660
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2661
+ const provider = String(input.provider || "").trim();
2662
+ if (!provider)
2663
+ throw new ProviderError("PROVIDER_INVALID", "A provider name is required.");
2664
+ if (input.enabled === true) {
2665
+ const rows = await store.listProviders(actor, isAdmin);
2666
+ const row = rows.find((candidate) => candidate.name === provider);
2667
+ if (!row || row.class !== "personal" || !row.usableByMe) {
2668
+ throw new ProviderError("PROVIDER_NOT_FOUND", `There is no provider named "${provider}".`);
2669
+ }
2670
+ const first = row ? this._providerTypeModels(row.typeId)[0] : null;
2671
+ const credential = await store.getCredential(provider, actor);
2672
+ if (!row || !first || !credential || !this._modelProviders
2673
+ || !resolveProviderCredential(this._modelProviders, credential, first.modelName)) {
2674
+ throw new ProviderError("PROVIDER_INVALID", `Provider "${provider}" does not have a usable credential and endpoint.`);
2675
+ }
2676
+ }
2677
+ const systemUseEnabled = await store.setSystemUse(provider, input.enabled === true, actor, isAdmin);
2678
+ await this._auditProviderMutation(viewer, "setProviderSystemUse", provider, { enabled: systemUseEnabled });
2679
+ return { provider, systemUseEnabled };
2680
+ }
2681
+ async getLegacyProviderMigrationStatus(viewer) {
2682
+ const { store, isAdmin } = await this._providerActor(viewer);
2683
+ if (!isAdmin)
2684
+ throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can read legacy provider migration status.");
2685
+ return store.getLegacyKeyMigrationStatus();
2686
+ }
2687
+ async adoptLegacySystemGitHubCopilotKey(viewer, input) {
2688
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2689
+ if (!isAdmin)
2690
+ throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can adopt the legacy System key.");
2691
+ const adopted = await store.adoptLegacySystemGitHubKey(String(input.name || "").trim(), actor, isAdmin);
2692
+ await this._auditProviderMutation(viewer, "adoptLegacySystemGitHubCopilotKey", adopted.name, {
2693
+ class: "personal", systemUseEnabled: true,
2694
+ });
2695
+ return { provider: adopted, status: await store.getLegacyKeyMigrationStatus() };
2696
+ }
2697
+ async getModelDefaults(viewer) {
2698
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2699
+ const [configured, credentials, systemOverrides] = await Promise.all([
2700
+ store.getDefaults(actor),
2701
+ store.allCredentials(),
2702
+ store.listSystemAgentModels(),
2703
+ ]);
2704
+ const clusterFallback = this._firstAvailableTuple(credentials, (credential) => credential.class === "shared");
2705
+ const userFallback = this._firstAvailableTuple(credentials, (credential) => credential.class === "shared" || (actor !== null && credential.ownerUserId === actor));
2706
+ const systemFallback = this._firstAvailableTuple(credentials, (credential) => credential.class === "shared" || credential.systemUseEnabled === true);
2707
+ const resolveConfigured = (candidates, eligible, fallback) => {
2708
+ try {
2709
+ if (candidates.some((candidate) => candidate.tuple.provider || candidate.tuple.model)) {
2710
+ const resolved = resolveRuntimeModelSelection(this._modelProviders, credentials, {
2711
+ defaults: candidates,
2712
+ eligible,
2713
+ });
2714
+ return { tuple: resolved, source: resolved.source, error: null };
2715
+ }
2716
+ return { tuple: fallback, source: "first_available", error: null };
2717
+ }
2718
+ catch (error) {
2719
+ return { tuple: null, source: "first_available", error: error?.message || String(error) };
2720
+ }
2721
+ };
2722
+ const clusterResolution = this._modelProviders
2723
+ ? resolveConfigured([{ tuple: configured.cluster, source: "cluster_default" }], (credential) => credential.class === "shared", clusterFallback)
2724
+ : { tuple: null, source: "first_available", error: "Provider type catalog is unavailable." };
2725
+ const userCandidates = configured.mine.provider || configured.mine.model
2726
+ ? [{ tuple: configured.mine, source: "user_default" }]
2727
+ : configured.cluster.provider || configured.cluster.model
2728
+ ? [{ tuple: configured.cluster, source: "cluster_default" }]
2729
+ : [];
2730
+ const userResolution = this._modelProviders
2731
+ ? resolveConfigured(userCandidates, (credential) => credential.class === "shared" || (actor !== null && credential.ownerUserId === actor), userFallback)
2732
+ : { tuple: null, source: "first_available", error: "Provider type catalog is unavailable." };
2733
+ const systemResolution = this._modelProviders
2734
+ ? resolveConfigured([{ tuple: configured.system, source: "system_default" }], (credential) => credential.class === "shared" || credential.systemUseEnabled === true, systemFallback)
2735
+ : { tuple: null, source: "first_available", error: "Provider type catalog is unavailable." };
2736
+ return {
2737
+ userSession: {
2738
+ configured: configured.mine,
2739
+ effective: this._resolvedDefault(userResolution.tuple, userResolution.source),
2740
+ error: userResolution.error,
2741
+ },
2742
+ clusterSession: {
2743
+ configured: configured.cluster,
2744
+ effective: this._resolvedDefault(clusterResolution.tuple, clusterResolution.source),
2745
+ error: clusterResolution.error,
2746
+ },
2747
+ system: {
2748
+ configured: isAdmin ? configured.system : normalizeDefaultTuple(null),
2749
+ effective: isAdmin
2750
+ ? this._resolvedDefault(systemResolution.tuple, systemResolution.source)
2751
+ : null,
2752
+ error: isAdmin ? systemResolution.error : null,
2753
+ updatedBy: isAdmin ? configured.systemUpdatedBy : null,
2754
+ updatedAt: isAdmin ? configured.systemUpdatedAt : null,
2755
+ },
2756
+ systemOverrides: isAdmin ? systemOverrides : [],
2757
+ };
2758
+ }
2759
+ async setSystemModelDefault(viewer, input) {
2760
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2761
+ if (!isAdmin)
2762
+ throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can set the system default.");
2763
+ const requested = normalizeDefaultTuple({
2764
+ provider: input.provider,
2765
+ model: input.model,
2766
+ reasoning: input.reasoningEffort ?? null,
2767
+ context: input.contextTier ?? null,
2768
+ });
2769
+ const configured = await this._validatedDefaultTuple(store, actor, isAdmin, requested, "system");
2770
+ const disposition = input.restartExisting
2771
+ ? normalizeSystemRestartDisposition(input.restartExisting.disposition)
2772
+ : null;
2773
+ await store.setSystemDefault(actor, isAdmin, configured);
2774
+ await this._auditProviderMutation(viewer, "setSystemModelDefault", configured.provider, {
2775
+ model: configured.model,
2776
+ cleared: configured.provider === null,
2777
+ restartDisposition: disposition,
2778
+ });
2779
+ const defaults = await this.getModelDefaults(viewer);
2780
+ if (!input.restartExisting) {
2781
+ return { configured, effective: defaults.system.effective, restart: null };
2782
+ }
2783
+ if (!defaults.system.effective) {
2784
+ throw new ProviderError("PROVIDER_INVALID", "No system-eligible provider is available for restart.");
2785
+ }
2786
+ const operationId = [
2787
+ "system-default",
2788
+ disposition,
2789
+ defaults.system.effective.model,
2790
+ defaults.system.effective.reasoningEffort ?? "",
2791
+ defaults.system.effective.contextTier ?? "",
2792
+ ].join(":");
2793
+ const overridden = new Set(defaults.systemOverrides.map((override) => override.agentId));
2794
+ const candidatePlans = this._getSystemAgentPlans().filter((plan) => !overridden.has(plan.agent.id));
2795
+ const plans = [];
2796
+ for (const plan of candidatePlans) {
2797
+ const row = await this._catalog.getSession(plan.sessionId).catch(() => null);
2798
+ const rollout = await store.getSystemRestart(plan.agent.id);
2799
+ const retryingSameOperation = rollout?.operationId === operationId
2800
+ && rollout.status !== "complete";
2801
+ const live = Boolean(row && !row.deletedAt
2802
+ && !["completed", "failed", "error", "cancelled"].includes(row.state));
2803
+ if (!retryingSameOperation && !live)
2804
+ continue;
2805
+ if (!retryingSameOperation && row.orchestrationId && row.state !== "pending"
2806
+ && row.model === defaults.system.effective.model
2807
+ && (row.reasoningEffort ?? null) === (defaults.system.effective.reasoningEffort ?? null)
2808
+ && (row.contextTier ?? null) === (defaults.system.effective.contextTier ?? null))
2809
+ continue;
2810
+ plans.push(plan);
2811
+ }
2812
+ const failures = [];
2813
+ let restarted = 0;
2814
+ for (const plan of plans) {
2815
+ try {
2816
+ const outcome = await this.restartSystemSession(plan.agent.id, {
2817
+ disposition: disposition,
2818
+ model: defaults.system.effective.model,
2819
+ reasoningEffort: defaults.system.effective.reasoningEffort,
2820
+ contextTier: defaults.system.effective.contextTier,
2821
+ modelResolutionSource: defaults.system.effective.source,
2822
+ operationId,
2823
+ reason: "Applying the configured system model default",
2824
+ });
2825
+ if (!outcome.skippedReason)
2826
+ restarted += 1;
2827
+ }
2828
+ catch (error) {
2829
+ failures.push({ agentId: plan.agent.id, error: error?.message || String(error) });
2830
+ }
2831
+ }
2832
+ return {
2833
+ configured,
2834
+ effective: defaults.system.effective,
2835
+ restart: { requested: true, disposition: disposition, affected: plans.length, restarted, failures },
2836
+ };
2837
+ }
2838
+ async setSystemSessionModel(viewer, input) {
2839
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2840
+ if (!isAdmin)
2841
+ throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can set a system-agent model.");
2842
+ const plan = this._resolveSystemAgentPlan(input.agentId);
2843
+ const next = await this._validatedDefaultTuple(store, actor, isAdmin, normalizeDefaultTuple({
2844
+ provider: input.provider,
2845
+ model: input.model,
2846
+ reasoning: input.reasoningEffort ?? null,
2847
+ context: input.contextTier ?? null,
2848
+ }), "system");
2849
+ await store.setSystemAgentModel(plan.agent.id, actor, isAdmin, next);
2850
+ await this._auditProviderMutation(viewer, "setSystemSessionModel", plan.agent.id, {
2851
+ provider: next.provider, model: next.model,
2852
+ });
2853
+ const row = await this._catalog.getSession(plan.sessionId).catch(() => null);
2854
+ if (row && !row.deletedAt && row.orchestrationId) {
2855
+ await this.sendCommand(plan.sessionId, {
2856
+ cmd: "set_model",
2857
+ id: buildLifecycleCommandId("set-system-model"),
2858
+ args: {
2859
+ model: next.model,
2860
+ reasoningEffort: next.reasoning,
2861
+ contextTier: next.context,
2862
+ source: "system_default",
2863
+ },
2864
+ });
2865
+ }
2866
+ return (await store.listSystemAgentModels()).find((override) => override.agentId === plan.agent.id);
2867
+ }
2868
+ async clearSystemSessionModel(viewer, agentId) {
2869
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2870
+ if (!isAdmin)
2871
+ throw new ProviderError("PROVIDER_FORBIDDEN", "Only an administrator can clear a system-agent model.");
2872
+ const plan = this._resolveSystemAgentPlan(agentId);
2873
+ const cleared = await store.clearSystemAgentModel(plan.agent.id, actor, isAdmin);
2874
+ if (cleared)
2875
+ await this._auditProviderMutation(viewer, "clearSystemSessionModel", plan.agent.id, { cleared: true });
2876
+ if (cleared) {
2877
+ const defaults = await this.getModelDefaults(viewer);
2878
+ const row = await this._catalog.getSession(plan.sessionId).catch(() => null);
2879
+ if (row && !row.deletedAt && row.orchestrationId && defaults.system.effective) {
2880
+ await this.sendCommand(plan.sessionId, {
2881
+ cmd: "set_model",
2882
+ id: buildLifecycleCommandId("clear-system-model"),
2883
+ args: {
2884
+ model: defaults.system.effective.model,
2885
+ reasoningEffort: defaults.system.effective.reasoningEffort,
2886
+ contextTier: defaults.system.effective.contextTier,
2887
+ source: "system_default",
2888
+ },
2889
+ });
2890
+ }
2891
+ }
2892
+ return { agentId: plan.agent.id, cleared };
2893
+ }
2894
+ /**
2895
+ * Where the tokens went: totals, the daily chart, and one breakdown, over
2896
+ * the same filters. Three reads because they group differently, one
2897
+ * answer because a report that shows a total and a chart from different
2898
+ * filters is a report nobody can act on.
2899
+ *
2900
+ * The breakdown asks for one row more than it returns, so `truncated`
2901
+ * reports the cut rather than guessing at it.
2902
+ */
2903
+ async getProviderUsage(viewer, query = {}) {
2904
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2905
+ const dimension = PROVIDER_USAGE_DIMENSIONS.includes(query.dimension)
2906
+ ? query.dimension
2907
+ : "provider";
2908
+ const limit = clampInteger(query.limit ?? undefined, 40, 1, 200);
2909
+ const filters = {
2910
+ days: clampInteger(query.days ?? undefined, 7, 1, 365),
2911
+ // `mine` beats a supplied id: it is the one form that cannot name
2912
+ // anybody but the caller.
2913
+ ownerUserId: query.mine === true ? actor : (query.ownerUserId ?? null),
2914
+ provider: query.provider ?? null,
2915
+ model: query.model ?? null,
2916
+ sessionId: query.sessionId ?? null,
2917
+ chargeClass: query.chargeClass ?? null,
2918
+ };
2919
+ const [totals, daily, breakdown] = await Promise.all([
2920
+ store.usageTotals(actor, isAdmin, filters),
2921
+ store.usageDaily(actor, isAdmin, filters),
2922
+ store.usageBreakdown(actor, isAdmin, dimension, filters, limit + 1),
2923
+ ]);
2924
+ return {
2925
+ totals,
2926
+ daily,
2927
+ breakdown: breakdown.slice(0, limit),
2928
+ dimension,
2929
+ truncated: breakdown.length > limit,
2930
+ };
2931
+ }
2932
+ /** Sessions waiting on a limit right now, with what is holding each one. */
2933
+ async listPausedSessions(viewer) {
2934
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
2935
+ return { sessions: await store.listPaused(actor, isAdmin) };
2936
+ }
2937
+ // ─── Agent packages / worker registry ────────────────────
2938
+ _agentPackagePrincipal(owner, isAdmin) {
2939
+ if (owner?.provider && owner?.subject) {
2940
+ return { provider: owner.provider, subject: owner.subject };
2941
+ }
2942
+ if (isAdmin)
2943
+ return null;
2944
+ const error = new Error("agent-package operations require an authenticated principal");
2945
+ error.code = "FORBIDDEN";
2946
+ throw error;
2947
+ }
2948
+ _agentPackageSelector(selector, isAdmin = false) {
2949
+ if (!selector || typeof selector !== "object")
2950
+ return null;
2951
+ const scope = selector.scope === "shared" || selector.scope === "user" ? selector.scope : null;
2952
+ const provider = String(selector.ownerProvider || selector.owner?.provider || "").trim();
2953
+ const subject = String(selector.ownerSubject || selector.owner?.subject || "").trim();
2954
+ const owner = isAdmin && provider && subject ? { provider, subject } : null;
2955
+ if (!scope && !owner)
2956
+ return null;
2957
+ return { ...(scope ? { scope } : {}), ...(owner ? { owner } : {}) };
2958
+ }
2959
+ async _mapAgentPackageErrors(run) {
2960
+ try {
2961
+ return await run();
2962
+ }
2963
+ catch (error) {
2964
+ const message = String(error?.message || "");
2965
+ if (/AGENT_(?:PACKAGE|SOURCE)_[A-Z_]*FORBIDDEN/.test(message))
2966
+ error.code = "FORBIDDEN";
2967
+ else if (/AGENT_(?:PACKAGE|SOURCE)_[A-Z_]*NOT_FOUND/.test(message))
2968
+ error.code = "NOT_FOUND";
2969
+ else if (/AGENT_PACKAGE_(?:SEMVER_CONFLICT|SCOPE_MISMATCH|NAME_TAKEN|AMBIGUOUS)/.test(message))
2970
+ error.code = "CONFLICT";
2971
+ else if (/AGENT_PACKAGE_(?:BAD_(?:SCOPE|NAME)|NO_OWNER|RESERVED_NAME)/.test(message))
2972
+ error.code = "INVALID_REQUEST";
2973
+ throw error;
2974
+ }
2975
+ }
2976
+ _requireAgentPackageArtifacts() {
2977
+ if (!this._artifactStore) {
2978
+ throw new Error("agent-package artifact operations require an artifact store in direct mode");
2979
+ }
2980
+ return this._artifactStore;
2981
+ }
2982
+ async listAgentPackages(owner, isAdmin) {
2983
+ this._ensureStarted();
2984
+ return this._catalog.listAgentPackages(this._agentPackagePrincipal(owner, isAdmin), isAdmin);
2985
+ }
2986
+ async getAgentPackage(name, owner, isAdmin, selector) {
2987
+ this._ensureStarted();
2988
+ return this._catalog.getAgentPackage(name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin));
2989
+ }
2990
+ async listAgentWorkerState() {
2991
+ this._ensureStarted();
2992
+ return this._catalog.listAgentWorkerState();
2993
+ }
2994
+ async listWorkers() {
2995
+ this._ensureStarted();
2996
+ return this._catalog.listWorkers();
2997
+ }
2998
+ async setAgentPackageScope(name, scope, owner, isAdmin, selector) {
2999
+ this._ensureStarted();
3000
+ const targetScope = scope === "shared" ? "shared" : "user";
3001
+ const principal = this._agentPackagePrincipal(owner, isAdmin);
3002
+ const ownerOverride = this._agentPackageSelector(selector, isAdmin)?.owner ?? null;
3003
+ let sourceSelector = null;
3004
+ if (targetScope === "user")
3005
+ sourceSelector = { scope: "shared", ...(ownerOverride ? { owner: ownerOverride } : {}) };
3006
+ else if (ownerOverride)
3007
+ sourceSelector = { scope: "user", owner: ownerOverride };
3008
+ await this._mapAgentPackageErrors(() => this._catalog.setAgentPackageScope(name, targetScope, principal, isAdmin, sourceSelector));
3009
+ return { ok: true };
3010
+ }
3011
+ async setAgentPackageEnabled(name, enabled, owner, isAdmin, selector) {
3012
+ this._ensureStarted();
3013
+ await this._mapAgentPackageErrors(() => this._catalog.setAgentPackageEnabled(name, Boolean(enabled), this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin)));
3014
+ return { ok: true };
3015
+ }
3016
+ async pinAgentPackageVersion(name, semver, owner, isAdmin, selector) {
3017
+ this._ensureStarted();
3018
+ await this._mapAgentPackageErrors(() => this._catalog.pinAgentPackageVersion(name, semver, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin)));
3019
+ return { ok: true };
3020
+ }
3021
+ async deleteAgentPackage(name, owner, isAdmin, selector) {
3022
+ this._ensureStarted();
3023
+ await this._mapAgentPackageErrors(() => deleteAgentPackageEverywhere({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin)));
3024
+ return { ok: true };
3025
+ }
3026
+ async publishAgentPackageDirectory(dir, scope, owner, isAdmin, opts = {}) {
3027
+ this._ensureStarted();
3028
+ return this._mapAgentPackageErrors(() => publishAgentPackageDir({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, {
3029
+ dir,
3030
+ scope,
3031
+ owner: this._agentPackagePrincipal(owner, isAdmin),
3032
+ createdBy: opts.createdBy ?? null,
3033
+ isAdmin,
3034
+ ...(opts.reservedAgentNames?.length ? { validate: { reservedAgentNames: opts.reservedAgentNames } } : {}),
3035
+ }));
3036
+ }
3037
+ async uploadAgentPackage(files, scope, owner, isAdmin, opts = {}) {
3038
+ const invalid = (message) => Object.assign(new Error(message), { code: "INVALID_REQUEST" });
3039
+ const tooLarge = (message) => Object.assign(new Error(message), { code: "PAYLOAD_TOO_LARGE" });
3040
+ if (!Array.isArray(files) || files.length === 0)
3041
+ throw invalid("upload requires files: [{ path, contentBase64 }]");
3042
+ if (files.length > 2_000)
3043
+ throw tooLarge("upload exceeds 2000 files");
3044
+ const normalizedPaths = files.map((file) => String(file?.path || ""));
3045
+ const pathSet = new Set();
3046
+ for (const rel of normalizedPaths) {
3047
+ if (pathSet.has(rel))
3048
+ throw invalid(`upload contains duplicate file path: ${rel}`);
3049
+ const parts = rel.split("/");
3050
+ for (let index = 1; index < parts.length; index++) {
3051
+ const parent = parts.slice(0, index).join("/");
3052
+ if (pathSet.has(parent))
3053
+ throw invalid(`upload path conflicts with file path: ${parent} and ${rel}`);
3054
+ }
3055
+ for (const existing of pathSet) {
3056
+ if (existing.startsWith(`${rel}/`))
3057
+ throw invalid(`upload path conflicts with file path: ${rel} and ${existing}`);
3058
+ }
3059
+ pathSet.add(rel);
3060
+ }
3061
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ps-agent-upload-"));
3062
+ try {
3063
+ let total = 0;
3064
+ for (const file of files) {
3065
+ const rel = String(file?.path || "");
3066
+ if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.split("/").some((part) => part === "..")) {
3067
+ throw invalid(`upload file path is not a safe relative path: ${rel}`);
3068
+ }
3069
+ const encoded = String(file?.contentBase64 || "");
3070
+ const remaining = 2 * 1024 * 1024 - total;
3071
+ if (encoded.length > Math.ceil(remaining * 4 / 3) + 4)
3072
+ throw tooLarge("upload exceeds the 2 MB envelope");
3073
+ const bytes = Buffer.from(encoded, "base64");
3074
+ total += bytes.length;
3075
+ if (total > 2 * 1024 * 1024)
3076
+ throw tooLarge("upload exceeds the 2 MB envelope");
3077
+ const target = path.resolve(tmp, rel);
3078
+ if (target === tmp || !target.startsWith(tmp + path.sep))
3079
+ throw invalid(`upload file path escapes the package root: ${rel}`);
3080
+ fs.mkdirSync(path.dirname(target), { recursive: true });
3081
+ fs.writeFileSync(target, bytes);
3082
+ }
3083
+ return await this.publishAgentPackageDirectory(tmp, scope, owner, isAdmin, opts);
3084
+ }
3085
+ catch (error) {
3086
+ if (error instanceof AgentPackageValidationError) {
3087
+ const wrapped = new Error(error.message);
3088
+ wrapped.code = "VALIDATION_FAILED";
3089
+ wrapped.validation = error.validation;
3090
+ throw wrapped;
3091
+ }
3092
+ throw error;
3093
+ }
3094
+ finally {
3095
+ fs.rmSync(tmp, { recursive: true, force: true });
3096
+ }
3097
+ }
3098
+ async _agentPackageVersion(name, semver, owner, isAdmin, selector) {
3099
+ this._ensureStarted();
3100
+ return resolveAgentPackageVersion(this._catalog, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin), semver);
3101
+ }
3102
+ async getAgentPackageTree(name, semver, owner, isAdmin, selector) {
3103
+ this._ensureStarted();
3104
+ const { version, entries } = await readAgentPackageVersionEntries({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin), semver);
3105
+ const dirs = new Set();
3106
+ const files = entries.map((entry) => {
3107
+ const parts = entry.name.split("/");
3108
+ for (let index = 1; index < parts.length; index++)
3109
+ dirs.add(parts.slice(0, index).join("/"));
3110
+ return { path: entry.name, size: entry.body.length };
3111
+ }).sort((left, right) => left.path.localeCompare(right.path));
3112
+ return { name, semver: version.semver, sha256: version.sha256, dirs: [...dirs].sort(), files };
3113
+ }
3114
+ async getAgentPackageFile(name, semver, filePath, owner, isAdmin, selector) {
3115
+ this._ensureStarted();
3116
+ const { version, entries } = await readAgentPackageVersionEntries({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, name, this._agentPackagePrincipal(owner, isAdmin), isAdmin, this._agentPackageSelector(selector, isAdmin), semver);
3117
+ const entry = entries.find((candidate) => candidate.name === filePath);
3118
+ if (!entry)
3119
+ throw Object.assign(new Error(`"${filePath}" is not in ${name}@${version.semver}`), { code: "NOT_FOUND" });
3120
+ const binary = entry.body.includes(0);
3121
+ let body = entry.body.subarray(0, 256 * 1024);
3122
+ let text = "";
3123
+ if (!binary) {
3124
+ const decoder = new TextDecoder("utf-8", { fatal: true });
3125
+ for (let trim = 0; trim <= 4; trim++) {
3126
+ try {
3127
+ text = decoder.decode(trim ? body.subarray(0, body.length - trim) : body);
3128
+ if (trim)
3129
+ body = body.subarray(0, body.length - trim);
3130
+ break;
3131
+ }
3132
+ catch {
3133
+ if (trim === 4)
3134
+ text = body.toString("utf8");
3135
+ }
3136
+ }
3137
+ }
3138
+ return {
3139
+ name, semver: version.semver, path: filePath, size: entry.body.length,
3140
+ truncated: entry.body.length > body.length,
3141
+ binary,
3142
+ encoding: binary ? "base64" : "utf8",
3143
+ content: binary ? body.toString("base64") : text,
3144
+ };
3145
+ }
3146
+ async downloadAgentPackage(name, semver, owner, isAdmin, selector) {
3147
+ this._ensureStarted();
3148
+ const { version } = await this._agentPackageVersion(name, semver, owner, isAdmin, selector);
3149
+ const body = await fetchAgentPackageTarGz(this._requireAgentPackageArtifacts(), version.artifactFilename, version.sha256);
3150
+ return {
3151
+ name,
3152
+ semver: version.semver,
3153
+ sha256: version.sha256,
3154
+ filename: `${name}@${version.semver}.tgz`,
3155
+ contentType: "application/gzip",
3156
+ body,
3157
+ };
3158
+ }
3159
+ async republishAgentPackageVersion(name, semver, targetScope, owner, isAdmin, opts = {}) {
3160
+ this._ensureStarted();
3161
+ const principal = this._agentPackagePrincipal(owner, isAdmin);
3162
+ const target = targetScope === "shared" ? "shared" : "user";
3163
+ const sourceScope = target === "shared" ? "user" : "shared";
3164
+ return this._mapAgentPackageErrors(async () => {
3165
+ const selectedOwner = this._agentPackageSelector(opts.selector, isAdmin)?.owner ?? null;
3166
+ const { detail, version } = await this._agentPackageVersion(name, semver, principal, isAdmin, {
3167
+ scope: sourceScope,
3168
+ ...(selectedOwner ? { owner: selectedOwner } : {}),
3169
+ });
3170
+ const targz = await fetchAgentPackageTarGz(this._requireAgentPackageArtifacts(), version.artifactFilename, version.sha256);
3171
+ const outcome = await publishPackedAgentPackage({ catalog: this._catalog, artifactStore: this._requireAgentPackageArtifacts() }, {
3172
+ manifest: version.manifest,
3173
+ targz,
3174
+ sha256: version.sha256,
3175
+ scope: target,
3176
+ owner: principal,
3177
+ createdBy: `republish of ${sourceScope} ${name}@${version.semver}`
3178
+ + (opts.createdBy ? ` by ${opts.createdBy}` : ""),
3179
+ isAdmin,
3180
+ sourceId: detail.sourceId ?? null,
3181
+ commitSha: version.commitSha ?? null,
3182
+ });
3183
+ const { manifest: _manifest, ...summary } = outcome;
3184
+ return summary;
3185
+ });
3186
+ }
2038
3187
  // ─── Models ──────────────────────────────────────────────
2039
3188
  /**
2040
3189
  * List all available models across all configured providers.
@@ -2043,6 +3192,7 @@ export class PilotSwarmManagementClient {
2043
3192
  if (!this._modelProviders)
2044
3193
  return [];
2045
3194
  return this._modelProviders.allModels.map((m) => ({
3195
+ catalogKind: "provider_type",
2046
3196
  qualifiedName: m.qualifiedName,
2047
3197
  providerId: m.providerId,
2048
3198
  providerType: m.providerType,
@@ -2057,6 +3207,31 @@ export class PilotSwarmManagementClient {
2057
3207
  credentialAvailable: this.getModelCredentialStatus(m.qualifiedName).credentialAvailable,
2058
3208
  }));
2059
3209
  }
3210
+ async listRuntimeModels(viewer) {
3211
+ const typeModels = this.listModels();
3212
+ const { store, actor, isAdmin } = await this._providerActor(viewer);
3213
+ const [providers, credentials] = await Promise.all([
3214
+ store.listProviders(actor, isAdmin),
3215
+ store.allCredentials(),
3216
+ ]);
3217
+ const credentialByName = new Map(credentials.map((credential) => [credential.name, credential]));
3218
+ const out = [];
3219
+ for (const provider of providers.filter((candidate) => candidate.usableByMe)) {
3220
+ const credential = credentialByName.get(provider.name);
3221
+ for (const model of typeModels.filter((candidate) => candidate.providerId === provider.typeId)) {
3222
+ const credentialAvailable = Boolean(credential && this._modelProviders
3223
+ && resolveProviderCredential(this._modelProviders, credential, model.modelName));
3224
+ out.push({
3225
+ ...model,
3226
+ catalogKind: "runtime_provider",
3227
+ providerId: provider.name,
3228
+ qualifiedName: `${provider.name}:${model.modelName}`,
3229
+ credentialAvailable,
3230
+ });
3231
+ }
3232
+ }
3233
+ return out;
3234
+ }
2060
3235
  /**
2061
3236
  * Get models grouped by provider for display.
2062
3237
  */
@@ -2064,6 +3239,7 @@ export class PilotSwarmManagementClient {
2064
3239
  if (!this._modelProviders)
2065
3240
  return [];
2066
3241
  return this._modelProviders.getModelsByProvider().map(g => ({
3242
+ catalogKind: "provider_type",
2067
3243
  providerId: g.providerId,
2068
3244
  type: g.type,
2069
3245
  models: g.models.map((m) => ({