toolroll 0.9.52 → 0.9.54

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 (187) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +55 -80
  3. package/dist/agentconfig.d.ts +11 -36
  4. package/dist/agentconfig.js +3 -62
  5. package/dist/approval-policy.d.ts +1 -1
  6. package/dist/approval-policy.js +1 -1
  7. package/dist/board.d.ts +3 -19
  8. package/dist/board.js +4 -42
  9. package/dist/browser/workspace.js +18 -18
  10. package/dist/browser-workspace.d.ts +2 -1
  11. package/dist/builder.d.ts +9 -40
  12. package/dist/builder.js +50 -257
  13. package/dist/chat-actions.js +3 -14
  14. package/dist/chat-ask.js +8 -8
  15. package/dist/chat-controls.d.ts +0 -4
  16. package/dist/chat-controls.js +0 -1
  17. package/dist/chat-core.d.ts +171 -0
  18. package/dist/chat-core.js +288 -0
  19. package/dist/chat-decide.d.ts +10 -8
  20. package/dist/chat-decide.js +27 -24
  21. package/dist/chat-delivery-state.d.ts +51 -75
  22. package/dist/chat-delivery-state.js +140 -170
  23. package/dist/chat-delivery.d.ts +21 -4
  24. package/dist/chat-delivery.js +313 -195
  25. package/dist/chat-flow.d.ts +29 -1
  26. package/dist/chat-flow.js +24 -50
  27. package/dist/chat-messages.d.ts +319 -0
  28. package/dist/chat-messages.js +841 -0
  29. package/dist/chat-migration.d.ts +30 -0
  30. package/dist/chat-migration.js +802 -0
  31. package/dist/chat-quiet.d.ts +3 -0
  32. package/dist/chat-quiet.js +7 -2
  33. package/dist/chat-task-actions.d.ts +1 -1
  34. package/dist/chat-task-actions.js +0 -2
  35. package/dist/claim.d.ts +79 -244
  36. package/dist/claim.js +688 -1023
  37. package/dist/cli.d.ts +2 -0
  38. package/dist/cli.js +10 -2
  39. package/dist/contracts/chat-content.d.ts +71 -2
  40. package/dist/contracts/chat-content.js +22 -3
  41. package/dist/contracts/chat-tables.d.ts +11 -4
  42. package/dist/contracts/chat-tables.js +165 -87
  43. package/dist/contracts/cli.d.ts +0 -13
  44. package/dist/contracts/cli.js +0 -2
  45. package/dist/contracts/console-api.d.ts +5 -10
  46. package/dist/contracts/console-api.js +4 -10
  47. package/dist/contracts/discord-callback.js +2 -1
  48. package/dist/contracts/flow.d.ts +50 -0
  49. package/dist/contracts/flow.js +26 -1
  50. package/dist/contracts/lead-tools.d.ts +0 -13
  51. package/dist/contracts/lead-tools.js +3 -3
  52. package/dist/contracts/slack-callback.js +3 -2
  53. package/dist/contracts/store-json.d.ts +1 -29
  54. package/dist/contracts/store-json.js +4 -17
  55. package/dist/contracts/teams-callback.js +2 -1
  56. package/dist/converse.d.ts +1 -11
  57. package/dist/converse.js +6 -38
  58. package/dist/coordinator.d.ts +0 -1
  59. package/dist/coordinator.js +3 -5
  60. package/dist/demo.js +16 -26
  61. package/dist/desktop-update-gate.js +0 -1
  62. package/dist/desktop-update.js +1 -3
  63. package/dist/discord-chat.js +19 -38
  64. package/dist/discord-settings.js +4 -13
  65. package/dist/discord.js +4 -14
  66. package/dist/dispatch.d.ts +1 -1
  67. package/dist/dispatch.js +1 -1
  68. package/dist/dispose.d.ts +2 -3
  69. package/dist/dispose.js +17 -92
  70. package/dist/envelope.js +0 -3
  71. package/dist/exec.d.ts +0 -64
  72. package/dist/exec.js +3 -356
  73. package/dist/exhaustion.d.ts +22 -76
  74. package/dist/exhaustion.js +14 -140
  75. package/dist/export.js +8 -8
  76. package/dist/flow-engine.js +1 -1
  77. package/dist/flow-schedule.d.ts +154 -0
  78. package/dist/flow-schedule.js +321 -0
  79. package/dist/flow-share.js +1 -1
  80. package/dist/flow-triggers.d.ts +6 -1
  81. package/dist/flow-triggers.js +65 -10
  82. package/dist/flows-cli.js +8 -4
  83. package/dist/flows-ui.js +2 -0
  84. package/dist/flows.js +1 -1
  85. package/dist/guides.js +9 -29
  86. package/dist/invoke.d.ts +1 -24
  87. package/dist/invoke.js +17 -259
  88. package/dist/lead-status.js +2 -4
  89. package/dist/lead-voice.js +1 -1
  90. package/dist/mate-doors.d.ts +1 -2
  91. package/dist/mate-doors.js +12 -25
  92. package/dist/mate-tools.d.ts +2 -2
  93. package/dist/mate-tools.js +13 -24
  94. package/dist/modes.d.ts +19 -8
  95. package/dist/modes.js +24 -14
  96. package/dist/notification-rows.d.ts +30 -0
  97. package/dist/notification-rows.js +102 -0
  98. package/dist/operate-remote-arguments.js +2 -6
  99. package/dist/operate-remote.js +6 -7
  100. package/dist/operate.d.ts +0 -6
  101. package/dist/operate.js +226 -2248
  102. package/dist/phase-routing.d.ts +13 -23
  103. package/dist/phase-routing.js +13 -35
  104. package/dist/plan-auto.js +2 -2
  105. package/dist/plane-review.js +5 -11
  106. package/dist/planner-source.js +2 -1
  107. package/dist/policy.d.ts +1 -4
  108. package/dist/policy.js +0 -8
  109. package/dist/process-recovery-eligibility.js +2 -3
  110. package/dist/project-delete.js +24 -41
  111. package/dist/proposal.d.ts +4 -24
  112. package/dist/proposal.js +3 -88
  113. package/dist/provider-auth.d.ts +3 -3
  114. package/dist/provider-auth.js +4 -6
  115. package/dist/provider.d.ts +11 -25
  116. package/dist/provider.js +4 -36
  117. package/dist/push.js +1 -1
  118. package/dist/recipe-ui.js +6 -6
  119. package/dist/recipes.d.ts +3 -3
  120. package/dist/recipes.js +27 -23
  121. package/dist/result-shots.js +2 -2
  122. package/dist/retention.js +1 -1
  123. package/dist/run-closeout.d.ts +1 -1
  124. package/dist/run-closeout.js +1 -4
  125. package/dist/scope.d.ts +11 -65
  126. package/dist/scope.js +17 -82
  127. package/dist/serve.js +15 -24
  128. package/dist/server/chat.js +7 -30
  129. package/dist/server/flows.js +11 -10
  130. package/dist/server/guards.js +0 -1
  131. package/dist/server/pages-assets.js +1 -1
  132. package/dist/server/pages.js +0 -2
  133. package/dist/server/people-tokens.js +0 -4
  134. package/dist/server/route-table.d.ts +1 -1
  135. package/dist/server/route-table.js +2 -17
  136. package/dist/server/settings.js +6 -12
  137. package/dist/server/shared.d.ts +28 -212
  138. package/dist/server/shared.js +71 -780
  139. package/dist/server/task-views.js +9 -76
  140. package/dist/server/tasks.d.ts +1 -50
  141. package/dist/server/tasks.js +36 -946
  142. package/dist/setup-guide.d.ts +2 -2
  143. package/dist/slack-api.d.ts +5 -1
  144. package/dist/slack-api.js +2 -2
  145. package/dist/slack-chat.d.ts +9 -6
  146. package/dist/slack-chat.js +29 -53
  147. package/dist/slack-settings.js +6 -15
  148. package/dist/slack.js +6 -16
  149. package/dist/store.d.ts +200 -1702
  150. package/dist/store.js +811 -5439
  151. package/dist/surface.js +10 -24
  152. package/dist/task-composer.d.ts +1 -1
  153. package/dist/task-composer.js +1 -1
  154. package/dist/task-control.d.ts +3 -10
  155. package/dist/task-control.js +2 -7
  156. package/dist/teammate-desk.js +1 -1
  157. package/dist/teammate-question.js +12 -12
  158. package/dist/teammates-ui.js +1 -1
  159. package/dist/teams-chat.js +12 -9
  160. package/dist/teams-settings.js +2 -2
  161. package/dist/teams.js +4 -5
  162. package/dist/telegram-flow.js +5 -5
  163. package/dist/telegram-mate.d.ts +0 -3
  164. package/dist/telegram-mate.js +0 -1
  165. package/dist/telegram.js +4 -4
  166. package/dist/templates.d.ts +11 -7
  167. package/dist/templates.js +12 -9
  168. package/dist/toolroll-update.d.ts +20 -0
  169. package/dist/toolroll-update.js +104 -5
  170. package/dist/work-index.js +2 -6
  171. package/dist/workspace-revision.js +3 -1
  172. package/dist/workspace-ui.js +1 -1
  173. package/package.json +1 -1
  174. package/scripts/recovery-canary.mjs +3 -3
  175. package/dist/contest.d.ts +0 -318
  176. package/dist/contest.js +0 -753
  177. package/dist/held.d.ts +0 -150
  178. package/dist/held.js +0 -799
  179. package/dist/liveness.d.ts +0 -30
  180. package/dist/liveness.js +0 -42
  181. package/dist/pricing.d.ts +0 -45
  182. package/dist/pricing.js +0 -77
  183. package/dist/routine.d.ts +0 -228
  184. package/dist/routine.js +0 -764
  185. package/dist/slack-state.d.ts +0 -13
  186. package/dist/slack-state.js +0 -10
  187. package/dist/supervisor.mjs +0 -297
