@coreplane/switchboard 1.213.0 → 1.215.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 (29) hide show
  1. package/dist/assets/config/config.example.yaml +7 -1
  2. package/dist/assets/deploy/cloudflare/artifactsCopy.ts +180 -0
  3. package/dist/assets/deploy/cloudflare/ensure-bucket.mjs +71 -0
  4. package/dist/assets/deploy/cloudflare/package.json +1 -1
  5. package/dist/assets/deploy/cloudflare/worker.ts +19 -1
  6. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +15 -0
  7. package/dist/assets/deploy/cloudflare-memory/worker.ts +568 -6
  8. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +11 -2
  9. package/dist/assets/deploy/profile.example.json +1 -1
  10. package/dist/assets/package-lock.json +3 -3
  11. package/dist/assets/package.json +1 -1
  12. package/dist/assets/source.json +3 -3
  13. package/dist/assets/src/agents/registry.ts +39 -14
  14. package/dist/assets/src/config/profile.ts +8 -5
  15. package/dist/assets/src/core/coordinator/contract.ts +6 -0
  16. package/dist/assets/src/core/runEvents.ts +18 -9
  17. package/dist/assets/src/core/runFriction.ts +9 -2
  18. package/dist/assets/src/core/runLedger/sessionLog.ts +207 -0
  19. package/dist/assets/src/core/runLedger/transcript.ts +187 -0
  20. package/dist/assets/src/core/runLedger/types.ts +25 -4
  21. package/dist/assets/src/core/runRecord.ts +62 -1
  22. package/dist/assets/src/core/ship/coordinator.ts +70 -33
  23. package/dist/assets/src/core/trace/workerTrace.ts +2 -0
  24. package/dist/assets/src/deploy/profile.ts +11 -0
  25. package/dist/assets/src/execution/residentInstanceId.ts +11 -3
  26. package/dist/assets/src/execution/residentText.ts +17 -2
  27. package/dist/assets/src/providers/types.ts +6 -0
  28. package/dist/cli.js +1689 -627
  29. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1404,19 +1404,19 @@ function clampBashTimeout(requested) {
1404
1404
  return Math.min(Math.max(Math.trunc(requested), BASH_TIMEOUT_MIN_MS), BASH_TIMEOUT_MAX_MS);
1405
1405
  }
1406
1406
  function bashBudgetWithinRun(wantedMs, remainingMs) {
1407
- const secs = (ms) => Math.max(0, Math.round(ms / 1e3));
1407
+ const secs2 = (ms) => Math.max(0, Math.round(ms / 1e3));
1408
1408
  const cap = Math.trunc(remainingMs - RUN_DEADLINE_RESERVE_MS);
1409
1409
  if (cap < BASH_TIMEOUT_MIN_MS) {
1410
1410
  return {
1411
1411
  kind: "exhausted",
1412
- note: `run budget exhausted \u2014 ${secs(remainingMs)}s of wall clock left, inside the ${secs(RUN_DEADLINE_RESERVE_MS)}s write-up reserve, so the command was not run; write up what you have now`
1412
+ note: `run budget exhausted \u2014 ${secs2(remainingMs)}s of wall clock left, inside the ${secs2(RUN_DEADLINE_RESERVE_MS)}s write-up reserve, so the command was not run; write up what you have now`
1413
1413
  };
1414
1414
  }
1415
1415
  if (cap >= wantedMs) return { kind: "unchanged" };
1416
1416
  return {
1417
1417
  kind: "clipped",
1418
1418
  timeoutMs: cap,
1419
- note: `[timeout clipped to ${secs(cap)}s \u2014 the run's wall clock ends in ${secs(remainingMs)}s]`
1419
+ note: `[timeout clipped to ${secs2(cap)}s \u2014 the run's wall clock ends in ${secs2(remainingMs)}s]`
1420
1420
  };
1421
1421
  }
