@llblab/pi-actors 0.43.1 → 0.45.0
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/AGENTS.md +6 -6
- package/BACKLOG.md +1 -189
- package/CHANGELOG.md +419 -402
- package/README.md +8 -6
- package/dist/index.js +7 -3
- package/dist/lib/async-runs.d.ts +2 -0
- package/dist/lib/async-runs.js +41 -34
- package/dist/lib/config.js +1 -0
- package/dist/lib/file-state.d.ts +3 -1
- package/dist/lib/file-state.js +25 -7
- package/dist/lib/inspector-overlay.js +46 -30
- package/dist/lib/inspector.js +8 -2
- package/dist/lib/limits.d.ts +10 -0
- package/dist/lib/limits.js +10 -0
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- package/dist/lib/recipes-discovery.js +20 -3
- package/dist/lib/recipes-references.d.ts +10 -0
- package/dist/lib/recipes-references.js +168 -5
- package/dist/lib/run-evidence-policy.d.ts +95 -0
- package/dist/lib/run-evidence-policy.js +177 -0
- package/dist/lib/run-ui-runtime.js +2 -0
- package/dist/lib/runs-controls.d.ts +7 -5
- package/dist/lib/runs-controls.js +186 -49
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +25 -1
- package/dist/lib/runs-trace.js +410 -21
- package/dist/lib/runtime-triage.js +6 -22
- package/dist/lib/schema.d.ts +3 -0
- package/dist/lib/schema.js +34 -1
- package/dist/lib/tools-inspect.js +58 -14
- package/dist/lib/tools-local.js +27 -10
- package/dist/lib/tools-spawn.js +2 -2
- package/dist/lib/trace-projection.js +90 -44
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +31 -74
- package/dist/scripts/music-player.mjs +41 -129
- package/dist/scripts/release-gates.mjs +26 -4
- package/dist/skills/actors/SKILL.md +9 -8
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/README.md +0 -1
- package/docs/actor-inspector.md +3 -2
- package/docs/async-runs.md +10 -8
- package/docs/command-templates.md +2 -2
- package/docs/recipe-library.md +4 -2
- package/docs/template-recipes.md +10 -4
- package/docs/tool-registry.md +1 -1
- package/index.ts +9 -3
- package/lib/async-runs.ts +51 -54
- package/lib/config.ts +4 -0
- package/lib/file-state.ts +28 -7
- package/lib/inspector-overlay.ts +29 -17
- package/lib/inspector.ts +16 -2
- package/lib/limits.ts +10 -0
- package/lib/observability.ts +55 -57
- package/lib/recipes-discovery.ts +23 -3
- package/lib/recipes-references.ts +227 -7
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-controls.ts +177 -108
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +496 -21
- package/lib/runtime-triage.ts +11 -25
- package/lib/schema.ts +53 -1
- package/lib/tools-inspect.ts +56 -15
- package/lib/tools-local.ts +49 -19
- package/lib/tools-spawn.ts +2 -2
- package/lib/trace-projection.ts +137 -75
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +31 -74
- package/scripts/music-player.mjs +41 -129
- package/scripts/release-gates.mjs +26 -4
- package/skills/actors/SKILL.md +9 -8
- package/skills/swarm/SKILL.md +2 -2
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -39
- package/lib/runtime-notifier.ts +0 -211
package/lib/runs-trace.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Run Trace event journal.
|
|
3
|
-
* Zones: structured event validation,
|
|
3
|
+
* Zones: structured event validation, bounded atomic retention, resilient reads
|
|
4
4
|
* Owns canonical Run-local Trace persistence; lifecycle projection and owner attention delivery stay in adapters.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
closeSync,
|
|
10
|
+
fstatSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
9
15
|
import { join } from "node:path";
|
|
10
16
|
|
|
11
|
-
import { withFileMutationLock } from "./file-state.ts";
|
|
17
|
+
import { withFileMutationLock, writeTextAtomic } from "./file-state.ts";
|
|
12
18
|
import * as Limits from "./limits.ts";
|
|
13
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
accumulateTraceCompactionStatistics,
|
|
21
|
+
selectNewestTraceSuffix,
|
|
22
|
+
traceAppendFits,
|
|
23
|
+
traceCompactionMarkerInput,
|
|
24
|
+
TRACE_COMPACTION_KIND,
|
|
25
|
+
TRACE_COMPACTION_VERSION,
|
|
26
|
+
type TraceCompactionData,
|
|
27
|
+
type TraceRetentionCandidate,
|
|
28
|
+
} from "./run-evidence-policy.ts";
|
|
29
|
+
import type { StateReadDiagnostic } from "./state-readers.ts";
|
|
14
30
|
|
|
15
31
|
export interface TraceEvent {
|
|
16
32
|
id: string;
|
|
@@ -30,7 +46,56 @@ export interface AppendTraceEventInput {
|
|
|
30
46
|
attention?: TraceEvent["attention"];
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
interface EncodedTraceEvent {
|
|
50
|
+
encoded: string;
|
|
51
|
+
encodedBytes: number;
|
|
52
|
+
event: TraceEvent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface TraceReadWindow {
|
|
56
|
+
content: Buffer;
|
|
57
|
+
fileBytes: number;
|
|
58
|
+
leadingPartialLines: number;
|
|
59
|
+
omittedPrefixBytes: number;
|
|
60
|
+
readBytes: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RunTraceReadEvent {
|
|
64
|
+
event: TraceEvent;
|
|
65
|
+
ordinal: number;
|
|
66
|
+
}
|
|
67
|
+
export interface RunTraceJournalRead {
|
|
68
|
+
diagnostics: StateReadDiagnostic[];
|
|
69
|
+
events: RunTraceReadEvent[];
|
|
70
|
+
fileBytes: number;
|
|
71
|
+
omittedPrefixBytes: number;
|
|
72
|
+
readBytes: number;
|
|
73
|
+
}
|
|
74
|
+
export interface RunTraceSummary {
|
|
75
|
+
compacted: boolean; compactions_total: number; dropped_bytes: number;
|
|
76
|
+
dropped_event_count_exact: boolean; dropped_events: number;
|
|
77
|
+
history_complete: boolean; retained_bytes: number; retained_events: number;
|
|
78
|
+
}
|
|
79
|
+
interface ParsedTraceJournal {
|
|
80
|
+
candidateBytes: number;
|
|
81
|
+
candidates: TraceRetentionCandidate<EncodedTraceEvent>[];
|
|
82
|
+
droppedBytes: number;
|
|
83
|
+
droppedValidEvents: number;
|
|
84
|
+
malformedLines: number;
|
|
85
|
+
physicalEvents: number;
|
|
86
|
+
previousCompaction?: TraceCompactionData;
|
|
87
|
+
}
|
|
88
|
+
|
|
33
89
|
const KIND_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+)*$/;
|
|
90
|
+
const TRACE_FIELDS = new Set([
|
|
91
|
+
"attention",
|
|
92
|
+
"data",
|
|
93
|
+
"id",
|
|
94
|
+
"kind",
|
|
95
|
+
"level",
|
|
96
|
+
"summary",
|
|
97
|
+
"ts",
|
|
98
|
+
]);
|
|
34
99
|
const TRACE_INPUT_FIELDS = new Set([
|
|
35
100
|
"attention",
|
|
36
101
|
"data",
|
|
@@ -47,11 +112,28 @@ const FORBIDDEN_FIELDS = new Set([
|
|
|
47
112
|
"to",
|
|
48
113
|
"type",
|
|
49
114
|
]);
|
|
115
|
+
const COMPACTION_DATA_FIELDS = new Set([
|
|
116
|
+
"compactions_total",
|
|
117
|
+
"dropped_bytes_total",
|
|
118
|
+
"dropped_event_count_exact",
|
|
119
|
+
"dropped_malformed_lines_total",
|
|
120
|
+
"dropped_valid_events_total",
|
|
121
|
+
"history_complete",
|
|
122
|
+
"retained_bytes",
|
|
123
|
+
"retained_events",
|
|
124
|
+
"version",
|
|
125
|
+
]);
|
|
126
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
50
127
|
|
|
51
128
|
export function runTraceFile(stateDir: string): string {
|
|
52
129
|
return join(stateDir, "trace.jsonl");
|
|
53
130
|
}
|
|
54
131
|
|
|
132
|
+
function hasExactFields(record: Record<string, unknown>, fields: Set<string>): boolean {
|
|
133
|
+
const keys = Object.keys(record);
|
|
134
|
+
return keys.length === fields.size && keys.every((key) => fields.has(key));
|
|
135
|
+
}
|
|
136
|
+
|
|
55
137
|
function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
|
|
56
138
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
57
139
|
throw new Error("Trace event must be an object");
|
|
@@ -68,6 +150,9 @@ function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
|
|
|
68
150
|
if (typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind)) {
|
|
69
151
|
throw new Error("Trace event kind must be a lowercase semantic token");
|
|
70
152
|
}
|
|
153
|
+
if (record.kind === TRACE_COMPACTION_KIND) {
|
|
154
|
+
throw new Error(`Trace event kind is runtime-reserved: ${TRACE_COMPACTION_KIND}`);
|
|
155
|
+
}
|
|
71
156
|
if (record.summary !== undefined && typeof record.summary !== "string") {
|
|
72
157
|
throw new Error("Trace event summary must be a string");
|
|
73
158
|
}
|
|
@@ -101,36 +186,426 @@ function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
|
|
|
101
186
|
};
|
|
102
187
|
}
|
|
103
188
|
|
|
189
|
+
function encodeTraceEvent(event: TraceEvent): EncodedTraceEvent {
|
|
190
|
+
let serialized: string;
|
|
191
|
+
try {
|
|
192
|
+
serialized = JSON.stringify(event);
|
|
193
|
+
} catch {
|
|
194
|
+
throw new Error("Trace event data must be JSON-serializable");
|
|
195
|
+
}
|
|
196
|
+
if (Buffer.byteLength(serialized) > Limits.TRACE_EVENT_MAX_BYTES) {
|
|
197
|
+
throw new Error(`Trace event exceeds ${Limits.TRACE_EVENT_MAX_BYTES} bytes`);
|
|
198
|
+
}
|
|
199
|
+
const encoded = `${serialized}\n`;
|
|
200
|
+
return { encoded, encodedBytes: Buffer.byteLength(encoded), event };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createTraceEvent(input: AppendTraceEventInput): EncodedTraceEvent {
|
|
204
|
+
return encodeTraceEvent({
|
|
205
|
+
id: randomUUID(),
|
|
206
|
+
ts: new Date().toISOString(),
|
|
207
|
+
...normalizeTraceEventInput(input),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readTraceWindow(path: string): TraceReadWindow {
|
|
212
|
+
let fd: number;
|
|
213
|
+
try {
|
|
214
|
+
fd = openSync(path, "r");
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
217
|
+
return {
|
|
218
|
+
content: Buffer.alloc(0),
|
|
219
|
+
fileBytes: 0,
|
|
220
|
+
leadingPartialLines: 0,
|
|
221
|
+
omittedPrefixBytes: 0,
|
|
222
|
+
readBytes: 0,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const fileBytes = fstatSync(fd).size;
|
|
229
|
+
if (!Number.isSafeInteger(fileBytes) || fileBytes < 0) {
|
|
230
|
+
throw new Error("Trace journal size is not safely representable");
|
|
231
|
+
}
|
|
232
|
+
const readBytes = Math.min(fileBytes, Limits.TRACE_JOURNAL_MAX_BYTES);
|
|
233
|
+
const offset = fileBytes - readBytes;
|
|
234
|
+
const buffer = Buffer.allocUnsafe(readBytes);
|
|
235
|
+
let consumed = 0;
|
|
236
|
+
while (consumed < readBytes) {
|
|
237
|
+
const count = readSync(fd, buffer, consumed, readBytes - consumed, offset + consumed);
|
|
238
|
+
if (count === 0) throw new Error("Trace journal changed during bounded read");
|
|
239
|
+
consumed += count;
|
|
240
|
+
}
|
|
241
|
+
if (fstatSync(fd).size !== fileBytes) {
|
|
242
|
+
throw new Error("Trace journal changed during bounded read");
|
|
243
|
+
}
|
|
244
|
+
if (offset === 0) {
|
|
245
|
+
return {
|
|
246
|
+
content: buffer,
|
|
247
|
+
fileBytes,
|
|
248
|
+
leadingPartialLines: 0,
|
|
249
|
+
omittedPrefixBytes: 0,
|
|
250
|
+
readBytes,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const preceding = Buffer.allocUnsafe(1);
|
|
254
|
+
if (readSync(fd, preceding, 0, 1, offset - 1) !== 1) {
|
|
255
|
+
throw new Error("Trace journal changed during bounded read");
|
|
256
|
+
}
|
|
257
|
+
if (preceding[0] === 0x0a) {
|
|
258
|
+
return {
|
|
259
|
+
content: buffer,
|
|
260
|
+
fileBytes,
|
|
261
|
+
leadingPartialLines: 0,
|
|
262
|
+
omittedPrefixBytes: offset,
|
|
263
|
+
readBytes,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const newline = buffer.indexOf(0x0a);
|
|
267
|
+
const discarded = newline < 0 ? buffer.length : newline + 1;
|
|
268
|
+
return {
|
|
269
|
+
content: buffer.subarray(discarded),
|
|
270
|
+
fileBytes,
|
|
271
|
+
leadingPartialLines: 1,
|
|
272
|
+
omittedPrefixBytes: offset + discarded,
|
|
273
|
+
readBytes,
|
|
274
|
+
};
|
|
275
|
+
} finally {
|
|
276
|
+
closeSync(fd);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isTraceEvent(value: unknown): value is TraceEvent {
|
|
281
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
282
|
+
const record = value as Record<string, unknown>;
|
|
283
|
+
if (Object.keys(record).some((key) => !TRACE_FIELDS.has(key))) return false;
|
|
284
|
+
if (
|
|
285
|
+
typeof record.id !== "string" || !record.id ||
|
|
286
|
+
typeof record.ts !== "string" || !Number.isFinite(Date.parse(record.ts)) ||
|
|
287
|
+
typeof record.kind !== "string" || !KIND_PATTERN.test(record.kind)
|
|
288
|
+
) return false;
|
|
289
|
+
if (record.summary !== undefined && typeof record.summary !== "string") return false;
|
|
290
|
+
if (
|
|
291
|
+
record.level !== undefined &&
|
|
292
|
+
record.level !== "info" &&
|
|
293
|
+
record.level !== "warning" &&
|
|
294
|
+
record.level !== "error"
|
|
295
|
+
) return false;
|
|
296
|
+
return record.attention === undefined ||
|
|
297
|
+
record.attention === "notify" ||
|
|
298
|
+
record.attention === "followup";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isCount(value: unknown): value is number {
|
|
302
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function compactionData(value: unknown): TraceCompactionData | undefined {
|
|
306
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
307
|
+
const record = value as Record<string, unknown>;
|
|
308
|
+
if (!hasExactFields(record, COMPACTION_DATA_FIELDS)) return undefined;
|
|
309
|
+
if (
|
|
310
|
+
record.version !== TRACE_COMPACTION_VERSION ||
|
|
311
|
+
!isCount(record.compactions_total) ||
|
|
312
|
+
!isCount(record.dropped_valid_events_total) ||
|
|
313
|
+
!isCount(record.dropped_malformed_lines_total) ||
|
|
314
|
+
!isCount(record.dropped_bytes_total) ||
|
|
315
|
+
typeof record.dropped_event_count_exact !== "boolean" ||
|
|
316
|
+
!isCount(record.retained_events) ||
|
|
317
|
+
!isCount(record.retained_bytes) ||
|
|
318
|
+
record.history_complete !== false
|
|
319
|
+
) return undefined;
|
|
320
|
+
return record as unknown as TraceCompactionData;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function markerData(event: TraceEvent): TraceCompactionData | undefined {
|
|
324
|
+
return event.kind === TRACE_COMPACTION_KIND &&
|
|
325
|
+
event.level === "warning" &&
|
|
326
|
+
event.summary === undefined &&
|
|
327
|
+
event.attention === undefined
|
|
328
|
+
? compactionData(event.data)
|
|
329
|
+
: undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function decodeTraceLine(line: Buffer): { event: TraceEvent; text: string } {
|
|
333
|
+
if (line.length === 0 || line.length > Limits.TRACE_EVENT_MAX_BYTES) {
|
|
334
|
+
throw new Error("Trace journal line is empty or oversized");
|
|
335
|
+
}
|
|
336
|
+
const text = UTF8_DECODER.decode(line);
|
|
337
|
+
const event = JSON.parse(text) as unknown;
|
|
338
|
+
if (!isTraceEvent(event) ||
|
|
339
|
+
(event.kind === TRACE_COMPACTION_KIND && !markerData(event))) {
|
|
340
|
+
throw new Error("Trace journal line is not a canonical event");
|
|
341
|
+
}
|
|
342
|
+
return { event, text };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseTraceJournal(window: TraceReadWindow): ParsedTraceJournal {
|
|
346
|
+
let candidates: TraceRetentionCandidate<EncodedTraceEvent>[] = [];
|
|
347
|
+
let candidateHead = 0;
|
|
348
|
+
let candidateBytes = 0;
|
|
349
|
+
let droppedBytes = window.omittedPrefixBytes;
|
|
350
|
+
let droppedValidEvents = 0;
|
|
351
|
+
let malformedLines = window.leadingPartialLines;
|
|
352
|
+
let physicalEvents = 0;
|
|
353
|
+
let previousCompaction: TraceCompactionData | undefined;
|
|
354
|
+
const dropOldestCandidate = (): void => {
|
|
355
|
+
const dropped = candidates[candidateHead++]!;
|
|
356
|
+
candidateBytes -= dropped.encodedBytes;
|
|
357
|
+
droppedBytes += dropped.encodedBytes;
|
|
358
|
+
droppedValidEvents += 1;
|
|
359
|
+
if (candidateHead > 2_048) {
|
|
360
|
+
candidates = candidates.slice(candidateHead);
|
|
361
|
+
candidateHead = 0;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const consume = (line: Buffer, physicalBytes: number): void => {
|
|
365
|
+
let decoded: ReturnType<typeof decodeTraceLine>;
|
|
366
|
+
try {
|
|
367
|
+
decoded = decodeTraceLine(line);
|
|
368
|
+
} catch {
|
|
369
|
+
malformedLines += 1;
|
|
370
|
+
droppedBytes += physicalBytes;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const { event, text } = decoded;
|
|
374
|
+
physicalEvents += 1;
|
|
375
|
+
if (event.kind === TRACE_COMPACTION_KIND) {
|
|
376
|
+
previousCompaction = markerData(event)!;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const encoded = `${text}\n`;
|
|
380
|
+
const candidate: TraceRetentionCandidate<EncodedTraceEvent> = {
|
|
381
|
+
encodedBytes: Buffer.byteLength(encoded),
|
|
382
|
+
value: { encoded, encodedBytes: Buffer.byteLength(encoded), event },
|
|
383
|
+
};
|
|
384
|
+
candidates.push(candidate);
|
|
385
|
+
candidateBytes += candidate.encodedBytes;
|
|
386
|
+
while (
|
|
387
|
+
candidates.length - candidateHead > Limits.TRACE_JOURNAL_TARGET_EVENTS ||
|
|
388
|
+
candidateBytes > Limits.TRACE_JOURNAL_TARGET_BYTES
|
|
389
|
+
) dropOldestCandidate();
|
|
390
|
+
};
|
|
391
|
+
let start = 0;
|
|
392
|
+
while (start < window.content.length) {
|
|
393
|
+
const newline = window.content.indexOf(0x0a, start);
|
|
394
|
+
if (newline < 0) {
|
|
395
|
+
malformedLines += 1;
|
|
396
|
+
droppedBytes += window.content.length - start;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
consume(window.content.subarray(start, newline), newline - start + 1);
|
|
400
|
+
start = newline + 1;
|
|
401
|
+
}
|
|
402
|
+
candidates = candidates.slice(candidateHead);
|
|
403
|
+
return {
|
|
404
|
+
candidateBytes,
|
|
405
|
+
candidates,
|
|
406
|
+
droppedBytes,
|
|
407
|
+
droppedValidEvents,
|
|
408
|
+
malformedLines,
|
|
409
|
+
physicalEvents,
|
|
410
|
+
...(previousCompaction ? { previousCompaction } : {}),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function markerReserveBytes(id: string, ts: string): number {
|
|
415
|
+
const maximum = Number.MAX_SAFE_INTEGER;
|
|
416
|
+
return encodeTraceEvent({
|
|
417
|
+
id,
|
|
418
|
+
ts,
|
|
419
|
+
...traceCompactionMarkerInput({
|
|
420
|
+
version: TRACE_COMPACTION_VERSION,
|
|
421
|
+
compactions_total: maximum,
|
|
422
|
+
dropped_valid_events_total: maximum,
|
|
423
|
+
dropped_malformed_lines_total: maximum,
|
|
424
|
+
dropped_bytes_total: maximum,
|
|
425
|
+
dropped_event_count_exact: false,
|
|
426
|
+
retained_events: maximum,
|
|
427
|
+
retained_bytes: maximum,
|
|
428
|
+
history_complete: false,
|
|
429
|
+
}),
|
|
430
|
+
}).encodedBytes;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function createCompactionMarker(
|
|
434
|
+
id: string,
|
|
435
|
+
ts: string,
|
|
436
|
+
previous: TraceCompactionData | undefined,
|
|
437
|
+
delta: Omit<Parameters<typeof accumulateTraceCompactionStatistics>[1], "retained_bytes">,
|
|
438
|
+
retainedWithoutMarkerBytes: number,
|
|
439
|
+
): EncodedTraceEvent {
|
|
440
|
+
let markerBytes = 0;
|
|
441
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
442
|
+
const data = accumulateTraceCompactionStatistics(previous, {
|
|
443
|
+
...delta,
|
|
444
|
+
retained_bytes: retainedWithoutMarkerBytes + markerBytes,
|
|
445
|
+
});
|
|
446
|
+
const marker = encodeTraceEvent({
|
|
447
|
+
id,
|
|
448
|
+
ts,
|
|
449
|
+
...traceCompactionMarkerInput(data),
|
|
450
|
+
});
|
|
451
|
+
if (marker.encodedBytes === markerBytes) return marker;
|
|
452
|
+
markerBytes = marker.encodedBytes;
|
|
453
|
+
}
|
|
454
|
+
throw new Error("Trace compaction marker size did not converge");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function compactTraceJournal(
|
|
458
|
+
path: string,
|
|
459
|
+
window: TraceReadWindow,
|
|
460
|
+
parsed: ParsedTraceJournal,
|
|
461
|
+
appended: EncodedTraceEvent,
|
|
462
|
+
): void {
|
|
463
|
+
const markerId = randomUUID();
|
|
464
|
+
const markerTs = new Date().toISOString();
|
|
465
|
+
const reserveBytes = markerReserveBytes(markerId, markerTs);
|
|
466
|
+
const selection = selectNewestTraceSuffix(parsed.candidates, {
|
|
467
|
+
bytes: appended.encodedBytes + reserveBytes,
|
|
468
|
+
events: 2,
|
|
469
|
+
});
|
|
470
|
+
const marker = createCompactionMarker(
|
|
471
|
+
markerId,
|
|
472
|
+
markerTs,
|
|
473
|
+
parsed.previousCompaction,
|
|
474
|
+
{
|
|
475
|
+
dropped_valid_events: parsed.droppedValidEvents + selection.droppedEvents,
|
|
476
|
+
dropped_malformed_lines: parsed.malformedLines,
|
|
477
|
+
dropped_bytes:
|
|
478
|
+
parsed.droppedBytes + parsed.candidateBytes - selection.retainedBytes,
|
|
479
|
+
dropped_event_count_exact: window.omittedPrefixBytes === 0,
|
|
480
|
+
retained_events: selection.retainedEvents + 2,
|
|
481
|
+
},
|
|
482
|
+
selection.retainedBytes + appended.encodedBytes,
|
|
483
|
+
);
|
|
484
|
+
if (marker.encodedBytes > reserveBytes) {
|
|
485
|
+
throw new Error("Trace compaction marker exceeded its reserved bound");
|
|
486
|
+
}
|
|
487
|
+
const content = [
|
|
488
|
+
...selection.retained.map(({ value }) => value.encoded),
|
|
489
|
+
appended.encoded,
|
|
490
|
+
marker.encoded,
|
|
491
|
+
].join("");
|
|
492
|
+
const bytes = Buffer.byteLength(content);
|
|
493
|
+
const events = selection.retainedEvents + 2;
|
|
494
|
+
if (
|
|
495
|
+
bytes > Limits.TRACE_JOURNAL_TARGET_BYTES ||
|
|
496
|
+
events > Limits.TRACE_JOURNAL_TARGET_EVENTS ||
|
|
497
|
+
bytes > Limits.TRACE_JOURNAL_MAX_BYTES ||
|
|
498
|
+
events > Limits.TRACE_JOURNAL_MAX_EVENTS
|
|
499
|
+
) {
|
|
500
|
+
throw new Error("Trace compaction could not satisfy journal bounds");
|
|
501
|
+
}
|
|
502
|
+
writeTextAtomic(path, content);
|
|
503
|
+
}
|
|
504
|
+
|
|
104
505
|
export function appendRunTraceEvent(
|
|
105
506
|
stateDir: string,
|
|
106
507
|
input: AppendTraceEventInput,
|
|
107
508
|
): TraceEvent {
|
|
509
|
+
const appended = createTraceEvent(input);
|
|
108
510
|
const path = runTraceFile(stateDir);
|
|
109
511
|
return withFileMutationLock(path, () => {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
512
|
+
const window = readTraceWindow(path);
|
|
513
|
+
const parsed = parseTraceJournal(window);
|
|
514
|
+
const completeJournal =
|
|
515
|
+
window.omittedPrefixBytes === 0 && parsed.malformedLines === 0;
|
|
516
|
+
if (
|
|
517
|
+
completeJournal &&
|
|
518
|
+
traceAppendFits(
|
|
519
|
+
{ bytes: window.fileBytes, events: parsed.physicalEvents },
|
|
520
|
+
appended.encodedBytes,
|
|
521
|
+
)
|
|
522
|
+
) {
|
|
523
|
+
writeFileSync(path, appended.encoded, { flag: "a" });
|
|
524
|
+
} else {
|
|
525
|
+
compactTraceJournal(path, window, parsed, appended);
|
|
526
|
+
}
|
|
527
|
+
return appended.event;
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function readRunTraceJournal(stateDir: string): RunTraceJournalRead {
|
|
532
|
+
const path = runTraceFile(stateDir);
|
|
533
|
+
let window: TraceReadWindow;
|
|
534
|
+
try {
|
|
535
|
+
window = readTraceWindow(path);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return {
|
|
538
|
+
diagnostics: [{
|
|
539
|
+
message: error instanceof Error ? error.message : String(error),
|
|
540
|
+
path,
|
|
541
|
+
}],
|
|
542
|
+
events: [],
|
|
543
|
+
fileBytes: 0,
|
|
544
|
+
omittedPrefixBytes: 0,
|
|
545
|
+
readBytes: 0,
|
|
115
546
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
547
|
+
}
|
|
548
|
+
const diagnostics: StateReadDiagnostic[] = [];
|
|
549
|
+
let events: RunTraceReadEvent[] = [];
|
|
550
|
+
let eventHead = 0;
|
|
551
|
+
let ordinal = window.leadingPartialLines;
|
|
552
|
+
const diagnose = (message: string, line?: number): void => {
|
|
553
|
+
diagnostics.push({ message, path, ...(line === undefined ? {} : { line }) });
|
|
554
|
+
if (diagnostics.length > Limits.TRACE_EVENT_MAX_READ) diagnostics.shift();
|
|
555
|
+
};
|
|
556
|
+
const retain = (event: TraceEvent): void => {
|
|
557
|
+
events.push({ event, ordinal });
|
|
558
|
+
if (events.length - eventHead > Limits.TRACE_JOURNAL_MAX_EVENTS) eventHead += 1;
|
|
559
|
+
if (eventHead > Limits.TRACE_JOURNAL_MAX_EVENTS) {
|
|
560
|
+
events = events.slice(eventHead);
|
|
561
|
+
eventHead = 0;
|
|
121
562
|
}
|
|
122
|
-
|
|
123
|
-
|
|
563
|
+
};
|
|
564
|
+
if (window.leadingPartialLines) {
|
|
565
|
+
diagnose("Omitted partial leading Trace line", 1);
|
|
566
|
+
}
|
|
567
|
+
let start = 0;
|
|
568
|
+
while (start < window.content.length) {
|
|
569
|
+
ordinal += 1;
|
|
570
|
+
const newline = window.content.indexOf(0x0a, start);
|
|
571
|
+
if (newline < 0) {
|
|
572
|
+
diagnose("Incomplete trailing Trace line", ordinal);
|
|
573
|
+
break;
|
|
124
574
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
575
|
+
try {
|
|
576
|
+
retain(decodeTraceLine(window.content.subarray(start, newline)).event);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
diagnose(error instanceof Error ? error.message : String(error), ordinal);
|
|
579
|
+
}
|
|
580
|
+
start = newline + 1;
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
diagnostics,
|
|
584
|
+
events: events.slice(eventHead),
|
|
585
|
+
fileBytes: window.fileBytes,
|
|
586
|
+
omittedPrefixBytes: window.omittedPrefixBytes,
|
|
587
|
+
readBytes: window.readBytes,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function summarizeRunTraceJournal(read: RunTraceJournalRead): RunTraceSummary {
|
|
592
|
+
const marker = [...read.events].reverse().map(({ event }) => markerData(event)).find(Boolean);
|
|
593
|
+
const incomplete = Boolean(marker) || read.omittedPrefixBytes > 0;
|
|
594
|
+
return {
|
|
595
|
+
history_complete: !incomplete, compacted: Boolean(marker),
|
|
596
|
+
compactions_total: marker?.compactions_total ?? 0,
|
|
597
|
+
dropped_events: marker?.dropped_valid_events_total ?? 0,
|
|
598
|
+
dropped_bytes: marker?.dropped_bytes_total ?? read.omittedPrefixBytes,
|
|
599
|
+
dropped_event_count_exact: marker?.dropped_event_count_exact ?? !incomplete,
|
|
600
|
+
retained_events: read.events.length, retained_bytes: read.readBytes,
|
|
601
|
+
};
|
|
128
602
|
}
|
|
129
603
|
|
|
130
604
|
export function readRunTraceEvents(
|
|
131
605
|
stateDir: string,
|
|
132
606
|
limit = Limits.TRACE_EVENT_MAX_READ,
|
|
133
607
|
): TraceEvent[] {
|
|
134
|
-
|
|
135
|
-
|
|
608
|
+
return readRunTraceJournal(stateDir).events
|
|
609
|
+
.slice(-Math.max(1, Math.min(limit, Limits.TRACE_EVENT_MAX_READ)))
|
|
610
|
+
.map(({ event }) => event);
|
|
136
611
|
}
|
package/lib/runtime-triage.ts
CHANGED
|
@@ -5,17 +5,15 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as ControlProjection from "./control-projection.ts";
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
classifyRunControlRecord,
|
|
10
|
+
runControlStatusTimestamp,
|
|
11
|
+
type RunControlStatus,
|
|
12
|
+
} from "./run-evidence-policy.ts";
|
|
13
|
+
import type { RunControlRecord } from "./runs-controls.ts";
|
|
9
14
|
|
|
10
15
|
export const RUNTIME_CONTROL_STALE_AFTER_MS = 5 * 60 * 1_000;
|
|
11
16
|
|
|
12
|
-
const PENDING_STATUSES = new Set<RunControlStatus>([
|
|
13
|
-
"queued",
|
|
14
|
-
"delivered",
|
|
15
|
-
"claimed",
|
|
16
|
-
]);
|
|
17
|
-
const TERMINAL_STATUSES = new Set<RunControlStatus>(["handled", "failed"]);
|
|
18
|
-
|
|
19
17
|
export interface RuntimeTriageRun {
|
|
20
18
|
run: string;
|
|
21
19
|
runInstanceId?: string;
|
|
@@ -49,17 +47,6 @@ function diagnostic(
|
|
|
49
47
|
return { diagnostic: { reason, run } };
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
function statusTimestamp(
|
|
53
|
-
record: Record<string, unknown>,
|
|
54
|
-
status: RunControlStatus,
|
|
55
|
-
): unknown {
|
|
56
|
-
if (status === "failed") return record.failed_at;
|
|
57
|
-
if (status === "handled") return record.handled_at;
|
|
58
|
-
if (status === "claimed") return record.claimed_at;
|
|
59
|
-
if (status === "delivered") return record.delivered_at;
|
|
60
|
-
return record.queued_at;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
50
|
export function classifyRuntimeControl(
|
|
64
51
|
run: RuntimeTriageRun,
|
|
65
52
|
value: unknown,
|
|
@@ -70,9 +57,8 @@ export function classifyRuntimeControl(
|
|
|
70
57
|
}
|
|
71
58
|
const record = value as Record<string, unknown>;
|
|
72
59
|
const status = record.status as RunControlStatus;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
60
|
+
const recordClass = classifyRunControlRecord(record);
|
|
61
|
+
if (!recordClass) return diagnostic(run.run, "invalid_status");
|
|
76
62
|
if (typeof record.id !== "string" || !record.id) {
|
|
77
63
|
return diagnostic(run.run, "invalid_control_id");
|
|
78
64
|
}
|
|
@@ -82,12 +68,12 @@ export function classifyRuntimeControl(
|
|
|
82
68
|
if (typeof record.run_instance_id !== "string" || !record.run_instance_id) {
|
|
83
69
|
return diagnostic(run.run, "invalid_generation");
|
|
84
70
|
}
|
|
85
|
-
const timestamp =
|
|
71
|
+
const timestamp = runControlStatusTimestamp(record, status);
|
|
86
72
|
const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
|
|
87
73
|
if (!Number.isFinite(timestampMs)) {
|
|
88
74
|
return diagnostic(run.run, "invalid_status_timestamp");
|
|
89
75
|
}
|
|
90
|
-
if (
|
|
76
|
+
if (recordClass === "terminal") return {};
|
|
91
77
|
const ageMs = Math.max(0, Math.floor(nowMs - timestampMs));
|
|
92
78
|
const staleReason: RuntimePendingControl["reason"] | undefined =
|
|
93
79
|
!run.runInstanceId
|
|
@@ -108,7 +94,7 @@ export function classifyRuntimeControl(
|
|
|
108
94
|
reason: staleReason ?? "within_age_threshold",
|
|
109
95
|
run: run.run,
|
|
110
96
|
status: status as RuntimePendingControl["status"],
|
|
111
|
-
status_at:
|
|
97
|
+
status_at: runControlStatusTimestamp(
|
|
112
98
|
projected as unknown as Record<string, unknown>,
|
|
113
99
|
status,
|
|
114
100
|
) as string,
|