toolroll 0.9.96 → 0.9.98

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 (131) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/approval-authority.d.ts +10 -1
  3. package/dist/approval-authority.js +18 -2
  4. package/dist/browser/workspace.css +2 -2
  5. package/dist/browser/workspace.js +32 -32
  6. package/dist/browser-workspace.d.ts +6 -3
  7. package/dist/chat-actions.d.ts +40 -5
  8. package/dist/chat-actions.js +124 -13
  9. package/dist/chat-card.js +4 -1
  10. package/dist/chat-channel.d.ts +3 -0
  11. package/dist/chat-channel.js +24 -7
  12. package/dist/chat-decide.js +4 -2
  13. package/dist/chat-delivery.d.ts +5 -1
  14. package/dist/chat-delivery.js +74 -25
  15. package/dist/chat-follow.d.ts +6 -3
  16. package/dist/chat-follow.js +23 -7
  17. package/dist/chat-live-schema.d.ts +17 -0
  18. package/dist/chat-live-schema.js +32 -0
  19. package/dist/chat-live.d.ts +161 -0
  20. package/dist/chat-live.js +491 -0
  21. package/dist/chat-task-actions.js +1 -1
  22. package/dist/chat-terms.d.ts +8 -0
  23. package/dist/chat-terms.js +14 -0
  24. package/dist/chat-voice.js +1 -1
  25. package/dist/contracts/chat-actions.d.ts +6 -0
  26. package/dist/contracts/chat-actions.js +2 -0
  27. package/dist/contracts/cli.d.ts +6 -0
  28. package/dist/contracts/cli.js +3 -0
  29. package/dist/contracts/console-api.d.ts +1 -0
  30. package/dist/contracts/console-api.js +1 -0
  31. package/dist/contracts/lead-tools.d.ts +17 -0
  32. package/dist/contracts/lead-tools.js +4 -1
  33. package/dist/contracts/store-json.d.ts +1 -0
  34. package/dist/discord-api.d.ts +3 -1
  35. package/dist/discord-api.js +9 -3
  36. package/dist/discord-chat.d.ts +13 -3
  37. package/dist/discord-chat.js +77 -21
  38. package/dist/discord.js +8 -1
  39. package/dist/fast-start.d.ts +83 -6
  40. package/dist/fast-start.js +112 -15
  41. package/dist/home-groups.d.ts +39 -0
  42. package/dist/home-groups.js +33 -0
  43. package/dist/lead-answer-eval.d.ts +93 -0
  44. package/dist/lead-answer-eval.js +259 -0
  45. package/dist/lead-capabilities.js +1 -1
  46. package/dist/lead-cli-live.d.ts +56 -0
  47. package/dist/lead-cli-live.js +200 -0
  48. package/dist/lead-cli.d.ts +3 -0
  49. package/dist/lead-cli.js +34 -4
  50. package/dist/lead-context.js +2 -2
  51. package/dist/lead-contract.d.ts +1 -1
  52. package/dist/lead-contract.js +10 -7
  53. package/dist/lead-doors.d.ts +32 -0
  54. package/dist/lead-doors.js +80 -14
  55. package/dist/lead-latency-schema.d.ts +24 -0
  56. package/dist/lead-latency-schema.js +36 -0
  57. package/dist/lead-progress.d.ts +10 -1
  58. package/dist/lead-progress.js +1 -1
  59. package/dist/lead-queue-runtime.d.ts +1 -1
  60. package/dist/lead-queue-runtime.js +1 -1
  61. package/dist/lead-route.d.ts +56 -0
  62. package/dist/lead-route.js +104 -0
  63. package/dist/lead-status.d.ts +3 -0
  64. package/dist/lead-status.js +8 -3
  65. package/dist/lead-tools.d.ts +20 -0
  66. package/dist/lead-tools.js +66 -3
  67. package/dist/lead-voice.js +1 -1
  68. package/dist/lead.d.ts +4 -2
  69. package/dist/lead.js +97 -28
  70. package/dist/live-reply.d.ts +83 -0
  71. package/dist/live-reply.js +302 -0
  72. package/dist/local-owner.js +2 -0
  73. package/dist/one-step-chat-schema.d.ts +22 -0
  74. package/dist/one-step-chat-schema.js +37 -0
  75. package/dist/operate.d.ts +27 -1
  76. package/dist/operate.js +245 -33
  77. package/dist/project-delete.js +9 -0
  78. package/dist/project-knowledge.d.ts +10 -10
  79. package/dist/publish.d.ts +2 -0
  80. package/dist/publish.js +1 -1
  81. package/dist/pull-request-flow.d.ts +4 -0
  82. package/dist/pull-request-flow.js +16 -1
  83. package/dist/reply-shape.d.ts +9 -0
  84. package/dist/reply-shape.js +13 -0
  85. package/dist/reply-voice.d.ts +23 -0
  86. package/dist/reply-voice.js +37 -0
  87. package/dist/scope.d.ts +6 -1
  88. package/dist/scope.js +3 -2
  89. package/dist/serve.js +11 -4
  90. package/dist/server/chat.d.ts +2 -1
  91. package/dist/server/chat.js +22 -10
  92. package/dist/server/chrome.js +2 -2
  93. package/dist/server/home.d.ts +4 -3
  94. package/dist/server/home.js +33 -28
  95. package/dist/server/live.js +18 -21
  96. package/dist/server/render-chat.d.ts +5 -0
  97. package/dist/server/render-chat.js +15 -8
  98. package/dist/server/render-pages.js +2 -2
  99. package/dist/server/render-settings.d.ts +8 -0
  100. package/dist/server/render-settings.js +13 -2
  101. package/dist/server/route-table.js +2 -0
  102. package/dist/server/settings.js +14 -1
  103. package/dist/setup-guide.d.ts +2 -2
  104. package/dist/slack-api.js +11 -0
  105. package/dist/slack-chat.d.ts +11 -1
  106. package/dist/slack-chat.js +50 -5
  107. package/dist/slack-settings.js +8 -1
  108. package/dist/slack.js +10 -3
  109. package/dist/store.d.ts +33 -2
  110. package/dist/store.js +92 -16
  111. package/dist/subscription-chat.d.ts +2 -0
  112. package/dist/subscription-chat.js +3 -1
  113. package/dist/teams-chat.d.ts +9 -2
  114. package/dist/teams-chat.js +50 -2
  115. package/dist/teams.js +5 -2
  116. package/dist/telegram-lead.d.ts +3 -0
  117. package/dist/telegram-lead.js +132 -14
  118. package/dist/telegram-live.d.ts +116 -0
  119. package/dist/telegram-live.js +224 -0
  120. package/dist/telegram-status.js +9 -4
  121. package/dist/telegram.d.ts +12 -0
  122. package/dist/telegram.js +142 -10
  123. package/dist/toolroll-update.js +6 -1
  124. package/dist/tui-contract.d.ts +2 -0
  125. package/dist/tui-source.js +3 -0
  126. package/dist/tui.js +41 -41
  127. package/dist/typed-start.d.ts +51 -0
  128. package/dist/typed-start.js +98 -0
  129. package/dist/wake-hub.d.ts +72 -0
  130. package/dist/wake-hub.js +151 -0
  131. package/package.json +1 -1
