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
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* is answered with why (`value.so: must be a Toolroll button token`), and nothing is done.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { CHAT_BUTTON_TOKEN } from "./chat-content.js";
|
|
8
9
|
import { parseContract } from "./contract.js";
|
|
9
|
-
export const teamsSubmitSchema = z.object({ so: z.string().regex(
|
|
10
|
+
export const teamsSubmitSchema = z.object({ so: z.string().regex(CHAT_BUTTON_TOKEN, { error: "must be a Toolroll button token" }) });
|
|
10
11
|
/** A submit's one-time token. */
|
|
11
12
|
export function readTeamsSubmit(value) {
|
|
12
13
|
const read = parseContract(z.object({ value: teamsSubmitSchema }), { value });
|
package/dist/discord-chat.js
CHANGED
|
@@ -190,17 +190,13 @@ export async function deliverDiscordPart(options) {
|
|
|
190
190
|
if (!options.current() ||
|
|
191
191
|
!state.owns(identity.installation, options.owner, now))
|
|
192
192
|
return false;
|
|
193
|
-
const row = state
|
|
194
|
-
.prepare("SELECT p.* FROM chat_part p JOIN chat_event e ON e.id=p.event WHERE e.installation=? AND p.state='pending' AND (e.kind!='notice' OR ?=1) AND (p.next_at IS NULL OR p.next_at<=?) ORDER BY p.id LIMIT 1")
|
|
195
|
-
.get(identity.installation, options.canNotify?.() === false ? 0 : 1, now.toISOString());
|
|
193
|
+
const row = state.nextPart(identity.installation, options.canNotify?.() !== false, now);
|
|
196
194
|
if (!row)
|
|
197
195
|
return false;
|
|
198
196
|
const event = state.event(row.event), binding = event.binding === null ? null : state.bindingById(event.binding);
|
|
199
197
|
if (!binding ||
|
|
200
198
|
new Date(row.created).getTime() + 86_400_000 < now.getTime()) {
|
|
201
|
-
state
|
|
202
|
-
.prepare("UPDATE chat_part SET state='dropped',problem='Delivery expired or access changed; open the saved chat' WHERE id=?")
|
|
203
|
-
.run(row.id);
|
|
199
|
+
state.dropPart(row.id, "Delivery expired or access changed; open the saved chat");
|
|
204
200
|
return true;
|
|
205
201
|
}
|
|
206
202
|
try {
|
|
@@ -214,16 +210,16 @@ export async function deliverDiscordPart(options) {
|
|
|
214
210
|
throw new DiscordError("Connected projects changed");
|
|
215
211
|
let text = content.text, buttons = [], file;
|
|
216
212
|
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
217
|
-
state.
|
|
213
|
+
state.dropPart(row.id, "Removed by retention");
|
|
218
214
|
return true;
|
|
219
215
|
}
|
|
220
216
|
// A screenshot sent with a result replies to that result's message once it is placed, and waits until then.
|
|
221
|
-
const result = content.shot?.follows == null ?
|
|
222
|
-
if (result !==
|
|
223
|
-
state.
|
|
217
|
+
const result = content.shot?.follows == null ? null : state.part(content.shot.follows);
|
|
218
|
+
if (result !== null && !discordId(result.message) && result.state === "pending") {
|
|
219
|
+
state.holdPart(row.id, shotWaitsUntil(result.next_at, now));
|
|
224
220
|
return true;
|
|
225
221
|
}
|
|
226
|
-
const follows = result?.
|
|
222
|
+
const follows = result?.message;
|
|
227
223
|
const thread = discordId(follows) ? String(follows) : event.thread;
|
|
228
224
|
if (content.image) {
|
|
229
225
|
const image = verifyResultImage(store, options.evidenceRoot, repos, content.image);
|
|
@@ -260,8 +256,7 @@ export async function deliverDiscordPart(options) {
|
|
|
260
256
|
preview.text.length <= 3400 &&
|
|
261
257
|
discordPlain(text).length <= 3900) {
|
|
262
258
|
buttons = state
|
|
263
|
-
.
|
|
264
|
-
.all(row.id, now.toISOString())
|
|
259
|
+
.partTokens(row.id, now)
|
|
265
260
|
.map((action) => ({
|
|
266
261
|
type: 2,
|
|
267
262
|
style: action.phase === "yes" ? 4 : action.phase === "confirm" ? 1 : 2,
|
|
@@ -317,6 +312,10 @@ export async function deliverDiscordPart(options) {
|
|
|
317
312
|
custom_id: `so_${one.token}`,
|
|
318
313
|
}))
|
|
319
314
|
: []),
|
|
315
|
+
// A result's, plan's, failure's or pull request's own buttons (chat-decide.ts): its acts, then its links.
|
|
316
|
+
...(content.decide?.rows.flat() ?? []).flatMap((one, index) => "token" in one
|
|
317
|
+
? [{ type: 2, style: index === 0 ? 1 : 2, label: one.label.slice(0, 80), custom_id: `so_${one.token}` }]
|
|
318
|
+
: link(options.origin(), one.link)),
|
|
320
319
|
...link(options.origin(), content.link),
|
|
321
320
|
...(content.also ?? []).flatMap(one => link(options.origin(), one)),
|
|
322
321
|
];
|
|
@@ -355,13 +354,7 @@ export async function deliverDiscordPart(options) {
|
|
|
355
354
|
// Preserve the original remote attachment; never upload it again on an edit.
|
|
356
355
|
file = undefined;
|
|
357
356
|
}
|
|
358
|
-
state
|
|
359
|
-
.prepare("UPDATE chat_part SET message=?,uploaded=? WHERE id=?")
|
|
360
|
-
.run(target, content.image &&
|
|
361
|
-
Array.isArray(found.attachments) &&
|
|
362
|
-
found.attachments.length
|
|
363
|
-
? 1
|
|
364
|
-
: 0, row.id);
|
|
357
|
+
state.setPartFile(row.id, null, Boolean(content.image && Array.isArray(found.attachments) && found.attachments.length), target);
|
|
365
358
|
}
|
|
366
359
|
}
|
|
367
360
|
await channelAccess(shared, binding, session?.ceilingDigest);
|
|
@@ -396,17 +389,13 @@ export async function deliverDiscordPart(options) {
|
|
|
396
389
|
!answer.attachments.some((v) => object(v).filename === file.name &&
|
|
397
390
|
Number(object(v).size) === file.bytes.length))))
|
|
398
391
|
throw new DiscordError("Discord did not confirm the message and file identity", 15_000, true);
|
|
399
|
-
state
|
|
400
|
-
|
|
401
|
-
.run(answer.id, row.id);
|
|
402
|
-
state
|
|
403
|
-
.prepare("UPDATE chat_runtime SET problem=NULL WHERE installation=? AND owner=?")
|
|
404
|
-
.run(identity.installation, options.owner);
|
|
392
|
+
state.partSent(row.id, String(answer.id), options.clock?.() ?? new Date());
|
|
393
|
+
state.setProblem(identity.installation, options.owner, null);
|
|
405
394
|
return true;
|
|
406
395
|
}
|
|
407
396
|
catch (error) {
|
|
408
397
|
if (error instanceof ChatDeliveryError && error.permanent) {
|
|
409
|
-
state.
|
|
398
|
+
state.dropPart(row.id, error.message);
|
|
410
399
|
return true;
|
|
411
400
|
}
|
|
412
401
|
// No permission to attach files here: one plain line instead of the result's screenshots.
|
|
@@ -420,18 +409,10 @@ export async function deliverDiscordPart(options) {
|
|
|
420
409
|
: new DiscordError("Discord delivery is waiting to retry", 15_000, true);
|
|
421
410
|
const until = new Date((options.clock?.() ?? new Date()).getTime() +
|
|
422
411
|
Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
|
|
423
|
-
state
|
|
424
|
-
|
|
425
|
-
.run(problem.uncertain ? 1 : 0, until, problem.message, row.id);
|
|
426
|
-
state
|
|
427
|
-
.prepare("UPDATE chat_runtime SET problem=? WHERE installation=? AND owner=?")
|
|
428
|
-
.run(problem.code === "ratelimited"
|
|
429
|
-
? "Discord asked us to wait. Saved replies will retry."
|
|
430
|
-
: problem.message, identity.installation, options.owner);
|
|
412
|
+
state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
|
|
413
|
+
state.setProblem(identity.installation, options.owner, problem.code === "ratelimited" ? "Discord asked us to wait. Saved replies will retry." : problem.message);
|
|
431
414
|
if (problem.code === "ratelimited")
|
|
432
|
-
state
|
|
433
|
-
.prepare("UPDATE chat_runtime SET retry_at=? WHERE installation=?")
|
|
434
|
-
.run(until, identity.installation);
|
|
415
|
+
state.deferUntil(identity.installation, until);
|
|
435
416
|
return true;
|
|
436
417
|
}
|
|
437
418
|
}
|
package/dist/discord-settings.js
CHANGED
|
@@ -9,14 +9,10 @@ const escape = (text) => text
|
|
|
9
9
|
.replaceAll("'", "'");
|
|
10
10
|
export function discordSettingsHtml(store, dir, csrf, options = {}) {
|
|
11
11
|
const credentials = loadDiscordCredentials(dir), state = new ChatState(store, "discord"), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
|
|
12
|
-
const runtime = credentials
|
|
13
|
-
? state.db
|
|
14
|
-
.prepare("SELECT connected,problem,lease_until FROM discord_runtime WHERE installation=?")
|
|
15
|
-
.get(credentials.installation)
|
|
16
|
-
: null;
|
|
12
|
+
const runtime = credentials ? state.runtime(credentials.installation) : null;
|
|
17
13
|
const now = options.now ?? new Date(), live = runtime &&
|
|
18
|
-
|
|
19
|
-
runtime.
|
|
14
|
+
runtime.leaseUntil !== null &&
|
|
15
|
+
runtime.leaseUntil > now.toISOString() &&
|
|
20
16
|
runtime.connected;
|
|
21
17
|
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">`;
|
|
22
18
|
const password = '<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>';
|
|
@@ -44,12 +40,7 @@ export function discordSettingsHtml(store, dir, csrf, options = {}) {
|
|
|
44
40
|
'<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/discord">Check connection</a>';
|
|
45
41
|
}
|
|
46
42
|
else if (binding && state.live(binding)) {
|
|
47
|
-
const pending =
|
|
48
|
-
.prepare("SELECT count(*) n FROM discord_part p JOIN discord_event e ON e.id=p.event WHERE e.binding=? AND p.state='pending'")
|
|
49
|
-
.get(binding.id)?.n ?? 0);
|
|
50
|
-
const failed = Number(state.db
|
|
51
|
-
.prepare("SELECT count(*) n FROM discord_part p JOIN discord_event e ON e.id=p.event WHERE e.binding=? AND p.state='dropped'")
|
|
52
|
-
.get(binding.id)?.n ?? 0);
|
|
43
|
+
const { pending, dropped: failed } = state.partCounts(binding.id);
|
|
53
44
|
content +=
|
|
54
45
|
`<p>Your Discord account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>` +
|
|
55
46
|
"<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>" +
|
package/dist/discord.js
CHANGED
|
@@ -37,9 +37,7 @@ export async function followDiscord(options) {
|
|
|
37
37
|
current.botToken === credentials.botToken &&
|
|
38
38
|
state.owns(credentials.installation, owner));
|
|
39
39
|
};
|
|
40
|
-
const problem = (message) => state
|
|
41
|
-
.prepare("UPDATE chat_runtime SET problem=? WHERE installation=? AND owner=?")
|
|
42
|
-
.run(message, credentials.installation, owner);
|
|
40
|
+
const problem = (message) => state.setProblem(credentials.installation, owner, message);
|
|
43
41
|
const api = discordApi(credentials.botToken);
|
|
44
42
|
const client = new Client({
|
|
45
43
|
// Guild messages and their content are needed for rooms; the Message
|
|
@@ -63,9 +61,7 @@ export async function followDiscord(options) {
|
|
|
63
61
|
if (!same() || !validated)
|
|
64
62
|
return;
|
|
65
63
|
connected = true;
|
|
66
|
-
state
|
|
67
|
-
.prepare("UPDATE chat_runtime SET connected=?,problem=NULL WHERE installation=? AND owner=?")
|
|
68
|
-
.run(new Date().toISOString(), credentials.installation, owner);
|
|
64
|
+
state.setConnected(credentials.installation, owner, new Date());
|
|
69
65
|
};
|
|
70
66
|
client.on("shardReady", online);
|
|
71
67
|
client.on("shardResume", online);
|
|
@@ -133,11 +129,7 @@ export async function followDiscord(options) {
|
|
|
133
129
|
try {
|
|
134
130
|
await client.login(credentials.botToken);
|
|
135
131
|
while (same()) {
|
|
136
|
-
|
|
137
|
-
.prepare("SELECT retry_at FROM chat_runtime WHERE installation=?")
|
|
138
|
-
.get(credentials.installation)?.retry_at;
|
|
139
|
-
if (!connected ||
|
|
140
|
-
(typeof retry === "string" && retry > new Date().toISOString())) {
|
|
132
|
+
if (!connected || state.retryAt(credentials.installation) > new Date().toISOString()) {
|
|
141
133
|
await pause(1000);
|
|
142
134
|
continue;
|
|
143
135
|
}
|
|
@@ -160,9 +152,7 @@ export async function followDiscord(options) {
|
|
|
160
152
|
alive = false;
|
|
161
153
|
options.signal.removeEventListener("abort", stop);
|
|
162
154
|
await client.destroy().catch(() => { });
|
|
163
|
-
state
|
|
164
|
-
.prepare("UPDATE chat_runtime SET owner=NULL,lease_until=NULL,connected=NULL WHERE installation=? AND owner=?")
|
|
165
|
-
.run(credentials.installation, owner);
|
|
155
|
+
state.stop(credentials.installation, owner);
|
|
166
156
|
}
|
|
167
157
|
await pause(5000);
|
|
168
158
|
}
|
package/dist/export.js
CHANGED
|
@@ -29,20 +29,20 @@ import { readEmailSettings } from "./email-settings.js";
|
|
|
29
29
|
import { SUBSCRIPTION_CAPABLE, readAuthMode } from "./keys.js";
|
|
30
30
|
export const EXPORT_FORMAT = "standing-orders/export/v1";
|
|
31
31
|
/** Tables that hold nothing but secrets or short-lived sign-in material, and derived search indexes. */
|
|
32
|
-
const SECRET_TABLES = new Set(["web_session", "ceremony_nonce", "
|
|
32
|
+
const SECRET_TABLES = new Set(["web_session", "ceremony_nonce", "chat_pair", "oauth_code", "oauth_refresh"]);
|
|
33
33
|
const DERIVED_TABLE = /^(sqlite_|memory_search)/;
|
|
34
34
|
/** Columns that hold a secret (or what stands in for one): never exported. */
|
|
35
35
|
const SECRET_COLUMNS = new Set([
|
|
36
36
|
"api_token.secret_hash", "lead_credential.secret_hash", "approver.credential_hash", "coordinator_credential.credential_hash", "runner.credential_hash", "invite.token_hash",
|
|
37
37
|
"flow_trigger.hook_hash", "push_subscription.endpoint", "push_subscription.p256dh", "push_subscription.auth",
|
|
38
|
-
"chat_turn.credential_key", "mate_session.credential_key", "mate_turn.credential_key", "
|
|
38
|
+
"chat_turn.credential_key", "mate_session.credential_key", "mate_turn.credential_key", "chat_runtime.push_url", "workflow_preview.token",
|
|
39
39
|
"quota.credential_fp", "oauth_grant.renew_hash", "oauth_client.source_hash",
|
|
40
40
|
]);
|
|
41
41
|
/** A column named like this holds a secret unless it is reviewed below; an unreviewed one is dropped. */
|
|
42
42
|
const SECRET_NAME = /hash|secret|token|passw|cookie|csrf|credential|p256dh|^auth$|key|nonce|endpoint|url/i;
|
|
43
43
|
/** Reviewed: named like a secret, but it isn't one (a seal, a digest, a lookup name, an address with no key). */
|
|
44
44
|
const REVIEWED_COLUMNS = new Set([
|
|
45
|
-
"artifact.key", "budget.scope_key", "mate_thread.scope_key", "diff_comment.source_key", "
|
|
45
|
+
"artifact.key", "budget.scope_key", "mate_thread.scope_key", "diff_comment.source_key", "chat_meta.key",
|
|
46
46
|
"flow_trigger_event.key", "installation_fact.key", "mcp_idempotency.key", "memory_gap.key", "mutation.idempotency_key", "notification.dedupe_key",
|
|
47
47
|
"service_cursor.key", "ledger_checkpoint.hash", "ledger_seal.hash", "merge_intent.grant_terms_hash", "plan_revision.parent_hash",
|
|
48
48
|
"publication.body_hash", "publication.pr_url", "team_message.payload_hash", "team_request.payload_hash", "backend_grant.credential_scope",
|
|
@@ -55,7 +55,7 @@ export function categoryOf(table) {
|
|
|
55
55
|
[/^(action_ledger|ledger_seal|ledger_checkpoint|sync_ledger)$/, "ledger"],
|
|
56
56
|
[/^teammate/, "teammates"],
|
|
57
57
|
[/^(flow|workflow_)/, "flows"],
|
|
58
|
-
[/^(chat_|mate_|team_|
|
|
58
|
+
[/^(chat_|mate_|team_|notification|push_)/, "chats"],
|
|
59
59
|
[/^(approver|api_token|lead_credential|invite|sso_identity|coordinator_|oauth_)/, "people"],
|
|
60
60
|
[/^(run|artifact|claim|worktree$|execution_slot|criterion_review|diff_comment|proof_|review_request|repair_chain|incident|publication$|merge_|side_spend|knowledge_snapshot|learning_snapshot|skill_snapshot)/, "runs"],
|
|
61
61
|
[/^(task|hold$|plan_|scope_|tool_seal|decision|external_|skill_test)/, "tasks"],
|
package/dist/mate-tools.js
CHANGED
|
@@ -494,7 +494,7 @@ const MATE_TOOL_HANDLERS = {
|
|
|
494
494
|
description: "When the owner asks to approve one thing (a plan waiting on them, or a flow card waiting at a Person decides step), send that item's own Approve and Send back buttons to their paired chat right away. Their tap there is the approval (two taps, as on any decision card); you never approve. Give task for a plan, or card for a flow card (its number from get_flows). When it can't be approved in chat, the answer says why: say that in one line and open show_control approval (a plan) or the flow, never that you can't approve.",
|
|
495
495
|
handle: (ctx, args) => {
|
|
496
496
|
const pairedHere = ctx.channel === "telegram";
|
|
497
|
-
const paired = ctx.store.handle.prepare("SELECT 1 FROM
|
|
497
|
+
const paired = ctx.store.handle.prepare("SELECT 1 FROM chat_binding WHERE provider = 'telegram' AND approver = ? AND generation = ? AND revoked IS NULL LIMIT 1")
|
|
498
498
|
.get(ctx.who.name, ctx.store.accountOf(ctx.who.name)?.generation ?? -1) !== undefined;
|
|
499
499
|
const stamp = ctx.now.getTime();
|
|
500
500
|
if (args["card"] !== undefined) {
|
|
@@ -520,7 +520,8 @@ const MATE_TOOL_HANDLERS = {
|
|
|
520
520
|
if (task === null || admittedRef(ctx, task) === null)
|
|
521
521
|
return notFound();
|
|
522
522
|
const current = ctx.store.taskFamilyOf(task, ctx.who.repos, false)?.current.id ?? task;
|
|
523
|
-
|
|
523
|
+
// Its buttons go to the paired Telegram chat (pairedHere), so it is that chat's authority that counts.
|
|
524
|
+
const plan = planInChat(ctx.store, current, ctx.who.name, ctx.now, "telegram", ctx.evidenceRoot);
|
|
524
525
|
if (!plan.ok)
|
|
525
526
|
return { ok: true, body: { offered: false, task: current, why: plan.why, control: "approval" } };
|
|
526
527
|
if (!pairedHere || !paired)
|
package/dist/modes.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* owns the transactions; the tick owns the rails; serve owns the
|
|
14
14
|
* ceremony screens.
|
|
15
15
|
*/
|
|
16
|
+
import { type ChatProvider } from "./contracts/chat-tables.js";
|
|
16
17
|
export type ModeName = "standard" | "hands-off";
|
|
17
18
|
export type ModeTerms = {
|
|
18
19
|
name: ModeName;
|
|
@@ -64,12 +65,16 @@ export type ModeTerms = {
|
|
|
64
65
|
* `repairAuto` is also true — a mode may sign a cap without signing the
|
|
65
66
|
* authority, but never the reverse. */
|
|
66
67
|
repairMaxAttempts: number;
|
|
67
|
-
/** Explicit opt-in: the signer's paired
|
|
68
|
+
/** Explicit opt-in: the signer's paired chat may approve this
|
|
68
69
|
* repository's plans and merge its ready pull requests with two taps,
|
|
69
70
|
* for the mode's lifetime. A plan that widens permissions, exceeds the
|
|
70
71
|
* per-attempt budget or touches protected paths still opens Toolroll.
|
|
71
72
|
* Legacy modes read FALSE — never inherited, only freshly signed. */
|
|
72
73
|
chatApprove: boolean;
|
|
74
|
+
/** The chat apps `chatApprove` names, signed with it. Absent on a mode signed before chat apps other than Telegram
|
|
75
|
+
* could approve: its words said "your paired Telegram chat", so it stays Telegram only (chatApproveChatsOf). A wider
|
|
76
|
+
* grant is only ever a fresh signature naming the apps. */
|
|
77
|
+
chatApproveChats?: readonly ChatProvider[];
|
|
73
78
|
absoluteExpiry: string;
|
|
74
79
|
};
|
|
75
80
|
export declare const MODE_MAX_DAYS = 7;
|
|
@@ -82,6 +87,13 @@ export declare function modeTermsJson(terms: ModeTerms): string;
|
|
|
82
87
|
export declare function modeDigestOf(terms: ModeTerms): string;
|
|
83
88
|
/** Strict rehydration — anything unexpected is null, never a guess. */
|
|
84
89
|
export declare function modeTermsFromJson(json: string | null): ModeTerms | null;
|
|
90
|
+
/** The chat apps a mode lets its signer approve from: none without chatApprove; Telegram alone for a grant signed
|
|
91
|
+
* before it named its apps (that is what its words said); otherwise exactly the apps it names. */
|
|
92
|
+
export declare function chatApproveChatsOf(terms: Pick<ModeTerms, "chatApprove" | "chatApproveChats">): readonly ChatProvider[];
|
|
93
|
+
/** What a new chatApprove signature names: every chat app, said in its words. */
|
|
94
|
+
export declare const CHAT_APPROVE_ALL: readonly ChatProvider[];
|
|
95
|
+
/** "Telegram", "Telegram or Slack", "Telegram, Slack, Discord or Teams". */
|
|
96
|
+
export declare const chatNames: (chats: readonly ChatProvider[]) => string;
|
|
85
97
|
/** Every term in words — what the ceremony renders and the password
|
|
86
98
|
* signs. The reversal sentence is verbatim from the chain (C1). */
|
|
87
99
|
export declare function modeWords(terms: ModeTerms): string[];
|
package/dist/modes.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* ceremony screens.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
+
import { CHAT_PROVIDERS, isChatProvider } from "./contracts/chat-tables.js";
|
|
17
18
|
export const MODE_MAX_DAYS = 7;
|
|
18
19
|
/** The presets are STARTING POINTS the ceremony renders in full — the
|
|
19
20
|
* signature always covers the resolved terms, never a label. */
|
|
@@ -120,6 +121,10 @@ export function modeTermsFromJson(json) {
|
|
|
120
121
|
(t["repairMaxAttempts"] === undefined ||
|
|
121
122
|
(typeof t["repairMaxAttempts"] === "number" && Number.isInteger(t["repairMaxAttempts"]) && t["repairMaxAttempts"] >= 0 && t["repairMaxAttempts"] <= 3)) &&
|
|
122
123
|
(t["chatApprove"] === undefined || typeof t["chatApprove"] === "boolean") &&
|
|
124
|
+
// chatApproveChats: absent (a Telegram-only grant, or none), or the apps a chatApprove term names, each once.
|
|
125
|
+
(t["chatApproveChats"] === undefined ||
|
|
126
|
+
(t["chatApprove"] === true && Array.isArray(t["chatApproveChats"]) && t["chatApproveChats"].length > 0 &&
|
|
127
|
+
t["chatApproveChats"].every(isChatProvider) && new Set(t["chatApproveChats"]).size === t["chatApproveChats"].length)) &&
|
|
123
128
|
typeof t["absoluteExpiry"] === "string" &&
|
|
124
129
|
!Number.isNaN(Date.parse(t["absoluteExpiry"]))) {
|
|
125
130
|
return {
|
|
@@ -138,11 +143,22 @@ export function modeTermsFromJson(json) {
|
|
|
138
143
|
repairAuto: t["repairAuto"] === true,
|
|
139
144
|
repairMaxAttempts: typeof t["repairMaxAttempts"] === "number" ? t["repairMaxAttempts"] : 0,
|
|
140
145
|
chatApprove: t["chatApprove"] === true,
|
|
146
|
+
...(Array.isArray(t["chatApproveChats"]) ? { chatApproveChats: [...t["chatApproveChats"]] } : {}),
|
|
141
147
|
absoluteExpiry: t["absoluteExpiry"],
|
|
142
148
|
};
|
|
143
149
|
}
|
|
144
150
|
return null;
|
|
145
151
|
}
|
|
152
|
+
/** The chat apps a mode lets its signer approve from: none without chatApprove; Telegram alone for a grant signed
|
|
153
|
+
* before it named its apps (that is what its words said); otherwise exactly the apps it names. */
|
|
154
|
+
export function chatApproveChatsOf(terms) {
|
|
155
|
+
return !terms.chatApprove ? [] : terms.chatApproveChats ?? ["telegram"];
|
|
156
|
+
}
|
|
157
|
+
/** What a new chatApprove signature names: every chat app, said in its words. */
|
|
158
|
+
export const CHAT_APPROVE_ALL = CHAT_PROVIDERS;
|
|
159
|
+
const CHAT_NAMES = { telegram: "Telegram", slack: "Slack", discord: "Discord", teams: "Teams" };
|
|
160
|
+
/** "Telegram", "Telegram or Slack", "Telegram, Slack, Discord or Teams". */
|
|
161
|
+
export const chatNames = (chats) => chats.length <= 1 ? chats.map(one => CHAT_NAMES[one]).join("") : `${chats.slice(0, -1).map(one => CHAT_NAMES[one]).join(", ")} or ${CHAT_NAMES[chats[chats.length - 1]]}`;
|
|
146
162
|
/** Every term in words — what the ceremony renders and the password
|
|
147
163
|
* signs. The reversal sentence is verbatim from the chain (C1). */
|
|
148
164
|
export function modeWords(terms) {
|
|
@@ -178,7 +194,7 @@ export function modeWords(terms) {
|
|
|
178
194
|
...(terms.repairAuto
|
|
179
195
|
? ["historical automatic repair grants are retained on record but no longer schedule work"] : []),
|
|
180
196
|
...(terms.chatApprove
|
|
181
|
-
? [
|
|
197
|
+
? [`your paired ${chatNames(chatApproveChatsOf(terms))} chat may approve this repository's plans and merge its ready pull requests, two taps each, without your password; a plan that widens permissions, exceeds the per-attempt cap above or touches protected paths still opens Toolroll`] : []),
|
|
182
198
|
`everything above ends at ${terms.absoluteExpiry.slice(0, 16).replace("T", " ")} — revoking it earlier is one click, and every act it covered falls back to its own ceremony`,
|
|
183
199
|
];
|
|
184
200
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Saved notifications, read and told apart without a store: the row readers, the lifecycle key and the predicates
|
|
2
|
+
* the store and the chat core (chat-messages.ts) share. A leaf: it imports types only. */
|
|
3
|
+
import type { MateProposal, Notification, NotificationReceipt } from "./store.js";
|
|
4
|
+
/** The one task a card is about: a task its confirmation created (a new
|
|
5
|
+
* task, a revision), else the task it names (and, for feedback on a result,
|
|
6
|
+
* that result). Null for a card about no single task. A pointer for
|
|
7
|
+
* replies, never an authority. */
|
|
8
|
+
export declare function proposalTaskOf(proposal: MateProposal | null): {
|
|
9
|
+
task: string;
|
|
10
|
+
run: number | null;
|
|
11
|
+
} | null;
|
|
12
|
+
/** Every lifecycle dedupe key starts here: `life:<kind>:<identity>:<ordinal>`. */
|
|
13
|
+
export declare const LIFECYCLE_KEY_PREFIX = "life:";
|
|
14
|
+
/**
|
|
15
|
+
* Task lifecycle updates (Telegram task updates, 2026-09-16): the CLOSED
|
|
16
|
+
* vocabulary of routine progress facts the shared mutations record beside
|
|
17
|
+
* the change that made them true. None carries a push class — they are
|
|
18
|
+
* digest-eligible progress, never a page — and every one is project-bound
|
|
19
|
+
* through the task it names. A decision, incident, gap, stall, publication
|
|
20
|
+
* or merge page keeps its own producer; nothing here repeats one.
|
|
21
|
+
*/
|
|
22
|
+
export declare const LIFECYCLE_KINDS: readonly ["task-filed", "scope-approved", "approval-withdrawn", "task-held", "task-released", "task-queued", "task-requeued", "task-cancelled", "run-started", "run-phase", "check-progress", "run-finished", "run-stopping", "run-stopped", "run-resumed", "review-requested", "review-finished", "acceptance-evidence", "acceptance-ready"];
|
|
23
|
+
export type LifecycleKind = (typeof LIFECYCLE_KINDS)[number];
|
|
24
|
+
/** A progress fact, told apart by its durable key — never by display text. */
|
|
25
|
+
export declare function isLifecycleNotification(row: Pick<Notification, "dedupeKey">): boolean;
|
|
26
|
+
/** Only known progress producers can repaint a card. Decisions and urgent
|
|
27
|
+
* incidents keep their own alerts; a routine retry updates its saved attempt. */
|
|
28
|
+
export declare function isTelegramProgressNotification(row: Pick<Notification, "dedupeKey" | "kind" | "pushClass" | "run">): boolean;
|
|
29
|
+
export declare function readNotification(row: Record<string, unknown>): Notification;
|
|
30
|
+
export declare function readNotificationReceipt(row: Record<string, unknown>): NotificationReceipt;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** The one task a card is about: a task its confirmation created (a new
|
|
2
|
+
* task, a revision), else the task it names (and, for feedback on a result,
|
|
3
|
+
* that result). Null for a card about no single task. A pointer for
|
|
4
|
+
* replies, never an authority. */
|
|
5
|
+
export function proposalTaskOf(proposal) {
|
|
6
|
+
if (proposal === null)
|
|
7
|
+
return null;
|
|
8
|
+
const named = typeof proposal.payload["task"] === "string" && proposal.payload["task"] !== "" ? proposal.payload["task"] : null;
|
|
9
|
+
const creates = proposal.kind === "task" || (proposal.kind === "review" && proposal.payload["operation"] === "revise");
|
|
10
|
+
const made = creates && proposal.state === "confirmed" ? proposal.outcome?.["taskId"] : undefined;
|
|
11
|
+
if (typeof made === "string" && made !== "" && made !== named)
|
|
12
|
+
return { task: made, run: null };
|
|
13
|
+
if (named === null)
|
|
14
|
+
return null;
|
|
15
|
+
const run = proposal.kind === "review" ? proposal.payload["run"] : null;
|
|
16
|
+
return { task: named, run: typeof run === "number" && Number.isSafeInteger(run) ? run : null };
|
|
17
|
+
}
|
|
18
|
+
/** Every lifecycle dedupe key starts here: `life:<kind>:<identity>:<ordinal>`. */
|
|
19
|
+
export const LIFECYCLE_KEY_PREFIX = "life:";
|
|
20
|
+
/**
|
|
21
|
+
* Task lifecycle updates (Telegram task updates, 2026-09-16): the CLOSED
|
|
22
|
+
* vocabulary of routine progress facts the shared mutations record beside
|
|
23
|
+
* the change that made them true. None carries a push class — they are
|
|
24
|
+
* digest-eligible progress, never a page — and every one is project-bound
|
|
25
|
+
* through the task it names. A decision, incident, gap, stall, publication
|
|
26
|
+
* or merge page keeps its own producer; nothing here repeats one.
|
|
27
|
+
*/
|
|
28
|
+
export const LIFECYCLE_KINDS = [
|
|
29
|
+
"task-filed",
|
|
30
|
+
"scope-approved",
|
|
31
|
+
"approval-withdrawn",
|
|
32
|
+
"task-held",
|
|
33
|
+
"task-released",
|
|
34
|
+
"task-queued",
|
|
35
|
+
"task-requeued",
|
|
36
|
+
"task-cancelled",
|
|
37
|
+
"run-started",
|
|
38
|
+
"run-phase",
|
|
39
|
+
"check-progress",
|
|
40
|
+
"run-finished",
|
|
41
|
+
"run-stopping",
|
|
42
|
+
"run-stopped",
|
|
43
|
+
"run-resumed",
|
|
44
|
+
"review-requested",
|
|
45
|
+
"review-finished",
|
|
46
|
+
"acceptance-evidence",
|
|
47
|
+
"acceptance-ready",
|
|
48
|
+
];
|
|
49
|
+
/** A progress fact, told apart by its durable key — never by display text. */
|
|
50
|
+
export function isLifecycleNotification(row) {
|
|
51
|
+
return row.dedupeKey.startsWith(LIFECYCLE_KEY_PREFIX);
|
|
52
|
+
}
|
|
53
|
+
/** Only known progress producers can repaint a card. Decisions and urgent
|
|
54
|
+
* incidents keep their own alerts; a routine retry updates its saved attempt. */
|
|
55
|
+
export function isTelegramProgressNotification(row) {
|
|
56
|
+
if (row.kind === "check-progress" && row.pushClass === "progress" && row.run !== null)
|
|
57
|
+
return isLifecycleNotification(row);
|
|
58
|
+
if (row.pushClass !== null)
|
|
59
|
+
return false;
|
|
60
|
+
if (row.kind === "build-failed" && row.run !== null && row.dedupeKey === `run:${row.run}:failed`)
|
|
61
|
+
return true;
|
|
62
|
+
// The owner's lead took the attempt on, or let it lapse (lead-voice.ts): its card repaints in place.
|
|
63
|
+
if ((row.kind === "lead-on-it" || row.kind === "lead-lapsed") && row.run !== null)
|
|
64
|
+
return true;
|
|
65
|
+
return isLifecycleNotification(row) &&
|
|
66
|
+
["run-started", "run-phase", "run-finished", "review-requested", "review-finished", "run-stopping", "run-stopped", "run-resumed", "task-held", "task-released"].includes(row.kind);
|
|
67
|
+
}
|
|
68
|
+
export function readNotification(row) {
|
|
69
|
+
return {
|
|
70
|
+
id: Number(row["id"]),
|
|
71
|
+
recipient: row["recipient"] == null ? null : String(row["recipient"]),
|
|
72
|
+
dedupeKey: String(row["dedupe_key"]),
|
|
73
|
+
scope: row["provenance_scope"],
|
|
74
|
+
project: row["project"] == null ? null : String(row["project"]),
|
|
75
|
+
taskRef: row["task_ref"] == null ? null : Number(row["task_ref"]),
|
|
76
|
+
taskId: row["task_id"] == null ? null : String(row["task_id"]),
|
|
77
|
+
run: row["source_run"] == null ? null : Number(row["source_run"]),
|
|
78
|
+
kind: String(row["kind"]),
|
|
79
|
+
subject: String(row["subject"]),
|
|
80
|
+
body: String(row["body"]),
|
|
81
|
+
createdAt: String(row["created_at"]),
|
|
82
|
+
resolvedAt: row["resolved_at"] === null || row["resolved_at"] === undefined
|
|
83
|
+
? null
|
|
84
|
+
: String(row["resolved_at"]),
|
|
85
|
+
pushClass: (row["push_class"] === null || row["push_class"] === undefined) && String(row["kind"]) === "check-progress"
|
|
86
|
+
? "progress"
|
|
87
|
+
: row["push_class"] === null || row["push_class"] === undefined
|
|
88
|
+
? null
|
|
89
|
+
: String(row["push_class"]),
|
|
90
|
+
link: row["link"] === null || row["link"] === undefined ? null : String(row["link"]),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function readNotificationReceipt(row) {
|
|
94
|
+
return {
|
|
95
|
+
...readNotification(row),
|
|
96
|
+
attempts: Number(row["attempts"]),
|
|
97
|
+
lastAttemptAt: row["last_attempt_at"] == null ? null : String(row["last_attempt_at"]),
|
|
98
|
+
lastError: row["last_error"] == null ? null : String(row["last_error"]),
|
|
99
|
+
deliveredAt: row["delivered_at"] == null ? null : String(row["delivered_at"]),
|
|
100
|
+
receipt: row["receipt"] == null ? null : String(row["receipt"]),
|
|
101
|
+
};
|
|
102
|
+
}
|
package/dist/operate.js
CHANGED
|
@@ -175,7 +175,7 @@ import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordina
|
|
|
175
175
|
import { serveMcp } from "./mcp.js";
|
|
176
176
|
import { createInterface } from "node:readline";
|
|
177
177
|
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
|
|
178
|
-
import { presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
178
|
+
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
179
179
|
import { WorktreePool } from "./worktree.js";
|
|
180
180
|
import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
|
|
181
181
|
import { worktreeAdoptionNotice } from "./worktree-notices.js";
|
|
@@ -4659,8 +4659,11 @@ async function modeCommand(positional, flags, context) {
|
|
|
4659
4659
|
return fail(write, json, "mode set", "invalid", "--plan-auto requires automatic filing approval", EXIT.refused);
|
|
4660
4660
|
}
|
|
4661
4661
|
// Approving plans and merges from the signer's paired chat: never a preset default, only this flag.
|
|
4662
|
-
|
|
4662
|
+
// The signature names every chat app it covers: a grant signed before that named Telegram alone, and stays so.
|
|
4663
|
+
if (flag(flags, "chat-approve")) {
|
|
4663
4664
|
terms.chatApprove = true;
|
|
4665
|
+
terms.chatApproveChats = CHAT_APPROVE_ALL;
|
|
4666
|
+
}
|
|
4664
4667
|
// Automerge requires a live merge-capable grant on the repo (D1).
|
|
4665
4668
|
if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now)) {
|
|
4666
4669
|
return fail(write, json, "mode set", "no-grant", "automerge needs a merge-capable publication grant on this repo — file one first", EXIT.refused);
|
package/dist/plane-review.js
CHANGED
|
@@ -175,17 +175,11 @@ export function reviewPlane(store, now, canSee = () => true) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
// Chat replies that couldn't be delivered.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
add(`chat/${app}`, `${name} replies weren't delivered`, ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["problem"])) || "dropped"}` });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (tableExists(store, "telegram_conversation_part")) {
|
|
187
|
-
for (const row of store.handle.prepare("SELECT last_error, attempts FROM telegram_conversation_part WHERE state = 'dropped' AND created_at >= ?").all(since)) {
|
|
188
|
-
add("chat/telegram", "Telegram replies weren't delivered", ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["last_error"])) || "dropped"}` });
|
|
178
|
+
if (tableExists(store, "chat_part")) {
|
|
179
|
+
const names = { telegram: "Telegram", slack: "Slack", discord: "Discord", teams: "Teams" };
|
|
180
|
+
for (const row of store.handle.prepare("SELECT provider, problem, attempts FROM chat_part WHERE state = 'dropped' AND created >= ? ORDER BY provider = 'telegram' DESC, provider, id").all(since)) {
|
|
181
|
+
const app = String(row["provider"]);
|
|
182
|
+
add(`chat/${app}`, `${names[app] ?? app} replies weren't delivered`, ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["problem"])) || "dropped"}` });
|
|
189
183
|
}
|
|
190
184
|
}
|
|
191
185
|
if (tableExists(store, "notification_delivery")) {
|
package/dist/project-delete.js
CHANGED
|
@@ -207,35 +207,30 @@ function deleteRows(store, repo, d, now) {
|
|
|
207
207
|
const notifications = json(ids(db, `SELECT id FROM notification WHERE project = ? OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, T, R));
|
|
208
208
|
const questions = json(ids(db, `SELECT id FROM teammate_question WHERE ${IN("teammate")} OR ${IN("card")}`, M, C));
|
|
209
209
|
const proposals = json(ids(db, `SELECT id FROM mate_proposal WHERE ${IN("thread")}`, H));
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
del("
|
|
224
|
-
del("
|
|
225
|
-
del("
|
|
226
|
-
del("
|
|
227
|
-
del("
|
|
228
|
-
del("telegram_flow_prompt", IN("card"), C);
|
|
229
|
-
del("telegram_flow_choice", IN("card"), C);
|
|
210
|
+
// A chat message about one of the project's tasks (its turn named it), with its reply. Event ids are text ('m123'),
|
|
211
|
+
// and the same id can be another app's: each is kept whole, as provider:id.
|
|
212
|
+
const conversations = json(list(db, `SELECT provider || ':' || id FROM chat_event WHERE kind = 'message' AND json_extract(payload, '$.about.task') IN (SELECT value FROM json_each(?))`, K));
|
|
213
|
+
// Every app's planned parts about the project's work: a result screenshot, or a message naming one of its tasks or runs.
|
|
214
|
+
const parts = `SELECT provider || ':' || id FROM chat_part WHERE provider || ':' || event IN (SELECT value FROM json_each(?))
|
|
215
|
+
OR json_extract(payload, '$.image.taskId') IN (SELECT value FROM json_each(?)) OR json_extract(payload, '$.image.run') IN (SELECT value FROM json_each(?))`;
|
|
216
|
+
const doomedParts = `provider || ':' || part IN (${parts})`;
|
|
217
|
+
// Chats about the project's work, on every app.
|
|
218
|
+
del("chat_progress", IN("run"), R);
|
|
219
|
+
del("chat_action", `${IN("proposal")} OR ${IN("decision")} OR ${doomedParts}`, proposals, decisions, conversations, K, R);
|
|
220
|
+
del("chat_flow_action", `${IN("card")} OR ${doomedParts}`, C, conversations, K, R);
|
|
221
|
+
del("chat_flow_prompt", IN("card"), C);
|
|
222
|
+
del("chat_flow_choice", `${IN("card")} OR ${doomedParts}`, C, conversations, K, R);
|
|
223
|
+
del("chat_flow_note", IN("card"), C);
|
|
224
|
+
del("chat_question_action", `${IN("question")} OR ${doomedParts}`, questions, conversations, K, R);
|
|
225
|
+
del("chat_question_prompt", IN("question"), questions);
|
|
226
|
+
del("chat_ask_action", `${IN("turn")} OR ${doomedParts}`, U, conversations, K, R);
|
|
227
|
+
del("chat_note_draft", IN("decision"), decisions);
|
|
230
228
|
del("flow_send", IN("card"), C);
|
|
231
|
-
del("telegram_question_action", IN("question"), questions);
|
|
232
|
-
del("telegram_question_prompt", IN("question"), questions);
|
|
233
229
|
del("chat_decide_action", IN("task_id"), K);
|
|
234
230
|
del("chat_decide_prompt", IN("task_id"), K);
|
|
235
|
-
del("
|
|
236
|
-
del("
|
|
237
|
-
del("
|
|
238
|
-
del("telegram_outbound_message", `project = ? OR ${IN("notification")} OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, notifications, T, R);
|
|
231
|
+
del("chat_message_ref", `${IN("decision")} OR ${IN("task_id")} OR ${IN("run")} OR project = ? OR ${IN("notification")} OR ${IN("task_ref")}`, decisions, K, R, repo, notifications, T);
|
|
232
|
+
del("chat_part", `provider || ':' || id IN (${parts})`, conversations, K, R);
|
|
233
|
+
del("chat_event", `provider || ':' || id IN (SELECT value FROM json_each(?))`, conversations);
|
|
239
234
|
del("chat_card_task", IN("task_ref"), T);
|
|
240
235
|
del("chat_batch_item", IN("task_ref"), T);
|
|
241
236
|
del("result_shot_sent", IN("run"), R);
|
package/dist/result-shots.js
CHANGED
|
@@ -169,6 +169,6 @@ export function refuseResultShots(state, store, part, content, app, now) {
|
|
|
169
169
|
const title = chatTitle(store, content.image?.taskId ?? content.task ?? "");
|
|
170
170
|
const line = { text: `Screenshots for ${title} weren't sent: the ${app} app isn't allowed to upload files here.`,
|
|
171
171
|
...(content.task === undefined ? {} : { task: content.task }), ...(content.run === undefined ? {} : { run: content.run }) };
|
|
172
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE id=?").run(savedChatPart(line), now.toISOString(), part.id);
|
|
173
|
-
state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE event=? AND id<>? AND state='pending'").run(part.event, part.id);
|
|
172
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=?").run(savedChatPart(line), now.toISOString(), part.id);
|
|
173
|
+
state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE provider=:provider AND event=? AND id<>? AND state='pending'").run(part.event, part.id);
|
|
174
174
|
}
|