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.
@@ -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;AASD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAyCrF"}
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;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAW3D,wEAAwE;AAExE,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO;AACnD,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,wEAAwE;AAExE;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,OAAyB;IAC1E,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,iBAAiB,CAAC;IACxD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,IAAI,kBAAkB,CAAC,CAAC,CAAC;IAErF,IAAI,CAAC;QACH,iCAAiC;QACjC,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC1B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,4DAA4D;QAC5D,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAElD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;QACnE,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC;QAE1C,kCAAkC;QAClC,MAAM,OAAO,GAAG,QAAQ,GAAG,eAAe,CAAC;QAC3C,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACvD,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAEjC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;QACnE,mEAAmE;QACnE,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,QAAQ,GAAG,eAAe,CAAC;YAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3B,cAAc,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,gCAAgC,EAAE,CAAC,CAAC;YAC1F,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;AACH,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.636",
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,5 @@
1
+ {
2
+ "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.",
3
+ "frozenAt": "2026-06-21",
4
+ "violations": []
5
+ }
@@ -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-21T21:13:59.179Z",
5
- "instarVersion": "1.3.636",
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.