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.
Files changed (52) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/activity-line.d.ts +26 -0
  3. package/dist/activity-line.js +47 -0
  4. package/dist/browser/workspace.js +25 -25
  5. package/dist/browser-workspace.d.ts +18 -1
  6. package/dist/browser-workspace.js +9 -3
  7. package/dist/builder.js +4 -2
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +4 -0
  10. package/dist/contracts/cli.d.ts +87 -0
  11. package/dist/contracts/cli.js +2 -0
  12. package/dist/contracts/health.d.ts +84 -0
  13. package/dist/contracts/health.js +11 -0
  14. package/dist/flow-card-task.d.ts +2 -0
  15. package/dist/flow-card-task.js +57 -0
  16. package/dist/flow-live.d.ts +7 -15
  17. package/dist/flow-live.js +77 -115
  18. package/dist/flows-ui.js +15 -1
  19. package/dist/health-cli.d.ts +3 -0
  20. package/dist/health-cli.js +79 -0
  21. package/dist/live-bus.d.ts +89 -0
  22. package/dist/live-bus.js +321 -0
  23. package/dist/live.d.ts +14 -0
  24. package/dist/live.js +71 -22
  25. package/dist/metrics.d.ts +3 -0
  26. package/dist/metrics.js +53 -1
  27. package/dist/operate.js +43 -25
  28. package/dist/planner.js +4 -3
  29. package/dist/read-executor-worker.d.ts +1 -0
  30. package/dist/read-executor-worker.js +26 -0
  31. package/dist/read-executor.d.ts +49 -0
  32. package/dist/read-executor.js +119 -0
  33. package/dist/request-budget.d.ts +1 -0
  34. package/dist/request-budget.js +6 -0
  35. package/dist/serve.d.ts +7 -0
  36. package/dist/serve.js +118 -42
  37. package/dist/server-telemetry.d.ts +86 -0
  38. package/dist/server-telemetry.js +263 -0
  39. package/dist/setup-guide.d.ts +2 -2
  40. package/dist/sqlite-telemetry.d.ts +9 -0
  41. package/dist/sqlite-telemetry.js +98 -0
  42. package/dist/store.d.ts +22 -0
  43. package/dist/store.js +59 -3
  44. package/dist/surface.js +2 -1
  45. package/dist/task-activity.d.ts +27 -0
  46. package/dist/task-activity.js +53 -0
  47. package/dist/task-live.d.ts +8 -0
  48. package/dist/task-live.js +47 -0
  49. package/dist/work-index.js +12 -1
  50. package/dist/workspace-revision.d.ts +2 -0
  51. package/dist/workspace-revision.js +3 -1
  52. 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
+ }
@@ -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.handle.prepare(`${projection}, selected_page AS MATERIALIZED (
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
- const KEY = 'workspace-content:v1';
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.46",
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"