@coreplane/switchboard 1.260.5 → 1.260.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/assets/deploy/cloudflare-memory/worker.ts +17 -3
  2. package/dist/assets/package-lock.json +3 -3
  3. package/dist/assets/package.json +1 -1
  4. package/dist/assets/source.json +3 -3
  5. package/dist/assets/src/core/budgets.ts +5 -0
  6. package/dist/assets/src/core/coordinator/driver.ts +43 -6
  7. package/dist/assets/src/core/refusal.ts +1 -1
  8. package/dist/assets/src/core/runLedger/decisions.ts +10 -1
  9. package/dist/assets/src/core/runLedger/types.ts +3 -0
  10. package/dist/assets/src/core/ship/coordinator.ts +29 -6
  11. package/dist/assets/web/dist/.vite/manifest.json +67 -67
  12. package/dist/assets/web/dist/assets/{DeliveryPage-Cf1joUyA.js → DeliveryPage-BUJckSuW.js} +1 -1
  13. package/dist/assets/web/dist/assets/{HomePage-DZEAnCCv.js → HomePage-CBQpcnaW.js} +1 -1
  14. package/dist/assets/web/dist/assets/{PendingTurnRow-4B8JwETU.js → PendingTurnRow-DIC86uWA.js} +1 -1
  15. package/dist/assets/web/dist/assets/{PlanePage-CHU2szQs.js → PlanePage-CfrbMh8t.js} +1 -1
  16. package/dist/assets/web/dist/assets/{ResidentDetailPage-L5SRcN_O.js → ResidentDetailPage-C-SgOUTl.js} +1 -1
  17. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BIFf8qyE.js → ResidentsIndexPage-CiDhvXok.js} +1 -1
  18. package/dist/assets/web/dist/assets/{RunFoldRow-DzPIshJx.js → RunFoldRow-DZ8huwUU.js} +1 -1
  19. package/dist/assets/web/dist/assets/{RunRoutePage-Cru5IVux.js → RunRoutePage-Cp6wicYc.js} +3 -3
  20. package/dist/assets/web/dist/assets/{RunsIndexPage-CBkdPAdj.js → RunsIndexPage-BPXtfwwx.js} +1 -1
  21. package/dist/assets/web/dist/assets/{ScheduledPage-D_R55BDk.js → ScheduledPage-DbvP48HI.js} +1 -1
  22. package/dist/assets/web/dist/assets/{SettingsPage-DlmZvAvf.js → SettingsPage-B87lZaiN.js} +1 -1
  23. package/dist/assets/web/dist/assets/{SilentTurn-PWMDMbMG.js → SilentTurn-CPED_XCV.js} +1 -1
  24. package/dist/assets/web/dist/assets/{StatusDot-Bq0mYMFo.js → StatusDot-CFLn-DZ2.js} +1 -1
  25. package/dist/assets/web/dist/assets/{Tooltip-Bs6zd7yE.js → Tooltip-Dx9YBVEx.js} +1 -1
  26. package/dist/assets/web/dist/assets/{UnitRoutePage-BMGEZCvn.js → UnitRoutePage-d4A2X_o7.js} +1 -1
  27. package/dist/assets/web/dist/assets/budgets-BQgcCyvb.js +1 -0
  28. package/dist/assets/web/dist/assets/{dist-EkKVbIM-.js → dist-Cn51vWpw.js} +1 -1
  29. package/dist/assets/web/dist/assets/{indexRow-5s3hNzZ9.js → indexRow-BsWcHHVh.js} +1 -1
  30. package/dist/assets/web/dist/assets/{main-Disot0n_.js → main-DYwRHh0F.js} +2 -2
  31. package/dist/assets/web/dist/assets/{sseReplay-9gcMpylx.js → sseReplay-DGRDRLua.js} +1 -1
  32. package/dist/cli.js +728 -556
  33. package/package.json +1 -1
  34. package/dist/assets/web/dist/assets/budgets-0szUBIMv.js +0 -1
@@ -76,6 +76,7 @@ import {
76
76
  phaseTransition,
77
77
  reclaimPhase,
78
78
  selectReclaim,
79
+ unreadInbox,
79
80
  } from "../../src/core/runLedger/decisions.ts";
