toolroll 0.9.55 → 0.9.56

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 (200) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +24 -8
  3. package/dist/action-ledger.d.ts +2 -4
  4. package/dist/action-ledger.js +2 -10
  5. package/dist/approval-policy.d.ts +5 -5
  6. package/dist/approval-policy.js +4 -4
  7. package/dist/approval-rules-ui.js +2 -2
  8. package/dist/browser/workspace.js +5 -5
  9. package/dist/browser-workspace.d.ts +12 -12
  10. package/dist/budget-alerts.js +3 -3
  11. package/dist/chat-actions.d.ts +23 -18
  12. package/dist/chat-actions.js +110 -88
  13. package/dist/chat-ask.d.ts +2 -2
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-channel.d.ts +15 -15
  16. package/dist/chat-channel.js +20 -20
  17. package/dist/chat-controls.d.ts +0 -4
  18. package/dist/chat-controls.js +0 -1
  19. package/dist/chat-core.js +1 -1
  20. package/dist/chat-delivery-state.js +2 -2
  21. package/dist/chat-delivery.d.ts +2 -2
  22. package/dist/chat-delivery.js +27 -34
  23. package/dist/chat-flow.js +3 -3
  24. package/dist/chat-messages.d.ts +1 -1
  25. package/dist/chat-messages.js +4 -4
  26. package/dist/chat-migration.d.ts +1 -1
  27. package/dist/chat-migration.js +11 -11
  28. package/dist/chat-rooms.js +3 -3
  29. package/dist/chat-warmth.d.ts +2 -2
  30. package/dist/cli.d.ts +1 -1
  31. package/dist/cli.js +6 -7
  32. package/dist/contracts/chat-actions.d.ts +32 -27
  33. package/dist/contracts/chat-actions.js +29 -11
  34. package/dist/contracts/chat-content.d.ts +1 -1
  35. package/dist/contracts/chat-content.js +1 -1
  36. package/dist/contracts/chat-tables.js +4 -4
  37. package/dist/contracts/console-api.d.ts +6 -6
  38. package/dist/contracts/console-api.js +7 -7
  39. package/dist/contracts/flow-propose.d.ts +3 -3
  40. package/dist/contracts/flow.d.ts +132 -125
  41. package/dist/contracts/flow.js +27 -12
  42. package/dist/contracts/lead-context.d.ts +1 -1
  43. package/dist/contracts/lead-context.js +2 -2
  44. package/dist/contracts/lead-tools.d.ts +20 -18
  45. package/dist/contracts/lead-tools.js +15 -15
  46. package/dist/contracts/stage-output.d.ts +1 -1
  47. package/dist/contracts/stage-output.js +1 -1
  48. package/dist/contracts/store-json.d.ts +23 -22
  49. package/dist/contracts/store-json.js +17 -17
  50. package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
  51. package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
  52. package/dist/converse.d.ts +23 -23
  53. package/dist/converse.js +28 -28
  54. package/dist/coordinator-proposals.d.ts +2 -2
  55. package/dist/coordinator-proposals.js +1 -1
  56. package/dist/coordinator.js +1 -1
  57. package/dist/deprecations.d.ts +12 -0
  58. package/dist/deprecations.js +20 -0
  59. package/dist/desktop-update-gate.js +8 -4
  60. package/dist/desktop-update.js +6 -1
  61. package/dist/discord-chat.js +6 -6
  62. package/dist/dispatch.d.ts +1 -1
  63. package/dist/dispatch.js +1 -1
  64. package/dist/envelope.js +0 -1
  65. package/dist/export-ui.js +1 -1
  66. package/dist/export.js +7 -7
  67. package/dist/flow-engine.d.ts +2 -2
  68. package/dist/flow-engine.js +21 -21
  69. package/dist/flow-insights.d.ts +1 -1
  70. package/dist/flow-share.js +13 -8
  71. package/dist/flow-steps.d.ts +5 -5
  72. package/dist/flow-steps.js +19 -19
  73. package/dist/flow-triggers.d.ts +1 -1
  74. package/dist/flow-triggers.js +1 -1
  75. package/dist/flows-cli.js +1 -1
  76. package/dist/flows-ui.js +20 -20
  77. package/dist/flows.js +19 -19
  78. package/dist/guides.js +4 -3
  79. package/dist/invoke.js +1 -1
  80. package/dist/kits-ui.js +5 -5
  81. package/dist/kits.d.ts +12 -12
  82. package/dist/kits.js +31 -31
  83. package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
  84. package/dist/{mate-cli.js → lead-cli.js} +38 -38
  85. package/dist/lead-commitments.js +4 -4
  86. package/dist/lead-context.d.ts +2 -2
  87. package/dist/lead-context.js +9 -9
  88. package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
  89. package/dist/{mate-contract.js → lead-contract.js} +7 -6
  90. package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
  91. package/dist/{mate-doors.js → lead-doors.js} +24 -24
  92. package/dist/lead-follow.d.ts +5 -5
  93. package/dist/lead-follow.js +9 -9
  94. package/dist/lead-people.d.ts +5 -5
  95. package/dist/lead-people.js +14 -14
  96. package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
  97. package/dist/{mate-progress.js → lead-progress.js} +3 -3
  98. package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
  99. package/dist/{mate-tools.js → lead-tools.js} +109 -101
  100. package/dist/{mate.d.ts → lead.d.ts} +27 -27
  101. package/dist/{mate.js → lead.js} +102 -102
  102. package/dist/limits-ui.d.ts +1 -1
  103. package/dist/limits-ui.js +1 -1
  104. package/dist/mcp-connect.d.ts +1 -1
  105. package/dist/mcp-connect.js +3 -3
  106. package/dist/mcp-core.js +1 -1
  107. package/dist/memory-pass.d.ts +2 -2
  108. package/dist/memory-pass.js +7 -7
  109. package/dist/notification-rows.d.ts +2 -2
  110. package/dist/operate.d.ts +3 -3
  111. package/dist/operate.js +35 -34
  112. package/dist/policy.d.ts +1 -1
  113. package/dist/principal.d.ts +1 -1
  114. package/dist/principal.js +2 -2
  115. package/dist/project-delete-ui.js +1 -1
  116. package/dist/project-delete.d.ts +1 -1
  117. package/dist/project-delete.js +26 -26
  118. package/dist/project-memory.js +1 -1
  119. package/dist/project-tools.d.ts +1 -1
  120. package/dist/project-tools.js +1 -1
  121. package/dist/recipe-ui.js +1 -1
  122. package/dist/retention.js +4 -4
  123. package/dist/scope.d.ts +2 -2
  124. package/dist/scout-report.d.ts +1 -1
  125. package/dist/scout-report.js +1 -1
  126. package/dist/serve.js +40 -45
  127. package/dist/server/chat.d.ts +4 -4
  128. package/dist/server/chat.js +97 -97
  129. package/dist/server/chrome.js +3 -3
  130. package/dist/server/flows.js +69 -62
  131. package/dist/server/guards.js +3 -3
  132. package/dist/server/http.d.ts +3 -3
  133. package/dist/server/http.js +1 -1
  134. package/dist/server/live.js +6 -6
  135. package/dist/server/people-tokens.js +1 -1
  136. package/dist/server/remote.js +12 -10
  137. package/dist/server/render-chat.d.ts +30 -30
  138. package/dist/server/render-chat.js +33 -33
  139. package/dist/server/render-pages.js +2 -2
  140. package/dist/server/render-settings.d.ts +7 -0
  141. package/dist/server/render-settings.js +3 -0
  142. package/dist/server/route-table.js +9 -7
  143. package/dist/server/runtime.d.ts +13 -13
  144. package/dist/server/settings.js +14 -13
  145. package/dist/server/tasks.js +6 -5
  146. package/dist/setup-guide.d.ts +2 -2
  147. package/dist/slack-chat.d.ts +2 -2
  148. package/dist/slack-chat.js +6 -6
  149. package/dist/spend-ui.d.ts +3 -3
  150. package/dist/spend-ui.js +9 -9
  151. package/dist/spend.d.ts +14 -14
  152. package/dist/spend.js +31 -31
  153. package/dist/store-baseline.d.ts +19 -0
  154. package/dist/store-baseline.js +2873 -0
  155. package/dist/store.d.ts +197 -249
  156. package/dist/store.js +747 -3084
  157. package/dist/subagent-admin.d.ts +53 -0
  158. package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
  159. package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
  160. package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
  161. package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
  162. package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
  163. package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
  164. package/dist/{teammate-question.js → subagent-question.js} +17 -17
  165. package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
  166. package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
  167. package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
  168. package/dist/{teammate-week.js → subagent-week.js} +23 -23
  169. package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
  170. package/dist/{teammate-work.js → subagent-work.js} +50 -50
  171. package/dist/subagents-ui.d.ts +22 -0
  172. package/dist/{teammates-ui.js → subagents-ui.js} +45 -45
  173. package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
  174. package/dist/{teammates.js → subagents.js} +21 -21
  175. package/dist/subscription-chat.d.ts +13 -13
  176. package/dist/subscription-chat.js +19 -19
  177. package/dist/surface.d.ts +19 -2
  178. package/dist/surface.js +7 -31
  179. package/dist/team-cli.d.ts +1 -0
  180. package/dist/team-cli.js +14 -2
  181. package/dist/team-leads.js +13 -13
  182. package/dist/team-runtime.d.ts +2 -2
  183. package/dist/team-runtime.js +8 -8
  184. package/dist/teams-chat.js +6 -6
  185. package/dist/telegram-flow.d.ts +3 -3
  186. package/dist/telegram-flow.js +3 -3
  187. package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
  188. package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
  189. package/dist/telegram-status.js +1 -1
  190. package/dist/telegram-team.d.ts +1 -1
  191. package/dist/telegram-team.js +5 -5
  192. package/dist/telegram.d.ts +1 -1
  193. package/dist/telegram.js +9 -30
  194. package/dist/text-limits.d.ts +18 -18
  195. package/dist/text-limits.js +18 -18
  196. package/dist/toolroll-update.d.ts +8 -0
  197. package/dist/toolroll-update.js +45 -8
  198. package/package.json +1 -2
  199. package/dist/teammate-admin.d.ts +0 -53
  200. package/dist/teammates-ui.d.ts +0 -21
