@coreplane/switchboard 1.261.0 → 1.261.2

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.2",
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.2",
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.2",
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.2",
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.2",
3
+ "commit": "652eb70199d24be067ee91e1c9e14f48770a594c",
4
+ "builtAt": "2026-09-24T02:09:02.047Z"
5
5
  }
@@ -293,6 +293,20 @@ export interface RunFinishedPayload {
293
293
  parentInstanceId: string;
294
294
  }
295
295
 
296
+ /** Durable authority for publishing another commit to an existing pull
297
+ * request. Every identity field is repeated deliberately: publication is
298
+ * allowed only when the unit, checkout and a fresh remote read all agree with
299
+ * this exact record. */
300
+ export interface ExistingPrPublicationBinding {
301
+ repo: string;
302
+ pr: number;
303
+ headRef: string;
304
+ baseRef: string;
305
+ expectedHeadSha: string;
306
+ publicationRef: string;
307
+ owner: { instanceId: string; unit: string };
308
+ }
309
+
296
310
  /** What a coordinator's spawn stamps on the child's every row: the instance
297
311
  * the child belongs to and the key the spawn carried — and, for the child's
298
312
  * own post-step, the base its pull request targets. */
@@ -310,6 +324,10 @@ export interface CoordinatorTag {
310
324
  * back off its ledger events; `coordinatorFields` still leaves it off the
311
325
  * rows, so rows and records keep the shape written before it existed. */
312
326
  base?: string;
327
+ /** The existing-PR publication authority this child received from its
328
+ * durable unit row and current Workflow round. Published on the run's event
329
+ * stream so a rehost retains the same fence. */
330
+ publication?: ExistingPrPublicationBinding;
313
331
  }
314
332
 
315
333
  /** The tag as the two flat record fields, or nothing — so a row, a summary
@@ -489,6 +507,9 @@ export interface CoordinatorUnit {
489
507
  * — no pre-check, no branch, no round 0. A task string's row only; written by
490
508
  * the hand-off, read by the driver into the machine's input. */
491
509
  resume?: { pr: number; headSha?: string; url?: string };
510
+ /** Exact existing-PR publication authority. Absent for a fresh unit branch;
511
+ * an adopted or resumed PR may publish only through this binding. */
512
+ publication?: ExistingPrPublicationBinding;
492
513
  /** The decision-record number reserved at admission for this unit. A unit
493
514
  * that writes a record carries it through every attempt and briefs its child
494
515
  * as `record: NNNN`; the child never scans the directory for a number. */
@@ -549,6 +570,23 @@ const isResume = (v: unknown): boolean =>
549
570
  isFinite(v.pr) &&
550
571
  (v.headSha === undefined || isText(v.headSha)) &&
551
572
  (v.url === undefined || isText(v.url, 2048));
573
+ const isFullSha = (v: unknown): v is string => typeof v === "string" && /^[0-9a-f]{40}$/i.test(v);
574
+ const isPublication = (v: unknown): v is ExistingPrPublicationBinding =>
575
+ isObject(v) &&
576
+ typeof v.repo === "string" &&
577
+ REPO_SLUG.test(v.repo) &&
578
+ typeof v.pr === "number" &&
579
+ Number.isInteger(v.pr) &&
580
+ v.pr > 0 &&
581
+ isText(v.headRef) &&
582
+ isText(v.baseRef) &&
583
+ isFullSha(v.expectedHeadSha) &&
584
+ isText(v.publicationRef) &&
585
+ isObject(v.owner) &&
586
+ typeof v.owner.instanceId === "string" &&
587
+ INSTANCE_ID_PATTERN.test(v.owner.instanceId) &&
588
+ typeof v.owner.unit === "string" &&
589
+ UNIT_PATTERN.test(v.owner.unit);
552
590
  const isThread = (v: unknown): boolean => isObject(v) && isText(v.threadKey) && isOptionalText(v.sourceUrl);
553
591
 
