@coreplane/switchboard 1.243.0 → 1.244.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/dist/assets/deploy/cloudflare-memory/worker.ts +148 -0
  2. package/dist/assets/deploy/cloudflare-resident/gc.ts +8 -1
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +433 -117
  4. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +13 -2
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +1 -1
  9. package/dist/assets/src/core/authz/types.ts +10 -0
  10. package/dist/assets/src/core/budgets.ts +36 -0
  11. package/dist/assets/src/core/coordinator/contract.ts +32 -0
  12. package/dist/assets/src/core/coordinator/driver.ts +75 -12
  13. package/dist/assets/src/core/delivery.ts +1 -1
  14. package/dist/assets/src/core/runEvents.ts +50 -7
  15. package/dist/assets/src/core/runFriction.ts +2 -1
  16. package/dist/assets/src/core/runRecord.ts +31 -0
  17. package/dist/assets/src/core/ship/contract.ts +2 -1
  18. package/dist/assets/src/core/ship/coordinator.ts +211 -13
  19. package/dist/assets/src/core/ship/renewal.ts +155 -0
  20. package/dist/assets/src/core/trace/attrs.ts +11 -0
  21. package/dist/assets/src/core/trace/types.ts +4 -3
  22. package/dist/assets/src/execution/residentRefresh.ts +24 -4
  23. package/dist/assets/src/execution/residentText.ts +8 -3
  24. package/dist/assets/src/mcp/registry.ts +3 -0
  25. package/dist/assets/web/dist/.vite/manifest.json +417 -417
  26. package/dist/assets/web/dist/assets/{AppShell-IDMGG6yi.js → AppShell-Ck4hSZDi.js} +1 -1
  27. package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +2 -0
  28. package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +1 -0
  29. package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +2 -0
  30. package/dist/assets/web/dist/assets/{NotFoundPage-C97EhJcQ.js → NotFoundPage-CiGrVOyv.js} +1 -1
  31. package/dist/assets/web/dist/assets/{PendingTurnRow-BT9RhFZ7.js → PendingTurnRow-BHCSpsPf.js} +1 -1
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-DACalNVF.js → ResidentDetailPage-C5USV_8P.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-SHPdu6uZ.js → ResidentsIndexPage-CSKisWnX.js} +1 -1
  34. package/dist/assets/web/dist/assets/RunFoldRow-BPMQxTcl.js +1 -0
  35. package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +9 -0
  36. package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +1 -0
  37. package/dist/assets/web/dist/assets/{RunsTabs-BUfdk0lH.js → RunsTabs-BWRHzhtT.js} +1 -1
  38. package/dist/assets/web/dist/assets/{ScheduledPage-DJ8HiCPt.js → ScheduledPage-DBaChcJZ.js} +1 -1
  39. package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +1 -0
  40. package/dist/assets/web/dist/assets/{StatusDot-Dw0T1M-P.js → StatusDot-_Q_bmReh.js} +1 -1
  41. package/dist/assets/web/dist/assets/{Tooltip-BbLuIAiS.js → Tooltip-Cafsgd4m.js} +1 -1
  42. package/dist/assets/web/dist/assets/{UnitRoutePage-DicUG96U.js → UnitRoutePage-CdoBZwtf.js} +1 -1
  43. package/dist/assets/web/dist/assets/{angular-html-oBNfPJR0.js → angular-html-Dail07rr.js} +1 -1
  44. package/dist/assets/web/dist/assets/{angular-ts-BvNwsyWA.js → angular-ts-BReExio8.js} +1 -1
  45. package/dist/assets/web/dist/assets/{apl-CNUdRlYf.js → apl-DhNO6Yyx.js} +1 -1
  46. package/dist/assets/web/dist/assets/{astro-Zb0NriSe.js → astro-BYotTY4C.js} +1 -1
  47. package/dist/assets/web/dist/assets/{blade-qPRVheqq.js → blade-mUxTtD1X.js} +1 -1
  48. package/dist/assets/web/dist/assets/{c-D8Awx4YO.js → c-DhuiwcSQ.js} +1 -1
  49. package/dist/assets/web/dist/assets/{chapel-Bt72Mhsx.js → chapel-GD2vFtRi.js} +1 -1
  50. package/dist/assets/web/dist/assets/{cobol-BOBacexg.js → cobol-BhFDwkes.js} +1 -1
  51. package/dist/assets/web/dist/assets/{coffee-E4u0liHW.js → coffee-DWUgbE0r.js} +1 -1
  52. package/dist/assets/web/dist/assets/{cpp-q2sLNlul.js → cpp-CO9XFjGW.js} +1 -1
  53. package/dist/assets/web/dist/assets/{crystal-DyWqUnlb.js → crystal-B9pVurpn.js} +1 -1
  54. package/dist/assets/web/dist/assets/{css-CQY0hFsD.js → css-CFVVV6lI.js} +1 -1
  55. package/dist/assets/web/dist/assets/{dist-twkFmSUY.js → dist-BtPs2AkQ.js} +2 -2
  56. package/dist/assets/web/dist/assets/{edge-C1MwhJkX.js → edge-BDhIDQIb.js} +1 -1
  57. package/dist/assets/web/dist/assets/{elixir-Bb3YbHfn.js → elixir-DG3J8WoD.js} +1 -1
  58. package/dist/assets/web/dist/assets/{elm-DAN9IGQw.js → elm-FiOdiaOE.js} +1 -1
  59. package/dist/assets/web/dist/assets/{erb-BEB8Xlsj.js → erb-vxbtW503.js} +1 -1
  60. package/dist/assets/web/dist/assets/{git-rebase-tqpjRfxO.js → git-rebase-DINz8M3H.js} +1 -1
  61. package/dist/assets/web/dist/assets/{glimmer-js-Ccbo65zR.js → glimmer-js-yann6r9i.js} +1 -1
  62. package/dist/assets/web/dist/assets/{glimmer-ts-B6WBVMpE.js → glimmer-ts-CXTbrZn_.js} +1 -1
  63. package/dist/assets/web/dist/assets/{glsl-tRec3Fcu.js → glsl-DNYC-hq0.js} +1 -1
  64. package/dist/assets/web/dist/assets/{graphql-P8kbxT4F.js → graphql-CwzhHzi9.js} +1 -1
  65. package/dist/assets/web/dist/assets/{hack-H9Zhkagy.js → hack-BBXF1cwR.js} +1 -1
  66. package/dist/assets/web/dist/assets/{haml-DtEnpn7Z.js → haml-oEqBcsTy.js} +1 -1
  67. package/dist/assets/web/dist/assets/{handlebars-DkgPfoAz.js → handlebars-YE5thkT1.js} +1 -1
  68. package/dist/assets/web/dist/assets/{html-D30RXpIs.js → html-Dp4jRQ1j.js} +1 -1
  69. package/dist/assets/web/dist/assets/{html-derivative-DwozLrEx.js → html-derivative-DCT1GYtQ.js} +1 -1
  70. package/dist/assets/web/dist/assets/{http-DXuzBAPm.js → http-BPcGbmzJ.js} +1 -1
  71. package/dist/assets/web/dist/assets/{hurl-d1UUJIt_.js → hurl-CQnNHwyd.js} +1 -1
  72. package/dist/assets/web/dist/assets/{indexRow-B_s5tKyq.js → indexRow-BmK74Vp1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{java-DL0gWf34.js → java-Dm8Ovj6W.js} +1 -1
  74. package/dist/assets/web/dist/assets/{javascript-Cl7vavnS.js → javascript-Btm9oJK9.js} +1 -1
  75. package/dist/assets/web/dist/assets/{jinja-CfQOWMX9.js → jinja-VZYjWF_U.js} +1 -1
  76. package/dist/assets/web/dist/assets/{jison-KdYirlqm.js → jison-Dcmjxa9x.js} +1 -1
  77. package/dist/assets/web/dist/assets/{json-C9cDQ-Qj.js → json-DoWiOGZ8.js} +1 -1
  78. package/dist/assets/web/dist/assets/{jsx-CRx5NItd.js → jsx-Dvmo55ma.js} +1 -1
  79. package/dist/assets/web/dist/assets/{julia-CmsQsZQl.js → julia-d8AtfV26.js} +1 -1
  80. package/dist/assets/web/dist/assets/{just-CsM3Q8TE.js → just-Cc5EStA-.js} +1 -1
  81. package/dist/assets/web/dist/assets/{latex-BXCh5YRX.js → latex-B5fKKIH7.js} +1 -1
  82. package/dist/assets/web/dist/assets/{liquid-BF2vwK8p.js → liquid-DUYI9JND.js} +1 -1
  83. package/dist/assets/web/dist/assets/{lua-DcMBATrl.js → lua-BhMwKWpw.js} +1 -1
  84. package/dist/assets/web/dist/assets/main-C4366PZF.css +1 -0
  85. package/dist/assets/web/dist/assets/main-DlTNAC6c.js +28 -0
  86. package/dist/assets/web/dist/assets/{marko-88MndKvG.js → marko-Cnf2cPAi.js} +1 -1
  87. package/dist/assets/web/dist/assets/{mdc-DJ4kVAd8.js → mdc-FDDXoir9.js} +1 -1
  88. package/dist/assets/web/dist/assets/{nginx-CP6mRgtV.js → nginx-q-8U_1jR.js} +1 -1
  89. package/dist/assets/web/dist/assets/{nim-QQfj3fpF.js → nim-DIA0TUWs.js} +1 -1
  90. package/dist/assets/web/dist/assets/{org-Bke3eBzc.js → org-CR88hy7S.js} +1 -1
  91. package/dist/assets/web/dist/assets/{perl-Bkh0N7Kz.js → perl-Bkd8N7ja.js} +1 -1
  92. package/dist/assets/web/dist/assets/{php-Dnv1Piya.js → php-BOs84mvy.js} +1 -1
  93. package/dist/assets/web/dist/assets/{pug-D6peFFI7.js → pug-BbpZBb3w.js} +1 -1
  94. package/dist/assets/web/dist/assets/{qml-D8PEs-C-.js → qml-D2jQpjFE.js} +1 -1
  95. package/dist/assets/web/dist/assets/{r-B1EL9b_j.js → r-DKzcAQ22.js} +1 -1
  96. package/dist/assets/web/dist/assets/{razor-BsMTIh2b.js → razor-SOQMWcC9.js} +1 -1
  97. package/dist/assets/web/dist/assets/{regexp-HhvC8spD.js → regexp-B6KK4W8d.js} +1 -1
  98. package/dist/assets/web/dist/assets/{rst-D5paAxpg.js → rst-kPSg-NPf.js} +1 -1
  99. package/dist/assets/web/dist/assets/{ruby-BrQwhLrl.js → ruby-cKS-JYLH.js} +1 -1
  100. package/dist/assets/web/dist/assets/{sas-afot2B1o.js → sas-DNxzE5gs.js} +1 -1
  101. package/dist/assets/web/dist/assets/{scss-Efm-mwuG.js → scss-DGcHQ5OO.js} +1 -1
  102. package/dist/assets/web/dist/assets/{shellscript-BK0Vv5fT.js → shellscript-CRZeZBa5.js} +1 -1
  103. package/dist/assets/web/dist/assets/{shellsession-BGqlMC7N.js → shellsession-DSNEL0HX.js} +1 -1
  104. package/dist/assets/web/dist/assets/{soy-M0b4UwGM.js → soy-zisNbkDO.js} +1 -1
  105. package/dist/assets/web/dist/assets/{sql-sxe6IE9j.js → sql-MG06KMMb.js} +1 -1
  106. package/dist/assets/web/dist/assets/{sseReplay-C9m_EB8J.js → sseReplay-DaRtc7nI.js} +4 -4
  107. package/dist/assets/web/dist/assets/{stata-nPF_ddLP.js → stata-CB_wKwv9.js} +1 -1
  108. package/dist/assets/web/dist/assets/{surrealql-D_GrC6u7.js → surrealql-DYdSt7re.js} +1 -1
  109. package/dist/assets/web/dist/assets/{svelte-DwL1AtNP.js → svelte-DHO-F7c7.js} +1 -1
  110. package/dist/assets/web/dist/assets/{templ-7s7LTDkc.js → templ-CUL5L4Lr.js} +1 -1
  111. package/dist/assets/web/dist/assets/{tex-Bx-5fMxe.js → tex-_JNEKjVW.js} +1 -1
  112. package/dist/assets/web/dist/assets/{ts-tags-DUMJnke_.js → ts-tags--9EpEV_8.js} +1 -1
  113. package/dist/assets/web/dist/assets/{tsx-BN8biPDe.js → tsx-BcNq2x2u.js} +1 -1
  114. package/dist/assets/web/dist/assets/{twig-8nIu84TN.js → twig-EsY5Dk_D.js} +1 -1
  115. package/dist/assets/web/dist/assets/{typescript-BooSPq_S.js → typescript-CyA_HiUH.js} +1 -1
  116. package/dist/assets/web/dist/assets/{typst-CTBiBsem.js → typst-SzaHlDt4.js} +1 -1
  117. package/dist/assets/web/dist/assets/{vue-C6Ft4Lea.js → vue-D3cn5acV.js} +1 -1
  118. package/dist/assets/web/dist/assets/{vue-html-Ba36dD5D.js → vue-html-6MxhUxOa.js} +1 -1
  119. package/dist/assets/web/dist/assets/{vue-vine-NkFexVo2.js → vue-vine-qJnqhgo9.js} +1 -1
  120. package/dist/assets/web/dist/assets/{xml-C_THnHXZ.js → xml-DyRpC7xv.js} +1 -1
  121. package/dist/assets/web/dist/assets/{xsl-D8G5xqjY.js → xsl-BXMRPAsg.js} +1 -1
  122. package/dist/assets/web/dist/assets/{yaml-jAMIJzge.js → yaml-BNmPCtAN.js} +1 -1
  123. package/dist/cli.js +2990 -706
  124. package/package.json +1 -1
  125. package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +0 -2
  126. package/dist/assets/web/dist/assets/DeliveryPage-n1tz5I_v.js +0 -1
  127. package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +0 -2
  128. package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +0 -1
  129. package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +0 -9
  130. package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +0 -1
  131. package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +0 -1
  132. package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +0 -1
  133. package/dist/assets/web/dist/assets/main-d-w-tIKt.js +0 -28
@@ -627,6 +627,27 @@ export class ScheduleDO extends DurableObject<Env> {
627
627
  // for optimistic concurrency: a `put` whose `expectedVersion` is stale is a 409,
628
628
  // never a silent clobber (the bot and the CLI both write this document).
629
629
 
630
+ /** What the bot posts to mint a confirmation: the facts this object judges on
631
+ * and the bot's own row as an opaque body (routing-and-config item 25). */
632
+ interface ConfirmationInput {
633
+ id: string;
634
+ threadKey: string;
635
+ requester: string;
636
+ body: Record<string, unknown>;
637
+ }
638
+ /** A stored confirmation as a consume returns it: the input plus the expiry this object stamped. */
639
+ interface ConfirmationRow extends ConfirmationInput {
640
+ expiresAt: number;
641
+ }
642
+ /** Why a consume or a cancel refused: the row is gone (`used`), past its expiry (`expired`), or someone else's (`foreign`). */
643
+ type ConfirmationRefusal = "used" | "expired" | "foreign";
644
+ type ConfirmationOutcome = { row: ConfirmationRow } | { refused: ConfirmationRefusal };
645
+ type ConfirmationCancelOutcome = { ok: true } | { refused: Exclude<ConfirmationRefusal, "expired"> };
646
+
647
+ function isJsonObject(v: unknown): v is Record<string, unknown> {
648
+ return typeof v === "object" && v !== null && !Array.isArray(v);
649
+ }
650
+
630
651
  export class ConfigDO extends DurableObject<Env> {
631
652
  private readonly sql: SqlStorage;
632
653
 
@@ -650,9 +671,91 @@ export class ConfigDO extends DurableObject<Env> {
650
671
  expires_at INTEGER NOT NULL,
651
672
  ticket TEXT NOT NULL
652
673
  );
674
+ CREATE TABLE IF NOT EXISTS confirmations (
675
+ id TEXT PRIMARY KEY,
676
+ thread_key TEXT NOT NULL,
677
+ requester TEXT NOT NULL,
678
+ expires_at INTEGER NOT NULL,
679
+ body TEXT NOT NULL
680
+ );
653
681
  `);
654
682
  }
655
683
 
684
+ // ---- Confirmations (docs/reference/specs/routing-and-config.md item 25): the
685
+ // row a routed write is offered as, minted by the bot and consumed once at
686
+ // the click. The body is the bot's row, opaque here; this object owns the
687
+ // three facts the consume decides on — the thread (one pending row per
688
+ // thread), the requester (the click must be theirs) and the expiry, stamped
689
+ // and judged on this object's clock so two bot processes alive during a roll
690
+ // read one answer. Nothing sweeps: expiry is checked on touch.
691
+
692
+ /** Insert the row and drop the thread's older one in one transaction; the
693
+ * expiry is `now + ttlMs`, stamped here and answered to the caller. */
694
+ async putConfirmation(row: ConfirmationInput, ttlMs: number, now: number): Promise<number> {
695
+ const expiresAt = now + ttlMs;
696
+ this.ctx.storage.transactionSync(() => {
697
+ this.sql.exec(`DELETE FROM confirmations WHERE thread_key = ?`, row.threadKey);
698
+ this.sql.exec(
699
+ `INSERT OR REPLACE INTO confirmations (id, thread_key, requester, expires_at, body) VALUES (?, ?, ?, ?, ?)`,
700
+ row.id,
701
+ row.threadKey,
702
+ row.requester,
703
+ expiresAt,
704
+ JSON.stringify(row.body),
705
+ );
706
+ });
707
+ return expiresAt;
708
+ }
709
+
710
+ /** Read, judge and delete in one transaction on the single-threaded object,
711
+ * so a confirmation runs at most once whatever the click timing: a missing
712
+ * row is `used`, a row past its expiry is deleted and `expired`, a requester
713
+ * none of `actorIds` names is `foreign` (the row stays for its requester),
714
+ * and otherwise the row is deleted and returned. */
715
+ async consumeConfirmation(id: string, actorIds: readonly string[], now: number): Promise<ConfirmationOutcome> {
716
+ return this.ctx.storage.transactionSync(() => {
717
+ const stored = this.readConfirmation(id);
718
+ if (!stored) return { refused: "used" };
719
+ if (stored.expiresAt <= now) {
720
+ this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
721
+ return { refused: "expired" };
722
+ }
723
+ if (!actorIds.includes(stored.requester)) return { refused: "foreign" };
724
+ this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
725
+ return { row: stored };
726
+ });
727
+ }
728
+
729
+ /** Delete the row under the same requester check as a consume; a missing row
730
+ * is `used`. Expiry plays no part: cancelling an expired offer still leaves
731
+ * nothing pending, which is what the click asked for. */
732
+ async cancelConfirmation(id: string, actorIds: readonly string[]): Promise<ConfirmationCancelOutcome> {
733
+ return this.ctx.storage.transactionSync(() => {
734
+ const stored = this.readConfirmation(id);
735
+ if (!stored) return { refused: "used" };
736
+ if (!actorIds.includes(stored.requester)) return { refused: "foreign" };
737
+ this.sql.exec(`DELETE FROM confirmations WHERE id = ?`, id);
738
+ return { ok: true };
739
+ });
740
+ }
741
+
742
+ private readConfirmation(id: string): ConfirmationRow | undefined {
743
+ const row = this.sql
744
+ .exec<{ thread_key: string; requester: string; expires_at: number; body: string }>(
745
+ `SELECT thread_key, requester, expires_at, body FROM confirmations WHERE id = ?`,
746
+ id,
747
+ )
748
+ .toArray()[0];
749
+ if (!row) return undefined;
750
+ return {
751
+ id,
752
+ threadKey: row.thread_key,
753
+ requester: row.requester,
754
+ expiresAt: row.expires_at,
755
+ body: parseStored(row.body, isJsonObject) ?? {},
756
+ };
757
+ }
758
+
656
759
  // ---- MCP sealed credentials + connect tickets (docs/reference/specs/mcp-tools.md items 15–16).
657
760
  // Ciphertext the bot sealed — opaque here — and one-time tickets: the two
658
761
  // things a config document must never carry, kept beside it on this object.
@@ -1052,8 +1155,21 @@ const CONFIG_ROUTES = new Set([
1052
1155
  "/config/tickets/put",
1053
1156
  "/config/tickets/get",
1054
1157
  "/config/tickets/transition",
1158
+ "/config/confirmations/put",
1159
+ "/config/confirmations/consume",
1160
+ "/config/confirmations/cancel",
1055
1161
  ]);
1056
1162
  const TICKET_STATES: ReadonlySet<string> = new Set<McpTicketState>(MCP_TICKET_STATES);
1163
+ /** A confirmation id as the bot mints it (a UUID) — one token, no whitespace, bounded. */
1164
+ const CONFIRMATION_ID_RE = /^[A-Za-z0-9_-]{8,128}$/;
1165
+
1166
+ /** The `{ id, actorIds }` a consume or a cancel carries, or the 400 that refuses it. */
1167
+ function confirmationClickOf(b: Record<string, unknown>): { id: string; actorIds: string[] } | Response {
1168
+ if (typeof b.id !== "string" || !CONFIRMATION_ID_RE.test(b.id)) return json({ error: "id malformed" }, 400);
1169
+ if (!Array.isArray(b.actorIds) || !b.actorIds.every((a): a is string => typeof a === "string"))
1170
+ return json({ error: "actorIds must be a list of actor ids" }, 400);
1171
+ return { id: b.id, actorIds: b.actorIds };
1172
+ }
1057
1173
 
1058
1174
  async function handleConfig(pathname: string, body: unknown, env: Env): Promise<Response> {
1059
1175
  const b = (typeof body === "object" && body !== null ? body : {}) as Record<string, unknown>;
@@ -1095,6 +1211,38 @@ async function handleConfig(pathname: string, body: unknown, env: Env): Promise<
1095
1211
  );
1096
1212
  return json({ ok: true, applied });
1097
1213
  }
1214
+ // Confirmations (routing-and-config item 25): the body is the bot's row,
1215
+ // stored verbatim; the expiry is this object's clock plus the ttl the bot
1216
+ // passed, never a timestamp the bot chose.
1217
+ case "/config/confirmations/put": {
1218
+ if (typeof b.id !== "string" || !CONFIRMATION_ID_RE.test(b.id)) return json({ error: "id malformed" }, 400);
1219
+ if (typeof b.threadKey !== "string" || !b.threadKey) return json({ error: "threadKey required" }, 400);
1220
+ if (typeof b.requester !== "string" || !b.requester) return json({ error: "requester required" }, 400);
1221
+ if (!isJsonObject(b.body)) return json({ error: "body must be a JSON object" }, 400);
1222
+ if (typeof b.ttlMs !== "number" || !Number.isInteger(b.ttlMs) || b.ttlMs < 0)
1223
+ return json({ error: "ttlMs must be a non-negative integer" }, 400);
1224
+ const expiresAt = await dO.putConfirmation(
1225
+ { id: b.id, threadKey: b.threadKey, requester: b.requester, body: b.body },
1226
+ b.ttlMs,
1227
+ systemClock(),
1228
+ );
1229
+ console.log(`[config/confirmations/put] ${b.threadKey} ${b.id} expires_at=${expiresAt}`);
1230
+ return json({ ok: true, expiresAt });
1231
+ }
1232
+ case "/config/confirmations/consume": {
1233
+ const click = confirmationClickOf(b);
1234
+ if (click instanceof Response) return click;
1235
+ const outcome = await dO.consumeConfirmation(click.id, click.actorIds, systemClock());
1236
+ console.log(`[config/confirmations/consume] ${click.id} ${"row" in outcome ? "consumed" : outcome.refused}`);
1237
+ return json(outcome);
1238
+ }
1239
+ case "/config/confirmations/cancel": {
1240
+ const click = confirmationClickOf(b);
1241
+ if (click instanceof Response) return click;
1242
+ const outcome = await dO.cancelConfirmation(click.id, click.actorIds);
1243
+ console.log(`[config/confirmations/cancel] ${click.id} ${"ok" in outcome ? "cancelled" : outcome.refused}`);
1244
+ return json(outcome);
1245
+ }
1098
1246
  default:
1099
1247
  break;
1100
1248
  }
@@ -79,6 +79,10 @@ export interface ReclaimInput {
79
79
  isDefaultRef: boolean;
80
80
  /** Thread exec/read/write currently running on this binding. */
81
81
  busy: number;
82
+ /** A run registered on the thread — attached and not yet released (item
83
+ * 44): its process lives in the container between the bot's calls, so
84
+ * `busy` reads 0 while its model thinks. Held is kept, whatever the fate. */
85
+ held: boolean;
82
86
  }
83
87
 
84
88
  /** The PR that decided the fate (for the audit trail): the open one, else the
@@ -93,11 +97,13 @@ export type ReclaimWhy =
93
97
  | "no-pr"
94
98
  | "fate-unknown"
95
99
  | "busy"
100
+ | "run-held"
96
101
  | "re-attached"
97
102
  | Extract<RefFate, "gone" | "merged" | "closed">;
98
103
 
99
104
  /** Evict this binding now? A finished ref (gone/merged/closed) is reclaimed
100
- * when nothing runs on it — and nothing else is asked. What its tree holds
105
+ * when nothing runs on it — no op in flight and no run registered from its
106
+ * attach to its release — and nothing else is asked. What its tree holds
101
107
  * is never a reason to keep it: a run starts from a clean tree, so dirt in a
102
108
  * tree no run is using protects nothing (the next attach wipes it); the
103
109
  * Worker measures and records what it removes instead. Keeps are named so
@@ -113,6 +119,7 @@ export function reclaimDecision(input: ReclaimInput): { reclaim: boolean; why: R
113
119
  return { reclaim: false, why: "fate-unknown" };
114
120
  }
115
121
  if (input.busy > 0) return { reclaim: false, why: "busy" };
122
+ if (input.held) return { reclaim: false, why: "run-held" };
116
123
  return { reclaim: true, why: input.fate };
117
124
  }
118
125