toolroll 0.9.95 โ†’ 0.9.97

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 (75) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/browser/workspace.css +2 -2
  3. package/dist/browser/workspace.js +32 -32
  4. package/dist/browser-workspace.d.ts +2 -0
  5. package/dist/card-render.d.ts +18 -1
  6. package/dist/card-render.js +40 -3
  7. package/dist/chat-delivery.d.ts +5 -1
  8. package/dist/chat-delivery.js +74 -25
  9. package/dist/chat-follow.d.ts +6 -3
  10. package/dist/chat-follow.js +23 -7
  11. package/dist/chat-live-schema.d.ts +17 -0
  12. package/dist/chat-live-schema.js +32 -0
  13. package/dist/chat-live.d.ts +161 -0
  14. package/dist/chat-live.js +491 -0
  15. package/dist/contracts/console-api.d.ts +1 -0
  16. package/dist/contracts/console-api.js +1 -0
  17. package/dist/desktop-update.d.ts +1 -1
  18. package/dist/discord-api.d.ts +3 -1
  19. package/dist/discord-api.js +9 -3
  20. package/dist/discord-chat.d.ts +13 -3
  21. package/dist/discord-chat.js +77 -21
  22. package/dist/discord.js +8 -1
  23. package/dist/lead-answer-eval.d.ts +86 -0
  24. package/dist/lead-answer-eval.js +255 -0
  25. package/dist/lead-cli-live.d.ts +56 -0
  26. package/dist/lead-cli-live.js +200 -0
  27. package/dist/lead-cli.d.ts +3 -0
  28. package/dist/lead-cli.js +34 -4
  29. package/dist/lead-latency-schema.d.ts +24 -0
  30. package/dist/lead-latency-schema.js +36 -0
  31. package/dist/lead-progress.d.ts +10 -1
  32. package/dist/lead-progress.js +1 -1
  33. package/dist/lead-queue-runtime.d.ts +1 -1
  34. package/dist/lead-queue-runtime.js +1 -1
  35. package/dist/lead-route.d.ts +56 -0
  36. package/dist/lead-route.js +104 -0
  37. package/dist/lead.d.ts +4 -2
  38. package/dist/lead.js +79 -26
  39. package/dist/live-reply.d.ts +83 -0
  40. package/dist/live-reply.js +302 -0
  41. package/dist/operate.js +39 -1
  42. package/dist/project-knowledge.d.ts +3 -3
  43. package/dist/reply-shape.d.ts +9 -0
  44. package/dist/reply-shape.js +13 -0
  45. package/dist/serve.js +11 -4
  46. package/dist/server/chat.d.ts +2 -1
  47. package/dist/server/chat.js +18 -6
  48. package/dist/server/live.js +18 -21
  49. package/dist/server/render-chat.d.ts +5 -0
  50. package/dist/server/render-chat.js +4 -2
  51. package/dist/server/render-settings.d.ts +8 -0
  52. package/dist/server/render-settings.js +13 -2
  53. package/dist/server/route-table.js +2 -0
  54. package/dist/server/settings.js +14 -1
  55. package/dist/slack-api.js +11 -0
  56. package/dist/slack-chat.d.ts +11 -1
  57. package/dist/slack-chat.js +50 -5
  58. package/dist/slack-settings.js +8 -1
  59. package/dist/slack.js +10 -3
  60. package/dist/store.d.ts +31 -2
  61. package/dist/store.js +59 -6
  62. package/dist/subscription-chat.d.ts +2 -0
  63. package/dist/subscription-chat.js +3 -1
  64. package/dist/teams-chat.d.ts +9 -2
  65. package/dist/teams-chat.js +50 -2
  66. package/dist/teams.js +5 -2
  67. package/dist/teardown-git.js +33 -3
  68. package/dist/telegram-lead.d.ts +3 -0
  69. package/dist/telegram-lead.js +63 -9
  70. package/dist/telegram-live.d.ts +116 -0
  71. package/dist/telegram-live.js +224 -0
  72. package/dist/telegram.d.ts +2 -0
  73. package/dist/telegram.js +50 -2
  74. package/dist/toolroll-update.js +4 -1
  75. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ import { createLiveReply, createLiveSender, LIVE_STATUS } from "./live-reply.js";
