tickmarkr 2.6.2 → 2.6.4
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/README.md +12 -2
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-lints.d.ts +1 -0
- package/dist/adapters/model-lints.js +48 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/types.d.ts +6 -0
- package/dist/cli/commands/approve.d.ts +6 -3
- package/dist/cli/commands/approve.js +43 -6
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +6 -1
- package/dist/cli/commands/fleet.js +264 -59
- package/dist/cli/commands/init.js +5 -3
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +40 -2
- package/dist/cli/commands/report.js +278 -11
- package/dist/cli/commands/resume.js +2 -4
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +33 -5
- package/dist/cli/commands/status.js +49 -4
- package/dist/cli/commands/verify.js +332 -121
- package/dist/compile/native.js +137 -0
- package/dist/config/config.d.ts +40 -9
- package/dist/config/config.js +133 -15
- package/dist/config/fleet-overlay.d.ts +13 -3
- package/dist/config/fleet-overlay.js +12 -8
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +9 -1
- package/dist/drivers/orca.js +29 -7
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/gates/acceptance.d.ts +7 -0
- package/dist/gates/acceptance.js +27 -5
- package/dist/gates/baseline.d.ts +20 -1
- package/dist/gates/baseline.js +100 -20
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +27 -8
- package/dist/gates/review.d.ts +6 -1
- package/dist/gates/review.js +122 -32
- package/dist/gates/run-gates.d.ts +54 -3
- package/dist/gates/run-gates.js +331 -45
- package/dist/gates/test-manifest.d.ts +42 -0
- package/dist/gates/test-manifest.js +69 -10
- package/dist/route/router.d.ts +23 -1
- package/dist/route/router.js +54 -16
- package/dist/run/consult.d.ts +3 -1
- package/dist/run/consult.js +4 -2
- package/dist/run/daemon.d.ts +2 -1
- package/dist/run/daemon.js +349 -79
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +123 -1
- package/dist/run/journal.js +480 -17
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/protocol.d.ts +15 -0
- package/dist/run/protocol.js +11 -1
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/live-store.d.ts +6 -0
- package/dist/tui/cockpit/live-store.js +36 -11
- package/dist/tui/cockpit/run-cockpit.js +2 -2
- package/dist/tui/cockpit/run-view.d.ts +2 -1
- package/dist/tui/cockpit/run-view.js +13 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +2 -0
- package/dist/tui/cockpit/setup-cockpit.js +4 -0
- package/package.json +2 -1
- package/schema/config.schema.json +8 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -2
- package/skills/tickmarkr-loop/SKILL.md +17 -5
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +4 -1
package/dist/run/lease.js
CHANGED
|
@@ -91,6 +91,38 @@ export class CommandLeases {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/** OBS-880/1071: the file lease's token, exported to the holder's descendants for the span it is held
|
|
95
|
+
* so a nested runner (a gate's suite under `tickmarkr verify`, a mutation child inside a leased suite)
|
|
96
|
+
* reenters instead of waiting on its own ancestor. Deliberately NOT COMMAND_LEASE_TOKEN_ENV: the
|
|
97
|
+
* installed daemon clears and rewrites that in-process token, and a command-lease token never names
|
|
98
|
+
* this file. Environment text alone proves nothing: see `reentrantHolder`. */
|
|
99
|
+
export const REPOSITORY_LEASE_TOKEN_ENV = "TICKMARKR_REPOSITORY_LEASE_TOKEN";
|
|
100
|
+
/** The strict ancestors of this process, from one `ps` snapshot; empty when unreadable (fail closed). */
|
|
101
|
+
const ancestorPids = () => new Promise((ok) => execFile("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8" }, (error, stdout) => {
|
|
102
|
+
const parent = new Map();
|
|
103
|
+
if (!error)
|
|
104
|
+
for (const line of stdout.split("\n")) {
|
|
105
|
+
const [pid, ppid] = line.trim().split(/\s+/).map(Number);
|
|
106
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid))
|
|
107
|
+
parent.set(pid, ppid);
|
|
108
|
+
}
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (let pid = error ? 0 : process.ppid; pid > 1 && !seen.has(pid); pid = parent.get(pid) ?? 0)
|
|
111
|
+
seen.add(pid);
|
|
112
|
+
ok(seen);
|
|
113
|
+
}));
|
|
114
|
+
/** Reentry is identity-bound: the inherited token must name the record at THIS repository's lease path
|
|
115
|
+
* right now, its holder must be alive, and that holder must be a strict ancestor of this process. A
|
|
116
|
+
* forged, stale (released or superseded), foreign-repository or command-lease token matches no record;
|
|
117
|
+
* an in-process sibling is not a descendant — each of those waits like any other runner. */
|
|
118
|
+
export async function reentrantHolder(path, inherited) {
|
|
119
|
+
if (!inherited)
|
|
120
|
+
return undefined;
|
|
121
|
+
const holder = readHolder(path);
|
|
122
|
+
if (holder?.token !== inherited || holder.pid === process.pid || !alive(holder.pid))
|
|
123
|
+
return undefined;
|
|
124
|
+
return (await ancestorPids()).has(holder.pid) ? holder : undefined;
|
|
125
|
+
}
|
|
94
126
|
const LEASE_FILE = "tickmarkr-runner.lease";
|
|
95
127
|
export async function repositoryLeasePath(cwd) {
|
|
96
128
|
const commonDir = await new Promise((ok, fail) => execFile("git", ["rev-parse", "--git-common-dir"], { cwd, encoding: "utf8" }, (error, stdout) => error ? fail(error) : ok(stdout.trim())));
|
|
@@ -283,6 +315,9 @@ const inspectAndReserve = (path, mine) => {
|
|
|
283
315
|
};
|
|
284
316
|
export async function withRepositoryLease(cwd, run, opts = {}) {
|
|
285
317
|
const path = await repositoryLeasePath(cwd);
|
|
318
|
+
// A descendant of the live holder runs inside its ancestor's reservation and releases nothing.
|
|
319
|
+
if (await reentrantHolder(path, opts.inherited ?? process.env[REPOSITORY_LEASE_TOKEN_ENV]))
|
|
320
|
+
return run();
|
|
286
321
|
const pollMs = opts.pollMs ?? 1_000;
|
|
287
322
|
const mine = { pid: process.pid, cwd, at: Date.now(), token: randomUUID() };
|
|
288
323
|
let waitingOn;
|
|
@@ -297,10 +332,20 @@ export async function withRepositoryLease(cwd, run, opts = {}) {
|
|
|
297
332
|
}
|
|
298
333
|
await new Promise((wake) => setTimeout(wake, pollMs));
|
|
299
334
|
}
|
|
335
|
+
// Exported before run() so every descendant forked or spawned during the hold can reenter; restored
|
|
336
|
+
// at release unless another reservation in this process has since exported its own.
|
|
337
|
+
const exported = process.env[REPOSITORY_LEASE_TOKEN_ENV];
|
|
338
|
+
process.env[REPOSITORY_LEASE_TOKEN_ENV] = mine.token;
|
|
300
339
|
try {
|
|
301
340
|
return await run();
|
|
302
341
|
}
|
|
303
342
|
finally {
|
|
343
|
+
if (process.env[REPOSITORY_LEASE_TOKEN_ENV] === mine.token) {
|
|
344
|
+
if (exported === undefined)
|
|
345
|
+
delete process.env[REPOSITORY_LEASE_TOKEN_ENV];
|
|
346
|
+
else
|
|
347
|
+
process.env[REPOSITORY_LEASE_TOKEN_ENV] = exported;
|
|
348
|
+
}
|
|
304
349
|
// Release is serialized with reclamation/acquisition and removes only this generation.
|
|
305
350
|
await withMutationLock(path, pollMs, undefined, () => {
|
|
306
351
|
if (readHolder(path)?.token === mine.token)
|
package/dist/run/protocol.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ export declare const EvidenceArtifactSchema: z.ZodObject<{
|
|
|
53
53
|
droppedBytes: z.ZodNumber;
|
|
54
54
|
truncated: z.ZodBoolean;
|
|
55
55
|
}, z.core.$strict>;
|
|
56
|
+
export declare const RedactionSchema: z.ZodObject<{
|
|
57
|
+
material: z.ZodBoolean;
|
|
58
|
+
counts: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
token: z.ZodNumber;
|
|
60
|
+
assignment: z.ZodNumber;
|
|
61
|
+
secretEnv: z.ZodNumber;
|
|
62
|
+
benignEnv: z.ZodNumber;
|
|
63
|
+
}, z.core.$strict>>;
|
|
64
|
+
}, z.core.$strict>;
|
|
56
65
|
export declare const GateEvidenceReceiptSchema: z.ZodObject<{
|
|
57
66
|
invocationId: z.ZodString;
|
|
58
67
|
nonce: z.ZodOptional<z.ZodString>;
|
|
@@ -85,6 +94,12 @@ export declare const GateEvidenceReceiptSchema: z.ZodObject<{
|
|
|
85
94
|
}>;
|
|
86
95
|
redaction: z.ZodObject<{
|
|
87
96
|
material: z.ZodBoolean;
|
|
97
|
+
counts: z.ZodOptional<z.ZodObject<{
|
|
98
|
+
token: z.ZodNumber;
|
|
99
|
+
assignment: z.ZodNumber;
|
|
100
|
+
secretEnv: z.ZodNumber;
|
|
101
|
+
benignEnv: z.ZodNumber;
|
|
102
|
+
}, z.core.$strict>>;
|
|
88
103
|
}, z.core.$strict>;
|
|
89
104
|
stdout: z.ZodObject<{
|
|
90
105
|
path: z.ZodString;
|
package/dist/run/protocol.js
CHANGED
|
@@ -47,6 +47,16 @@ export const EvidenceArtifactSchema = z.object({
|
|
|
47
47
|
if (ref.availability === "available" && ref.sha256 === null)
|
|
48
48
|
ctx.addIssue({ code: "custom", message: "available bytes require a hash" });
|
|
49
49
|
});
|
|
50
|
+
// OBS-1139: one count per original span, never a value. A legacy receipt carries `material` alone and
|
|
51
|
+
// keeps it as recorded; a counted one must agree — benign HOME/TMPDIR substitutions are never material.
|
|
52
|
+
const RedactionCountsSchema = z.object({
|
|
53
|
+
token: z.number().int().nonnegative(),
|
|
54
|
+
assignment: z.number().int().nonnegative(),
|
|
55
|
+
secretEnv: z.number().int().nonnegative(),
|
|
56
|
+
benignEnv: z.number().int().nonnegative(),
|
|
57
|
+
}).strict();
|
|
58
|
+
export const RedactionSchema = z.object({ material: z.boolean(), counts: RedactionCountsSchema.optional() }).strict()
|
|
59
|
+
.refine(r => !r.counts || r.material === (r.counts.token + r.counts.assignment + r.counts.secretEnv > 0), { message: "material must agree with the token, assignment and secret-environment counts" });
|
|
50
60
|
export const GateEvidenceReceiptSchema = z.object({
|
|
51
61
|
invocationId: NonEmptyStringSchema,
|
|
52
62
|
nonce: NonEmptyStringSchema.optional(),
|
|
@@ -64,7 +74,7 @@ export const GateEvidenceReceiptSchema = z.object({
|
|
|
64
74
|
timedOut: z.boolean().nullable(),
|
|
65
75
|
}).strict(),
|
|
66
76
|
availability: z.enum(EVIDENCE_AVAILABILITIES),
|
|
67
|
-
redaction:
|
|
77
|
+
redaction: RedactionSchema,
|
|
68
78
|
stdout: EvidenceArtifactSchema,
|
|
69
79
|
stderr: EvidenceArtifactSchema,
|
|
70
80
|
}).strict();
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { type GateEvidenceReceipt } from "./protocol.js";
|
|
2
2
|
export declare const RECEIPT_UNAVAILABLE_REASONS: readonly ["outside-root", "symlink", "hash-mismatch", "expired", "missing"];
|
|
3
3
|
export type ReceiptUnavailableReason = (typeof RECEIPT_UNAVAILABLE_REASONS)[number];
|
|
4
|
+
/** OBS-1140: at most this many newest eviction tombstones persist; an older eviction reads as missing. */
|
|
5
|
+
export declare const EVICTION_TOMBSTONE_LIMIT = 256;
|
|
6
|
+
/** Beside gate-evidence/, never inside it: tombstones are metadata, not retained artifact bytes. */
|
|
7
|
+
export declare const EVICTION_TOMBSTONES_FILE = "evidence-evictions.json";
|
|
8
|
+
/** Bound to the evicted bytes themselves, so a replacement artifact at the same path never inherits it. */
|
|
9
|
+
export interface EvictionTombstone {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly sha256: string;
|
|
12
|
+
readonly retainedBytes: number;
|
|
13
|
+
}
|
|
14
|
+
/** Newest last, at most the limit; anything malformed is no tombstone at all (fail closed to missing). */
|
|
15
|
+
export declare function parseEvictionTombstones(bytes: Buffer): EvictionTombstone[];
|
|
4
16
|
export type ReceiptResolution = {
|
|
5
17
|
readonly ok: true;
|
|
6
18
|
readonly path: string;
|
|
@@ -12,6 +24,16 @@ export type ReceiptResolution = {
|
|
|
12
24
|
};
|
|
13
25
|
/** Resolve one artifact reference against the run root it was recorded under. Fails closed. */
|
|
14
26
|
export declare function resolveReceipt(reference: unknown, runRoot: string): ReceiptResolution;
|
|
27
|
+
export type ReceiptRedaction = {
|
|
28
|
+
readonly material: boolean;
|
|
29
|
+
readonly counts: GateEvidenceReceipt["redaction"]["counts"] | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* OBS-1139: what a receipt says was withheld. A counted receipt reports its per-span counts; a legacy
|
|
33
|
+
* one keeps its recorded `material` with no counts; anything unparseable is material (fail closed).
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveReceiptRedaction(receipt: unknown): ReceiptRedaction;
|
|
36
|
+
export declare function formatReceiptRedaction(r: ReceiptRedaction): string;
|
|
15
37
|
/** Both artifacts of a receipt, stdout first, each resolved independently. */
|
|
16
38
|
export declare function resolveReceiptArtifacts(receipt: GateEvidenceReceipt, runRoot: string): ReceiptResolution[];
|
|
17
39
|
/** One line per artifact for a human surface: the reference, then its verified availability. */
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { EvidenceArtifactSchema } from "./protocol.js";
|
|
4
|
+
import { EvidenceArtifactSchema, RedactionSchema } from "./protocol.js";
|
|
5
5
|
// OBS-1101 retrieval (D-174): the one production reader of an execution-evidence receipt. It answers
|
|
6
6
|
// from the bytes on disk under the run root the receipt was minted in and nothing else — no
|
|
7
7
|
// re-execution, no substitute chosen by name or timestamp, no symlink followed anywhere: not in the
|
|
8
8
|
// reference, not at the root, not above it. The caller hands a canonical root (its own trust anchor);
|
|
9
9
|
// a root reached through any link is refused outright.
|
|
10
10
|
export const RECEIPT_UNAVAILABLE_REASONS = ["outside-root", "symlink", "hash-mismatch", "expired", "missing"];
|
|
11
|
+
/** OBS-1140: at most this many newest eviction tombstones persist; an older eviction reads as missing. */
|
|
12
|
+
export const EVICTION_TOMBSTONE_LIMIT = 256;
|
|
13
|
+
/** Beside gate-evidence/, never inside it: tombstones are metadata, not retained artifact bytes. */
|
|
14
|
+
export const EVICTION_TOMBSTONES_FILE = "evidence-evictions.json";
|
|
15
|
+
/** Newest last, at most the limit; anything malformed is no tombstone at all (fail closed to missing). */
|
|
16
|
+
export function parseEvictionTombstones(bytes) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(bytes.toString("utf8"));
|
|
19
|
+
return Array.isArray(raw) ? raw.filter((t) => typeof t?.path === "string"
|
|
20
|
+
&& typeof t.sha256 === "string" && Number.isInteger(t.retainedBytes)).slice(-EVICTION_TOMBSTONE_LIMIT) : [];
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
/** Resolve one artifact reference against the run root it was recorded under. Fails closed. */
|
|
12
27
|
export function resolveReceipt(reference, runRoot) {
|
|
13
28
|
const parsed = EvidenceArtifactSchema.safeParse(reference);
|
|
@@ -27,6 +42,8 @@ export function resolveReceipt(reference, runRoot) {
|
|
|
27
42
|
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
28
43
|
return { ok: false, path: ref.path, reason: "outside-root" };
|
|
29
44
|
const read = readUnderRoot(root, rel.split(sep));
|
|
45
|
+
if (read === "missing" && evictedByQuota(root, ref))
|
|
46
|
+
return { ok: false, path: ref.path, reason: "expired" };
|
|
30
47
|
if (typeof read === "string")
|
|
31
48
|
return { ok: false, path: ref.path, reason: read };
|
|
32
49
|
const sha256 = createHash("sha256").update(read).digest("hex");
|
|
@@ -34,6 +51,28 @@ export function resolveReceipt(reference, runRoot) {
|
|
|
34
51
|
return { ok: false, path: ref.path, reason: "hash-mismatch" };
|
|
35
52
|
return { ok: true, path: ref.path, sha256 };
|
|
36
53
|
}
|
|
54
|
+
/** A deletion is explained only by a tombstone naming this exact path, hash and length (OBS-1140). */
|
|
55
|
+
function evictedByQuota(root, ref) {
|
|
56
|
+
const bytes = readUnderRoot(root, [EVICTION_TOMBSTONES_FILE]);
|
|
57
|
+
return typeof bytes !== "string" && parseEvictionTombstones(bytes)
|
|
58
|
+
.some(t => t.path === ref.path && t.sha256 === ref.sha256 && t.retainedBytes === ref.retainedBytes);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* OBS-1139: what a receipt says was withheld. A counted receipt reports its per-span counts; a legacy
|
|
62
|
+
* one keeps its recorded `material` with no counts; anything unparseable is material (fail closed).
|
|
63
|
+
*/
|
|
64
|
+
export function resolveReceiptRedaction(receipt) {
|
|
65
|
+
const parsed = RedactionSchema.safeParse(receipt?.redaction);
|
|
66
|
+
if (!parsed.success)
|
|
67
|
+
return { material: true, counts: null };
|
|
68
|
+
return { material: parsed.data.material, counts: parsed.data.counts ?? null };
|
|
69
|
+
}
|
|
70
|
+
export function formatReceiptRedaction(r) {
|
|
71
|
+
const head = r.material ? "material" : "nonmaterial";
|
|
72
|
+
return r.counts
|
|
73
|
+
? `redaction ${head} token=${r.counts.token} assignment=${r.counts.assignment} secret-env=${r.counts.secretEnv} benign-env=${r.counts.benignEnv}`
|
|
74
|
+
: `redaction ${head} (uncounted)`;
|
|
75
|
+
}
|
|
37
76
|
const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
38
77
|
/** Every ancestor of the root, root first excluded — "/" itself cannot be a link. */
|
|
39
78
|
function ancestors(root) {
|
|
@@ -2,9 +2,19 @@ import type { JournalEvent } from "./journal.js";
|
|
|
2
2
|
export interface RepairSelectionDecision {
|
|
3
3
|
selectTests: boolean;
|
|
4
4
|
requiredFiles: string[];
|
|
5
|
-
reason: "no-test-failure" | "known-failing-files" | "legacy-test-failure" | "unattributed-test-failure"
|
|
5
|
+
reason: "no-test-failure" | "known-failing-files" | "legacy-test-failure" | "unattributed-test-failure";
|
|
6
6
|
}
|
|
7
7
|
type SelectionEvent = Pick<JournalEvent, "event" | "taskId" | "data">;
|
|
8
|
+
/** OBS-1199: repair selection is on unless a config says otherwise, independent of the optional
|
|
9
|
+
* execution budget (which stays subprocess-only). Either explicit `false` turns it off. */
|
|
10
|
+
export declare function repairSelectionEnabled(cfg?: {
|
|
11
|
+
executionPolicy?: {
|
|
12
|
+
repairSelection?: unknown;
|
|
13
|
+
};
|
|
14
|
+
gates?: {
|
|
15
|
+
repairSelection?: unknown;
|
|
16
|
+
};
|
|
17
|
+
}): boolean;
|
|
8
18
|
/** Diagnostic selection only: neither a narrow pass nor this decision can authorize integration.
|
|
9
19
|
* Distrust and known failing files survive later green results, approvals and resume. A historical
|
|
10
20
|
* ambiguous failure is never reinterpreted using a newly enabled policy. */
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** OBS-1199: repair selection is on unless a config says otherwise, independent of the optional
|
|
2
|
+
* execution budget (which stays subprocess-only). Either explicit `false` turns it off. */
|
|
3
|
+
export function repairSelectionEnabled(cfg) {
|
|
4
|
+
return cfg?.executionPolicy?.repairSelection !== false && cfg?.gates?.repairSelection !== false;
|
|
5
|
+
}
|
|
1
6
|
/** These are literal repository-relative identities. Existence and selection support are checked
|
|
2
7
|
* against the current worktree by the gate; this journal fold cannot establish either. */
|
|
3
8
|
function fileIdentities(value) {
|
|
@@ -32,16 +37,19 @@ export function repairSelectionDecision(events, taskId, enabled) {
|
|
|
32
37
|
distrust ??= "unattributed-test-failure";
|
|
33
38
|
continue;
|
|
34
39
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const selected = fileIdentities(data.selectedTests);
|
|
40
|
+
// OBS-1199: a full-suite red counts only when it positively says it was full — the replacement
|
|
41
|
+
// after a screen (`fullSuite`), or an ordinary full run whose recorded selection decision says
|
|
42
|
+
// so — and names its failing files. Those files join every later screen even when no selector
|
|
43
|
+
// reaches them; the merge candidate still runs the complete suite. No attribution means full.
|
|
42
44
|
const failing = fileIdentities(data.failingFiles);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const selected = fileIdentities(data.selectedTests);
|
|
46
|
+
const decision = data.selectionDecision;
|
|
47
|
+
const replacement = data.fullSuite === true;
|
|
48
|
+
const ordinaryFull = data.fullSuite === undefined && data.selectedTests === undefined
|
|
49
|
+
&& typeof decision === "object" && decision !== null && decision.scope === "full";
|
|
50
|
+
const screen = (data.fullSuite === undefined || data.fullSuite === false) && selected !== undefined
|
|
51
|
+
&& failing !== undefined && failing.every((file) => selected.includes(file));
|
|
52
|
+
if (!failing || !(replacement || ordinaryFull || screen)) {
|
|
45
53
|
distrust ??= "unattributed-test-failure";
|
|
46
54
|
continue;
|
|
47
55
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { JournalEvent } from "./journal.js";
|
|
2
|
+
/**
|
|
3
|
+
* OBS-1201: where a run's wall time went. Every instant of the engagement window lands in exactly ONE
|
|
4
|
+
* bucket — the first of WALL_PRIORITY with a span covering it — and what no span covers is residual,
|
|
5
|
+
* so the exposed buckets always sum to the wall. Task-time sums every span as recorded, so concurrent
|
|
6
|
+
* work (two workers, four suites waiting on one lease) stays visible without inflating the wall.
|
|
7
|
+
*
|
|
8
|
+
* Only journaled observations become spans: a fresh gate row's measured duration less the matched
|
|
9
|
+
* suite waits inside it, a worker launch matched to its result, a suite wait matched to its admission,
|
|
10
|
+
* and the gap a restart left between one engagement's last row and the next run-resume. A copy of
|
|
11
|
+
* evidence — a cache reuse, a journal replay, a row naming an invocation already counted — never adds
|
|
12
|
+
* a span; a reused full suite's row still times the fresh screen it carries. A row without a duration, a launch, wait or gate start with no matching end, and one cut by
|
|
13
|
+
* a restart are counted, never timed. Residual
|
|
14
|
+
* is unattributed time, not a claim of idleness. Pure: journal rows in, numbers out.
|
|
15
|
+
*/
|
|
16
|
+
export declare const WALL_PRIORITY: readonly ["interruption", "test", "semantics", "worker", "queue", "other-gate"];
|
|
17
|
+
export type WallBucket = (typeof WALL_PRIORITY)[number];
|
|
18
|
+
type PerBucket = Record<WallBucket, number>;
|
|
19
|
+
export interface WallBudget {
|
|
20
|
+
wallMs: number;
|
|
21
|
+
exposedMs: PerBucket;
|
|
22
|
+
residualMs: number;
|
|
23
|
+
taskMs: PerBucket;
|
|
24
|
+
/** Gate-result rows by provenance. A declined gate never ran and is none of these. */
|
|
25
|
+
evidence: {
|
|
26
|
+
fresh: number;
|
|
27
|
+
replay: number;
|
|
28
|
+
reuse: number;
|
|
29
|
+
unknown: number;
|
|
30
|
+
};
|
|
31
|
+
/** Evidence that exists but carries no timing: rows without a duration, unmatched and interrupted starts. */
|
|
32
|
+
untimed: Record<WallBucket, {
|
|
33
|
+
unknown: number;
|
|
34
|
+
unmatched: number;
|
|
35
|
+
interrupted: number;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** Undefined when the journal names no measurable window (no run-start, or unreadable timestamps). */
|
|
39
|
+
export declare function wallBudget(events: readonly JournalEvent[]): WallBudget | undefined;
|
|
40
|
+
/** Whole seconds past a minute, one decimal below ten seconds — never a rounded-away "0s" for a real span. */
|
|
41
|
+
export declare const formatSpan: (ms: number) => string;
|
|
42
|
+
/**
|
|
43
|
+
* One fact per bucket, then residual and the evidence tally — the words every surface prints. A
|
|
44
|
+
* bucket whose only evidence is untimed reads `unknown`, never a zero it did not measure.
|
|
45
|
+
*/
|
|
46
|
+
export declare function wallBudgetFacts(b: WallBudget): Array<[label: string, fact: string]>;
|
|
47
|
+
export declare const WALL_PRIORITY_TEXT: string;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { gateDeclined } from "../report/bundle.js";
|
|
2
|
+
/**
|
|
3
|
+
* OBS-1201: where a run's wall time went. Every instant of the engagement window lands in exactly ONE
|
|
4
|
+
* bucket — the first of WALL_PRIORITY with a span covering it — and what no span covers is residual,
|
|
5
|
+
* so the exposed buckets always sum to the wall. Task-time sums every span as recorded, so concurrent
|
|
6
|
+
* work (two workers, four suites waiting on one lease) stays visible without inflating the wall.
|
|
7
|
+
*
|
|
8
|
+
* Only journaled observations become spans: a fresh gate row's measured duration less the matched
|
|
9
|
+
* suite waits inside it, a worker launch matched to its result, a suite wait matched to its admission,
|
|
10
|
+
* and the gap a restart left between one engagement's last row and the next run-resume. A copy of
|
|
11
|
+
* evidence — a cache reuse, a journal replay, a row naming an invocation already counted — never adds
|
|
12
|
+
* a span; a reused full suite's row still times the fresh screen it carries. A row without a duration, a launch, wait or gate start with no matching end, and one cut by
|
|
13
|
+
* a restart are counted, never timed. Residual
|
|
14
|
+
* is unattributed time, not a claim of idleness. Pure: journal rows in, numbers out.
|
|
15
|
+
*/
|
|
16
|
+
export const WALL_PRIORITY = ["interruption", "test", "semantics", "worker", "queue", "other-gate"];
|
|
17
|
+
const perBucket = () => Object.fromEntries(WALL_PRIORITY.map((bucket) => [bucket, 0]));
|
|
18
|
+
const at = (e) => Date.parse(e.ts);
|
|
19
|
+
const measured = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
20
|
+
const bucketOf = (gate) => gate === "test" ? "test" : gate === "acceptance" || gate === "review" ? "semantics" : "other-gate";
|
|
21
|
+
// Rows another process writes around a restart — the operator's approval, the resuming daemon's
|
|
22
|
+
// reclaim and rehash audit — so the prior engagement's own last row is the one before them.
|
|
23
|
+
// ponytail: a closed list; a new out-of-engagement writer lengthens the interruption it precedes.
|
|
24
|
+
const OUTSIDE_ENGAGEMENT = new Set(["task-approved", "graph-rehash", "lock-reclaimed", "superseded"]);
|
|
25
|
+
const outsideEngagement = (e) => OUTSIDE_ENGAGEMENT.has(e.event) || (e.event === "exit-cause" && e.data.cause === "unclean");
|
|
26
|
+
const WORKER_END = new Set(["worker-result", "worker-dead", "worker-hard-timeout"]);
|
|
27
|
+
const invocationOf = (data) => {
|
|
28
|
+
if (typeof data.nonce === "string")
|
|
29
|
+
return data.nonce;
|
|
30
|
+
const receipt = data.evidenceReceipt;
|
|
31
|
+
return typeof receipt?.invocationId === "string" ? receipt.invocationId : undefined;
|
|
32
|
+
};
|
|
33
|
+
/** Undefined when the journal names no measurable window (no run-start, or unreadable timestamps). */
|
|
34
|
+
export function wallBudget(events) {
|
|
35
|
+
const first = events.findIndex((e) => e.event === "run-start");
|
|
36
|
+
if (first < 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const lastOf = (event) => events.map((e) => e.event).lastIndexOf(event);
|
|
39
|
+
const lastEnd = lastOf("run-end");
|
|
40
|
+
const lastResume = lastOf("run-resume");
|
|
41
|
+
const from = at(events[first]);
|
|
42
|
+
// A closed engagement ends at its run-end; an open or crashed one at its newest row.
|
|
43
|
+
const to = at(events[lastEnd > lastResume ? lastEnd : events.length - 1]);
|
|
44
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to < from)
|
|
45
|
+
return undefined;
|
|
46
|
+
const spans = [];
|
|
47
|
+
const span = (bucket, a, b) => {
|
|
48
|
+
const s = Math.max(a, from);
|
|
49
|
+
const t = Math.min(b, to);
|
|
50
|
+
if (Number.isFinite(s) && Number.isFinite(t) && t > s)
|
|
51
|
+
spans.push({ bucket, from: s, to: t });
|
|
52
|
+
};
|
|
53
|
+
const evidence = { fresh: 0, replay: 0, reuse: 0, unknown: 0 };
|
|
54
|
+
const untimed = Object.fromEntries(WALL_PRIORITY.map((bucket) => [bucket, { unknown: 0, unmatched: 0, interrupted: 0 }]));
|
|
55
|
+
const counted = new Set();
|
|
56
|
+
// A gate row is journaled when its whole round settles (acceptance, review and test often share one
|
|
57
|
+
// timestamp), so its measured duration is anchored at the gate's FIRST phase-start in the round.
|
|
58
|
+
const gateStarts = new Map();
|
|
59
|
+
const workers = new Map();
|
|
60
|
+
// A dispatch that never journals a launch (every pre-launch-row journal) ran a worker nobody timed.
|
|
61
|
+
const dispatched = new Set();
|
|
62
|
+
// A wait is owned by the gate whose command was admitted (the daemon names it when the phase is
|
|
63
|
+
// unambiguous; a parallel sibling leaves it absent). Its measurement started before admission, so
|
|
64
|
+
// a matched wait inside that gate's own span is queue, not service, and is cut out before priority
|
|
65
|
+
// applies. Sibling gates keep executing through it, so nothing is cut from them. A wait naming no
|
|
66
|
+
// gate belongs to the suite lease, which is the test gate's.
|
|
67
|
+
const owner = (task, gate) => `${task}\0${typeof gate === "string" ? gate : "test"}`;
|
|
68
|
+
const waits = new Map();
|
|
69
|
+
const queued = new Map();
|
|
70
|
+
const admit = (task, gate, ts) => {
|
|
71
|
+
const key = owner(task, gate);
|
|
72
|
+
const opened = waits.get(key);
|
|
73
|
+
if (opened === undefined)
|
|
74
|
+
return;
|
|
75
|
+
span("queue", opened, ts);
|
|
76
|
+
// One open wait per owner, closed in journal order: the list stays sorted and non-overlapping.
|
|
77
|
+
queued.set(key, [...(queued.get(key) ?? []), [opened, ts]]);
|
|
78
|
+
waits.delete(key);
|
|
79
|
+
};
|
|
80
|
+
const service = (gate, task, a, b) => {
|
|
81
|
+
const bucket = bucketOf(gate);
|
|
82
|
+
// One linear sweep over the sorted waits; span() drops any empty or inverted piece.
|
|
83
|
+
let cursor = a;
|
|
84
|
+
for (const [ws, we] of queued.get(owner(task, gate)) ?? []) {
|
|
85
|
+
if (we <= cursor || ws >= b)
|
|
86
|
+
continue;
|
|
87
|
+
span(bucket, cursor, ws);
|
|
88
|
+
cursor = Math.max(cursor, we);
|
|
89
|
+
}
|
|
90
|
+
span(bucket, cursor, b);
|
|
91
|
+
};
|
|
92
|
+
// A gate that started and never journaled its result is evidence without timing, never a zero.
|
|
93
|
+
const untimedStarts = (starts, kind) => {
|
|
94
|
+
for (const gate of starts?.keys() ?? [])
|
|
95
|
+
untimed[bucketOf(gate)][kind]++;
|
|
96
|
+
};
|
|
97
|
+
for (let i = first + 1; i < events.length; i++) {
|
|
98
|
+
const e = events[i];
|
|
99
|
+
const task = e.taskId ?? "";
|
|
100
|
+
const ts = at(e);
|
|
101
|
+
switch (e.event) {
|
|
102
|
+
case "run-resume": {
|
|
103
|
+
let last = i - 1;
|
|
104
|
+
while (last > first && outsideEngagement(events[last]))
|
|
105
|
+
last--;
|
|
106
|
+
span("interruption", at(events[last]), ts);
|
|
107
|
+
untimed.worker.interrupted += workers.size;
|
|
108
|
+
untimed.worker.unknown += dispatched.size;
|
|
109
|
+
untimed.queue.interrupted += waits.size;
|
|
110
|
+
for (const starts of gateStarts.values())
|
|
111
|
+
untimedStarts(starts, "interrupted");
|
|
112
|
+
workers.clear();
|
|
113
|
+
dispatched.clear();
|
|
114
|
+
waits.clear();
|
|
115
|
+
gateStarts.clear();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "phase-start": {
|
|
119
|
+
if (e.data.phase === "gates") {
|
|
120
|
+
untimedStarts(gateStarts.get(task), "unmatched");
|
|
121
|
+
gateStarts.delete(task);
|
|
122
|
+
}
|
|
123
|
+
else if (typeof e.data.gate === "string") {
|
|
124
|
+
const starts = gateStarts.get(task) ?? new Map();
|
|
125
|
+
if (!starts.has(e.data.gate))
|
|
126
|
+
starts.set(e.data.gate, ts);
|
|
127
|
+
gateStarts.set(task, starts);
|
|
128
|
+
}
|
|
129
|
+
if (e.data.admitted === true)
|
|
130
|
+
admit(task, e.data.gate, ts);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "suite-admitted":
|
|
134
|
+
admit(task, undefined, ts);
|
|
135
|
+
break;
|
|
136
|
+
case "suite-wait":
|
|
137
|
+
// Repeated rows while a wait is open re-report its count; they do not open a second wait.
|
|
138
|
+
if (!waits.has(owner(task, e.data.gate)))
|
|
139
|
+
waits.set(owner(task, e.data.gate), ts);
|
|
140
|
+
break;
|
|
141
|
+
case "task-dispatch":
|
|
142
|
+
if (dispatched.has(task))
|
|
143
|
+
untimed.worker.unknown++;
|
|
144
|
+
dispatched.add(task);
|
|
145
|
+
break;
|
|
146
|
+
case "worker-launch":
|
|
147
|
+
if (workers.has(task))
|
|
148
|
+
untimed.worker.unmatched++;
|
|
149
|
+
workers.set(task, ts);
|
|
150
|
+
dispatched.delete(task);
|
|
151
|
+
break;
|
|
152
|
+
case "gate-provisioned":
|
|
153
|
+
if (measured(e.data.durationMs))
|
|
154
|
+
service(typeof e.data.gate === "string" ? e.data.gate : "build", task, ts - e.data.durationMs, ts);
|
|
155
|
+
break;
|
|
156
|
+
case "gate-result": {
|
|
157
|
+
const gate = e.data.gate;
|
|
158
|
+
if (typeof gate !== "string")
|
|
159
|
+
break;
|
|
160
|
+
const anchor = gateStarts.get(task)?.get(gate);
|
|
161
|
+
gateStarts.get(task)?.delete(gate);
|
|
162
|
+
if (gateDeclined(e.data))
|
|
163
|
+
break;
|
|
164
|
+
const bucket = bucketOf(gate);
|
|
165
|
+
const invocation = invocationOf(e.data);
|
|
166
|
+
const identity = invocation === undefined ? undefined : `${gate}\0${invocation}`;
|
|
167
|
+
if (e.data.reused === true) {
|
|
168
|
+
evidence.reuse++;
|
|
169
|
+
// A held screen runs fresh before its full suite is carried from the cache, and the one
|
|
170
|
+
// merge-candidate row holds both: the screen's measured interval is fresh execution, the
|
|
171
|
+
// carried suite adds none. A replay's copy is not a measurement of this attempt.
|
|
172
|
+
const screen = e.data.fullSuite === true && e.data.replayedFromAttempt === undefined ? e.data.selectedDurationMs : undefined;
|
|
173
|
+
if (!measured(screen) || screen <= 0)
|
|
174
|
+
break;
|
|
175
|
+
evidence.fresh++;
|
|
176
|
+
const screenFrom = anchor ?? ts - screen;
|
|
177
|
+
service(gate, task, screenFrom, Math.min(screenFrom + screen, ts));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (e.data.replayedFromAttempt !== undefined || (identity !== undefined && counted.has(identity))) {
|
|
181
|
+
evidence.replay++;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (identity !== undefined)
|
|
185
|
+
counted.add(identity);
|
|
186
|
+
const d = e.data.durationMs;
|
|
187
|
+
if (!measured(d)) {
|
|
188
|
+
evidence.unknown++;
|
|
189
|
+
untimed[bucket].unknown++;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
evidence.fresh++;
|
|
193
|
+
const full = e.data.fullSuite === true && measured(e.data.fullDurationMs) && measured(e.data.selectedDurationMs);
|
|
194
|
+
if (anchor === undefined)
|
|
195
|
+
service(gate, task, ts - d, ts);
|
|
196
|
+
else if (full) {
|
|
197
|
+
// The full suite ends at the row; the selected run began at the gate's first phase-start.
|
|
198
|
+
const fullFrom = ts - e.data.fullDurationMs;
|
|
199
|
+
service(gate, task, anchor, Math.min(anchor + e.data.selectedDurationMs, fullFrom));
|
|
200
|
+
service(gate, task, fullFrom, ts);
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
service(gate, task, anchor, Math.min(anchor + d, ts));
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
if (WORKER_END.has(e.event) && workers.has(task)) {
|
|
208
|
+
span("worker", workers.get(task), ts);
|
|
209
|
+
workers.delete(task);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
untimed.worker.unmatched += workers.size;
|
|
214
|
+
untimed.worker.unknown += dispatched.size;
|
|
215
|
+
untimed.queue.unmatched += waits.size;
|
|
216
|
+
for (const starts of gateStarts.values())
|
|
217
|
+
untimedStarts(starts, "unmatched");
|
|
218
|
+
const taskMs = perBucket();
|
|
219
|
+
for (const s of spans)
|
|
220
|
+
taskMs[s.bucket] += s.to - s.from;
|
|
221
|
+
// One sweep over span edges: each segment goes to the highest-priority bucket still open over it.
|
|
222
|
+
const rank = (bucket) => WALL_PRIORITY.indexOf(bucket);
|
|
223
|
+
const edges = spans.flatMap((s) => [[s.from, rank(s.bucket), 1], [s.to, rank(s.bucket), -1]])
|
|
224
|
+
.sort((a, b) => a[0] - b[0]);
|
|
225
|
+
const open = WALL_PRIORITY.map(() => 0);
|
|
226
|
+
const exposedMs = perBucket();
|
|
227
|
+
let cursor = from;
|
|
228
|
+
for (const [t, r, delta] of edges) {
|
|
229
|
+
if (t > cursor) {
|
|
230
|
+
const top = open.findIndex((n) => n > 0);
|
|
231
|
+
if (top >= 0)
|
|
232
|
+
exposedMs[WALL_PRIORITY[top]] += t - cursor;
|
|
233
|
+
cursor = t;
|
|
234
|
+
}
|
|
235
|
+
open[r] += delta;
|
|
236
|
+
}
|
|
237
|
+
const wallMs = to - from;
|
|
238
|
+
const residualMs = wallMs - WALL_PRIORITY.reduce((sum, bucket) => sum + exposedMs[bucket], 0);
|
|
239
|
+
return { wallMs, exposedMs, residualMs, taskMs, evidence, untimed };
|
|
240
|
+
}
|
|
241
|
+
/** Whole seconds past a minute, one decimal below ten seconds — never a rounded-away "0s" for a real span. */
|
|
242
|
+
export const formatSpan = (ms) => {
|
|
243
|
+
if (ms > 0 && ms < 10_000)
|
|
244
|
+
return `${(Math.ceil(ms / 100) / 10).toString()}s`;
|
|
245
|
+
const s = Math.round(ms / 1_000);
|
|
246
|
+
if (s < 60)
|
|
247
|
+
return `${s}s`;
|
|
248
|
+
if (s < 3_600)
|
|
249
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
250
|
+
return `${Math.floor(s / 3_600)}h ${Math.floor((s % 3_600) / 60)}m`;
|
|
251
|
+
};
|
|
252
|
+
const plural = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
253
|
+
/**
|
|
254
|
+
* One fact per bucket, then residual and the evidence tally — the words every surface prints. A
|
|
255
|
+
* bucket whose only evidence is untimed reads `unknown`, never a zero it did not measure.
|
|
256
|
+
*/
|
|
257
|
+
export function wallBudgetFacts(b) {
|
|
258
|
+
const pct = (ms) => b.wallMs > 0 ? ` (${Math.round((100 * ms) / b.wallMs)}%)` : "";
|
|
259
|
+
const facts = WALL_PRIORITY.map((bucket) => {
|
|
260
|
+
const { unknown, unmatched, interrupted } = b.untimed[bucket];
|
|
261
|
+
const notes = [
|
|
262
|
+
...(unknown ? [bucket === "worker"
|
|
263
|
+
? `${unknown} ${unknown === 1 ? "dispatch" : "dispatches"} without a launch row`
|
|
264
|
+
: `${plural(unknown, "row")} without a duration`] : []),
|
|
265
|
+
...(unmatched ? [`${unmatched} unmatched`] : []),
|
|
266
|
+
...(interrupted ? [`${interrupted} cut by a restart`] : []),
|
|
267
|
+
];
|
|
268
|
+
if (!b.exposedMs[bucket] && !b.taskMs[bucket] && notes.length)
|
|
269
|
+
return [bucket, `unknown — ${notes.join(" · ")}`];
|
|
270
|
+
const timed = `${formatSpan(b.exposedMs[bucket])}${pct(b.exposedMs[bucket])} · task-time ${formatSpan(b.taskMs[bucket])}`;
|
|
271
|
+
return [bucket, [timed, ...notes.map((note) => `${note}, untimed`)].join(" · ")];
|
|
272
|
+
});
|
|
273
|
+
const { fresh, replay, reuse, unknown } = b.evidence;
|
|
274
|
+
return [
|
|
275
|
+
...facts,
|
|
276
|
+
["residual", `${formatSpan(b.residualMs)}${pct(b.residualMs)} — unattributed, not proven idle`],
|
|
277
|
+
["gate evidence", `fresh ${fresh} · replay ${replay} · reuse ${reuse} · unknown ${unknown}`],
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
export const WALL_PRIORITY_TEXT = [...WALL_PRIORITY, "residual"].join(" › ");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type JournalEvent } from "../../run/journal.js";
|
|
2
2
|
import { type OperatorRecord } from "../../run/operator-state.js";
|
|
3
3
|
export declare const OBSERVATION_INTERVAL_MS = 1000;
|
|
4
|
+
/** Stat-to-fstat observations one poll spends on a journal a writer keeps appending to. */
|
|
5
|
+
export declare const APPEND_RACE_OBSERVATIONS = 3;
|
|
4
6
|
export declare const STORE_LIMITS: {
|
|
5
7
|
readonly graphBytes: number;
|
|
6
8
|
readonly history: 256;
|
|
@@ -61,12 +63,16 @@ export declare class JournalTail {
|
|
|
61
63
|
private bytesRead;
|
|
62
64
|
private lastSuccessfulReadAt?;
|
|
63
65
|
private failure?;
|
|
66
|
+
private stale;
|
|
64
67
|
constructor(source: string, hooks?: {
|
|
65
68
|
record?: (record: OperatorRecord) => void;
|
|
66
69
|
reset?: () => void;
|
|
67
70
|
});
|
|
68
71
|
private reset;
|
|
72
|
+
/** A same-inode append between stat and fstat is retried; exhaustion keeps the last good snapshot, pending. */
|
|
69
73
|
poll(now?: number): TailSnapshot;
|
|
74
|
+
/** One stat-to-fstat observation. False when an append raced it; nothing was consumed or reset. */
|
|
75
|
+
private observe;
|
|
70
76
|
private appendCarry;
|
|
71
77
|
snapshot(): TailSnapshot;
|
|
72
78
|
/** Demand paging scans with fixed buffers, independent of the retained history; no growing line index. */
|