@coreplane/switchboard 1.217.0 → 1.218.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 (25) hide show
  1. package/dist/assets/config/config.example.yaml +6 -0
  2. package/dist/assets/deploy/cloudflare/worker.ts +4 -0
  3. package/dist/assets/deploy/cloudflare-memory/worker.ts +96 -6
  4. package/dist/assets/deploy/secrets.manifest.json +12 -0
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +18 -0
  9. package/dist/assets/src/core/runEvents.ts +6 -0
  10. package/dist/assets/src/core/runFriction.ts +4 -2
  11. package/dist/assets/src/core/runLedger/sessionLog.ts +22 -0
  12. package/dist/assets/src/core/runLedger/types.ts +17 -0
  13. package/dist/assets/src/mcp/registry.ts +33 -5
  14. package/dist/assets/web/dist/.vite/manifest.json +18 -18
  15. package/dist/assets/web/dist/assets/{ResidentDetailPage-CWyEd5fq.js → ResidentDetailPage-DvCt96Dk.js} +1 -1
  16. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DqFqRTWb.js → ResidentsIndexPage-BZaYja1-.js} +1 -1
  17. package/dist/assets/web/dist/assets/{RunRoutePage-CghwVr_X.js → RunRoutePage-DI4vzCUu.js} +4 -4
  18. package/dist/assets/web/dist/assets/{RunsIndexPage-BcYehvW5.js → RunsIndexPage-Ch9uP4p4.js} +1 -1
  19. package/dist/assets/web/dist/assets/{ScheduledPage-mKa20Qdd.js → ScheduledPage-DAkAHMCH.js} +1 -1
  20. package/dist/assets/web/dist/assets/{StatusDot-QMofJG-E.js → StatusDot-CfBonGcV.js} +1 -1
  21. package/dist/assets/web/dist/assets/{Tooltip-D-zVPexc.js → Tooltip-CBD36E3W.js} +1 -1
  22. package/dist/assets/web/dist/assets/{dist-D43zKxNV.js → dist-DQ1U73LF.js} +1 -1
  23. package/dist/assets/web/dist/assets/{main-ClTMtz8n.js → main-CdLnG-yl.js} +2 -2
  24. package/dist/cli.js +357 -92
  25. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -206,6 +206,9 @@ async function ensureWorkArea(at, version2, workers, deps) {
206
206
  }
207
207
  return { ok: true, copied: plan3.copy, installed: plan3.install };
208
208
  }
209
+ function checkoutInstallHolds(root, dir, exists) {
210
+ return exists(join3(root, dir, "node_modules", "wrangler")) || exists(join3(root, "node_modules", "wrangler"));
211
+ }
209
212
  function imageBuiltOutsideDir(renderedConfig) {
210
213
  const code = renderedConfig.split("\n").filter((l) => !l.trimStart().startsWith("//")).join("\n");
211
214
  for (const m of code.matchAll(/"image"\s*:\s*"([^"]+)"/g)) {
@@ -1796,7 +1799,7 @@ function getAgent(name) {
1796
1799
  }
1797
1800
  return a;
1798
1801
  }
