toolroll 0.9.72 → 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.
Files changed (137) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/dist/agent-fence.d.ts +5 -0
  4. package/dist/agent-fence.js +42 -7
  5. package/dist/agent-turns.js +5 -0
  6. package/dist/agents-live.d.ts +31 -0
  7. package/dist/agents-live.js +43 -0
  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 +6 -0
  12. package/dist/builder.js +1 -1
  13. package/dist/chat-actions.js +13 -8
  14. package/dist/chat-continuity.js +10 -0
  15. package/dist/chat-decide.d.ts +47 -1
  16. package/dist/chat-decide.js +170 -4
  17. package/dist/chat-quiet.js +3 -3
  18. package/dist/check-report.js +2 -1
  19. package/dist/coding-provider.js +4 -2
  20. package/dist/commit-extract.d.ts +17 -0
  21. package/dist/commit-extract.js +82 -4
  22. package/dist/contracts/console-api.d.ts +3 -3
  23. package/dist/contracts/console-api.js +4 -4
  24. package/dist/contracts/release-terms.d.ts +7 -1
  25. package/dist/contracts/release-terms.js +6 -2
  26. package/dist/deploy-gate.js +5 -2
  27. package/dist/desktop-update.d.ts +1 -1
  28. package/dist/dispose.js +35 -2
  29. package/dist/exec.d.ts +1 -1
  30. package/dist/exec.js +20 -19
  31. package/dist/first-run.d.ts +17 -1
  32. package/dist/first-run.js +38 -0
  33. package/dist/flow-gallery-ui.js +4 -2
  34. package/dist/flow-gallery.d.ts +3 -0
  35. package/dist/flow-gallery.js +6 -2
  36. package/dist/flow-triggers.d.ts +8 -0
  37. package/dist/flow-triggers.js +15 -0
  38. package/dist/flows-cli.js +13 -3
  39. package/dist/guides.js +10 -1
  40. package/dist/invoke.d.ts +13 -0
  41. package/dist/invoke.js +21 -12
  42. package/dist/kits-ui.js +4 -2
  43. package/dist/kits.d.ts +6 -4
  44. package/dist/kits.js +9 -11
  45. package/dist/knowledge-ui.d.ts +4 -1
  46. package/dist/knowledge-ui.js +10 -5
  47. package/dist/lead-progress.d.ts +4 -1
  48. package/dist/lead-progress.js +3 -1
  49. package/dist/lead.js +4 -0
  50. package/dist/live-bus.d.ts +2 -0
  51. package/dist/live-bus.js +1 -0
  52. package/dist/live-children-read.d.ts +52 -0
  53. package/dist/live-children-read.js +333 -0
  54. package/dist/live-children.d.ts +73 -0
  55. package/dist/live-children.js +318 -0
  56. package/dist/live-detail.d.ts +4 -3
  57. package/dist/live-detail.js +9 -5
  58. package/dist/live-input.js +7 -8
  59. package/dist/live-main-read.d.ts +19 -0
  60. package/dist/live-main-read.js +91 -0
  61. package/dist/live-open.d.ts +28 -0
  62. package/dist/live-open.js +125 -0
  63. package/dist/live-records.d.ts +10 -1
  64. package/dist/live-records.js +16 -5
  65. package/dist/live.d.ts +54 -22
  66. package/dist/live.js +218 -69
  67. package/dist/mcp-connect.d.ts +10 -0
  68. package/dist/mcp-connect.js +2 -0
  69. package/dist/operate.js +3 -2
  70. package/dist/peek-cli.d.ts +10 -3
  71. package/dist/peek-cli.js +17 -8
  72. package/dist/planner.js +1 -1
  73. package/dist/project-tools.js +3 -1
  74. package/dist/protected-storage.d.ts +67 -0
  75. package/dist/protected-storage.js +398 -0
  76. package/dist/provider-limits.d.ts +3 -1
  77. package/dist/provider-limits.js +5 -2
  78. package/dist/provider.js +6 -12
  79. package/dist/release-adapters.d.ts +8 -1
  80. package/dist/release-adapters.js +84 -14
  81. package/dist/release-authority.d.ts +6 -0
  82. package/dist/release-authority.js +9 -0
  83. package/dist/release-card.d.ts +76 -0
  84. package/dist/release-card.js +252 -0
  85. package/dist/release-carry.d.ts +3 -0
  86. package/dist/release-carry.js +23 -4
  87. package/dist/release-cli.js +36 -3
  88. package/dist/release-git.d.ts +43 -0
  89. package/dist/release-git.js +106 -0
  90. package/dist/release.d.ts +11 -2
  91. package/dist/release.js +82 -6
  92. package/dist/repository-context-ui.js +2 -1
  93. package/dist/run-efficiency.d.ts +3 -3
  94. package/dist/run-efficiency.js +20 -8
  95. package/dist/scope.d.ts +7 -0
  96. package/dist/scope.js +22 -5
  97. package/dist/scout.d.ts +3 -0
  98. package/dist/scout.js +5 -3
  99. package/dist/serve.js +34 -23
  100. package/dist/server/agents-live-lead.d.ts +47 -0
  101. package/dist/server/agents-live-lead.js +116 -0
  102. package/dist/server/agents-live-room.d.ts +23 -0
  103. package/dist/server/agents-live-room.js +88 -0
  104. package/dist/server/chat.js +84 -30
  105. package/dist/server/chrome.js +2 -2
  106. package/dist/server/console.css +68 -40
  107. package/dist/server/flows.js +40 -10
  108. package/dist/server/live.d.ts +4 -0
  109. package/dist/server/live.js +42 -2
  110. package/dist/server/pages.js +81 -1
  111. package/dist/server/render-chat.d.ts +15 -1
  112. package/dist/server/render-chat.js +14 -7
  113. package/dist/server/render-pages.d.ts +8 -4
  114. package/dist/server/render-pages.js +55 -50
  115. package/dist/server/render-settings.js +1 -1
  116. package/dist/server/render-tasks.js +10 -3
  117. package/dist/server/route-table.js +5 -1
  118. package/dist/server/runtime.d.ts +1 -1
  119. package/dist/server/settings.js +29 -8
  120. package/dist/server/task-views.js +1 -1
  121. package/dist/server/tasks.js +59 -41
  122. package/dist/setup-guide.d.ts +2 -2
  123. package/dist/store.d.ts +34 -1
  124. package/dist/store.js +81 -2
  125. package/dist/stream-scope.d.ts +18 -0
  126. package/dist/stream-scope.js +28 -0
  127. package/dist/subagent-memory.d.ts +1 -0
  128. package/dist/subagent-memory.js +1 -1
  129. package/dist/subagent-tools.d.ts +22 -6
  130. package/dist/subagent-tools.js +44 -8
  131. package/dist/subagent-work.d.ts +2 -0
  132. package/dist/subagent-work.js +5 -2
  133. package/dist/subagents-ui.js +6 -4
  134. package/dist/subscription-chat.js +5 -3
  135. package/dist/telegram.d.ts +6 -1
  136. package/dist/telegram.js +58 -6
  137. package/package.json +1 -1
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Reading a run's subagents' file (docs/plans/agents-view.md §2.3, N1): forward from a sealed cursor, or — on a first
3
+ * read or a cursor that doesn't hold — from the newest checkpoint near the end, and never from a guess.
4
+ *
5
+ * The file is display state the machine running the agent could alter, so the reader trusts nothing it reads back:
6
+ * every record is re-checked (shape, caps, its stream open, a close only of its own stream's open step), re-redacted
7
+ * under that stream's own key state, and read under the run's captured "Show commands" answer (never the file's).
8
+ * What fails is quarantined: counted, never shown, never promoted to the run's own conversation.
9
+ *
10
+ * The cursor carries the state the next window needs — offset, file generation, each stream's parent, open steps and
11
+ * key state — bounded (17 streams, 32 open steps, 4 KiB encoded), bound to this run and file, and sealed with an HMAC
12
+ * under a key the caller keeps outside any build's reach. A valid cursor is state, never permission: the caller proves
13
+ * access on every read. One that doesn't verify, or names another run or file, restarts visibly.
14
+ */
15
+ import { createHmac, timingSafeEqual } from "node:crypto";
16
+ import { closeSync, readSync } from "node:fs";
17
+ import { withholdCommands } from "./live.js";
18
+ import { openLiveTrusted, READ_WINDOW, readLiveLines, SCAN_BUDGET } from "./live-open.js";
19
+ import { parseLiveRecord, redactLiveLines, validKeyTail } from "./live-records.js";
20
+ import { CHILD_OPEN_STEPS_CAP, CHILD_STREAMS_CAP } from "./live-children.js";
21
+ export const CURSOR_CAP = 4 * 1024;
22
+ /** Longer than any real key block: past this much of a stream's text with no END marker, its key state is known again. */
23
+ export const UNKNOWN_KEY_BYTES = 64 * 1024;
24
+ const RECORD_CAP = 8 * 1024;
25
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
26
+ const streamNo = (value) => (Number.isInteger(value) && value >= 1 && value <= CHILD_STREAMS_CAP ? value : null);
27
+ const PARENT = /^(?:s|c(?:[1-9]|1[0-6])s)[1-9][0-9]{0,8}$/;
28
+ const childStep = (s) => new RegExp(`^c${s}(s[1-9][0-9]{0,8})$`);
29
+ /** A stream list (from a checkpoint or a cursor) within every bound, or null. */
30
+ function streamsOf(value) {
31
+ if (!Array.isArray(value) || value.length > CHILD_STREAMS_CAP)
32
+ return null;
33
+ const out = [];
34
+ let open = 0;
35
+ for (const raw of value) {
36
+ const one = object(raw);
37
+ const s = streamNo(one?.["s"]);
38
+ if (one === null || s === null || out.some(prior => prior.s === s))
39
+ return null;
40
+ const parent = one["parent"], via = one["via"], ids = one["open"], tail = one["tail"] ?? "";
41
+ if (!(parent === null || (typeof parent === "string" && PARENT.test(parent))))
42
+ return null;
43
+ // A subagent nests under one listed before it: no cycles, nothing dangling.
44
+ if (via !== undefined && (streamNo(via) === null || !out.some(prior => prior.s === via)))
45
+ return null;
46
+ if (!Array.isArray(ids) || !ids.every(id => typeof id === "string" && childStep(s).test(id)) || new Set(ids).size !== ids.length)
47
+ return null;
48
+ if (typeof one["inKey"] !== "boolean" || typeof tail !== "string" || !validKeyTail(tail))
49
+ return null;
50
+ // Absent means the default; an explicit null (or anything else) is not a value these fields take.
51
+ const ended = one["ended"] === undefined ? false : one["ended"], unknown = one["unknown"] === undefined ? 0 : one["unknown"];
52
+ if (typeof ended !== "boolean" || !Number.isSafeInteger(unknown) || unknown < 0 || unknown > UNKNOWN_KEY_BYTES)
53
+ return null;
54
+ open += ids.length;
55
+ out.push({ s, parent: parent, ...(via === undefined ? {} : { via: via }), open: ids, inKey: one["inKey"], tail, ended, unknown: unknown });
56
+ }
57
+ return open <= CHILD_OPEN_STEPS_CAP ? out : null;
58
+ }
59
+ /** Seal a reader's state under the caller's key (the same form for the run's own file and its subagents'). */
60
+ export function sealCursor(state, key) {
61
+ const body = Buffer.from(JSON.stringify(state), "utf8").toString("base64url");
62
+ return `${body}.${createHmac("sha256", key).update(body).digest("base64url")}`;
63
+ }
64
+ /** A sealed state's object when the seal holds and it is within the cap; null otherwise (the caller re-checks it). */
65
+ export function openCursor(cursor, key) {
66
+ if (cursor.length > CURSOR_CAP)
67
+ return null;
68
+ const [body, mac, extra] = cursor.split(".");
69
+ if (body === undefined || mac === undefined || extra !== undefined)
70
+ return null;
71
+ const want = createHmac("sha256", key).update(body).digest();
72
+ const given = Buffer.from(mac, "base64url");
73
+ if (given.length !== want.length || !timingSafeEqual(given, want))
74
+ return null;
75
+ try {
76
+ return object(JSON.parse(Buffer.from(body, "base64url").toString("utf8")));
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ const seal = (state, key) => sealCursor(state, key);
83
+ function unseal(cursor, key, run, gen) {
84
+ try {
85
+ const raw = openCursor(cursor, key);
86
+ const streams = streamsOf(raw?.["streams"]);
87
+ if (raw === null || streams === null || raw["run"] !== run || raw["gen"] !== gen)
88
+ return null;
89
+ const off = raw["off"];
90
+ if (!Number.isSafeInteger(off) || off < 0 || typeof raw["skipping"] !== "boolean" || typeof raw["awaiting"] !== "boolean")
91
+ return null;
92
+ return { run, gen, off: off, skipping: raw["skipping"], awaiting: raw["awaiting"], streams };
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ /** Where a read with no usable cursor begins: the file's start when it is small, else the newest checkpoint in its last
99
+ * SCAN_BUDGET bytes; with none there, nothing is shown until the next checkpoint (fails closed). */
100
+ function bootstrap(fd, size, run, gen) {
101
+ const empty = { run, gen, off: 0, skipping: false, awaiting: false, streams: [] };
102
+ if (size <= SCAN_BUDGET)
103
+ return { state: empty, earlier: false };
104
+ const from = size - SCAN_BUDGET;
105
+ const buffer = Buffer.alloc(SCAN_BUDGET);
106
+ let got = 0;
107
+ while (got < buffer.length) {
108
+ const n = readSync(fd, buffer, got, buffer.length - got, from + got);
109
+ if (n <= 0)
110
+ break;
111
+ got += n;
112
+ }
113
+ const first = buffer.indexOf(0x0a);
114
+ let found = -1;
115
+ for (let at = first + 1; at > 0 && at < got;) {
116
+ const lf = buffer.indexOf(0x0a, at);
117
+ if (lf === -1)
118
+ break;
119
+ if (buffer.subarray(at, lf).indexOf('"k":"ckpt"') !== -1) {
120
+ // A checkpoint this reader can use: one record, within the record cap, whose streams pass every bound.
121
+ try {
122
+ const record = lf - at <= RECORD_CAP ? object(JSON.parse(buffer.subarray(at, lf).toString("utf8"))) : null;
123
+ if (record?.["k"] === "ckpt" && streamsOf(record["streams"]) !== null)
124
+ found = at;
125
+ }
126
+ catch { /* not one */ }
127
+ }
128
+ at = lf + 1;
129
+ }
130
+ return found === -1
131
+ ? { state: { ...empty, off: from, skipping: true, awaiting: true }, earlier: true }
132
+ : { state: { ...empty, off: from + found, awaiting: true }, earlier: true };
133
+ }
134
+ const ISO = (value) => typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value)) ? value : undefined;
135
+ const DISPLAY_KINDS = new Set(["session", "message", "edit", "command", "read", "search", "lookup", "plan", "delegate", "tool"]);
136
+ /** A learned stream's display, from its checkpoint entry: each field checked, text re-redacted under the stream's own key
137
+ * state (unknown for a stream first seen here, so a target reads hidden); anything malformed is simply left out. */
138
+ function displayOf(one, entry, commands) {
139
+ const desc = typeof entry?.["desc"] === "string" && entry["desc"].length <= 200 ? redactLiveLines([entry["desc"]])[0] : undefined;
140
+ const at = ISO(entry?.["at"]);
141
+ const views = [{ k: "child", s: one.s, ...(at === undefined ? {} : { at }), parent: one.parent, ...(one.via === undefined ? {} : { via: one.via }), ...(desc === undefined ? {} : { desc }) }];
142
+ const steps = Array.isArray(entry?.["steps"]) ? entry["steps"].slice(0, CHILD_OPEN_STEPS_CAP) : [];
143
+ for (const raw of steps) {
144
+ const step = object(raw);
145
+ const id = step?.["id"], kind = step?.["kind"], target = step?.["target"];
146
+ if (typeof id !== "string" || !one.open.includes(id) || typeof kind !== "string" || !DISPLAY_KINDS.has(kind) || (target !== undefined && (typeof target !== "string" || target.length > 300)))
147
+ continue;
148
+ const local = childStep(one.s).exec(id)?.[1];
149
+ if (local === undefined)
150
+ continue;
151
+ const keys = { inKey: one.inKey || one.unknown > 0, tail: one.tail };
152
+ const parsed = parseLiveRecord(JSON.stringify({ k: "step", at: at ?? "", id: local, kind, ...(target === undefined ? {} : { target }) }), keys);
153
+ // Under the run's own "Show commands" answer, like every other record.
154
+ const [shown] = parsed === null ? [] : withholdCommands([parsed], commands);
155
+ if (shown !== undefined && shown.k === "step")
156
+ views.push({ ...shown, id, s: one.s });
157
+ }
158
+ if (one.ended) {
159
+ const endedAt = ISO(entry?.["endedAt"]), ok = entry?.["ok"];
160
+ views.push({ k: "end", s: one.s, ...(endedAt === undefined ? {} : { at: endedAt }), ok: ok === true || ok === false ? ok : null });
161
+ }
162
+ return views;
163
+ }
164
+ /** One line under the state: what it shows, or null when it is quarantined (`counted` says whether it counts). */
165
+ /** A record's own time, when it is a plain ISO time (display only). */
166
+ const stampOf = (record) => {
167
+ const at = record["at"];
168
+ return typeof at === "string" && at.length <= 40 && Number.isFinite(Date.parse(at)) ? { at } : {};
169
+ };
170
+ /** The bytes of text a record carries (its words or its output): what uncertainty about a key block runs down by. */
171
+ function scannedText(record) {
172
+ const lines = record["k"] === "say" ? record["t"] : record["k"] === "done" ? record["out"] : undefined;
173
+ return Array.isArray(lines) ? lines.reduce((sum, line) => sum + (typeof line === "string" ? Buffer.byteLength(line, "utf8") : 0), 0) : 0;
174
+ }
175
+ /** A line that couldn't be read may have carried a key marker for any stream: every stream's key state is unknown. */
176
+ function unknowable(state) {
177
+ for (const one of state.streams)
178
+ one.unknown = UNKNOWN_KEY_BYTES;
179
+ }
180
+ function apply(state, text, commands) {
181
+ if (Buffer.byteLength(text, "utf8") > RECORD_CAP) {
182
+ unknowable(state);
183
+ return null;
184
+ }
185
+ let record;
186
+ try {
187
+ record = object(JSON.parse(text));
188
+ }
189
+ catch {
190
+ unknowable(state);
191
+ return null;
192
+ }
193
+ if (record === null)
194
+ return null;
195
+ if (record["k"] === "ckpt") {
196
+ const streams = streamsOf(record["streams"]);
197
+ // A checkpoint names every stream this reader already follows: one that drops a stream would let its number (and
198
+ // its open key block) be reused.
199
+ if (streams === null || !state.streams.every(known => streams.some(next => next.s === known.s)))
200
+ return null;
201
+ // The file's own word on key state never weakens what this reader knows: a stream it has followed keeps its own
202
+ // key state (the checkpoint can only add caution); one it first learns of here starts unknown.
203
+ const learned = streams.filter(next => !state.streams.some(one => one.s === next.s));
204
+ state.streams = streams.map(next => {
205
+ const known = state.streams.find(one => one.s === next.s);
206
+ return known === undefined
207
+ ? { ...next, inKey: false, tail: "", unknown: UNKNOWN_KEY_BYTES }
208
+ : { ...next, inKey: known.inKey, tail: known.tail, unknown: next.inKey ? Math.max(known.unknown, UNKNOWN_KEY_BYTES) : known.unknown };
209
+ });
210
+ state.awaiting = false;
211
+ // A stream first learned here (a read that began mid-file) is shown as the subagent it is, its open steps, and how
212
+ // it ended — display only, read from the checkpoint and checked here, never kept in the sealed cursor.
213
+ const raw = Array.isArray(record["streams"]) ? record["streams"] : [];
214
+ return learned.flatMap(next => displayOf(state.streams.find(one => one.s === next.s), raw.map(object).find(entry => entry?.["s"] === next.s) ?? null, commands));
215
+ }
216
+ if (state.awaiting)
217
+ return [];
218
+ if (record["k"] === "over")
219
+ return [{ k: "over" }];
220
+ if (record["k"] === "cap")
221
+ return [{ k: "cap" }];
222
+ const s = streamNo(record["s"]);
223
+ if (s === null)
224
+ return null;
225
+ if (record["k"] === "child") {
226
+ const parent = record["parent"], via = record["via"], desc = record["desc"];
227
+ if (state.streams.some(one => one.s === s) || state.streams.length >= CHILD_STREAMS_CAP)
228
+ return null;
229
+ if (!(parent === null || (typeof parent === "string" && PARENT.test(parent))))
230
+ return null;
231
+ if (via !== undefined && !state.streams.some(one => one.s === via))
232
+ return null;
233
+ if (desc !== undefined && (typeof desc !== "string" || desc.length > 200))
234
+ return null;
235
+ state.streams.push({ s, parent: parent, ...(via === undefined ? {} : { via: via }), open: [], inKey: false, tail: "", ended: false, unknown: 0 });
236
+ const shown = typeof desc === "string" ? redactLiveLines([desc])[0] : undefined;
237
+ return [{ k: "child", s, ...stampOf(record), parent: parent, ...(via === undefined ? {} : { via: via }), ...(shown === undefined ? {} : { desc: shown }) }];
238
+ }
239
+ const stream = state.streams.find(one => one.s === s);
240
+ if (stream === undefined || stream.ended)
241
+ return null;
242
+ if (record["k"] === "end") {
243
+ const ok = record["ok"];
244
+ if (!(ok === true || ok === false || ok === null))
245
+ return null;
246
+ stream.ended = true;
247
+ stream.open = [];
248
+ return [{ k: "end", s, ...stampOf(record), ok }];
249
+ }
250
+ // The run's own record shape, under this stream's key state, with its id as the run's own recorder writes it.
251
+ const id = record["id"];
252
+ let local;
253
+ if (id !== undefined) {
254
+ const match = typeof id === "string" ? childStep(s).exec(id) : null;
255
+ if (match === null)
256
+ return null;
257
+ local = match[1];
258
+ }
259
+ const { s: _s, ...rest } = record;
260
+ const body = JSON.stringify(local === undefined ? rest : { ...rest, id: local });
261
+ // Read on a copy of the key state; it is kept only if the record passes every check below.
262
+ const keys = { inKey: stream.inKey || stream.unknown > 0, tail: stream.tail };
263
+ const parsed = parseLiveRecord(body, keys);
264
+ // The block as actually observed (not forced by uncertainty): the same record read under the observed state.
265
+ const observed = { inKey: stream.inKey, tail: stream.tail };
266
+ parseLiveRecord(body, observed);
267
+ if (parsed === null || parsed.k === "open")
268
+ return null;
269
+ if (parsed.k === "step" && parsed.once !== true) {
270
+ const total = state.streams.reduce((sum, one) => sum + one.open.length, 0);
271
+ if (total >= CHILD_OPEN_STEPS_CAP || stream.open.includes(id))
272
+ return null; // full, or already open
273
+ stream.open.push(id);
274
+ }
275
+ else if (parsed.k === "done") {
276
+ const at = stream.open.indexOf(id);
277
+ if (at === -1)
278
+ return null; // never one this stream has open
279
+ stream.open.splice(at, 1);
280
+ }
281
+ // An observed block ends only at a valid END; uncertainty also runs down by the text this record actually carried.
282
+ stream.inKey = keys.inKey ? observed.inKey : false;
283
+ stream.unknown = keys.inKey ? Math.max(0, stream.unknown - scannedText(record)) : 0;
284
+ stream.tail = keys.tail ?? "";
285
+ if (parsed.k === "say" && parsed.t.length === 0)
286
+ return []; // all inside a key block: nothing to show
287
+ const [shown] = withholdCommands([parsed], commands);
288
+ return [{ ...shown, ...(local === undefined ? {} : { id: id }), s }];
289
+ }
290
+ /** One window of a run's subagents' file. `commands` is the run's own captured answer (store.ts runLiveCommands). */
291
+ export function readChildWindow(root, runId, cursor, options) {
292
+ const name = `${Math.trunc(runId)}.sub.jsonl`;
293
+ const opened = openLiveTrusted(root, name);
294
+ if (opened === "missing" || opened === "unavailable")
295
+ return { ok: false, reason: opened };
296
+ try {
297
+ let state = cursor === null ? null : unseal(cursor, options.key, runId, opened.gen);
298
+ let earlier = false;
299
+ const restarted = cursor !== null && (state === null || state.off > opened.size);
300
+ if (state === null || state.off > opened.size) {
301
+ ({ state, earlier } = bootstrap(opened.fd, opened.size, runId, opened.gen));
302
+ // One request's reading budget: a bootstrap that scanned the end of the file reads nothing more now.
303
+ if (earlier)
304
+ return { ok: true, records: [], cursor: seal(state, options.key), restarted, earlier, quarantined: 0, skipped: 0, more: true };
305
+ }
306
+ else
307
+ earlier = state.awaiting;
308
+ const from = state.off;
309
+ const read = readLiveLines(opened.fd, opened.size, state.off, state.skipping);
310
+ if (read.skipped > 0)
311
+ unknowable(state);
312
+ const records = [];
313
+ let quarantined = 0;
314
+ for (const line of read.lines) {
315
+ const shown = apply(state, line.text, options.commands);
316
+ if (shown === null)
317
+ quarantined += 1;
318
+ else
319
+ records.push(...shown);
320
+ }
321
+ state.off = read.next;
322
+ state.skipping = read.skipping;
323
+ // More only when this read stopped short of what is written and moved: an unfinished last line waits for growth.
324
+ const more = read.next < opened.size && (read.next > from) && (read.skipping || opened.size - from > READ_WINDOW);
325
+ return { ok: true, records, cursor: seal(state, options.key), restarted, earlier: earlier || state.awaiting, quarantined, skipped: read.skipped, more };
326
+ }
327
+ catch {
328
+ return { ok: false, reason: "unavailable" };
329
+ }
330
+ finally {
331
+ closeSync(opened.fd);
332
+ }
333
+ }
@@ -0,0 +1,73 @@
1
+ import { type LiveRecord, type LiveRecorderOptions } from "./live-records.js";
2
+ export declare const CHILD_STREAMS_CAP = 16;
3
+ export declare const CHILD_DELEGATIONS_CAP = 32;
4
+ export declare const CHILD_OPEN_STEPS_CAP = 32;
5
+ export declare const CHILD_DEPTH_CAP = 2;
6
+ /** A checkpoint at least this often, in bytes of child records. */
7
+ export declare const CHILD_CHECKPOINT_BYTES: number;
8
+ export type ChildCheckpoint = {
9
+ s: number;
10
+ parent: string | null;
11
+ via?: number;
12
+ open: string[];
13
+ inKey: boolean;
14
+ tail?: string;
15
+ ended?: true;
16
+ /** What the reader shows for a stream it first learns of here (a read that starts mid-file): its description, start,
17
+ * open steps (as written, already redacted) and, once ended, when and how. Display only; never sealed in a cursor. */
18
+ desc?: string;
19
+ at?: string;
20
+ steps?: {
21
+ id: string;
22
+ kind: string;
23
+ target?: string;
24
+ }[];
25
+ endedAt?: string;
26
+ ok?: boolean | null;
27
+ };
28
+ export type ChildRecord = (LiveRecord & {
29
+ s: number;
30
+ }) | {
31
+ k: "child";
32
+ s: number;
33
+ at: string;
34
+ parent: string | null;
35
+ via?: number;
36
+ desc?: string;
37
+ } | {
38
+ k: "end";
39
+ s: number;
40
+ at: string;
41
+ ok: boolean | null;
42
+ } | {
43
+ k: "over";
44
+ at: string;
45
+ } | {
46
+ k: "ckpt";
47
+ at: string;
48
+ streams: ChildCheckpoint[];
49
+ dropped: number;
50
+ };
51
+ /** At most about this many bytes of one event's records reach the file; past it words and one-off steps give way to one
52
+ * `cut`, so a checkpoint (written between events) is never far behind any position. */
53
+ export declare const CHILD_EVENT_WORDS_CAP: number;
54
+ /** The child recorder: every stream event in, the subagents' records out. Hand `parentStep` to the run's own recorder
55
+ * (LiveRecorderOptions.onStep) so a delegation names the parent's step it really opened. Never throws. */
56
+ export declare function createChildRecorder(options: Omit<LiveRecorderOptions, "keyState" | "onStep">): {
57
+ observe(event: Record<string, unknown>): ChildRecord[];
58
+ parentStep(call: string | null, id: string, target: string | null): void;
59
+ end(): ChildRecord[];
60
+ checkpoint(): ChildRecord;
61
+ };
62
+ /**
63
+ * The subagents' file, opened the first time a subagent speaks (a run without one has none): created here (O_EXCL,
64
+ * O_NOFOLLOW, 0600, the live directory 0700), appended to, and closed with the run. It stops at LIVE_SUB_FILE_CAP
65
+ * with one `cap` record, reserved inside the cap. Fail-soft: no live view is ever worth a build.
66
+ */
67
+ export declare function childLogWriter(root: string, runId: number): {
68
+ /** Whether the next batch should be preceded by a checkpoint (take it BEFORE the batch is recorded: its state is then
69
+ * exactly the state at that position in the file). */
70
+ due(): boolean;
71
+ append(records: readonly ChildRecord[], before: ChildRecord | null): void;
72
+ close(): void;
73
+ };