@coreplane/switchboard 1.214.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.
package/dist/cli.js CHANGED
@@ -2892,8 +2892,11 @@ function validateRunHistory(rh) {
2892
2892
  if (v !== void 0 && (!Number.isInteger(v) || v < 1))
2893
2893
  throw new Error(`config.yaml: runHistory.${key} must be an integer >= 1`);
2894
2894
  }
2895
- if (rh.maxBytes !== void 0 && (!Number.isInteger(rh.maxBytes) || rh.maxBytes < 1))
2896
- 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
+ }
2897
2900
  if (rh.store !== void 0 && rh.store !== "worker" && rh.store !== "file")
2898
2901
  throw new Error('config.yaml: runHistory.store must be "worker" or "file"');
2899
2902
  if (rh.worker !== void 0) {
@@ -12790,6 +12793,7 @@ function isCoordinatorUnit(v) {
12790
12793
  if (!isText(r.unit, 32) || !isText(r.slug) || !isText(r.branch) || !isOptionalText(r.title)) return false;
12791
12794
  if (!Array.isArray(r.dependsOn) || !r.dependsOn.every((d) => isText(d, 32))) return false;
12792
12795
  if (!isOptionalText(r.threadKey) || !isOptionalText(r.sourceUrl)) return false;
12796
+ if (r.reviewThread !== void 0 && !isThread(r.reviewThread)) return false;
12793
12797
  if (r.issue !== void 0 && !isFinite(r.issue)) return false;
12794
12798
  if (r.pr !== void 0 && !isPr(r.pr)) return false;
12795
12799
  if (r.resume !== void 0 && !isResume(r.resume)) return false;
@@ -12800,7 +12804,7 @@ function isCoordinatorUnit(v) {
12800
12804
  if (r.startedAt !== void 0 && !isFinite(r.startedAt)) return false;
12801
12805
  return true;
12802
12806
  }
12803
- 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;
12804
12808
  var init_contract3 = __esm({
12805
12809
  "../../src/core/coordinator/contract.ts"() {
12806
12810
  "use strict";
@@ -12821,6 +12825,7 @@ var init_contract3 = __esm({
12821
12825
  isObject2 = (v) => typeof v === "object" && v !== null;
12822
12826
  isPr = (v) => isObject2(v) && isFinite(v.number) && isText(v.url, 2048);
12823
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);
12824
12829
  }
12825
12830
  });
12826
12831
 
@@ -13596,6 +13601,17 @@ var init_runFriction = __esm({
13596
13601
  });
13597
13602
 
13598
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
+ }
13599
13615
  function isRunProfileRecord(v) {
13600
13616
  if (typeof v !== "object" || v === null) return false;
13601
13617
  const p = v;
@@ -13674,7 +13690,12 @@ function clampRetentionPolicy(partial) {
13674
13690
  const [lo, hi] = RETENTION_BOUNDS[k];
13675
13691
  return Math.min(hi, Math.max(lo, n2));
13676
13692
  };
13677
- 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
+ };
13678
13699
  }
13679
13700
  function isOptionalString(v) {
13680
13701
  return v === void 0 || typeof v === "string";
@@ -13741,6 +13762,7 @@ function isRunRecord(v) {
13741
13762
  if (r.parentRunId !== void 0 && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
13742
13763
  return false;
13743
13764
  if (r.seed !== void 0 && !RUN_SEEDS.includes(r.seed)) return false;
13765
+ if (r.session !== void 0 && !isRunSession(r.session)) return false;
13744
13766
  if (r.parentInstanceId === void 0 !== (r.idempotencyKey === void 0)) return false;
13745
13767
  if (r.parentInstanceId !== void 0 && (typeof r.parentInstanceId !== "string" || !INSTANCE_ID_PATTERN.test(r.parentInstanceId)))
13746
13768
  return false;
@@ -13872,7 +13894,7 @@ function dropSpanPairsFromTheMiddle(events, eventBudget) {
13872
13894
  }
13873
13895
  return events.filter((_, k) => !dropped.has(k));
13874
13896
  }
13875
- var RUN_STATUSES, RUN_ID_PATTERN, REVIEW_HEAD_PATTERN, RUN_SEEDS, 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;
13876
13898
  var init_runRecord = __esm({
13877
13899
  "../../src/core/runRecord.ts"() {
13878
13900
  "use strict";
@@ -13885,6 +13907,8 @@ var init_runRecord = __esm({
13885
13907
  RUN_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
13886
13908
  REVIEW_HEAD_PATTERN = /^[0-9a-f]{7,40}$/;
13887
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;
13888
13912
  MACHINE_CLASSES_IN_RECORD = {
13889
13913
  none: true,
13890
13914
  blank: true,
@@ -13910,12 +13934,14 @@ var init_runRecord = __esm({
13910
13934
  DEFAULT_RETENTION_POLICY = {
13911
13935
  retentionDays: 30,
13912
13936
  maxRuns: 5e3,
13913
- maxBytes: 2 * GIB2
13937
+ maxBytes: 2 * GIB2,
13938
+ sessionLogMaxBytes: 200 * MIB
13914
13939
  };
13915
13940
  RETENTION_BOUNDS = {
13916
13941
  retentionDays: [1, 365],
13917
13942
  maxRuns: [1, 2e4],
13918
- maxBytes: [16 * MIB, 8 * GIB2]
13943
+ maxBytes: [16 * MIB, 8 * GIB2],
13944
+ sessionLogMaxBytes: [16 * MIB, 2 * GIB2]
13919
13945
  };
13920
13946
  MAX_RECORD_BYTES = 1.5 * MIB;
13921
13947
  MAX_EVENT_BYTES = 64 * KIB;
@@ -22079,6 +22105,9 @@ function reclaimedRunRecord(input) {
22079
22105
  ...row.meta.profile && row.meta.agent ? { profile: { preset: row.meta.agent, ...row.meta.profile } } : {},
22080
22106
  ...row.meta.parentRunId !== void 0 ? { parentRunId: row.meta.parentRunId } : {},
22081
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 } : {},
22082
22111
  // A coordinator's child keeps its instance and key on the close (item 48),
22083
22112
  // so the state Worker's finish still sends the parent its event.
22084
22113
  ...row.meta.parentInstanceId !== void 0 && row.meta.idempotencyKey !== void 0 ? { coordinator: { parentInstanceId: row.meta.parentInstanceId, idempotencyKey: row.meta.idempotencyKey } } : {},
@@ -22148,7 +22177,8 @@ function assembleRunRecord(input) {
22148
22177
  ...input.profile !== void 0 ? { profile: input.profile } : {},
22149
22178
  ...input.parentRunId !== void 0 ? { parentRunId: input.parentRunId } : {},
22150
22179
  ...coordinatorFields(input.coordinator),
22151
- ...input.seed !== void 0 ? { seed: input.seed } : {}
22180
+ ...input.seed !== void 0 ? { seed: input.seed } : {},
22181
+ ...input.session !== void 0 ? { session: input.session } : {}
22152
22182
  });
22153
22183
  return fitted.eventCount !== fitted.storedEventCount ? { ...fitted, truncated: true } : fitted;
22154
22184
  }
@@ -22346,7 +22376,9 @@ async function admit(deps, ctx) {
22346
22376
  threadKey: msg.threadKey,
22347
22377
  state: resume.row.state,
22348
22378
  lastStep: resume.lastStep.step,
22349
- 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 } : {}
22350
22382
  });
22351
22383
  await root.span(
22352
22384
  "dispatch.admission",
@@ -22496,6 +22528,8 @@ async function adoptCarriedRun(deps, ctx) {
22496
22528
  state: resume.row.state,
22497
22529
  lastStep: resume.lastStep.step,
22498
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 } : {},
22499
22533
  ...ctx.hooks.adopt
22500
22534
  });
22501
22535
  }
@@ -24590,6 +24624,12 @@ function routablePresets() {
24590
24624
  maxMinutes
24591
24625
  }));
24592
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
+ }
24593
24633
  function renderPresetTable(presets) {
24594
24634
  const rows = presets.map(
24595
24635
  (p) => `| \`${p.name}\` | ${oneLine(p.description)} | ${p.machine} | ${p.identity} | ${p.maxMinutes} min |`
@@ -24604,7 +24644,10 @@ function routeMaxOutputTokens(compound) {
24604
24644
  }
24605
24645
  function routeTool(presets, compound) {
24606
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);
24607
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.` : "";
24608
24651
  const properties = {
24609
24652
  preset: {
24610
24653
  type: "string",
@@ -24615,7 +24658,7 @@ function routeTool(presets, compound) {
24615
24658
  ...compound ? {
24616
24659
  parts: {
24617
24660
  type: "array",
24618
- description: `only when the request has two or more INDEPENDENT asks on different subjects, each rewritten so it stands alone, with preset "${COMPOUND_PRESET}". 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.`,
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.`,
24619
24662
  minItems: 2,
24620
24663
  maxItems: compound.maxParts,
24621
24664
  items: {
@@ -24623,7 +24666,7 @@ function routeTool(presets, compound) {
24623
24666
  additionalProperties: false,
24624
24667
  required: ["preset", "text"],
24625
24668
  properties: {
24626
- preset: { type: "string", enum: names, description: "the preset this part runs on" },
24669
+ preset: { type: "string", enum: readers, description: "the read-only preset this part runs on" },
24627
24670
  text: { type: "string", description: "the part as a request of its own" }
24628
24671
  }
24629
24672
  }
@@ -24660,7 +24703,7 @@ function buildRoutePrompt(input) {
24660
24703
  "",
24661
24704
  "Presets you may pick:",
24662
24705
  renderPresetTable(input.presets),
24663
- ...input.compound ? ["", ...compoundRules(input.compound)] : []
24706
+ ...input.compound ? ["", ...compoundRules(input.compound, input.presets)] : []
24664
24707
  ].join("\n");
24665
24708
  const user = [
24666
24709
  `Earlier directives in this thread: ${directivesLine(input.recentDirectives)}`,
@@ -24671,13 +24714,20 @@ function buildRoutePrompt(input) {
24671
24714
  ].join("\n");
24672
24715
  return { system, user, tool: routeTool(input.presets, input.compound) };
24673
24716
  }
24674
- 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}\``);
24675
24720
  return [
24676
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:",
24677
- `{"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>"}`,
24678
- `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)] : []
24679
24725
  ];
24680
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
+ }
24681
24731
  function imperativeRule(writers) {
24682
24732
  const names = writers.map((w) => `\`${w}\``).join(" or ");
24683
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.`;
@@ -24729,16 +24779,20 @@ function parseCompound(parts, reason, allowed2, offer) {
24729
24779
  if (tidy.length === 0) return rejected(`part ${n2} has no text`);
24730
24780
  out.push({ preset, text: tidy });
24731
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) } };
24732
24784
  return { preset: COMPOUND_PRESET, reason, parts: out };
24733
24785
  }
24734
24786
  async function route(input, model, opts = {}) {
24735
- 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));
24736
24789
  if (offered.length === 0) return { preset: void 0, reason: "no preset the requester may run" };
24737
- 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 } : {} });
24738
24792
  let raw;
24739
24793
  try {
24740
24794
  raw = await model(prompt, {
24741
- maxTokens: routeMaxOutputTokens(input.compound),
24795
+ maxTokens: routeMaxOutputTokens(compound),
24742
24796
  signal: AbortSignal.timeout(opts.timeoutMs ?? ROUTE_TIMEOUT_MS)
24743
24797
  });
24744
24798
  } catch (err2) {
@@ -24750,7 +24804,7 @@ async function route(input, model, opts = {}) {
24750
24804
  return parseRouteAnswer(
24751
24805
  raw,
24752
24806
  offered.map((p) => p.name),
24753
- input.compound
24807
+ compound
24754
24808
  );
24755
24809
  }
24756
24810
  function providerRouteModel(provider, model, opts = {}) {
@@ -24824,18 +24878,24 @@ async function routeRequest(deps, ctx) {
24824
24878
  effort: directives.effort ?? sticky.effort
24825
24879
  }
24826
24880
  });
24827
- const parts = decision.parts;
24881
+ const { parts, collapsed } = decision;
24828
24882
  console.log(
24829
- `[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}`
24830
24884
  );
24831
24885
  return {
24832
24886
  kind: "routed",
24833
24887
  resolved,
24834
- 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
+ }
24835
24895
  };
24836
24896
  }
24837
- function routedLabel(reason) {
24838
- return `routed: ${reason}`;
24897
+ function routedLabel(reason, collapsed) {
24898
+ return `routed: ${reason}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}`;
24839
24899
  }
24840
24900
  function routedPartLines(parts) {
24841
24901
  return parts.map((p) => {
@@ -24851,7 +24911,7 @@ function compoundBrief(text, parts) {
24851
24911
  ...parts.map((p, i) => `${i + 1}. \`${p.preset}\`: ${p.text}`)
24852
24912
  ].join("\n");
