@coreplane/switchboard 1.214.0 → 1.215.1

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.`;
@@ -24696,15 +24746,18 @@ function parseRouteAnswer(raw, allowed2, compound) {
24696
24746
  }
24697
24747
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
24698
24748
  return { preset: void 0, reason: `not a single JSON object: ${tidyReason(trimmed)}` };
24699
- const { preset, reason, parts } = parsed;
24700
- if (typeof preset !== "string")
24749
+ const { preset: named, reason, parts } = parsed;
24750
+ const preset = typeof named === "string" ? named : Array.isArray(parts) ? COMPOUND_PRESET : void 0;
24751
+ const inferred = typeof named !== "string" && preset === COMPOUND_PRESET;
24752
+ if (preset === void 0)
24701
24753
  return { preset: void 0, reason: `missing preset in the router's answer: ${tidyReason(trimmed)}` };
24702
- if (typeof reason !== "string")
24754
+ if (typeof reason !== "string" && !inferred)
24703
24755
  return { preset: void 0, reason: `missing reason in the router's answer: ${tidyReason(trimmed)}` };
24704
- if (preset === COMPOUND_PRESET) return parseCompound(parts, tidyReason(reason), allowed2, compound);
24756
+ const tidy = typeof reason === "string" ? tidyReason(reason) : "compound inferred from parts";
24757
+ if (preset === COMPOUND_PRESET) return parseCompound(parts, tidy, allowed2, compound);
24705
24758
  if (!allowed2.includes(preset))
24706
24759
  return { preset: void 0, reason: `router said "${tidyReason(preset)}", not a preset the requester may run` };
24707
- return { preset, reason: tidyReason(reason) };
24760
+ return { preset, reason: tidy };
24708
24761
  }