@@ -9,8 +9,9 @@
9
9
  * their own chat; otherwise, and for a plan that widens permissions, exceeds the limit or touches protected paths, the
10
10
  * card keeps its link.
11
11
  *
12
- * Channel-agnostic: Telegram renders these today, and Slack, Discord and Teams reach the same acts through the same
13
- * tokens (the `channel` column). Every act runs through the door the console uses. */
12
+ * The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
13
+ * offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
14
+ * console uses. */
14
15
  import { DECIDE_ACTS } from "./contracts/chat-callback-rows.js";
15
16
  import { type ChatApprovalLimits } from "./chat-approval.js";
16
17
  import { type VerifiedApprover } from "./principal.js";
@@ -78,10 +79,11 @@ export type DecideOffer = {
78
79
  text: string | null;
79
80
  rows: Array<Array<ActSpec | DecideLink>>;
80
81
  };
81
- /** What lets this person's chat approve on this repository now: their lasting chat-approval setting (chat-approval.ts),
82
- * or else a live signed mode carrying the chatApprove term, signed by them. `digest` names that authority for the
82
+ /** What lets this person's chat on `channel` approve on this repository now: their lasting chat-approval setting
83
+ * (chat-approval.ts), or else a live signed mode carrying the chatApprove term, signed by them, that names this chat
84
+ * app (a mode signed before the term named its apps covers Telegram alone). `digest` names that authority for the
83
85
  * ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
84
- export declare function chatApproveMode(store: Store, repo: string | null, approver: string, now: Date): {
86
+ export declare function chatApproveMode(store: Store, repo: string | null, approver: string, now: Date, channel: DecideChannel): {
85
87
  ok: true;
86
88
  digest: string;
87
89
  limits: ChatApprovalLimits;
@@ -94,7 +96,7 @@ export declare function chatApproveMode(store: Store, repo: string | null, appro
94
96
  export declare function permissionPlainWords(profile: Scope["profile"] | null | undefined): string | null;
95
97
  /** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
96
98
  * Toolroll instead. `digest` binds the scope and the plan document the card shows. */