1422
1422
  function bashTimeoutNote(timeoutMs) {
@@ -1758,6 +1758,31 @@ function runawayTurnCap(maxMinutes) {
1758
1758
  function loopBudget(maxMinutes) {
1759
1759
  return { maxMinutes, maxTurns: runawayTurnCap(maxMinutes) };
1760
1760
  }
1761
+ function conductorSystem(siblings) {
1762
+ const readers = siblings.filter((a) => a.identity !== "write");
1763
+ const writers = siblings.filter((a) => a.identity === "write");
1764
+ const rows = readers.map(
1765
+ (a) => `- \`${a.name}\`${machineNeedsRepo(a.machine) ? " (needs the repository)" : ""}: ${a.description}`
1766
+ );
1767
+ return `You are Switchboard's conductor: you coordinate other runs instead of doing the work yourself, answering a request from Slack.
1768
+
1769
+ You have no workspace and no shell. Your tools: \`spawn_run\` (start a child run), \`send_to_run\` (steer a live child: your text reaches it as a follow-up at its next step), \`await_runs\` (wait for your children to end and get each end \u2014 its status and final reply \u2014 back as data), \`list_runs\` (the runs you may see \u2014 your own children by default), \`get_run_status\` (one run: whether it is running, what it is doing, and its final reply once it finished), the GitHub reads \u2014 \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repositories), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\` \u2014 \`web_fetch\` (read a public URL), and \`update_status\`.
1770
+
1771
+ WHAT A CHILD IS. A child is an ordinary Switchboard run started as the person who asked you \u2014 exactly the run they could start by hand with \`agent:<preset>\` \u2014 in a thread of its own in this channel, visible to everyone there, with its own status card and run page, and under their permissions: a preset they may not run, a repository they may not use, or a profile a boundary caps is refused in the child's thread, and the refusal comes back to you as the tool result naming the gate. Children cannot spawn children. You may have a few live at once (the deployment's \`spawn.maxChildren\`, three by default); a spawn past the cap is refused until one finishes. A child's wall clock is capped by what is left of yours.
1772
+
1773
+ THE PRESETS a child can run \u2014 a child reads, so only a preset whose identity is \`none\` or \`read\`:
1774
+ ${rows.join("\n")}
1775
+
1776
+ A preset that writes \u2014 ${writers.map((a) => `\`${a.name}\``).join(", ")} \u2014 is refused by name (\`spawn_identity\`): a spawned child never holds a write credential, so pushing a branch or opening a pull request is the requester's to start by hand with \`agent:<preset>\`; say so in your answer instead of spawning it.
1777
+
1778
+ ROUTED COMPOUNDS. A request may arrive already split: the router found independent parts, and the message ends with the line "Routed as a compound request: N independent parts" followed by a numbered list, one part per line as \`<preset>\`: <text>. Spawn exactly those children \u2014 one \`spawn_run\` per line, the preset as listed, the line's text as the child's prompt (it already stands alone; add the repository where the preset needs one) \u2014 then \`await_runs\` them all and compile. Never merge, drop or add a part; a part whose spawn is refused is reported as refused, by the gate's name.
1779
+
1780
+ HOW TO WORK. Fan out, await, compile. Read the request and split it into children only where the parts are independent; a request one preset answers is one child. Spawn each child with a prompt that says what it should do, and the repository where the preset needs one: a child starts from this conversation's text so far \u2014 every user and assistant turn before your call, never your tool calls, their results or your thinking \u2014 and your prompt is its one new turn, so tell it what to do rather than repeat what was said. Then call \`await_runs\` once with every child's id: it returns when all of them have ended, or earlier \u2014 at the edge of your own budget, at a stop, or when a follow-up lands in this thread \u2014 and \`ended\` says which; a child still running at the cut keeps running (name it in your answer, or await again after a follow-up). Steer a child with \`send_to_run\` when the request changes or a child is heading the wrong way. A child that ended \u2014 finished, failed, interrupted by a restart \u2014 is reported as it ended and never restarted; spawn a new child if the work still matters. Then compile: one answer from the write-ups \`await_runs\` returned. Never do a child's job yourself, and never claim a child finished or found something you did not read from \`await_runs\` or \`get_run_status\`.
1781
+
1782
+ Maintain the user-facing status card with the update_status tool: one item per child (\u25CB pending, \u2731 running, \u2713 finished \u2014 only once await_runs or get_run_status said so).
1783
+
1784
+ Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack: lead with the outcome, then one line per child \u2014 its preset, its thread, its status and its result in a sentence \u2014 and what is still running, if anything.`;
1785
+ }
1761
1786
  function presetDoor(def) {
1762
1787
  if (def.routable !== false) return "routed";
1763
1788
  return def.name === COMPOUND_PRESET ? "compound" : "directive";
@@ -1769,7 +1794,7 @@ function getAgent(name) {
1769
1794
  }
1770
1795
  return a;
1771
1796
  }
1772
- 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, CONDUCTOR_SYSTEM, COMPOUND_PRESET, AGENTS;
1797
+ 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;
1773
1798
  var init_registry = __esm({
1774
1799
  "../../src/agents/registry.ts"() {
1775
1800
  "use strict";
@@ -1797,6 +1822,8 @@ EVERY PR includes one that already exists when you push \u2014 opened by a perso
1797
1822
  RESIDENT_TOOLCHAIN = `The resident image carries ${IMAGE_TOOLCHAIN}; plus yarn and bun \u2014 and no Docker.`;
1798
1823
  SHOW_FILES = `Files the person should SEE go through the attach_file tool: a screenshot from \`playwright screenshot\`, a rendered PDF, a recording \u2014 it posts the workspace file into this conversation, where an image renders inline. Use it whenever you produce an image worth showing (a visual change, a rendered page, a before/after); a link to a file on GitHub is not a picture.
1799
1824
  SCREENSHOTS GO TO BOTH PLACES, ALL OF THEM: when the request asks for screenshots, or the change is visual, every capture is attached here with attach_file AND published on the pull request \u2014 commit the images to an assets branch (never the PR's own diff) and reference them from the description's validation section or a PR comment so they render inline there too \u2014 unless the request names one destination. Never attach a subset and link the rest.
1825
+ 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.
1826
+ 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.
1800
1827
  Text stays in your message; do not attach what you can say.`;
1801
1828
  CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
1802
1829
 
@@ -1953,26 +1980,11 @@ READ-ONLY: NEVER open a pull request, and never commit or push \u2014 no branch,
1953
1980
  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.
1954
1981
 
1955
1982
  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.`;
1956
- CONDUCTOR_SYSTEM = `You are Switchboard's conductor: you coordinate other runs instead of doing the work yourself, answering a request from Slack.
1957
-
1958
- You have no workspace and no shell. Your tools: \`spawn_run\` (start a child run), \`send_to_run\` (steer a live child: your text reaches it as a follow-up at its next step), \`await_runs\` (wait for your children to end and get each end \u2014 its status and final reply \u2014 back as data), \`list_runs\` (the runs you may see \u2014 your own children by default), \`get_run_status\` (one run: whether it is running, what it is doing, and its final reply once it finished), the GitHub reads \u2014 \`github_repos\`, \`github_tree\` / \`github_file\` (browse and read our repositories), \`github_search_code\`, \`github_issue_list\` / \`github_issue_get\` \u2014 \`web_fetch\` (read a public URL), and \`update_status\`.
1959
-
1960
- WHAT A CHILD IS. A child is an ordinary Switchboard run started as the person who asked you \u2014 exactly the run they could start by hand with \`agent:<preset>\` \u2014 in a thread of its own in this channel, visible to everyone there, with its own status card and run page, and under their permissions: a preset they may not run, a repository they may not use, or a profile a boundary caps is refused in the child's thread, and the refusal comes back to you as the tool result naming the gate. Children cannot spawn children. You may have a few live at once (the deployment's \`spawn.maxChildren\`, three by default); a spawn past the cap is refused until one finishes. A child's wall clock is capped by what is left of yours.
1961
-
1962
- THE PRESETS a child can run: \`research\` (a question the web or our repositories answer), \`coding\` (implement a change and open a pull request; needs the repository), \`review\` (review a pull request; needs its URL), \`explore\` (a long, read-only investigation with a shell; needs the repository), \`general\` (a quick answer with the GitHub tools), \`ship\` (coding, review and fixes until a pull request is merge-ready; needs the repository).
1963
-
1964
- ROUTED COMPOUNDS. A request may arrive already split: the router found independent parts, and the message ends with the line "Routed as a compound request: N independent parts" followed by a numbered list, one part per line as \`<preset>\`: <text>. Spawn exactly those children \u2014 one \`spawn_run\` per line, the preset as listed, the line's text as the child's prompt (it already stands alone; add the repository where the preset needs one) \u2014 then \`await_runs\` them all and compile. Never merge, drop or add a part; a part whose spawn is refused is reported as refused, by the gate's name.
1965
-
1966
- HOW TO WORK. Fan out, await, compile. Read the request and split it into children only where the parts are independent; a request one preset answers is one child. Spawn each child with a self-contained prompt \u2014 everything it needs, since it sees none of this thread \u2014 and the repository where the preset needs one. Then call \`await_runs\` once with every child's id: it returns when all of them have ended, or earlier \u2014 at the edge of your own budget, at a stop, or when a follow-up lands in this thread \u2014 and \`ended\` says which; a child still running at the cut keeps running (name it in your answer, or await again after a follow-up). Steer a child with \`send_to_run\` when the request changes or a child is heading the wrong way. A child that ended \u2014 finished, failed, interrupted by a restart \u2014 is reported as it ended and never restarted; spawn a new child if the work still matters. Then compile: one answer from the write-ups \`await_runs\` returned. Never do a child's job yourself, and never claim a child finished or found something you did not read from \`await_runs\` or \`get_run_status\`.
1967
-
1968
- Maintain the user-facing status card with the update_status tool: one item per child (\u25CB pending, \u2731 running, \u2713 finished \u2014 only once await_runs or get_run_status said so).
1969
-
1970
- Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack: lead with the outcome, then one line per child \u2014 its preset, its thread, its status and its result in a sentence \u2014 and what is still running, if anything.`;
1971
1983
  COMPOUND_PRESET = "conductor";
1972
- AGENTS = {
1984
+ WORK_PRESETS = {
1973
1985
  general: {
1974
1986
  name: "general",
1975
- description: "Default assistant on the configured model: answers directly, reads the org's repos and manages their issues over GitHub, reads URLs. No workspace or shell.",
1987
+ description: "Default assistant: answers directly, reads URLs, manages issues; the preset for any question the org's GitHub answers. No workspace or shell.",
1976
1988
  system: GENERAL_SYSTEM,
1977
1989
  toolset: "assistant",
1978
1990
  // The GitHub tools are REST in the bot process, so a general ask never
@@ -2047,7 +2059,7 @@ Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks
2047
2059
  },
2048
2060
  research: {
2049
2061
  name: "research",
2050
- description: "Answers questions with web search, URL reading, and read access to the org's repos and issues over GitHub. No workspace.",
2062
+ description: "Answers questions that need the web (search, URL reading); GitHub for context, not for a question GitHub alone answers. No workspace.",
2051
2063
  system: RESEARCH_SYSTEM,
2052
2064
  toolset: "web",
2053
2065
  machine: "none",
@@ -2073,11 +2085,14 @@ Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks
2073
2085
  // would expire between them, so the 2× write buys reads for the whole run.
2074
2086
  cacheTtl: "1h"
2075
2087
  // No built-in effort: the deployment decides, as for coding.
2076
- },
2088
+ }
2089
+ };
2090
+ AGENTS = {
2091
+ ...WORK_PRESETS,
2077
2092
  conductor: {
2078
2093
  name: "conductor",
2079
2094
  description: "Coordinates other runs: spawns child runs as the requester \u2014 each in a thread of its own, under their permissions \u2014 follows them, and reports. No workspace or shell.",
2080
- system: CONDUCTOR_SYSTEM,
2095
+ system: conductorSystem(Object.values(WORK_PRESETS)),
2081
2096
  toolset: "conductor",
2082
2097
  // Nothing is provisioned and no credential minted: the run tools call the
2083
2098
  // dispatcher, the GitHub reads are REST in the bot process.
@@ -2821,6 +2836,8 @@ function validateRouting(routing, providers) {
2821
2836
  throw new Error(`config.yaml: routing.${key} is not a known key`);
2822
2837
  if (routing.auto !== void 0 && typeof routing.auto !== "boolean")
2823
2838
  throw new Error("config.yaml: routing.auto must be true or false");
2839
+ if (routing.answer !== void 0 && !ROUTE_ANSWER_MODES.includes(routing.answer))
2840
+ throw new Error(`config.yaml: routing.answer must be ${ROUTE_ANSWER_MODES.join(" or ")}`);
2824
2841
  if (routing.model !== void 0) {
2825
2842
  if (typeof routing.model !== "string" || !routing.model.includes("/"))
2826
2843
  throw new Error("config.yaml: routing.model must be a <provider>/<model> ref");
@@ -2875,8 +2892,11 @@ function validateRunHistory(rh) {
2875
2892
  if (v !== void 0 && (!Number.isInteger(v) || v < 1))
2876
2893
  throw new Error(`config.yaml: runHistory.${key} must be an integer >= 1`);
2877
2894
  }
2878
- if (rh.maxBytes !== void 0 && (!Number.isInteger(rh.maxBytes) || rh.maxBytes < 1))
2879
- throw new Error("config.yaml: runHistory.maxBytes must be an integer >= 1");
2895
+ for (const key of ["maxBytes", "sessionLogMaxBytes"]) {
2896
+ const v = rh[key];
2897
+ if (v !== void 0 && (!Number.isInteger(v) || v < 1))
2898
+ throw new Error(`config.yaml: runHistory.${key} must be an integer >= 1`);
2899
+ }
2880
2900
  if (rh.store !== void 0 && rh.store !== "worker" && rh.store !== "file")
2881
2901
  throw new Error('config.yaml: runHistory.store must be "worker" or "file"');
2882
2902
  if (rh.worker !== void 0) {
@@ -2917,7 +2937,7 @@ function validateRuntimeOverrides(ro) {
2917
2937
  }
2918
2938
  }
2919
2939
  }
2920
- var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, ROUTING_KEYS, SHIP_KEYS, SPAWN_KEYS;
2940
+ var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, ROUTE_ANSWER_MODES, ROUTING_KEYS, SHIP_KEYS, SPAWN_KEYS;
2921
2941
  var init_validate = __esm({
2922
2942
  "../../src/config/validate.ts"() {
2923
2943
  "use strict";
@@ -2966,7 +2986,8 @@ var init_validate = __esm({
2966
2986
  };
2967
2987
  BOUNDARY_KEYS = { maxMinutes: true, maxIdentity: true, machines: true };
2968
2988
  MIN_BOUNDARY_MINUTES = 2;
2969
- ROUTING_KEYS = { auto: true, model: true };
2989
+ ROUTE_ANSWER_MODES = ["tool", "text"];
2990
+ ROUTING_KEYS = { auto: true, model: true, answer: true };
2970
2991
  SHIP_KEYS = { maxRounds: true, maxMinutes: true };
2971
2992
  SPAWN_KEYS = { maxChildren: true };
2972
2993
  }
@@ -2987,7 +3008,7 @@ function identityWithin(identity2, cap) {
2987
3008
  return IDENTITY_ORDER[identity2] <= IDENTITY_ORDER[cap];
2988
3009
  }
2989
3010
  function budgetedAgent(agent, profile) {
2990
- return { ...agent, maxMinutes: profile.minutes };
3011
+ return { ...agent, maxMinutes: profile.minutes, maxTurns: runawayTurnCap(profile.minutes) };
2991
3012
  }
2992
3013
  function intersectBoundaries(layers) {
2993
3014
  const out = {};
@@ -3058,6 +3079,7 @@ var IDENTITY_ORDER, MACHINE_ORDER;
3058
3079
  var init_profile = __esm({
3059
3080
  "../../src/config/profile.ts"() {
3060
3081
  "use strict";
3082
+ init_registry();
3061
3083
  IDENTITY_ORDER = { none: 0, read: 1, write: 2 };
3062
3084
  MACHINE_ORDER = { none: 0, blank: 1, "repo-cold": 2, "repo-resident": 3 };
3063
3085
  }
@@ -4833,7 +4855,14 @@ var init_profile2 = __esm({
4833
4855
  images: z2.enum(IMAGE_MODES).default("build"),
4834
4856
  /** The Cloudflare Access application in front of the bot's dashboards, when
4835
4857
  * there is one: the team domain the JWT is issued by and the app's AUD. */
4836
- access: z2.object({ teamDomain: hostname, aud: z2.string().regex(/^[0-9a-f]{64}$/) }).optional()
4858
+ access: z2.object({ teamDomain: hostname, aud: z2.string().regex(/^[0-9a-f]{64}$/) }).optional(),
4859
+ /** The artifact store's bucket (docs/reference/specs/execution.md item 20), when the installation
4860
+ * has one: the bot Worker's template binds it as `ARTIFACTS` and `deploy` creates it before the
4861
+ * upload. The bot's runtime config (`artifacts.r2.bucket`) must name the same bucket — the two
4862
+ * are held equal by `artifacts check`, not by the profile. R2's own bucket-name rules. */
4863
+ artifacts: z2.object({
4864
+ bucket: z2.string().regex(/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/, "an R2 bucket name: 3\u201363 lowercase letters, digits and hyphens")
4865
+ }).optional()
4837
4866
  });
4838
4867
  EXAMPLE_ACCOUNT = "00000000000000000000000000000000";
4839
4868
  }
@@ -4906,7 +4935,11 @@ function planDeploy(opts, host, loaded, images) {
4906
4935
  setEnv: w.preflight ? { [w.preflight.baseUrlEnv]: w.baseUrl, ...opts.force ? { [w.preflight.forceEnv]: "1" } : {} } : {},
4907
4936
  ...opts.force && w.preflight ? { forcedBy: w.preflight.forceEnv } : {},
4908
4937
  requiredEnv: w.requiredEnv ?? [],
4909
- capabilities: w.capabilities ?? [],
4938
+ // The bot's `npm run deploy` creates the artifacts bucket before the upload
4939
+ // (deploy/cloudflare/ensure-bucket.mjs) when the profile names one — that
4940
+ // needs R2 on the credential, checked up front like every capability; an
4941
+ // installation without artifacts asks nothing extra of its token.
4942
+ capabilities: [...w.capabilities ?? [], ...w.name === "bot" && profile.artifacts ? [R2_CAPABILITY] : []],
4910
4943
  retryOnPreflightRefusal: !!w.preflight && !opts.force,
4911
4944
  ...w.waitMaxMs !== void 0 ? { waitMaxMs: w.waitMaxMs } : {},
4912
4945
  ...w.preflight?.healthUrl ? { healthUrl: w.preflight.healthUrl } : {},
@@ -5039,7 +5072,7 @@ function classifyDeployOutput(exitCode, output) {
5039
5072
  const firstError = lines.find((l) => isErrorish(l) && !/^npm ERR!/i.test(l)) ?? lines.find(isErrorish);
5040
5073
  return { kind: "failed", reason: firstError ?? `exit ${exitCode}` };
5041
5074
  }
5042
- var UNSET_ENV, SANDBOX_CONTAINER_CLASS, SANDBOX_BEARER_ENV, RESIDENT_BEARER_ENVS, RESIDENT_WAIT_MAX_MS, CONTAINERS_CAPABILITY, WORKER_DIRS, WORKER_SPECS, DEPLOY_ORDER, CONFIG_DOCUMENT_KEY, hasImage, ANSI_SEQUENCE;
5075
+ var UNSET_ENV, SANDBOX_CONTAINER_CLASS, SANDBOX_BEARER_ENV, R2_CAPABILITY, RESIDENT_BEARER_ENVS, RESIDENT_WAIT_MAX_MS, CONTAINERS_CAPABILITY, WORKER_DIRS, WORKER_SPECS, DEPLOY_ORDER, CONFIG_DOCUMENT_KEY, hasImage, ANSI_SEQUENCE;
5043
5076
  var init_plan = __esm({
5044
5077
  "../../src/deploy/plan.ts"() {
5045
5078
  "use strict";
@@ -5051,6 +5084,10 @@ var init_plan = __esm({
5051
5084
  UNSET_ENV = ["CLOUDFLARE_ACCOUNT_ID"];
5052
5085
  SANDBOX_CONTAINER_CLASS = "SwitchboardSandbox";
5053
5086
  SANDBOX_BEARER_ENV = "SANDBOX_TOKEN";
5087
+ R2_CAPABILITY = {
5088
+ command: ["wrangler", "r2", "bucket", "list"],
5089
+ needs: "Workers R2 Storage: Edit"
5090
+ };
5054
5091
  RESIDENT_BEARER_ENVS = ["RESIDENT_ADMIN_TOKEN", "RESIDENT_OPERATOR_TOKEN", "RESIDENT_READ_TOKEN"];
5055
5092
  RESIDENT_WAIT_MAX_MS = 30 * 6e4;
5056
5093
  CONTAINERS_CAPABILITY = {
@@ -5123,10 +5160,7 @@ var init_plan = __esm({
5123
5160
  // A container image like the bot's — checked here too, since an --affected
5124
5161
  // release can select the resident without the bot — plus the BACKUP_BUCKET
5125
5162
  // R2 binding wrangler validates on deploy.
5126
- capabilities: [
5127
- CONTAINERS_CAPABILITY,
5128
- { command: ["wrangler", "r2", "bucket", "list"], needs: "Workers R2 Storage: Edit" }
5129
- ],
5163
+ capabilities: [CONTAINERS_CAPABILITY, R2_CAPABILITY],
5130
5164
  waitMaxMs: RESIDENT_WAIT_MAX_MS,
5131
5165
  why: "per-repo DOs \u2014 preflight refuses while a resident has a run in flight or is provisioning (a refresh or restore mid-cycle only warns: it resumes after the swap)"
5132
5166
  },
@@ -5680,6 +5714,7 @@ function templateView(profile, kind, published) {
5680
5714
  ...hasImage2(kind) ? { image: containerImage(kind, profile, published) } : {},
5681
5715
  urls: { publicBaseUrl: urls.publicBaseUrl, stateWorkerUrl: urls.stateWorkerUrl },
5682
5716
  access: profile.access,
5717
+ artifacts: profile.artifacts,
5683
5718
  bot: { script: profile.workers.bot.script }
5684
5719
  };
5685
5720
  }
@@ -7811,7 +7846,8 @@ var init_runEvents = __esm({
7811
7846
  "review_not_posted",
7812
7847
  "compacted",
7813
7848
  "harness_error",
7814
- "tool_refused"
7849
+ "tool_refused",
7850
+ "stuck_loop"
7815
7851
  ];
7816
7852
  ACTOR_ID_CHARS = "A-Za-z0-9:@._-";
7817
7853
  ACTOR_ID_MAX = 128;
@@ -8108,6 +8144,13 @@ var init_host3 = __esm({
8108
8144
  });
8109
8145
 
8110
8146
  // ../../src/execution/binaryRead.ts
8147
+ function statCommandFor(resolvedPath) {
8148
+ return `stat -c %s -- ${resolvedPath}`;
8149
+ }
8150
+ function parseByteSize(stdout) {
8151
+ const m = /^\s*(\d{1,15})\s*$/.exec(stdout);
8152
+ return m ? Number(m[1]) : null;
8153
+ }
8111
8154
  function tooLargeMessage(path, bytes) {
8112
8155
  const size = bytes === void 0 ? "" : ` (${bytes} bytes)`;
8113
8156
  return `${path}${size} is over the ${MAX_READ_BYTES}-byte cap of a binary read`;
@@ -8731,9 +8774,9 @@ function sendDeadlineMs(budgetMs) {
8731
8774
  return budgetMs + EXEC_CALL_MARGIN_MS;
8732
8775
  }
8733
8776
  function noAnswerMessage(route3, budgetMs) {
8734
- const secs = (ms) => Math.round(ms / 1e3);
8777
+ const secs2 = (ms) => Math.round(ms / 1e3);
8735
8778
  const exec = route3 === "/exec";
8736
- return `sandbox worker ${route3} gave no answer within ${secs(sendDeadlineMs(budgetMs))}s (${exec ? "command budget" : "budget"} ${secs(budgetMs)}s + ${secs(EXEC_CALL_MARGIN_MS)}s margin) \u2014 the sandbox may be gone; ${exec ? "the command may still be running in it" : "the operation may still have run in it"}`;
8779
+ return `sandbox worker ${route3} gave no answer within ${secs2(sendDeadlineMs(budgetMs))}s (${exec ? "command budget" : "budget"} ${secs2(budgetMs)}s + ${secs2(EXEC_CALL_MARGIN_MS)}s margin) \u2014 the sandbox may be gone; ${exec ? "the command may still be running in it" : "the operation may still have run in it"}`;
8737
8780
  }
8738
8781
  function waitForSlot(ms, waitedMs, signal) {
8739
8782
  return new Promise((resolve12, reject) => {
@@ -8896,6 +8939,9 @@ ${parts}`);
8896
8939
  function residentText(text) {
8897
8940
  return redactAndCap(stripAnsi(text), RESIDENT_TEXT_CAP);
8898
8941
  }
8942
+ function residentErrorText(text) {
8943
+ return redactAndCap(stripAnsi(text), RESIDENT_ERROR_CAP);
8944
+ }
8899
8945
  function sanitizeResidentBody(data) {
8900
8946
  return walk(data, SANITIZE_DEPTH);
8901
8947
  }
@@ -8905,7 +8951,7 @@ function walk(value, depth) {
8905
8951
  const src = value;
8906
8952
  const out = {};
8907
8953
  for (const [key, v] of Object.entries(src)) {
8908
- out[key] = SANITIZED_FIELDS.includes(key) && typeof v === "string" ? residentText(v) : walk(v, depth - 1);
8954
+ out[key] = SANITIZED_FIELDS.includes(key) && typeof v === "string" ? key === "error" ? residentErrorText(v) : residentText(v) : walk(v, depth - 1);
8909
8955
  }
8910
8956
  if (typeof src.stderr === "string" && src.stderr === src.error && typeof out.error === "string") {
8911
8957
  out.stderr = out.error;
@@ -8915,12 +8961,13 @@ function walk(value, depth) {
8915
8961
  function residentState(value) {
8916
8962
  return typeof value === "string" && PROBE_STATES.has(value) ? value : "unknown";
8917
8963
  }
8918
- var RESIDENT_TEXT_CAP, SANITIZED_FIELDS, SANITIZE_DEPTH, PROBE_STATES;
8964
+ var RESIDENT_TEXT_CAP, RESIDENT_ERROR_CAP, SANITIZED_FIELDS, SANITIZE_DEPTH, PROBE_STATES;
8919
8965
  var init_residentText = __esm({
8920
8966
  "../../src/execution/residentText.ts"() {
8921
8967
  "use strict";
8922
8968
  init_redact();
8923
8969
  RESIDENT_TEXT_CAP = 300;
8970
+ RESIDENT_ERROR_CAP = 1e3;
8924
8971
  SANITIZED_FIELDS = ["error", "reason", "summary"];
8925
8972
  SANITIZE_DEPTH = 4;
8926
8973
  PROBE_STATES = /* @__PURE__ */ new Set([
@@ -10928,7 +10975,8 @@ function buildAnthropicParams(req) {
10928
10975
  ...req.system ? { system: [{ type: "text", text: req.system, cache_control: cache2 }] } : {},
10929
10976
  messages,
10930
10977
  ...effort2 ? { output_config: { effort: effort2 } } : {},
10931
- ...tools.length > 0 ? { tools } : {}
10978
+ ...tools.length > 0 ? { tools } : {},
10979
+ ...req.toolChoice ? { tool_choice: { type: "tool", name: req.toolChoice.name } } : {}
10932
10980
  };
10933
10981
  }
10934
10982
  function effortFor(model, effort2) {
@@ -12745,6 +12793,7 @@ function isCoordinatorUnit(v) {
12745
12793
  if (!isText(r.unit, 32) || !isText(r.slug) || !isText(r.branch) || !isOptionalText(r.title)) return false;
12746
12794
  if (!Array.isArray(r.dependsOn) || !r.dependsOn.every((d) => isText(d, 32))) return false;
12747
12795
  if (!isOptionalText(r.threadKey) || !isOptionalText(r.sourceUrl)) return false;
12796
+ if (r.reviewThread !== void 0 && !isThread(r.reviewThread)) return false;
12748
12797
  if (r.issue !== void 0 && !isFinite(r.issue)) return false;
12749
12798
  if (r.pr !== void 0 && !isPr(r.pr)) return false;
12750
12799
  if (r.resume !== void 0 && !isResume(r.resume)) return false;
@@ -12755,7 +12804,7 @@ function isCoordinatorUnit(v) {
12755
12804
  if (r.startedAt !== void 0 && !isFinite(r.startedAt)) return false;
12756
12805
  return true;
12757
12806
  }
12758
- var COORDINATOR_IDENTITY, COORDINATOR_STEP_ACTION, PLAN_MERGE_ACTION, COORDINATOR_STEP_PATH_PREFIX, INSTANCE_ID_PATTERN, STEP_NAME_PATTERN, IDEMPOTENCY_KEY_PATTERN, REPO_SLUG2, MAX_TEXT, MAX_REPORT, MAX_ROUNDS, isText, isOptionalText, isFinite, isObject2, isPr, isResume;
12807
+ var COORDINATOR_IDENTITY, COORDINATOR_STEP_ACTION, PLAN_MERGE_ACTION, COORDINATOR_STEP_PATH_PREFIX, INSTANCE_ID_PATTERN, STEP_NAME_PATTERN, IDEMPOTENCY_KEY_PATTERN, REPO_SLUG2, MAX_TEXT, MAX_REPORT, MAX_ROUNDS, isText, isOptionalText, isFinite, isObject2, isPr, isResume, isThread;
12759
12808
  var init_contract3 = __esm({
12760
12809
  "../../src/core/coordinator/contract.ts"() {
12761
12810
  "use strict";
@@ -12776,6 +12825,7 @@ var init_contract3 = __esm({
12776
12825
  isObject2 = (v) => typeof v === "object" && v !== null;
12777
12826
  isPr = (v) => isObject2(v) && isFinite(v.number) && isText(v.url, 2048);
12778
12827
  isResume = (v) => isObject2(v) && isFinite(v.pr) && (v.headSha === void 0 || isText(v.headSha)) && (v.url === void 0 || isText(v.url, 2048));
12828
+ isThread = (v) => isObject2(v) && isText(v.threadKey) && isOptionalText(v.sourceUrl);
12779
12829
  }
12780
12830
  });
12781
12831
 
@@ -13288,7 +13338,7 @@ function analyzeRunFriction(events, opts = {}) {
13288
13338
  }
13289
13339
  if (ev.type === "tool_call") {
13290
13340
  toolCalls++;
13291
- if (failedCalls.has(`${ev.tool} ${ev.summary}`)) {
13341
+ if (failedCalls.has(callSignature(ev.tool, ev.summary))) {
13292
13342
  findings.push({
13293
13343
  category: "retry",
13294
13344
  severity: "low",
@@ -13310,7 +13360,7 @@ function analyzeRunFriction(events, opts = {}) {
13310
13360
  const durationMs = span ? durationOfSpan(span) : void 0;
13311
13361
  const timed = (f) => durationMs !== void 0 ? { ...f, durationMs } : f;
13312
13362
  const interval = span && durationMs !== void 0 ? { interval: { start: span.startedAt, end: span.startedAt + durationMs } } : {};
13313
- if (!ev.ok) failedCalls.add(`${ev.tool} ${callSummary}`);
13363
+ if (!ev.ok) failedCalls.add(callSignature(ev.tool, callSummary));
13314
13364
  if (ev.infra) {
13315
13365
  findings.push(
13316
13366
  timed({
@@ -13503,7 +13553,7 @@ function formatFrictionReport(d) {
13503
13553
  }
13504
13554
  return lines.join("\n");
13505
13555
  }
13506
- var FRICTION_CATEGORIES, CATEGORY_LABEL, DENOMINATOR_OF, DEFAULT_SLOW_TOOL_MS, DEFAULT_SLOW_MODEL_TURN_MS, INSTALL_SEGMENT, QUOTED;
13556
+ var FRICTION_CATEGORIES, CATEGORY_LABEL, DENOMINATOR_OF, DEFAULT_SLOW_TOOL_MS, DEFAULT_SLOW_MODEL_TURN_MS, INSTALL_SEGMENT, QUOTED, callSignature;
13507
13557
  var init_runFriction = __esm({
13508
13558
  "../../src/core/runFriction.ts"() {
13509
13559
  "use strict";
@@ -13546,10 +13596,22 @@ var init_runFriction = __esm({
13546
13596
  DEFAULT_SLOW_MODEL_TURN_MS = 6e4;
13547
13597
  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|$))/;
13548
13598
  QUOTED = /"(?:[^"\\]|\\.)*"|'[^']*'/g;
13599
+ callSignature = (tool, summary) => `${tool} ${summary}`;
13549
13600
  }
13550
13601
  });
13551
13602
 
13552
13603
  // ../../src/core/runRecord.ts
13604
+ function isRunSession(v) {
13605
+ if (typeof v !== "object" || v === null) return false;
13606
+ const s = v;
13607
+ if (typeof s.key !== "string" || !SESSION_KEY_PATTERN.test(s.key)) return false;
13608
+ if (!isIndex(s.seedFrom) || !isIndex(s.request) || s.request < s.seedFrom) return false;
13609
+ if (s.range === "broken") return true;
13610
+ if (typeof s.range !== "object" || s.range === null) return false;
13611
+ const range = s.range;
13612
+ if (!isIndex(range.from) || range.from < s.seedFrom) return false;
13613
+ return range.to === void 0 || isIndex(range.to) && range.to >= range.from;
13614
+ }
13553
13615
  function isRunProfileRecord(v) {
13554
13616
  if (typeof v !== "object" || v === null) return false;
13555
13617
  const p = v;
@@ -13628,7 +13690,12 @@ function clampRetentionPolicy(partial) {
13628
13690
  const [lo, hi] = RETENTION_BOUNDS[k];
13629
13691
  return Math.min(hi, Math.max(lo, n2));
13630
13692
  };
13631
- return { retentionDays: field("retentionDays"), maxRuns: field("maxRuns"), maxBytes: field("maxBytes") };
13693
+ return {
13694
+ retentionDays: field("retentionDays"),
13695
+ maxRuns: field("maxRuns"),
13696
+ maxBytes: field("maxBytes"),
13697
+ sessionLogMaxBytes: field("sessionLogMaxBytes")
13698
+ };
13632
13699
  }
13633
13700
  function isOptionalString(v) {
13634
13701
  return v === void 0 || typeof v === "string";
@@ -13694,6 +13761,8 @@ function isRunRecord(v) {
13694
13761
  if (r.profile !== void 0 && !isRunProfileRecord(r.profile)) return false;
13695
13762
  if (r.parentRunId !== void 0 && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
13696
13763
  return false;
13764
+ if (r.seed !== void 0 && !RUN_SEEDS.includes(r.seed)) return false;
13765
+ if (r.session !== void 0 && !isRunSession(r.session)) return false;
13697
13766
  if (r.parentInstanceId === void 0 !== (r.idempotencyKey === void 0)) return false;
13698
13767
  if (r.parentInstanceId !== void 0 && (typeof r.parentInstanceId !== "string" || !INSTANCE_ID_PATTERN.test(r.parentInstanceId)))
13699
13768
  return false;
@@ -13825,7 +13894,7 @@ function dropSpanPairsFromTheMiddle(events, eventBudget) {
13825
13894
  }
13826
13895
  return events.filter((_, k) => !dropped.has(k));
13827
13896
  }
13828
- var RUN_STATUSES, RUN_ID_PATTERN, REVIEW_HEAD_PATTERN, MACHINE_CLASSES_IN_RECORD, IDENTITIES_IN_RECORD, BOUNDARY_SCOPES_IN_RECORD, RUN_LIST_DEFAULT_LIMIT, RUN_LIST_MAX_LIMIT, RUN_EVENTS_DEFAULT_PAGE, RUN_EVENTS_MAX_PAGE, CHANNEL_VISIBILITIES2, KIB, MIB, GIB2, DEFAULT_RETENTION_POLICY, RETENTION_BOUNDS, MAX_RECORD_BYTES, MAX_EVENT_BYTES, ELLIPSIS, encoder;
13897
+ var RUN_STATUSES, RUN_ID_PATTERN, REVIEW_HEAD_PATTERN, RUN_SEEDS, SESSION_KEY_PATTERN, isIndex, MACHINE_CLASSES_IN_RECORD, IDENTITIES_IN_RECORD, BOUNDARY_SCOPES_IN_RECORD, RUN_LIST_DEFAULT_LIMIT, RUN_LIST_MAX_LIMIT, RUN_EVENTS_DEFAULT_PAGE, RUN_EVENTS_MAX_PAGE, CHANNEL_VISIBILITIES2, KIB, MIB, GIB2, DEFAULT_RETENTION_POLICY, RETENTION_BOUNDS, MAX_RECORD_BYTES, MAX_EVENT_BYTES, ELLIPSIS, encoder;
13829
13898
  var init_runRecord = __esm({
13830
13899
  "../../src/core/runRecord.ts"() {
13831
13900
  "use strict";
@@ -13837,6 +13906,9 @@ var init_runRecord = __esm({
13837
13906
  RUN_STATUSES = ["completed", "stopped_soft", "stopped_hard", "failed", "interrupted"];
13838
13907
  RUN_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
13839
13908
  REVIEW_HEAD_PATTERN = /^[0-9a-f]{7,40}$/;
13909
+ RUN_SEEDS = ["channel", "parent"];
13910
+ SESSION_KEY_PATTERN = /^[A-Za-z0-9_.:@+/-]{1,512}$/;
13911
+ isIndex = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
13840
13912
  MACHINE_CLASSES_IN_RECORD = {
13841
13913
  none: true,
13842
13914
  blank: true,
@@ -13862,12 +13934,14 @@ var init_runRecord = __esm({
13862
13934
  DEFAULT_RETENTION_POLICY = {
13863
13935
  retentionDays: 30,
13864
13936
  maxRuns: 5e3,
13865
- maxBytes: 2 * GIB2
13937
+ maxBytes: 2 * GIB2,
13938
+ sessionLogMaxBytes: 200 * MIB
13866
13939
  };
13867
13940
  RETENTION_BOUNDS = {
13868
13941
  retentionDays: [1, 365],
13869
13942
  maxRuns: [1, 2e4],
13870
- maxBytes: [16 * MIB, 8 * GIB2]
13943
+ maxBytes: [16 * MIB, 8 * GIB2],
13944
+ sessionLogMaxBytes: [16 * MIB, 2 * GIB2]
13871
13945
  };
13872
13946
  MAX_RECORD_BYTES = 1.5 * MIB;
13873
13947
  MAX_EVENT_BYTES = 64 * KIB;
@@ -19896,7 +19970,7 @@ function createRunsService(deps) {
19896
19970
  const analyze = deps.analyze ?? ((events, opts) => analyzeRunFriction(events, opts));
19897
19971
  const clock3 = deps.clock ?? systemClock;
19898
19972
  const warn = deps.warn ?? ((m) => console.warn(m));
19899
- const describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
19973
+ const describe11 = (err2) => err2 instanceof Error ? err2.message : String(err2);
19900
19974
  let listing;
19901
19975
  const liveRows = () => {
19902
19976
  if (!ledger) return null;
@@ -19916,7 +19990,7 @@ function createRunsService(deps) {
19916
19990
  try {
19917
19991
  rows = await pending2;
19918
19992
  } catch (err2) {
19919
- warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe9(err2)}`);
19993
+ warn(`[runs] run ledger list failed \u2014 showing this process's runs only: ${describe11(err2)}`);
19920
19994
  return [];
19921
19995
  }
19922
19996
  const foreign = rows.filter((row) => !registry.getById(row.runId));
@@ -19926,7 +20000,7 @@ function createRunsService(deps) {
19926
20000
  try {
19927
20001
  events = await ledger.readEvents(row.runId);
19928
20002
  } catch (err2) {
19929
- warn(`[runs] run ledger events read failed for ${row.runId}: ${describe9(err2)}`);
20003
+ warn(`[runs] run ledger events read failed for ${row.runId}: ${describe11(err2)}`);
19930
20004
  }
19931
20005
  return ledgerView(row, events);
19932
20006
  })
@@ -19940,7 +20014,7 @@ function createRunsService(deps) {
19940
20014
  if (!row) return null;
19941
20015
  return { row, events: await ledger.readEvents(id) };
19942
20016
  } catch (err2) {
19943
- warn(`[runs] run ledger read failed for ${id}: ${describe9(err2)}`);
20017
+ warn(`[runs] run ledger read failed for ${id}: ${describe11(err2)}`);
19944
20018
  return null;
19945
20019
  }
19946
20020
  };
@@ -19958,7 +20032,7 @@ function createRunsService(deps) {
19958
20032
  row = await storeSummary(id);
19959
20033
  } catch (err2) {
19960
20034
  warn(
19961
- `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe9(err2)}`
20035
+ `[runs] history store read failed for ${id} \u2014 serving the registry row without its record: ${describe11(err2)}`
19962
20036
  );
19963
20037
  return {};
19964
20038
  }
@@ -20127,7 +20201,7 @@ function createRunsService(deps) {
20127
20201
  const r = await ledger.requestStop(id, mode);
20128
20202
  if (r.ok) return { ok: true, value: { id, mode, state: "stopping" } };
20129
20203
  } catch (err2) {
20130
- warn(`[runs] run ledger stop failed for ${id}: ${describe9(err2)}`);
20204
+ warn(`[runs] run ledger stop failed for ${id}: ${describe11(err2)}`);
20131
20205
  }
20132
20206
  }
20133
20207
  return await storeSummary(id) ? conflict : notFound;
@@ -21502,6 +21576,11 @@ function liveViewLink(id, token2) {
21502
21576
  if (!base) return void 0;
21503
21577
  return `${base.replace(/\/+$/, "")}/runs/${encodeURIComponent(id)}?t=${encodeURIComponent(token2)}`;
21504
21578
  }
21579
+ function runPageLink(id) {
21580
+ const base = process.env.PUBLIC_BASE_URL?.trim();
21581
+ if (!base) return void 0;
21582
+ return `${base.replace(/\/+$/, "")}/runs/${encodeURIComponent(id)}`;
21583
+ }
21505
21584
  function stripPlatformPrefix(id) {
21506
21585
  const i = id.indexOf(":");
21507
21586
  return i === -1 ? id : id.slice(i + 1);
@@ -21748,7 +21827,11 @@ function durableInboxMessage(msg, text, at, from) {
21748
21827
  ...msg.userName !== void 0 ? { userName: msg.userName } : {},
21749
21828
  ...msg.sourceUrl !== void 0 ? { sourceUrl: msg.sourceUrl } : {},
21750
21829
  ...msg.channelName !== void 0 ? { channelName: msg.channelName } : {},
21751
- ...from !== void 0 ? { fromRunId: from.runId } : {}
21830
+ ...from !== void 0 ? { fromRunId: from.runId } : {},
21831
+ // Staged references are metadata (record 0033) — a few hundred bytes each —
21832
+ // so they ride the base row and survive the restart whatever the
21833
+ // attachments below do.
21834
+ ...msg.staged && msg.staged.length > 0 ? { staged: msg.staged.map(stagedRow) } : {}
21752
21835
  };
21753
21836
  const images = msg.images ?? [];
21754
21837
  const documents = msg.documents ?? [];
@@ -21761,6 +21844,17 @@ function durableInboxMessage(msg, text, at, from) {
21761
21844
  if (Buffer.byteLength(JSON.stringify(withAttachments), "utf8") <= DURABLE_INBOX_MAX_BYTES) return withAttachments;
21762
21845
  return { ...base, attachmentsDropped: { images: images.length, documents: documents.length } };
21763
21846
  }
21847
+ function stagedFromInbox(v) {
21848
+ if (!Array.isArray(v)) return void 0;
21849
+ const out = v.flatMap((e) => {
21850
+ if (typeof e !== "object" || e === null) return [];
21851
+ const r = e;
21852
+ if (typeof r.name !== "string" || typeof r.size !== "number" || typeof r.type !== "string" || typeof r.url !== "string" || typeof r.messageId !== "string")
21853
+ return [];
21854
+ return [{ name: r.name, size: r.size, type: r.type, url: r.url, messageId: r.messageId }];
21855
+ });
21856
+ return out.length > 0 ? out : void 0;
21857
+ }
21764
21858
  function messageFromInbox(stored, fallbackAt) {
21765
21859
  const m = stored;
21766
21860
  const str7 = (k) => typeof m[k] === "string" ? m[k] : void 0;
@@ -21777,6 +21871,7 @@ function messageFromInbox(stored, fallbackAt) {
21777
21871
  const at = typeof m.at === "number" && Number.isFinite(m.at) ? m.at : fallbackAt;
21778
21872
  const images = attachmentsFromInbox(m.images);
21779
21873
  const documents = attachmentsFromInbox(m.documents);
21874
+ const staged = stagedFromInbox(m.staged);
21780
21875
  const note = droppedNote(m.attachmentsDropped);
21781
21876
  const msg = {
21782
21877
  channelId,
@@ -21789,7 +21884,8 @@ ${note}` : text,
21789
21884
  ...sourceUrl !== void 0 ? { sourceUrl } : {},
21790
21885
  ...channelName !== void 0 ? { channelName } : {},
21791
21886
  ...images ? { images } : {},
21792
- ...documents ? { documents } : {}
21887
+ ...documents ? { documents } : {},
21888
+ ...staged ? { staged } : {}
21793
21889
  };
21794
21890
  return { msg, at, ...fromRunId !== void 0 ? { from: { runId: fromRunId } } : {} };
21795
21891
  }
@@ -21810,7 +21906,7 @@ function droppedNote(dropped) {
21810
21906
  if (n2 <= 0) return void 0;
21811
21907
  return `(${n2} attachment${n2 === 1 ? "" : "s"} from this reply could not be carried across the bot's restart and ${n2 === 1 ? "is" : "are"} not attached.)`;
21812
21908
  }
21813
- var DURABLE_INBOX_MAX_BYTES, attachmentRow;
21909
+ var DURABLE_INBOX_MAX_BYTES, attachmentRow, stagedRow;
21814
21910
  var init_inboxMessage = __esm({
21815
21911
  "../../src/core/runLedger/inboxMessage.ts"() {
21816
21912
  "use strict";
@@ -21820,6 +21916,7 @@ var init_inboxMessage = __esm({
21820
21916
  data: a.data,
21821
21917
  ...a.name !== void 0 ? { name: a.name } : {}
21822
21918
  });
21919
+ stagedRow = (s) => ({ name: s.name, size: s.size, type: s.type, url: s.url, messageId: s.messageId });
21823
21920
  }
21824
21921
  });
21825
21922
 
@@ -21851,13 +21948,15 @@ function mergeFollowUps(inputs) {
21851
21948
  const last = inputs[inputs.length - 1];
21852
21949
  const images = inputs.flatMap((i) => i.images ?? []);
21853
21950
  const documents = inputs.flatMap((i) => i.documents ?? []);
21951
+ const staged = inputs.flatMap((i) => i.staged ?? []);
21854
21952
  return {
21855
21953
  text: inputs.map((i) => i.text).join("\n\n"),
21856
21954
  userId: last.userId,
21857
21955
  ...last.userName !== void 0 ? { userName: last.userName } : {},
21858
21956
  ...last.sourceUrl !== void 0 ? { sourceUrl: last.sourceUrl } : {},
21859
21957
  ...images.length > 0 ? { images } : {},
21860
- ...documents.length > 0 ? { documents } : {}
21958
+ ...documents.length > 0 ? { documents } : {},
21959
+ ...staged.length > 0 ? { staged } : {}
21861
21960
  };
21862
21961
  }
21863
21962
  var FollowUpInbox, ThreadAdmission, elapsed, linkSuffix, followUpHeader, supersededHeader;
@@ -21969,6 +22068,7 @@ function interruptedRunRecord(summary, snap, finishedAt) {
21969
22068
  channelVisibility: summary.channelVisibility ?? "unknown",
21970
22069
  repo: summary.repo,
21971
22070
  ...summary.parentRunId !== void 0 ? { parentRunId: summary.parentRunId } : {},
22071
+ ...summary.seed !== void 0 ? { seed: summary.seed } : {},
21972
22072
  ...summary.parentInstanceId !== void 0 && summary.idempotencyKey !== void 0 ? { coordinator: { parentInstanceId: summary.parentInstanceId, idempotencyKey: summary.idempotencyKey } } : {},
21973
22073
  finishedAt,
21974
22074
  status: "interrupted",
@@ -22004,6 +22104,10 @@ function reclaimedRunRecord(input) {
22004
22104
  // included); a row from before profiles existed leaves the record without one.
22005
22105
  ...row.meta.profile && row.meta.agent ? { profile: { preset: row.meta.agent, ...row.meta.profile } } : {},
22006
22106
  ...row.meta.parentRunId !== void 0 ? { parentRunId: row.meta.parentRunId } : {},
22107
+ ...row.meta.seed !== void 0 ? { seed: row.meta.seed } : {},
22108
+ // The row's place in its session log (item 53): the range stays open on a
22109
+ // record closed here, since the closer has no turn count to end it with.
22110
+ ...row.meta.session !== void 0 ? { session: row.meta.session } : {},
22007
22111
  // A coordinator's child keeps its instance and key on the close (item 48),
22008
22112
  // so the state Worker's finish still sends the parent its event.
22009
22113
  ...row.meta.parentInstanceId !== void 0 && row.meta.idempotencyKey !== void 0 ? { coordinator: { parentInstanceId: row.meta.parentInstanceId, idempotencyKey: row.meta.idempotencyKey } } : {},
@@ -22072,7 +22176,9 @@ function assembleRunRecord(input) {
22072
22176
  ...input.reviewPost !== void 0 ? { reviewPost: redactReviewPost(input.reviewPost) } : {},
22073
22177
  ...input.profile !== void 0 ? { profile: input.profile } : {},
22074
22178
  ...input.parentRunId !== void 0 ? { parentRunId: input.parentRunId } : {},
22075
- ...coordinatorFields(input.coordinator)
22179
+ ...coordinatorFields(input.coordinator),
22180
+ ...input.seed !== void 0 ? { seed: input.seed } : {},
22181
+ ...input.session !== void 0 ? { session: input.session } : {}
22076
22182
  });
22077
22183
  return fitted.eventCount !== fitted.storedEventCount ? { ...fitted, truncated: true } : fitted;
22078
22184
  }
@@ -22080,7 +22186,20 @@ function profileRecordOf(agent, profile) {
22080
22186
  return { preset: agent.name, ...profile };
22081
22187
  }
22082
22188
  function writeTombstone(deps, ctx) {
22083
- const { msg, agent, profile, resolved, repoCtx, channelVisibility, run: run2, registry, resume, parentRunId, coordinator } = ctx;
22189
+ const {
22190
+ msg,
22191
+ agent,
22192
+ profile,
22193
+ resolved,
22194
+ repoCtx,
22195
+ channelVisibility,
22196
+ run: run2,
22197
+ registry,
22198
+ resume,
22199
+ parentRunId,
22200
+ coordinator,
22201
+ seed
22202
+ } = ctx;
22084
22203
  if (!resume) {
22085
22204
  const startSnap = registry.snapshot(run2.id, run2.token);
22086
22205
  if (startSnap) {
@@ -22096,6 +22215,7 @@ function writeTombstone(deps, ctx) {
22096
22215
  profile: profileRecordOf(agent, profile),
22097
22216
  ...parentRunId !== void 0 ? { parentRunId } : {},
22098
22217
  ...coordinator !== void 0 ? { coordinator } : {},
22218
+ ...seed !== void 0 ? { seed } : {},
22099
22219
  finishedAt: startSnap.startedAt,
22100
22220
  status: "interrupted",
22101
22221
  diagnosis: analyzeRunFriction(startSnap.events, {
@@ -22130,7 +22250,8 @@ function registerFinishRecord(deps, ctx) {
22130
22250
  dispositions,
22131
22251
  reviewPost,
22132
22252
  parentRunId,
22133
- coordinator
22253
+ coordinator,
22254
+ seed
22134
22255
  } = ctx;
22135
22256
  ending.register({
22136
22257
  runId: run2.id,
@@ -22155,7 +22276,8 @@ function registerFinishRecord(deps, ctx) {
22155
22276
  ...dispositions !== void 0 ? { dispositions } : {},
22156
22277
  ...reviewPost !== void 0 ? { reviewPost } : {},
22157
22278
  ...parentRunId !== void 0 ? { parentRunId } : {},
22158
- ...coordinator !== void 0 ? { coordinator } : {}
22279
+ ...coordinator !== void 0 ? { coordinator } : {},
22280
+ ...seed !== void 0 ? { seed } : {}
22159
22281
  }),
22160
22282
  { span: root, ...ledgerRun ? { via: ledgerRun.sink } : {} }
22161
22283
  )
@@ -22188,6 +22310,7 @@ function followUpOf(msg, text, at, opts) {
22188
22310
  ...msg.sourceUrl !== void 0 ? { sourceUrl: msg.sourceUrl } : {},
22189
22311
  ...msg.images !== void 0 ? { images: msg.images } : {},
22190
22312
  ...msg.documents !== void 0 ? { documents: msg.documents } : {},
22313
+ ...msg.staged !== void 0 ? { staged: msg.staged } : {},
22191
22314
  at,
22192
22315
  ...opts.ledgerSeq !== void 0 ? { ledgerSeq: opts.ledgerSeq } : {},
22193
22316
  ...opts.from !== void 0 ? { from: opts.from } : {},
@@ -22253,7 +22376,9 @@ async function admit(deps, ctx) {
22253
22376
  threadKey: msg.threadKey,
22254
22377
  state: resume.row.state,
22255
22378
  lastStep: resume.lastStep.step,
22256
- lastSeq: resume.lastSeq
22379
+ lastSeq: resume.lastSeq,
22380
+ // The row's place in its session log, so the resumed run appends where it left off.
22381
+ ...resume.row.meta.session ? { session: resume.row.meta.session } : {}
22257
22382
  });
22258
22383
  await root.span(
22259
22384
  "dispatch.admission",
@@ -22403,6 +22528,8 @@ async function adoptCarriedRun(deps, ctx) {
22403
22528
  state: resume.row.state,
22404
22529
  lastStep: resume.lastStep.step,
22405
22530
  lastSeq: resume.lastSeq,
22531
+ // The row's place in its session log, so the resumed run appends where it left off.
22532
+ ...resume.row.meta.session ? { session: resume.row.meta.session } : {},
22406
22533
  ...ctx.hooks.adopt
22407
22534
  });
22408
22535
  }
@@ -23353,6 +23480,7 @@ function summaryOf(run2) {
23353
23480
  ...m?.parentRunId !== void 0 ? { parentRunId: m.parentRunId } : {},
23354
23481
  ...m?.parentInstanceId !== void 0 ? { parentInstanceId: m.parentInstanceId } : {},
23355
23482
  ...m?.idempotencyKey !== void 0 ? { idempotencyKey: m.idempotencyKey } : {},
23483
+ ...m?.seed !== void 0 ? { seed: m.seed } : {},
23356
23484
  finished: run2.finished,
23357
23485
  startedAt: run2.startedAt,
23358
23486
  ...run2.finishedAt !== void 0 ? { finishedAt: run2.finishedAt } : {},
@@ -24254,6 +24382,21 @@ var init_awaitChildren = __esm({
24254
24382
  }
24255
24383
  });
24256
24384
 
24385
+ // ../../src/core/dispatch/textTurns.ts
24386
+ function textTurnsOf(messages) {
24387
+ const turns = [];
24388
+ for (const m of messages) {
24389
+ const text = m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
24390
+ if (text.length > 0) turns.push({ role: m.role, text });
24391
+ }
24392
+ return turns;
24393
+ }
24394
+ var init_textTurns = __esm({
24395
+ "../../src/core/dispatch/textTurns.ts"() {
24396
+ "use strict";
24397
+ }
24398
+ });
24399
+
24257
24400
  // ../../src/core/dispatch/spawn.ts
24258
24401
  function maxChildrenOf(cfg) {
24259
24402
  return cfg?.maxChildren ?? DEFAULT_MAX_CHILDREN;
@@ -24290,6 +24433,7 @@ function watchedChild(io, on) {
24290
24433
  };
24291
24434
  if (io.attach) watched2.attach = (file) => io.attach(file);
24292
24435
  if (io.attachFile) watched2.attachFile = (file) => io.attachFile(file);
24436
+ if (io.uploadTicket) watched2.uploadTicket = (file) => io.uploadTicket(file);
24293
24437
  if (io.runFinished) watched2.runFinished = (receipt) => io.runFinished(receipt);
24294
24438
  if (io.openThread) watched2.openThread = (lead) => io.openThread(lead);
24295
24439
  return watched2;
@@ -24302,6 +24446,12 @@ async function spawnChild(deps, parent, request) {
24302
24446
  `a child run cannot spawn: this run is itself a child, and a tree is ${MAX_SPAWN_DEPTH} level deep \u2014 the run that started it is the one to ask`
24303
24447
  );
24304
24448
  }
24449
+ if (AGENTS[request.preset]?.identity === "write") {
24450
+ return refused5(
24451
+ "spawn_identity",
24452
+ `\`${request.preset}\` runs as a \`write\` identity \u2014 it pushes branches and opens pull requests \u2014 and a spawned child never writes: it reads this conversation and reports; the person who asked starts that work by hand with \`agent:${request.preset}\``
24453
+ );
24454
+ }
24305
24455
  const minutesLeft = Math.floor(parent.remainingMs / 6e4);
24306
24456
  if (minutesLeft < MIN_BOUNDARY_MINUTES) {
24307
24457
  return refused5(
@@ -24354,8 +24504,10 @@ async function spawnChild(deps, parent, request) {
24354
24504
  lastReply = text;
24355
24505
  }
24356
24506
  });
24507
+ const seed = parent.conversation ? textTurnsOf(parent.conversation) : void 0;
24357
24508
  const settled = deps.dispatch(deps.core, child, io, {
24358
- parent: { runId: parent.runId, depth: parent.depth + 1, remainingMs: parent.remainingMs }
24509
+ parent: { runId: parent.runId, depth: parent.depth + 1, remainingMs: parent.remainingMs },
24510
+ ...seed ? { seed } : {}
24359
24511
  }).then(
24360
24512
  (outcome) => ({ kind: "ended", outcome }),
24361
24513
  (err2) => ({ kind: "threw", err: err2 })
@@ -24396,8 +24548,8 @@ function spawnCapabilityFor(deps, parent) {
24396
24548
  };
24397
24549
  let previous = Promise.resolve();
24398
24550
  return {
24399
- spawn: (request, remainingMs) => {
24400
- const turn = previous.then(() => spawnChild(withHook, { ...parent, remainingMs }, request));
24551
+ spawn: (request, at) => {
24552
+ const turn = previous.then(() => spawnChild(withHook, { ...parent, ...at }, request));
24401
24553
  previous = turn.catch(() => {
24402
24554
  });
24403
24555
  return turn;
@@ -24441,11 +24593,13 @@ var DEFAULT_MAX_CHILDREN, MAX_SPAWN_DEPTH, describe2, nullSpawnCapability;
24441
24593
  var init_spawn = __esm({
24442
24594
  "../../src/core/dispatch/spawn.ts"() {
24443
24595
  "use strict";
24596
+ init_registry();
24444
24597
  init_validate();
24445
24598
  init_actor();
24446
24599
  init_runsService();
24447
24600
  init_admission();
24448
24601
  init_awaitChildren();
24602
+ init_textTurns();
24449
24603
  DEFAULT_MAX_CHILDREN = 3;
24450
24604
  MAX_SPAWN_DEPTH = 1;
24451
24605
  describe2 = (err2) => err2 instanceof Error ? err2.message : String(err2);
@@ -24470,12 +24624,61 @@ function routablePresets() {
24470
24624
  maxMinutes
24471
24625
  }));
24472
24626
  }
24627
+ function partPresets(presets) {
24628
+ return presets.filter((p) => p.identity !== "write");
24629
+ }
24630
+ function nameList(names) {
24631
+ return names.length <= 1 ? names.join("") : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
24632
+ }
24473
24633
  function renderPresetTable(presets) {
24474
24634
  const rows = presets.map(
24475
24635
  (p) => `| \`${p.name}\` | ${oneLine(p.description)} | ${p.machine} | ${p.identity} | ${p.maxMinutes} min |`
24476
24636
  );
24477
24637
  return ["| name | what it does | machine | credential | budget |", "|---|---|---|---|---|", ...rows].join("\n");
24478
24638
  }
24639
+ function routeMaxOutputTokens(compound) {
24640
+ const PART_JSON_OVERHEAD = 40;
24641
+ const ANSWER_JSON_OVERHEAD = 80;
24642
+ const chars = ANSWER_JSON_OVERHEAD + ROUTE_REASON_CAP + (compound ? compound.maxParts * (ROUTE_PART_TEXT_CAP + PART_JSON_OVERHEAD) : 0);
24643
+ return Math.max(ROUTE_MIN_OUTPUT_TOKENS, Math.ceil(chars / 3));
24644
+ }
24645
+ function routeTool(presets, compound) {
24646
+ const names = presets.map((p) => p.name);
24647
+ const readers = partPresets(presets).map((p) => p.name);
24648
+ const writers = presets.filter((p) => p.identity === "write").map((p) => p.name);
24649
+ const table = presets.map((p) => `${p.name}: ${oneLine(p.description)}`).join("; ");
24650
+ const writeAsk = writers.length > 0 ? ` An ask that needs ${nameList(writers)} is never a part: when any part would need it, omit parts and answer ${nameList(writers)} for the whole request.` : "";
24651
+ const properties = {
24652
+ preset: {
24653
+ type: "string",
24654
+ enum: compound ? [...names, COMPOUND_PRESET] : names,
24655
+ description: `the least capable preset whose description covers the request \u2014 ${table}${compound ? `; ${COMPOUND_PRESET}: only for a compound request, with parts` : ""}`
24656
+ },
24657
+ reason: { type: "string", description: "one line, under 100 characters: why this preset" },
24658
+ ...compound ? {
24659
+ parts: {
24660
+ type: "array",
24661
+ description: `only when the request has two or more INDEPENDENT asks on different subjects, each rewritten so it stands alone and each on a read-only preset (${nameList(readers)}), with preset "${COMPOUND_PRESET}".${writeAsk} A single ask with several steps is one request on one preset: omit parts and name that preset. When one ask is in doubt, omit parts.`,
24662
+ minItems: 2,
24663
+ maxItems: compound.maxParts,
24664
+ items: {
24665
+ type: "object",
24666
+ additionalProperties: false,
24667
+ required: ["preset", "text"],
24668
+ properties: {
24669
+ preset: { type: "string", enum: readers, description: "the read-only preset this part runs on" },
24670
+ text: { type: "string", description: "the part as a request of its own" }
24671
+ }
24672
+ }
24673
+ }
24674
+ } : {}
24675
+ };
24676
+ return {
24677
+ name: ROUTE_TOOL_NAME,
24678
+ description: "Route the request: name the preset it runs on and why.",
24679
+ inputSchema: { type: "object", additionalProperties: false, required: ["preset", "reason"], properties }
24680
+ };
24681
+ }
24479
24682
  function quoteRequest(text) {
24480
24683
  const bent = text.replace(/<(\/?)request>/gi, "\u2039$1request\u203A");
24481
24684
  if (bent.length <= ROUTE_TEXT_CAP) return bent;
@@ -24500,7 +24703,7 @@ function buildRoutePrompt(input) {
24500
24703
  "",
24501
24704
  "Presets you may pick:",
24502
24705
  renderPresetTable(input.presets),
24503
- ...input.compound ? ["", ...compoundRules(input.compound)] : []
24706
+ ...input.compound ? ["", ...compoundRules(input.compound, input.presets)] : []
24504
24707
  ].join("\n");
24505
24708
  const user = [
24506
24709
  `Earlier directives in this thread: ${directivesLine(input.recentDirectives)}`,
@@ -24509,18 +24712,25 @@ function buildRoutePrompt(input) {
24509
24712
  quoteRequest(input.text),
24510
24713
  "</request>"
24511
24714
  ].join("\n");
24512
- return { system, user };
24715
+ return { system, user, tool: routeTool(input.presets, input.compound) };
24513
24716
  }
24514
- function compoundRules(offer) {
24717
+ function compoundRules(offer, presets) {
24718
+ const readers = partPresets(presets).map((p) => p.name);
24719
+ const writers = presets.filter((p) => p.identity === "write").map((p) => `\`${p.name}\``);
24515
24720
  return [
24516
24721
  "Compound requests: when the request has two or more INDEPENDENT parts \u2014 neither part needs the other's result, and each would stand alone as a request of its own \u2014 answer this form instead, and only then:",
24517
- `{"preset": "${COMPOUND_PRESET}", "parts": [{"text": "<one part, rewritten so it stands alone>", "preset": "<a name from the table>"}, \u2026], "reason": "<one line: why the parts are independent>"}`,
24518
- `Independent means neither part needs the other's result. A single ask with several steps ("clone it, run the tests, tell me what fails") is NOT compound: it is one request on one preset, however many steps it takes. Two asks on two different subjects ("summarize what is in the docs folder, and run the lint on main in a sandbox") ARE compound even when both are read-only or would land on the same preset \u2014 the same preset may appear twice. When one ask is in doubt, do not split it. At most ${offer.maxParts} parts; each part's preset comes from the table above, chosen for that part alone by the same rules as a single request \u2014 least capable first.`
24722
+ `{"preset": "${COMPOUND_PRESET}", "parts": [{"text": "<one part, rewritten so it stands alone>", "preset": "<one of ${readers.join(", ")}>"}, \u2026], "reason": "<one line: why the parts are independent>"}`,
24723
+ `Independent means neither part needs the other's result. A single ask with several steps ("clone it, run the tests, tell me what fails") is NOT compound: it is one request on one preset, however many steps it takes. Two asks on two different subjects ("summarize what is in the docs folder, and run the lint on main in a sandbox") ARE compound even when both are read-only or would land on the same preset \u2014 the same preset may appear twice. When one ask is in doubt, do not split it. At most ${offer.maxParts} parts. Each part runs as a child that only reads, so each part's preset is one of ${readers.map((r) => `\`${r}\``).join(", ")} (the rows above whose credential is none or read), chosen for that part alone by the same rules as a single request \u2014 least capable first.`,
24724
+ ...writers.length > 0 ? [writeAskRule(writers)] : []
24519
24725
  ];
24520
24726
  }
24727
+ function writeAskRule(writers) {
24728
+ const names = writers.join(" or ");
24729
+ return `An ask that needs ${names} is never a part: a part runs as a child that only reads, and ${names} pushes. When any part of the request would need ${names}, do not split it: answer ${names} alone for the whole request as typed, and it reads what it must before it changes anything ("review PR 7 and fix what it finds" is one ${names} request; "research X and open a PR for Y" is one ${names} request).`;
24730
+ }
24521
24731
  function imperativeRule(writers) {
24522
24732
  const names = writers.map((w) => `\`${w}\``).join(" or ");
24523
- return `Short imperatives: one terse order to change something or to make a failure go away \u2014 "fix it", "make it pass", "make the tests green", "add X", "rename Y", "bump Z" \u2014 is a request to change code even when it names no file, repository or cause: the channel or thread it arrives in is bound to a repository, and the preset that implements changes finds the failure itself. For it, answer ${names}. A question or a read-only ask about the same failure \u2014 "why did ci fail?", "check whether ci is red", "tell me why the build failed", "list the failing tests" \u2014 changes nothing: answer a read-only preset that covers it, never ${names}. An ask to look at, check or judge a pull request is a review, not an order to change it.`;
24733
+ return `Short imperatives: one terse order to change something or to make a failure go away \u2014 "fix it", "make it pass", "make the tests green", "add X", "rename Y", "bump Z" \u2014 is a request to change code even when it names no file, repository or cause: the channel or thread it arrives in is bound to a repository, and the preset that implements changes finds the failure itself. For it, answer ${names}. A question or a read-only ask about the same failure \u2014 "why did ci fail?", "check whether ci is red", "tell me why the build failed", "list the failing tests" \u2014 changes nothing: answer a read-only preset that covers it, never ${names}. An ask to look at, check or judge a pull request \u2014 named by a link or a number, or the thread's own ("this PR") \u2014 is a review, not an order to change it; a question about a failure with no pull request in view is not a review.`;
24524
24734
  }
24525
24735
  function tidyReason(reason) {
24526
24736
  const line3 = oneLine(redactAndCap(reason, ROUTE_REASON_CAP));
@@ -24537,8 +24747,10 @@ function parseRouteAnswer(raw, allowed2, compound) {
24537
24747
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
24538
24748
  return { preset: void 0, reason: `not a single JSON object: ${tidyReason(trimmed)}` };
24539
24749
  const { preset, reason, parts } = parsed;
24540
- if (typeof preset !== "string") return { preset: void 0, reason: "missing preset in the router's answer" };
24541
- if (typeof reason !== "string") return { preset: void 0, reason: "missing reason in the router's answer" };
24750
+ if (typeof preset !== "string")
24751
+ return { preset: void 0, reason: `missing preset in the router's answer: ${tidyReason(trimmed)}` };
24752
+ if (typeof reason !== "string")
24753
+ return { preset: void 0, reason: `missing reason in the router's answer: ${tidyReason(trimmed)}` };
24542
24754
  if (preset === COMPOUND_PRESET) return parseCompound(parts, tidyReason(reason), allowed2, compound);
24543
24755
  if (!allowed2.includes(preset))
24544
24756
  return { preset: void 0, reason: `router said "${tidyReason(preset)}", not a preset the requester may run` };
@@ -24567,16 +24779,20 @@ function parseCompound(parts, reason, allowed2, offer) {
24567
24779
  if (tidy.length === 0) return rejected(`part ${n2} has no text`);
24568
24780
  out.push({ preset, text: tidy });
24569
24781
  }
24782
+ const writer = out.find((p) => identityOf(p.preset) === "write");
24783
+ if (writer) return { preset: writer.preset, reason, collapsed: { presets: out.map((p) => p.preset) } };
24570
24784
  return { preset: COMPOUND_PRESET, reason, parts: out };
24571
24785
  }
24572
24786
  async function route(input, model, opts = {}) {
24573
- const offered = input.presets.filter((p) => input.allowed.includes(p.name));
24787
+ const { compound: offer, ...rest } = input;
24788
+ const offered = rest.presets.filter((p) => rest.allowed.includes(p.name));
24574
24789
  if (offered.length === 0) return { preset: void 0, reason: "no preset the requester may run" };
24575
- const prompt = buildRoutePrompt({ ...input, presets: offered });
24790
+ const compound = offer && partPresets(offered).length > 0 ? offer : void 0;
24791
+ const prompt = buildRoutePrompt({ ...rest, presets: offered, ...compound ? { compound } : {} });
24576
24792
  let raw;
24577
24793
  try {
24578
24794
  raw = await model(prompt, {
24579
- maxTokens: ROUTE_MAX_OUTPUT_TOKENS,
24795
+ maxTokens: routeMaxOutputTokens(compound),
24580
24796
  signal: AbortSignal.timeout(opts.timeoutMs ?? ROUTE_TIMEOUT_MS)
24581
24797
  });
24582
24798
  } catch (err2) {
@@ -24588,18 +24804,25 @@ async function route(input, model, opts = {}) {
24588
24804
  return parseRouteAnswer(
24589
24805
  raw,
24590
24806
  offered.map((p) => p.name),
24591
- input.compound
24807
+ compound
24592
24808
  );
24593
24809
  }
24594
- function providerRouteModel(provider, model) {
24595
- return async (prompt, opts) => {
24810
+ function providerRouteModel(provider, model, opts = {}) {
24811
+ const forced = (opts.answer ?? "tool") === "tool";
24812
+ return async (prompt, call2) => {
24596
24813
  const result = await provider.complete({
24597
24814
  model,
24598
24815
  system: prompt.system,
24599
24816
  messages: [{ role: "user", content: [{ type: "text", text: prompt.user }] }],
24600
- maxTokens: opts.maxTokens,
24601
- signal: opts.signal
24817
+ maxTokens: call2.maxTokens,
24818
+ signal: call2.signal,
24819
+ ...forced ? { tools: [prompt.tool], toolChoice: { type: "tool", name: prompt.tool.name } } : {}
24602
24820
  });
24821
+ if (result.stopReason === "max_tokens") throw new Error(`answer cut at the output cap (${call2.maxTokens} tokens)`);
24822
+ const answer = result.content.find(
24823
+ (p) => p.type === "tool_use" && p.name === prompt.tool.name
24824
+ );
24825
+ if (answer) return JSON.stringify(answer.input);
24603
24826
  return result.content.filter((p) => p.type === "text").map((p) => p.text).join("");
24604
24827
  };
24605
24828
  }
@@ -24617,7 +24840,9 @@ async function routeRequest(deps, ctx) {
24617
24840
  if (deps.routeModel) model = deps.routeModel;
24618
24841
  else {
24619
24842
  const ref = parseModelRef(modelRef2);
24620
- model = providerRouteModel(deps.providers.get(ref.provider), ref.model);
24843
+ model = providerRouteModel(deps.providers.get(ref.provider), ref.model, {
24844
+ ...cfg.routing?.answer ? { answer: cfg.routing.answer } : {}
24845
+ });
24621
24846
  }
24622
24847
  } catch (err2) {
24623
24848
  console.log(`[route] ${msg.threadKey} not routed: ${err2 instanceof Error ? err2.message : String(err2)}`);
@@ -24653,18 +24878,24 @@ async function routeRequest(deps, ctx) {
24653
24878
  effort: directives.effort ?? sticky.effort
24654
24879
  }
24655
24880
  });
24656
- const parts = decision.parts;
24881
+ const { parts, collapsed } = decision;
24657
24882
  console.log(
24658
- `[route] ${msg.threadKey} routed to ${decision.preset} on ${modelRef2}${parts ? ` (${parts.length} parts)` : ""}: ${decision.reason}`
24883
+ `[route] ${msg.threadKey} routed to ${decision.preset} on ${modelRef2}${parts ? ` (${parts.length} parts)` : ""}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}: ${decision.reason}`
24659
24884
  );
24660
24885
  return {
24661
24886
  kind: "routed",
24662
24887
  resolved,
24663
- route: { preset: decision.preset, reason: decision.reason, model: modelRef2, ...parts ? { parts } : {} }
24888
+ route: {
24889
+ preset: decision.preset,
24890
+ reason: decision.reason,
24891
+ model: modelRef2,
24892
+ ...parts ? { parts } : {},
24893
+ ...collapsed ? { collapsed } : {}
24894
+ }
24664
24895
  };
24665
24896
  }
24666
- function routedLabel(reason) {
24667
- return `routed: ${reason}`;
24897
+ function routedLabel(reason, collapsed) {
24898
+ return `routed: ${reason}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}`;
24668
24899
  }
24669
24900
  function routedPartLines(parts) {
24670
24901
  return parts.map((p) => {
@@ -24680,7 +24911,7 @@ function compoundBrief(text, parts) {
24680
24911
  ...parts.map((p, i) => `${i + 1}. \`${p.preset}\`: ${p.text}`)
24681
24912
  ].join("\n");
24682
24913
  }
24683
- var ROUTE_TEXT_CAP, ROUTE_REASON_CAP, ROUTE_MAX_OUTPUT_TOKENS, ROUTE_TIMEOUT_MS, ROUTE_PART_TEXT_CAP, ROUTE_PART_LINE_CAP, COMPOUND_BRIEF_HEADING, ROUTED_CARD_FOOTER;
24914
+ var ROUTE_TEXT_CAP, ROUTE_REASON_CAP, ROUTE_MIN_OUTPUT_TOKENS, ROUTE_TOOL_NAME, ROUTE_TIMEOUT_MS, ROUTE_PART_TEXT_CAP, ROUTE_PART_LINE_CAP, COMPOUND_BRIEF_HEADING, identityOf, ROUTED_CARD_FOOTER;
24684
24915
  var init_route = __esm({
24685
24916
  "../../src/core/dispatch/route.ts"() {
24686
24917
  "use strict";
@@ -24691,11 +24922,13 @@ var init_route = __esm({
24691
24922
  init_spawn();
24692
24923
  ROUTE_TEXT_CAP = 2e3;
24693
24924
  ROUTE_REASON_CAP = 120;
24694
- ROUTE_MAX_OUTPUT_TOKENS = 200;
24925
+ ROUTE_MIN_OUTPUT_TOKENS = 200;
24926
+ ROUTE_TOOL_NAME = "route";
24695
24927
  ROUTE_TIMEOUT_MS = 8e3;
24696
24928
  ROUTE_PART_TEXT_CAP = 1e3;
24697
24929
  ROUTE_PART_LINE_CAP = 100;
24698
24930
  COMPOUND_BRIEF_HEADING = "Routed as a compound request";
24931
+ identityOf = (preset) => AGENTS[preset]?.identity;
24699
24932
  ROUTED_CARD_FOOTER = "reply agent:<preset> to run it another way";
24700
24933
  }
24701
24934
  });
@@ -25723,19 +25956,162 @@ ${skill.body}${footer}`;
25723
25956
  }
25724
25957
  });
25725
25958
 
25959
+ // ../../src/artifacts/contentType.ts
25960
+ function contentTypeFor(name) {
25961
+ const dot = name.lastIndexOf(".");
25962
+ if (dot < 0 || dot === name.length - 1) return OCTET_STREAM;
25963
+ return BY_EXTENSION[name.slice(dot + 1).toLowerCase()] ?? OCTET_STREAM;
25964
+ }
25965
+ var BY_EXTENSION, OCTET_STREAM, INLINE_IMAGE_TYPES;
25966
+ var init_contentType = __esm({
25967
+ "../../src/artifacts/contentType.ts"() {
25968
+ "use strict";
25969
+ BY_EXTENSION = {
25970
+ png: "image/png",
25971
+ jpg: "image/jpeg",
25972
+ jpeg: "image/jpeg",
25973
+ gif: "image/gif",
25974
+ webp: "image/webp",
25975
+ svg: "image/svg+xml",
25976
+ pdf: "application/pdf",
25977
+ mp4: "video/mp4",
25978
+ webm: "video/webm",
25979
+ mov: "video/quicktime",
25980
+ mp3: "audio/mpeg",
25981
+ wav: "audio/wav",
25982
+ txt: "text/plain",
25983
+ md: "text/markdown",
25984
+ csv: "text/csv",
25985
+ json: "application/json",
25986
+ html: "text/html",
25987
+ zip: "application/zip",
25988
+ gz: "application/gzip",
25989
+ tar: "application/x-tar"
25990
+ };
25991
+ OCTET_STREAM = "application/octet-stream";
25992
+ INLINE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
25993
+ }
25994
+ });
25995
+
25996
+ // ../../src/artifacts/keys.ts
25997
+ function safeBasename(name) {
25998
+ const segments2 = name.split(/[\\/]+/).filter((s) => s.length > 0);
25999
+ const last = segments2[segments2.length - 1] ?? "";
26000
+ const reduced = last.replace(SAFE, "_").replace(/\.{2,}/g, "_").replace(/_+/g, "_");
26001
+ if (reduced === "" || /^[._-]+$/.test(reduced)) return "file";
26002
+ return reduced;
26003
+ }
26004
+ function threadKeySafe(threadKey) {
26005
+ return threadKey.replace(/:/g, "-").replace(SAFE, "_");
26006
+ }
26007
+ function outboundKey(runId3, seq, name) {
26008
+ return `runs/${runId3}/out/${seq}-${safeBasename(name)}`;
26009
+ }
26010
+ function inboundKey(threadKey, messageTs, index, name) {
26011
+ return `threads/${threadKeySafe(threadKey)}/in/${messageTs}/${index}-${safeBasename(name)}`;
26012
+ }
26013
+ var SAFE;
26014
+ var init_keys = __esm({
26015
+ "../../src/artifacts/keys.ts"() {
26016
+ "use strict";
26017
+ SAFE = /[^A-Za-z0-9._-]+/g;
26018
+ }
26019
+ });
26020
+
25726
26021
  // ../../src/tools/attach.ts
26022
+ function transferBudgetMs(size) {
26023
+ return Math.ceil(size / TRANSFER_FLOOR_BYTES_PER_MS) + TRANSFER_SETUP_MS;
26024
+ }
25727
26025
  function fileNameOf2(path) {
25728
26026
  const segments2 = path.split("/").filter(Boolean);
25729
26027
  return segments2[segments2.length - 1] ?? path;
25730
26028
  }
25731
- var attachFileTool;
26029
+ function putCommandFor(path, contentType, url) {
26030
+ return `curl -fsS -T ${shellQuote(path)} -H ${shellQuote(`Content-Type: ${contentType}`)} ${shellQuote(url)}`;
26031
+ }
26032
+ function postCommandFor(path, url) {
26033
+ return `curl -fsS --data-binary @${shellQuote(path)} ${shellQuote(url)}`;
26034
+ }
26035
+ async function attachThroughStore(path, name, lead, artifacts, ctx) {
26036
+ const exec = (command2, timeoutMs2) => {
26037
+ const opts = { timeoutMs: timeoutMs2, ...ctx.signal ? { signal: ctx.signal } : {} };
26038
+ return ctx.executor.exec(command2, opts);
26039
+ };
26040
+ const statOut = await exec(statCommandFor(shellQuote(path)), STAT_TIMEOUT_MS);
26041
+ if (parseExitPrefix(statOut).failed) return `error: could not read ${path}: ${statOut.trim()}`;
26042
+ const size = parseByteSize(statOut);
26043
+ if (size === null) return `error: could not measure ${path}: ${statOut.trim()}`;
26044
+ if (size === 0) return `error: ${path} is empty \u2014 nothing to attach`;
26045
+ if (size > MAX_ARTIFACT_BYTES) {
26046
+ return `error: ${path} is ${size} bytes; attach_file takes files up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) \u2014 link to the file instead`;
26047
+ }
26048
+ let timeoutMs = BASH_TIMEOUT_MAX_MS;
26049
+ if (ctx.remainingMs) {
26050
+ const budget = bashBudgetWithinRun(BASH_TIMEOUT_MAX_MS, ctx.remainingMs());
26051
+ if (budget.kind === "exhausted") return `error: ${budget.note}`;
26052
+ if (budget.kind === "clipped") timeoutMs = budget.timeoutMs;
26053
+ }
26054
+ const needed = transferBudgetMs(size);
26055
+ if (needed > timeoutMs) {
26056
+ return `error: ${name} is ${size} bytes and needs about ${secs(needed)}s to move at ${TRANSFER_FLOOR_BYTES_PER_MS} KB/s; the run has ${secs(timeoutMs)}s of command budget left \u2014 write up what you have and link to the file instead`;
26057
+ }
26058
+ const contentType = contentTypeFor(name);
26059
+ const key = outboundKey(artifacts.runId, artifacts.nextSeq(), name);
26060
+ const putUrl = await artifacts.store.presignPut(key, contentType);
26061
+ const putOut = await exec(putCommandFor(path, contentType, putUrl), timeoutMs);
26062
+ if (parseExitPrefix(putOut).failed) {
26063
+ return `error: the upload of ${name} to the artifact store failed: ${putOut.trim()}`;
26064
+ }
26065
+ const head = await artifacts.store.head(key);
26066
+ if (!head)
26067
+ return `error: the artifact store holds nothing under ${key} after the upload of ${name} \u2014 nothing was posted`;
26068
+ if (head.size !== size) {
26069
+ return `error: the artifact store holds ${head.size} bytes for ${name}, not the ${size} measured \u2014 nothing was posted`;
26070
+ }
26071
+ ctx.publish?.({ type: "artifact", direction: "out", key, name, size, contentType });
26072
+ if (!ctx.uploadTicket) {
26073
+ const where = artifacts.runUrl ? ` \u2014 ${artifacts.runUrl}` : "";
26074
+ await artifacts.reply(`${lead}
26075
+ \u{1F4CE} ${name} (${size} bytes) is on the run page${where}`);
26076
+ return `attached ${name} (${size} bytes) to the run page; this conversation's channel takes no file uploads, so the lead and the page's link were posted instead`;
26077
+ }
26078
+ const kept = "the file is kept on the run page";
26079
+ let ticket;
26080
+ try {
26081
+ ticket = await ctx.uploadTicket({ name, size });
26082
+ } catch (err2) {
26083
+ return `error: the channel refused an upload ticket for ${name}: ${describe3(err2)}; ${kept}`;
26084
+ }
26085
+ const postOut = await exec(postCommandFor(path, ticket.url), timeoutMs);
26086
+ if (parseExitPrefix(postOut).failed) {
26087
+ return `error: the upload of ${name} to the channel failed: ${postOut.trim()}; ${kept}`;
26088
+ }
26089
+ try {
26090
+ await ticket.complete(lead);
26091
+ } catch (err2) {
26092
+ return `error: the channel refused to complete the upload of ${name}: ${describe3(err2)}; ${kept}`;
26093
+ }
26094
+ return `attached ${name} (${size} bytes) to the conversation and the run page`;
26095
+ }
26096
+ var MAX_ARTIFACT_BYTES, TRANSFER_FLOOR_BYTES_PER_MS, TRANSFER_SETUP_MS, STAT_TIMEOUT_MS, describe3, secs, attachFileTool;
25732
26097
  var init_attach = __esm({
25733
26098
  "../../src/tools/attach.ts"() {
25734
26099
  "use strict";
26100
+ init_contentType();
26101
+ init_keys();
26102
+ init_runEvents();
26103
+ init_bashTimeout();
25735
26104
  init_binaryRead();
26105
+ init_shellQuote();
26106
+ MAX_ARTIFACT_BYTES = 1073741824;
26107
+ TRANSFER_FLOOR_BYTES_PER_MS = 1024;
26108
+ TRANSFER_SETUP_MS = 1e4;
26109
+ STAT_TIMEOUT_MS = 3e4;
26110
+ describe3 = (err2) => err2 instanceof Error ? err2.message : String(err2);
26111
+ secs = (ms) => Math.max(0, Math.round(ms / 1e3));
25736
26112
  attachFileTool = {
25737
26113
  name: "attach_file",
25738
- description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only, up to ${MAX_READ_BYTES} bytes.`,
26114
+ description: `Post a file from the workspace into the conversation so the person sees it inline \u2014 a screenshot (e.g. from \`playwright screenshot\`), a rendered PDF, a recording, a log. Use it whenever you produce an image worth showing: a link to a file is not a picture. Whole files only: up to ${MAX_ARTIFACT_BYTES} bytes (1 GiB) where the artifact store is configured, ${MAX_READ_BYTES} bytes otherwise \u2014 the result says which.`,
25739
26115
  inputSchema: {
25740
26116
  type: "object",
25741
26117
  properties: {
@@ -25752,6 +26128,7 @@ var init_attach = __esm({
25752
26128
  if (!path) return "error: path is required";
25753
26129
  const name = fileNameOf2(path);
25754
26130
  const lead = typeof input.comment === "string" && input.comment.trim() ? input.comment.trim() : name;
26131
+ if (ctx.artifacts) return attachThroughStore(path, name, lead, ctx.artifacts, ctx);
25755
26132
  if (!ctx.attach) {
25756
26133
  return "attach_file is not available here: this conversation's channel takes no file uploads \u2014 link to the file instead";
25757
26134
  }
@@ -25763,13 +26140,13 @@ var init_attach = __esm({
25763
26140
  try {
25764
26141
  bytes = await readBytes(path);
25765
26142
  } catch (err2) {
25766
- return `error: could not read ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`;
26143
+ return `error: could not read ${path}: ${describe3(err2)}`;
25767
26144
  }
25768
26145
  if (bytes.byteLength === 0) return `error: ${path} is empty \u2014 nothing to attach`;
25769
26146
  try {
25770
26147
  await ctx.attach({ name, bytes, lead });
25771
26148
  } catch (err2) {
25772
- return `error: the channel refused the upload of ${name}: ${err2 instanceof Error ? err2.message : String(err2)}`;
26149
+ return `error: the channel refused the upload of ${name}: ${describe3(err2)}`;
25773
26150
  }
25774
26151
  return `attached ${name} (${bytes.byteLength} bytes) to the conversation`;
25775
26152
  }
@@ -25902,7 +26279,7 @@ async function nextTick(wait, ids, ms, signal) {
25902
26279
  signal?.removeEventListener("abort", end);
25903
26280
  }
25904
26281
  }
25905
- var UNAVAILABLE2, NOT_FOUND, PRESETS, CHILDREN_PAGES_MAX, spawnRunTool, sendToRunTool, AWAIT_IDS_MAX, awaitRunsTool, listRunsTool, getRunStatusTool, RUN_TOOLS;
26282
+ var UNAVAILABLE2, NOT_FOUND, CHILD_PRESETS, WRITE_PRESETS, REPO_PRESETS, CHILDREN_PAGES_MAX, spawnRunTool, sendToRunTool, AWAIT_IDS_MAX, awaitRunsTool, listRunsTool, getRunStatusTool, RUN_TOOLS;
25906
26283
  var init_runs2 = __esm({
25907
26284
  "../../src/tools/runs.ts"() {
25908
26285
  "use strict";
@@ -25917,16 +26294,24 @@ var init_runs2 = __esm({
25917
26294
  init_runsService();
25918
26295
  UNAVAILABLE2 = "run tools are not available in this context.";
25919
26296
  NOT_FOUND = "not_found";
25920
- PRESETS = () => Object.keys(AGENTS);
26297
+ CHILD_PRESETS = () => Object.values(AGENTS).filter((a) => a.name !== "conductor" && a.identity !== "write").map((a) => a.name);
26298
+ WRITE_PRESETS = () => Object.values(AGENTS).filter((a) => a.identity === "write").map((a) => a.name);
26299
+ REPO_PRESETS = () => Object.values(AGENTS).filter((a) => machineNeedsRepo(a.machine)).map((a) => a.name);
25921
26300
  CHILDREN_PAGES_MAX = 25;
25922
26301
  spawnRunTool = {
25923
26302
  name: "spawn_run",
25924
- description: "Start a child run as the person who asked you: an ordinary Switchboard run of the named preset, in a thread of its own in this channel, under their permissions \u2014 what they could start by hand with `agent:<preset>`. `prompt` is everything the child needs (it sees none of this thread); `repo` (owner/name) for a preset that works in a repository (coding, review, explore, ship); `budget` narrows the child's wall clock in whole minutes (at least 2; it is also capped by what is left of yours). Returns the child's run id, its thread and a link, or a refusal by name: a preset or repository the requester may not use, a boundary the child's profile exceeds, the fan-out cap, or a channel that cannot open a thread. A child cannot spawn children.",
26303
+ description: `Start a child run as the person who asked you: an ordinary Switchboard run of the named preset, in a thread of its own in this channel, under their permissions \u2014 what they could start by hand with \`agent:<preset>\`. The child is a reader of this conversation: it starts from this conversation's text so far (every user and assistant turn, never your tool calls or their results) plus \`prompt\`, its one new turn \u2014 so say what it should do. Presets a child can run: ${CHILD_PRESETS().join(", ")}; a preset that writes (${WRITE_PRESETS().join(", ")}) is refused \`spawn_identity\`. \`repo\` (owner/name) for a preset that works in a repository (${REPO_PRESETS().join(", ")}); \`budget\` narrows the child's wall clock in whole minutes (at least 2; it is also capped by what is left of yours). Returns the child's run id, its thread and a link, or a refusal by name: a preset or repository the requester may not use, a boundary the child's profile exceeds, the fan-out cap, or a channel that cannot open a thread. A child cannot spawn children.`,
25925
26304
  inputSchema: {
25926
26305
  type: "object",
25927
26306
  properties: {
25928
- preset: { type: "string", description: `The preset the child runs: one of ${PRESETS().join(", ")}` },
25929
- prompt: { type: "string", description: "The child's whole request, self-contained" },
26307
+ preset: {
26308
+ type: "string",
26309
+ description: `The preset the child runs: one of ${CHILD_PRESETS().join(", ")} (a preset that writes \u2014 ${WRITE_PRESETS().join(", ")} \u2014 is refused spawn_identity)`
26310
+ },
26311
+ prompt: {
26312
+ type: "string",
26313
+ description: "What the child should do \u2014 its one new turn after this conversation's text"
26314
+ },
25930
26315
  repo: { type: "string", description: "owner/name of the repository the child works in, for a repository preset" },
25931
26316
  budget: { type: "integer", description: "The child's wall clock in whole minutes, at least 2 (optional)" }
25932
26317
  },
@@ -25934,9 +26319,10 @@ var init_runs2 = __esm({
25934
26319
  },
25935
26320
  async run(input, ctx) {
25936
26321
  const preset = String(input.preset ?? "");
25937
- if (!Object.hasOwn(AGENTS, preset)) return `error: unknown preset "${preset}" \u2014 one of ${PRESETS().join(", ")}`;
26322
+ if (!Object.hasOwn(AGENTS, preset))
26323
+ return `error: unknown preset "${preset}" \u2014 one of ${CHILD_PRESETS().join(", ")}`;
25938
26324
  const prompt = String(input.prompt ?? "").trim();
25939
- if (!prompt) return "error: prompt is required \u2014 the child's whole request, since it sees none of this thread";
26325
+ if (!prompt) return "error: prompt is required \u2014 what the child should do, after this conversation's text";
25940
26326
  const request = { preset, prompt };
25941
26327
  if (input.repo !== void 0 && String(input.repo).trim()) request.repo = String(input.repo).trim();
25942
26328
  if (input.budget !== void 0) {
@@ -25946,7 +26332,10 @@ var init_runs2 = __esm({
25946
26332
  request.budget = budget;
25947
26333
  }
25948
26334
  const spawn7 = ctx.spawn ?? nullSpawnCapability;
25949
- const out = await spawn7.spawn(request, ctx.remainingMs?.() ?? Number.POSITIVE_INFINITY);
26335
+ const out = await spawn7.spawn(request, {
26336
+ remainingMs: ctx.remainingMs?.() ?? Number.POSITIVE_INFINITY,
26337
+ ...ctx.conversation ? { conversation: ctx.conversation() } : {}
26338
+ });
25950
26339
  if (out.kind === "refused") return `spawn refused (${out.reason}): ${out.message}`;
25951
26340
  return `spawned a ${preset} run: ${out.runId} in thread ${out.threadKey}${out.url ? ` (${out.url})` : ""} \u2014 it runs on its own; await_runs waits for it and returns its reply; get_run_status ${out.runId} answers its progress meanwhile.`;
25952
26341
  }
@@ -26310,7 +26699,7 @@ ${raw.trim()}`;
26310
26699
  };
26311
26700
  submitDispositionsTool = {
26312
26701
  name: "submit_dispositions",
26313
- description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id from the review (F1, F2, \u2026) \u2014 use exactly those ids; an unknown id is rejected by name. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. Dispositions are recorded only inside a ship pipeline's fix round; anywhere else the call is an honest no-op that says nothing was recorded.",
26702
+ description: "Record one disposition per review finding after addressing them: `fixed` (the finding is addressed in your pushed code) or `declined` (deliberately not doing it \u2014 the note says why). `findingId` is the finding's stable id from the review (F1, F2, \u2026) \u2014 use exactly those ids; an id the review never issued answers nothing and is dropped when the plan runner reads your record. Every finding gets exactly one entry, every severity included (nits too). Call it once with the complete set after your last push; a later call replaces the earlier one. The set rides this run's record, where the plan runner reads it for the re-review.",
26314
26703
  inputSchema: {
26315
26704
  type: "object",
26316
26705
  properties: {
@@ -26333,15 +26722,7 @@ ${raw.trim()}`;
26333
26722
  async run(input, ctx) {
26334
26723
  const parsed = parseDispositionsInput(input);
26335
26724
  if (!parsed) return "error: dispositions must be an array of { findingId, disposition: fixed|declined, note }";
26336
- if (ctx.knownFindingIds) {
26337
- const known = new Set(ctx.knownFindingIds);
26338
- const unknown = [...new Set(parsed.dispositions.map((d) => d.findingId).filter((id) => !known.has(id)))];
26339
- if (unknown.length) {
26340
- return `error: unknown finding id${unknown.length === 1 ? "" : "s"} ${unknown.join(", ")} \u2014 use exactly the ids from the review's findings list`;
26341
- }
26342
- }
26343
- if (!ctx.onDispositions)
26344
- return "no ship fix round is active here \u2014 dispositions were not recorded (they apply only when addressing a ship review's findings)";
26725
+ if (!ctx.onDispositions) return "no run is recording dispositions here";
26345
26726
  ctx.onDispositions(parsed.dispositions);
26346
26727
  const drops = parsed.dropped.length ? ` (dropped: ${parsed.dropped.join("; ")})` : "";
26347
26728
  return `dispositions recorded: ${parsed.dispositions.length}${drops}; a later call replaces this one`;
@@ -26673,14 +27054,34 @@ async function runLoop(opts, now, note, emit, agentSpan) {
26673
27054
  const deadline = now() + (opts.resume ? opts.resume.remainingMs : opts.agent.maxMinutes * 6e4);
26674
27055
  const warnAt = deadline - Math.min(3 * 6e4, opts.agent.maxMinutes * 15e3);
26675
27056
  toolContext.remainingMs = () => deadline - now();
27057
+ toolContext.conversation = () => messages;
26676
27058
  let warned = false;
26677
27059
  let sandboxDead = false;
27060
+ let stuckLoop = false;
27061
+ let stuckSig;
27062
+ let stuckStreak = 0;
27063
+ let stuckNudged = false;
27064
+ const trackStuck = (tu, ok2) => {
27065
+ const sig = callSignature(tu.name, redactAndCap(describeToolCall(tu)));
27066
+ if (ok2) {
27067
+ if (sig === stuckSig) {
27068
+ stuckSig = void 0;
27069
+ stuckStreak = 0;
27070
+ stuckNudged = false;
27071
+ }
27072
+ return;
27073
+ }
27074
+ if (sig !== stuckSig) stuckNudged = false;
27075
+ stuckStreak = sig === stuckSig ? stuckStreak + 1 : 1;
27076
+ stuckSig = sig;
27077
+ };
26678
27078
  let turn = 0;
26679
27079
  const wouldStep = (turns, iteration) => turns < opts.agent.maxTurns && iteration < opts.agent.maxTurns * 2 && now() < deadline && !control?.requested;
26680
27080
  const pendingFollowUps = () => (opts.inbox?.size ?? 0) > 0;
26681
27081
  let inboxConsumedSeq = opts.resume?.inboxConsumedSeq ?? 0;
26682
- const drainFollowUps = (superseded = false) => {
27082
+ const drainFollowUps = async (superseded = false) => {
26683
27083
  const inputs = opts.inbox?.drain() ?? [];
27084
+ const stagedLine = opts.stageFollowUps ? await opts.stageFollowUps(inputs) : "";
26684
27085
  for (const input of inputs) {
26685
27086
  if (input.ledgerSeq !== void 0 && input.ledgerSeq > inboxConsumedSeq) inboxConsumedSeq = input.ledgerSeq;
26686
27087
  const source2 = {
@@ -26691,7 +27092,10 @@ async function runLoop(opts, now, note, emit, agentSpan) {
26691
27092
  emit({ type: "input", text: redactSecrets(input.text), ...Object.keys(source2).length > 0 ? { source: source2 } : {} });
26692
27093
  note("follow_up", `follow-up folded in: ${redactSecrets(followUpSnippet(input))}`);
26693
27094
  }
26694
- const parts = [{ type: "text", text: followUpPrompt(inputs, { superseded }) }];
27095
+ const prompt = followUpPrompt(inputs, { superseded });
27096
+ const parts = [{ type: "text", text: stagedLine ? `${prompt}
27097
+
27098
+ ${stagedLine}` : prompt }];
26695
27099
  for (const input of inputs) {
26696
27100
  for (const img of input.images ?? []) parts.push({ type: "image", mediaType: img.mediaType, data: img.data });
26697
27101
  for (const doc of input.documents ?? [])
@@ -26708,12 +27112,15 @@ async function runLoop(opts, now, note, emit, agentSpan) {
26708
27112
  const body = async (callSpan) => {
26709
27113
  if (announceIt) announce(tu, callSpan?.id);
26710
27114
  const spanId = callSpan ? { spanId: callSpan.id } : {};
26711
- const settle2 = (ok2, extra = {}) => callSpan?.end(ok2 ? "ok" : "error", {
26712
- callId: tu.id,
26713
- ok: ok2,
26714
- ...extra.exitCode !== void 0 ? { exitCode: extra.exitCode } : {},
26715
- ...extra.infra ? { infra: true } : {}
26716
- });
27115
+ const settle2 = (ok2, extra = {}) => {
27116
+ trackStuck(tu, ok2);
27117
+ callSpan?.end(ok2 ? "ok" : "error", {
27118
+ callId: tu.id,
27119
+ ok: ok2,
27120
+ ...extra.exitCode !== void 0 ? { exitCode: extra.exitCode } : {},
27121
+ ...extra.infra ? { infra: true } : {}
27122
+ });
27123
+ };
26717
27124
  const tool = toolsByName.get(tu.name);
26718
27125
  if (!tool) {
26719
27126
  emit({
@@ -26897,7 +27304,7 @@ async function runLoop(opts, now, note, emit, agentSpan) {
26897
27304
  turn++;
26898
27305
  if (text2) emit({ type: "assistant", text: redactSecrets(text2) });
26899
27306
  messages.push({ role: "assistant", content: result.content });
26900
- messages.push({ role: "user", content: drainFollowUps(true) });
27307
+ messages.push({ role: "user", content: await drainFollowUps(true) });
26901
27308
  continue;
26902
27309
  }
26903
27310
  return text2 || "_(no response)_";
@@ -26922,6 +27329,10 @@ async function runLoop(opts, now, note, emit, agentSpan) {
26922
27329
  reportedUpTo = messages.length;
26923
27330
  }
26924
27331
  const results = await dispatchToolUses(toolUses);
27332
+ if (!stuckNudged && stuckStreak >= STUCK_NUDGE_AT) {
27333
+ stuckNudged = true;
27334
+ results.push({ type: "text", text: stuckLoopNudge() });
27335
+ }
26925
27336
  if (!warned && now() >= warnAt) {
26926
27337
  warned = true;
26927
27338
  const minutesLeft = Math.max(1, Math.round((deadline - now()) / 6e4));
@@ -26929,12 +27340,16 @@ async function runLoop(opts, now, note, emit, agentSpan) {
26929
27340
  results.push({ type: "text", text: wrapUpInstruction(minutesLeft) });
26930
27341
  }
26931
27342
  const dead = execTracker.consecutiveInfraFailures >= MAX_CONSECUTIVE_INFRA_FAILURES;
26932
- if (pendingFollowUps() && !dead && wouldStep(turn, iteration + 1)) results.push(...drainFollowUps());
27343
+ if (pendingFollowUps() && !dead && wouldStep(turn, iteration + 1)) results.push(...await drainFollowUps());
26933
27344
  messages.push({ role: "user", content: results });
26934
27345
  if (dead) {
26935
27346
  sandboxDead = true;
26936
27347
  break;
26937
27348
  }
27349
+ if (stuckStreak >= STUCK_WRITE_UP_AT) {
27350
+ stuckLoop = true;
27351
+ break;
27352
+ }
26938
27353
  }
26939
27354
  narrateHeld();
26940
27355
  if (control?.requested === "soft") {
@@ -26946,6 +27361,11 @@ async function runLoop(opts, now, note, emit, agentSpan) {
26946
27361
  note("sandbox_dead", `${diagnosis} \u2014 aborting instead of retrying into a dead sandbox`);
26947
27362
  return await finishSandboxDead(complete2, opts, messages, system, diagnosis);
26948
27363
  }
27364
+ if (stuckLoop) {
27365
+ note("stuck_loop", stuckLoopNote());
27366
+ const text2 = await runFinale(complete2, opts, messages, system, stuckLoopInstruction());
27367
+ return stuckLoopAnswer(text2);
27368
+ }
26949
27369
  const wasTimeout = now() >= deadline;
26950
27370
  const elapsedMs = opts.agent.maxMinutes * 6e4 - (deadline - now());
26951
27371
  const pace = turnGuardPace(turn, elapsedMs);
@@ -27047,7 +27467,7 @@ function withSpanId(e, spanId) {
27047
27467
  return e;
27048
27468
  }
27049
27469
  }
27050
- var MAX_CONSECUTIVE_INFRA_FAILURES, HardStopError, HARD_STOP_MESSAGE, wrapUpNote, wrapUpInstruction, WRITE_UP_REQUEST, timeBudgetInstruction, turnGuardInstruction, SOFT_STOP_INSTRUCTION, turnGuardPace, timeBudgetNote, turnGuardNote, softStopNote, hardStopNote, timeBudgetAnswer, turnGuardAnswer, softStopAnswer, FinaleTimeoutError, FINALE_TIMEOUT_MS;
27470
+ var MAX_CONSECUTIVE_INFRA_FAILURES, HardStopError, HARD_STOP_MESSAGE, wrapUpNote, wrapUpInstruction, WRITE_UP_REQUEST, timeBudgetInstruction, turnGuardInstruction, SOFT_STOP_INSTRUCTION, turnGuardPace, timeBudgetNote, turnGuardNote, softStopNote, hardStopNote, timeBudgetAnswer, turnGuardAnswer, STUCK_NUDGE_AT, STUCK_WRITE_UP_AT, stuckLoopNudge, stuckLoopNote, stuckLoopInstruction, stuckLoopAnswer, softStopAnswer, FinaleTimeoutError, FINALE_TIMEOUT_MS;
27051
27471
  var init_runner = __esm({
27052
27472
  "../../src/runner.ts"() {
27053
27473
  "use strict";
@@ -27058,6 +27478,7 @@ var init_runner = __esm({
27058
27478
  init_tracingExecutor();
27059
27479
  init_workspace();
27060
27480
  init_formatDuration();
27481
+ init_runFriction();
27061
27482
  MAX_CONSECUTIVE_INFRA_FAILURES = 2;
27062
27483
  HardStopError = class extends Error {
27063
27484
  constructor() {
@@ -27083,6 +27504,14 @@ ${text}` : `Stopped at the ${maxMinutes}-minute budget without finishing. Partia
27083
27504
  turnGuardAnswer = (text, pace) => text ? `\u26A0\uFE0F _Stopped after ${pace} \u2014 that pace looks like a loop; findings so far:_
27084
27505
 
27085
27506
  ${text}` : `Stopped after ${pace} \u2014 that pace looks like a loop \u2014 without finishing. Partial work may exist in the workspace \u2014 look for a retry loop in the run's events before trying again.`;
27507
+ STUCK_NUDGE_AT = 3;
27508
+ STUCK_WRITE_UP_AT = 6;
27509
+ stuckLoopNudge = () => `\u26A0\uFE0F That exact tool call has now failed ${STUCK_NUDGE_AT} times identically. Change approach \u2014 different arguments, a different tool, a smaller step \u2014 or write up what you have.`;
27510
+ stuckLoopNote = () => `stuck loop: the same call failed ${STUCK_WRITE_UP_AT} times identically \u2014 writing up findings so far`;
27511
+ stuckLoopInstruction = () => `The same tool call has failed ${STUCK_WRITE_UP_AT} times identically; the run is stuck and can make no more tool calls. ${WRITE_UP_REQUEST}`;
27512
+ stuckLoopAnswer = (text) => text ? `\u26A0\uFE0F _Stopped after the same call failed ${STUCK_WRITE_UP_AT} times identically \u2014 findings so far:_
27513
+
27514
+ ${text}` : `Stopped after the same call failed ${STUCK_WRITE_UP_AT} times identically, without finishing. Partial work may exist in the workspace \u2014 look at the failing call in the run's events before trying again.`;
27086
27515
  softStopAnswer = (text) => text ? `\u23F9 _Stopped early by an operator (soft stop) \u2014 findings so far:_
27087
27516
 
27088
27517
  ${text}` : "\u23F9 Stopped early by an operator (soft stop) before any findings were written. Partial work may exist in the workspace.";
@@ -27925,11 +28354,107 @@ var init_messages = __esm({
27925
28354
  init_runEvents();
27926
28355
  init_runRecord();
27927
28356
  init_reply();
28357
+ init_textTurns();
27928
28358
  CONTEXT_MAX_ITEMS = 20;
27929
28359
  CONTEXT_MAX_BYTES = 256 * 1024;
27930
28360
  }
27931
28361
  });
27932
28362
 
28363
+ // ../../src/core/dispatch/staging.ts
28364
+ function stagedBasename(index, name) {
28365
+ return `${index}-${safeBasename(name)}`;
28366
+ }
28367
+ function pullCommandFor(url, basename4) {
28368
+ return `mkdir -p ${ATTACHMENTS_DIR} && curl -fsS -o ${shellQuote(`${ATTACHMENTS_DIR}/${basename4}`)} ${shellQuote(url)}`;
28369
+ }
28370
+ function excludeCommand() {
28371
+ const entry = shellQuote(`${ATTACHMENTS_DIR}/`);
28372
+ return `mkdir -p .git/info && { grep -qxF ${entry} .git/info/exclude 2>/dev/null || echo ${entry} >> .git/info/exclude; }`;
28373
+ }
28374
+ function formatSize(bytes) {
28375
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
28376
+ if (bytes >= 1e6) return `${Math.round(bytes / 1e6)} MB`;
28377
+ if (bytes >= 1e3) return `${Math.round(bytes / 1e3)} KB`;
28378
+ return `${bytes} B`;
28379
+ }
28380
+ function attachmentsLine(outcomes) {
28381
+ const landed = outcomes.filter((o) => o.error === void 0);
28382
+ const failed = outcomes.filter((o) => o.error !== void 0);
28383
+ const parts = [];
28384
+ if (landed.length > 0) {
28385
+ parts.push(
28386
+ `Attached files are in ./${ATTACHMENTS_DIR}/: ${landed.map((o) => `${o.basename} (${formatSize(o.file.size)}, ${o.file.type})`).join(", ")}`
28387
+ );
28388
+ }
28389
+ for (const o of failed) parts.push(`${o.file.name} could not be staged: ${o.error}`);
28390
+ return parts.join(". ");
28391
+ }
28392
+ function noWorkspaceLine(files) {
28393
+ return files.map(
28394
+ (f) => `this agent has no workspace for ${f.name} (${formatSize(f.size)}, ${f.type}); ask \`agent:coding\` to work with it`
28395
+ ).join(". ");
28396
+ }
28397
+ function stagingIndex() {
28398
+ let n2 = 0;
28399
+ return () => ++n2;
28400
+ }
28401
+ async function copyStaged(files, deps) {
28402
+ return Promise.all(
28403
+ files.map(async (file) => {
28404
+ const index = deps.nextIndex();
28405
+ const basename4 = stagedBasename(index, file.name);
28406
+ const key = inboundKey(deps.threadKey, file.messageId, index, file.name);
28407
+ try {
28408
+ await deps.store.copyFromUrl({ url: file.url, size: file.size, key });
28409
+ deps.publish?.({
28410
+ type: "artifact",
28411
+ direction: "in",
28412
+ key,
28413
+ name: file.name,
28414
+ size: file.size,
28415
+ contentType: file.type
28416
+ });
28417
+ return { file, basename: basename4, key };
28418
+ } catch (err2) {
28419
+ return { file, basename: basename4, key, error: `the copy into the store failed: ${describe4(err2)}` };
28420
+ }
28421
+ })
28422
+ );
28423
+ }
28424
+ async function pullStaged(outcomes, deps) {
28425
+ const landed = outcomes.filter((o) => o.error === void 0);
28426
+ if (landed.length === 0) return [...outcomes];
28427
+ const timeoutMs = deps.timeoutMs ?? BASH_TIMEOUT_MAX_MS;
28428
+ const opts = { timeoutMs, ...deps.signal ? { signal: deps.signal } : {} };
28429
+ if (deps.resident) await deps.executor.exec(excludeCommand(), opts);
28430
+ const pulled = /* @__PURE__ */ new Map();
28431
+ for (const o of landed) {
28432
+ const url = await deps.store.presignGet(o.key);
28433
+ const out = await deps.executor.exec(pullCommandFor(url, o.basename), opts);
28434
+ pulled.set(o.key, parseExitPrefix(out).failed ? `the pull into the workspace failed: ${out.trim()}` : void 0);
28435
+ }
28436
+ return outcomes.map((o) => {
28437
+ const error = o.error ?? pulled.get(o.key);
28438
+ return error === void 0 ? { file: o.file, basename: o.basename, key: o.key } : { ...o, error };
28439
+ });
28440
+ }
28441
+ async function stageIntoWorkspace(files, deps) {
28442
+ const outcomes = await pullStaged(await copyStaged(files, deps), deps);
28443
+ return { line: attachmentsLine(outcomes), outcomes };
28444
+ }
28445
+ var ATTACHMENTS_DIR, describe4;
28446
+ var init_staging = __esm({
28447
+ "../../src/core/dispatch/staging.ts"() {
28448
+ "use strict";
28449
+ init_keys();
28450
+ init_bashTimeout();
28451
+ init_shellQuote();
28452
+ init_runEvents();
28453
+ ATTACHMENTS_DIR = "attachments";
28454
+ describe4 = (err2) => err2 instanceof Error ? err2.message : String(err2);
28455
+ }
28456
+ });
28457
+
27933
28458
  // ../../src/core/modelProxy/runBearers.ts
27934
28459
  import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
27935
28460
  function token(runId3, secret2) {
@@ -28481,7 +29006,7 @@ function startMemoryRead(deps, ctx) {
28481
29006
  async function openAckCard(deps, ctx) {
28482
29007
  const { io, agent, resolved, startedAt, clock: clock3, root, trace, route: route3 } = ctx;
28483
29008
  const shell = createCardShell({
28484
- label: `*${agent.name}* on \`${resolved.modelRef}\`${route3 ? ` \xB7 ${routedLabel(route3.reason)}` : ""}`,
29009
+ label: `*${agent.name}* on \`${resolved.modelRef}\`${route3 ? ` \xB7 ${routedLabel(route3.reason, route3.collapsed)}` : ""}`,
28485
29010
  startedAt,
28486
29011
  now: clock3,
28487
29012
  ...route3?.parts ? { lead: routedPartLines(route3.parts) } : {},
@@ -28525,6 +29050,8 @@ async function registerRun(deps, ctx) {
28525
29050
  admitted,
28526
29051
  parentRunId,
28527
29052
  coordinator,
29053
+ seed,
29054
+ seedTurns,
28528
29055
  agentSource,
28529
29056
  route: route3
28530
29057
  } = ctx;
@@ -28557,7 +29084,8 @@ async function registerRun(deps, ctx) {
28557
29084
  ...msg.sourceUrl !== void 0 ? { sourceUrl: msg.sourceUrl } : {},
28558
29085
  ...msg.userName !== void 0 ? { userName: msg.userName } : {},
28559
29086
  ...parentRunId !== void 0 ? { parentRunId } : {},
28560
- ...coordinatorFields(coordinator)
29087
+ ...coordinatorFields(coordinator),
29088
+ ...seed !== void 0 ? { seed } : {}
28561
29089
  },
28562
29090
  // Under the run's id, at the card's start (the reservation's, or the
28563
29091
  // carried row's) — a resume replays its events, a restart starts them
@@ -28606,7 +29134,7 @@ async function registerRun(deps, ctx) {
28606
29134
  if (!resume) publishMeta(repoCtx);
28607
29135
  if (!resume && route3) registry.publish(run2.id, { type: "route", ...route3, at: clock3() });
28608
29136
  if (!resume && deps.config.config.runHistory?.includeContext !== false) {
28609
- for (const text of contextMessageTexts(history, humanize)) publishText("context", text);
29137
+ for (const text of contextMessageTexts(seedTurns ?? history, humanize)) publishText("context", text);
28610
29138
  }
28611
29139
  return { run: run2, runId: runId3, channelVisibility, liveUrl, publishText, publishMeta };
28612
29140
  }
@@ -28628,7 +29156,8 @@ async function reserveRun(deps, ctx) {
28628
29156
  admitted,
28629
29157
  root,
28630
29158
  parentRunId,
28631
- coordinator
29159
+ coordinator,
29160
+ seed
28632
29161
  } = ctx;
28633
29162
  if (!resume && !restart) {
28634
29163
  const requestRow = durableInboxMessage(msg, msg.text, receivedAt);
@@ -28656,6 +29185,7 @@ async function reserveRun(deps, ctx) {
28656
29185
  profile,
28657
29186
  ...parentRunId !== void 0 ? { parentRunId } : {},
28658
29187
  ...coordinatorFields(coordinator),
29188
+ ...seed !== void 0 ? { seed } : {},
28659
29189
  request: requestRow
28660
29190
  },
28661
29191
  card: card.handle ?? null,
@@ -28885,7 +29415,8 @@ async function claimRun(deps, ctx) {
28885
29415
  clock: clock3,
28886
29416
  root,
28887
29417
  parentRunId,
28888
- coordinator
29418
+ coordinator,
29419
+ seed
28889
29420
  } = ctx;
28890
29421
  const { resident, binding } = selection;
28891
29422
  let ledgerRun = ctx.ledgerRun;
@@ -28915,6 +29446,7 @@ async function claimRun(deps, ctx) {
28915
29446
  profile,
28916
29447
  ...parentRunId !== void 0 ? { parentRunId } : {},
28917
29448
  ...coordinatorFields(coordinator),
29449
+ ...seed !== void 0 ? { seed } : {},
28918
29450
  selection: resident === true ? "resident" : "sandbox",
28919
29451
  ...binding?.workspace !== void 0 ? { workspace: binding.workspace } : {},
28920
29452
  ...requestRow !== void 0 ? { request: requestRow } : {}
@@ -29113,6 +29645,157 @@ var init_container = __esm({
29113
29645
  }
29114
29646
  });
29115
29647
 
29648
+ // ../../src/core/harness/pi/extensionSource.ts
29649
+ var BLOCKED_AT_DOOR_PREFIX, BLOCKED_UNAVAILABLE_PREFIX, PI_EXTENSION_SOURCE;
29650
+ var init_extensionSource = __esm({
29651
+ "../../src/core/harness/pi/extensionSource.ts"() {
29652
+ "use strict";
29653
+ BLOCKED_AT_DOOR_PREFIX = "authorization refused at the door: ";
29654
+ BLOCKED_UNAVAILABLE_PREFIX = "authorization unavailable: ";
29655
+ PI_EXTENSION_SOURCE = `// Switchboard's pi harness extension. Written into the run's directory by the
29656
+ // bot before pi starts; loaded with \`-e\`. Imports nothing.
29657
+
29658
+ const BEARER_ENV = "SWITCHBOARD_RUN_BEARER";
29659
+ const URL_ENV = "SWITCHBOARD_HARNESS_URL";
29660
+ /** How long the tool_call hook keeps asking an unreachable bot before it blocks. */
29661
+ const AUTHORIZE_WAIT_MS = 90_000;
29662
+ const AUTHORIZE_RETRY_MS = 2_000;
29663
+
29664
+ function settings() {
29665
+ const url = process.env[URL_ENV];
29666
+ const bearer = process.env[BEARER_ENV];
29667
+ if (!url || !bearer) throw new Error("switchboard harness: " + URL_ENV + " and " + BEARER_ENV + " must be set");
29668
+ return { base: url.replace(/\\/$/, ""), bearer };
29669
+ }
29670
+
29671
+ /** The bot answered, and said no: the status rides along so a caller can tell
29672
+ * a verdict from a bot it could not reach. */
29673
+ class HarnessAnswerError extends Error {
29674
+ constructor(message, status) {
29675
+ super(message);
29676
+ this.status = status;
29677
+ }
29678
+ }
29679
+
29680
+ async function call(method, path, body, signal) {
29681
+ const { base, bearer } = settings();
29682
+ const res = await fetch(base + path, {
29683
+ method,
29684
+ headers: { authorization: "Bearer " + bearer, "content-type": "application/json", accept: "application/json" },
29685
+ body: body === undefined ? undefined : JSON.stringify(body),
29686
+ signal,
29687
+ });
29688
+ const text = await res.text();
29689
+ let json;
29690
+ try {
29691
+ json = JSON.parse(text);
29692
+ } catch {
29693
+ throw new Error("switchboard harness: " + method + " " + path + " answered " + res.status + " with a body that is not JSON");
29694
+ }
29695
+ if (!res.ok) {
29696
+ throw new HarnessAnswerError(
29697
+ "switchboard harness: " + method + " " + path + " answered " + res.status + ": " + (json.error || text),
29698
+ res.status,
29699
+ );
29700
+ }
29701
+ return json;
29702
+ }
29703
+
29704
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29705
+
29706
+ /** The bot's verdict on one tool call. A verdict answers at once, and so does
29707
+ * a refusal at the door \u2014 a 4xx: the bearer wrong, expired or revoked, the
29708
+ * run not on the harness \u2014 since nothing later changes it. A bot that cannot
29709
+ * be reached or is failing (a network error, a 5xx, a body that is not JSON)
29710
+ * is asked again every two seconds until the wait is up, then the call is
29711
+ * blocked naming why. */
29712
+ async function authorize(event) {
29713
+ const started = Date.now();
29714
+ let lastError = "";
29715
+ for (;;) {
29716
+ try {
29717
+ return await call("POST", "/harness/authorize", {
29718
+ toolCallId: event.toolCallId,
29719
+ tool: event.toolName,
29720
+ input: event.input,
29721
+ });
29722
+ } catch (err) {
29723
+ lastError = err instanceof Error ? err.message : String(err);
29724
+ const status = err instanceof HarnessAnswerError ? err.status : undefined;
29725
+ if (status !== undefined && status >= 400 && status < 500) {
29726
+ return { allow: false, reason: "${BLOCKED_AT_DOOR_PREFIX}" + lastError };
29727
+ }
29728
+ if (Date.now() - started >= AUTHORIZE_WAIT_MS) {
29729
+ return { allow: false, reason: "${BLOCKED_UNAVAILABLE_PREFIX}the bot did not answer for 90 s (" + lastError + ")" };
29730
+ }
29731
+ await sleep(AUTHORIZE_RETRY_MS);
29732
+ }
29733
+ }
29734
+ }
29735
+
29736
+ function relayTool(def) {
29737
+ return {
29738
+ name: def.name,
29739
+ label: def.name,
29740
+ description: def.description,
29741
+ parameters: def.inputSchema,
29742
+ async execute(toolCallId, params, signal) {
29743
+ const answer = await call("POST", "/harness/tool", { toolCallId, tool: def.name, input: params }, signal);
29744
+ if (answer.isError) throw new Error(answer.content.map((c) => (c.type === "text" ? c.text : "")).join("\\n"));
29745
+ return { content: answer.content, details: {} };
29746
+ },
29747
+ };
29748
+ }
29749
+
29750
+ export default async function switchboardHarness(pi) {
29751
+ const { tools } = await call("GET", "/harness/tools");
29752
+ for (const def of tools) pi.registerTool(relayTool(def));
29753
+ pi.on("tool_call", async (event) => {
29754
+ const verdict = await authorize(event);
29755
+ if (!verdict.allow) return { block: true, reason: verdict.reason };
29756
+ return undefined;
29757
+ });
29758
+ }
29759
+ `;
29760
+ }
29761
+ });
29762
+
29763
+ // ../../src/core/harness/pi/protocol.ts
29764
+ function parsePiLine(line3) {
29765
+ try {
29766
+ const value = JSON.parse(line3);
29767
+ if (typeof value === "object" && value !== null && typeof value.type === "string") {
29768
+ return value;
29769
+ }
29770
+ } catch {
29771
+ }
29772
+ return void 0;
29773
+ }
29774
+ function piAnsweredWithoutRunning(event) {
29775
+ if (event.type !== "tool_execution_end" || event.isError !== true) return void 0;
29776
+ const tool = typeof event.toolName === "string" ? event.toolName : "";
29777
+ const result = event.result;
29778
+ const content = Array.isArray(result?.content) ? result.content : [];
29779
+ const text = content.filter(
29780
+ (p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
29781
+ ).map((p) => p.text).join("\n");
29782
+ const validation = `Validation failed for tool "${tool}":
29783
+ `;
29784
+ if (text.startsWith(validation)) {
29785
+ const problems = text.slice(validation.length).split("\n\n")[0].split("\n").map((line3) => line3.replace(/^\s*-\s*/, "").trim()).filter((line3) => line3.length > 0);
29786
+ return `its arguments failed pi's validation (${problems.join("; ")})`;
29787
+ }
29788
+ if (text.startsWith(`Tool call "${tool}" was not executed: the response hit the output token limit`))
29789
+ return "its arguments were cut by the output token limit";
29790
+ if (text === `Tool ${tool} not found`) return "the tool is not on pi's list";
29791
+ return void 0;
29792
+ }
29793
+ var init_protocol = __esm({
29794
+ "../../src/core/harness/pi/protocol.ts"() {
29795
+ "use strict";
29796
+ }
29797
+ });
29798
+
29116
29799
  // ../../src/core/harness/pi/bridge.ts
29117
29800
  function describePiToolCall(tool, args) {
29118
29801
  const input = isRecord3(args) ? args : void 0;
@@ -29140,6 +29823,8 @@ var init_bridge2 = __esm({
29140
29823
  "use strict";
29141
29824
  init_formatDuration();
29142
29825
  init_runEvents();
29826
+ init_extensionSource();
29827
+ init_protocol();
29143
29828
  PI_EVENT_DISPOSITION = {
29144
29829
  response: "structure",
29145
29830
  agent_start: "structure",
@@ -29182,8 +29867,23 @@ var init_bridge2 = __esm({
29182
29867
  /** Text beside a bookkeeping-only call, held until the next message decides (run-loop item 15). */
29183
29868
  heldAnswer;
29184
29869
  assistantStartedAt;
29870
+ /** The calls under way: their span, their tool, and whether their end is
29871
+ * judged against the gate (harness-pi item 7). */
29185
29872
  openTools = /* @__PURE__ */ new Map();
29873
+ /** The calls the gate saw — the extension asked `/harness/authorize` for
29874
+ * them (`gateSaw`) — not yet ended. */
29875
+ vetted = /* @__PURE__ */ new Set();
29876
+ /** Whether a call starting now is judged against the gate when it ends.
29877
+ * The harness turns it off while catching up on a re-attach: those calls
29878
+ * were vetted by the bot generation that died, and this one never heard. */
29879
+ judgeGate = true;
29186
29880
  summarizationRetries = 0;
29881
+ /** The gate saw this call: the extension's `tool_call` hook asked the bot
29882
+ * for it, whatever the answer. Called from the authorize route, before pi
29883
+ * runs the tool — so it always precedes the call's `tool_execution_end`. */
29884
+ gateSaw(callId) {
29885
+ this.vetted.add(callId);
29886
+ }
29187
29887
  /** The run's answer as it stands: the last text-only assistant message, else a held bookkeeping text. */
29188
29888
  answer() {
29189
29889
  return this.answerText ?? this.heldAnswer;
@@ -29222,10 +29922,10 @@ var init_bridge2 = __esm({
29222
29922
  this.onToolStart(event);
29223
29923
  break;
29224
29924
  case "tool_execution_end":
29225
- this.onToolEnd(event);
29925
+ this.onToolEnd(event, out);
29226
29926
  break;
29227
29927
  case "compaction_end":
29228
- this.onCompactionEnd(event);
29928
+ this.onCompactionEnd(event, out);
29229
29929
  break;
29230
29930
  case "summarization_retry_scheduled":
29231
29931
  this.summarizationRetries++;
@@ -29285,7 +29985,7 @@ var init_bridge2 = __esm({
29285
29985
  const tool = str5(event.toolName);
29286
29986
  const callId = str5(event.toolCallId);
29287
29987
  const span = this.deps.agentSpan?.start(`tool.${tool}`);
29288
- this.openTools.set(callId, { span, tool });
29988
+ this.openTools.set(callId, { span, tool, judged: this.judgeGate });
29289
29989
  this.toolCalls++;
29290
29990
  const input = isRecord3(event.args) ? event.args : void 0;
29291
29991
  const command2 = tool === "bash" && typeof input?.command === "string" ? { command: redactAndCap(input.command, COMMAND_CAP) } : {};
@@ -29298,7 +29998,7 @@ var init_bridge2 = __esm({
29298
29998
  ...span ? { spanId: span.id } : {}
29299
29999
  });
29300
30000
  }
29301
- onToolEnd(event) {
30001
+ onToolEnd(event, out) {
29302
30002
  const callId = str5(event.toolCallId);
29303
30003
  const open = this.openTools.get(callId);
29304
30004
  this.openTools.delete(callId);
@@ -29321,8 +30021,25 @@ var init_bridge2 = __esm({
29321
30021
  ok: ok2,
29322
30022
  ...exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {}
29323
30023
  });
30024
+ const seen = this.vetted.delete(callId);
30025
+ if (!seen && open?.judged) {
30026
+ const rejection = piAnsweredWithoutRunning(event);
30027
+ if (rejection !== void 0) {
30028
+ this.note(
30029
+ "harness_error",
30030
+ `pi answered the ${tool} call ${callId} itself, before the gate: ${redactAndCap(rejection, 300)}; nothing ran`
30031
+ );
30032
+ } else if (isError && (text.startsWith(BLOCKED_AT_DOOR_PREFIX) || text.startsWith(BLOCKED_UNAVAILABLE_PREFIX))) {
30033
+ this.note(
30034
+ "harness_error",
30035
+ `the extension blocked the ${tool} call ${callId} without the gate's verdict: ${redactAndCap(text, 300)}; nothing ran`
30036
+ );
30037
+ } else {
30038
+ out.gateBypassed = { callId, tool };
30039
+ }
30040
+ }
29324
30041
  }
29325
- onCompactionEnd(event) {
30042
+ onCompactionEnd(event, out) {
29326
30043
  const result = isRecord3(event.result) ? event.result : void 0;
29327
30044
  if (event.aborted === true || !result) {
29328
30045
  this.note(
@@ -29339,6 +30056,13 @@ var init_bridge2 = __esm({
29339
30056
  "compacted",
29340
30057
  `pi compacted the context (${str5(event.reason)}): ${before ?? "?"} \u2192 about ${after ?? "?"} tokens; the transcript keeps the originals${retries > 0 ? `; ${retries} summarization retr${retries === 1 ? "y" : "ies"}` : ""}`
29341
30058
  );
30059
+ if (typeof result.summary === "string") {
30060
+ out.compaction = {
30061
+ summary: result.summary,
30062
+ ...before !== void 0 ? { tokensBefore: before } : {},
30063
+ ...typeof result.firstKeptEntryId === "string" ? { firstKeptEntryId: result.firstKeptEntryId } : {}
30064
+ };
30065
+ }
29342
30066
  }
29343
30067
  onUiRequest(event, out) {
29344
30068
  const method = str5(event.method);
@@ -29360,6 +30084,7 @@ var init_bridge2 = __esm({
29360
30084
  this.emit({ type: "tool_result", tool: open.tool, ok: false, callId, summary: redactAndCap(reason) });
29361
30085
  }
29362
30086
  this.openTools.clear();
30087
+ this.vetted.clear();
29363
30088
  }
29364
30089
  };
29365
30090
  }
@@ -29406,7 +30131,7 @@ function chatMessageOf(message2) {
29406
30131
  return void 0;
29407
30132
  }
29408
30133
  }
29409
- function piSessionFile(messages, opts) {
30134
+ function piSessionFile(messages, opts, compactions = []) {
29410
30135
  const lines = [];
29411
30136
  const stamp = new Date(opts.at).toISOString();
29412
30137
  lines.push(
@@ -29415,12 +30140,35 @@ function piSessionFile(messages, opts) {
29415
30140
  let parentId = null;
29416
30141
  let n2 = 0;
29417
30142
  const toolNames = /* @__PURE__ */ new Map();
30143
+ const firstEntryOf = [];
30144
+ const nextId = () => (n2++).toString(16).padStart(8, "0");
29418
30145
  const push = (message2) => {
29419
- const id = (n2++).toString(16).padStart(8, "0");
30146
+ const id = nextId();
29420
30147
  lines.push(JSON.stringify({ type: "message", id, parentId, timestamp: stamp, message: message2 }));
29421
30148
  parentId = id;
30149
+ return id;
29422
30150
  };
29423
- for (const message2 of messages) {
30151
+ const pushCompaction = (c) => {
30152
+ const id = nextId();
30153
+ const kept = c.keptBefore !== void 0 ? firstEntryOf[c.keptBefore] : void 0;
30154
+ lines.push(
30155
+ JSON.stringify({
30156
+ type: "compaction",
30157
+ id,
30158
+ parentId,
30159
+ timestamp: stamp,
30160
+ summary: c.entry.summary,
30161
+ firstKeptEntryId: kept ?? c.entry.firstKeptEntryId ?? "",
30162
+ tokensBefore: c.entry.tokensBefore ?? 0
30163
+ })
30164
+ );
30165
+ parentId = id;
30166
+ };
30167
+ const byPosition = /* @__PURE__ */ new Map();
30168
+ for (const c of compactions) byPosition.set(c.before, [...byPosition.get(c.before) ?? [], c]);
30169
+ messages.forEach((message2, i) => {
30170
+ for (const c of byPosition.get(i) ?? []) pushCompaction(c);
30171
+ firstEntryOf[i] = n2.toString(16).padStart(8, "0");
29424
30172
  if (message2.role === "assistant") {
29425
30173
  const content = [];
29426
30174
  for (const p of message2.content) {
@@ -29448,7 +30196,7 @@ function piSessionFile(messages, opts) {
29448
30196
  stopReason: hasTool ? "toolUse" : "stop",
29449
30197
  timestamp: opts.at
29450
30198
  });
29451
- continue;
30199
+ return;
29452
30200
  }
29453
30201
  const results = message2.content.filter(
29454
30202
  (p) => p.type === "tool_result"
@@ -29475,7 +30223,8 @@ function piSessionFile(messages, opts) {
29475
30223
  });
29476
30224
  }
29477
30225
  if (rest.length > 0) push({ role: "user", content: rest, timestamp: opts.at });
29478
- }
30226
+ });
30227
+ for (const c of byPosition.get(messages.length) ?? []) pushCompaction(c);
29479
30228
  return lines.join("\n") + "\n";
29480
30229
  }
29481
30230
  function sessionIdOf(at) {
@@ -29532,124 +30281,36 @@ var init_mirror = __esm({
29532
30281
  this.idx += turns.length;
29533
30282
  await this.deps.onStep(report2);
29534
30283
  }
29535
- };
29536
- }
29537
- });
29538
-
29539
- // ../../src/core/harness/pi/extensionSource.ts
29540
- var PI_EXTENSION_SOURCE;
29541
- var init_extensionSource = __esm({
29542
- "../../src/core/harness/pi/extensionSource.ts"() {
29543
- "use strict";
29544
- PI_EXTENSION_SOURCE = `// Switchboard's pi harness extension. Written into the run's directory by the
29545
- // bot before pi starts; loaded with \`-e\`. Imports nothing.
29546
-
29547
- const BEARER_ENV = "SWITCHBOARD_RUN_BEARER";
29548
- const URL_ENV = "SWITCHBOARD_HARNESS_URL";
29549
- /** How long the tool_call hook keeps asking an unreachable bot before it blocks. */
29550
- const AUTHORIZE_WAIT_MS = 90_000;
29551
- const AUTHORIZE_RETRY_MS = 2_000;
29552
-
29553
- function settings() {
29554
- const url = process.env[URL_ENV];
29555
- const bearer = process.env[BEARER_ENV];
29556
- if (!url || !bearer) throw new Error("switchboard harness: " + URL_ENV + " and " + BEARER_ENV + " must be set");
29557
- return { base: url.replace(/\\/$/, ""), bearer };
29558
- }
29559
-
29560
- /** The bot answered, and said no: the status rides along so a caller can tell
29561
- * a verdict from a bot it could not reach. */
29562
- class HarnessAnswerError extends Error {
29563
- constructor(message, status) {
29564
- super(message);
29565
- this.status = status;
29566
- }
29567
- }
29568
-
29569
- async function call(method, path, body, signal) {
29570
- const { base, bearer } = settings();
29571
- const res = await fetch(base + path, {
29572
- method,
29573
- headers: { authorization: "Bearer " + bearer, "content-type": "application/json", accept: "application/json" },
29574
- body: body === undefined ? undefined : JSON.stringify(body),
29575
- signal,
29576
- });
29577
- const text = await res.text();
29578
- let json;
29579
- try {
29580
- json = JSON.parse(text);
29581
- } catch {
29582
- throw new Error("switchboard harness: " + method + " " + path + " answered " + res.status + " with a body that is not JSON");
29583
- }
29584
- if (!res.ok) {
29585
- throw new HarnessAnswerError(
29586
- "switchboard harness: " + method + " " + path + " answered " + res.status + ": " + (json.error || text),
29587
- res.status,
29588
- );
29589
- }
29590
- return json;
29591
- }
29592
-
29593
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29594
-
29595
- /** The bot's verdict on one tool call. A verdict answers at once, and so does
29596
- * a refusal at the door \u2014 a 4xx: the bearer wrong, expired or revoked, the
29597
- * run not on the harness \u2014 since nothing later changes it. A bot that cannot
29598
- * be reached or is failing (a network error, a 5xx, a body that is not JSON)
29599
- * is asked again every two seconds until the wait is up, then the call is
29600
- * blocked naming why. */
29601
- async function authorize(event) {
29602
- const started = Date.now();
29603
- let lastError = "";
29604
- for (;;) {
29605
- try {
29606
- return await call("POST", "/harness/authorize", {
29607
- toolCallId: event.toolCallId,
29608
- tool: event.toolName,
29609
- input: event.input,
29610
- });
29611
- } catch (err) {
29612
- lastError = err instanceof Error ? err.message : String(err);
29613
- const status = err instanceof HarnessAnswerError ? err.status : undefined;
29614
- if (status !== undefined && status >= 400 && status < 500) {
29615
- return { allow: false, reason: "authorization refused at the door: " + lastError };
29616
- }
29617
- if (Date.now() - started >= AUTHORIZE_WAIT_MS) {
29618
- return { allow: false, reason: "authorization unavailable: the bot did not answer for 90 s (" + lastError + ")" };
30284
+ /** pi compacted (session-log item 6): the results pending since the last
30285
+ * assistant turn go as the user turn they would have been, the entry as the
30286
+ * row after them — its own step with nothing in flight, so the log holds
30287
+ * the summary where pi wrote it and the index moves past it. */
30288
+ async onCompaction(entry, turn) {
30289
+ if (!this.deps.onStep) return;
30290
+ const turns = [];
30291
+ if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
30292
+ this.pendingUser = [];
30293
+ const report2 = {
30294
+ turns,
30295
+ firstIdx: this.idx,
30296
+ compaction: entry,
30297
+ inFlight: [],
30298
+ turn,
30299
+ iteration: this.iteration,
30300
+ remainingMs: Math.max(0, this.deps.remainingMs()),
30301
+ inboxConsumedSeq: this.inboxConsumedSeq
30302
+ };
30303
+ this.idx += turns.length + 1;
30304
+ await this.deps.onStep(report2);
29619
30305
  }
29620
- await sleep(AUTHORIZE_RETRY_MS);
29621
- }
29622
- }
29623
- }
29624
-
29625
- function relayTool(def) {
29626
- return {
29627
- name: def.name,
29628
- label: def.name,
29629
- description: def.description,
29630
- parameters: def.inputSchema,
29631
- async execute(toolCallId, params, signal) {
29632
- const answer = await call("POST", "/harness/tool", { toolCallId, tool: def.name, input: params }, signal);
29633
- if (answer.isError) throw new Error(answer.content.map((c) => (c.type === "text" ? c.text : "")).join("\\n"));
29634
- return { content: answer.content, details: {} };
29635
- },
29636
- };
29637
- }
29638
-
29639
- export default async function switchboardHarness(pi) {
29640
- const { tools } = await call("GET", "/harness/tools");
29641
- for (const def of tools) pi.registerTool(relayTool(def));
29642
- pi.on("tool_call", async (event) => {
29643
- const verdict = await authorize(event);
29644
- if (!verdict.allow) return { block: true, reason: verdict.reason };
29645
- return undefined;
29646
- });
29647
- }
29648
- `;
30306
+ };
29649
30307
  }
29650
30308
  });
29651
30309
 
29652
30310
  // ../../src/core/harness/pi/process.ts
30311
+ function piBuiltinToolsFor(identity2) {
30312
+ return identity2 === "write" ? PI_BUILTIN_TOOLS : PI_READ_TOOLS;
30313
+ }
29653
30314
  function piThinkingLevel(effort2) {
29654
30315
  return effort2;
29655
30316
  }
@@ -29679,7 +30340,7 @@ function piLaunchArgs(spec) {
29679
30340
  "-e",
29680
30341
  spec.paths.extension,
29681
30342
  "--tools",
29682
- [...PI_BUILTIN_TOOLS, ...spec.relayTools].join(","),
30343
+ [...piBuiltinToolsFor(spec.identity), ...spec.relayTools].join(","),
29683
30344
  "--provider",
29684
30345
  PROXY_PROVIDER,
29685
30346
  "--model",
@@ -29698,6 +30359,13 @@ function piLaunchEnv(spec, bearer) {
29698
30359
  PI_TELEMETRY: "0"
29699
30360
  };
29700
30361
  }
30362
+ function takesAdaptiveThinking(modelId) {
30363
+ const version2 = /(?:^|-)(\d{1,2})(?:-(\d{1,2}))?(?=-|$)/.exec(modelId);
30364
+ if (!version2) return true;
30365
+ const major = Number(version2[1]);
30366
+ const minor = version2[2] === void 0 ? 0 : Number(version2[2]);
30367
+ return major > 4 || major === 4 && minor >= 6;
30368
+ }
29701
30369
  function piModelsJson(spec) {
29702
30370
  const anthropic = spec.model.providerType === "anthropic";
29703
30371
  const base = spec.harnessUrl.replace(/\/$/, "");
@@ -29713,6 +30381,7 @@ function piModelsJson(spec) {
29713
30381
  id: spec.model.id,
29714
30382
  name: spec.model.id,
29715
30383
  reasoning: true,
30384
+ ...anthropic ? { compat: { forceAdaptiveThinking: takesAdaptiveThinking(spec.model.id) } } : {},
29716
30385
  input: ["text", "image"],
29717
30386
  contextWindow: 2e5,
29718
30387
  maxTokens: spec.model.maxTokens,
@@ -29726,17 +30395,18 @@ function piModelsJson(spec) {
29726
30395
  2
29727
30396
  ) + "\n";
29728
30397
  }
29729
- function harnessPromptNote(relayTools) {
30398
+ function harnessPromptNote(relayTools, identity2) {
30399
+ const write = identity2 === "write";
29730
30400
  return [
29731
- "HARNESS NOTE: this run's workspace tools are pi's own \u2014 `read`, `bash`, `edit`, `write`, `grep`, `find` and `ls`, all running in the worktree.",
29732
- "Where these instructions say `read_file` use `read`; where they say `write_file` use `write` (a whole file) or `edit` (a targeted change).",
30401
+ write ? "HARNESS NOTE: this run's workspace tools are pi's own \u2014 `read`, `bash`, `edit`, `write`, `grep`, `find` and `ls`, all running in the worktree." : "HARNESS NOTE: this run's workspace tools are pi's own \u2014 `read`, `bash`, `grep`, `find` and `ls`, all running in the worktree; it has no `edit` and no `write`: the run is read-only, and a call to either is refused.",
30402
+ write ? "Where these instructions say `read_file` use `read`; where they say `write_file` use `write` (a whole file) or `edit` (a targeted change)." : "Where these instructions say `read_file` use `read`.",
29733
30403
  relayTools.length > 0 ? `Every other tool named above is available under its own name: ${relayTools.map((t) => `\`${t}\``).join(", ")}.` : "No other tools are available in this run."
29734
30404
  ].join(" ");
29735
30405
  }
29736
30406
  function piSystemPrompt(spec) {
29737
30407
  return `${spec.system.trimEnd()}
29738
30408
 
29739
- ${harnessPromptNote(spec.relayTools)}
30409
+ ${harnessPromptNote(spec.relayTools, spec.identity)}
29740
30410
  `;
29741
30411
  }
29742
30412
  function piSettingsJson() {
@@ -29750,7 +30420,7 @@ function piLaunchFiles(spec) {
29750
30420
  { path: spec.paths.extension, content: PI_EXTENSION_SOURCE }
29751
30421
  ];
29752
30422
  }
29753
- var RUN_BEARER_ENV, HARNESS_URL_ENV, PROXY_PROVIDER, PI_BUILTIN_TOOLS;
30423
+ var RUN_BEARER_ENV, HARNESS_URL_ENV, PROXY_PROVIDER, PI_BUILTIN_TOOLS, PI_READ_TOOLS;
29754
30424
  var init_process = __esm({
29755
30425
  "../../src/core/harness/pi/process.ts"() {
29756
30426
  "use strict";
@@ -29759,23 +30429,7 @@ var init_process = __esm({
29759
30429
  HARNESS_URL_ENV = "SWITCHBOARD_HARNESS_URL";
29760
30430
  PROXY_PROVIDER = "switchboard";
29761
30431
  PI_BUILTIN_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
29762
- }
29763
- });
29764
-
29765
- // ../../src/core/harness/pi/protocol.ts
29766
- function parsePiLine(line3) {
29767
- try {
29768
- const value = JSON.parse(line3);
29769
- if (typeof value === "object" && value !== null && typeof value.type === "string") {
29770
- return value;
29771
- }
29772
- } catch {
29773
- }
29774
- return void 0;
29775
- }
29776
- var init_protocol = __esm({
29777
- "../../src/core/harness/pi/protocol.ts"() {
29778
- "use strict";
30432
+ PI_READ_TOOLS = ["read", "bash", "grep", "find", "ls"];
29779
30433
  }
29780
30434
  });
29781
30435
 
@@ -29923,6 +30577,7 @@ async function runPiHarness(deps, run2) {
29923
30577
  let writeUp;
29924
30578
  let writeUpAt;
29925
30579
  let hardStopped = false;
30580
+ let bypass;
29926
30581
  const toolsBlocked = () => {
29927
30582
  if (!writeUp) return void 0;
29928
30583
  if (writeUp.kind === "time")
@@ -29931,14 +30586,16 @@ async function runPiHarness(deps, run2) {
29931
30586
  return "the run has hit its turn guard: no more tool calls \u2014 write your final answer now";
29932
30587
  return "an operator asked this run to stop: no more tool calls \u2014 write your final answer now";
29933
30588
  };
30589
+ const rules = { ...run2.rules, identity: run2.agent.identity };
29934
30590
  const live = {
29935
30591
  runId: run2.runId,
29936
30592
  tools: run2.tools,
29937
30593
  toolContext: run2.toolContext,
29938
30594
  ...run2.backend ? { backend: run2.backend } : {},
29939
- rules: run2.rules,
30595
+ rules,
29940
30596
  emit,
29941
30597
  toolSpan: (callId) => bridge.openSpan(callId),
30598
+ gateSaw: (callId) => bridge.gateSaw(callId),
29942
30599
  toolsBlocked
29943
30600
  };
29944
30601
  const forget = deps.registry.register(live);
@@ -29948,6 +30605,7 @@ async function runPiHarness(deps, run2) {
29948
30605
  model: { id: run2.model.id, providerType: run2.model.providerType, maxTokens: run2.agent.maxTokens },
29949
30606
  harnessUrl: deps.harnessUrl,
29950
30607
  ...run2.effort ? { effort: run2.effort } : {},
30608
+ identity: run2.agent.identity,
29951
30609
  system: run2.system,
29952
30610
  relayTools: run2.tools.map((t) => t.name)
29953
30611
  };
@@ -29983,25 +30641,33 @@ async function runPiHarness(deps, run2) {
29983
30641
  let session;
29984
30642
  if (run2.resume) {
29985
30643
  const settled2 = settlementResults(run2.resume.settlements);
29986
- session = { stem: "resumed", messages: settled2 ? [...run2.resume.messages, settled2] : run2.resume.messages };
30644
+ session = {
30645
+ stem: "resumed",
30646
+ messages: settled2 ? [...run2.resume.messages, settled2] : run2.resume.messages,
30647
+ compactions: run2.resume.compactions ?? []
30648
+ };
29987
30649
  } else {
29988
30650
  const earlier2 = splitSeed(run2.messages).session;
29989
- if (earlier2.length > 0) session = { stem: "seed", messages: earlier2 };
30651
+ if (earlier2.length > 0) session = { stem: "seed", messages: earlier2, compactions: [] };
29990
30652
  }
29991
30653
  let sessionPath;
29992
30654
  if (session) {
29993
30655
  sessionPath = `${paths.sessionDir}/${session.stem}-${now()}.jsonl`;
29994
30656
  await container.writeFile(
29995
30657
  sessionPath,
29996
- piSessionFile(session.messages, {
29997
- cwd: run2.rules.checkout,
29998
- model: {
29999
- provider: run2.model.provider,
30000
- id: run2.model.id,
30001
- api: run2.model.providerType === "anthropic" ? "anthropic-messages" : "openai-completions"
30658
+ piSessionFile(
30659
+ session.messages,
30660
+ {
30661
+ cwd: run2.rules.checkout,
30662
+ model: {
30663
+ provider: run2.model.provider,
30664
+ id: run2.model.id,
30665
+ api: run2.model.providerType === "anthropic" ? "anthropic-messages" : "openai-completions"
30666
+ },
30667
+ at: now()
30002
30668
  },
30003
- at: now()
30004
- })
30669
+ session.compactions
30670
+ )
30005
30671
  );
30006
30672
  }
30007
30673
  if (run2.resume) {
@@ -30108,8 +30774,15 @@ async function runPiHarness(deps, run2) {
30108
30774
  if (next.done) break;
30109
30775
  const event = parsePiLine(next.value);
30110
30776
  if (!event) continue;
30777
+ bridge.judgeGate = !catchingUp;
30111
30778
  const obs = bridge.observe(event);
30112
30779
  for (const reply of obs.replies) transport.send(reply);
30780
+ if (obs.gateBypassed) {
30781
+ bypass = new GateBypassed(obs.gateBypassed.tool, obs.gateBypassed.callId);
30782
+ note("harness_error", `${bypass.message} \u2014 the run is stopped`);
30783
+ transport.send({ type: "abort" });
30784
+ break;
30785
+ }
30113
30786
  if (obs.response) {
30114
30787
  const r = obs.response;
30115
30788
  if (r.id === "state" && r.success === true && typeof r.data?.sessionFile === "string") {
@@ -30123,6 +30796,7 @@ async function runPiHarness(deps, run2) {
30123
30796
  }
30124
30797
  }
30125
30798
  if (obs.message) await mirror.onMessage(obs.message, bridge.turns);
30799
+ if (obs.compaction) await mirror.onCompaction(obs.compaction, bridge.turns);
30126
30800
  if (obs.providerError !== void 0) {
30127
30801
  if (catchingUp)
30128
30802
  note("harness_error", `a model call failed while the bot was away (${obs.providerError}); continuing`);
@@ -30140,6 +30814,7 @@ async function runPiHarness(deps, run2) {
30140
30814
  note("stopped", hardStopNote(), "hard");
30141
30815
  return HARD_STOP_MESSAGE;
30142
30816
  }
30817
+ if (bypass) throw bypass;
30143
30818
  if (!settled) {
30144
30819
  const tail = await container.tail(paths.errLog, 2e3);
30145
30820
  throw new Error(`pi exited before the run settled${tail.trim() ? `: ${redactAndCap(tail.trim(), 400)}` : ""}`);
@@ -30152,15 +30827,17 @@ async function runPiHarness(deps, run2) {
30152
30827
  return text || "_(no response)_";
30153
30828
  } finally {
30154
30829
  transport?.close();
30155
- bridge.closeOpenSpans(hardStopped ? "the run was hard-stopped" : "the run ended");
30830
+ bridge.closeOpenSpans(
30831
+ hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
30832
+ );
30156
30833
  save();
30157
30834
  forget();
30158
30835
  if (pid !== void 0) await container.kill(pid).catch(() => {
30159
30836
  });
30160
- agentSpan?.end(hardStopped ? "error" : "ok");
30837
+ agentSpan?.end(hardStopped || bypass ? "error" : "ok");
30161
30838
  }
30162
30839
  }
30163
- var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CONTINUE_PROMPT, PromptRefused;
30840
+ var NATIVE_WORKSPACE_TOOLS, FINALE_TIMEOUT_MS2, CONTINUE_PROMPT, PromptRefused, GateBypassed;
30164
30841
  var init_harness = __esm({
30165
30842
  "../../src/core/harness/pi/harness.ts"() {
30166
30843
  "use strict";
@@ -30181,6 +30858,12 @@ var init_harness = __esm({
30181
30858
  this.name = "PromptRefused";
30182
30859
  }
30183
30860
  };
30861
+ GateBypassed = class extends Error {
30862
+ constructor(tool, callId) {
30863
+ super(`the gate was bypassed: pi ran ${tool} (call ${callId}) without asking the bot`);
30864
+ this.name = "GateBypassed";
30865
+ }
30866
+ };
30184
30867
  }
30185
30868
  });
30186
30869
 
@@ -30648,8 +31331,8 @@ var init_descriptionTurn = __esm({
30648
31331
  // ../../src/core/statusCardLabel.ts
30649
31332
  function quietSuffix(quietMs, inFlightTool) {
30650
31333
  if (quietMs <= QUIET_SUFFIX_AFTER_MS) return "";
30651
- const secs = Math.round(quietMs / 1e3);
30652
- return inFlightTool ? ` \u2014 running ${inFlightTool} (${secs}s)` : ` \u2014 thinking (${secs}s since last tool)`;
31334
+ const secs2 = Math.round(quietMs / 1e3);
31335
+ return inFlightTool ? ` \u2014 running ${inFlightTool} (${secs2}s)` : ` \u2014 thinking (${secs2}s since last tool)`;
30653
31336
  }
30654
31337
  function inFlightToolAfter(current, e) {
30655
31338
  if (e.type === "tool_call") return e.tool;
@@ -30701,7 +31384,7 @@ async function runLoop2(deps, ctx) {
30701
31384
  wait,
30702
31385
  parentRunId,
30703
31386
  coordinator,
30704
- fixRound
31387
+ seed
30705
31388
  } = ctx;
30706
31389
  const agent = budgetedAgent(ctx.agent, profile);
30707
31390
  const { executor, binding } = round2.selection;
@@ -30804,7 +31487,7 @@ async function runLoop2(deps, ctx) {
30804
31487
  handoff = h;
30805
31488
  ledgerRun?.setState({ handoff: h });
30806
31489
  };
30807
- const restoredDispositions = fixRound ? parseDispositionsInput({ dispositions: restored.dispositions })?.dispositions : void 0;
31490
+ const restoredDispositions = parseDispositionsInput({ dispositions: restored.dispositions })?.dispositions;
30808
31491
  let dispositions = restoredDispositions;
30809
31492
  const onDispositions = (d) => {
30810
31493
  dispositions = d;
@@ -30822,10 +31505,23 @@ async function runLoop2(deps, ctx) {
30822
31505
  let runDiagnosis;
30823
31506
  const releaseWorkspace = (span) => round2.release({ hardStopped: run2.control.requested === "hard", ...span ? { span } : {} });
30824
31507
  const attachFile = io.attachFile?.bind(io);
31508
+ const uploadTicket = io.uploadTicket?.bind(io);
31509
+ const nextStagedIndex = ctx.stagingIndex ?? stagingIndex();
31510
+ let artifactSeq = 0;
31511
+ const runUrl = runPageLink(run2.id);
31512
+ const artifacts = deps.artifacts ? {
31513
+ store: deps.artifacts,
31514
+ runId: run2.id,
31515
+ nextSeq: () => ++artifactSeq,
31516
+ ...runUrl ? { runUrl } : {},
31517
+ reply: (text) => io.reply(text)
31518
+ } : void 0;
30825
31519
  const toolContext = {
30826
31520
  executor,
30827
31521
  reportProgress,
30828
31522
  ...attachFile ? { attach: attachFile } : {},
31523
+ ...artifacts ? { artifacts } : {},
31524
+ ...uploadTicket ? { uploadTicket } : {},
30829
31525
  web: webCapability(),
30830
31526
  skills: deps.skills,
30831
31527
  github: githubCapabilityFor(deps, msg.userId),
@@ -30838,7 +31534,7 @@ async function runLoop2(deps, ctx) {
30838
31534
  onDigest,
30839
31535
  onPrDescription,
30840
31536
  onHandoff,
30841
- ...fixRound ? { knownFindingIds: fixRound.findingIds, onDispositions } : {}
31537
+ onDispositions
30842
31538
  };
30843
31539
  const harness = effectiveHarness(agent, deps.config.config.harness);
30844
31540
  try {
@@ -30899,6 +31595,7 @@ async function runLoop2(deps, ctx) {
30899
31595
  ...resume ? {
30900
31596
  resume: {
30901
31597
  messages: resume.plan.messages,
31598
+ compactions: resume.plan.compactions,
30902
31599
  settlements: resume.plan.settlements,
30903
31600
  remainingMs: resume.plan.remainingMs,
30904
31601
  turn: resume.plan.turn,
@@ -30927,6 +31624,23 @@ async function runLoop2(deps, ctx) {
30927
31624
  // operator stop from /runs
30928
31625
  inbox: admitted.inbox,
30929
31626
  // thread follow-ups steered into this run (thread-admission item 2)
31627
+ // A steered follow-up's staged files (record 0033): copied into the
31628
+ // store and pulled into this workspace before the model reads the turn.
31629
+ ...deps.artifacts ? {
31630
+ stageFollowUps: async (inputs) => {
31631
+ const files = inputs.flatMap((i) => i.staged ?? []);
31632
+ if (files.length === 0) return "";
31633
+ const staged = await stageIntoWorkspace(files, {
31634
+ store: deps.artifacts,
31635
+ threadKey: msg.threadKey,
31636
+ publish: (e) => registry.publish(run2.id, e),
31637
+ nextIndex: nextStagedIndex,
31638
+ executor,
31639
+ resident: round2.selection.resident !== void 0
31640
+ });
31641
+ return staged.line;
31642
+ }
31643
+ } : {},
30930
31644
  // The step record before each step's tools (run-history item 35).
30931
31645
  ...ledgerRun ? { onStep: ledgerRun.step.bind(ledgerRun) } : {},
30932
31646
  // A resume re-enters the loop from the plan (run-history item 37).
@@ -31150,7 +31864,8 @@ async function runLoop2(deps, ctx) {
31150
31864
  ...dispositions !== void 0 ? { dispositions } : {},
31151
31865
  ...reviewPost !== void 0 ? { reviewPost } : {},
31152
31866
  ...parentRunId !== void 0 ? { parentRunId } : {},
31153
- ...coordinator !== void 0 ? { coordinator } : {}
31867
+ ...coordinator !== void 0 ? { coordinator } : {},
31868
+ ...seed !== void 0 ? { seed } : {}
31154
31869
  });
31155
31870
  if (runFailed)
31156
31871
  await root.span(
@@ -31209,6 +31924,7 @@ var init_runLoop = __esm({
31209
31924
  init_secrets2();
31210
31925
  init_record();
31211
31926
  init_reply();
31927
+ init_staging();
31212
31928
  init_run2();
31213
31929
  }
31214
31930
  });
@@ -31459,6 +32175,12 @@ function openPlanCursor(graph, selected) {
31459
32175
  for (const id of order) visit(id, []);
31460
32176
  return cursor;
31461
32177
  }
32178
+ function matchDispositions(findings, dispositions) {
32179
+ const issued = new Set(findings.map((f) => f.id));
32180
+ const matched = dispositions.filter((d) => issued.has(d.findingId));
32181
+ const dropped = [...new Set(dispositions.filter((d) => !issued.has(d.findingId)).map((d) => d.findingId))];
32182
+ return { matched, dropped };
32183
+ }
31462
32184
  var MIN, SHIP_ROUND_RESERVE_MS, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, WAIT_MARGIN_MS, WAIT_CHUNK_MS, MERGE_POLL_MS, MERGE_WAIT_MAX_MS, BUSY_RETRY_MS;
31463
32185
  var init_coordinator = __esm({
31464
32186
  "../../src/core/ship/coordinator.ts"() {
@@ -31481,29 +32203,6 @@ var init_coordinator = __esm({
31481
32203
  }
31482
32204
  });
31483
32205
 
31484
- // ../../src/core/ship/codingChild.ts
31485
- function buildShipFixTurn(input) {
31486
- const findings = input.findings.map(formatFinding).join("\n") || "(the review listed no structured findings \u2014 address its prose)";
31487
- return `The review of ${input.where} requested changes. Load the \`address-review-findings\` skill and address EVERY finding below, nits included: record one disposition per finding with submit_dispositions (fixed|declined, with a note), squash to coherent commits, resubmit the PR description with submit_pr_description, and push the branch. Never merge and never approve.
31488
-
31489
- Findings:
31490
- ${findings}
31491
-
31492
- Review:
31493
- ${input.review}`;
31494
- }
31495
- function withContractInFirstUserTurn(messages, block) {
31496
- const at = messages.findIndex((m) => m.role === "user");
31497
- if (at < 0) return [...messages, { role: "user", content: [{ type: "text", text: block }] }];
31498
- return messages.map((m, i) => i === at ? { ...m, content: [...m.content, { type: "text", text: block }] } : m);
31499
- }
31500
- var init_codingChild = __esm({
31501
- "../../src/core/ship/codingChild.ts"() {
31502
- "use strict";
31503
- init_reviewVerdict();
31504
- }
31505
- });
31506
-
31507
32206
  // ../../src/core/ship/reviewChild.ts
31508
32207
  function buildShipReviewTurn(input) {
31509
32208
  const at = input.headSha ? ` at head \`${input.headSha}\`` : "";
@@ -31512,13 +32211,15 @@ function buildShipReviewTurn(input) {
31512
32211
  }
31513
32212
  const findings = input.prior.findings.map(formatFinding).join("\n") || "(none recorded)";
31514
32213
  const dispositions = input.prior.dispositions.map((d) => `${d.findingId}: ${d.disposition}${d.note ? ` \u2014 ${d.note}` : ""}`).join("\n") || "(none recorded)";
32214
+ const dropped = input.prior.dropped !== void 0 && input.prior.dropped.length > 0 ? `
32215
+ Dispositions naming no finding of the previous round (dropped): ${input.prior.dropped.join(", ")}` : "";
31515
32216
  return `Re-review pull request ${input.where}${at} \u2014 review round ${input.round} of this ship pipeline. Load the \`re-review-delta\` skill: narrow your READING to the delta since the previously reviewed head and verify each prior finding's disposition, but your verdict still covers the full diff against base. Carry every unresolved prior finding forward under its existing id.
31516
32217
 
31517
32218
  Previous round's findings:
31518
32219
  ${findings}
31519
32220
 
31520
32221
  Fix round's dispositions:
31521
- ${dispositions}`;
32222
+ ${dispositions}${dropped}`;
31522
32223
  }
31523
32224
  var init_reviewChild = __esm({
31524
32225
  "../../src/core/ship/reviewChild.ts"() {
@@ -31564,6 +32265,16 @@ async function contractFor(instance, unit, readers) {
31564
32265
  ...issue ? { issue } : {}
31565
32266
  });
31566
32267
  }
32268
+ function findingsRequest(input) {
32269
+ const findings = input.findings.map(formatFinding).join("\n") || "(the review listed no structured findings, address its prose)";
32270
+ return `The review of ${input.where} requested changes. Load the \`address-review-findings\` skill and address every finding below, nits included: record one disposition per finding with submit_dispositions (fixed or declined, with a note), squash to coherent commits, resubmit the pull request description with submit_pr_description, and push the branch. Never merge and never approve.
32271
+
32272
+ Findings:
32273
+ ${findings}
32274
+
32275
+ Review:
32276
+ ${input.review}`;
32277
+ }
31567
32278
  async function composeChild(brief, instance, unit, readers) {
31568
32279
  const where = `${instance.repo}#${"pr" in brief ? brief.pr : ""}`;
31569
32280
  switch (brief.kind) {
@@ -31573,10 +32284,13 @@ async function composeChild(brief, instance, unit, readers) {
31573
32284
  return { preset: "coding", prompt, ref: unit.branch, contract };
31574
32285
  }
31575
32286
  case "review": {
31576
- const prior = brief.prior !== void 0 ? {
31577
- findings: (await facts(readers, brief.prior.reviewRunId)).findings ?? [],
31578
- dispositions: brief.prior.fixRunId !== void 0 ? (await facts(readers, brief.prior.fixRunId)).dispositions ?? [] : []
31579
- } : void 0;
32287
+ let prior;
32288
+ if (brief.prior !== void 0) {
32289
+ const findings = (await facts(readers, brief.prior.reviewRunId)).findings ?? [];
32290
+ const recorded = brief.prior.codingRunId !== void 0 ? (await facts(readers, brief.prior.codingRunId)).dispositions ?? [] : [];
32291
+ const { matched, dropped } = matchDispositions(findings, recorded);
32292
+ prior = { findings, dispositions: matched, dropped };
32293
+ }
31580
32294
  const turn = buildShipReviewTurn({
31581
32295
  where,
31582
32296
  round: brief.round,
@@ -31588,16 +32302,18 @@ async function composeChild(brief, instance, unit, readers) {
31588
32302
 
31589
32303
  ${turn}`, contract };
31590
32304
  }
31591
- case "fix": {
32305
+ case "findings": {
31592
32306
  const review = await facts(readers, brief.reviewRunId);
31593
- const findings = review.findings ?? [];
31594
32307
  return {
31595
32308
  preset: "coding",
31596
- prompt: buildShipFixTurn({ where, findings, review: review.finalReply ?? "" }),
31597
- ref: unit.branch,
31598
- fixRound: { findingIds: findings.map((f) => f.id) }
32309
+ prompt: findingsRequest({ where, findings: review.findings ?? [], review: review.finalReply ?? "" }),
32310
+ ref: unit.branch
31599
32311
  };
31600
32312
  }
32313
+ default: {
32314
+ const unknown = brief;
32315
+ throw new Error(`unknown brief kind ${JSON.stringify(unknown.kind)}`);
32316
+ }
31601
32317
  }
31602
32318
  }
31603
32319
  async function facts(readers, runId3) {
@@ -31610,7 +32326,8 @@ var init_briefs = __esm({
31610
32326
  "../../src/core/coordinator/briefs.ts"() {
31611
32327
  "use strict";
31612
32328
  init_directives();
31613
- init_codingChild();
32329
+ init_reviewVerdict();
32330
+ init_coordinator();
31614
32331
  init_contract();
31615
32332
  init_preflight();
31616
32333
  init_reviewChild();
@@ -31675,7 +32392,7 @@ async function plan(deps, input) {
31675
32392
  try {
31676
32393
  planId = planIdOf(request.planPath);
31677
32394
  } catch (err2) {
31678
- return { ok: false, reply: `\u{1F6AB} ${describe3(err2)}` };
32395
+ return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
31679
32396
  }
31680
32397
  let text;
31681
32398
  try {
@@ -31683,7 +32400,7 @@ async function plan(deps, input) {
31683
32400
  } catch (err2) {
31684
32401
  return {
31685
32402
  ok: false,
31686
- reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe3(err2)}`
32403
+ reply: `\u{1F6AB} The plan \`${request.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe5(err2)}`
31687
32404
  };
31688
32405
  }
31689
32406
  const graph = parsePlanGraph(text, planId);
@@ -31696,7 +32413,7 @@ async function plan(deps, input) {
31696
32413
  try {
31697
32414
  selected = openPlanCursor(graph, request.units).order;
31698
32415
  } catch (err2) {
31699
- return { ok: false, reply: `\u{1F6AB} ${describe3(err2)}` };
32416
+ return { ok: false, reply: `\u{1F6AB} ${describe5(err2)}` };
31700
32417
  }
31701
32418
  return { ok: true, planned: { kind: "plan", planId, path: request.planPath, base, graph, selected, identity: identity2 } };
31702
32419
  }
@@ -31812,7 +32529,7 @@ async function start(deps, input, instance, units, where, write) {
31812
32529
  try {
31813
32530
  answer = await deps.create(instance.id);
31814
32531
  } catch (err2) {
31815
- answer = { kind: "unanswered", reason: describe3(err2) };
32532
+ answer = { kind: "unanswered", reason: describe5(err2) };
31816
32533
  }
31817
32534
  switch (answer.kind) {
31818
32535
  case "created": {
@@ -31834,7 +32551,7 @@ async function start(deps, input, instance, units, where, write) {
31834
32551
  );
31835
32552
  }
31836
32553
  }
31837
- var refused3, describe3, RUNNING, ENDED;
32554
+ var refused3, describe5, RUNNING, ENDED;
31838
32555
  var init_handOff = __esm({
31839
32556
  "../../src/core/coordinator/handOff.ts"() {
31840
32557
  "use strict";
@@ -31842,7 +32559,7 @@ var init_handOff = __esm({
31842
32559
  init_coordinator();
31843
32560
  init_briefs();
31844
32561
  refused3 = (reply) => ({ status: "aborted", reply });
31845
- describe3 = (err2) => err2 instanceof Error ? err2.message : String(err2);
32562
+ describe5 = (err2) => err2 instanceof Error ? err2.message : String(err2);
31846
32563
  RUNNING = /* @__PURE__ */ new Set(["queued", "running", "paused", "waiting", "waitingForPause"]);
31847
32564
  ENDED = /* @__PURE__ */ new Set(["complete", "errored", "terminated"]);
31848
32565
  }
@@ -32459,6 +33176,18 @@ var init_ship = __esm({
32459
33176
  }
32460
33177
  });
32461
33178
 
33179
+ // ../../src/core/ship/codingChild.ts
33180
+ function withContractInFirstUserTurn(messages, block) {
33181
+ const at = messages.findIndex((m) => m.role === "user");
33182
+ if (at < 0) return [...messages, { role: "user", content: [{ type: "text", text: block }] }];
33183
+ return messages.map((m, i) => i === at ? { ...m, content: [...m.content, { type: "text", text: block }] } : m);
33184
+ }
33185
+ var init_codingChild = __esm({
33186
+ "../../src/core/ship/codingChild.ts"() {
33187
+ "use strict";
33188
+ }
33189
+ });
33190
+
32462
33191
  // ../../src/core/dispatch/settle.ts
32463
33192
  function settleThread(deps, ctx) {
32464
33193
  const { msg, admitted, runLoopStarted, control } = ctx;
@@ -32533,22 +33262,22 @@ function createRunEnding(deps) {
32533
33262
  try {
32534
33263
  deps.registry.seal(id, replyOk === void 0 ? {} : { replyOk });
32535
33264
  } catch (err2) {
32536
- log(`[ending] seal ${id}: ${describe4(err2)}`);
33265
+ log(`[ending] seal ${id}: ${describe6(err2)}`);
32537
33266
  }
32538
33267
  try {
32539
33268
  afterSeal?.();
32540
33269
  } catch (err2) {
32541
- log(`[ending] after seal ${id}: ${describe4(err2)}`);
33270
+ log(`[ending] after seal ${id}: ${describe6(err2)}`);
32542
33271
  }
32543
33272
  }
32544
33273
  for (const entry of pending2.splice(0)) {
32545
33274
  try {
32546
33275
  const result = deps.registry.seal(entry.runId);
32547
33276
  void Promise.resolve(entry.write(result, entry.failedAfterFinish)).catch(
32548
- (err2) => log(`[ending] record ${entry.runId}: ${describe4(err2)}`)
33277
+ (err2) => log(`[ending] record ${entry.runId}: ${describe6(err2)}`)
32549
33278
  );
32550
33279
  } catch (err2) {
32551
- log(`[ending] record ${entry.runId}: ${describe4(err2)}`);
33280
+ log(`[ending] record ${entry.runId}: ${describe6(err2)}`);
32552
33281
  }
32553
33282
  }
32554
33283
  };
@@ -32558,7 +33287,7 @@ function createRunEnding(deps) {
32558
33287
  try {
32559
33288
  deps.onFinished?.(runId3);
32560
33289
  } catch (err2) {
32561
- log(`[ending] finished ${runId3}: ${describe4(err2)}`);
33290
+ log(`[ending] finished ${runId3}: ${describe6(err2)}`);
32562
33291
  }
32563
33292
  },
32564
33293
  register(entry) {
@@ -32586,7 +33315,7 @@ function createRunEnding(deps) {
32586
33315
  }
32587
33316
  };
32588
33317
  }
32589
- function describe4(err2) {
33318
+ function describe6(err2) {
32590
33319
  return err2 instanceof Error ? err2.message : String(err2);
32591
33320
  }
32592
33321
  var init_runEnding = __esm({
@@ -32715,6 +33444,7 @@ async function dispatch(deps, msg, io, opts = {}) {
32715
33444
  if (outcome.kind === "redispatch")
32716
33445
  return dispatch(deps, msg, io, {
32717
33446
  ...opts.parent ? { parent: opts.parent } : {},
33447
+ ...opts.seed ? { seed: opts.seed } : {},
32718
33448
  ...opts.coordinator ? { coordinator: opts.coordinator } : {}
32719
33449
  });
32720
33450
  if (outcome.kind !== "proceed") return ended;
@@ -32784,7 +33514,8 @@ async function dispatch(deps, msg, io, opts = {}) {
32784
33514
  }
32785
33515
  const contractBlock = opts.contract ? renderContract(opts.contract, { maxChars: DEFAULT_CONTRACT_MAX_CHARS }).text : void 0;
32786
33516
  const requestText = route3?.parts ? compoundBrief(directives.text, route3.parts) : directives.text;
32787
- const built = buildMessages(history, requestText, msg.images, msg.documents);
33517
+ const seed = opts.seed ? "parent" : "channel";
33518
+ const built = buildMessages(opts.seed ?? history, requestText, msg.images, msg.documents);
32788
33519
  const messages = resume ? resume.plan.messages : contractBlock !== void 0 && agent.name !== "review" ? withContractInFirstUserTurn(built, contractBlock) : built;
32789
33520
  const headPreflight = await authorizePrHead({
32790
33521
  msg,
@@ -32821,11 +33552,21 @@ async function dispatch(deps, msg, io, opts = {}) {
32821
33552
  admitted,
32822
33553
  parentRunId,
32823
33554
  coordinator,
33555
+ seed,
33556
+ ...opts.seed ? { seedTurns: opts.seed } : {},
32824
33557
  agentSource,
32825
33558
  ...routeEvent ? { route: routeEvent } : {}
32826
33559
  });
32827
33560
  const { run: run2, runId: runId3, channelVisibility, liveUrl, publishText, publishMeta } = registration;
32828
33561
  registered = run2;
33562
+ const staged = !resume && deps.artifacts && msg.staged && msg.staged.length > 0 ? msg.staged : [];
33563
+ const nextStagedIndex = stagingIndex();
33564
+ const stagedCopies = staged.length > 0 && agent.machine !== "none" ? copyStaged(staged, {
33565
+ store: deps.artifacts,
33566
+ threadKey: msg.threadKey,
33567
+ nextIndex: nextStagedIndex,
33568
+ publish: (e) => registry.publish(runId3, e)
33569
+ }) : void 0;
32829
33570
  const reservation = await reserveRun(deps, {
32830
33571
  msg,
32831
33572
  agent,
@@ -32843,7 +33584,8 @@ async function dispatch(deps, msg, io, opts = {}) {
32843
33584
  admitted,
32844
33585
  root,
32845
33586
  parentRunId,
32846
- coordinator
33587
+ coordinator,
33588
+ seed
32847
33589
  });
32848
33590
  if (reservation) {
32849
33591
  reserved = reservation.reserved;
@@ -32873,6 +33615,22 @@ async function dispatch(deps, msg, io, opts = {}) {
32873
33615
  });
32874
33616
  return ended;
32875
33617
  }
33618
+ if (staged.length > 0) {
33619
+ const line3 = stagedCopies ? attachmentsLine(
33620
+ await pullStaged(await stagedCopies, {
33621
+ store: deps.artifacts,
33622
+ executor,
33623
+ resident: resident !== void 0
33624
+ })
33625
+ ) : noWorkspaceLine(staged);
33626
+ const request = messages[messages.length - 1];
33627
+ if (request && request.role === "user" && line3) {
33628
+ request.content = Array.isArray(request.content) ? [...request.content, { type: "text", text: line3 }] : [
33629
+ { type: "text", text: request.content },
33630
+ { type: "text", text: line3 }
33631
+ ];
33632
+ }
33633
+ }
32876
33634
  const bearer = mintRunBearer(deps, { runId: runId3, agent, profile, resolved, registry, root, clock: clock3 });
32877
33635
  const headGate = await authorizeAttachedHead(deps, {
32878
33636
  msg,
@@ -32937,7 +33695,8 @@ async function dispatch(deps, msg, io, opts = {}) {
32937
33695
  registry,
32938
33696
  resume,
32939
33697
  parentRunId,
32940
- coordinator
33698
+ coordinator,
33699
+ seed
32941
33700
  });
32942
33701
  ledgerRun = await claimRun(deps, {
32943
33702
  msg,
@@ -32960,7 +33719,8 @@ async function dispatch(deps, msg, io, opts = {}) {
32960
33719
  clock: clock3,
32961
33720
  root,
32962
33721
  parentRunId,
32963
- coordinator
33722
+ coordinator,
33723
+ seed
32964
33724
  });
32965
33725
  const { spawn: spawn7, runs, steer, wait } = runToolCapabilities(
32966
33726
  { core: deps, dispatch, registry, clock: clock3 },
@@ -32980,6 +33740,7 @@ async function dispatch(deps, msg, io, opts = {}) {
32980
33740
  agent,
32981
33741
  profile,
32982
33742
  resolved,
33743
+ stagingIndex: nextStagedIndex,
32983
33744
  provider,
32984
33745
  model,
32985
33746
  messages,
@@ -33013,7 +33774,7 @@ async function dispatch(deps, msg, io, opts = {}) {
33013
33774
  wait,
33014
33775
  parentRunId,
33015
33776
  coordinator,
33016
- ...opts.fixRound ? { fixRound: opts.fixRound } : {},
33777
+ seed,
33017
33778
  ...bearer !== void 0 ? { bearer } : {}
33018
33779
  });
33019
33780
  const { answer, prNote, toolCalls, runDiagnosis, checklistAsLeft, checklistCheckedOff, releaseWorkspace } = ran;
@@ -33128,6 +33889,7 @@ var init_dispatcher = __esm({
33128
33889
  init_route();
33129
33890
  init_authorize2();
33130
33891
  init_messages();
33892
+ init_staging();
33131
33893
  init_provision();
33132
33894
  init_run2();
33133
33895
  init_runLoop();
@@ -33146,7 +33908,7 @@ var init_dispatcher = __esm({
33146
33908
  });
33147
33909
 
33148
33910
  // ../../src/core/runHistoryWriter.ts
33149
- function describe5(err2) {
33911
+ function describe7(err2) {
33150
33912
  return err2 instanceof Error ? err2.message : String(err2);
33151
33913
  }
33152
33914
  function createRunHistoryWriter(opts) {
@@ -33163,7 +33925,7 @@ function createRunHistoryWriter(opts) {
33163
33925
  try {
33164
33926
  opts.onPersisted?.(id);
33165
33927
  } catch (err2) {
33166
- opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe5(err2)}`);
33928
+ opts.warn(`[run-history] onPersisted hook failed for ${id}: ${describe7(err2)}`);
33167
33929
  }
33168
33930
  };
33169
33931
  const attemptAll = async (record2, flag2, sink, span) => {
@@ -33186,12 +33948,12 @@ function createRunHistoryWriter(opts) {
33186
33948
  }
33187
33949
  if (err2 instanceof PermanentStoreError) {
33188
33950
  failures++;
33189
- opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe5(err2)}`);
33951
+ opts.warn(`[run-history] ${record2.id} not persisted (permanent, not retried): ${describe7(err2)}`);
33190
33952
  return;
33191
33953
  }
33192
33954
  if (attempt >= attempts) {
33193
33955
  failures++;
33194
- opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe5(err2)}`);
33956
+ opts.warn(`[run-history] ${record2.id} not persisted after ${attempts} attempts: ${describe7(err2)}`);
33195
33957
  return;
33196
33958
  }
33197
33959
  await sleep2(jittered(RUN_HISTORY_RETRY_DELAYS_MS[attempt - 1]));
@@ -33211,7 +33973,7 @@ function createRunHistoryWriter(opts) {
33211
33973
  }
33212
33974
  const p = attemptAll(record2, flag2, writeOpts?.via ?? opts.store, writeOpts?.span).catch((err2) => {
33213
33975
  failures++;
33214
- opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe5(err2)}`);
33976
+ opts.warn(`[run-history] ${record2.id} writer failed unexpectedly: ${describe7(err2)}`);
33215
33977
  }).finally(() => {
33216
33978
  if (flag2) provisionalFlags.delete(flag2);
33217
33979
  void inFlight.delete(p);
@@ -33288,7 +34050,12 @@ function toListItem(record2, bytes) {
33288
34050
  return { ...rest, bytes };
33289
34051
  }
33290
34052
  function retentionPolicyOf(cfg) {
33291
- return clampRetentionPolicy({ retentionDays: cfg.retentionDays, maxRuns: cfg.maxRuns, maxBytes: cfg.maxBytes });
34053
+ return clampRetentionPolicy({
34054
+ retentionDays: cfg.retentionDays,
34055
+ maxRuns: cfg.maxRuns,
34056
+ maxBytes: cfg.maxBytes,
34057
+ sessionLogMaxBytes: cfg.sessionLogMaxBytes
34058
+ });
33292
34059
  }
33293
34060
  function buildRunStore(cfg, secrets, deps) {
33294
34061
  if (!cfg) return null;
@@ -33532,6 +34299,24 @@ var init_flusher = __esm({
33532
34299
  }
33533
34300
  });
33534
34301
 
34302
+ // ../../src/core/runLedger/sessionLog.ts
34303
+ function sessionKey(threadKey, agent) {
34304
+ return `${threadKey}:${agent ?? "-"}`;
34305
+ }
34306
+ function requestIndex(seed) {
34307
+ for (let i = seed.length - 1; i >= 0; i--) if (seed[i].role === "user") return i;
34308
+ return 0;
34309
+ }
34310
+ var DEFAULT_SESSION_LOG_MAX_BYTES;
34311
+ var init_sessionLog = __esm({
34312
+ "../../src/core/runLedger/sessionLog.ts"() {
34313
+ "use strict";
34314
+ init_runRecord();
34315
+ init_runRecord();
34316
+ DEFAULT_SESSION_LOG_MAX_BYTES = DEFAULT_RETENTION_POLICY.sessionLogMaxBytes;
34317
+ }
34318
+ });
34319
+
33535
34320
  // ../../src/core/runLedger/types.ts
33536
34321
  var LEASE_MS, HEARTBEAT_MS, APPEND_FLUSH_MS, APPEND_FLUSH_EVENTS, TRANSCRIPT_REQUEST_BYTES, TRANSCRIPT_PART_BYTES, ATTACHMENT_REF_BYTES, GEN_PATTERN;
33537
34322
  var init_types5 = __esm({
@@ -33577,36 +34362,45 @@ function createLedgerWriteThrough(opts) {
33577
34362
  "[ledger] state Worker has no run-ledger routes \u2014 deploy it before this bot version; runs are not tracked until then"
33578
34363
  );
33579
34364
  };
33580
- async function claim(req, phase) {
34365
+ async function claim(req, opts2 = {}) {
33581
34366
  for (let attempt = 1; ; attempt++) {
33582
34367
  try {
34368
+ let session;
34369
+ if (opts2.seed) {
34370
+ const key = sessionKey(req.threadKey, req.meta.agent);
34371
+ const next = await ledger.sessionTail(key);
34372
+ session = { key, seedFrom: next, request: next + requestIndex(opts2.seed), range: { from: next } };
34373
+ }
33583
34374
  const result = await ledger.claim({
33584
34375
  runId: req.runId,
33585
34376
  threadKey: req.threadKey,
33586
34377
  gen,
33587
34378
  leaseMs,
33588
34379
  startedAt: req.startedAt,
33589
- meta: req.meta,
34380
+ meta: session ? { ...req.meta, session } : req.meta,
33590
34381
  card: req.card ?? null,
33591
34382
  system: req.system,
33592
34383
  tools: req.tools,
33593
34384
  ...req.state !== void 0 ? { state: req.state } : {},
33594
- ...phase ? { phase } : {}
34385
+ ...opts2.phase ? { phase: opts2.phase } : {}
33595
34386
  });
33596
- if (result.ok) return "ok";
33597
- if (result.live.runId === req.runId) return "fenced";
34387
+ if (result.ok) {
34388
+ if (session) await ledger.claimSession(session.key, req.runId, gen);
34389
+ return session ? { outcome: "ok", session } : { outcome: "ok" };
34390
+ }
34391
+ if (result.live.runId === req.runId) return { outcome: "fenced" };
33598
34392
  warn(
33599
34393
  `[ledger] ${req.threadKey} not tracked: the thread's live row belongs to run ${result.live.runId} (started ${new Date(result.live.startedAt).toISOString()}) \u2014 reclaim is the resume phase's`
33600
34394
  );
33601
- return "untracked";
34395
+ return { outcome: "untracked" };
33602
34396
  } catch (err2) {
33603
34397
  if (err2 instanceof RouteMissingError) {
33604
34398
  routeMissing();
33605
- return "untracked";
34399
+ return { outcome: "untracked" };
33606
34400
  }
33607
34401
  if (err2 instanceof PermanentStoreError || attempt >= claimAttempts) {
33608
- warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe6(err2)}`);
33609
- return "untracked";
34402
+ warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe8(err2)}`);
34403
+ return { outcome: "untracked" };
33610
34404
  }
33611
34405
  await sleep2(RETRY_MS[Math.min(attempt, RETRY_MS.length) - 1]);
33612
34406
  }
@@ -33626,6 +34420,15 @@ function createLedgerWriteThrough(opts) {
33626
34420
  seeded = false;
33627
34421
  adopted = false;
33628
34422
  handedOff = false;
34423
+ /** The run's place in its session log (session-log item 2); undefined for
34424
+ * a run without a conversation of its own and for an adopted row claimed
34425
+ * before the log existed. */
34426
+ session;
34427
+ /** Turns of the run's conversation on the ledger, counted from its seed —
34428
+ * the last step record's `turnIndex` — so the finish can close the range. */
34429
+ turnsWritten;
34430
+ /** A detach left the log short of what the model saw: the record says `broken`. */
34431
+ broken = false;
33629
34432
  stepNo;
33630
34433
  lastSeq;
33631
34434
  state;
@@ -33646,7 +34449,10 @@ function createLedgerWriteThrough(opts) {
33646
34449
  },
33647
34450
  ...opts.schedule ? { schedule: opts.schedule } : {}
33648
34451
  });
33649
- constructor(req, from = { stepNo: 0, lastSeq: 0 }) {
34452
+ constructor(req, from = {
34453
+ stepNo: 0,
34454
+ lastSeq: 0
34455
+ }) {
33650
34456
  this.runId = req.runId;
33651
34457
  this.threadKey = req.threadKey;
33652
34458
  this.state = req.state ?? {};
@@ -33655,6 +34461,11 @@ function createLedgerWriteThrough(opts) {
33655
34461
  this.stepNo = from.stepNo;
33656
34462
  this.lastSeq = from.lastSeq;
33657
34463
  this.adopted = from.resumable === true;
34464
+ this.session = from.session;
34465
+ }
34466
+ /** A reservation learns its session when the prompt's claim promotes it. */
34467
+ bindSession(session) {
34468
+ this.session = session;
33658
34469
  }
33659
34470
  get resumable() {
33660
34471
  return !this.detached && (this.seeded || this.adopted);
@@ -33662,11 +34473,24 @@ function createLedgerWriteThrough(opts) {
33662
34473
  markHandedOff() {
33663
34474
  this.handedOff = true;
33664
34475
  }
34476
+ /** The run's range as the finished record carries it (session-log item 2):
34477
+ * closed at the last turn written, or `broken` after a detach — the log
34478
+ * ends short of what the model saw, and the next run in the session must
34479
+ * know. A run that wrote no turn leaves the range open. */
34480
+ sessionOnRecord() {
34481
+ if (!this.session) return void 0;
34482
+ if (this.broken) return { ...this.session, range: "broken" };
34483
+ const from = this.session.range === "broken" ? this.session.seedFrom : this.session.range.from;
34484
+ const to = this.turnsWritten !== void 0 && this.turnsWritten > 0 ? this.session.seedFrom + this.turnsWritten - 1 : void 0;
34485
+ return { ...this.session, range: to !== void 0 && to >= from ? { from, to } : { from } };
34486
+ }
33665
34487
  sink = {
33666
- put: async (record2) => {
34488
+ put: async (plain4) => {
33667
34489
  this.finished = true;
33668
34490
  live.delete(this);
33669
34491
  await this.close();
34492
+ const session = this.sessionOnRecord();
34493
+ const record2 = session ? { ...plain4, session } : plain4;
33670
34494
  let result;
33671
34495
  try {
33672
34496
  result = await ledger.finish(this.runId, gen, record2);
@@ -33687,6 +34511,7 @@ function createLedgerWriteThrough(opts) {
33687
34511
  detach(reason) {
33688
34512
  if (this.detached) return;
33689
34513
  this.detached = true;
34514
+ this.broken = true;
33690
34515
  warn(`[ledger] ${this.threadKey} run ${this.runId} detached: ${reason} \u2014 this run is not resumable`);
33691
34516
  this.stopHeartbeat();
33692
34517
  live.delete(this);
@@ -33704,13 +34529,15 @@ function createLedgerWriteThrough(opts) {
33704
34529
  * no record at all was killed before its conversation was stored and
33705
34530
  * closes `interrupted`. */
33706
34531
  async seed(messages, budgetMs) {
33707
- const turns = messages.map((message2, idx) => ({ idx, message: message2 }));
34532
+ const base = this.session?.seedFrom ?? 0;
34533
+ const turns = messages.map((message2, i) => ({ idx: base + i, message: message2 }));
33708
34534
  try {
33709
- const seeded = await ledger.seed(this.runId, gen, turns);
34535
+ const seeded = await ledger.seed(this.runId, gen, turns, this.session?.key);
33710
34536
  if (!seeded.ok) {
33711
34537
  this.detach(`seed refused (${seeded.reason})`);
33712
34538
  return;
33713
34539
  }
34540
+ this.turnsWritten = messages.length;
33714
34541
  const recorded = await ledger.step(
33715
34542
  this.runId,
33716
34543
  gen,
@@ -33729,16 +34556,19 @@ function createLedgerWriteThrough(opts) {
33729
34556
  if (!recorded.ok) this.detach(`seed record refused (${recorded.reason})`);
33730
34557
  else this.seeded = true;
33731
34558
  } catch (err2) {
33732
- this.detach(`seed failed: ${describe6(err2)}`);
34559
+ this.detach(`seed failed: ${describe8(err2)}`);
33733
34560
  }
33734
34561
  }
33735
34562
  async step(report2) {
33736
34563
  if (this.detached) return;
33737
- const turns = report2.turns.map((message2, i) => ({ idx: report2.firstIdx + i, message: message2 }));
34564
+ const base = this.session?.seedFrom ?? 0;
34565
+ const turns = report2.turns.map((message2, i) => ({ idx: base + report2.firstIdx + i, message: message2 }));
34566
+ if (report2.compaction)
34567
+ turns.push({ idx: base + report2.firstIdx + report2.turns.length, compaction: report2.compaction });
33738
34568
  const record2 = {
33739
34569
  step: ++this.stepNo,
33740
34570
  seq: this.lastSeq,
33741
- turnIndex: report2.firstIdx + report2.turns.length,
34571
+ turnIndex: report2.firstIdx + turns.length,
33742
34572
  inFlight: report2.inFlight,
33743
34573
  inboxConsumedSeq: report2.inboxConsumedSeq,
33744
34574
  remainingMs: report2.remainingMs,
@@ -33747,12 +34577,13 @@ function createLedgerWriteThrough(opts) {
33747
34577
  };
33748
34578
  for (let attempt = 1; ; attempt++) {
33749
34579
  try {
33750
- const result = await ledger.step(this.runId, gen, record2, turns);
34580
+ const result = await ledger.step(this.runId, gen, record2, turns, this.session?.key);
33751
34581
  if (!result.ok) this.detach(`step ${record2.step} refused (${result.reason})`);
34582
+ else this.turnsWritten = record2.turnIndex;
33752
34583
  return;
33753
34584
  } catch (err2) {
33754
34585
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
33755
- this.detach(`step ${record2.step} failed: ${describe6(err2)}`);
34586
+ this.detach(`step ${record2.step} failed: ${describe8(err2)}`);
33756
34587
  return;
33757
34588
  }
33758
34589
  await sleep2(RETRY_MS[0]);
@@ -33785,7 +34616,7 @@ function createLedgerWriteThrough(opts) {
33785
34616
  } catch (err2) {
33786
34617
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
33787
34618
  this.stateDirty = true;
33788
- warn(`[ledger] ${this.threadKey} state not written: ${describe6(err2)}`);
34619
+ warn(`[ledger] ${this.threadKey} state not written: ${describe8(err2)}`);
33789
34620
  return;
33790
34621
  }
33791
34622
  await sleep2(RETRY_MS[0]);
@@ -33803,7 +34634,7 @@ function createLedgerWriteThrough(opts) {
33803
34634
  this.detach(`finishing refused (fenced)`);
33804
34635
  return "fenced";
33805
34636
  } catch (err2) {
33806
- warn(`[ledger] ${this.threadKey} finishing failed: ${describe6(err2)}`);
34637
+ warn(`[ledger] ${this.threadKey} finishing failed: ${describe8(err2)}`);
33807
34638
  return "unavailable";
33808
34639
  }
33809
34640
  }
@@ -33818,7 +34649,7 @@ function createLedgerWriteThrough(opts) {
33818
34649
  warn(`[ledger] ${this.threadKey} abandon refused (fenced) \u2014 the row is another generation's`);
33819
34650
  } catch (err2) {
33820
34651
  warn(
33821
- `[ledger] ${this.threadKey} abandon failed: ${describe6(err2)} \u2014 the sweep will take the row once its lease lapses`
34652
+ `[ledger] ${this.threadKey} abandon failed: ${describe8(err2)} \u2014 the sweep will take the row once its lease lapses`
33822
34653
  );
33823
34654
  }
33824
34655
  }
@@ -33839,7 +34670,7 @@ function createLedgerWriteThrough(opts) {
33839
34670
  this.onStop(result.stop);
33840
34671
  }
33841
34672
  } catch (err2) {
33842
- warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe6(err2)}`);
34673
+ warn(`[ledger] ${this.threadKey} heartbeat failed: ${describe8(err2)}`);
33843
34674
  }
33844
34675
  }
33845
34676
  stopHeartbeat() {
@@ -33856,8 +34687,8 @@ function createLedgerWriteThrough(opts) {
33856
34687
  return {
33857
34688
  gen,
33858
34689
  async reserve(req) {
33859
- const claimed2 = await claim({ ...req, system: "", tools: [], state: {} }, "attaching");
33860
- if (claimed2 !== "ok") return void 0;
34690
+ const claimed2 = await claim({ ...req, system: "", tools: [], state: {} }, { phase: "attaching" });
34691
+ if (claimed2.outcome !== "ok") return void 0;
33861
34692
  const run2 = new TrackedRun(req);
33862
34693
  run2.startHeartbeat();
33863
34694
  live.add(run2);
@@ -33865,35 +34696,46 @@ function createLedgerWriteThrough(opts) {
33865
34696
  },
33866
34697
  async open(req) {
33867
34698
  const reserved = req.reservation;
34699
+ const seed = req.seed?.messages;
33868
34700
  if (reserved instanceof TrackedRun) {
33869
34701
  if (!reserved.tracked()) return void 0;
33870
- const claimed3 = await claim(req);
33871
- if (claimed3 === "fenced") {
34702
+ const claimed3 = await claim(req, seed ? { seed } : {});
34703
+ if (claimed3.outcome === "fenced") {
33872
34704
  reserved.detach("promotion refused (fenced)");
33873
34705
  return void 0;
33874
34706
  }
33875
- if (claimed3 !== "ok") {
34707
+ if (claimed3.outcome !== "ok") {
33876
34708
  await reserved.close();
33877
34709
  live.delete(reserved);
33878
34710
  return void 0;
33879
34711
  }
34712
+ if (claimed3.session) reserved.bindSession(claimed3.session);
33880
34713
  if (req.seed) await reserved.seed(req.seed.messages, req.seed.budgetMs);
33881
34714
  return reserved;
33882
34715
  }
33883
- const claimed2 = await claim(req);
33884
- if (claimed2 === "fenced") {
34716
+ const claimed2 = await claim(req, seed ? { seed } : {});
34717
+ if (claimed2.outcome === "fenced") {
33885
34718
  warn(`[ledger] ${req.threadKey} not tracked: run ${req.runId} is live under another generation`);
33886
34719
  return void 0;
33887
34720
  }
33888
- if (claimed2 !== "ok") return void 0;
33889
- const run2 = new TrackedRun(req);
34721
+ if (claimed2.outcome !== "ok") return void 0;
34722
+ const run2 = new TrackedRun(req, {
34723
+ stepNo: 0,
34724
+ lastSeq: 0,
34725
+ ...claimed2.session ? { session: claimed2.session } : {}
34726
+ });
33890
34727
  if (req.seed) await run2.seed(req.seed.messages, req.seed.budgetMs);
33891
34728
  run2.startHeartbeat();
33892
34729
  live.add(run2);
33893
34730
  return run2;
33894
34731
  },
33895
34732
  adopt(req) {
33896
- const run2 = new TrackedRun(req, { stepNo: req.lastStep, lastSeq: req.lastSeq, resumable: true });
34733
+ const run2 = new TrackedRun(req, {
34734
+ stepNo: req.lastStep,
34735
+ lastSeq: req.lastSeq,
34736
+ resumable: true,
34737
+ ...req.session ? { session: req.session } : {}
34738
+ });
33897
34739
  run2.startHeartbeat();
33898
34740
  live.add(run2);
33899
34741
  return run2;
@@ -33906,7 +34748,7 @@ function createLedgerWriteThrough(opts) {
33906
34748
  warn(`[ledger] inbox push refused for run ${runId3}: no live row \u2014 the follow-up rides in memory only`);
33907
34749
  return void 0;
33908
34750
  } catch (err2) {
33909
- warn(`[ledger] inbox push failed for run ${runId3}: ${describe6(err2)} \u2014 the follow-up rides in memory only`);
34751
+ warn(`[ledger] inbox push failed for run ${runId3}: ${describe8(err2)} \u2014 the follow-up rides in memory only`);
33910
34752
  return void 0;
33911
34753
  }
33912
34754
  },
@@ -33915,7 +34757,7 @@ function createLedgerWriteThrough(opts) {
33915
34757
  return await ledger.readInbox(runId3, afterSeq);
33916
34758
  } catch (err2) {
33917
34759
  warn(
33918
- err2 instanceof RouteMissingError ? `[ledger] state Worker has no inbox read route \u2014 run ${runId3} resumes with the reclaim's inbox snapshot only` : `[ledger] inbox read failed for run ${runId3}: ${describe6(err2)} \u2014 resuming with the reclaim's snapshot only`
34760
+ err2 instanceof RouteMissingError ? `[ledger] state Worker has no inbox read route \u2014 run ${runId3} resumes with the reclaim's inbox snapshot only` : `[ledger] inbox read failed for run ${runId3}: ${describe8(err2)} \u2014 resuming with the reclaim's snapshot only`
33919
34761
  );
33920
34762
  return [];
33921
34763
  }
@@ -33932,17 +34774,18 @@ function createLedgerWriteThrough(opts) {
33932
34774
  for (const r of candidates) if (markedSet.has(r.runId)) r.markHandedOff();
33933
34775
  return { marked };
33934
34776
  } catch (err2) {
33935
- return { marked: [], failed: describe6(err2) };
34777
+ return { marked: [], failed: describe8(err2) };
33936
34778
  }
33937
34779
  }
33938
34780
  };
33939
34781
  }
33940
- var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe6;
34782
+ var NullLedgerWriteThrough, NullLedgerRun, RETRY_MS, describe8;
33941
34783
  var init_writeThrough = __esm({
33942
34784
  "../../src/core/runLedger/writeThrough.ts"() {
33943
34785
  "use strict";
33944
34786
  init_runStoreWorker();
33945
34787
  init_flusher();
34788
+ init_sessionLog();
33946
34789
  init_types5();
33947
34790
  NullLedgerWriteThrough = class {
33948
34791
  constructor(gen, fallback) {
@@ -34000,7 +34843,7 @@ var init_writeThrough = __esm({
34000
34843
  }
34001
34844
  };
34002
34845
  RETRY_MS = [200, 800];
34003
- describe6 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34846
+ describe8 = (err2) => err2 instanceof Error ? err2.message : String(err2);
34004
34847
  }
34005
34848
  });
34006
34849
 
@@ -34092,6 +34935,24 @@ var init_residentFleet = __esm({
34092
34935
  });
34093
34936
 
34094
34937
  // ../../src/providers/openaiCompat.ts
34938
+ function buildOpenAIBody(req) {
34939
+ const messages = [];
34940
+ if (req.system) messages.push({ role: "system", content: req.system });
34941
+ for (const m of req.messages) messages.push(...toOAIMessages(m));
34942
+ const body = {
34943
+ model: req.model,
34944
+ max_tokens: req.maxTokens,
34945
+ messages
34946
+ };
34947
+ if (req.tools && req.tools.length > 0) {
34948
+ body.tools = req.tools.map((t) => ({
34949
+ type: "function",
34950
+ function: { name: t.name, description: t.description, parameters: t.inputSchema }
34951
+ }));
34952
+ if (req.toolChoice) body.tool_choice = { type: "function", function: { name: req.toolChoice.name } };
34953
+ }
34954
+ return body;
34955
+ }
34095
34956
  function toOAIMessages(m) {
34096
34957
  if (m.role === "assistant") {
34097
34958
  const text = m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
@@ -34178,20 +35039,7 @@ var init_openaiCompat = __esm({
34178
35039
  this.apiKey = cfg.apiKeyEnv ? processSecrets.named(cfg.apiKeyEnv) : void 0;
34179
35040
  }
34180
35041
  async complete(req) {
34181
- const messages = [];
34182
- if (req.system) messages.push({ role: "system", content: req.system });
34183
- for (const m of req.messages) messages.push(...toOAIMessages(m));
34184
- const body = {
34185
- model: req.model,
34186
- max_tokens: req.maxTokens,
34187
- messages
34188
- };
34189
- if (req.tools && req.tools.length > 0) {
34190
- body.tools = req.tools.map((t) => ({
34191
- type: "function",
34192
- function: { name: t.name, description: t.description, parameters: t.inputSchema }
34193
- }));
34194
- }
35042
+ const body = buildOpenAIBody(req);
34195
35043
  const res = await fetch(`${this.baseUrl}/chat/completions`, {
34196
35044
  method: "POST",
34197
35045
  headers: {
@@ -34683,6 +35531,7 @@ function classifyDocument(mimetype, name) {
34683
35531
  async function fetchImages(files, maxImages, maxTotalBytes = Infinity) {
34684
35532
  const images = [];
34685
35533
  const skipped = [];
35534
+ const skippedFiles = [];
34686
35535
  let bytes = 0;
34687
35536
  const candidates = [];
34688
35537
  for (const f of files ?? []) {
@@ -34690,6 +35539,7 @@ async function fetchImages(files, maxImages, maxTotalBytes = Infinity) {
34690
35539
  const url = f.url_private_download ?? f.url_private;
34691
35540
  if (!url || !f.mimetype || !IMAGE_TYPES2.has(f.mimetype) || (f.size ?? 0) > MAX_IMAGE_BYTES || candidates.length >= maxImages) {
34692
35541
  skipped.push(label);
35542
+ skippedFiles.push(f);
34693
35543
  continue;
34694
35544
  }
34695
35545
  candidates.push({ f, label, url, mediaType: f.mimetype });
@@ -34703,12 +35553,13 @@ async function fetchImages(files, maxImages, maxTotalBytes = Infinity) {
34703
35553
  const buf2 = downloads[i];
34704
35554
  if (!buf2 || buf2.byteLength > MAX_IMAGE_BYTES || bytes + buf2.byteLength > maxTotalBytes) {
34705
35555
  skipped.push(label);
35556
+ skippedFiles.push(f);
34706
35557
  return;
34707
35558
  }
34708
35559
  bytes += buf2.byteLength;
34709
35560
  images.push({ mediaType, data: buf2.toString("base64"), name: f.name });
34710
35561
  });
34711
- return { images, skipped, bytes };
35562
+ return { images, skipped, skippedFiles, bytes };
34712
35563
  }
34713
35564
  async function downloadSlackFile(url, label, isLoginPage) {
34714
35565
  const token2 = processSecrets.get("SLACK_BOT_TOKEN");
@@ -34727,6 +35578,7 @@ async function downloadSlackFile(url, label, isLoginPage) {
34727
35578
  async function fetchDocuments(files, maxDocs, maxTotalBytes = Infinity) {
34728
35579
  const documents = [];
34729
35580
  const skipped = [];
35581
+ const skippedFiles = [];
34730
35582
  let bytes = 0;
34731
35583
  const candidates = [];
34732
35584
  for (const f of files ?? []) {
@@ -34735,6 +35587,7 @@ async function fetchDocuments(files, maxDocs, maxTotalBytes = Infinity) {
34735
35587
  const kind = classifyDocument(f.mimetype, f.name);
34736
35588
  if (!url || !kind || (f.size ?? 0) > MAX_DOCUMENT_BYTES || candidates.length >= maxDocs) {
34737
35589
  skipped.push(label);
35590
+ skippedFiles.push(f);
34738
35591
  continue;
34739
35592
  }
34740
35593
  candidates.push({ f, label, url, kind });
@@ -34748,6 +35601,7 @@ async function fetchDocuments(files, maxDocs, maxTotalBytes = Infinity) {
34748
35601
  const buf2 = downloads[i];
34749
35602
  if (!buf2 || buf2.byteLength > MAX_DOCUMENT_BYTES || bytes + buf2.byteLength > maxTotalBytes) {
34750
35603
  skipped.push(label);
35604
+ skippedFiles.push(f);
34751
35605
  return;
34752
35606
  }
34753
35607
  bytes += buf2.byteLength;
@@ -34757,9 +35611,43 @@ async function fetchDocuments(files, maxDocs, maxTotalBytes = Infinity) {
34757
35611
  name: f.name
34758
35612
  });
34759
35613
  });
34760
- return { documents, skipped, bytes };
35614
+ return { documents, skipped, skippedFiles, bytes };
34761
35615
  }
34762
- var IMAGE_TYPES2, MAX_IMAGE_BYTES, MAX_IMAGES_PER_MESSAGE, MAX_HISTORY_IMAGES, MAX_HISTORY_IMAGE_BYTES, PDF_TYPE2, TEXT_MIME_TYPES, GENERIC_MIME_TYPES, TEXT_EXTENSIONS, SECRET_FILE_EXTENSIONS, SECRET_FILE_PREFIXES, MAX_DOCUMENT_BYTES, MAX_DOCS_PER_MESSAGE, MAX_HISTORY_DOCS, MAX_HISTORY_DOCUMENT_BYTES, fileLabel;
35616
+ function stagedFiles(unsupported, opts) {
35617
+ if (!opts.staging) return { staged: [], skipped: unsupported.map(fileLabel) };
35618
+ const staged = [];
35619
+ const skipped = [];
35620
+ let total = 0;
35621
+ for (const f of unsupported) {
35622
+ const label = fileLabel(f);
35623
+ const url = f.url_private_download ?? f.url_private;
35624
+ const size = f.size ?? 0;
35625
+ if (isSecretFile(f.name)) {
35626
+ skipped.push(`${label} \u2014 looks like a credential or key file`);
35627
+ continue;
35628
+ }
35629
+ if (!url || !f.name || size <= 0) {
35630
+ skipped.push(`${label} \u2014 Slack gave no download URL, name or size`);
35631
+ continue;
35632
+ }
35633
+ if (size > MAX_STAGED_FILE_BYTES) {
35634
+ skipped.push(`${label} \u2014 ${size} bytes is over the ${MAX_STAGED_FILE_BYTES}-byte per-file ceiling`);
35635
+ continue;
35636
+ }
35637
+ if (staged.length >= MAX_STAGED_PER_MESSAGE) {
35638
+ skipped.push(`${label} \u2014 more than ${MAX_STAGED_PER_MESSAGE} files on one message`);
35639
+ continue;
35640
+ }
35641
+ if (total + size > opts.maxBytesPerMessage) {
35642
+ skipped.push(`${label} \u2014 the message's files exceed the ${opts.maxBytesPerMessage}-byte budget`);
35643
+ continue;
35644
+ }
35645
+ total += size;
35646
+ staged.push({ name: f.name, size, type: f.mimetype ?? "application/octet-stream", url, messageId: opts.messageId });
35647
+ }
35648
+ return { staged, skipped };
35649
+ }
35650
+ var IMAGE_TYPES2, MAX_IMAGE_BYTES, MAX_IMAGES_PER_MESSAGE, MAX_HISTORY_IMAGES, MAX_HISTORY_IMAGE_BYTES, PDF_TYPE2, TEXT_MIME_TYPES, GENERIC_MIME_TYPES, TEXT_EXTENSIONS, SECRET_FILE_EXTENSIONS, SECRET_FILE_PREFIXES, MAX_DOCUMENT_BYTES, MAX_DOCS_PER_MESSAGE, MAX_HISTORY_DOCS, MAX_HISTORY_DOCUMENT_BYTES, fileLabel, MAX_STAGED_FILE_BYTES, MAX_STAGED_PER_MESSAGE;
34763
35651
  var init_attachments = __esm({
34764
35652
  "../../src/channels/slack/attachments.ts"() {
34765
35653
  "use strict";
@@ -34842,6 +35730,8 @@ var init_attachments = __esm({
34842
35730
  MAX_HISTORY_DOCS = 20;
34843
35731
  MAX_HISTORY_DOCUMENT_BYTES = 32 * 1024 * 1024;
34844
35732
  fileLabel = (f) => `${f.name ?? f.id ?? "file"} (${f.mimetype ?? "unknown type"})`;
35733
+ MAX_STAGED_FILE_BYTES = 1073741824;
35734
+ MAX_STAGED_PER_MESSAGE = 10;
34845
35735
  }
34846
35736
  });
34847
35737
 
@@ -35480,7 +36370,13 @@ async function handle(deps, { client, statusClient }, ev) {
35480
36370
  const receivedAt = systemClock();
35481
36371
  const trace = startRequestRoot(deps, { channel: "slack", receivedAt, originAt: tsMs(ev.ts) });
35482
36372
  try {
35483
- const received = await trace.root.span("slack.receive", (span) => receiveSlackMessage(client, ev, span));
36373
+ const received = await trace.root.span(
36374
+ "slack.receive",
36375
+ (span) => receiveSlackMessage(client, ev, span, {
36376
+ staging: deps.artifacts !== void 0,
36377
+ maxBytesPerMessage: deps.config.config.artifacts?.inbound?.maxBytesPerMessage ?? ARTIFACT_DEFAULTS.maxBytesPerMessage
36378
+ })
36379
+ );
35484
36380
  if (!received) {
35485
36381
  trace.root.end("ok", { status: "refused" });
35486
36382
  return;
@@ -35495,7 +36391,7 @@ async function handle(deps, { client, statusClient }, ev) {
35495
36391
  if (!trace.root.ended) trace.root.end("error", { status: "failed" });
35496
36392
  }
35497
36393
  }
35498
- async function receiveSlackMessage(client, ev, span) {
36394
+ async function receiveSlackMessage(client, ev, span, policy) {
35499
36395
  const drop = await dedupeDelivery(client, ev);
35500
36396
  if (drop) {
35501
36397
  console.log(`[redelivery] ${ev.channel}:${ev.ts} dropped: ${drop}`);
@@ -35509,12 +36405,13 @@ async function receiveSlackMessage(client, ev, span) {
35509
36405
  const delayNote = ev.caughtUp ? new SlackIO(client, ev).reply(catchUpDelayNote(ev.ts, systemClock())).catch((err2) => {
35510
36406
  console.error(`[catch-up] ${ev.channel}:${ev.ts} delay note failed: ${err2.message}`);
35511
36407
  }) : Promise.resolve();
35512
- const [{ images, skipped: skippedImages }, { documents, skipped: skippedDocs }] = await Promise.all([
36408
+ const [{ images, skippedFiles: imagePassSkipped }, { documents, skippedFiles: documentPassSkipped }] = await Promise.all([
35513
36409
  fetchImages(ev.files, MAX_IMAGES_PER_MESSAGE),
35514
36410
  fetchDocuments(ev.files, MAX_DOCS_PER_MESSAGE),
35515
36411
  delayNote
35516
36412
  ]);
35517
- const skipped = skippedImages.filter((s) => skippedDocs.includes(s));
36413
+ const unsupported = imagePassSkipped.filter((f) => documentPassSkipped.includes(f));
36414
+ const { staged, skipped } = stagedFiles(unsupported, { ...policy, messageId: ev.ts });
35518
36415
  const [channelName, userName, team] = await Promise.all([
35519
36416
  resolveChannelName(client, ev.channel),
35520
36417
  resolveUserName(client, ev.user),
@@ -35532,7 +36429,8 @@ async function receiveSlackMessage(client, ev, span) {
35532
36429
  userName,
35533
36430
  sourceUrl: team ? slackPermalink(team, ev.channel, ev.ts, ev.threadTs) : void 0,
35534
36431
  images: images.length > 0 ? images : void 0,
35535
- documents: documents.length > 0 ? documents : void 0
36432
+ documents: documents.length > 0 ? documents : void 0,
36433
+ ...staged.length > 0 ? { staged } : {}
35536
36434
  };
35537
36435
  }
35538
36436
  function resumeSlackIO(client, run2, opts = {}) {
@@ -35599,6 +36497,7 @@ var init_slack = __esm({
35599
36497
  init_statusCard();
35600
36498
  init_slackCatchUp();
35601
36499
  init_secrets2();
36500
+ init_config();
35602
36501
  init_slackCatchUpStatus();
35603
36502
  init_slackSocketStatus();
35604
36503
  init_slackTriggers();
@@ -35664,6 +36563,30 @@ ${file.text}`));
35664
36563
  initial_comment: mdToMrkdwn(file.lead)
35665
36564
  });
35666
36565
  }
36566
+ /** The external-upload flow (docs/reference/specs/slack-channel.md item 10,
36567
+ * record 0033): `files.getUploadURLExternal` mints a URL for exactly
36568
+ * `size` bytes under `name`; the run's container POSTs the file there; then
36569
+ * `complete` shares it into this thread with the lead as the comment
36570
+ * (`files.completeUploadExternal`). The bot never sees the bytes. A ticket
36571
+ * Slack answers without a URL or id is refused by name. */
36572
+ async uploadTicket(file) {
36573
+ const minted = await this.client.files.getUploadURLExternal({ filename: file.name, length: file.size });
36574
+ const url = minted.upload_url;
36575
+ const id = minted.file_id;
36576
+ if (!url || !id)
36577
+ throw new Error(`files.getUploadURLExternal answered without an upload_url and file_id for ${file.name}`);
36578
+ return {
36579
+ url,
36580
+ complete: async (lead) => {
36581
+ await this.client.files.completeUploadExternal({
36582
+ files: [{ id, title: file.name }],
36583
+ channel_id: this.ev.channel,
36584
+ thread_ts: this.ev.threadTs,
36585
+ initial_comment: mdToMrkdwn(lead)
36586
+ });
36587
+ }
36588
+ };
36589
+ }
35667
36590
  async post(mrkdwn) {
35668
36591
  for (const chunk of chunkText(mrkdwn, SLACK_MSG_LIMIT)) {
35669
36592
  await this.client.chat.postMessage({
@@ -36385,31 +37308,6 @@ var init_mcp3 = __esm({
36385
37308
  }
36386
37309
  });
36387
37310
 
36388
- // ../../src/artifacts/contentType.ts
36389
- var INLINE_IMAGE_TYPES;
36390
- var init_contentType = __esm({
36391
- "../../src/artifacts/contentType.ts"() {
36392
- "use strict";
36393
- INLINE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
36394
- }
36395
- });
36396
-
36397
- // ../../src/artifacts/keys.ts
36398
- function safeBasename(name) {
36399
- const segments2 = name.split(/[\\/]+/).filter((s) => s.length > 0);
36400
- const last = segments2[segments2.length - 1] ?? "";
36401
- const reduced = last.replace(SAFE, "_").replace(/\.{2,}/g, "_").replace(/_+/g, "_");
36402
- if (reduced === "" || /^[._-]+$/.test(reduced)) return "file";
36403
- return reduced;
36404
- }
36405
- var SAFE;
36406
- var init_keys = __esm({
36407
- "../../src/artifacts/keys.ts"() {
36408
- "use strict";
36409
- SAFE = /[^A-Za-z0-9._-]+/g;
36410
- }
36411
- });
36412
-
36413
37311
  // ../../src/channels/scheduledPanel.ts
36414
37312
  function buildScheduledRows(schedules, firings, liveRuns, now) {
36415
37313
  const byName = /* @__PURE__ */ new Map();
@@ -37226,7 +38124,7 @@ var init_liveView = __esm({
37226
38124
  // ../../src/channels/webAssets.ts
37227
38125
  import { readdirSync as readdirSync3, readFileSync as readFileSync16, statSync as statSync3 } from "node:fs";
37228
38126
  import { join as join13 } from "node:path";
37229
- function contentTypeFor(path) {
38127
+ function contentTypeFor2(path) {
37230
38128
  const dot = path.lastIndexOf(".");
37231
38129
  return (dot === -1 ? void 0 : CONTENT_TYPES[path.slice(dot)]) ?? "application/octet-stream";
37232
38130
  }
@@ -37271,7 +38169,7 @@ function loadWebAssets(distDir) {
37271
38169
  return true;
37272
38170
  }
37273
38171
  res.writeHead(200, {
37274
- "content-type": contentTypeFor(path),
38172
+ "content-type": contentTypeFor2(path),
37275
38173
  "content-length": String(body.byteLength),
37276
38174
  "cache-control": "public, max-age=31536000, immutable"
37277
38175
  });
@@ -38157,6 +39055,16 @@ function turnRows(idx, message2, opts = {}) {
38157
39055
  const refBytes = opts.attachmentRefBytes ?? ATTACHMENT_REF_BYTES;
38158
39056
  const rows = [];
38159
39057
  const attachments = [];
39058
+ if (isCompaction(message2)) {
39059
+ const json2 = JSON.stringify({ compaction: message2.compaction });
39060
+ const bytes = utf8ByteLength(json2);
39061
+ if (bytes > partBytes) {
39062
+ throw new Error(
39063
+ `transcript: the compaction row at turn ${idx} is ${bytes} bytes, over the ${partBytes} row budget`
39064
+ );
39065
+ }
39066
+ return { rows: [{ idx, part: 0, json: json2 }], attachments };
39067
+ }
38160
39068
  message2.content.forEach((part, i) => {
38161
39069
  let stored = part;
38162
39070
  if (hasData(part) && part.data.length > refBytes) {
@@ -38190,49 +39098,69 @@ function chunkRows(rows, maxBytes) {
38190
39098
  if (current.length > 0) chunks.push(current);
38191
39099
  return chunks;
38192
39100
  }
38193
- function assembleTranscript(rows, attachments) {
39101
+ function assembleTranscript(rows, attachments, base = 0) {
38194
39102
  const byRef = new Map(attachments.map((a) => [a.ref, a]));
38195
39103
  const byTurn = /* @__PURE__ */ new Map();
38196
39104
  for (const row of rows) {
38197
- let parts = byTurn.get(row.idx);
38198
- if (!parts) byTurn.set(row.idx, parts = /* @__PURE__ */ new Map());
39105
+ const idx = row.idx - base;
39106
+ let parts = byTurn.get(idx);
39107
+ if (!parts) byTurn.set(idx, parts = /* @__PURE__ */ new Map());
38199
39108
  parts.set(row.part, JSON.parse(row.json));
38200
39109
  }
38201
39110
  const messages = [];
39111
+ const compactions = [];
39112
+ const messageIndexOf = /* @__PURE__ */ new Map();
39113
+ const gap = (why) => ({
39114
+ complete: false,
39115
+ turns: messages.length + compactions.length,
39116
+ messages,
39117
+ compactions,
39118
+ gap: why
39119
+ });
38202
39120
  const turnCount = byTurn.size === 0 ? 0 : Math.max(...byTurn.keys()) + 1;
38203
39121
  for (let idx = 0; idx < turnCount; idx++) {
38204
39122
  const parts = byTurn.get(idx);
38205
- if (!parts) return { complete: false, turns: messages.length, messages, gap: `turn ${idx} is missing` };
39123
+ if (!parts) return gap(`turn ${idx} is missing`);
39124
+ const first = parts.get(0);
39125
+ if (first && "compaction" in first) {
39126
+ compactions.push({ before: messages.length, entry: first.compaction });
39127
+ continue;
39128
+ }
38206
39129
  const content = [];
38207
39130
  let role;
38208
39131
  const partCount = Math.max(...parts.keys()) + 1;
38209
39132
  for (let p = 0; p < partCount; p++) {
38210
39133
  const stored = parts.get(p);
38211
- if (!stored)
38212
- return { complete: false, turns: messages.length, messages, gap: `turn ${idx} is missing part ${p}` };
39134
+ if (!stored || "compaction" in stored) return gap(`turn ${idx} is missing part ${p}`);
38213
39135
  role = stored.role;
38214
39136
  const part = stored.part;
38215
39137
  if (part.dataRef !== void 0) {
38216
39138
  const att = byRef.get(part.dataRef);
38217
- if (!att)
38218
- return { complete: false, turns: messages.length, messages, gap: `attachment ${part.dataRef} is missing` };
39139
+ if (!att) return gap(`attachment ${part.dataRef} is missing`);
38219
39140
  const { dataRef: _ref, ...rest } = part;
38220
39141
  content.push({ ...rest, data: att.data });
38221
39142
  } else {
38222
39143
  content.push(part);
38223
39144
  }
38224
39145
  }
39146
+ messageIndexOf.set(idx, messages.length);
38225
39147
  messages.push({ role: role ?? "user", content });
38226
39148
  }
38227
- return { complete: true, turns: messages.length, messages };
39149
+ for (const c of compactions) {
39150
+ if (c.entry.keptFrom === void 0) continue;
39151
+ const kept = messageIndexOf.get(c.entry.keptFrom - base);
39152
+ if (kept !== void 0) c.keptBefore = kept;
39153
+ }
39154
+ return { complete: true, turns: messages.length + compactions.length, messages, compactions };
38228
39155
  }
38229
- var attachmentRef;
39156
+ var attachmentRef, isCompaction;
38230
39157
  var init_transcript = __esm({
38231
39158
  "../../src/core/runLedger/transcript.ts"() {
38232
39159
  "use strict";
38233
39160
  init_runRecord();
38234
39161
  init_types5();
38235
39162
  attachmentRef = (idx, part) => `t${idx}p${part}`;
39163
+ isCompaction = (v) => "compaction" in v;
38236
39164
  }
38237
39165
  });
38238
39166
 
@@ -38247,6 +39175,9 @@ function buildRunLedger(cfg, secrets, deps = {}) {
38247
39175
  baseUrl: worker.baseUrl,
38248
39176
  token: token2.reveal(),
38249
39177
  storeKey: RUN_STORE_KEY,
39178
+ // The same clamped policy the run store proposes, so the session logs and
39179
+ // the run records are bounded by one configuration.
39180
+ sessionLogMaxBytes: retentionPolicyOf(cfg).sessionLogMaxBytes,
38250
39181
  ...deps.fetch ? { fetch: deps.fetch } : {}
38251
39182
  });
38252
39183
  }
@@ -38255,7 +39186,9 @@ var init_runLedgerWorker = __esm({
38255
39186
  "../../src/core/runLedgerWorker.ts"() {
38256
39187
  "use strict";
38257
39188
  init_runRecord();
39189
+ init_runStore();
38258
39190
  init_runStoreWorker();
39191
+ init_sessionLog();
38259
39192
  init_transcript();
38260
39193
  init_types5();
38261
39194
  WorkerRunLedger = class {
@@ -38319,41 +39252,94 @@ var init_runLedgerWorker = __esm({
38319
39252
  }
38320
39253
  };
38321
39254
  }
38322
- await this.post("/runs/transcript/owner", { runId: req.runId, gen: req.gen });
38323
39255
  return { ok: true };
38324
39256
  }
38325
- async writeTurns(runId3, gen, turns) {
39257
+ /** The write route and its body for a run's rows: the session log when the
39258
+ * run has one, the run's own transcript object otherwise. */
39259
+ target(runId3, gen, session) {
39260
+ if (session === void 0) return { path: "/runs/transcript/write", body: { runId: runId3, gen } };
39261
+ this.checkSessionKey(session);
39262
+ return { path: "/runs/session/write", body: { key: session, gen } };
39263
+ }
39264
+ async writeTurns(runId3, gen, turns, session) {
38326
39265
  const rows = [];
38327
39266
  const attachments = [];
38328
39267
  for (const t of turns) {
38329
- const out = turnRows(t.idx, t.message);
39268
+ const out = turnRows(t.idx, "message" in t ? t.message : { compaction: t.compaction });
38330
39269
  rows.push(...out.rows);
38331
39270
  attachments.push(...out.attachments);
38332
39271
  }
39272
+ const { path, body } = this.target(runId3, gen, session);
38333
39273
  for (const a of attachments) {
38334
- const r = await this.post("/runs/transcript/write", { runId: runId3, gen, rows: [], attachments: [a] });
39274
+ const r = await this.post(path, { ...body, rows: [], attachments: [a] });
38335
39275
  const f = this.fenceResult(r);
38336
39276
  if (!f.ok) return f;
38337
39277
  }
38338
39278
  const chunks = chunkRows(rows, TRANSCRIPT_REQUEST_BYTES - 4096);
38339
39279
  for (const chunk of chunks.length ? chunks : [[]]) {
38340
39280
  if (chunk.length === 0 && attachments.length > 0) continue;
38341
- const r = await this.post("/runs/transcript/write", { runId: runId3, gen, rows: chunk, attachments: [] });
39281
+ const r = await this.post(path, { ...body, rows: chunk, attachments: [] });
38342
39282
  const f = this.fenceResult(r);
38343
39283
  if (!f.ok) return f;
38344
39284
  }
38345
39285
  return { ok: true };
38346
39286
  }
38347
- async seed(runId3, gen, turns) {
39287
+ async seed(runId3, gen, turns, session) {
38348
39288
  this.checkIds(runId3, gen);
38349
- return this.writeTurns(runId3, gen, turns);
39289
+ return this.writeTurns(runId3, gen, turns, session);
38350
39290
  }
38351
- async step(runId3, gen, record2, turns) {
39291
+ async step(runId3, gen, record2, turns, session) {
38352
39292
  this.checkIds(runId3, gen);
38353
- const written = await this.writeTurns(runId3, gen, turns);
39293
+ const written = await this.writeTurns(runId3, gen, turns, session);
38354
39294
  if (!written.ok) return written;
38355
39295
  return this.fenceResult(await this.post("/runs/step", { storeKey: this.opts.storeKey, runId: runId3, gen, record: record2 }));
38356
39296
  }
39297
+ checkSessionKey(key) {
39298
+ if (!SESSION_KEY_PATTERN.test(key))
39299
+ throw new PermanentStoreError(`run ledger: malformed session key ${JSON.stringify(key)}`);
39300
+ }
39301
+ async sessionTail(key) {
39302
+ this.checkSessionKey(key);
39303
+ const r = await this.post("/runs/session/tail", { key });
39304
+ return typeof r.data.next === "number" ? r.data.next : 0;
39305
+ }
39306
+ async claimSession(key, runId3, gen, maxBytes) {
39307
+ this.checkSessionKey(key);
39308
+ this.checkIds(runId3, gen);
39309
+ await this.post("/runs/session/owner", {
39310
+ key,
39311
+ runId: runId3,
39312
+ gen,
39313
+ maxBytes: maxBytes ?? this.opts.sessionLogMaxBytes ?? DEFAULT_SESSION_LOG_MAX_BYTES
39314
+ });
39315
+ }
39316
+ async releaseSession(key, runId3, gen) {
39317
+ this.checkSessionKey(key);
39318
+ this.checkIds(runId3, gen);
39319
+ return this.fenceResult(await this.post("/runs/session/clear-owner", { key, runId: runId3, gen }));
39320
+ }
39321
+ async readSession(key, from, to) {
39322
+ this.checkSessionKey(key);
39323
+ const r = await this.post("/runs/session/read", { key, from, ...to !== void 0 ? { to } : {} });
39324
+ return assembleTranscript(
39325
+ Array.isArray(r.data.rows) ? r.data.rows : [],
39326
+ Array.isArray(r.data.attachments) ? r.data.attachments : [],
39327
+ from
39328
+ );
39329
+ }
39330
+ async readSessionTail(key, maxBytes) {
39331
+ this.checkSessionKey(key);
39332
+ const r = await this.post("/runs/session/read-tail", { key, maxBytes });
39333
+ const from = typeof r.data.from === "number" ? r.data.from : 0;
39334
+ return {
39335
+ from,
39336
+ transcript: assembleTranscript(
39337
+ Array.isArray(r.data.rows) ? r.data.rows : [],
39338
+ Array.isArray(r.data.attachments) ? r.data.attachments : [],
39339
+ from
39340
+ )
39341
+ };
39342
+ }
38357
39343
  async heartbeat(runId3, gen, leaseMs) {
38358
39344
  this.checkIds(runId3, gen);
38359
39345
  const r = await this.post("/runs/heartbeat", { storeKey: this.opts.storeKey, runId: runId3, gen, leaseMs });
@@ -38405,22 +39391,23 @@ var init_runLedgerWorker = __esm({
38405
39391
  const r = await this.post("/runs/finish", { storeKey: this.opts.storeKey, runId: runId3, gen, record: record2 });
38406
39392
  const f = this.fenceResult(r);
38407
39393
  if (!f.ok) return f;
38408
- await this.post("/runs/transcript/clear", { runId: runId3 }).catch(() => {
38409
- });
39394
+ if (record2.session)
39395
+ await this.post("/runs/session/clear-owner", { key: record2.session.key, runId: runId3, gen }).catch(() => {
39396
+ });
38410
39397
  return { ok: true, stored: r.data.stored === true };
38411
39398
  }
38412
39399
  async abandon(runId3, gen) {
38413
39400
  this.checkIds(runId3, gen);
38414
- const f = this.fenceResult(await this.post("/runs/abandon", { storeKey: this.opts.storeKey, runId: runId3, gen }));
38415
- if (!f.ok) return f;
38416
- await this.post("/runs/transcript/clear", { runId: runId3 }).catch(() => {
38417
- });
38418
- return f;
39401
+ return this.fenceResult(await this.post("/runs/abandon", { storeKey: this.opts.storeKey, runId: runId3, gen }));
38419
39402
  }
38420
39403
  async reclaim(gen, now, leaseMs) {
38421
39404
  const r = await this.post("/runs/reclaim", { storeKey: this.opts.storeKey, gen, now, leaseMs });
38422
39405
  const runs = Array.isArray(r.data.runs) ? r.data.runs : [];
38423
- for (const run2 of runs) await this.post("/runs/transcript/owner", { runId: run2.row.runId, gen });
39406
+ for (const run2 of runs) {
39407
+ const session = run2.row.meta?.session;
39408
+ if (session) await this.claimSession(session.key, run2.row.runId, gen);
39409
+ else await this.post("/runs/transcript/owner", { runId: run2.row.runId, gen });
39410
+ }
38424
39411
  return runs;
38425
39412
  }
38426
39413
  async listLive() {
@@ -38468,6 +39455,116 @@ var init_decisions = __esm({
38468
39455
  }
38469
39456
  });
38470
39457
 
39458
+ // ../../src/core/runLedger/resume.ts
39459
+ function transcriptSource(meta) {
39460
+ return meta.session ? { kind: "session", key: meta.session.key, from: meta.session.seedFrom } : { kind: "run" };
39461
+ }
39462
+ function settlementFor(toolUse, tools) {
39463
+ const tool = tools.get(toolUse.name);
39464
+ if (!tool) {
39465
+ return {
39466
+ toolUse,
39467
+ action: "synthetic",
39468
+ text: `Tool ${toolUse.name} is not available after the bot restarted; continue without it.`
39469
+ };
39470
+ }
39471
+ if (tool.sideEffectFree || RERUN_SAFE_TOOLS.has(toolUse.name)) return { toolUse, action: "rerun" };
39472
+ return {
39473
+ toolUse,
39474
+ action: "synthetic",
39475
+ text: `The bot restarted while this ${toolUse.name} call was in flight; its effects are unknown \u2014 re-check them before re-running it.`
39476
+ };
39477
+ }
39478
+ function planResume(input) {
39479
+ const { transcript, lastStep } = input;
39480
+ if (!lastStep) return { kind: "interrupted", why: "no step record: killed before its conversation was stored" };
39481
+ if (!transcript.complete) return { kind: "interrupted", why: `transcript incomplete: ${transcript.gap}` };
39482
+ const completeness = transcriptCompleteness({
39483
+ lastStep,
39484
+ seedTurns: lastStep.turnIndex,
39485
+ transcriptTurns: transcript.turns
39486
+ });
39487
+ if (completeness.kind === "interrupted") return completeness;
39488
+ const tools = /* @__PURE__ */ new Map();
39489
+ for (const t of input.tools) tools.set(t.name, t);
39490
+ const messages = transcript.messages;
39491
+ const last = messages[messages.length - 1];
39492
+ const calls = toolUsesOf(last);
39493
+ if (completeness.kind === "resume") {
39494
+ if (lastStep.inFlight.length === 0) {
39495
+ if (last?.role !== "user") {
39496
+ return {
39497
+ kind: "interrupted",
39498
+ why: "transcript ends with an assistant turn but the last step recorded nothing in flight"
39499
+ };
39500
+ }
39501
+ return {
39502
+ kind: "resume",
39503
+ messages,
39504
+ compactions: transcript.compactions,
39505
+ settlements: [],
39506
+ stepRecorded: true,
39507
+ inboxConsumedSeq: lastStep.inboxConsumedSeq,
39508
+ step: lastStep.step,
39509
+ turn: lastStep.turn,
39510
+ iteration: lastStep.iteration,
39511
+ remainingMs: lastStep.remainingMs
39512
+ };
39513
+ }
39514
+ const recorded = new Set(lastStep.inFlight.map((c) => c.callId));
39515
+ if (calls.length === 0 || calls.length !== recorded.size || !calls.every((c) => recorded.has(c.id))) {
39516
+ return {
39517
+ kind: "interrupted",
39518
+ why: `the last step's calls in flight (${[...recorded].join(", ")}) do not match the transcript's last assistant turn`
39519
+ };
39520
+ }
39521
+ return {
39522
+ kind: "resume",
39523
+ messages,
39524
+ compactions: transcript.compactions,
39525
+ settlements: calls.map((c) => settlementFor(c, tools)),
39526
+ stepRecorded: true,
39527
+ inboxConsumedSeq: lastStep.inboxConsumedSeq,
39528
+ step: lastStep.step,
39529
+ turn: lastStep.turn,
39530
+ iteration: lastStep.iteration,
39531
+ remainingMs: lastStep.remainingMs
39532
+ };
39533
+ }
39534
+ if (calls.length === 0) {
39535
+ return { kind: "interrupted", why: "the next step's turns landed but its assistant turn has no tool calls" };
39536
+ }
39537
+ const bookkeepingOnly = calls.every((c) => c.name === "update_status");
39538
+ return {
39539
+ kind: "resume",
39540
+ messages,
39541
+ compactions: transcript.compactions,
39542
+ settlements: calls.map((c) => ({ toolUse: c, action: "rerun" })),
39543
+ stepRecorded: false,
39544
+ inboxConsumedSeq: lastStep.inboxConsumedSeq,
39545
+ step: lastStep.step + 1,
39546
+ turn: lastStep.turn + (bookkeepingOnly ? 0 : 1),
39547
+ iteration: lastStep.iteration + 1,
39548
+ remainingMs: lastStep.remainingMs
39549
+ };
39550
+ }
39551
+ var RERUN_SAFE_TOOLS, toolUsesOf;
39552
+ var init_resume = __esm({
39553
+ "../../src/core/runLedger/resume.ts"() {
39554
+ "use strict";
39555
+ init_decisions();
39556
+ RERUN_SAFE_TOOLS = /* @__PURE__ */ new Set([
39557
+ "write_file",
39558
+ "update_status",
39559
+ "submit_verdict",
39560
+ "submit_pr_description",
39561
+ "submit_handoff",
39562
+ "submit_dispositions"
39563
+ ]);
39564
+ toolUsesOf = (message2) => message2?.role === "assistant" ? message2.content.filter((p) => p.type === "tool_use") : [];
39565
+ }
39566
+ });
39567
+
38471
39568
  // ../../src/core/boot.ts
38472
39569
  function closureNote(agent, prUrl2) {
38473
39570
  if (agent === "ship") return shipInterruptedNote(prUrl2);
@@ -38491,7 +39588,7 @@ async function reclaimRuns(opts) {
38491
39588
  reclaimed = await ledger.reclaim(gen, now(), LEASE_MS);
38492
39589
  } catch (err2) {
38493
39590
  warn(
38494
- err2 instanceof RouteMissingError ? "[reclaim] state Worker has no run-ledger routes \u2014 nothing to take over" : `[reclaim] reclaim failed: ${describe7(err2)} \u2014 runs the last generation left stay on the ledger until the next boot`
39591
+ err2 instanceof RouteMissingError ? "[reclaim] state Worker has no run-ledger routes \u2014 nothing to take over" : `[reclaim] reclaim failed: ${describe9(err2)} \u2014 runs the last generation left stay on the ledger until the next boot`
38495
39592
  );
38496
39593
  return outcome;
38497
39594
  }
@@ -38515,7 +39612,7 @@ async function reclaimRuns(opts) {
38515
39612
  status3 = "interrupted";
38516
39613
  why = "reserved at admission without its request: nothing to restart from";
38517
39614
  } else {
38518
- const verdict = await completenessVerdict(ledger, row.runId, run2.lastStep);
39615
+ const verdict = await completenessVerdict(ledger, row, run2.lastStep);
38519
39616
  if (verdict.resumable && run2.lastStep) {
38520
39617
  const events2 = await ledger.readEvents(row.runId);
38521
39618
  outcome.resumable.push({
@@ -38558,8 +39655,8 @@ async function reclaimRuns(opts) {
38558
39655
  `[reclaim] ${row.runId} ${row.threadKey} closed ${status3} (from ${run2.reclaimedFrom}; ${why}; ${events.length} event(s))`
38559
39656
  );
38560
39657
  } catch (err2) {
38561
- outcome.failed.push({ runId: row.runId, error: describe7(err2) });
38562
- warn(`[reclaim] ${row.runId} ${row.threadKey}: ${describe7(err2)} \u2014 left on the ledger for the next boot`);
39658
+ outcome.failed.push({ runId: row.runId, error: describe9(err2) });
39659
+ warn(`[reclaim] ${row.runId} ${row.threadKey}: ${describe9(err2)} \u2014 left on the ledger for the next boot`);
38563
39660
  }
38564
39661
  }
38565
39662
  try {
@@ -38575,7 +39672,7 @@ async function reclaimRuns(opts) {
38575
39672
  });
38576
39673
  }
38577
39674
  } catch (err2) {
38578
- warn(`[reclaim] listing live runs failed: ${describe7(err2)} \u2014 the card sweep runs without the ledger's guard`);
39675
+ warn(`[reclaim] listing live runs failed: ${describe9(err2)} \u2014 the card sweep runs without the ledger's guard`);
38579
39676
  }
38580
39677
  if (reclaimed.length > 0 || outcome.liveElsewhere.length > 0) {
38581
39678
  log(
@@ -38597,7 +39694,7 @@ function startReclaimSweep(opts) {
38597
39694
  await opts.onOutcome(outcome);
38598
39695
  }
38599
39696
  } catch (err2) {
38600
- warn(`[reclaim] sweep failed: ${describe7(err2)}`);
39697
+ warn(`[reclaim] sweep failed: ${describe9(err2)}`);
38601
39698
  } finally {
38602
39699
  running = false;
38603
39700
  }
@@ -38616,13 +39713,14 @@ async function closeReclaimed(ledger, gen, input) {
38616
39713
  const result = await ledger.finish(input.row.runId, gen, record2);
38617
39714
  return result.ok ? { ok: true, record: record2 } : { ok: false, reason: result.reason ?? "refused" };
38618
39715
  }
38619
- async function completenessVerdict(ledger, runId3, lastStep) {
39716
+ async function completenessVerdict(ledger, row, lastStep) {
38620
39717
  if (!lastStep) return { resumable: false, why: "no step record: killed before its conversation was stored" };
38621
39718
  let transcript;
38622
39719
  try {
38623
- transcript = await ledger.readTranscript(runId3);
39720
+ const source2 = transcriptSource(row.meta);
39721
+ transcript = source2.kind === "session" ? await ledger.readSession(source2.key, source2.from) : await ledger.readTranscript(row.runId);
38624
39722
  } catch (err2) {
38625
- return { resumable: false, why: `transcript unreadable (${describe7(err2)})` };
39723
+ return { resumable: false, why: `transcript unreadable (${describe9(err2)})` };
38626
39724
  }
38627
39725
  if (!transcript.complete) return { resumable: false, why: `transcript incomplete: ${transcript.gap}` };
38628
39726
  const verdict = transcriptCompleteness({
@@ -38643,12 +39741,13 @@ async function completenessVerdict(ledger, runId3, lastStep) {
38643
39741
  return { resumable: false, why: verdict.why };
38644
39742
  }
38645
39743
  }
38646
- var TERMINAL, describe7;
39744
+ var TERMINAL, describe9;
38647
39745
  var init_boot = __esm({
38648
39746
  "../../src/core/boot.ts"() {
38649
39747
  "use strict";
38650
39748
  init_runStoreWorker();
38651
39749
  init_decisions();
39750
+ init_resume();
38652
39751
  init_types5();
38653
39752
  init_record();
38654
39753
  init_shipPipeline();
@@ -38659,111 +39758,7 @@ var init_boot = __esm({
38659
39758
  "failed",
38660
39759
  "interrupted"
38661
39760
  ]);
38662
- describe7 = (err2) => err2 instanceof Error ? err2.message : String(err2);
38663
- }
38664
- });
38665
-
38666
- // ../../src/core/runLedger/resume.ts
38667
- function settlementFor(toolUse, tools) {
38668
- const tool = tools.get(toolUse.name);
38669
- if (!tool) {
38670
- return {
38671
- toolUse,
38672
- action: "synthetic",
38673
- text: `Tool ${toolUse.name} is not available after the bot restarted; continue without it.`
38674
- };
38675
- }
38676
- if (tool.sideEffectFree || RERUN_SAFE_TOOLS.has(toolUse.name)) return { toolUse, action: "rerun" };
38677
- return {
38678
- toolUse,
38679
- action: "synthetic",
38680
- text: `The bot restarted while this ${toolUse.name} call was in flight; its effects are unknown \u2014 re-check them before re-running it.`
38681
- };
38682
- }
38683
- function planResume(input) {
38684
- const { transcript, lastStep } = input;
38685
- if (!lastStep) return { kind: "interrupted", why: "no step record: killed before its conversation was stored" };
38686
- if (!transcript.complete) return { kind: "interrupted", why: `transcript incomplete: ${transcript.gap}` };
38687
- const completeness = transcriptCompleteness({
38688
- lastStep,
38689
- seedTurns: lastStep.turnIndex,
38690
- transcriptTurns: transcript.turns
38691
- });
38692
- if (completeness.kind === "interrupted") return completeness;
38693
- const tools = /* @__PURE__ */ new Map();
38694
- for (const t of input.tools) tools.set(t.name, t);
38695
- const messages = transcript.messages;
38696
- const last = messages[messages.length - 1];
38697
- const calls = toolUsesOf(last);
38698
- if (completeness.kind === "resume") {
38699
- if (lastStep.inFlight.length === 0) {
38700
- if (last?.role !== "user") {
38701
- return {
38702
- kind: "interrupted",
38703
- why: "transcript ends with an assistant turn but the last step recorded nothing in flight"
38704
- };
38705
- }
38706
- return {
38707
- kind: "resume",
38708
- messages,
38709
- settlements: [],
38710
- stepRecorded: true,
38711
- inboxConsumedSeq: lastStep.inboxConsumedSeq,
38712
- step: lastStep.step,
38713
- turn: lastStep.turn,
38714
- iteration: lastStep.iteration,
38715
- remainingMs: lastStep.remainingMs
38716
- };
38717
- }
38718
- const recorded = new Set(lastStep.inFlight.map((c) => c.callId));
38719
- if (calls.length === 0 || calls.length !== recorded.size || !calls.every((c) => recorded.has(c.id))) {
38720
- return {
38721
- kind: "interrupted",
38722
- why: `the last step's calls in flight (${[...recorded].join(", ")}) do not match the transcript's last assistant turn`
38723
- };
38724
- }
38725
- return {
38726
- kind: "resume",
38727
- messages,
38728
- settlements: calls.map((c) => settlementFor(c, tools)),
38729
- stepRecorded: true,
38730
- inboxConsumedSeq: lastStep.inboxConsumedSeq,
38731
- step: lastStep.step,
38732
- turn: lastStep.turn,
38733
- iteration: lastStep.iteration,
38734
- remainingMs: lastStep.remainingMs
38735
- };
38736
- }
38737
- if (calls.length === 0) {
38738
- return { kind: "interrupted", why: "the next step's turns landed but its assistant turn has no tool calls" };
38739
- }
38740
- const bookkeepingOnly = calls.every((c) => c.name === "update_status");
38741
- return {
38742
- kind: "resume",
38743
- messages,
38744
- settlements: calls.map((c) => ({ toolUse: c, action: "rerun" })),
38745
- stepRecorded: false,
38746
- inboxConsumedSeq: lastStep.inboxConsumedSeq,
38747
- step: lastStep.step + 1,
38748
- turn: lastStep.turn + (bookkeepingOnly ? 0 : 1),
38749
- iteration: lastStep.iteration + 1,
38750
- remainingMs: lastStep.remainingMs
38751
- };
38752
- }
38753
- var RERUN_SAFE_TOOLS, toolUsesOf;
38754
- var init_resume = __esm({
38755
- "../../src/core/runLedger/resume.ts"() {
38756
- "use strict";
38757
- init_decisions();
38758
- RERUN_SAFE_TOOLS = /* @__PURE__ */ new Set([
38759
- "write_file",
38760
- "update_status",
38761
- "submit_verdict",
38762
- "submit_pr_description",
38763
- "submit_handoff",
38764
- "submit_dispositions"
38765
- ]);
38766
- toolUsesOf = (message2) => message2?.role === "assistant" ? message2.content.filter((p) => p.type === "tool_use") : [];
39761
+ describe9 = (err2) => err2 instanceof Error ? err2.message : String(err2);
38767
39762
  }
38768
39763
  });
38769
39764
 
@@ -39495,11 +40490,14 @@ var init_adminModelProxy = __esm({
39495
40490
 
39496
40491
  // ../../src/core/harness/pi/toolRules.ts
39497
40492
  import { isAbsolute as isAbsolute5, relative, resolve as resolve11 } from "node:path";
40493
+ function reachFor(identity2) {
40494
+ return identity2 === "write" ? CODING_REACH : READ_REACH;
40495
+ }
39498
40496
  function judgeToolCall(tool, input, ctx) {
39499
40497
  const bundle = PI_TOOL_BUNDLES[tool];
39500
- if (bundle === void 0) return outside(`${tool} is not in any bundle the coding preset reaches`);
39501
- if (!CODING_REACH.has(bundle)) {
39502
- return outside(`${tool} is the \`${bundle}\` bundle; the coding preset's reach does not include it`);
40498
+ if (bundle === void 0) return outside(`${tool} is not in any bundle the ${ctx.identity} identity reaches`);
40499
+ if (!reachFor(ctx.identity).has(bundle)) {
40500
+ return outside(`${tool} is the \`${bundle}\` bundle, outside the ${ctx.identity} identity's reach`);
39503
40501
  }
39504
40502
  const args = isRecord5(input) ? input : {};
39505
40503
  if (tool === "bash") return judgeBash(args.command, ctx);
@@ -39514,6 +40512,13 @@ function judgeBash(command2, ctx) {
39514
40512
  if (MERGE_OR_APPROVE.test(command2)) {
39515
40513
  return refused4("merge/approve \u2014 a coding run never merges or approves a pull request");
39516
40514
  }
40515
+ if (ctx.identity !== "write") {
40516
+ if (ANY_PUSH.test(command2)) return refused4("read-only \u2014 a read-identity run never pushes");
40517
+ if (GH_WRITE_VERB.test(command2) || GH_API_WRITE.test(command2) || CURL_GITHUB_WRITE.test(command2)) {
40518
+ return refused4("read-only \u2014 a read-identity run never writes to GitHub");
40519
+ }
40520
+ return allowed;
40521
+ }
39517
40522
  for (const match of command2.matchAll(GIT_PUSH)) {
39518
40523
  const verdict = judgePush(match[1], ctx);
39519
40524
  if (verdict.verdict !== "allowed") return verdict;
@@ -39549,7 +40554,7 @@ function judgePath(path, ctx) {
39549
40554
  if (rel.startsWith("..") || isAbsolute5(rel)) return refused4(`path \u2014 \`${path}\` resolves outside the checkout`);
39550
40555
  return allowed;
39551
40556
  }
39552
- var PI_TOOL_BUNDLES, CODING_REACH, allowed, refused4, outside, isRecord5, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_PUSH;
40557
+ var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, allowed, refused4, outside, isRecord5, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE;
39553
40558
  var init_toolRules = __esm({
39554
40559
  "../../src/core/harness/pi/toolRules.ts"() {
39555
40560
  "use strict";
@@ -39573,6 +40578,7 @@ var init_toolRules = __esm({
39573
40578
  "github-write",
39574
40579
  "pr"
39575
40580
  ]);
40581
+ READ_REACH = /* @__PURE__ */ new Set(["shell", "files", "web", "search", "github-read", "verdict"]);
39576
40582
  allowed = { verdict: "allowed" };
39577
40583
  refused4 = (reason) => ({ verdict: "refused", reason });
39578
40584
  outside = (reason) => ({ verdict: "outside-profile", reason });
@@ -39581,7 +40587,17 @@ var init_toolRules = __esm({
39581
40587
  ENV_DUMP = /(^|[;&|(]\s*)((printenv|env)(\s+-[A-Za-z0-9-]+)*|export\s+-p|declare\s+-[a-zA-Z]*[px][a-zA-Z]*|set)\s*($|[|;&>)])|\/proc\/(self|\d+)\/environ|process\.env(?![.[\w])/;
39582
40588
  CREDENTIAL_VAR = /\$\{?[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b|printenv\s+[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b/;
39583
40589
  MERGE_OR_APPROVE = /\bgh\s+pr\s+(merge|review)\b|\/pulls\/\d+\/(merge|reviews)\b/;
39584
- GIT_PUSH = /\bgit\s+push\b([^;&|]*)/g;
40590
+ GIT_THEN_PUSH = String.raw`\bgit(?:\s+(?:-C\s+\S+|--git-dir=\S+|--work-tree=\S+|-c\s+\S+|--no-pager))*\s+push\b`;
40591
+ GIT_PUSH = new RegExp(`${GIT_THEN_PUSH}([^;&|]*)`, "g");
40592
+ ANY_PUSH = new RegExp(GIT_THEN_PUSH);
40593
+ GH_WRITE_VERB = /\bgh\s+(pr|issue)\s+(comment|create|edit|close|reopen|ready|lock|unlock|review|merge|delete|transfer|pin|unpin)\b/;
40594
+ WRITE_METHOD = "(POST|post|PATCH|patch|PUT|put|DELETE|delete)";
40595
+ GH_API_WRITE = new RegExp(
40596
+ `\\bgh\\s+api\\b(?=[^;&|]*(?:\\s(?:-X|--method)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-f|-F|--field|--raw-field|--input)[\\s=]))`
40597
+ );
40598
+ CURL_GITHUB_WRITE = new RegExp(
40599
+ `\\bcurl\\b(?=[^;&|]*api\\.github\\.com)(?=[^;&|]*(?:\\s(?:-X|--request)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-d|--data(?:-\\w+)?|--json|-T|--upload-file)[\\s=]))`
40600
+ );
39585
40601
  }
39586
40602
  });
39587
40603
 
@@ -39590,6 +40606,7 @@ function relayedToolDefinitions(harness) {
39590
40606
  return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
39591
40607
  }
39592
40608
  function authorizeToolCall(harness, ask) {
40609
+ harness.gateSaw(ask.toolCallId);
39593
40610
  const blocked = harness.toolsBlocked();
39594
40611
  const refuse3 = (reason) => {
39595
40612
  harness.emit({
@@ -39949,9 +40966,12 @@ function parseBrief(v) {
39949
40966
  const p = b.prior;
39950
40967
  if (typeof p !== "object" || p === null || typeof p.reviewRunId !== "string" || !RUN_ID_PATTERN.test(p.reviewRunId))
39951
40968
  return invalid("brief.prior.reviewRunId must be a run id");
39952
- if (p.fixRunId !== void 0 && (typeof p.fixRunId !== "string" || !RUN_ID_PATTERN.test(p.fixRunId)))
39953
- return invalid("brief.prior.fixRunId must be a run id");
39954
- prior = { reviewRunId: p.reviewRunId, ...p.fixRunId !== void 0 ? { fixRunId: p.fixRunId } : {} };
40969
+ if (p.codingRunId !== void 0 && (typeof p.codingRunId !== "string" || !RUN_ID_PATTERN.test(p.codingRunId)))
40970
+ return invalid("brief.prior.codingRunId must be a run id");
40971
+ prior = {
40972
+ reviewRunId: p.reviewRunId,
40973
+ ...p.codingRunId !== void 0 ? { codingRunId: p.codingRunId } : {}
40974
+ };
39955
40975
  }
39956
40976
  return {
39957
40977
  ok: true,
@@ -39965,15 +40985,15 @@ function parseBrief(v) {
39965
40985
  }
39966
40986
  };
39967
40987
  }
39968
- case "fix": {
40988
+ case "findings": {
39969
40989
  const n2 = pr();
39970
40990
  if (!n2.ok) return n2;
39971
40991
  const review = runId3("reviewRunId");
39972
40992
  if (!review.ok) return review;
39973
- return { ok: true, value: { kind: "fix", unit: b.unit, pr: n2.value, reviewRunId: review.value } };
40993
+ return { ok: true, value: { kind: "findings", unit: b.unit, pr: n2.value, reviewRunId: review.value } };
39974
40994
  }
39975
40995
  default:
39976
- return invalid("brief.kind must be contract, review or fix");
40996
+ return invalid("brief.kind must be contract, review or findings");
39977
40997
  }
39978
40998
  }
39979
40999
  function parseObject(text) {
@@ -40036,6 +41056,30 @@ function unitThread(instance, row) {
40036
41056
  const sourceUrl = row?.sourceUrl ?? (threadKey === instance.threadKey ? instance.sourceUrl : void 0);
40037
41057
  return { threadKey, sourceUrl };
40038
41058
  }
41059
+ async function openThreadFromRequester(deps, instance, lead, at) {
41060
+ const parent = deps.ioFor({ threadKey: instance.threadKey, userId: instance.userId });
41061
+ if (!parent?.openThread) return { ok: false, response: json(503, { ok: false, error: "no_channel", at }) };
41062
+ try {
41063
+ const opened = await parent.openThread(lead);
41064
+ return {
41065
+ ok: true,
41066
+ thread: {
41067
+ threadKey: opened.thread.threadKey,
41068
+ ...opened.thread.sourceUrl !== void 0 ? { sourceUrl: opened.thread.sourceUrl } : {}
41069
+ }
41070
+ };
41071
+ } catch (err2) {
41072
+ return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe10(err2), at }) };
41073
+ }
41074
+ }
41075
+ async function ensureReviewThread(deps, instance, row, at) {
41076
+ if (row.reviewThread !== void 0) return { ok: true, row, thread: row.reviewThread };
41077
+ const opened = await openThreadFromRequester(deps, instance, reviewLead(instance, row), at);
41078
+ if (!opened.ok) return opened;
41079
+ const updated = { ...row, reviewThread: opened.thread };
41080
+ await deps.instances.putUnits([updated]);
41081
+ return { ok: true, row: updated, thread: opened.thread };
41082
+ }
40039
41083
  function authorizeStep(headers, step, deps) {
40040
41084
  const raw = headers.authorization;
40041
41085
  const authorization = Array.isArray(raw) ? raw[0] : raw;
@@ -40109,6 +41153,7 @@ function watched(io, on) {
40109
41153
  };
40110
41154
  if (io.attach) out.attach = (file) => io.attach(file);
40111
41155
  if (io.attachFile) out.attachFile = (file) => io.attachFile(file);
41156
+ if (io.uploadTicket) out.uploadTicket = (file) => io.uploadTicket(file);
40112
41157
  if (io.runFinished) out.runFinished = (receipt) => io.runFinished(receipt);
40113
41158
  if (io.openThread) out.openThread = (lead) => io.openThread(lead);
40114
41159
  return out;
@@ -40123,8 +41168,19 @@ async function spawn6(body, deps) {
40123
41168
  const at = (deps.clock ?? systemClock)();
40124
41169
  const unit = await unitRowOf(deps, instance, req.unit);
40125
41170
  if (!unit.ok) return unit.response;
40126
- const thread = unitThread(instance, unit.row);
40127
- if (thread.threadKey === void 0) return json(409, { ok: false, error: "unit_not_started", unit: req.unit, at });
41171
+ const own = unitThread(instance, unit.row);
41172
+ if (own.threadKey === void 0) return json(409, { ok: false, error: "unit_not_started", unit: req.unit, at });
41173
+ let row = unit.row;
41174
+ let thread = {
41175
+ threadKey: own.threadKey,
41176
+ ...own.sourceUrl !== void 0 ? { sourceUrl: own.sourceUrl } : {}
41177
+ };
41178
+ if (req.preset === "review" && row !== void 0) {
41179
+ const review = await ensureReviewThread(deps, instance, row, at);
41180
+ if (!review.ok) return review.response;
41181
+ row = review.row;
41182
+ thread = review.thread;
41183
+ }
40128
41184
  const threadKey = thread.threadKey;
40129
41185
  const key = idempotencyKeyFor(instance.id, req.step);
40130
41186
  const live = await liveOnThread(deps.runs, instance, threadKey);
@@ -40135,13 +41191,12 @@ async function spawn6(body, deps) {
40135
41191
  if (!io) return json(503, { ok: false, error: "no_channel", at });
40136
41192
  let turn;
40137
41193
  if (req.brief !== void 0) {
40138
- if (unit.row === void 0) return json(400, { ok: false, error: "a brief needs the unit it runs for" });
41194
+ if (row === void 0) return json(400, { ok: false, error: "a brief needs the unit it runs for" });
40139
41195
  try {
40140
- const composed = await composeChild(req.brief, instance, unit.row, briefReaders(deps, instance, io));
40141
- turn = composed;
41196
+ turn = await composeChild(req.brief, instance, row, briefReaders(deps, instance, io));
40142
41197
  } catch (err2) {
40143
- log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe8(err2)}`);
40144
- return json(502, { ok: false, error: "brief_failed", message: describe8(err2), at });
41198
+ log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe10(err2)}`);
41199
+ return json(502, { ok: false, error: "brief_failed", message: describe10(err2), at });
40145
41200
  }
40146
41201
  } else {
40147
41202
  turn = { prompt: req.prompt };
@@ -40184,15 +41239,14 @@ async function spawn6(body, deps) {
40184
41239
  };
40185
41240
  const settled = deps.dispatch(msg, child, {
40186
41241
  coordinator: tag,
40187
- ...turn.contract !== void 0 ? { contract: turn.contract } : {},
40188
- ...turn.fixRound !== void 0 ? { fixRound: turn.fixRound } : {}
41242
+ ...turn.contract !== void 0 ? { contract: turn.contract } : {}
40189
41243
  }).then(
40190
41244
  (outcome) => ({ kind: "ended", outcome }),
40191
41245
  (err2) => ({ kind: "threw", err: err2 })
40192
41246
  );
40193
41247
  void settled.then((end) => {
40194
41248
  if (end.kind === "threw")
40195
- log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe8(end.err)}`);
41249
+ log(`[coordinator] ${instance.id} ${req.step}: the child's dispatch threw: ${describe10(end.err)}`);
40196
41250
  });
40197
41251
  const first = await Promise.race([started.then((id) => ({ kind: "started", id })), settled]);
40198
41252
  if (first.kind === "started" || startedId !== void 0) {
@@ -40200,7 +41254,7 @@ async function spawn6(body, deps) {
40200
41254
  log(`[coordinator] ${instance.id} ${req.step}: spawned ${req.preset} run ${runId3} in ${threadKey}`);
40201
41255
  return json(200, { ok: true, runId: runId3, threadKey, at });
40202
41256
  }
40203
- if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe8(first.err), at });
41257
+ if (first.kind === "threw") return json(502, { ok: false, error: "spawn_failed", message: describe10(first.err), at });
40204
41258
  const refusal = first.outcome.refusal;
40205
41259
  if (refusal === "coordinator_thread_live") {
40206
41260
  const now = await liveOnThread(deps.runs, instance, threadKey);
@@ -40353,7 +41407,7 @@ async function prCheck(body, deps) {
40353
41407
  at
40354
41408
  });
40355
41409
  } catch (err2) {
40356
- return json(502, { ok: false, error: "github_unavailable", message: describe8(err2), at });
41410
+ return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
40357
41411
  }
40358
41412
  }
40359
41413
  async function plan2(body, deps) {
@@ -40402,19 +41456,18 @@ async function unitStart(body, deps) {
40402
41456
  ...instance.sourceUrl !== void 0 ? { sourceUrl: instance.sourceUrl } : {}
40403
41457
  };
40404
41458
  } else {
40405
- const parent = deps.ioFor({ threadKey: instance.threadKey, userId: instance.userId });
40406
- if (!parent?.openThread) return json(503, { ok: false, error: "no_channel", at });
40407
- try {
40408
- const opened = await parent.openThread(unitLead(instance, row));
40409
- row = {
40410
- ...row,
40411
- threadKey: opened.thread.threadKey,
40412
- ...opened.thread.sourceUrl !== void 0 ? { sourceUrl: opened.thread.sourceUrl } : {}
40413
- };
40414
- } catch (err2) {
40415
- return json(502, { ok: false, error: "thread_failed", message: describe8(err2), at });
40416
- }
41459
+ const opened = await openThreadFromRequester(deps, instance, unitLead(instance, row), at);
41460
+ if (!opened.ok) return opened.response;
41461
+ row = { ...row, ...opened.thread };
41462
+ }
41463
+ }
41464
+ if (row.reviewThread === void 0) {
41465
+ const review = await ensureReviewThread(deps, instance, row, at);
41466
+ if (!review.ok) {
41467
+ if (row !== unit.row) await deps.instances.putUnits([row]);
41468
+ return review.response;
40417
41469
  }
41470
+ row = review.row;
40418
41471
  }
40419
41472
  if (row.issue === void 0 && row.unit !== TASK_UNIT) {
40420
41473
  const issue = await unitIssueOf(deps, instance.repo, row.unit);
@@ -40422,10 +41475,13 @@ async function unitStart(body, deps) {
40422
41475
  }
40423
41476
  row = { ...row, startedAt: row.startedAt ?? at };
40424
41477
  await deps.instances.putUnits([row]);
40425
- (deps.log ?? console.log)(`[coordinator] ${instance.id} ${row.unit}: started in ${row.threadKey}`);
41478
+ (deps.log ?? console.log)(
41479
+ `[coordinator] ${instance.id} ${row.unit}: started in ${row.threadKey}, review in ${row.reviewThread?.threadKey}`
41480
+ );
40426
41481
  return json(200, {
40427
41482
  ok: true,
40428
41483
  threadKey: row.threadKey,
41484
+ reviewThreadKey: row.reviewThread?.threadKey,
40429
41485
  branch: row.branch,
40430
41486
  base: instance.base ?? "main",
40431
41487
  ...row.issue !== void 0 ? { issue: row.issue } : {},
@@ -40437,6 +41493,12 @@ function unitLead(instance, row) {
40437
41493
  const from = instance.sourceUrl !== void 0 ? `[the *ship* run](${instance.sourceUrl})` : "the *ship* run";
40438
41494
  return `\u21B3 *ship* unit ${row.unit}${row.title ? ` \u2014 ${row.title}` : ""} for ${who2}, from ${from}: \`${row.branch}\` in ${instance.repo}`;
40439
41495
  }
41496
+ function reviewLead(instance, row) {
41497
+ const who2 = instance.userName ?? instance.userId;
41498
+ const from = instance.sourceUrl !== void 0 ? `[the *ship* run](${instance.sourceUrl})` : "the *ship* run";
41499
+ const what = row.unit === TASK_UNIT ? "the task" : `unit ${row.unit}${row.title ? ` \u2014 ${row.title}` : ""}`;
41500
+ return `\u21B3 *ship* review of ${what} for ${who2}, from ${from}: \`${row.branch}\` in ${instance.repo}`;
41501
+ }
40440
41502
  async function branch(body, deps) {
40441
41503
  const id = parseInstanceId(body.parentInstanceId);
40442
41504
  if (!id.ok) return json(400, { ok: false, error: id.error });
@@ -40454,7 +41516,7 @@ async function branch(body, deps) {
40454
41516
  await deps.createBranchRef(instance.repo, name, base);
40455
41517
  return json(200, { ok: true, branch: name, base, at });
40456
41518
  } catch (err2) {
40457
- return json(200, { ok: false, reason: describe8(err2), at });
41519
+ return json(200, { ok: false, reason: describe10(err2), at });
40458
41520
  }
40459
41521
  }
40460
41522
  function unitLines(units) {
@@ -40506,7 +41568,7 @@ async function round(body, deps) {
40506
41568
  instance,
40507
41569
  units.map((u) => u.unit === updated.unit ? updated : u)
40508
41570
  ).catch(
40509
- (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe8(err2)}`)
41571
+ (err2) => (deps.log ?? console.warn)(`[coordinator] ${instance.id}: the card could not be redrawn: ${describe10(err2)}`)
40510
41572
  );
40511
41573
  return json(200, { ok: true, at });
40512
41574
  }
@@ -40540,7 +41602,7 @@ async function unitEnd(body, deps) {
40540
41602
  told = true;
40541
41603
  } catch (err2) {
40542
41604
  (deps.log ?? console.warn)(
40543
- `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe8(err2)}`
41605
+ `[coordinator] ${instance.id} ${row.unit}: the report could not be posted: ${describe10(err2)}`
40544
41606
  );
40545
41607
  }
40546
41608
  }
@@ -40554,7 +41616,7 @@ ${ending.report}` + (rendered !== void 0 ? `
40554
41616
  ${rendered}` : "");
40555
41617
  await deps.github.commentIssue(instance.repo, row.issue, comment).catch(
40556
41618
  (err2) => (deps.log ?? console.warn)(
40557
- `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe8(err2)}`
41619
+ `[coordinator] ${instance.id} ${row.unit}: the board comment could not be posted: ${describe10(err2)}`
40558
41620
  )
40559
41621
  );
40560
41622
  }
@@ -40609,7 +41671,7 @@ async function merge(body, deps, subject2) {
40609
41671
  try {
40610
41672
  facts2 = await deps.fetchPrFacts(pr);
40611
41673
  } catch (err2) {
40612
- return json(502, { ok: false, error: "github_unavailable", message: describe8(err2), at });
41674
+ return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
40613
41675
  }
40614
41676
  if (facts2 === void 0)
40615
41677
  return json(502, { ok: false, error: "github_unavailable", message: `${where} could not be read`, at });
@@ -40661,7 +41723,7 @@ async function merge(body, deps, subject2) {
40661
41723
  title: facts2.title ?? `Merge pull request #${pr.number}`
40662
41724
  });
40663
41725
  } catch (err2) {
40664
- return json(502, { ok: false, error: "github_unavailable", message: describe8(err2), at });
41726
+ return json(502, { ok: false, error: "github_unavailable", message: describe10(err2), at });
40665
41727
  }
40666
41728
  if (!merged.ok) {
40667
41729
  log(
@@ -40836,13 +41898,13 @@ function createAdminCoordinatorHandler(deps) {
40836
41898
  }
40837
41899
  write(res, await answerCoordinatorStep(door, read.body, deps));
40838
41900
  } catch (err2) {
40839
- (deps.log ?? console.error)(`[coordinator] ${describe8(err2)}`);
41901
+ (deps.log ?? console.error)(`[coordinator] ${describe10(err2)}`);
40840
41902
  write(res, json(500, { ok: false, error: "internal error" }));
40841
41903
  }
40842
41904
  })();
40843
41905
  };
40844
41906
  }
40845
- var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json, describe8, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, ROUND_OUTCOMES, ENDING_ICON, STEPS;
41907
+ var COORDINATOR_ADMIN_PREFIX, MAX_SPAWN_PROMPT_CHARS, FINISHED_LOOKBACK_PAGES, MAX_ADMIN_BODY_BYTES, UNIT_ID, PRESETS_OF_KIND, invalid, json, describe10, EVERY_RUN, REVIEW_POSTED_CHECKS, REVIEW_POSTED_RECHECK_MS, ROUND_OUTCOMES, ENDING_ICON, STEPS;
40846
41908
  var init_adminCoordinator = __esm({
40847
41909
  "../../src/channels/adminCoordinator.ts"() {
40848
41910
  "use strict";
@@ -40871,11 +41933,11 @@ var init_adminCoordinator = __esm({
40871
41933
  PRESETS_OF_KIND = {
40872
41934
  contract: "coding",
40873
41935
  review: "review",
40874
- fix: "coding"
41936
+ findings: "coding"
40875
41937
  };
40876
41938
  invalid = (error) => ({ ok: false, error });
40877
41939
  json = (status3, body) => ({ status: status3, body });
40878
- describe8 = (err2) => err2 instanceof Error ? err2.message : String(err2);
41940
+ describe10 = (err2) => err2 instanceof Error ? err2.message : String(err2);
40879
41941
  EVERY_RUN = { kind: "all" };
40880
41942
  REVIEW_POSTED_CHECKS = 3;
40881
41943
  REVIEW_POSTED_RECHECK_MS = 2e3;