toolroll 0.9.73 → 0.9.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +6 -0
- package/dist/builder.js +1 -1
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-quiet.js +3 -3
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +35 -2
- package/dist/exec.js +13 -16
- package/dist/first-run.d.ts +17 -1
- package/dist/first-run.js +38 -0
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/operate.js +3 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/release-adapters.js +4 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +3 -0
- package/dist/repository-context-ui.js +2 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +34 -23
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +88 -0
- package/dist/server/chat.js +84 -30
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +81 -1
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +8 -4
- package/dist/server/render-pages.js +55 -50
- package/dist/server/render-tasks.js +10 -3
- package/dist/server/route-table.js +5 -1
- package/dist/server/runtime.d.ts +1 -1
- package/dist/server/settings.js +3 -3
- package/dist/server/task-views.js +1 -1
- package/dist/server/tasks.js +59 -41
- package/dist/store.d.ts +31 -1
- package/dist/store.js +75 -2
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subscription-chat.js +5 -3
- 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
|
+
}
|
package/dist/live-records.d.ts
CHANGED
|
@@ -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(
|
|
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 {};
|
package/dist/live-records.js
CHANGED
|
@@ -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(
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
100
|
-
*
|
|
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):
|
|
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
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
};
|