554
592
  /** Structural check on a record from outside the process (a Worker response, an HTTP body). */
@@ -663,6 +701,7 @@ export function isCoordinatorUnit(v: unknown): v is CoordinatorUnit {
663
701
  if (r.issue !== undefined && !isFinite(r.issue)) return false;
664
702
  if (r.pr !== undefined && !isPr(r.pr)) return false;
665
703
  if (r.resume !== undefined && !isResume(r.resume)) return false;
704
+ if (r.publication !== undefined && !isPublication(r.publication)) return false;
666
705
  if (r.record !== undefined && (typeof r.record !== "string" || !/^\d{4}$/.test(r.record))) return false;
667
706
  if (r.lastPush !== undefined && !isText(r.lastPush)) return false;
668
707
  if (r.segments !== undefined && (!Array.isArray(r.segments) || !r.segments.every(isSegment))) return false;
@@ -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
  /**
@@ -208,6 +208,11 @@ export type RunNoteKind =
208
208
  * after the model's last turn — the release itself runs after the record
209
209
  * is sealed — and set on the card's label too, so the loss is never silent. */
210
210
  | "work_left_behind"
211
+ /** An existing pull request's exact publication fence denied the push while
212
+ * the workspace held a local checkpoint. The workspace stays attached
213
+ * instead of being discarded or redirected; the summary names the head,
214
+ * counts, denial and that retention beyond the run remains unverified. */
215
+ | "publication_blocked"
211
216
  /** The run's ending may have left a command running in its workspace — a
212
217
  * call cut by the ending's abort or interrupt, or open when the run failed —
213
218
  * so the release tears the workspace down rather than pair it for the
@@ -356,6 +361,7 @@ export const RUN_NOTE_KINDS = [
356
361
  "ledger_untracked",
357
362
  "rebind_refused",
358
363
  "work_left_behind",
364
+ "publication_blocked",
359
365
  "workspace_torn_down",
360
366
  "pr_not_opened",
361
367
  "pr_head_unverified",
@@ -953,7 +959,15 @@ export type RunEvent =
953
959
  * instead of letting the binding ref — the unit branch itself — stand in.
954
960
  * `base` is absent when the spawn knew none; the post-step then falls to
955
961
  * the coordinator store's `instance.base`. Additive: unknown → ignored. */
956
- | { type: "coordinator_tag"; parentInstanceId: string; unit?: string; base?: string; seq?: number; at?: number }
962
+ | {
963
+ type: "coordinator_tag";
964
+ parentInstanceId: string;
965
+ unit?: string;
966
+ base?: string;
967
+ publication?: import("./coordinator/contract.js").ExistingPrPublicationBinding;
968
+ seq?: number;
969
+ at?: number;
970
+ }
957
971
  /** The plan runner instance a ship run's hand-off created (record 0051 R2;
958
972
  * docs/reference/specs/run-history.md item 2): published by the ship branch
959
973
  * after `handOffToCoordinator` succeeds, straight to the registry like
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { ChatMessage } from "../chatMessage.js";
7
7
  import { PROVIDER_FAILURE_CAUSES, type ProviderFailureCause, type ToolDef } from "../provider.js";
8
+ import type { Verbosity } from "../verbosity.js";
8
9
  import type { ChannelVisibility } from "../authz/types.js";
9
10
  import type { RunProfile } from "../../config/profile.js";
10
11
  import type { RunEvent } from "../runEvents.js";
@@ -60,6 +61,9 @@ export interface LiveRunMeta {
60
61
  /** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
61
62
  postedBy?: string;
62
63
  effort?: string;
64
+ /** The request's resolved display level. Hosted parents have no model loop
65
+ * to rebuild it from, and their input event deliberately omits directives. */
66
+ verbosity?: Verbosity;
63
67
  /** Decision-record reservation assigned before the attach, so a restart of
64
68
  * the admitted task keeps the same brief and process environment. */
65
69
  record?: string;