80
81
  import { INTAKE_DELIVERY_CLAIM_MS, intakeReceiptRetentionMs, minutesToMs, PLANE } from "../../src/core/budgets.ts";
81
82
  import { PROVIDER_FAILURE_CAUSES, type ProviderFailureCause } from "../../src/core/provider.ts";
@@ -3256,15 +3257,22 @@ export class RunHistoryDO extends DurableObject<Env> {
3256
3257
  .exec<{ json: string }>(`SELECT json FROM run_steps WHERE run_id = ? ORDER BY step DESC LIMIT 1`, row.runId)
3257
3258
  .toArray()[0];
3258
3259
  const lastStep = stepRow ? (JSON.parse(stepRow.json) as StepRecord) : null;
3259
- const consumed = lastStep?.inboxConsumedSeq ?? 0;
3260
+ // Past the cursor, plus the deferred rows at or below it: the lowest
3261
+ // deferred seq bounds the read, and the record's predicate filters.
3262
+ const unread = unreadInbox(lastStep);
3263
+ const from = Math.min(
3264
+ lastStep?.inboxConsumedSeq ?? 0,
3265
+ ...(lastStep?.inboxDeferredSeqs ?? []).map((s) => s - 1),
3266
+ );
3260
3267
  const inbox = this.sql
3261
3268
  .exec<{ seq: number; json: string }>(
3262
3269
  `SELECT seq, json FROM run_inbox WHERE run_id = ? AND seq > ? ORDER BY seq ASC`,
3263
3270
  row.runId,
3264
- consumed,
3271
+ from,
3265
3272
  )
3266
3273
  .toArray()
3267
- .map((r) => ({ seq: r.seq, message: JSON.parse(r.json) as Record<string, unknown> }));
3274
+ .map((r) => ({ seq: r.seq, message: JSON.parse(r.json) as Record<string, unknown> }))
3275
+ .filter(unread);
3268
3276
  const jobs = this.sql
3269
3277
  .exec<{ kind: string; json: string }>(`SELECT kind, json FROM run_jobs WHERE run_id = ?`, row.runId)
3270
3278
  .toArray()
@@ -5484,6 +5492,12 @@ function parseStep(v: unknown): Validated<StepRecord> {
5484
5492
  return invalid(`record.${k} must be a non-negative integer`);
5485
5493
  }
5486
5494
  }
5495
+ if (
5496
+ s.inboxDeferredSeqs !== undefined &&
5497
+ (!Array.isArray(s.inboxDeferredSeqs) ||
5498
+ !s.inboxDeferredSeqs.every((seq) => typeof seq === "number" && Number.isInteger(seq) && seq > 0))
5499
+ )
5500
+ return invalid("record.inboxDeferredSeqs must be an array of positive integers");
5487
5501
  if (!Array.isArray(s.inFlight)) return invalid("record.inFlight must be an array");
5488
5502
  for (const c of s.inFlight) {
5489
5503
  const call = c as Record<string, unknown>;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.260.5",
3
+ "version": "1.260.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.260.5",
9
+ "version": "1.260.6",
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.260.5",
21063
+ "version": "1.260.6",
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.260.5",
3
+ "version": "1.260.6",
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.260.5",
3
- "commit": "3b665eb6f1bb239a78981014dae4bc88a50264a7",
4
- "builtAt": "2026-09-22T23:49:01.800Z"
2
+ "version": "1.260.6",
3
+ "commit": "903ae2855833637fbcc0c0a22d54e7268be75965",
4
+ "builtAt": "2026-09-23T04:57:07.101Z"
5
5
  }
@@ -254,6 +254,11 @@ export const HOSTED_DEADLINE_MARGIN_MINUTES = 60;
254
254
  * and the pause before a busy spawn is asked again. */
255
255
  export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes: 5, busyRetryMinutes: 2 } as const;
256
256
 