@@ -1,11 +1,11 @@
1
1
  import { CHAT_ACTIONS, sharedActionAllowsChallenge, sharedActionPayload } from "./chat-actions.js";
2
- import { taskInCeiling, mirrorToTaskChat, couldNotAnswerText, channelRepos as telegramConversationRepos, resolveChannelMate as resolveTelegramMate, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText } from "./chat-channel.js";
3
- export { channelRepos as telegramConversationRepos, resolveChannelMate as resolveTelegramMate, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText, CHAT_ACTION_PARITY as TELEGRAM_ACTION_PARITY, parityGaps } from "./chat-channel.js";
2
+ import { taskInCeiling, mirrorToTaskChat, couldNotAnswerText, channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText } from "./chat-channel.js";
3
+ export { channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText, CHAT_ACTION_PARITY as TELEGRAM_ACTION_PARITY, parityGaps } from "./chat-channel.js";
4
4
  /**
5
5
  * The paired phone as one more way to use the same assistant.
6
6
  *
7
- * Transport only. An ordinary private Telegram message becomes a mate turn
8
- * through `runMateTurn` — the same saved thread, proposal rows, confirm
7
+ * Transport only. An ordinary private Telegram message becomes a lead turn
8
+ * through `runLeadTurn` — the same saved thread, proposal rows, confirm
9
9
  * doors and result actions the console and `toolroll chat` use —
10
10
  * and every act is still a card the operator confirms. This module owns
11
11
  * what is specific to the wire: proving the pairing (chat AND immutable
@@ -23,10 +23,10 @@ export { channelRepos as telegramConversationRepos, resolveChannelMate as resolv
23
23
  * the console and CLI state), and the door re-proves everything again.
24
24
  */
25
25
  import { createHash, randomBytes } from "node:crypto";
26
- import { MATE_MESSAGE_MAX_CHARS, mateFailureText, runMateTurn } from "./mate.js";
27
- import { confirmMateProposal, dismissMateProposal } from "./mate-doors.js";
26
+ import { LEAD_MESSAGE_MAX_CHARS, leadFailureText, runLeadTurn } from "./lead.js";
27
+ import { confirmLeadProposal, dismissLeadProposal } from "./lead-doors.js";
28
28
  import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
29
- import { MATE_ASK_OTHER } from "./store.js";
29
+ import { LEAD_ASK_OTHER } from "./store.js";
30
30
  import { phoneText } from "./telegram-status.js";
31
31
  import { shapeReplyParts, telegramReply } from "./reply-shape.js";
32
32
  import { WARM_EMOJI, warmTurn } from "./chat-warmth.js";
@@ -112,7 +112,7 @@ export function askKeyboard(ask) {
112
112
  const rows = [];
113
113
  for (let at = 0; at < ask.options.length; at += 2)
114
114
  rows.push(ask.options.slice(at, at + 2).map((label, offset) => telegramButton(label.slice(0, 60), `ask:${ask.turn}:${at + offset}`)));
115
- rows.push([telegramButton(MATE_ASK_OTHER, `ask:${ask.turn}:x`)]);
115
+ rows.push([telegramButton(LEAD_ASK_OTHER, `ask:${ask.turn}:x`)]);
116
116
  return rows;
117
117
  }
