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
@@ -1,210 +1,180 @@
1
- /** Durable private-chat receipts. Each transport has its own tables and lease. */
2
- import { createHash, randomBytes } from "node:crypto";
1
+ /** Durable chat receipts on the shared chat core: what each app's chats sent us and what we plan to send them. */
2
+ import { randomBytes } from "node:crypto";
3
3
  import { MATE_ASK_TTL_MS } from "./store.js";
4
- import { chatSchema, chatTables } from "./contracts/chat-tables.js";
4
+ import { ChatCore, chatHash } from "./chat-core.js";
5
+ import { ChatMessages } from "./chat-messages.js";
5
6
  import { readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
6
- export { savedChatPart };
7
- export { chatSchema, chatTables };
8
- export const chatHash = (text) => createHash("sha256").update(text).digest("hex");
9
- export class ChatState {
10
- store;
11
- channel;
12
- constructor(store, channel) {
13
- this.store = store;
14
- this.channel = channel;
15
- }
16
- prepare(sql) {
17
- return this.db.prepare(sql.replace(/\bchat_(binding|pair|event|part|action|progress|runtime|room|meta|flow_action|flow_prompt|flow_choice|flow_note|question_action|question_prompt|ask_action)\b/g, `${this.channel}_$1`));
18
- }
19
- /** Small transport facts a channel must remember per chat (a Teams service URL). Never credentials. */
20
- meta(installation, key) {
21
- const row = this.prepare("SELECT value FROM chat_meta WHERE installation=? AND key=?").get(installation, key);
22
- return row === undefined ? null : String(row.value);
23
- }
24
- setMeta(installation, key, value, now) {
25
- this.prepare("INSERT INTO chat_meta(installation,key,value,updated) VALUES(?,?,?,?) ON CONFLICT(installation,key) DO UPDATE SET value=excluded.value,updated=excluded.updated").run(installation, key, value, now.toISOString());
26
- }
27
- // ---- rooms (v73) -----------------------------------------------------------
28
- readRoom(row) {
29
- return { id: Number(row.id), installation: String(row.installation), chat: String(row.chat), kind: row.kind === "group" ? "group" : "private",
30
- conversation: String(row.conversation), boundBy: String(row.bound_by), binding: Number(row.binding), cursor: Number(row.cursor) };
31
- }
32
- room(installation, chat) {
33
- const row = this.prepare("SELECT * FROM chat_room WHERE installation=? AND chat=? AND revoked IS NULL").get(installation, chat);
34
- return row === undefined ? null : this.readRoom(row);
35
- }
36
- rooms(installation) {
37
- return this.prepare("SELECT * FROM chat_room WHERE installation=? AND revoked IS NULL ORDER BY id").all(installation).map(row => this.readRoom(row));
38
- }
39
- /** Point a chat at a conversation; an earlier choice for the same chat is revoked and delivery starts from now. */
40
- bindRoom(args, now) {
41
- return this.store.transact(() => {
42
- const stamp = now.toISOString();
43
- // Check the expected collision before revoking the current selection.
44
- if (args.kind === "group") {
45
- const group = this.prepare("SELECT installation, chat FROM chat_room WHERE conversation=? AND kind='group' AND revoked IS NULL").get(args.conversation);
46
- if (group !== undefined && (group.installation !== args.installation || group.chat !== args.chat))
47
- return { ok: false, reason: "group-taken" };
48
- }
49
- this.prepare("UPDATE chat_room SET revoked=?,revoked_by=? WHERE installation=? AND chat=? AND revoked IS NULL").run(stamp, args.by, args.installation, args.chat);
50
- const thread = this.db.prepare("SELECT thread FROM team_conversation WHERE id=?").get(args.conversation);
51
- if (thread === undefined)
52
- throw new Error("no such team conversation");
53
- const cursor = Number(this.db.prepare("SELECT COALESCE(MAX(id),0) AS n FROM mate_message WHERE thread=?").get(Number(thread["thread"]))?.["n"] ?? 0);
54
- this.prepare("INSERT INTO chat_room(installation,chat,kind,conversation,binding,bound_by,bound,cursor) VALUES(?,?,?,?,?,?,?,?)").run(args.installation, args.chat, args.kind, args.conversation, args.binding, args.by, stamp, cursor);
55
- return { ok: true };
56
- });
57
- }
58
- unbindRoom(installation, chat, by, now) {
59
- return Number(this.prepare("UPDATE chat_room SET revoked=?,revoked_by=? WHERE installation=? AND chat=? AND revoked IS NULL").run(now.toISOString(), by, installation, chat).changes) > 0;
60
- }
61
- /** The shared rooms core's view of this installation's room records. */
62
- roomBackend(installation, now) {
63
- const roomOf = (room) => room === null ? null : { id: room.id, chatId: room.chat, kind: room.kind, conversation: room.conversation, boundBy: room.boundBy, binding: room.binding, cursor: room.cursor };
64
- return {
65
- room: chat => roomOf(this.room(installation, chat)),
66
- bind: args => this.bindRoom({ installation, chat: args.chatId, kind: args.kind, conversation: args.conversation, by: args.by, binding: args.binding }, now),
67
- unbind: (chat, by) => this.unbindRoom(installation, chat, by, now),
68
- grant: id => { const binding = this.bindingById(id); return binding === null || !this.live(binding) ? null : { approver: binding.approver, generation: binding.generation, chatId: binding.channel }; },
69
- };
70
- }
71
- advanceRoomCursor(id, messageId) {
72
- return Number(this.prepare("UPDATE chat_room SET cursor=? WHERE id=? AND cursor<? AND revoked IS NULL").run(messageId, id, messageId).changes) > 0;
73
- }
74
- get db() {
75
- return this.store.handle;
76
- }
77
- /** Every unrevoked binding for an installation, oldest first: one per
78
- * paired person (v73). Liveness is checked per binding with `live`. */
79
- bindings(installation) {
80
- return this.prepare("SELECT * FROM chat_binding WHERE installation=? AND revoked IS NULL ORDER BY id").all(installation);
81
- }
82
- /** The unrevoked binding one channel member holds, if any. */
83
- bindingFor(installation, member) {
84
- return (this.prepare("SELECT * FROM chat_binding WHERE installation=? AND member=? AND revoked IS NULL").get(installation, member) ?? null);
85
- }
86
- bindingById(id) {
87
- return (this.prepare("SELECT * FROM chat_binding WHERE id=? AND revoked IS NULL").get(id) ?? null);
88
- }
89
- /** The oldest unrevoked binding — the single-person reading kept for
90
- * status lines and fixtures; inbound, delivery and notices read
91
- * `bindingFor` / `bindingById` / `bindings`. */
92
- binding(installation) {
93
- return this.bindings(installation)[0] ?? null;
94
- }
95
- live(binding) {
96
- const current = this.bindingById(binding.id);
97
- const account = this.store.accountOf(binding.approver);
98
- return (current !== null &&
99
- account?.role === "approver" &&
100
- account.revokedAt === null &&
101
- account.generation === binding.generation);
102
- }
103
- /** End one person's pairing and everything their chat could still do; teammates' bindings stay. */
104
- revokeBinding(binding, now = new Date()) {
105
- this.store.transact(() => {
106
- this.prepare("UPDATE chat_binding SET revoked=? WHERE id=? AND revoked IS NULL").run(now.toISOString(), binding.id);
107
- this.prepare("UPDATE chat_event SET state='dropped',payload='{}',problem='Chat disconnected' WHERE binding=? AND state='queued'").run(binding.id);
108
- this.prepare("UPDATE chat_part SET state='dropped',problem='Chat disconnected' WHERE state='pending' AND event IN (SELECT id FROM chat_event WHERE binding=?)").run(binding.id);
109
- });
110
- }
111
- revoke(installation, now = new Date()) {
112
- this.store.transact(() => {
113
- this.prepare("UPDATE chat_binding SET revoked=? WHERE installation=? AND revoked IS NULL").run(now.toISOString(), installation);
114
- this.prepare("UPDATE chat_pair SET consumed=? WHERE installation=? AND consumed IS NULL").run(now.toISOString(), installation);
115
- this.prepare("UPDATE chat_event SET state='dropped',payload='{}',problem='Chat disconnected' WHERE installation=? AND state='queued'").run(installation);
116
- this.prepare("UPDATE chat_part SET state='dropped',problem='Chat disconnected' WHERE state='pending' AND event IN (SELECT id FROM chat_event WHERE installation=?)").run(installation);
117
- });
118
- }
7
+ export { savedChatPart, chatHash };
8
+ const DAY_MS = 86_400_000;
9
+ /** One app's chats on the shared core (the same object Telegram's run on): its events, planned parts and their buttons. */
10
+ export class ChatState extends ChatMessages {
11
+ constructor(store, provider) {
12
+ super(store, provider);
13
+ }
14
+ /** Pair-time half kept for the settings pages: mint a code for this installation and return it (only its hash is kept). */
119
15
  pairing(installation, approver, generation, now = new Date()) {
120
16
  const code = randomBytes(16).toString("hex");
121
- this.store.transact(() => {
122
- // A fresh code replaces this person's outstanding ones; a teammate's pending code is theirs.
123
- this.prepare("UPDATE chat_pair SET consumed=? WHERE installation=? AND approver=? AND consumed IS NULL").run(now.toISOString(), installation, approver);
124
- this.prepare("INSERT INTO chat_pair VALUES(?,?,?,?,?,NULL)").run(chatHash(code), installation, approver, generation, new Date(now.getTime() + 600_000).toISOString());
125
- });
17
+ this.savePairing({ hash: chatHash(code), installation, approver, generation, by: approver }, now);
126
18
  return code;
127
19
  }
128
- pair(identity, hash, member, channel, now) {
20
+ /**
21
+ * Consume a code sent in a chat (the shared rules, chat-core.ts). The installation's notice cursor starts from now
22
+ * on its FIRST pairing; a later person joins the running cursor (their own history is fenced by their binding's
23
+ * creation time).
24
+ */
25
+ pair(identity, hash, member, channel, now, event = null) {
129
26
  return this.store.transact(() => {
130
- const pair = this.prepare("SELECT approver,generation FROM chat_pair WHERE hash=? AND installation=? AND consumed IS NULL AND expires>?").get(hash, identity.installation, now.toISOString());
131
- if (!pair)
27
+ const paired = this.consumePairing({ hash, installation: identity.installation, team: identity.team ?? identity.app, app: identity.app, member, channel, event }, now);
28
+ if (!paired.ok)
132
29
  return null;
133
- // One binding per channel member, and one channel identity per person.
134
- if (this.bindingFor(identity.installation, member))
135
- return null;
136
- if (this.bindings(identity.installation).some(one => one.approver === String(pair.approver) && this.live(one)))
137
- return null;
138
- const account = this.store.accountOf(String(pair.approver));
139
- if (account?.role !== "approver" ||
140
- account.revokedAt !== null ||
141
- account.generation !== Number(pair.generation))
142
- return null;
143
- this.prepare("UPDATE chat_pair SET consumed=? WHERE hash=?").run(now.toISOString(), hash);
144
- this.prepare("INSERT INTO chat_binding(installation,team,app,member,channel,approver,generation,created) VALUES(?,?,?,?,?,?,?,?)").run(identity.installation, identity.team ?? identity.app, identity.app, member, channel, String(pair.approver), Number(pair.generation), now.toISOString());
145
- // The installation's notice cursor starts from now on its FIRST pairing;
146
- // a later person joins the running cursor (their own history is fenced
147
- // by their binding's creation time).
148
- this.prepare("INSERT OR IGNORE INTO chat_runtime(installation, notification) VALUES(?, (SELECT COALESCE(MAX(id),0) FROM notification))").run(identity.installation);
149
- return this.bindingFor(identity.installation, member);
30
+ this.prepare("INSERT OR IGNORE INTO chat_runtime (provider, installation, notification) VALUES (:provider, ?, (SELECT COALESCE(MAX(id), 0) FROM notification))")
31
+ .run(identity.installation);
32
+ return paired.binding;
150
33
  });
151
34
  }
152
- /** Save a received event with its body (contracts/chat-content.ts), versioned. */
35
+ /** The 60-second lease the Slack, Discord and Teams workers renew. */
36
+ lease(installation, owner, now) {
37
+ return this.acquire(installation, owner, 60_000, now).ok;
38
+ }
39
+ /** Save a received event with its body (contracts/chat-content.ts), versioned. False: it was already here. */
153
40
  enqueue(event) {
154
- return (Number(this.prepare("INSERT OR IGNORE INTO chat_event(id,installation,binding,kind,channel,member,ts,thread,payload,created) VALUES(?,?,?,?,?,?,?,?,?,?)").run(event.id, event.installation, event.binding, event.kind, event.channel, event.member, event.ts, event.thread, savedChatEventBody(event.payload), event.created).changes) === 1);
41
+ return Number(this.prepare(`INSERT OR IGNORE INTO chat_event (provider, id, installation, binding, kind, channel, member, ts, thread, payload, created)
42
+ VALUES (:provider, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
43
+ .run(event.id, event.installation, event.binding, event.kind, event.channel, event.member, event.ts, event.thread, savedChatEventBody(event.payload), event.created).changes) === 1;
155
44
  }
156
45
  event(id) {
157
- return (this.prepare("SELECT * FROM chat_event WHERE id=?").get(id) ?? null);
46
+ return this.prepare("SELECT * FROM chat_event WHERE provider = :provider AND id = ?").get(id) ?? null;
158
47
  }
159
48
  next(installation, now) {
160
- return (this.prepare("SELECT * FROM chat_event WHERE installation=? AND state='queued' AND (next_at IS NULL OR next_at<=?) ORDER BY created,id LIMIT 1").get(installation, now.toISOString()) ?? null);
49
+ return this.prepare(`SELECT * FROM chat_event WHERE provider = :provider AND installation = ? AND kind <> 'update' AND state = 'queued'
50
+ AND (next_at IS NULL OR next_at <= ?) ORDER BY created, id LIMIT 1`).get(installation, now.toISOString()) ?? null;
161
51
  }
162
52
  defer(event, problem, until) {
163
- this.prepare("UPDATE chat_event SET next_at=?,problem=? WHERE id=? AND state='queued'").run(until.toISOString(), problem, event);
53
+ this.prepare("UPDATE chat_event SET next_at = ?, problem = ? WHERE provider = :provider AND id = ? AND state = 'queued'").run(until.toISOString(), problem, event);
164
54
  }
165
55
  finish(event, dropped = false) {
166
- this.prepare("UPDATE chat_event SET state=?,payload='{}' WHERE id=?").run(dropped ? "dropped" : "done", event);
56
+ this.prepare("UPDATE chat_event SET state = ?, payload = '{}' WHERE provider = :provider AND id = ?").run(dropped ? "dropped" : "done", event);
57
+ }
58
+ /** Point an event at the binding its pairing made. */
59
+ setEventBinding(event, binding) {
60
+ this.prepare("UPDATE chat_event SET binding = ? WHERE provider = :provider AND id = ?").run(binding, event);
167
61
  }
168
62
  part(id) {
169
- return (this.prepare("SELECT * FROM chat_part WHERE id=?").get(id) ?? null);
63
+ return this.prepare("SELECT * FROM chat_part WHERE provider = :provider AND id = ?").get(id) ?? null;
170
64
  }
65
+ /** The reply to an event, part by part, with each part's buttons minted with it; the event is then handled. Planned once. */
171
66
  plan(event, parts, now) {
172
67
  this.store.transact(() => {
173
68
  for (const [ordinal, part] of parts.entries()) {
174
- const inserted = this.prepare("INSERT OR IGNORE INTO chat_part(event,ordinal,payload,created) VALUES(?,?,?,?)").run(event, ordinal, savedChatPart(part), now.toISOString());
175
- if (Number(inserted.changes) && part.proposal)
176
- this.tokens(Number(inserted.lastInsertRowid), part.proposal, ["confirm", "dismiss"], now);
177
- if (Number(inserted.changes) && part.question)
69
+ if (this.prepare("SELECT 1 AS hit FROM chat_part WHERE provider = :provider AND event = ? AND ordinal = ?").get(event, ordinal) !== undefined)
70
+ continue;
71
+ const id = this.nextId("chat_part");
72
+ this.prepare("INSERT INTO chat_part (provider, id, event, ordinal, payload, created) VALUES (:provider, ?, ?, ?, ?, ?)").run(id, event, ordinal, savedChatPart(part), now.toISOString());
73
+ const token = () => randomBytes(16).toString("hex");
74
+ if (part.proposal)
75
+ this.tokens(id, part.proposal, ["confirm", "dismiss"], now);
76
+ if (part.question)
178
77
  for (const one of part.question.choices)
179
- this.prepare("INSERT INTO chat_question_action(token,part,question,choice,expires) VALUES(?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.question.id, one.choice, new Date(now.getTime() + 7 * 86_400_000).toISOString());
180
- if (Number(inserted.changes) && part.ask)
78
+ this.prepare("INSERT INTO chat_question_action (provider, token, part, question, choice, created, expires) VALUES (:provider, ?, ?, ?, ?, ?, ?)")
79
+ .run(token(), id, part.question.id, one.choice, now.toISOString(), new Date(now.getTime() + 7 * DAY_MS).toISOString());
80
+ if (part.ask)
181
81
  for (const choice of [...part.ask.options.map((_, index) => index), null])
182
- this.prepare("INSERT INTO chat_ask_action(token,part,turn,choice,expires) VALUES(?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.ask.turn, choice, new Date(now.getTime() + MATE_ASK_TTL_MS).toISOString());
183
- if (Number(inserted.changes) && part.choose)
82
+ this.prepare("INSERT INTO chat_ask_action (provider, token, part, turn, choice, expires) VALUES (:provider, ?, ?, ?, ?, ?)")
83
+ .run(token(), id, part.ask.turn, choice, new Date(now.getTime() + MATE_ASK_TTL_MS).toISOString());
84
+ if (part.choose)
184
85
  for (const one of part.choose.options)
185
- this.prepare("INSERT INTO chat_flow_choice(token,part,card,entry,choice,label,expires) VALUES(?,?,?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.choose.card, part.choose.entry, one.choice, one.label, new Date(now.getTime() + 7 * 86_400_000).toISOString());
186
- if (Number(inserted.changes) && part.note)
86
+ this.prepare("INSERT INTO chat_flow_choice (provider, token, part, card, entry, choice, label, created, expires) VALUES (:provider, ?, ?, ?, ?, ?, ?, ?, ?)")
87
+ .run(token(), id, part.choose.card, part.choose.entry, one.choice, one.label, now.toISOString(), new Date(now.getTime() + 7 * DAY_MS).toISOString());
88
+ if (part.note)
187
89
  for (const answer of ["yes", "no"])
188
- this.prepare("INSERT INTO chat_flow_note(token,part,card,entry,held,answer,expires) VALUES(?,?,?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.note.card, part.note.entry, part.note.held, answer, new Date(now.getTime() + 86_400_000).toISOString());
189
- if (Number(inserted.changes) && part.flow)
90
+ this.prepare("INSERT INTO chat_flow_note (provider, token, part, card, entry, held, answer, expires) VALUES (:provider, ?, ?, ?, ?, ?, ?, ?)")
91
+ .run(token(), id, part.note.card, part.note.entry, part.note.held, answer, new Date(now.getTime() + DAY_MS).toISOString());
92
+ if (part.flow)
190
93
  for (const action of part.flow.actions)
191
- this.prepare("INSERT INTO chat_flow_action(token,part,card,entry,action,expires) VALUES(?,?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.flow.card, part.flow.entry, action, new Date(now.getTime() + 7 * 86_400_000).toISOString());
94
+ this.prepare("INSERT INTO chat_flow_action (provider, token, part, card, entry, action, created, expires) VALUES (:provider, ?, ?, ?, ?, ?, ?, ?)")
95
+ .run(token(), id, part.flow.card, part.flow.entry, action, now.toISOString(), new Date(now.getTime() + 7 * DAY_MS).toISOString());
192
96
  }
193
97
  this.finish(event);
194
98
  });
195
99
  }
100
+ /** A proposal card's buttons on its part: earlier ones on that part are spent. Yes and Cancel last ten minutes. */
196
101
  tokens(part, proposal, phases, now) {
197
- this.prepare("UPDATE chat_action SET consumed=? WHERE part=? AND consumed IS NULL").run(now.toISOString(), part);
102
+ this.prepare("UPDATE chat_action SET consumed = ? WHERE provider = :provider AND part = ? AND consumed IS NULL").run(now.toISOString(), part);
198
103
  for (const phase of phases)
199
- this.prepare("INSERT INTO chat_action VALUES(?,?,?,?,?,NULL)").run(randomBytes(16).toString("hex"), part, proposal, phase, new Date(now.getTime() +
200
- (phase === "yes" || phase === "cancel" ? 600_000 : 86_400_000)).toISOString());
201
- }
202
- lease(installation, owner, now) {
203
- this.prepare("INSERT OR IGNORE INTO chat_runtime(installation) VALUES(?)").run(installation);
204
- return (Number(this.prepare("UPDATE chat_runtime SET owner=?,lease_until=? WHERE installation=? AND (owner=? OR lease_until IS NULL OR lease_until<=?)").run(owner, new Date(now.getTime() + 60_000).toISOString(), installation, owner, now.toISOString()).changes) === 1);
104
+ this.prepare("INSERT INTO chat_action (provider, token, part, proposal, phase, created, expires) VALUES (:provider, ?, ?, ?, ?, ?, ?)").run(randomBytes(16).toString("hex"), part, proposal, phase, now.toISOString(), new Date(now.getTime() + (phase === "yes" || phase === "cancel" ? 600_000 : DAY_MS)).toISOString());
105
+ }
106
+ /** A proposal card's live buttons on one part, in the order they were minted. */
107
+ partTokens(part, now) {
108
+ return this.prepare("SELECT token, phase FROM chat_action WHERE provider = :provider AND part = ? AND consumed IS NULL AND expires > ? ORDER BY rowid")
109
+ .all(part, now.toISOString()).map(row => ({ token: String(row["token"]), phase: String(row["phase"]) }));
110
+ }
111
+ // ---- sending, the same for every app's worker ----------------------------------------------------------------------
112
+ /** The next part to send for an installation: notices only while notifications may go out. */
113
+ nextPart(installation, notices, now, order = "id") {
114
+ return this.prepare(`SELECT p.* FROM chat_part p JOIN chat_event e ON e.provider = p.provider AND e.id = p.event
115
+ WHERE p.provider = :provider AND e.installation = ? AND p.state = 'pending' AND (e.kind <> 'notice' OR ? = 1)
116
+ AND (p.next_at IS NULL OR p.next_at <= ?) ORDER BY ${order === "created" ? "p.created, " : ""}p.id LIMIT 1`)
117
+ .get(installation, notices ? 1 : 0, now.toISOString()) ?? null;
118
+ }
119
+ /** A part that will not go: why, in words the saved chat shows. */
120
+ dropPart(id, problem) {
121
+ this.prepare("UPDATE chat_part SET state = 'dropped', next_at = NULL, problem = ? WHERE provider = :provider AND id = ?").run(problem, id);
122
+ }
123
+ /** Try again no earlier than then (a screenshot waiting for its result's message). */
124
+ holdPart(id, until) {
125
+ this.prepare("UPDATE chat_part SET next_at = ? WHERE provider = :provider AND id = ?").run(until, id);
126
+ }
127
+ /** The app's upload identity for a part's file, and whether its bytes are up. */
128
+ setPartFile(id, file, uploaded, message) {
129
+ if (message !== undefined)
130
+ this.prepare("UPDATE chat_part SET message = ? WHERE provider = :provider AND id = ?").run(message, id);
131
+ if (file !== null)
132
+ this.prepare("UPDATE chat_part SET file = ? WHERE provider = :provider AND id = ?").run(file, id);
133
+ if (uploaded !== undefined)
134
+ this.prepare("UPDATE chat_part SET uploaded = ? WHERE provider = :provider AND id = ?").run(uploaded ? 1 : 0, id);
135
+ }
136
+ /**
137
+ * Sent: on the app's own message id when it gave one (an uploaded file may not). A card's decide buttons
138
+ * (chat-decide.ts) now ride that message, and any older ones on it stop working; a "What should change?" names it.
139
+ */
140
+ partSent(id, message, now, attempt = true) {
141
+ this.store.transact(() => {
142
+ this.prepare(`UPDATE chat_part SET state = 'sent', message = COALESCE(?, message), attempts = attempts + ?, problem = NULL, next_at = NULL, sent = ?
143
+ WHERE provider = :provider AND id = ?`).run(message, attempt ? 1 : 0, now.toISOString(), id);
144
+ const row = this.prepare(`SELECT p.payload, p.message, b.channel AS own FROM chat_part p JOIN chat_event e ON e.provider = p.provider AND e.id = p.event
145
+ LEFT JOIN chat_binding b ON b.provider = e.provider AND b.id = e.binding WHERE p.provider = :provider AND p.id = ?`).get(id);
146
+ const read = row === undefined ? null : readChatPart(String(row["payload"]));
147
+ const placed = row?.["message"] == null ? null : String(row["message"]);
148
+ if (read?.ok !== true || placed === null)
149
+ return;
150
+ const content = read.value, chat = content.channel ?? (row?.["own"] == null ? null : String(row["own"]));
151
+ if (chat !== null) {
152
+ // What the message shows now is all that acts on it: buttons it no longer shows stop working.
153
+ const tokens = JSON.stringify((content.decide?.rows ?? []).flat().flatMap(one => "token" in one ? [one.token] : []));
154
+ this.prepare(`UPDATE chat_decide_action SET consumed_at = ? WHERE channel = :provider AND chat = ? AND message = ? AND consumed_at IS NULL
155
+ AND token NOT IN (SELECT value FROM json_each(?))`).run(now.toISOString(), chat, placed, tokens);
156
+ if (content.decide !== undefined)
157
+ this.prepare("UPDATE chat_decide_action SET message = ? WHERE channel = :provider AND message IS NULL AND token IN (SELECT value FROM json_each(?))").run(placed, tokens);
158
+ }
159
+ if (content.prompt !== undefined)
160
+ this.prepare("UPDATE chat_decide_prompt SET message = ? WHERE channel = :provider AND id = ? AND message IS NULL").run(placed, content.prompt);
161
+ });
205
162
  }
206
- owns(installation, owner, now = new Date()) {
207
- return !!this.prepare("SELECT 1 FROM chat_runtime WHERE installation=? AND owner=? AND lease_until>?").get(installation, owner, now.toISOString());
163
+ /** Not sent this time: counted, named, and (when the answer was lost rather than refused) counted as uncertain. */
164
+ partFailed(id, failure, giveUpAfter) {
165
+ this.prepare(`UPDATE chat_part SET attempts = attempts + 1, uncertain = uncertain + ?, next_at = ?, problem = ?,
166
+ state = CASE WHEN ? IS NOT NULL AND attempts >= ? THEN 'dropped' ELSE state END WHERE provider = :provider AND id = ?`)
167
+ .run(failure.uncertain ? 1 : 0, failure.until, failure.problem, giveUpAfter ?? null, giveUpAfter ?? 0, id);
168
+ }
169
+ /** One person's replies still waiting to send, and those that could not be delivered (settings show both). */
170
+ partCounts(binding) {
171
+ const row = this.prepare(`SELECT SUM(p.state = 'pending') AS pending, SUM(p.state = 'dropped') AS dropped FROM chat_part p
172
+ JOIN chat_event e ON e.provider = p.provider AND e.id = p.event WHERE p.provider = :provider AND e.binding = ?`).get(binding);
173
+ return { pending: Number(row?.["pending"] ?? 0), dropped: Number(row?.["dropped"] ?? 0) };
174
+ }
175
+ /** Replace a part's content and send it again (a repaint after a tap). */
176
+ repaint(id, content) {
177
+ this.prepare("UPDATE chat_part SET payload = ?, state = 'pending', next_at = NULL WHERE provider = :provider AND id = ?").run(savedChatPart(content), id);
208
178
  }
209
179
  }
210
180
  /** A saved part's content. One that can't be read is a delivery problem that names the field, never a guess. */
@@ -1,8 +1,8 @@
1
1
  import { type WarmHooks } from "./chat-warmth.js";
2
- import { type DoorOptions } from "./mate-doors.js";
3
- import { ChatState, type ChatBinding, type ChatEvent, type ChatIdentity } from "./chat-delivery-state.js";
2
+ import { ChatState, type ChatBinding, type ChatContent, type ChatEvent, type ChatIdentity } from "./chat-delivery-state.js";
4
3
  import { type Store } from "./store.js";
5
4
  import type { SubscriptionMateRunner } from "./subscription-chat.js";
5
+ import { type ChatMerge } from "./chat-decide.js";
6
6
  export declare const chatObject: (v: unknown) => Record<string, unknown>;
7
7
  export type ChatDeliveryOptions = {
8
8
  store: Store;
@@ -16,19 +16,35 @@ export type ChatDeliveryOptions = {
16
16
  current: () => boolean;
17
17
  origin: () => string | null;
18
18
  subscriptionRunner?: SubscriptionMateRunner;
19
- held?: DoorOptions["held"];
20
19
  canNotify?: () => boolean;
21
20
  clock?: () => Date;
22
21
  partSize?: number;
23
22
  maxProposal?: number;
24
23
  /** The app's warm touches for one owner message (a 👍, typing), where the app and the bot's permissions allow them. */
25
24
  warm?: (event: ChatEvent, binding: ChatBinding) => WarmHooks;
25
+ /** Merges a pull request approved in chat (tests script it); otherwise the project's own merge. */
26
+ merge?: (input: {
27
+ runId: number;
28
+ by: string;
29
+ }) => Promise<{
30
+ ok: true;
31
+ } | {
32
+ ok: false;
33
+ message: string;
34
+ }>;
26
35
  };
27
36
  export declare const splitChatText: (text: string, size?: number) => string[];
28
37
  export declare function channelAccess(options: ChatDeliveryOptions, binding: ChatBinding, ceiling?: string): Promise<string[]>;
38
+ /** A merge approved in chat, waiting on GitHub, and the card that says how it went. */
39
+ type PendingMerge = {
40
+ merge: ChatMerge;
41
+ part: number;
42
+ shown: string;
43
+ content: ChatContent;
44
+ };
29
45
  export declare function processChatEvent(options: ChatDeliveryOptions): Promise<boolean>;
30
46
  /** Bound to the paired member, exact channel/message, pending proposal and current ceiling. */
31
- export declare function applyChatAction(options: ChatDeliveryOptions, event: ChatEvent, binding: ChatBinding, repos: readonly string[]): void;
47
+ export declare function applyChatAction(options: ChatDeliveryOptions, event: ChatEvent, binding: ChatBinding, repos: readonly string[]): PendingMerge | null;
32
48
  /**
33
49
  * Carry new conversation messages to every room that follows one: the
34
50
  * lead's replies, teammates' messages (never the room's own), and each
@@ -39,3 +55,4 @@ export declare function applyChatAction(options: ChatDeliveryOptions, event: Cha
39
55
  export declare function planRoomMessages(options: ChatDeliveryOptions): Promise<void>;
40
56
  /** One progress card per exact result; separate urgent facts retain their own review link. */
41
57
  export declare function planChatNotifications(options: ChatDeliveryOptions): Promise<void>;
58
+ export {};