@coreplane/switchboard 1.217.0 → 1.219.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 +20 -0
  9. package/dist/assets/src/core/runEvents.ts +6 -0
  10. package/dist/assets/src/core/runFriction.ts +40 -3
  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 +522 -120
  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.`;
@@ -1979,8 +1992,12 @@ TIME. Your budget is up to two hours \u2014 less when a boundary or the request'
1979
1992
 
1980
1993
  READ-ONLY: NEVER open a pull request, and never commit or push \u2014 no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:coding\`.
1981
1994
 
1995
+ You cannot attach or post files: your whole answer is text. Never say a file is attached or below \u2014 name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
1996
+
1982
1997
  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
1998
 
1999
+ ${NOTEPAD}
2000
+
1984
2001
  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
2002
  COMPOUND_PRESET = "conductor";
1986
2003
  WORK_PRESETS = {
@@ -2603,6 +2620,11 @@ function splitCredentialKey(key) {
2603
2620
  const name = key.slice(slash + 1);
2604
2621
  return parseMcpScopeKey(scopeKey) && MCP_SERVER_NAME_RE.test(name) ? { scopeKey, name } : void 0;
2605
2622
  }
2623
+ function isHeadersEnv(v) {
2624
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
2625
+ const entries = Object.entries(v);
2626
+ return entries.length > 0 && entries.length <= MCP_HEADERS_MAX && entries.every(([name, envVar]) => isStr(name, 64) && isStr(envVar, 128));
2627
+ }
2606
2628
  function isOutcome(v) {
2607
2629
  if (!v || typeof v !== "object" || Array.isArray(v)) return false;
2608
2630
  const o = v;
@@ -2611,7 +2633,7 @@ function isOutcome(v) {
2611
2633
  function isMcpServerEntry(v) {
2612
2634
  if (!v || typeof v !== "object" || Array.isArray(v)) return false;
2613
2635
  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));
2636
+ 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
2637
  }
2616
2638
  function isSealedCredential(v) {
2617
2639
  if (!v || typeof v !== "object") return false;
@@ -2626,7 +2648,7 @@ function isMcpTicket(v) {
2626
2648
  }
2627
2649
  function serverView(scopeKey, name, entry, opts) {
2628
2650
  const parsed = parseMcpScopeKey(scopeKey);
2629
- const state = entry.auth === "none" ? "connected" : entry.tokenEnv ? "static" : opts.hasCredential ? "connected" : "awaiting_credential";
2651
+ const state = entry.auth === "none" ? entry.headersEnv ? "static" : "connected" : entry.tokenEnv ? "static" : opts.hasCredential ? "connected" : "awaiting_credential";
2630
2652
  return {
2631
2653
  name,
2632
2654
  scope: parsed?.kind ?? "org",
@@ -2648,7 +2670,7 @@ function safeUrl(raw) {
2648
2670
  return "(invalid url)";
2649
2671
  }
2650
2672
  }
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;
2673
+ 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
2674
  var init_registry2 = __esm({
2653
2675
  "../../src/mcp/registry.ts"() {
2654
2676
  "use strict";
@@ -2669,6 +2691,8 @@ var init_registry2 = __esm({
2669
2691
  MCP_SERVERS_PER_SCOPE_MAX = 32;
2670
2692
  isStr = (v, max = 4096) => typeof v === "string" && v.length > 0 && v.length <= max;
2671
2693
  isNum = (v) => typeof v === "number" && Number.isFinite(v);
2694
+ MCP_HEADERS_MAX = 8;
2695
+ MCP_HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,64}$/;
2672
2696
  }
2673
2697
  });
2674
2698
 
@@ -2753,7 +2777,9 @@ function validateMcpServers(layer, source2) {
2753
2777
  `${source2}: ${path}.${name}: server names are slugs (lowercase letters, digits, dashes; \u2264 ${MCP_SERVER_NAME_MAX} chars)`
2754
2778
  );
2755
2779
  if (!isMcpServerEntry(raw))
2756
- throw new Error(`${source2}: ${path}.${name} must be { url, auth: none|bearer|oauth, agents?, tokenEnv? }`);
2780
+ throw new Error(
2781
+ `${source2}: ${path}.${name} must be { url, auth: none|bearer|oauth, agents?, tokenEnv?, headersEnv? }`
2782
+ );
2757
2783
  try {
2758
2784
  assertUrlAllowed(raw.url);
2759
2785
  } catch (err2) {
@@ -2774,6 +2800,14 @@ function validateMcpServers(layer, source2) {
2774
2800
  }
2775
2801
  if (raw.tokenEnv !== void 0 && raw.auth !== "bearer")
2776
2802
  throw new Error(`${source2}: ${path}.${name}.tokenEnv only applies to auth: bearer`);
2803
+ for (const header4 of Object.keys(raw.headersEnv ?? {})) {
2804
+ if (!MCP_HEADER_NAME_RE.test(header4))
2805
+ throw new Error(`${source2}: ${path}.${name}.headersEnv: "${header4}" is not an HTTP header name`);
2806
+ if (header4.toLowerCase() === "authorization")
2807
+ throw new Error(
2808
+ `${source2}: ${path}.${name}.headersEnv: the Authorization header is \`auth\`'s \u2014 use auth: bearer with tokenEnv`
2809
+ );
2810
+ }
2777
2811
  }
2778
2812
  };
2779
2813
  check("defaults.mcpServers", "org", layer.defaults?.mcpServers);
