@coreplane/switchboard 1.261.0 → 1.261.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.
@@ -2963,7 +2963,7 @@ export class RunHistoryDO extends DurableObject<Env> {
2963
2963
  req.card ? JSON.stringify(req.card) : null,
2964
2964
  req.system,
2965
2965
  JSON.stringify(req.tools),
2966
- JSON.stringify(req.state ?? {}),
2966
+ JSON.stringify({ ...existing!.state, ...(req.state ?? {}) }),
2967
2967
  req.runId,
2968
2968
  );
2969
2969
  this.registerSession(req);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.261.0",
3
+ "version": "1.261.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.261.0",
9
+ "version": "1.261.1",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -21060,7 +21060,7 @@
21060
21060
  },
21061
21061
  "packages/switchboard": {
21062
21062
  "name": "@coreplane/switchboard",
21063
- "version": "1.261.0",
21063
+ "version": "1.261.1",
21064
21064
  "license": "Apache-2.0",
21065
21065
  "dependencies": {
21066
21066
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.261.0",
3
+ "version": "1.261.1",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.261.0",
3
- "commit": "c60b26943f774d0ba0e71af0df8db7d1d90613c5",
4
- "builtAt": "2026-09-23T08:28:35.014Z"
2
+ "version": "1.261.1",
3
+ "commit": "3746a1c5fa2d30bebd66baec1fb3534a1f927e3a",
4
+ "builtAt": "2026-09-23T21:48:10.555Z"
5
5
  }
@@ -510,8 +510,9 @@ function prCheckReturn(step: string, a: BotAnswer): Extract<StepReturn, { type:
510
510
  },
511
511
  at,
512
512
  };
