toolroll 0.9.51 → 0.9.52
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 +12 -0
- package/dist/backup.d.ts +8 -0
- package/dist/backup.js +10 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +1 -1
- package/dist/browser-workspace.d.ts +1 -0
- package/dist/database-reclaim.d.ts +4 -0
- package/dist/database-reclaim.js +13 -0
- package/dist/demo.d.ts +1 -1
- package/dist/demo.js +1 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/live-bus.d.ts +3 -3
- package/dist/live-bus.js +3 -3
- package/dist/monitoring.js +32 -8
- package/dist/notification-delivery.d.ts +19 -0
- package/dist/notification-delivery.js +37 -0
- package/dist/operate-remote-arguments.js +1 -1
- package/dist/operate-remote.js +2 -2
- package/dist/operate.js +49 -119
- package/dist/process-recovery-settlement.js +1 -0
- package/dist/retention.js +6 -5
- package/dist/serve.js +1 -1
- package/dist/server/settings.js +3 -1
- package/dist/server/shared.d.ts +1 -0
- package/dist/server/shared.js +2 -0
- package/dist/store.d.ts +38 -43
- package/dist/store.js +255 -134
- package/dist/surface.js +1 -2
- package/dist/toolroll-update.d.ts +14 -3
- package/dist/toolroll-update.js +105 -14
- package/dist/webhooks.d.ts +10 -22
- package/dist/webhooks.js +43 -81
- package/dist/work-index.js +2 -2
- package/dist/workspace-revision.d.ts +28 -1
- package/dist/workspace-revision.js +276 -49
- package/package.json +1 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Database } from "./store.js";
|
|
2
|
+
/** Reclaim whole free pages on databases created without auto_vacuum. Call only after committing writes.
|
|
3
|
+
* A migration needs at least 1 MiB to justify the I/O; retention reclaims whenever more than 25% is free. */
|
|
4
|
+
export declare function reclaimDatabase(db: Database, reason: "migration" | "retention"): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Reclaim whole free pages on databases created without auto_vacuum. Call only after committing writes.
|
|
2
|
+
* A migration needs at least 1 MiB to justify the I/O; retention reclaims whenever more than 25% is free. */
|
|
3
|
+
export function reclaimDatabase(db, reason) {
|
|
4
|
+
const free = Number(db.prepare("PRAGMA freelist_count").get()["freelist_count"]);
|
|
5
|
+
const pages = Number(db.prepare("PRAGMA page_count").get()["page_count"]);
|
|
6
|
+
const size = Number(db.prepare("PRAGMA page_size").get()["page_size"]);
|
|
7
|
+
if (reason === "migration" ? free * size < 1024 * 1024 : free * 4 <= pages)
|
|
8
|
+
return false;
|
|
9
|
+
db.exec("VACUUM");
|
|
10
|
+
// VACUUM writes through WAL too: checkpoint so the main file (and its backups) shrinks now.
|
|
11
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
12
|
+
return true;
|
|
13
|
+
}
|
package/dist/demo.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - The database is stamped `demo` BEFORE any row exists — an append-only
|
|
9
9
|
* installation fact with no unset API. Every spending or external-effect
|
|
10
10
|
* command (tick, watch, build, publish, reconcile, daemon, bridge,
|
|
11
|
-
*
|
|
11
|
+
* intake) fails closed on the stamp, so a kept sandbox
|
|
12
12
|
* can never be mistaken for real work by a worker pointed at it later.
|
|
13
13
|
* The console banner is decoration; the fence is enforcement.
|
|
14
14
|
* - The seeded history is SYNTHETIC and does not pretend otherwise: runs
|
package/dist/demo.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - The database is stamped `demo` BEFORE any row exists — an append-only
|
|
9
9
|
* installation fact with no unset API. Every spending or external-effect
|
|
10
10
|
* command (tick, watch, build, publish, reconcile, daemon, bridge,
|
|
11
|
-
*
|
|
11
|
+
* intake) fails closed on the stamp, so a kept sandbox
|
|
12
12
|
* can never be mistaken for real work by a worker pointed at it later.
|
|
13
13
|
* The console banner is decoration; the fence is enforcement.
|
|
14
14
|
* - The seeded history is SYNTHETIC and does not pretend otherwise: runs
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -94,13 +94,13 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
94
94
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
95
95
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
96
96
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
97
|
-
error?: never;
|
|
98
97
|
active: boolean;
|
|
99
98
|
phase: string;
|
|
100
99
|
detail: string;
|
|
101
100
|
running: boolean;
|
|
102
101
|
canResume: boolean;
|
|
103
102
|
canCancel: boolean;
|
|
103
|
+
error?: never;
|
|
104
104
|
automaticRecovery?: never;
|
|
105
105
|
recoveryAttempts?: never;
|
|
106
106
|
recoveryRepairs?: never;
|
package/dist/live-bus.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live views push on write. Nothing here polls once a second.
|
|
3
3
|
*
|
|
4
|
-
* - The bus.
|
|
5
|
-
*
|
|
4
|
+
* - The bus. Every connection's write wrapper (workspace-revision.ts)
|
|
5
|
+
* moves one counter per committed meaningful write. After each in-process
|
|
6
6
|
* transact() COMMIT, and whenever another process (the worker, the CLI)
|
|
7
7
|
* writes the database's write-ahead log, the revision is read once; when
|
|
8
8
|
* it moved, every subscriber hears `{ revision }`. A rolled-back or quiet
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* gets one `reload` and ends so its EventSource can reconnect.
|
|
21
21
|
*
|
|
22
22
|
* Transport seam: a future WebSocket (live cursors) subscribes to the same
|
|
23
|
-
* LiveBus beside these SSE rooms; nothing in the Store or
|
|
23
|
+
* LiveBus beside these SSE rooms; nothing in the Store or its write wrapper changes.
|
|
24
24
|
*/
|
|
25
25
|
import { type FSWatcher } from "node:fs";
|
|
26
26
|
import type { ServerResponse } from "node:http";
|
package/dist/live-bus.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live views push on write. Nothing here polls once a second.
|
|
3
3
|
*
|
|
4
|
-
* - The bus.
|
|
5
|
-
*
|
|
4
|
+
* - The bus. Every connection's write wrapper (workspace-revision.ts)
|
|
5
|
+
* moves one counter per committed meaningful write. After each in-process
|
|
6
6
|
* transact() COMMIT, and whenever another process (the worker, the CLI)
|
|
7
7
|
* writes the database's write-ahead log, the revision is read once; when
|
|
8
8
|
* it moved, every subscriber hears `{ revision }`. A rolled-back or quiet
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* gets one `reload` and ends so its EventSource can reconnect.
|
|
21
21
|
*
|
|
22
22
|
* Transport seam: a future WebSocket (live cursors) subscribes to the same
|
|
23
|
-
* LiveBus beside these SSE rooms; nothing in the Store or
|
|
23
|
+
* LiveBus beside these SSE rooms; nothing in the Store or its write wrapper changes.
|
|
24
24
|
*/
|
|
25
25
|
import { watch } from "node:fs";
|
|
26
26
|
import { WORKSPACE_REVISION_KEY } from "./workspace-revision.js";
|
package/dist/monitoring.js
CHANGED
|
@@ -24,6 +24,7 @@ export const SIGNATURE_HEADER = "x-standing-orders-signature";
|
|
|
24
24
|
const BATCH = 200;
|
|
25
25
|
const LEASE_MS = 60_000;
|
|
26
26
|
const TIMEOUT_MS = 10_000;
|
|
27
|
+
const CHECKPOINT_EVERY_MS = 60 * 60_000;
|
|
27
28
|
/** An audit event: the ledger entry and its seal, as the stream sends it. */
|
|
28
29
|
export function auditEvent(entry, instance) {
|
|
29
30
|
return {
|
|
@@ -41,15 +42,34 @@ const nanos = (at) => `${BigInt(Date.parse(at)) * 1000000n}`;
|
|
|
41
42
|
/** One pass over every destination that's set up. Returns what it sent, per destination. */
|
|
42
43
|
export async function monitoringPass(store, settings, deps) {
|
|
43
44
|
const sent = {};
|
|
45
|
+
const heads = [];
|
|
46
|
+
const audit = (delivered) => { if (delivered.head !== null)
|
|
47
|
+
heads.push(delivered.head); return delivered.count; };
|
|
44
48
|
// A new destination gets the whole audit history; traces start from now.
|
|
45
49
|
if (settings.webhook !== null)
|
|
46
|
-
sent.webhook = await deliver(store, "webhook", targetOf(settings.webhook.url), () => 0, deps, entries => postSigned(settings.webhook, entries, store, deps));
|
|
50
|
+
sent.webhook = audit(await deliver(store, "webhook", targetOf(settings.webhook.url), () => 0, deps, entries => postSigned(settings.webhook, entries, store, deps)));
|
|
47
51
|
if (settings.folder !== null)
|
|
48
|
-
sent.folder = await deliver(store, "folder", targetOf(settings.folder.path), () => 0, deps, async (entries) => { appendFolder(settings.folder.path, entries, deps.instance); return entries.length; });
|
|
52
|
+
sent.folder = audit(await deliver(store, "folder", targetOf(settings.folder.path), () => 0, deps, async (entries) => { appendFolder(settings.folder.path, entries, deps.instance); return entries.length; }));
|
|
49
53
|
if (settings.traces !== null)
|
|
50
|
-
sent.traces = await deliver(store, "traces", targetOf(settings.traces.endpoint), () => store.ledgerHeadId(), deps, entries => postTraces(settings.traces, entries, store, deps));
|
|
54
|
+
sent.traces = (await deliver(store, "traces", targetOf(settings.traces.endpoint), () => store.ledgerHeadId(), deps, entries => postTraces(settings.traces, entries, store, deps))).count;
|
|
55
|
+
checkpointSent(store, heads, (deps.now ?? (() => new Date()))());
|
|
51
56
|
return sent;
|
|
52
57
|
}
|
|
58
|
+
/** The furthest head the audit stream delivered, kept as an automatic checkpoint once the whole chain verifies.
|
|
59
|
+
* Automatic checkpoints write no ledger entry, so recording one gives the stream nothing new to send (and checkpoint)
|
|
60
|
+
* next pass. At most one an hour, so a busy stream neither floods the table nor walks the whole chain every pass. */
|
|
61
|
+
function checkpointSent(store, heads, now) {
|
|
62
|
+
const head = heads.reduce((best, one) => best === null || (one !== null && one.through > best.through) ? one : best, null);
|
|
63
|
+
if (head === null)
|
|
64
|
+
return;
|
|
65
|
+
const newest = store.ledgerCheckpoints(1)[0];
|
|
66
|
+
if (newest !== undefined && (newest.through >= head.through || now.getTime() - Date.parse(newest.at) < CHECKPOINT_EVERY_MS))
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
store.ledgerCheckpoint("system", now, { head });
|
|
70
|
+
}
|
|
71
|
+
catch { /* a busy database: the next delivery tries again */ }
|
|
72
|
+
}
|
|
53
73
|
/** A destination's name in its status row: a digest of its address (the address itself may carry a key). */
|
|
54
74
|
export const targetOf = (address) => hex(`standing-orders/monitoring/${address}`, 16);
|
|
55
75
|
/** Send what's due to one destination: hold it, start it over if it now points elsewhere, read after its cursor,
|
|
@@ -57,26 +77,30 @@ export const targetOf = (address) => hex(`standing-orders/monitoring/${address}`
|
|
|
57
77
|
async function deliver(store, sink, target, start, deps, send) {
|
|
58
78
|
const now = deps.now ?? (() => new Date());
|
|
59
79
|
const at = now();
|
|
80
|
+
const none = { count: 0, head: null };
|
|
60
81
|
if (!store.holdMonitoring(sink, deps.holder, at, new Date(at.getTime() + LEASE_MS)))
|
|
61
|
-
return
|
|
82
|
+
return none;
|
|
62
83
|
let status = store.monitoringStatus(sink)[0];
|
|
63
84
|
if (status === undefined || status.target !== target) {
|
|
64
85
|
store.resetMonitoring(sink, target, start());
|
|
65
86
|
status = store.monitoringStatus(sink)[0];
|
|
66
87
|
}
|
|
67
88
|
if (status.nextTryAt != null && Date.parse(status.nextTryAt) > at.getTime())
|
|
68
|
-
return
|
|
89
|
+
return none;
|
|
69
90
|
const from = status.through;
|
|
70
91
|
const entries = store.sealedAfter(from, BATCH);
|
|
71
92
|
if (entries.length === 0)
|
|
72
|
-
return
|
|
93
|
+
return none;
|
|
94
|
+
const last = entries[entries.length - 1];
|
|
73
95
|
try {
|
|
74
96
|
const count = await send(entries);
|
|
75
|
-
|
|
97
|
+
if (!store.monitoringDelivered(sink, deps.holder, target, from, last.id, count, now()))
|
|
98
|
+
return none;
|
|
99
|
+
return { count, head: last.seal === null ? null : { through: last.id, hash: last.seal.hash } };
|
|
76
100
|
}
|
|
77
101
|
catch (error) {
|
|
78
102
|
store.monitoringFailed(sink, deps.holder, target, error instanceof Error ? error.message : String(error), now(), new Date(now().getTime() + backoff(status.failures + 1)));
|
|
79
|
-
return
|
|
103
|
+
return none;
|
|
80
104
|
}
|
|
81
105
|
}
|
|
82
106
|
async function post(url, body, headers, deps) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Notification, Store } from "./store.js";
|
|
2
|
+
type Claim = Notification & {
|
|
3
|
+
destination: string;
|
|
4
|
+
generation: number;
|
|
5
|
+
};
|
|
6
|
+
/** URLs and shell commands can contain secrets. Only a destination hash enters saved receipts. */
|
|
7
|
+
export declare function notificationDestination(kind: string, target: string): string;
|
|
8
|
+
/** One-release compatibility for operator-configured, installation-wide senders. Personal notifications never leave here. */
|
|
9
|
+
export declare function claimNotifications(store: Store, destination: string, owner: string, now: Date): Claim[];
|
|
10
|
+
export declare function finalizeNotification(store: Store, row: Claim, owner: string, outcome: {
|
|
11
|
+
ok: true;
|
|
12
|
+
receipt: string | null;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
}, now: Date): boolean;
|
|
17
|
+
/** Recheck after earlier network sends: expiry or resolution must stop this send, not just its receipt. */
|
|
18
|
+
export declare function notificationClaimHeld(store: Store, row: Claim, owner: string, now: Date): boolean;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** URLs and shell commands can contain secrets. Only a destination hash enters saved receipts. */
|
|
3
|
+
export function notificationDestination(kind, target) {
|
|
4
|
+
return `${kind}:${createHash("sha256").update(target).digest("hex")}`;
|
|
5
|
+
}
|
|
6
|
+
/** One-release compatibility for operator-configured, installation-wide senders. Personal notifications never leave here. */
|
|
7
|
+
export function claimNotifications(store, destination, owner, now) {
|
|
8
|
+
return store.transact(() => {
|
|
9
|
+
const db = store.handle, stamp = now.toISOString();
|
|
10
|
+
db.prepare(`INSERT OR IGNORE INTO notification_delivery(notification, destination)
|
|
11
|
+
SELECT id, ? FROM notification WHERE resolved_at IS NULL AND recipient IS NULL`).run(destination);
|
|
12
|
+
const claims = db.prepare(`SELECT d.notification, d.claim_generation FROM notification_delivery d
|
|
13
|
+
JOIN notification n ON n.id = d.notification WHERE d.destination = ? AND n.resolved_at IS NULL AND n.recipient IS NULL
|
|
14
|
+
AND d.delivered_at IS NULL AND (d.receipt IS NULL OR d.receipt NOT LIKE 'skipped:%')
|
|
15
|
+
AND (d.claim_owner IS NULL OR d.claim_expires_at <= ?) ORDER BY n.id`).all(destination, stamp);
|
|
16
|
+
return claims.map(raw => {
|
|
17
|
+
const id = Number(raw["notification"]), generation = Number(raw["claim_generation"]) + 1;
|
|
18
|
+
db.prepare(`UPDATE notification_delivery SET claim_owner = ?, claim_expires_at = ?, claim_generation = ?
|
|
19
|
+
WHERE notification = ? AND destination = ?`).run(owner, new Date(now.getTime() + 60_000).toISOString(), generation, id, destination);
|
|
20
|
+
return { ...store.notificationById(id), destination, generation };
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function finalizeNotification(store, row, owner, outcome, now) {
|
|
25
|
+
const skipped = outcome.ok && outcome.receipt?.startsWith("skipped:");
|
|
26
|
+
return Number(store.handle.prepare(`UPDATE notification_delivery SET attempts = attempts + 1, last_attempt_at = ?,
|
|
27
|
+
delivered_at = ?, receipt = ?, last_error = ?, claim_owner = NULL, claim_expires_at = NULL
|
|
28
|
+
WHERE notification = ? AND destination = ? AND claim_owner = ? AND claim_generation = ?
|
|
29
|
+
AND claim_expires_at > ? AND delivered_at IS NULL`).run(now.toISOString(), outcome.ok && !skipped ? now.toISOString() : null, outcome.ok ? outcome.receipt : null, outcome.ok ? null : outcome.error, row.id, row.destination, owner, row.generation, now.toISOString()).changes) === 1;
|
|
30
|
+
}
|
|
31
|
+
/** Recheck after earlier network sends: expiry or resolution must stop this send, not just its receipt. */
|
|
32
|
+
export function notificationClaimHeld(store, row, owner, now) {
|
|
33
|
+
return store.handle.prepare(`SELECT 1 FROM notification_delivery d JOIN notification n ON n.id = d.notification
|
|
34
|
+
WHERE d.notification = ? AND d.destination = ? AND d.claim_owner = ? AND d.claim_generation = ?
|
|
35
|
+
AND d.claim_expires_at > ? AND d.delivered_at IS NULL AND n.resolved_at IS NULL AND n.recipient IS NULL`)
|
|
36
|
+
.get(row.id, row.destination, owner, row.generation, now.toISOString()) !== undefined;
|
|
37
|
+
}
|
|
@@ -10,7 +10,7 @@ const args = (positionals = '', values = '', references = {}) => ({
|
|
|
10
10
|
* digest. Neither dereferences a conversation, task or proposal. Creation IDs are never looked up. */
|
|
11
11
|
export const REMOTE_ARGUMENTS = new Map([
|
|
12
12
|
...['status', 'ready', 'grants', 'gaps', 'sync', 'runner list', 'coordinator list', 'incident list',
|
|
13
|
-
'routine list', 'intake show', 'intake run', 'template list', 'webhook status', '
|
|
13
|
+
'routine list', 'intake show', 'intake run', 'template list', 'webhook status', 'review show',
|
|
14
14
|
'chat-approval show', 'chat-approval off'].map(row => [row, args()]),
|
|
15
15
|
['integrations', args('', 'saved')],
|
|
16
16
|
// --person and --token-name are names compared inside the reader's own view; --token stays a refused credential.
|
package/dist/operate-remote.js
CHANGED
|
@@ -46,7 +46,7 @@ export const REMOTE_SCOPES = new Map([
|
|
|
46
46
|
...["list", "show", "add", "refresh", "pause", "resume", "run-now"].map(action => [`routine ${action}`, installation]),
|
|
47
47
|
["config show", installation], ["verify show", repo], ["intake show", installation], ["intake run", installation], ["intake preview", installation],
|
|
48
48
|
["intake pr-comments", installation], ["template list", installation], ["template show", installation], ["contest show", installation],
|
|
49
|
-
["contest exclude", installation], ["webhook status", installation], ["
|
|
49
|
+
["contest exclude", installation], ["webhook status", installation], ["review show", installation],
|
|
50
50
|
["chat-approval show", self], ["chat-approval off", self],
|
|
51
51
|
// The person's own remote actions, or everyone's within their projects for an approver (remote-audit.ts).
|
|
52
52
|
["audit", self],
|
|
@@ -64,7 +64,7 @@ export const REMOTE_SCOPES = new Map([
|
|
|
64
64
|
export const REMOTE_ALL_PROJECTS_ONLY = new Map([
|
|
65
65
|
// Settings, connections and acts that belong to the installation as a whole, not to any one project.
|
|
66
66
|
...["integrations", "grants", "sync", "incident resolve", "routine add", "routine refresh", "routine pause", "routine resume", "routine run-now",
|
|
67
|
-
"config show", "intake run", "contest exclude", "webhook status"
|
|
67
|
+
"config show", "intake run", "contest exclude", "webhook status"].map(row => [row, "installation-wide"]),
|
|
68
68
|
// Cross-project lists and reads a limited person is still refused; scoping them is later work.
|
|
69
69
|
...["runner list", "coordinator list", "outbox list", "incident list", "routine list", "routine show", "intake show", "intake preview",
|
|
70
70
|
"intake pr-comments", "template list", "template show", "contest show", "review show"].map(row => [row, "not yet scoped"]),
|
package/dist/operate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { notificationClaimHeld, claimNotifications, finalizeNotification, notificationDestination } from "./notification-delivery.js";
|
|
1
2
|
import { adapterPolicy } from "./server/route-policy.js";
|
|
2
3
|
import { UNSENT_REPLY_MS } from "./telegram-settings.js";
|
|
3
4
|
import { checkPublicCommand } from "./public-check.js";
|
|
@@ -47,8 +48,6 @@ import { runRequestedUndos, sendTeammateWeeklies } from "./teammate-week.js";
|
|
|
47
48
|
import { refreshConnections } from "./mcp-connect.js";
|
|
48
49
|
import { followDiscord } from "./discord.js";
|
|
49
50
|
import { followTeams } from "./teams.js";
|
|
50
|
-
import { loadDiscordCredentials } from "./discord-api.js";
|
|
51
|
-
import { loadSlackCredentials } from "./slack-api.js";
|
|
52
51
|
import { followSlack } from "./slack.js";
|
|
53
52
|
import { validateScopeText } from "./task-text.js";
|
|
54
53
|
import { runMemoryCommand } from "./memory-cli.js";
|
|
@@ -197,7 +196,7 @@ import { isRiskLevel, isTaskSize, legOf, projectRoute, riskConsequence, routeDig
|
|
|
197
196
|
import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
|
|
198
197
|
import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
|
|
199
198
|
import { effectiveConcurrency, maySlotTake, parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, ProjectPasses, projectConcurrency, saveProjectConcurrency, savedProjectConcurrency } from "./project-concurrency.js";
|
|
200
|
-
import {
|
|
199
|
+
import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, saveConsoleUrl, savePrimary } from "./webhooks.js";
|
|
201
200
|
import { auditOf, inspectionOf, isProviderId, MONEY_CAPABILITIES, PROVIDER_IDS, validModelId, validateSpec, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
202
201
|
import { attestProvider, attestationOf, versionInRange } from "./attest.js";
|
|
203
202
|
import { recognizesEligible } from "./exhaustion.js";
|
|
@@ -575,21 +574,14 @@ Agents — which provider and model each phase runs on
|
|
|
575
574
|
says REVIEW was not read)
|
|
576
575
|
|
|
577
576
|
The outbox — facts that want a person, durably
|
|
578
|
-
toolroll webhook set slack|discord <url>
|
|
579
|
-
UI-only chat mirrors: every page a
|
|
580
|
-
message with a console link; acting
|
|
581
|
-
stays in the console. Delivers when
|
|
582
|
-
Telegram is not configured.
|
|
583
577
|
toolroll webhook set console-url <http://host:port>
|
|
584
578
|
toolroll webhook primary telegram|slack|discord
|
|
585
579
|
which service receives alerts when
|
|
586
580
|
several are connected (asked once,
|
|
587
581
|
the first time you add a second)
|
|
588
|
-
toolroll webhook status
|
|
589
|
-
toolroll outbox list [--all]
|
|
590
|
-
toolroll outbox deliver --cmd <
|
|
591
|
-
$TOOLROLL_KIND / _SUBJECT / _BODY;
|
|
592
|
-
exit 0 delivered receipts, 1 any fail
|
|
582
|
+
toolroll webhook status
|
|
583
|
+
toolroll outbox list [--all] pending facts (--all: resolved too)
|
|
584
|
+
toolroll outbox deliver --cmd <cmd> deliver through an operator command
|
|
593
585
|
|
|
594
586
|
Runners — the machines that may be given work
|
|
595
587
|
toolroll runner register <name> [--capacity <n>] [--token-file <path>]
|
|
@@ -1063,8 +1055,8 @@ function telegramConversation(context, options = {}) {
|
|
|
1063
1055
|
evidenceRoot: context.evidenceRoot,
|
|
1064
1056
|
...(context.mateSeams?.subscriptionRunner === undefined ? {} : { subscriptionRunner: context.mateSeams.subscriptionRunner }),
|
|
1065
1057
|
...(context.heldCoordinator === undefined ? {} : { held: context.heldCoordinator }),
|
|
1066
|
-
// Re-read on every card and every `/task`: the same console-url
|
|
1067
|
-
//
|
|
1058
|
+
// Re-read on every card and every `/task`: the same console-url chat
|
|
1059
|
+
// links use, held to an https origin, and — inside `up`, where this
|
|
1068
1060
|
// process also serves the console — equal to that console's own
|
|
1069
1061
|
// `--public-url`, or no link at all.
|
|
1070
1062
|
phoneOrigin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: options.serverOrigin ?? null }),
|
|
@@ -4922,7 +4914,7 @@ async function briefCommand(flags, context) {
|
|
|
4922
4914
|
lines.push(` #${pull.number} ${pull.title}`);
|
|
4923
4915
|
}
|
|
4924
4916
|
if (pending.length > 0) {
|
|
4925
|
-
lines.push(` ▸ OUTBOX ${pending.length} undelivered — toolroll outbox
|
|
4917
|
+
lines.push(` ▸ OUTBOX ${pending.length} undelivered — toolroll outbox list`);
|
|
4926
4918
|
}
|
|
4927
4919
|
if (decisions.length > 0) {
|
|
4928
4920
|
const overdue = decisions.filter(one => one.state === "expired").length;
|
|
@@ -5429,18 +5421,16 @@ async function incidentCommand(positional, flags, context) {
|
|
|
5429
5421
|
]);
|
|
5430
5422
|
}
|
|
5431
5423
|
/**
|
|
5432
|
-
* `toolroll webhook
|
|
5433
|
-
*
|
|
5434
|
-
*
|
|
5435
|
-
* beside the database, or the environment, never anywhere else.
|
|
5424
|
+
* `toolroll webhook status|primary|set console-url` — messaging settings:
|
|
5425
|
+
* which connected chat service receives alerts, and the console address
|
|
5426
|
+
* chat links open.
|
|
5436
5427
|
*/
|
|
5437
|
-
async function webhookCommand(positional,
|
|
5438
|
-
const {
|
|
5428
|
+
async function webhookCommand(positional, _flags, context) {
|
|
5429
|
+
const { write, json } = context;
|
|
5439
5430
|
const dir = dirname(context.databaseFile);
|
|
5440
5431
|
const [action, which, value] = positional;
|
|
5441
5432
|
const telegramConfigured = loadBotToken(process.env, context.telegramTokenFile) !== null;
|
|
5442
5433
|
if (action === undefined || action === "status") {
|
|
5443
|
-
const targets = loadWebhookTargets(process.env, dir);
|
|
5444
5434
|
const consoleUrl = loadConsoleUrl(process.env, dir);
|
|
5445
5435
|
const primary = effectivePrimary(process.env, dir, telegramConfigured);
|
|
5446
5436
|
if (json) {
|
|
@@ -5456,29 +5446,8 @@ async function webhookCommand(positional, flags, context) {
|
|
|
5456
5446
|
write(` Choose: toolroll webhook primary telegram|slack|discord`);
|
|
5457
5447
|
}
|
|
5458
5448
|
write(` links ${consoleUrl ?? "NOT SET — messages will carry no console link; toolroll webhook set console-url http://host:port"}`);
|
|
5459
|
-
if (targets.length === 0) {
|
|
5460
|
-
write("");
|
|
5461
|
-
write(" toolroll webhook set slack https://hooks.slack.com/services/…");
|
|
5462
|
-
write(" toolroll webhook set discord https://discord.com/api/webhooks/…");
|
|
5463
|
-
write(` (or export ${SLACK_ENV} / ${DISCORD_ENV})`);
|
|
5464
|
-
}
|
|
5465
|
-
write("");
|
|
5466
|
-
write(" Mirrors deliver when Telegram is not configured; with a paired Telegram");
|
|
5467
|
-
write(" chat, Telegram carries the page (it can hold buttons) and mirrors stay quiet.");
|
|
5468
5449
|
return EXIT.ok;
|
|
5469
5450
|
}
|
|
5470
|
-
if (action === "test") {
|
|
5471
|
-
const targets = loadWebhookTargets(process.env, dir);
|
|
5472
|
-
if (targets.length === 0) {
|
|
5473
|
-
return fail(write, json, "webhook test", "unconfigured", "no webhook configured — `toolroll webhook set slack|discord <url>`", EXIT.refused);
|
|
5474
|
-
}
|
|
5475
|
-
store.enqueueNotification({ source: { installation: true }, dedupeKey: `webhook-test:${clock().getTime()}`, kind: "test", subject: "toolroll webhook test", body: "If you can read this, the mirror works. Acting happens in the console." }, clock());
|
|
5476
|
-
const report = await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock });
|
|
5477
|
-
if (report.problems.length > 0) {
|
|
5478
|
-
return fail(write, json, "webhook test", "delivery", report.problems.join("; "), EXIT.failed);
|
|
5479
|
-
}
|
|
5480
|
-
return succeed(write, json, "webhook test", { sent: report.sent }, () => [`Sent ${report.sent} message(s). Check the channel.`]);
|
|
5481
|
-
}
|
|
5482
5451
|
if (action === "primary") {
|
|
5483
5452
|
if (which === undefined || !isMessagingChannel(which)) {
|
|
5484
5453
|
return fail(write, json, "webhook primary", "usage", "primary is one of telegram, slack, discord", EXIT.usage);
|
|
@@ -5492,46 +5461,13 @@ async function webhookCommand(positional, flags, context) {
|
|
|
5492
5461
|
`Alerts now go to ${which}. ${which === "telegram" ? "Buttons and replies work there as always." : "Telegram (if configured) still accepts taps and replies — it just stops sending alerts."}`,
|
|
5493
5462
|
]);
|
|
5494
5463
|
}
|
|
5495
|
-
if (action
|
|
5496
|
-
|
|
5497
|
-
return succeed(write, json, "webhook clear", { which }, () => [`${which} mirror cleared.`]);
|
|
5464
|
+
if (action !== "set" || which !== "console-url" || value === undefined) {
|
|
5465
|
+
return fail(write, json, "webhook", "usage", "`toolroll webhook [status|primary <service>|set console-url <url>]`", EXIT.usage);
|
|
5498
5466
|
}
|
|
5499
|
-
|
|
5500
|
-
return fail(write, json, "webhook", "usage", "`toolroll webhook [status|test|set slack|discord|console-url <value>|clear slack|discord]`", EXIT.usage);
|
|
5501
|
-
}
|
|
5502
|
-
if (which === "console-url") {
|
|
5503
|
-
const saved = saveConsoleUrl(dir, value);
|
|
5504
|
-
if (!saved.ok)
|
|
5505
|
-
return fail(write, json, "webhook set", "invalid", saved.message, EXIT.usage);
|
|
5506
|
-
return succeed(write, json, "webhook set", { which }, () => [`Console links will open ${value.replace(/\/+$/, "")}.`]);
|
|
5507
|
-
}
|
|
5508
|
-
if (which !== "slack" && which !== "discord") {
|
|
5509
|
-
return fail(write, json, "webhook set", "usage", "set what? slack, discord, or console-url", EXIT.usage);
|
|
5510
|
-
}
|
|
5511
|
-
const saved = saveWebhook(dir, which, value);
|
|
5467
|
+
const saved = saveConsoleUrl(dir, value);
|
|
5512
5468
|
if (!saved.ok)
|
|
5513
5469
|
return fail(write, json, "webhook set", "invalid", saved.message, EXIT.usage);
|
|
5514
|
-
|
|
5515
|
-
// which one pages — once, right here, not at 3am when both fire.
|
|
5516
|
-
const after = effectivePrimary(process.env, dir, telegramConfigured);
|
|
5517
|
-
let chosen = null;
|
|
5518
|
-
if (after.implicit && loadPrimary(process.env, dir) === null && after.configured.length > 1 && interactive() && !json) {
|
|
5519
|
-
write(`You now have ${after.configured.join(" and ")} connected.`);
|
|
5520
|
-
const answer = (await ask(`Which service should receive alerts? [${after.configured.join("/")}] `)).trim().toLowerCase();
|
|
5521
|
-
if (isMessagingChannel(answer) && after.configured.includes(answer)) {
|
|
5522
|
-
savePrimary(dir, answer);
|
|
5523
|
-
chosen = answer;
|
|
5524
|
-
}
|
|
5525
|
-
else {
|
|
5526
|
-
write(`Left unchosen — ${after.channel} receives alerts by default. Decide any time: toolroll webhook primary <service>`);
|
|
5527
|
-
}
|
|
5528
|
-
}
|
|
5529
|
-
return succeed(write, json, "webhook set", { which, ...(chosen === null ? {} : { primary: chosen }) }, () => [
|
|
5530
|
-
`${which} mirror configured — the URL lives in a private file beside the database.`,
|
|
5531
|
-
...(chosen === null ? [] : [`${chosen} carries the pages.`]),
|
|
5532
|
-
...(after.implicit && chosen === null && !interactive() ? [`Several services are configured — choose the pager: toolroll webhook primary <service>`] : []),
|
|
5533
|
-
`Send yourself a proof: toolroll webhook test`,
|
|
5534
|
-
]);
|
|
5470
|
+
return succeed(write, json, "webhook set", { which }, () => [`Console links will open ${value.replace(/\/+$/, "")}.`]);
|
|
5535
5471
|
}
|
|
5536
5472
|
/**
|
|
5537
5473
|
* `toolroll providers resume <provider>` — a person says the sign-in works
|
|
@@ -7865,6 +7801,7 @@ async function runWatchLoop(args) {
|
|
|
7865
7801
|
let lastTick = 0;
|
|
7866
7802
|
let lastBridge = 0;
|
|
7867
7803
|
let lastPush = 0;
|
|
7804
|
+
let lastWebhook = 0;
|
|
7868
7805
|
let ticks = 0;
|
|
7869
7806
|
let built = 0;
|
|
7870
7807
|
let brokeCount = 0;
|
|
@@ -8160,6 +8097,14 @@ async function runWatchLoop(args) {
|
|
|
8160
8097
|
// Push is additive; a broken pass never stops the watch.
|
|
8161
8098
|
}
|
|
8162
8099
|
}
|
|
8100
|
+
if (now - lastWebhook >= bridgeEveryMs) {
|
|
8101
|
+
lastWebhook = now;
|
|
8102
|
+
const dir = dirname(context.databaseFile);
|
|
8103
|
+
const primary = effectivePrimary(process.env, dir, loadBotToken(process.env, context.telegramTokenFile) !== null);
|
|
8104
|
+
const targets = activeWebhookTargets(process.env, dir, primary.channel);
|
|
8105
|
+
if (targets.length > 0)
|
|
8106
|
+
await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock: context.clock });
|
|
8107
|
+
}
|
|
8163
8108
|
// The embedded follower owns the wire while it lives; the timer-driven
|
|
8164
8109
|
// pass is the fallback shape for a watch started before a token existed.
|
|
8165
8110
|
if (follower === null && now - lastBridge >= bridgeEveryMs) {
|
|
@@ -8175,12 +8120,6 @@ async function runWatchLoop(args) {
|
|
|
8175
8120
|
quiet.length = 0;
|
|
8176
8121
|
await bridgeCommand(["telegram"], passFlags(primary.channel === "telegram" ? {} : { "inbound-only": "1" }), quietContext);
|
|
8177
8122
|
}
|
|
8178
|
-
if (primary.channel !== null && primary.channel !== "telegram") {
|
|
8179
|
-
const targets = loadWebhookTargets(process.env, dir).filter(one => one.kind === primary.channel && (one.kind !== "slack" || loadSlackCredentials(dir) === null) && (one.kind !== "discord" || loadDiscordCredentials(dir) === null));
|
|
8180
|
-
if (targets.length > 0) {
|
|
8181
|
-
await webhookPass(store, { targets, consoleUrl: loadConsoleUrl(process.env, dir), clock: context.clock });
|
|
8182
|
-
}
|
|
8183
|
-
}
|
|
8184
8123
|
}
|
|
8185
8124
|
// Work-conserving: if the world moved while we worked — or we just
|
|
8186
8125
|
// finished something that may have freed a dependent — go again now.
|
|
@@ -8194,7 +8133,7 @@ async function runWatchLoop(args) {
|
|
|
8194
8133
|
// due, a signal lands, or the trial window (--for) ends. Reading one
|
|
8195
8134
|
// integer from SQLite twice a second is the entire idle cost — no
|
|
8196
8135
|
// token has anywhere to be spent from here.
|
|
8197
|
-
const idleUntil = Math.min(lastTick + tickEveryMs, lastBridge + bridgeEveryMs, deadline ?? Number.MAX_SAFE_INTEGER) - Date.now();
|
|
8136
|
+
const idleUntil = Math.min(lastTick + tickEveryMs, lastBridge + bridgeEveryMs, lastWebhook + bridgeEveryMs, deadline ?? Number.MAX_SAFE_INTEGER) - Date.now();
|
|
8198
8137
|
// Doze for the WHOLE idle window, waking early only for a signal or a
|
|
8199
8138
|
// sequence move — not for one 500ms step and then another pass
|
|
8200
8139
|
// (2026-09-05: that read as two ticks a second, all night, with
|
|
@@ -9804,18 +9743,6 @@ async function publishCommand(positional, flags, context) {
|
|
|
9804
9743
|
return succeed(write, json, "publish", { report }, lines);
|
|
9805
9744
|
}
|
|
9806
9745
|
// ---- the outbox -----------------------------------------------------------
|
|
9807
|
-
/**
|
|
9808
|
-
* `toolroll outbox list|deliver` — reading and draining the durable
|
|
9809
|
-
* outbox. Delivery runs an operator-supplied command once per pending row;
|
|
9810
|
-
* the notification's text reaches it as environment variables, never
|
|
9811
|
-
* substituted into the command line, because subjects and bodies quote
|
|
9812
|
-
* things agents and repositories said and a shell must not meet those.
|
|
9813
|
-
*
|
|
9814
|
-
* toolroll outbox deliver --cmd 'curl -d "$TOOLROLL_SUBJECT" ntfy.sh/mine'
|
|
9815
|
-
*
|
|
9816
|
-
* Exit 0 when everything pending delivered (or nothing was pending);
|
|
9817
|
-
* 1 when any delivery failed — a broken channel is breakage, not a "no".
|
|
9818
|
-
*/
|
|
9819
9746
|
/**
|
|
9820
9747
|
* `toolroll peek [<run>] [--tmux] [--lines <n>]` — watch live agents
|
|
9821
9748
|
* in the terminal: one pane per open run with its stage and transcript
|
|
@@ -9865,6 +9792,11 @@ async function peekCommand(positional, flags, context) {
|
|
|
9865
9792
|
}
|
|
9866
9793
|
return runPeek(store, context.evidenceRoot, { ...(runId === undefined ? {} : { runId }), io: { stdout: process.stdout, stdin: process.stdin }, clock });
|
|
9867
9794
|
}
|
|
9795
|
+
/**
|
|
9796
|
+
* `toolroll outbox list [--all]` — read the durable outbox: the facts that
|
|
9797
|
+
* want a person, pending until resolved. An operator command can also deliver
|
|
9798
|
+
* them with its own destination receipts.
|
|
9799
|
+
*/
|
|
9868
9800
|
async function outboxCommand(positional, flags, context) {
|
|
9869
9801
|
const { store, write, json, clock } = context;
|
|
9870
9802
|
const [action] = positional;
|
|
@@ -9876,17 +9808,12 @@ async function outboxCommand(positional, flags, context) {
|
|
|
9876
9808
|
return EXIT.ok;
|
|
9877
9809
|
}
|
|
9878
9810
|
if (notifications.length === 0) {
|
|
9879
|
-
write(wanted === "pending" ? "Nothing
|
|
9811
|
+
write(wanted === "pending" ? "Nothing pending." : "The outbox is empty.");
|
|
9880
9812
|
return EXIT.ok;
|
|
9881
9813
|
}
|
|
9882
9814
|
for (const one of notifications) {
|
|
9883
|
-
const state = one.deliveredAt !== null
|
|
9884
|
-
? `delivered ${one.deliveredAt}`
|
|
9885
|
-
: one.attempts > 0
|
|
9886
|
-
? `pending, ${one.attempts} failed attempt(s): ${one.lastError ?? ""}`
|
|
9887
|
-
: "pending";
|
|
9888
9815
|
write(` #${one.id} ${one.kind.padEnd(18)} ${one.subject}`);
|
|
9889
|
-
write(` ${
|
|
9816
|
+
write(` ${one.resolvedAt === null ? "pending" : `resolved ${one.resolvedAt}`}`);
|
|
9890
9817
|
}
|
|
9891
9818
|
return EXIT.ok;
|
|
9892
9819
|
}
|
|
@@ -9898,14 +9825,9 @@ async function outboxCommand(positional, flags, context) {
|
|
|
9898
9825
|
if (command === undefined) {
|
|
9899
9826
|
return fail(write, json, "outbox deliver", "usage", "--cmd says how: it runs once per notification, reading $TOOLROLL_KIND, $TOOLROLL_SUBJECT, $TOOLROLL_BODY", EXIT.usage);
|
|
9900
9827
|
}
|
|
9901
|
-
//
|
|
9902
|
-
// outbox, and select-then-send-then-record from two deliverers pages a
|
|
9903
|
-
// person twice. The claim is a short lease on the act of sending; a
|
|
9904
|
-
// deliverer that dies mid-send leaves rows that unclaim by expiry.
|
|
9828
|
+
// The exact command owns a leased destination receipt, including across concurrent CLI invocations.
|
|
9905
9829
|
const owner = `outbox-${randomUUID()}`;
|
|
9906
|
-
// Push
|
|
9907
|
-
// depend on globally-undelivered notifications — Telegram or a webhook
|
|
9908
|
-
// may already have stamped delivered_at.
|
|
9830
|
+
// Push delivery has independent receipts and must keep working if this command fails.
|
|
9909
9831
|
let pushed = 0;
|
|
9910
9832
|
try {
|
|
9911
9833
|
pushed = (await pushPass(store, { configDir: dirname(context.databaseFile), clock })).accepted;
|
|
@@ -9913,7 +9835,7 @@ async function outboxCommand(positional, flags, context) {
|
|
|
9913
9835
|
catch {
|
|
9914
9836
|
// additive; the shell delivery below still runs
|
|
9915
9837
|
}
|
|
9916
|
-
const pending = store
|
|
9838
|
+
const pending = claimNotifications(store, notificationDestination("command", command), owner, clock());
|
|
9917
9839
|
if (pending.length === 0) {
|
|
9918
9840
|
return succeed(write, json, "outbox deliver", { delivered: 0, failed: 0, pushed }, () => [
|
|
9919
9841
|
pushed > 0 ? `Nothing for the shell command; ${pushed} push(es) accepted.` : "Nothing waiting to be delivered.",
|
|
@@ -9922,6 +9844,10 @@ async function outboxCommand(positional, flags, context) {
|
|
|
9922
9844
|
let delivered = 0;
|
|
9923
9845
|
let failed = 0;
|
|
9924
9846
|
for (const one of pending) {
|
|
9847
|
+
if (!notificationClaimHeld(store, one, owner, clock())) {
|
|
9848
|
+
failed++;
|
|
9849
|
+
continue;
|
|
9850
|
+
}
|
|
9925
9851
|
const sent = await run("sh", ["-lc", command], {
|
|
9926
9852
|
timeoutMs: 30_000,
|
|
9927
9853
|
env: {
|
|
@@ -9933,14 +9859,16 @@ async function outboxCommand(positional, flags, context) {
|
|
|
9933
9859
|
});
|
|
9934
9860
|
if (sent.code === 0) {
|
|
9935
9861
|
const receipt = sent.stdout.split("\n")[0]?.trim() ?? "";
|
|
9936
|
-
store
|
|
9937
|
-
|
|
9862
|
+
if (finalizeNotification(store, one, owner, { ok: true, receipt: receipt === "" ? null : receipt }, clock()))
|
|
9863
|
+
delivered++;
|
|
9864
|
+
else
|
|
9865
|
+
failed++;
|
|
9938
9866
|
}
|
|
9939
9867
|
else {
|
|
9940
9868
|
const error = sent.timedOut
|
|
9941
9869
|
? "timed out"
|
|
9942
9870
|
: sent.stderr.split("\n")[0]?.trim() || `exit ${sent.code}`;
|
|
9943
|
-
store
|
|
9871
|
+
finalizeNotification(store, one, owner, { ok: false, error }, clock());
|
|
9944
9872
|
failed++;
|
|
9945
9873
|
}
|
|
9946
9874
|
}
|
|
@@ -10171,6 +10099,7 @@ async function statusCommand(positional, flags, context) {
|
|
|
10171
10099
|
// An update waiting, on what, and the action: before anything else, as it holds up new work.
|
|
10172
10100
|
const waiting = waitingUpdate(context.databaseFile, run => context.store.stopQuiescenceProblem(run) !== null, context.clock());
|
|
10173
10101
|
const updateWaiting = waiting === null ? {} : { updateWaiting: { app: waiting.app, version: waiting.version, stopped: waiting.stopped, run: waiting.run, on: waiting.on, action: waiting.action } };
|
|
10102
|
+
const legacyWebhookWarning = loadWebhookTargets(process.env, dirname(context.databaseFile)).length > 0 ? LEGACY_WEBHOOK_WARNING : null;
|
|
10174
10103
|
const projects = projectBuildsStatus(context);
|
|
10175
10104
|
const projectsLine = projects.length === 0 ? null
|
|
10176
10105
|
: `Builds by project: ${projects.slice(0, 8).map(one => `${one.name} ${one.running} of ${one.limit}`).join(", ")}${projects.length > 8 ? ", …" : ""}`;
|
|
@@ -10178,7 +10107,7 @@ async function statusCommand(positional, flags, context) {
|
|
|
10178
10107
|
const unsent = context.store.unsentTelegramReplies(null, null, new Date(context.clock().getTime() - UNSENT_REPLY_MS));
|
|
10179
10108
|
const unsentLine = unsentRepliesLine(unsent);
|
|
10180
10109
|
const unsentReplies = unsent.length === 0 ? {} : { unsentReplies: unsent.map(one => ({ ...one, retry: "toolroll bridge telegram retry" })) };
|
|
10181
|
-
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations, ...updateWaiting, ...unsentReplies }, () => [...(waiting === null ? [] : [waiting.words]), ...(unsentLine === null ? [] : [unsentLine]), ...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
10110
|
+
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations, ...updateWaiting, ...unsentReplies, ...(legacyWebhookWarning === null ? {} : { legacyWebhookWarning }) }, () => [...(waiting === null ? [] : [waiting.words]), ...(unsentLine === null ? [] : [unsentLine]), ...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine]), ...(legacyWebhookWarning === null ? [] : [legacyWebhookWarning])]);
|
|
10182
10111
|
}
|
|
10183
10112
|
function unsentRepliesLine(unsent) {
|
|
10184
10113
|
if (unsent.length === 0)
|
|
@@ -12829,6 +12758,7 @@ async function backupCommand(positional, flags, context) {
|
|
|
12829
12758
|
return fail(context.write, context.json, command, "failed", `The backup failed: ${made.error}`, EXIT.failed, { backup: made.id });
|
|
12830
12759
|
return succeed(context.write, context.json, command, { backup: made }, () => [
|
|
12831
12760
|
`Backed up to ${made.file} (${bytesWords(made.bytes)}).${made.removed === 0 ? "" : ` Removed ${made.removed} older ${made.removed === 1 ? "backup" : "backups"}.`}`,
|
|
12761
|
+
...(made.checkpoint !== null && "problem" in made.checkpoint ? [`The activity log failed its tamper check, so it was not anchored: ${made.checkpoint.problem}`] : []),
|
|
12832
12762
|
]);
|
|
12833
12763
|
}
|
|
12834
12764
|
const settings = store.backupSettings();
|