toolroll 0.9.73 → 0.9.75

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 (145) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/dist/agent-turns.js +5 -0
  4. package/dist/agents-live.d.ts +31 -0
  5. package/dist/agents-live.js +43 -0
  6. package/dist/assignment.d.ts +3 -0
  7. package/dist/assignment.js +27 -8
  8. package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
  9. package/dist/browser/workspace.css +2 -2
  10. package/dist/browser/workspace.js +31 -30
  11. package/dist/browser-workspace.d.ts +7 -1
  12. package/dist/builder.js +1 -1
  13. package/dist/chat-acceptance.d.ts +4 -1
  14. package/dist/chat-acceptance.js +13 -3
  15. package/dist/chat-continuity.js +10 -0
  16. package/dist/chat-quiet.js +3 -3
  17. package/dist/cli.js +1 -1
  18. package/dist/coding-handoff.js +4 -3
  19. package/dist/coding-ui.js +2 -2
  20. package/dist/coding-workspace.js +16 -4
  21. package/dist/contracts/console-api.d.ts +5 -1
  22. package/dist/contracts/console-api.js +10 -0
  23. package/dist/control-ui.d.ts +3 -2
  24. package/dist/control-ui.js +7 -2
  25. package/dist/demo.js +5 -3
  26. package/dist/deploy-gate.js +5 -2
  27. package/dist/desktop-update.d.ts +4 -1
  28. package/dist/desktop-update.js +43 -9
  29. package/dist/dispose.js +35 -2
  30. package/dist/exec.js +13 -16
  31. package/dist/first-run.d.ts +20 -1
  32. package/dist/first-run.js +71 -0
  33. package/dist/flow-card-task.js +3 -2
  34. package/dist/flow-unattended.d.ts +2 -0
  35. package/dist/flow-unattended.js +12 -1
  36. package/dist/integrations.d.ts +4 -3
  37. package/dist/integrations.js +5 -6
  38. package/dist/invoke.d.ts +13 -0
  39. package/dist/invoke.js +21 -12
  40. package/dist/knowledge-ui.d.ts +4 -1
  41. package/dist/knowledge-ui.js +10 -5
  42. package/dist/lead-contract.d.ts +1 -1
  43. package/dist/lead-contract.js +2 -2
  44. package/dist/lead-follow.js +3 -1
  45. package/dist/lead-progress.d.ts +4 -1
  46. package/dist/lead-progress.js +3 -1
  47. package/dist/lead.js +4 -0
  48. package/dist/live-bus.d.ts +2 -0
  49. package/dist/live-bus.js +1 -0
  50. package/dist/live-children-read.d.ts +52 -0
  51. package/dist/live-children-read.js +333 -0
  52. package/dist/live-children.d.ts +73 -0
  53. package/dist/live-children.js +318 -0
  54. package/dist/live-detail.d.ts +4 -3
  55. package/dist/live-detail.js +9 -5
  56. package/dist/live-input.js +7 -8
  57. package/dist/live-main-read.d.ts +19 -0
  58. package/dist/live-main-read.js +91 -0
  59. package/dist/live-open.d.ts +28 -0
  60. package/dist/live-open.js +125 -0
  61. package/dist/live-records.d.ts +10 -1
  62. package/dist/live-records.js +16 -5
  63. package/dist/live.d.ts +54 -22
  64. package/dist/live.js +218 -69
  65. package/dist/openrouter-models.d.ts +5 -2
  66. package/dist/openrouter-models.js +7 -4
  67. package/dist/operate.js +5 -2
  68. package/dist/peek-cli.d.ts +10 -3
  69. package/dist/peek-cli.js +17 -8
  70. package/dist/planner.js +1 -1
  71. package/dist/project-learning.js +44 -6
  72. package/dist/project-tools.js +19 -1
  73. package/dist/provider-limits.d.ts +3 -1
  74. package/dist/provider-limits.js +5 -2
  75. package/dist/provider.js +6 -12
  76. package/dist/pull-request-ui.d.ts +1 -1
  77. package/dist/pull-request-ui.js +5 -1
  78. package/dist/push.d.ts +2 -0
  79. package/dist/push.js +24 -19
  80. package/dist/read-only-request.d.ts +4 -0
  81. package/dist/read-only-request.js +15 -0
  82. package/dist/release-adapters.js +4 -0
  83. package/dist/release.d.ts +3 -0
  84. package/dist/release.js +3 -0
  85. package/dist/repository-context-ui.js +2 -1
  86. package/dist/repository-context.js +6 -1
  87. package/dist/result-follow-ups.js +20 -1
  88. package/dist/result-review.js +5 -1
  89. package/dist/run-efficiency.d.ts +3 -3
  90. package/dist/run-efficiency.js +20 -8
  91. package/dist/scout.d.ts +3 -0
  92. package/dist/scout.js +5 -3
  93. package/dist/serve.js +196 -29
  94. package/dist/server/agents-live-filed.d.ts +17 -0
  95. package/dist/server/agents-live-filed.js +97 -0
  96. package/dist/server/agents-live-lead.d.ts +47 -0
  97. package/dist/server/agents-live-lead.js +116 -0
  98. package/dist/server/agents-live-room.d.ts +23 -0
  99. package/dist/server/agents-live-room.js +91 -0
  100. package/dist/server/chat.js +115 -53
  101. package/dist/server/chrome.js +2 -2
  102. package/dist/server/console.css +68 -40
  103. package/dist/server/http.d.ts +5 -0
  104. package/dist/server/last-checks.d.ts +17 -0
  105. package/dist/server/last-checks.js +22 -0
  106. package/dist/server/live.d.ts +4 -0
  107. package/dist/server/live.js +42 -2
  108. package/dist/server/pages.js +98 -4
  109. package/dist/server/people-tokens.js +7 -4
  110. package/dist/server/render-chat.d.ts +15 -1
  111. package/dist/server/render-chat.js +14 -7
  112. package/dist/server/render-pages.d.ts +10 -5
  113. package/dist/server/render-pages.js +83 -74
  114. package/dist/server/render-results.d.ts +11 -2
  115. package/dist/server/render-results.js +54 -12
  116. package/dist/server/render-settings.js +3 -3
  117. package/dist/server/render-tasks.js +13 -3
  118. package/dist/server/route-table.js +19 -2
  119. package/dist/server/runtime.d.ts +17 -2
  120. package/dist/server/settings.js +85 -32
  121. package/dist/server/task-views.js +1 -2
  122. package/dist/server/tasks.js +70 -43
  123. package/dist/storage-ui.d.ts +2 -0
  124. package/dist/storage-ui.js +1 -1
  125. package/dist/storage.js +2 -1
  126. package/dist/store.d.ts +34 -1
  127. package/dist/store.js +108 -11
  128. package/dist/stream-scope.d.ts +18 -0
  129. package/dist/stream-scope.js +28 -0
  130. package/dist/subagent-work.js +3 -0
  131. package/dist/subscription-chat.js +5 -3
  132. package/dist/task-status.d.ts +28 -1
  133. package/dist/task-status.js +93 -11
  134. package/dist/teardown-git.d.ts +27 -1
  135. package/dist/teardown-git.js +185 -39
  136. package/dist/telegram-progress.js +4 -4
  137. package/dist/telegram-status.js +8 -3
  138. package/dist/tool-secret-store.js +8 -0
  139. package/dist/toolroll-update-ui.d.ts +2 -0
  140. package/dist/toolroll-update-ui.js +4 -3
  141. package/dist/toolroll-update.js +10 -9
  142. package/dist/work-index.js +29 -12
  143. package/dist/workspace-ui.d.ts +5 -0
  144. package/dist/workspace-ui.js +38 -10
  145. package/package.json +1 -1
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Opening and reading a live file for the Agents view (docs/plans/agents-view.md §3, N4, N5): one opener and one
3
+ * bounded line reader, shared by every reader and observer that slice adds.
4
+ *
5
+ * The opener. Node has no openat, so a file can't be opened relative to a held directory; instead the open is checked
6
+ * on both sides. Before: the live directory is a real directory (not a link), owned by this process's user, writable
7
+ * by no one else, and it resolves inside the data root. The file is opened O_NOFOLLOW | O_NONBLOCK (a link or a FIFO
8
+ * never blocks or redirects), and its descriptor must be a plain file with exactly one link, this user's, on the
9
+ * directory's device. After: the directory path still names the same directory, and the file path still names the
10
+ * very file opened. Anything else is "unavailable" — never a guess, never another file's contents or timing.
11
+ *
12
+ * The reader. One request reads at most READ_WINDOW bytes and looks at most SCAN_BUDGET past its start for a line's
13
+ * end: a longer line is skipped and said so, and the scan resumes past it on the next request rather than blocking
14
+ * this one. An unfinished last line stays pending.
15
+ */
16
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
17
+ import { join, sep } from "node:path";
18
+ import { liveDir } from "./live.js";
19
+ export const READ_WINDOW = 64 * 1024;
20
+ export const SCAN_BUDGET = 256 * 1024;
21
+ const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
22
+ const uid = () => (typeof process.getuid === "function" ? BigInt(process.getuid()) : null);
23
+ /** Open `name` in the run's live directory under `root`, or say it is missing or unavailable. */
24
+ export function openLiveTrusted(root, name, seams = {}) {
25
+ if (!/^\d{1,15}\.(?:jsonl|sub\.jsonl)$/.test(name))
26
+ return "unavailable";
27
+ const dir = liveDir(root);
28
+ /** The live directory as it must be: a real directory of this user's, writable by no one else, inside the root. */
29
+ const trustedDir = () => {
30
+ const stat = lstatSync(dir, { bigint: true });
31
+ const real = realpathSync(dir), base = realpathSync(root), me = uid();
32
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 18n) !== 0n || !real.startsWith(base + sep))
33
+ return null;
34
+ return me !== null && stat.uid !== me ? null : stat;
35
+ };
36
+ let before;
37
+ try {
38
+ const checked = trustedDir();
39
+ if (checked === null)
40
+ return "unavailable";
41
+ before = checked;
42
+ }
43
+ catch (error) {
44
+ return error.code === "ENOENT" ? "missing" : "unavailable";
45
+ }
46
+ const path = join(dir, name);
47
+ let fd;
48
+ try {
49
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
50
+ }
51
+ catch (error) {
52
+ return error.code === "ENOENT" ? "missing" : "unavailable";
53
+ }
54
+ try {
55
+ seams.afterOpen?.();
56
+ const me = uid();
57
+ const ours = (stat) => stat.isFile() && stat.nlink === 1n && stat.dev === before.dev && (me === null || stat.uid === me);
58
+ const file = fstatSync(fd, { bigint: true });
59
+ if (!ours(file))
60
+ throw new Error("not ours");
61
+ // After the open: the whole directory predicate again (not just its identity), the path still naming this file,
62
+ // and the descriptor still this user's single-link plain file, right before it is accepted.
63
+ const after = trustedDir();
64
+ if (after === null || !same(after, before) || !same(lstatSync(path, { bigint: true }), file))
65
+ throw new Error("moved");
66
+ const now = fstatSync(fd, { bigint: true });
67
+ if (!ours(now) || !same(now, file))
68
+ throw new Error("changed");
69
+ return { fd, gen: `${now.dev}:${now.ino}:${now.birthtimeNs}`, size: Number(now.size) };
70
+ }
71
+ catch {
72
+ try {
73
+ closeSync(fd);
74
+ }
75
+ catch { /* already */ }
76
+ return "unavailable";
77
+ }
78
+ }
79
+ /** Read complete lines from `from` (discarding first through the next newline when `skipping`), within the budgets. */
80
+ export function readLiveLines(fd, size, from, skipping = false) {
81
+ const out = { lines: [], next: from, skipped: 0, skipping: false };
82
+ if (from >= size)
83
+ return { ...out, skipping };
84
+ const want = Math.min(size - from, READ_WINDOW);
85
+ const buffer = Buffer.alloc(want);
86
+ let got = 0;
87
+ while (got < want) {
88
+ const n = readSync(fd, buffer, got, want - got, from + got);
89
+ if (n <= 0)
90
+ break;
91
+ got += n;
92
+ }
93
+ let at = 0;
94
+ if (skipping) {
95
+ const lf = buffer.subarray(0, got).indexOf(0x0a);
96
+ if (lf === -1)
97
+ return { ...out, next: from + got, skipping: true };
98
+ at = lf + 1;
99
+ }
100
+ for (;;) {
101
+ const lf = buffer.subarray(at, got).indexOf(0x0a);
102
+ if (lf === -1)
103
+ break;
104
+ out.lines.push({ text: buffer.subarray(at, at + lf).toString("utf8"), end: from + at + lf + 1 });
105
+ at += lf + 1;
106
+ }
107
+ out.next = from + at;
108
+ // Nothing whole in a full window: one line longer than the window. Look on, within the budget, for its end.
109
+ if (out.lines.length === 0 && !skipping && got === READ_WINDOW) {
110
+ const scan = Buffer.alloc(16 * 1024);
111
+ for (let pos = from + got; pos < Math.min(size, from + SCAN_BUDGET);) {
112
+ const n = readSync(fd, scan, 0, Math.min(scan.length, from + SCAN_BUDGET - pos, size - pos), pos);
113
+ if (n <= 0)
114
+ break;
115
+ const lf = scan.subarray(0, n).indexOf(0x0a);
116
+ if (lf !== -1)
117
+ return { ...out, next: pos + lf + 1, skipped: 1 };
118
+ pos += n;
119
+ }
120
+ // Past the budget (or the end so far) still inside it: resume discarding there next time.
121
+ const stop = Math.min(size, from + SCAN_BUDGET);
122
+ return { ...out, next: stop, skipped: 1, skipping: true };
123
+ }
124
+ return out;
125
+ }
@@ -104,12 +104,20 @@ export declare function liveOutput(raw: string, keys?: KeyState): string[];
104
104
  export declare function livePath(base: string | null, given: unknown): string | null;
