instar 1.3.636 → 1.3.637
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/dist/core/storage/JsonlStore.d.ts +54 -0
- package/dist/core/storage/JsonlStore.d.ts.map +1 -0
- package/dist/core/storage/JsonlStore.js +81 -0
- package/dist/core/storage/JsonlStore.js.map +1 -0
- package/dist/utils/jsonl-rotation.d.ts +37 -0
- package/dist/utils/jsonl-rotation.d.ts.map +1 -1
- package/dist/utils/jsonl-rotation.js +86 -0
- package/dist/utils/jsonl-rotation.js.map +1 -1
- package/package.json +2 -2
- package/scripts/bounded-accumulation-retention-baseline.json +81 -0
- package/scripts/bounded-accumulation-wholefile-read-baseline.json +5 -0
- package/scripts/lint-no-wholefile-sync-read.js +128 -0
- package/scripts/lint-store-retention-declared.js +93 -0
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/state-coherence-registry.json +71 -10
- package/upgrades/1.3.637.md +41 -0
- package/upgrades/side-effects/bounded-accumulation-increment-1.md +103 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonlStore — the registered accessor for append-only JSONL persistence
|
|
3
|
+
* (Bounded Accumulation standard §3a/§3b).
|
|
4
|
+
*
|
|
5
|
+
* Every persistent JSONL store routes through this class so that:
|
|
6
|
+
* 1. The retention policy (maxBytes / keepSegments / archive) is declared and
|
|
7
|
+
* ENFORCED at the one place data is written — not hoped for at each callsite.
|
|
8
|
+
* 2. The lint surface is a single funnel: raw `fs.appendFileSync` to a `.instar/`
|
|
9
|
+
* path in `src/` is itself a Lint-1 failure; an author must use this accessor,
|
|
10
|
+
* which makes the retention declaration unskippable.
|
|
11
|
+
* 3. Rotation is the event-loop-SAFE segment cut (rename, not read-filter-rewrite),
|
|
12
|
+
* gated behind a cached byte-counter so the size check is amortized — even the
|
|
13
|
+
* O(1) `statSync` is not paid on every append.
|
|
14
|
+
*
|
|
15
|
+
* This is the storage twin of the funnels SafeFsExecutor / SafeGitExecutor already
|
|
16
|
+
* establish for destructive fs/git ops: one chokepoint that a lint can enforce.
|
|
17
|
+
*/
|
|
18
|
+
export interface JsonlStoreOptions {
|
|
19
|
+
/** Maximum active-file size in bytes before a segment is cut. Default: 32MB. */
|
|
20
|
+
maxBytes?: number;
|
|
21
|
+
/** Rotated segments to retain (oldest beyond this are unlinked, unless archive). Default: 4. */
|
|
22
|
+
keepSegments?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Compliance-hold: rotated segments are NEVER unlinked. For audit/forensic trails
|
|
25
|
+
* that must never lose their oldest entries. Default: false.
|
|
26
|
+
*/
|
|
27
|
+
archive?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Amortize the rotation size-check: only check (and possibly rotate) after this many
|
|
30
|
+
* bytes have been appended since the last check. Keeps the hot append path a pure
|
|
31
|
+
* `appendFileSync` + counter bump. Default: 64KB.
|
|
32
|
+
*/
|
|
33
|
+
checkEveryBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class JsonlStore {
|
|
36
|
+
private readonly filePath;
|
|
37
|
+
private readonly maxBytes;
|
|
38
|
+
private readonly keepSegments;
|
|
39
|
+
private readonly archive;
|
|
40
|
+
private readonly checkEveryBytes;
|
|
41
|
+
/** Bytes appended since the last rotation size-check (the cached counter). */
|
|
42
|
+
private bytesSinceCheck;
|
|
43
|
+
constructor(filePath: string, options?: JsonlStoreOptions);
|
|
44
|
+
/** Append one raw line (a trailing newline is added if absent). */
|
|
45
|
+
append(line: string): void;
|
|
46
|
+
/** Append one JSON-serialized object as a line. */
|
|
47
|
+
appendObject(obj: unknown): void;
|
|
48
|
+
/** Force a rotation size-check now (e.g. on a periodic sweep), bypassing the counter. */
|
|
49
|
+
checkRotationNow(): boolean;
|
|
50
|
+
/** The active file path this store writes to. */
|
|
51
|
+
get path(): string;
|
|
52
|
+
private ensureDir;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=JsonlStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JsonlStore.d.ts","sourceRoot":"","sources":["../../../src/core/storage/JsonlStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,8EAA8E;IAC9E,OAAO,CAAC,eAAe,CAAK;gBAEhB,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB;IAQ7D,mEAAmE;IACnE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAgB1B,mDAAmD;IACnD,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAIhC,yFAAyF;IACzF,gBAAgB,IAAI,OAAO;IAS3B,iDAAiD;IACjD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,SAAS;CAOlB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonlStore — the registered accessor for append-only JSONL persistence
|
|
3
|
+
* (Bounded Accumulation standard §3a/§3b).
|
|
4
|
+
*
|
|
5
|
+
* Every persistent JSONL store routes through this class so that:
|
|
6
|
+
* 1. The retention policy (maxBytes / keepSegments / archive) is declared and
|
|
7
|
+
* ENFORCED at the one place data is written — not hoped for at each callsite.
|
|
8
|
+
* 2. The lint surface is a single funnel: raw `fs.appendFileSync` to a `.instar/`
|
|
9
|
+
* path in `src/` is itself a Lint-1 failure; an author must use this accessor,
|
|
10
|
+
* which makes the retention declaration unskippable.
|
|
11
|
+
* 3. Rotation is the event-loop-SAFE segment cut (rename, not read-filter-rewrite),
|
|
12
|
+
* gated behind a cached byte-counter so the size check is amortized — even the
|
|
13
|
+
* O(1) `statSync` is not paid on every append.
|
|
14
|
+
*
|
|
15
|
+
* This is the storage twin of the funnels SafeFsExecutor / SafeGitExecutor already
|
|
16
|
+
* establish for destructive fs/git ops: one chokepoint that a lint can enforce.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { maybeRotateJsonlSegment } from '../../utils/jsonl-rotation.js';
|
|
21
|
+
const DEFAULT_MAX_BYTES = 32 * 1024 * 1024; // 32MB
|
|
22
|
+
const DEFAULT_KEEP_SEGMENTS = 4;
|
|
23
|
+
const DEFAULT_CHECK_EVERY_BYTES = 64 * 1024; // 64KB
|
|
24
|
+
export class JsonlStore {
|
|
25
|
+
filePath;
|
|
26
|
+
maxBytes;
|
|
27
|
+
keepSegments;
|
|
28
|
+
archive;
|
|
29
|
+
checkEveryBytes;
|
|
30
|
+
/** Bytes appended since the last rotation size-check (the cached counter). */
|
|
31
|
+
bytesSinceCheck = 0;
|
|
32
|
+
constructor(filePath, options = {}) {
|
|
33
|
+
this.filePath = filePath;
|
|
34
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
35
|
+
this.keepSegments = Math.max(1, options.keepSegments ?? DEFAULT_KEEP_SEGMENTS);
|
|
36
|
+
this.archive = options.archive ?? false;
|
|
37
|
+
this.checkEveryBytes = Math.max(1, options.checkEveryBytes ?? DEFAULT_CHECK_EVERY_BYTES);
|
|
38
|
+
}
|
|
39
|
+
/** Append one raw line (a trailing newline is added if absent). */
|
|
40
|
+
append(line) {
|
|
41
|
+
const data = line.endsWith('\n') ? line : line + '\n';
|
|
42
|
+
this.ensureDir();
|
|
43
|
+
fs.appendFileSync(this.filePath, data);
|
|
44
|
+
this.bytesSinceCheck += Buffer.byteLength(data);
|
|
45
|
+
if (this.bytesSinceCheck >= this.checkEveryBytes) {
|
|
46
|
+
this.bytesSinceCheck = 0;
|
|
47
|
+
// O(1) segment cut when over maxBytes; never throws.
|
|
48
|
+
maybeRotateJsonlSegment(this.filePath, {
|
|
49
|
+
maxBytes: this.maxBytes,
|
|
50
|
+
keepSegments: this.keepSegments,
|
|
51
|
+
archive: this.archive,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Append one JSON-serialized object as a line. */
|
|
56
|
+
appendObject(obj) {
|
|
57
|
+
this.append(JSON.stringify(obj));
|
|
58
|
+
}
|
|
59
|
+
/** Force a rotation size-check now (e.g. on a periodic sweep), bypassing the counter. */
|
|
60
|
+
checkRotationNow() {
|
|
61
|
+
this.bytesSinceCheck = 0;
|
|
62
|
+
return maybeRotateJsonlSegment(this.filePath, {
|
|
63
|
+
maxBytes: this.maxBytes,
|
|
64
|
+
keepSegments: this.keepSegments,
|
|
65
|
+
archive: this.archive,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** The active file path this store writes to. */
|
|
69
|
+
get path() {
|
|
70
|
+
return this.filePath;
|
|
71
|
+
}
|
|
72
|
+
ensureDir() {
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// directory likely exists; append will surface a real error if not
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=JsonlStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JsonlStore.js","sourceRoot":"","sources":["../../../src/core/storage/JsonlStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAoBxE,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO;AACnD,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAChC,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO;AAEpD,MAAM,OAAO,UAAU;IACJ,QAAQ,CAAS;IACjB,QAAQ,CAAS;IACjB,YAAY,CAAS;IACrB,OAAO,CAAU;IACjB,eAAe,CAAS;IACzC,8EAA8E;IACtE,eAAe,GAAG,CAAC,CAAC;IAE5B,YAAY,QAAgB,EAAE,UAA6B,EAAE;QAC3D,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;QACtD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC,CAAC;QAC/E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC;QACxC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,eAAe,IAAI,yBAAyB,CAAC,CAAC;IAC3F,CAAC;IAED,mEAAmE;IACnE,MAAM,CAAC,IAAY;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;QACtD,IAAI,CAAC,SAAS,EAAE,CAAC;QACjB,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,eAAe,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACjD,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;YACzB,qDAAqD;YACrD,uBAAuB,CAAC,IAAI,CAAC,QAAQ,EAAE;gBACrC,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,YAAY,EAAE,IAAI,CAAC,YAAY;gBAC/B,OAAO,EAAE,IAAI,CAAC,OAAO;aACtB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,mDAAmD;IACnD,YAAY,CAAC,GAAY;QACvB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,CAAC;IAED,yFAAyF;IACzF,gBAAgB;QACd,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,OAAO,uBAAuB,CAAC,IAAI,CAAC,QAAQ,EAAE;YAC5C,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB,CAAC,CAAC;IACL,CAAC;IAED,iDAAiD;IACjD,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAEO,SAAS;QACf,IAAI,CAAC;YACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;QACrE,CAAC;IACH,CAAC;CACF"}
|
|
@@ -21,7 +21,44 @@ export interface RotationOptions {
|
|
|
21
21
|
* Check if a JSONL file exceeds its size limit, and if so, rotate it
|
|
22
22
|
* by keeping only the most recent lines.
|
|
23
23
|
*
|
|
24
|
+
* NON-CONFORMANT to the Bounded Accumulation standard (§3.5): this rotates by
|
|
25
|
+
* `readFileSync` (whole file) + `split` + `writeFileSync`, which blocks the event
|
|
26
|
+
* loop for hundreds of ms on a multi-MB file — the exact stall the standard exists
|
|
27
|
+
* to kill. Prefer {@link maybeRotateJsonlSegment} (constant-time rename) for any
|
|
28
|
+
* store registered `access: 'streamed'`. Retained for back-compat callers pending
|
|
29
|
+
* their migration (Bounded Accumulation Increment 2).
|
|
30
|
+
*
|
|
24
31
|
* @returns true if rotation occurred, false otherwise
|
|
25
32
|
*/
|
|
26
33
|
export declare function maybeRotateJsonl(filePath: string, options?: RotationOptions): boolean;
|
|
34
|
+
export interface SegmentRotationOptions {
|
|
35
|
+
/** Maximum active-file size in bytes before a segment is cut. Default: 10MB */
|
|
36
|
+
maxBytes?: number;
|
|
37
|
+
/** Number of rotated segments to retain (oldest beyond this are unlinked). Default: 4 */
|
|
38
|
+
keepSegments?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Compliance-hold mode: rotated segments are NEVER unlinked. An audit/forensic
|
|
41
|
+
* trail (security.jsonl, destructive-ops.jsonl) must not lose its oldest entries —
|
|
42
|
+
* it is bounded by archive policy, never by drop. Default: false.
|
|
43
|
+
*/
|
|
44
|
+
archive?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Segment-based JSONL rotation — the event-loop-SAFE alternative to
|
|
48
|
+
* {@link maybeRotateJsonl}'s read-filter-rewrite (Bounded Accumulation standard §3.5).
|
|
49
|
+
*
|
|
50
|
+
* When the active file exceeds `maxBytes`, the active file is RENAMED to a numbered
|
|
51
|
+
* segment `<name>.<seq>` (a constant-time metadata op — NO file read, NO whole-file
|
|
52
|
+
* rewrite) and a fresh empty active file is opened. Oldest segments beyond
|
|
53
|
+
* `keepSegments` are unlinked — UNLESS `archive: true`, which retains every segment
|
|
54
|
+
* (compliance/audit trails that must never drop their oldest entries).
|
|
55
|
+
*
|
|
56
|
+
* Why it exists: rotating a 14MB hot log via the old read+split+rewrite path froze the
|
|
57
|
+
* event loop for hundreds of ms on the append path; renaming a segment is O(1). Callers
|
|
58
|
+
* SHOULD gate the size-check behind a cached byte-counter (see `JsonlStore`) so even the
|
|
59
|
+
* O(1) `statSync` is not paid on every append.
|
|
60
|
+
*
|
|
61
|
+
* @returns true if a segment was cut, false otherwise. Never throws.
|
|
62
|
+
*/
|
|
63
|
+
export declare function maybeRotateJsonlSegment(filePath: string, options?: SegmentRotationOptions): boolean;
|
|
27
64
|
//# sourceMappingURL=jsonl-rotation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonl-rotation.d.ts","sourceRoot":"","sources":["../../src/utils/jsonl-rotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"jsonl-rotation.d.ts","sourceRoot":"","sources":["../../src/utils/jsonl-rotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAcD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAyCrF;AAID,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAqDnG"}
|
|
@@ -12,15 +12,27 @@
|
|
|
12
12
|
* - Lazy rotation — called before/after append, no background timers
|
|
13
13
|
*/
|
|
14
14
|
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
15
16
|
import { SafeFsExecutor } from '../core/SafeFsExecutor.js';
|
|
16
17
|
// ── Constants ────────────────────────────────────────────────────────
|
|
17
18
|
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024; // 10MB
|
|
18
19
|
const DEFAULT_KEEP_RATIO = 0.75;
|
|
20
|
+
const DEFAULT_KEEP_SEGMENTS = 4;
|
|
21
|
+
function escapeRegExp(s) {
|
|
22
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
23
|
+
}
|
|
19
24
|
// ── Public API ───────────────────────────────────────────────────────
|
|
20
25
|
/**
|
|
21
26
|
* Check if a JSONL file exceeds its size limit, and if so, rotate it
|
|
22
27
|
* by keeping only the most recent lines.
|
|
23
28
|
*
|
|
29
|
+
* NON-CONFORMANT to the Bounded Accumulation standard (§3.5): this rotates by
|
|
30
|
+
* `readFileSync` (whole file) + `split` + `writeFileSync`, which blocks the event
|
|
31
|
+
* loop for hundreds of ms on a multi-MB file — the exact stall the standard exists
|
|
32
|
+
* to kill. Prefer {@link maybeRotateJsonlSegment} (constant-time rename) for any
|
|
33
|
+
* store registered `access: 'streamed'`. Retained for back-compat callers pending
|
|
34
|
+
* their migration (Bounded Accumulation Increment 2).
|
|
35
|
+
*
|
|
24
36
|
* @returns true if rotation occurred, false otherwise
|
|
25
37
|
*/
|
|
26
38
|
export function maybeRotateJsonl(filePath, options) {
|
|
@@ -61,4 +73,78 @@ export function maybeRotateJsonl(filePath, options) {
|
|
|
61
73
|
return false;
|
|
62
74
|
}
|
|
63
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Segment-based JSONL rotation — the event-loop-SAFE alternative to
|
|
78
|
+
* {@link maybeRotateJsonl}'s read-filter-rewrite (Bounded Accumulation standard §3.5).
|
|
79
|
+
*
|
|
80
|
+
* When the active file exceeds `maxBytes`, the active file is RENAMED to a numbered
|
|
81
|
+
* segment `<name>.<seq>` (a constant-time metadata op — NO file read, NO whole-file
|
|
82
|
+
* rewrite) and a fresh empty active file is opened. Oldest segments beyond
|
|
83
|
+
* `keepSegments` are unlinked — UNLESS `archive: true`, which retains every segment
|
|
84
|
+
* (compliance/audit trails that must never drop their oldest entries).
|
|
85
|
+
*
|
|
86
|
+
* Why it exists: rotating a 14MB hot log via the old read+split+rewrite path froze the
|
|
87
|
+
* event loop for hundreds of ms on the append path; renaming a segment is O(1). Callers
|
|
88
|
+
* SHOULD gate the size-check behind a cached byte-counter (see `JsonlStore`) so even the
|
|
89
|
+
* O(1) `statSync` is not paid on every append.
|
|
90
|
+
*
|
|
91
|
+
* @returns true if a segment was cut, false otherwise. Never throws.
|
|
92
|
+
*/
|
|
93
|
+
export function maybeRotateJsonlSegment(filePath, options) {
|
|
94
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
95
|
+
const keepSegments = Math.max(1, options?.keepSegments ?? DEFAULT_KEEP_SEGMENTS);
|
|
96
|
+
const archive = options?.archive ?? false;
|
|
97
|
+
try {
|
|
98
|
+
let size;
|
|
99
|
+
try {
|
|
100
|
+
size = fs.statSync(filePath).size; // O(1) — no file read
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false; // no active file yet → nothing to rotate
|
|
104
|
+
}
|
|
105
|
+
if (size <= maxBytes)
|
|
106
|
+
return false;
|
|
107
|
+
const dir = path.dirname(filePath);
|
|
108
|
+
const base = path.basename(filePath);
|
|
109
|
+
// Rotated segments are "<base>.<seq>" (optionally ".gz"); seq is monotonic.
|
|
110
|
+
const segRe = new RegExp('^' + escapeRegExp(base) + '\\.(\\d+)(?:\\.gz)?$');
|
|
111
|
+
const segments = [];
|
|
112
|
+
let maxSeq = 0;
|
|
113
|
+
for (const f of fs.readdirSync(dir)) {
|
|
114
|
+
const m = f.match(segRe);
|
|
115
|
+
if (!m)
|
|
116
|
+
continue;
|
|
117
|
+
const seq = parseInt(m[1], 10);
|
|
118
|
+
if (!Number.isFinite(seq))
|
|
119
|
+
continue;
|
|
120
|
+
segments.push({ seq, name: f });
|
|
121
|
+
if (seq > maxSeq)
|
|
122
|
+
maxSeq = seq;
|
|
123
|
+
}
|
|
124
|
+
const nextSeq = maxSeq + 1;
|
|
125
|
+
// Cut the segment: rename active → segment (constant-time), open a fresh active.
|
|
126
|
+
fs.renameSync(filePath, path.join(dir, base + '.' + nextSeq));
|
|
127
|
+
fs.writeFileSync(filePath, '');
|
|
128
|
+
// Prune oldest segments beyond keepSegments — NEVER in archive (compliance) mode.
|
|
129
|
+
if (!archive) {
|
|
130
|
+
const cutoff = nextSeq - keepSegments; // segments with seq <= cutoff are dropped
|
|
131
|
+
for (const s of segments) {
|
|
132
|
+
if (s.seq <= cutoff) {
|
|
133
|
+
try {
|
|
134
|
+
SafeFsExecutor.safeUnlinkSync(path.join(dir, s.name), {
|
|
135
|
+
operation: 'src/utils/jsonl-rotation.ts:maybeRotateJsonlSegment',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// best effort — a failed unlink just leaves an extra old segment
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
64
150
|
//# sourceMappingURL=jsonl-rotation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonl-rotation.js","sourceRoot":"","sources":["../../src/utils/jsonl-rotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"jsonl-rotation.js","sourceRoot":"","sources":["../../src/utils/jsonl-rotation.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instar",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.637",
|
|
4
4
|
"description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test:contract": "node scripts/run-contract-tests.js",
|
|
29
29
|
"test:contract:raw": "vitest run --config vitest.contract.config.ts",
|
|
30
30
|
"test:all": "vitest run && vitest run --config vitest.integration.config.ts && vitest run --config vitest.e2e.config.ts",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-store-retention-declared.js && node scripts/lint-no-wholefile-sync-read.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js",
|
|
32
32
|
"lint:destructive": "node scripts/lint-no-direct-destructive.js",
|
|
33
33
|
"lint:dev-agent-dark-gate": "node scripts/lint-dev-agent-dark-gate.js",
|
|
34
34
|
"lint:guard-manifest": "node scripts/lint-guard-manifest.js",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "FROZEN baseline of registry categories without a retention policy at Bounded Accumulation Increment 1. The lint forbids NEW categories without retention; this set may only SHRINK (D6 set-monotonicity). Giving a baselined category retention counts the backlog down.",
|
|
3
|
+
"frozenAt": "2026-06-21",
|
|
4
|
+
"categories": [
|
|
5
|
+
"attention-queue",
|
|
6
|
+
"audit-a2a-messages",
|
|
7
|
+
"audit-activity",
|
|
8
|
+
"audit-apprenticeship-decisions",
|
|
9
|
+
"audit-decision-journal",
|
|
10
|
+
"audit-operation-log",
|
|
11
|
+
"audit-prompt-gate",
|
|
12
|
+
"audit-reap-log",
|
|
13
|
+
"audit-reaper",
|
|
14
|
+
"audit-recovery-events",
|
|
15
|
+
"audit-sentinel-events",
|
|
16
|
+
"audit-skill-telemetry",
|
|
17
|
+
"audit-trust-chain",
|
|
18
|
+
"audit-watchdog-interventions",
|
|
19
|
+
"autonomous-working-artifacts",
|
|
20
|
+
"commitment-opkeys",
|
|
21
|
+
"commitment-pending-mutations",
|
|
22
|
+
"commitment-replicas",
|
|
23
|
+
"commitments",
|
|
24
|
+
"conversation-live-tail",
|
|
25
|
+
"derived-discovery",
|
|
26
|
+
"derived-docs-code-sync",
|
|
27
|
+
"derived-framework-model-preferences",
|
|
28
|
+
"derived-memory-index",
|
|
29
|
+
"derived-project-map",
|
|
30
|
+
"derived-projects-digest",
|
|
31
|
+
"derived-topic-memory",
|
|
32
|
+
"ephemeral-caches",
|
|
33
|
+
"fleet-config",
|
|
34
|
+
"guard-posture-episodes",
|
|
35
|
+
"guard-posture-peers",
|
|
36
|
+
"guard-posture-snapshot",
|
|
37
|
+
"job-definitions",
|
|
38
|
+
"job-run-state",
|
|
39
|
+
"learned-preferences",
|
|
40
|
+
"lease-coordination-state",
|
|
41
|
+
"legacy-singletons",
|
|
42
|
+
"listener-daemon-internals",
|
|
43
|
+
"machine-identity-keys",
|
|
44
|
+
"message-store-and-threads",
|
|
45
|
+
"messaging-adapter-state",
|
|
46
|
+
"nonce-sequence-watermarks",
|
|
47
|
+
"pending-pulls",
|
|
48
|
+
"per-session-state",
|
|
49
|
+
"platform-attachment-caches",
|
|
50
|
+
"process-restart-coordination",
|
|
51
|
+
"projects-registry",
|
|
52
|
+
"pull-opkeys",
|
|
53
|
+
"quota-state",
|
|
54
|
+
"relationships",
|
|
55
|
+
"relay-delivery-queues",
|
|
56
|
+
"remote-ack-queue",
|
|
57
|
+
"remote-close-audit",
|
|
58
|
+
"resume-queue",
|
|
59
|
+
"secret-vault",
|
|
60
|
+
"session-monitor-ctx-notified",
|
|
61
|
+
"slack-pending-registrations",
|
|
62
|
+
"slack-permission-decisions",
|
|
63
|
+
"slack-relationship-baselines",
|
|
64
|
+
"soul-identity-documents",
|
|
65
|
+
"stream-tickets",
|
|
66
|
+
"threadline-conversation-state",
|
|
67
|
+
"threadline-pairing-result",
|
|
68
|
+
"threadline-transport-internals",
|
|
69
|
+
"threadline-trust-profiles",
|
|
70
|
+
"topic-ownership-current",
|
|
71
|
+
"topic-project-bindings",
|
|
72
|
+
"trust-elevation-incidents",
|
|
73
|
+
"uncertain-correction-capture-backlog",
|
|
74
|
+
"uncertain-instructions-tracking",
|
|
75
|
+
"uncertain-project-round-worktrees",
|
|
76
|
+
"uncertain-shared-state-ledger",
|
|
77
|
+
"uncertain-topic-intent-store",
|
|
78
|
+
"users-registry",
|
|
79
|
+
"visibility-guard"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-no-wholefile-sync-read.js — Bounded Accumulation §3c (Lint 2).
|
|
4
|
+
*
|
|
5
|
+
* Forbids a whole-file SYNCHRONOUS read of a store the registry marks
|
|
6
|
+
* `access: 'streamed'` (a store that can exceed 8MB): `JSON.parse(fs.readFileSync(p))`
|
|
7
|
+
* or `fs.readFileSync(p, ...).split(...)`. Reading a multi-MB file whole on the event
|
|
8
|
+
* loop is the stall this standard exists to kill (#1239; the cartographer index,
|
|
9
|
+
* instar#1069). Such stores must be read by streaming / segment / SQLite.
|
|
10
|
+
*
|
|
11
|
+
* COVERAGE HONESTY (No Silent Degradation): this is a static guardrail, NOT complete.
|
|
12
|
+
* It resolves a `streamed` store by its path BASENAME appearing as a literal near the
|
|
13
|
+
* read. A read via a variable path (`fs.readFileSync(this.logPath)`) is NOT statically
|
|
14
|
+
* resolvable and is NOT caught here — that gap is closed by the accessor funnel (route
|
|
15
|
+
* all reads through src/core/storage/, Bounded Accumulation Increment 2). The complete
|
|
16
|
+
* runtime check is the growth-burst test; this lint is the cheap forward ratchet that
|
|
17
|
+
* stops a NEW literal whole-file read of a bounded store.
|
|
18
|
+
*
|
|
19
|
+
* Ships WARN-then-ratchet: a FROZEN baseline grandfathers today's literal hits; a NEW
|
|
20
|
+
* hit fails. Exit codes: 0 pass · 1 new violation · 2 cannot-read.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
28
|
+
// --registry / --baseline / --root override the defaults (for tests + alternate checkouts).
|
|
29
|
+
function argVal(flag) {
|
|
30
|
+
const i = process.argv.indexOf(flag);
|
|
31
|
+
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : null;
|
|
32
|
+
}
|
|
33
|
+
const REGISTRY = path.resolve(argVal('--registry') || path.join(ROOT, 'src', 'data', 'state-coherence-registry.json'));
|
|
34
|
+
const BASELINE = path.resolve(argVal('--baseline') || path.join(ROOT, 'scripts', 'bounded-accumulation-wholefile-read-baseline.json'));
|
|
35
|
+
const SRC = path.resolve(argVal('--root') || path.join(ROOT, 'src'));
|
|
36
|
+
|
|
37
|
+
// Files exempt: the accessor + rotation definitions, and the standard's own machinery.
|
|
38
|
+
const EXEMPT = [/\/core\/storage\//, /\/utils\/jsonl-rotation\.ts$/];
|
|
39
|
+
|
|
40
|
+
function streamedBasenames() {
|
|
41
|
+
const reg = JSON.parse(fs.readFileSync(REGISTRY, 'utf8'));
|
|
42
|
+
const names = new Set();
|
|
43
|
+
for (const e of reg.entries || []) {
|
|
44
|
+
if (e.retention && (e.retention.access === 'streamed')) {
|
|
45
|
+
for (const p of e.paths || []) {
|
|
46
|
+
const b = path.basename(p).replace(/\*/g, '');
|
|
47
|
+
if (b && b.endsWith('.jsonl')) names.add(b);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return names;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walk(dir, out) {
|
|
55
|
+
for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
56
|
+
const fp = path.join(dir, f.name);
|
|
57
|
+
if (f.isDirectory()) walk(fp, out);
|
|
58
|
+
else if (f.name.endsWith('.ts') && !f.name.endsWith('.test.ts')) out.push(fp);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A whole-file sync read: JSON.parse(...readFileSync...) OR readFileSync(...).split
|
|
64
|
+
const WHOLE_READ = /(JSON\.parse\([^)]*readFileSync|readFileSync\s*\([^)]*\)\s*\.\s*split)/;
|
|
65
|
+
|
|
66
|
+
let registryBasenames;
|
|
67
|
+
try {
|
|
68
|
+
registryBasenames = streamedBasenames();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error('lint-no-wholefile-sync-read: cannot read registry: ' + e.message);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let baseline = { violations: [] };
|
|
75
|
+
try {
|
|
76
|
+
baseline = JSON.parse(fs.readFileSync(BASELINE, 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
// No baseline yet → treat as empty (first run records nothing as grandfathered).
|
|
79
|
+
}
|
|
80
|
+
const baselineSet = new Set((baseline.violations || []).map((v) => v.file + '::' + v.basename));
|
|
81
|
+
|
|
82
|
+
const found = [];
|
|
83
|
+
for (const file of walk(SRC, [])) {
|
|
84
|
+
if (EXEMPT.some((re) => re.test(file))) continue;
|
|
85
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
const line = lines[i];
|
|
88
|
+
if (!WHOLE_READ.test(line)) continue;
|
|
89
|
+
// Resolve to a streamed store by a basename literal on this or the 2 lines above.
|
|
90
|
+
const ctx = (lines[i - 2] || '') + '\n' + (lines[i - 1] || '') + '\n' + line;
|
|
91
|
+
for (const b of registryBasenames) {
|
|
92
|
+
if (ctx.includes(b)) {
|
|
93
|
+
found.push({ file: path.relative(ROOT, file), basename: b, line: i + 1 });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Generate-baseline mode: record current hits as grandfathered.
|
|
100
|
+
if (process.argv.includes('--write-baseline')) {
|
|
101
|
+
fs.writeFileSync(
|
|
102
|
+
BASELINE,
|
|
103
|
+
JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
note: 'FROZEN baseline of literal whole-file-sync reads of streamed stores at Bounded Accumulation Increment 1. May only shrink. New hits fail the lint.',
|
|
106
|
+
frozenAt: '2026-06-21',
|
|
107
|
+
violations: found.map((f) => ({ file: f.file, basename: f.basename })),
|
|
108
|
+
},
|
|
109
|
+
null,
|
|
110
|
+
2,
|
|
111
|
+
) + '\n',
|
|
112
|
+
);
|
|
113
|
+
console.log(`lint-no-wholefile-sync-read: wrote baseline with ${found.length} grandfathered hit(s).`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const newViolations = found.filter((f) => !baselineSet.has(f.file + '::' + f.basename));
|
|
118
|
+
if (newViolations.length) {
|
|
119
|
+
console.error('lint-no-wholefile-sync-read: FAIL — new whole-file sync read(s) of a streamed store:');
|
|
120
|
+
for (const v of newViolations) {
|
|
121
|
+
console.error(` • ${v.file}:${v.line} reads ${v.basename} whole. Use streaming / segment / SQLite, or route through src/core/storage/.`);
|
|
122
|
+
}
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
console.log(
|
|
126
|
+
`lint-no-wholefile-sync-read: OK — ${found.length} literal hit(s), all grandfathered (${baselineSet.size} baselined).`,
|
|
127
|
+
);
|
|
128
|
+
process.exit(0);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-store-retention-declared.js — Bounded Accumulation §3b (Lint 1).
|
|
4
|
+
*
|
|
5
|
+
* Every persistent store (a category in state-coherence-registry.json) MUST declare a
|
|
6
|
+
* retention policy. This lint is the RATCHET: it FAILS on a NEW category that has no
|
|
7
|
+
* `retention`, while grandfathering the frozen legacy backlog (the §4 retrofit counts
|
|
8
|
+
* that backlog down). It also enforces D6 set-monotonicity — the frozen baseline may
|
|
9
|
+
* only SHRINK: growing it (gaming the ratchet by adding a new store to the allowlist
|
|
10
|
+
* instead of giving it retention) fails.
|
|
11
|
+
*
|
|
12
|
+
* Pairs with lint-state-registry.js (which forces a write-site to be REGISTERED at all);
|
|
13
|
+
* this lint forces a registered store to also be BOUNDED.
|
|
14
|
+
*
|
|
15
|
+
* Exit codes: 0 pass · 1 violation · 2 cannot-read (fail-loud, never silently skip).
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
23
|
+
// --registry / --baseline override the defaults (for tests + alternate checkouts).
|
|
24
|
+
function argVal(flag) {
|
|
25
|
+
const i = process.argv.indexOf(flag);
|
|
26
|
+
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : null;
|
|
27
|
+
}
|
|
28
|
+
const REGISTRY = path.resolve(argVal('--registry') || path.join(ROOT, 'src', 'data', 'state-coherence-registry.json'));
|
|
29
|
+
const BASELINE = path.resolve(argVal('--baseline') || path.join(ROOT, 'scripts', 'bounded-accumulation-retention-baseline.json'));
|
|
30
|
+
|
|
31
|
+
// The category count of the frozen baseline at Increment 1. The baseline may only
|
|
32
|
+
// shrink; a larger set means the allowlist was grown to dodge the ratchet (D6).
|
|
33
|
+
const FROZEN_BASELINE_COUNT = 75;
|
|
34
|
+
|
|
35
|
+
function hasRetention(e) {
|
|
36
|
+
return !!(e && e.retention && typeof e.retention === 'object' && Object.keys(e.retention).length > 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let registry, baseline;
|
|
40
|
+
try {
|
|
41
|
+
registry = JSON.parse(fs.readFileSync(REGISTRY, 'utf8'));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error('lint-store-retention-declared: cannot read registry: ' + e.message);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
baseline = JSON.parse(fs.readFileSync(BASELINE, 'utf8'));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('lint-store-retention-declared: cannot read frozen baseline: ' + e.message);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const baselineSet = new Set(baseline.categories || []);
|
|
54
|
+
const errors = [];
|
|
55
|
+
|
|
56
|
+
// D6: the frozen baseline may only shrink.
|
|
57
|
+
if ((baseline.categories || []).length > FROZEN_BASELINE_COUNT) {
|
|
58
|
+
errors.push(
|
|
59
|
+
`Retention baseline GREW (${baseline.categories.length} > frozen ${FROZEN_BASELINE_COUNT}). ` +
|
|
60
|
+
`The baseline may only shrink (D6 set-monotonicity) — declare a retention policy on the new ` +
|
|
61
|
+
`store instead of adding it to the grandfathered allowlist.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Every no-retention category must be in the frozen baseline (grandfathered legacy).
|
|
66
|
+
for (const e of registry.entries || []) {
|
|
67
|
+
if (hasRetention(e)) continue;
|
|
68
|
+
if (!baselineSet.has(e.category)) {
|
|
69
|
+
errors.push(
|
|
70
|
+
`Store category "${e.category}" has NO retention policy and is not in the frozen baseline. ` +
|
|
71
|
+
`Every persistent store must declare a retention policy (Bounded Accumulation §2). Add a ` +
|
|
72
|
+
`"retention" field to its state-coherence-registry.json entry — one of: ` +
|
|
73
|
+
`{class:'A',access:'streamed',maxBytes,keepSegments} (rotating log), ` +
|
|
74
|
+
`{class:'C',access:'streamed',complianceHold:true} (audit/forensic — archive, never drop), ` +
|
|
75
|
+
`{class:'sqlite',access:'sqlite',maxAgeMs} (indexed store), ` +
|
|
76
|
+
`{class:'R',access:'protocol-reader',boundedBy:'replication-protocol'} (replication substrate), or ` +
|
|
77
|
+
`{class:'resolution',boundedByResolution:true,maxOpenItems} (actionable queue).`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (errors.length) {
|
|
83
|
+
console.error('lint-store-retention-declared: FAIL');
|
|
84
|
+
for (const e of errors) console.error(' • ' + e);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const retentioned = (registry.entries || []).filter(hasRetention).length;
|
|
89
|
+
console.log(
|
|
90
|
+
`lint-store-retention-declared: OK — ${retentioned} stores retentioned, ` +
|
|
91
|
+
`${baselineSet.size} grandfathered (retrofit backlog, may only shrink).`,
|
|
92
|
+
);
|
|
93
|
+
process.exit(0);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-22T00:10:26.994Z",
|
|
5
|
+
"instarVersion": "1.3.637",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
"paths": [
|
|
15
15
|
"state/coherence-journal/"
|
|
16
16
|
],
|
|
17
|
-
"grandfathered": false
|
|
17
|
+
"grandfathered": false,
|
|
18
|
+
"retention": {
|
|
19
|
+
"class": "R",
|
|
20
|
+
"access": "protocol-reader",
|
|
21
|
+
"boundedBy": "replication-protocol",
|
|
22
|
+
"note": "CARVED OUT — bounded inside CoherenceJournal via KindRetention + the new seq-floor prune guard; never externally rotated"
|
|
23
|
+
}
|
|
18
24
|
},
|
|
19
25
|
{
|
|
20
26
|
"category": "pending-pulls",
|
|
@@ -171,7 +177,13 @@
|
|
|
171
177
|
"paths": [
|
|
172
178
|
"state/coherence-journal/*.topic-placement.jsonl"
|
|
173
179
|
],
|
|
174
|
-
"grandfathered": false
|
|
180
|
+
"grandfathered": false,
|
|
181
|
+
"retention": {
|
|
182
|
+
"class": "R",
|
|
183
|
+
"access": "protocol-reader",
|
|
184
|
+
"boundedBy": "replication-protocol",
|
|
185
|
+
"note": "CARVED OUT — replication substrate"
|
|
186
|
+
}
|
|
175
187
|
},
|
|
176
188
|
{
|
|
177
189
|
"category": "autonomous-working-artifacts",
|
|
@@ -432,7 +444,12 @@
|
|
|
432
444
|
"state/resource-ledger.db",
|
|
433
445
|
"state/resources.db"
|
|
434
446
|
],
|
|
435
|
-
"grandfathered": false
|
|
447
|
+
"grandfathered": false,
|
|
448
|
+
"retention": {
|
|
449
|
+
"class": "sqlite",
|
|
450
|
+
"access": "sqlite",
|
|
451
|
+
"maxAgeMs": 2592000000
|
|
452
|
+
}
|
|
436
453
|
},
|
|
437
454
|
{
|
|
438
455
|
"category": "lease-coordination-state",
|
|
@@ -608,7 +625,14 @@
|
|
|
608
625
|
"state/destructive-ops.jsonl",
|
|
609
626
|
"logs/destructive-ops.jsonl"
|
|
610
627
|
],
|
|
611
|
-
"grandfathered": false
|
|
628
|
+
"grandfathered": false,
|
|
629
|
+
"retention": {
|
|
630
|
+
"class": "C",
|
|
631
|
+
"access": "streamed",
|
|
632
|
+
"complianceHold": true,
|
|
633
|
+
"keepSegments": 0,
|
|
634
|
+
"note": "forensic trail — archive, never drop-delete (replaces SafeGitExecutor.maybeRotateAuditLog drop-rotate)"
|
|
635
|
+
}
|
|
612
636
|
},
|
|
613
637
|
{
|
|
614
638
|
"category": "audit-job-runs",
|
|
@@ -621,7 +645,13 @@
|
|
|
621
645
|
"state/job-runs.jsonl",
|
|
622
646
|
"logs/job-runs.jsonl"
|
|
623
647
|
],
|
|
624
|
-
"grandfathered": false
|
|
648
|
+
"grandfathered": false,
|
|
649
|
+
"retention": {
|
|
650
|
+
"class": "A",
|
|
651
|
+
"access": "streamed",
|
|
652
|
+
"maxBytes": 33554432,
|
|
653
|
+
"keepSegments": 4
|
|
654
|
+
}
|
|
625
655
|
},
|
|
626
656
|
{
|
|
627
657
|
"category": "audit-reaper",
|
|
@@ -671,7 +701,14 @@
|
|
|
671
701
|
"state/security.jsonl",
|
|
672
702
|
"logs/security.jsonl"
|
|
673
703
|
],
|
|
674
|
-
"grandfathered": false
|
|
704
|
+
"grandfathered": false,
|
|
705
|
+
"retention": {
|
|
706
|
+
"class": "C",
|
|
707
|
+
"access": "streamed",
|
|
708
|
+
"complianceHold": true,
|
|
709
|
+
"keepSegments": 0,
|
|
710
|
+
"note": "forensic trail — archive, never drop-delete (replaces SecurityLog maybeRotateJsonl keep-ratio trim)"
|
|
711
|
+
}
|
|
675
712
|
},
|
|
676
713
|
{
|
|
677
714
|
"category": "audit-decision-journal",
|
|
@@ -749,7 +786,13 @@
|
|
|
749
786
|
"state/telegram-messages.jsonl",
|
|
750
787
|
"logs/telegram-messages.jsonl"
|
|
751
788
|
],
|
|
752
|
-
"grandfathered": false
|
|
789
|
+
"grandfathered": false,
|
|
790
|
+
"retention": {
|
|
791
|
+
"class": "A",
|
|
792
|
+
"access": "streamed",
|
|
793
|
+
"maxBytes": 33554432,
|
|
794
|
+
"keepSegments": 4
|
|
795
|
+
}
|
|
753
796
|
},
|
|
754
797
|
{
|
|
755
798
|
"category": "audit-a2a-messages",
|
|
@@ -814,7 +857,13 @@
|
|
|
814
857
|
"state/feedback.jsonl",
|
|
815
858
|
"logs/feedback.jsonl"
|
|
816
859
|
],
|
|
817
|
-
"grandfathered": false
|
|
860
|
+
"grandfathered": false,
|
|
861
|
+
"retention": {
|
|
862
|
+
"class": "A",
|
|
863
|
+
"access": "streamed",
|
|
864
|
+
"maxBytes": 33554432,
|
|
865
|
+
"keepSegments": 4
|
|
866
|
+
}
|
|
818
867
|
},
|
|
819
868
|
{
|
|
820
869
|
"category": "audit-apprenticeship-decisions",
|
|
@@ -840,7 +889,13 @@
|
|
|
840
889
|
"state/token-ledger.db",
|
|
841
890
|
"state/tokens.db"
|
|
842
891
|
],
|
|
843
|
-
"grandfathered": false
|
|
892
|
+
"grandfathered": false,
|
|
893
|
+
"retention": {
|
|
894
|
+
"class": "sqlite",
|
|
895
|
+
"access": "sqlite",
|
|
896
|
+
"maxAgeMs": 2592000000,
|
|
897
|
+
"note": "incremental-vacuum + batched delete off-thread (Increment 2)"
|
|
898
|
+
}
|
|
844
899
|
},
|
|
845
900
|
{
|
|
846
901
|
"category": "derived-semantic-memory",
|
|
@@ -856,7 +911,13 @@
|
|
|
856
911
|
"state/episodes/",
|
|
857
912
|
"memory/semantic.jsonl"
|
|
858
913
|
],
|
|
859
|
-
"grandfathered": false
|
|
914
|
+
"grandfathered": false,
|
|
915
|
+
"retention": {
|
|
916
|
+
"class": "sqlite",
|
|
917
|
+
"access": "sqlite",
|
|
918
|
+
"maxAgeMs": 7776000000,
|
|
919
|
+
"note": "mixed .db + memory/semantic.jsonl; jsonl arm is A-class streamed in retrofit"
|
|
920
|
+
}
|
|
860
921
|
},
|
|
861
922
|
{
|
|
862
923
|
"category": "derived-topic-memory",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Instar gains a new constitutional standard — **Bounded Accumulation: every persistent store must
|
|
9
|
+
declare a ceiling and stay under it** — and the first slice of its enforcement. This is the
|
|
10
|
+
storage-dimension twin of the existing "No Unbounded Loops" and "Bounded Notification Surface"
|
|
11
|
+
standards: it exists because an agent that runs for months accumulates data in append-only logs and
|
|
12
|
+
databases that only ever grow, and reading one of those multi-MB files all at once freezes the
|
|
13
|
+
single-threaded event loop (the cause of recurring health-check failures and restarts).
|
|
14
|
+
|
|
15
|
+
This increment is non-behavioral — it adds the substrate without changing how any current store
|
|
16
|
+
behaves: a registry that records each store's retention policy, an event-loop-safe segment-rotation
|
|
17
|
+
primitive (rename, never read-and-rewrite), a `JsonlStore` accessor that all JSONL persistence will
|
|
18
|
+
route through, a growth-burst test that proves retention actually bounds a store on disk, and two
|
|
19
|
+
build-time lints that ratchet new violations (a new store must declare a ceiling; a new whole-file
|
|
20
|
+
synchronous read of a large store is rejected) while grandfathering the existing tree.
|
|
21
|
+
|
|
22
|
+
## What to Tell Your User
|
|
23
|
+
|
|
24
|
+
Nothing changes today in how your agent behaves. This lays the foundation that stops your agent's
|
|
25
|
+
on-disk data from growing without bound and stops the kind of large-file reads that briefly freeze
|
|
26
|
+
it. The actual trimming of existing oversized files, and the one-time cleanup of data that has
|
|
27
|
+
already accumulated, come in the next increments (the cleanup is operator-gated — it asks before
|
|
28
|
+
deleting any history).
|
|
29
|
+
|
|
30
|
+
## Summary of New Capabilities
|
|
31
|
+
|
|
32
|
+
- A retention policy field on every store in the state-coherence registry (10 stores declared,
|
|
33
|
+
75 legacy categories tracked as a shrink-only backlog).
|
|
34
|
+
- `maybeRotateJsonlSegment` (event-loop-safe rename rotation) + `JsonlStore` (the accessor funnel
|
|
35
|
+
with an amortized size-check) in `src/core/storage/`.
|
|
36
|
+
- Two CI lints — `lint-store-retention-declared` (a new store must declare a retention policy) and
|
|
37
|
+
`lint-no-wholefile-sync-read` (no new literal whole-file synchronous read of a streamed store) —
|
|
38
|
+
both wired into `npm run lint` over a frozen baseline (only NEW violations fail).
|
|
39
|
+
- A growth-burst invariant test that floods a registered store and asserts the on-disk footprint
|
|
40
|
+
stays bounded by the declared policy (and that compliance-hold audit stores never drop their
|
|
41
|
+
oldest segment).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Side-Effects Review — Bounded Accumulation Increment 1 (registry + accessor + 2 lints + rotation fix)
|
|
2
|
+
|
|
3
|
+
**Slug:** `bounded-accumulation-increment-1` · **Tier:** 2 (spec-driven; converged + operator-approved)
|
|
4
|
+
**Spec:** `docs/specs/bounded-accumulation-standard.md` (review-convergence 2026-06-21, approved)
|
|
5
|
+
|
|
6
|
+
## Summary of the change
|
|
7
|
+
|
|
8
|
+
The non-behavioral first increment of the Bounded Accumulation standard. It adds the ENFORCEMENT
|
|
9
|
+
substrate without changing any existing store's runtime behavior:
|
|
10
|
+
- `src/utils/jsonl-rotation.ts`: adds `maybeRotateJsonlSegment` — event-loop-safe segment rotation
|
|
11
|
+
(rename active → numbered segment, fresh active, unlink-oldest; O(1), NO whole-file read). The
|
|
12
|
+
existing `maybeRotateJsonl` (read-filter-rewrite) is left intact but marked non-conformant.
|
|
13
|
+
- `src/core/storage/JsonlStore.ts`: the registered accessor funnel — append + a cached-byte-counter
|
|
14
|
+
throttle so even the O(1) statSync isn't paid per append.
|
|
15
|
+
- `src/data/state-coherence-registry.json`: extends 10 in-scope entries with a `retention` policy
|
|
16
|
+
(A/C/sqlite/R classes). 75 legacy categories are frozen as the retrofit backlog.
|
|
17
|
+
- `scripts/lint-store-retention-declared.js` (Lint 1) + `scripts/lint-no-wholefile-sync-read.js`
|
|
18
|
+
(Lint 2), wired into `npm run lint`, each with a frozen baseline.
|
|
19
|
+
|
|
20
|
+
## Decision-point inventory
|
|
21
|
+
|
|
22
|
+
All decisions are frozen in the spec §6 (D1–D9): 32MB/4-segment A-class default, 30-day token-ledger,
|
|
23
|
+
compliance-hold (archive-never-delete) for audit logs, R-class carve-out for replication journals,
|
|
24
|
+
set-monotonic ratchet, 8MB streamed threshold. No new decision is introduced at the callsite.
|
|
25
|
+
|
|
26
|
+
## 1. Over-block (false positive)
|
|
27
|
+
|
|
28
|
+
- Lint 1 fails a registry entry with no `retention` that is not in the frozen baseline. False positive
|
|
29
|
+
only if a genuinely-new store is legitimately unbounded — which the standard forbids by definition;
|
|
30
|
+
the author declares a policy (incl. `boundedByResolution`). Cannot mis-fire on a legacy store (all
|
|
31
|
+
75 are baselined).
|
|
32
|
+
- Lint 2 fails a NEW literal whole-file read of a streamed store. Honest coverage limit: only literal
|
|
33
|
+
paths; dynamic paths are not flagged (no false positives, some false negatives — closed by the
|
|
34
|
+
accessor funnel in Increment 2).
|
|
35
|
+
|
|
36
|
+
## 2. Under-block (false negative)
|
|
37
|
+
|
|
38
|
+
Lint 2's dynamic-path gap (e.g. `readFileSync(this.logPath)`) is the known limit — documented in the
|
|
39
|
+
lint header and in spec §3c. The runtime growth-burst test is the complete check for stores it
|
|
40
|
+
exercises; the accessor funnel (Increment 2) closes the read-side gap. Not silently hidden.
|
|
41
|
+
|
|
42
|
+
## 4. Signal vs authority
|
|
43
|
+
|
|
44
|
+
The lints are deterministic STRUCTURAL checks (a path/registry match), permitted to block like the
|
|
45
|
+
existing funnel lints (lint-state-registry, lint-no-blocking-process-scans). The SEMANTIC judgment
|
|
46
|
+
("is this store actually actionable / which class?") stays with the author + reviewer, never the
|
|
47
|
+
regex. Ships ratchet-over-frozen-baseline (warn-then-ratchet per Maturation Path): the current tree
|
|
48
|
+
is grandfathered; only NEW violations fail.
|
|
49
|
+
|
|
50
|
+
## 5. Interactions
|
|
51
|
+
|
|
52
|
+
The new `maybeRotateJsonlSegment` is additive — no existing caller of `maybeRotateJsonl` is changed,
|
|
53
|
+
so no store's rotation behavior changes in Increment 1 (verified: the existing
|
|
54
|
+
`tests/unit/jsonl-rotation.test.ts` 11 tests still pass). The registry gains fields read only by the
|
|
55
|
+
two new lints. No existing reader of the registry breaks (additive JSON fields).
|
|
56
|
+
|
|
57
|
+
## 6. External surfaces
|
|
58
|
+
|
|
59
|
+
None. No new HTTP route, no config-contract change, no messaging, no persistence-schema change. The
|
|
60
|
+
two `scripts/*.js` lints run only at lint/CI time.
|
|
61
|
+
|
|
62
|
+
## 6b. Operator-surface quality
|
|
63
|
+
|
|
64
|
+
N/A — no operator/dashboard/approval surface is touched.
|
|
65
|
+
|
|
66
|
+
## Framework generality
|
|
67
|
+
|
|
68
|
+
N/A — `JsonlStore`/the lints are framework-agnostic infrastructure; not part of the session
|
|
69
|
+
launch/inject abstraction. Works identically regardless of the agent's framework.
|
|
70
|
+
|
|
71
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
72
|
+
|
|
73
|
+
The registry's `retention.access` and the new `coherenceScope` usage declare per-store posture.
|
|
74
|
+
Retention is MACHINE-LOCAL by design (each machine's `.instar/` stores differ in size; the rotator
|
|
75
|
+
runs per-machine). The replication substrate (`state/coherence-journal/**`) is explicitly R-class
|
|
76
|
+
(carved OUT of generic rotation — naive truncation would resurrect deleted PII); it is bounded inside
|
|
77
|
+
its own protocol (the seq-floor prune guard is Increment 2). No replicated state is introduced.
|
|
78
|
+
|
|
79
|
+
## 8. Rollback cost
|
|
80
|
+
|
|
81
|
+
Trivial. The lints ship over a frozen baseline (no current-tree failures); removing them from
|
|
82
|
+
`package.json` reverts the enforcement. `maybeRotateJsonlSegment` + `JsonlStore` are new, unused by
|
|
83
|
+
any runtime store yet (Increment 2 wires them), so reverting is a clean delete. The registry fields
|
|
84
|
+
are additive data. Nothing to migrate back.
|
|
85
|
+
|
|
86
|
+
## Evidence pointers
|
|
87
|
+
|
|
88
|
+
- `tests/unit/jsonl-segment-rotation.test.ts` (7): rename-not-rewrite, prune-beyond-keep, archive
|
|
89
|
+
never-prunes, JsonlStore amortized check + bounded-under-flood.
|
|
90
|
+
- `tests/integration/store-growth-burst-invariant.test.ts` (3): A-class on-disk bounded under a
|
|
91
|
+
20k-entry flood; C-class never drops its oldest segment; every registry A-class entry has an
|
|
92
|
+
enforceable maxBytes.
|
|
93
|
+
- `tests/unit/bounded-accumulation-lints.test.ts` (7): both lints, both sides of each boundary
|
|
94
|
+
(pass current / fail new-unretentioned / pass new-with-retention / fail grown-baseline; Lint 2
|
|
95
|
+
pass-clean / fail-new-literal-read / pass-grandfathered).
|
|
96
|
+
- Existing `tests/unit/jsonl-rotation.test.ts` (11) still green → no regression to the old path.
|
|
97
|
+
- `npm run lint` green (the two new lints in the chain).
|
|
98
|
+
|
|
99
|
+
## Conclusion
|
|
100
|
+
|
|
101
|
+
A non-behavioral enforcement substrate: the registry declares ceilings, two lints ratchet new
|
|
102
|
+
violations, and the event-loop-safe rotation primitive + accessor are ready for Increment 2 to wire
|
|
103
|
+
to real stores. Grandfathers the current tree, fails only NEW violations, trivially reversible. Ship.
|