257
+ /** A finish event and the run record's durable write are separate deliveries.
258
+ * After the runner receives its own event, it gives the record this short
259
+ * visibility bound before treating an opaque absence as permanent. */
260
+ export const SHIP_RECORD_VISIBILITY = { retries: 3, retryMs: secondsToMs(1) } as const;
261
+
257
262
  /** How long a restarting close gives its replacement dispatch to claim the
258
263
  * same run before a read with no successor treats the close as final
259
264
  * (run-history item 47a). One whole coordinator wait chunk lets an ordinary
@@ -39,7 +39,15 @@
39
39
  // plan's re-issue. A task string's ship branch waits for a person. Node-free:
40
40
  // the shim Worker imports this by relative path.
41
41
 
42
- import { DAY_MS, DEFAULT_GRANT, GRANT_RENEWALS_MAX, IDLE_DAYS_MAX, type Grant, type GrantSource } from "../budgets.js";
42
+ import {
43
+ DAY_MS,
44
+ DEFAULT_GRANT,
45
+ GRANT_RENEWALS_MAX,
46
+ IDLE_DAYS_MAX,
47
+ SHIP_RECORD_VISIBILITY,
48
+ type Grant,
49
+ type GrantSource,
50
+ } from "../budgets.js";
43
51
  import { isFindingShape, type Finding } from "../reviewVerdict.js";
44
52
  import { DEFAULT_VERBOSITY, isVerbosity, type Verbosity } from "../verbosity.js";
45
53
  import {
@@ -640,15 +648,47 @@ async function call(
640
648
  bot: CoordinatorBot,
641
649
  route: CoordinatorStepRoute,
642
650
  body: Record<string, unknown>,
651
+ acceptOpaqueNotFound = false,
643
652
  ): Promise<BotReply> {
644
653
  const reply = await bot.step(route, body);
645
654
  const read = readBotAnswer(reply.status, reply.text);
646
- if (!read.ok) throw new Error(`the bot did not answer ${route}: ${read.reason}`);
655
+ if (!read.ok) {
656
+ if (acceptOpaqueNotFound && isOpaqueNotFound(reply)) return reply;
657
+ throw new Error(`the bot did not answer ${route}: ${read.reason}`);
658
+ }
647
659
  const transient = transientRefusal(read.answer);
648
660
  if (transient !== undefined) throw new Error(transient);
649
661
  return reply;
650
662
  }
651
663
 
664
+ /** The read door's authorization mask: absent and denied are deliberately the
665
+ * same reply. The runner may pace that reply only from its own addressed
666
+ * finish event; parsing it here never changes what another caller sees. */
667
+ function isOpaqueNotFound(reply: BotReply): boolean {
668
+ if (reply.status !== 404) return false;
669
+ try {
670
+ const body = JSON.parse(reply.text) as { ok?: unknown; error?: unknown };
671
+ return body?.ok === false && body.error === "not_found";
672
+ } catch {
673
+ return false;
674
+ }
675
+ }
676
+
677
+ async function readRecordStep(
678
+ step: StepRunner,
679
+ bot: CoordinatorBot,
680
+ action: Extract<CoordinatorAction, { type: "read-record" }>,
681
+ body: Record<string, unknown>,
682
+ ): Promise<BotReply> {
683
+ for (let retry = 0; ; retry++) {
684
+ const name = retry === 0 ? action.step : `${action.step}/record-read/${retry}`;
685
+ const reply = await step.do(name, STEP_CONFIG, () => call(bot, "read-record", body, true));
686
+ if (!isOpaqueNotFound(reply) || action.finishedObserved !== true || retry >= SHIP_RECORD_VISIBILITY.retries)
687
+ return reply;
688
+ await step.sleep(`${action.step}/record-visible/${retry + 1}`, SHIP_RECORD_VISIBILITY.retryMs);
689
+ }
690
+ }
691
+
652
692
  /** The answer a stored reply carries — the reply passed `call` once, so it reads the same on replay. */
