@coreplane/switchboard 1.223.0 → 1.225.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/assets/config/config.example.yaml +5 -1
  2. package/dist/assets/deploy/cloudflare-resident/Dockerfile +47 -12
  3. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +28 -9
  4. package/dist/assets/package-lock.json +980 -4
  5. package/dist/assets/package.json +2 -1
  6. package/dist/assets/source.json +3 -3
  7. package/dist/assets/src/providers/types.ts +3 -0
  8. package/dist/assets/web/dist/.vite/manifest.json +20 -20
  9. package/dist/assets/web/dist/assets/CostsPage-BlPwsI2T.js +3 -0
  10. package/dist/assets/web/dist/assets/{ResidentDetailPage-BTg4oFcn.js → ResidentDetailPage-Cu4Ve2j4.js} +1 -1
  11. package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dr2qkjKI.js → ResidentsIndexPage-_QIDVyOj.js} +1 -1
  12. package/dist/assets/web/dist/assets/RunRoutePage-DxgcHX-0.js +12 -0
  13. package/dist/assets/web/dist/assets/{RunsIndexPage-DEkZXNnw.js → RunsIndexPage-JQvnDmo0.js} +1 -1
  14. package/dist/assets/web/dist/assets/{ScheduledPage-wA7HE6UW.js → ScheduledPage-DEVRWqZi.js} +1 -1
  15. package/dist/assets/web/dist/assets/{StatusDot-7ojvQlTS.js → StatusDot-BDAKF1Bp.js} +1 -1
  16. package/dist/assets/web/dist/assets/{Tooltip-Cx88lWUW.js → Tooltip-BFncyNUQ.js} +1 -1
  17. package/dist/assets/web/dist/assets/{dist-B85zXO-d.js → dist-DJfQXPjH.js} +1 -1
  18. package/dist/assets/web/dist/assets/{main-BdoqEBpN.js → main-BjhiJxQq.js} +2 -2
  19. package/dist/assets/web/dist/assets/main-DymOuq2l.css +1 -0
  20. package/dist/cli.js +404 -89
  21. package/package.json +2 -1
  22. package/dist/assets/web/dist/assets/CostsPage-C-ximW1N.js +0 -2
  23. package/dist/assets/web/dist/assets/RunRoutePage-CTbnfyBQ.js +0 -12
  24. package/dist/assets/web/dist/assets/main-C1tNx19B.css +0 -1
package/dist/cli.js CHANGED
@@ -11448,6 +11448,41 @@ var init_config3 = __esm({
11448
11448
  }
11449
11449
  });
11450
11450
 
11451
+ // ../../src/providers/types.ts
11452
+ function toolResultText(content) {
11453
+ if (typeof content === "string") return content;
11454
+ return content.map((p) => {
11455
+ if (p.type === "text") return p.text;
11456
+ if (p.type === "image") return `[image ${p.mediaType}, ${base64Bytes(p.data)} bytes]`;
11457
+ return `[document ${p.name ?? "document"} (${p.mediaType}), ${base64Bytes(p.data)} bytes]`;
11458
+ }).join("\n");
11459
+ }
11460
+ function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
11461
+ const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
11462
+ \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
11463
+ if (typeof content === "string") return capText(content);
11464
+ return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
11465
+ }
11466
+ function base64Bytes(b64) {
11467
+ const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
11468
+ return Math.floor(b64.length * 3 / 4) - padding;
11469
+ }
11470
+ function parseModelRef(ref) {
11471
+ const i = ref.indexOf("/");
11472
+ if (i === -1) {
11473
+ throw new Error(`Model "${ref}" must be qualified as "<provider>/<model>", e.g. "anthropic/claude-opus-5"`);
11474
+ }
11475
+ return { provider: ref.slice(0, i), model: ref.slice(i + 1) };
11476
+ }
11477
+ var MAX_TOOL_RESULT_CHARS, ANTHROPIC_API_KEY_ENV;
11478
+ var init_types2 = __esm({
11479
+ "../../src/providers/types.ts"() {
11480
+ "use strict";
11481
+ MAX_TOOL_RESULT_CHARS = 12e4;
11482
+ ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
11483
+ }
11484
+ });
11485
+
11451
11486
  // ../../src/providers/anthropic.ts
11452
11487
  import Anthropic from "@anthropic-ai/sdk";