1799
- var MACHINE_CLASSES, IDENTITIES, HARNESSES, RUNAWAY_TURNS_PER_MINUTE, PR_DESCRIPTION_TEMPLATE, NEVER_MERGE, CONTRACT_HEADINGS_LIST, UNIT_CONTRACT, UNIT_HANDOFF, IMAGE_TOOLCHAIN, SANDBOX_TOOLCHAIN, RESIDENT_TOOLCHAIN, SHOW_FILES, CODING_SYSTEM, CODING_SYSTEM_RESIDENT, REVIEW_VERDICT_INSTRUCTION, REVIEW_SPEC_CHECK, REVIEW_UNIT_CONTRACT, REVIEW_WHOLE_CHANGE, REVIEW_SYSTEM, REVIEW_SYSTEM_RESIDENT, RESEARCH_SYSTEM, GENERAL_SYSTEM, EXPLORE_SYSTEM, COMPOUND_PRESET, WORK_PRESETS, AGENTS;
1802
+ var MACHINE_CLASSES, IDENTITIES, HARNESSES, RUNAWAY_TURNS_PER_MINUTE, PR_DESCRIPTION_TEMPLATE, NEVER_MERGE, CONTRACT_HEADINGS_LIST, UNIT_CONTRACT, UNIT_HANDOFF, IMAGE_TOOLCHAIN, SANDBOX_TOOLCHAIN, RESIDENT_TOOLCHAIN, SHOW_FILES, NOTEPAD, CODING_SYSTEM, CODING_SYSTEM_RESIDENT, REVIEW_VERDICT_INSTRUCTION, REVIEW_SPEC_CHECK, REVIEW_UNIT_CONTRACT, REVIEW_WHOLE_CHANGE, REVIEW_SYSTEM, REVIEW_SYSTEM_RESIDENT, RESEARCH_SYSTEM, GENERAL_SYSTEM, EXPLORE_SYSTEM, COMPOUND_PRESET, WORK_PRESETS, AGENTS;
1800
1803
  var init_registry = __esm({
1801
1804
  "../../src/agents/registry.ts"() {
1802
1805
  "use strict";
@@ -1827,6 +1830,8 @@ SCREENSHOTS GO TO BOTH PLACES, ALL OF THEM: when the request asks for screenshot
1827
1830
  Whole files: up to 1 GiB where the artifact store is configured (a recording, a large PDF), 10 MiB otherwise \u2014 the tool's result says which applies; over the limit, link to the file.
1828
1831
  Files the person dropped on the thread that were too large to show you inline are already in ./attachments/ in your workspace when the turn's text names them (a video for ffmpeg, a large PDF, a zip); read them from there \u2014 never ask for a re-upload.
1829
1832
  Text stays in your message; do not attach what you can say.`;
1833
+ NOTEPAD = `YOUR NOTES AND YOUR REACH BACK. This thread's conversation outlives your context window and this run: every turn \u2014 yours, the person's, every tool call and its output, from this run and the runs before it in this thread \u2014 is kept in a log you can search with the \`recall\` tool (words \u2192 the matching turns with their numbers; a turn number \u2192 that turn whole). When something you need is no longer in front of you, recall it instead of redoing the work or guessing.
1834
+ Keep notes with the \`notes\` tool: one short document, replaced whole each time, at most 8 KiB \u2014 decisions and their reasons, the names of things you found (files, tests, commits, the head your tests were green at), what is not yet proven. They are the one thing sure to survive a compaction and to reach the next run in this thread: they ride your system prompt at its start and come back to you right after a compaction. Write them when you decide something worth keeping, not only at the end.`;
1830
1835
  CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
1831
1836
 
1832
1837
  You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
@@ -1856,6 +1861,8 @@ ${PR_DESCRIPTION_TEMPLATE}
1856
1861
 
1857
1862
  ${SHOW_FILES}
1858
1863
 
1864
+ ${NOTEPAD}
1865
+
1859
1866
  Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (\u25CB pending items), then update it whenever an item starts (\u2731) or finishes (\u2713). Items are short outcomes ("Clone repo and read the diff", "Run the test suite"), never commands. Mark an item \u2713 only after it has actually happened \u2014 never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
1860
1867
 
1861
1868
  If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
@@ -1890,6 +1897,8 @@ ${PR_DESCRIPTION_TEMPLATE}
1890
1897
 
1891
1898
  ${SHOW_FILES}
1892
1899
 
1900
+ ${NOTEPAD}
1901
+
1893
1902
  Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (\u25CB pending items), then update it whenever an item starts (\u2731) or finishes (\u2713). Items are short outcomes ("Implement the fix", "Run the test suite"), never commands. Mark an item \u2713 only after it has actually happened \u2014 never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
1894
1903
 
1895
1904
  Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
@@ -1922,6 +1931,8 @@ REVIEW THE PR'S OWN HEAD, NOTHING ELSE: the commit you read must be the PR's hea
1922
1931
 
1923
1932
  ${REVIEW_VERDICT_INSTRUCTION}
1924
1933
 
1934
+ ${NOTEPAD}
1935
+
1925
1936
  Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending), update as items start (\u2731) and finish (\u2713 \u2014 only after they actually happened; never pre-mark reporting steps). Items are short outcomes, never commands.
1926
1937
 
1927
1938
  Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
@@ -1948,6 +1959,8 @@ REVIEW THE PR'S OWN HEAD, NOTHING ELSE: the commit you read must be the PR's hea
1948
1959
 
1949
1960
  ${REVIEW_VERDICT_INSTRUCTION}
1950
1961
 
1962
+ ${NOTEPAD}
1963
+
1951
1964
  Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending), update as items start (\u2731) and finish (\u2713 \u2014 only after they actually happened; never pre-mark reporting steps). Items are short outcomes, never commands.
1952
1965
 
1953
1966
  Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
@@ -1981,6 +1994,8 @@ READ-ONLY: NEVER open a pull request, and never commit or push \u2014 no branch,
1981
1994
 
1982
1995
  Maintain the user-facing status card with the update_status tool: post your plan as a checklist (\u25CB pending) once you have it, and update items as they start (\u2731) and finish (\u2713 \u2014 only after they actually happened). Items are short outcomes ("Clone and install", "Time the full suite"), never commands.
1983
1996
 
1997
+ ${NOTEPAD}
1998
+
1984
1999
  Report outcomes faithfully: a check you could not run is "could not check", never a guess. Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks \u2014 render the claim table as aligned rows inside a code block). Your final message is posted to Slack: lead with the overall verdict in one line, then the claim table, then what a follow-up should do.`;
1985
2000
  COMPOUND_PRESET = "conductor";
1986
2001
  WORK_PRESETS = {
@@ -2603,6 +2618,11 @@ function splitCredentialKey(key) {
2603
2618
  const name = key.slice(slash + 1);
2604
2619
  return parseMcpScopeKey(scopeKey) && MCP_SERVER_NAME_RE.test(name) ? { scopeKey, name } : void 0;
2605
2620
  }
2621
+ function isHeadersEnv(v) {
2622
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
2623
+ const entries = Object.entries(v);
2624
+ return entries.length > 0 && entries.length <= MCP_HEADERS_MAX && entries.every(([name, envVar]) => isStr(name, 64) && isStr(envVar, 128));
2625
+ }
2606
2626
  function isOutcome(v) {
2607
2627
  if (!v || typeof v !== "object" || Array.isArray(v)) return false;
2608
2628
  const o = v;
@@ -2611,7 +2631,7 @@ function isOutcome(v) {
2611
2631
  function isMcpServerEntry(v) {
2612
2632
  if (!v || typeof v !== "object" || Array.isArray(v)) return false;
2613
2633
  const e = v;
2614
- return isStr(e.url, MCP_URL_MAX) && (e.agents === void 0 || Array.isArray(e.agents) && e.agents.length > 0 && e.agents.length <= MCP_AGENTS_MAX && e.agents.every((a) => isStr(a, 32))) && (e.auth === "none" || e.auth === "bearer" || e.auth === "oauth") && (e.tokenEnv === void 0 || isStr(e.tokenEnv, 128)) && (e.addedBy === void 0 || isStr(e.addedBy, 260)) && (e.addedAt === void 0 || isNum(e.addedAt));
2634
+ return isStr(e.url, MCP_URL_MAX) && (e.agents === void 0 || Array.isArray(e.agents) && e.agents.length > 0 && e.agents.length <= MCP_AGENTS_MAX && e.agents.every((a) => isStr(a, 32))) && (e.auth === "none" || e.auth === "bearer" || e.auth === "oauth") && (e.tokenEnv === void 0 || isStr(e.tokenEnv, 128)) && (e.headersEnv === void 0 || isHeadersEnv(e.headersEnv)) && (e.addedBy === void 0 || isStr(e.addedBy, 260)) && (e.addedAt === void 0 || isNum(e.addedAt));
2615
2635
  }
2616
2636
  function isSealedCredential(v) {
2617
2637
  if (!v || typeof v !== "object") return false;
@@ -2626,7 +2646,7 @@ function isMcpTicket(v) {
2626
2646
  }
2627
2647
  function serverView(scopeKey, name, entry, opts) {
2628
2648
  const parsed = parseMcpScopeKey(scopeKey);
2629
- const state = entry.auth === "none" ? "connected" : entry.tokenEnv ? "static" : opts.hasCredential ? "connected" : "awaiting_credential";
2649
+ const state = entry.auth === "none" ? entry.headersEnv ? "static" : "connected" : entry.tokenEnv ? "static" : opts.hasCredential ? "connected" : "awaiting_credential";
2630
2650
  return {
2631
2651
  name,
2632
2652
  scope: parsed?.kind ?? "org",
@@ -2648,7 +2668,7 @@ function safeUrl(raw) {
2648
2668
  return "(invalid url)";
2649
2669
  }
2650
2670
  }
2651
- var MCP_TICKET_STATES, MCP_TICKET_TTL_MS, MCP_SERVER_NAME_MAX, MCP_SERVER_NAME_RE, MCP_URL_MAX, MCP_AGENTS_MAX, MCP_SELF_SERVE_AGENTS, MCP_TOKEN_MAX_CHARS, MCP_SERVERS_PER_SCOPE_MAX, isStr, isNum;
2671
+ var MCP_TICKET_STATES, MCP_TICKET_TTL_MS, MCP_SERVER_NAME_MAX, MCP_SERVER_NAME_RE, MCP_URL_MAX, MCP_AGENTS_MAX, MCP_SELF_SERVE_AGENTS, MCP_TOKEN_MAX_CHARS, MCP_SERVERS_PER_SCOPE_MAX, isStr, isNum, MCP_HEADERS_MAX, MCP_HEADER_NAME_RE;
2652
2672
  var init_registry2 = __esm({
2653
2673
  "../../src/mcp/registry.ts"() {
2654
2674
  "use strict";
@@ -2669,6 +2689,8 @@ var init_registry2 = __esm({
2669
2689
  MCP_SERVERS_PER_SCOPE_MAX = 32;
2670
2690
  isStr = (v, max = 4096) => typeof v === "string" && v.length > 0 && v.length <= max;
2671
2691
  isNum = (v) => typeof v === "number" && Number.isFinite(v);
2692
+ MCP_HEADERS_MAX = 8;
2693
+ MCP_HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,64}$/;
2672
2694
  }
2673
2695
  });
2674
2696
 
@@ -2753,7 +2775,9 @@ function validateMcpServers(layer, source2) {
2753
2775
  `${source2}: ${path}.${name}: server names are slugs (lowercase letters, digits, dashes; \u2264 ${MCP_SERVER_NAME_MAX} chars)`
2754
2776
  );
2755
2777
  if (!isMcpServerEntry(raw))
2756
- throw new Error(`${source2}: ${path}.${name} must be { url, auth: none|bearer|oauth, agents?, tokenEnv? }`);
2778
+ throw new Error(
2779
+ `${source2}: ${path}.${name} must be { url, auth: none|bearer|oauth, agents?, tokenEnv?, headersEnv? }`
2780
+ );
2757
2781
  try {
2758
2782
  assertUrlAllowed(raw.url);
2759
2783
  } catch (err2) {
@@ -2774,6 +2798,14 @@ function validateMcpServers(layer, source2) {
2774
2798
  }
2775
2799
  if (raw.tokenEnv !== void 0 && raw.auth !== "bearer")
2776
2800
  throw new Error(`${source2}: ${path}.${name}.tokenEnv only applies to auth: bearer`);
2801
+ for (const header4 of Object.keys(raw.headersEnv ?? {})) {
2802
+ if (!MCP_HEADER_NAME_RE.test(header4))
2803
+ throw new Error(`${source2}: ${path}.${name}.headersEnv: "${header4}" is not an HTTP header name`);
2804
+ if (header4.toLowerCase() === "authorization")
2805
+ throw new Error(
2806
+ `${source2}: ${path}.${name}.headersEnv: the Authorization header is \`auth\`'s \u2014 use auth: bearer with tokenEnv`
2807
+ );
2808
+ }
2777
2809
  }
2778
2810
  };
2779
2811
  check("defaults.mcpServers", "org", layer.defaults?.mcpServers);