@@ -4989,7 +5023,7 @@ function planDeploy(opts, host, loaded, images) {
4989
5023
  function formatPlan(plan3) {
4990
5024
  const missing = plan3.checks.nodeModulesMissing;
4991
5025
  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`;
5026
+ 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
5027
  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
5028
  const lines = [
4995
5029
  ...plan3.affected ? [formatAffectedText(plan3.affected)] : [],
@@ -6241,7 +6275,7 @@ var init_sandboxLiveGate = __esm({
6241
6275
  // ../../src/deploy/run.ts
6242
6276
  import { spawn as spawn3 } from "node:child_process";
6243
6277
  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";
6278
+ import { dirname as dirname4, isAbsolute as isAbsolute3 } from "node:path";
6245
6279
  function run(cmd, args, opts) {
6246
6280
  const env = { ...process.env, ...opts.set ?? {} };
6247
6281
  for (const k of opts.unset ?? []) delete env[k];
@@ -6348,7 +6382,7 @@ function packageModeProblems(plan3) {
6348
6382
  return problems;
6349
6383
  }
6350
6384
  function hasNodeModules(dir) {
6351
- if (OPERATOR_ROOT.mode === "checkout") return existsSync5(join5(OPERATOR_ROOT.root, dir, "node_modules"));
6385
+ if (OPERATOR_ROOT.mode === "checkout") return checkoutInstallHolds(OPERATOR_ROOT.root, dir, existsSync5);
6352
6386
  const state = readWorkAreaState(OPERATOR_ROOT.workArea);
6353
6387
  return state.kind === "stamped" && state.stamp.installed.includes(dir);
6354
6388
  }
@@ -6514,10 +6548,10 @@ async function pushConfigOnHost(opts) {
6514
6548
  return pushConfigDocument(read, { stateWorkerUrl: opts.stateWorkerUrl, key: opts.key, env: process.env });
6515
6549
  }
6516
6550
  async function ensureNodeModules(step, io) {
6517
- const dir = workerDir(step.dir);
6518
6551
  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 });
6552
+ const cwd = OPERATOR_ROOT.mode === "checkout" ? OPERATOR_ROOT.root : workerDir(step.dir);
6553
+ io.log(`[deploy:all] ${step.name}: install missing \u2014 npm ci in ${cwd}`);
6554
+ const r = await run("npm", ["ci", "--silent"], { cwd });
6521
6555
  if (r.code !== 0) io.warn(r.output);
6522
6556
  return r.code === 0;
6523
6557
  }
@@ -7824,9 +7858,9 @@ var init_secrets = __esm({
7824
7858
  import { spawn as spawn4 } from "node:child_process";
7825
7859
  import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
7826
7860
  import { homedir as homedir2 } from "node:os";
7827
- import { isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "node:path";
7861
+ import { isAbsolute as isAbsolute4, join as join5, resolve as resolve4 } from "node:path";
7828
7862
  function expandDir(path, root = OPERATOR_ROOT.root) {
7829
- if (path === "~" || path.startsWith("~/")) return join6(homedir2(), path.slice(1));
7863
+ if (path === "~" || path.startsWith("~/")) return join5(homedir2(), path.slice(1));
7830
7864
  if (isAbsolute4(path)) return path;
7831
7865
  return resolve4(root, path);
7832
7866
  }
@@ -7870,7 +7904,7 @@ async function opFieldLabels(source2) {
7870
7904
  }
7871
7905
  function wranglerBin(dir, workspaceRoot, exists = existsSync6) {
7872
7906
  for (const base of [dir, workspaceRoot]) {
7873
- const bin = join6(base, "node_modules", ".bin", "wrangler");
7907
+ const bin = join5(base, "node_modules", ".bin", "wrangler");
7874
7908
  if (exists(bin)) return bin;
7875
7909
  }
7876
7910
  return "wrangler";
@@ -7891,7 +7925,7 @@ var init_secretsHost = __esm({
7891
7925
  if (source2.kind === "dir") {
7892
7926
  const dir = expandDir(source2.path);
7893
7927
  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)))) };
7928
+ return { ok: true, present: new Set(names.filter((n2) => existsSync6(join5(dir, n2)))) };
7895
7929
  }
7896
7930
  const labels = await opFieldLabels(source2);
7897
7931
  if (!labels.ok) return labels;
@@ -7900,7 +7934,7 @@ var init_secretsHost = __esm({
7900
7934
  put: async (source2, dir, name) => {
7901
7935
  let value;
7902
7936
  if (source2.kind === "dir") {
7903
- value = readFileSync7(join6(expandDir(source2.path), name), "utf8");
7937
+ value = readFileSync7(join5(expandDir(source2.path), name), "utf8");
7904
7938
  } else {
7905
7939
  const r2 = await spawnCollect("op", ["read", `op://${source2.vault}/${source2.item}/${name}`]);
7906
7940
  if (r2.code !== 0) return { code: r2.code, output: `op read exited ${r2.code} \u2014 ${r2.stderr.trim()}` };
@@ -8114,11 +8148,11 @@ var init_readingDiff = __esm({
8114
8148
  // ../../src/core/meatProcess.ts
8115
8149
  import { spawn as spawn5 } from "node:child_process";
8116
8150
  import { accessSync, constants } from "node:fs";
8117
- import { delimiter, join as join7 } from "node:path";
8151
+ import { delimiter, join as join6 } from "node:path";
8118
8152
  function meatOnPath(env, binary = MEAT_BINARY) {
8119
8153
  return (env.PATH ?? "").split(delimiter).filter((dir) => dir !== "").some((dir) => {
8120
8154
  try {
8121
- accessSync(join7(dir, binary), constants.X_OK);
8155
+ accessSync(join6(dir, binary), constants.X_OK);
8122
8156
  return true;
8123
8157
  } catch {
8124
8158
  return false;
@@ -8198,10 +8232,10 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
8198
8232
  import { randomBytes } from "node:crypto";
8199
8233
  import { createInterface } from "node:readline/promises";
8200
8234
  import { Writable } from "node:stream";
8201
- import { dirname as dirname5, join as join8, resolve as resolve5 } from "node:path";
8235
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "node:path";
8202
8236
  async function readTemplates(root = PACKAGE_ROOT) {
8203
8237
  const read = async (key) => {
8204
- const path = join8(root, TEMPLATE_PATHS[key]);
8238
+ const path = join7(root, TEMPLATE_PATHS[key]);
8205
8239
  if (!existsSync7(path)) throw new Error(`${TEMPLATE_PATHS[key]}: no such file under ${root}`);
8206
8240
  return readFile(path, "utf8");
8207
8241
  };
@@ -8215,7 +8249,7 @@ function isCheckoutRoot(cwd = process.cwd(), root = PACKAGE_ROOT) {
8215
8249
  }
8216
8250
  }
8217
8251
  async function writePlannedFile(file, cwd = process.cwd()) {
8218
- const path = join8(cwd, file.path);
8252
+ const path = join7(cwd, file.path);
8219
8253
  await mkdir(dirname5(path), { recursive: true });
8220
8254
  const temp = `${path}.${randomBytes(6).toString("hex")}.tmp`;
8221
8255
  try {
@@ -8250,7 +8284,7 @@ function ttyPrompter(io = { stdin: process.stdin, stdout: process.stdout }) {
8250
8284
  };
8251
8285
  }
8252
8286
  function projectFact(name, root) {
8253
- const facts2 = JSON.parse(readFileSync8(join8(root, "project.json"), "utf8"));
8287
+ const facts2 = JSON.parse(readFileSync8(join7(root, "project.json"), "utf8"));
8254
8288
  const value = facts2[name];
8255
8289
  if (typeof value !== "string") throw new Error(`project.json: no \`${name}\` fact`);
8256
8290
  return value;
@@ -8266,7 +8300,7 @@ function hostSetupIO(cwd = OPERATOR_ROOT.root, startedIn = process.cwd()) {
8266
8300
  templates: () => readTemplates(),
8267
8301
  // Where the files went when that is not where the operator stands — `init` prints it.
8268
8302
  root: () => resolve5(cwd) === resolve5(startedIn) ? void 0 : cwd,
8269
- exists: async (path) => existsSync7(join8(cwd, path)),
8303
+ exists: async (path) => existsSync7(join7(cwd, path)),
8270
8304
  write: (file) => writePlannedFile(file, cwd),
8271
8305
  // A file the operator NAMED (`--github-private-key-file app.pem`) is where the operator stands,
8272
8306
  // not in the installation: `init` run from ~/Downloads reads ~/Downloads/app.pem.
@@ -8322,9 +8356,9 @@ var init_binaryRead = __esm({
8322
8356
 
8323
8357
  // ../../src/secrets.ts
8324
8358
  import { readFileSync as readFileSync9 } from "node:fs";
8325
- import { join as join9 } from "node:path";
8359
+ import { join as join8 } from "node:path";
8326
8360
  function manifestNames() {
8327
- const raw = JSON.parse(readFileSync9(join9(PACKAGE_ROOT, MANIFEST_PATH), "utf8"));
8361
+ const raw = JSON.parse(readFileSync9(join8(PACKAGE_ROOT, MANIFEST_PATH), "utf8"));
8328
8362
  const parsed = parseManifest2(raw);
8329
8363
  if (!parsed.ok) throw new Error(`${MANIFEST_PATH} is invalid \u2014 ${parsed.problems.join("; ")}`);
8330
8364
  return parsed.manifest.secrets.map((s) => s.name);
@@ -12482,7 +12516,7 @@ ${quote(current ?? "")}`;
12482
12516
  });
12483
12517
 
12484
12518
  // ../../src/core/commands/contract.ts
12485
- import { basename, dirname as dirname8, join as join10 } from "node:path";
12519
+ import { basename, dirname as dirname8, join as join9 } from "node:path";
12486
12520
  import { z as z7 } from "zod";
12487
12521
  function repoRootOfPlan(planPath) {
12488
12522
  const plans = dirname8(planPath);
@@ -12508,7 +12542,7 @@ var init_contract2 = __esm({
12508
12542
  init_commandRegistry();
12509
12543
  init_contract();
12510
12544
  defineCommand4 = commandDefiner();
12511
- SPECS_DIR = join10("docs", "reference", "specs");
12545
+ SPECS_DIR = join9("docs", "reference", "specs");
12512
12546
  RULES_FILES = ["AGENTS.md", "CLAUDE.md"];
12513
12547
  contractRender = defineCommand4({
12514
12548
  id: "contract.render",
@@ -12532,10 +12566,10 @@ var init_contract2 = __esm({
12532
12566
  const unit = parsePlanUnit(planMarkdown, options.unit);
12533
12567
  const specs = /* @__PURE__ */ new Map();
12534
12568
  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)));
12569
+ specs.set(spec, await deps.contract.readFile(join9(root, SPECS_DIR, spec)));
12536
12570
  let agentRules;
12537
12571
  for (const file of RULES_FILES) {
12538
- const text = await deps.contract.readFile(join10(root, file));
12572
+ const text = await deps.contract.readFile(join9(root, file));
12539
12573
  if (text !== void 0) {
12540
12574
  agentRules = { file, text };
12541
12575
  break;
@@ -13460,7 +13494,7 @@ function isSetupInstallCommand(summary) {
13460
13494
  return INSTALL_SEGMENT.test(cmd.replace(QUOTED, '""'));
13461
13495
  }
13462
13496
  function isNarrative(ev) {
13463
- return ev.type === "input" || ev.type === "context" || ev.type === "assistant" || ev.type === "answer" || ev.type === "run_meta";
13497
+ return ev.type === "input" || ev.type === "context" || ev.type === "assistant" || ev.type === "answer" || ev.type === "notes" || ev.type === "run_meta";
13464
13498
  }
13465
13499
  function spanSet(events) {
13466
13500
  const normalized = normalizeSpans(events);
@@ -13581,6 +13615,8 @@ function analyzeRunFriction(events, opts = {}) {
13581
13615
  let sideFactEvents = 0;
13582
13616
  let spanEvents = 0;
13583
13617
  let wrapUp;
13618
+ let answer;
13619
+ let outboundArtifacts = 0;
13584
13620
  events.forEach((ev, index) => {
13585
13621
  flushTurns(index);
13586
13622
  if (isSpanRecord(ev)) {
@@ -13589,8 +13625,10 @@ function analyzeRunFriction(events, opts = {}) {
13589
13625
  }
13590
13626
  if (isNarrative(ev)) {
13591
13627
  narrativeEvents++;
13628
+ if (ev.type === "answer") answer = { index, text: ev.text };
13592
13629
  return;
13593
13630
  }
13631
+ if (ev.type === "artifact" && ev.direction === "out") outboundArtifacts++;
13594
13632
  if (ev.type === "skill_use" || ev.type === "artifact" || ev.type === "review_artifact" || ev.type === "pr_description" || ev.type === "pr_opened" || ev.type === "review_posted" || ev.type === "ship_round" || ev.type === "route") {
13595
13633
  sideFactEvents++;
13596
13634
  return;
@@ -13736,6 +13774,20 @@ function analyzeRunFriction(events, opts = {}) {
13736
13774
  f.interval = { start: at, end: Math.max(at, windowEnd) };
13737
13775
  }
13738
13776
  }
13777
+ if (answer !== void 0 && outboundArtifacts === 0) {
13778
+ const promised = ATTACHMENT_PROMISE.exec(answer.text);
13779
+ if (promised) {
13780
+ const line3 = answer.text.slice(0, promised.index).split("\n").pop() + answer.text.slice(promised.index).split("\n")[0];
13781
+ const trimmed = line3.trim();
13782
+ const quoted = trimmed.length > 120 ? `\u2026${trimmed.slice(trimmed.length - 119)}` : trimmed;
13783
+ findings.push({
13784
+ category: "unkept_promise",
13785
+ severity: "low",
13786
+ summary: `unkept promise: the reply says a file is attached but the run produced none \u2014 "${quoted}"`,
13787
+ eventIndex: answer.index
13788
+ });
13789
+ }
13790
+ }
13739
13791
  let toolTimeMs;
13740
13792
  for (const s of toolSpans) {
13741
13793
  const d = durationOfSpan(s);
@@ -13812,7 +13864,7 @@ function formatFrictionReport(d) {
13812
13864
  }
13813
13865
  return lines.join("\n");
13814
13866
  }
13815
- var FRICTION_CATEGORIES, CATEGORY_LABEL, DENOMINATOR_OF, DEFAULT_SLOW_TOOL_MS, DEFAULT_SLOW_MODEL_TURN_MS, INSTALL_SEGMENT, QUOTED, callSignature;
13867
+ var FRICTION_CATEGORIES, CATEGORY_LABEL, DENOMINATOR_OF, ATTACHMENT_PROMISE, DEFAULT_SLOW_TOOL_MS, DEFAULT_SLOW_MODEL_TURN_MS, INSTALL_SEGMENT, QUOTED, callSignature;
13816
13868
  var init_runFriction = __esm({
13817
13869
  "../../src/core/runFriction.ts"() {
13818
13870
  "use strict";
@@ -13829,7 +13881,8 @@ var init_runFriction = __esm({
13829
13881
  "setup_install",
13830
13882
  "wrap_up",
13831
13883
  "budget_hit",
13832
- "infra_failure"
13884
+ "infra_failure",
13885
+ "unkept_promise"
13833
13886
  ];
13834
13887
  CATEGORY_LABEL = {
13835
13888
  slow_tool: "slow tool calls",
@@ -13839,7 +13892,8 @@ var init_runFriction = __esm({
13839
13892
  setup_install: "the repo's setup/install",
13840
13893
  wrap_up: "agent wind-down",
13841
13894
  budget_hit: "budget hits",
13842
- infra_failure: "infra failures"
13895
+ infra_failure: "infra failures",
13896
+ unkept_promise: "unkept promises"
13843
13897
  };
13844
13898
  DENOMINATOR_OF = {
13845
13899
  slow_tool: "tool",
@@ -13849,8 +13903,10 @@ var init_runFriction = __esm({
13849
13903
  slow_model_turn: "run",
13850
13904
  wrap_up: "run",
13851
13905
  budget_hit: "run",
13852
- infra_failure: "run"
13906
+ infra_failure: "run",
13907
+ unkept_promise: "run"
13853
13908
  };
13909
+ ATTACHMENT_PROMISE = /\b(?:attached (?:below|here|above)|see (?:the )?attached|i(?:'ve| have) attached|(?:is|are) attached\b|attachments? (?:(?:is|are) )?(?:below|here))/i;
13854
13910
  DEFAULT_SLOW_TOOL_MS = 3e4;
13855
13911
  DEFAULT_SLOW_MODEL_TURN_MS = 6e4;
13856
13912
  INSTALL_SEGMENT = /(?:^|&&|;|\|\|?)\s*(?:sudo\s+)?(?:corepack\s+)?(?:(?:npm|pnpm|bun)\s+(?:install|i|ci|add)(?=\s|$)|yarn(?:\s+(?:install|add)(?=\s|$)|\s*$)|npx\s+playwright\s+install(?=\s|$)|(?:python3?\s+-m\s+)?pip3?\s+install(?=\s|$)|poetry\s+install(?=\s|$)|uv\s+(?:sync|pip\s+install)(?=\s|$)|apt(?:-get)?\s+install(?=\s|$)|apk\s+add(?=\s|$)|brew\s+install(?=\s|$)|bundle\s+install(?=\s|$)|gem\s+install(?=\s|$)|cargo\s+(?:fetch|build)(?=\s|$)|go\s+mod\s+download(?=\s|$)|git\s+clone(?=\s|$)|make\s+(?:deps|install|setup)(?=\s|$))/;
@@ -14998,6 +15054,8 @@ function patternSignature(f) {
14998
15054
  }
14999
15055
  case "wrap_up":
15000
15056
  return "wrap_up:wrap_up";
15057
+ case "unkept_promise":
15058
+ return "unkept_promise:attachment";
15001
15059
  case "slow_model_turn":
15002
15060
  return "slow_model_turn:model_turn";
15003
15061
  case "infra_failure": {
@@ -15217,6 +15275,8 @@ ${evidence}`);
15217
15275
  return `Runs ended with \`${p.signature.replace(/^mid-tool /, "")}\` still outstanding \u2014 the run (or its transport) was cut while the tool ran. Correlate the affected runs with deploys/drains (\`[drain]\` log lines), the sandbox command timeout (exit 124 / heartbeat streaming, docs/reference/specs/execution.md), and the executor's error surfacing.`;
15218
15276
  }
15219
15277
  return `The exec transport failed during \`${p.signature}\` in ${p.runIds.length} runs (the sandbox, not the command). Check the sandbox/resident Worker logs (\`deploy/bin/cf-logs\`) around the affected runs for the underlying error, and whether the command's runtime exceeds the executor's timeout.`;
15278
+ case "unkept_promise":
15279
+ return `In ${p.runIds.length} runs the reply told the person a file was attached and the run produced none \u2014 no outbound \`artifact\` event, so nothing reached the thread or the run page. Read the affected runs' presets: a preset without \`attach_file\` in its toolset (\`src/agents/registry.ts\`) must say in its prompt that its whole answer is text and name the workspace path instead of promising an attachment; an ask that needs the file belongs on a preset with the full toolset (the router's description of the preset should say it cannot post files). If the run did call \`attach_file\` and it failed, the paired \`failed_tool\` proposal has the cause.`;
15220
15280
  case "long_run":
15221
15281
  return `These \`${p.signature}\` runs took \u2265 ${LONG_RUN_FACTOR}\xD7 the median run time \u2014 the closest available proxy for a cost spike (there is no per-run token accounting yet). Open each affected run's friction report for its dominant cause; if the shape of work is legitimately long, split it or lower the agent's effort tier; if it is not, tighten \`maxMinutes\` so a runaway run is cut earlier.`;
15222
15282
  }
@@ -15258,6 +15318,7 @@ var init_frictionProposals = __esm({
15258
15318
  wrap_up: "agent wind-down",
15259
15319
  budget_hit: "budget hit",
15260
15320
  infra_failure: "infra failure",
15321
+ unkept_promise: "unkept promise",
15261
15322
  long_run: "long run"
15262
15323
  };
15263
15324
  RESULT_TAIL = / → [\s\S]*$/;
@@ -15383,6 +15444,7 @@ function isRunEvent(v) {
15383
15444
  case "input":
15384
15445
  case "assistant":
15385
15446
  case "context":
15447
+ case "notes":
15386
15448
  return typeof o.text === "string";
15387
15449
  // the narrative events carry text, not a summary
15388
15450
  case "run_meta":
@@ -17231,7 +17293,11 @@ var init_service = __esm({
17231
17293
  /** A fresh connect link for a runtime bearer/oauth server (first connect, or a re-key). */
17232
17294
  async connect(actor, target, name) {
17233
17295
  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)`);
17296
+ if (entry.auth === "none")
17297
+ throw new McpServiceError(
17298
+ "invalid_input",
17299
+ 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)`
17300
+ );
17235
17301
  if (entry.tokenEnv)
17236
17302
  throw new McpServiceError(
17237
17303
  "invalid_input",
@@ -17708,6 +17774,15 @@ var init_service = __esm({
17708
17774
  url: r.entry.url,
17709
17775
  agents: r.entry.agents ?? [...MCP_SELF_SERVE_AGENTS]
17710
17776
  };
17777
+ if (r.entry.headersEnv) {
17778
+ const headers = {};
17779
+ for (const [header4, envVar] of Object.entries(r.entry.headersEnv)) {
17780
+ const value = this.opts.bearers.named(envVar);
17781
+ if (!value) return { name: r.name, unavailable: `${envVar} is not set on the bot` };
17782
+ headers[header4] = value.reveal();
17783
+ }
17784
+ base.headers = headers;
17785
+ }
17711
17786
  if (r.entry.auth === "none") return { spec: base };
17712
17787
  if (r.entry.tokenEnv) {
17713
17788
  const token2 = this.opts.bearers.named(r.entry.tokenEnv);
@@ -17827,7 +17902,7 @@ async function settleConnect(deps, output) {
17827
17902
  }
17828
17903
  }
17829
17904
  function renderServerLine(r) {
17830
- const state = r.state === "connected" ? "\u2705 connected" : r.state === "static" ? "\u2705 static (tokenEnv)" : "\u23F3 awaiting credential";
17905
+ const state = r.state === "connected" ? "\u2705 connected" : r.state === "static" ? "\u2705 static (bot env)" : "\u23F3 awaiting credential";
17831
17906
  const agents = Array.isArray(r.agents) ? r.agents.join(", ") : "";
17832
17907
  const pinned = r.source === "config" ? " \xB7 pinned in config.yaml" : "";
17833
17908
  return `\u2022 \`${String(r.name)}\` (${String(r.scope)}) ${state} \u2014 ${String(r.url)} \xB7 agents: ${agents} \xB7 auth: ${String(r.auth)}${pinned}`;
@@ -21929,6 +22004,8 @@ function activityLine(e) {
21929
22004
  return "request received";
21930
22005
  case "context":
21931
22006
  return "context recorded";
22007
+ case "notes":
22008
+ return "\u{1F4DD} notes saved";
21932
22009
  case "answer":
21933
22010
  return "answer ready";
21934
22011
  case "run_meta":
@@ -23347,7 +23424,7 @@ async function resolveRepoContext(msg, history = [], probe, residentSlugs) {
23347
23424
  };
23348
23425
  const resolveAddressed = async (a) => {
23349
23426
  if (a.slug !== void 0) return probe && await vet(a.slug) ? a.slug : void 0;
23350
- const matches = (await listSlugs() ?? []).filter((slug) => slug.split("/")[1] === a.name);
23427
+ const matches = (await listSlugs() ?? []).filter((slug) => slug.split("/")[1]?.toLowerCase() === a.name);
23351
23428
  return matches.length === 1 ? matches[0] : void 0;
23352
23429
  };
23353
23430
  const strongNow = s.pr?.repo ?? (s.repoStrong ? s.repo : void 0) ?? (s.addressed ? await resolveAddressed(s.addressed) : void 0);
@@ -26916,6 +26993,134 @@ var init_runs2 = __esm({
26916
26993
  }
26917
26994
  });
26918
26995
 
26996
+ // ../../src/core/runLedger/sessionLog.ts
26997
+ function sessionKey(threadKey, agent) {
26998
+ return `${threadKey}:${agent ?? "-"}`;
26999
+ }
27000
+ function requestIndex(seed) {
27001
+ for (let i = seed.length - 1; i >= 0; i--) if (seed[i].role === "user") return i;
27002
+ return 0;
27003
+ }
27004
+ var DEFAULT_SESSION_LOG_MAX_BYTES, NOTEPAD_MAX_BYTES, GAP_MARKER;
27005
+ var init_sessionLog = __esm({
27006
+ "../../src/core/runLedger/sessionLog.ts"() {
27007
+ "use strict";
27008
+ init_runRecord();
27009
+ init_runRecord();
27010
+ DEFAULT_SESSION_LOG_MAX_BYTES = DEFAULT_RETENTION_POLICY.sessionLogMaxBytes;
27011
+ NOTEPAD_MAX_BYTES = 8192;
27012
+ 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.]";
27013
+ }
27014
+ });
27015
+
27016
+ // ../../src/tools/session.ts
27017
+ function sessionCapabilityFor(run2, ledger) {
27018
+ const session = run2?.session;
27019
+ if (!session) return void 0;
27020
+ return {
27021
+ session,
27022
+ search: (query, limit) => ledger.searchSession(session.key, query, limit),
27023
+ readTurn: async (idx) => (await ledger.readSession(session.key, idx, idx)).messages[0],
27024
+ readNotepad: () => ledger.readNotepad(session.key),
27025
+ writeNotepad: (text) => ledger.writeNotepad(session.key, text)
27026
+ };
27027
+ }
27028
+ async function mayRead(ctx) {
27029
+ if (!ctx.runs?.runId) return false;
27030
+ const res = await ctx.runs.service.getRun(ctx.runs.runId);
27031
+ return res.ok && authorize(ctx.runs.actor, "runs:read", runResource(res.value)).allow;
27032
+ }
27033
+ var UNAVAILABLE3, DEFAULT_HITS, MAX_HITS, SNIPPET_CHARS, snippetOf, recallTool, notesTool, SESSION_TOOLS;
27034
+ var init_session = __esm({
27035
+ "../../src/tools/session.ts"() {
27036
+ "use strict";
27037
+ init_authorize();
27038
+ init_runEvents();
27039
+ init_sessionLog();
27040
+ init_runRecord();
27041
+ init_runsService();
27042
+ UNAVAILABLE3 = "the session tools are not available in this context: this run has no session log.";
27043
+ DEFAULT_HITS = 5;
27044
+ MAX_HITS = 50;
27045
+ SNIPPET_CHARS = 300;
27046
+ snippetOf = (text) => {
27047
+ const line3 = text.replace(/\s+/g, " ").trim();
27048
+ return line3.length > SNIPPET_CHARS ? `${line3.slice(0, SNIPPET_CHARS - 1)}\u2026` : line3;
27049
+ };
27050
+ recallTool = {
27051
+ name: "recall",
27052
+ 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).",
27053
+ inputSchema: {
27054
+ type: "object",
27055
+ properties: {
27056
+ query: { type: "string", description: "Words to search for; matching is by word, in relevance order" },
27057
+ limit: { type: "integer", description: "How many turns to return (default 5, at most 50)" },
27058
+ turn: {
27059
+ type: "integer",
27060
+ description: "A turn number from an earlier recall: returns that turn whole instead of searching"
27061
+ }
27062
+ }
27063
+ },
27064
+ sideEffectFree: true,
27065
+ async run(input, ctx) {
27066
+ if (!ctx.session) return UNAVAILABLE3;
27067
+ const turn = typeof input.turn === "number" ? Math.trunc(input.turn) : void 0;
27068
+ const query = typeof input.query === "string" ? input.query.trim() : "";
27069
+ if (turn === void 0 && query.length === 0) return "recall needs a query or a turn number.";
27070
+ if (!await mayRead(ctx)) return JSON.stringify(turn !== void 0 ? { turn, content: null } : { hits: [] });
27071
+ if (turn !== void 0) {
27072
+ const message2 = turn >= 0 ? await ctx.session.readTurn(turn) : void 0;
27073
+ if (!message2) return `no turn ${turn} in this session's log.`;
27074
+ return JSON.stringify({ turn, role: message2.role, content: message2.content });
27075
+ }
27076
+ const limit = Math.min(
27077
+ MAX_HITS,
27078
+ Math.max(1, typeof input.limit === "number" ? Math.trunc(input.limit) : DEFAULT_HITS)
27079
+ );
27080
+ const { hits, gaps } = await ctx.session.search(query, limit);
27081
+ const notes = [];
27082
+ if (hits.length === 0)
27083
+ notes.push("no turn of this session's log matches; your notes (notes {}) may hold what you look for");
27084
+ if (ctx.session.session.seedFrom === 0) notes.push("this is the first run of its session: the log begins with it");
27085
+ return JSON.stringify({
27086
+ hits: hits.map((h) => ({
27087
+ turn: h.idx,
27088
+ ...h.role !== void 0 ? { role: h.role } : {},
27089
+ snippet: snippetOf(h.text)
27090
+ })),
27091
+ ...gaps.length ? {
27092
+ 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`
27093
+ } : {},
27094
+ ...notes.length ? { note: notes.join("; ") } : {}
27095
+ });
27096
+ }
27097
+ };
27098
+ notesTool = {
27099
+ name: "notes",
27100
+ 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.",
27101
+ inputSchema: {
27102
+ type: "object",
27103
+ properties: { text: { type: "string", description: "The whole notepad as it should read from now on" } }
27104
+ },
27105
+ async run(input, ctx) {
27106
+ if (!ctx.session) return UNAVAILABLE3;
27107
+ if (typeof input.text !== "string") {
27108
+ const current = await ctx.session.readNotepad();
27109
+ return current && current.text.length > 0 ? current.text : "(your notes for this thread are empty)";
27110
+ }
27111
+ const bytes = utf8ByteLength(input.text);
27112
+ if (bytes > NOTEPAD_MAX_BYTES)
27113
+ return `your notes are ${bytes} bytes; the notepad holds at most ${NOTEPAD_MAX_BYTES} \u2014 shorten them and write again.`;
27114
+ const written = await ctx.session.writeNotepad(input.text);
27115
+ if (!written.ok) return `notes not saved (${written.reason}): another generation drives this run now.`;
27116
+ ctx.publish?.({ type: "notes", text: redactSecrets(input.text) });
27117
+ return `notes saved (${bytes} bytes); they ride your system prompt at the next run in this thread and reach you again after a compaction.`;
27118
+ }
27119
+ };
27120
+ SESSION_TOOLS = [recallTool, notesTool];
27121
+ }
27122
+ });
27123
+
26919
27124
  // ../../src/tools/workspace.ts
26920
27125
  import { z as z21 } from "zod";
26921
27126
  var bashTool, readFileTool, writeFileTool, NAME_STATUS_MARK, NAME_STATUS_MARK_LINE, OUTPUT_CUT_RE, diffDigestTool, submitVerdictTool, submitDispositionsTool, submitPrDescriptionTool, handoffEntry, submitHandoffTool, updateStatusTool, TOOLSETS;
@@ -26934,6 +27139,7 @@ var init_workspace = __esm({
26934
27139
  init_skills();
26935
27140
  init_attach();
26936
27141
  init_runs2();
27142
+ init_session();
26937
27143
  bashTool = {
26938
27144
  name: "bash",
26939
27145
  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 +27509,8 @@ ${raw.trim()}`;
27303
27509
  listSkillsTool,
27304
27510
  useSkillTool,
27305
27511
  ...GITHUB_READ_TOOLS,
27306
- ...GITHUB_ISSUE_WRITE_TOOLS
27512
+ ...GITHUB_ISSUE_WRITE_TOOLS,
27513
+ ...SESSION_TOOLS
27307
27514
  ],
27308
27515
  readonly: [
27309
27516
  bashTool,
@@ -27314,7 +27521,8 @@ ${raw.trim()}`;
27314
27521
  diffDigestTool,
27315
27522
  listSkillsTool,
27316
27523
  useSkillTool,
27317
- ...GITHUB_READ_TOOLS
27524
+ ...GITHUB_READ_TOOLS,
27525
+ ...SESSION_TOOLS
27318
27526
  ],
27319
27527
  web: [webFetchTool, webSearchTool, updateStatusTool, ...GITHUB_READ_TOOLS],
27320
27528
  /** The general agent: no workspace, no shell — GitHub reads + issue writes
@@ -27334,7 +27542,8 @@ ${raw.trim()}`;
27334
27542
  webSearchTool,
27335
27543
  listSkillsTool,
27336
27544
  useSkillTool,
27337
- ...GITHUB_READ_TOOLS
27545
+ ...GITHUB_READ_TOOLS,
27546
+ ...SESSION_TOOLS
27338
27547
  ],
27339
27548
  /** The conductor (docs/reference/specs/agent-conductor.md): the five run
27340
27549
  * tools — the only toolset that holds them, so no other preset can start,
@@ -28212,7 +28421,7 @@ Target repository: ${input.repo}. ` + (workspace ? `Your shell starts in the wor
28212
28421
  const agentSystem = (head) => {
28213
28422
  return [residentSystem ?? agent.system, targetBlock(head), input.contract, blocks.skills, blocks.mcp].filter((b) => Boolean(b)).join("\n\n");
28214
28423
  };
28215
- return (head) => [blocks.memory, blocks.config, blocks.about, blocks.instructions, agentSystem(head)].filter((part) => Boolean(part)).join("\n\n");
28424
+ return (head) => [blocks.memory, blocks.notes, blocks.config, blocks.about, blocks.instructions, agentSystem(head)].filter((part) => Boolean(part)).join("\n\n");
28216
28425
  }
28217
28426
  async function settleReviewedHead(input) {
28218
28427
  return input.span ? input.span.span("run.settle_reviewed_head", (span) => settle(input, span)) : settle(input, void 0);
@@ -28790,7 +28999,9 @@ function attachmentsLine(outcomes) {
28790
28999
  const parts = [];
28791
29000
  if (landed.length > 0) {
28792
29001
  parts.push(
28793
- `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map((o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type})`).join(", ")}`
29002
+ `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map(
29003
+ (o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type}${o.earlier ? ", from earlier in the thread" : ""})`
29004
+ ).join(", ")}`
28794
29005
  );
28795
29006
  }
28796
29007
  for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
@@ -28842,9 +29053,63 @@ async function pullStaged(outcomes, deps) {
28842
29053
  }
28843
29054
  return outcomes.map((o) => {
28844
29055
  const error = o.error ?? pulled.get(o.key);
28845
- return error === void 0 ? { file: o.file, basename: o.basename, key: o.key } : { ...o, error };
29056
+ if (error !== void 0) return { ...o, error };
29057
+ const { error: _dropped, ...landedOutcome } = o;
29058
+ return landedOutcome;
28846
29059
  });
28847
29060
  }
29061
+ function threadInboundArtifacts(runsOldestFirst) {
29062
+ const seen = /* @__PURE__ */ new Set();
29063
+ const out = [];
29064
+ for (const events of runsOldestFirst) {
29065
+ for (const e of events) {
29066
+ if (e.type !== "artifact" || e.direction !== "in" || seen.has(e.key)) continue;
29067
+ seen.add(e.key);
29068
+ out.push({ key: e.key, name: e.name, size: e.size, contentType: e.contentType });
29069
+ }
29070
+ }
29071
+ return out;
29072
+ }
29073
+ async function stageThreadArtifacts(artifacts, deps) {
29074
+ const outcomes = [];
29075
+ for (const a of artifacts) {
29076
+ const file = { name: a.name, size: a.size, type: a.contentType };
29077
+ let held;
29078
+ try {
29079
+ held = await deps.store.head(a.key) !== null;
29080
+ } catch (err2) {
29081
+ outcomes.push({
29082
+ file,
29083
+ basename: safeBasename(a.name),
29084
+ key: a.key,
29085
+ earlier: true,
29086
+ error: `the store could not be asked: ${describe4(err2)}`
29087
+ });
29088
+ continue;
29089
+ }
29090
+ if (!held) {
29091
+ outcomes.push({
29092
+ file,
29093
+ basename: safeBasename(a.name),
29094
+ key: a.key,
29095
+ earlier: true,
29096
+ error: "the store no longer holds it (its retention passed)"
29097
+ });
29098
+ continue;
29099
+ }
29100
+ const basename4 = stagedBasename(deps.nextIndex(), a.name);
29101
+ deps.publish?.({
29102
+ type: "artifact",
29103
+ direction: "in",
29104
+ key: a.key,
29105
+ name: a.name,
29106
+ size: a.size,
29107
+ contentType: a.contentType
29108
+ });
29109
+ outcomes.push({ file, basename: basename4, key: a.key, earlier: true });
29110
+ }
29111
+ return outcomes;
29112
+ }
28848
29113
  async function stageIntoWorkspace(files, deps) {
28849
29114
  const outcomes = await pullStaged(await copyStaged(files, deps), deps);
28850
29115
  return { line: attachmentsLine(outcomes), outcomes };
@@ -29055,13 +29320,13 @@ var init_frontmatter = __esm({
29055
29320
 
29056
29321
  // ../../src/skills/stores.ts
29057
29322
  import { existsSync as existsSync11, readFileSync as readFileSync13, readdirSync as readdirSync2 } from "node:fs";
29058
- import { join as join11 } from "node:path";
29323
+ import { join as join10 } from "node:path";
29059
29324
  function loadBundledSkills(dir) {
29060
29325
  if (!existsSync11(dir)) return [];
29061
29326
  const skills = [];
29062
29327
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
29063
29328
  if (!entry.isDirectory()) continue;
29064
- const file = join11(dir, entry.name, "SKILL.md");
29329
+ const file = join10(dir, entry.name, "SKILL.md");
29065
29330
  if (!existsSync11(file)) continue;
29066
29331
  try {
29067
29332
  skills.push(parseSkillMarkdown(readFileSync13(file, "utf8")));
@@ -29618,6 +29883,20 @@ function mintRunBearer(deps, ctx) {
29618
29883
  publish: (event) => registry.publish(runId3, event)
29619
29884
  });
29620
29885
  }
29886
+ function sessionNotesBlock(session) {
29887
+ const parts = [];
29888
+ const notepad = session.notepad?.trim();
29889
+ const summary = session.summary?.trim();
29890
+ if (notepad)
29891
+ parts.push(
29892
+ "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
29893
+ );
29894
+ if (summary)
29895
+ parts.push(
29896
+ "SUMMARY OF THE EARLIER CONVERSATION (written when the context was compacted; every earlier turn is still reachable with `recall`):\n" + summary
29897
+ );
29898
+ return parts.length ? parts.join("\n\n") : void 0;
29899
+ }
29621
29900
  async function composePrompt(deps, ctx) {
29622
29901
  const {
29623
29902
  msg,
@@ -29680,6 +29959,7 @@ async function composePrompt(deps, ctx) {
29680
29959
  );
29681
29960
  const instructionsBlock = customInstructionsBlock(scopes);
29682
29961
  const memoryBlock = await root.span("dispatch.compose", () => memoryBlockP);
29962
+ const notesBlock = ctx.session ? sessionNotesBlock(ctx.session) : void 0;
29683
29963
  const composeSystem = makeSystemComposer({
29684
29964
  agent,
29685
29965
  resident: resident === true,
@@ -29695,6 +29975,7 @@ async function composePrompt(deps, ctx) {
29695
29975
  ...ctx.contract !== void 0 ? { contract: ctx.contract } : {},
29696
29976
  blocks: {
29697
29977
  memory: memoryBlock,
29978
+ notes: notesBlock,
29698
29979
  config: configBlock,
29699
29980
  about: aboutBlock,
29700
29981
  instructions: instructionsBlock,
@@ -30872,6 +31153,14 @@ var init_transport = __esm({
30872
31153
  function relayedTools(tools) {
30873
31154
  return tools.filter((t) => !NATIVE_WORKSPACE_TOOLS.has(t.name));
30874
31155
  }
31156
+ function compactionSteer(notepad, opts = {}) {
31157
+ const notes = notepad?.trim();
31158
+ 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";
31159
+ if (opts.unavailable)
31160
+ return reach + "Your notes for this thread could not be read just now; `notes {}` reads them on demand, and `notes { text }` still writes them.";
31161
+ return reach + (notes ? `YOUR NOTES FOR THIS THREAD, as you last wrote them with \`notes\`:
31162
+ ${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.");
31163
+ }
30875
31164
  function splitSeed(seed) {
30876
31165
  for (let i = seed.length - 1; i >= 0; i--) {
30877
31166
  if (seed[i].role === "user") return { session: seed.slice(0, i), prompt: seed[i] };
@@ -31050,6 +31339,7 @@ async function runPiHarness(deps, run2) {
31050
31339
  writeUpAt = now();
31051
31340
  transport.send({ type: "steer", message: instruction });
31052
31341
  };
31342
+ let steers = Promise.resolve();
31053
31343
  const drainFollowUps = () => {
31054
31344
  const inputs = run2.inbox?.drain() ?? [];
31055
31345
  if (inputs.length === 0) return;
@@ -31067,7 +31357,26 @@ async function runPiHarness(deps, run2) {
31067
31357
  const images = inputs.flatMap(
31068
31358
  (i) => (i.images ?? []).map((img) => ({ type: "image", data: img.data, mimeType: img.mediaType }))
31069
31359
  );
31070
- transport.send({ type: "steer", message: followUpPrompt(inputs), ...images.length > 0 ? { images } : {} });
31360
+ steers = steers.then(async () => {
31361
+ let stagedLine = "";
31362
+ if (run2.stageFollowUps) {
31363
+ try {
31364
+ stagedLine = await run2.stageFollowUps(inputs);
31365
+ } catch (err2) {
31366
+ stagedLine = `Attached files could not be staged: ${err2 instanceof Error ? err2.message : String(err2)}`;
31367
+ note("follow_up", `staging the follow-up's files failed: ${redactSecrets(stagedLine)}`);
31368
+ }
31369
+ }
31370
+ if (settled) return;
31371
+ const prompt = followUpPrompt(inputs);
31372
+ transport.send({
31373
+ type: "steer",
31374
+ message: stagedLine ? `${prompt}
31375
+
31376
+ ${stagedLine}` : prompt,
31377
+ ...images.length > 0 ? { images } : {}
31378
+ });
31379
+ });
31071
31380
  };
31072
31381
  const check = () => {
31073
31382
  const requested = run2.control?.requested;
@@ -31150,7 +31459,23 @@ async function runPiHarness(deps, run2) {
31150
31459
  }
31151
31460
  }
31152
31461
  if (obs.message) await mirror.onMessage(obs.message, bridge.turns);
31153
- if (obs.compaction) await mirror.onCompaction(obs.compaction, bridge.turns);
31462
+ if (obs.compaction) {
31463
+ await mirror.onCompaction(obs.compaction, bridge.turns);
31464
+ if (!writeUp && !hardStopped && !catchingUp) {
31465
+ let notepad;
31466
+ let unavailable = false;
31467
+ try {
31468
+ notepad = run2.notepad ? (await run2.notepad())?.text : void 0;
31469
+ } catch (err2) {
31470
+ unavailable = true;
31471
+ note(
31472
+ "harness_error",
31473
+ `the notepad could not be read for the compaction steer (${err2 instanceof Error ? err2.message : String(err2)}); steered without it`
31474
+ );
31475
+ }
31476
+ transport.send({ type: "steer", message: compactionSteer(notepad, { unavailable }) });
31477
+ }
31478
+ }
31154
31479
  if (obs.providerError !== void 0) {
31155
31480
  if (catchingUp)
31156
31481
  note("harness_error", `a model call failed while the bot was away (${obs.providerError}); continuing`);
@@ -31738,6 +32063,7 @@ async function runLoop2(deps, ctx) {
31738
32063
  runs,
31739
32064
  steer,
31740
32065
  wait,
32066
+ session,
31741
32067
  parentRunId,
31742
32068
  coordinator,
31743
32069
  seed
@@ -31863,6 +32189,19 @@ async function runLoop2(deps, ctx) {
31863
32189
  const attachFile = io.attachFile?.bind(io);
31864
32190
  const uploadTicket = io.uploadTicket?.bind(io);
31865
32191
  const nextStagedIndex = ctx.stagingIndex ?? stagingIndex();
32192
+ const stageFollowUps = deps.artifacts ? async (inputs) => {
32193
+ const files = inputs.flatMap((i) => i.staged ?? []);
32194
+ if (files.length === 0) return "";
32195
+ const staged = await stageIntoWorkspace(files, {
32196
+ store: deps.artifacts,
32197
+ threadKey: msg.threadKey,
32198
+ publish: (e) => registry.publish(run2.id, e),
32199
+ nextIndex: nextStagedIndex,
32200
+ executor,
32201
+ resident: round2.selection.resident !== void 0
32202
+ });
32203
+ return staged.line;
32204
+ } : void 0;
31866
32205
  let artifactSeq = 0;
31867
32206
  const artifacts = deps.artifacts ? {
31868
32207
  store: deps.artifacts,
@@ -31885,6 +32224,7 @@ async function runLoop2(deps, ctx) {
31885
32224
  ...runs ? { runs } : {},
31886
32225
  ...steer ? { steer } : {},
31887
32226
  ...wait ? { wait } : {},
32227
+ ...session ? { session } : {},
31888
32228
  onVerdict,
31889
32229
  onDigest,
31890
32230
  onPrDescription,
@@ -31932,6 +32272,7 @@ async function runLoop2(deps, ctx) {
31932
32272
  messages,
31933
32273
  tools: relayedTools(mergeTools(TOOLSETS[agent.toolset] ?? [], mcpForRun?.tools)),
31934
32274
  toolContext,
32275
+ ...session ? { notepad: () => session.readNotepad() } : {},
31935
32276
  rules: {
31936
32277
  checkout: binding?.workspace ?? "/workspace",
31937
32278
  ...ownBranch !== void 0 ? { branch: ownBranch } : {},
@@ -31941,6 +32282,7 @@ async function runLoop2(deps, ctx) {
31941
32282
  span: root,
31942
32283
  control: run2.control,
31943
32284
  inbox: admitted.inbox,
32285
+ ...stageFollowUps ? { stageFollowUps } : {},
31944
32286
  onEvent,
31945
32287
  onProgress,
31946
32288
  ...ledgerRun ? {
@@ -31979,23 +32321,7 @@ async function runLoop2(deps, ctx) {
31979
32321
  // operator stop from /runs
31980
32322
  inbox: admitted.inbox,
31981
32323
  // thread follow-ups steered into this run (thread-admission item 2)
31982
- // A steered follow-up's staged files (record 0033): copied into the
31983
- // store and pulled into this workspace before the model reads the turn.
31984
- ...deps.artifacts ? {
31985
- stageFollowUps: async (inputs) => {
31986
- const files = inputs.flatMap((i) => i.staged ?? []);
31987
- if (files.length === 0) return "";
31988
- const staged = await stageIntoWorkspace(files, {
31989
- store: deps.artifacts,
31990
- threadKey: msg.threadKey,
31991
- publish: (e) => registry.publish(run2.id, e),
31992
- nextIndex: nextStagedIndex,
31993
- executor,
31994
- resident: round2.selection.resident !== void 0
31995
- });
31996
- return staged.line;
31997
- }
31998
- } : {},
32324
+ ...stageFollowUps ? { stageFollowUps } : {},
31999
32325
  // The step record before each step's tools (run-history item 35).
32000
32326
  ...ledgerRun ? { onStep: ledgerRun.step.bind(ledgerRun) } : {},
32001
32327
  // A resume re-enters the loop from the plan (run-history item 37).
@@ -33665,25 +33991,32 @@ var init_lineage = __esm({
33665
33991
  }
33666
33992
  });
33667
33993
 
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
33994
  // ../../src/core/dispatch/thread.ts
33995
+ async function readThreadArtifacts(service, thread, warn = (line3) => console.warn(line3)) {
33996
+ const perRun = [];
33997
+ for (const run2 of [...thread].reverse()) {
33998
+ const events = [];
33999
+ let afterSeq;
34000
+ try {
34001
+ for (; ; ) {
34002
+ const r = await service.getRunEvents(run2.id, afterSeq === void 0 ? {} : { afterSeq });
34003
+ if (!r.ok) {
34004
+ warn(`[thread] ${run2.id}: reading its received files stopped after ${events.length} event(s) \u2014 ${r.error}`);
34005
+ break;
34006
+ }
34007
+ events.push(...r.value.events);
34008
+ if (r.value.nextAfterSeq === void 0) break;
34009
+ afterSeq = r.value.nextAfterSeq;
34010
+ }
34011
+ } catch (err2) {
34012
+ warn(
34013
+ `[thread] ${run2.id}: reading its received files failed after ${events.length} event(s) \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`
34014
+ );
34015
+ }
34016
+ perRun.push(events);
34017
+ }
34018
+ return threadInboundArtifacts(perRun);
34019
+ }
33687
34020
  async function readThread(service, threadKey) {
33688
34021
  try {
33689
34022
  const page2 = await service.listRuns({
@@ -33718,6 +34051,7 @@ var THREAD_READ_LIMIT;
33718
34051
  var init_thread = __esm({
33719
34052
  "../../src/core/dispatch/thread.ts"() {
33720
34053
  "use strict";
34054
+ init_staging();
33721
34055
  THREAD_READ_LIMIT = 8;
33722
34056
  }
33723
34057
  });
@@ -33798,7 +34132,15 @@ async function sessionSeedFor(input) {
33798
34132
  history: input.history,
33799
34133
  request: input.request
33800
34134
  });
33801
- return seed ? { seed, notes: seed.notes } : { notes: [] };
34135
+ if (!seed) return { notes: [] };
34136
+ try {
34137
+ const notepad = await input.ledger.readNotepad(key);
34138
+ if (notepad && notepad.text.trim().length > 0) seed.notepad = notepad.text;
34139
+ } catch (err2) {
34140
+ const why = err2 instanceof Error ? err2.message : String(err2);
34141
+ seed.notes.push(`session seed: the notepad of ${key} could not be read (${why}) \u2014 the run starts without it`);
34142
+ }
34143
+ return { seed, notes: seed.notes };
33802
34144
  }
33803
34145
  function withoutThinking(message2) {
33804
34146
  if (message2.role !== "assistant") return message2;
@@ -33806,7 +34148,7 @@ function withoutThinking(message2) {
33806
34148
  if (content.length === message2.content.length) return message2;
33807
34149
  return { ...message2, content: content.length ? content : [{ type: "text", text: "(reasoning omitted)" }] };
33808
34150
  }
33809
- var SEED_BUDGET_TOKENS, SEED_BUDGET_BYTES, GAP_MARKER, settledResult;
34151
+ var SEED_BUDGET_TOKENS, SEED_BUDGET_BYTES, settledResult;
33810
34152
  var init_seed = __esm({
33811
34153
  "../../src/core/dispatch/seed.ts"() {
33812
34154
  "use strict";
@@ -33815,7 +34157,6 @@ var init_seed = __esm({
33815
34157
  init_thread();
33816
34158
  SEED_BUDGET_TOKENS = 6e4;
33817
34159
  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
34160
  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
34161
  }
33821
34162
  });
@@ -34189,6 +34530,13 @@ async function dispatch(deps, msg, io, opts = {}) {
34189
34530
  nextIndex: nextStagedIndex,
34190
34531
  publish: (e) => registry.publish(runId3, e)
34191
34532
  }) : void 0;
34533
+ const earlierStaged = !resume && deps.artifacts && agent.machine !== "none" && thread && thread.length > 0 ? readThreadArtifacts(runsService, thread).then(
34534
+ (found) => stageThreadArtifacts(found, {
34535
+ store: deps.artifacts,
34536
+ nextIndex: nextStagedIndex,
34537
+ publish: (e) => registry.publish(runId3, e)
34538
+ })
34539
+ ) : void 0;
34192
34540
  const reservation = await reserveRun(deps, {
34193
34541
  msg,
34194
34542
  agent,
@@ -34238,9 +34586,10 @@ async function dispatch(deps, msg, io, opts = {}) {
34238
34586
  });
34239
34587
  return ended;
34240
34588
  }
34241
- if (staged.length > 0) {
34242
- const line3 = stagedCopies ? attachmentsLine(
34243
- await pullStaged(await stagedCopies, {
34589
+ const earlier2 = earlierStaged ? await earlierStaged : [];
34590
+ if (staged.length > 0 || earlier2.length > 0) {
34591
+ const line3 = stagedCopies || earlier2.length > 0 ? attachmentsLine(
34592
+ await pullStaged([...stagedCopies ? await stagedCopies : [], ...earlier2], {
34244
34593
  store: deps.artifacts,
34245
34594
  executor,
34246
34595
  resident: resident !== void 0
@@ -34289,7 +34638,15 @@ async function dispatch(deps, msg, io, opts = {}) {
34289
34638
  verifiedAtAttach,
34290
34639
  resume,
34291
34640
  root,
34292
- ...contractBlock !== void 0 && agent.name === "review" ? { contract: contractBlock } : {}
34641
+ ...contractBlock !== void 0 && agent.name === "review" ? { contract: contractBlock } : {},
34642
+ // What the session already knows (session-log item 10): the notepad and
34643
+ // the newest compaction's summary the seed brought back, for the prompt.
34644
+ ...session && (session.notepad !== void 0 || session.summary !== void 0) ? {
34645
+ session: {
34646
+ ...session.notepad !== void 0 ? { notepad: session.notepad } : {},
34647
+ ...session.summary !== void 0 ? { summary: session.summary } : {}
34648
+ }
34649
+ } : {}
34293
34650
  });
34294
34651
  const { mcpForRun, composeSystem, system } = prompt;
34295
34652
  const reviewHead = prompt.reviewHead;
@@ -34347,6 +34704,7 @@ async function dispatch(deps, msg, io, opts = {}) {
34347
34704
  seed,
34348
34705
  ...session ? { seedLog: session.log } : {}
34349
34706
  });
34707
+ const sessionTools = sessionCapabilityFor(ledgerRun, deps.runLedger);
34350
34708
  const { spawn: spawn7, runs, steer, wait } = runToolCapabilities(
34351
34709
  { core: deps, dispatch, registry, clock: clock3 },
34352
34710
  {
@@ -34397,6 +34755,7 @@ async function dispatch(deps, msg, io, opts = {}) {
34397
34755
  runs,
34398
34756
  steer,
34399
34757
  wait,
34758
+ ...sessionTools ? { session: sessionTools } : {},
34400
34759
  parentRunId,
34401
34760
  coordinator,
34402
34761
  seed,
@@ -34527,6 +34886,7 @@ var init_dispatcher = __esm({
34527
34886
  init_settle();
34528
34887
  init_lineage();
34529
34888
  init_seed();
34889
+ init_session();
34530
34890
  init_thread();
34531
34891
  init_select();
34532
34892
  init_spawn();
@@ -34645,7 +35005,7 @@ var init_runHistoryWriter = __esm({
34645
35005
 
34646
35006
  // ../../src/core/runStore.ts
34647
35007
  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";
35008
+ import { join as join11, resolve as resolve9 } from "node:path";
34649
35009
  function isValidRunId(id) {
34650
35010
  return typeof id === "string" && RUN_ID_PATTERN.test(id);
34651
35011
  }
@@ -34693,7 +35053,7 @@ function buildRunStore(cfg, secrets, deps) {
34693
35053
  const now = deps.now ?? Date.now;
34694
35054
  const policy = retentionPolicyOf(cfg);
34695
35055
  if (cfg.store === "file") {
34696
- const store = new FileRunStore(join12(deps.dataDir, RUNS_DIR), { policy, now });
35056
+ const store = new FileRunStore(join11(deps.dataDir, RUNS_DIR), { policy, now });
34697
35057
  store.sweep();
34698
35058
  const timer = (deps.setInterval ?? setInterval)(() => store.sweep(), SWEEP_INTERVAL_MS);
34699
35059
  timer.unref?.();
@@ -34760,7 +35120,7 @@ var init_runStore = __esm({
34760
35120
  this.now = opts.now ?? Date.now;
34761
35121
  }
34762
35122
  recordPath(id) {
34763
- return join12(this.dir, `${id}.json`);
35123
+ return join11(this.dir, `${id}.json`);
34764
35124
  }
34765
35125
  ensureDir() {
34766
35126
  mkdirSync8(this.dir, { recursive: true, mode: DIR_MODE });
@@ -34782,7 +35142,7 @@ var init_runStore = __esm({
34782
35142
  }
34783
35143
  /** Every valid index line, a repeated id resolved to its last line. */
34784
35144
  readIndex() {
34785
- const path = join12(this.dir, INDEX_FILE);
35145
+ const path = join11(this.dir, INDEX_FILE);
34786
35146
  if (!existsSync12(path)) return [];
34787
35147
  const byId = /* @__PURE__ */ new Map();
34788
35148
  for (const line3 of readFileSync14(path, "utf8").split("\n")) {
@@ -34814,7 +35174,7 @@ var init_runStore = __esm({
34814
35174
  const keptIds = new Set(kept.map((r) => r.id));
34815
35175
  for (const item of index) if (!keptIds.has(item.id)) rmSync3(this.recordPath(item.id), { force: true });
34816
35176
  this.writeAtomic(
34817
- join12(this.dir, INDEX_FILE),
35177
+ join11(this.dir, INDEX_FILE),
34818
35178
  kept.map((r) => JSON.stringify(r)).join("\n") + (kept.length ? "\n" : "")
34819
35179
  );
34820
35180
  return kept;
@@ -35044,7 +35404,10 @@ function createLedgerWriteThrough(opts) {
35044
35404
  /** The run's place in its session log (session-log item 2); undefined for
35045
35405
  * a run without a conversation of its own and for an adopted row claimed
35046
35406
  * before the log existed. */
35047
- session;
35407
+ sessionRow;
35408
+ get session() {
35409
+ return this.sessionRow;
35410
+ }
35048
35411
  /** Turns of the run's conversation on the ledger, counted from its seed —
35049
35412
  * the last step record's `turnIndex` — so the finish can close the range. */
35050
35413
  turnsWritten;
@@ -35082,11 +35445,11 @@ function createLedgerWriteThrough(opts) {
35082
35445
  this.stepNo = from.stepNo;
35083
35446
  this.lastSeq = from.lastSeq;
35084
35447
  this.adopted = from.resumable === true;
35085
- this.session = from.session;
35448
+ this.sessionRow = from.session;
35086
35449
  }
35087
35450
  /** A reservation learns its session when the prompt's claim promotes it. */
35088
35451
  bindSession(session) {
35089
- this.session = session;
35452
+ this.sessionRow = session;
35090
35453
  }
35091
35454
  get resumable() {
35092
35455
  return !this.detached && (this.seeded || this.adopted);
@@ -35099,11 +35462,11 @@ function createLedgerWriteThrough(opts) {
35099
35462
  * ends short of what the model saw, and the next run in the session must
35100
35463
  * know. A run that wrote no turn leaves the range open. */
35101
35464
  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 } };
35465
+ if (!this.sessionRow) return void 0;
35466
+ if (this.broken) return { ...this.sessionRow, range: "broken" };
35467
+ const from = this.sessionRow.range === "broken" ? this.sessionRow.seedFrom : this.sessionRow.range.from;
35468
+ const to = this.turnsWritten !== void 0 && this.turnsWritten > 0 ? this.sessionRow.seedFrom + this.turnsWritten - 1 : void 0;
35469
+ return { ...this.sessionRow, range: to !== void 0 && to >= from ? { from, to } : { from } };
35107
35470
  }
35108
35471
  sink = {
35109
35472
  put: async (plain4) => {
@@ -35150,11 +35513,11 @@ function createLedgerWriteThrough(opts) {
35150
35513
  * no record at all was killed before its conversation was stored and
35151
35514
  * closes `interrupted`. */
35152
35515
  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;
35516
+ const base = this.sessionRow?.seedFrom ?? 0;
35517
+ const reused = this.sessionRow && this.sessionRow.range !== "broken" ? this.sessionRow.range.from - this.sessionRow.seedFrom : 0;
35155
35518
  const turns = messages.slice(reused).map((message2, i) => ({ idx: base + reused + i, message: message2 }));
35156
35519
  try {
35157
- const seeded = await ledger.seed(this.runId, gen, turns, this.session?.key);
35520
+ const seeded = await ledger.seed(this.runId, gen, turns, this.sessionRow?.key);
35158
35521
  if (!seeded.ok) {
35159
35522
  this.detach(`seed refused (${seeded.reason})`);
35160
35523
  return;
@@ -35174,7 +35537,7 @@ function createLedgerWriteThrough(opts) {
35174
35537
  iteration: 0
35175
35538
  },
35176
35539
  [],
35177
- this.session?.key
35540
+ this.sessionRow?.key
35178
35541
  );
35179
35542
  if (!recorded.ok) this.detach(`seed record refused (${recorded.reason})`);
35180
35543
  else this.seeded = true;
@@ -35184,7 +35547,7 @@ function createLedgerWriteThrough(opts) {
35184
35547
  }
35185
35548
  async step(report2) {
35186
35549
  if (this.detached) return;
35187
- const base = this.session?.seedFrom ?? 0;
35550
+ const base = this.sessionRow?.seedFrom ?? 0;
35188
35551
  const turns = report2.turns.map((message2, i) => ({ idx: base + report2.firstIdx + i, message: message2 }));
35189
35552
  if (report2.compaction)
35190
35553
  turns.push({ idx: base + report2.firstIdx + report2.turns.length, compaction: report2.compaction });
@@ -35200,7 +35563,7 @@ function createLedgerWriteThrough(opts) {
35200
35563
  };
35201
35564
  for (let attempt = 1; ; attempt++) {
35202
35565
  try {
35203
- const result = await ledger.step(this.runId, gen, record2, turns, this.session?.key);
35566
+ const result = await ledger.step(this.runId, gen, record2, turns, this.sessionRow?.key);
35204
35567
  if (!result.ok) this.detach(`step ${record2.step} refused (${result.reason})`);
35205
35568
  else this.turnsWritten = record2.turnIndex;
35206
35569
  return;
@@ -35386,6 +35749,10 @@ function createLedgerWriteThrough(opts) {
35386
35749
  }
35387
35750
  },
35388
35751
  readSessionTail: (key, maxBytes) => ledger.readSessionTail(key, maxBytes),
35752
+ readSession: (key, from, to) => ledger.readSession(key, from, to),
35753
+ searchSession: (key, query, limit) => ledger.searchSession(key, query, limit),
35754
+ readNotepad: (key) => ledger.readNotepad(key),
35755
+ writeNotepad: (key, text) => ledger.writeNotepad(key, gen, text),
35389
35756
  async handoff() {
35390
35757
  const candidates = [...live].filter((r) => r.resumable && r.tracked() && !r.handedOff);
35391
35758
  if (candidates.length === 0) return { marked: [] };
@@ -35439,6 +35806,18 @@ var init_writeThrough = __esm({
35439
35806
  async readSessionTail(_key, _maxBytes) {
35440
35807
  return { from: 0, transcript: { complete: true, turns: 0, messages: [], compactions: [] } };
35441
35808
  }
35809
+ async readSession(_key, _from, _to) {
35810
+ return { complete: true, turns: 0, messages: [], compactions: [] };
35811
+ }
35812
+ async searchSession(_key, _query, _limit) {
35813
+ return { hits: [], gaps: [] };
35814
+ }
35815
+ async readNotepad(_key) {
35816
+ return null;
35817
+ }
35818
+ async writeNotepad(_key, _text) {
35819
+ return { ok: false, reason: "unknown-run" };
35820
+ }
35442
35821
  async handoff() {
35443
35822
  return { marked: [] };
35444
35823
  }
@@ -35452,6 +35831,7 @@ var init_writeThrough = __esm({
35452
35831
  sink;
35453
35832
  resumable = false;
35454
35833
  handedOff = false;
35834
+ session = void 0;
35455
35835
  tracked() {
35456
35836
  return false;
35457
35837
  }
@@ -35931,7 +36311,10 @@ function httpMcpClientFactory(fetchImpl) {
35931
36311
  return (server) => new StreamableHttpMcpClient({
35932
36312
  url: server.url,
35933
36313
  fetch: fetchImpl,
35934
- ...server.auth?.type === "bearer" ? { headers: { authorization: `Bearer ${server.auth.token}` } } : {}
36314
+ headers: {
36315
+ ...server.headers,
36316
+ ...server.auth?.type === "bearer" ? { authorization: `Bearer ${server.auth.token}` } : {}
36317
+ }
35935
36318
  });
35936
36319
  }
35937
36320
  var init_mcp2 = __esm({
@@ -38760,18 +39143,18 @@ var init_liveView = __esm({
38760
39143
 
38761
39144
  // ../../src/channels/webAssets.ts
38762
39145
  import { readdirSync as readdirSync3, readFileSync as readFileSync16, statSync as statSync3 } from "node:fs";
38763
- import { join as join13 } from "node:path";
39146
+ import { join as join12 } from "node:path";
38764
39147
  function contentTypeFor2(path) {
38765
39148
  const dot = path.lastIndexOf(".");
38766
39149
  return (dot === -1 ? void 0 : CONTENT_TYPES[path.slice(dot)]) ?? "application/octet-stream";
38767
39150
  }
38768
39151
  function webDistDir(env, packageRoot) {
38769
- return env.SWITCHBOARD_WEB_DIST ?? join13(packageRoot, WEB_DIST_DIR);
39152
+ return env.SWITCHBOARD_WEB_DIST ?? join12(packageRoot, WEB_DIST_DIR);
38770
39153
  }
38771
39154
  function loadWebAssets(distDir) {
38772
39155
  let manifest;
38773
39156
  try {
38774
- manifest = JSON.parse(readFileSync16(join13(distDir, ".vite", "manifest.json"), "utf8"));
39157
+ manifest = JSON.parse(readFileSync16(join12(distDir, ".vite", "manifest.json"), "utf8"));
38775
39158
  } catch (err2) {
38776
39159
  throw new Error(
38777
39160
  `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 +39168,9 @@ function loadWebAssets(distDir) {
38785
39168
  css: (entryChunk.css ?? []).map((f) => `/${f}`)
38786
39169
  };
38787
39170
  const files = /* @__PURE__ */ new Map();
38788
- const assetsDir = join13(distDir, "assets");
39171
+ const assetsDir = join12(distDir, "assets");
38789
39172
  for (const name of readdirSync3(assetsDir)) {
38790
- const full = join13(assetsDir, name);
39173
+ const full = join12(assetsDir, name);
38791
39174
  if (statSync3(full).isFile()) files.set(`/assets/${name}`, readFileSync16(full));
38792
39175
  }
38793
39176
  const serve2 = (req, res) => {
@@ -39977,6 +40360,25 @@ var init_runLedgerWorker = __esm({
39977
40360
  )
39978
40361
  };
39979
40362
  }
40363
+ async searchSession(key, query, limit) {
40364
+ this.checkSessionKey(key);
40365
+ const r = await this.post("/runs/session/search", { key, query, limit });
40366
+ return {
40367
+ hits: Array.isArray(r.data.hits) ? r.data.hits : [],
40368
+ gaps: Array.isArray(r.data.gaps) ? r.data.gaps : []
40369
+ };
40370
+ }
40371
+ async readNotepad(key) {
40372
+ this.checkSessionKey(key);
40373
+ const r = await this.post("/runs/session/notepad", { key });
40374
+ const n2 = r.data.notepad;
40375
+ return n2 && typeof n2.text === "string" && typeof n2.updatedAt === "number" ? { text: n2.text, updatedAt: n2.updatedAt } : null;
40376
+ }
40377
+ async writeNotepad(key, gen, text) {
40378
+ this.checkSessionKey(key);
40379
+ if (!GEN_PATTERN.test(gen)) throw new PermanentStoreError(`run ledger: malformed generation`);
40380
+ return this.fenceResult(await this.post("/runs/session/notepad/write", { key, gen, text }));
40381
+ }
39980
40382
  async heartbeat(runId3, gen, leaseMs) {
39981
40383
  this.checkIds(runId3, gen);
39982
40384
  const r = await this.post("/runs/heartbeat", { storeKey: this.opts.storeKey, runId: runId3, gen, leaseMs });
@@ -42834,7 +43236,7 @@ __export(index_exports, {
42834
43236
  runBot: () => runBot
42835
43237
  });
42836
43238
  import { createServer } from "node:http";
42837
- import { join as join14 } from "node:path";
43239
+ import { join as join13 } from "node:path";
42838
43240
  async function runBot() {
42839
43241
  for (const v of ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]) {
42840
43242
  if (!processSecrets.get(v)) {
@@ -43512,7 +43914,7 @@ var init_index = __esm({
43512
43914
  init_secrets2();
43513
43915
  DATA_DIR = installationPath(OPERATOR_ROOT, "data");
43514
43916
  CONFIG_PATH2 = process.env.SWITCHBOARD_CONFIG ?? installationPath(OPERATOR_ROOT, "config/config.yaml");
43515
- OVERRIDES_PATH = process.env.SWITCHBOARD_OVERRIDES ?? join14(DATA_DIR, "overrides.json");
43917
+ OVERRIDES_PATH = process.env.SWITCHBOARD_OVERRIDES ?? join13(DATA_DIR, "overrides.json");
43516
43918
  PROCESS_STARTED_AT = systemClock();
43517
43919
  sampleProcessMetrics = startProcessMetrics();
43518
43920
  INTERRUPTED_WRITE_BUDGET_MS = 1e4;
@@ -43559,7 +43961,7 @@ init_secrets2();
43559
43961
  import { Console } from "node:console";
43560
43962
  import { existsSync as existsSync14, mkdtempSync, writeFileSync as writeFileSync9 } from "node:fs";
43561
43963
  import { tmpdir } from "node:os";
43562
- import { basename as basename3, join as join15 } from "node:path";
43964
+ import { basename as basename3, join as join14 } from "node:path";
43563
43965
  var CONFIG_PATH3 = process.env.SWITCHBOARD_CONFIG ?? installationPath(OPERATOR_ROOT, "config/config.yaml");
43564
43966
  var DATA_DIR2 = installationPath(OPERATOR_ROOT, "data");
43565
43967
  var CLI_CALLER = { kind: "cli", id: CLI_ACTOR.id, actor: CLI_ACTOR };
@@ -43733,8 +44135,8 @@ var ConsoleIO = class _ConsoleIO {
43733
44135
  /** The harness's file upload: the bytes written under the attachments dir,
43734
44136
  * the lead printed like a reply with the path a person can open. */
43735
44137
  async attachFile(file) {
43736
- this.attachments.dir ??= mkdtempSync(join15(tmpdir(), "switchboard-attachments-"));
43737
- const path = join15(this.attachments.dir, basename3(file.name));
44138
+ this.attachments.dir ??= mkdtempSync(join14(tmpdir(), "switchboard-attachments-"));
44139
+ const path = join14(this.attachments.dir, basename3(file.name));
43738
44140
  writeFileSync9(path, file.bytes);
43739
44141
  await this.reply(`${file.lead}
43740
44142
  \u{1F4CE} ${file.name} (${file.bytes.byteLength} bytes) \u2192 ${path}`);
@@ -43815,7 +44217,7 @@ function cliCapabilities(configPath, env, secrets, opts = {}) {
43815
44217
  }
43816
44218
  function wireCli() {
43817
44219
  const warn = (m) => console.error(m);
43818
- const botConfig = bindBotConfig(CONFIG_PATH3, join15(DATA_DIR2, "cli-overrides.json"));
44220
+ const botConfig = bindBotConfig(CONFIG_PATH3, join14(DATA_DIR2, "cli-overrides.json"));
43819
44221
  let loaded;
43820
44222
  const bot = () => loaded ??= botConfig().then((config) => ({
43821
44223
  config,