653
693
  function answerOf(route: CoordinatorStepRoute, reply: BotReply): BotAnswer {
654
694
  const read = readBotAnswer(reply.status, reply.text);
@@ -745,10 +785,7 @@ async function perform(
745
785
  case "read-record":
746
786
  return readRecordReturn(
747
787
  action.step,
748
- answerOf(
749
- "read-record",
750
- await step.do(action.step, STEP_CONFIG, () => call(bot, "read-record", { ...tag, runId: action.runId })),
751
- ),
788
+ answerOf("read-record", await readRecordStep(step, bot, action, { ...tag, runId: action.runId })),
752
789
  );
753
790
  case "steer":
754
791
  return steerReturn(
@@ -47,7 +47,7 @@ const CAUSE_OF = {
47
47
  repo_not_onboarded: "request",
48
48
  repo_access: "policy",
49
49
  pr_head_unknown: "system",
50
- branch_moved: "system",
50
+ workspace_head_mismatch: "system",
51
51
  coordinator_thread_live: "system",
52
52
  live_agent_allowlist: "policy",
53
53
  follow_up_refused: "request",
@@ -2,7 +2,7 @@
2
2
  // Durable Object applies them inside one transaction; the in-memory ledger
3
3
  // applies them in tests; both agree because this is the only copy.
4
4
 
5
- import type { ClaimResult, FenceResult, IntakeReceipt, IntakeWriteResult, LivePhase } from "./types.js";
5
+ import type { ClaimResult, FenceResult, IntakeReceipt, IntakeWriteResult, LivePhase, StepRecord } from "./types.js";
6
6
 
7
7
  /** One live run per thread. The existing row, if any, is what `live_runs` holds
8
8
  * for the thread; the same run re-claimed by its owner is idempotent (a retry
@@ -80,6 +80,15 @@ export function selectReclaim<T extends { leaseUntil: number; phase: LivePhase;
80
80
  return rows.filter((r) => r.ownerGen !== gen && (r.phase === "handoff" || r.leaseUntil <= now));
81
81
  }
82
82
 
83
+ /** Which inbox rows a reclaim hands the next generation (run-history item 40):
84
+ * every row past the last record's cursor, and the rows at or below it the
85
+ * record names deferred — handed to the run, never read by its model. */
86
+ export function unreadInbox(lastStep: Pick<StepRecord, "inboxConsumedSeq" | "inboxDeferredSeqs"> | null) {
87
+ const consumed = lastStep?.inboxConsumedSeq ?? 0;
88
+ const deferred = new Set(lastStep?.inboxDeferredSeqs ?? []);
89
+ return (item: { seq: number }): boolean => item.seq > consumed || deferred.has(item.seq);
90
+ }
91
+
83
92
  /** The compare-and-swap table for a run's phase. `attaching → live` (the
84
93
  * prompt landed) or `→ finishing` (the dispatch failed before it — never
85
94
  * handoff: an attaching run has nothing to resume from, so the drain waits
@@ -179,6 +179,9 @@ export interface StepRecord {
179
179
  inFlight: InFlightCall[];
180
180
  /** The inbox `seq` the run has consumed up to (0 = none). */
181
181
  inboxConsumedSeq: number;
182
+ /** Seqs at or below `inboxConsumedSeq` the run has not consumed yet: a
183
+ * reclaim offers them again with every row past the cursor. */
184
+ inboxDeferredSeqs?: number[];
182
185
  remainingMs: number;
183
186
  turn: number;
184
187
  iteration: number;
@@ -550,7 +550,7 @@ export type CoordinatorAction =
550
550
  | { type: "branch"; step: string; branch: string; from: string }
551
551
  | { type: "spawn"; step: string; preset: ChildPreset; round: RoundRef; budgetMinutes: number; brief: Brief }
552
552
  | { type: "wait"; step: string; runId: string; timeoutMs: number }
553
- | { type: "read-record"; step: string; runId: string }
553
+ | { type: "read-record"; step: string; runId: string; finishedObserved?: true }
554
554
  /** Tell a child whose pull request became terminal or moved to end at its
555
555
  * next turn boundary without publishing anything. This is a steer through
556
556
  * the child's inbox, never a stop that tears down its workspace. */
@@ -1170,7 +1170,7 @@ type Phase =
1170
1170
  | { at: "busy-wait"; round: RoundRef; runId?: string; n: number }
1171
1171
  /** `until`: when the child's budget plus the margin runs out, counted from the spawn's answer — the wait's last slice ends there. */
1172
1172
  | { at: "wait"; round: RoundRef; runId: string; n: number; until: number }
1173
- | { at: "read"; round: RoundRef; runId: string; n: number; until: number }
1173
+ | { at: "read"; round: RoundRef; runId: string; n: number; until: number; finishedObserved: boolean }
1174
1174
  /** A live child lost authority because its pull request became terminal,
1175
1175
  * its reviewed head moved, or its branch disappeared. Steer once, then
1176
1176
  * drain the child to its recorded end while ignoring every late artifact. */
@@ -1197,6 +1197,7 @@ type Phase =
1197
1197
  n: number;
1198
1198
  until: number;
1199
1199
  supersession: ChildSupersession;
1200
+ finishedObserved: boolean;
1200
1201
  }
1201
1202
  /** A moved-head child has drained. Re-read the adopted pull request before
1202
1203
  * restarting review because it may have merged, closed or moved again. */
@@ -1533,7 +1534,12 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
1533
1534
  timeoutMs: Math.min(waitSliceMs(s.clock, p.until), s.pr !== undefined ? PR_TRANSITION_GUARD_MS : WAIT_CHUNK_MS),
1534
1535
  };
1535
1536
  case "read":
1536
- return { type: "read-record", step: `${roundStep(s, p.round)}/read/${p.n}`, runId: p.runId };
1537
+ return {
1538
+ type: "read-record",
1539
+ step: `${roundStep(s, p.round)}/read/${p.n}`,
1540
+ runId: p.runId,
1541
+ ...(p.finishedObserved ? { finishedObserved: true as const } : {}),
1542
+ };
1537
1543
  case "steer":
1538
1544
  return {
1539
1545
  type: "steer",
@@ -1553,6 +1559,7 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
1553
1559
  type: "read-record",
1554
1560
  step: `${roundStep(s, p.round)}/superseded/read/${p.n}`,
1555
1561
  runId: p.runId,
1562
+ ...(p.finishedObserved ? { finishedObserved: true as const } : {}),
1556
1563
  };
1557
1564
  case "superseded-pr-check":
1558
1565
  return {
@@ -2960,11 +2967,23 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
2960
2967
  notes: [],
2961
2968
  };
2962
2969
  }
2963
- case "wait":
2970
+ case "wait": {
2971
+ const r = ret as Extract<StepReturn, { type: "wait" }>;
2964
2972
  return {
2965
- state: { ...s, phase: { at: "read", round: p.round, runId: p.runId, n: p.n, until: p.until } },
2973
+ state: {
2974
+ ...s,
2975
+ phase: {
2976
+ at: "read",
2977
+ round: p.round,
2978
+ runId: p.runId,
2979
+ n: p.n,
2980
+ until: p.until,
2981
+ finishedObserved: r.outcome === "event",
2982
+ },
2983
+ },
2966
2984
  notes: [],
2967
2985
  };
2986
+ }
2968
2987
  case "read": {
2969
2988
  const r = ret as Extract<StepReturn, { type: "read-record" }>;
2970
2989
  const supersession = childSupersession(clocked, p.round, r.pullRequest);
@@ -2979,6 +2998,7 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
2979
2998
  n: p.n,
2980
2999
  until: p.until,
2981
3000
  supersession,
3001
+ finishedObserved: p.finishedObserved,
2982
3002
  };
2983
3003
  return finishSupersededChild(clocked, terminalPhase);
2984
3004
  }
@@ -3098,7 +3118,8 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
3098
3118
  notes: [],
3099
3119
  };
3100
3120
  }
3101
- case "superseded-wait":
3121
+ case "superseded-wait": {
3122
+ const r = ret as Extract<StepReturn, { type: "wait" }>;
3102
3123
  return {
3103
3124
  state: {
3104
3125
  ...s,
@@ -3109,10 +3130,12 @@ export function applyReturn(s: UnitPipelineState, ret: StepReturn): Transition {
3109
3130
  n: p.n,
3110
3131
  until: p.until,
3111
3132
  supersession: p.supersession,
3133
+ finishedObserved: r.outcome === "event",
3112
3134
  },
3113
3135
  },
3114
3136
  notes: [],
3115
3137
  };
3138
+ }
3116
3139
  case "superseded-read": {
3117
3140
  const r = ret as Extract<StepReturn, { type: "read-record" }>;
3118
3141
  if (r.run.finished) return finishSupersededChild(clocked, p);