118
118
  /** Mint the card's tokens before the send, so a tap can never name a token that does not exist. */
@@ -151,7 +151,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
151
151
  ack("that button is stale — send /status to see what still waits");
152
152
  return { effects, confirmed: false, ignored: true };
153
153
  }
154
- const proposal = store.getMateProposal(action.proposal);
154
+ const proposal = store.getLeadProposal(action.proposal);
155
155
  if (proposal === null) {
156
156
  store.consumeTelegramProposalAction(token, now);
157
157
  ack("that card no longer exists");
@@ -209,10 +209,10 @@ export function applyProposalTap(store, binding, token, message, repos, options,
209
209
  ack("that button was already used");
210
210
  return { effects, confirmed: false, ignored: true };
211
211
  }
212
- const done = dismissMateProposal(store, who, proposal.id, now);
212
+ const done = dismissLeadProposal(store, who, proposal.id, now);
213
213
  store.consumeTelegramProposalActions(proposal.id, now);
214
214
  ack(done ? "dismissed" : "that proposal was already acted on");
215
- edit(done ? "Dismissed." : proposalOutcomeText(store.getMateProposal(proposal.id) ?? proposal));
215
+ edit(done ? "Dismissed." : proposalOutcomeText(store.getLeadProposal(proposal.id) ?? proposal));
216
216
  return { effects, confirmed: false, ignored: !done };
217
217
  }
218
218
  if (action.phase === "cancel") {
@@ -273,7 +273,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
273
273
  ack(action.phase === "yes" ? "that confirmation expired — start again from Confirm" : "that button was already used");
274
274
  return { effects, confirmed: false, ignored: true };
275
275
  }