24853
24913
  }
24854
- 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, 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;
24855
24915
  var init_route = __esm({
24856
24916
  "../../src/core/dispatch/route.ts"() {
24857
24917
  "use strict";
@@ -24868,6 +24928,7 @@ var init_route = __esm({
24868
24928
  ROUTE_PART_TEXT_CAP = 1e3;
24869
24929
  ROUTE_PART_LINE_CAP = 100;
24870
24930
  COMPOUND_BRIEF_HEADING = "Routed as a compound request";
24931
+ identityOf = (preset) => AGENTS[preset]?.identity;
24871
24932
  ROUTED_CARD_FOOTER = "reply agent:<preset> to run it another way";
24872
24933
  }
24873
24934
  });
@@ -26638,7 +26699,7 @@ ${raw.trim()}`;
26638
26699
  };
26639
26700
  submitDispositionsTool = {
26640
26701
  name: "submit_dispositions",
26641
- 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.",
26642
26703
  inputSchema: {
26643
26704
  type: "object",
26644
26705
  properties: {
@@ -26661,15 +26722,7 @@ ${raw.trim()}`;
26661
26722
  async run(input, ctx) {
26662
26723
  const parsed = parseDispositionsInput(input);
26663
26724
  if (!parsed) return "error: dispositions must be an array of { findingId, disposition: fixed|declined, note }";
26664
- if (ctx.knownFindingIds) {
26665
- const known = new Set(ctx.knownFindingIds);
26666
- const unknown = [...new Set(parsed.dispositions.map((d) => d.findingId).filter((id) => !known.has(id)))];
26667
- if (unknown.length) {
26668
- return `error: unknown finding id${unknown.length === 1 ? "" : "s"} ${unknown.join(", ")} \u2014 use exactly the ids from the review's findings list`;
26669
- }
26670
- }
26671
- if (!ctx.onDispositions)
26672
- 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";
26673
26726
  ctx.onDispositions(parsed.dispositions);
26674
26727
  const drops = parsed.dropped.length ? ` (dropped: ${parsed.dropped.join("; ")})` : "";
26675
26728
  return `dispositions recorded: ${parsed.dispositions.length}${drops}; a later call replaces this one`;
@@ -28953,7 +29006,7 @@ function startMemoryRead(deps, ctx) {
28953
29006
  async function openAckCard(deps, ctx) {
28954
29007
  const { io, agent, resolved, startedAt, clock: clock3, root, trace, route: route3 } = ctx;
28955
29008
  const shell = createCardShell({
28956
- 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)}` : ""}`,
28957
29010
  startedAt,
28958
29011
  now: clock3,
28959
29012
  ...route3?.parts ? { lead: routedPartLines(route3.parts) } : {},
@@ -29872,7 +29925,7 @@ var init_bridge2 = __esm({
29872
29925
  this.onToolEnd(event, out);
29873
29926
  break;
29874
29927
  case "compaction_end":
29875
- this.onCompactionEnd(event);
29928
+ this.onCompactionEnd(event, out);
29876
29929
  break;
29877
29930
  case "summarization_retry_scheduled":
29878
29931
  this.summarizationRetries++;
@@ -29986,7 +30039,7 @@ var init_bridge2 = __esm({
29986
30039
  }
29987
30040
  }
29988
30041
  }
29989
- onCompactionEnd(event) {
30042
+ onCompactionEnd(event, out) {
29990
30043
  const result = isRecord3(event.result) ? event.result : void 0;
29991
30044
  if (event.aborted === true || !result) {
29992
30045
  this.note(
@@ -30003,6 +30056,13 @@ var init_bridge2 = __esm({
30003
30056
  "compacted",
30004
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"}` : ""}`
30005
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
+ }
30006
30066
  }
30007
30067
  onUiRequest(event, out) {
30008
30068
  const method = str5(event.method);
@@ -30071,7 +30131,7 @@ function chatMessageOf(message2) {
30071
30131
  return void 0;
30072
30132
  }
30073
30133
  }
30074
- function piSessionFile(messages, opts) {
30134
+ function piSessionFile(messages, opts, compactions = []) {
30075
30135
  const lines = [];
30076
30136
  const stamp = new Date(opts.at).toISOString();
30077
30137
  lines.push(
@@ -30080,12 +30140,35 @@ function piSessionFile(messages, opts) {
30080
30140
  let parentId = null;
30081
30141
  let n2 = 0;
30082
30142
  const toolNames = /* @__PURE__ */ new Map();
30143
+ const firstEntryOf = [];
30144
+ const nextId = () => (n2++).toString(16).padStart(8, "0");
30083
30145
  const push = (message2) => {
30084
- const id = (n2++).toString(16).padStart(8, "0");
30146
+ const id = nextId();
30085
30147
  lines.push(JSON.stringify({ type: "message", id, parentId, timestamp: stamp, message: message2 }));
30086
30148
  parentId = id;
30149
+ return id;
30087
30150
  };
30088
- 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");
30089
30172
  if (message2.role === "assistant") {
30090
30173
  const content = [];
30091
30174
  for (const p of message2.content) {
@@ -30113,7 +30196,7 @@ function piSessionFile(messages, opts) {
30113
30196
  stopReason: hasTool ? "toolUse" : "stop",
30114
30197
  timestamp: opts.at
30115
30198
  });
30116
- continue;
30199
+ return;
30117
30200
  }
30118
30201
  const results = message2.content.filter(
30119
30202
  (p) => p.type === "tool_result"
@@ -30140,7 +30223,8 @@ function piSessionFile(messages, opts) {
30140
30223
  });
30141
30224
  }
30142
30225
  if (rest.length > 0) push({ role: "user", content: rest, timestamp: opts.at });
30143
- }
30226
+ });
30227
+ for (const c of byPosition.get(messages.length) ?? []) pushCompaction(c);
30144
30228
  return lines.join("\n") + "\n";
30145
30229
  }
30146
30230
  function sessionIdOf(at) {
@@ -30197,6 +30281,28 @@ var init_mirror = __esm({
30197
30281
  this.idx += turns.length;
30198
30282
  await this.deps.onStep(report2);
30199
30283
  }
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);
30305
+ }
30200
30306
  };
30201
30307
  }
30202
30308
  });
@@ -30535,25 +30641,33 @@ async function runPiHarness(deps, run2) {
30535
30641
  let session;
30536
30642
  if (run2.resume) {
30537
30643
  const settled2 = settlementResults(run2.resume.settlements);
30538
- 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
+ };
30539
30649
  } else {
30540
30650
  const earlier2 = splitSeed(run2.messages).session;
30541
- if (earlier2.length > 0) session = { stem: "seed", messages: earlier2 };
30651
+ if (earlier2.length > 0) session = { stem: "seed", messages: earlier2, compactions: [] };
30542
30652
  }
30543
30653
  let sessionPath;
30544
30654
  if (session) {
30545
30655
  sessionPath = `${paths.sessionDir}/${session.stem}-${now()}.jsonl`;
30546
30656
  await container.writeFile(
30547
30657
  sessionPath,
30548
- piSessionFile(session.messages, {
30549
- cwd: run2.rules.checkout,
30550
- model: {
30551
- provider: run2.model.provider,
30552
- id: run2.model.id,
30553
- 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()
30554
30668
  },
30555
- at: now()
30556
- })
30669
+ session.compactions
30670
+ )
30557
30671
  );
30558
30672
  }
30559
30673
  if (run2.resume) {
@@ -30682,6 +30796,7 @@ async function runPiHarness(deps, run2) {
30682
30796
  }
30683
30797
  }
30684
30798
  if (obs.message) await mirror.onMessage(obs.message, bridge.turns);
30799
+ if (obs.compaction) await mirror.onCompaction(obs.compaction, bridge.turns);
30685
30800
  if (obs.providerError !== void 0) {
30686
30801
  if (catchingUp)
30687
30802
  note("harness_error", `a model call failed while the bot was away (${obs.providerError}); continuing`);
@@ -31269,8 +31384,7 @@ async function runLoop2(deps, ctx) {
31269
31384
  wait,
31270
31385
  parentRunId,
31271
31386
  coordinator,
31272
- seed,
31273
- fixRound
31387
+ seed
31274
31388
  } = ctx;
31275
31389
  const agent = budgetedAgent(ctx.agent, profile);
31276
31390
  const { executor, binding } = round2.selection;
@@ -31373,7 +31487,7 @@ async function runLoop2(deps, ctx) {
31373
31487
  handoff = h;
31374
31488
  ledgerRun?.setState({ handoff: h });
31375
31489
  };
31376
- const restoredDispositions = fixRound ? parseDispositionsInput({ dispositions: restored.dispositions })?.dispositions : void 0;
31490
+ const restoredDispositions = parseDispositionsInput({ dispositions: restored.dispositions })?.dispositions;
31377
31491
  let dispositions = restoredDispositions;
31378
31492
  const onDispositions = (d) => {
31379
31493
  dispositions = d;
@@ -31420,7 +31534,7 @@ async function runLoop2(deps, ctx) {
31420
31534
  onDigest,
31421
31535
  onPrDescription,
31422
31536
  onHandoff,
31423
- ...fixRound ? { knownFindingIds: fixRound.findingIds, onDispositions } : {}
31537
+ onDispositions
31424
31538
  };
31425
31539
  const harness = effectiveHarness(agent, deps.config.config.harness);
31426
31540
  try {
@@ -31481,6 +31595,7 @@ async function runLoop2(deps, ctx) {
31481
31595
  ...resume ? {
31482
31596
  resume: {
31483
31597
  messages: resume.plan.messages,
31598
+ compactions: resume.plan.compactions,
31484
31599
  settlements: resume.plan.settlements,
31485
31600
  remainingMs: resume.plan.remainingMs,
31486
31601
  turn: resume.plan.turn,
@@ -32060,6 +32175,12 @@ function openPlanCursor(graph, selected) {
32060
32175
  for (const id of order) visit(id, []);
32061
32176
  return cursor;
32062
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
+ }
32063
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;
32064
32185
  var init_coordinator = __esm({
32065
32186
  "../../src/core/ship/coordinator.ts"() {
@@ -32082,29 +32203,6 @@ var init_coordinator = __esm({
32082
32203
  }
32083
32204
  });
32084
32205
 
32085
- // ../../src/core/ship/codingChild.ts
32086
- function buildShipFixTurn(input) {
32087
- const findings = input.findings.map(formatFinding).join("\n") || "(the review listed no structured findings \u2014 address its prose)";
32088
- 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.
32089
-
32090
- Findings:
32091
- ${findings}
32092
-
32093
- Review:
32094
- ${input.review}`;
32095
- }
32096
- function withContractInFirstUserTurn(messages, block) {
32097
- const at = messages.findIndex((m) => m.role === "user");
32098
- if (at < 0) return [...messages, { role: "user", content: [{ type: "text", text: block }] }];
32099
- return messages.map((m, i) => i === at ? { ...m, content: [...m.content, { type: "text", text: block }] } : m);
32100
- }
32101
- var init_codingChild = __esm({
32102
- "../../src/core/ship/codingChild.ts"() {
32103
- "use strict";
32104
- init_reviewVerdict();
32105
- }
32106
- });
32107
-
32108
32206
  // ../../src/core/ship/reviewChild.ts
32109
32207
  function buildShipReviewTurn(input) {
32110
32208
  const at = input.headSha ? ` at head \`${input.headSha}\`` : "";
@@ -32113,13 +32211,15 @@ function buildShipReviewTurn(input) {
32113
32211
  }
32114
32212
  const findings = input.prior.findings.map(formatFinding).join("\n") || "(none recorded)";
32115
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(", ")}` : "";
32116
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.
32117
32217
 
32118
32218
  Previous round's findings:
32119
32219
  ${findings}
32120
32220
 
32121
32221
  Fix round's dispositions:
32122
- ${dispositions}`;
32222
+ ${dispositions}${dropped}`;
32123
32223
  }
32124
32224
  var init_reviewChild = __esm({
32125
32225
  "../../src/core/ship/reviewChild.ts"() {
@@ -32165,6 +32265,16 @@ async function contractFor(instance, unit, readers) {
32165
32265
  ...issue ? { issue } : {}
32166
32266
  });
32167
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
+ }
32168
32278
  async function composeChild(brief, instance, unit, readers) {
32169
32279
  const where = `${instance.repo}#${"pr" in brief ? brief.pr : ""}`;
32170
32280
  switch (brief.kind) {
@@ -32174,10 +32284,13 @@ async function composeChild(brief, instance, unit, readers) {
32174
32284
  return { preset: "coding", prompt, ref: unit.branch, contract };
32175
32285
  }
32176
32286
  case "review": {
32177
- const prior = brief.prior !== void 0 ? {
32178
- findings: (await facts(readers, brief.prior.reviewRunId)).findings ?? [],
32179
- dispositions: brief.prior.fixRunId !== void 0 ? (await facts(readers, brief.prior.fixRunId)).dispositions ?? [] : []
32180
- } : 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
+ }
32181
32294
  const turn = buildShipReviewTurn({
32182
32295
  where,
32183
32296
  round: brief.round,
@@ -32189,16 +32302,18 @@ async function composeChild(brief, instance, unit, readers) {
32189
32302
 
32190
32303
  ${turn}`, contract };
32191
32304
  }
32192
- case "fix": {
32305
+ case "findings": {
32193
32306
  const review = await facts(readers, brief.reviewRunId);
32194
- const findings = review.findings ?? [];
32195
32307
  return {
32196
32308
  preset: "coding",
32197
- prompt: buildShipFixTurn({ where, findings, review: review.finalReply ?? "" }),
32198
- ref: unit.branch,
32199
- fixRound: { findingIds: findings.map((f) => f.id) }
32309
+ prompt: findingsRequest({ where, findings: review.findings ?? [], review: review.finalReply ?? "" }),
32310
+ ref: unit.branch
32200
32311
  };
32201
32312
  }
32313
+ default: {
32314
+ const unknown = brief;
32315
+ throw new Error(`unknown brief kind ${JSON.stringify(unknown.kind)}`);
32316
+ }
32202
32317
  }
32203
32318
  }
32204
32319
  async function facts(readers, runId3) {
@@ -32211,7 +32326,8 @@ var init_briefs = __esm({
32211
32326
  "../../src/core/coordinator/briefs.ts"() {
32212
32327
  "use strict";
32213
32328
  init_directives();
32214
- init_codingChild();
32329
+ init_reviewVerdict();
32330
+ init_coordinator();
32215
32331
  init_contract();
32216
32332
  init_preflight();
32217
32333
  init_reviewChild();
@@ -33060,6 +33176,18 @@ var init_ship = __esm({
33060
33176
  }
33061
33177
  });
33062
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
+
33063
33191
  // ../../src/core/dispatch/settle.ts
33064
33192
  function settleThread(deps, ctx) {
33065
33193
  const { msg, admitted, runLoopStarted, control } = ctx;
@@ -33647,7 +33775,6 @@ async function dispatch(deps, msg, io, opts = {}) {
33647
33775
  parentRunId,
33648
33776
  coordinator,
33649
33777
  seed,
33650
- ...opts.fixRound ? { fixRound: opts.fixRound } : {},
33651
33778
  ...bearer !== void 0 ? { bearer } : {}
33652
33779
  });
33653
33780
  const { answer, prNote, toolCalls, runDiagnosis, checklistAsLeft, checklistCheckedOff, releaseWorkspace } = ran;
@@ -33923,7 +34050,12 @@ function toListItem(record2, bytes) {
33923
34050
  return { ...rest, bytes };
33924
34051
  }
33925
34052
  function retentionPolicyOf(cfg) {
33926
- 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
+ });
33927
34059
  }
33928
34060
  function buildRunStore(cfg, secrets, deps) {
33929
34061
  if (!cfg) return null;
@@ -34167,6 +34299,24 @@ var init_flusher = __esm({
34167
34299
  }
34168
34300
  });
34169
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
+
34170
34320
  // ../../src/core/runLedger/types.ts
34171
34321
  var LEASE_MS, HEARTBEAT_MS, APPEND_FLUSH_MS, APPEND_FLUSH_EVENTS, TRANSCRIPT_REQUEST_BYTES, TRANSCRIPT_PART_BYTES, ATTACHMENT_REF_BYTES, GEN_PATTERN;
34172
34322
  var init_types5 = __esm({
@@ -34212,36 +34362,45 @@ function createLedgerWriteThrough(opts) {
34212
34362
  "[ledger] state Worker has no run-ledger routes \u2014 deploy it before this bot version; runs are not tracked until then"
34213
34363
  );
34214
34364
  };
34215
- async function claim(req, phase) {
34365
+ async function claim(req, opts2 = {}) {
34216
34366
  for (let attempt = 1; ; attempt++) {
34217
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
+ }
34218
34374
  const result = await ledger.claim({
34219
34375
  runId: req.runId,
34220
34376
  threadKey: req.threadKey,
34221
34377
  gen,
34222
34378
  leaseMs,
34223
34379
  startedAt: req.startedAt,
34224
- meta: req.meta,
34380
+ meta: session ? { ...req.meta, session } : req.meta,
34225
34381
  card: req.card ?? null,
34226
34382
  system: req.system,
34227
34383
  tools: req.tools,
34228
34384
  ...req.state !== void 0 ? { state: req.state } : {},
34229
- ...phase ? { phase } : {}
34385
+ ...opts2.phase ? { phase: opts2.phase } : {}
34230
34386
  });
34231
- if (result.ok) return "ok";
34232
- 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" };
34233
34392
  warn(
34234
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`
34235
34394
  );
34236
- return "untracked";
34395
+ return { outcome: "untracked" };
34237
34396
  } catch (err2) {
34238
34397
  if (err2 instanceof RouteMissingError) {
34239
34398
  routeMissing();
34240
- return "untracked";
34399
+ return { outcome: "untracked" };
34241
34400
  }
34242
34401
  if (err2 instanceof PermanentStoreError || attempt >= claimAttempts) {
34243
34402
  warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe8(err2)}`);
34244
- return "untracked";
34403
+ return { outcome: "untracked" };
34245
34404
  }
34246
34405
  await sleep2(RETRY_MS[Math.min(attempt, RETRY_MS.length) - 1]);
34247
34406
  }
@@ -34261,6 +34420,15 @@ function createLedgerWriteThrough(opts) {
34261
34420
  seeded = false;
34262
34421
  adopted = false;
34263
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;
34264
34432
  stepNo;
34265
34433
  lastSeq;
34266
34434
  state;
@@ -34281,7 +34449,10 @@ function createLedgerWriteThrough(opts) {
34281
34449
  },
34282
34450
  ...opts.schedule ? { schedule: opts.schedule } : {}
34283
34451
  });
34284
- constructor(req, from = { stepNo: 0, lastSeq: 0 }) {
34452
+ constructor(req, from = {
34453
+ stepNo: 0,
34454
+ lastSeq: 0
34455
+ }) {
34285
34456
  this.runId = req.runId;
34286
34457
  this.threadKey = req.threadKey;
34287
34458
  this.state = req.state ?? {};
@@ -34290,6 +34461,11 @@ function createLedgerWriteThrough(opts) {
34290
34461
  this.stepNo = from.stepNo;
34291
34462
  this.lastSeq = from.lastSeq;
34292
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;
34293
34469
  }
34294
34470
  get resumable() {
34295
34471
  return !this.detached && (this.seeded || this.adopted);
@@ -34297,11 +34473,24 @@ function createLedgerWriteThrough(opts) {
34297
34473
  markHandedOff() {
34298
34474
  this.handedOff = true;
34299
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
+ }
34300
34487
  sink = {
34301
- put: async (record2) => {
34488
+ put: async (plain4) => {
34302
34489
  this.finished = true;
34303
34490
  live.delete(this);
34304
34491
  await this.close();
34492
+ const session = this.sessionOnRecord();
34493
+ const record2 = session ? { ...plain4, session } : plain4;
34305
34494
  let result;
34306
34495
  try {
34307
34496
  result = await ledger.finish(this.runId, gen, record2);
@@ -34322,6 +34511,7 @@ function createLedgerWriteThrough(opts) {
34322
34511
  detach(reason) {
34323
34512
  if (this.detached) return;
34324
34513
  this.detached = true;
34514
+ this.broken = true;
34325
34515
  warn(`[ledger] ${this.threadKey} run ${this.runId} detached: ${reason} \u2014 this run is not resumable`);
34326
34516
  this.stopHeartbeat();
34327
34517
  live.delete(this);
@@ -34339,13 +34529,15 @@ function createLedgerWriteThrough(opts) {
34339
34529
  * no record at all was killed before its conversation was stored and
34340
34530
  * closes `interrupted`. */
34341
34531
  async seed(messages, budgetMs) {
34342
- 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 }));
34343
34534
  try {
34344
- const seeded = await ledger.seed(this.runId, gen, turns);
34535
+ const seeded = await ledger.seed(this.runId, gen, turns, this.session?.key);
34345
34536
  if (!seeded.ok) {
34346
34537
  this.detach(`seed refused (${seeded.reason})`);
34347
34538
  return;
34348
34539
  }
34540
+ this.turnsWritten = messages.length;
34349
34541
  const recorded = await ledger.step(
34350
34542
  this.runId,
34351
34543
  gen,
@@ -34369,11 +34561,14 @@ function createLedgerWriteThrough(opts) {
34369
34561
  }
34370
34562
  async step(report2) {
34371
34563
  if (this.detached) return;
34372
- 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 });
34373
34568
  const record2 = {
34374
34569
  step: ++this.stepNo,
34375
34570
  seq: this.lastSeq,
34376
- turnIndex: report2.firstIdx + report2.turns.length,
34571
+ turnIndex: report2.firstIdx + turns.length,
34377
34572
  inFlight: report2.inFlight,
34378
34573
  inboxConsumedSeq: report2.inboxConsumedSeq,
34379
34574
  remainingMs: report2.remainingMs,
@@ -34382,8 +34577,9 @@ function createLedgerWriteThrough(opts) {
34382
34577
  };
34383
34578
  for (let attempt = 1; ; attempt++) {
34384
34579
  try {
34385
- const result = await ledger.step(this.runId, gen, record2, turns);
34580
+ const result = await ledger.step(this.runId, gen, record2, turns, this.session?.key);
34386
34581
  if (!result.ok) this.detach(`step ${record2.step} refused (${result.reason})`);
34582
+ else this.turnsWritten = record2.turnIndex;
34387
34583
  return;
34388
34584
  } catch (err2) {
34389
34585
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
@@ -34491,8 +34687,8 @@ function createLedgerWriteThrough(opts) {
34491
34687
  return {
34492
34688
  gen,
34493
34689
  async reserve(req) {
34494
- const claimed2 = await claim({ ...req, system: "", tools: [], state: {} }, "attaching");
34495
- 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;
34496
34692
  const run2 = new TrackedRun(req);
34497
34693
  run2.startHeartbeat();
34498
34694
  live.add(run2);
@@ -34500,35 +34696,46 @@ function createLedgerWriteThrough(opts) {
34500
34696
  },
34501
34697
  async open(req) {
34502
34698
  const reserved = req.reservation;
34699
+ const seed = req.seed?.messages;
34503
34700
  if (reserved instanceof TrackedRun) {
34504
34701
  if (!reserved.tracked()) return void 0;
34505
- const claimed3 = await claim(req);
34506
- if (claimed3 === "fenced") {
34702
+ const claimed3 = await claim(req, seed ? { seed } : {});
34703
+ if (claimed3.outcome === "fenced") {
34507
34704
  reserved.detach("promotion refused (fenced)");
34508
34705
  return void 0;
34509
34706
  }
34510
- if (claimed3 !== "ok") {
34707
+ if (claimed3.outcome !== "ok") {
34511
34708
  await reserved.close();
34512
34709
  live.delete(reserved);
34513
34710
  return void 0;
34514
34711
  }
34712
+ if (claimed3.session) reserved.bindSession(claimed3.session);
34515
34713
  if (req.seed) await reserved.seed(req.seed.messages, req.seed.budgetMs);
34516
34714
  return reserved;
34517
34715
  }
34518
- const claimed2 = await claim(req);
34519
- if (claimed2 === "fenced") {
34716
+ const claimed2 = await claim(req, seed ? { seed } : {});
34717
+ if (claimed2.outcome === "fenced") {
34520
34718
  warn(`[ledger] ${req.threadKey} not tracked: run ${req.runId} is live under another generation`);
34521
34719
  return void 0;
34522
34720
  }
34523
- if (claimed2 !== "ok") return void 0;
34524
- 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
+ });
34525
34727
  if (req.seed) await run2.seed(req.seed.messages, req.seed.budgetMs);
34526
34728
  run2.startHeartbeat();
34527
34729
  live.add(run2);
34528
34730
  return run2;
34529
34731
  },
34530
34732
  adopt(req) {
34531
- 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
+ });
34532
34739
  run2.startHeartbeat();
34533
34740
  live.add(run2);
34534
34741
  return run2;
@@ -34578,6 +34785,7 @@ var init_writeThrough = __esm({
34578
34785
  "use strict";
34579
34786
  init_runStoreWorker();
34580
34787
  init_flusher();
34788
+ init_sessionLog();
34581
34789
  init_types5();
34582
34790
  NullLedgerWriteThrough = class {
34583
34791
  constructor(gen, fallback) {
@@ -38847,6 +39055,16 @@ function turnRows(idx, message2, opts = {}) {
38847
39055
  const refBytes = opts.attachmentRefBytes ?? ATTACHMENT_REF_BYTES;
38848
39056
  const rows = [];
38849
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
+ }
38850
39068
  message2.content.forEach((part, i) => {
38851
39069
  let stored = part;
38852
39070
  if (hasData(part) && part.data.length > refBytes) {
@@ -38880,49 +39098,69 @@ function chunkRows(rows, maxBytes) {
38880
39098
  if (current.length > 0) chunks.push(current);
38881
39099
  return chunks;
38882
39100
  }
38883
- function assembleTranscript(rows, attachments) {
39101
+ function assembleTranscript(rows, attachments, base = 0) {
38884
39102
  const byRef = new Map(attachments.map((a) => [a.ref, a]));
38885
39103
  const byTurn = /* @__PURE__ */ new Map();
38886
39104
  for (const row of rows) {
38887
- let parts = byTurn.get(row.idx);
38888
- 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());
38889
39108
  parts.set(row.part, JSON.parse(row.json));
38890
39109
  }
38891
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
+ });
38892
39120
  const turnCount = byTurn.size === 0 ? 0 : Math.max(...byTurn.keys()) + 1;
38893
39121
  for (let idx = 0; idx < turnCount; idx++) {
38894
39122
  const parts = byTurn.get(idx);
38895
- 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
+ }
38896
39129
  const content = [];
38897
39130
  let role;
38898
39131
  const partCount = Math.max(...parts.keys()) + 1;
38899
39132
  for (let p = 0; p < partCount; p++) {
38900
39133
  const stored = parts.get(p);
38901
- if (!stored)
38902
- 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}`);
38903
39135
  role = stored.role;
38904
39136
  const part = stored.part;
38905
39137
  if (part.dataRef !== void 0) {
38906
39138
  const att = byRef.get(part.dataRef);
38907
- if (!att)
38908
- return { complete: false, turns: messages.length, messages, gap: `attachment ${part.dataRef} is missing` };
39139
+ if (!att) return gap(`attachment ${part.dataRef} is missing`);
38909
39140
  const { dataRef: _ref, ...rest } = part;
38910
39141
  content.push({ ...rest, data: att.data });
38911
39142
  } else {
38912
39143
  content.push(part);
38913
39144
  }
38914
39145
  }
39146
+ messageIndexOf.set(idx, messages.length);
38915
39147
  messages.push({ role: role ?? "user", content });
38916
39148
  }
38917
- 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 };
38918
39155
  }
38919
- var attachmentRef;
39156
+ var attachmentRef, isCompaction;
38920
39157
  var init_transcript = __esm({
38921
39158
  "../../src/core/runLedger/transcript.ts"() {
38922
39159
  "use strict";
38923
39160
  init_runRecord();
38924
39161
  init_types5();
38925
39162
  attachmentRef = (idx, part) => `t${idx}p${part}`;
39163
+ isCompaction = (v) => "compaction" in v;
38926
39164
  }
38927
39165
  });
38928
39166
 
@@ -38937,6 +39175,9 @@ function buildRunLedger(cfg, secrets, deps = {}) {
38937
39175
  baseUrl: worker.baseUrl,
38938
39176
  token: token2.reveal(),
38939
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,
38940
39181
  ...deps.fetch ? { fetch: deps.fetch } : {}
38941
39182
  });
38942
39183
  }
@@ -38945,7 +39186,9 @@ var init_runLedgerWorker = __esm({
38945
39186
  "../../src/core/runLedgerWorker.ts"() {
38946
39187
  "use strict";
38947
39188
  init_runRecord();
39189
+ init_runStore();
38948
39190
  init_runStoreWorker();
39191
+ init_sessionLog();
38949
39192
  init_transcript();
38950
39193
  init_types5();
38951
39194
  WorkerRunLedger = class {
@@ -39009,41 +39252,94 @@ var init_runLedgerWorker = __esm({
39009
39252
  }
39010
39253
  };
39011
39254
  }
39012
- await this.post("/runs/transcript/owner", { runId: req.runId, gen: req.gen });
39013
39255
  return { ok: true };
39014
39256
  }
39015
- 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) {
39016
39265
  const rows = [];
39017
39266
  const attachments = [];
39018
39267
  for (const t of turns) {
39019
- const out = turnRows(t.idx, t.message);
39268
+ const out = turnRows(t.idx, "message" in t ? t.message : { compaction: t.compaction });
39020
39269
  rows.push(...out.rows);
39021
39270
  attachments.push(...out.attachments);
39022
39271
  }
39272
+ const { path, body } = this.target(runId3, gen, session);
39023
39273
  for (const a of attachments) {
39024
- 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] });
39025
39275
  const f = this.fenceResult(r);
39026
39276
  if (!f.ok) return f;
39027
39277
  }
39028
39278
  const chunks = chunkRows(rows, TRANSCRIPT_REQUEST_BYTES - 4096);
39029
39279
  for (const chunk of chunks.length ? chunks : [[]]) {
39030
39280
  if (chunk.length === 0 && attachments.length > 0) continue;
39031
- 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: [] });
39032
39282
  const f = this.fenceResult(r);
39033
39283
  if (!f.ok) return f;
39034
39284
  }
39035
39285
  return { ok: true };
39036
39286
  }
39037
- async seed(runId3, gen, turns) {
39287
+ async seed(runId3, gen, turns, session) {
39038
39288
  this.checkIds(runId3, gen);
39039
- return this.writeTurns(runId3, gen, turns);
39289
+ return this.writeTurns(runId3, gen, turns, session);
39040
39290
  }
39041
- async step(runId3, gen, record2, turns) {
39291
+ async step(runId3, gen, record2, turns, session) {
39042
39292
  this.checkIds(runId3, gen);
39043
- const written = await this.writeTurns(runId3, gen, turns);
39293
+ const written = await this.writeTurns(runId3, gen, turns, session);
39044
39294
  if (!written.ok) return written;
39045
39295
  return this.fenceResult(await this.post("/runs/step", { storeKey: this.opts.storeKey, runId: runId3, gen, record: record2 }));
39046
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
+ }
39047
39343
  async heartbeat(runId3, gen, leaseMs) {
39048
39344
  this.checkIds(runId3, gen);
39049
39345
  const r = await this.post("/runs/heartbeat", { storeKey: this.opts.storeKey, runId: runId3, gen, leaseMs });
@@ -39095,22 +39391,23 @@ var init_runLedgerWorker = __esm({
39095
39391
  const r = await this.post("/runs/finish", { storeKey: this.opts.storeKey, runId: runId3, gen, record: record2 });
39096
39392
  const f = this.fenceResult(r);
39097
39393
  if (!f.ok) return f;
39098
- await this.post("/runs/transcript/clear", { runId: runId3 }).catch(() => {
39099
- });
39394
+ if (record2.session)
39395
+ await this.post("/runs/session/clear-owner", { key: record2.session.key, runId: runId3, gen }).catch(() => {
39396
+ });
39100
39397
  return { ok: true, stored: r.data.stored === true };
39101
39398
  }
39102
39399
  async abandon(runId3, gen) {
39103
39400
  this.checkIds(runId3, gen);
39104
- const f = this.fenceResult(await this.post("/runs/abandon", { storeKey: this.opts.storeKey, runId: runId3, gen }));
39105
- if (!f.ok) return f;
39106
- await this.post("/runs/transcript/clear", { runId: runId3 }).catch(() => {
39107
- });
39108
- return f;
39401
+ return this.fenceResult(await this.post("/runs/abandon", { storeKey: this.opts.storeKey, runId: runId3, gen }));
39109
39402
  }
39110
39403
  async reclaim(gen, now, leaseMs) {
39111
39404
  const r = await this.post("/runs/reclaim", { storeKey: this.opts.storeKey, gen, now, leaseMs });
39112
39405
  const runs = Array.isArray(r.data.runs) ? r.data.runs : [];
39113
- 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
+ }
39114
39411
  return runs;
39115
39412
  }
39116
39413
  async listLive() {
@@ -39158,6 +39455,116 @@ var init_decisions = __esm({
39158
39455
  }
39159
39456
  });
39160
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
+
39161
39568
  // ../../src/core/boot.ts
39162
39569
  function closureNote(agent, prUrl2) {
39163
39570
  if (agent === "ship") return shipInterruptedNote(prUrl2);
@@ -39205,7 +39612,7 @@ async function reclaimRuns(opts) {
39205
39612
  status3 = "interrupted";
39206
39613
  why = "reserved at admission without its request: nothing to restart from";
39207
39614
  } else {
39208
- const verdict = await completenessVerdict(ledger, row.runId, run2.lastStep);
39615
+ const verdict = await completenessVerdict(ledger, row, run2.lastStep);
39209
39616
  if (verdict.resumable && run2.lastStep) {
39210
39617
  const events2 = await ledger.readEvents(row.runId);
39211
39618
  outcome.resumable.push({
@@ -39306,11 +39713,12 @@ async function closeReclaimed(ledger, gen, input) {
39306
39713
  const result = await ledger.finish(input.row.runId, gen, record2);
39307
39714
  return result.ok ? { ok: true, record: record2 } : { ok: false, reason: result.reason ?? "refused" };
39308
39715
  }
39309
- async function completenessVerdict(ledger, runId3, lastStep) {
39716
+ async function completenessVerdict(ledger, row, lastStep) {
39310
39717
  if (!lastStep) return { resumable: false, why: "no step record: killed before its conversation was stored" };
39311
39718
  let transcript;
39312
39719
  try {
39313
- 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);
39314
39722
  } catch (err2) {
39315
39723
  return { resumable: false, why: `transcript unreadable (${describe9(err2)})` };
39316
39724
  }
@@ -39339,6 +39747,7 @@ var init_boot = __esm({
39339
39747
  "use strict";
39340
39748
  init_runStoreWorker();
39341
39749
  init_decisions();
39750
+ init_resume();
39342
39751
  init_types5();
39343
39752
  init_record();
39344
39753
  init_shipPipeline();
@@ -39353,110 +39762,6 @@ var init_boot = __esm({
39353
39762
  }
39354
39763
  });
39355
39764
 
39356
- // ../../src/core/runLedger/resume.ts
39357
- function settlementFor(toolUse, tools) {
39358
- const tool = tools.get(toolUse.name);
39359
- if (!tool) {
39360
- return {
39361
- toolUse,
39362
- action: "synthetic",
39363
- text: `Tool ${toolUse.name} is not available after the bot restarted; continue without it.`
39364
- };
39365
- }
39366
- if (tool.sideEffectFree || RERUN_SAFE_TOOLS.has(toolUse.name)) return { toolUse, action: "rerun" };
39367
- return {
39368
- toolUse,
39369
- action: "synthetic",
39370
- text: `The bot restarted while this ${toolUse.name} call was in flight; its effects are unknown \u2014 re-check them before re-running it.`
39371
- };
39372
- }
39373
- function planResume(input) {
39374
- const { transcript, lastStep } = input;
39375
- if (!lastStep) return { kind: "interrupted", why: "no step record: killed before its conversation was stored" };
39376
- if (!transcript.complete) return { kind: "interrupted", why: `transcript incomplete: ${transcript.gap}` };
39377
- const completeness = transcriptCompleteness({
39378
- lastStep,
39379
- seedTurns: lastStep.turnIndex,
39380
- transcriptTurns: transcript.turns
39381
- });
39382
- if (completeness.kind === "interrupted") return completeness;
39383
- const tools = /* @__PURE__ */ new Map();
39384
- for (const t of input.tools) tools.set(t.name, t);
39385
- const messages = transcript.messages;
39386
- const last = messages[messages.length - 1];
39387
- const calls = toolUsesOf(last);
39388
- if (completeness.kind === "resume") {
39389
- if (lastStep.inFlight.length === 0) {
39390
- if (last?.role !== "user") {
39391
- return {
39392
- kind: "interrupted",
39393
- why: "transcript ends with an assistant turn but the last step recorded nothing in flight"
39394
- };
39395
- }
39396
- return {
39397
- kind: "resume",
39398
- messages,
39399
- settlements: [],
39400
- stepRecorded: true,
39401
- inboxConsumedSeq: lastStep.inboxConsumedSeq,
39402
- step: lastStep.step,
39403
- turn: lastStep.turn,
39404
- iteration: lastStep.iteration,
39405
- remainingMs: lastStep.remainingMs
39406
- };
39407
- }
39408
- const recorded = new Set(lastStep.inFlight.map((c) => c.callId));
39409
- if (calls.length === 0 || calls.length !== recorded.size || !calls.every((c) => recorded.has(c.id))) {
39410
- return {
39411
- kind: "interrupted",
39412
- why: `the last step's calls in flight (${[...recorded].join(", ")}) do not match the transcript's last assistant turn`
39413
- };
39414
- }
39415
- return {
39416
- kind: "resume",
39417
- messages,
39418
- settlements: calls.map((c) => settlementFor(c, tools)),
39419
- stepRecorded: true,
39420
- inboxConsumedSeq: lastStep.inboxConsumedSeq,
39421
- step: lastStep.step,
39422
- turn: lastStep.turn,
39423
- iteration: lastStep.iteration,
39424
- remainingMs: lastStep.remainingMs
39425
- };
39426
- }
39427
- if (calls.length === 0) {
39428
- return { kind: "interrupted", why: "the next step's turns landed but its assistant turn has no tool calls" };
39429
- }
39430
- const bookkeepingOnly = calls.every((c) => c.name === "update_status");
39431
- return {
39432
- kind: "resume",
39433
- messages,
39434
- settlements: calls.map((c) => ({ toolUse: c, action: "rerun" })),
39435
- stepRecorded: false,
39436
- inboxConsumedSeq: lastStep.inboxConsumedSeq,
39437
- step: lastStep.step + 1,
39438
- turn: lastStep.turn + (bookkeepingOnly ? 0 : 1),
39439
- iteration: lastStep.iteration + 1,
39440
- remainingMs: lastStep.remainingMs
39441
- };
39442
- }
39443
- var RERUN_SAFE_TOOLS, toolUsesOf;
39444
- var init_resume = __esm({
39445
- "../../src/core/runLedger/resume.ts"() {
39446
- "use strict";
39447
- init_decisions();
39448
- RERUN_SAFE_TOOLS = /* @__PURE__ */ new Set([
39449
- "write_file",
39450
- "update_status",
39451
- "submit_verdict",
39452
- "submit_pr_description",
39453
- "submit_handoff",
39454
- "submit_dispositions"
39455
- ]);
39456
- toolUsesOf = (message2) => message2?.role === "assistant" ? message2.content.filter((p) => p.type === "tool_use") : [];
39457
- }
39458
- });
39459
-
39460
39765
  // ../../src/core/resumeLaunch.ts
39461
39766
  function knownToolsFor(agent) {
39462
39767
  return (TOOLSETS[agent.toolset] ?? []).map((t) => ({
@@ -40661,9 +40966,12 @@ function parseBrief(v) {
40661
40966
  const p = b.prior;
40662
40967
  if (typeof p !== "object" || p === null || typeof p.reviewRunId !== "string" || !RUN_ID_PATTERN.test(p.reviewRunId))
40663
40968
  return invalid("brief.prior.reviewRunId must be a run id");
40664
- if (p.fixRunId !== void 0 && (typeof p.fixRunId !== "string" || !RUN_ID_PATTERN.test(p.fixRunId)))
40665
- return invalid("brief.prior.fixRunId must be a run id");
40666
- 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
+ };
40667
40975
  }
40668
40976
  return {
40669
40977
  ok: true,
@@ -40677,15 +40985,15 @@ function parseBrief(v) {
40677
40985
  }
40678
40986
  };
40679
40987
  }
40680
- case "fix": {
40988
+ case "findings": {
40681
40989
  const n2 = pr();
40682
40990
  if (!n2.ok) return n2;
40683
40991
  const review = runId3("reviewRunId");
40684
40992
  if (!review.ok) return review;
40685
- 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 } };
40686
40994
  }
40687
40995
  default:
40688
- return invalid("brief.kind must be contract, review or fix");
40996
+ return invalid("brief.kind must be contract, review or findings");
40689
40997
  }
40690
40998
  }
40691
40999
  function parseObject(text) {
@@ -40748,6 +41056,30 @@ function unitThread(instance, row) {
40748
41056
  const sourceUrl = row?.sourceUrl ?? (threadKey === instance.threadKey ? instance.sourceUrl : void 0);
40749
41057
  return { threadKey, sourceUrl };
40750
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
+ }
40751
41083
  function authorizeStep(headers, step, deps) {
40752
41084
  const raw = headers.authorization;
40753
41085
  const authorization = Array.isArray(raw) ? raw[0] : raw;
@@ -40836,8 +41168,19 @@ async function spawn6(body, deps) {
40836
41168
  const at = (deps.clock ?? systemClock)();
40837
41169
  const unit = await unitRowOf(deps, instance, req.unit);
40838
41170
  if (!unit.ok) return unit.response;
40839
- const thread = unitThread(instance, unit.row);
40840
- 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
+ }
40841
41184
  const threadKey = thread.threadKey;
40842
41185
  const key = idempotencyKeyFor(instance.id, req.step);
40843
41186
  const live = await liveOnThread(deps.runs, instance, threadKey);
@@ -40848,10 +41191,9 @@ async function spawn6(body, deps) {
40848
41191
  if (!io) return json(503, { ok: false, error: "no_channel", at });
40849
41192
  let turn;
40850
41193
  if (req.brief !== void 0) {
40851
- 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" });
40852
41195
  try {
40853
- const composed = await composeChild(req.brief, instance, unit.row, briefReaders(deps, instance, io));
40854
- turn = composed;
41196
+ turn = await composeChild(req.brief, instance, row, briefReaders(deps, instance, io));
40855
41197
  } catch (err2) {
40856
41198
  log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe10(err2)}`);
40857
41199
  return json(502, { ok: false, error: "brief_failed", message: describe10(err2), at });
@@ -40897,8 +41239,7 @@ async function spawn6(body, deps) {
40897
41239
  };
40898
41240
  const settled = deps.dispatch(msg, child, {
40899
41241
  coordinator: tag,
40900
- ...turn.contract !== void 0 ? { contract: turn.contract } : {},
40901
- ...turn.fixRound !== void 0 ? { fixRound: turn.fixRound } : {}
41242
+ ...turn.contract !== void 0 ? { contract: turn.contract } : {}
40902
41243
  }).then(
40903
41244
  (outcome) => ({ kind: "ended", outcome }),
40904
41245
  (err2) => ({ kind: "threw", err: err2 })
@@ -41115,30 +41456,32 @@ async function unitStart(body, deps) {
41115
41456
  ...instance.sourceUrl !== void 0 ? { sourceUrl: instance.sourceUrl } : {}
41116
41457
  };
41117
41458
  } else {
41118
- const parent = deps.ioFor({ threadKey: instance.threadKey, userId: instance.userId });
41119
- if (!parent?.openThread) return json(503, { ok: false, error: "no_channel", at });
41120
- try {
41121
- const opened = await parent.openThread(unitLead(instance, row));
41122
- row = {
41123
- ...row,
41124
- threadKey: opened.thread.threadKey,
41125
- ...opened.thread.sourceUrl !== void 0 ? { sourceUrl: opened.thread.sourceUrl } : {}
41126
- };
41127
- } catch (err2) {
41128
- return json(502, { ok: false, error: "thread_failed", message: describe10(err2), at });
41129
- }
41459
+ const opened = await openThreadFromRequester(deps, instance, unitLead(instance, row), at);
41460
+ if (!opened.ok) return opened.response;
41461
+ row = { ...row, ...opened.thread };
41130
41462
  }
41131
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;
41469
+ }
41470
+ row = review.row;
41471
+ }
41132
41472
  if (row.issue === void 0 && row.unit !== TASK_UNIT) {
41133
41473
  const issue = await unitIssueOf(deps, instance.repo, row.unit);
41134
41474
  if (issue !== void 0) row = { ...row, issue };
41135
41475
  }
41136
41476
  row = { ...row, startedAt: row.startedAt ?? at };
41137
41477
  await deps.instances.putUnits([row]);
41138
- (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
+ );
41139
41481
  return json(200, {
41140
41482
  ok: true,
41141
41483
  threadKey: row.threadKey,
41484
+ reviewThreadKey: row.reviewThread?.threadKey,
41142
41485
  branch: row.branch,
41143
41486
  base: instance.base ?? "main",
41144
41487
  ...row.issue !== void 0 ? { issue: row.issue } : {},
@@ -41150,6 +41493,12 @@ function unitLead(instance, row) {
41150
41493
  const from = instance.sourceUrl !== void 0 ? `[the *ship* run](${instance.sourceUrl})` : "the *ship* run";
41151
41494
  return `\u21B3 *ship* unit ${row.unit}${row.title ? ` \u2014 ${row.title}` : ""} for ${who2}, from ${from}: \`${row.branch}\` in ${instance.repo}`;
41152
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
+ }
41153
41502
  async function branch(body, deps) {
41154
41503
  const id = parseInstanceId(body.parentInstanceId);
41155
41504
  if (!id.ok) return json(400, { ok: false, error: id.error });
@@ -41584,7 +41933,7 @@ var init_adminCoordinator = __esm({
41584
41933
  PRESETS_OF_KIND = {
41585
41934
  contract: "coding",
41586
41935
  review: "review",
41587
- fix: "coding"
41936
+ findings: "coding"
41588
41937
  };
41589
41938
  invalid = (error) => ({ ok: false, error });
41590
41939
  json = (status3, body) => ({ status: status3, body });