toolroll 0.9.50 → 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 +21 -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/mcp-oauth.d.ts +1 -1
- package/dist/mcp-oauth.js +3 -2
- 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.d.ts +1 -1
- package/dist/operate.js +54 -120
- package/dist/process-recovery-settlement.js +1 -0
- package/dist/request-budget.d.ts +5 -2
- package/dist/request-budget.js +7 -3
- package/dist/retention.js +6 -5
- package/dist/scope.d.ts +1 -0
- package/dist/scope.js +19 -9
- package/dist/serve.js +15 -10
- package/dist/server/people-tokens.js +1 -2
- package/dist/server/remote.js +19 -4
- package/dist/server/settings.js +3 -1
- package/dist/server/shared.d.ts +1 -0
- package/dist/server/shared.js +2 -0
- package/dist/sign-in-guard.d.ts +41 -16
- package/dist/sign-in-guard.js +147 -24
- package/dist/source-key.d.ts +2 -0
- package/dist/source-key.js +49 -0
- package/dist/store.d.ts +38 -43
- package/dist/store.js +255 -134
- package/dist/surface.js +1 -2
- package/dist/team-http.d.ts +3 -0
- package/dist/team-http.js +3 -0
- 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
package/dist/store.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { reclaimDatabase } from "./database-reclaim.js";
|
|
1
2
|
import { chatSchema, chatTables } from "./contracts/chat-tables.js";
|
|
2
3
|
import { ServerTelemetry } from "./server-telemetry.js";
|
|
3
4
|
import { instrumentDatabase, measureWriteWait } from "./sqlite-telemetry.js";
|
|
5
|
+
import { trackWorkspaceWrites } from "./workspace-revision.js";
|
|
4
6
|
import { parseStoreColumn, readStoreColumn, readStoreStringifiedList, readStoreTextList } from "./contracts/store-json.js";
|
|
5
7
|
import { assessmentFromSavedEvidence, verificationEvidence } from "./verification-evidence.js";
|
|
6
8
|
import { LEARNING_SCHEMA, queueLearning } from "./project-learning.js";
|
|
@@ -582,7 +584,10 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
582
584
|
// v111 keeps an API token's project limit and its rotation: the token that replaced it, and when the old one stops.
|
|
583
585
|
// v112 keeps request budgets for API tokens: owner overrides (request_budget_limit) and saved usage (request_budget_usage).
|
|
584
586
|
// v113 keeps MCP sign-ins (OAuth): registered clients, one-time codes, and each grant's binding to its API token.
|
|
585
|
-
|
|
587
|
+
// v114 names one exact shape, migrated once: a settled run keeps one process summary instead of its exited witnesses,
|
|
588
|
+
// notification's single-destination delivery columns move into legacy receipts, and the workspace revision triggers are gone
|
|
589
|
+
// (the write wrapper bumps the revision). Every later DDL change bumps the version.
|
|
590
|
+
export const SCHEMA_VERSION = 114;
|
|
586
591
|
/**
|
|
587
592
|
* The migrations `toolroll update` may carry a database through in place: each
|
|
588
593
|
* one only adds (a table, a column, an accepted value) and leaves every saved row
|
|
@@ -596,8 +601,13 @@ export const SCHEMA_VERSION = 113;
|
|
|
596
601
|
* api_token columns (project limit, rotation); existing tokens read as before.
|
|
597
602
|
* v112 only adds the two request-budget tables.
|
|
598
603
|
* v113 adds MCP sign-in tables and API-token purpose metadata.
|
|
604
|
+
* v114 is the one declared exception to "only adds", and the rehearsal checks
|
|
605
|
+
* it as such: a settled run's exited process witnesses become one summary row
|
|
606
|
+
* (rows removed equal the witnesses summarized), and notification drops its
|
|
607
|
+
* delivery columns only after every value moves into one legacy receipt per row
|
|
608
|
+
* (rows and ids carried whole; legacy receipts added equal the rows carried).
|
|
599
609
|
*/
|
|
600
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([110, 111, 112, 113]);
|
|
610
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([110, 111, 112, 113, 114]);
|
|
601
611
|
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
602
612
|
export function updateSafeSchema(version) {
|
|
603
613
|
if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
|
|
@@ -2324,26 +2334,7 @@ CREATE TABLE IF NOT EXISTS incident (
|
|
|
2324
2334
|
-- lives only in a process's memory dies with the process, at exactly the
|
|
2325
2335
|
-- moment it was most worth sending. dedupe_key is an episode identity: a
|
|
2326
2336
|
-- gap nags once per occurrence, not once per cron firing and not forever.
|
|
2327
|
-
|
|
2328
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2329
|
-
dedupe_key TEXT NOT NULL UNIQUE,
|
|
2330
|
-
kind TEXT NOT NULL,
|
|
2331
|
-
subject TEXT NOT NULL,
|
|
2332
|
-
body TEXT NOT NULL,
|
|
2333
|
-
created_at TEXT NOT NULL,
|
|
2334
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
2335
|
-
last_attempt_at TEXT,
|
|
2336
|
-
last_error TEXT,
|
|
2337
|
-
delivered_at TEXT,
|
|
2338
|
-
-- What the delivery command said on success — the closest thing to a
|
|
2339
|
-
-- provider receipt a shell command can hand back.
|
|
2340
|
-
receipt TEXT,
|
|
2341
|
-
-- The push surface (arc 3, v23): a CLOSED attention class and a
|
|
2342
|
-
-- machine-minted console link, stamped by producers at enqueue.
|
|
2343
|
-
-- Unstamped kinds never reach a phone; subject/body never do either.
|
|
2344
|
-
push_class TEXT CHECK (push_class IN ('decision','pick','merge','attention')),
|
|
2345
|
-
link TEXT
|
|
2346
|
-
);
|
|
2337
|
+
${NOTIFICATION_DDL("notification")};
|
|
2347
2338
|
|
|
2348
2339
|
-- Away mode (v34, mate arc §10): the Telegram bridge's digest cadence.
|
|
2349
2340
|
-- One row. every_ms NULL = off (every fact pages as it lands); set, the
|
|
@@ -2707,8 +2698,9 @@ CREATE UNIQUE INDEX IF NOT EXISTS telegram_team_chat_live
|
|
|
2707
2698
|
CREATE UNIQUE INDEX IF NOT EXISTS telegram_team_chat_group
|
|
2708
2699
|
ON telegram_team_chat (conversation) WHERE revoked_at IS NULL AND kind = 'group';
|
|
2709
2700
|
|
|
2710
|
-
-- v61: destination receipts around the existing outbox, independent of
|
|
2711
|
-
--
|
|
2701
|
+
-- v61: destination receipts around the existing outbox, independent of
|
|
2702
|
+
-- push_delivery: where each notification went, per destination (v114
|
|
2703
|
+
-- removed the old single-destination receipt). No second scheduler.
|
|
2712
2704
|
CREATE TABLE IF NOT EXISTS notification_delivery (
|
|
2713
2705
|
notification INTEGER NOT NULL REFERENCES notification(id) ON DELETE RESTRICT,
|
|
2714
2706
|
destination TEXT NOT NULL,
|
|
@@ -3779,6 +3771,19 @@ CREATE TABLE IF NOT EXISTS run_process (
|
|
|
3779
3771
|
container_identity TEXT
|
|
3780
3772
|
);
|
|
3781
3773
|
CREATE INDEX IF NOT EXISTS run_process_by_run ON run_process(run, exited_at);
|
|
3774
|
+
-- v114: a settled run (finished, every witness exited) keeps this one row
|
|
3775
|
+
-- instead of its exited witnesses. Its presence is the proof the run's
|
|
3776
|
+
-- processes were witnessed and all exited, so a run with neither witnesses
|
|
3777
|
+
-- nor a summary still reads as "exit unproven".
|
|
3778
|
+
CREATE TABLE IF NOT EXISTS run_process_summary (
|
|
3779
|
+
run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,
|
|
3780
|
+
witnesses INTEGER NOT NULL CHECK (witnesses > 0),
|
|
3781
|
+
process_groups INTEGER NOT NULL,
|
|
3782
|
+
first_observed_at TEXT NOT NULL,
|
|
3783
|
+
last_observed_at TEXT NOT NULL,
|
|
3784
|
+
last_exited_at TEXT NOT NULL,
|
|
3785
|
+
settled_at TEXT NOT NULL
|
|
3786
|
+
);
|
|
3782
3787
|
|
|
3783
3788
|
CREATE INDEX IF NOT EXISTS task_by_state ON task (state);
|
|
3784
3789
|
CREATE INDEX IF NOT EXISTS edge_by_blocker ON task_edge (blocker);
|
|
@@ -4071,6 +4076,12 @@ function initializeStore(db, file) {
|
|
|
4071
4076
|
if (!tableExists(db, table))
|
|
4072
4077
|
throw new Error(`${file}: Discord history is missing; refusing to recreate receipts`);
|
|
4073
4078
|
}
|
|
4079
|
+
// A current file is this build's exact shape: the checks above read, the connection gets its settings, and no DDL
|
|
4080
|
+
// runs. Everything below runs once, for a fresh file or on the way up from an older version.
|
|
4081
|
+
if (preflight === SCHEMA_VERSION) {
|
|
4082
|
+
db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
|
|
4083
|
+
return new Store(db);
|
|
4084
|
+
}
|
|
4074
4085
|
if (preflight !== null && preflight > 0 && preflight < SCHEMA_VERSION) {
|
|
4075
4086
|
const stamped = db.prepare("UPDATE schema_version SET version = ? WHERE version = ?").run(-preflight, preflight);
|
|
4076
4087
|
if (Number(stamped.changes) !== 1) {
|
|
@@ -4281,6 +4292,11 @@ CREATE UNIQUE INDEX IF NOT EXISTS one_blocked_revision_per_task
|
|
|
4281
4292
|
ON plan_revision (task_ref) WHERE status = 'blocked';
|
|
4282
4293
|
CREATE INDEX IF NOT EXISTS run_checkpoint_by_run ON run_checkpoint (run, id);
|
|
4283
4294
|
CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, id);`);
|
|
4295
|
+
if (preflight !== null) {
|
|
4296
|
+
migrateToV114(db);
|
|
4297
|
+
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed.
|
|
4298
|
+
reclaimDatabase(db, "migration");
|
|
4299
|
+
}
|
|
4284
4300
|
// THE BOOKKEEPING WRITE, checked (raw authority repair): migrate() has
|
|
4285
4301
|
// already done the work by the time this runs; the row is bookkeeping
|
|
4286
4302
|
// about it, not the trigger for it — and it is written ONLY from the
|
|
@@ -4508,10 +4524,6 @@ function migrate(db, origin) {
|
|
|
4508
4524
|
addColumn(db, "task_ref", "repo", "TEXT");
|
|
4509
4525
|
addColumn(db, "claim", "released_by", "TEXT");
|
|
4510
4526
|
addColumn(db, "notification", "resolved_at", "TEXT");
|
|
4511
|
-
// Delivery claiming: two deliverers (the bridge, `outbox deliver`) must
|
|
4512
|
-
// not both send one row. A claim is a short lease on the act of sending.
|
|
4513
|
-
addColumn(db, "notification", "claim_owner", "TEXT");
|
|
4514
|
-
addColumn(db, "notification", "claim_expires_at", "TEXT");
|
|
4515
4527
|
addColumn(db, "approver", "generation", "INTEGER NOT NULL DEFAULT 1");
|
|
4516
4528
|
// v77: a lead thread belongs to the lead conversation, a project or a task.
|
|
4517
4529
|
addColumn(db, "mate_thread", "scope_kind", "TEXT NOT NULL DEFAULT 'lead' CHECK (scope_kind IN ('lead','project','task'))");
|
|
@@ -5471,6 +5483,93 @@ function reviewRequestOriginOf(row) {
|
|
|
5471
5483
|
* cannot prove, and the upgrade refuses in words rather than binding a
|
|
5472
5484
|
* retry allowance to it.
|
|
5473
5485
|
*/
|
|
5486
|
+
/**
|
|
5487
|
+
* v114, once, on the way up (the sentinel is already stamped): the workspace
|
|
5488
|
+
* revision triggers go (the write wrapper bumps the revision now), notification
|
|
5489
|
+
* is rebuilt without its delivery columns once their values are legacy receipts, and every settled
|
|
5490
|
+
* run's exited witnesses become its process summary. Each step is idempotent,
|
|
5491
|
+
* so a resumed upgrade finishes it.
|
|
5492
|
+
*/
|
|
5493
|
+
function migrateToV114(db) {
|
|
5494
|
+
for (const row of db.prepare("SELECT name FROM sqlite_master WHERE type = 'trigger' AND name LIKE 'workspace_revision_v1_%'").all()) {
|
|
5495
|
+
db.exec(`DROP TRIGGER IF EXISTS "${String(row["name"])}"`);
|
|
5496
|
+
}
|
|
5497
|
+
const columns = db.prepare("PRAGMA table_info(notification)").all().map(row => String(row["name"]));
|
|
5498
|
+
if (columns.some(column => NOTIFICATION_DROPPED.includes(column))) {
|
|
5499
|
+
// Recognized by its columns, not its DDL text: older files reached this shape by different addColumn orders.
|
|
5500
|
+
const known = new Set([...NOTIFICATION_COLUMNS, ...NOTIFICATION_DROPPED]);
|
|
5501
|
+
const unknown = columns.filter(column => !known.has(column));
|
|
5502
|
+
if (unknown.length > 0 || NOTIFICATION_COLUMNS.some(column => !columns.includes(column))) {
|
|
5503
|
+
throw new Error(`the notification table's columns are not a shape this migration knows${unknown.length > 0 ? ` (${unknown.join(", ")})` : ""} — refusing to rebuild it`);
|
|
5504
|
+
}
|
|
5505
|
+
db.exec("PRAGMA foreign_keys = OFF");
|
|
5506
|
+
try {
|
|
5507
|
+
db.exec("BEGIN IMMEDIATE");
|
|
5508
|
+
try {
|
|
5509
|
+
// The AUTOINCREMENT bookkeeping comes back as it was (rebuildExact's rule): the counter, never the surviving
|
|
5510
|
+
// max id, in the original row order, and no row the predecessor never wrote.
|
|
5511
|
+
const sequence = db.prepare("SELECT name, seq FROM sqlite_sequence ORDER BY rowid").all();
|
|
5512
|
+
carryLegacyDelivery(db, columns);
|
|
5513
|
+
db.exec(NOTIFICATION_DDL("notification_next"));
|
|
5514
|
+
const names = NOTIFICATION_COLUMNS.join(", ");
|
|
5515
|
+
db.exec(`INSERT INTO notification_next (${names}) SELECT ${names} FROM notification ORDER BY id`);
|
|
5516
|
+
db.exec("DROP TABLE notification");
|
|
5517
|
+
db.exec("ALTER TABLE notification_next RENAME TO notification");
|
|
5518
|
+
db.exec("DELETE FROM sqlite_sequence");
|
|
5519
|
+
const restore = db.prepare("INSERT INTO sqlite_sequence (name, seq) VALUES (?, ?)");
|
|
5520
|
+
for (const row of sequence)
|
|
5521
|
+
restore.run(row.name, typeof row.seq === "bigint" ? row.seq : BigInt(row.seq));
|
|
5522
|
+
if (db.prepare("PRAGMA foreign_key_check").all().length > 0)
|
|
5523
|
+
throw new Error("foreign keys did not survive the notification rebuild");
|
|
5524
|
+
db.exec("COMMIT");
|
|
5525
|
+
}
|
|
5526
|
+
catch (error) {
|
|
5527
|
+
db.exec("ROLLBACK");
|
|
5528
|
+
throw error;
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
finally {
|
|
5532
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
if (db.prepare("SELECT 1 FROM run_process WHERE exited_at IS NOT NULL LIMIT 1").get() === undefined)
|
|
5536
|
+
return;
|
|
5537
|
+
db.exec("BEGIN IMMEDIATE");
|
|
5538
|
+
try {
|
|
5539
|
+
compactSettledRunProcesses(db, new Date());
|
|
5540
|
+
db.exec("COMMIT");
|
|
5541
|
+
}
|
|
5542
|
+
catch (error) {
|
|
5543
|
+
db.exec("ROLLBACK");
|
|
5544
|
+
throw error;
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
/**
|
|
5548
|
+
* Settled custody, compacted (v114): a finished run whose every process
|
|
5549
|
+
* witness has exited keeps one run_process_summary row instead of them. A run
|
|
5550
|
+
* with any open witness keeps every row, so no custody fence ever reads less
|
|
5551
|
+
* than it did. Idempotent; the caller owns the transaction. Returns the
|
|
5552
|
+
* witnesses removed.
|
|
5553
|
+
*/
|
|
5554
|
+
function compactSettledRunProcesses(db, now, only = null) {
|
|
5555
|
+
const runs = db.prepare(`SELECT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
5556
|
+
WHERE r.outcome IS NOT NULL AND r.finished_at IS NOT NULL AND (? IS NULL OR p.run = ?)
|
|
5557
|
+
GROUP BY p.run HAVING COUNT(*) = COUNT(p.exited_at)`).all(only, only).map(row => Number(row["run"]));
|
|
5558
|
+
if (runs.length === 0)
|
|
5559
|
+
return 0;
|
|
5560
|
+
const summarize = db.prepare(`INSERT INTO run_process_summary (run, witnesses, process_groups, first_observed_at, last_observed_at, last_exited_at, settled_at)
|
|
5561
|
+
SELECT run, COUNT(*), SUM(process_group), MIN(observed_at), MAX(observed_at), MAX(exited_at), ? FROM run_process WHERE run = ? GROUP BY run
|
|
5562
|
+
ON CONFLICT (run) DO UPDATE SET witnesses = witnesses + excluded.witnesses, process_groups = process_groups + excluded.process_groups,
|
|
5563
|
+
first_observed_at = MIN(first_observed_at, excluded.first_observed_at), last_observed_at = MAX(last_observed_at, excluded.last_observed_at),
|
|
5564
|
+
last_exited_at = MAX(last_exited_at, excluded.last_exited_at), settled_at = excluded.settled_at`);
|
|
5565
|
+
const remove = db.prepare("DELETE FROM run_process WHERE run = ? AND exited_at IS NOT NULL");
|
|
5566
|
+
let removed = 0;
|
|
5567
|
+
for (const run of runs) {
|
|
5568
|
+
summarize.run(now.toISOString(), run);
|
|
5569
|
+
removed += Number(remove.run(run).changes);
|
|
5570
|
+
}
|
|
5571
|
+
return removed;
|
|
5572
|
+
}
|
|
5474
5573
|
function migrateToV50(db) {
|
|
5475
5574
|
if (!tableExists(db, "run") || !tableExists(db, "review_request"))
|
|
5476
5575
|
return;
|
|
@@ -5570,6 +5669,60 @@ function V51_ARTIFACT_DDL(name) {
|
|
|
5570
5669
|
function V38_INCIDENT_DDL(name) {
|
|
5571
5670
|
return V34_INCIDENT_DDL(name).replace("'plan-attempts-exhausted','malformed-report'", "'plan-attempts-exhausted','malformed-report','malformed-proof'");
|
|
5572
5671
|
}
|
|
5672
|
+
/** The durable outbox (§6). A notification is a fact that something wants a
|
|
5673
|
+
* person, recorded next to the record that made it true. dedupe_key is an
|
|
5674
|
+
* episode identity: a gap nags once per occurrence. Where it went is
|
|
5675
|
+
* notification_delivery's, per destination (v114 moved the old
|
|
5676
|
+
* single-destination attempt, receipt and claim columns into legacy receipts). */
|
|
5677
|
+
function NOTIFICATION_DDL(name) {
|
|
5678
|
+
return `CREATE TABLE IF NOT EXISTS ${name} (
|
|
5679
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5680
|
+
dedupe_key TEXT NOT NULL UNIQUE,
|
|
5681
|
+
kind TEXT NOT NULL,
|
|
5682
|
+
subject TEXT NOT NULL,
|
|
5683
|
+
body TEXT NOT NULL,
|
|
5684
|
+
created_at TEXT NOT NULL,
|
|
5685
|
+
-- The push surface (arc 3, v23): a CLOSED attention class and a
|
|
5686
|
+
-- machine-minted console link, stamped by producers at enqueue.
|
|
5687
|
+
push_class TEXT CHECK (push_class IN ('decision','pick','merge','attention')),
|
|
5688
|
+
link TEXT,
|
|
5689
|
+
resolved_at TEXT,
|
|
5690
|
+
provenance_scope TEXT NOT NULL DEFAULT 'unknown' CHECK (provenance_scope IN ('unknown','installation','project','task')),
|
|
5691
|
+
project TEXT,
|
|
5692
|
+
task_ref INTEGER REFERENCES task_ref(id),
|
|
5693
|
+
task_id TEXT,
|
|
5694
|
+
source_run INTEGER REFERENCES run(id),
|
|
5695
|
+
recipient TEXT
|
|
5696
|
+
)`;
|
|
5697
|
+
}
|
|
5698
|
+
const NOTIFICATION_COLUMNS = ["id", "dedupe_key", "kind", "subject", "body", "created_at", "push_class", "link", "resolved_at",
|
|
5699
|
+
"provenance_scope", "project", "task_ref", "task_id", "source_run", "recipient"];
|
|
5700
|
+
/** What v114 drops from notification: the removed single-destination delivery's columns. Their values move first. */
|
|
5701
|
+
const NOTIFICATION_DROPPED = ["attempts", "last_attempt_at", "last_error", "delivered_at", "receipt", "claim_owner", "claim_expires_at"];
|
|
5702
|
+
/** The receipt v114 keeps a notification's old single-destination delivery under. No sender claims it: every real
|
|
5703
|
+
* destination is a kind and a 64-character hash, or a Telegram binding. */
|
|
5704
|
+
export const LEGACY_NOTIFICATION_DESTINATION = "legacy:single-destination";
|
|
5705
|
+
/**
|
|
5706
|
+
* v114, inside the notification rebuild's transaction: every row with any saved delivery value (attempts is never
|
|
5707
|
+
* null, so every row the old schema wrote) gets one legacy receipt holding exactly those values, then the copy is
|
|
5708
|
+
* proved field for field. A reserved receipt that differs, or one with nothing to carry, refuses the upgrade.
|
|
5709
|
+
*/
|
|
5710
|
+
function carryLegacyDelivery(db, columns) {
|
|
5711
|
+
const present = NOTIFICATION_DROPPED.filter(column => columns.includes(column));
|
|
5712
|
+
if (present.length === 0)
|
|
5713
|
+
return;
|
|
5714
|
+
const value = (column) => present.includes(column) ? `n.${column}` : column === "attempts" ? "0" : "NULL";
|
|
5715
|
+
const populated = present.map(column => `n.${column} IS NOT NULL`).join(" OR ");
|
|
5716
|
+
db.prepare(`INSERT OR IGNORE INTO notification_delivery (notification, destination, ${NOTIFICATION_DROPPED.join(", ")})
|
|
5717
|
+
SELECT n.id, ?, ${NOTIFICATION_DROPPED.map(value).join(", ")} FROM notification n WHERE ${populated} ORDER BY n.id`).run(LEGACY_NOTIFICATION_DESTINATION);
|
|
5718
|
+
const expected = Number(db.prepare(`SELECT COUNT(*) AS n FROM notification n WHERE ${populated}`).get()?.["n"]);
|
|
5719
|
+
const matched = Number(db.prepare(`SELECT COUNT(*) AS n FROM notification n JOIN notification_delivery d ON d.notification = n.id AND d.destination = ?
|
|
5720
|
+
WHERE (${populated}) AND ${NOTIFICATION_DROPPED.map(column => `d.${column} IS ${value(column)}`).join(" AND ")}`).get(LEGACY_NOTIFICATION_DESTINATION)?.["n"]);
|
|
5721
|
+
const reserved = Number(db.prepare("SELECT COUNT(*) AS n FROM notification_delivery WHERE destination = ?").get(LEGACY_NOTIFICATION_DESTINATION)?.["n"]);
|
|
5722
|
+
if (matched !== expected || reserved !== expected) {
|
|
5723
|
+
throw new Error(`notification delivery history did not copy whole (${matched} of ${expected} rows match, ${reserved} legacy receipts) — refusing to drop its columns`);
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5573
5726
|
/**
|
|
5574
5727
|
* One exact copy-rename (v4 review, finding 7 — substrings are not
|
|
5575
5728
|
* recognizers): the stored DDL must equal the known old shape or the
|
|
@@ -7358,7 +7511,8 @@ export class Store {
|
|
|
7358
7511
|
db;
|
|
7359
7512
|
constructor(db) {
|
|
7360
7513
|
this.telemetry = new ServerTelemetry();
|
|
7361
|
-
|
|
7514
|
+
// Outside in: telemetry, then the workspace revision (one bump per committed meaningful write), then SQLite.
|
|
7515
|
+
this.db = instrumentDatabase(trackWorkspaceWrites(db), this.telemetry);
|
|
7362
7516
|
}
|
|
7363
7517
|
/** TESTS ONLY: raw database access for migration fixtures. Production
|
|
7364
7518
|
* code never calls this — the typed methods are the API. */
|
|
@@ -9621,11 +9775,28 @@ export class Store {
|
|
|
9621
9775
|
return report;
|
|
9622
9776
|
}
|
|
9623
9777
|
/** A checkpoint of the chain as it stands: its head, recorded here and handed back to be copied off the machine.
|
|
9624
|
-
* Never over a chain that doesn't verify: that would vouch for what broke it.
|
|
9625
|
-
|
|
9778
|
+
* Never over a chain that doesn't verify: that would vouch for what broke it. `automatic` (before a backup, after
|
|
9779
|
+
* a monitoring delivery): at the verified head, or at `head` (one that was sent) once the walk reached it, only past
|
|
9780
|
+
* the newest checkpoint (null otherwise), and with no ledger entry of its own, so a checkpoint never makes new
|
|
9781
|
+
* activity for the next backup or delivery to checkpoint again. */
|
|
9782
|
+
ledgerCheckpoint(by, now, automatic) {
|
|
9626
9783
|
const report = this.ledgerChain({ full: true });
|
|
9627
9784
|
if (!report.ok)
|
|
9628
9785
|
return { problem: report.problem?.what ?? "the chain doesn't verify" };
|
|
9786
|
+
if (automatic !== undefined) {
|
|
9787
|
+
const head = automatic.head ?? (report.through === null ? null : { through: report.through, hash: report.head });
|
|
9788
|
+
if (head === null || report.through === null || head.through > report.through)
|
|
9789
|
+
return null;
|
|
9790
|
+
return this.transact(() => {
|
|
9791
|
+
if (this.db.prepare("SELECT hash FROM ledger_seal WHERE id = ?").get(head.through)?.["hash"] !== head.hash)
|
|
9792
|
+
return { problem: `entry #${head.through} isn't sealed as ${head.hash.slice(0, 12)}` };
|
|
9793
|
+
const newest = this.db.prepare(`SELECT MAX(through) AS through FROM ledger_checkpoint WHERE typeof(through) = 'integer' AND through ${IN_RANGE}`).get()?.["through"];
|
|
9794
|
+
if (newest != null && Number(newest) >= head.through)
|
|
9795
|
+
return null;
|
|
9796
|
+
this.db.prepare("INSERT INTO ledger_checkpoint (through, hash, at, by) VALUES (?, ?, ?, ?)").run(head.through, head.hash, now.toISOString(), by);
|
|
9797
|
+
return head;
|
|
9798
|
+
});
|
|
9799
|
+
}
|
|
9629
9800
|
return this.transact(() => {
|
|
9630
9801
|
sealLedger(this.db);
|
|
9631
9802
|
const last = this.db.prepare(`SELECT id, hash FROM ledger_seal WHERE id ${IN_RANGE} ORDER BY id DESC LIMIT 1`).get();
|
|
@@ -14073,6 +14244,11 @@ export class Store {
|
|
|
14073
14244
|
recordProviderReadiness(runner, observations, now) {
|
|
14074
14245
|
this.transact(() => {
|
|
14075
14246
|
for (const one of observations) {
|
|
14247
|
+
// The same observation again only renews its time: a quiet write that moves no workspace revision.
|
|
14248
|
+
const renewed = this.db.prepare("UPDATE provider_readiness SET observed_at = ? WHERE runner = ? AND provider = ? AND state = ? AND reason = ? AND probe IS ?")
|
|
14249
|
+
.run(now.toISOString(), runner, one.provider, one.state, one.reason, one.probe);
|
|
14250
|
+
if (Number(renewed.changes) > 0)
|
|
14251
|
+
continue;
|
|
14076
14252
|
this.db
|
|
14077
14253
|
.prepare(`INSERT INTO provider_readiness (runner, provider, state, reason, probe, observed_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
14078
14254
|
ON CONFLICT (runner, provider) DO UPDATE SET state = excluded.state, reason = excluded.reason,
|
|
@@ -15766,37 +15942,28 @@ export class Store {
|
|
|
15766
15942
|
return this.db.prepare("SELECT * FROM notification WHERE id>? ORDER BY id LIMIT ?").all(id, Math.max(1, Math.min(100, limit))).map(readNotification);
|
|
15767
15943
|
}
|
|
15768
15944
|
listNotifications(only = "pending") {
|
|
15769
|
-
//
|
|
15770
|
-
//
|
|
15771
|
-
//
|
|
15772
|
-
const where = only === "pending" ? "WHERE
|
|
15945
|
+
// Pending means unresolved: a decision answered is a fact that stopped
|
|
15946
|
+
// wanting a person. Delivery state lives per destination
|
|
15947
|
+
// (notification_delivery), not on the fact.
|
|
15948
|
+
const where = only === "pending" ? "WHERE resolved_at IS NULL" : "";
|
|
15773
15949
|
return this.db
|
|
15774
15950
|
.prepare(`SELECT * FROM notification ${where} ORDER BY id`)
|
|
15775
15951
|
.all()
|
|
15776
15952
|
.map(readNotification);
|
|
15777
15953
|
}
|
|
15778
|
-
recordDelivery(id, outcome, now) {
|
|
15779
|
-
if (outcome.ok) {
|
|
15780
|
-
this.db
|
|
15781
|
-
.prepare(`UPDATE notification SET delivered_at = ?, receipt = ?, attempts = attempts + 1, last_attempt_at = ?, last_error = NULL
|
|
15782
|
-
WHERE id = ?`)
|
|
15783
|
-
.run(now.toISOString(), outcome.receipt, now.toISOString(), id);
|
|
15784
|
-
return;
|
|
15785
|
-
}
|
|
15786
|
-
this.db
|
|
15787
|
-
.prepare(`UPDATE notification SET attempts = attempts + 1, last_attempt_at = ?, last_error = ?
|
|
15788
|
-
WHERE id = ?`)
|
|
15789
|
-
.run(now.toISOString(), outcome.error, id);
|
|
15790
|
-
}
|
|
15791
15954
|
/**
|
|
15792
15955
|
* Close a gap's notification episode. Called when its capability verifies:
|
|
15793
15956
|
* the next failure is a *new* fact, and a dedupe key that suppressed it
|
|
15794
15957
|
* forever would be a gap the operator hears about exactly once per lifetime.
|
|
15795
15958
|
*/
|
|
15796
15959
|
clearGapEpisode(repo, kind, name) {
|
|
15797
|
-
|
|
15798
|
-
|
|
15799
|
-
.
|
|
15960
|
+
const key = `gap:${repo}:${kind}:${name}`;
|
|
15961
|
+
this.transact(() => {
|
|
15962
|
+
// Its v114 legacy receipt goes with it, as the old columns went with the row.
|
|
15963
|
+
this.db.prepare("DELETE FROM notification_delivery WHERE destination = ? AND notification IN (SELECT id FROM notification WHERE dedupe_key = ?)")
|
|
15964
|
+
.run(LEGACY_NOTIFICATION_DESTINATION, key);
|
|
15965
|
+
this.db.prepare("DELETE FROM notification WHERE dedupe_key = ?").run(key);
|
|
15966
|
+
});
|
|
15800
15967
|
}
|
|
15801
15968
|
/**
|
|
15802
15969
|
* The other way an episode ends: the fact stopped wanting a person. Unlike
|
|
@@ -21814,8 +21981,10 @@ export class Store {
|
|
|
21814
21981
|
if (this.stopQuiescenceProblem(runId) !== null)
|
|
21815
21982
|
return false;
|
|
21816
21983
|
const recordExit = this.db.prepare("UPDATE run_process SET exited_at = ? WHERE run = ? AND exited_at IS NULL");
|
|
21817
|
-
for (const owned of this.ownedRunsOf(runId))
|
|
21984
|
+
for (const owned of this.ownedRunsOf(runId)) {
|
|
21818
21985
|
recordExit.run(now.toISOString(), owned);
|
|
21986
|
+
compactSettledRunProcesses(this.db, now, owned);
|
|
21987
|
+
}
|
|
21819
21988
|
const { changes } = this.db
|
|
21820
21989
|
.prepare("UPDATE run_stop SET settled_at = ?, settlement = ? WHERE run = ? AND settled_at IS NULL")
|
|
21821
21990
|
.run(now.toISOString(), settlement, runId);
|
|
@@ -21851,7 +22020,7 @@ export class Store {
|
|
|
21851
22020
|
return fact(id, "alive", `run #${id}'s held supervisor has not finished shutdown`);
|
|
21852
22021
|
const witnesses = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(id);
|
|
21853
22022
|
let incompleteSpawn = false;
|
|
21854
|
-
if (run.providerStartedAt !== null && witnesses.length === 0) {
|
|
22023
|
+
if (run.providerStartedAt !== null && witnesses.length === 0 && !this.processesSummarized(id)) {
|
|
21855
22024
|
return fact(id, "unprovable", `run #${id} may have spawned before its process witness was recorded; exit is unproven`);
|
|
21856
22025
|
}
|
|
21857
22026
|
for (const witness of witnesses) {
|
|
@@ -21972,14 +22141,27 @@ export class Store {
|
|
|
21972
22141
|
for (const run of runs)
|
|
21973
22142
|
recorded += this.recordRunProcessExits(run, now);
|
|
21974
22143
|
if (runs.length < batch)
|
|
21975
|
-
|
|
22144
|
+
break;
|
|
21976
22145
|
after = runs[runs.length - 1];
|
|
21977
22146
|
}
|
|
22147
|
+
// A run whose witnesses all exited before it finished settles here, on the same pass.
|
|
22148
|
+
this.compactRunProcesses(now);
|
|
22149
|
+
return recorded;
|
|
21978
22150
|
}
|
|
21979
22151
|
finishUnspawnedProcess(witness, now) {
|
|
21980
22152
|
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND pid IS NULL AND ((containment IS NULL AND container IS NULL) OR container_empty_at IS NOT NULL)")
|
|
21981
22153
|
.run(now.toISOString(), witness);
|
|
21982
22154
|
}
|
|
22155
|
+
/** Settled custody, compacted (v114; compactSettledRunProcesses): a finished run whose witnesses all exited keeps
|
|
22156
|
+
* one summary row. A settled stop, an approver's settlement and a recovery compact their run at once; the worker's
|
|
22157
|
+
* reconcile pass (recordFinishedRunExits) compacts every other settled run, including one that settled at run end. */
|
|
22158
|
+
compactRunProcesses(now, only) {
|
|
22159
|
+
return this.transact(() => compactSettledRunProcesses(this.db, now, only ?? null));
|
|
22160
|
+
}
|
|
22161
|
+
/** Whether a run's witnesses were compacted into its summary: they existed, and all exited. */
|
|
22162
|
+
processesSummarized(runId) {
|
|
22163
|
+
return this.db.prepare("SELECT 1 AS hit FROM run_process_summary WHERE run = ?").get(runId) !== undefined;
|
|
22164
|
+
}
|
|
21983
22165
|
/** The reconcile road for a pid-less witness a crash or a thrown spawn
|
|
21984
22166
|
* left behind: once its run has finished, no live child is tracked for it
|
|
21985
22167
|
* here, every other witness of the run has exited and none of the run's
|
|
@@ -22095,7 +22277,7 @@ export class Store {
|
|
|
22095
22277
|
}
|
|
22096
22278
|
// A run that may have spawned before any witness was written has none
|
|
22097
22279
|
// to settle: the approver's word is recorded as one ended witness.
|
|
22098
|
-
if (rows.length === 0 && run.providerStartedAt !== null) {
|
|
22280
|
+
if (rows.length === 0 && run.providerStartedAt !== null && !this.processesSummarized(args.runId)) {
|
|
22099
22281
|
const id = Number(this.db.prepare("INSERT INTO run_process (run,host,process_group,observed_at,boot_id,exited_at) VALUES (?,?,0,?,?,?)")
|
|
22100
22282
|
.run(args.runId, hostname(), now.toISOString(), currentBootId(), now.toISOString()).lastInsertRowid);
|
|
22101
22283
|
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
@@ -22113,6 +22295,7 @@ export class Store {
|
|
|
22113
22295
|
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND run = ? AND exited_at IS NULL").run(now.toISOString(), id, args.runId);
|
|
22114
22296
|
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
22115
22297
|
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
22298
|
+
compactSettledRunProcesses(this.db, now, args.runId);
|
|
22116
22299
|
this.settleQuiescentStops(now);
|
|
22117
22300
|
return { ok: true, witnesses: ids, repeated: false };
|
|
22118
22301
|
});
|
|
@@ -22148,7 +22331,8 @@ export class Store {
|
|
|
22148
22331
|
* must independently prove gone; this never grants authority to signal. */
|
|
22149
22332
|
runCustodyProvenGone(runId) {
|
|
22150
22333
|
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ?").all(runId);
|
|
22151
|
-
|
|
22334
|
+
// A summarized run's witnesses all exited, each on recorded proof.
|
|
22335
|
+
return (rows.length > 0 || this.processesSummarized(runId)) && rows.every(row => {
|
|
22152
22336
|
if (String(row["host"]) !== hostname())
|
|
22153
22337
|
return false;
|
|
22154
22338
|
if (provenDeadByBootChange({ host: String(row["host"]), bootId: row["boot_id"] == null ? null : String(row["boot_id"]) }))
|
|
@@ -22853,63 +23037,11 @@ export class Store {
|
|
|
22853
23037
|
return true;
|
|
22854
23038
|
});
|
|
22855
23039
|
}
|
|
22856
|
-
/**
|
|
23040
|
+
/** The notifications this pairing holds, each with its delivery state there. */
|
|
22857
23041
|
telegramDeliveries(binding) {
|
|
22858
23042
|
return this.db.prepare(`SELECT n.*, d.attempts, d.last_attempt_at, d.last_error, d.delivered_at, d.receipt
|
|
22859
23043
|
FROM notification n JOIN notification_delivery d ON d.notification = n.id WHERE d.destination = ? ORDER BY n.id`)
|
|
22860
|
-
.all(this.telegramDestination(binding)).map(
|
|
22861
|
-
}
|
|
22862
|
-
/**
|
|
22863
|
-
* Claim pending notifications for one deliverer. Two deliverers — the
|
|
22864
|
-
* bridge and `outbox deliver` — select-then-send-then-record, and without
|
|
22865
|
-
* a claim both can send the same row in the gap. The claim is a short
|
|
22866
|
-
* lease on the act of sending; a deliverer that dies mid-send leaves rows
|
|
22867
|
-
* that unclaim themselves by expiry.
|
|
22868
|
-
*/
|
|
22869
|
-
claimDeliveries(owner, ttlMs, now, only = "all") {
|
|
22870
|
-
return this.transact(() => {
|
|
22871
|
-
const stamp = now.toISOString();
|
|
22872
|
-
// `urgent` (v34, digests): only what must page NOW — a decision, or
|
|
22873
|
-
// an attention-class fact. Routine rows stay unclaimed and pending
|
|
22874
|
-
// until a pass asks for `all`; nothing about them is written here.
|
|
22875
|
-
const rows = this.db
|
|
22876
|
-
.prepare(`SELECT id FROM notification
|
|
22877
|
-
WHERE delivered_at IS NULL AND resolved_at IS NULL AND recipient IS NULL
|
|
22878
|
-
AND (claim_owner IS NULL OR claim_expires_at <= ?)
|
|
22879
|
-
AND (? = 'all' OR dedupe_key LIKE 'decision:%' OR COALESCE(push_class, '') = 'attention')
|
|
22880
|
-
ORDER BY id`)
|
|
22881
|
-
.all(stamp, only)
|
|
22882
|
-
.map(row => Number(row["id"]));
|
|
22883
|
-
const expires = new Date(now.getTime() + ttlMs).toISOString();
|
|
22884
|
-
for (const id of rows) {
|
|
22885
|
-
this.db
|
|
22886
|
-
.prepare("UPDATE notification SET claim_owner = ?, claim_expires_at = ? WHERE id = ?")
|
|
22887
|
-
.run(owner, expires, id);
|
|
22888
|
-
}
|
|
22889
|
-
return rows
|
|
22890
|
-
.map(id => this.db.prepare("SELECT * FROM notification WHERE id = ?").get(id))
|
|
22891
|
-
.filter((row) => row !== undefined)
|
|
22892
|
-
.map(readNotification);
|
|
22893
|
-
});
|
|
22894
|
-
}
|
|
22895
|
-
/** Finalize only what this owner still holds. A lapsed claim writes nothing. */
|
|
22896
|
-
finalizeDelivery(id, owner, outcome, now) {
|
|
22897
|
-
const stamp = now.toISOString();
|
|
22898
|
-
if (outcome.ok) {
|
|
22899
|
-
const { changes } = this.db
|
|
22900
|
-
.prepare(`UPDATE notification SET delivered_at = ?, receipt = ?, attempts = attempts + 1,
|
|
22901
|
-
last_attempt_at = ?, last_error = NULL,
|
|
22902
|
-
claim_owner = NULL, claim_expires_at = NULL
|
|
22903
|
-
WHERE id = ? AND claim_owner = ? AND delivered_at IS NULL`)
|
|
22904
|
-
.run(stamp, outcome.receipt, stamp, id, owner);
|
|
22905
|
-
return Number(changes) > 0;
|
|
22906
|
-
}
|
|
22907
|
-
const { changes } = this.db
|
|
22908
|
-
.prepare(`UPDATE notification SET attempts = attempts + 1, last_attempt_at = ?, last_error = ?,
|
|
22909
|
-
claim_owner = NULL, claim_expires_at = NULL
|
|
22910
|
-
WHERE id = ? AND claim_owner = ?`)
|
|
22911
|
-
.run(stamp, outcome.error, id, owner);
|
|
22912
|
-
return Number(changes) > 0;
|
|
23044
|
+
.all(this.telegramDestination(binding)).map(readNotificationReceipt);
|
|
22913
23045
|
}
|
|
22914
23046
|
// ---- the Telegram digest (v34, away mode) --------------------------------
|
|
22915
23047
|
/** The digest cadence: null everyMs = off. */
|
|
@@ -23283,21 +23415,6 @@ export class Store {
|
|
|
23283
23415
|
markTelegramDigestSent(now) {
|
|
23284
23416
|
this.db.prepare("UPDATE telegram_digest SET last_sent_at = ? WHERE id = 1").run(now.toISOString());
|
|
23285
23417
|
}
|
|
23286
|
-
/** Every batched row finalized delivered AND the anchor moved, in ONE
|
|
23287
|
-
* transaction (v4 review, finding 5): a digest is either recorded whole
|
|
23288
|
-
* or not at all. Rows whose claim lapsed meanwhile are reported, not
|
|
23289
|
-
* silently skipped. */
|
|
23290
|
-
finalizeDigest(ids, owner, receipt, now) {
|
|
23291
|
-
return this.transact(() => {
|
|
23292
|
-
let finalized = 0;
|
|
23293
|
-
for (const id of ids) {
|
|
23294
|
-
if (this.finalizeDelivery(id, owner, { ok: true, receipt }, now))
|
|
23295
|
-
finalized++;
|
|
23296
|
-
}
|
|
23297
|
-
this.markTelegramDigestSent(now);
|
|
23298
|
-
return { finalized, lapsed: ids.length - finalized };
|
|
23299
|
-
});
|
|
23300
|
-
}
|
|
23301
23418
|
/** Routine rows waiting for the next digest: pending, and not urgent.
|
|
23302
23419
|
* A row the live pairing delivered, or skipped as pre-pairing history,
|
|
23303
23420
|
* is settled and not waiting. */
|
|
@@ -23329,9 +23446,8 @@ export class Store {
|
|
|
23329
23446
|
*/
|
|
23330
23447
|
pendingForAttention() {
|
|
23331
23448
|
const holds = Object.values(TELEGRAM_HOLD_REASONS);
|
|
23332
|
-
//
|
|
23333
|
-
//
|
|
23334
|
-
// pending filter, and exclude resolved facts from both arms.
|
|
23449
|
+
// Select the troubled union before applying the global pending filter,
|
|
23450
|
+
// and exclude resolved facts from both arms.
|
|
23335
23451
|
return this.db
|
|
23336
23452
|
.prepare(`WITH troubled AS (
|
|
23337
23453
|
SELECT d.notification AS id FROM notification_delivery d
|
|
@@ -23344,7 +23460,7 @@ export class Store {
|
|
|
23344
23460
|
)
|
|
23345
23461
|
SELECT n.* FROM notification n
|
|
23346
23462
|
WHERE n.resolved_at IS NULL
|
|
23347
|
-
AND ((n.
|
|
23463
|
+
AND ((n.recipient IS NULL AND substr(n.dedupe_key, 1, ${LIFECYCLE_KEY_PREFIX.length}) <> '${LIFECYCLE_KEY_PREFIX}')
|
|
23348
23464
|
OR n.id IN (SELECT id FROM troubled))
|
|
23349
23465
|
ORDER BY n.id`)
|
|
23350
23466
|
.all(...holds)
|
|
@@ -23823,11 +23939,6 @@ function readNotification(row) {
|
|
|
23823
23939
|
subject: String(row["subject"]),
|
|
23824
23940
|
body: String(row["body"]),
|
|
23825
23941
|
createdAt: String(row["created_at"]),
|
|
23826
|
-
attempts: Number(row["attempts"]),
|
|
23827
|
-
lastAttemptAt: row["last_attempt_at"] === null ? null : String(row["last_attempt_at"]),
|
|
23828
|
-
lastError: row["last_error"] === null ? null : String(row["last_error"]),
|
|
23829
|
-
deliveredAt: row["delivered_at"] === null ? null : String(row["delivered_at"]),
|
|
23830
|
-
receipt: row["receipt"] === null ? null : String(row["receipt"]),
|
|
23831
23942
|
resolvedAt: row["resolved_at"] === null || row["resolved_at"] === undefined
|
|
23832
23943
|
? null
|
|
23833
23944
|
: String(row["resolved_at"]),
|
|
@@ -23839,6 +23950,16 @@ function readNotification(row) {
|
|
|
23839
23950
|
link: row["link"] === null || row["link"] === undefined ? null : String(row["link"]),
|
|
23840
23951
|
};
|
|
23841
23952
|
}
|
|
23953
|
+
function readNotificationReceipt(row) {
|
|
23954
|
+
return {
|
|
23955
|
+
...readNotification(row),
|
|
23956
|
+
attempts: Number(row["attempts"]),
|
|
23957
|
+
lastAttemptAt: row["last_attempt_at"] == null ? null : String(row["last_attempt_at"]),
|
|
23958
|
+
lastError: row["last_error"] == null ? null : String(row["last_error"]),
|
|
23959
|
+
deliveredAt: row["delivered_at"] == null ? null : String(row["delivered_at"]),
|
|
23960
|
+
receipt: row["receipt"] == null ? null : String(row["receipt"]),
|
|
23961
|
+
};
|
|
23962
|
+
}
|
|
23842
23963
|
function readPushSubscription(row) {
|
|
23843
23964
|
return {
|
|
23844
23965
|
id: Number(row["id"]),
|
package/dist/surface.js
CHANGED
|
@@ -416,7 +416,6 @@ const GUIDE_ROWS = [
|
|
|
416
416
|
operator("intake clear", "clear intake configuration"),
|
|
417
417
|
operator("contest exclude", "exclude a contestant from a tournament"),
|
|
418
418
|
operator("webhook primary", "choose which service receives alerts"),
|
|
419
|
-
operator("webhook test", "send a test message"),
|
|
420
419
|
operator("bridge", "the Telegram bridge (pairing, tokens, the follower)"),
|
|
421
420
|
operator("enroll", "grant a repository authority (backends, dispatch) — its own explicit yes"),
|
|
422
421
|
operator("revoke", "take an authority grant back"),
|
|
@@ -481,7 +480,7 @@ const REMOTE_YES = [
|
|
|
481
480
|
"runner list", "coordinator list", "cap list", "cap add", "outbox list", "incident list", "incident resolve",
|
|
482
481
|
"routine list", "routine show", "routine add", "routine refresh", "routine pause", "routine resume", "routine run-now",
|
|
483
482
|
"config show", "verify show", "intake show", "intake run", "intake preview", "intake pr-comments", "template list", "template show",
|
|
484
|
-
"contest show", "contest exclude", "webhook status", "
|
|
483
|
+
"contest show", "contest exclude", "webhook status", "review show", "chat-approval show", "chat-approval off",
|
|
485
484
|
"knowledge search", "knowledge impact", "knowledge refresh",
|
|
486
485
|
"memory search", "memory decisions", "memory show", "memory decide", "memory retire", "memory review", "memory status",
|
|
487
486
|
"flows list", "flows show", "flows export", "flows create", "flows edit", "flows trigger add", "flows trigger pause", "flows trigger resume",
|
package/dist/team-http.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import { type TeamActor, type TeamExecute } from './team-contract.js';
|
|
3
3
|
import { type Admission } from './request-budget.js';
|
|
4
4
|
export declare const TEAM_REQUEST_BYTES: number;
|
|
5
|
+
/** How long a password bearer's proof stands on a long /api/team/events stream before its password is checked again.
|
|
6
|
+
* The account, its revocation and its credential generation are still rechecked on every tick. */
|
|
7
|
+
export declare const TEAM_PASSWORD_REVERIFY_MS: number;
|
|
5
8
|
/** Account role belongs to HTTP authentication; API tokens carry their own scope. */
|
|
6
9
|
export type TeamHttpActor = TeamActor & {
|
|
7
10
|
role?: 'approver' | 'viewer';
|
package/dist/team-http.js
CHANGED
|
@@ -2,6 +2,9 @@ import { adapterPolicy } from "./server/route-policy.js";
|
|
|
2
2
|
import { TEAM_OPERATIONS, TEAM_MUTATIONS, teamScopeAllows } from './team-contract.js';
|
|
3
3
|
import { limitWords } from './request-budget.js';
|
|
4
4
|
export const TEAM_REQUEST_BYTES = 64 * 1024;
|
|
5
|
+
/** How long a password bearer's proof stands on a long /api/team/events stream before its password is checked again.
|
|
6
|
+
* The account, its revocation and its credential generation are still rechecked on every tick. */
|
|
7
|
+
export const TEAM_PASSWORD_REVERIFY_MS = 5 * 60_000;
|
|
5
8
|
const failure = (code, message) => ({ version: 1, ok: false, code, message });
|
|
6
9
|
function send(response, status, value, headers = {}) {
|
|
7
10
|
if (response.destroyed || response.writableEnded)
|