24709
24762
  function parseCompound(parts, reason, allowed2, offer) {
24710
24763
  const rejected = (why) => ({
@@ -24729,16 +24782,20 @@ function parseCompound(parts, reason, allowed2, offer) {
24729
24782
  if (tidy.length === 0) return rejected(`part ${n2} has no text`);
24730
24783
  out.push({ preset, text: tidy });
24731
24784
  }
24785
+ const writer = out.find((p) => identityOf(p.preset) === "write");
24786
+ if (writer) return { preset: writer.preset, reason, collapsed: { presets: out.map((p) => p.preset) } };
24732
24787
  return { preset: COMPOUND_PRESET, reason, parts: out };
24733
24788
  }
24734
24789
  async function route(input, model, opts = {}) {
24735
- const offered = input.presets.filter((p) => input.allowed.includes(p.name));
24790
+ const { compound: offer, ...rest } = input;
24791
+ const offered = rest.presets.filter((p) => rest.allowed.includes(p.name));
24736
24792
  if (offered.length === 0) return { preset: void 0, reason: "no preset the requester may run" };
24737
- const prompt = buildRoutePrompt({ ...input, presets: offered });
24793
+ const compound = offer && partPresets(offered).length > 0 ? offer : void 0;
24794
+ const prompt = buildRoutePrompt({ ...rest, presets: offered, ...compound ? { compound } : {} });
24738
24795
  let raw;
24739
24796
  try {
24740
24797
  raw = await model(prompt, {
24741
- maxTokens: routeMaxOutputTokens(input.compound),
24798
+ maxTokens: routeMaxOutputTokens(compound),
24742
24799
  signal: AbortSignal.timeout(opts.timeoutMs ?? ROUTE_TIMEOUT_MS)
24743
24800
  });
24744
24801
  } catch (err2) {
@@ -24750,7 +24807,7 @@ async function route(input, model, opts = {}) {
24750
24807
  return parseRouteAnswer(
24751
24808
  raw,
24752
24809
  offered.map((p) => p.name),
24753
- input.compound
24810
+ compound
24754
24811
  );
24755
24812
  }
24756
24813
  function providerRouteModel(provider, model, opts = {}) {
@@ -24824,18 +24881,24 @@ async function routeRequest(deps, ctx) {
24824
24881
  effort: directives.effort ?? sticky.effort
24825
24882
  }
24826
24883
  });
24827
- const parts = decision.parts;
24884
+ const { parts, collapsed } = decision;
24828
24885
  console.log(
24829
- `[route] ${msg.threadKey} routed to ${decision.preset} on ${modelRef2}${parts ? ` (${parts.length} parts)` : ""}: ${decision.reason}`
24886
+ `[route] ${msg.threadKey} routed to ${decision.preset} on ${modelRef2}${parts ? ` (${parts.length} parts)` : ""}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}: ${decision.reason}`
24830
24887
  );
24831
24888
  return {
24832
24889
  kind: "routed",
24833
24890
  resolved,
24834
- route: { preset: decision.preset, reason: decision.reason, model: modelRef2, ...parts ? { parts } : {} }
24891
+ route: {
24892
+ preset: decision.preset,
24893
+ reason: decision.reason,
24894
+ model: modelRef2,
24895
+ ...parts ? { parts } : {},
24896
+ ...collapsed ? { collapsed } : {}
24897
+ }
24835
24898
  };
24836
24899
  }
24837
- function routedLabel(reason) {
24838
- return `routed: ${reason}`;
24900
+ function routedLabel(reason, collapsed) {
24901
+ return `routed: ${reason}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}`;
24839
24902
  }
24840
24903
  function routedPartLines(parts) {
24841
24904
  return parts.map((p) => {
@@ -24851,7 +24914,7 @@ function compoundBrief(text, parts) {
24851
24914
  ...parts.map((p, i) => `${i + 1}. \`${p.preset}\`: ${p.text}`)
24852
24915
  ].join("\n");
24853
24916
  }
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;
24917
+ 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
24918
  var init_route = __esm({
24856
24919
  "../../src/core/dispatch/route.ts"() {
24857
24920
  "use strict";
@@ -24868,6 +24931,7 @@ var init_route = __esm({
24868
24931
  ROUTE_PART_TEXT_CAP = 1e3;
24869
24932
  ROUTE_PART_LINE_CAP = 100;
24870
24933
  COMPOUND_BRIEF_HEADING = "Routed as a compound request";
24934
+ identityOf = (preset) => AGENTS[preset]?.identity;
24871
24935
  ROUTED_CARD_FOOTER = "reply agent:<preset> to run it another way";
24872
24936
  }
24873
24937
  });
@@ -26638,7 +26702,7 @@ ${raw.trim()}`;
26638
26702
  };
26639
26703
  submitDispositionsTool = {
26640
26704
  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.",
26705
+ 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
26706
  inputSchema: {
26643
26707
  type: "object",
26644
26708
  properties: {
@@ -26661,15 +26725,7 @@ ${raw.trim()}`;
26661
26725
  async run(input, ctx) {
26662
26726
  const parsed = parseDispositionsInput(input);
26663
26727
  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)";
26728
+ if (!ctx.onDispositions) return "no run is recording dispositions here";
26673
26729
  ctx.onDispositions(parsed.dispositions);
26674
26730
  const drops = parsed.dropped.length ? ` (dropped: ${parsed.dropped.join("; ")})` : "";
26675
26731
  return `dispositions recorded: ${parsed.dispositions.length}${drops}; a later call replaces this one`;
@@ -28953,7 +29009,7 @@ function startMemoryRead(deps, ctx) {
28953
29009
  async function openAckCard(deps, ctx) {
28954
29010
  const { io, agent, resolved, startedAt, clock: clock3, root, trace, route: route3 } = ctx;
28955
29011
  const shell = createCardShell({
28956
- label: `*${agent.name}* on \`${resolved.modelRef}\`${route3 ? ` \xB7 ${routedLabel(route3.reason)}` : ""}`,
29012
+ label: `*${agent.name}* on \`${resolved.modelRef}\`${route3 ? ` \xB7 ${routedLabel(route3.reason, route3.collapsed)}` : ""}`,
28957
29013
  startedAt,
28958
29014
  now: clock3,
28959
29015
  ...route3?.parts ? { lead: routedPartLines(route3.parts) } : {},
@@ -29872,7 +29928,7 @@ var init_bridge2 = __esm({
29872
29928
  this.onToolEnd(event, out);
29873
29929
  break;
29874
29930
  case "compaction_end":
29875
- this.onCompactionEnd(event);
29931
+ this.onCompactionEnd(event, out);
29876
29932
  break;
29877
29933
  case "summarization_retry_scheduled":
29878
29934
  this.summarizationRetries++;
@@ -29986,7 +30042,7 @@ var init_bridge2 = __esm({
29986
30042
  }
29987
30043
  }
29988
30044
  }
29989
- onCompactionEnd(event) {
30045
+ onCompactionEnd(event, out) {
29990
30046
  const result = isRecord3(event.result) ? event.result : void 0;
29991
30047
  if (event.aborted === true || !result) {
29992
30048
  this.note(
@@ -30003,6 +30059,13 @@ var init_bridge2 = __esm({
30003
30059
  "compacted",
30004
30060
  `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
30061
  );
30062
+ if (typeof result.summary === "string") {
30063
+ out.compaction = {
30064
+ summary: result.summary,
30065
+ ...before !== void 0 ? { tokensBefore: before } : {},
30066
+ ...typeof result.firstKeptEntryId === "string" ? { firstKeptEntryId: result.firstKeptEntryId } : {}
30067
+ };
30068
+ }
30006
30069
  }
30007
30070
  onUiRequest(event, out) {
30008
30071
  const method = str5(event.method);
@@ -30071,7 +30134,7 @@ function chatMessageOf(message2) {
30071
30134
  return void 0;
30072
30135
  }
30073
30136
  }
30074
- function piSessionFile(messages, opts) {
30137
+ function piSessionFile(messages, opts, compactions = []) {
30075
30138
  const lines = [];
30076
30139
  const stamp = new Date(opts.at).toISOString();
30077
30140
  lines.push(
@@ -30080,12 +30143,35 @@ function piSessionFile(messages, opts) {
30080
30143
  let parentId = null;
30081
30144
  let n2 = 0;
30082
30145
  const toolNames = /* @__PURE__ */ new Map();
30146
+ const firstEntryOf = [];
30147
+ const nextId = () => (n2++).toString(16).padStart(8, "0");
30083
30148
  const push = (message2) => {
30084
- const id = (n2++).toString(16).padStart(8, "0");
30149
+ const id = nextId();
30085
30150
  lines.push(JSON.stringify({ type: "message", id, parentId, timestamp: stamp, message: message2 }));
30086
30151
  parentId = id;
30152
+ return id;
30087
30153
  };
30088
- for (const message2 of messages) {
30154
+ const pushCompaction = (c) => {
30155
+ const id = nextId();
30156
+ const kept = c.keptBefore !== void 0 ? firstEntryOf[c.keptBefore] : void 0;
30157
+ lines.push(
30158
+ JSON.stringify({
30159
+ type: "compaction",
30160
+ id,
30161
+ parentId,
30162
+ timestamp: stamp,
30163
+ summary: c.entry.summary,
30164
+ firstKeptEntryId: kept ?? c.entry.firstKeptEntryId ?? "",
30165
+ tokensBefore: c.entry.tokensBefore ?? 0
30166
+ })
30167
+ );
30168
+ parentId = id;
30169
+ };
30170
+ const byPosition = /* @__PURE__ */ new Map();
30171
+ for (const c of compactions) byPosition.set(c.before, [...byPosition.get(c.before) ?? [], c]);
30172
+ messages.forEach((message2, i) => {
30173
+ for (const c of byPosition.get(i) ?? []) pushCompaction(c);
30174
+ firstEntryOf[i] = n2.toString(16).padStart(8, "0");
30089
30175
  if (message2.role === "assistant") {
30090
30176
  const content = [];
30091
30177
  for (const p of message2.content) {
@@ -30113,7 +30199,7 @@ function piSessionFile(messages, opts) {
30113
30199
  stopReason: hasTool ? "toolUse" : "stop",
30114
30200
  timestamp: opts.at
30115
30201
  });
30116
- continue;
30202
+ return;
30117
30203
  }
30118
30204
  const results = message2.content.filter(
30119
30205
  (p) => p.type === "tool_result"
@@ -30140,7 +30226,8 @@ function piSessionFile(messages, opts) {
30140
30226
  });
30141
30227
  }
30142
30228
  if (rest.length > 0) push({ role: "user", content: rest, timestamp: opts.at });
30143
- }
30229
+ });
30230
+ for (const c of byPosition.get(messages.length) ?? []) pushCompaction(c);
30144
30231
  return lines.join("\n") + "\n";
30145
30232
  }
30146
30233
  function sessionIdOf(at) {
@@ -30197,6 +30284,28 @@ var init_mirror = __esm({
30197
30284
  this.idx += turns.length;
30198
30285
  await this.deps.onStep(report2);
30199
30286
  }
30287
+ /** pi compacted (session-log item 6): the results pending since the last
30288
+ * assistant turn go as the user turn they would have been, the entry as the
30289
+ * row after them — its own step with nothing in flight, so the log holds
30290
+ * the summary where pi wrote it and the index moves past it. */
30291
+ async onCompaction(entry, turn) {
30292
+ if (!this.deps.onStep) return;
30293
+ const turns = [];
30294
+ if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
30295
+ this.pendingUser = [];
30296
+ const report2 = {
30297
+ turns,
30298
+ firstIdx: this.idx,
30299
+ compaction: entry,
30300
+ inFlight: [],
30301
+ turn,
30302
+ iteration: this.iteration,
30303
+ remainingMs: Math.max(0, this.deps.remainingMs()),
30304
+ inboxConsumedSeq: this.inboxConsumedSeq
30305
+ };
30306
+ this.idx += turns.length + 1;
30307
+ await this.deps.onStep(report2);
30308
+ }
30200
30309
  };
30201
30310
  }
30202
30311
  });
@@ -30535,25 +30644,33 @@ async function runPiHarness(deps, run2) {
30535
30644
  let session;
30536
30645
  if (run2.resume) {
30537
30646
  const settled2 = settlementResults(run2.resume.settlements);
30538
- session = { stem: "resumed", messages: settled2 ? [...run2.resume.messages, settled2] : run2.resume.messages };
30647
+ session = {
30648
+ stem: "resumed",
30649
+ messages: settled2 ? [...run2.resume.messages, settled2] : run2.resume.messages,
30650
+ compactions: run2.resume.compactions ?? []
30651
+ };
30539
30652
  } else {
30540
30653
  const earlier2 = splitSeed(run2.messages).session;
30541
- if (earlier2.length > 0) session = { stem: "seed", messages: earlier2 };
30654
+ if (earlier2.length > 0) session = { stem: "seed", messages: earlier2, compactions: [] };
30542
30655
  }
30543
30656
  let sessionPath;
30544
30657
  if (session) {
30545
30658
  sessionPath = `${paths.sessionDir}/${session.stem}-${now()}.jsonl`;
30546
30659
  await container.writeFile(
30547
30660
  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"
30661
+ piSessionFile(
30662
+ session.messages,
30663
+ {
30664
+ cwd: run2.rules.checkout,
30665
+ model: {
30666
+ provider: run2.model.provider,
30667
+ id: run2.model.id,
30668
+ api: run2.model.providerType === "anthropic" ? "anthropic-messages" : "openai-completions"
30669
+ },
30670
+ at: now()
30554
30671
  },
30555
- at: now()
30556
- })
30672
+ session.compactions
30673
+ )
30557
30674
  );
30558
30675
  }
30559
30676
  if (run2.resume) {
@@ -30682,6 +30799,7 @@ async function runPiHarness(deps, run2) {
30682
30799
  }
30683
30800
  }
30684
30801
  if (obs.message) await mirror.onMessage(obs.message, bridge.turns);
30802
+ if (obs.compaction) await mirror.onCompaction(obs.compaction, bridge.turns);
30685
30803
  if (obs.providerError !== void 0) {
30686
30804
  if (catchingUp)
30687
30805
  note("harness_error", `a model call failed while the bot was away (${obs.providerError}); continuing`);
@@ -31269,8 +31387,7 @@ async function runLoop2(deps, ctx) {
31269
31387
  wait,
31270
31388
  parentRunId,
31271
31389
  coordinator,
31272
- seed,
31273
- fixRound
31390
+ seed
31274
31391
  } = ctx;
31275
31392
  const agent = budgetedAgent(ctx.agent, profile);
31276
31393
  const { executor, binding } = round2.selection;
@@ -31373,7 +31490,7 @@ async function runLoop2(deps, ctx) {
31373
31490
  handoff = h;
31374
31491
  ledgerRun?.setState({ handoff: h });
31375
31492
  };
31376
- const restoredDispositions = fixRound ? parseDispositionsInput({ dispositions: restored.dispositions })?.dispositions : void 0;
31493
+ const restoredDispositions = parseDispositionsInput({ dispositions: restored.dispositions })?.dispositions;
31377
31494
  let dispositions = restoredDispositions;
31378
31495
  const onDispositions = (d) => {
31379
31496
  dispositions = d;
@@ -31420,7 +31537,7 @@ async function runLoop2(deps, ctx) {
31420
31537
  onDigest,
31421
31538
  onPrDescription,
31422
31539
  onHandoff,
31423
- ...fixRound ? { knownFindingIds: fixRound.findingIds, onDispositions } : {}
31540
+ onDispositions
31424
31541
  };
31425
31542
  const harness = effectiveHarness(agent, deps.config.config.harness);
31426
31543
  try {
@@ -31481,6 +31598,7 @@ async function runLoop2(deps, ctx) {
31481
31598
  ...resume ? {
31482
31599
  resume: {
31483
31600
  messages: resume.plan.messages,
31601
+ compactions: resume.plan.compactions,
31484
31602
  settlements: resume.plan.settlements,
31485
31603
  remainingMs: resume.plan.remainingMs,
31486
31604
  turn: resume.plan.turn,
@@ -32060,6 +32178,12 @@ function openPlanCursor(graph, selected) {
32060
32178
  for (const id of order) visit(id, []);
32061
32179
  return cursor;
32062
32180
  }
32181
+ function matchDispositions(findings, dispositions) {
32182
+ const issued = new Set(findings.map((f) => f.id));
32183
+ const matched = dispositions.filter((d) => issued.has(d.findingId));
32184
+ const dropped = [...new Set(dispositions.filter((d) => !issued.has(d.findingId)).map((d) => d.findingId))];
32185
+ return { matched, dropped };
32186
+ }
32063
32187
  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
32188
  var init_coordinator = __esm({
32065
32189
  "../../src/core/ship/coordinator.ts"() {
@@ -32082,29 +32206,6 @@ var init_coordinator = __esm({
32082
32206
  }
32083
32207
  });
32084
32208
 
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
32209
  // ../../src/core/ship/reviewChild.ts
32109
32210
  function buildShipReviewTurn(input) {
32110
32211
  const at = input.headSha ? ` at head \`${input.headSha}\`` : "";
@@ -32113,13 +32214,15 @@ function buildShipReviewTurn(input) {
32113
32214
  }
32114
32215
  const findings = input.prior.findings.map(formatFinding).join("\n") || "(none recorded)";
32115
32216
  const dispositions = input.prior.dispositions.map((d) => `${d.findingId}: ${d.disposition}${d.note ? ` \u2014 ${d.note}` : ""}`).join("\n") || "(none recorded)";
32217
+ const dropped = input.prior.dropped !== void 0 && input.prior.dropped.length > 0 ? `
32218
+ Dispositions naming no finding of the previous round (dropped): ${input.prior.dropped.join(", ")}` : "";
32116
32219
  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
32220
 
32118
32221
  Previous round's findings:
32119
32222
  ${findings}
32120
32223
 
32121
32224
  Fix round's dispositions:
32122
- ${dispositions}`;
32225
+ ${dispositions}${dropped}`;
32123
32226
  }
32124
32227
  var init_reviewChild = __esm({
32125
32228
  "../../src/core/ship/reviewChild.ts"() {
@@ -32165,6 +32268,16 @@ async function contractFor(instance, unit, readers) {
32165
32268
  ...issue ? { issue } : {}
32166
32269
  });
32167
32270
  }
32271
+ function findingsRequest(input) {
32272
+ const findings = input.findings.map(formatFinding).join("\n") || "(the review listed no structured findings, address its prose)";
32273
+ 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.
32274
+
32275
+ Findings:
32276
+ ${findings}
32277
+
32278
+ Review:
32279
+ ${input.review}`;
32280
+ }
32168
32281
  async function composeChild(brief, instance, unit, readers) {
32169
32282
  const where = `${instance.repo}#${"pr" in brief ? brief.pr : ""}`;
32170
32283
  switch (brief.kind) {
@@ -32174,10 +32287,13 @@ async function composeChild(brief, instance, unit, readers) {
32174
32287
  return { preset: "coding", prompt, ref: unit.branch, contract };
32175
32288
  }
32176
32289
  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;
32290
+ let prior;
32291
+ if (brief.prior !== void 0) {
32292
+ const findings = (await facts(readers, brief.prior.reviewRunId)).findings ?? [];
32293
+ const recorded = brief.prior.codingRunId !== void 0 ? (await facts(readers, brief.prior.codingRunId)).dispositions ?? [] : [];
32294
+ const { matched, dropped } = matchDispositions(findings, recorded);
32295
+ prior = { findings, dispositions: matched, dropped };
32296
+ }
32181
32297
  const turn = buildShipReviewTurn({
32182
32298
  where,
32183
32299
  round: brief.round,
@@ -32189,16 +32305,18 @@ async function composeChild(brief, instance, unit, readers) {
32189
32305
 
32190
32306
  ${turn}`, contract };
32191
32307
  }
32192
- case "fix": {
32308
+ case "findings": {
32193
32309
  const review = await facts(readers, brief.reviewRunId);
32194
- const findings = review.findings ?? [];
32195
32310
  return {
32196
32311
  preset: "coding",
32197
- prompt: buildShipFixTurn({ where, findings, review: review.finalReply ?? "" }),
32198
- ref: unit.branch,
32199
- fixRound: { findingIds: findings.map((f) => f.id) }
32312
+ prompt: findingsRequest({ where, findings: review.findings ?? [], review: review.finalReply ?? "" }),
32313
+ ref: unit.branch
32200
32314
  };
32201
32315
  }
32316
+ default: {
32317
+ const unknown = brief;
32318
+ throw new Error(`unknown brief kind ${JSON.stringify(unknown.kind)}`);
32319
+ }
32202
32320
  }
32203
32321
  }
32204
32322
  async function facts(readers, runId3) {
@@ -32211,7 +32329,8 @@ var init_briefs = __esm({
32211
32329
  "../../src/core/coordinator/briefs.ts"() {
32212
32330
  "use strict";
32213
32331
  init_directives();
32214
- init_codingChild();
32332
+ init_reviewVerdict();
32333
+ init_coordinator();
32215
32334
  init_contract();
32216
32335
  init_preflight();
32217
32336
  init_reviewChild();
@@ -33060,6 +33179,18 @@ var init_ship = __esm({
33060
33179
  }
33061
33180
  });
33062
33181
 
33182
+ // ../../src/core/ship/codingChild.ts
33183
+ function withContractInFirstUserTurn(messages, block) {
33184
+ const at = messages.findIndex((m) => m.role === "user");
33185
+ if (at < 0) return [...messages, { role: "user", content: [{ type: "text", text: block }] }];
33186
+ return messages.map((m, i) => i === at ? { ...m, content: [...m.content, { type: "text", text: block }] } : m);
33187
+ }
33188
+ var init_codingChild = __esm({
33189
+ "../../src/core/ship/codingChild.ts"() {
33190
+ "use strict";
33191
+ }
33192
+ });
33193
+
33063
33194
  // ../../src/core/dispatch/settle.ts
33064
33195
  function settleThread(deps, ctx) {
33065
33196
  const { msg, admitted, runLoopStarted, control } = ctx;
@@ -33647,7 +33778,6 @@ async function dispatch(deps, msg, io, opts = {}) {
33647
33778
  parentRunId,
33648
33779
  coordinator,
33649
33780
  seed,
33650
- ...opts.fixRound ? { fixRound: opts.fixRound } : {},
33651
33781
  ...bearer !== void 0 ? { bearer } : {}
33652
33782
  });
33653
33783
  const { answer, prNote, toolCalls, runDiagnosis, checklistAsLeft, checklistCheckedOff, releaseWorkspace } = ran;
@@ -33923,7 +34053,12 @@ function toListItem(record2, bytes) {
33923
34053
  return { ...rest, bytes };
33924
34054
  }
33925
34055
  function retentionPolicyOf(cfg) {
33926
- return clampRetentionPolicy({ retentionDays: cfg.retentionDays, maxRuns: cfg.maxRuns, maxBytes: cfg.maxBytes });
34056
+ return clampRetentionPolicy({
34057
+ retentionDays: cfg.retentionDays,
34058
+ maxRuns: cfg.maxRuns,
34059
+ maxBytes: cfg.maxBytes,
34060
+ sessionLogMaxBytes: cfg.sessionLogMaxBytes
34061
+ });
33927
34062
  }
33928
34063
  function buildRunStore(cfg, secrets, deps) {
33929
34064
  if (!cfg) return null;
@@ -34167,6 +34302,24 @@ var init_flusher = __esm({
34167
34302
  }
34168
34303
  });
34169
34304
 
34305
+ // ../../src/core/runLedger/sessionLog.ts
34306
+ function sessionKey(threadKey, agent) {
34307
+ return `${threadKey}:${agent ?? "-"}`;
34308
+ }
34309
+ function requestIndex(seed) {
34310
+ for (let i = seed.length - 1; i >= 0; i--) if (seed[i].role === "user") return i;
34311
+ return 0;
34312
+ }
34313
+ var DEFAULT_SESSION_LOG_MAX_BYTES;
34314
+ var init_sessionLog = __esm({
34315
+ "../../src/core/runLedger/sessionLog.ts"() {
34316
+ "use strict";
34317
+ init_runRecord();
34318
+ init_runRecord();
34319
+ DEFAULT_SESSION_LOG_MAX_BYTES = DEFAULT_RETENTION_POLICY.sessionLogMaxBytes;
34320
+ }
34321
+ });
34322
+
34170
34323
  // ../../src/core/runLedger/types.ts
34171
34324
  var LEASE_MS, HEARTBEAT_MS, APPEND_FLUSH_MS, APPEND_FLUSH_EVENTS, TRANSCRIPT_REQUEST_BYTES, TRANSCRIPT_PART_BYTES, ATTACHMENT_REF_BYTES, GEN_PATTERN;
34172
34325
  var init_types5 = __esm({
@@ -34212,36 +34365,45 @@ function createLedgerWriteThrough(opts) {
34212
34365
  "[ledger] state Worker has no run-ledger routes \u2014 deploy it before this bot version; runs are not tracked until then"
34213
34366
  );
34214
34367
  };
34215
- async function claim(req, phase) {
34368
+ async function claim(req, opts2 = {}) {
34216
34369
  for (let attempt = 1; ; attempt++) {
34217
34370
  try {
34371
+ let session;
34372
+ if (opts2.seed) {
34373
+ const key = sessionKey(req.threadKey, req.meta.agent);
34374
+ const next = await ledger.sessionTail(key);
34375
+ session = { key, seedFrom: next, request: next + requestIndex(opts2.seed), range: { from: next } };
34376
+ }
34218
34377
  const result = await ledger.claim({
34219
34378
  runId: req.runId,
34220
34379
  threadKey: req.threadKey,
34221
34380
  gen,
34222
34381
  leaseMs,
34223
34382
  startedAt: req.startedAt,
34224
- meta: req.meta,
34383
+ meta: session ? { ...req.meta, session } : req.meta,
34225
34384
  card: req.card ?? null,
34226
34385
  system: req.system,
34227
34386
  tools: req.tools,
34228
34387
  ...req.state !== void 0 ? { state: req.state } : {},
34229
- ...phase ? { phase } : {}
34388
+ ...opts2.phase ? { phase: opts2.phase } : {}
34230
34389
  });
34231
- if (result.ok) return "ok";
34232
- if (result.live.runId === req.runId) return "fenced";
34390
+ if (result.ok) {
34391
+ if (session) await ledger.claimSession(session.key, req.runId, gen);
34392
+ return session ? { outcome: "ok", session } : { outcome: "ok" };
34393
+ }
34394
+ if (result.live.runId === req.runId) return { outcome: "fenced" };
34233
34395
  warn(
34234
34396
  `[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
34397
  );
34236
- return "untracked";
34398
+ return { outcome: "untracked" };
34237
34399
  } catch (err2) {
34238
34400
  if (err2 instanceof RouteMissingError) {
34239
34401
  routeMissing();
34240
- return "untracked";
34402
+ return { outcome: "untracked" };
34241
34403
  }
34242
34404
  if (err2 instanceof PermanentStoreError || attempt >= claimAttempts) {
34243
34405
  warn(`[ledger] ${req.threadKey} not tracked: claim failed after ${attempt} attempt(s): ${describe8(err2)}`);
34244
- return "untracked";
34406
+ return { outcome: "untracked" };
34245
34407
  }
34246
34408
  await sleep2(RETRY_MS[Math.min(attempt, RETRY_MS.length) - 1]);
34247
34409
  }
@@ -34261,6 +34423,15 @@ function createLedgerWriteThrough(opts) {
34261
34423
  seeded = false;
34262
34424
  adopted = false;
34263
34425
  handedOff = false;
34426
+ /** The run's place in its session log (session-log item 2); undefined for
34427
+ * a run without a conversation of its own and for an adopted row claimed
34428
+ * before the log existed. */
34429
+ session;
34430
+ /** Turns of the run's conversation on the ledger, counted from its seed —
34431
+ * the last step record's `turnIndex` — so the finish can close the range. */
34432
+ turnsWritten;
34433
+ /** A detach left the log short of what the model saw: the record says `broken`. */
34434
+ broken = false;
34264
34435
  stepNo;
34265
34436
  lastSeq;
34266
34437
  state;
@@ -34281,7 +34452,10 @@ function createLedgerWriteThrough(opts) {
34281
34452
  },
34282
34453
  ...opts.schedule ? { schedule: opts.schedule } : {}
34283
34454
  });
34284
- constructor(req, from = { stepNo: 0, lastSeq: 0 }) {
34455
+ constructor(req, from = {
34456
+ stepNo: 0,
34457
+ lastSeq: 0
34458
+ }) {
34285
34459
  this.runId = req.runId;
34286
34460
  this.threadKey = req.threadKey;
34287
34461
  this.state = req.state ?? {};
@@ -34290,6 +34464,11 @@ function createLedgerWriteThrough(opts) {
34290
34464
  this.stepNo = from.stepNo;
34291
34465
  this.lastSeq = from.lastSeq;
34292
34466
  this.adopted = from.resumable === true;
34467
+ this.session = from.session;
34468
+ }
34469
+ /** A reservation learns its session when the prompt's claim promotes it. */
34470
+ bindSession(session) {
34471
+ this.session = session;
34293
34472
  }
34294
34473
  get resumable() {
34295
34474
  return !this.detached && (this.seeded || this.adopted);
@@ -34297,11 +34476,24 @@ function createLedgerWriteThrough(opts) {
34297
34476
  markHandedOff() {
34298
34477
  this.handedOff = true;
34299
34478
  }
34479
+ /** The run's range as the finished record carries it (session-log item 2):
34480
+ * closed at the last turn written, or `broken` after a detach — the log
34481
+ * ends short of what the model saw, and the next run in the session must
34482
+ * know. A run that wrote no turn leaves the range open. */
34483
+ sessionOnRecord() {
34484
+ if (!this.session) return void 0;
34485
+ if (this.broken) return { ...this.session, range: "broken" };
34486
+ const from = this.session.range === "broken" ? this.session.seedFrom : this.session.range.from;
34487
+ const to = this.turnsWritten !== void 0 && this.turnsWritten > 0 ? this.session.seedFrom + this.turnsWritten - 1 : void 0;
34488
+ return { ...this.session, range: to !== void 0 && to >= from ? { from, to } : { from } };
34489
+ }
34300
34490
  sink = {
34301
- put: async (record2) => {
34491
+ put: async (plain4) => {
34302
34492
  this.finished = true;
34303
34493
  live.delete(this);
34304
34494
  await this.close();
34495
+ const session = this.sessionOnRecord();
34496
+ const record2 = session ? { ...plain4, session } : plain4;
34305
34497
  let result;
34306
34498
  try {
34307
34499
  result = await ledger.finish(this.runId, gen, record2);
@@ -34322,6 +34514,7 @@ function createLedgerWriteThrough(opts) {
34322
34514
  detach(reason) {
34323
34515
  if (this.detached) return;
34324
34516
  this.detached = true;
34517
+ this.broken = true;
34325
34518
  warn(`[ledger] ${this.threadKey} run ${this.runId} detached: ${reason} \u2014 this run is not resumable`);
34326
34519
  this.stopHeartbeat();
34327
34520
  live.delete(this);
@@ -34339,13 +34532,15 @@ function createLedgerWriteThrough(opts) {
34339
34532
  * no record at all was killed before its conversation was stored and
34340
34533
  * closes `interrupted`. */
34341
34534
  async seed(messages, budgetMs) {
34342
- const turns = messages.map((message2, idx) => ({ idx, message: message2 }));
34535
+ const base = this.session?.seedFrom ?? 0;
34536
+ const turns = messages.map((message2, i) => ({ idx: base + i, message: message2 }));
34343
34537
  try {
34344
- const seeded = await ledger.seed(this.runId, gen, turns);
34538
+ const seeded = await ledger.seed(this.runId, gen, turns, this.session?.key);
34345
34539
  if (!seeded.ok) {
34346
34540
  this.detach(`seed refused (${seeded.reason})`);
34347
34541
  return;
34348
34542
  }
34543
+ this.turnsWritten = messages.length;
34349
34544
  const recorded = await ledger.step(
34350
34545
  this.runId,
34351
34546
  gen,
@@ -34369,11 +34564,14 @@ function createLedgerWriteThrough(opts) {
34369
34564
  }
34370
34565
  async step(report2) {
34371
34566
  if (this.detached) return;
34372
- const turns = report2.turns.map((message2, i) => ({ idx: report2.firstIdx + i, message: message2 }));
34567
+ const base = this.session?.seedFrom ?? 0;
34568
+ const turns = report2.turns.map((message2, i) => ({ idx: base + report2.firstIdx + i, message: message2 }));
34569
+ if (report2.compaction)
34570
+ turns.push({ idx: base + report2.firstIdx + report2.turns.length, compaction: report2.compaction });
34373
34571
  const record2 = {
34374
34572
  step: ++this.stepNo,
34375
34573
  seq: this.lastSeq,
34376
- turnIndex: report2.firstIdx + report2.turns.length,
34574
+ turnIndex: report2.firstIdx + turns.length,
34377
34575
  inFlight: report2.inFlight,
34378
34576
  inboxConsumedSeq: report2.inboxConsumedSeq,
34379
34577
  remainingMs: report2.remainingMs,
@@ -34382,8 +34580,9 @@ function createLedgerWriteThrough(opts) {
34382
34580
  };
34383
34581
  for (let attempt = 1; ; attempt++) {
34384
34582
  try {
34385
- const result = await ledger.step(this.runId, gen, record2, turns);
34583
+ const result = await ledger.step(this.runId, gen, record2, turns, this.session?.key);
34386
34584
  if (!result.ok) this.detach(`step ${record2.step} refused (${result.reason})`);
34585
+ else this.turnsWritten = record2.turnIndex;
34387
34586
  return;
34388
34587
  } catch (err2) {
34389
34588
  if (err2 instanceof RouteMissingError || err2 instanceof PermanentStoreError || attempt >= 2) {
@@ -34491,8 +34690,8 @@ function createLedgerWriteThrough(opts) {
34491
34690
  return {
34492
34691
  gen,
34493
34692
  async reserve(req) {
34494
- const claimed2 = await claim({ ...req, system: "", tools: [], state: {} }, "attaching");
34495
- if (claimed2 !== "ok") return void 0;
34693
+ const claimed2 = await claim({ ...req, system: "", tools: [], state: {} }, { phase: "attaching" });
34694
+ if (claimed2.outcome !== "ok") return void 0;
34496
34695
  const run2 = new TrackedRun(req);
34497
34696
  run2.startHeartbeat();
34498
34697
  live.add(run2);
@@ -34500,35 +34699,46 @@ function createLedgerWriteThrough(opts) {
34500
34699
  },
34501
34700
  async open(req) {
34502
34701
  const reserved = req.reservation;
34702
+ const seed = req.seed?.messages;
34503
34703
  if (reserved instanceof TrackedRun) {
34504
34704
  if (!reserved.tracked()) return void 0;
34505
- const claimed3 = await claim(req);
34506
- if (claimed3 === "fenced") {
34705
+ const claimed3 = await claim(req, seed ? { seed } : {});
34706
+ if (claimed3.outcome === "fenced") {
34507
34707
  reserved.detach("promotion refused (fenced)");
34508
34708
  return void 0;
34509
34709
  }
34510
- if (claimed3 !== "ok") {
34710
+ if (claimed3.outcome !== "ok") {
34511
34711
  await reserved.close();
34512
34712
  live.delete(reserved);
34513
34713
  return void 0;
34514
34714
  }
34715
+ if (claimed3.session) reserved.bindSession(claimed3.session);
34515
34716
  if (req.seed) await reserved.seed(req.seed.messages, req.seed.budgetMs);
34516
34717
  return reserved;
34517
34718
  }
34518
- const claimed2 = await claim(req);
34519
- if (claimed2 === "fenced") {
34719
+ const claimed2 = await claim(req, seed ? { seed } : {});
34720
+ if (claimed2.outcome === "fenced") {
34520
34721
  warn(`[ledger] ${req.threadKey} not tracked: run ${req.runId} is live under another generation`);
34521
34722
  return void 0;
34522
34723
  }
34523
- if (claimed2 !== "ok") return void 0;
34524
- const run2 = new TrackedRun(req);
34724
+ if (claimed2.outcome !== "ok") return void 0;
34725
+ const run2 = new TrackedRun(req, {
34726
+ stepNo: 0,
34727
+ lastSeq: 0,
34728
+ ...claimed2.session ? { session: claimed2.session } : {}
34729
+ });
34525
34730
  if (req.seed) await run2.seed(req.seed.messages, req.seed.budgetMs);
34526
34731
  run2.startHeartbeat();
34527
34732
  live.add(run2);
34528
34733
  return run2;
34529
34734
  },
34530
34735
  adopt(req) {
34531
- const run2 = new TrackedRun(req, { stepNo: req.lastStep, lastSeq: req.lastSeq, resumable: true });
34736
+ const run2 = new TrackedRun(req, {
34737
+ stepNo: req.lastStep,
34738
+ lastSeq: req.lastSeq,
34739
+ resumable: true,
34740
+ ...req.session ? { session: req.session } : {}
34741
+ });
34532
34742
  run2.startHeartbeat();
34533
34743
  live.add(run2);
34534
34744
  return run2;
@@ -34578,6 +34788,7 @@ var init_writeThrough = __esm({
34578
34788
  "use strict";
34579
34789
  init_runStoreWorker();
34580
34790
  init_flusher();
34791
+ init_sessionLog();
34581
34792
  init_types5();
34582
34793
  NullLedgerWriteThrough = class {
34583
34794
  constructor(gen, fallback) {
@@ -38847,6 +39058,16 @@ function turnRows(idx, message2, opts = {}) {
38847
39058
  const refBytes = opts.attachmentRefBytes ?? ATTACHMENT_REF_BYTES;
38848
39059
  const rows = [];
38849
39060
  const attachments = [];
39061
+ if (isCompaction(message2)) {
39062
+ const json2 = JSON.stringify({ compaction: message2.compaction });
39063
+ const bytes = utf8ByteLength(json2);
39064
+ if (bytes > partBytes) {
39065
+ throw new Error(
39066
+ `transcript: the compaction row at turn ${idx} is ${bytes} bytes, over the ${partBytes} row budget`
39067
+ );
39068
+ }
39069
+ return { rows: [{ idx, part: 0, json: json2 }], attachments };
39070
+ }
38850
39071
  message2.content.forEach((part, i) => {
38851
39072
  let stored = part;
38852
39073
  if (hasData(part) && part.data.length > refBytes) {
@@ -38880,49 +39101,69 @@ function chunkRows(rows, maxBytes) {
38880
39101
  if (current.length > 0) chunks.push(current);
38881
39102
  return chunks;
38882
39103
  }
38883
- function assembleTranscript(rows, attachments) {
39104
+ function assembleTranscript(rows, attachments, base = 0) {
38884
39105
  const byRef = new Map(attachments.map((a) => [a.ref, a]));
38885
39106
  const byTurn = /* @__PURE__ */ new Map();
38886
39107
  for (const row of rows) {
38887
- let parts = byTurn.get(row.idx);
38888
- if (!parts) byTurn.set(row.idx, parts = /* @__PURE__ */ new Map());
39108
+ const idx = row.idx - base;
39109
+ let parts = byTurn.get(idx);
39110
+ if (!parts) byTurn.set(idx, parts = /* @__PURE__ */ new Map());
38889
39111
  parts.set(row.part, JSON.parse(row.json));
38890
39112
  }
38891
39113
  const messages = [];
39114
+ const compactions = [];
39115
+ const messageIndexOf = /* @__PURE__ */ new Map();
39116
+ const gap = (why) => ({
39117
+ complete: false,
39118
+ turns: messages.length + compactions.length,
39119
+ messages,
39120
+ compactions,
39121
+ gap: why
39122
+ });
38892
39123
  const turnCount = byTurn.size === 0 ? 0 : Math.max(...byTurn.keys()) + 1;
38893
39124
  for (let idx = 0; idx < turnCount; idx++) {
38894
39125
  const parts = byTurn.get(idx);
38895
- if (!parts) return { complete: false, turns: messages.length, messages, gap: `turn ${idx} is missing` };
39126
+ if (!parts) return gap(`turn ${idx} is missing`);
39127
+ const first = parts.get(0);
39128
+ if (first && "compaction" in first) {
39129
+ compactions.push({ before: messages.length, entry: first.compaction });
39130
+ continue;
39131
+ }
38896
39132
  const content = [];
38897
39133
  let role;
38898
39134
  const partCount = Math.max(...parts.keys()) + 1;
38899
39135
  for (let p = 0; p < partCount; p++) {
38900
39136
  const stored = parts.get(p);
38901
- if (!stored)
38902
- return { complete: false, turns: messages.length, messages, gap: `turn ${idx} is missing part ${p}` };
39137
+ if (!stored || "compaction" in stored) return gap(`turn ${idx} is missing part ${p}`);
38903
39138
  role = stored.role;
38904
39139
  const part = stored.part;
38905
39140
  if (part.dataRef !== void 0) {
38906
39141
  const att = byRef.get(part.dataRef);
38907
- if (!att)
38908
- return { complete: false, turns: messages.length, messages, gap: `attachment ${part.dataRef} is missing` };
39142
+ if (!att) return gap(`attachment ${part.dataRef} is missing`);
38909
39143
  const { dataRef: _ref, ...rest } = part;
38910
39144
  content.push({ ...rest, data: att.data });
38911
39145
  } else {
38912
39146
  content.push(part);
38913
39147
  }
38914
39148
  }
39149
+ messageIndexOf.set(idx, messages.length);
38915
39150
  messages.push({ role: role ?? "user", content });
38916
39151
  }
38917
- return { complete: true, turns: messages.length, messages };
39152
+ for (const c of compactions) {
39153
+ if (c.entry.keptFrom === void 0) continue;
39154
+ const kept = messageIndexOf.get(c.entry.keptFrom - base);
39155
+ if (kept !== void 0) c.keptBefore = kept;
39156
+ }
39157
+ return { complete: true, turns: messages.length + compactions.length, messages, compactions };
38918
39158
  }
38919
- var attachmentRef;
39159
+ var attachmentRef, isCompaction;
38920
39160
  var init_transcript = __esm({
38921
39161
  "../../src/core/runLedger/transcript.ts"() {
38922
39162
  "use strict";
38923
39163
  init_runRecord();
38924
39164
  init_types5();
38925
39165
  attachmentRef = (idx, part) => `t${idx}p${part}`;
39166
+ isCompaction = (v) => "compaction" in v;
38926
39167
  }
38927
39168
  });
38928
39169
 
@@ -38937,6 +39178,9 @@ function buildRunLedger(cfg, secrets, deps = {}) {
38937
39178
  baseUrl: worker.baseUrl,
38938
39179
  token: token2.reveal(),
38939
39180
  storeKey: RUN_STORE_KEY,
39181
+ // The same clamped policy the run store proposes, so the session logs and
39182
+ // the run records are bounded by one configuration.
39183
+ sessionLogMaxBytes: retentionPolicyOf(cfg).sessionLogMaxBytes,
38940
39184
  ...deps.fetch ? { fetch: deps.fetch } : {}
38941
39185
  });
38942
39186
  }
@@ -38945,7 +39189,9 @@ var init_runLedgerWorker = __esm({
38945
39189
  "../../src/core/runLedgerWorker.ts"() {
38946
39190
  "use strict";
38947
39191
  init_runRecord();
39192
+ init_runStore();
38948
39193
  init_runStoreWorker();
39194
+ init_sessionLog();
38949
39195
  init_transcript();
38950
39196
  init_types5();
38951
39197
  WorkerRunLedger = class {
@@ -39009,41 +39255,94 @@ var init_runLedgerWorker = __esm({
39009
39255
  }
39010
39256
  };
39011
39257
  }
39012
- await this.post("/runs/transcript/owner", { runId: req.runId, gen: req.gen });
39013
39258
  return { ok: true };
39014
39259
  }
39015
- async writeTurns(runId3, gen, turns) {
39260
+ /** The write route and its body for a run's rows: the session log when the
39261
+ * run has one, the run's own transcript object otherwise. */
39262
+ target(runId3, gen, session) {
39263
+ if (session === void 0) return { path: "/runs/transcript/write", body: { runId: runId3, gen } };
39264
+ this.checkSessionKey(session);
39265
+ return { path: "/runs/session/write", body: { key: session, gen } };
39266
+ }
39267
+ async writeTurns(runId3, gen, turns, session) {
39016
39268
  const rows = [];
39017
39269
  const attachments = [];
39018
39270
  for (const t of turns) {
39019
- const out = turnRows(t.idx, t.message);
39271
+ const out = turnRows(t.idx, "message" in t ? t.message : { compaction: t.compaction });
39020
39272
  rows.push(...out.rows);
39021
39273
  attachments.push(...out.attachments);
39022
39274
  }
39275
+ const { path, body } = this.target(runId3, gen, session);
39023
39276
  for (const a of attachments) {
39024
- const r = await this.post("/runs/transcript/write", { runId: runId3, gen, rows: [], attachments: [a] });
39277
+ const r = await this.post(path, { ...body, rows: [], attachments: [a] });
39025
39278
  const f = this.fenceResult(r);
39026
39279
  if (!f.ok) return f;
39027
39280
  }
39028
39281
  const chunks = chunkRows(rows, TRANSCRIPT_REQUEST_BYTES - 4096);
39029
39282
  for (const chunk of chunks.length ? chunks : [[]]) {
39030
39283
  if (chunk.length === 0 && attachments.length > 0) continue;
39031
- const r = await this.post("/runs/transcript/write", { runId: runId3, gen, rows: chunk, attachments: [] });
39284
+ const r = await this.post(path, { ...body, rows: chunk, attachments: [] });
39032
39285
  const f = this.fenceResult(r);
39033
39286
  if (!f.ok) return f;
39034
39287
  }
39035
39288
  return { ok: true };
39036
39289
  }
39037
- async seed(runId3, gen, turns) {
39290
+ async seed(runId3, gen, turns, session) {
39038
39291
  this.checkIds(runId3, gen);
39039
- return this.writeTurns(runId3, gen, turns);
39292
+ return this.writeTurns(runId3, gen, turns, session);
39040
39293
  }
39041
- async step(runId3, gen, record2, turns) {
39294
+ async step(runId3, gen, record2, turns, session) {
39042
39295
  this.checkIds(runId3, gen);
39043
- const written = await this.writeTurns(runId3, gen, turns);
39296
+ const written = await this.writeTurns(runId3, gen, turns, session);
39044
39297
  if (!written.ok) return written;
39045
39298
  return this.fenceResult(await this.post("/runs/step", { storeKey: this.opts.storeKey, runId: runId3, gen, record: record2 }));
39046
39299
  }
39300
+ checkSessionKey(key) {
39301
+ if (!SESSION_KEY_PATTERN.test(key))
39302
+ throw new PermanentStoreError(`run ledger: malformed session key ${JSON.stringify(key)}`);
39303
+ }
39304
+ async sessionTail(key) {
39305
+ this.checkSessionKey(key);
39306
+ const r = await this.post("/runs/session/tail", { key });
39307
+ return typeof r.data.next === "number" ? r.data.next : 0;
39308
+ }
39309
+ async claimSession(key, runId3, gen, maxBytes) {
39310
+ this.checkSessionKey(key);
39311
+ this.checkIds(runId3, gen);
39312
+ await this.post("/runs/session/owner", {
39313
+ key,
39314
+ runId: runId3,
39315
+ gen,
39316
+ maxBytes: maxBytes ?? this.opts.sessionLogMaxBytes ?? DEFAULT_SESSION_LOG_MAX_BYTES
39317
+ });
39318
+ }
39319
+ async releaseSession(key, runId3, gen) {
39320
+ this.checkSessionKey(key);
39321
+ this.checkIds(runId3, gen);
39322
+ return this.fenceResult(await this.post("/runs/session/clear-owner", { key, runId: runId3, gen }));
39323
+ }
39324
+ async readSession(key, from, to) {
39325
+ this.checkSessionKey(key);
39326
+ const r = await this.post("/runs/session/read", { key, from, ...to !== void 0 ? { to } : {} });
39327
+ return assembleTranscript(
39328
+ Array.isArray(r.data.rows) ? r.data.rows : [],
39329
+ Array.isArray(r.data.attachments) ? r.data.attachments : [],
39330
+ from
39331
+ );
39332
+ }
39333
+ async readSessionTail(key, maxBytes) {
39334
+ this.checkSessionKey(key);
39335
+ const r = await this.post("/runs/session/read-tail", { key, maxBytes });
39336
+ const from = typeof r.data.from === "number" ? r.data.from : 0;
39337
+ return {
39338
+ from,
39339
+ transcript: assembleTranscript(
39340
+ Array.isArray(r.data.rows) ? r.data.rows : [],
39341
+ Array.isArray(r.data.attachments) ? r.data.attachments : [],
39342
+ from
39343
+ )
39344
+ };
39345
+ }
39047
39346
  async heartbeat(runId3, gen, leaseMs) {
39048
39347
  this.checkIds(runId3, gen);
39049
39348
  const r = await this.post("/runs/heartbeat", { storeKey: this.opts.storeKey, runId: runId3, gen, leaseMs });
@@ -39095,22 +39394,23 @@ var init_runLedgerWorker = __esm({
39095
39394
  const r = await this.post("/runs/finish", { storeKey: this.opts.storeKey, runId: runId3, gen, record: record2 });
39096
39395
  const f = this.fenceResult(r);
39097
39396
  if (!f.ok) return f;
39098
- await this.post("/runs/transcript/clear", { runId: runId3 }).catch(() => {
39099
- });
39397
+ if (record2.session)
39398
+ await this.post("/runs/session/clear-owner", { key: record2.session.key, runId: runId3, gen }).catch(() => {
39399
+ });
39100
39400
  return { ok: true, stored: r.data.stored === true };
39101
39401
  }
39102
39402
  async abandon(runId3, gen) {
39103
39403
  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;
39404
+ return this.fenceResult(await this.post("/runs/abandon", { storeKey: this.opts.storeKey, runId: runId3, gen }));
39109
39405
  }
39110
39406
  async reclaim(gen, now, leaseMs) {
39111
39407
  const r = await this.post("/runs/reclaim", { storeKey: this.opts.storeKey, gen, now, leaseMs });
39112
39408
  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 });
39409
+ for (const run2 of runs) {
39410
+ const session = run2.row.meta?.session;
39411
+ if (session) await this.claimSession(session.key, run2.row.runId, gen);
39412
+ else await this.post("/runs/transcript/owner", { runId: run2.row.runId, gen });
39413
+ }
39114
39414
  return runs;
39115
39415
  }
39116
39416
  async listLive() {
@@ -39158,6 +39458,116 @@ var init_decisions = __esm({
39158
39458
  }
39159
39459
  });
39160
39460
 
39461
+ // ../../src/core/runLedger/resume.ts
39462
+ function transcriptSource(meta) {
39463
+ return meta.session ? { kind: "session", key: meta.session.key, from: meta.session.seedFrom } : { kind: "run" };
39464
+ }
39465
+ function settlementFor(toolUse, tools) {
39466
+ const tool = tools.get(toolUse.name);
39467
+ if (!tool) {
39468
+ return {
39469
+ toolUse,
39470
+ action: "synthetic",
39471
+ text: `Tool ${toolUse.name} is not available after the bot restarted; continue without it.`
39472
+ };
39473
+ }
39474
+ if (tool.sideEffectFree || RERUN_SAFE_TOOLS.has(toolUse.name)) return { toolUse, action: "rerun" };
39475
+ return {
39476
+ toolUse,
39477
+ action: "synthetic",
39478
+ text: `The bot restarted while this ${toolUse.name} call was in flight; its effects are unknown \u2014 re-check them before re-running it.`
39479
+ };
39480
+ }
39481
+ function planResume(input) {
39482
+ const { transcript, lastStep } = input;
39483
+ if (!lastStep) return { kind: "interrupted", why: "no step record: killed before its conversation was stored" };
39484
+ if (!transcript.complete) return { kind: "interrupted", why: `transcript incomplete: ${transcript.gap}` };
39485
+ const completeness = transcriptCompleteness({
39486
+ lastStep,
39487
+ seedTurns: lastStep.turnIndex,
39488
+ transcriptTurns: transcript.turns
39489
+ });
39490
+ if (completeness.kind === "interrupted") return completeness;
39491
+ const tools = /* @__PURE__ */ new Map();
39492
+ for (const t of input.tools) tools.set(t.name, t);
39493
+ const messages = transcript.messages;
39494
+ const last = messages[messages.length - 1];
39495
+ const calls = toolUsesOf(last);
39496
+ if (completeness.kind === "resume") {
39497
+ if (lastStep.inFlight.length === 0) {
39498
+ if (last?.role !== "user") {
39499
+ return {
39500
+ kind: "interrupted",
39501
+ why: "transcript ends with an assistant turn but the last step recorded nothing in flight"
39502
+ };
39503
+ }
39504
+ return {
39505
+ kind: "resume",
39506
+ messages,
39507
+ compactions: transcript.compactions,
39508
+ settlements: [],
39509
+ stepRecorded: true,
39510
+ inboxConsumedSeq: lastStep.inboxConsumedSeq,
39511
+ step: lastStep.step,
39512
+ turn: lastStep.turn,
39513
+ iteration: lastStep.iteration,
39514
+ remainingMs: lastStep.remainingMs
39515
+ };
39516
+ }
39517
+ const recorded = new Set(lastStep.inFlight.map((c) => c.callId));
39518
+ if (calls.length === 0 || calls.length !== recorded.size || !calls.every((c) => recorded.has(c.id))) {
39519
+ return {
39520
+ kind: "interrupted",
39521
+ why: `the last step's calls in flight (${[...recorded].join(", ")}) do not match the transcript's last assistant turn`
39522
+ };
39523
+ }
39524
+ return {
39525
+ kind: "resume",
39526
+ messages,
39527
+ compactions: transcript.compactions,
39528
+ settlements: calls.map((c) => settlementFor(c, tools)),
39529
+ stepRecorded: true,
39530
+ inboxConsumedSeq: lastStep.inboxConsumedSeq,
39531
+ step: lastStep.step,
39532
+ turn: lastStep.turn,
39533
+ iteration: lastStep.iteration,
39534
+ remainingMs: lastStep.remainingMs
39535
+ };
39536
+ }
39537
+ if (calls.length === 0) {
39538
+ return { kind: "interrupted", why: "the next step's turns landed but its assistant turn has no tool calls" };
39539
+ }
39540
+ const bookkeepingOnly = calls.every((c) => c.name === "update_status");
39541
+ return {
39542
+ kind: "resume",
39543
+ messages,
39544
+ compactions: transcript.compactions,
39545
+ settlements: calls.map((c) => ({ toolUse: c, action: "rerun" })),
39546
+ stepRecorded: false,
39547
+ inboxConsumedSeq: lastStep.inboxConsumedSeq,
39548
+ step: lastStep.step + 1,
39549
+ turn: lastStep.turn + (bookkeepingOnly ? 0 : 1),
39550
+ iteration: lastStep.iteration + 1,
39551
+ remainingMs: lastStep.remainingMs
39552
+ };
39553
+ }
39554
+ var RERUN_SAFE_TOOLS, toolUsesOf;
39555
+ var init_resume = __esm({
39556
+ "../../src/core/runLedger/resume.ts"() {
39557
+ "use strict";
39558
+ init_decisions();
39559
+ RERUN_SAFE_TOOLS = /* @__PURE__ */ new Set([
39560
+ "write_file",
39561
+ "update_status",
39562
+ "submit_verdict",
39563
+ "submit_pr_description",
39564
+ "submit_handoff",
39565
+ "submit_dispositions"
39566
+ ]);
39567
+ toolUsesOf = (message2) => message2?.role === "assistant" ? message2.content.filter((p) => p.type === "tool_use") : [];
39568
+ }
39569
+ });
39570
+
39161
39571
  // ../../src/core/boot.ts
39162
39572
  function closureNote(agent, prUrl2) {
39163
39573
  if (agent === "ship") return shipInterruptedNote(prUrl2);
@@ -39205,7 +39615,7 @@ async function reclaimRuns(opts) {
39205
39615
  status3 = "interrupted";
39206
39616
  why = "reserved at admission without its request: nothing to restart from";
39207
39617
  } else {
39208
- const verdict = await completenessVerdict(ledger, row.runId, run2.lastStep);
39618
+ const verdict = await completenessVerdict(ledger, row, run2.lastStep);
39209
39619
  if (verdict.resumable && run2.lastStep) {
39210
39620
  const events2 = await ledger.readEvents(row.runId);
39211
39621
  outcome.resumable.push({
@@ -39306,11 +39716,12 @@ async function closeReclaimed(ledger, gen, input) {
39306
39716
  const result = await ledger.finish(input.row.runId, gen, record2);
39307
39717
  return result.ok ? { ok: true, record: record2 } : { ok: false, reason: result.reason ?? "refused" };
39308
39718
  }
39309
- async function completenessVerdict(ledger, runId3, lastStep) {
39719
+ async function completenessVerdict(ledger, row, lastStep) {
39310
39720
  if (!lastStep) return { resumable: false, why: "no step record: killed before its conversation was stored" };
39311
39721
  let transcript;
39312
39722
  try {
39313
- transcript = await ledger.readTranscript(runId3);
39723
+ const source2 = transcriptSource(row.meta);
39724
+ transcript = source2.kind === "session" ? await ledger.readSession(source2.key, source2.from) : await ledger.readTranscript(row.runId);
39314
39725
  } catch (err2) {
39315
39726
  return { resumable: false, why: `transcript unreadable (${describe9(err2)})` };
39316
39727
  }
@@ -39339,6 +39750,7 @@ var init_boot = __esm({
39339
39750
  "use strict";
39340
39751
  init_runStoreWorker();
39341
39752
  init_decisions();
39753
+ init_resume();
39342
39754
  init_types5();
39343
39755
  init_record();
39344
39756
  init_shipPipeline();
@@ -39353,110 +39765,6 @@ var init_boot = __esm({
39353
39765
  }
39354
39766
  });
39355
39767
 
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
39768
  // ../../src/core/resumeLaunch.ts
39461
39769
  function knownToolsFor(agent) {
39462
39770
  return (TOOLSETS[agent.toolset] ?? []).map((t) => ({
@@ -40661,9 +40969,12 @@ function parseBrief(v) {
40661
40969
  const p = b.prior;
40662
40970
  if (typeof p !== "object" || p === null || typeof p.reviewRunId !== "string" || !RUN_ID_PATTERN.test(p.reviewRunId))
40663
40971
  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 } : {} };
40972
+ if (p.codingRunId !== void 0 && (typeof p.codingRunId !== "string" || !RUN_ID_PATTERN.test(p.codingRunId)))
40973
+ return invalid("brief.prior.codingRunId must be a run id");
40974
+ prior = {
40975
+ reviewRunId: p.reviewRunId,
40976
+ ...p.codingRunId !== void 0 ? { codingRunId: p.codingRunId } : {}
40977
+ };
40667
40978
  }
40668
40979
  return {
40669
40980
  ok: true,
@@ -40677,15 +40988,15 @@ function parseBrief(v) {
40677
40988
  }
40678
40989
  };
40679
40990
  }
40680
- case "fix": {
40991
+ case "findings": {
40681
40992
  const n2 = pr();
40682
40993
  if (!n2.ok) return n2;
40683
40994
  const review = runId3("reviewRunId");
40684
40995
  if (!review.ok) return review;
40685
- return { ok: true, value: { kind: "fix", unit: b.unit, pr: n2.value, reviewRunId: review.value } };
40996
+ return { ok: true, value: { kind: "findings", unit: b.unit, pr: n2.value, reviewRunId: review.value } };
40686
40997
  }
40687
40998
  default:
40688
- return invalid("brief.kind must be contract, review or fix");
40999
+ return invalid("brief.kind must be contract, review or findings");
40689
41000
  }
40690
41001
  }
40691
41002
  function parseObject(text) {
@@ -40748,6 +41059,30 @@ function unitThread(instance, row) {
40748
41059
  const sourceUrl = row?.sourceUrl ?? (threadKey === instance.threadKey ? instance.sourceUrl : void 0);
40749
41060
  return { threadKey, sourceUrl };
40750
41061
  }
41062
+ async function openThreadFromRequester(deps, instance, lead, at) {
41063
+ const parent = deps.ioFor({ threadKey: instance.threadKey, userId: instance.userId });
41064
+ if (!parent?.openThread) return { ok: false, response: json(503, { ok: false, error: "no_channel", at }) };
41065
+ try {
41066
+ const opened = await parent.openThread(lead);
41067
+ return {
41068
+ ok: true,
41069
+ thread: {
41070
+ threadKey: opened.thread.threadKey,
41071
+ ...opened.thread.sourceUrl !== void 0 ? { sourceUrl: opened.thread.sourceUrl } : {}
41072
+ }
41073
+ };
41074
+ } catch (err2) {
41075
+ return { ok: false, response: json(502, { ok: false, error: "thread_failed", message: describe10(err2), at }) };
41076
+ }
41077
+ }
41078
+ async function ensureReviewThread(deps, instance, row, at) {
41079
+ if (row.reviewThread !== void 0) return { ok: true, row, thread: row.reviewThread };
41080
+ const opened = await openThreadFromRequester(deps, instance, reviewLead(instance, row), at);
41081
+ if (!opened.ok) return opened;
41082
+ const updated = { ...row, reviewThread: opened.thread };
41083
+ await deps.instances.putUnits([updated]);
41084
+ return { ok: true, row: updated, thread: opened.thread };
41085
+ }
40751
41086
  function authorizeStep(headers, step, deps) {
40752
41087
  const raw = headers.authorization;
40753
41088
  const authorization = Array.isArray(raw) ? raw[0] : raw;
@@ -40836,8 +41171,19 @@ async function spawn6(body, deps) {
40836
41171
  const at = (deps.clock ?? systemClock)();
40837
41172
  const unit = await unitRowOf(deps, instance, req.unit);
40838
41173
  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 });
41174
+ const own = unitThread(instance, unit.row);
41175
+ if (own.threadKey === void 0) return json(409, { ok: false, error: "unit_not_started", unit: req.unit, at });
41176
+ let row = unit.row;
41177
+ let thread = {
41178
+ threadKey: own.threadKey,
41179
+ ...own.sourceUrl !== void 0 ? { sourceUrl: own.sourceUrl } : {}
41180
+ };
41181
+ if (req.preset === "review" && row !== void 0) {
41182
+ const review = await ensureReviewThread(deps, instance, row, at);
41183
+ if (!review.ok) return review.response;
41184
+ row = review.row;
41185
+ thread = review.thread;
41186
+ }
40841
41187
  const threadKey = thread.threadKey;
40842
41188
  const key = idempotencyKeyFor(instance.id, req.step);
40843
41189
  const live = await liveOnThread(deps.runs, instance, threadKey);
@@ -40848,10 +41194,9 @@ async function spawn6(body, deps) {
40848
41194
  if (!io) return json(503, { ok: false, error: "no_channel", at });
40849
41195
  let turn;
40850
41196
  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" });
41197
+ if (row === void 0) return json(400, { ok: false, error: "a brief needs the unit it runs for" });
40852
41198
  try {
40853
- const composed = await composeChild(req.brief, instance, unit.row, briefReaders(deps, instance, io));
40854
- turn = composed;
41199
+ turn = await composeChild(req.brief, instance, row, briefReaders(deps, instance, io));
40855
41200
  } catch (err2) {
40856
41201
  log(`[coordinator] ${instance.id} ${req.step}: the brief could not be composed: ${describe10(err2)}`);
40857
41202
  return json(502, { ok: false, error: "brief_failed", message: describe10(err2), at });
@@ -40897,8 +41242,7 @@ async function spawn6(body, deps) {
40897
41242
  };
40898
41243
  const settled = deps.dispatch(msg, child, {
40899
41244
  coordinator: tag,
40900
- ...turn.contract !== void 0 ? { contract: turn.contract } : {},
40901
- ...turn.fixRound !== void 0 ? { fixRound: turn.fixRound } : {}
41245
+ ...turn.contract !== void 0 ? { contract: turn.contract } : {}
40902
41246
  }).then(
40903
41247
  (outcome) => ({ kind: "ended", outcome }),
40904
41248
  (err2) => ({ kind: "threw", err: err2 })
@@ -41115,30 +41459,32 @@ async function unitStart(body, deps) {
41115
41459
  ...instance.sourceUrl !== void 0 ? { sourceUrl: instance.sourceUrl } : {}
41116
41460
  };
41117
41461
  } 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
- }
41462
+ const opened = await openThreadFromRequester(deps, instance, unitLead(instance, row), at);
41463
+ if (!opened.ok) return opened.response;
41464
+ row = { ...row, ...opened.thread };
41130
41465
  }
41131
41466
  }
41467
+ if (row.reviewThread === void 0) {
41468
+ const review = await ensureReviewThread(deps, instance, row, at);
41469
+ if (!review.ok) {
41470
+ if (row !== unit.row) await deps.instances.putUnits([row]);
41471
+ return review.response;
41472
+ }
41473
+ row = review.row;
41474
+ }
41132
41475
  if (row.issue === void 0 && row.unit !== TASK_UNIT) {
41133
41476
  const issue = await unitIssueOf(deps, instance.repo, row.unit);
41134
41477
  if (issue !== void 0) row = { ...row, issue };
41135
41478
  }
41136
41479
  row = { ...row, startedAt: row.startedAt ?? at };
41137
41480
  await deps.instances.putUnits([row]);
41138
- (deps.log ?? console.log)(`[coordinator] ${instance.id} ${row.unit}: started in ${row.threadKey}`);
41481
+ (deps.log ?? console.log)(
41482
+ `[coordinator] ${instance.id} ${row.unit}: started in ${row.threadKey}, review in ${row.reviewThread?.threadKey}`
41483
+ );
41139
41484
  return json(200, {
41140
41485
  ok: true,
41141
41486
  threadKey: row.threadKey,
41487
+ reviewThreadKey: row.reviewThread?.threadKey,
41142
41488
  branch: row.branch,
41143
41489
  base: instance.base ?? "main",
41144
41490
  ...row.issue !== void 0 ? { issue: row.issue } : {},
@@ -41150,6 +41496,12 @@ function unitLead(instance, row) {
41150
41496
  const from = instance.sourceUrl !== void 0 ? `[the *ship* run](${instance.sourceUrl})` : "the *ship* run";
41151
41497
  return `\u21B3 *ship* unit ${row.unit}${row.title ? ` \u2014 ${row.title}` : ""} for ${who2}, from ${from}: \`${row.branch}\` in ${instance.repo}`;
41152
41498
  }
41499
+ function reviewLead(instance, row) {
41500
+ const who2 = instance.userName ?? instance.userId;
41501
+ const from = instance.sourceUrl !== void 0 ? `[the *ship* run](${instance.sourceUrl})` : "the *ship* run";
41502
+ const what = row.unit === TASK_UNIT ? "the task" : `unit ${row.unit}${row.title ? ` \u2014 ${row.title}` : ""}`;
41503
+ return `\u21B3 *ship* review of ${what} for ${who2}, from ${from}: \`${row.branch}\` in ${instance.repo}`;
41504
+ }
41153
41505
  async function branch(body, deps) {
41154
41506
  const id = parseInstanceId(body.parentInstanceId);
41155
41507
  if (!id.ok) return json(400, { ok: false, error: id.error });
@@ -41584,7 +41936,7 @@ var init_adminCoordinator = __esm({
41584
41936
  PRESETS_OF_KIND = {
41585
41937
  contract: "coding",
41586
41938
  review: "review",
41587
- fix: "coding"
41939
+ findings: "coding"
41588
41940
  };
41589
41941
  invalid = (error) => ({ ok: false, error });
41590
41942
  json = (status3, body) => ({ status: status3, body });