97
- export declare function planInChat(store: Store, taskId: string, approver: string, now: Date, root?: string): {
99
+ export declare function planInChat(store: Store, taskId: string, approver: string, now: Date, channel: DecideChannel, root?: string): {
98
100
  ok: true;
99
101
  modeDigest: string;
100
102
  source: "setting" | "mode";
@@ -105,7 +107,7 @@ export declare function planInChat(store: Store, taskId: string, approver: strin
105
107
  why: string;
106
108
  };
107
109
  /** A ready pull request this person's chat may merge now, bound to the exact commit. */
108
- export declare function mergeInChat(store: Store, taskId: string, run: number, approver: string, now: Date): {
110
+ export declare function mergeInChat(store: Store, taskId: string, run: number, approver: string, now: Date, channel: DecideChannel): {
109
111
  ok: true;
110
112
  modeDigest: string;
111
113
  source: "setting" | "mode";
@@ -119,7 +121,7 @@ export declare function mergeInChat(store: Store, taskId: string, run: number, a
119
121
  export declare function planCardText(store: Store, taskId: string, scope: Scope, root?: string, channel?: DecideChannel): string;
120
122
  /** The acts a card about this target can take in chat right now, or null when none can (the card keeps its own
121
123
  * link). Reads only; nothing is minted. */
122
- export declare function decideOffer(store: Store, target: DecideTarget, who: VerifiedApprover, now: Date, root?: string, channel?: DecideChannel): DecideOffer | null;
124
+ export declare function decideOffer(store: Store, target: DecideTarget, who: VerifiedApprover, now: Date, channel: DecideChannel, root?: string): DecideOffer | null;
123
125
  /** One fingerprint of what the card offers, without its tokens: a repaint with the same offer keeps its buttons. */
124
126
  export declare function offerFingerprint(offer: DecideOffer): string;
125
127
  /** The card's buttons, minted now (message unknown until it is sent: `placeDecideTokens` puts them on it). */
@@ -9,8 +9,9 @@
9
9
  * their own chat; otherwise, and for a plan that widens permissions, exceeds the limit or touches protected paths, the
10
10
  * card keeps its link.
11
11
  *
12
- * Channel-agnostic: Telegram renders these today, and Slack, Discord and Teams reach the same acts through the same
13
- * tokens (the `channel` column). Every act runs through the door the console uses. */
12
+ * The same in every chat app: Telegram's bridge and the shared engine Slack, Discord and Teams run on (chat-delivery.ts)
13
+ * offer, mint and answer these through the same tokens (the `channel` column). Every act runs through the door the
14
+ * console uses. */
14
15
  import { DECIDE_ACTS, readDecideActionRow } from "./contracts/chat-callback-rows.js";
15
16
  import { createHash, randomBytes } from "node:crypto";
16
17
  import { acceptAndCompleteAsOperator, assignmentOf, owedAcceptance } from "./assignment.js";
@@ -19,7 +20,7 @@ import { applyChatTaskAction, chatTaskStamp } from "./chat-task-actions.js";
19
20
  import { chatTitle } from "./chat-voice.js";
20
21
  import { readVerifiedArtifact } from "./evidence.js";
21
22
  import { effectiveChatApproval } from "./chat-approval.js";
22
- import { modeTermsFromJson } from "./modes.js";
23
+ import { chatApproveChatsOf, chatNames, modeTermsFromJson } from "./modes.js";
23
24
  import { parseExecutionPlanDocument } from "./plan.js";
24
25
  import { verifyApproverStanding } from "./principal.js";
25
26
  import { pullRequestFollowOf } from "./pull-request-flow.js";
@@ -47,10 +48,11 @@ const sha = (text) => createHash("sha256").update(text).digest("hex");
47
48
  const isAct = (one) => "act" in one;
48
49
  const title = (store, taskId) => phoneText(chatTitle(store, taskId), 60);
49
50
  // ---- the chatApprove term ----------------------------------------------------------
50
- /** What lets this person's chat approve on this repository now: their lasting chat-approval setting (chat-approval.ts),
51
- * or else a live signed mode carrying the chatApprove term, signed by them. `digest` names that authority for the
51
+ /** What lets this person's chat on `channel` approve on this repository now: their lasting chat-approval setting
52
+ * (chat-approval.ts), or else a live signed mode carrying the chatApprove term, signed by them, that names this chat
53
+ * app (a mode signed before the term named its apps covers Telegram alone). `digest` names that authority for the
52
54
  * ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
53
- export function chatApproveMode(store, repo, approver, now) {
55
+ export function chatApproveMode(store, repo, approver, now, channel) {
54
56
  if (repo === null)
55
57
  return { ok: false, why: "Approving from chat isn't turned on for this project." };
56
58
  const setting = effectiveChatApproval(store, repo, approver);
@@ -62,6 +64,9 @@ export function chatApproveMode(store, repo, approver, now) {
62
64
  return setting;
63
65
  if (mode.signedBy !== approver || !store.accountCanAccess(approver, repo))
64
66
  return { ok: false, why: "Only the person who signed this project's mode can approve from chat." };
67
+ const chats = chatApproveChatsOf(terms);
68
+ if (!chats.includes(channel))
69
+ return { ok: false, why: `Your mode approves from ${chatNames(chats)} only. Sign it again to approve from ${chatNames([channel])}.` };
65
70
  return { ok: true, digest: mode.digest, limits: { fullAccess: terms.permissionDefault === "escalated", capMicrousd: terms.perAttemptBudgetMicrousd }, source: "mode" };
66
71
  }
67
72
  /** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
@@ -87,7 +92,7 @@ function fullAccess(profile) {
87
92
  const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
88
93
  /** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
89
94
  * Toolroll instead. `digest` binds the scope and the plan document the card shows. */
90
- export function planInChat(store, taskId, approver, now, root) {
95
+ export function planInChat(store, taskId, approver, now, channel, root) {
91
96
  const ref = store.lookupRef(taskId);
92
97
  const scope = store.getScope(taskId);
93
98
  if (ref === null || ref.repo === null || scope === null)
@@ -96,7 +101,7 @@ export function planInChat(store, taskId, approver, now, root) {
96
101
  return { ok: false, why: "This plan is already approved." };
97
102
  if (ref.plan === "requested")
98
103
  return { ok: false, why: "The plan is still being written." };
99
- const mode = chatApproveMode(store, ref.repo, approver, now);
104
+ const mode = chatApproveMode(store, ref.repo, approver, now, channel);
100
105
  if (!mode.ok)
101
106
  return mode;
102
107
  if (store.getTask(taskId)?.state === "cancelled" || store.getTask(taskId)?.state === "done")
@@ -104,11 +109,9 @@ export function planInChat(store, taskId, approver, now, root) {
104
109
  const family = store.taskFamilyOf(taskId, [ref.repo], false);
105
110
  if (family === null || family.problem !== null || family.current.id !== taskId)
106
111
  return { ok: false, why: "A newer version of this task is current." };
107
- // What the mode's seal refuses anyway, said first: model- or coordinator-written terms, a race, an unreadable route.
112
+ // What the mode's seal refuses anyway, said first: model- or coordinator-written terms, an unreadable route.
108
113
  if (ref.coordinatorCid !== null || (scope.proposedVia ?? null) !== null)
109
114
  return { ok: false, why: "This plan was written for you, so you approve it in Toolroll." };
110
- if (store.activeTournamentTerms(ref.id) !== null)
111
- return { ok: false, why: "Agents compete on this task, so you approve it in Toolroll." };
112
115
  if (scope.profileState === "unresolved" || scope.routeEra == null || (scope.termsProblem ?? null) !== null)
113
116
  return { ok: false, why: "This plan can't say exactly which agents run, so you approve it in Toolroll." };
114
117
  const yours = mode.source === "setting" ? "your chat approval setting" : "your mode";
@@ -150,7 +153,7 @@ export function planInChat(store, taskId, approver, now, root) {
150
153
  return { ok: true, modeDigest: mode.digest, source: mode.source, scope, digest: sha(`${scope.digest}:${plan?.sha256 ?? ""}`) };
151
154
  }
152
155
  /** A ready pull request this person's chat may merge now, bound to the exact commit. */
153
- export function mergeInChat(store, taskId, run, approver, now) {
156
+ export function mergeInChat(store, taskId, run, approver, now, channel) {
154
157
  const ref = store.lookupRef(taskId);
155
158
  const publication = store.publicationForRun(run);
156
159
  const follow = publication === null ? null : pullRequestFollowOf(store, publication.id);
@@ -160,7 +163,7 @@ export function mergeInChat(store, taskId, run, approver, now) {
160
163
  return { ok: false, why: "This pull request is already merged." };
161
164
  if (follow.readyHead !== publication.headSha)
162
165
  return { ok: false, why: "This pull request changed since this card was sent." };
163
- const mode = chatApproveMode(store, ref.repo, approver, now);
166
+ const mode = chatApproveMode(store, ref.repo, approver, now, channel);
164
167
  if (!mode.ok)
165
168
  return mode;
166
169
  return { ok: true, modeDigest: mode.digest, source: mode.source, head: publication.headSha };
@@ -218,7 +221,7 @@ export function planCardText(store, taskId, scope, root, channel) {
218
221
  // ---- what a card offers ---------------------------------------------------------------
219
222
  /** The acts a card about this target can take in chat right now, or null when none can (the card keeps its own
220
223
  * link). Reads only; nothing is minted. */
221
- export function decideOffer(store, target, who, now, root, channel) {
224
+ export function decideOffer(store, target, who, now, channel, root) {
222
225
  const ref = store.lookupRef(target.taskId);
223
226
  if (ref === null || ref.repo === null || !who.repos.includes(ref.repo))
224
227
  return null;
@@ -243,14 +246,14 @@ export function decideOffer(store, target, who, now, root, channel) {
243
246
  return { text: null, rows: [[{ act: "retry", run: null, digest: stamp }], [{ label: DECIDE_LABELS.look, path: chatControlHref("task", target.taskId) }]] };
244
247
  }
245
248
  case "plan": {
246
- const plan = planInChat(store, target.taskId, who.name, now, root);
249
+ const plan = planInChat(store, target.taskId, who.name, now, channel, root);
247
250
  if (!plan.ok)
248
251
  return null;
249
252
  return { text: planCardText(store, target.taskId, plan.scope, root, channel),
250
253
  rows: [[{ act: "approve", run: null, digest: plan.digest }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: plan.digest }]] };
251
254
  }
252
255
  case "merge": {
253
- const merge = mergeInChat(store, target.taskId, target.run, who.name, now);
256
+ const merge = mergeInChat(store, target.taskId, target.run, who.name, now, channel);
254
257
  if (!merge.ok)
255
258
  return null;
256
259
  return { text: null, rows: [[{ act: "merge", run: target.run, digest: merge.head }], [{ label: DECIDE_LABELS.look, path: chatResultHref(target.taskId, target.run, "changes") }]] };
@@ -350,7 +353,7 @@ function targetOf(row) {
350
353
  }
351
354
  }
352
355
  /** Why the card's act no longer applies, or null while it still does — checked at the offer and again at the yes. */
353
- function staleWhy(store, row, who, now, root) {
356
+ function staleWhy(store, row, who, now, channel, root) {
354
357
  switch (row.act) {
355
358
  case "accept":
356
359
  case "changes": {
@@ -368,13 +371,13 @@ function staleWhy(store, row, who, now, root) {
368
371
  }
369
372
  case "approve":
370
373
  case "not-now": {
371
- const plan = planInChat(store, row.taskId, who.name, now, root);
374
+ const plan = planInChat(store, row.taskId, who.name, now, channel, root);
372
375
  if (!plan.ok)
373
376
  return plan.why;
374
377
  return plan.digest === row.digest ? null : "The plan changed since this card was sent.";
375
378
  }
376
379
  case "merge": {
377
- const merge = mergeInChat(store, row.taskId, row.run ?? 0, who.name, now);
380
+ const merge = mergeInChat(store, row.taskId, row.run ?? 0, who.name, now, channel);
378
381
  if (!merge.ok)
379
382
  return merge.why;
380
383
  return merge.head === row.digest ? null : "This pull request changed since this card was sent.";
@@ -461,13 +464,13 @@ export function applyDecideTap(store, seat, input) {
461
464
  }
462
465
  if (row.phase === "cancel") {
463
466
  spend();
464
- const offer = decideOffer(store, target, who, now, input.root, seat.channel);
467
+ const offer = decideOffer(store, target, who, now, seat.channel, input.root);
465
468
  if (offer === null)
466
469
  return { ack: "Cancelled.", edit: { text: body, rows: linksFor(target), tokens: [] } };
467
470
  const minted = mintDecideButtons(store, seat, target, offer, now, input.message);
468
471
  return { ack: "Cancelled.", edit: { text: offer.text ?? body, ...minted } };
469
472
  }
470
- const why = staleWhy(store, row, who, now, input.root);
473
+ const why = staleWhy(store, row, who, now, seat.channel, input.root);
471
474
  if (why !== null)
472
475
  return stale(why);
473
476
  if (row.phase === "offer") {
@@ -505,7 +508,7 @@ export function applyDecideTap(store, seat, input) {
505
508
  return retried.ok ? done("✓ Queued again. Existing approvals and holds still apply.") : notDone(retried.message);
506
509
  }
507
510
  case "approve": {
508
- const plan = planInChat(store, row.taskId, who.name, now, input.root);
511
+ const plan = planInChat(store, row.taskId, who.name, now, seat.channel, input.root);
509
512
  if (!plan.ok)
510
513
  return notDone(plan.why);
511
514
  // The owner's own yes, sealed as theirs ("chat"), never as a mode's: it stands like the console's after the mode or setting ends.
@@ -516,7 +519,7 @@ export function applyDecideTap(store, seat, input) {
516
519
  return done("✓ Approved. Work starts when a worker is free.");
517
520
  }
518
521
  case "merge": {
519
- const merge = mergeInChat(store, row.taskId, row.run ?? 0, who.name, now);
522
+ const merge = mergeInChat(store, row.taskId, row.run ?? 0, who.name, now, seat.channel);
520
523
  if (!merge.ok)
521
524
  return notDone(merge.why);
522
525
  const via = `via ${seat.channel} · chat binding #${seat.binding} · ${merge.source} ${merge.modeDigest} · ${merge.head.slice(0, 12)}`;
@@ -569,7 +572,7 @@ export function applyDecideFeedback(store, seat, prompt, text, repos, root, now)
569
572
  const verified = verifyApproverStanding(store, seat.approver, seat.generation, repos);
570
573
  if (!verified.ok)
571
574
  return { said: "That result isn't available here now. Nothing was sent.", link: null };
572
- const why = staleWhy(store, { act: "changes", taskId: prompt.taskId, run: prompt.run, digest: prompt.digest }, verified.who, now, root);
575
+ const why = staleWhy(store, { act: "changes", taskId: prompt.taskId, run: prompt.run, digest: prompt.digest }, verified.who, now, seat.channel, root);
573
576
  if (why !== null)
574
577
  return { said: `${why} Your feedback wasn't sent.`, link: { label: "Open task", path: chatControlHref("task", prompt.taskId) } };
575
578
  if (root === undefined)
@@ -1,8 +1,7 @@
1
- import { type Store } from "./store.js";
2
- import { chatSchema, chatTables } from "./contracts/chat-tables.js";
1
+ import { ChatCore, chatHash, type ChatBinding, type ChatProvider, type ChatRoom } from "./chat-core.js";
2
+ import { ChatMessages } from "./chat-messages.js";
3
3
  import { savedChatPart, type ChatContent, type ChatEventBody } from "./contracts/chat-content.js";
4
- export { savedChatPart, type ChatContent, type ChatEventBody };
5
- export { chatSchema, chatTables };
4
+ export { savedChatPart, chatHash, type ChatContent, type ChatEventBody, type ChatBinding, type ChatProvider, type ChatRoom };
6
5
  export type ChatIdentity = {
7
6
  installation: string;
8
7
  team?: string;
@@ -10,18 +9,6 @@ export type ChatIdentity = {
10
9
  bot: string;
11
10
  workspace: string;
12
11
  };
13
- export type ChatBinding = {
14
- id: number;
15
- installation: string;
16
- team: string;
17
- app: string;
18
- member: string;
19
- channel: string;
20
- approver: string;
21
- generation: number;
22
- created: string;
23
- revoked: string | null;
24
- };
25
12
  export type ChatEvent = {
26
13
  id: string;
27
14
  installation: string;
@@ -51,64 +38,20 @@ export type ChatPart = {
51
38
  next_at: string | null;
52
39
  created: string;
53
40
  };
54
- export type ChatRoom = {
55
- id: number;
56
- installation: string;
57
- chat: string;
58
- kind: "group" | "private";
59
- conversation: string;
60
- boundBy: string;
61
- binding: number;
62
- cursor: number;
63
- };
64
- export declare const chatHash: (text: string) => string;
65
- export declare class ChatState {
66
- readonly store: Store;
67
- readonly channel: "slack" | "discord" | "teams";
68
- constructor(store: Store, channel: "slack" | "discord" | "teams");
69
- prepare(sql: string): import("./store.js").Statement;
70
- /** Small transport facts a channel must remember per chat (a Teams service URL). Never credentials. */
71
- meta(installation: string, key: string): string | null;
72
- setMeta(installation: string, key: string, value: string, now: Date): void;
73
- private readRoom;
74
- room(installation: string, chat: string): ChatRoom | null;
75
- rooms(installation: string): ChatRoom[];
76
- /** Point a chat at a conversation; an earlier choice for the same chat is revoked and delivery starts from now. */
77
- bindRoom(args: {
78
- installation: string;
79
- chat: string;
80
- kind: "group" | "private";
81
- conversation: string;
82
- by: string;
83
- binding: number;
84
- }, now: Date): {
85
- ok: true;
86
- } | {
87
- ok: false;
88
- reason: "group-taken";
89
- };
90
- unbindRoom(installation: string, chat: string, by: string, now: Date): boolean;
91
- /** The shared rooms core's view of this installation's room records. */
92
- roomBackend(installation: string, now: Date): import("./chat-rooms.js").RoomBackend;
93
- advanceRoomCursor(id: number, messageId: number): boolean;
94
- get db(): import("./store.js").Database;
95
- /** Every unrevoked binding for an installation, oldest first: one per
96
- * paired person (v73). Liveness is checked per binding with `live`. */
97
- bindings(installation: string): ChatBinding[];
98
- /** The unrevoked binding one channel member holds, if any. */
99
- bindingFor(installation: string, member: string): ChatBinding | null;
100
- bindingById(id: number): ChatBinding | null;
101
- /** The oldest unrevoked binding — the single-person reading kept for
102
- * status lines and fixtures; inbound, delivery and notices read
103
- * `bindingFor` / `bindingById` / `bindings`. */
104
- binding(installation: string): ChatBinding | null;
105
- live(binding: ChatBinding): boolean;
106
- /** End one person's pairing and everything their chat could still do; teammates' bindings stay. */
107
- revokeBinding(binding: ChatBinding, now?: Date): void;
108
- revoke(installation: string, now?: Date): void;
41
+ /** One app's chats on the shared core (the same object Telegram's run on): its events, planned parts and their buttons. */
42
+ export declare class ChatState extends ChatMessages {
43
+ constructor(store: ChatCore["store"], provider: ChatProvider);
44
+ /** Pair-time half kept for the settings pages: mint a code for this installation and return it (only its hash is kept). */
109
45
  pairing(installation: string, approver: string, generation: number, now?: Date): string;
110
- pair(identity: ChatIdentity, hash: string, member: string, channel: string, now: Date): ChatBinding | null;
111
- /** Save a received event with its body (contracts/chat-content.ts), versioned. */
46
+ /**
47
+ * Consume a code sent in a chat (the shared rules, chat-core.ts). The installation's notice cursor starts from now
48
+ * on its FIRST pairing; a later person joins the running cursor (their own history is fenced by their binding's
49
+ * creation time).
50
+ */
51
+ pair(identity: ChatIdentity, hash: string, member: string, channel: string, now: Date, event?: string | null): ChatBinding | null;
52
+ /** The 60-second lease the Slack, Discord and Teams workers renew. */
53
+ lease(installation: string, owner: string, now: Date): boolean;
54
+ /** Save a received event with its body (contracts/chat-content.ts), versioned. False: it was already here. */
112
55
  enqueue(event: Omit<ChatEvent, "session" | "state" | "next_at" | "payload"> & {
113
56
  payload: ChatEventBody;
114
57
  }): boolean;
@@ -116,11 +59,44 @@ export declare class ChatState {
116
59
  next(installation: string, now: Date): ChatEvent | null;
117
60
  defer(event: string, problem: string, until: Date): void;
118
61
  finish(event: string, dropped?: boolean): void;
62
+ /** Point an event at the binding its pairing made. */
63
+ setEventBinding(event: string, binding: number): void;
119
64
  part(id: number): ChatPart | null;
65
+ /** The reply to an event, part by part, with each part's buttons minted with it; the event is then handled. Planned once. */
120
66
  plan(event: string, parts: ChatContent[], now: Date): void;
67
+ /** A proposal card's buttons on its part: earlier ones on that part are spent. Yes and Cancel last ten minutes. */
121
68
  tokens(part: number, proposal: number, phases: Array<"confirm" | "dismiss" | "yes" | "cancel">, now: Date): void;
122
- lease(installation: string, owner: string, now: Date): boolean;
123
- owns(installation: string, owner: string, now?: Date): boolean;
69
+ /** A proposal card's live buttons on one part, in the order they were minted. */
70
+ partTokens(part: number, now: Date): {
71
+ token: string;
72
+ phase: string;
73
+ }[];
74
+ /** The next part to send for an installation: notices only while notifications may go out. */
75
+ nextPart(installation: string, notices: boolean, now: Date, order?: "id" | "created"): ChatPart | null;
76
+ /** A part that will not go: why, in words the saved chat shows. */
77
+ dropPart(id: number, problem: string): void;
78
+ /** Try again no earlier than then (a screenshot waiting for its result's message). */
79
+ holdPart(id: number, until: string): void;
80
+ /** The app's upload identity for a part's file, and whether its bytes are up. */
81
+ setPartFile(id: number, file: string | null, uploaded?: boolean, message?: string | null): void;
82
+ /**
83
+ * Sent: on the app's own message id when it gave one (an uploaded file may not). A card's decide buttons
84
+ * (chat-decide.ts) now ride that message, and any older ones on it stop working; a "What should change?" names it.
85
+ */
86
+ partSent(id: number, message: string | null, now: Date, attempt?: boolean): void;
87
+ /** Not sent this time: counted, named, and (when the answer was lost rather than refused) counted as uncertain. */
88
+ partFailed(id: number, failure: {
89
+ problem: string;
90
+ until: string;
91
+ uncertain: boolean;
92
+ }, giveUpAfter?: number): void;
93
+ /** One person's replies still waiting to send, and those that could not be delivered (settings show both). */
94
+ partCounts(binding: number): {
95
+ pending: number;
96
+ dropped: number;
97
+ };
98
+ /** Replace a part's content and send it again (a repaint after a tap). */
99
+ repaint(id: number, content: ChatContent): void;
124
100
  }
125
101
  /** A saved part's content. One that can't be read is a delivery problem that names the field, never a guess. */
126
102
  export declare function partContent(payload: string): ChatContent;