105
105
  /** A command's first line, "…" when more followed or it was cut. */
106
106
  export declare function liveCommand(given: unknown): string | null;
107
+ declare const short: (given: unknown) => string | null;
108
+ /** A short display form of one field: its first line, cleaned, redacted and cut (a delegated task's description). */
109
+ export declare const liveShort: typeof short;
107
110
  export type LiveRecorderOptions = {
108
111
  /** The run's own checkout; null when it has none (every path is "a file"). */
109
112
  base: string | null;
110
113
  /** The project's "Show commands in the live view" setting, read at open. */
111
114
  commands: boolean;
112
115
  clock?: () => Date;
116
+ /** The key-block state to carry (the caller's, so it can write it down in a checkpoint); a fresh one by default. */
117
+ keyState?: KeyState;
118
+ /** Told of each step as it is recorded: the provider's call id (null when it has none), our id, and the target as
119
+ * written (redacted, under the key state). Observation only: it changes nothing recorded. */
120
+ onStep?: (call: string | null, id: string, target: string | null) => void;
113
121
  };
114
122
  export type LiveRecorder = {
115
123
  /** The records one stream event adds. Never throws. */
@@ -117,7 +125,7 @@ export type LiveRecorder = {
117
125
  /** The process ended: every step still open closes unknown. */
118
126
  end(): LiveRecord[];
119
127
  };
120
- export declare function createLiveRecorder(options: LiveRecorderOptions): LiveRecorder;
128
+ export declare function createLiveRecorder(given: LiveRecorderOptions): LiveRecorder;
121
129
  /** One record as its file line; an oversized one becomes `cut`. */
122
130
  export declare function serializeLiveRecord(record: LiveRecord): string;
123
131
  /**
@@ -138,3 +146,4 @@ export declare function stepWords(step: {
138
146
  }): string;
139
147
  /** Records as the plain lines the run page and `toolroll peek` show. */
140
148
  export declare function projectLiveText(records: readonly LiveRecord[]): string[];
149
+ export {};
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { isAbsolute, relative, resolve, sep } from "node:path";
17
17
  import { scanForSecrets } from "./evidence.js";
18
+ import { topLevelEvent } from "./stream-scope.js";
18
19
  export const LIVE_ACTIVITY_KINDS = ["session", "message", "edit", "command", "read", "search", "lookup", "plan", "delegate", "tool"];
19
20
  /** A record's serialized size limit: a longer one is replaced by `cut`. */
20
21
  export const LIVE_RECORD_CAP = 8 * 1024;
@@ -349,6 +350,8 @@ function liveHost(given) {
349
350
  }
350
351
  }