2
+ import { shapedWindow, telegramReply } from "./reply-shape.js";
3
+ /** The acknowledgement on the owner's message: "seen, working on it". Removed once the answer or the failure notice is
4
+ * delivered; never replaced by a "done" mark (an answer isn't finished work). */
5
+ export const ACK_EMOJI = "๐Ÿ‘€";
6
+ /** At most one reaction change per chat this often; a burst waits its turn, in order. */
7
+ export const ACK_GAP_MS = 1_000;
8
+ /** A reaction change that meets Telegram's pause waits it out when it is this short; a longer pause skips it. */
9
+ const ACK_MAX_WAIT_MS = 30_000;
10
+ /** Drafts start this far apart and widen (doubling, up to the cap) whenever Telegram asks for a pause. */
11
+ export const DRAFT_SPACING_MS = 1_200;
12
+ const DRAFT_MAX_SPACING_MS = 10_000;
13
+ /** Telegram keeps a draft about 30 seconds: one that has not changed is sent again after this long. */
14
+ export const DRAFT_KEEPALIVE_MS = 25_000;
15
+ /** Telegram's message limit, counted after entities (UTF-16 units, as JS strings are). */
16
+ export const TELEGRAM_TEXT_MAX = 4_096;
17
+ /** The body of a writing draft within `budget` characters once rendered: cut at a safe place in the shaped text first
18
+ * (never inside code, a link, a bold anchor or an emoji), rendered, measured, and cut smaller if it still doesn't fit.
19
+ * A body that can't fit (a single link longer than the budget) shows nothing. */
20
+ export function draftBody(shaped, budget) {
21
+ if (budget <= 0)
22
+ return { text: "", entities: [] };
23
+ let size = budget;
24
+ for (let attempt = 0; attempt < 8 && size >= 1; attempt += 1) {
25
+ const window = shapedWindow(shaped, size);
26
+ const cut = window.length < shaped.length;
27
+ const rendered = telegramReply(cut ? window.trimEnd() : window);
28
+ const text = cut ? `${rendered.text} โ€ฆ` : rendered.text;
29
+ if (text.length <= budget)
30
+ return { text, entities: rendered.entities };
31
+ size = Math.min(size - 1, Math.floor(size * budget / text.length) - 2);
32
+ }
33
+ return { text: "", entities: [] };
34
+ }
35
+ /**
36
+ * One live view as a draft: the status line in italics, then (once text exists) the partial reply as text plus
37
+ * entities, never Markdown. Plain "Thinkingโ€ฆ" is Telegram's own empty-text placeholder. Closed views send nothing.
38
+ */
39
+ export function telegramDraftOf(view) {
40
+ if (view.phase === "closed")
41
+ return null;
42
+ if (view.phase === "queued")
43
+ return { text: LIVE_STATUS.queued, entities: [{ type: "italic", offset: 0, length: LIVE_STATUS.queued.length }] };
44
+ if (view.phase === "working") {
45
+ if (view.status === LIVE_STATUS.thinking)
46
+ return { text: "", entities: [] };
47
+ return { text: view.status, entities: [{ type: "italic", offset: 0, length: view.status.length }] };
48
+ }
49
+ const head = view.status;
50
+ const gap = "\n\n";
51
+ const body = draftBody(view.text, TELEGRAM_TEXT_MAX - head.length - gap.length);
52
+ if (body.text === "")
53
+ return { text: head, entities: [{ type: "italic", offset: 0, length: head.length }] };
54
+ const shift = head.length + gap.length;
55
+ return {
56
+ text: `${head}${gap}${body.text}`,
57
+ entities: [{ type: "italic", offset: 0, length: head.length }, ...body.entities.map(one => ({ ...one, offset: one.offset + shift }))],
58
+ };
59
+ }
60
+ /** One draft on the wire, under the shared pause. A retry_after is recorded for every sender of this bot. */
61
+ async function sendDraft(store, botId, transport, clock, chatId, draftId, payload) {
62
+ if (store.telegramRetryAt(botId) > clock().toISOString())
63
+ return "paused";
64
+ let answer;
65
+ try {
66
+ answer = await transport("sendMessageDraft", { chat_id: Number(chatId), draft_id: draftId, text: payload.text, ...(payload.entities.length === 0 ? {} : { entities: payload.entities }) });
67
+ }
68
+ catch {
69
+ return "lost";
70
+ }
71
+ if (answer.ok)
72
+ return "sent";
73
+ const retry = answer.parameters?.retry_after;
74
+ if (typeof retry === "number" && Number.isFinite(retry) && retry > 0) {
75
+ store.deferTelegram(botId, new Date(clock().getTime() + Math.ceil(retry) * 1_000).toISOString());
76
+ return "paused";
77
+ }
78
+ return answer.uncertain === true ? "lost" : "refused";
79
+ }
80
+ // ---- the ๐Ÿ‘€ acknowledgement ----------------------------------------------------------
81
+ /** One lane per chat: reaction changes go out in order (an ack never lands after its own removal), spaced. */
82
+ const ackLanes = new Map();
83
+ const pause = (ms) => new Promise(resolve => { const timer = setTimeout(resolve, ms); timer.unref?.(); });
84
+ /**
85
+ * Put ๐Ÿ‘€ on the owner's message (`on`) or take it off. Best effort and never awaited by the caller's durable work: a
86
+ * missing pairing, a failure or a long pause skips it silently. `paired` is re-checked at the moment of sending; a
87
+ * reaction carries no project data, so the pairing alone fences it. Resolves once this change was tried.
88
+ */
89
+ export function telegramAck(args) {
90
+ const key = `${args.botId}:${args.chatId}`;
91
+ const lane = ackLanes.get(key) ?? { tail: Promise.resolve(), lastAt: Number.NEGATIVE_INFINITY };
92
+ ackLanes.set(key, lane);
93
+ const gap = args.gapMs ?? ACK_GAP_MS;
94
+ // Never throws: a store closed meanwhile (a stop) or anything else skips the change.
95
+ const run = () => change().catch(() => undefined);
96
+ const change = async () => {
97
+ const spaced = lane.lastAt + gap - Date.now();
98
+ if (spaced > 0)
99
+ await pause(spaced);
100
+ const held = Date.parse(args.store.telegramRetryAt(args.botId)) - args.clock().getTime();
101
+ if (Number.isFinite(held) && held > 0) {
102
+ if (held > ACK_MAX_WAIT_MS)
103
+ return;
104
+ await pause(held);
105
+ }
106
+ if (!args.paired())
107
+ return;
108
+ lane.lastAt = Date.now();
109
+ try {
110
+ const answer = await args.transport("setMessageReaction", {
111
+ chat_id: Number(args.chatId), message_id: Number(args.messageId), reaction: args.on ? [{ type: "emoji", emoji: ACK_EMOJI }] : [],
112
+ });
113
+ const retry = answer.ok ? undefined : answer.parameters?.retry_after;
114
+ if (typeof retry === "number" && Number.isFinite(retry) && retry > 0)
115
+ args.store.deferTelegram(args.botId, new Date(args.clock().getTime() + Math.ceil(retry) * 1_000).toISOString());
116
+ }
117
+ catch { /* skipped silently */ }
118
+ };
119
+ const done = lane.tail.then(run, run);
120
+ lane.tail = done;
121
+ // A finished lane is forgotten; the next change starts a fresh one (spacing restarts from now).
122
+ void done.then(() => { if (ackLanes.get(key) === lane && lane.tail === done && Date.now() - lane.lastAt >= gap)
123
+ ackLanes.delete(key); });
124
+ return done;
125
+ }
126
+ // ---- the draft --------------------------------------------------------------------------
127
+ /** Chats (bot:binding) whose turn has a live draft in this process: a waiting message's draft never covers it. */
128
+ const liveChats = new Set();
129
+ /** When each waiting message last had its "Waiting for your earlier messagesโ€ฆ" draft (bot:row). */
130
+ const waitingDrafts = new Map();
131
+ /**
132
+ * One running turn's draft. `standing` is the caller's full proof for a draft that carries words (it may await; the
133
+ * close is re-checked after it); `paired` is enough for Telegram's empty "Thinkingโ€ฆ". A draft Telegram refuses outright
134
+ * (not a pause, not a lost answer) turns this turn's drafts off; previews are best effort.
135
+ */
136
+ export function createTelegramLiveDraft(args) {
137
+ const reply = createLiveReply(args.shape);
138
+ const keepAlive = args.timing?.keepAliveMs ?? DRAFT_KEEPALIVE_MS;
139
+ const base = args.timing?.draftSpacingMs ?? DRAFT_SPACING_MS;
140
+ const chat = `${args.botId}:${args.binding}`;
141
+ let spacing = base;
142
+ let lastSentAt = Date.now();
143
+ let off = false;
144
+ liveChats.add(chat);
145
+ /** What the draft shows now: a preview closed as a possible password or key keeps only the status line. */
146
+ const current = () => {
147
+ const view = reply.view();
148
+ if (view.phase !== "closed")
149
+ return view;
150
+ return view.why === "blocked" ? { phase: "working", status: LIVE_STATUS.held } : null;
151
+ };
152
+ const sender = createLiveSender({
153
+ spacingMs: () => spacing,
154
+ send: async (view) => {
155
+ if (off)
156
+ return;
157
+ const payload = telegramDraftOf(view);
158
+ const turn = reply.turn;
159
+ if (payload === null || turn === null)
160
+ return;
161
+ const allowed = payload.text === "" ? args.paired() : await args.standing(turn);
162
+ if (!allowed || sender.closed)
163
+ return;
164
+ const outcome = await sendDraft(args.store, args.botId, args.transport, args.clock, args.chatId, args.draftId, payload);
165
+ if (outcome === "sent")
166
+ lastSentAt = Date.now();
167
+ else if (outcome === "paused")
168
+ spacing = Math.min(DRAFT_MAX_SPACING_MS, spacing * 2);
169
+ else if (outcome === "refused")
170
+ off = true;
171
+ },
172
+ });
173
+ const timer = setInterval(() => {
174
+ if (sender.closed || Date.now() - lastSentAt < keepAlive)
175
+ return;
176
+ const view = current();
177
+ if (view !== null)
178
+ sender.offer(view);
179
+ }, Math.max(50, Math.min(keepAlive, 5_000)));
180
+ timer.unref?.();
181
+ return {
182
+ get turn() { return reply.turn; },
183
+ onProgress: event => {
184
+ // The first event names the turn: Telegram's "Thinkingโ€ฆ" shows at once, even before the view changes.
185
+ const first = reply.turn === null;
186
+ if (!reply.push(event) && !first)
187
+ return;
188
+ const view = current();
189
+ if (view !== null)
190
+ sender.offer(view);
191
+ },
192
+ close: async () => {
193
+ clearInterval(timer);
194
+ liveChats.delete(chat);
195
+ await sender.close();
196
+ },
197
+ };
198
+ }
199
+ /**
200
+ * A message waiting for the lead (busy, or behind the owner's earlier messages): "Waiting for your earlier messagesโ€ฆ"
201
+ * as a draft, refreshed only after the keep-alive interval, and never while this chat's running turn has its own
202
+ * draft. Carries no project data: the pairing and the claim fence it.
203
+ */
204
+ export async function telegramWaitingDraft(args) {
205
+ const key = `${args.botId}:${args.draftId}`;
206
+ if (liveChats.has(`${args.botId}:${args.binding}`))
207
+ return;
208
+ const last = waitingDrafts.get(key);
209
+ if (last !== undefined && Date.now() - last < (args.timing?.keepAliveMs ?? DRAFT_KEEPALIVE_MS))
210
+ return;
211
+ if (!args.allowed())
212
+ return;
213
+ const payload = telegramDraftOf({ phase: "queued" });
214
+ if (await sendDraft(args.store, args.botId, args.transport, args.clock, args.chatId, args.draftId, payload) !== "sent")
215
+ return;
216
+ waitingDrafts.set(key, Date.now());
217
+ // Bounded: the oldest entries go first.
218
+ while (waitingDrafts.size > 500)
219
+ waitingDrafts.delete(waitingDrafts.keys().next().value);
220
+ }
221
+ /** The message is answered or failed: its waiting draft is forgotten. */
222
+ export function forgetWaitingDraft(botId, draftId) {
223
+ waitingDrafts.delete(`${botId}:${draftId}`);
224
+ }
@@ -258,3 +258,5 @@ export declare function digestText(rows: readonly Notification[], since: string
258
258
  */
259
259
  /** At most this many of a chat's messages wait for the lead at once (the personal lead queue's bound). */
260
260
  export declare const TELEGRAM_MAX_WAITING = 10;
261
+ /** A pushed update was kept for this bot: wake its bridge if it runs in this process (docs/plans/responsive-chat.md ยง3.1). */
262
+ export declare function wakeTelegramBridge(botId: string): void;
package/dist/telegram.js CHANGED
@@ -47,6 +47,7 @@ import { phoneCommand, phoneStatus, phoneTaskView, PHONE_CONSOLE_FOOTER, PHONE_H
47
47
  import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
48
48
  import { envValue } from "./names.js";
49
49
  import { applyProposalTap, processTelegramConversations, phoneLinkButton, replyContextFor, telegramConversationRepos, telegramRequestId, tooLongText, whichTaskText, } from "./telegram-lead.js";
50
+ import { telegramAck } from "./telegram-live.js";
50
51
  import { askOf, parseTelegramUpdate, pickOf, readTelegramButtonData, readTelegramUpdate, telegramButton } from "./contracts/telegram-callback.js";
51
52
  /** Read the enrolled project list on demand. No callback means no task data,
52
53
  * never an implicit all-database ceiling. Shared by pass and embedded follower. */
@@ -119,7 +120,11 @@ export function keepPushedUpdate(store, botId, body, now) {
119
120
  console.warn(`Ignoring a pushed Telegram update: ${update.issues.map(issue => issue.line).join("; ")}`);
120
121
  return { ok: true, kept: false };
121
122
  }
122
- return { ok: true, kept: store.queueTelegramUpdate(botId, update.value.update_id, JSON.stringify(raw), now) };
123
+ const kept = store.queueTelegramUpdate(botId, update.value.update_id, JSON.stringify(raw), now);
124
+ // The bridge in this process (if any) stops idling and drains it now; elsewhere it waits at most one cycle.
125
+ if (kept)
126
+ wakeTelegramBridge(botId);
127
+ return { ok: true, kept };
123
128
  }
124
129
  /**
125
130
  * Environment wins, the credential file beside the database otherwise.
@@ -387,6 +392,21 @@ export async function followBridge(store, options) {
387
392
  }
388
393
  signal.addEventListener("abort", finish, { once: true });
389
394
  }));
395
+ /** The push idle: the ordinary wait, ended early when this process keeps a push for this bot. */
396
+ const idle = (ms) => {
397
+ if (bridgeWokenEarly.delete(botId))
398
+ return Promise.resolve();
399
+ let wake = () => { };
400
+ const woken = new Promise(resolve => { wake = resolve; });
401
+ const wakers = bridgeWakers.get(botId) ?? new Set();
402
+ bridgeWakers.set(botId, wakers);
403
+ wakers.add(wake);
404
+ return Promise.race([wait(ms), woken]).finally(() => {
405
+ wakers.delete(wake);
406
+ if (wakers.size === 0 && bridgeWakers.get(botId) === wakers)
407
+ bridgeWakers.delete(botId);
408
+ });
409
+ };
390
410
  const total = { cycles: 0, sent: 0, answered: 0, paired: 0, ignored: 0, problems: [] };
391
411
  let commandsListed = false;
392
412
  let failures = 0;
@@ -568,7 +588,8 @@ export async function followBridge(store, options) {
568
588
  // While Telegram pushes (v98) there is no long poll to wait on: a pushed update waits at most this long.
569
589
  const floor = pushing() ? PUSH_IDLE_MS : FOLLOW_IDLE_FLOOR_MS;
570
590
  if (!signal.aborted && report.sent === 0 && report.answered === 0 && (report.chatAnswered ?? 0) === 0 && took < floor) {
571
- await wait(floor - took);
591
+ // While Telegram pushes, a push kept by this process's hook ends the idle at once.
592
+ await (pushing() ? idle(floor - took) : wait(floor - took));
572
593
  }
573
594
  }
574
595
  }
