toolroll 0.9.53 → 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.
- package/CHANGELOG.md +8 -0
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.js +4 -13
- package/dist/discord.js +4 -14
- package/dist/export.js +4 -4
- package/dist/mate-tools.js +3 -2
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/operate.js +5 -2
- package/dist/plane-review.js +5 -11
- package/dist/project-delete.js +21 -26
- package/dist/result-shots.js +2 -2
- package/dist/retention.js +1 -1
- package/dist/server/settings.js +5 -4
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.js +6 -15
- package/dist/slack.js +6 -16
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/teammate-question.js +12 -12
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.js +2 -2
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
|
@@ -30,8 +30,8 @@ const who = (store, question) => { const mate = store.getTeammate(question.teamm
|
|
|
30
30
|
/** Everyone else's buttons retire once a question is answered anywhere. */
|
|
31
31
|
function retireEverywhere(store, question, now, state) {
|
|
32
32
|
store.retireTelegramQuestion(question, now);
|
|
33
|
-
state?.prepare("UPDATE chat_question_action SET consumed=? WHERE question=? AND consumed IS NULL").run(now.toISOString(), question);
|
|
34
|
-
state?.prepare("UPDATE chat_question_prompt SET consumed=? WHERE question=? AND consumed IS NULL").run(now.toISOString(), question);
|
|
33
|
+
state?.prepare("UPDATE chat_question_action SET consumed=? WHERE provider=:provider AND question=? AND consumed IS NULL").run(now.toISOString(), question);
|
|
34
|
+
state?.prepare("UPDATE chat_question_prompt SET consumed=? WHERE provider=:provider AND question=? AND consumed IS NULL").run(now.toISOString(), question);
|
|
35
35
|
}
|
|
36
36
|
// ---- Telegram ----------------------------------------------------------------------
|
|
37
37
|
/** Buttons for the question, minted before the send (for the person it asks only); `place` stamps the message they land on. */
|
|
@@ -91,23 +91,23 @@ export function questionParts(store, notification, binding) {
|
|
|
91
91
|
}
|
|
92
92
|
/** The live buttons on one part, in the order they were minted. */
|
|
93
93
|
export function chatQuestionButtons(state, part, now) {
|
|
94
|
-
const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
|
|
94
|
+
const row = state.prepare("SELECT payload FROM chat_part WHERE provider=:provider AND id=?").get(part);
|
|
95
95
|
const content = row === undefined ? null : partContent(String(row["payload"]));
|
|
96
96
|
const labels = new Map((content?.question?.choices ?? []).map(one => [one.choice ?? "", one.label]));
|
|
97
|
-
return state.prepare("SELECT token,choice FROM chat_question_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
|
|
97
|
+
return state.prepare("SELECT token,choice FROM chat_question_action WHERE provider=:provider AND part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
|
|
98
98
|
.map(one => ({ token: String(one.token), label: (labels.get(one.choice ?? "") ?? IN_WORDS).slice(0, 75), words: one.choice === null }));
|
|
99
99
|
}
|
|
100
100
|
/** Show the tapped notice again with the answer, and without its buttons. */
|
|
101
101
|
function repaint(state, part, event, line) {
|
|
102
|
-
const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
|
|
102
|
+
const row = state.prepare("SELECT payload FROM chat_part WHERE provider=:provider AND id=?").get(part);
|
|
103
103
|
const before = row === undefined ? { text: "" } : partContent(String(row["payload"]));
|
|
104
104
|
const content = { text: `${before.text}\n\n${line}`.slice(0, 3400), edit: event.ts, ...(before.link === undefined ? {} : { link: before.link }), ...(before.channel === undefined ? {} : { channel: before.channel }) };
|
|
105
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(savedChatPart(content), part);
|
|
105
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE provider=:provider AND id=?").run(savedChatPart(content), part);
|
|
106
106
|
}
|
|
107
107
|
/** A tapped question button, inside the action's transaction; false when the token isn't one. */
|
|
108
108
|
export function applyChatQuestionTap(options, event, binding, token, now) {
|
|
109
109
|
const { store, state } = options;
|
|
110
|
-
const action = state.prepare("SELECT a
|
|
110
|
+
const action = state.prepare("SELECT a.token,a.part,a.question,a.choice,a.expires,a.consumed,p.message,e.binding AS owner,e.channel FROM chat_question_action a JOIN chat_part p ON p.provider=a.provider AND p.id=a.part JOIN chat_event e ON e.provider=p.provider AND e.id=p.event WHERE a.provider=:provider AND a.token=?").get(token);
|
|
111
111
|
if (action === undefined)
|
|
112
112
|
return false;
|
|
113
113
|
const say = (text) => state.plan(event.id, [{ text }], now);
|
|
@@ -126,9 +126,9 @@ export function applyChatQuestionTap(options, event, binding, token, now) {
|
|
|
126
126
|
}
|
|
127
127
|
if (action["choice"] === null) {
|
|
128
128
|
// One open prompt per person, across flow decisions and questions: the newest is the one the next message answers.
|
|
129
|
-
state.prepare("UPDATE chat_question_prompt SET consumed=? WHERE binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
|
|
130
|
-
state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
|
|
131
|
-
state.prepare("INSERT INTO chat_question_prompt(binding,question,created,expires) VALUES(
|
|
129
|
+
state.prepare("UPDATE chat_question_prompt SET consumed=? WHERE provider=:provider AND binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
|
|
130
|
+
state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE provider=:provider AND binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
|
|
131
|
+
state.prepare("INSERT INTO chat_question_prompt(provider,id,binding,question,created,expires) VALUES(:provider,(SELECT COALESCE(MAX(id),0)+1 FROM chat_question_prompt WHERE provider=:provider),?,?,?,?)").run(binding.id, questionId, now.toISOString(), new Date(now.getTime() + PROMPT_MS).toISOString());
|
|
132
132
|
say(`Your next message here is your answer to ${who(store, question)}: “${question.question}” Send “cancel” to leave it.`);
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
@@ -149,14 +149,14 @@ export function answerChatQuestionPrompt(options, event, binding, input, now) {
|
|
|
149
149
|
if (event.channel !== binding.channel)
|
|
150
150
|
return false;
|
|
151
151
|
return store.transact(() => {
|
|
152
|
-
const prompt = state.prepare("SELECT * FROM chat_question_prompt WHERE binding=? AND consumed IS NULL AND expires>? ORDER BY id DESC LIMIT 1").get(binding.id, now.toISOString());
|
|
152
|
+
const prompt = state.prepare("SELECT * FROM chat_question_prompt WHERE provider=:provider AND binding=? AND consumed IS NULL AND expires>? ORDER BY id DESC LIMIT 1").get(binding.id, now.toISOString());
|
|
153
153
|
if (prompt === undefined)
|
|
154
154
|
return false;
|
|
155
155
|
const id = Number(prompt["id"]), questionId = Number(prompt["question"]);
|
|
156
156
|
const say = (text) => state.plan(event.id, [{ text }], now);
|
|
157
157
|
const said = input.text.trim();
|
|
158
158
|
if (/^cancel\.?$/i.test(said)) {
|
|
159
|
-
state.prepare("UPDATE chat_question_prompt SET consumed=? WHERE id=?").run(now.toISOString(), id);
|
|
159
|
+
state.prepare("UPDATE chat_question_prompt SET consumed=? WHERE provider=:provider AND id=?").run(now.toISOString(), id);
|
|
160
160
|
say("Left it unanswered. The buttons on the message still work.");
|
|
161
161
|
return true;
|
|
162
162
|
}
|
package/dist/teams-chat.js
CHANGED
|
@@ -167,12 +167,12 @@ export async function deliverTeamsPart(options) {
|
|
|
167
167
|
const shared = teamsDelivery(options), state = shared.state, { store, identity } = options, now = options.clock?.() ?? new Date();
|
|
168
168
|
if (!options.current() || !state.owns(identity.installation, options.owner, now))
|
|
169
169
|
return false;
|
|
170
|
-
const row = state.
|
|
170
|
+
const row = state.nextPart(identity.installation, options.canNotify?.() !== false, now, "created");
|
|
171
171
|
if (!row)
|
|
172
172
|
return false;
|
|
173
173
|
const event = state.event(row.event), binding = event.binding === null ? null : state.bindingById(event.binding);
|
|
174
174
|
if (!binding || new Date(row.created).getTime() + 86_400_000 < now.getTime()) {
|
|
175
|
-
state.
|
|
175
|
+
state.dropPart(row.id, "Delivery expired or access changed; open the saved chat");
|
|
176
176
|
return true;
|
|
177
177
|
}
|
|
178
178
|
try {
|
|
@@ -189,7 +189,7 @@ export async function deliverTeamsPart(options) {
|
|
|
189
189
|
throw new TeamsError("Connected projects changed");
|
|
190
190
|
let text = content.text, actions = [];
|
|
191
191
|
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
192
|
-
state.
|
|
192
|
+
state.dropPart(row.id, "Removed by retention");
|
|
193
193
|
return true;
|
|
194
194
|
}
|
|
195
195
|
if (content.image) {
|
|
@@ -206,7 +206,7 @@ export async function deliverTeamsPart(options) {
|
|
|
206
206
|
const preview = proposalPreview(store, proposal, repos, "teams");
|
|
207
207
|
text = content.phase === "armed" ? armedCardText(proposal, preview.text) : preview.text.replace("\n\nConfirm or Dismiss below. Nothing changes until you confirm.", "");
|
|
208
208
|
if (preview.buttons && preview.text.length <= PLATFORM_LIMITS.teams) {
|
|
209
|
-
actions = state.
|
|
209
|
+
actions = state.partTokens(row.id, now)
|
|
210
210
|
.map(action => ({ type: "Action.Submit", title: action.phase === "yes" ? armedYesLabel(proposal) : action.phase === "cancel" ? "Cancel" : action.phase === "dismiss" ? "Dismiss" : "Confirm", data: { so: String(action.token) } }));
|
|
211
211
|
if (!actions.length)
|
|
212
212
|
text = "This confirmation expired. Ask for a fresh proposal.";
|
|
@@ -224,7 +224,11 @@ export async function deliverTeamsPart(options) {
|
|
|
224
224
|
const asked = content.question ? chatQuestionButtons(state, row.id, now).map(one => ({ type: "Action.Submit", title: one.label.slice(0, 80), data: { so: one.token } })) : [];
|
|
225
225
|
// The lead's question to its owner: its options, then "Something else".
|
|
226
226
|
const owner = content.ask ? chatAskButtons(state, row.id, now).map(one => ({ type: "Action.Submit", title: one.label.slice(0, 80), data: { so: one.token } })) : [];
|
|
227
|
-
|
|
227
|
+
// A result's, plan's, failure's or pull request's own buttons (chat-decide.ts): its acts, then its links.
|
|
228
|
+
const decided = (content.decide?.rows.flat() ?? []).flatMap((one, index) => "token" in one
|
|
229
|
+
? [{ type: "Action.Submit", title: one.label.slice(0, 80), data: { so: one.token }, ...(index === 0 ? { style: "positive" } : {}) }]
|
|
230
|
+
: openUrlAction(options.origin(), one.link));
|
|
231
|
+
actions = [...flow, ...asked, ...owner, ...decided, ...openUrlAction(options.origin(), content.link), ...(content.also ?? []).flatMap(one => openUrlAction(options.origin(), one))];
|
|
228
232
|
}
|
|
229
233
|
const target = content.edit ?? row.message;
|
|
230
234
|
// The lead's own reply goes out in Teams' Markdown (bold anchors, labelled links); everything else stays plain.
|
|
@@ -238,17 +242,16 @@ export async function deliverTeamsPart(options) {
|
|
|
238
242
|
const messageId = target ?? (typeof answer.id === "string" ? answer.id : null);
|
|
239
243
|
if (messageId === null)
|
|
240
244
|
throw new TeamsError("Teams did not confirm the message", 15_000, true);
|
|
241
|
-
state.
|
|
245
|
+
state.partSent(row.id, messageId, now);
|
|
242
246
|
return true;
|
|
243
247
|
}
|
|
244
248
|
catch (error) {
|
|
245
249
|
if (error instanceof ChatDeliveryError && error.permanent) {
|
|
246
|
-
state.
|
|
250
|
+
state.dropPart(row.id, error.message);
|
|
247
251
|
return true;
|
|
248
252
|
}
|
|
249
253
|
const problem = error instanceof TeamsError ? error : new TeamsError("Teams delivery failed", 15_000, true);
|
|
250
|
-
state.
|
|
251
|
-
.run(problem.uncertain ? 1 : 0, new Date(now.getTime() + problem.retryMs).toISOString(), problem.message, row.id);
|
|
254
|
+
state.partFailed(row.id, { problem: problem.message, until: new Date(now.getTime() + problem.retryMs).toISOString(), uncertain: problem.uncertain }, 20);
|
|
252
255
|
return true;
|
|
253
256
|
}
|
|
254
257
|
}
|
package/dist/teams-settings.js
CHANGED
|
@@ -8,9 +8,9 @@ export function teamsSettingsHtml(store, dir, csrf, options = {}) {
|
|
|
8
8
|
const bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [];
|
|
9
9
|
const binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null;
|
|
10
10
|
const others = bindings.length - (binding === null ? 0 : 1);
|
|
11
|
-
const runtime = credentials ? state.
|
|
11
|
+
const runtime = credentials ? state.runtime(credentials.installation) : null;
|
|
12
12
|
const now = options.now ?? new Date();
|
|
13
|
-
const live = runtime &&
|
|
13
|
+
const live = runtime && runtime.leaseUntil !== null && runtime.leaseUntil > now.toISOString() && runtime.connected;
|
|
14
14
|
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">`;
|
|
15
15
|
const password = '<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>';
|
|
16
16
|
const post = (action, content) => `<form method="post" action="/settings/teams/${action}" class="card">${hidden}${content}</form>`;
|
package/dist/teams.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function followTeams(options) {
|
|
|
93
93
|
const current = loadTeamsCredentials(options.dir);
|
|
94
94
|
return alive && !options.signal.aborted && current?.installation === credentials.installation && current.secret === credentials.secret && state.owns(credentials.installation, owner);
|
|
95
95
|
};
|
|
96
|
-
const problem = (message) => state.
|
|
96
|
+
const problem = (message) => state.setProblem(credentials.installation, owner, message);
|
|
97
97
|
const stop = () => { alive = false; };
|
|
98
98
|
options.signal.addEventListener("abort", stop, { once: true });
|
|
99
99
|
const heartbeat = setInterval(() => { if (!same() || !state.lease(credentials.installation, owner, new Date()))
|
|
@@ -103,7 +103,7 @@ export async function followTeams(options) {
|
|
|
103
103
|
// Connected means the app can sign in; inbound reachability is the operator's endpoint to verify.
|
|
104
104
|
try {
|
|
105
105
|
await teamsAccessToken(credentials, options.fetcher);
|
|
106
|
-
state.
|
|
106
|
+
state.setConnected(credentials.installation, owner, new Date());
|
|
107
107
|
}
|
|
108
108
|
catch (error) {
|
|
109
109
|
problem(error instanceof TeamsError ? error.message : "Microsoft sign-in failed. Check the Teams app credentials.");
|
|
@@ -111,8 +111,7 @@ export async function followTeams(options) {
|
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
113
|
while (same()) {
|
|
114
|
-
|
|
115
|
-
if (typeof retry === "string" && retry > new Date().toISOString()) {
|
|
114
|
+
if (state.retryAt(credentials.installation) > new Date().toISOString()) {
|
|
116
115
|
await pause(1000);
|
|
117
116
|
continue;
|
|
118
117
|
}
|
|
@@ -134,7 +133,7 @@ export async function followTeams(options) {
|
|
|
134
133
|
clearInterval(heartbeat);
|
|
135
134
|
alive = false;
|
|
136
135
|
options.signal.removeEventListener("abort", stop);
|
|
137
|
-
state.
|
|
136
|
+
state.stop(credentials.installation, owner);
|
|
138
137
|
}
|
|
139
138
|
await pause(5000);
|
|
140
139
|
}
|
package/dist/telegram-flow.js
CHANGED
|
@@ -180,7 +180,7 @@ export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
|
180
180
|
store.retireTelegramFlowVisit(action.card, action.entry, now);
|
|
181
181
|
const mint = (phase) => {
|
|
182
182
|
const token = randomBytes(16).toString("hex");
|
|
183
|
-
store.
|
|
183
|
+
store.telegramChat().prepare("INSERT INTO chat_flow_action (provider, token, binding, chat, message, card, entry, action, created, expires) VALUES (:provider,?,?,?,?,?,?,?,?,?)")
|
|
184
184
|
.run(token, binding.id, binding.chatId, message.messageId, action.card, action.entry, phase, now.toISOString(), new Date(now.getTime() + CONFIRM_TTL_MS).toISOString());
|
|
185
185
|
return token;
|
|
186
186
|
};
|
|
@@ -201,17 +201,17 @@ export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
|
201
201
|
export function flowConfirmOf(store, token) {
|
|
202
202
|
if (!/^[0-9a-f]{32}$/.test(token))
|
|
203
203
|
return null;
|
|
204
|
-
const row = store.
|
|
204
|
+
const row = store.telegramChat().prepare("SELECT * FROM chat_flow_action WHERE provider = :provider AND token = ? AND action IN ('yes','cancel')").get(token);
|
|
205
205
|
if (row === undefined)
|
|
206
206
|
return null;
|
|
207
|
-
return { token: String(row["token"]), binding: Number(row["binding"]), chatId: String(row["
|
|
208
|
-
phase: String(row["
|
|
207
|
+
return { token: String(row["token"]), binding: Number(row["binding"]), chatId: String(row["chat"]), messageId: String(row["message"]), card: Number(row["card"]), entry: Number(row["entry"]),
|
|
208
|
+
phase: String(row["action"]) === "cancel" ? "cancel" : "yes", expiresAt: String(row["expires"]), consumedAt: row["consumed"] == null ? null : String(row["consumed"]) };
|
|
209
209
|
}
|
|
210
210
|
/** The second tap, inside the update's transaction: Yes decides the card through the console's door, once; Cancel puts
|
|
211
211
|
* the card's own buttons back. The caller has proved the binding, chat and message are the ones it was armed on. */
|
|
212
212
|
export function applyFlowConfirm(store, binding, confirm, message, repos, now) {
|
|
213
213
|
const body = message.text.replace(FLOW_QUESTION, "");
|
|
214
|
-
const spend = () => store.
|
|
214
|
+
const spend = () => store.telegramChat().prepare("UPDATE chat_flow_action SET consumed = ? WHERE provider = :provider AND action IN ('yes','cancel') AND card = ? AND entry = ? AND message = ? AND consumed IS NULL").run(now.toISOString(), confirm.card, confirm.entry, confirm.messageId);
|
|
215
215
|
if (confirm.consumedAt !== null || confirm.expiresAt <= now.toISOString()) {
|
|
216
216
|
spend();
|
|
217
217
|
const now_ = flowWhereNow(store, confirm.card, repos);
|
package/dist/telegram.js
CHANGED
|
@@ -634,7 +634,7 @@ function decideOfferFor(store, binding, target, projects, now, root) {
|
|
|
634
634
|
if (target === null || target === undefined)
|
|
635
635
|
return null;
|
|
636
636
|
const principal = verifyApproverStanding(store, binding.approver, binding.approverGeneration, telegramConversationRepos(store, binding.approver, projects));
|
|
637
|
-
return principal.ok ? decideOffer(store, target, principal.who, now,
|
|
637
|
+
return principal.ok ? decideOffer(store, target, principal.who, now, "telegram", root) : null;
|
|
638
638
|
}
|
|
639
639
|
/** Team-conversation traffic to the chats that follow one — after the outbox, under the same delivery switch, never a problem to raise when nothing follows anything. */
|
|
640
640
|
async function deliverTeam(store, botId, transport, clock, report, readProjects, canDeliver, phoneOrigin) {
|
|
@@ -2518,9 +2518,9 @@ function noteDigestOf(note) {
|
|
|
2518
2518
|
* machine — "never silently answer without the expected note").
|
|
2519
2519
|
*/
|
|
2520
2520
|
function expiredDraftGuard(store, binding, decisionId, now) {
|
|
2521
|
-
const expired = store.
|
|
2522
|
-
.prepare(`SELECT id FROM
|
|
2523
|
-
WHERE binding = ? AND decision = ? AND state IN ('pending','armed') AND
|
|
2521
|
+
const expired = store.telegramChat()
|
|
2522
|
+
.prepare(`SELECT id FROM chat_note_draft
|
|
2523
|
+
WHERE provider = :provider AND binding = ? AND decision = ? AND state IN ('pending','armed') AND expires <= ?`)
|
|
2524
2524
|
.get(binding.id, decisionId, now.toISOString());
|
|
2525
2525
|
if (expired === undefined)
|
|
2526
2526
|
return false;
|
|
@@ -103,6 +103,12 @@ export declare function releaseNotes(changelog: string, version: string): string
|
|
|
103
103
|
* by its directory), and the directory after, so a crash never leaves a torn
|
|
104
104
|
* database, catalog or command. */
|
|
105
105
|
export declare function durableRename(temp: string, target: string): void;
|
|
106
|
+
type Moved = {
|
|
107
|
+
into: string;
|
|
108
|
+
where: string;
|
|
109
|
+
rows?: string;
|
|
110
|
+
};
|
|
111
|
+
export declare const CHAT_MOVES: Record<string, Moved>;
|
|
106
112
|
/** The tables a migration may remove whole (v115's removed features). */
|
|
107
113
|
export declare const RETIRED_TABLES: readonly string[];
|
|
108
114
|
/** A saved routine, as its move into a scheduled flow must account for it. */
|
|
@@ -127,6 +133,10 @@ export type TableDigest = {
|
|
|
127
133
|
nonNull: Record<string, number>;
|
|
128
134
|
};
|
|
129
135
|
routines?: RoutineDigest[];
|
|
136
|
+
moved?: {
|
|
137
|
+
rows: number;
|
|
138
|
+
found: number;
|
|
139
|
+
};
|
|
130
140
|
};
|
|
131
141
|
/** Each saved table's columns, row count and last rowid; the append-only ones hashed (deploy-browser's rehearsal check).
|
|
132
142
|
* Bounded: only the ledger and small tables are read row by row. */
|
package/dist/toolroll-update.js
CHANGED
|
@@ -324,9 +324,46 @@ export function durableRename(temp, target) {
|
|
|
324
324
|
* `receives`: the only rows the table may gain are those receipts. `compacted`: rows may go only into summary rows,
|
|
325
325
|
* whose `sum` grows by exactly as many. `retired`: a removed feature's table, which may go whole. `fromRoutines`: rows
|
|
326
326
|
* kept as they were, and the only rows the table may gain are one per saved routine, moved into a scheduled flow (its
|
|
327
|
-
* flow, or its schedule trigger naming that routine) — on only when the routine was approved and running.
|
|
327
|
+
* flow, or its schedule trigger naming that routine) — on only when the routine was approved and running. `moved`:
|
|
328
|
+
* the table may go only when every row it had (or the ones `rows` counts) arrived in `into`: the rows there that
|
|
329
|
+
* `where` picks grew by exactly that many. */
|
|
328
330
|
const LEGACY_DESTINATION = "legacy:single-destination";
|
|
331
|
+
/** v116: every old per-app chat table into the shared chat tables, keyed by provider (chat-migration.ts). Several old
|
|
332
|
+
* tables fan into one shared table; each picks out only its own rows there, so every count is checked on its own. */
|
|
333
|
+
const TELEGRAM_MOVES = {
|
|
334
|
+
telegram_binding: { into: "chat_binding", where: "1" },
|
|
335
|
+
telegram_pairing: { into: "chat_pair", where: "1" },
|
|
336
|
+
telegram_team_chat: { into: "chat_room", where: "1" },
|
|
337
|
+
// An applied update is a receipt; a pushed one still waiting is queued (one already applied was spent with it).
|
|
338
|
+
telegram_update: { into: "chat_event", where: "kind = 'update' AND state = 'done'" },
|
|
339
|
+
telegram_inbox: { into: "chat_event", where: "kind = 'update' AND state = 'queued'", rows: "update_id NOT IN (SELECT update_id FROM telegram_update)" },
|
|
340
|
+
telegram_conversation: { into: "chat_event", where: "kind = 'message'" },
|
|
341
|
+
telegram_conversation_part: { into: "chat_part", where: "1" },
|
|
342
|
+
telegram_proposal_action: { into: "chat_action", where: "proposal IS NOT NULL" },
|
|
343
|
+
telegram_action: { into: "chat_action", where: "decision IS NOT NULL" },
|
|
344
|
+
telegram_flow_action: { into: "chat_flow_action", where: "action IN ('approve','edit','send-back')" },
|
|
345
|
+
telegram_flow_confirm: { into: "chat_flow_action", where: "action IN ('yes','cancel')" },
|
|
346
|
+
telegram_flow_choice: { into: "chat_flow_choice", where: "1" },
|
|
347
|
+
telegram_flow_prompt: { into: "chat_flow_prompt", where: "1" },
|
|
348
|
+
telegram_question_action: { into: "chat_question_action", where: "1" },
|
|
349
|
+
telegram_question_prompt: { into: "chat_question_prompt", where: "1" },
|
|
350
|
+
telegram_decision_message: { into: "chat_message_ref", where: "kind = 'decision'" },
|
|
351
|
+
telegram_task_message: { into: "chat_message_ref", where: "kind = 'task'" },
|
|
352
|
+
telegram_outbound_message: { into: "chat_message_ref", where: "kind = 'notification'" },
|
|
353
|
+
telegram_note_draft: { into: "chat_note_draft", where: "1" },
|
|
354
|
+
// A bot's lease and its rate-limit wait share its one runtime row.
|
|
355
|
+
bridge_lease: { into: "chat_runtime", where: "lease_until IS NOT NULL" },
|
|
356
|
+
telegram_retry: { into: "chat_runtime", where: "retry_at IS NOT NULL" },
|
|
357
|
+
telegram_digest: { into: "chat_digest", where: "1", rows: "id = 1" },
|
|
358
|
+
};
|
|
359
|
+
const APP_TABLES = ["binding", "pair", "event", "part", "action", "progress", "runtime", "room", "meta",
|
|
360
|
+
"flow_action", "flow_prompt", "flow_choice", "flow_note", "question_action", "question_prompt", "ask_action"];
|
|
361
|
+
export const CHAT_MOVES = {
|
|
362
|
+
...Object.fromEntries(Object.entries(TELEGRAM_MOVES).map(([table, move]) => [table, { ...move, where: `provider = 'telegram' AND (${move.where})` }])),
|
|
363
|
+
...Object.fromEntries(["slack", "discord", "teams"].flatMap(app => APP_TABLES.map(suffix => [`${app}_${suffix}`, { into: `chat_${suffix}`, where: `provider = '${app}'` }]))),
|
|
364
|
+
};
|
|
329
365
|
const HISTORY_RULES = {
|
|
366
|
+
...Object.fromEntries(Object.entries(CHAT_MOVES).map(([table, moved]) => [table, { moved }])),
|
|
330
367
|
action_ledger: { appendOnly: true }, ledger_seal: { appendOnly: true }, ledger_checkpoint: { appendOnly: true },
|
|
331
368
|
// v114: a finished run whose process witnesses have all exited keeps one summary row in place of them.
|
|
332
369
|
run_process: { compacted: { into: "run_process_summary", sum: "witnesses" } }, run_process_summary: { appendOnly: true },
|
|
@@ -363,6 +400,8 @@ function receipts(db, name, destination, columns) {
|
|
|
363
400
|
const r = db.prepare("SELECT count(*) AS n" + columns.map(column => ",count(" + quote(column) + ") AS " + quote(column)).join("") + " FROM " + quote(name) + " WHERE destination = ?").get(destination);
|
|
364
401
|
return { count: Number(r["n"]), nonNull: Object.fromEntries(columns.map(column => [column, Number(r[column])])) };
|
|
365
402
|
}
|
|
403
|
+
/** The rows a moved table's rule picks out in its destination (none while there is no such table). */
|
|
404
|
+
const arrived = (db, move) => db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(move.into) ? Number(db.prepare("SELECT count(*) AS n FROM " + quote(move.into) + " WHERE " + move.where).get()["n"]) : 0;
|
|
366
405
|
const summed = (db, into) => db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(into.into) ? Number(db.prepare("SELECT coalesce(sum(" + quote(into.sum) + "),0) AS n FROM " + quote(into.into)).get()["n"]) : 0;
|
|
367
406
|
/** Each saved table's columns, row count and last rowid; the append-only ones hashed (deploy-browser's rehearsal check).
|
|
368
407
|
* Bounded: only the ledger and small tables are read row by row. */
|
|
@@ -388,6 +427,9 @@ export function historySnapshot(db) {
|
|
|
388
427
|
t.summed = summed(db, rule.compacted);
|
|
389
428
|
if (rule?.fromRoutines)
|
|
390
429
|
t.routines = savedRoutines(db);
|
|
430
|
+
if (rule?.moved)
|
|
431
|
+
t.moved = { found: arrived(db, rule.moved),
|
|
432
|
+
rows: rule.moved.rows === undefined ? t.count : Number(db.prepare("SELECT count(*) AS n FROM " + quote(name) + " WHERE " + rule.moved.rows).get()["n"]) };
|
|
391
433
|
return t;
|
|
392
434
|
});
|
|
393
435
|
}
|
|
@@ -450,8 +492,13 @@ export function changedHistory(db, before) {
|
|
|
450
492
|
return got !== undefined && want !== undefined && got.count === want.count && Object.entries(want.nonNull).every(([c, n]) => got.nonNull[c] === n) && t.carry === want.count;
|
|
451
493
|
};
|
|
452
494
|
return before.filter(t => {
|
|
453
|
-
if (!tables.has(t.name))
|
|
454
|
-
|
|
495
|
+
if (!tables.has(t.name)) {
|
|
496
|
+
const rule = HISTORY_RULES[t.name];
|
|
497
|
+
if (rule?.retired === true)
|
|
498
|
+
return false;
|
|
499
|
+
const move = rule?.moved;
|
|
500
|
+
return move === undefined || t.moved === undefined || arrived(db, move) - t.moved.found !== t.moved.rows;
|
|
501
|
+
}
|
|
455
502
|
const rule = HISTORY_RULES[t.name], present = new Set(tableColumns(db, t.name));
|
|
456
503
|
if (t.columns.some(c => !present.has(c) && (!rule?.dropped?.includes(c) || (t.nonNull?.[c] !== 0 && !accounted(rule, t)))))
|
|
457
504
|
return true;
|
|
@@ -4,10 +4,12 @@ export const WORKSPACE_REVISION_KEY = 'workspace-content:v1';
|
|
|
4
4
|
const KEY = WORKSPACE_REVISION_KEY;
|
|
5
5
|
const MAX_AGE_MS = 60_000;
|
|
6
6
|
const OMIT = new Set(['schema_version', 'service_cursor', 'wake', 'notification_delivery', 'sqlite_sequence',
|
|
7
|
-
'team_read', 'team_request', 'push_delivery', '
|
|
7
|
+
'team_read', 'team_request', 'push_delivery', 'chat_message_ref']);
|
|
8
8
|
const QUIET = {
|
|
9
9
|
runner: ['heartbeat_at'], claim: ['heartbeat_at', 'expires_at'], watch_lease: ['heartbeat_at', 'expires_at'],
|
|
10
10
|
watch_episode: ['ticks'], provider_readiness: ['observed_at'],
|
|
11
|
+
// A chat worker's lease, poll cursor and rate-limit wait are its own bookkeeping; its connection and problem are news.
|
|
12
|
+
chat_runtime: ['owner', 'lease_until', 'generation', 'cursor', 'heartbeat', 'retry_at'],
|
|
11
13
|
};
|
|
12
14
|
/** A quiet beat that revives what a view already rendered as gone: the rows a beat may move, read before and after.
|
|
13
15
|
* A runner beat after its liveness window (or backwards) is a revival; a lease renewed at or past its expiry is one. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.54",
|
|
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"
|
package/dist/slack-state.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** Slack compatibility names for the shared durable transport state. */
|
|
2
|
-
import { ChatState } from "./chat-delivery-state.js";
|
|
3
|
-
import type { Store } from "./store.js";
|
|
4
|
-
export { chatHash as slackHash } from "./chat-delivery-state.js";
|
|
5
|
-
export type { ChatBinding as SlackBinding, ChatEvent as SlackEvent, ChatPart as SlackPart, ChatContent as SlackContent, } from "./chat-delivery-state.js";
|
|
6
|
-
import type { ChatIdentity } from "./chat-delivery-state.js";
|
|
7
|
-
export type SlackIdentity = ChatIdentity & {
|
|
8
|
-
team: string;
|
|
9
|
-
};
|
|
10
|
-
export declare const SLACK_SCHEMA: string, SLACK_TABLES: string[];
|
|
11
|
-
export declare class SlackState extends ChatState {
|
|
12
|
-
constructor(store: Store);
|
|
13
|
-
}
|
package/dist/slack-state.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** Slack compatibility names for the shared durable transport state. */
|
|
2
|
-
import { ChatState } from "./chat-delivery-state.js";
|
|
3
|
-
import { chatSchema, chatTables } from "./contracts/chat-tables.js";
|
|
4
|
-
export { chatHash as slackHash } from "./chat-delivery-state.js";
|
|
5
|
-
export const SLACK_SCHEMA = chatSchema("slack"), SLACK_TABLES = chatTables("slack");
|
|
6
|
-
export class SlackState extends ChatState {
|
|
7
|
-
constructor(store) {
|
|
8
|
-
super(store, "slack");
|
|
9
|
-
}
|
|
10
|
-
}
|