toolroll 0.9.60 → 0.9.61
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 +15 -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/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 +157 -0
- package/dist/landed-cleanup.js +707 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.js +1 -1
- 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 +188 -16
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-delete-ui.d.ts +11 -0
- package/dist/project-delete-ui.js +29 -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/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 +12 -2
- package/dist/server/render-tasks.js +27 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +2 -1
- package/dist/server/task-views.js +9 -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 +41 -0
- package/dist/storage-sweep-process.js +187 -0
- package/dist/storage-sweep.d.ts +30 -6
- package/dist/storage-sweep.js +214 -28
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +16 -4
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +163 -2
- package/dist/store.js +350 -15
- package/dist/surface.js +4 -2
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -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 +3 -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 +3 -1
- package/dist/worktree.js +93 -10
- 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,43 @@ CREATE TABLE IF NOT EXISTS run_activity (
|
|
|
354
355
|
at TEXT NOT NULL
|
|
355
356
|
);
|
|
356
357
|
`;
|
|
358
|
+
/** Pausing a running Claude build (live steering, v121: additive only). A person's Pause asks the build to
|
|
359
|
+
* stop its current turn and keep its session: one row per request, bound to the exact run it was asked of, under the
|
|
360
|
+
* request id the asker chose (a repeated request is the same request). `note` is the steering note a Pause and steer
|
|
361
|
+
* sends right after the interrupt. The builder's dispatcher stamps sent_at when the interrupt is written, paused_at when
|
|
362
|
+
* the turn stopped, resumed_at when a later note continued the session, and ended_at when the run stopped taking input
|
|
363
|
+
* first. Never a stop: nothing here ends, holds or reclaims an attempt (run_stop does that). A person's Resume (run_resume)
|
|
364
|
+
* continues a paused build with a short continue frame and no note: one row per request, bound to the exact paused run;
|
|
365
|
+
* sent_at when the frame was written, ended_at when the run was no longer paused to take it. */
|
|
366
|
+
export const LIVE_STEER_SCHEMA = `
|
|
367
|
+
CREATE TABLE IF NOT EXISTS run_pause (
|
|
368
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
369
|
+
run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,
|
|
370
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
371
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
372
|
+
requested_by TEXT NOT NULL,
|
|
373
|
+
requested_via TEXT NOT NULL CHECK (requested_via IN ('cli','web','chat')),
|
|
374
|
+
requested_at TEXT NOT NULL,
|
|
375
|
+
note INTEGER REFERENCES task_steer(id) ON DELETE SET NULL,
|
|
376
|
+
sent_at TEXT,
|
|
377
|
+
paused_at TEXT,
|
|
378
|
+
resumed_at TEXT,
|
|
379
|
+
ended_at TEXT
|
|
380
|
+
);
|
|
381
|
+
CREATE INDEX IF NOT EXISTS run_pause_by_run ON run_pause (run, id);
|
|
382
|
+
CREATE TABLE IF NOT EXISTS run_resume (
|
|
383
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
384
|
+
run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,
|
|
385
|
+
task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,
|
|
386
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
387
|
+
requested_by TEXT NOT NULL,
|
|
388
|
+
requested_via TEXT NOT NULL CHECK (requested_via IN ('cli','web','chat')),
|
|
389
|
+
requested_at TEXT NOT NULL,
|
|
390
|
+
sent_at TEXT,
|
|
391
|
+
ended_at TEXT
|
|
392
|
+
);
|
|
393
|
+
CREATE INDEX IF NOT EXISTS run_resume_by_run ON run_resume (run, id);
|
|
394
|
+
`;
|
|
357
395
|
export const CHAT_APPROVAL_SCHEMA = `
|
|
358
396
|
CREATE TABLE IF NOT EXISTS chat_approval_setting (
|
|
359
397
|
approver TEXT NOT NULL,
|
|
@@ -564,7 +602,10 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
564
602
|
// (decision.superseded_reason: its task's scope changed, or the task was cancelled).
|
|
565
603
|
// v120 lets a running build read steering: a note the pulse put in the build's steering folder (task_steer.mailed_run,
|
|
566
604
|
// mailed_at) and the moment the agent moved it to handled/ (acked_at).
|
|
567
|
-
|
|
605
|
+
// v121 adds, and changes no saved row: the Agents board (run.attempt_key; agent_turn, agent_now, agent_question,
|
|
606
|
+
// agent_seen, agent_view), live steering (a note's live identity and sends, a run's stdin session; run_pause and
|
|
607
|
+
// run_resume) and landed work's teardown journal (landed_cleanup, landed_cleanup_item).
|
|
608
|
+
export const SCHEMA_VERSION = 121;
|
|
568
609
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
569
610
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
570
611
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -688,9 +729,10 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
688
729
|
* before the old tables drop, and the references that named them name credential in place, their rows untouched. v119 only adds four columns
|
|
689
730
|
* (planning_generation on task_ref, run and plan_revision, defaulting to 0, and decision.superseded_reason, null);
|
|
690
731
|
* 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.
|
|
732
|
+
* three nullable task_steer columns (the running build's steering folder), checked the same way. v121 only adds
|
|
733
|
+
* new tables and the columns in V121_ADDED_COLUMNS (null, or no sends), checked the same way.
|
|
692
734
|
*/
|
|
693
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120]);
|
|
735
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121]);
|
|
694
736
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
695
737
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
696
738
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -710,6 +752,32 @@ export const V120_ADDED_COLUMNS = Object.freeze({
|
|
|
710
752
|
steer_closed_at: { definition: "TEXT", saved: "NULL" },
|
|
711
753
|
},
|
|
712
754
|
});
|
|
755
|
+
/**
|
|
756
|
+
* Live steering's added columns (v121), with the value every row saved before them reads. A note carries the identity
|
|
757
|
+
* its stdin copy is sent under (live_uuid, minted when it is filed), how many times it was sent to the running build
|
|
758
|
+
* and when last, and when it was given up on as not picked up.
|
|
759
|
+
* 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;
|
|
760
|
+
* and how long a paused session may wait before it ends (the idle limit; none under a wall-clock limit).
|
|
761
|
+
*/
|
|
762
|
+
export const LIVE_STEER_COLUMNS = Object.freeze({
|
|
763
|
+
task_steer: {
|
|
764
|
+
live_uuid: { definition: "TEXT", saved: "NULL" },
|
|
765
|
+
live_sends: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" },
|
|
766
|
+
live_sent_at: { definition: "TEXT", saved: "NULL" },
|
|
767
|
+
live_missed_at: { definition: "TEXT", saved: "NULL" },
|
|
768
|
+
},
|
|
769
|
+
run: {
|
|
770
|
+
live_input_opened_at: { definition: "TEXT", saved: "NULL" },
|
|
771
|
+
live_input_closed_at: { definition: "TEXT", saved: "NULL" },
|
|
772
|
+
live_pause_limit_ms: { definition: "INTEGER", saved: "NULL" },
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
/** v121: the columns it adds, by table, with the value every row saved before it reads: live steering's, and a run's
|
|
776
|
+
* attempt key (the Agents board; runs admitted before it read none). Its new tables carry no saved rows. */
|
|
777
|
+
export const V121_ADDED_COLUMNS = Object.freeze({
|
|
778
|
+
task_steer: LIVE_STEER_COLUMNS.task_steer,
|
|
779
|
+
run: { ...LIVE_STEER_COLUMNS.run, attempt_key: { definition: "TEXT", saved: "NULL" } },
|
|
780
|
+
});
|
|
713
781
|
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
714
782
|
export function updateSafeSchema(version) {
|
|
715
783
|
if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
|
|
@@ -3440,6 +3508,11 @@ function initializeStore(db, file) {
|
|
|
3440
3508
|
db.exec(CHAT_APPROVAL_SCHEMA);
|
|
3441
3509
|
// What a running agent did last (no version bump: additive only; absent means no word yet).
|
|
3442
3510
|
db.exec(RUN_ACTIVITY_SCHEMA);
|
|
3511
|
+
// v121: the Agents board: each run's attempt key (V121_ADDED_COLUMNS, below), its turn history and current snapshot,
|
|
3512
|
+
// the agent's open questions, who has seen a finished run and who is watching one now. Older runs read null keys.
|
|
3513
|
+
db.exec(AGENT_BOARD_SCHEMA);
|
|
3514
|
+
for (const [column, definition] of AGENT_NOW_COLUMNS)
|
|
3515
|
+
addColumn(db, "agent_now", column, definition);
|
|
3443
3516
|
// A flow's "Send to me" and "Person chooses" zones (no version bump: additive only).
|
|
3444
3517
|
db.exec(FLOW_SEND_SCHEMA);
|
|
3445
3518
|
db.exec(SPEND_SCHEMA);
|
|
@@ -3465,6 +3538,8 @@ function initializeStore(db, file) {
|
|
|
3465
3538
|
db.exec(CHECKOUT_CLEANUP_SCHEMA);
|
|
3466
3539
|
// The daily storage sweep's record (no version bump: a new table, nothing carried over).
|
|
3467
3540
|
db.exec(STORAGE_SWEEP_SCHEMA);
|
|
3541
|
+
// v121: landed work's teardown journal (landed-cleanup.ts): new tables.
|
|
3542
|
+
db.exec(LANDED_CLEANUP_SCHEMA);
|
|
3468
3543
|
db.exec(BACKUP_SCHEMA);
|
|
3469
3544
|
// Sprint 8: the organisation policy (one row, or none: nothing restricted).
|
|
3470
3545
|
db.exec(POLICY_SCHEMA);
|
|
@@ -3514,6 +3589,13 @@ function initializeStore(db, file) {
|
|
|
3514
3589
|
for (const [column, { definition }] of Object.entries(columns))
|
|
3515
3590
|
addColumn(db, table, column, definition);
|
|
3516
3591
|
}
|
|
3592
|
+
// v121: live steering and the Agents board's attempt key (additive only; saved notes read as never sent, saved runs as
|
|
3593
|
+
// never taking input and keyless), and the pause and resume requests.
|
|
3594
|
+
for (const [table, columns] of Object.entries(V121_ADDED_COLUMNS)) {
|
|
3595
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
3596
|
+
addColumn(db, table, column, definition);
|
|
3597
|
+
}
|
|
3598
|
+
db.exec(LIVE_STEER_SCHEMA);
|
|
3517
3599
|
installLedgerTriggers(db);
|
|
3518
3600
|
// Attention/history indexes come AFTER migration: on a database whose
|
|
3519
3601
|
// constrained tables still carry a pre-rebuild shape, creating a partial
|
|
@@ -11765,6 +11847,8 @@ export class Store {
|
|
|
11765
11847
|
// duplicate before any request is consumed.
|
|
11766
11848
|
reviewAttempt);
|
|
11767
11849
|
const id = Number(inserted.lastInsertRowid);
|
|
11850
|
+
// The attempt key (Agents board): opaque, minted with the row, and the only key its stream state is recorded under.
|
|
11851
|
+
this.db.prepare("UPDATE run SET attempt_key = ? WHERE id = ?").run(mintAttemptKey(), id);
|
|
11768
11852
|
// The provenance row lands in the same transaction as the run row —
|
|
11769
11853
|
// proved above, written here; a run and its route are one fact.
|
|
11770
11854
|
this.writeRunRoute(id, stamp, run.now);
|
|
@@ -12080,6 +12164,8 @@ export class Store {
|
|
|
12080
12164
|
// retained witnesses and every owned run; recovery revisits a pending
|
|
12081
12165
|
// stop after an orphan finishes shutdown.
|
|
12082
12166
|
this.recordRunProcessExits(id, result.now);
|
|
12167
|
+
// An ended attempt's open questions close unanswered: a reply would reach no one.
|
|
12168
|
+
closeRunQuestions(this, id, result.now);
|
|
12083
12169
|
// Use the existing reconciliation proof now, while exits are fresh. In
|
|
12084
12170
|
// particular, a returned --candidate check need not await the next scan.
|
|
12085
12171
|
this.settleUnspawnedWitnesses(result.now, id);
|
|
@@ -12837,8 +12923,12 @@ export class Store {
|
|
|
12837
12923
|
}
|
|
12838
12924
|
/** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
|
|
12839
12925
|
recordRunActivity(run, kind, now) {
|
|
12840
|
-
this.
|
|
12841
|
-
.
|
|
12926
|
+
this.transact(() => {
|
|
12927
|
+
this.db.prepare("INSERT INTO run_activity (run, kind, at) VALUES (?, ?, ?) ON CONFLICT (run) DO UPDATE SET kind = excluded.kind, at = excluded.at")
|
|
12928
|
+
.run(run, kind, now.toISOString());
|
|
12929
|
+
// Fresh activity is not Stuck: an open board hears the mark clear with this same write (agent-board.ts).
|
|
12930
|
+
this.db.prepare("UPDATE agent_now SET stuck_at = NULL WHERE run = ? AND stuck_at IS NOT NULL").run(run);
|
|
12931
|
+
});
|
|
12842
12932
|
}
|
|
12843
12933
|
/** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
|
|
12844
12934
|
runActivity(run) {
|
|
@@ -13014,13 +13104,19 @@ export class Store {
|
|
|
13014
13104
|
const state = String(task["state"]);
|
|
13015
13105
|
if (state === "done" || state === "cancelled")
|
|
13016
13106
|
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());
|
|
13107
|
+
const id = this.insertSteerNote(Number(ref["id"]), author, valid.note, now);
|
|
13020
13108
|
this.bumpWake();
|
|
13021
|
-
return { ok: true, id
|
|
13109
|
+
return { ok: true, id };
|
|
13022
13110
|
}), result => result.ok);
|
|
13023
13111
|
}
|
|
13112
|
+
/** One verified note, with the identity a running build's stdin copy is sent under: filing it is also what queues
|
|
13113
|
+
* its live delivery, in the same transaction (the builder's dispatcher sends whatever is filed and unsent). */
|
|
13114
|
+
insertSteerNote(taskRef, author, note, now) {
|
|
13115
|
+
const inserted = this.db
|
|
13116
|
+
.prepare("INSERT INTO task_steer (task_ref, author, note, created_at, authorship_state, live_uuid) VALUES (?, ?, ?, ?, 'verified', ?)")
|
|
13117
|
+
.run(taskRef, author, note, now.toISOString(), randomUUID());
|
|
13118
|
+
return Number(inserted.lastInsertRowid);
|
|
13119
|
+
}
|
|
13024
13120
|
/** Every steering note of a task, filing order, delivery state included. */
|
|
13025
13121
|
listSteerNotes(taskRef) {
|
|
13026
13122
|
return this.db
|
|
@@ -13071,11 +13167,12 @@ export class Store {
|
|
|
13071
13167
|
.all(taskRef, runId, live, live);
|
|
13072
13168
|
const stamp = now.toISOString();
|
|
13073
13169
|
for (const row of rows) {
|
|
13170
|
+
// A brief that quotes it starts its live delivery over: an earlier build's sends and give-up are not this one's.
|
|
13074
13171
|
this.db
|
|
13075
|
-
.prepare("UPDATE task_steer SET attached_run = ?, attached_at =
|
|
13172
|
+
.prepare("UPDATE task_steer SET attached_run = ?, attached_at = ?, live_sends = 0, live_sent_at = NULL, live_missed_at = NULL WHERE id = ?")
|
|
13076
13173
|
.run(runId, stamp, Number(row["id"]));
|
|
13077
13174
|
}
|
|
13078
|
-
return rows.map(row => readSteerNote({ ...row, attached_run: runId, attached_at: stamp }));
|
|
13175
|
+
return rows.map(row => readSteerNote({ ...row, attached_run: runId, attached_at: stamp, live_sends: 0, live_sent_at: null, live_missed_at: null }));
|
|
13079
13176
|
});
|
|
13080
13177
|
}
|
|
13081
13178
|
/**
|
|
@@ -13135,9 +13232,12 @@ export class Store {
|
|
|
13135
13232
|
* reading: delivery settles only on the agent's own acknowledgement. */
|
|
13136
13233
|
markSteerMailed(noteId, runId, now) {
|
|
13137
13234
|
const { changes } = this.db
|
|
13138
|
-
.prepare(`UPDATE task_steer SET mailed_run = ?, mailed_at =
|
|
13235
|
+
.prepare(`UPDATE task_steer SET mailed_run = ?, mailed_at = ?,
|
|
13236
|
+
live_sends = CASE WHEN mailed_run IS ? THEN live_sends ELSE 0 END,
|
|
13237
|
+
live_sent_at = CASE WHEN mailed_run IS ? THEN live_sent_at ELSE NULL END,
|
|
13238
|
+
live_missed_at = CASE WHEN mailed_run IS ? THEN live_missed_at ELSE NULL END
|
|
13139
13239
|
WHERE id = ? AND delivered_at IS NULL AND superseded_at IS NULL`)
|
|
13140
|
-
.run(runId, now.toISOString(), noteId);
|
|
13240
|
+
.run(runId, now.toISOString(), runId, runId, runId, noteId);
|
|
13141
13241
|
return Number(changes) > 0;
|
|
13142
13242
|
}
|
|
13143
13243
|
/**
|
|
@@ -13156,6 +13256,201 @@ export class Store {
|
|
|
13156
13256
|
this.bumpWake();
|
|
13157
13257
|
return Number(changes) > 0;
|
|
13158
13258
|
}
|
|
13259
|
+
/**
|
|
13260
|
+
* The task's build that takes live input right now (live steering): the steering build (steeringLiveBuild) whose
|
|
13261
|
+
* Claude session reads stdin. Only this run can take a note in seconds, or a Pause. Null otherwise.
|
|
13262
|
+
*/
|
|
13263
|
+
liveInputRun(taskRef, now) {
|
|
13264
|
+
const run = this.steeringLiveBuild(taskRef, now);
|
|
13265
|
+
if (run === null)
|
|
13266
|
+
return null;
|
|
13267
|
+
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);
|
|
13268
|
+
return row === undefined ? null : run;
|
|
13269
|
+
}
|
|
13270
|
+
/** This run's Claude session reads stdin now (live steering), and how long a paused session may wait (null: no idle limit). */
|
|
13271
|
+
markLiveInputOpened(runId, now, pauseLimitMs = null) {
|
|
13272
|
+
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")
|
|
13273
|
+
.run(now.toISOString(), pauseLimitMs === null || !Number.isSafeInteger(pauseLimitMs) || pauseLimitMs <= 0 ? null : pauseLimitMs, runId);
|
|
13274
|
+
}
|
|
13275
|
+
/** This run's stdin is closed (live steering): notes reach it only through its folder, and a Pause is over. */
|
|
13276
|
+
markLiveInputClosed(runId, now) {
|
|
13277
|
+
const stamp = now.toISOString();
|
|
13278
|
+
this.transact(() => {
|
|
13279
|
+
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);
|
|
13280
|
+
this.db.prepare("UPDATE run_pause SET ended_at = ? WHERE run = ? AND ended_at IS NULL AND resumed_at IS NULL").run(stamp, runId);
|
|
13281
|
+
this.db.prepare("UPDATE run_resume SET ended_at = ? WHERE run = ? AND sent_at IS NULL AND ended_at IS NULL").run(stamp, runId);
|
|
13282
|
+
});
|
|
13283
|
+
}
|
|
13284
|
+
/** One steering note by id. */
|
|
13285
|
+
steerNote(noteId) {
|
|
13286
|
+
const row = this.db.prepare("SELECT * FROM task_steer WHERE id = ?").get(noteId);
|
|
13287
|
+
return row === undefined ? null : readSteerNote(row);
|
|
13288
|
+
}
|
|
13289
|
+
/**
|
|
13290
|
+
* The note's copy was written to this run's stdin (live steering): one more send, stamped. Sending is not reading —
|
|
13291
|
+
* only the agent moving its file to handled/ is (ackSteerNote). Refused once the note is read, ended, given up on,
|
|
13292
|
+
* or mailed to another run.
|
|
13293
|
+
*/
|
|
13294
|
+
recordLiveSend(noteId, runId, now) {
|
|
13295
|
+
const { changes } = this.db
|
|
13296
|
+
.prepare(`UPDATE task_steer SET live_sends = live_sends + 1, live_sent_at = ?
|
|
13297
|
+
WHERE id = ? AND mailed_run = ? AND delivered_at IS NULL AND superseded_at IS NULL AND live_missed_at IS NULL`)
|
|
13298
|
+
.run(now.toISOString(), noteId, runId);
|
|
13299
|
+
return Number(changes) > 0;
|
|
13300
|
+
}
|
|
13301
|
+
/** The running build was sent this note every time it is owed and never moved it to handled/: Not picked up. A later
|
|
13302
|
+
* acknowledgement still reads it (ackSteerNote ignores this), and an attempt that ends returns it to the next brief. */
|
|
13303
|
+
markLiveMissed(noteId, runId, now) {
|
|
13304
|
+
const { changes } = this.db
|
|
13305
|
+
.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")
|
|
13306
|
+
.run(now.toISOString(), noteId, runId);
|
|
13307
|
+
if (Number(changes) > 0)
|
|
13308
|
+
this.bumpWake();
|
|
13309
|
+
return Number(changes) > 0;
|
|
13310
|
+
}
|
|
13311
|
+
/**
|
|
13312
|
+
* Pause a running Claude build, or Pause and steer it (live steering): asked of the exact run the person saw, under
|
|
13313
|
+
* their request id. In one transaction the note (when there is one) is filed — Ruling 11's verified author — and the
|
|
13314
|
+
* pause is queued ahead of it, so the dispatcher writes the interrupt before the note. A repeated request id is the
|
|
13315
|
+
* same request; a run that no longer takes live input is stale. Pause never stops, holds or reclaims the attempt.
|
|
13316
|
+
*/
|
|
13317
|
+
requestPause(request, now) {
|
|
13318
|
+
return this.transact(() => {
|
|
13319
|
+
const requestId = request.requestId.trim();
|
|
13320
|
+
if (!/^[A-Za-z0-9._:-]{8,100}$/.test(requestId))
|
|
13321
|
+
return { ok: false, reason: "invalid-request", detail: "the request id is 8 to 100 letters, digits or . _ : -" };
|
|
13322
|
+
const seen = this.db.prepare("SELECT * FROM run_pause WHERE request_id = ?").get(requestId);
|
|
13323
|
+
if (seen !== undefined) {
|
|
13324
|
+
const pause = readPause(seen);
|
|
13325
|
+
if (pause.run !== request.runId)
|
|
13326
|
+
return { ok: false, reason: "duplicate", detail: "that request id was already used for another attempt" };
|
|
13327
|
+
return { ok: true, pause: pause.id, note: pause.note, duplicate: true };
|
|
13328
|
+
}
|
|
13329
|
+
const task = this.db.prepare("SELECT state FROM task WHERE id = ?").get(request.taskId);
|
|
13330
|
+
const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, request.taskId);
|
|
13331
|
+
if (task === undefined || ref === undefined)
|
|
13332
|
+
return { ok: false, reason: "unknown-task", detail: "no such task" };
|
|
13333
|
+
const state = String(task["state"]);
|
|
13334
|
+
if (state === "done" || state === "cancelled")
|
|
13335
|
+
return { ok: false, reason: "task-finished", detail: "this task is finished" };
|
|
13336
|
+
const taskRef = Number(ref["id"]);
|
|
13337
|
+
if (this.liveInputRun(taskRef, now) !== request.runId) {
|
|
13338
|
+
return { ok: false, reason: "stale", detail: `build #${request.runId} isn't taking live input now — reload and decide against the current attempt` };
|
|
13339
|
+
}
|
|
13340
|
+
let noteId = null;
|
|
13341
|
+
if (request.note !== undefined) {
|
|
13342
|
+
const valid = validateNote(request.note);
|
|
13343
|
+
if (!valid.ok)
|
|
13344
|
+
return { ok: false, reason: "invalid-note", detail: valid.problem };
|
|
13345
|
+
noteId = this.insertSteerNote(taskRef, request.by, valid.note, now);
|
|
13346
|
+
}
|
|
13347
|
+
const inserted = this.db
|
|
13348
|
+
.prepare("INSERT INTO run_pause (run, task_ref, request_id, requested_by, requested_via, requested_at, note) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
13349
|
+
.run(request.runId, taskRef, requestId, request.by, request.via, now.toISOString(), noteId);
|
|
13350
|
+
this.bumpWake();
|
|
13351
|
+
return { ok: true, pause: Number(inserted.lastInsertRowid), note: noteId, duplicate: false };
|
|
13352
|
+
});
|
|
13353
|
+
}
|
|
13354
|
+
/** The pauses this run has not yet written to stdin, in request order (the dispatcher writes these first). */
|
|
13355
|
+
unsentPauses(runId) {
|
|
13356
|
+
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);
|
|
13357
|
+
}
|
|
13358
|
+
/** The interrupt for this pause was written to the run's stdin. */
|
|
13359
|
+
markPauseSent(pauseId, now) {
|
|
13360
|
+
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);
|
|
13361
|
+
}
|
|
13362
|
+
/** The run's current turn stopped: every pause sent and still waiting reads paused. */
|
|
13363
|
+
markRunPaused(runId, now) {
|
|
13364
|
+
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);
|
|
13365
|
+
if (Number(changed.changes) > 0)
|
|
13366
|
+
this.bumpWake();
|
|
13367
|
+
}
|
|
13368
|
+
/** A note continued the paused session: its pauses are over. */
|
|
13369
|
+
markRunResumed(runId, now) {
|
|
13370
|
+
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);
|
|
13371
|
+
}
|
|
13372
|
+
/** The run could not take this pause (its stdin closed first). */
|
|
13373
|
+
endPause(pauseId, now) {
|
|
13374
|
+
this.db.prepare("UPDATE run_pause SET ended_at = ? WHERE id = ? AND ended_at IS NULL").run(now.toISOString(), pauseId);
|
|
13375
|
+
}
|
|
13376
|
+
/** Where this run's latest pause stands: asked and not yet stopped (pausing), stopped (paused), or none. */
|
|
13377
|
+
pauseStateOf(runId) {
|
|
13378
|
+
return this.livePauseOf(runId)?.state ?? null;
|
|
13379
|
+
}
|
|
13380
|
+
/** This run's latest pause, when it paused, and how long a paused build may wait before it ends; null with none. */
|
|
13381
|
+
livePauseOf(runId) {
|
|
13382
|
+
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
|
|
13383
|
+
WHERE p.run = ? AND p.resumed_at IS NULL AND p.ended_at IS NULL ORDER BY p.id DESC LIMIT 1`).get(runId);
|
|
13384
|
+
if (row === undefined)
|
|
13385
|
+
return null;
|
|
13386
|
+
const pausedAt = row["paused_at"] === null ? null : String(row["paused_at"]);
|
|
13387
|
+
const limit = row["live_pause_limit_ms"] === null || row["live_pause_limit_ms"] === undefined ? null : Number(row["live_pause_limit_ms"]);
|
|
13388
|
+
return { state: pausedAt === null ? "pausing" : "paused", pausedAt, limitMs: limit };
|
|
13389
|
+
}
|
|
13390
|
+
/**
|
|
13391
|
+
* Resume a paused Claude build (live steering): asked of the exact run the person saw, under their request id, and
|
|
13392
|
+
* only while that run is paused. The dispatcher sends a short continue frame — never a note, so nothing is filed.
|
|
13393
|
+
* A repeated request id is the same request; a run that no longer takes live input is stale.
|
|
13394
|
+
*/
|
|
13395
|
+
requestResume(request, now) {
|
|
13396
|
+
return this.transact(() => {
|
|
13397
|
+
const requestId = request.requestId.trim();
|
|
13398
|
+
if (!/^[A-Za-z0-9._:-]{8,100}$/.test(requestId))
|
|
13399
|
+
return { ok: false, reason: "invalid-request", detail: "the request id is 8 to 100 letters, digits or . _ : -" };
|
|
13400
|
+
const seen = this.db.prepare("SELECT id, run FROM run_resume WHERE request_id = ?").get(requestId);
|
|
13401
|
+
if (seen !== undefined) {
|
|
13402
|
+
if (Number(seen["run"]) !== request.runId)
|
|
13403
|
+
return { ok: false, reason: "duplicate", detail: "that request id was already used for another attempt" };
|
|
13404
|
+
return { ok: true, resume: Number(seen["id"]), duplicate: true };
|
|
13405
|
+
}
|
|
13406
|
+
const task = this.db.prepare("SELECT state FROM task WHERE id = ?").get(request.taskId);
|
|
13407
|
+
const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, request.taskId);
|
|
13408
|
+
if (task === undefined || ref === undefined)
|
|
13409
|
+
return { ok: false, reason: "unknown-task", detail: "no such task" };
|
|
13410
|
+
const state = String(task["state"]);
|
|
13411
|
+
if (state === "done" || state === "cancelled")
|
|
13412
|
+
return { ok: false, reason: "task-finished", detail: "this task is finished" };
|
|
13413
|
+
const taskRef = Number(ref["id"]);
|
|
13414
|
+
if (this.liveInputRun(taskRef, now) !== request.runId) {
|
|
13415
|
+
return { ok: false, reason: "stale", detail: `build #${request.runId} isn't taking live input now — reload and decide against the current attempt` };
|
|
13416
|
+
}
|
|
13417
|
+
if (this.pauseStateOf(request.runId) !== "paused")
|
|
13418
|
+
return { ok: false, reason: "not-paused", detail: `build #${request.runId} isn't paused` };
|
|
13419
|
+
const inserted = this.db
|
|
13420
|
+
.prepare("INSERT INTO run_resume (run, task_ref, request_id, requested_by, requested_via, requested_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
13421
|
+
.run(request.runId, taskRef, requestId, request.by, request.via, now.toISOString());
|
|
13422
|
+
this.bumpWake();
|
|
13423
|
+
return { ok: true, resume: Number(inserted.lastInsertRowid), duplicate: false };
|
|
13424
|
+
});
|
|
13425
|
+
}
|
|
13426
|
+
/** The resumes this run has not yet sent, in request order. */
|
|
13427
|
+
unsentResumes(runId) {
|
|
13428
|
+
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)
|
|
13429
|
+
.map(row => ({ id: Number(row["id"]), requestId: String(row["request_id"]) }));
|
|
13430
|
+
}
|
|
13431
|
+
/** The continue frame was written: the resume is sent, and the run's pauses are over. */
|
|
13432
|
+
markResumeSent(resumeId, runId, now) {
|
|
13433
|
+
this.transact(() => {
|
|
13434
|
+
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);
|
|
13435
|
+
this.markRunResumed(runId, now);
|
|
13436
|
+
});
|
|
13437
|
+
this.bumpWake();
|
|
13438
|
+
}
|
|
13439
|
+
/** The run was no longer paused (a note continued it first) or stopped taking input: nothing to send. */
|
|
13440
|
+
endResume(resumeId, now) {
|
|
13441
|
+
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);
|
|
13442
|
+
}
|
|
13443
|
+
/** Every resume of a task, newest first (history and tests). */
|
|
13444
|
+
resumesForTask(taskRef) {
|
|
13445
|
+
return this.db.prepare("SELECT * FROM run_resume WHERE task_ref = ? ORDER BY id DESC").all(taskRef).map(row => ({
|
|
13446
|
+
id: Number(row["id"]), run: Number(row["run"]), requestId: String(row["request_id"]), requestedVia: String(row["requested_via"]),
|
|
13447
|
+
sentAt: row["sent_at"] === null ? null : String(row["sent_at"]), endedAt: row["ended_at"] === null ? null : String(row["ended_at"]),
|
|
13448
|
+
}));
|
|
13449
|
+
}
|
|
13450
|
+
/** Every pause of a task, newest first (history and tests). */
|
|
13451
|
+
pausesForTask(taskRef) {
|
|
13452
|
+
return this.db.prepare("SELECT * FROM run_pause WHERE task_ref = ? ORDER BY id DESC").all(taskRef).map(readPause);
|
|
13453
|
+
}
|
|
13159
13454
|
/** A finished task's pending notes settle superseded — shown, typed, done. */
|
|
13160
13455
|
supersedeSteerNotes(taskRef, now) {
|
|
13161
13456
|
const { changes } = this.db
|
|
@@ -17442,10 +17737,24 @@ export class Store {
|
|
|
17442
17737
|
this.db.prepare("UPDATE push_delivery SET state = 'retired', last_error = ? WHERE id = ? AND state = 'pending'")
|
|
17443
17738
|
.run(elsewhere ? TELEGRAM_SKIPPED_OTHER_CHAT : "quiet", Number(raw["pair"]));
|
|
17444
17739
|
}
|
|
17740
|
+
else
|
|
17741
|
+
this.settleWatchedPush(Number(raw["pair"]), readNotification(raw), String(raw["approver"]), now);
|
|
17445
17742
|
}
|
|
17446
17743
|
}
|
|
17447
17744
|
return Number(changes);
|
|
17448
17745
|
}
|
|
17746
|
+
/** A pending pair about a run its person is watching waits for the watch to lapse; one about a finished run they have
|
|
17747
|
+
* since seen retires unsent (agent-board.ts watchedPing). False when the pair must not be sent now. */
|
|
17748
|
+
settleWatchedPush(pair, row, approver, now) {
|
|
17749
|
+
const decided = watchedPing(this, row, approver, now);
|
|
17750
|
+
if (decided === "send")
|
|
17751
|
+
return true;
|
|
17752
|
+
if (decided === "skip")
|
|
17753
|
+
this.db.prepare("UPDATE push_delivery SET state = 'retired', last_error = 'seen' WHERE id = ? AND state = 'pending'").run(pair);
|
|
17754
|
+
else
|
|
17755
|
+
this.db.prepare("UPDATE push_delivery SET next_attempt_at = ? WHERE id = ? AND state = 'pending'").run(decided.hold, pair);
|
|
17756
|
+
return false;
|
|
17757
|
+
}
|
|
17449
17758
|
/**
|
|
17450
17759
|
* Claim due pairs: pending (respecting next_attempt_at) or claimed-but-
|
|
17451
17760
|
* expired (reclaim bumps the generation — the old owner loses every
|
|
@@ -17464,6 +17773,11 @@ export class Store {
|
|
|
17464
17773
|
.map(row => Number(row["id"]));
|
|
17465
17774
|
const taken = [];
|
|
17466
17775
|
for (const id of due) {
|
|
17776
|
+
// A ping held while its person watched the run is decided again now: held on, retired, or sent.
|
|
17777
|
+
const watched = this.db.prepare(`SELECT n.*, s.approver AS approver FROM push_delivery p JOIN push_subscription s ON s.id = p.subscription
|
|
17778
|
+
JOIN notification n ON n.id = p.notification WHERE p.id = ? AND p.state = 'pending'`).get(id);
|
|
17779
|
+
if (watched !== undefined && !this.settleWatchedPush(id, readNotification(watched), String(watched["approver"]), now))
|
|
17780
|
+
continue;
|
|
17467
17781
|
const { changes } = this.db
|
|
17468
17782
|
.prepare(`UPDATE push_delivery SET state = 'claimed', claim_owner = ?, claim_expires_at = ?,
|
|
17469
17783
|
claim_generation = claim_generation + 1
|
|
@@ -17741,6 +18055,27 @@ function readSteerNote(row) {
|
|
|
17741
18055
|
mailedRun: row["mailed_run"] === null || row["mailed_run"] === undefined ? null : Number(row["mailed_run"]),
|
|
17742
18056
|
mailedAt: row["mailed_at"] === null || row["mailed_at"] === undefined ? null : String(row["mailed_at"]),
|
|
17743
18057
|
ackedAt: row["acked_at"] === null || row["acked_at"] === undefined ? null : String(row["acked_at"]),
|
|
18058
|
+
liveUuid: row["live_uuid"] === null || row["live_uuid"] === undefined ? null : String(row["live_uuid"]),
|
|
18059
|
+
liveSends: Number(row["live_sends"] ?? 0),
|
|
18060
|
+
liveSentAt: row["live_sent_at"] === null || row["live_sent_at"] === undefined ? null : String(row["live_sent_at"]),
|
|
18061
|
+
liveMissedAt: row["live_missed_at"] === null || row["live_missed_at"] === undefined ? null : String(row["live_missed_at"]),
|
|
18062
|
+
};
|
|
18063
|
+
}
|
|
18064
|
+
function readPause(row) {
|
|
18065
|
+
const text = (key) => row[key] === null || row[key] === undefined ? null : String(row[key]);
|
|
18066
|
+
return {
|
|
18067
|
+
id: Number(row["id"]),
|
|
18068
|
+
run: Number(row["run"]),
|
|
18069
|
+
taskRef: Number(row["task_ref"]),
|
|
18070
|
+
requestId: String(row["request_id"]),
|
|
18071
|
+
requestedBy: String(row["requested_by"]),
|
|
18072
|
+
requestedVia: String(row["requested_via"]),
|
|
18073
|
+
requestedAt: String(row["requested_at"]),
|
|
18074
|
+
note: row["note"] === null || row["note"] === undefined ? null : Number(row["note"]),
|
|
18075
|
+
sentAt: text("sent_at"),
|
|
18076
|
+
pausedAt: text("paused_at"),
|
|
18077
|
+
resumedAt: text("resumed_at"),
|
|
18078
|
+
endedAt: text("ended_at"),
|
|
17744
18079
|
};
|
|
17745
18080
|
}
|
|
17746
18081
|
function readDecision(row) {
|
package/dist/surface.js
CHANGED
|
@@ -242,7 +242,9 @@ const GUIDE_ROWS = [
|
|
|
242
242
|
positionals: [{ name: "id", required: true, meaning: "the task" }],
|
|
243
243
|
flags: [jsonFlag, dbFlag, keyFlag, { name: "undo", takesValue: false, meaning: "back to filing order" }],
|
|
244
244
|
notableReasons: ["unknown-task", "claimed"] },
|
|
245
|
-
operator("task steer", "one credentialed note for the task's agent — a running build reads it when it next checks for notes, otherwise the next attempt does; steering speaks with the operator's voice"),
|
|
245
|
+
operator("task steer", "one credentialed note for the task's agent — a running Claude build gets it in seconds, another running build reads it when it next checks for notes, otherwise the next attempt does; --now pauses the running Claude build first; steering speaks with the operator's voice"),
|
|
246
|
+
operator("task pause", "stop a running Claude build's current turn and keep the build (--run <id> names the attempt): its next note continues it; never a stop"),
|
|
247
|
+
operator("task continue", "resume a paused Claude build where it stopped, without a note (--run <id> names the attempt)"),
|
|
246
248
|
{ invocation: "task assign", synopsis: "reserve a task for one worker, or return it to the shared queue", audience: "agent", agentMayInvoke: true, mutation: "keyed",
|
|
247
249
|
positionals: [{ name: "id", required: true, meaning: "the task" }],
|
|
248
250
|
flags: [jsonFlag, dbFlag, keyFlag,
|
|
@@ -449,7 +451,7 @@ const REMOTE_STEP_UP = [
|
|
|
449
451
|
const REMOTE_YES = [
|
|
450
452
|
"status", "integrations", "ready", "gaps", "grants", "sync", "audit",
|
|
451
453
|
"task add", "task ask", "task checks", "task add-tests", "task list", "task show", "check-progress", "task wait", "task complete", "task revise",
|
|
452
|
-
"task state", "task block", "task unblock", "task next", "task steer", "task assign", "task scope", "task plan", "task hold", "task unhold", "task require",
|
|
454
|
+
"task state", "task block", "task unblock", "task next", "task steer", "task pause", "task continue", "task assign", "task scope", "task plan", "task hold", "task unhold", "task require",
|
|
453
455
|
"task requeue", "task review", "task repair", "task route", "task reopen", "task stop", "task resume",
|
|
454
456
|
"runner list", "coordinator list", "cap list", "cap add", "outbox list", "incident list", "incident resolve",
|
|
455
457
|
"config show", "verify show", "intake show", "intake run", "intake preview", "intake pr-comments", "template list", "template show",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const QUARANTINE_NOTE = ".toolroll-sweep.json";
|
|
2
|
+
type Journal<T> = {
|
|
3
|
+
version: 1;
|
|
4
|
+
name: string;
|
|
5
|
+
data: T;
|
|
6
|
+
deleting: boolean;
|
|
7
|
+
pid: number;
|
|
8
|
+
host: string;
|
|
9
|
+
bootId: string | null;
|
|
10
|
+
};
|
|
11
|
+
export type Quarantine<T> = {
|
|
12
|
+
path: string;
|
|
13
|
+
original: string;
|
|
14
|
+
journal: Journal<T>;
|
|
15
|
+
};
|
|
16
|
+
export declare function quarantineEntries<T>(root: string): Quarantine<T>[];
|
|
17
|
+
/** A small marker closes discovery before the atomic rename. It is never published half-written. */
|
|
18
|
+
export declare function quarantine<T>(original: string, data: T): Quarantine<T> | null;
|
|
19
|
+
/** Returns null if another sweep owns it (or won recovery). */
|
|
20
|
+
export declare function claimQuarantine<T>(entry: Quarantine<T>): Quarantine<T> | null;
|
|
21
|
+
export declare function releaseQuarantine<T>(entry: Quarantine<T>): void;
|
|
22
|
+
/** A partially deleted tree must never become discoverable again. */
|
|
23
|
+
export declare function restoreQuarantine<T>(entry: Quarantine<T>): boolean;
|
|
24
|
+
/** Keep the journal until all potentially large removals succeeded. Recovery never guesses at a half-deleted tree. */
|
|
25
|
+
export declare function deleteQuarantine<T>(entry: Quarantine<T>, remove: (path: string) => void | Promise<void>): Promise<void>;
|
|
26
|
+
/** Staged pruning is also called synchronously by update/deploy. */
|
|
27
|
+
export declare function deleteQuarantineSync<T>(entry: Quarantine<T>, remove: (path: string) => void): void;
|
|
28
|
+
export {};
|