toolroll 0.9.46 → 0.9.48
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 +27 -0
- package/dist/activity-line.d.ts +26 -0
- package/dist/activity-line.js +47 -0
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +18 -1
- package/dist/browser-workspace.js +9 -3
- package/dist/builder.js +4 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/contracts/cli.d.ts +87 -0
- package/dist/contracts/cli.js +2 -0
- package/dist/contracts/health.d.ts +84 -0
- package/dist/contracts/health.js +11 -0
- package/dist/flow-card-task.d.ts +2 -0
- package/dist/flow-card-task.js +57 -0
- package/dist/flow-live.d.ts +7 -15
- package/dist/flow-live.js +77 -115
- package/dist/flows-ui.js +15 -1
- package/dist/health-cli.d.ts +3 -0
- package/dist/health-cli.js +79 -0
- package/dist/live-bus.d.ts +89 -0
- package/dist/live-bus.js +321 -0
- package/dist/live.d.ts +14 -0
- package/dist/live.js +71 -22
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +53 -1
- package/dist/operate.js +43 -25
- package/dist/planner.js +4 -3
- package/dist/read-executor-worker.d.ts +1 -0
- package/dist/read-executor-worker.js +26 -0
- package/dist/read-executor.d.ts +49 -0
- package/dist/read-executor.js +119 -0
- package/dist/request-budget.d.ts +1 -0
- package/dist/request-budget.js +6 -0
- package/dist/serve.d.ts +7 -0
- package/dist/serve.js +118 -42
- package/dist/server-telemetry.d.ts +86 -0
- package/dist/server-telemetry.js +263 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/sqlite-telemetry.d.ts +9 -0
- package/dist/sqlite-telemetry.js +98 -0
- package/dist/store.d.ts +22 -0
- package/dist/store.js +59 -3
- package/dist/surface.js +2 -1
- package/dist/task-activity.d.ts +27 -0
- package/dist/task-activity.js +53 -0
- package/dist/task-live.d.ts +8 -0
- package/dist/task-live.js +47 -0
- package/dist/work-index.js +12 -1
- package/dist/workspace-revision.d.ts +2 -0
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a running agent did last, recorded and read (live tasks).
|
|
3
|
+
*
|
|
4
|
+
* The recorder sits beside the live log on the agent's stream and keeps one
|
|
5
|
+
* row per run: a fixed kind from live.ts's vocabulary and when. No text, no
|
|
6
|
+
* tool names, no arguments are ever kept, so nothing here can leak a secret.
|
|
7
|
+
* The reader picks the newer of that row and the run's latest progress
|
|
8
|
+
* checkpoint, and asks the runner's heartbeat whether the worker still
|
|
9
|
+
* answers. Neither side reads or stats a live-log file.
|
|
10
|
+
*/
|
|
11
|
+
import { ACTIVITY_WORDS } from "./activity-line.js";
|
|
12
|
+
import { liveActivityKind } from "./live.js";
|
|
13
|
+
import { isAlive } from "./runner.js";
|
|
14
|
+
/** At most one write per run this often while the kind stays the same: the age is in seconds, not milliseconds. */
|
|
15
|
+
const SAME_KIND_EVERY_MS = 5_000;
|
|
16
|
+
/** Watch a run's stream for what the agent does, alongside its live log (which may be null). Fail-soft: a write
|
|
17
|
+
* that fails never touches the run. */
|
|
18
|
+
export function withRunActivity(liveLog, store, run, clock) {
|
|
19
|
+
let last = null;
|
|
20
|
+
return {
|
|
21
|
+
observe(event) {
|
|
22
|
+
liveLog?.observe(event);
|
|
23
|
+
const kind = liveActivityKind(event);
|
|
24
|
+
if (kind === null)
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
const now = clock();
|
|
28
|
+
if (last !== null && last.kind === kind && now.getTime() - last.at < SAME_KIND_EVERY_MS)
|
|
29
|
+
return;
|
|
30
|
+
store.recordRunActivity(run, kind, now);
|
|
31
|
+
last = { kind, at: now.getTime() };
|
|
32
|
+
}
|
|
33
|
+
catch { /* display state: the next event tries again */ }
|
|
34
|
+
},
|
|
35
|
+
close() { liveLog?.close(); },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** A live run's last activity: the newer of its recorded kind and its latest progress checkpoint, else when it
|
|
39
|
+
* started; and "offline" only when its registered worker's heartbeat has lapsed. */
|
|
40
|
+
export function runActivityOf(store, run, now) {
|
|
41
|
+
const recorded = store.runActivity(run.id);
|
|
42
|
+
let what = ACTIVITY_WORDS["started"], at = run.startedAt;
|
|
43
|
+
if (recorded.kind !== null && recorded.at !== null && recorded.at >= at) {
|
|
44
|
+
what = ACTIVITY_WORDS[recorded.kind] ?? ACTIVITY_WORDS["tool"];
|
|
45
|
+
at = recorded.at;
|
|
46
|
+
}
|
|
47
|
+
if (recorded.progressAt !== null && recorded.progressAt > at) {
|
|
48
|
+
what = ACTIVITY_WORDS["progress"];
|
|
49
|
+
at = recorded.progressAt;
|
|
50
|
+
}
|
|
51
|
+
const worker = store.getRunner(run.runner)?.runner;
|
|
52
|
+
return { what, at, worker: worker === undefined || isAlive(worker, now) ? "online" : "offline" };
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type LiveRoomOptions, type LiveRooms, type LiveViewer } from "./live-bus.js";
|
|
2
|
+
import { type Store } from "./store.js";
|
|
3
|
+
export type TaskViewer = LiveViewer;
|
|
4
|
+
/** What changes when anything on the task page changes: one read over the family, kept short and opaque. */
|
|
5
|
+
export declare function taskFingerprint(store: Store, root: string, now: Date, livenessMs?: number): string | null;
|
|
6
|
+
export type TaskRooms = LiveRooms<string, TaskViewer>;
|
|
7
|
+
/** Rooms keyed by the task family's root. With a bus, a write reaches every open page at once; without one, only the safety net. */
|
|
8
|
+
export declare function createTaskRooms(fingerprint: (root: string) => string | null, options?: LiveRoomOptions): TaskRooms;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live task page (live tasks): a task open in several browsers stays the
|
|
3
|
+
* same in all of them, and each shows who else has it open. The same design
|
|
4
|
+
* as the live canvas (flow-live.ts):
|
|
5
|
+
*
|
|
6
|
+
* - A change nudge. When the live bus says the workspace was written
|
|
7
|
+
* (live-bus.ts), one indexed read keyed by the task family's root takes
|
|
8
|
+
* the family's fingerprint (its versions' states, runs, decisions,
|
|
9
|
+
* progress checkpoints, what the agent did last, and whether each live
|
|
10
|
+
* run's worker still answers); when it changed, every open page hears
|
|
11
|
+
* "change" and reads the task again the usual way. The nudge carries no
|
|
12
|
+
* task data. With nothing written, an open page costs no reads; a
|
|
13
|
+
* 30-second check heals a missed signal (and notices a lapsed worker).
|
|
14
|
+
* - Who's here. Everyone else with the task open, by name, one line per
|
|
15
|
+
* person however many pages they have. Memory only; it goes when the page
|
|
16
|
+
* closes or hides.
|
|
17
|
+
*
|
|
18
|
+
* Streams are refresh hints only: nothing here writes, and a page that can't
|
|
19
|
+
* keep one open reads itself on its usual beat.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { createLiveRooms } from "./live-bus.js";
|
|
23
|
+
import { DEFAULT_LIVENESS_MS } from "./runner.js";
|
|
24
|
+
import { BUILT_IN } from "./store.js";
|
|
25
|
+
/** What changes when anything on the task page changes: one read over the family, kept short and opaque. */
|
|
26
|
+
export function taskFingerprint(store, root, now, livenessMs = DEFAULT_LIVENESS_MS) {
|
|
27
|
+
const row = store.handle.prepare(`WITH RECURSIVE family(ref, ext, depth) AS (
|
|
28
|
+
SELECT id, external_id, 0 FROM task_ref WHERE backend = ?3 AND external_id = ?1
|
|
29
|
+
UNION
|
|
30
|
+
SELECT child.id, child.external_id, family.depth + 1 FROM family
|
|
31
|
+
JOIN task_ref child ON child.backend = ?3 AND child.revision_of = family.ext WHERE family.depth < 63
|
|
32
|
+
)
|
|
33
|
+
SELECT (SELECT count(*) || ':' || group_concat(t.state || '@' || t.updated_at, ',') FROM family JOIN task t ON t.id = family.ext) AS tasks,
|
|
34
|
+
(SELECT count(*) || ':' || coalesce(max(r.id), 0) || ':' || coalesce(sum(r.outcome IS NULL), 0) || ':' || coalesce(max(coalesce(r.finished_at, r.started_at)), '') || ':' || coalesce(group_concat(r.phase), '')
|
|
35
|
+
FROM run r WHERE r.task_ref IN (SELECT ref FROM family)) AS runs,
|
|
36
|
+
(SELECT count(*) || ':' || coalesce(max(d.id), 0) || ':' || coalesce(sum(d.state = 'answered'), 0) FROM decision d JOIN run r ON r.id = d.run WHERE r.task_ref IN (SELECT ref FROM family)) AS decisions,
|
|
37
|
+
(SELECT coalesce(max(c.id), 0) FROM run_checkpoint c WHERE c.task_ref IN (SELECT ref FROM family)) AS progress,
|
|
38
|
+
(SELECT coalesce(max(a.at), '') FROM run_activity a JOIN run r ON r.id = a.run WHERE r.task_ref IN (SELECT ref FROM family) AND r.outcome IS NULL) AS activity,
|
|
39
|
+
(SELECT coalesce(group_concat(w.heartbeat_at > ?2), '') FROM run r JOIN runner w ON w.name = r.runner WHERE r.task_ref IN (SELECT ref FROM family) AND r.outcome IS NULL) AS workers
|
|
40
|
+
WHERE EXISTS (SELECT 1 FROM family)`).get(root, new Date(now.getTime() - livenessMs).toISOString(), BUILT_IN);
|
|
41
|
+
return row === undefined ? null : createHash("sha256").update(Object.values(row).map(String).join("|")).digest("hex").slice(0, 16);
|
|
42
|
+
}
|
|
43
|
+
/** Rooms keyed by the task family's root. With a bus, a write reaches every open page at once; without one, only the safety net. */
|
|
44
|
+
export function createTaskRooms(fingerprint, options = {}) {
|
|
45
|
+
// Everyone on the task but you, once each, by name.
|
|
46
|
+
return createLiveRooms(fingerprint, (viewers, me) => [...new Set(viewers.map(one => one.name).filter(name => name !== me))].sort((a, b) => a.localeCompare(b)), options);
|
|
47
|
+
}
|
package/dist/work-index.js
CHANGED
|
@@ -242,6 +242,17 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
242
242
|
FROM claimed c LEFT JOIN action_ledger completed ON completed.id=c.checked_id WHERE ($state IS NULL OR c.state=$state) AND ($leadId IS NULL OR EXISTS(SELECT 1 FROM team_task_owner own WHERE own.task_ref=c.root_ref AND own.lead=$leadId))
|
|
243
243
|
)`;
|
|
244
244
|
const registered = new WeakMap();
|
|
245
|
+
const statements = new WeakMap();
|
|
246
|
+
/** The page query is long and has one text per view filter: compile each once per connection. */
|
|
247
|
+
function prepared(store, sql) {
|
|
248
|
+
let cache = statements.get(store.handle);
|
|
249
|
+
if (cache === undefined)
|
|
250
|
+
statements.set(store.handle, cache = new Map());
|
|
251
|
+
let statement = cache.get(sql);
|
|
252
|
+
if (statement === undefined)
|
|
253
|
+
cache.set(sql, statement = store.handle.prepare(sql));
|
|
254
|
+
return statement;
|
|
255
|
+
}
|
|
245
256
|
/** Connection-local pure parsing, with exactly the authoritative stored-scope
|
|
246
257
|
* validator. The callback cannot query, inspect files or grant authority. */
|
|
247
258
|
function registerValidators(store) {
|
|
@@ -358,7 +369,7 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
358
369
|
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(WORK_INDEX_MAX_LIMIT, Math.floor(options.limit))) : WORK_INDEX_PAGE_LIMIT;
|
|
359
370
|
const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
|
|
360
371
|
const filter = view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1' : view === 'completed' ? "code='complete'" : '1';
|
|
361
|
-
const rows = store
|
|
372
|
+
const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
|
|
362
373
|
SELECT * FROM ranked WHERE ${filter} AND ($cursorRoot=0 OR
|
|
363
374
|
${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
|
|
364
375
|
ORDER BY ${ORDER},sort_at DESC,root_ref DESC LIMIT $limit
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Store } from './store.js';
|
|
2
|
+
/** The service_cursor row the revision triggers move (live-bus.ts listens for it). */
|
|
3
|
+
export declare const WORKSPACE_REVISION_KEY = "workspace-content:v1";
|
|
2
4
|
export type WorkspaceRevision = {
|
|
3
5
|
current(): string;
|
|
4
6
|
expiresAt(now: Date): number;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* metadata only: it never grants access or certifies task/process state. */
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { DEFAULT_LIVENESS_MS } from './runner.js';
|
|
5
|
-
|
|
5
|
+
/** The service_cursor row the revision triggers move (live-bus.ts listens for it). */
|
|
6
|
+
export const WORKSPACE_REVISION_KEY = 'workspace-content:v1';
|
|
7
|
+
const KEY = WORKSPACE_REVISION_KEY;
|
|
6
8
|
const PREFIX = 'workspace_revision_v1_';
|
|
7
9
|
const MAX_AGE_MS = 60_000;
|
|
8
10
|
const OMIT = new Set(['schema_version', 'service_cursor', 'wake', 'notification_delivery',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.48",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|