@@ -1886,6 +1907,19 @@ function applyConversation(context, update, effects) {
1886
1907
  request: telegramRequestId(botId, binding.id, update.update_id), text, context: context_, taskId, sourceRun,
1887
1908
  }, clock());
1888
1909
  report.chatQueued = (report.chatQueued ?? 0) + 1;
1910
+ // Saved: ๐Ÿ‘€ on the owner's message right away (docs/plans/responsive-chat.md ยง3.1), once this update's transaction
1911
+ // has committed. It comes off when the answer or the failure notice is delivered. Never awaited: a burst of messages
1912
+ // waits its turn on the chat's own lane, not the drain.
1913
+ const gapMs = context.conversation?.liveTiming?.ackGapMs;
1914
+ effects.push(async () => {
1915
+ void telegramAck({
1916
+ store, botId, transport, clock, chatId: binding.chatId, messageId: String(message.message_id), on: true, ...(gapMs === undefined ? {} : { gapMs }),
1917
+ paired: () => {
1918
+ const live = store.liveTelegramBindingById(binding.id);
1919
+ return live !== null && live.approverGeneration === binding.approverGeneration && store.accountOf(live.approver)?.role === "approver";
1920
+ },
1921
+ });
1922
+ });
1889
1923
  }
1890
1924
  /** The picker's buttons: one task per row (its title and where it stands),
1891
1925
  * carried by the task's numeric reference so the data fits Telegram's 64
@@ -2605,3 +2639,17 @@ function expiredDraftGuard(store, binding, decisionId, now) {
2605
2639
  store.setNoteDraftState(Number(expired["id"]), "discarded");
2606
2640
  return true;
2607
2641
  }
2642
+ /** Bridges in this process waiting out their push idle, by bot: a kept push ends the wait early. */
2643
+ const bridgeWakers = new Map();
2644
+ /** Bots with a push kept while no bridge idled (it was mid-cycle): the next idle doesn't wait. */
2645
+ const bridgeWokenEarly = new Set();
2646
+ /** A pushed update was kept for this bot: wake its bridge if it runs in this process (docs/plans/responsive-chat.md ยง3.1). */
2647
+ export function wakeTelegramBridge(botId) {
2648
+ const wakers = bridgeWakers.get(botId);
2649
+ if (wakers === undefined || wakers.size === 0) {
2650
+ bridgeWokenEarly.add(botId);
2651
+ return;
2652
+ }
2653
+ for (const wake of wakers)
2654
+ wake();
2655
+ }
@@ -43,6 +43,7 @@ import { V151_ADDED_COLUMNS } from "./storage.js";
43
43
  import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS, V148_ADDED_COLUMNS, V150_ADDED_COLUMNS } from "./store.js";