351
352
  const short = (given) => (typeof given === "string" ? liveTarget(given.split(/\r?\n/)[0] ?? "", SHORT_CAP) : null);
353
+ /** A short display form of one field: its first line, cleaned, redacted and cut (a delegated task's description). */
354
+ export const liveShort = short;
352
355
  /** Claude tools: our kind, and whether it is a command. */
353
356
  const CLAUDE_KINDS = new Map([
354
357
  ["Edit", "edit"], ["Write", "edit"], ["MultiEdit", "edit"], ["NotebookEdit", "edit"],
@@ -393,11 +396,14 @@ function resultText(content) {
393
396
  .map(part => String(part["text"]))
394
397
  .join("\n");
395
398
  }
396
- export function createLiveRecorder(options) {
397
- const clock = options.clock ?? (() => new Date());
399
+ export function createLiveRecorder(given) {
400
+ // The settings are read once, here: nothing the caller (or a callback) changes later reaches the recording.
401
+ const options = Object.freeze({ base: given.base, commands: given.commands === true });
402
+ const notifyStep = given.onStep;
403
+ const clock = given.clock ?? (() => new Date());
398
404
  let sequence = 0;
399
405
  /** A key block the agent began in one message stays hidden through the next, until it ends or a new session starts. */
400
- const keyState = { inKey: false };
406
+ const keyState = given.keyState ?? { inKey: false };
401
407
  /** Provider call id → our step, while it is open. */
402
408
  const open = new Map();
403
409
  const stamp = () => clock().toISOString();
@@ -412,6 +418,12 @@ export function createLiveRecorder(options) {
412
418
  const id = `s${++sequence}`;
413
419
  const keyed = keyedTarget(target, keyState);
414
420
  const step = { k: "step", at, id, kind, ...(keyed === null ? {} : { target: keyed }) };
421
+ // Detached: called with no receiver, so it never sees (or changes) the settings above.
422
+ try {
423
+ if (notifyStep !== undefined)
424
+ notifyStep(call, id, keyed);
425
+ }
426
+ catch { /* observation only */ }
415
427
  if (call === null)
416
428
  return [{ ...step, once: true }];
417
429
  if (open.size >= OPEN_STEPS_CAP)
@@ -440,8 +452,7 @@ export function createLiveRecorder(options) {
440
452
  const claude = (event, at) => {
441
453
  const type = event["type"];
442
454
  // A subagent's words, tools, turns and sessions are not this run's conversation — and never close its steps.
443
- const parent = event["parent_tool_use_id"];
444
- if (parent !== undefined && parent !== null)
455
+ if (!topLevelEvent(event))
445
456
  return [];
446
457
  if (type === "system")
447
458
  return event["subtype"] === "init" ? (keyState.inKey = false, [...closeAll(at), { k: "start", at }]) : [];
package/dist/live.d.ts CHANGED
@@ -19,13 +19,20 @@ export declare const LIVE_FILE_CAP: number;
19
19
  export declare const LIVE_REDACTED_LINE = "[a credential-shaped line was redacted]";
20
20
  /** Finalized runs keep their live file this long for the final drain. */
21
21
  export declare const LIVE_RETAIN_MS: number;
22
- /** The bounded sweep: at most this many FINALIZED files survive a pass. */
22
+ /** The bounded sweep: at most this many FINALIZED runs' files survive a pass (a run's records and its subagents' file
23
+ * count as one). */
23
24
  export declare const LIVE_FILE_BOUND = 500;
25
+ /** A run's subagent file (docs/plans/agents-view.md §2.3) is never larger than this; its writer stops there. */
26
+ export declare const LIVE_SUB_FILE_CAP: number;
27
+ /** All finished and orphan live files together stay under this; the oldest runs go first past it. */
28
+ export declare const LIVE_TOTAL_BYTES: number;
24
29
  export declare function liveDir(root: string): string;
25
30
  /** The exact filename, from the numeric id and nothing else — no user
26
31
  * input is ever parsed into this path. Records (`.jsonl`) are what every
27
32
  * run writes now; the name is the format. */
28
33
  export declare function liveLogPath(root: string, runId: number): string;
34
+ /** A run's subagents' records (docs/plans/agents-view.md §2.3): the run's file, swept with it. */
35
+ export declare function liveSubLogPath(root: string, runId: number): string;
29
36
  /** The plain-text file runs wrote before the live conversation; read until it ages out. */
30
37
  export declare function legacyLiveLogPath(root: string, runId: number): string;
31
38
  export { LIVE_ACTIVITY_KINDS, type LiveActivityKind } from "./live-records.js";
@@ -78,43 +85,68 @@ export type LiveWindowRead = {
78
85
  * finalization. Legacy text is cleaned again on the way out.
79
86
  */
80
87
  export declare function readLiveWindow(root: string, runId: number, from: number, allowTail?: boolean): LiveWindowRead;
81
- /** A window's complete lines as the plain words the run page and `toolroll peek` show, whatever its format. */
88
+ /** A window's complete lines as the plain words the run page and `toolroll peek` show, whatever its format. `commands` is
89
+ * the run's own captured answer (store.ts runLiveCommands), never the file's say. */
82
90
  export declare function liveWindowLines(read: {
83
91
  text: string;
84
92
  format: LiveFormat;
85
- }, keys?: KeyState): string[];
86
- /** A window as records, whatever its format: an older plain file's lines become the agent's words. */
93
+ }, commands: boolean, keys?: KeyState): string[];
94
+ /** A window as records, whatever its format: an older plain file's lines become the agent's words. Command text and
95
+ * output show only when the run's own captured answer (`commands`) is on. */
87
96
  export declare function liveWindowRecords(read: {
88
97
  text: string;
89
98
  format: LiveFormat;
90
- }, keys?: KeyState): LiveRecord[];
99
+ }, commands: boolean, keys?: KeyState): LiveRecord[];
100
+ /**
101
+ * Enforce a run's captured "Show commands" answer on read: the file is build-writable, so an `open` record, a command
102
+ * step's text or a step's output may say anything. With the answer off, every `open` reads the base tier, command steps
103
+ * lose their text and steps lose their output. (The older plain-text format predates command detail and is unchanged.)
104
+ */
105
+ export declare function withholdCommands(records: LiveRecord[], commands: boolean): LiveRecord[];
91
106
  export type LiveSweepVerdict = {
92
107
  /** run id, or null when the name is not a run's file. */
93
108
  runId: number | null;
94
109
  fileName: string;
95
- mtimeMs: number;
110
+ /** The most a file of this name is ever written to (records, subagents, older text); null for a name we never write. */
111
+ cap: number | null;
96
112
  };
97
113
  /**
98
- * Enumerate the live directory for the reconcile sweep. Pure listing — the
99
- * CALLER decides which runs are finalized and when; this module only
100
- * refuses to guess. Non-run names are reported with runId null (orphans
101
- * from nothing this plane wrote; aged on mtime like everything else).
114
+ * Enumerate the live directory for the reconcile sweep: names only. Pure listing — the CALLER settles which runs are
115
+ * finalized before it measures anything; this module only refuses to guess. Non-run names are reported with runId
116
+ * null (orphans from nothing this plane wrote; aged on mtime like everything else).
102
117
  */
103
118
  export declare function listLiveFiles(root: string): LiveSweepVerdict[];
104
- /** Remove one live file, silently: a sweep never throws over display state. */
105
- export declare function removeLiveFile(root: string, fileName: string): void;
119
+ /** Remove one live file, silently: a sweep never throws over display state. True when it is gone now. */
120
+ export declare function removeLiveFile(root: string, fileName: string, unlink?: (path: string) => void): boolean;
121
+ type SweepRun = {
122
+ outcome: string | null;
123
+ finishedAt: string | null;
124
+ };
125
+ /** What the sweep reads of the store: a run's state, and (production) the write lock run admission also takes. */
126
+ export type LiveSweepStore = {
127
+ getRun(id: number): SweepRun | null;
128
+ transact?<T>(body: () => T): T;
129
+ };
106
130
  /**
107
131
  * The reconcile sweep (arc 1 §2): ACTIVE runs are exempt ABSOLUTELY — the
108
- * transport's no-truncation assumption depends on it. Finalized runs age
109
- * from finished_at; orphans (no run row, or a name this plane never
110
- * writes) age from mtime; and the 500-file bound applies only WITHIN the
111
- * finalized-or-orphan set, oldest first.
132
+ * transport's no-truncation assumption depends on it — and a run's files
133
+ * (its records, its subagents' file, an older text file) go together, as one.
134
+ * A run is settled finished (or absent) BEFORE its files are measured.
135
+ * Finalized runs age from finished_at; orphans (no run row, or a name this
136
+ * plane never writes) age from mtime. Among the finalized-or-orphan: a file
137
+ * far past its own cap goes at once, then the day's retention, then the
138
+ * 500-run bound, then the total byte limit, oldest first, until each limit is
139
+ * really met or nothing eligible is left. Each removal re-proves, under the
140
+ * store's write lock (the one run admission takes), that the run is still
141
+ * not active and the file is the very one measured; otherwise it is kept. A
142
+ * file that can't be removed stays counted and never stops the rest.
112
143
  */
113
- export declare function sweepLiveLogs(store: {
114
- getRun(id: number): {
115
- outcome: string | null;
116
- finishedAt: string | null;
117
- } | null;
118
- }, root: string, now: Date): {
144
+ export declare function sweepLiveLogs(store: LiveSweepStore, root: string, now: Date, io?: {
145
+ unlink?: (path: string) => void;
146
+ lstat?: (path: string) => import("node:fs").Stats;
147
+ /** Test seams: before a removal starts, and right after its run-state check (before the claim). */
148
+ beforeRemove?: (fileName: string) => void;
149
+ afterCheck?: (fileName: string) => void;
150
+ }): {
119
151
  removed: string[];
120
152
  };