toolroll 0.9.60 → 0.9.62
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 +32 -0
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment.d.ts +4 -0
- package/dist/assignment.js +8 -6
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +113 -13
- package/dist/chat-channel.js +9 -0
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/cli.js +28 -11
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +14 -3
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +170 -0
- package/dist/landed-cleanup.js +814 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +4 -4
- package/dist/lead-doors.js +45 -1
- package/dist/lead-tools.js +40 -7
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -26
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-admission.d.ts +10 -6
- package/dist/project-admission.js +33 -11
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +32 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +65 -8
- package/dist/provider.js +80 -21
- package/dist/remote-main.d.ts +57 -0
- package/dist/remote-main.js +179 -0
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +19 -1
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +12 -0
- package/dist/server/render-tasks.d.ts +15 -2
- package/dist/server/render-tasks.js +30 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +5 -3
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-status.d.ts +8 -2
- package/dist/steer-status.js +19 -5
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +48 -0
- package/dist/storage-sweep-process.js +204 -0
- package/dist/storage-sweep-schedule.d.ts +30 -0
- package/dist/storage-sweep-schedule.js +164 -0
- package/dist/storage-sweep.d.ts +31 -6
- package/dist/storage-sweep.js +165 -39
- package/dist/storage-ui.d.ts +4 -0
- package/dist/storage-ui.js +22 -7
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +200 -3
- package/dist/store.js +443 -17
- package/dist/surface.js +8 -4
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-base.d.ts +35 -0
- package/dist/task-base.js +51 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +57 -3
- package/dist/worktree.js +201 -23
- package/package.json +1 -1
- package/scripts/provider-canary.mjs +235 -147
package/dist/store.js
CHANGED
|
@@ -11,6 +11,7 @@ import { trackWorkspaceWrites } from "./workspace-revision.js";
|
|
|
11
11
|
import { parseStoreColumn, readStoreColumn, readStoreStringifiedList, readStoreTextList } from "./contracts/store-json.js";
|
|
12
12
|
import { assessmentFromSavedEvidence, verificationEvidence } from "./verification-evidence.js";
|
|
13
13
|
import { LEARNING_SCHEMA, queueLearning } from "./project-learning.js";
|
|
14
|
+
import { AGENT_BOARD_SCHEMA, AGENT_NOW_COLUMNS, closeRunQuestions, mintAttemptKey, watchedPing } from "./agent-board.js";
|
|
14
15
|
import { SKILLS_SCHEMA } from "./project-skills.js";
|
|
15
16
|
import { KNOWLEDGE_SCHEMA } from "./project-knowledge.js";
|
|
16
17
|
import { MEMORY_SCHEMA } from "./project-memory.js";
|
|
@@ -50,7 +51,7 @@ import { hashSecret, mintLeadToken, parseCredential, secretMatches, tokenPurpose
|
|
|
50
51
|
* are deferred until after M4, and a column that exists to serve a feature
|
|
51
52
|
* nobody has designed yet is a column that will be wrong when they do.
|
|
52
53
|
*/
|
|
53
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
54
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
54
55
|
import { existsSync, mkdirSync, statSync, unlinkSync } from "node:fs";
|
|
55
56
|
import { createRequire } from "node:module";
|
|
56
57
|
import { dirname } from "node:path";
|
|
@@ -83,7 +84,7 @@ import { REQUEST_BUDGET_SCHEMA } from "./request-budget.js";
|
|
|
83
84
|
import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
|
|
84
85
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, subagentFilers, usd } from "./spend.js";
|
|
85
86
|
import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
|
|
86
|
-
import { CHECKOUT_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
87
|
+
import { CHECKOUT_CLEANUP_SCHEMA, LANDED_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
87
88
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
88
89
|
import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
|
|
89
90
|
import { configBase, envValue, namedPath } from "./names.js";
|
|
@@ -354,6 +355,59 @@ CREATE TABLE IF NOT EXISTS run_activity (
|
|
|
354
355
|
at TEXT NOT NULL
|
|
355
356
|
);
|
|
356
357
|
`;
|
|
358
|
+
/** A task branch moved to current main before an attempt (fresh base; v122, a new table: additive only): the branch
|
|
359
|
+
* had no commits of its own and no changes, so it started over from `to_rev`. `after_run` is the newest run id when it
|
|
360
|
+
* moved: the branch's whole-task diff runs from the first builder base recorded after it (firstBuilderBase), never from
|
|
361
|
+
* the older main it left. */
|
|
362
|
+
export const BRANCH_BASE_MOVE_SCHEMA = `
|
|
363
|
+
CREATE TABLE IF NOT EXISTS branch_base_move (
|
|
364
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
365
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
366
|
+
branch TEXT NOT NULL,
|
|
367
|
+
from_rev TEXT NOT NULL,
|
|
368
|
+
to_rev TEXT NOT NULL,
|
|
369
|
+
after_run INTEGER NOT NULL,
|
|
370
|
+
moved_at TEXT NOT NULL
|
|
371
|
+
);
|
|
372
|
+
CREATE INDEX IF NOT EXISTS branch_base_move_branch ON branch_base_move (task_ref, branch, id);
|
|
373
|
+
`;
|
|
374
|
+
/** Pausing a running Claude build (live steering, v121: additive only). A person's Pause asks the build to
|
|
375
|
+
* stop its current turn and keep its session: one row per request, bound to the exact run it was asked of, under the
|
|
376
|
+
* request id the asker chose (a repeated request is the same request). `note` is the steering note a Pause and steer
|
|
377
|
+
* sends right after the interrupt. The builder's dispatcher stamps sent_at when the interrupt is written, paused_at when
|
|
378
|
+
* the turn stopped, resumed_at when a later note continued the session, and ended_at when the run stopped taking input
|
|
379
|
+
* first. Never a stop: nothing here ends, holds or reclaims an attempt (run_stop does that). A person's Resume (run_resume)
|
|
380
|
+
* continues a paused build with a short continue frame and no note: one row per request, bound to the exact paused run;
|
|
381
|
+
* sent_at when the frame was written, ended_at when the run was no longer paused to take it. */
|
|
382
|
+
export const LIVE_STEER_SCHEMA = `
|
|
383
|
+
CREATE TABLE IF NOT EXISTS run_pause (
|
|
384
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
385
|
+
run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,
|
|
386
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
387
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
388
|
+
requested_by TEXT NOT NULL,
|
|
389
|
+
requested_via TEXT NOT NULL CHECK (requested_via IN ('cli','web','chat')),
|
|
390
|
+
requested_at TEXT NOT NULL,
|
|
391
|
+
note INTEGER REFERENCES task_steer(id) ON DELETE SET NULL,
|
|
392
|
+
sent_at TEXT,
|
|
393
|
+
paused_at TEXT,
|
|
394
|
+
resumed_at TEXT,
|
|
395
|
+
ended_at TEXT
|
|
396
|
+
);
|
|
397
|
+
CREATE INDEX IF NOT EXISTS run_pause_by_run ON run_pause (run, id);
|
|
398
|
+
CREATE TABLE IF NOT EXISTS run_resume (
|
|
399
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
400
|
+
run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,
|
|
401
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
402
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
403
|
+
requested_by TEXT NOT NULL,
|
|
404
|
+
requested_via TEXT NOT NULL CHECK (requested_via IN ('cli','web','chat')),
|
|
405
|
+
requested_at TEXT NOT NULL,
|
|
406
|
+
sent_at TEXT,
|
|
407
|
+
ended_at TEXT
|
|
408
|
+
);
|
|
409
|
+
CREATE INDEX IF NOT EXISTS run_resume_by_run ON run_resume (run, id);
|
|
410
|
+
`;
|
|
357
411
|
export const CHAT_APPROVAL_SCHEMA = `
|
|
358
412
|
CREATE TABLE IF NOT EXISTS chat_approval_setting (
|
|
359
413
|
approver TEXT NOT NULL,
|
|
@@ -564,7 +618,13 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
564
618
|
// (decision.superseded_reason: its task's scope changed, or the task was cancelled).
|
|
565
619
|
// v120 lets a running build read steering: a note the pulse put in the build's steering folder (task_steer.mailed_run,
|
|
566
620
|
// mailed_at) and the moment the agent moved it to handled/ (acked_at).
|
|
567
|
-
|
|
621
|
+
// v121 adds, and changes no saved row: the Agents board (run.attempt_key; agent_turn, agent_now, agent_question,
|
|
622
|
+
// agent_seen, agent_view), live steering (a note's live identity and sends, a run's stdin session; run_pause and
|
|
623
|
+
// run_resume) and landed work's teardown journal (landed_cleanup, landed_cleanup_item).
|
|
624
|
+
// v122 adds, and changes no saved row: the remote URL an owner's project add recorded (project.remote_url), the only
|
|
625
|
+
// remote whose main automatic completion trusts (remote-main.ts; saved projects read none until it is recorded), and
|
|
626
|
+
// fresh base's record of a task branch moved to current main before an attempt (branch_base_move).
|
|
627
|
+
export const SCHEMA_VERSION = 122;
|
|
568
628
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
569
629
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
570
630
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -688,9 +748,12 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
688
748
|
* before the old tables drop, and the references that named them name credential in place, their rows untouched. v119 only adds four columns
|
|
689
749
|
* (planning_generation on task_ref, run and plan_revision, defaulting to 0, and decision.superseded_reason, null);
|
|
690
750
|
* the rehearsal checks every saved row reads the declared default (HISTORY_RULES' added columns). v120 only adds
|
|
691
|
-
* three nullable task_steer columns (the running build's steering folder), checked the same way.
|
|
751
|
+
* three nullable task_steer columns (the running build's steering folder), checked the same way. v121 only adds
|
|
752
|
+
* new tables and the columns in V121_ADDED_COLUMNS (null, or no sends), checked the same way. v122 only adds a new
|
|
753
|
+
* table (branch_base_move) and the nullable column in V122_ADDED_COLUMNS (a project's recorded remote), checked the
|
|
754
|
+
* same way.
|
|
692
755
|
*/
|
|
693
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120]);
|
|
756
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122]);
|
|
694
757
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
695
758
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
696
759
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -710,6 +773,37 @@ export const V120_ADDED_COLUMNS = Object.freeze({
|
|
|
710
773
|
steer_closed_at: { definition: "TEXT", saved: "NULL" },
|
|
711
774
|
},
|
|
712
775
|
});
|
|
776
|
+
/**
|
|
777
|
+
* Live steering's added columns (v121), with the value every row saved before them reads. A note carries the identity
|
|
778
|
+
* its stdin copy is sent under (live_uuid, minted when it is filed), how many times it was sent to the running build
|
|
779
|
+
* and when last, and when it was given up on as not picked up.
|
|
780
|
+
* A run says when its stdin session opened and closed: only then can a note reach it in seconds, or a Pause stop a turn;
|
|
781
|
+
* and how long a paused session may wait before it ends (the idle limit; none under a wall-clock limit).
|
|
782
|
+
*/
|
|
783
|
+
export const LIVE_STEER_COLUMNS = Object.freeze({
|
|
784
|
+
task_steer: {
|
|
785
|
+
live_uuid: { definition: "TEXT", saved: "NULL" },
|
|
786
|
+
live_sends: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" },
|
|
787
|
+
live_sent_at: { definition: "TEXT", saved: "NULL" },
|
|
788
|
+
live_missed_at: { definition: "TEXT", saved: "NULL" },
|
|
789
|
+
},
|
|
790
|
+
run: {
|
|
791
|
+
live_input_opened_at: { definition: "TEXT", saved: "NULL" },
|
|
792
|
+
live_input_closed_at: { definition: "TEXT", saved: "NULL" },
|
|
793
|
+
live_pause_limit_ms: { definition: "INTEGER", saved: "NULL" },
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
/** v121: the columns it adds, by table, with the value every row saved before it reads: live steering's, and a run's
|
|
797
|
+
* attempt key (the Agents board; runs admitted before it read none). Its new tables carry no saved rows. */
|
|
798
|
+
export const V121_ADDED_COLUMNS = Object.freeze({
|
|
799
|
+
task_steer: LIVE_STEER_COLUMNS.task_steer,
|
|
800
|
+
run: { ...LIVE_STEER_COLUMNS.run, attempt_key: { definition: "TEXT", saved: "NULL" } },
|
|
801
|
+
});
|
|
802
|
+
/** v122: the column it adds, with the value every row saved before it reads: the remote URL an owner's project add
|
|
803
|
+
* recorded (projects added before it read none, until `repos add <project> --remote <url>` records one). */
|
|
804
|
+
export const V122_ADDED_COLUMNS = Object.freeze({
|
|
805
|
+
project: { remote_url: { definition: "TEXT", saved: "NULL" } },
|
|
806
|
+
});
|
|
713
807
|
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
714
808
|
export function updateSafeSchema(version) {
|
|
715
809
|
if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
|
|
@@ -3440,6 +3534,13 @@ function initializeStore(db, file) {
|
|
|
3440
3534
|
db.exec(CHAT_APPROVAL_SCHEMA);
|
|
3441
3535
|
// What a running agent did last (no version bump: additive only; absent means no word yet).
|
|
3442
3536
|
db.exec(RUN_ACTIVITY_SCHEMA);
|
|
3537
|
+
// v122: a task branch moved to current main before an attempt (fresh base): a new table, nothing carried over.
|
|
3538
|
+
db.exec(BRANCH_BASE_MOVE_SCHEMA);
|
|
3539
|
+
// v121: the Agents board: each run's attempt key (V121_ADDED_COLUMNS, below), its turn history and current snapshot,
|
|
3540
|
+
// the agent's open questions, who has seen a finished run and who is watching one now. Older runs read null keys.
|
|
3541
|
+
db.exec(AGENT_BOARD_SCHEMA);
|
|
3542
|
+
for (const [column, definition] of AGENT_NOW_COLUMNS)
|
|
3543
|
+
addColumn(db, "agent_now", column, definition);
|
|
3443
3544
|
// A flow's "Send to me" and "Person chooses" zones (no version bump: additive only).
|
|
3444
3545
|
db.exec(FLOW_SEND_SCHEMA);
|
|
3445
3546
|
db.exec(SPEND_SCHEMA);
|
|
@@ -3465,6 +3566,14 @@ function initializeStore(db, file) {
|
|
|
3465
3566
|
db.exec(CHECKOUT_CLEANUP_SCHEMA);
|
|
3466
3567
|
// The daily storage sweep's record (no version bump: a new table, nothing carried over).
|
|
3467
3568
|
db.exec(STORAGE_SWEEP_SCHEMA);
|
|
3569
|
+
// v121: landed work's teardown journal (landed-cleanup.ts): new tables.
|
|
3570
|
+
db.exec(LANDED_CLEANUP_SCHEMA);
|
|
3571
|
+
// v122: the remote URL an owner's project add recorded (V122_ADDED_COLUMNS; additive only, saved projects read none):
|
|
3572
|
+
// automatic completion trusts only that remote's main (remote-main.ts), never the shared repository's config.
|
|
3573
|
+
for (const [table, columns] of Object.entries(V122_ADDED_COLUMNS)) {
|
|
3574
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3575
|
+
addColumn(db, table, column, definition);
|
|
3576
|
+
}
|
|
3468
3577
|
db.exec(BACKUP_SCHEMA);
|
|
3469
3578
|
// Sprint 8: the organisation policy (one row, or none: nothing restricted).
|
|
3470
3579
|
db.exec(POLICY_SCHEMA);
|
|
@@ -3514,6 +3623,13 @@ function initializeStore(db, file) {
|
|
|
3514
3623
|
for (const [column, { definition }] of Object.entries(columns))
|
|
3515
3624
|
addColumn(db, table, column, definition);
|
|
3516
3625
|
}
|
|
3626
|
+
// v121: live steering and the Agents board's attempt key (additive only; saved notes read as never sent, saved runs as
|
|
3627
|
+
// never taking input and keyless), and the pause and resume requests.
|
|
3628
|
+
for (const [table, columns] of Object.entries(V121_ADDED_COLUMNS)) {
|
|
3629
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3630
|
+
addColumn(db, table, column, definition);
|
|
3631
|
+
}
|
|
3632
|
+
db.exec(LIVE_STEER_SCHEMA);
|
|
3517
3633
|
installLedgerTriggers(db);
|
|
3518
3634
|
// Attention/history indexes come AFTER migration: on a database whose
|
|
3519
3635
|
// constrained tables still carry a pre-rebuild shape, creating a partial
|
|
@@ -3630,6 +3746,8 @@ function createBaseline(db, file) {
|
|
|
3630
3746
|
}
|
|
3631
3747
|
db.exec(BASELINE_SCHEMA);
|
|
3632
3748
|
db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(BASELINE_SCHEMA_VERSION);
|
|
3749
|
+
const installedAt = new Date().toISOString();
|
|
3750
|
+
db.prepare("INSERT INTO installation_fact (key, value, created_at) VALUES ('installed-at', ?, ?)").run(installedAt, installedAt);
|
|
3633
3751
|
db.exec("COMMIT");
|
|
3634
3752
|
}
|
|
3635
3753
|
catch (error) {
|
|
@@ -11765,6 +11883,8 @@ export class Store {
|
|
|
11765
11883
|
// duplicate before any request is consumed.
|
|
11766
11884
|
reviewAttempt);
|
|
11767
11885
|
const id = Number(inserted.lastInsertRowid);
|
|
11886
|
+
// The attempt key (Agents board): opaque, minted with the row, and the only key its stream state is recorded under.
|
|
11887
|
+
this.db.prepare("UPDATE run SET attempt_key = ? WHERE id = ?").run(mintAttemptKey(), id);
|
|
11768
11888
|
// The provenance row lands in the same transaction as the run row —
|
|
11769
11889
|
// proved above, written here; a run and its route are one fact.
|
|
11770
11890
|
this.writeRunRoute(id, stamp, run.now);
|
|
@@ -12056,13 +12176,46 @@ export class Store {
|
|
|
12056
12176
|
* legacy first-attempt behavior, unchanged byte-for-byte.
|
|
12057
12177
|
*/
|
|
12058
12178
|
firstBuilderBase(taskRef, branch) {
|
|
12179
|
+
// A branch moved to current main (fresh base) had no commits of its own:
|
|
12180
|
+
// its origin is the first base recorded after the move, not the older main.
|
|
12059
12181
|
const row = this.db
|
|
12060
12182
|
.prepare(`SELECT base_revision FROM run
|
|
12061
12183
|
WHERE task_ref = ? AND branch = ? AND role = 'builder' AND base_revision IS NOT NULL
|
|
12184
|
+
AND id > COALESCE((SELECT after_run FROM branch_base_move WHERE task_ref = ? AND branch = ? ORDER BY id DESC LIMIT 1), 0)
|
|
12062
12185
|
ORDER BY id ASC LIMIT 1`)
|
|
12063
|
-
.get(taskRef, branch);
|
|
12186
|
+
.get(taskRef, branch, taskRef, branch);
|
|
12187
|
+
return row === undefined ? null : row.base_revision;
|
|
12188
|
+
}
|
|
12189
|
+
/** Where a task branch's attempts start now: a builder branch's pinned base, or the newest base any other attempt on
|
|
12190
|
+
* it recorded (a planner never commits). Null before any attempt on the branch recorded one, or after a move to
|
|
12191
|
+
* current main until the next attempt records its base. */
|
|
12192
|
+
recordedBranchBase(taskRef, branch) {
|
|
12193
|
+
const pinned = this.firstBuilderBase(taskRef, branch);
|
|
12194
|
+
if (pinned !== null)
|
|
12195
|
+
return pinned;
|
|
12196
|
+
const row = this.db
|
|
12197
|
+
.prepare(`SELECT base_revision FROM run
|
|
12198
|
+
WHERE task_ref = ? AND branch = ? AND role <> 'builder' AND base_revision IS NOT NULL
|
|
12199
|
+
AND id > COALESCE((SELECT after_run FROM branch_base_move WHERE task_ref = ? AND branch = ? ORDER BY id DESC LIMIT 1), 0)
|
|
12200
|
+
ORDER BY id DESC LIMIT 1`)
|
|
12201
|
+
.get(taskRef, branch, taskRef, branch);
|
|
12064
12202
|
return row === undefined ? null : row.base_revision;
|
|
12065
12203
|
}
|
|
12204
|
+
/** Record a task branch's move to current main, before git moves it: a move that then fails leaves the next
|
|
12205
|
+
* attempt's own base (still the old one) as the branch's origin, which is exactly right. */
|
|
12206
|
+
recordBranchBaseMove(taskRef, branch, from, to, now) {
|
|
12207
|
+
this.db
|
|
12208
|
+
.prepare(`INSERT INTO branch_base_move (task_ref, branch, from_rev, to_rev, after_run, moved_at)
|
|
12209
|
+
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(id), 0) FROM run), ?)`)
|
|
12210
|
+
.run(taskRef, branch, from, to, now.toISOString());
|
|
12211
|
+
}
|
|
12212
|
+
/** The newest move of a task branch to current main, or null. */
|
|
12213
|
+
latestBranchBaseMove(taskRef, branch) {
|
|
12214
|
+
const row = this.db
|
|
12215
|
+
.prepare("SELECT from_rev, to_rev, after_run, moved_at FROM branch_base_move WHERE task_ref = ? AND branch = ? ORDER BY id DESC LIMIT 1")
|
|
12216
|
+
.get(taskRef, branch);
|
|
12217
|
+
return row === undefined ? null : { from: String(row["from_rev"]), to: String(row["to_rev"]), afterRun: Number(row["after_run"]), movedAt: String(row["moved_at"]) };
|
|
12218
|
+
}
|
|
12066
12219
|
finishRun(id, result) {
|
|
12067
12220
|
this.transact(() => {
|
|
12068
12221
|
// Read before the write (Telegram task updates): only the OPEN → closed
|
|
@@ -12080,6 +12233,8 @@ export class Store {
|
|
|
12080
12233
|
// retained witnesses and every owned run; recovery revisits a pending
|
|
12081
12234
|
// stop after an orphan finishes shutdown.
|
|
12082
12235
|
this.recordRunProcessExits(id, result.now);
|
|
12236
|
+
// An ended attempt's open questions close unanswered: a reply would reach no one.
|
|
12237
|
+
closeRunQuestions(this, id, result.now);
|
|
12083
12238
|
// Use the existing reconciliation proof now, while exits are fresh. In
|
|
12084
12239
|
// particular, a returned --candidate check need not await the next scan.
|
|
12085
12240
|
this.settleUnspawnedWitnesses(result.now, id);
|
|
@@ -12837,8 +12992,12 @@ export class Store {
|
|
|
12837
12992
|
}
|
|
12838
12993
|
/** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
|
|
12839
12994
|
recordRunActivity(run, kind, now) {
|
|
12840
|
-
this.
|
|
12841
|
-
.
|
|
12995
|
+
this.transact(() => {
|
|
12996
|
+
this.db.prepare("INSERT INTO run_activity (run, kind, at) VALUES (?, ?, ?) ON CONFLICT (run) DO UPDATE SET kind = excluded.kind, at = excluded.at")
|
|
12997
|
+
.run(run, kind, now.toISOString());
|
|
12998
|
+
// Fresh activity is not Stuck: an open board hears the mark clear with this same write (agent-board.ts).
|
|
12999
|
+
this.db.prepare("UPDATE agent_now SET stuck_at = NULL WHERE run = ? AND stuck_at IS NOT NULL").run(run);
|
|
13000
|
+
});
|
|
12842
13001
|
}
|
|
12843
13002
|
/** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
|
|
12844
13003
|
runActivity(run) {
|
|
@@ -13014,13 +13173,19 @@ export class Store {
|
|
|
13014
13173
|
const state = String(task["state"]);
|
|
13015
13174
|
if (state === "done" || state === "cancelled")
|
|
13016
13175
|
return { ok: false, reason: "task-finished" };
|
|
13017
|
-
const
|
|
13018
|
-
.prepare("INSERT INTO task_steer (task_ref, author, note, created_at, authorship_state) VALUES (?, ?, ?, ?, 'verified')")
|
|
13019
|
-
.run(Number(ref["id"]), author, valid.note, now.toISOString());
|
|
13176
|
+
const id = this.insertSteerNote(Number(ref["id"]), author, valid.note, now);
|
|
13020
13177
|
this.bumpWake();
|
|
13021
|
-
return { ok: true, id
|
|
13178
|
+
return { ok: true, id };
|
|
13022
13179
|
}), result => result.ok);
|
|
13023
13180
|
}
|
|
13181
|
+
/** One verified note, with the identity a running build's stdin copy is sent under: filing it is also what queues
|
|
13182
|
+
* its live delivery, in the same transaction (the builder's dispatcher sends whatever is filed and unsent). */
|
|
13183
|
+
insertSteerNote(taskRef, author, note, now) {
|
|
13184
|
+
const inserted = this.db
|
|
13185
|
+
.prepare("INSERT INTO task_steer (task_ref, author, note, created_at, authorship_state, live_uuid) VALUES (?, ?, ?, ?, 'verified', ?)")
|
|
13186
|
+
.run(taskRef, author, note, now.toISOString(), randomUUID());
|
|
13187
|
+
return Number(inserted.lastInsertRowid);
|
|
13188
|
+
}
|
|
13024
13189
|
/** Every steering note of a task, filing order, delivery state included. */
|
|
13025
13190
|
listSteerNotes(taskRef) {
|
|
13026
13191
|
return this.db
|
|
@@ -13071,11 +13236,12 @@ export class Store {
|
|
|
13071
13236
|
.all(taskRef, runId, live, live);
|
|
13072
13237
|
const stamp = now.toISOString();
|
|
13073
13238
|
for (const row of rows) {
|
|
13239
|
+
// A brief that quotes it starts its live delivery over: an earlier build's sends and give-up are not this one's.
|
|
13074
13240
|
this.db
|
|
13075
|
-
.prepare("UPDATE task_steer SET attached_run = ?, attached_at =
|
|
13241
|
+
.prepare("UPDATE task_steer SET attached_run = ?, attached_at = ?, live_sends = 0, live_sent_at = NULL, live_missed_at = NULL WHERE id = ?")
|
|
13076
13242
|
.run(runId, stamp, Number(row["id"]));
|
|
13077
13243
|
}
|
|
13078
|
-
return rows.map(row => readSteerNote({ ...row, attached_run: runId, attached_at: stamp }));
|
|
13244
|
+
return rows.map(row => readSteerNote({ ...row, attached_run: runId, attached_at: stamp, live_sends: 0, live_sent_at: null, live_missed_at: null }));
|
|
13079
13245
|
});
|
|
13080
13246
|
}
|
|
13081
13247
|
/**
|
|
@@ -13135,9 +13301,12 @@ export class Store {
|
|
|
13135
13301
|
* reading: delivery settles only on the agent's own acknowledgement. */
|
|
13136
13302
|
markSteerMailed(noteId, runId, now) {
|
|
13137
13303
|
const { changes } = this.db
|
|
13138
|
-
.prepare(`UPDATE task_steer SET mailed_run = ?, mailed_at =
|
|
13304
|
+
.prepare(`UPDATE task_steer SET mailed_run = ?, mailed_at = ?,
|
|
13305
|
+
live_sends = CASE WHEN mailed_run IS ? THEN live_sends ELSE 0 END,
|
|
13306
|
+
live_sent_at = CASE WHEN mailed_run IS ? THEN live_sent_at ELSE NULL END,
|
|
13307
|
+
live_missed_at = CASE WHEN mailed_run IS ? THEN live_missed_at ELSE NULL END
|
|
13139
13308
|
WHERE id = ? AND delivered_at IS NULL AND superseded_at IS NULL`)
|
|
13140
|
-
.run(runId, now.toISOString(), noteId);
|
|
13309
|
+
.run(runId, now.toISOString(), runId, runId, runId, noteId);
|
|
13141
13310
|
return Number(changes) > 0;
|
|
13142
13311
|
}
|
|
13143
13312
|
/**
|
|
@@ -13156,6 +13325,201 @@ export class Store {
|
|
|
13156
13325
|
this.bumpWake();
|
|
13157
13326
|
return Number(changes) > 0;
|
|
13158
13327
|
}
|
|
13328
|
+
/**
|
|
13329
|
+
* The task's build that takes live input right now (live steering): the steering build (steeringLiveBuild) whose
|
|
13330
|
+
* Claude session reads stdin. Only this run can take a note in seconds, or a Pause. Null otherwise.
|
|
13331
|
+
*/
|
|
13332
|
+
liveInputRun(taskRef, now) {
|
|
13333
|
+
const run = this.steeringLiveBuild(taskRef, now);
|
|
13334
|
+
if (run === null)
|
|
13335
|
+
return null;
|
|
13336
|
+
const row = this.db.prepare("SELECT 1 FROM run WHERE id = ? AND live_input_opened_at IS NOT NULL AND live_input_closed_at IS NULL").get(run);
|
|
13337
|
+
return row === undefined ? null : run;
|
|
13338
|
+
}
|
|
13339
|
+
/** This run's Claude session reads stdin now (live steering), and how long a paused session may wait (null: no idle limit). */
|
|
13340
|
+
markLiveInputOpened(runId, now, pauseLimitMs = null) {
|
|
13341
|
+
this.db.prepare("UPDATE run SET live_input_opened_at = ?, live_pause_limit_ms = ? WHERE id = ? AND outcome IS NULL AND live_input_opened_at IS NULL")
|
|
13342
|
+
.run(now.toISOString(), pauseLimitMs === null || !Number.isSafeInteger(pauseLimitMs) || pauseLimitMs <= 0 ? null : pauseLimitMs, runId);
|
|
13343
|
+
}
|
|
13344
|
+
/** This run's stdin is closed (live steering): notes reach it only through its folder, and a Pause is over. */
|
|
13345
|
+
markLiveInputClosed(runId, now) {
|
|
13346
|
+
const stamp = now.toISOString();
|
|
13347
|
+
this.transact(() => {
|
|
13348
|
+
this.db.prepare("UPDATE run SET live_input_closed_at = ? WHERE id = ? AND live_input_opened_at IS NOT NULL AND live_input_closed_at IS NULL").run(stamp, runId);
|
|
13349
|
+
this.db.prepare("UPDATE run_pause SET ended_at = ? WHERE run = ? AND ended_at IS NULL AND resumed_at IS NULL").run(stamp, runId);
|
|
13350
|
+
this.db.prepare("UPDATE run_resume SET ended_at = ? WHERE run = ? AND sent_at IS NULL AND ended_at IS NULL").run(stamp, runId);
|
|
13351
|
+
});
|
|
13352
|
+
}
|
|
13353
|
+
/** One steering note by id. */
|
|
13354
|
+
steerNote(noteId) {
|
|
13355
|
+
const row = this.db.prepare("SELECT * FROM task_steer WHERE id = ?").get(noteId);
|
|
13356
|
+
return row === undefined ? null : readSteerNote(row);
|
|
13357
|
+
}
|
|
13358
|
+
/**
|
|
13359
|
+
* The note's copy was written to this run's stdin (live steering): one more send, stamped. Sending is not reading —
|
|
13360
|
+
* only the agent moving its file to handled/ is (ackSteerNote). Refused once the note is read, ended, given up on,
|
|
13361
|
+
* or mailed to another run.
|
|
13362
|
+
*/
|
|
13363
|
+
recordLiveSend(noteId, runId, now) {
|
|
13364
|
+
const { changes } = this.db
|
|
13365
|
+
.prepare(`UPDATE task_steer SET live_sends = live_sends + 1, live_sent_at = ?
|
|
13366
|
+
WHERE id = ? AND mailed_run = ? AND delivered_at IS NULL AND superseded_at IS NULL AND live_missed_at IS NULL`)
|
|
13367
|
+
.run(now.toISOString(), noteId, runId);
|
|
13368
|
+
return Number(changes) > 0;
|
|
13369
|
+
}
|
|
13370
|
+
/** The running build was sent this note every time it is owed and never moved it to handled/: Not picked up. A later
|
|
13371
|
+
* acknowledgement still reads it (ackSteerNote ignores this), and an attempt that ends returns it to the next brief. */
|
|
13372
|
+
markLiveMissed(noteId, runId, now) {
|
|
13373
|
+
const { changes } = this.db
|
|
13374
|
+
.prepare("UPDATE task_steer SET live_missed_at = ? WHERE id = ? AND mailed_run = ? AND delivered_at IS NULL AND superseded_at IS NULL AND live_missed_at IS NULL")
|
|
13375
|
+
.run(now.toISOString(), noteId, runId);
|
|
13376
|
+
if (Number(changes) > 0)
|
|
13377
|
+
this.bumpWake();
|
|
13378
|
+
return Number(changes) > 0;
|
|
13379
|
+
}
|
|
13380
|
+
/**
|
|
13381
|
+
* Pause a running Claude build, or Pause and steer it (live steering): asked of the exact run the person saw, under
|
|
13382
|
+
* their request id. In one transaction the note (when there is one) is filed — Ruling 11's verified author — and the
|
|
13383
|
+
* pause is queued ahead of it, so the dispatcher writes the interrupt before the note. A repeated request id is the
|
|
13384
|
+
* same request; a run that no longer takes live input is stale. Pause never stops, holds or reclaims the attempt.
|
|
13385
|
+
*/
|
|
13386
|
+
requestPause(request, now) {
|
|
13387
|
+
return this.transact(() => {
|
|
13388
|
+
const requestId = request.requestId.trim();
|
|
13389
|
+
if (!/^[A-Za-z0-9._:-]{8,100}$/.test(requestId))
|
|
13390
|
+
return { ok: false, reason: "invalid-request", detail: "the request id is 8 to 100 letters, digits or . _ : -" };
|
|
13391
|
+
const seen = this.db.prepare("SELECT * FROM run_pause WHERE request_id = ?").get(requestId);
|
|
13392
|
+
if (seen !== undefined) {
|
|
13393
|
+
const pause = readPause(seen);
|
|
13394
|
+
if (pause.run !== request.runId)
|
|
13395
|
+
return { ok: false, reason: "duplicate", detail: "that request id was already used for another attempt" };
|
|
13396
|
+
return { ok: true, pause: pause.id, note: pause.note, duplicate: true };
|
|
13397
|
+
}
|
|
13398
|
+
const task = this.db.prepare("SELECT state FROM task WHERE id = ?").get(request.taskId);
|
|
13399
|
+
const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, request.taskId);
|
|
13400
|
+
if (task === undefined || ref === undefined)
|
|
13401
|
+
return { ok: false, reason: "unknown-task", detail: "no such task" };
|
|
13402
|
+
const state = String(task["state"]);
|
|
13403
|
+
if (state === "done" || state === "cancelled")
|
|
13404
|
+
return { ok: false, reason: "task-finished", detail: "this task is finished" };
|
|
13405
|
+
const taskRef = Number(ref["id"]);
|
|
13406
|
+
if (this.liveInputRun(taskRef, now) !== request.runId) {
|
|
13407
|
+
return { ok: false, reason: "stale", detail: `build #${request.runId} isn't taking live input now — reload and decide against the current attempt` };
|
|
13408
|
+
}
|
|
13409
|
+
let noteId = null;
|
|
13410
|
+
if (request.note !== undefined) {
|
|
13411
|
+
const valid = validateNote(request.note);
|
|
13412
|
+
if (!valid.ok)
|
|
13413
|
+
return { ok: false, reason: "invalid-note", detail: valid.problem };
|
|
13414
|
+
noteId = this.insertSteerNote(taskRef, request.by, valid.note, now);
|
|
13415
|
+
}
|
|
13416
|
+
const inserted = this.db
|
|
13417
|
+
.prepare("INSERT INTO run_pause (run, task_ref, request_id, requested_by, requested_via, requested_at, note) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
13418
|
+
.run(request.runId, taskRef, requestId, request.by, request.via, now.toISOString(), noteId);
|
|
13419
|
+
this.bumpWake();
|
|
13420
|
+
return { ok: true, pause: Number(inserted.lastInsertRowid), note: noteId, duplicate: false };
|
|
13421
|
+
});
|
|
13422
|
+
}
|
|
13423
|
+
/** The pauses this run has not yet written to stdin, in request order (the dispatcher writes these first). */
|
|
13424
|
+
unsentPauses(runId) {
|
|
13425
|
+
return this.db.prepare("SELECT * FROM run_pause WHERE run = ? AND sent_at IS NULL AND ended_at IS NULL ORDER BY id").all(runId).map(readPause);
|
|
13426
|
+
}
|
|
13427
|
+
/** The interrupt for this pause was written to the run's stdin. */
|
|
13428
|
+
markPauseSent(pauseId, now) {
|
|
13429
|
+
this.db.prepare("UPDATE run_pause SET sent_at = ? WHERE id = ? AND sent_at IS NULL AND ended_at IS NULL").run(now.toISOString(), pauseId);
|
|
13430
|
+
}
|
|
13431
|
+
/** The run's current turn stopped: every pause sent and still waiting reads paused. */
|
|
13432
|
+
markRunPaused(runId, now) {
|
|
13433
|
+
const changed = this.db.prepare("UPDATE run_pause SET paused_at = ? WHERE run = ? AND sent_at IS NOT NULL AND paused_at IS NULL AND resumed_at IS NULL AND ended_at IS NULL").run(now.toISOString(), runId);
|
|
13434
|
+
if (Number(changed.changes) > 0)
|
|
13435
|
+
this.bumpWake();
|
|
13436
|
+
}
|
|
13437
|
+
/** A note continued the paused session: its pauses are over. */
|
|
13438
|
+
markRunResumed(runId, now) {
|
|
13439
|
+
this.db.prepare("UPDATE run_pause SET resumed_at = ? WHERE run = ? AND sent_at IS NOT NULL AND resumed_at IS NULL AND ended_at IS NULL").run(now.toISOString(), runId);
|
|
13440
|
+
}
|
|
13441
|
+
/** The run could not take this pause (its stdin closed first). */
|
|
13442
|
+
endPause(pauseId, now) {
|
|
13443
|
+
this.db.prepare("UPDATE run_pause SET ended_at = ? WHERE id = ? AND ended_at IS NULL").run(now.toISOString(), pauseId);
|
|
13444
|
+
}
|
|
13445
|
+
/** Where this run's latest pause stands: asked and not yet stopped (pausing), stopped (paused), or none. */
|
|
13446
|
+
pauseStateOf(runId) {
|
|
13447
|
+
return this.livePauseOf(runId)?.state ?? null;
|
|
13448
|
+
}
|
|
13449
|
+
/** This run's latest pause, when it paused, and how long a paused build may wait before it ends; null with none. */
|
|
13450
|
+
livePauseOf(runId) {
|
|
13451
|
+
const row = this.db.prepare(`SELECT p.paused_at, r.live_pause_limit_ms FROM run_pause p JOIN run r ON r.id = p.run
|
|
13452
|
+
WHERE p.run = ? AND p.resumed_at IS NULL AND p.ended_at IS NULL ORDER BY p.id DESC LIMIT 1`).get(runId);
|
|
13453
|
+
if (row === undefined)
|
|
13454
|
+
return null;
|
|
13455
|
+
const pausedAt = row["paused_at"] === null ? null : String(row["paused_at"]);
|
|
13456
|
+
const limit = row["live_pause_limit_ms"] === null || row["live_pause_limit_ms"] === undefined ? null : Number(row["live_pause_limit_ms"]);
|
|
13457
|
+
return { state: pausedAt === null ? "pausing" : "paused", pausedAt, limitMs: limit };
|
|
13458
|
+
}
|
|
13459
|
+
/**
|
|
13460
|
+
* Resume a paused Claude build (live steering): asked of the exact run the person saw, under their request id, and
|
|
13461
|
+
* only while that run is paused. The dispatcher sends a short continue frame — never a note, so nothing is filed.
|
|
13462
|
+
* A repeated request id is the same request; a run that no longer takes live input is stale.
|
|
13463
|
+
*/
|
|
13464
|
+
requestResume(request, now) {
|
|
13465
|
+
return this.transact(() => {
|
|
13466
|
+
const requestId = request.requestId.trim();
|
|
13467
|
+
if (!/^[A-Za-z0-9._:-]{8,100}$/.test(requestId))
|
|
13468
|
+
return { ok: false, reason: "invalid-request", detail: "the request id is 8 to 100 letters, digits or . _ : -" };
|
|
13469
|
+
const seen = this.db.prepare("SELECT id, run FROM run_resume WHERE request_id = ?").get(requestId);
|
|
13470
|
+
if (seen !== undefined) {
|
|
13471
|
+
if (Number(seen["run"]) !== request.runId)
|
|
13472
|
+
return { ok: false, reason: "duplicate", detail: "that request id was already used for another attempt" };
|
|
13473
|
+
return { ok: true, resume: Number(seen["id"]), duplicate: true };
|
|
13474
|
+
}
|
|
13475
|
+
const task = this.db.prepare("SELECT state FROM task WHERE id = ?").get(request.taskId);
|
|
13476
|
+
const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, request.taskId);
|
|
13477
|
+
if (task === undefined || ref === undefined)
|
|
13478
|
+
return { ok: false, reason: "unknown-task", detail: "no such task" };
|
|
13479
|
+
const state = String(task["state"]);
|
|
13480
|
+
if (state === "done" || state === "cancelled")
|
|
13481
|
+
return { ok: false, reason: "task-finished", detail: "this task is finished" };
|
|
13482
|
+
const taskRef = Number(ref["id"]);
|
|
13483
|
+
if (this.liveInputRun(taskRef, now) !== request.runId) {
|
|
13484
|
+
return { ok: false, reason: "stale", detail: `build #${request.runId} isn't taking live input now — reload and decide against the current attempt` };
|
|
13485
|
+
}
|
|
13486
|
+
if (this.pauseStateOf(request.runId) !== "paused")
|
|
13487
|
+
return { ok: false, reason: "not-paused", detail: `build #${request.runId} isn't paused` };
|
|
13488
|
+
const inserted = this.db
|
|
13489
|
+
.prepare("INSERT INTO run_resume (run, task_ref, request_id, requested_by, requested_via, requested_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
13490
|
+
.run(request.runId, taskRef, requestId, request.by, request.via, now.toISOString());
|
|
13491
|
+
this.bumpWake();
|
|
13492
|
+
return { ok: true, resume: Number(inserted.lastInsertRowid), duplicate: false };
|
|
13493
|
+
});
|
|
13494
|
+
}
|
|
13495
|
+
/** The resumes this run has not yet sent, in request order. */
|
|
13496
|
+
unsentResumes(runId) {
|
|
13497
|
+
return this.db.prepare("SELECT id, request_id FROM run_resume WHERE run = ? AND sent_at IS NULL AND ended_at IS NULL ORDER BY id").all(runId)
|
|
13498
|
+
.map(row => ({ id: Number(row["id"]), requestId: String(row["request_id"]) }));
|
|
13499
|
+
}
|
|
13500
|
+
/** The continue frame was written: the resume is sent, and the run's pauses are over. */
|
|
13501
|
+
markResumeSent(resumeId, runId, now) {
|
|
13502
|
+
this.transact(() => {
|
|
13503
|
+
this.db.prepare("UPDATE run_resume SET sent_at = ? WHERE id = ? AND sent_at IS NULL AND ended_at IS NULL").run(now.toISOString(), resumeId);
|
|
13504
|
+
this.markRunResumed(runId, now);
|
|
13505
|
+
});
|
|
13506
|
+
this.bumpWake();
|
|
13507
|
+
}
|
|
13508
|
+
/** The run was no longer paused (a note continued it first) or stopped taking input: nothing to send. */
|
|
13509
|
+
endResume(resumeId, now) {
|
|
13510
|
+
this.db.prepare("UPDATE run_resume SET ended_at = ? WHERE id = ? AND sent_at IS NULL AND ended_at IS NULL").run(now.toISOString(), resumeId);
|
|
13511
|
+
}
|
|
13512
|
+
/** Every resume of a task, newest first (history and tests). */
|
|
13513
|
+
resumesForTask(taskRef) {
|
|
13514
|
+
return this.db.prepare("SELECT * FROM run_resume WHERE task_ref = ? ORDER BY id DESC").all(taskRef).map(row => ({
|
|
13515
|
+
id: Number(row["id"]), run: Number(row["run"]), requestId: String(row["request_id"]), requestedVia: String(row["requested_via"]),
|
|
13516
|
+
sentAt: row["sent_at"] === null ? null : String(row["sent_at"]), endedAt: row["ended_at"] === null ? null : String(row["ended_at"]),
|
|
13517
|
+
}));
|
|
13518
|
+
}
|
|
13519
|
+
/** Every pause of a task, newest first (history and tests). */
|
|
13520
|
+
pausesForTask(taskRef) {
|
|
13521
|
+
return this.db.prepare("SELECT * FROM run_pause WHERE task_ref = ? ORDER BY id DESC").all(taskRef).map(readPause);
|
|
13522
|
+
}
|
|
13159
13523
|
/** A finished task's pending notes settle superseded — shown, typed, done. */
|
|
13160
13524
|
supersedeSteerNotes(taskRef, now) {
|
|
13161
13525
|
const { changes } = this.db
|
|
@@ -15299,17 +15663,38 @@ export class Store {
|
|
|
15299
15663
|
// ---- projects ------------------------------------------------------------
|
|
15300
15664
|
/** Remember a project was opened. Upsert keeps added_at; recency always moves.
|
|
15301
15665
|
* A project added for the first time starts on Quick checks. */
|
|
15302
|
-
upsertProject(path, name, now) {
|
|
15666
|
+
upsertProject(path, name, now, admission) {
|
|
15303
15667
|
this.transact(() => {
|
|
15304
15668
|
const added = this.db.prepare("SELECT 1 FROM project WHERE path = ?").get(path) === undefined;
|
|
15305
15669
|
this.db
|
|
15306
15670
|
.prepare(`INSERT INTO project (path, name, added_at, last_opened_at) VALUES (?, ?, ?, ?)
|
|
15307
15671
|
ON CONFLICT (path) DO UPDATE SET name = excluded.name, last_opened_at = excluded.last_opened_at`)
|
|
15308
15672
|
.run(path, name, now.toISOString(), now.toISOString());
|
|
15673
|
+
// Admission owns remote changes; ordinary openings preserve the saved value.
|
|
15674
|
+
if (admission !== undefined)
|
|
15675
|
+
this.db.prepare("UPDATE project SET remote_url = ? WHERE path = ?").run(admission.remoteUrl, path);
|
|
15309
15676
|
if (added)
|
|
15310
15677
|
stampNewProjectLevel(this, path, now);
|
|
15311
15678
|
});
|
|
15312
15679
|
}
|
|
15680
|
+
/** Includes removed/legacy projects, so deleting a row never makes build-writable config authoritative again. */
|
|
15681
|
+
projectAdmissionKnown(path) {
|
|
15682
|
+
return this.db.prepare(`SELECT 1 FROM project WHERE path = ?
|
|
15683
|
+
UNION ALL SELECT 1 FROM action_ledger WHERE repo = ?
|
|
15684
|
+
UNION ALL SELECT 1 FROM task_ref WHERE repo = ?
|
|
15685
|
+
UNION ALL SELECT 1 FROM worktree WHERE repo = ? LIMIT 1`).get(path, path, path, path) !== undefined;
|
|
15686
|
+
}
|
|
15687
|
+
/** Full structured URL audit, without the generic ledger's display-text truncation. */
|
|
15688
|
+
recordProjectRemote(actor, repo, previous, remoteUrl, now) {
|
|
15689
|
+
this.db.prepare(`INSERT INTO action_ledger(at,actor,repo,task_id,run_id,action,outcome,source,detail)
|
|
15690
|
+
VALUES (?,?,?,NULL,NULL,'project remote recorded','recorded','access',?)`)
|
|
15691
|
+
.run(now.toISOString(), actor, repo, JSON.stringify({ previousRemote: previous, remoteUrl }));
|
|
15692
|
+
}
|
|
15693
|
+
/** The remote URL captured on admission or explicitly supplied by the owner; null when none was (or the project was removed). */
|
|
15694
|
+
projectRemote(path) {
|
|
15695
|
+
const url = this.db.prepare("SELECT remote_url FROM project WHERE path = ?").get(path)?.["remote_url"];
|
|
15696
|
+
return typeof url === "string" && url !== "" ? url : null;
|
|
15697
|
+
}
|
|
15313
15698
|
/** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
|
|
15314
15699
|
forgetProject(path) {
|
|
15315
15700
|
this.db.prepare("DELETE FROM project WHERE path = ?").run(path);
|
|
@@ -15324,6 +15709,7 @@ export class Store {
|
|
|
15324
15709
|
name: String(row["name"]),
|
|
15325
15710
|
addedAt: String(row["added_at"]),
|
|
15326
15711
|
lastOpenedAt: String(row["last_opened_at"]),
|
|
15712
|
+
remoteUrl: typeof row["remote_url"] === "string" ? row["remote_url"] : null,
|
|
15327
15713
|
}));
|
|
15328
15714
|
}
|
|
15329
15715
|
/**
|
|
@@ -17442,10 +17828,24 @@ export class Store {
|
|
|
17442
17828
|
this.db.prepare("UPDATE push_delivery SET state = 'retired', last_error = ? WHERE id = ? AND state = 'pending'")
|
|
17443
17829
|
.run(elsewhere ? TELEGRAM_SKIPPED_OTHER_CHAT : "quiet", Number(raw["pair"]));
|
|
17444
17830
|
}
|
|
17831
|
+
else
|
|
17832
|
+
this.settleWatchedPush(Number(raw["pair"]), readNotification(raw), String(raw["approver"]), now);
|
|
17445
17833
|
}
|
|
17446
17834
|
}
|
|
17447
17835
|
return Number(changes);
|
|
17448
17836
|
}
|
|
17837
|
+
/** A pending pair about a run its person is watching waits for the watch to lapse; one about a finished run they have
|
|
17838
|
+
* since seen retires unsent (agent-board.ts watchedPing). False when the pair must not be sent now. */
|
|
17839
|
+
settleWatchedPush(pair, row, approver, now) {
|
|
17840
|
+
const decided = watchedPing(this, row, approver, now);
|
|
17841
|
+
if (decided === "send")
|
|
17842
|
+
return true;
|
|
17843
|
+
if (decided === "skip")
|
|
17844
|
+
this.db.prepare("UPDATE push_delivery SET state = 'retired', last_error = 'seen' WHERE id = ? AND state = 'pending'").run(pair);
|
|
17845
|
+
else
|
|
17846
|
+
this.db.prepare("UPDATE push_delivery SET next_attempt_at = ? WHERE id = ? AND state = 'pending'").run(decided.hold, pair);
|
|
17847
|
+
return false;
|
|
17848
|
+
}
|
|
17449
17849
|
/**
|
|
17450
17850
|
* Claim due pairs: pending (respecting next_attempt_at) or claimed-but-
|
|
17451
17851
|
* expired (reclaim bumps the generation — the old owner loses every
|
|
@@ -17464,6 +17864,11 @@ export class Store {
|
|
|
17464
17864
|
.map(row => Number(row["id"]));
|
|
17465
17865
|
const taken = [];
|
|
17466
17866
|
for (const id of due) {
|
|
17867
|
+
// A ping held while its person watched the run is decided again now: held on, retired, or sent.
|
|
17868
|
+
const watched = this.db.prepare(`SELECT n.*, s.approver AS approver FROM push_delivery p JOIN push_subscription s ON s.id = p.subscription
|
|
17869
|
+
JOIN notification n ON n.id = p.notification WHERE p.id = ? AND p.state = 'pending'`).get(id);
|
|
17870
|
+
if (watched !== undefined && !this.settleWatchedPush(id, readNotification(watched), String(watched["approver"]), now))
|
|
17871
|
+
continue;
|
|
17467
17872
|
const { changes } = this.db
|
|
17468
17873
|
.prepare(`UPDATE push_delivery SET state = 'claimed', claim_owner = ?, claim_expires_at = ?,
|
|
17469
17874
|
claim_generation = claim_generation + 1
|
|
@@ -17741,6 +18146,27 @@ function readSteerNote(row) {
|
|
|
17741
18146
|
mailedRun: row["mailed_run"] === null || row["mailed_run"] === undefined ? null : Number(row["mailed_run"]),
|
|
17742
18147
|
mailedAt: row["mailed_at"] === null || row["mailed_at"] === undefined ? null : String(row["mailed_at"]),
|
|
17743
18148
|
ackedAt: row["acked_at"] === null || row["acked_at"] === undefined ? null : String(row["acked_at"]),
|
|
18149
|
+
liveUuid: row["live_uuid"] === null || row["live_uuid"] === undefined ? null : String(row["live_uuid"]),
|
|
18150
|
+
liveSends: Number(row["live_sends"] ?? 0),
|
|
18151
|
+
liveSentAt: row["live_sent_at"] === null || row["live_sent_at"] === undefined ? null : String(row["live_sent_at"]),
|
|
18152
|
+
liveMissedAt: row["live_missed_at"] === null || row["live_missed_at"] === undefined ? null : String(row["live_missed_at"]),
|
|
18153
|
+
};
|
|
18154
|
+
}
|
|
18155
|
+
function readPause(row) {
|
|
18156
|
+
const text = (key) => row[key] === null || row[key] === undefined ? null : String(row[key]);
|
|
18157
|
+
return {
|
|
18158
|
+
id: Number(row["id"]),
|
|
18159
|
+
run: Number(row["run"]),
|
|
18160
|
+
taskRef: Number(row["task_ref"]),
|
|
18161
|
+
requestId: String(row["request_id"]),
|
|
18162
|
+
requestedBy: String(row["requested_by"]),
|
|
18163
|
+
requestedVia: String(row["requested_via"]),
|
|
18164
|
+
requestedAt: String(row["requested_at"]),
|
|
18165
|
+
note: row["note"] === null || row["note"] === undefined ? null : Number(row["note"]),
|
|
18166
|
+
sentAt: text("sent_at"),
|
|
18167
|
+
pausedAt: text("paused_at"),
|
|
18168
|
+
resumedAt: text("resumed_at"),
|
|
18169
|
+
endedAt: text("ended_at"),
|
|
17744
18170
|
};
|
|
17745
18171
|
}
|
|
17746
18172
|
function readDecision(row) {
|