@@ -0,0 +1,302 @@
1
+ /**
2
+ * The shared live-reply core (docs/plans/responsive-chat.md §2): one lead turn's `LeadProgress` turned into text-only
3
+ * views that every surface renders. Surfaces never read `LeadProgress` themselves.
4
+ *
5
+ * queued → working (one status line) → writing (status + display-safe partial text) → closed (answered | failed | blocked)
6
+ *
7
+ * Nothing here is durable or authoritative: the saved thread message and its cards remain the record, and no view ever
8
+ * carries a card, a button, an ask or an approval. A partial is the reply as written so far, cut back to what is
9
+ * settled (never an unfinished word, code span, link or bold run), withheld while its tail could be the start of a
10
+ * password or key, and shaped with the final reply's own presentation policy (presentation, not redaction).
11
+ *
12
+ * `createLiveSender` is the one sender per live surface message (§2.4): at most one request in flight, one pending
13
+ * slot holding only the newest view, a generation that only grows, and `close()` — after which nothing more is sent,
14
+ * and which resolves only once the request in flight has settled, so durable delivery can start strictly after it.
15
+ */
16
+ import { scanForSecrets } from "./evidence.js";
17
+ import { shapeReply, shapeReplyPrefix } from "./reply-shape.js";
18
+ /** The status line's fixed words (§2.3). A running tool reads as its own label plus "…". */
19
+ export const LIVE_STATUS = {
20
+ queued: "Waiting for your earlier messages…",
21
+ thinking: "Thinking…",
22
+ writing: "Writing…",
23
+ repair: "Fixing the reply…",
24
+ shorten: "Shortening the reply…",
25
+ /** A preview closed because text looked like a password or key; the turn itself may still answer. */
26
+ held: "Working on it…",
27
+ };
28
+ /** One token that could still grow into a match of a secret family: a non-empty prefix of one of its openings, or the
29
+ * whole opening followed by its body characters. Preceded by a non-word character or the start, as the detectors'
30
+ * own word boundaries are. */
31
+ function opening(leads, body) {
32
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\/-]/g, "\\$&");
33
+ const alternatives = new Set();
34
+ for (const lead of leads) {
35
+ for (let length = 1; length < lead.length; length += 1)
36
+ alternatives.add(escape(lead.slice(0, length)));
37
+ alternatives.add(`${escape(lead)}${body}*`);
38
+ }
39
+ return `(?:${[...alternatives].join("|")})`;
40
+ }
41
+ /**
42
+ * The start of every `SECRET_PATTERNS` family (evidence.ts), checked against the end of the text written so far:
43
+ * while any matches, the partial is withheld. Single-token families look at the unfinished last token; the families
44
+ * whose match spans spaces (a private-key header, an npm `_authToken = value`) look at the whole last line.
45
+ */
46
+ const SECRET_STARTS = [
47
+ // private-key: "-----BEGIN RSA PRIVATE KEY-----", partway. No start boundary: the complete detector has none either.
48
+ /-{1,5}(?:B(?:E(?:G(?:I(?:N(?:[ \t][A-Z \t]*-{0,5})?)?)?)?)?)?$/,
49
+ // npm-auth-token: "_authToken = value", partway.
50
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9])${opening(["_authToken"], String.raw `[\s=]`)}\S*$`),
51
+ // aws-access-key, github-token, slack-token, openai-key, stripe-key, google-api-key, gitlab-token.
52
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9_])${opening(["AKIA", "ASIA"], "[0-9A-Z]")}$`),
53
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9_])${opening(["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"], "[A-Za-z0-9_]")}$`),
54
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9_])${opening(["xoxb-", "xoxa-", "xoxp-", "xoxr-", "xoxs-"], "[A-Za-z0-9-]")}$`),
55
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9_])${opening(["sk-"], "[A-Za-z0-9_-]")}$`),
56
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9_])${opening(["sk_live_", "sk_test_", "rk_live_", "rk_test_", "whsec_"], "[A-Za-z0-9]")}$`),
57
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9_])${opening(["AIza"], "[0-9A-Za-z_-]")}$`),
58
+ new RegExp(String.raw `(?:^|[^A-Za-z0-9_])${opening(["glpat-"], "[A-Za-z0-9_-]")}$`),
59
+ // telegram-bot-token: "123456789:AA…".
60
+ /(?:^|[^A-Za-z0-9_])\d+(?::(?:A(?:A[A-Za-z0-9_-]*)?)?)?$/,
61
+ // discord-bot-token: "M…24.…6.…27".
62
+ /(?:^|[^A-Za-z0-9_])[MN][A-Za-z\d]*(?:\.[\w-]*(?:\.[\w-]*)?)?$/,
63
+ // password-in-url: "scheme://user:password@host", partway (a scheme and its colon are enough to wait).
64
+ /(?:^|[^A-Za-z0-9+.-])[a-z][a-z0-9+.-]*:\S*$/i,
65
+ ];
66
+ /** Could the end of this text be the start of a password or key? Then nothing of it shows yet. */
67
+ export function secretMayStart(text) {
68
+ const lastLine = text.replace(/\r\n?/g, "\n").split("\n").at(-1) ?? "";
69
+ return SECRET_STARTS.some(pattern => pattern.test(lastLine));
70
+ }
71
+ /**
72
+ * What of a reply still being written is settled: everything before its last whitespace (the unfinished last word
73
+ * stays out), then cut back before any code fence, code span, bold run or link that hasn't closed yet. Trailing
74
+ * whitespace is trimmed.
75
+ */
76
+ export function settledText(raw) {
77
+ const text = raw.replace(/\r\n?/g, "\n");
78
+ let end = text.length;
79
+ while (end > 0 && !/\s/.test(text[end - 1]))
80
+ end -= 1;
81
+ const shown = text.slice(0, end);
82
+ let fence = -1, code = -1, bold = -1, link = -1;
83
+ let linkState = null;
84
+ for (let index = 0; index < shown.length; index += 1) {
85
+ if (shown.startsWith("```", index)) {
86
+ fence = fence === -1 ? index : -1;
87
+ index += 2;
88
+ continue;
89
+ }
90
+ if (fence !== -1)
91
+ continue;
92
+ const char = shown[index];
93
+ if (char === "\n") {
94
+ code = -1;
95
+ bold = -1;
96
+ link = -1;
97
+ linkState = null;
98
+ continue;
99
+ }
100
+ if (char === "`") {
101
+ code = code === -1 ? index : -1;
102
+ continue;
103
+ }
104
+ if (code !== -1)
105
+ continue;
106
+ if (shown.startsWith("**", index)) {
107
+ bold = bold === -1 ? index : -1;
108
+ index += 1;
109
+ continue;
110
+ }
111
+ if (char === "[" && linkState === null) {
112
+ link = index;
113
+ linkState = "label";
114
+ continue;
115
+ }
116
+ if (linkState === "label" && char === "]") {
117
+ if (shown[index + 1] === "(") {
118
+ linkState = "url";
119
+ index += 1;
120
+ }
121
+ else {
122
+ link = -1;
123
+ linkState = null;
124
+ }
125
+ continue;
126
+ }
127
+ if (linkState === "url" && (char === ")" || /\s/.test(char))) {
128
+ link = -1;
129
+ linkState = null;
130
+ }
131
+ }
132
+ const cut = Math.min(...[fence, code, bold, link].filter(at => at !== -1), shown.length);
133
+ return shown.slice(0, cut).trimEnd();
134
+ }
135
+ /**
136
+ * A partial as it may be shown: `blocked` when it holds a password or key (the preview closes), `withheld` while its
137
+ * tail could still become one (show nothing new), else the settled text shaped for display. A step's finished text
138
+ * (`final`, secret-checked whole by the engine) shows whole.
139
+ */
140
+ export function partialDisplay(raw, final, shape = {}) {
141
+ if (scanForSecrets(raw).length > 0)
142
+ return { kind: "blocked" };
143
+ if (!final && secretMayStart(raw))
144
+ return { kind: "withheld" };
145
+ const text = final ? shapeReply(raw, shape) : shapeReplyPrefix(settledText(raw), shape);
146
+ // Shaping only relabels and removes; a secret in what it returns would mean it came from somewhere else. Never shown.
147
+ if (scanForSecrets(text).length > 0)
148
+ return { kind: "blocked" };
149
+ return { kind: "shown", text };
150
+ }
151
+ /** One turn's live reply. `shape`: the owner's message (`asked`) and this app's origins, as the final reply is shaped. */
152
+ export function createLiveReply(shape = {}, options = {}) {
153
+ const steps = [];
154
+ let queued = options.queued === true;
155
+ let closed = null;
156
+ let turn = null;
157
+ let last = "";
158
+ const current = () => steps.at(-1) ?? (steps.push({ calls: [], raw: "", shown: "" }), steps.at(-1));
159
+ const status = () => {
160
+ const step = steps.at(-1);
161
+ const running = step?.calls.filter(call => call.state === "running").at(-1);
162
+ if (running !== undefined)
163
+ return `${running.label}…`;
164
+ if (step?.reason !== undefined)
165
+ return LIVE_STATUS[step.reason];
166
+ if (closed === "blocked")
167
+ return LIVE_STATUS.held;
168
+ if (step !== undefined && step.raw.trim() !== "")
169
+ return LIVE_STATUS.writing;
170
+ return LIVE_STATUS.thinking;
171
+ };
172
+ const view = () => {
173
+ if (closed !== null)
174
+ return { phase: "closed", why: closed };
175
+ if (queued && steps.length === 0)
176
+ return { phase: "queued" };
177
+ const text = [...steps].reverse().find(step => step.shown !== "")?.shown ?? "";
178
+ return text === "" ? { phase: "working", status: status() } : { phase: "writing", status: status(), text };
179
+ };
180
+ const changed = () => {
181
+ const now = JSON.stringify(view());
182
+ if (now === last)
183
+ return false;
184
+ last = now;
185
+ return true;
186
+ };
187
+ last = JSON.stringify(view());
188
+ return {
189
+ get turn() { return turn; },
190
+ push(event) {
191
+ if (closed !== null)
192
+ return false;
193
+ turn ??= event.turn;
194
+ queued = false;
195
+ switch (event.kind) {
196
+ case "started": break;
197
+ case "step":
198
+ steps.push({ calls: [], raw: "", shown: "", ...(event.reason === undefined ? {} : { reason: event.reason }) });
199
+ break;
200
+ case "tool":
201
+ current().calls.push({ id: event.id, label: event.label, state: "running" });
202
+ break;
203
+ case "tool-result":
204
+ for (const step of steps) {
205
+ const index = step.calls.findIndex(call => call.id === event.id);
206
+ if (index !== -1) {
207
+ step.calls[index] = { id: event.id, label: step.calls[index].label, ...event.outcome };
208
+ break;
209
+ }
210
+ }
211
+ break;
212
+ case "text": {
213
+ const step = current();
214
+ step.raw = event.text;
215
+ const shown = partialDisplay(event.text, event.final === true, shape);
216
+ if (shown.kind === "blocked") {
217
+ closed = "blocked";
218
+ for (const one of steps)
219
+ one.shown = "";
220
+ }
221
+ else if (shown.kind === "shown")
222
+ step.shown = shown.text;
223
+ break;
224
+ }
225
+ case "blocked":
226
+ closed = "blocked";
227
+ for (const one of steps)
228
+ one.shown = "";
229
+ break;
230
+ }
231
+ return changed();
232
+ },
233
+ close(why) {
234
+ if (closed === null)
235
+ closed = why;
236
+ for (const one of steps)
237
+ one.shown = "";
238
+ changed();
239
+ },
240
+ view,
241
+ status,
242
+ tools: () => steps.map(step => step.calls.map(call => ({ ...call }))),
243
+ };
244
+ }
245
+ /**
246
+ * The serialized sender for one live surface message (§2.4). `standing` re-proves the viewer's right to see this turn
247
+ * before every send (it may await; the close is re-checked after it). `spacingMs` is the least time between two sends,
248
+ * read before each one (a surface widens it after the platform asks for a pause). A failed send drops that view:
249
+ * previews are best effort.
250
+ */
251
+ export function createLiveSender(options) {
252
+ const spacing = () => typeof options.spacingMs === "function" ? options.spacingMs() : options.spacingMs ?? 0;
253
+ const now = options.now ?? Date.now;
254
+ let generation = 0;
255
+ let closed = false;
256
+ let pending = null;
257
+ let inflight = null;
258
+ let timer = null;
259
+ let lastSent = Number.NEGATIVE_INFINITY;
260
+ const pump = () => {
261
+ if (closed || inflight !== null || timer !== null || pending === null)
262
+ return;
263
+ const wait = lastSent + spacing() - now();
264
+ if (wait > 0) {
265
+ timer = setTimeout(() => { timer = null; pump(); }, wait);
266
+ timer.unref?.();
267
+ return;
268
+ }
269
+ const next = pending;
270
+ pending = null;
271
+ inflight = (async () => {
272
+ try {
273
+ if (options.standing !== undefined && !(await options.standing()))
274
+ return;
275
+ if (closed)
276
+ return;
277
+ lastSent = now();
278
+ await options.send(next.view, next.generation);
279
+ }
280
+ catch { /* best effort: this view is dropped */ }
281
+ })().finally(() => { inflight = null; pump(); });
282
+ };
283
+ return {
284
+ get generation() { return generation; },
285
+ get closed() { return closed; },
286
+ offer(view) {
287
+ if (closed)
288
+ return;
289
+ generation += 1;
290
+ pending = { view, generation };
291
+ pump();
292
+ },
293
+ async close() {
294
+ closed = true;
295
+ pending = null;
296
+ if (timer !== null)
297
+ clearTimeout(timer);
298
+ timer = null;
299
+ await inflight;
300
+ },
301
+ };
302
+ }
@@ -181,6 +181,8 @@ export const CHAT_ACTION_LOCAL_ACTS = Object.freeze({
181
181
  result_accept: "complete-task",
182
182
  task_cancel: "stop-run",
183
183
  task_resume: "resume-run",
184
+ // A Start card: the fast start's own act (approve() allows it only with a fast marker).
185
+ task_start: "build-now",
184
186
  });