44
44
  import { V152_ADDED_COLUMNS } from "./code-map-store.js";
45
45
  import { V153_ADDED_COLUMNS } from "./plain-schema.js";
46
+ import { V158_ADDED_COLUMNS } from "./lead-latency-schema.js";
46
47
  import { isNewer, REGISTRY } from "./releases.js";
47
48
  import { markNeverIndex } from "./never-index.js";
48
49
  import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
@@ -429,7 +430,9 @@ const HISTORY_RULES = {
429
430
  // v154 adds a table only (check_stamp): no saved row changes, so no rule is needed for it.
430
431
  // v155 adds tables only (fleet-schema.ts): no saved row changes, so no rule is needed for it.
431
432
  // v156 adds tables only (fallback chains): likewise.
432
- ...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS, V148_ADDED_COLUMNS, V150_ADDED_COLUMNS, V151_ADDED_COLUMNS, V152_ADDED_COLUMNS, V153_ADDED_COLUMNS))
433
+ // v158 (after v157's chat-app tables): a saved lead step reads no tool names and no failure detail, and a saved reply no route and no failure detail
434
+ // (NULL); the quick-answer switch is a new table.
435
+ ...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS, V147_ADDED_COLUMNS, V148_ADDED_COLUMNS, V150_ADDED_COLUMNS, V151_ADDED_COLUMNS, V152_ADDED_COLUMNS, V153_ADDED_COLUMNS, V158_ADDED_COLUMNS))
433
436
  .map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
434
437
  };
435
438
  /** The tables a migration may remove whole (v115's removed features). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.95",
3
+ "version": "0.9.97",
4
4
  "description": "A control plane for unattended coding agents โ€” queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"