@@ -4989,7 +5021,7 @@ function planDeploy(opts, host, loaded, images) {
4989
5021
  function formatPlan(plan3) {
4990
5022
  const missing = plan3.checks.nodeModulesMissing;
4991
5023
  const checkout = plan3.root.mode === "checkout";
4992
- const nodeModules = missing.length === 0 ? "node_modules present in every dir" : checkout ? `node_modules missing in ${missing.join(", ")} \u2014 the runner will \`npm ci\` there first` : `${missing.join(", ")} not installed under ${WORK_AREA_DIR}/ \u2014 the runner will \`npm ci\` them there first`;
5024
+ const nodeModules = missing.length === 0 ? "node_modules present in every dir" : checkout ? `install missing for ${missing.join(", ")} \u2014 the runner will \`npm ci\` at the checkout's root first` : `${missing.join(", ")} not installed under ${WORK_AREA_DIR}/ \u2014 the runner will \`npm ci\` them there first`;
4993
5025
  const tree = checkout ? `${plan3.checks.cleanTree ? "clean tree" : "tree unchecked"}; ${plan3.checks.atOriginMain ? "HEAD == origin/main" : "any branch (--allow-branch)"}` : `Worker sources are the package's (version ${plan3.root.version ?? "?"}), materialised under ${WORK_AREA_DIR}/ \u2014 no git`;
4994
5026
  const lines = [
4995
5027
  ...plan3.affected ? [formatAffectedText(plan3.affected)] : [],
@@ -6241,7 +6273,7 @@ var init_sandboxLiveGate = __esm({
6241
6273
  // ../../src/deploy/run.ts
6242
6274
  import { spawn as spawn3 } from "node:child_process";
6243
6275
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
6244
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join5 } from "node:path";
6276
+ import { dirname as dirname4, isAbsolute as isAbsolute3 } from "node:path";
6245
6277
  function run(cmd, args, opts) {
6246
6278
  const env = { ...process.env, ...opts.set ?? {} };
6247
6279
  for (const k of opts.unset ?? []) delete env[k];
@@ -6348,7 +6380,7 @@ function packageModeProblems(plan3) {
6348
6380
  return problems;
6349
6381
  }
6350
6382
  function hasNodeModules(dir) {
6351
- if (OPERATOR_ROOT.mode === "checkout") return existsSync5(join5(OPERATOR_ROOT.root, dir, "node_modules"));
6383
+ if (OPERATOR_ROOT.mode === "checkout") return checkoutInstallHolds(OPERATOR_ROOT.root, dir, existsSync5);
6352
6384
  const state = readWorkAreaState(OPERATOR_ROOT.workArea);
6353
6385
  return state.kind === "stamped" && state.stamp.installed.includes(dir);
6354
6386
  }
@@ -6514,10 +6546,10 @@ async function pushConfigOnHost(opts) {
6514
6546
  return pushConfigDocument(read, { stateWorkerUrl: opts.stateWorkerUrl, key: opts.key, env: process.env });
6515
6547
  }
6516
6548
  async function ensureNodeModules(step, io) {
6517
- const dir = workerDir(step.dir);
6518
6549
  if (hasNodeModules(step.dir)) return true;
6519
- io.log(`[deploy:all] ${step.name}: node_modules missing \u2014 npm ci`);
6520
- const r = await run("npm", ["ci", "--silent"], { cwd: dir });
6550
+ const cwd = OPERATOR_ROOT.mode === "checkout" ? OPERATOR_ROOT.root : workerDir(step.dir);
6551
+ io.log(`[deploy:all] ${step.name}: install missing \u2014 npm ci in ${cwd}`);
6552
+ const r = await run("npm", ["ci", "--silent"], { cwd });
6521
6553
  if (r.code !== 0) io.warn(r.output);
6522
6554
  return r.code === 0;
6523
6555
  }
@@ -7824,9 +7856,9 @@ var init_secrets = __esm({
7824
7856
  import { spawn as spawn4 } from "node:child_process";
7825
7857
  import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
7826
7858
  import { homedir as homedir2 } from "node:os";
7827
- import { isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "node:path";
7859
+ import { isAbsolute as isAbsolute4, join as join5, resolve as resolve4 } from "node:path";
7828
7860
  function expandDir(path, root = OPERATOR_ROOT.root) {
7829
- if (path === "~" || path.startsWith("~/")) return join6(homedir2(), path.slice(1));
7861
+ if (path === "~" || path.startsWith("~/")) return join5(homedir2(), path.slice(1));
7830
7862
  if (isAbsolute4(path)) return path;
7831
7863
  return resolve4(root, path);
7832
7864
  }
@@ -7870,7 +7902,7 @@ async function opFieldLabels(source2) {
7870
7902
  }
7871
7903
  function wranglerBin(dir, workspaceRoot, exists = existsSync6) {
7872
7904
  for (const base of [dir, workspaceRoot]) {
7873
- const bin = join6(base, "node_modules", ".bin", "wrangler");
7905
+ const bin = join5(base, "node_modules", ".bin", "wrangler");
7874
7906
  if (exists(bin)) return bin;
7875
7907
  }
7876
7908
  return "wrangler";
@@ -7891,7 +7923,7 @@ var init_secretsHost = __esm({
7891
7923
  if (source2.kind === "dir") {
7892
7924
  const dir = expandDir(source2.path);
7893
7925
  if (!existsSync6(dir)) return { ok: false, problem: `secretsSource ${source2.path}: no such directory (${dir})` };
7894
- return { ok: true, present: new Set(names.filter((n2) => existsSync6(join6(dir, n2)))) };
7926
+ return { ok: true, present: new Set(names.filter((n2) => existsSync6(join5(dir, n2)))) };
7895
7927
  }
7896
7928
  const labels = await opFieldLabels(source2);
7897
7929
  if (!labels.ok) return labels;
@@ -7900,7 +7932,7 @@ var init_secretsHost = __esm({
7900
7932
  put: async (source2, dir, name) => {
7901
7933
  let value;
7902
7934
  if (source2.kind === "dir") {
7903
- value = readFileSync7(join6(expandDir(source2.path), name), "utf8");
7935
+ value = readFileSync7(join5(expandDir(source2.path), name), "utf8");
7904
7936
  } else {
7905
7937
  const r2 = await spawnCollect("op", ["read", `op://${source2.vault}/${source2.item}/${name}`]);
7906
7938
  if (r2.code !== 0) return { code: r2.code, output: `op read exited ${r2.code} \u2014 ${r2.stderr.trim()}` };
@@ -8114,11 +8146,11 @@ var init_readingDiff = __esm({
8114
8146
  // ../../src/core/meatProcess.ts
8115
8147
  import { spawn as spawn5 } from "node:child_process";
8116
8148
  import { accessSync, constants } from "node:fs";
8117
- import { delimiter, join as join7 } from "node:path";
8149
+ import { delimiter, join as join6 } from "node:path";
8118
8150
  function meatOnPath(env, binary = MEAT_BINARY) {
8119
8151
  return (env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").some((dir) => {
8120
8152
  try {
8121
- accessSync(join7(dir, binary), constants.X_OK);
8153
+ accessSync(join6(dir, binary), constants.X_OK);
8122
8154
  return true;
8123
8155
  } catch {
8124
8156
  return false;
@@ -8198,10 +8230,10 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
8198
8230
  import { randomBytes } from "node:crypto";
8199
8231
  import { createInterface } from "node:readline/promises";
8200
8232
  import { Writable } from "node:stream";
8201
- import { dirname as dirname5, join as join8, resolve as resolve5 } from "node:path";
8233
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "node:path";
8202
8234
  async function readTemplates(root = PACKAGE_ROOT) {
8203
8235
  const read = async (key) => {
8204
- const path = join8(root, TEMPLATE_PATHS[key]);
8236
+ const path = join7(root, TEMPLATE_PATHS[key]);
8205
8237
  if (!existsSync7(path)) throw new Error(`${TEMPLATE_PATHS[key]}: no such file under ${root}`);
8206
8238
  return readFile(path, "utf8");
8207
8239
  };
@@ -8215,7 +8247,7 @@ function isCheckoutRoot(cwd = process.cwd(), root = PACKAGE_ROOT) {
8215
8247
  }
8216
8248
  }
8217
8249
  async function writePlannedFile(file, cwd = process.cwd()) {
8218
- const path = join8(cwd, file.path);
8250
+ const path = join7(cwd, file.path);
8219
8251
  await mkdir(dirname5(path), { recursive: true });
8220
8252
  const temp = `${path}.${randomBytes(6).toString("hex")}.tmp`;
8221
8253
  try {
@@ -8250,7 +8282,7 @@ function ttyPrompter(io = { stdin: process.stdin, stdout: process.stdout }) {
8250
8282
  };
8251
8283
  }
8252
8284
  function projectFact(name, root) {
8253
- const facts2 = JSON.parse(readFileSync8(join8(root, "project.json"), "utf8"));
8285
+ const facts2 = JSON.parse(readFileSync8(join7(root, "project.json"), "utf8"));
8254
8286
  const value = facts2[name];
8255
8287
  if (typeof value !== "string") throw new Error(`project.json: no \`${name}\` fact`);
8256
8288
  return value;
@@ -8266,7 +8298,7 @@ function hostSetupIO(cwd = OPERATOR_ROOT.root, startedIn = process.cwd()) {
8266
8298
  templates: () => readTemplates(),
8267
8299
  // Where the files went when that is not where the operator stands — `init` prints it.
8268
8300
  root: () => resolve5(cwd) === resolve5(startedIn) ? void 0 : cwd,
8269
- exists: async (path) => existsSync7(join8(cwd, path)),
8301
+ exists: async (path) => existsSync7(join7(cwd, path)),
8270
8302
  write: (file) => writePlannedFile(file, cwd),
8271
8303
  // A file the operator NAMED (`--github-private-key-file app.pem`) is where the operator stands,
8272
8304
  // not in the installation: `init` run from ~/Downloads reads ~/Downloads/app.pem.
@@ -8322,9 +8354,9 @@ var init_binaryRead = __esm({
8322
8354
 
8323
8355
  // ../../src/secrets.ts
8324
8356
  import { readFileSync as readFileSync9 } from "node:fs";
8325
- import { join as join9 } from "node:path";
8357
+ import { join as join8 } from "node:path";
8326
8358
  function manifestNames() {
8327
- const raw = JSON.parse(readFileSync9(join9(PACKAGE_ROOT, MANIFEST_PATH), "utf8"));
8359
+ const raw = JSON.parse(readFileSync9(join8(PACKAGE_ROOT, MANIFEST_PATH), "utf8"));
8328
8360
  const parsed = parseManifest2(raw);
8329
8361
  if (!parsed.ok) throw new Error(`${MANIFEST_PATH} is invalid \u2014 ${parsed.problems.join("; ")}`);
8330
8362
  return parsed.manifest.secrets.map((s) => s.name);
@@ -12482,7 +12514,7 @@ ${quote(current ?? "")}`;
12482
12514
  });
12483
12515
 
12484
12516
  // ../../src/core/commands/contract.ts
12485
- import { basename, dirname as dirname8, join as join10 } from "node:path";
12517
+ import { basename, dirname as dirname8, join as join9 } from "node:path";
12486
12518
  import { z as z7 } from "zod";
12487
12519
  function repoRootOfPlan(planPath) {
12488
12520
  const plans = dirname8(planPath);
@@ -12508,7 +12540,7 @@ var init_contract2 = __esm({
12508
12540
  init_commandRegistry();
12509
12541
  init_contract();
12510
12542
  defineCommand4 = commandDefiner();
12511
- SPECS_DIR = join10("docs", "reference", "specs");
12543
+ SPECS_DIR = join9("docs", "reference", "specs");
12512
12544
  RULES_FILES = ["AGENTS.md", "CLAUDE.md"];
12513
12545
  contractRender = defineCommand4({
12514
12546
  id: "contract.render",
@@ -12532,10 +12564,10 @@ var init_contract2 = __esm({
12532
12564
  const unit = parsePlanUnit(planMarkdown, options.unit);
12533
12565
  const specs = /* @__PURE__ */ new Map();
12534
12566
  for (const spec of new Set(unit ? specItemRefs(unit.section).map((r) => r.spec) : []))
12535
- specs.set(spec, await deps.contract.readFile(join10(root, SPECS_DIR, spec)));
12567
+ specs.set(spec, await deps.contract.readFile(join9(root, SPECS_DIR, spec)));
12536
12568
  let agentRules;
12537
12569
  for (const file of RULES_FILES) {
12538
- const text = await deps.contract.readFile(join10(root, file));
12570
+ const text = await deps.contract.readFile(join9(root, file));
12539
12571
  if (text !== void 0) {
12540
12572
  agentRules = { file, text };
12541
12573
  break;
@@ -13460,7 +13492,7 @@ function isSetupInstallCommand(summary) {
13460
13492
  return INSTALL_SEGMENT.test(cmd.replace(QUOTED, '""'));
13461
13493
  }
13462
13494
  function isNarrative(ev) {
13463
- return ev.type === "input" || ev.type === "context" || ev.type === "assistant" || ev.type === "answer" || ev.type === "run_meta";
13495
+ return ev.type === "input" || ev.type === "context" || ev.type === "assistant" || ev.type === "answer" || ev.type === "notes" || ev.type === "run_meta";
13464
13496
  }
13465
13497
  function spanSet(events) {
13466
13498
  const normalized = normalizeSpans(events);
@@ -15383,6 +15415,7 @@ function isRunEvent(v) {
15383
15415
  case "input":
15384
15416
  case "assistant":
15385
15417
  case "context":
15418
+ case "notes":
15386
15419
  return typeof o.text === "string";
15387
15420
  // the narrative events carry text, not a summary
15388
15421
  case "run_meta":
@@ -17231,7 +17264,11 @@ var init_service = __esm({
17231
17264
  /** A fresh connect link for a runtime bearer/oauth server (first connect, or a re-key). */
17232
17265
  async connect(actor, target, name) {
17233
17266
  const { scopeKey, entry, source: source2 } = this.owned(target, name);
17234
- if (entry.auth === "none") throw new McpServiceError("invalid_input", `"${name}" needs no credential (auth: none)`);
17267
+ if (entry.auth === "none")
17268
+ throw new McpServiceError(
17269
+ "invalid_input",
17270
+ entry.headersEnv ? `"${name}" is pinned in config.yaml with headersEnv \u2014 its headers are environment variables on the bot, not a stored credential` : `"${name}" needs no credential (auth: none)`
17271
+ );
17235
17272
  if (entry.tokenEnv)
17236
17273
  throw new McpServiceError(
17237
17274
  "invalid_input",
@@ -17708,6 +17745,15 @@ var init_service = __esm({
17708
17745
  url: r.entry.url,
17709
17746
  agents: r.entry.agents ?? [...MCP_SELF_SERVE_AGENTS]
17710
17747
  };
17748
+ if (r.entry.headersEnv) {
17749
+ const headers = {};
17750
+ for (const [header4, envVar] of Object.entries(r.entry.headersEnv)) {
17751
+ const value = this.opts.bearers.named(envVar);
17752
+ if (!value) return { name: r.name, unavailable: `${envVar} is not set on the bot` };
17753
+ headers[header4] = value.reveal();
17754
+ }
17755
+ base.headers = headers;
17756
+ }
17711
17757
  if (r.entry.auth === "none") return { spec: base };
17712
17758
  if (r.entry.tokenEnv) {
17713
17759
  const token2 = this.opts.bearers.named(r.entry.tokenEnv);
@@ -17827,7 +17873,7 @@ async function settleConnect(deps, output) {
17827
17873
  }
17828
17874
  }
17829
17875
  function renderServerLine(r) {
17830
- const state = r.state === "connected" ? "\u2705 connected" : r.state === "static" ? "\u2705 static (tokenEnv)" : "\u23F3 awaiting credential";
17876
+ const state = r.state === "connected" ? "\u2705 connected" : r.state === "static" ? "\u2705 static (bot env)" : "\u23F3 awaiting credential";
17831
17877
  const agents = Array.isArray(r.agents) ? r.agents.join(", ") : "";
17832
17878
  const pinned = r.source === "config" ? " \xB7 pinned in config.yaml" : "";
17833
17879
  return `\u2022 \`${String(r.name)}\` (${String(r.scope)}) ${state} \u2014 ${String(r.url)} \xB7 agents: ${agents} \xB7 auth: ${String(r.auth)}${pinned}`;
@@ -21929,6 +21975,8 @@ function activityLine(e) {
21929
21975
  return "request received";
21930
21976
  case "context":
21931
21977
  return "context recorded";
21978
+ case "notes":
21979
+ return "\u{1F4DD} notes saved";
21932
21980
  case "answer":
21933
21981
  return "answer ready";
21934
21982
  case "run_meta":
@@ -26916,6 +26964,134 @@ var init_runs2 = __esm({
26916
26964
  }
26917
26965
  });
26918
26966
 
26967
+ // ../../src/core/runLedger/sessionLog.ts
26968
+ function sessionKey(threadKey, agent) {
26969
+ return `${threadKey}:${agent ?? "-"}`;
26970
+ }
26971
+ function requestIndex(seed) {
26972
+ for (let i = seed.length - 1; i >= 0; i--) if (seed[i].role === "user") return i;
26973
+ return 0;
26974
+ }
26975
+ var DEFAULT_SESSION_LOG_MAX_BYTES, NOTEPAD_MAX_BYTES, GAP_MARKER;
26976
+ var init_sessionLog = __esm({
26977
+ "../../src/core/runLedger/sessionLog.ts"() {
26978
+ "use strict";
26979
+ init_runRecord();
26980
+ init_runRecord();
26981
+ DEFAULT_SESSION_LOG_MAX_BYTES = DEFAULT_RETENTION_POLICY.sessionLogMaxBytes;
26982
+ NOTEPAD_MAX_BYTES = 8192;
26983
+ GAP_MARKER = "[The log of this conversation ends short of what the previous run saw: its connection to the ledger broke, so its later turns and its final reply are not here.]";
26984
+ }
26985
+ });
26986
+
26987
+ // ../../src/tools/session.ts
26988
+ function sessionCapabilityFor(run2, ledger) {
26989
+ const session = run2?.session;
26990
+ if (!session) return void 0;
26991
+ return {
26992
+ session,
26993
+ search: (query, limit) => ledger.searchSession(session.key, query, limit),
26994
+ readTurn: async (idx) => (await ledger.readSession(session.key, idx, idx)).messages[0],
26995
+ readNotepad: () => ledger.readNotepad(session.key),
26996
+ writeNotepad: (text) => ledger.writeNotepad(session.key, text)
26997
+ };
26998
+ }
26999
+ async function mayRead(ctx) {
27000
+ if (!ctx.runs?.runId) return false;
27001
+ const res = await ctx.runs.service.getRun(ctx.runs.runId);
27002
+ return res.ok && authorize(ctx.runs.actor, "runs:read", runResource(res.value)).allow;
27003
+ }
27004
+ var UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
27005
+ var init_session = __esm({
27006
+ "../../src/tools/session.ts"() {
27007
+ "use strict";
27008
+ init_authorize();
27009
+ init_runEvents();
27010
+ init_sessionLog();
27011
+ init_runRecord();
27012
+ init_runsService();
27013
+ UNAVAILABLE3 = "the session tools are not available in this context: this run has no session log.";
27014
+ DEFAULT_HITS = 5;
27015
+ MAX_HITS = 50;
27016
+ SNIPPET_CHARS = 300;
27017
+ snippetOf = (text) => {
27018
+ const line3 = text.replace(/\s+/g, " ").trim();
27019
+ return line3.length > SNIPPET_CHARS ? `${line3.slice(0, SNIPPET_CHARS - 1)}\u2026` : line3;
27020
+ };
27021
+ recallTool = {
27022
+ name: "recall",
27023
+ description: "Search this conversation's whole log \u2014 every turn of this thread on this agent, earlier runs' included and everything a compaction removed from your window \u2014 for words, and get the matching turns in relevance order with their turn numbers and a snippet; or pass `turn` to read one turn whole (a tool result, a command, a message). Use it when you need something said or seen earlier that is not in front of you: a failing test's name, a command's output, a decision. `limit` defaults to 5 (at most 50).",
27024
+ inputSchema: {
27025
+ type: "object",
27026
+ properties: {
27027
+ query: { type: "string", description: "Words to search for; matching is by word, in relevance order" },
27028
+ limit: { type: "integer", description: "How many turns to return (default 5, at most 50)" },
27029
+ turn: {
27030
+ type: "integer",
27031
+ description: "A turn number from an earlier recall: returns that turn whole instead of searching"
27032
+ }
27033
+ }
27034
+ },
27035
+ sideEffectFree: true,
27036
+ async run(input, ctx) {
27037
+ if (!ctx.session) return UNAVAILABLE3;
27038
+ const turn = typeof input.turn === "number" ? Math.trunc(input.turn) : void 0;
27039
+ const query = typeof input.query === "string" ? input.query.trim() : "";
27040
+ if (turn === void 0 && query.length === 0) return "recall needs a query or a turn number.";
27041
+ if (!await mayRead(ctx)) return JSON.stringify(turn !== void 0 ? { turn, content: null } : { hits: [] });
27042
+ if (turn !== void 0) {
27043
+ const message2 = turn >= 0 ? await ctx.session.readTurn(turn) : void 0;
27044
+ if (!message2) return `no turn ${turn} in this session's log.`;
27045
+ return JSON.stringify({ turn, role: message2.role, content: message2.content });
27046
+ }
27047
+ const limit = Math.min(
27048
+ MAX_HITS,
27049
+ Math.max(1, typeof input.limit === "number" ? Math.trunc(input.limit) : DEFAULT_HITS)
27050
+ );
27051
+ const { hits, gaps } = await ctx.session.search(query, limit);
27052
+ const notes = [];
27053
+ if (hits.length === 0)
27054
+ notes.push("no turn of this session's log matches; your notes (notes {}) may hold what you look for");
27055
+ if (ctx.session.session.seedFrom === 0) notes.push("this is the first run of its session: the log begins with it");
27056
+ return JSON.stringify({
27057
+ hits: hits.map((h) => ({
27058
+ turn: h.idx,
27059
+ ...h.role !== void 0 ? { role: h.role } : {},
27060
+ snippet: snippetOf(h.text)
27061
+ })),
27062
+ ...gaps.length ? {
27063
+ gaps: `the log has a gap at ${gaps.map((g) => `turn ${g}`).join(", ")}: a run before it detached from the ledger, so turns it saw after that point are not here`
27064
+ } : {},
27065
+ ...notes.length ? { note: notes.join("; ") } : {}
27066
+ });
27067
+ }
27068
+ };
27069
+ notesTool = {
27070
+ name: "notes",
27071
+ description: "Your notes for this thread \u2014 one document, replaced whole on every write, at most 8 KiB. Keep in it what must survive a compaction and reach the next run in this thread: decisions and their reasons, the names of things you found (files, tests, commits), what is not yet proven. They ride your system prompt at the start of every later run and reach you again right after a compaction. Call with `text` to write; with nothing to read.",
27072
+ inputSchema: {
27073
+ type: "object",
27074
+ properties: { text: { type: "string", description: "The whole notepad as it should read from now on" } }
27075
+ },
27076
+ async run(input, ctx) {
27077
+ if (!ctx.session) return UNAVAILABLE3;
27078
+ if (typeof input.text !== "string") {
27079
+ const current = await ctx.session.readNotepad();
27080
+ return current && current.text.length > 0 ? current.text : "(your notes for this thread are empty)";
27081
+ }
27082
+ const bytes = utf8ByteLength(input.text);
27083
+ if (bytes > NOTEPAD_MAX_BYTES)
27084
+ return `your notes are ${bytes} bytes; the notepad holds at most ${NOTEPAD_MAX_BYTES} \u2014 shorten them and write again.`;
27085
+ const written = await ctx.session.writeNotepad(input.text);
27086
+ if (!written.ok) return `notes not saved (${written.reason}): another generation drives this run now.`;
27087
+ ctx.publish?.({ type: "notes", text: redactSecrets(input.text) });
27088
+ return `notes saved (${bytes} bytes); they ride your system prompt at the next run in this thread and reach you again after a compaction.`;
27089
+ }
27090
+ };
27091
+ SESSION_TOOLS = [recallTool, notesTool];
27092
+ }
27093
+ });
27094
+
26919
27095
  // ../../src/tools/workspace.ts
26920
27096
  import { z as z21 } from "zod";
26921
27097
  var bashTool, readFileTool, writeFileTool, NAME_STATUS_MARK, NAME_STATUS_MARK_LINE, OUTPUT_CUT_RE, diffDigestTool, submitVerdictTool, submitDispositionsTool, submitPrDescriptionTool, handoffEntry, submitHandoffTool, updateStatusTool, TOOLSETS;
@@ -26934,6 +27110,7 @@ var init_workspace = __esm({
26934
27110
  init_skills();
26935
27111
  init_attach();
26936
27112
  init_runs2();
27113
+ init_session();
26937
27114
  bashTool = {
26938
27115
  name: "bash",
26939
27116
  description: "Run a bash command in the workspace directory. Use for git, gh, tests, builds, and inspecting files. Commands time out after 5 minutes (300000 ms) by default; pass timeoutMs when a command legitimately needs longer \u2014 a full test suite, a large build \u2014 up to the 1200000 ms (20 minute) maximum; a command is also clipped to the run's own remaining time, so it can never outlive the run. Output is truncated at 30k characters.",
@@ -27303,7 +27480,8 @@ ${raw.trim()}`;
27303
27480
  listSkillsTool,
27304
27481
  useSkillTool,
27305
27482
  ...GITHUB_READ_TOOLS,
27306
- ...GITHUB_ISSUE_WRITE_TOOLS
27483
+ ...GITHUB_ISSUE_WRITE_TOOLS,
27484
+ ...SESSION_TOOLS
27307
27485
  ],
27308
27486
  readonly: [
27309
27487
  bashTool,
@@ -27314,7 +27492,8 @@ ${raw.trim()}`;
27314
27492
  diffDigestTool,
27315
27493
  listSkillsTool,
27316
27494
  useSkillTool,
27317
- ...GITHUB_READ_TOOLS
27495
+ ...GITHUB_READ_TOOLS,
27496
+ ...SESSION_TOOLS
27318
27497
  ],
27319
27498
  web: [webFetchTool, webSearchTool, updateStatusTool, ...GITHUB_READ_TOOLS],
27320
27499
  /** The general agent: no workspace, no shell — GitHub reads + issue writes
@@ -27334,7 +27513,8 @@ ${raw.trim()}`;
27334
27513
  webSearchTool,
27335
27514
  listSkillsTool,
27336
27515
  useSkillTool,
27337
- ...GITHUB_READ_TOOLS
27516
+ ...GITHUB_READ_TOOLS,
27517
+ ...SESSION_TOOLS
27338
27518
  ],
27339
27519
  /** The conductor (docs/reference/specs/agent-conductor.md): the five run
27340
27520
  * tools — the only toolset that holds them, so no other preset can start,
@@ -28212,7 +28392,7 @@ Target repository: ${input.repo}. ` + (workspace ? `Your shell starts in the wor
28212
28392
  const agentSystem = (head) => {
28213
28393
  return [residentSystem ?? agent.system, targetBlock(head), input.contract, blocks.skills, blocks.mcp].filter((b) => Boolean(b)).join("\n\n");
28214
28394
  };
28215
- return (head) => [blocks.memory, blocks.config, blocks.about, blocks.instructions, agentSystem(head)].filter((part) => Boolean(part)).join("\n\n");
28395
+ return (head) => [blocks.memory, blocks.notes, blocks.config, blocks.about, blocks.instructions, agentSystem(head)].filter((part) => Boolean(part)).join("\n\n");
28216
28396
  }
28217
28397
  async function settleReviewedHead(input) {
28218
28398
  return input.span ? input.span.span("run.settle_reviewed_head", (span) => settle(input, span)) : settle(input, void 0);
@@ -29055,13 +29235,13 @@ var init_frontmatter = __esm({
29055
29235
 
29056
29236
  // ../../src/skills/stores.ts
29057
29237
  import { existsSync as existsSync11, readFileSync as readFileSync13, readdirSync as readdirSync2 } from "node:fs";
29058
- import { join as join11 } from "node:path";
29238
+ import { join as join10 } from "node:path";
29059
29239
  function loadBundledSkills(dir) {
29060
29240
  if (!existsSync11(dir)) return [];
29061
29241
  const skills = [];
29062
29242
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
29063
29243
  if (!entry.isDirectory()) continue;
29064
- const file = join11(dir, entry.name, "SKILL.md");
29244
+ const file = join10(dir, entry.name, "SKILL.md");
29065
29245
  if (!existsSync11(file)) continue;
29066
29246
  try {
29067
29247
  skills.push(parseSkillMarkdown(readFileSync13(file, "utf8")));
@@ -29618,6 +29798,20 @@ function mintRunBearer(deps, ctx) {
29618
29798
  publish: (event) => registry.publish(runId3, event)
29619
29799
  });
29620
29800
  }
29801
+ function sessionNotesBlock(session) {
29802
+ const parts = [];
29803
+ const notepad = session.notepad?.trim();
29804
+ const summary = session.summary?.trim();
29805
+ if (notepad)
29806
+ parts.push(
29807
+ "YOUR NOTES FOR THIS THREAD (kept with the `notes` tool \u2014 the one thing sure to survive a compaction and reach the next run here; replace them whole when they change):\n" + notepad
29808
+ );
29809
+ if (summary)
29810
+ parts.push(
29811
+ "SUMMARY OF THE EARLIER CONVERSATION (written when the context was compacted; every earlier turn is still reachable with `recall`):\n" + summary
29812
+ );
29813
+ return parts.length ? parts.join("\n\n") : void 0;
29814
+ }
29621
29815
  async function composePrompt(deps, ctx) {
29622
29816
  const {
29623
29817
  msg,
@@ -29680,6 +29874,7 @@ async function composePrompt(deps, ctx) {
29680
29874
  );
29681
29875
  const instructionsBlock = customInstructionsBlock(scopes);
29682
29876
  const memoryBlock = await root.span("dispatch.compose", () => memoryBlockP);
29877
+ const notesBlock = ctx.session ? sessionNotesBlock(ctx.session) : void 0;
29683
29878
  const composeSystem = makeSystemComposer({
29684
29879
  agent,
29685
29880
  resident: resident === true,
@@ -29695,6 +29890,7 @@ async function composePrompt(deps, ctx) {
29695
29890
  ...ctx.contract !== void 0 ? { contract: ctx.contract } : {},
29696
29891
  blocks: {
29697
29892
  memory: memoryBlock,
29893
+ notes: notesBlock,
29698
29894
  config: configBlock,
29699
29895
  about: aboutBlock,
29700
29896
  instructions: instructionsBlock,
@@ -30872,6 +31068,14 @@ var init_transport = __esm({
30872
31068
  function relayedTools(tools) {
30873
31069
  return tools.filter((t) => !NATIVE_WORKSPACE_TOOLS.has(t.name));
30874
31070
  }
31071
+ function compactionSteer(notepad, opts = {}) {
31072
+ const notes = notepad?.trim();
31073
+ const reach = "Your context was just compacted: the summary now standing in for the earlier turns is pi's, and every one of those turns is still reachable with the `recall` tool (search by words, or read a turn by its number).\n\n";
31074
+ if (opts.unavailable)
31075
+ return reach + "Your notes for this thread could not be read just now; `notes {}` reads them on demand, and `notes { text }` still writes them.";
31076
+ return reach + (notes ? `YOUR NOTES FOR THIS THREAD, as you last wrote them with \`notes\`:
31077
+ ${notes}` : "Your notes for this thread are empty. Write them with the `notes` tool when you have decisions, the names of things you found and what is not yet proven worth keeping \u2014 they survive every compaction and reach the next run in this thread.");
31078
+ }
30875
31079
  function splitSeed(seed) {
30876
31080
  for (let i = seed.length - 1; i >= 0; i--) {
30877
31081
  if (seed[i].role === "user") return { session: seed.slice(0, i), prompt: seed[i] };
@@ -31150,7 +31354,23 @@ async function runPiHarness(deps, run2) {
31150
31354
  }
31151
31355
  }
31152
31356
  if (obs.message) await mirror.onMessage(obs.message, bridge.turns);
31153
- if (obs.compaction) await mirror.onCompaction(obs.compaction, bridge.turns);
31357
+ if (obs.compaction) {
31358
+ await mirror.onCompaction(obs.compaction, bridge.turns);
31359
+ if (!writeUp && !hardStopped && !catchingUp) {
31360
+ let notepad;
31361
+ let unavailable = false;
31362
+ try {
31363
+ notepad = run2.notepad ? (await run2.notepad())?.text : void 0;
31364
+ } catch (err2) {
31365
+ unavailable = true;
31366
+ note(
31367
+ "harness_error",
31368
+ `the notepad could not be read for the compaction steer (${err2 instanceof Error ? err2.message : String(err2)}); steered without it`
31369
+ );
31370
+ }
31371
+ transport.send({ type: "steer", message: compactionSteer(notepad, { unavailable }) });
31372
+ }
31373
+ }
31154
31374
  if (obs.providerError !== void 0) {
31155
31375
  if (catchingUp)
31156
31376
  note("harness_error", `a model call failed while the bot was away (${obs.providerError}); continuing`);
@@ -31738,6 +31958,7 @@ async function runLoop2(deps, ctx) {
31738
31958
  runs,
31739
31959
  steer,
31740
31960
  wait,
31961
+ session,
31741
31962
  parentRunId,
31742
31963
  coordinator,
31743
31964
  seed
@@ -31885,6 +32106,7 @@ async function runLoop2(deps, ctx) {
31885
32106
  ...runs ? { runs } : {},
31886
32107
  ...steer ? { steer } : {},
31887
32108
  ...wait ? { wait } : {},
32109
+ ...session ? { session } : {},
31888
32110
  onVerdict,
31889
32111
  onDigest,
31890
32112
  onPrDescription,
@@ -31932,6 +32154,7 @@ async function runLoop2(deps, ctx) {
31932
32154
  messages,
31933
32155
  tools: relayedTools(mergeTools(TOOLSETS[agent.toolset] ?? [], mcpForRun?.tools)),
31934
32156
  toolContext,
32157
+ ...session ? { notepad: () => session.readNotepad() } : {},
31935
32158
  rules: {
31936
32159
  checkout: binding?.workspace ?? "/workspace",
31937
32160
  ...ownBranch !== void 0 ? { branch: ownBranch } : {},
@@ -33665,24 +33888,6 @@ var init_lineage = __esm({
33665
33888
  }
33666
33889
  });
33667
33890
 
33668
- // ../../src/core/runLedger/sessionLog.ts
33669
- function sessionKey(threadKey, agent) {
33670
- return `${threadKey}:${agent ?? "-"}`;
33671
- }
33672
- function requestIndex(seed) {
33673
- for (let i = seed.length - 1; i >= 0; i--) if (seed[i].role === "user") return i;
33674
- return 0;
33675
- }
33676
- var DEFAULT_SESSION_LOG_MAX_BYTES;
33677
- var init_sessionLog = __esm({
33678
- "../../src/core/runLedger/sessionLog.ts"() {
33679
- "use strict";
33680
- init_runRecord();
33681
- init_runRecord();
33682
- DEFAULT_SESSION_LOG_MAX_BYTES = DEFAULT_RETENTION_POLICY.sessionLogMaxBytes;
33683
- }
33684
- });
33685
-
33686
33891
  // ../../src/core/dispatch/thread.ts
33687
33892
  async function readThread(service, threadKey) {
33688
33893
  try {
@@ -33798,7 +34003,15 @@ async function sessionSeedFor(input) {
33798
34003
  history: input.history,
33799
34004
  request: input.request
33800
34005
  });
33801
- return seed ? { seed, notes: seed.notes } : { notes: [] };
34006
+ if (!seed) return { notes: [] };
34007
+ try {
34008
+ const notepad = await input.ledger.readNotepad(key);
34009
+ if (notepad && notepad.text.trim().length > 0) seed.notepad = notepad.text;
34010
+ } catch (err2) {
34011
+ const why = err2 instanceof Error ? err2.message : String(err2);
34012
+ seed.notes.push(`session seed: the notepad of ${key} could not be read (${why}) \u2014 the run starts without it`);
34013
+ }
34014
+ return { seed, notes: seed.notes };
33802
34015
  }
33803
34016
  function withoutThinking(message2) {
33804
34017
  if (message2.role !== "assistant") return message2;
@@ -33806,7 +34019,7 @@ function withoutThinking(message2) {
33806
34019
  if (content.length === message2.content.length) return message2;
33807
34020
  return { ...message2, content: content.length ? content : [{ type: "text", text: "(reasoning omitted)" }] };
33808
34021
  }
33809
- var SEED_BUDGET_TOKENS, SEED_BUDGET_BYTES, GAP_MARKER, settledResult;
34022
+ var SEED_BUDGET_TOKENS, SEED_BUDGET_BYTES, settledResult;
33810
34023
  var init_seed = __esm({
33811
34024
  "../../src/core/dispatch/seed.ts"() {
33812
34025
  "use strict";
@@ -33815,7 +34028,6 @@ var init_seed = __esm({
33815
34028
  init_thread();
33816
34029
  SEED_BUDGET_TOKENS = 6e4;
33817
34030
  SEED_BUDGET_BYTES = SEED_BUDGET_TOKENS * 4;
33818
- GAP_MARKER = "[The log of this conversation ends short of what the previous run saw: its connection to the ledger broke, so its later turns and its final reply are not here.]";
33819
34031
  settledResult = (toolName) => `The run that made this call ended before this ${toolName} call's result was recorded; its effects are unknown \u2014 re-check them before repeating it.`;
33820
34032
  }
33821
34033
  });
@@ -34289,7 +34501,15 @@ async function dispatch(deps, msg, io, opts = {}) {
34289
34501
  verifiedAtAttach,
34290
34502
  resume,
34291
34503
  root,
34292
- ...contractBlock !== void 0 && agent.name === "review" ? { contract: contractBlock } : {}
34504
+ ...contractBlock !== void 0 && agent.name === "review" ? { contract: contractBlock } : {},
34505
+ // What the session already knows (session-log item 10): the notepad and
34506
+ // the newest compaction's summary the seed brought back, for the prompt.
34507
+ ...session && (session.notepad !== void 0 || session.summary !== void 0) ? {
34508
+ session: {
34509
+ ...session.notepad !== void 0 ? { notepad: session.notepad } : {},
34510
+ ...session.summary !== void 0 ? { summary: session.summary } : {}
34511
+ }
34512
+ } : {}
34293
34513
  });
34294
34514
  const { mcpForRun, composeSystem, system } = prompt;
34295
34515
  const reviewHead = prompt.reviewHead;
@@ -34347,6 +34567,7 @@ async function dispatch(deps, msg, io, opts = {}) {
34347
34567
  seed,
34348
34568
  ...session ? { seedLog: session.log } : {}
34349
34569
  });
34570
+ const sessionTools = sessionCapabilityFor(ledgerRun, deps.runLedger);
34350
34571
  const { spawn: spawn7, runs, steer, wait } = runToolCapabilities(
34351
34572
  { core: deps, dispatch, registry, clock: clock3 },
34352
34573
  {
@@ -34397,6 +34618,7 @@ async function dispatch(deps, msg, io, opts = {}) {
34397
34618
  runs,
34398
34619
  steer,
34399
34620
  wait,
34621
+ ...sessionTools ? { session: sessionTools } : {},
34400
34622
  parentRunId,
34401
34623
  coordinator,
34402
34624
  seed,
@@ -34527,6 +34749,7 @@ var init_dispatcher = __esm({
34527
34749
  init_settle();
34528
34750
  init_lineage();
34529
34751
  init_seed();
34752
+ init_session();
34530
34753
  init_thread();
34531
34754
  init_select();
34532
34755
  init_spawn();
@@ -34645,7 +34868,7 @@ var init_runHistoryWriter = __esm({
34645
34868
 
34646
34869
  // ../../src/core/runStore.ts
34647
34870
  import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
34648
- import { join as join12, resolve as resolve9 } from "node:path";
34871
+ import { join as join11, resolve as resolve9 } from "node:path";
34649
34872
  function isValidRunId(id) {
34650
34873
  return typeof id === "string" && RUN_ID_PATTERN.test(id);
34651
34874
  }
@@ -34693,7 +34916,7 @@ function buildRunStore(cfg, secrets, deps) {
34693
34916
  const now = deps.now ?? Date.now;
34694
34917
  const policy = retentionPolicyOf(cfg);
34695
34918
  if (cfg.store === "file") {
34696
- const store = new FileRunStore(join12(deps.dataDir, RUNS_DIR), { policy, now });
34919
+ const store = new FileRunStore(join11(deps.dataDir, RUNS_DIR), { policy, now });
34697
34920
  store.sweep();
34698
34921
  const timer = (deps.setInterval ?? setInterval)(() => store.sweep(), SWEEP_INTERVAL_MS);
34699
34922
  timer.unref?.();
@@ -34760,7 +34983,7 @@ var init_runStore = __esm({
34760
34983
  this.now = opts.now ?? Date.now;
34761
34984
  }
34762
34985
  recordPath(id) {
34763
- return join12(this.dir, `${id}.json`);
34986
+ return join11(this.dir, `${id}.json`);
34764
34987
  }
34765
34988
  ensureDir() {
34766
34989
  mkdirSync8(this.dir, { recursive: true, mode: DIR_MODE });
@@ -34782,7 +35005,7 @@ var init_runStore = __esm({
34782
35005
  }
34783
35006
  /** Every valid index line, a repeated id resolved to its last line. */
34784
35007
  readIndex() {
34785
- const path = join12(this.dir, INDEX_FILE);
35008
+ const path = join11(this.dir, INDEX_FILE);
34786
35009
  if (!existsSync12(path)) return [];
34787
35010
  const byId = /* @__PURE__ */ new Map();
34788
35011
  for (const line3 of readFileSync14(path, "utf8").split("\n")) {
@@ -34814,7 +35037,7 @@ var init_runStore = __esm({
34814
35037
  const keptIds = new Set(kept.map((r) => r.id));
34815
35038
  for (const item of index) if (!keptIds.has(item.id)) rmSync3(this.recordPath(item.id), { force: true });
34816
35039
  this.writeAtomic(
34817
- join12(this.dir, INDEX_FILE),
35040
+ join11(this.dir, INDEX_FILE),
34818
35041
  kept.map((r) => JSON.stringify(r)).join("\n") + (kept.length ? "\n" : "")
34819
35042
  );
34820
35043
  return kept;
@@ -35044,7 +35267,10 @@ function createLedgerWriteThrough(opts) {
35044
35267
  /** The run's place in its session log (session-log item 2); undefined for
35045
35268
  * a run without a conversation of its own and for an adopted row claimed
35046
35269
  * before the log existed. */
35047
- session;
35270
+ sessionRow;
35271
+ get session() {
35272
+ return this.sessionRow;
35273
+ }
35048
35274
  /** Turns of the run's conversation on the ledger, counted from its seed —
35049
35275
  * the last step record's `turnIndex` — so the finish can close the range. */
35050
35276
  turnsWritten;
@@ -35082,11 +35308,11 @@ function createLedgerWriteThrough(opts) {
35082
35308
  this.stepNo = from.stepNo;
35083
35309
  this.lastSeq = from.lastSeq;
35084
35310
  this.adopted = from.resumable === true;
35085
- this.session = from.session;
35311
+ this.sessionRow = from.session;
35086
35312
  }
35087
35313
  /** A reservation learns its session when the prompt's claim promotes it. */
35088
35314
  bindSession(session) {
35089
- this.session = session;
35315
+ this.sessionRow = session;
35090
35316
  }
35091
35317
  get resumable() {
35092
35318
  return !this.detached && (this.seeded || this.adopted);
@@ -35099,11 +35325,11 @@ function createLedgerWriteThrough(opts) {
35099
35325
  * ends short of what the model saw, and the next run in the session must
35100
35326
  * know. A run that wrote no turn leaves the range open. */
35101
35327
  sessionOnRecord() {
35102
- if (!this.session) return void 0;
35103
- if (this.broken) return { ...this.session, range: "broken" };
35104
- const from = this.session.range === "broken" ? this.session.seedFrom : this.session.range.from;
35105
- const to = this.turnsWritten !== void 0 && this.turnsWritten > 0 ? this.session.seedFrom + this.turnsWritten - 1 : void 0;
35106
- return { ...this.session, range: to !== void 0 && to >= from ? { from, to } : { from } };
35328
+ if (!this.sessionRow) return void 0;
35329
+ if (this.broken) return { ...this.sessionRow, range: "broken" };
35330
+ const from = this.sessionRow.range === "broken" ? this.sessionRow.seedFrom : this.sessionRow.range.from;
35331
+ const to = this.turnsWritten !== void 0 && this.turnsWritten > 0 ? this.sessionRow.seedFrom + this.turnsWritten - 1 : void 0;
35332
+ return { ...this.sessionRow, range: to !== void 0 && to >= from ? { from, to } : { from } };
35107
35333
  }
35108
35334
  sink = {
35109
35335
  put: async (plain4) => {
@@ -35150,11 +35376,11 @@ function createLedgerWriteThrough(opts) {
35150
35376
  * no record at all was killed before its conversation was stored and
35151
35377
  * closes `interrupted`. */
35152
35378
  async seed(messages, budgetMs) {
35153
- const base = this.session?.seedFrom ?? 0;
35154
- const reused = this.session && this.session.range !== "broken" ? this.session.range.from - this.session.seedFrom : 0;
35379
+ const base = this.sessionRow?.seedFrom ?? 0;
35380
+ const reused = this.sessionRow && this.sessionRow.range !== "broken" ? this.sessionRow.range.from - this.sessionRow.seedFrom : 0;
35155
35381
  const turns = messages.slice(reused).map((message2, i) => ({ idx: base + reused + i, message: message2 }));
35156
35382
  try {
35157
- const seeded = await ledger.seed(this.runId, gen, turns, this.session?.key);
35383
+ const seeded = await ledger.seed(this.runId, gen, turns, this.sessionRow?.key);
35158
35384
  if (!seeded.ok) {
35159
35385
  this.detach(`seed refused (${seeded.reason})`);
35160
35386
  return;
@@ -35174,7 +35400,7 @@ function createLedgerWriteThrough(opts) {
35174
35400
  iteration: 0
35175
35401
  },
35176
35402
  [],
35177
- this.session?.key
35403
+ this.sessionRow?.key
35178
35404
  );
35179
35405
  if (!recorded.ok) this.detach(`seed record refused (${recorded.reason})`);
35180
35406
  else this.seeded = true;
@@ -35184,7 +35410,7 @@ function createLedgerWriteThrough(opts) {
35184
35410
  }
35185
35411
  async step(report2) {
35186
35412
  if (this.detached) return;
35187
- const base = this.session?.seedFrom ?? 0;
35413
+ const base = this.sessionRow?.seedFrom ?? 0;
35188
35414
  const turns = report2.turns.map((message2, i) => ({ idx: base + report2.firstIdx + i, message: message2 }));
35189
35415
  if (report2.compaction)
35190
35416
  turns.push({ idx: base + report2.firstIdx + report2.turns.length, compaction: report2.compaction });
@@ -35200,7 +35426,7 @@ function createLedgerWriteThrough(opts) {
35200
35426
  };
35201
35427
  for (let attempt = 1; ; attempt++) {
35202
35428
  try {
35203
- const result = await ledger.step(this.runId, gen, record2, turns, this.session?.key);
35429
+ const result = await ledger.step(this.runId, gen, record2, turns, this.sessionRow?.key);
35204
35430
  if (!result.ok) this.detach(`step ${record2.step} refused (${result.reason})`);
35205
35431
  else this.turnsWritten = record2.turnIndex;
35206
35432
  return;
@@ -35386,6 +35612,10 @@ function createLedgerWriteThrough(opts) {
35386
35612
  }
35387
35613
  },
35388
35614
  readSessionTail: (key, maxBytes) => ledger.readSessionTail(key, maxBytes),
35615
+ readSession: (key, from, to) => ledger.readSession(key, from, to),
35616
+ searchSession: (key, query, limit) => ledger.searchSession(key, query, limit),
35617
+ readNotepad: (key) => ledger.readNotepad(key),
35618
+ writeNotepad: (key, text) => ledger.writeNotepad(key, gen, text),
35389
35619
  async handoff() {
35390
35620
  const candidates = [...live].filter((r) => r.resumable && r.tracked() && !r.handedOff);
35391
35621
  if (candidates.length === 0) return { marked: [] };
@@ -35439,6 +35669,18 @@ var init_writeThrough = __esm({
35439
35669
  async readSessionTail(_key, _maxBytes) {
35440
35670
  return { from: 0, transcript: { complete: true, turns: 0, messages: [], compactions: [] } };
35441
35671
  }
35672
+ async readSession(_key, _from, _to) {
35673
+ return { complete: true, turns: 0, messages: [], compactions: [] };
35674
+ }
35675
+ async searchSession(_key, _query, _limit) {
35676
+ return { hits: [], gaps: [] };
35677
+ }
35678
+ async readNotepad(_key) {
35679
+ return null;
35680
+ }
35681
+ async writeNotepad(_key, _text) {
35682
+ return { ok: false, reason: "unknown-run" };
35683
+ }
35442
35684
  async handoff() {
35443
35685
  return { marked: [] };
35444
35686
  }
@@ -35452,6 +35694,7 @@ var init_writeThrough = __esm({
35452
35694
  sink;
35453
35695
  resumable = false;
35454
35696
  handedOff = false;
35697
+ session = void 0;
35455
35698
  tracked() {
35456
35699
  return false;
35457
35700
  }
@@ -35931,7 +36174,10 @@ function httpMcpClientFactory(fetchImpl) {
35931
36174
  return (server) => new StreamableHttpMcpClient({
35932
36175
  url: server.url,
35933
36176
  fetch: fetchImpl,
35934
- ...server.auth?.type === "bearer" ? { headers: { authorization: `Bearer ${server.auth.token}` } } : {}
36177
+ headers: {
36178
+ ...server.headers,
36179
+ ...server.auth?.type === "bearer" ? { authorization: `Bearer ${server.auth.token}` } : {}
36180
+ }
35935
36181
  });
35936
36182
  }
35937
36183
  var init_mcp2 = __esm({
@@ -38760,18 +39006,18 @@ var init_liveView = __esm({
38760
39006
 
38761
39007
  // ../../src/channels/webAssets.ts
38762
39008
  import { readdirSync as readdirSync3, readFileSync as readFileSync16, statSync as statSync3 } from "node:fs";
38763
- import { join as join13 } from "node:path";
39009
+ import { join as join12 } from "node:path";
38764
39010
  function contentTypeFor2(path) {
38765
39011
  const dot = path.lastIndexOf(".");
38766
39012
  return (dot === -1 ? void 0 : CONTENT_TYPES[path.slice(dot)]) ?? "application/octet-stream";
38767
39013
  }
38768
39014
  function webDistDir(env, packageRoot) {
38769
- return env.SWITCHBOARD_WEB_DIST ?? join13(packageRoot, WEB_DIST_DIR);
39015
+ return env.SWITCHBOARD_WEB_DIST ?? join12(packageRoot, WEB_DIST_DIR);
38770
39016
  }
38771
39017
  function loadWebAssets(distDir) {
38772
39018
  let manifest;
38773
39019
  try {
38774
- manifest = JSON.parse(readFileSync16(join13(distDir, ".vite", "manifest.json"), "utf8"));
39020
+ manifest = JSON.parse(readFileSync16(join12(distDir, ".vite", "manifest.json"), "utf8"));
38775
39021
  } catch (err2) {
38776
39022
  throw new Error(
38777
39023
  `web app manifest not found under ${distDir} \u2014 build it first (npm run build in web/): ${err2 instanceof Error ? err2.message : String(err2)}`,
@@ -38785,9 +39031,9 @@ function loadWebAssets(distDir) {
38785
39031
  css: (entryChunk.css ?? []).map((f) => `/${f}`)
38786
39032
  };
38787
39033
  const files = /* @__PURE__ */ new Map();
38788
- const assetsDir = join13(distDir, "assets");
39034
+ const assetsDir = join12(distDir, "assets");
38789
39035
  for (const name of readdirSync3(assetsDir)) {
38790
- const full = join13(assetsDir, name);
39036
+ const full = join12(assetsDir, name);
38791
39037
  if (statSync3(full).isFile()) files.set(`/assets/${name}`, readFileSync16(full));
38792
39038
  }
38793
39039
  const serve2 = (req, res) => {
@@ -39977,6 +40223,25 @@ var init_runLedgerWorker = __esm({
39977
40223
  )
39978
40224
  };
39979
40225
  }
40226
+ async searchSession(key, query, limit) {
40227
+ this.checkSessionKey(key);
40228
+ const r = await this.post("/runs/session/search", { key, query, limit });
40229
+ return {
40230
+ hits: Array.isArray(r.data.hits) ? r.data.hits : [],
40231
+ gaps: Array.isArray(r.data.gaps) ? r.data.gaps : []
40232
+ };
40233
+ }
40234
+ async readNotepad(key) {
40235
+ this.checkSessionKey(key);
40236
+ const r = await this.post("/runs/session/notepad", { key });
40237
+ const n2 = r.data.notepad;
40238
+ return n2 && typeof n2.text === "string" && typeof n2.updatedAt === "number" ? { text: n2.text, updatedAt: n2.updatedAt } : null;
40239
+ }
40240
+ async writeNotepad(key, gen, text) {
40241
+ this.checkSessionKey(key);
40242
+ if (!GEN_PATTERN.test(gen)) throw new PermanentStoreError(`run ledger: malformed generation`);
40243
+ return this.fenceResult(await this.post("/runs/session/notepad/write", { key, gen, text }));
40244
+ }
39980
40245
  async heartbeat(runId3, gen, leaseMs) {
39981
40246
  this.checkIds(runId3, gen);
39982
40247
  const r = await this.post("/runs/heartbeat", { storeKey: this.opts.storeKey, runId: runId3, gen, leaseMs });
@@ -42834,7 +43099,7 @@ __export(index_exports, {
42834
43099
  runBot: () => runBot
42835
43100
  });
42836
43101
  import { createServer } from "node:http";
42837
- import { join as join14 } from "node:path";
43102
+ import { join as join13 } from "node:path";
42838
43103
  async function runBot() {
42839
43104
  for (const v of ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]) {
42840
43105
  if (!processSecrets.get(v)) {
@@ -43512,7 +43777,7 @@ var init_index = __esm({
43512
43777
  init_secrets2();
43513
43778
  DATA_DIR = installationPath(OPERATOR_ROOT, "data");
43514
43779
  CONFIG_PATH2 = process.env.SWITCHBOARD_CONFIG ?? installationPath(OPERATOR_ROOT, "config/config.yaml");
43515
- OVERRIDES_PATH = process.env.SWITCHBOARD_OVERRIDES ?? join14(DATA_DIR, "overrides.json");
43780
+ OVERRIDES_PATH = process.env.SWITCHBOARD_OVERRIDES ?? join13(DATA_DIR, "overrides.json");
43516
43781
  PROCESS_STARTED_AT = systemClock();
43517
43782
  sampleProcessMetrics = startProcessMetrics();
43518
43783
  INTERRUPTED_WRITE_BUDGET_MS = 1e4;
@@ -43559,7 +43824,7 @@ init_secrets2();
43559
43824
  import { Console } from "node:console";
43560
43825
  import { existsSync as existsSync14, mkdtempSync, writeFileSync as writeFileSync9 } from "node:fs";
43561
43826
  import { tmpdir } from "node:os";
43562
- import { basename as basename3, join as join15 } from "node:path";
43827
+ import { basename as basename3, join as join14 } from "node:path";
43563
43828
  var CONFIG_PATH3 = process.env.SWITCHBOARD_CONFIG ?? installationPath(OPERATOR_ROOT, "config/config.yaml");
43564
43829
  var DATA_DIR2 = installationPath(OPERATOR_ROOT, "data");
43565
43830
  var CLI_CALLER = { kind: "cli", id: CLI_ACTOR.id, actor: CLI_ACTOR };
@@ -43733,8 +43998,8 @@ var ConsoleIO = class _ConsoleIO {
43733
43998
  /** The harness's file upload: the bytes written under the attachments dir,
43734
43999
  * the lead printed like a reply with the path a person can open. */
43735
44000
  async attachFile(file) {
43736
- this.attachments.dir ??= mkdtempSync(join15(tmpdir(), "switchboard-attachments-"));
43737
- const path = join15(this.attachments.dir, basename3(file.name));
44001
+ this.attachments.dir ??= mkdtempSync(join14(tmpdir(), "switchboard-attachments-"));
44002
+ const path = join14(this.attachments.dir, basename3(file.name));
43738
44003
  writeFileSync9(path, file.bytes);
43739
44004
  await this.reply(`${file.lead}
43740
44005
  \u{1F4CE} ${file.name} (${file.bytes.byteLength} bytes) \u2192 ${path}`);
@@ -43815,7 +44080,7 @@ function cliCapabilities(configPath, env, secrets, opts = {}) {
43815
44080
  }
43816
44081
  function wireCli() {
43817
44082
  const warn = (m) => console.error(m);
43818
- const botConfig = bindBotConfig(CONFIG_PATH3, join15(DATA_DIR2, "cli-overrides.json"));
44083
+ const botConfig = bindBotConfig(CONFIG_PATH3, join14(DATA_DIR2, "cli-overrides.json"));
43819
44084
  let loaded;
43820
44085
  const bot = () => loaded ??= botConfig().then((config) => ({
43821
44086
  config,