11453
11488
  function anthropicApiKey(providers, secrets) {
@@ -11568,12 +11603,12 @@ function usageFromAnthropic(u) {
11568
11603
  if (typeof o.cache_creation_input_tokens === "number") usage.cacheWriteTokens = o.cache_creation_input_tokens;
11569
11604
  return usage;
11570
11605
  }
11571
- var ANTHROPIC_API_KEY_ENV, AnthropicProvider, EPHEMERAL, CACHEABLE_BLOCKS;
11606
+ var AnthropicProvider, EPHEMERAL, CACHEABLE_BLOCKS;
11572
11607
  var init_anthropic = __esm({
11573
11608
  "../../src/providers/anthropic.ts"() {
11574
11609
  "use strict";
11575
11610
  init_secrets2();
11576
- ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
11611
+ init_types2();
11577
11612
  AnthropicProvider = class {
11578
11613
  name;
11579
11614
  client;
@@ -11671,6 +11706,8 @@ function parseCostsConfig(raw) {
11671
11706
  const r = raw;
11672
11707
  if (typeof r.cloudflareAccountId !== "string" || !r.cloudflareAccountId)
11673
11708
  throw new Error("costs.cloudflareAccountId is required");
11709
+ if (r.cloudflareAccountName !== void 0 && typeof r.cloudflareAccountName !== "string")
11710
+ throw new Error("costs.cloudflareAccountName must be a string when given");
11674
11711
  if (!r.groups || typeof r.groups !== "object")
11675
11712
  throw new Error("costs.groups must be a mapping of group \u2192 { workers, containerApps }");
11676
11713
  const groups = {};
@@ -11693,6 +11730,7 @@ function parseCostsConfig(raw) {
11693
11730
  }
11694
11731
  return {
11695
11732
  cloudflareAccountId: r.cloudflareAccountId,
11733
+ ...typeof r.cloudflareAccountName === "string" && r.cloudflareAccountName ? { cloudflareAccountName: r.cloudflareAccountName } : {},
11696
11734
  cloudflareTokenEnv: typeof r.cloudflareTokenEnv === "string" ? r.cloudflareTokenEnv : DEFAULT_CF_TOKEN_ENV,
11697
11735
  anthropicAdminKeyEnv: typeof r.anthropicAdminKeyEnv === "string" ? r.anthropicAdminKeyEnv : DEFAULT_ANTHROPIC_ADMIN_ENV,
11698
11736
  groups
@@ -11772,7 +11810,7 @@ function eachDay(range) {
11772
11810
  for (let d = range.from; d <= range.to; d = addDays2(d, 1)) out.push(d);
11773
11811
  return out;
11774
11812
  }
11775
- function buildCostReport(group, cfg, usage, llm, range) {
11813
+ function buildCostReport(group, cfg, usage, llm, range, meta) {
11776
11814
  const attribution = attributionOf(cfg, usage);
11777
11815
  const workers = new Set(cfg.workers);
11778
11816
  const namespaces = attribution.durableObjectNamespaces;
@@ -11890,7 +11928,12 @@ function buildCostReport(group, cfg, usage, llm, range) {
11890
11928
  llmAvailable: llm !== null && !!cfg.anthropicWorkspaceId,
11891
11929
  days,
11892
11930
  totals: { cloudUsd, llmUsd, total: cloudUsd + llmUsd, byResource },
11893
- account: { cloudUsd: accountCloudUsd },
11931
+ generatedAt: meta.generatedAt ?? systemClock(),
11932
+ account: {
11933
+ id: meta.accountId,
11934
+ ...meta.accountName ? { name: meta.accountName } : {},
11935
+ cloudUsd: accountCloudUsd
11936
+ },
11894
11937
  attribution
11895
11938
  };
11896
11939
  }
@@ -11931,9 +11974,14 @@ function createCostsService(cfg, cloudflare, llm, now = () => new Date(systemClo
11931
11974
  async report(group, daysParam) {
11932
11975
  const g = cfg.groups[group];
11933
11976
  if (!g) throw new Error(`unknown cost group ${group}`);
11934
- const range = resolveRange(daysParam, now());
11977
+ const at = now();
11978
+ const range = resolveRange(daysParam, at);
11935
11979
  const [usage, llmRows] = await Promise.all([cloudflare.fetchUsage(range), llm.fetchDailyCost(range)]);
11936
- return buildCostReport(group, g, usage, llmRows, range);
11980
+ return buildCostReport(group, g, usage, llmRows, range, {
11981
+ accountId: cfg.cloudflareAccountId,
11982
+ accountName: cfg.cloudflareAccountName,
11983
+ generatedAt: at.getTime()
11984
+ });
11937
11985
  }
11938
11986
  };
11939
11987
  }
@@ -16685,7 +16733,7 @@ var init_connect = __esm({
16685
16733
 
16686
16734
  // ../../src/mcp/types.ts
16687
16735
  var McpError;
16688
- var init_types2 = __esm({
16736
+ var init_types3 = __esm({
16689
16737
  "../../src/mcp/types.ts"() {
16690
16738
  "use strict";
16691
16739
  McpError = class extends Error {
@@ -16780,7 +16828,7 @@ var MCP_PROTOCOL_VERSION, MCP_REQUEST_TIMEOUT_MS, MCP_MAX_RESPONSE_BYTES, MCP_MA
16780
16828
  var init_client = __esm({
16781
16829
  "../../src/mcp/client.ts"() {
16782
16830
  "use strict";
16783
- init_types2();
16831
+ init_types3();
16784
16832
  MCP_PROTOCOL_VERSION = "2025-06-18";
16785
16833
  MCP_REQUEST_TIMEOUT_MS = 3e4;
16786
16834
  MCP_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
@@ -17495,7 +17543,7 @@ var init_bridge = __esm({
17495
17543
  "../../src/mcp/bridge.ts"() {
17496
17544
  "use strict";
17497
17545
  init_commandRegistry();
17498
- init_types2();
17546
+ init_types3();
17499
17547
  init_classify();
17500
17548
  MCP_TOOL_NAME_MAX = 64;
17501
17549
  MCP_TOOL_PREFIX = "mcp__";
@@ -18527,40 +18575,6 @@ var init_mcp = __esm({
18527
18575
  }
18528
18576
  });
18529
18577
 
18530
- // ../../src/providers/types.ts
18531
- function toolResultText(content) {
18532
- if (typeof content === "string") return content;
18533
- return content.map((p) => {
18534
- if (p.type === "text") return p.text;
18535
- if (p.type === "image") return `[image ${p.mediaType}, ${base64Bytes(p.data)} bytes]`;
18536
- return `[document ${p.name ?? "document"} (${p.mediaType}), ${base64Bytes(p.data)} bytes]`;
18537
- }).join("\n");
18538
- }
18539
- function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
18540
- const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
18541
- \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
18542
- if (typeof content === "string") return capText(content);
18543
- return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
18544
- }
18545
- function base64Bytes(b64) {
18546
- const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
18547
- return Math.floor(b64.length * 3 / 4) - padding;
18548
- }
18549
- function parseModelRef(ref) {
18550
- const i = ref.indexOf("/");
18551
- if (i === -1) {
18552
- throw new Error(`Model "${ref}" must be qualified as "<provider>/<model>", e.g. "anthropic/claude-opus-5"`);
18553
- }
18554
- return { provider: ref.slice(0, i), model: ref.slice(i + 1) };
18555
- }
18556
- var MAX_TOOL_RESULT_CHARS;
18557
- var init_types3 = __esm({
18558
- "../../src/providers/types.ts"() {
18559
- "use strict";
18560
- MAX_TOOL_RESULT_CHARS = 12e4;
18561
- }
18562
- });
18563
-
18564
18578
  // ../../src/core/memory/scorer.ts
18565
18579
  function tokenize2(text) {
18566
18580
  return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
@@ -19383,7 +19397,7 @@ async function memoryContextBlock(organization, cfg, injected, query, userId, sc
19383
19397
  var init_memory = __esm({
19384
19398
  "../../src/core/memory/index.ts"() {
19385
19399
  "use strict";
19386
- init_types3();
19400
+ init_types2();
19387
19401
  init_scorer();
19388
19402
  init_scope();
19389
19403
  init_stores();
@@ -22578,7 +22592,9 @@ function afterReply(deps, ctx) {
22578
22592
  scheduleReflection({
22579
22593
  cfg: deps.config.config.memory,
22580
22594
  store: deps.memory,
22581
- providers: deps.providers,
22595
+ // The extractor's one call goes through pi's model library
22596
+ // (harness-pi.md item 13), never the loop's own provider adapters.
22597
+ providers: deps.completions,
22582
22598
  runModelRef: resolved.modelRef,
22583
22599
  gate: { toolCalls, historyTurns: history.length, agentName: resolved.agentName },
22584
22600
  threadKey: msg.threadKey,
@@ -25123,7 +25139,7 @@ var init_resolve = __esm({
25123
25139
  init_registry();
25124
25140
  init_profile();
25125
25141
  init_directives();
25126
- init_types3();
25142
+ init_types2();
25127
25143
  init_factory();
25128
25144
  init_githubRepoProbe();
25129
25145
  init_repoContext();
@@ -28206,7 +28222,7 @@ async function routeRequest(deps, ctx) {
28206
28222
  if (deps.routeModel) model = deps.routeModel;
28207
28223
  else {
28208
28224
  const ref = parseModelRef(modelRef2);
28209
- model = providerRouteModel(deps.providers.get(ref.provider), ref.model, {
28225
+ model = providerRouteModel(deps.completions.get(ref.provider), ref.model, {
28210
28226
  ...cfg.routing?.answer ? { answer: cfg.routing.answer } : {}
28211
28227
  });
28212
28228
  }
@@ -28276,7 +28292,7 @@ var init_route = __esm({
28276
28292
  init_registry();
28277
28293
  init_workspace();
28278
28294
  init_config2();
28279
- init_types3();
28295
+ init_types2();
28280
28296
  init_redact();
28281
28297
  init_statusCardFrame();
28282
28298
  init_spawn();
@@ -28883,7 +28899,7 @@ var MAX_CONSECUTIVE_INFRA_FAILURES, HardStopError, HARD_STOP_MESSAGE, wrapUpNote
28883
28899
  var init_runner = __esm({
28884
28900
  "../../src/runner.ts"() {
28885
28901
  "use strict";
28886
- init_types3();
28902
+ init_types2();
28887
28903
  init_runEvents();
28888
28904
  init_threadAdmission();
28889
28905
  init_executor();
@@ -30673,7 +30689,7 @@ var init_provision = __esm({
30673
30689
  init_resident();
30674
30690
  init_memory();
30675
30691
  init_runBearers();
30676
- init_types3();
30692
+ init_types2();
30677
30693
  init_skills2();
30678
30694
  init_source();
30679
30695
  init_configAwareness();
@@ -31681,7 +31697,7 @@ var init_bridge2 = __esm({
31681
31697
  return this.answerText ?? this.heldAnswer;
31682
31698
  }
31683
31699
  observe(event) {
31684
- const out = { replies: [], settled: false, turnEnded: false };
31700
+ const out = { replies: [], settled: false };
31685
31701
  const disposition = PI_EVENT_DISPOSITION[event.type];
31686
31702
  if (disposition === void 0) {
31687
31703
  this.note("harness_error", `pi emitted an event kind this build does not know: ${redactAndCap(event.type, 80)}`);
@@ -31698,9 +31714,6 @@ var init_bridge2 = __esm({
31698
31714
  case "agent_settled":
31699
31715
  out.settled = true;
31700
31716
  break;
31701
- case "turn_end":
31702
- out.turnEnded = true;
31703
- break;
31704
31717
  case "message_start":
31705
31718
  if (isRecord3(event.message) && event.message.role === "assistant") this.assistantStartedAt = this.deps.clock();
31706
31719
  break;
@@ -31893,6 +31906,7 @@ var init_bridge2 = __esm({
31893
31906
  });
31894
31907
 
31895
31908
  // ../../src/core/harness/pi/mirror.ts
31909
+ import { isDeepStrictEqual } from "node:util";
31896
31910
  function partsOf2(content) {
31897
31911
  if (typeof content === "string") return [{ type: "text", text: content }];
31898
31912
  if (!Array.isArray(content)) return [];
@@ -32042,11 +32056,14 @@ var init_mirror = __esm({
32042
32056
  constructor(deps) {
32043
32057
  this.deps = deps;
32044
32058
  this.idx = deps.seedLength;
32059
+ this.mirroredTail = deps.mirroredTail;
32045
32060
  }
32046
32061
  deps;
32047
32062
  idx;
32048
32063
  pendingUser = [];
32049
- seedEchoed = false;
32064
+ /** pi is about to echo the seed's prompt as a user message (see `expectSeedEcho`). */
32065
+ seedEchoPending = false;
32066
+ mirroredTail;
32050
32067
  iteration = 0;
32051
32068
  /** The highest ledger inbox seq folded in so far (run-history item 40). */
32052
32069
  inboxConsumedSeq = 0;
@@ -32054,19 +32071,43 @@ var init_mirror = __esm({
32054
32071
  get wired() {
32055
32072
  return this.deps.onStep !== void 0;
32056
32073
  }
32074
+ /** pi answered the seed's prompt: the first user message without results
32075
+ * that follows is its echo — the seed, on the ledger already — and is not
32076
+ * written twice. A continue prompt (harness-pi item 8) expects none: its
32077
+ * echo is a turn the model was told, folded in like a steer's text. */
32078
+ expectSeedEcho() {
32079
+ this.seedEchoPending = true;
32080
+ }
32081
+ /** The row about to be written, against the transcript's tail — compared
32082
+ * once, with the first row the mirror meets: equal means the ledger holds
32083
+ * it already (read again, harness-pi item 8), together with the results
32084
+ * pending before it, which were its step's user turn. */
32085
+ alreadyHeld(row) {
32086
+ const tail = this.mirroredTail;
32087
+ if (tail === void 0) return false;
32088
+ this.mirroredTail = void 0;
32089
+ if (!isDeepStrictEqual(row, tail)) return false;
32090
+ this.pendingUser = [];
32091
+ return true;
32092
+ }
32093
+ /** Feeds one finished message; answers whether the ledger now holds
32094
+ * everything up to it — a step written, or an assistant turn found already
32095
+ * there — so the harness can move the row's offset past it. A result or a
32096
+ * steer's text waits in memory for the next step, and the answer is no. */
32057
32097
  async onMessage(message3, turn) {
32058
- if (!this.deps.onStep) return;
32098
+ if (!this.deps.onStep) return false;
32059
32099
  const chat = chatMessageOf(message3);
32060
- if (!chat) return;
32061
- if (chat.content.length === 0) return;
32100
+ if (!chat) return false;
32101
+ if (chat.content.length === 0) return false;
32062
32102
  if (chat.role === "user") {
32063
- if (!this.seedEchoed && !chat.content.some((p) => p.type === "tool_result")) {
32064
- this.seedEchoed = true;
32065
- return;
32103
+ if (this.seedEchoPending && !chat.content.some((p) => p.type === "tool_result")) {
32104
+ this.seedEchoPending = false;
32105
+ return false;
32066
32106
  }
32067
32107
  this.pendingUser.push(...chat.content);
32068
- return;
32108
+ return false;
32069
32109
  }
32110
+ if (this.alreadyHeld({ turn: chat })) return true;
32070
32111
  const turns = [];
32071
32112
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
32072
32113
  this.pendingUser = [];
@@ -32083,13 +32124,16 @@ var init_mirror = __esm({
32083
32124
  };
32084
32125
  this.idx += turns.length;
32085
32126
  await this.deps.onStep(report2);
32127
+ return true;
32086
32128
  }
32087
32129
  /** pi compacted (session-log item 6): the results pending since the last
32088
32130
  * assistant turn go as the user turn they would have been, the entry as the
32089
32131
  * row after them — its own step with nothing in flight, so the log holds
32090
- * the summary where pi wrote it and the index moves past it. */
32132
+ * the summary where pi wrote it and the index moves past it. Answers as
32133
+ * `onMessage` does: the ledger holds everything up to the entry. */
32091
32134
  async onCompaction(entry, turn) {
32092
- if (!this.deps.onStep) return;
32135
+ if (!this.deps.onStep) return false;
32136
+ if (this.alreadyHeld({ compaction: entry })) return true;
32093
32137
  const turns = [];
32094
32138
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
32095
32139
  this.pendingUser = [];
@@ -32105,6 +32149,7 @@ var init_mirror = __esm({
32105
32149
  };
32106
32150
  this.idx += turns.length + 1;
32107
32151
  await this.deps.onStep(report2);
32152
+ return true;
32108
32153
  }
32109
32154
  };
32110
32155
  }
@@ -32121,11 +32166,16 @@ var init_transport = __esm({
32121
32166
  constructor(deps) {
32122
32167
  this.deps = deps;
32123
32168
  this.offset = deps.offset ?? 0;
32169
+ this.consumedOffset = this.offset;
32124
32170
  this.lines = this.read();
32125
32171
  }
32126
32172
  deps;
32127
- /** The byte the next read starts at — the re-attach fact. */
32173
+ /** The byte the next read starts at: the read position, moved by whole chunks. */
32128
32174
  offset;
32175
+ /** The boundary after the record last handed out, moved by records: where a
32176
+ * reader that has seen everything so far continues from — the harness's
32177
+ * re-attach fact, taken when nothing handed out is left unwritten. */
32178
+ consumedOffset;
32129
32179
  /** pi was found dead while the harness still listened. */
32130
32180
  exited = false;
32131
32181
  closed = false;
@@ -32178,11 +32228,15 @@ var init_transport = __esm({
32178
32228
  /** Complete lines out of the buffered bytes; the partial tail waits for more. */
32179
32229
  *split(chunk) {
32180
32230
  this.buffer = Buffer.concat([this.buffer, Buffer.from(chunk)]);
32231
+ const base = this.offset - this.buffer.length;
32181
32232
  let start3 = 0;
32182
32233
  for (let i = this.buffer.indexOf(NEWLINE, start3); i >= 0; i = this.buffer.indexOf(NEWLINE, start3)) {
32183
32234
  let line3 = this.buffer.subarray(start3, i).toString("utf8");
32184
32235
  if (line3.endsWith("\r")) line3 = line3.slice(0, -1);
32185
- if (line3.length > 0) yield line3;
32236
+ if (line3.length > 0) {
32237
+ this.consumedOffset = base + i + 1;
32238
+ yield line3;
32239
+ }
32186
32240
  start3 = i + 1;
32187
32241
  }
32188
32242
  this.buffer = this.buffer.subarray(start3);
@@ -32192,6 +32246,7 @@ var init_transport = __esm({
32192
32246
  });
32193
32247
 
32194
32248
  // ../../src/core/harness/pi/harness.ts
32249
+ import { randomUUID as randomUUID2 } from "node:crypto";
32195
32250
  function piHarnessFactsOf(value) {
32196
32251
  if (typeof value !== "object" || value === null) return void 0;
32197
32252
  const v = value;
@@ -32252,6 +32307,13 @@ function settlementResults(settlements) {
32252
32307
  }))
32253
32308
  };
32254
32309
  }
32310
+ function ledgerTailOf(resume) {
32311
+ if (!resume) return void 0;
32312
+ const closing = resume.compactions?.filter((c) => c.before === resume.messages.length).at(-1);
32313
+ if (closing) return { compaction: closing.entry };
32314
+ const last = resume.messages.at(-1);
32315
+ return last?.role === "assistant" ? { turn: last } : void 0;
32316
+ }
32255
32317
  function settlementAnswer(s) {
32256
32318
  return { content: [{ type: "text", text: settlementText(s) }], isError: true };
32257
32319
  }
@@ -32296,10 +32358,12 @@ async function runPiHarness(deps, run2) {
32296
32358
  };
32297
32359
  }
32298
32360
  bridge.turns = run2.resume?.turn ?? 0;
32361
+ const tail = ledgerTailOf(run2.resume);
32299
32362
  const mirror = new PiMirror({
32300
32363
  onStep: run2.onStep,
32301
- seedLength: run2.resume?.messages.length ?? run2.messages.length,
32302
- remainingMs: () => deadline - now()
32364
+ seedLength: run2.resume ? run2.resume.messages.length + (run2.resume.compactions?.length ?? 0) : run2.messages.length,
32365
+ remainingMs: () => deadline - now(),
32366
+ ...tail ? { mirroredTail: tail } : {}
32303
32367
  });
32304
32368
  mirror.inboxConsumedSeq = run2.resume?.inboxConsumedSeq ?? 0;
32305
32369
  let writeUp;
@@ -32336,12 +32400,17 @@ async function runPiHarness(deps, run2) {
32336
32400
  let pid;
32337
32401
  let transport;
32338
32402
  let facts2;
32403
+ let mirrored = 0;
32339
32404
  const save = () => {
32340
32405
  if (facts2) {
32341
- if (transport) facts2 = { ...facts2, logOffset: transport.offset };
32406
+ facts2 = { ...facts2, logOffset: mirrored };
32342
32407
  run2.saveFacts?.(facts2);
32343
32408
  }
32344
32409
  };
32410
+ const held = () => {
32411
+ if (transport) mirrored = transport.consumedOffset;
32412
+ save();
32413
+ };
32345
32414
  let catchingUp = false;
32346
32415
  try {
32347
32416
  const recorded = run2.resume?.facts;
@@ -32368,13 +32437,14 @@ async function runPiHarness(deps, run2) {
32368
32437
  if (reattached && recorded && paths !== void 0) {
32369
32438
  pid = recorded.pid;
32370
32439
  facts2 = { ...recorded, ...recorded.container === void 0 && here !== void 0 ? { container: here } : {} };
32440
+ mirrored = recorded.logOffset;
32371
32441
  transport = new PiRpcTransport({
32372
32442
  container,
32373
32443
  paths,
32374
32444
  pid,
32375
32445
  pollMs: deps.pollMs ?? 750,
32376
32446
  sleep: deps.sleep,
32377
- offset: recorded.logOffset
32447
+ offset: mirrored
32378
32448
  });
32379
32449
  catchingUp = true;
32380
32450
  const inFlight = run2.resume?.settlements.length ?? 0;
@@ -32452,12 +32522,15 @@ async function runPiHarness(deps, run2) {
32452
32522
  save();
32453
32523
  transport = new PiRpcTransport({ container, paths, pid, pollMs: deps.pollMs ?? 750, sleep: deps.sleep });
32454
32524
  }
32455
- transport.send({ id: "retry", type: "set_auto_retry", enabled: false });
32456
- transport.send({ id: "state", type: "get_state" });
32525
+ const stamp = `${now()}-${randomUUID2().slice(0, 8)}`;
32526
+ const ids = { retry: `retry:${stamp}`, state: `state:${stamp}`, prompt: `prompt:${stamp}` };
32527
+ const continuing = reattached || run2.resume !== void 0;
32528
+ transport.send({ id: ids.retry, type: "set_auto_retry", enabled: false });
32529
+ transport.send({ id: ids.state, type: "get_state" });
32457
32530
  if (reattached)
32458
- transport.send({ id: "prompt", type: "prompt", message: CONTINUE_PROMPT, streamingBehavior: "steer" });
32459
- else if (run2.resume) transport.send({ id: "prompt", type: "prompt", message: CONTINUE_PROMPT });
32460
- else transport.send({ id: "prompt", type: "prompt", ...promptOf(run2.messages) });
32531
+ transport.send({ id: ids.prompt, type: "prompt", message: CONTINUE_PROMPT, streamingBehavior: "steer" });
32532
+ else if (run2.resume) transport.send({ id: ids.prompt, type: "prompt", message: CONTINUE_PROMPT });
32533
+ else transport.send({ id: ids.prompt, type: "prompt", ...promptOf(run2.messages) });
32461
32534
  let warned = false;
32462
32535
  let settled = false;
32463
32536
  let stopMode;
@@ -32575,19 +32648,20 @@ ${stagedLine}` : prompt,
32575
32648
  }
32576
32649
  if (obs.response) {
32577
32650
  const r = obs.response;
32578
- if (r.id === "state" && r.success === true && typeof r.data?.sessionFile === "string") {
32651
+ if (r.id === ids.state && r.success === true && typeof r.data?.sessionFile === "string") {
32579
32652
  const data = r.data;
32580
32653
  facts2 = { ...facts2 ?? { pid, logOffset: 0, root: paths.dir }, sessionFile: String(data.sessionFile) };
32581
32654
  save();
32582
32655
  }
32583
- if (r.id === "prompt") {
32656
+ if (r.id === ids.prompt) {
32584
32657
  catchingUp = false;
32585
32658
  if (r.success === false) throw new PromptRefused(String(r.error ?? "no reason"));
32659
+ if (!continuing) mirror.expectSeedEcho();
32586
32660
  }
32587
32661
  }
32588
- if (obs.message) await mirror.onMessage(obs.message, bridge.turns);
32662
+ if (obs.message && await mirror.onMessage(obs.message, bridge.turns)) held();
32589
32663
  if (obs.compaction) {
32590
- await mirror.onCompaction(obs.compaction, bridge.turns);
32664
+ if (await mirror.onCompaction(obs.compaction, bridge.turns)) held();
32591
32665
  if (!writeUp && !hardStopped && !catchingUp) {
32592
32666
  let notepad;
32593
32667
  let unavailable = false;
@@ -32608,7 +32682,6 @@ ${stagedLine}` : prompt,
32608
32682
  note("harness_error", `a model call failed while the bot was away (${obs.providerError}); continuing`);
32609
32683
  else providerError = obs.providerError;
32610
32684
  }
32611
- if (obs.turnEnded) save();
32612
32685
  if (obs.settled && !catchingUp) {
32613
32686
  settled = true;
32614
32687
  break;
@@ -32622,8 +32695,8 @@ ${stagedLine}` : prompt,
32622
32695
  }
32623
32696
  if (bypass) throw bypass;
32624
32697
  if (!settled) {
32625
- const tail = await container.tail(paths.errLog, 2e3);
32626
- throw new Error(`pi exited before the run settled${tail.trim() ? `: ${redactAndCap(tail.trim(), 400)}` : ""}`);
32698
+ const tail2 = await container.tail(paths.errLog, 2e3);
32699
+ throw new Error(`pi exited before the run settled${tail2.trim() ? `: ${redactAndCap(tail2.trim(), 400)}` : ""}`);
32627
32700
  }
32628
32701
  if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
32629
32702
  const text = bridge.answer() ?? "";
@@ -33949,7 +34022,7 @@ var init_runLoop = __esm({
33949
34022
  "use strict";
33950
34023
  init_profile();
33951
34024
  init_runner();
33952
- init_types3();
34025
+ init_types2();
33953
34026
  init_workspace();
33954
34027
  init_select();
33955
34028
  init_botHostContainer();
@@ -36787,8 +36860,8 @@ var init_types5 = __esm({
36787
36860
  });
36788
36861
 
36789
36862
  // ../../src/core/runLedger/writeThrough.ts
36790
- import { randomUUID as randomUUID2 } from "node:crypto";
36791
- function mintGeneration(now = Date.now, random = randomUUID2) {
36863
+ import { randomUUID as randomUUID3 } from "node:crypto";
36864
+ function mintGeneration(now = Date.now, random = randomUUID3) {
36792
36865
  const stamp = new Date(now()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
36793
36866
  const gen = `${stamp}-${random().replace(/-/g, "").slice(0, 8)}`;
36794
36867
  if (!GEN_PATTERN.test(gen)) throw new Error(`minted generation ${gen} does not match GEN_PATTERN`);
@@ -37510,7 +37583,7 @@ var OpenAICompatProvider;
37510
37583
  var init_openaiCompat = __esm({
37511
37584
  "../../src/providers/openaiCompat.ts"() {
37512
37585
  "use strict";
37513
- init_types3();
37586
+ init_types2();
37514
37587
  init_secrets2();
37515
37588
  OpenAICompatProvider = class {
37516
37589
  name;
@@ -37618,6 +37691,241 @@ var init_registry3 = __esm({
37618
37691
  }
37619
37692
  });
37620
37693
 
37694
+ // ../../src/core/harness/piAi.ts
37695
+ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
37696
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
37697
+ function piApiFor(type) {
37698
+ return type === "anthropic" ? "anthropic-messages" : "openai-completions";
37699
+ }
37700
+ function piStreamOptions(api, req, key) {
37701
+ const shared = {
37702
+ ...key !== void 0 ? { apiKey: key } : { apiKey: "unused", headers: { Authorization: null } },
37703
+ maxTokens: req.maxTokens,
37704
+ ...req.signal ? { signal: req.signal } : {},
37705
+ cacheRetention: req.cacheTtl === "1h" ? "long" : "short"
37706
+ };
37707
+ if (!req.toolChoice) return shared;
37708
+ return api === "anthropic-messages" ? { ...shared, toolChoice: { type: "tool", name: req.toolChoice.name } } : { ...shared, toolChoice: { type: "function", function: { name: req.toolChoice.name } } };
37709
+ }
37710
+ function toPiContext(req, model, clock3) {
37711
+ const callNames = /* @__PURE__ */ new Map();
37712
+ const messages = [];
37713
+ for (const turn of req.messages) {
37714
+ if (turn.role === "assistant") {
37715
+ for (const part of turn.content) if (part.type === "tool_use") callNames.set(part.id, part.name);
37716
+ messages.push(assistantMessage(turn, model, clock3()));
37717
+ } else {
37718
+ messages.push(...userMessages(turn, callNames, clock3()));
37719
+ }
37720
+ }
37721
+ const tools = req.tools?.map((t) => ({
37722
+ name: t.name,
37723
+ description: t.description,
37724
+ parameters: t.inputSchema
37725
+ }));
37726
+ return {
37727
+ ...req.system !== void 0 ? { systemPrompt: req.system } : {},
37728
+ messages,
37729
+ ...tools && tools.length > 0 ? { tools } : {}
37730
+ };
37731
+ }
37732
+ function assistantMessage(turn, model, timestamp) {
37733
+ const content = [];
37734
+ for (const part of turn.content) {
37735
+ if (part.type === "text") content.push({ type: "text", text: part.text });
37736
+ else if (part.type === "tool_use")
37737
+ content.push({ type: "toolCall", id: part.id, name: part.name, arguments: argumentsOf(part.input) });
37738
+ else if (part.type === "thinking")
37739
+ content.push({ type: "thinking", thinking: part.thinking, thinkingSignature: part.signature });
37740
+ else if (part.type === "redacted_thinking")
37741
+ content.push({ type: "thinking", thinking: "", thinkingSignature: part.data, redacted: true });
37742
+ }
37743
+ return {
37744
+ role: "assistant",
37745
+ content,
37746
+ api: model.api,
37747
+ provider: model.provider,
37748
+ model: model.id,
37749
+ usage: { ...ZERO_USAGE, cost: { ...ZERO_USAGE.cost } },
37750
+ stopReason: content.some((p) => p.type === "toolCall") ? "toolUse" : "stop",
37751
+ timestamp
37752
+ };
37753
+ }
37754
+ function argumentsOf(input) {
37755
+ return typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
37756
+ }
37757
+ function userMessages(turn, callNames, timestamp) {
37758
+ const out = [];
37759
+ const rest = [];
37760
+ for (const part of turn.content) {
37761
+ if (part.type === "tool_result") {
37762
+ const result = {
37763
+ role: "toolResult",
37764
+ toolCallId: part.toolUseId,
37765
+ toolName: callNames.get(part.toolUseId) ?? part.toolUseId,
37766
+ content: toolResultParts(part.content),
37767
+ isError: part.isError === true,
37768
+ timestamp
37769
+ };
37770
+ out.push(result);
37771
+ } else {
37772
+ const visible = visiblePart(part);
37773
+ if (visible) rest.push(visible);
37774
+ }
37775
+ }
37776
+ if (rest.length > 0) out.push({ role: "user", content: rest, timestamp });
37777
+ return out;
37778
+ }
37779
+ function toolResultParts(content) {
37780
+ if (typeof content === "string") return [{ type: "text", text: content }];
37781
+ return content.map((part) => visiblePart(part)).filter((p) => p !== void 0);
37782
+ }
37783
+ function visiblePart(part) {
37784
+ switch (part.type) {
37785
+ case "text":
37786
+ return { type: "text", text: part.text };
37787
+ case "image":
37788
+ return { type: "image", data: part.data, mimeType: part.mediaType };
37789
+ case "document":
37790
+ return {
37791
+ type: "text",
37792
+ text: `
37793
+
37794
+ [attached file: ${part.name ?? "document"} (${part.mediaType}); not supported by this provider]
37795
+ `
37796
+ };
37797
+ default:
37798
+ return void 0;
37799
+ }
37800
+ }
37801
+ function fromPiMessage(message3, provider) {
37802
+ if (message3.stopReason === "error" || message3.stopReason === "aborted") {
37803
+ throw new Error(`Provider "${provider}" (${message3.model}): ${message3.errorMessage ?? "the model call failed"}`);
37804
+ }
37805
+ const content = [];
37806
+ for (const part of message3.content) {
37807
+ if (part.type === "text") content.push({ type: "text", text: part.text });
37808
+ else if (part.type === "toolCall")
37809
+ content.push({ type: "tool_use", id: part.id, name: part.name, input: part.arguments });
37810
+ else if (part.type === "thinking") {
37811
+ content.push(
37812
+ part.redacted ? { type: "redacted_thinking", data: part.thinkingSignature ?? "" } : { type: "thinking", thinking: part.thinking, signature: part.thinkingSignature ?? "" }
37813
+ );
37814
+ }
37815
+ }
37816
+ return { content, stopReason: STOP_REASONS[message3.stopReason] ?? "other", usage: usageOf(message3.usage) };
37817
+ }
37818
+ function usageOf(usage) {
37819
+ return {
37820
+ inputTokens: usage.input,
37821
+ outputTokens: usage.output,
37822
+ cacheReadTokens: usage.cacheRead,
37823
+ cacheWriteTokens: usage.cacheWrite
37824
+ };
37825
+ }
37826
+ var ANTHROPIC_BASE_URL, ZERO_RATES, ZERO_USAGE, CONTEXT_WINDOW, PiAiProviders, PiAiProvider, STOP_REASONS;
37827
+ var init_piAi = __esm({
37828
+ "../../src/core/harness/piAi.ts"() {
37829
+ "use strict";
37830
+ init_types2();
37831
+ init_secrets2();
37832
+ init_clock();
37833
+ ANTHROPIC_BASE_URL = "https://api.anthropic.com";
37834
+ ZERO_RATES = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
37835
+ ZERO_USAGE = { ...ZERO_RATES, totalTokens: 0, cost: { ...ZERO_RATES, total: 0 } };
37836
+ CONTEXT_WINDOW = 2e5;
37837
+ PiAiProviders = class {
37838
+ providers = /* @__PURE__ */ new Map();
37839
+ constructor(configs, opts = {}) {
37840
+ for (const [name, cfg] of Object.entries(configs)) this.providers.set(name, new PiAiProvider(name, cfg, opts));
37841
+ }
37842
+ get(name) {
37843
+ const provider = this.providers.get(name);
37844
+ if (!provider) {
37845
+ throw new Error(`Unknown provider "${name}". Configured providers: ${[...this.providers.keys()].join(", ")}`);
37846
+ }
37847
+ return provider;
37848
+ }
37849
+ names() {
37850
+ return [...this.providers.keys()];
37851
+ }
37852
+ };
37853
+ PiAiProvider = class {
37854
+ name;
37855
+ api;
37856
+ baseUrl;
37857
+ keyEnv;
37858
+ secrets;
37859
+ clock;
37860
+ scripted;
37861
+ fetchImpl;
37862
+ streams;
37863
+ constructor(name, cfg, opts = {}) {
37864
+ this.name = name;
37865
+ this.api = piApiFor(cfg.type);
37866
+ if (cfg.type === "openai-compatible" && !cfg.baseUrl) {
37867
+ throw new Error(`Provider "${name}": openai-compatible providers require baseUrl`);
37868
+ }
37869
+ this.baseUrl = (cfg.baseUrl ?? ANTHROPIC_BASE_URL).replace(/\/+$/, "");
37870
+ this.keyEnv = cfg.apiKeyEnv ?? (cfg.type === "anthropic" ? ANTHROPIC_API_KEY_ENV : void 0);
37871
+ this.secrets = opts.secrets ?? processSecrets;
37872
+ this.clock = opts.clock ?? systemClock;
37873
+ this.scripted = opts.apis?.[this.api];
37874
+ this.fetchImpl = opts.fetch;
37875
+ }
37876
+ /** The model pi is handed for one request: the block as its provider, the
37877
+ * request's output cap as its own, no thinking (neither caller asks for
37878
+ * any), text and images in, the zero rate card. */
37879
+ model(id, maxTokens) {
37880
+ return {
37881
+ id,
37882
+ name: id,
37883
+ api: this.api,
37884
+ provider: this.name,
37885
+ baseUrl: this.baseUrl,
37886
+ reasoning: false,
37887
+ input: ["text", "image"],
37888
+ cost: { ...ZERO_RATES },
37889
+ contextWindow: CONTEXT_WINDOW,
37890
+ maxTokens
37891
+ };
37892
+ }
37893
+ /** One completion: the key revealed into pi's request options and nowhere
37894
+ * else, the request in pi's shape, pi's adapter on the wire, its final
37895
+ * message back in the vocabulary. A missing key fails here, by variable
37896
+ * name, before any request; a provider error or an abort is thrown with
37897
+ * pi's message — `route()` and `reflect()` catch either as they catch a
37898
+ * native adapter's throw. */
37899
+ async complete(req) {
37900
+ let key;
37901
+ if (this.keyEnv !== void 0) {
37902
+ const secret2 = this.secrets.named(this.keyEnv);
37903
+ if (!secret2) throw new Error(`Provider "${this.name}": ${this.keyEnv} is not set`);
37904
+ key = secret2.reveal();
37905
+ }
37906
+ const model = this.model(req.model, req.maxTokens);
37907
+ const options = {
37908
+ ...piStreamOptions(this.api, req, key),
37909
+ ...this.fetchImpl ? { fetch: this.fetchImpl } : {}
37910
+ };
37911
+ const message3 = await this.api$().stream(model, toPiContext(req, model, this.clock), options).result();
37912
+ return fromPiMessage(message3, this.name);
37913
+ }
37914
+ /** pi's implementation of this provider's API, loaded on the first call and
37915
+ * kept; a scripted one stands in for it in tests. */
37916
+ api$() {
37917
+ this.streams ??= this.scripted ?? (this.api === "anthropic-messages" ? anthropicMessagesApi() : openAICompletionsApi());
37918
+ return this.streams;
37919
+ }
37920
+ };
37921
+ STOP_REASONS = {
37922
+ stop: "end_turn",
37923
+ toolUse: "tool_use",
37924
+ length: "max_tokens"
37925
+ };
37926
+ }
37927
+ });
37928
+
37621
37929
  // ../../src/mcp/secretStore.ts
37622
37930
  import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
37623
37931
  import { dirname as dirname10, resolve as resolve10 } from "node:path";
@@ -37806,7 +38114,7 @@ var init_mcp2 = __esm({
37806
38114
  init_sealed();
37807
38115
  init_secretStore();
37808
38116
  init_service();
37809
- init_types2();
38117
+ init_types3();
37810
38118
  init_registry2();
37811
38119
  init_client();
37812
38120
  init_fake();
@@ -39981,7 +40289,7 @@ var init_webShell = __esm({
39981
40289
  init_favicon();
39982
40290
  init_residentsModel();
39983
40291
  init_webSeed();
39984
- PAGE_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
40292
+ PAGE_CSP = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
39985
40293
  FORM_PAGE_CSP = PAGE_CSP.replace("form-action 'none'", "form-action 'self'");
39986
40294
  WEB_HTML_HEADERS = {
39987
40295
  "content-type": "text/html; charset=utf-8",
@@ -42714,6 +43022,7 @@ var init_modelProxy = __esm({
42714
43022
  "use strict";
42715
43023
  init_anthropic();
42716
43024
  init_openaiCompat();
43025
+ init_types2();
42717
43026
  init_http();
42718
43027
  ANTHROPIC_MESSAGES_PATH = "/v1/messages";
42719
43028
  OPENAI_CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
@@ -43076,7 +43385,7 @@ var init_relay = __esm({
43076
43385
  "../../src/core/harness/pi/relay.ts"() {
43077
43386
  "use strict";
43078
43387
  init_tracingExecutor();
43079
- init_types3();
43388
+ init_types2();
43080
43389
  init_awaitChildren();
43081
43390
  init_runEvents();
43082
43391
  init_toolRules();
@@ -44675,6 +44984,7 @@ async function runBot() {
44675
44984
  });
44676
44985
  console.log(`[capabilities] ${JSON.stringify(capabilities)}`);
44677
44986
  const providers = new ProviderRegistry(config.config.providers);
44987
+ const completions = new PiAiProviders(config.config.providers);
44678
44988
  const skills = new BundledSkillStore(DEFAULT_SKILLS_DIR);
44679
44989
  let slackEmailLookup;
44680
44990
  const mcpWiring = buildMcp(config, processSecrets, {
@@ -44782,6 +45092,7 @@ async function runBot() {
44782
45092
  const deps = {
44783
45093
  config,
44784
45094
  providers,
45095
+ completions,
44785
45096
  spanLog,
44786
45097
  runBearers,
44787
45098
  harness: {
@@ -45252,6 +45563,7 @@ var init_index = __esm({
45252
45563
  init_config2();
45253
45564
  init_capabilities();
45254
45565
  init_registry3();
45566
+ init_piAi();
45255
45567
  init_slack();
45256
45568
  init_slackChannelDirectory();
45257
45569
  init_http();
@@ -45370,6 +45682,7 @@ init_memory();
45370
45682
  init_residentAdmin();
45371
45683
  init_residentFleet();
45372
45684
  init_registry3();
45685
+ init_piAi();
45373
45686
  init_skills2();
45374
45687
  init_mcp2();
45375
45688
  init_source();
@@ -45697,6 +46010,7 @@ async function main() {
45697
46010
  globalThis.console = new Console({ stdout: process.stderr, stderr: process.stderr });
45698
46011
  const { config, runStore } = await bot();
45699
46012
  const providers = new ProviderRegistry(config.config.providers);
46013
+ const completions = new PiAiProviders(config.config.providers);
45700
46014
  const skills = new BundledSkillStore(DEFAULT_SKILLS_DIR);
45701
46015
  const capabilities = capabilitiesFrom(config.config, publicEnv(), processSecrets, {
45702
46016
  meatBinary: meatOnPath(publicEnv())
@@ -45713,6 +46027,7 @@ async function main() {
45713
46027
  const deps = {
45714
46028
  config,
45715
46029
  providers,
46030
+ completions,
45716
46031
  capabilities,
45717
46032
  residentFleet,
45718
46033
  ...artifacts ? { artifacts } : {},