513
- // A merged pull request is read whole or not at all: the merge commit and
514
- // the time are what the unit's ending and its report carry.
513
+ // A merged pull request is read whole or not at all: its source head is
514
+ // retained for findings reconciliation, while the merge commit and time
515
+ // are what the unit's ending and report carry.
515
516
  if (
516
517
  ok === true &&
517
518
  state === "merged" &&
@@ -527,6 +528,7 @@ function prCheckReturn(step: string, a: BotAnswer): Extract<StepReturn, { type:
527
528
  state: "merged",
528
529
  prNumber,
529
530
  url,
531
+ ...(typeof headSha === "string" ? { headSha } : {}),
530
532
  sha,
531
533
  mergedAt,
532
534
  ...(typeof mergedBy === "string" ? { mergedBy } : {}),
@@ -923,9 +925,19 @@ async function tellStepThrew(
923
925
  : at.round.kind === "review" && /\/review\/(read|checks)\b/.test(at.step)
924
926
  ? `after round ${at.round.index}'s review verdict (\`${at.step}\`)`
925
927
  : `in round ${at.round.index} (\`${at.step}\`)`;
926
- const report =
927
- `⚠️ The runner failed ${where}: ${line}\n\n` +
928
- "The unit remains bound to this thread; the next reply continues it, and a pull request already approved with green checks resumes at the checks step, never at a fresh coding round.";
928
+ const findingsReconcile = at.round?.kind === "findings" && /\/findings(?:\/a\d+)?\/pr-check$/.test(at.step);
929
+ const report = findingsReconcile
930
+ ? [
931
+ `⚠️ Review did not restart because Switchboard could not read the pull request after the completed findings work: ${line}`,
932
+ pr !== undefined
933
+ ? `Saved-work fact: the existing pull request is ${pr.url}, but its current branch and exact head were not verified.`
934
+ : "Saved-work fact: no open pull request or exact remote head was verified.",
935
+ "No review was started. Next action: retry ship when the pull request is readable so Switchboard can verify the exact head first.",
936
+ ].join("\n\n")
937
+ : [
938
+ `⚠️ Switchboard failed ${where}: ${line}`,
939
+ "The unit remains resumable from its recorded branch and pull request facts. No continuation action was scheduled. Next action: start ship again after the failing operation is available.",
940
+ ].join("\n\n");
929
941
  const body = {
930
942
  ...tag,
931
943
  ending: {
@@ -150,6 +150,10 @@ export interface ReviewVerdict {
150
150
  export const LGTM_TOKEN = "LGTM:";
151
151
  export const CHANGES_TOKEN = "Changes requested:";
152
152
  export const NO_VERDICT_LINE = "No verdict submitted — not approving.";
153
+ /** Conservative ceiling below GitHub's 65,536-character review-body limit.
154
+ * Counted as Unicode code points so clipping never splits a surrogate pair. */
155
+ export const MAX_REVIEW_POST_CODE_POINTS = 65_000;
156
+ const REVIEW_TRUNCATION_NOTE = "_(review truncated to fit GitHub's review size limit)_";
153
157
 
154
158
  /** Parse an arbitrary tool input into a verdict, or null when it is not one.
155
159
  * `addressSeverity` is the level in force for the run (default `minor`): an
@@ -383,7 +387,11 @@ function verdictMarker(verdict: ReviewVerdict | undefined, target: ReviewBodyTar
383
387
  * `approve`), a GitHub alert callout with the verdict word, the pinned head
384
388
  * and the finding counts, the findings as a table (severity, id + title, the
385
389
  * file linked at the head), the model's text folded under `Full review`, and
386
- * the machine-readable marker last. The prose decides nothing above the fold.
390
+ * the machine-readable marker last. Only the prose may be clipped to fit the
391
+ * platform: every fixed verdict section and the marker remain byte-for-byte.
392
+ * If those sections leave no room for a visible bounded review, the oversized
393
+ * body is returned so the publication boundary can refuse it instead of
394
+ * silently dropping the write-up. The prose decides nothing above the fold.
387
395
  */
388
396
  export function buildReviewPostBody(
389
397
  answer: string,
@@ -395,10 +403,10 @@ export function buildReviewPostBody(
395
403
  ...(target ? [`head \`${target.head.slice(0, 7)}\``] : []),
396
404
  verdict ? findingCounts(verdict.findings) : "the run ended without a submit_verdict call",
397
405
  ];
398
- const parts: string[] = [verdictLine(verdict), `> [!${calloutKind(verdict)}]\n> ${facts.join(" · ")}`];
406
+ const fixedParts: string[] = [verdictLine(verdict), `> [!${calloutKind(verdict)}]\n> ${facts.join(" · ")}`];
399
407
  const findings = verdict?.findings ?? [];
400
408
  if (findings.length > 0) {
401
- parts.push(
409
+ fixedParts.push(
402
410
  [
403
411
  "| Severity | Finding | Where |",
404
412
  "| --- | --- | --- |",
@@ -409,10 +417,29 @@ export function buildReviewPostBody(
409
417
  ].join("\n"),
410
418
  );
411
419
  }
420
+
421
+ const marker = verdictMarker(verdict, target);
422
+ const fixedBody = [...fixedParts, marker].join("\n\n");
412
423
  const prose = answer.trim();
413
- if (prose) parts.push(`<details>\n<summary>Full review</summary>\n\n${prose}\n\n</details>`);
414
- parts.push(verdictMarker(verdict, target));
415
- return parts.join("\n\n");
424
+ if (!prose) return fixedBody;
425
+
426
+ const details = (text: string, truncated: boolean): string =>
427
+ `<details>\n<summary>Full review</summary>\n\n${text}${truncated ? `${text ? "\n\n" : ""}${REVIEW_TRUNCATION_NOTE}` : ""}\n\n</details>`;
428
+ const render = (text: string, truncated: boolean): string =>
429
+ [...fixedParts, details(text, truncated), marker].join("\n\n");
430
+ const full = render(prose, false);
431
+ if ([...full].length <= MAX_REVIEW_POST_CODE_POINTS) return full;
432
+
433
+ const clippedShell = render("", true);
434
+ const proseSeparatorCodePoints = 2; // the blank line before the note when clipped prose is present
435
+ const proseBudget = MAX_REVIEW_POST_CODE_POINTS - [...clippedShell].length - proseSeparatorCodePoints;
436
+ if (proseBudget > 0) return render([...prose].slice(0, proseBudget).join(""), true);
437
+ if ([...clippedShell].length <= MAX_REVIEW_POST_CODE_POINTS) return clippedShell;
438
+
439
+ // A successful post must retain a visible write-up. Return the full body
440
+ // oversized when even the bounded shell cannot fit, so postReviewComment
441
+ // rejects before fetch and the review remains on the Slack-only path.
442
+ return full;
416
443
  }
417
444
 
418
445
  /**