185
187
  /** Every task card operation (chat-task-actions.ts), decided for the local owner. */
186
188
  export const CHAT_TASK_ACTION_LOCAL_ACTS = Object.freeze({
@@ -0,0 +1,22 @@
1
+ /**
2
+ * v159: one-step start from chat (docs/plans/one-step-chat.md). Adds only: a saved row reads as it did. Kept beside
3
+ * nothing else so the update road can read the lists without the store, and so the version is easy to renumber.
4
+ *
5
+ * - `fast_preview.turn`: the lead turn a chat start's preview was prepared in (fast-start.ts prepareChatStart). Saved
6
+ * rows read NULL; a chat preview without a turn is unusable, and the form's previews never have one.
7
+ * - `chat_typed_start` (C5): a typed yes to a Start card, recorded with the inbound message in the transaction that saves
8
+ * it (typed-start.ts): which card it answered (the proposal and the chat message that carried it), the card's action
9
+ * stamp then, how it was tied to the card (`reply` to it, or the `next` message after it), and once settled, the
10
+ * outcome and the task it started. A new table: nothing saved changes. `proposal` is a plain number, not a reference:
11
+ * the record has to outlive its card. Ending the chat deletes the card's proposal, and a yes already recorded for it
12
+ * must still be answered by Toolroll ("didn't start anything") and never handed to the lead as a new message. Proposal
13
+ * ids are never reused (AUTOINCREMENT), so a kept number can't come to name another card.
14
+ */
15
+ export declare const V159_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_typed_start (\n provider TEXT NOT NULL CHECK (provider IN ('telegram')),\n event TEXT NOT NULL,\n proposal INTEGER NOT NULL,\n card_message TEXT NOT NULL,\n stamp TEXT NOT NULL,\n how TEXT NOT NULL CHECK (how IN ('reply', 'next')),\n created TEXT NOT NULL,\n outcome TEXT CHECK (outcome IS NULL OR outcome IN ('started', 'refused', 'not-counted')),\n task_id TEXT,\n finished TEXT,\n PRIMARY KEY (provider, event),\n FOREIGN KEY (provider, event) REFERENCES chat_event(provider, id)\n);\nCREATE INDEX IF NOT EXISTS chat_typed_start_proposal ON chat_typed_start (proposal);\n";
16
+ export declare const V159_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
17
+ definition: string;
18
+ saved: string;
19
+ }>>>>;
20
+ export declare const V159_TABLES: readonly ["chat_typed_start"];
21
+ /** The v159 migration's steps, in order (a test seam may stop after each). */
22
+ export declare const V159_STEPS: readonly ["one-step-chat-tables", "one-step-chat-columns"];
@@ -0,0 +1,37 @@
1
+ /**
2
+ * v159: one-step start from chat (docs/plans/one-step-chat.md). Adds only: a saved row reads as it did. Kept beside
3
+ * nothing else so the update road can read the lists without the store, and so the version is easy to renumber.
4
+ *
5
+ * - `fast_preview.turn`: the lead turn a chat start's preview was prepared in (fast-start.ts prepareChatStart). Saved
6
+ * rows read NULL; a chat preview without a turn is unusable, and the form's previews never have one.
7
+ * - `chat_typed_start` (C5): a typed yes to a Start card, recorded with the inbound message in the transaction that saves
8
+ * it (typed-start.ts): which card it answered (the proposal and the chat message that carried it), the card's action
9
+ * stamp then, how it was tied to the card (`reply` to it, or the `next` message after it), and once settled, the
10
+ * outcome and the task it started. A new table: nothing saved changes. `proposal` is a plain number, not a reference:
11
+ * the record has to outlive its card. Ending the chat deletes the card's proposal, and a yes already recorded for it
12
+ * must still be answered by Toolroll ("didn't start anything") and never handed to the lead as a new message. Proposal
13
+ * ids are never reused (AUTOINCREMENT), so a kept number can't come to name another card.
14
+ */
15
+ export const V159_SCHEMA = `
16
+ CREATE TABLE IF NOT EXISTS chat_typed_start (
17
+ provider TEXT NOT NULL CHECK (provider IN ('telegram')),
18
+ event TEXT NOT NULL,
19
+ proposal INTEGER NOT NULL,
20
+ card_message TEXT NOT NULL,
21
+ stamp TEXT NOT NULL,
22
+ how TEXT NOT NULL CHECK (how IN ('reply', 'next')),
23
+ created TEXT NOT NULL,
24
+ outcome TEXT CHECK (outcome IS NULL OR outcome IN ('started', 'refused', 'not-counted')),
25
+ task_id TEXT,
26
+ finished TEXT,
27
+ PRIMARY KEY (provider, event),
28
+ FOREIGN KEY (provider, event) REFERENCES chat_event(provider, id)
29
+ );
30
+ CREATE INDEX IF NOT EXISTS chat_typed_start_proposal ON chat_typed_start (proposal);
31
+ `;
32
+ export const V159_ADDED_COLUMNS = Object.freeze({
33
+ fast_preview: { turn: { definition: "INTEGER", saved: "NULL" } },
34
+ });
35
+ export const V159_TABLES = Object.freeze(["chat_typed_start"]);
36
+ /** The v159 migration's steps, in order (a test seam may stop after each). */
37
+ export const V159_STEPS = Object.freeze(["one-step-chat-tables", "one-step-chat-columns"]);
package/dist/operate.d.ts CHANGED
@@ -5,7 +5,7 @@ import { type TriggerIo } from "./flow-triggers.js";
5
5
  import { type GhRead } from "./nightly-github.js";
6
6
  import type { StepIo } from "./flow-steps.js";
7
7
  import type { FetchLike } from "./flow-share.js";
8
- import { type Store } from "./store.js";
8
+ import { type Store, type Publication } from "./store.js";
9
9
  import { type LaptopSeams } from "./laptop-background.js";
10
10
  import { type DispatchAdapter } from "./sync.js";
11
11
  import { type ParentToRole, type RolePort, type UpRoleStart, type UpStartAuthority } from "./up-processes.js";
@@ -373,3 +373,29 @@ export declare function upLaptopPortCandidates(store: Store, fromSeam?: readonly
373
373
  /** The watch loop's background learning pass (docs/plans/learning-loops.md): advisory reads, each project at most every
374
374
  * 30 minutes through the plane's own gh. A failure here never reaches the loop: it is tried on the next pass. */
375
375
  export declare function refreshLearning(store: Store, now: () => Date, reads?: GhRead): Promise<void>;
376
+ /** A project loop's share in its process's service beats. */
377
+ export type ServiceBeatMember = {
378
+ context: Context;
379
+ progress: (line: string) => void;
380
+ /** This loop's reconciliation stands and it is not stopping: the fast CI pass runs only while every member's does. */
381
+ ciReady: () => boolean;
382
+ };
383
+ /** The delivery beat: web push and outgoing webhooks, off the build path. */
384
+ export declare const DELIVERY_EVERY_MS = 5000;
385
+ /** The fast CI beat: only while an opened pull request waits on its checks or its merge. */
386
+ export declare const FAST_CI_EVERY_MS = 30000;
387
+ /** The most pull requests one fast CI pass reads. */
388
+ export declare const FAST_CI_LIMIT = 10;
389
+ /**
390
+ * The opened pull requests the fast CI pass reads, oldest read or tried (`attempted`) first, at most `limit`: checks not settled (never read,
391
+ * running, or "none" shortly after an update), or green with its merge still waiting (a merge intent in flight, or a
392
+ * followed pull request not yet marked ready on its accepted commit). Failing and settled ones stay on the 5 minute pass.
393
+ */
394
+ export declare function fastCiWaiting(store: Store, now: Date, limit?: number, attempted?: ReadonlyMap<number, string>): Publication[];
395
+ /**
396
+ * Join this process's service beats: one delivery beat (web push and outgoing webhooks every 5 s, and at once when
397
+ * the wake moves with new notifications) and one fast CI beat (every 30 s, the existing `observeChecks` reader, only
398
+ * for pull requests that wait). One set per store, however many project loops join; the last to leave stops them.
399
+ * Opening a pull request stays on the tick path, and the 5 minute pass alone may set reconciliation ready.
400
+ */
401
+ export declare function joinServiceBeats(member: ServiceBeatMember): () => Promise<void>;