276
- const outcome = confirmMateProposal(store, who, proposal.id, now, {
276
+ const outcome = confirmLeadProposal(store, who, proposal.id, now, {
277
277
  via: "telegram",
278
278
  evidenceRoot: options.evidenceRoot,
279
279
  confirm: action.phase === "yes",
@@ -405,7 +405,7 @@ async function runTelegramConversation(row, args) {
405
405
  const deliver = async (outcomeWord) => {
406
406
  // The row as it is NOW: the session was bound after this claim read it.
407
407
  const bound = store.getTelegramConversation(row.id)?.session ?? null;
408
- const session = bound === null ? null : store.getMateSession(bound);
408
+ const session = bound === null ? null : store.getLeadSession(bound);
409
409
  if (session === null) {
410
410
  await notify("This conversation was restarted before my reply went out, so it wasn't sent. Nothing was changed. Send your message again.");
411
411
  finish({ state: "failed", outcome: "unsent:no-session" });
@@ -421,7 +421,7 @@ async function runTelegramConversation(row, args) {
421
421
  continue;
422
422
  const now = clock();
423
423
  if (part.kind === "card") {
424
- const proposal = part.proposal === null ? null : store.getMateProposal(part.proposal);
424
+ const proposal = part.proposal === null ? null : store.getLeadProposal(part.proposal);
425
425
  if (proposal === null || proposal.state !== "pending") {
426
426
  // Confirmed or dismissed from the console or the terminal before the card went out: moot, not lost.
427
427
  if (!store.dropTelegramConversationPart(row.id, part.ordinal, owner, proposal === null ? "the proposal is gone" : `the proposal was already ${proposal.state}`, now))
@@ -523,7 +523,7 @@ async function runTelegramConversation(row, args) {
523
523
  // A card's link is minted NOW, from the persisted proposal and the
524
524
  // origin configured at this moment: a retry after the setting changed
525
525
  // or went away carries the current truth, never a stored URL.
526
- const proposal = part.kind === "card" && part.proposal !== null ? store.getMateProposal(part.proposal) : null;
526
+ const proposal = part.kind === "card" && part.proposal !== null ? store.getLeadProposal(part.proposal) : null;
527
527
  const origin = proposal === null ? null : options.phoneOrigin?.() ?? null;
528
528
  const wanted = proposal === null ? null : proposalLink(store, proposal, repos);
529
529
  const keyboard = keyboardWith(part.keyboard, phoneLinkButton(origin, wanted));
@@ -597,11 +597,11 @@ async function runTelegramConversation(row, args) {
597
597
  const now = clock();
598
598
  const parts = shapeReplyParts(reply, PART_CAP, { asked: row.text, appOrigin: options.phoneOrigin?.() ?? null }, shaped => telegramReply(shaped).text.length).map((text, index) => ({ kind: "reply", text, replyTo: index === 0 ? row.messageId : null }));
599
599
  // The lead's question to its owner: one tap per option, then "Something else".
600
- const ask = store.mateAsk(turn);
600
+ const ask = store.leadAsk(turn);
601
601
  if (ask !== null)
602
602
  parts.push({ kind: "reply", text: phoneText(ask.question, 1_000), keyboard: askKeyboard(ask) });
603
603
  // Only a turn that answered may have its selection sent: a failed or revoked turn's rows were deleted with its drafts, and the state is read again here.
604
- const selected = store.getMateTurn(turn)?.state === "answered" ? store.listMateTurnEvidence(turn) : [];
604
+ const selected = store.getLeadTurn(turn)?.state === "answered" ? store.listLeadTurnEvidence(turn) : [];
605
605
  for (const image of selected)
606
606
  parts.push({ kind: "image", text: image.caption, taskId: image.taskId, run: image.run, artifact: image.artifact, sha256: image.sha256 });
607
607
  for (const proposal of proposals) {
@@ -625,8 +625,8 @@ async function runTelegramConversation(row, args) {
625
625
  const recover = async (session, turnId, repos) => {
626
626
  // A turn that died past its deadline is swept to failed first, so a
627
627
  // restart reports the truth instead of waiting on a ghost.
628
- store.sweepStaleMateTurns(clock());
629
- const turn = store.getMateTurn(turnId);
628
+ store.sweepStaleLeadTurns(clock());
629
+ const turn = store.getLeadTurn(turnId);
630
630
  if (turn === null) {
631
631
  finish({ state: "failed", outcome: "replayed:missing" });
632
632
  report.refused++;
@@ -638,13 +638,13 @@ async function runTelegramConversation(row, args) {
638
638
  return;
639
639
  }
640
640
  if (turn.state !== "answered") {
641
- await notify(mateFailureText(turn.failureReason));
641
+ await notify(leadFailureText(turn.failureReason));
642
642
  finish({ state: "failed", outcome: `replayed:${turn.failureReason ?? "failed"}` });
643
643
  report.refused++;
644
644
  return;
645
645
  }
646
- const reply = store.listMateMessages(turn.thread, 200).find(one => one.turn === turn.id && one.role === "assistant")?.text ?? "(the reply text is no longer in the thread)";
647
- const proposals = store.listMateProposals(turn.thread, ["pending"]).filter(one => one.turn === turn.id);
646
+ const reply = store.listLeadMessages(turn.thread, 200).find(one => one.turn === turn.id && one.role === "assistant")?.text ?? "(the reply text is no longer in the thread)";
647
+ const proposals = store.listLeadProposals(turn.thread, ["pending"]).filter(one => one.turn === turn.id);
648
648
  if (!plan(session, turnId, reply, proposals, repos) && store.listTelegramConversationParts(row.id).length === 0)
649
649
  return;
650
650
  await sendOrHandOff("replayed");
@@ -676,14 +676,14 @@ async function runTelegramConversation(row, args) {
676
676
  // 4. A session bound before an earlier dispatch: the receipt lives there,
677
677
  // whatever session is live today.
678
678
  if (row.session !== null) {
679
- const receipt = store.mateRequestReceipt(row.session, row.request);
679
+ const receipt = store.leadRequestReceipt(row.session, row.request);
680
680
  if (receipt !== null) {
681
681
  await recover(row.session, receipt.turn, repos);
682
682
  return;
683
683
  }
684
684
  }
685
685
  // 5. The session and thread — shared with the console and the CLI.
686
- const resolved = resolveTelegramMate(store, binding, repos, clock());
686
+ const resolved = resolveTelegramLead(store, binding, repos, clock());
687
687
  if (!resolved.ok && resolved.reason === "busy") {
688
688
  await requeue("busy");
689
689
  return;
@@ -722,7 +722,7 @@ async function runTelegramConversation(row, args) {
722
722
  });
723
723
  let outcome;
724
724
  try {
725
- outcome = await runMateTurn({
725
+ outcome = await runLeadTurn({
726
726
  store, who, session, thread, config, key: null, message: row.text, requestId: row.request,
727
727
  ...(row.context === null ? {} : { context: row.context }),
728
728
  ...(options.subscriptionRunner === undefined ? {} : { subscriptionRunner: options.subscriptionRunner }),
@@ -733,7 +733,7 @@ async function runTelegramConversation(row, args) {
733
733
  clearInterval(heartbeat);
734
734
  warm.stop();
735
735
  }
736
- const receipt = store.mateRequestReceipt(session.id, row.request);
736
+ const receipt = store.leadRequestReceipt(session.id, row.request);
737
737
  if (receipt !== null)
738
738
  store.bindTelegramConversationTurn(row.id, owner, session.id, receipt.turn);
739
739
  if (!outcome.ok && "refused" in outcome) {
@@ -761,7 +761,7 @@ async function runTelegramConversation(row, args) {
761
761
  if (row.taskId !== null)
762
762
  mirrorToTaskChat(store, who, row.taskId, "Telegram", row.text, outcome.reply, clock());
763
763
  // 7. The reply and its cards: durable first, then sent.
764
- const proposals = store.listMateProposals(thread.id, ["pending"]).filter(one => one.turn === outcome.turn);
764
+ const proposals = store.listLeadProposals(thread.id, ["pending"]).filter(one => one.turn === outcome.turn);
765
765
  if (!plan(session.id, outcome.turn, outcome.reply, proposals, who.repos) && store.listTelegramConversationParts(row.id).length === 0)
766
766
  return;
767
767
  await sendOrHandOff("answered");
@@ -39,7 +39,7 @@ export const PHONE_HELP = [
39
39
  "/task <name or number> — the same, by a word from its name",
40
40
  "/lead — back to talking about everything",
41
41
  "/status — what's happening across your projects",
42
- "/team — team conversations; /team <number> to talk in one, /team off to leave",
42
+ // D5: /team (the central team service's conversations) is deprecated: it still answers this release, unlisted.
43
43
  "/help — this message",
44
44
  "",
45
45
  "To answer an agent's question, tap its buttons; reply to the question to add a note. Approvals that need your password, cancelling and publishing finish in the Toolroll console, and a console button only takes you there. The computer must be awake and connected to reply.",
@@ -7,7 +7,7 @@
7
7
  * per-chat cursor read on every bridge cycle. No model runs here.
8
8
  */
9
9
  import type { Store } from "./store.js";
10
- import { type InlineButton } from "./telegram-mate.js";
10
+ import { type InlineButton } from "./telegram-lead.js";
11
11
  import type { TelegramTransport } from "./telegram.js";
12
12
  import type { TelegramMessage } from "./contracts/telegram-callback.js";
13
13
  export type TeamCommand = {
@@ -1,8 +1,8 @@
1
1
  import { renderReply, shapeReplyParts } from "./reply-shape.js";
2
2
  import { TeamLeads } from "./team-leads.js";
3
3
  import { proposalPreview, tooLongText } from "./chat-channel.js";
4
- import { PART_CAP, mintCardTokens, phoneLinkButton, splitParts } from "./telegram-mate.js";
5
- import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
4
+ import { PART_CAP, mintCardTokens, phoneLinkButton, splitParts } from "./telegram-lead.js";
5
+ import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
6
6
  import { phoneText } from "./telegram-status.js";
7
7
  /** `/team` lists, `/team <n>` chooses, `/team off` (or `private`) leaves. Any other slash text is not ours. */
8
8
  export function teamCommand(text) {
@@ -172,7 +172,7 @@ export function applyTeamInbound(input) {
172
172
  report.ignored++;
173
173
  return true;
174
174
  }
175
- if (trimmed.length > MATE_MESSAGE_MAX_CHARS) {
175
+ if (trimmed.length > LEAD_MESSAGE_MAX_CHARS) {
176
176
  say(chatId, tooLongText(trimmed.length));
177
177
  report.chatRefused = (report.chatRefused ?? 0) + 1;
178
178
  return true;
@@ -255,7 +255,7 @@ export async function deliverTeamChats(store, botId, transport, clock, report, p
255
255
  const row = conversationRow(store, chat.conversation);
256
256
  if (row === null)
257
257
  continue;
258
- const pending = store.handle.prepare(`SELECT m.id, m.role, m.text, m.turn, q.author, q.request_id, q.status FROM mate_message m
258
+ const pending = store.handle.prepare(`SELECT m.id, m.role, m.text, m.turn, q.author, q.request_id, q.status FROM lead_message m
259
259
  LEFT JOIN team_message q ON q.message = m.id WHERE m.thread = ? AND m.id > ? ORDER BY m.id LIMIT 20`).all(row.thread, chat.cursor);
260
260
  for (const message of pending) {
261
261
  const id = Number(message["id"]);
@@ -279,7 +279,7 @@ export async function deliverTeamChats(store, botId, transport, clock, report, p
279
279
  if (role === "assistant" && message["turn"] !== null) {
280
280
  const turn = Number(message["turn"]);
281
281
  const cardBinding = cardBindingFor(store, botId, chat, turn);
282
- for (const proposal of store.listMateProposals(row.thread, ["pending"]).filter(one => one.turn === turn)) {
282
+ for (const proposal of store.listLeadProposals(row.thread, ["pending"]).filter(one => one.turn === turn)) {
283
283
  const enrolled = await access();
284
284
  if (enrolled === null) {
285
285
  complete = false;
@@ -19,7 +19,7 @@
19
19
  * are one command.
20
20
  */
21
21
  import { type Store, type Notification } from "./store.js";
22
- import { type TelegramConversationOptions } from "./telegram-mate.js";
22
+ import { type TelegramConversationOptions } from "./telegram-lead.js";
23
23
  /** Read the enrolled project list on demand. No callback means no task data,
24
24
  * never an implicit all-database ceiling. Shared by pass and embedded follower. */
25
25
  export type TelegramReadProjects = () => Promise<readonly string[]>;
package/dist/telegram.js CHANGED
@@ -18,8 +18,7 @@
18
18
  * BotFather token plus `bridge telegram unpair` is the recovery, and both
19
19
  * are one command.
20
20
  */
21
- import { applyTelegramQuestionReply, applyTelegramQuestionTap, openQuestionOf, telegramQuestionButtons } from "./teammate-question.js";
22
- import { messageTeammate } from "./teammate-desk.js";
21
+ import { applyTelegramQuestionReply, applyTelegramQuestionTap, openQuestionOf, telegramQuestionButtons } from "./subagent-question.js";
23
22
  import { acceptanceEvidenceText } from "./chat-acceptance.js";
24
23
  import { verifyApproverStanding } from "./principal.js";
25
24
  import { resultImageFileName, resultTaskLabel, verifyResultImage } from "./chat-evidence.js";
@@ -43,9 +42,9 @@ import { focusContextFor, taskInCeiling } from "./chat-channel.js";
43
42
  import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, dropDecideTokens, hasLiveDecideTokens, linksFor, placeDecidePrompt, decideOffer, decideTargetOf, isDecideToken, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, placeDecideTokens, recordChatMerge, retireDecideTokens } from "./chat-decide.js";
44
43
  import { mergePullRequest } from "./pull-request-flow.js";
45
44
  import { phoneCommand, phoneStatus, phoneTaskView, PHONE_CONSOLE_FOOTER, PHONE_HELP, notificationIdentity, phoneTaskChoices, resolvePhoneTask, phoneFocusText, phoneTaskListText, phoneText, PHONE_NO_MATCH, PHONE_BACK_TO_LEAD } from "./telegram-status.js";
46
- import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
45
+ import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
47
46
  import { envValue } from "./names.js";
48
- import { applyProposalTap, processTelegramConversations, phoneLinkButton, replyContextFor, telegramConversationRepos, telegramRequestId, tooLongText, whichTaskText, } from "./telegram-mate.js";
47
+ import { applyProposalTap, processTelegramConversations, phoneLinkButton, replyContextFor, telegramConversationRepos, telegramRequestId, tooLongText, whichTaskText, } from "./telegram-lead.js";
49
48
  import { askOf, parseTelegramUpdate, pickOf, readTelegramButtonData, readTelegramUpdate, telegramButton } from "./contracts/telegram-callback.js";
50
49
  /** The environment name — and therefore the name the builder strips from agents (both names; see names.ts). */
51
50
  export { TELEGRAM_TOKEN_ENV as TOKEN_ENV, TELEGRAM_TOKEN_ENVS as TOKEN_ENVS } from "./names.js";
@@ -1246,7 +1245,7 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
1246
1245
  parts = voiced.map(one => one.text);
1247
1246
  entities = voiced.map(one => one.entities);
1248
1247
  }
1249
- // A teammate's question (v93): its options and "Answer in words", for the person it asks.
1248
+ // A subagent's question (v93): its options and "Answer in words", for the person it asks.
1250
1249
  const asked = flowKeys === null && choiceKeys === null ? openQuestionOf(store, notification.dedupeKey) : null;
1251
1250
  const questionKeys = asked === null ? null : telegramQuestionButtons(store, binding, asked, clock());
1252
1251
  const keys = flowKeys ?? choiceKeys ?? questionKeys;
@@ -1649,7 +1648,7 @@ function applyMessage(context, update, effects) {
1649
1648
  // assistant when a conversation is configured; otherwise silence.
1650
1649
  const binding = store.liveTelegramBindingFor(botId, String(from.id));
1651
1650
  // A reply to an Edit or Send back prompt (v86): the new draft, or the note it goes back with.
1652
- // A reply to a teammate's "Answer in words" prompt (v93): the answer.
1651
+ // A reply to a subagent's "Answer in words" prompt (v93): the answer.
1653
1652
  const questionPrompt = binding !== null && message.reply_to_message !== undefined && String(chat.id) === binding.chatId
1654
1653
  ? store.telegramQuestionPrompt(binding.chatId, String(message.reply_to_message.message_id), clock()) : null;
1655
1654
  if (binding !== null && questionPrompt !== null) {
@@ -1694,27 +1693,7 @@ function applyMessage(context, update, effects) {
1694
1693
  });
1695
1694
  return;
1696
1695
  }
1697
- // A message to a teammate by name (v96), in the person's own chat: a card on its desk, and the answer comes back here.
1698
- if (binding !== null && message.reply_to_message === undefined && message.chat?.type === "private" && String(chat.id) === binding.chatId && store.accountOf(binding.approver)?.role === "approver") {
1699
- const repos = context.projects === null ? store.knownRepos().filter(repo => store.accountCanAccess(binding.approver, repo)) : telegramConversationRepos(store, binding.approver, context.projects);
1700
- const handed = messageTeammate(store, { who: binding.approver, repos, via: "Telegram" }, message.text ?? "", clock());
1701
- if (handed !== null) {
1702
- effects.push(async () => {
1703
- let button = null;
1704
- if (handed.link !== undefined) {
1705
- try {
1706
- button = phoneLinkButton(context.conversation?.phoneOrigin?.() ?? null, handed.link);
1707
- }
1708
- catch {
1709
- button = null;
1710
- }
1711
- }
1712
- await transport("sendMessage", { chat_id: binding.chatId, text: handed.said, link_preview_options: { is_disabled: true }, reply_parameters: { message_id: message.message_id },
1713
- ...(button === null ? {} : { reply_markup: { inline_keyboard: [button] } }) });
1714
- });
1715
- return;
1716
- }
1717
- }
1696
+ // D5: a message naming a subagent ("@rosa …") goes to the lead like any other, which asks the subagent with a card.
1718
1697
  const repliedDecision = binding !== null && message.reply_to_message !== undefined
1719
1698
  ? store.decisionForTelegramMessage(binding.id, binding.chatId, String(message.reply_to_message.message_id))
1720
1699
  : null;
@@ -1799,7 +1778,7 @@ function applyConversation(context, update, effects) {
1799
1778
  report.ignored++;
1800
1779
  return;
1801
1780
  }
1802
- if (text.length > MATE_MESSAGE_MAX_CHARS) {
1781
+ if (text.length > LEAD_MESSAGE_MAX_CHARS) {
1803
1782
  say(tooLongText(text.length));
1804
1783
  report.chatRefused = (report.chatRefused ?? 0) + 1;
1805
1784
  return;
@@ -2150,7 +2129,7 @@ function applyCallback(context, update, effects) {
2150
2129
  editText(phoneFocusText(view.text), [[telegramButton("Back to the lead", "pick:lead")]]);
2151
2130
  return;
2152
2131
  }
2153
- // A teammate's question (v93): an option answers it; "Answer in words" asks for a reply.
2132
+ // A subagent's question (v93): an option answers it; "Answer in words" asks for a reply.
2154
2133
  const questionAction = store.getTelegramQuestionAction(token);
2155
2134
  if (questionAction !== null) {
2156
2135
  if (questionAction.binding !== binding.id || questionAction.chatId !== tapChat || (questionAction.messageId !== null && questionAction.messageId !== String(message.message_id))) {
@@ -2247,7 +2226,7 @@ function applyCallback(context, update, effects) {
2247
2226
  return;
2248
2227
  }
2249
2228
  const turn = askTap.turn;
2250
- const found = store.getMateTurn(turn)?.approver === binding.approver ? store.mateAskState(turn, clock()) : { state: "expired" };
2229
+ const found = store.getLeadTurn(turn)?.approver === binding.approver ? store.leadAskState(turn, clock()) : { state: "expired" };
2251
2230
  if (found.state !== "open") {
2252
2231
  const line = found.state === "answered" ? "That question was already answered." : "That question expired. Send your answer as a message.";
2253
2232
  ack(line);
@@ -22,12 +22,12 @@ export declare const TEXT_LIMITS: {
22
22
  readonly flowRef: 60;
23
23
  /** A Message zone's message, or the comment an Update zone leaves. */
24
24
  readonly flowMessage: 1000;
25
- /** Who decides a "Person decides" zone (a sign-in name), and the AI teammate who works a zone, as written. */
25
+ /** Who decides a "Person decides" zone (a sign-in name), and the subagent who works a zone, as written. */
26
26
  readonly flowDecider: 64;
27
- readonly flowTeammate: 40;
27
+ readonly flowSubagent: 40;
28
28
  /** A Build zone's other project: its path. */
29
29
  readonly flowRepo: 1000;
30
- /** A script's or teammate's answer, a choice's button, a Sort zone's answer and what it means, its question, and a score's level. */
30
+ /** A script's or subagent's answer, a choice's button, a Sort zone's answer and what it means, its question, and a score's level. */
31
31
  readonly flowAnswer: 40;
32
32
  readonly flowChoice: 40;
33
33
  readonly flowSortMeans: 200;
@@ -136,11 +136,11 @@ export declare const TEXT_LIMITS: {
136
136
  readonly decisionLabel: 120;
137
137
  readonly decisionConsequence: 500;
138
138
  readonly decisionAssignee: 120;
139
- /** A teammate's turn (src/contracts/teammate-turn.ts): the answer it picks (and each option it offers), its question, its reason and what it remembers. */
140
- readonly teammateAnswer: 60;
141
- readonly teammateQuestion: 600;
142
- readonly teammateReason: 400;
143
- readonly teammateRemember: 300;
139
+ /** A subagent's turn (src/contracts/subagent-turn.ts): the answer it picks (and each option it offers), its question, its reason and what it remembers. */
140
+ readonly subagentAnswer: 60;
141
+ readonly subagentQuestion: 600;
142
+ readonly subagentReason: 400;
143
+ readonly subagentRemember: 300;
144
144
  /** The lead's tools and the MCP gateway (src/contracts/lead-tools.ts, gateway-tools.ts): a task's id as a tool names
145
145
  * it, a task's title and one path it touches, and a project's path as the gateway names it (an assignment brief or
146
146
  * project context, and the rest of the gateway). */
@@ -175,17 +175,17 @@ export declare const TEXT_LIMITS: {
175
175
  readonly actionUrl: 500;
176
176
  readonly actionSecret: 64;
177
177
  readonly actionAbout: 240;
178
- /** A teammate change (propose_teammate): a soul section's title, a routine's schedule, a tool, an action, a limit's
178
+ /** A subagent change (propose_subagent): a soul section's title, a routine's schedule, a tool, an action, a limit's
179
179
  * field, a name, a soul file, a note, a choice and text. */
180
- readonly teammateSection: 60;
181
- readonly teammateSchedule: 80;
182
- readonly teammateTool: 40;
183
- readonly teammateAction: 64;
184
- readonly teammateName: 40;
185
- readonly teammateSoul: 12000;
186
- readonly teammateNote: 300;
187
- readonly teammateChoice: 60;
188
- readonly teammateText: 2000;
180
+ readonly subagentSection: 60;
181
+ readonly subagentSchedule: 80;
182
+ readonly subagentTool: 40;
183
+ readonly subagentAction: 64;
184
+ readonly subagentName: 40;
185
+ readonly subagentSoul: 12000;
186
+ readonly subagentNote: 300;
187
+ readonly subagentChoice: 60;
188
+ readonly subagentText: 2000;
189
189
  /** A project knowledge reference id, a promise and why it is released, what the lead remembers (its text, a
190
190
  * decision's reason and source), a person's name, and a question to the owner with its options. */
191
191
  readonly knowledgeReference: 20;
@@ -22,12 +22,12 @@ export const TEXT_LIMITS = {
22
22
  flowRef: 60,
23
23
  /** A Message zone's message, or the comment an Update zone leaves. */
24
24
  flowMessage: 1_000,
25
- /** Who decides a "Person decides" zone (a sign-in name), and the AI teammate who works a zone, as written. */
25
+ /** Who decides a "Person decides" zone (a sign-in name), and the subagent who works a zone, as written. */
26
26
  flowDecider: 64,
27
- flowTeammate: 40,
27
+ flowSubagent: 40,
28
28
  /** A Build zone's other project: its path. */
29
29
  flowRepo: 1_000,
30
- /** A script's or teammate's answer, a choice's button, a Sort zone's answer and what it means, its question, and a score's level. */
30
+ /** A script's or subagent's answer, a choice's button, a Sort zone's answer and what it means, its question, and a score's level. */
31
31
  flowAnswer: 40,
32
32
  flowChoice: 40,
33
33
  flowSortMeans: 200,
@@ -136,11 +136,11 @@ export const TEXT_LIMITS = {
136
136
  decisionLabel: 120,
137
137
  decisionConsequence: 500,
138
138
  decisionAssignee: 120,
139
- /** A teammate's turn (src/contracts/teammate-turn.ts): the answer it picks (and each option it offers), its question, its reason and what it remembers. */
140
- teammateAnswer: 60,
141
- teammateQuestion: 600,
142
- teammateReason: 400,
143
- teammateRemember: 300,
139
+ /** A subagent's turn (src/contracts/subagent-turn.ts): the answer it picks (and each option it offers), its question, its reason and what it remembers. */
140
+ subagentAnswer: 60,
141
+ subagentQuestion: 600,
142
+ subagentReason: 400,
143
+ subagentRemember: 300,
144
144
  /** The lead's tools and the MCP gateway (src/contracts/lead-tools.ts, gateway-tools.ts): a task's id as a tool names
145
145
  * it, a task's title and one path it touches, and a project's path as the gateway names it (an assignment brief or
146
146
  * project context, and the rest of the gateway). */
@@ -175,17 +175,17 @@ export const TEXT_LIMITS = {
175
175
  actionUrl: 500,
176
176
  actionSecret: 64,
177
177
  actionAbout: 240,
178
- /** A teammate change (propose_teammate): a soul section's title, a routine's schedule, a tool, an action, a limit's
178
+ /** A subagent change (propose_subagent): a soul section's title, a routine's schedule, a tool, an action, a limit's
179
179
  * field, a name, a soul file, a note, a choice and text. */
180
- teammateSection: 60,
181
- teammateSchedule: 80,
182
- teammateTool: 40,
183
- teammateAction: 64,
184
- teammateName: 40,
185
- teammateSoul: 12_000,
186
- teammateNote: 300,
187
- teammateChoice: 60,
188
- teammateText: 2_000,
180
+ subagentSection: 60,
181
+ subagentSchedule: 80,
182
+ subagentTool: 40,
183
+ subagentAction: 64,
184
+ subagentName: 40,
185
+ subagentSoul: 12_000,
186
+ subagentNote: 300,
187
+ subagentChoice: 60,
188
+ subagentText: 2_000,
189
189
  /** A project knowledge reference id, a promise and why it is released, what the lead remembers (its text, a
190
190
  * decision's reason and source), a person's name, and a question to the owner with its options. */
191
191
  knowledgeReference: 20,
@@ -137,6 +137,14 @@ export type TableDigest = {
137
137
  rows: number;
138
138
  found: number;
139
139
  };
140
+ compactedRuns?: {
141
+ run: number;
142
+ count: number;
143
+ }[];
144
+ summaries?: {
145
+ run: number;
146
+ hash: string;
147
+ }[];
140
148
  };
141
149
  /** Each saved table's columns, row count and last rowid; the append-only ones hashed (deploy-browser's rehearsal check).
142
150
  * Bounded: only the ledger and small tables are read row by row. */
@@ -35,7 +35,7 @@ import { processMayBeAlive } from "./process-liveness.js";
35
35
  import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
36
36
  import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
37
37
  import { NAME } from "./names.js";
38
- import { updateSafeSchema } from "./store.js";
38
+ import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES } from "./store.js";
39
39
  import { isNewer, REGISTRY } from "./releases.js";
40
40
  import { markNeverIndex } from "./never-index.js";
41
41
  import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
@@ -322,7 +322,8 @@ export function durableRename(temp, target) {
322
322
  * only when every saved value is null, or, with `carried`, when the receiving table gained exactly one receipt under
323
323
  * that destination per row holding a value, and as many non-null values per column as the dropped columns held.
324
324
  * `receives`: the only rows the table may gain are those receipts. `compacted`: rows may go only into summary rows,
325
- * whose `sum` grows by exactly as many. `retired`: a removed feature's table, which may go whole. `fromRoutines`: rows
325
+ * whose `sum` grows by exactly as many. `summarizes`: old summaries stay identical by run key; new ones account for
326
+ * that run's compacted witnesses, even below the previous maximum key. `retired`: a removed feature's table, which may go whole. `fromRoutines`: rows
326
327
  * kept as they were, and the only rows the table may gain are one per saved routine, moved into a scheduled flow (its
327
328
  * flow, or its schedule trigger naming that routine) — on only when the routine was approved and running. `moved`:
328
329
  * the table may go only when every row it had (or the ones `rows` counts) arrived in `into`: the rows there that
@@ -366,7 +367,7 @@ const HISTORY_RULES = {
366
367
  ...Object.fromEntries(Object.entries(CHAT_MOVES).map(([table, moved]) => [table, { moved }])),
367
368
  action_ledger: { appendOnly: true }, ledger_seal: { appendOnly: true }, ledger_checkpoint: { appendOnly: true },
368
369
  // v114: a finished run whose process witnesses have all exited keeps one summary row in place of them.
369
- run_process: { compacted: { into: "run_process_summary", sum: "witnesses" } }, run_process_summary: { appendOnly: true },
370
+ run_process: { compacted: { into: "run_process_summary", sum: "witnesses" } }, run_process_summary: { summarizes: "run_process" },
370
371
  // v114: single-destination delivery columns may be dropped only once their values are legacy receipts.
371
372
  notification: { dropped: ["attempts", "last_attempt_at", "last_error", "delivered_at", "receipt", "claim_owner", "claim_expires_at"], carried: { into: "notification_delivery", destination: LEGACY_DESTINATION } },
372
373
  notification_delivery: { receives: LEGACY_DESTINATION },
@@ -377,11 +378,36 @@ const HISTORY_RULES = {
377
378
  fallback_config: { retired: true }, fallback_cycle: { retired: true }, fallback_transition: { retired: true },
378
379
  // v115: each routine becomes one scheduled flow and its schedule trigger.
379
380
  flow: { fromRoutines: "flow" }, flow_trigger: { fromRoutines: "trigger" },
381
+ // v117 (D5): the lead's and the subagents' tables keep every row under their new names (mate_* → lead_*, teammate* →
382
+ // subagent*); a column renamed in a table that keeps its name keeps its values under the new one.
383
+ ...Object.fromEntries(Object.entries(V117_RENAMED_TABLES).map(([old, now]) => [old, { moved: { into: now, where: "1" } }])),
384
+ ...Object.fromEntries(V117_RENAMED_COLUMNS.filter(([table]) => !Object.values(V117_RENAMED_TABLES).includes(table)).map(([table, from, to]) => [table, { renamed: { [from]: to } }])),
380
385
  };
381
386
  /** The tables a migration may remove whole (v115's removed features). */
382
387
  export const RETIRED_TABLES = Object.freeze(Object.keys(HISTORY_RULES).filter(name => HISTORY_RULES[name].retired));
383
388
  const tableColumns = (db, name) => db.prepare("PRAGMA table_info(" + quote(name) + ")").all().map(r => String(r["name"]));
384
389
  const plainValue = (_, v) => v instanceof Uint8Array ? Buffer.from(v).toString("hex") : typeof v === "bigint" ? String(v) : v;
390
+ const rowHash = (row) => createHash("sha256").update(JSON.stringify(Object.values(row), plainValue)).digest("hex");
391
+ const summaryRows = (db, name, columns) => db.prepare("SELECT " + columns.map(quote).join(",") + " FROM " + quote(name) + " ORDER BY run").iterate();
392
+ /** Keep every saved summary byte-identical. A new key must replace all witnesses saved for that same run. */
393
+ function keptSummaries(db, name, before, source) {
394
+ const saved = new Map(before?.summaries?.map(row => [row.run, row.hash]));
395
+ const witnesses = new Map(source?.compactedRuns?.map(row => [row.run, row.count]));
396
+ const from = HISTORY_RULES[name].summarizes, sum = HISTORY_RULES[from].compacted.sum;
397
+ const remaining = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(from)
398
+ ? db.prepare("SELECT 1 FROM " + quote(from) + " WHERE run = ? LIMIT 1") : undefined;
399
+ for (const row of summaryRows(db, name, before?.columns ?? tableColumns(db, name))) {
400
+ const run = Number(row["run"]), hash = saved.get(run);
401
+ if (hash !== undefined) {
402
+ if (rowHash(row) !== hash)
403
+ return false;
404
+ saved.delete(run);
405
+ }
406
+ else if (!witnesses.has(run) || Number(row[sum]) !== witnesses.get(run) || !remaining || remaining.get(run))
407
+ return false;
408
+ }
409
+ return saved.size === 0;
410
+ }
385
411
  /** Rows streamed into a sha256, never held together: through a rowid in rowid order, or a WITHOUT ROWID table whole. */
386
412
  function rowsHash(db, name, columns, through) {
387
413
  const select = "SELECT " + (through === undefined ? "" : 'rowid AS "rowid:", ') + columns.map(quote).join(",") + " FROM " + quote(name);
@@ -423,8 +449,13 @@ export function historySnapshot(db) {
423
449
  t.received = receipts(db, name, rule.receives, columns.filter(column => column !== "notification" && column !== "destination"));
424
450
  if (rule?.appendOnly || rule?.fromRoutines)
425
451
  t.hash = rowsHash(db, name, columns, t.last).hash;
426
- if (rule?.compacted)
452
+ if (rule?.compacted) {
427
453
  t.summed = summed(db, rule.compacted);
454
+ t.compactedRuns = db.prepare("SELECT run, count(*) AS n FROM " + quote(name) + " GROUP BY run ORDER BY run").all()
455
+ .map(r => ({ run: Number(r["run"]), count: Number(r["n"]) }));
456
+ }
457
+ if (rule?.summarizes)
458
+ t.summaries = [...summaryRows(db, name, columns)].map(row => ({ run: Number(row["run"]), hash: rowHash(row) }));
428
459
  if (rule?.fromRoutines)
429
460
  t.routines = savedRoutines(db);
430
461
  if (rule?.moved)
@@ -500,7 +531,8 @@ export function changedHistory(db, before) {
500
531
  return move === undefined || t.moved === undefined || arrived(db, move) - t.moved.found !== t.moved.rows;
501
532
  }
502
533
  const rule = HISTORY_RULES[t.name], present = new Set(tableColumns(db, t.name));
503
- if (t.columns.some(c => !present.has(c) && (!rule?.dropped?.includes(c) || (t.nonNull?.[c] !== 0 && !accounted(rule, t)))))
534
+ const kept = (c) => present.has(c) || (rule?.renamed?.[c] !== undefined && present.has(rule.renamed[c]));
535
+ if (t.columns.some(c => !kept(c) && (!rule?.dropped?.includes(c) || (t.nonNull?.[c] !== 0 && !accounted(rule, t)))))
504
536
  return true;
505
537
  const columns = t.columns.filter(c => present.has(c));
506
538
  if (!t.rowid) {
@@ -515,9 +547,13 @@ export function changedHistory(db, before) {
515
547
  const after = rowsHash(db, t.name, columns, t.last);
516
548
  return after.count !== t.count || after.hash !== t.hash;
517
549
  }
550
+ if (rule?.summarizes)
551
+ return !keptSummaries(db, t.name, t, before.find(one => one.name === rule.summarizes));
518
552
  const r = db.prepare("SELECT count(*) AS n, coalesce(max(rowid),0) AS last FROM " + quote(t.name)).get(), count = Number(r["n"]), last = Number(r["last"]);
519
553
  if (rule?.compacted)
520
- return count > t.count || last > t.last || t.count - count !== summed(db, rule.compacted) - (t.summed ?? 0);
554
+ return count > t.count || last > t.last || t.count - count !== summed(db, rule.compacted) - (t.summed ?? 0)
555
+ // A newly created summary table has no digest of its own; check its keys here too.
556
+ || (!before.some(one => one.name === rule.compacted.into) && tables.has(rule.compacted.into) && !keptSummaries(db, rule.compacted.into, undefined, t));
521
557
  if (rule?.receives) {
522
558
  const added = gained.get(t.name)?.count ?? 0;
523
559
  return count - t.count !== added || (added > 0 && added !== owed.get(t.name)?.count) || last < t.last;
@@ -531,8 +567,9 @@ async function rehearse(j, system, source) {
531
567
  chmodSync(copy, 0o600);
532
568
  try {
533
569
  let db = new (sqlite().DatabaseSync)(copy, { readOnly: true });
534
- // Rows already pointing nowhere before the update are the install's own; only ones the migration makes refuse.
535
- const orphans = () => new Set([...db.prepare("PRAGMA foreign_key_check").iterate()].map(row => JSON.stringify(row)));
570
+ // Rows already pointing nowhere before the update are the install's own; only ones the migration makes refuse. (By
571
+ // today's table names: a renamed table's old orphan is the same row under its new name, orphanKey.)
572
+ const orphans = () => new Set([...db.prepare("PRAGMA foreign_key_check").iterate()].map(row => orphanKey(row)));
536
573
  const before = historySnapshot(db), orphaned = orphans();
537
574
  db.close();
538
575
  await system.rehearse(j.to.dist, copy);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.55",
3
+ "version": "0.9.56",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"
@@ -17,7 +17,6 @@
17
17
  "prepublishOnly": "npm run typecheck && npm run build",
18
18
  "nul-scan": "node scripts/nul-scan.mjs",
19
19
  "proof-preflight": "node scripts/proof-preflight.mjs",
20
- "fixture:v47": "node scripts/derive-v47-fixture.mjs",
21
20
  "build:desktop": "node scripts/build-desktop.mjs",
22
21
  "certify:restart": "npm run build && node scripts/restart-certification.mjs",
23
22
  "certify:launchd": "npm run build && node scripts/launchd-certification.mjs",