instar 1.3.645 → 1.3.647

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +6 -2
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/feedback-factory/store/JsonlFeedbackStore.d.ts +2 -0
  5. package/dist/feedback-factory/store/JsonlFeedbackStore.d.ts.map +1 -1
  6. package/dist/feedback-factory/store/JsonlFeedbackStore.js +36 -2
  7. package/dist/feedback-factory/store/JsonlFeedbackStore.js.map +1 -1
  8. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  9. package/dist/messaging/TelegramAdapter.js +6 -2
  10. package/dist/messaging/TelegramAdapter.js.map +1 -1
  11. package/dist/monitoring/CoherenceMonitor.d.ts.map +1 -1
  12. package/dist/monitoring/CoherenceMonitor.js +6 -1
  13. package/dist/monitoring/CoherenceMonitor.js.map +1 -1
  14. package/dist/monitoring/CommitmentSentinel.d.ts.map +1 -1
  15. package/dist/monitoring/CommitmentSentinel.js +7 -2
  16. package/dist/monitoring/CommitmentSentinel.js.map +1 -1
  17. package/dist/scheduler/JobRunHistory.d.ts +22 -0
  18. package/dist/scheduler/JobRunHistory.d.ts.map +1 -1
  19. package/dist/scheduler/JobRunHistory.js +141 -10
  20. package/dist/scheduler/JobRunHistory.js.map +1 -1
  21. package/dist/utils/jsonl-tail.d.ts +50 -0
  22. package/dist/utils/jsonl-tail.d.ts.map +1 -0
  23. package/dist/utils/jsonl-tail.js +96 -0
  24. package/dist/utils/jsonl-tail.js.map +1 -0
  25. package/package.json +1 -1
  26. package/src/data/builtin-manifest.json +2 -2
  27. package/upgrades/1.3.646.md +39 -0
  28. package/upgrades/1.3.647.md +56 -0
  29. package/upgrades/eli16/eventloop-bounded-jsonl-reads.md +47 -0
  30. package/upgrades/eli16/jobrunhistory-incremental-cache.md +43 -0
  31. package/upgrades/side-effects/eventloop-bounded-jsonl-reads.md +96 -0
  32. package/upgrades/side-effects/jobrunhistory-incremental-cache.md +61 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Bounded JSONL tail reader — read only the LAST window of an append-only log,
3
+ * never the whole file.
4
+ *
5
+ * Born from the 2026-06-22 event-loop-freeze batch: several timers and event
6
+ * handlers (CommitmentSentinel, CoherenceMonitor, PresenceProxy's
7
+ * checkLogForAgentResponse, TelegramAdapter.getMessageLog) each called
8
+ * `fs.readFileSync(messageLogPath, 'utf-8')` on the 12MB telegram-messages.jsonl
9
+ * just to look at the last ~50–200 lines. A 12MB synchronous read+split on a
10
+ * 5-minute timer froze the event loop for up to 20s per pass. These callers do
11
+ * not need the whole file — only its tail.
12
+ *
13
+ * Design (mirrors CoherenceJournal.readTailTolerant, generalized off the
14
+ * journal's typed entry shape):
15
+ * - statSync the file (O(1)) — read at most `maxBytes` from the END.
16
+ * - openSync + readSync the trailing window into a fixed buffer; never load
17
+ * the whole file. O(maxBytes), not O(file).
18
+ * - Discard the first (partial) line when the window started mid-file so a
19
+ * truncated record is never mis-parsed.
20
+ * - Return the raw trailing lines (newest LAST, file order preserved) — the
21
+ * caller parses + filters exactly as it did over the full split before.
22
+ *
23
+ * Never throws — a read failure returns an empty result, matching the
24
+ * @silent-fallback-ok behavior of every former full-file caller. Observability
25
+ * / housekeeping reads must never endanger the observed operation.
26
+ */
27
+ import fs from 'node:fs';
28
+ /** Default trailing window: 512KB. At ~200 bytes/line that is ~2,600 recent
29
+ * lines — far more than any caller's last-50/last-200 need, with headroom for
30
+ * long messages, while staying a small bounded read regardless of file size. */
31
+ export const DEFAULT_TAIL_BYTES = 512 * 1024;
32
+ /**
33
+ * Read the last `maxBytes` of `filePath` and return its non-empty lines in file
34
+ * order. Reads at most `maxBytes` from disk regardless of total file size.
35
+ */
36
+ export function readJsonlTailLines(filePath, maxBytes = DEFAULT_TAIL_BYTES) {
37
+ const empty = { lines: [], truncated: false };
38
+ try {
39
+ if (!fs.existsSync(filePath))
40
+ return empty;
41
+ }
42
+ catch {
43
+ return empty;
44
+ }
45
+ let size;
46
+ try {
47
+ size = fs.statSync(filePath).size;
48
+ }
49
+ catch {
50
+ return empty;
51
+ }
52
+ if (size === 0)
53
+ return empty;
54
+ const readBytes = Math.min(size, maxBytes);
55
+ const truncated = readBytes < size;
56
+ const start = size - readBytes;
57
+ let buf;
58
+ let fd = null;
59
+ try {
60
+ fd = fs.openSync(filePath, 'r');
61
+ buf = Buffer.alloc(readBytes);
62
+ fs.readSync(fd, buf, 0, readBytes, start);
63
+ }
64
+ catch {
65
+ return empty;
66
+ }
67
+ finally {
68
+ if (fd !== null) {
69
+ try {
70
+ fs.closeSync(fd);
71
+ }
72
+ catch {
73
+ /* best-effort */
74
+ }
75
+ }
76
+ }
77
+ let text = buf.toString('utf-8');
78
+ // If the window started mid-file, the first line is a partial record — drop it.
79
+ if (start > 0) {
80
+ const nl = text.indexOf('\n');
81
+ text = nl >= 0 ? text.slice(nl + 1) : '';
82
+ }
83
+ const lines = text.split('\n').filter((l) => l.length > 0);
84
+ return { lines, truncated };
85
+ }
86
+ /**
87
+ * Convenience: read the tail and return at most the last `limit` non-empty
88
+ * lines (file order). Equivalent to the common
89
+ * `readFileSync(...).split('\n').filter(Boolean).slice(-limit)` pattern, but
90
+ * bounded to a trailing `maxBytes` window instead of the whole file.
91
+ */
92
+ export function readJsonlTailLastLines(filePath, limit, maxBytes = DEFAULT_TAIL_BYTES) {
93
+ const { lines } = readJsonlTailLines(filePath, maxBytes);
94
+ return limit >= lines.length ? lines : lines.slice(-limit);
95
+ }
96
+ //# sourceMappingURL=jsonl-tail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-tail.js","sourceRoot":"","sources":["../../src/utils/jsonl-tail.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB;;iFAEiF;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,GAAG,IAAI,CAAC;AAU7C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAgB,EAChB,WAAmB,kBAAkB;IAErC,MAAM,KAAK,GAAmB,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC9D,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE7B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC;IAE/B,IAAI,GAAW,CAAC;IAChB,IAAI,EAAE,GAAkB,IAAI,CAAC;IAC7B,IAAI,CAAC;QACH,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAChC,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC9B,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;YAAS,CAAC;QACT,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACnB,CAAC;YAAC,MAAM,CAAC;gBACP,iBAAiB;YACnB,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjC,gFAAgF;IAChF,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3C,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3D,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAAgB,EAChB,KAAa,EACb,WAAmB,kBAAkB;IAErC,MAAM,EAAE,KAAK,EAAE,GAAG,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACzD,OAAO,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.645",
3
+ "version": "1.3.647",
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",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-22T18:50:01.119Z",
5
- "instarVersion": "1.3.645",
4
+ "generatedAt": "2026-06-22T20:47:55.471Z",
5
+ "instarVersion": "1.3.647",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,39 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ `JobRunHistory.readLines()` did a synchronous `readFileSync` + per-line `JSON.parse` of the entire
9
+ `.instar/ledger/job-runs.jsonl` (~13MB) on EVERY call — on the hot path of `findRun` /
10
+ `recordCompletion` / `recordHandoff` / `query` / `stats` (every job completion/spawn, ~60s cadence).
11
+ A live `/usr/bin/sample` caught the event loop frozen 13–16s in `ReadFileUtf8 → ParseJson →
12
+ WriteFileUtf8`, misreported by SleepWakeDetector as a ~15s "wake". This is the THIRD distinct
13
+ event-loop blocker behind the dashboard "disconnected" flapping (after sync tmux and sync keychain
14
+ reads). The read path now uses an incremental in-memory cache keyed on `(size, mtimeMs)`: unchanged
15
+ file → cache hit (zero IO); append → tail-read only (O(delta)); shrink/compaction → one full re-read.
16
+
17
+ ## What to Tell Your User
18
+
19
+ If your dashboard kept dropping its connection even after the tmux and keychain fixes, this was the
20
+ remaining cause: a 13MB job-history file re-read whole, every minute, on the server's main thread.
21
+ After this update that freeze is gone. (A separate cleanup — capping that file's unbounded growth —
22
+ is tracked separately.)
23
+
24
+ ## Summary of New Capabilities
25
+
26
+ - Incremental `(size, mtimeMs)`-keyed in-memory cache in `JobRunHistory`; unchanged file = zero-IO
27
+ cache hit, append = tail-read only, compaction = one full re-read. Existing semantics (append-only,
28
+ dedup-last-wins, torn-line skip, corrupt-file recovery) preserved exactly.
29
+ - Removes the third event-loop blocker behind the dashboard flapping + the associated SleepWakeDetector
30
+ false-wakes.
31
+
32
+ ## Evidence
33
+
34
+ Root cause diagnosed by a live `/usr/bin/sample` (main thread in ReadFileUtf8 → ParseJson →
35
+ WriteFileUtf8) + an mtime-watch catching the 13MB rewrite, correlated with 15–20s freezes every
36
+ ~20–60s. Fix covered by 4 new regression tests (cache-hit does zero full-file reads across 75 ops;
37
+ external append via tail-read; torn-line skip; full re-read on shrink) on top of 30 existing; full
38
+ scheduler suite green (346 tests total); tsc clean; no-silent-fallbacks + bounded-accumulation +
39
+ no-wholefile-sync-read lints green.
@@ -0,0 +1,56 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ FOUR callers each did a synchronous `fs.readFileSync` of the ~12MB `.instar/telegram-messages.jsonl`
9
+ just to inspect its last 50–200 lines: `CommitmentSentinel.parseRecentMessages` (5-min timer),
10
+ `CoherenceMonitor`'s output-sanity check (5-min timer), `checkLogForAgentResponse` (PresenceProxy ack
11
+ path, event-driven), and `TelegramAdapter.getMessageLog` (analysis route). A 12MB sync read + split
12
+ on a 5-minute timer froze the event loop up to ~20s per pass — the same ~0-CPU false-sleep signature
13
+ the JobRunHistory fix removed. Separately, `JsonlFeedbackStore` re-read + re-`JSON.parse`d the entire
14
+ ~14MB `feedback.jsonl` at the start of every processing pass even when nothing had changed. This is
15
+ the THIRD + FOURTH event-loop-blocker class behind the dashboard "disconnected" flapping (after sync
16
+ tmux, sync keychain, and the JobRunHistory 13MB re-read).
17
+
18
+ The fix is two behavior-preserving mechanisms: (A) a shared bounded tail-read utility
19
+ (`src/utils/jsonl-tail.ts`) that reads only the last 512KB via `openSync`/`readSync` — mirroring
20
+ `CoherenceJournal.readTailTolerant` — used by the four telegram-log readers; and (B) a
21
+ `(size, mtimeMs)` load cache in `JsonlFeedbackStore` (mirroring the JobRunHistory cache) that serves a
22
+ clone of the prior parse when the file is byte-for-byte unchanged.
23
+
24
+ ## What to Tell Your User
25
+
26
+ If your dashboard kept dropping its connection even after the tmux, keychain, and job-history fixes,
27
+ these two large files were the remaining cause: a 12MB message log re-read whole on a 5-minute timer,
28
+ and a 14MB feedback log re-parsed from scratch every pass. After this update both freezes are gone —
29
+ the message-log checks read only the last chunk of the file (instant regardless of size), and the
30
+ feedback log is served from memory when nothing changed. (A separate cleanup — capping those files'
31
+ unbounded growth — is tracked separately.)
32
+
33
+ ## Summary of New Capabilities
34
+
35
+ - New `src/utils/jsonl-tail.ts` bounded tail-reader (`readJsonlTailLines` / `readJsonlTailLastLines`):
36
+ reads only the last `maxBytes` (default 512KB ≈ 2,600 lines) via `statSync`/`openSync`/`readSync`,
37
+ drops the partial first line, never loads the whole file, never throws.
38
+ - The four telegram-log tail readers (CommitmentSentinel, CoherenceMonitor, PresenceProxy ack path,
39
+ TelegramAdapter.getMessageLog) converted from whole-12MB-file sync reads to bounded tail-reads;
40
+ parsed/filtered results are identical to the old full-split-then-slice.
41
+ - `JsonlFeedbackStore` gains a `(size, mtimeMs)`-keyed in-memory load cache: unchanged file = zero-IO
42
+ clone-on-serve, any change = full re-read. Existing load semantics (dedup-last-wins, torn-line skip,
43
+ corrupt-file recovery) preserved exactly.
44
+ - Removes the third + fourth event-loop-blocker class behind the dashboard flapping and the
45
+ associated SleepWakeDetector false-wakes.
46
+
47
+ ## Evidence
48
+
49
+ Root cause diagnosed by a live profiler catching the event loop frozen in a whole-file
50
+ ReadFileUtf8 → split on a 5-minute timer (the ~0-CPU false-sleep signature). Fix covered by 48 tests
51
+ across four files: `jsonl-tail` (9 — tail correctness, partial-line drop, missing/empty file,
52
+ truncation flag), `CoherenceMonitor-bounded-read` (2 — a `fs.readFileSync` spy FAILS if the whole
53
+ 12MB log is read, AND a bad pattern in a RECENT agent message at the END of a multi-MB log is still
54
+ detected via the tail), `CommitmentSentinel` (26), `jsonl-feedback-store` (11 — including cache-hit
55
+ serves zero-IO and any change re-reads). Cross-impact suites green; tsc exit 0; no-silent-fallbacks +
56
+ bounded-accumulation lints green.
@@ -0,0 +1,47 @@
1
+ # Bounded tail-reads for the 12MB telegram-log + 14MB feedback-store — ELI16
2
+
3
+ ## What this is
4
+
5
+ Your agent's server runs everything on one main thread. If any single operation makes that thread
6
+ wait, *everything* waits — the dashboard, your messages, every background check. When that wait is
7
+ long enough, the dashboard's live connection drops (the "Disconnected" flapping you may have seen).
8
+
9
+ This fixes the THIRD and FOURTH freezes behind that flapping. (The first was a slow shared terminal
10
+ manager; the second was reading the macOS keychain the blocking way; the third was a 13MB job-history
11
+ file re-read whole every minute — all already fixed.) This one is about two more big files.
12
+
13
+ ## What was wrong
14
+
15
+ Your agent keeps a running log of every Telegram message it has ever sent or received, in a file
16
+ called `telegram-messages.jsonl`. That file has grown to about **12 megabytes**. Several background
17
+ checks only ever need to glance at the LAST few messages in that log — the last 50, or the last 100.
18
+ But the way they were written, they read the **entire 12MB file** from disk and chopped it up, just
19
+ to look at the tail. And some of these checks run on a **5-minute timer**, every few minutes, forever.
20
+ Each one froze the server for up to **20 seconds**. (A second large file, a 14MB feedback log, had
21
+ the same problem: it was re-read and re-parsed from scratch every processing pass even when nothing
22
+ had changed.)
23
+
24
+ A frozen main thread uses almost no CPU, so the freeze even looked to the agent like the laptop had
25
+ briefly gone to sleep — a misleading signal on top of a real problem.
26
+
27
+ ## What's new
28
+
29
+ Two simple changes, both behavior-preserving:
30
+
31
+ - The background checks that only want the tail of the message log now read just the **last chunk**
32
+ of the file (a 512-kilobyte window — far more than enough to hold the last few hundred messages),
33
+ not the whole thing. So they're instant no matter how big the file gets.
34
+ - The 14MB feedback log now remembers the file's size and last-modified time. If nothing changed
35
+ since it last read it, it returns the copy it already has, reading nothing from disk.
36
+
37
+ So those per-call whole-file reads are gone from every one of these hot paths, and the freezes they
38
+ caused are eliminated. Everything these checks actually do — what they look for, how they parse,
39
+ how they recover from a corrupt file — is unchanged, and proven by tests. One of the new tests even
40
+ plants a problem message at the very END of a multi-megabyte log to prove the tail-read still catches
41
+ recent entries.
42
+
43
+ ## What you need to decide
44
+
45
+ Nothing. It's self-contained and low-risk, fully covered by tests. One honest note: the log files
46
+ themselves still grow large over time — putting a size cap on them is a separate cleanup I've flagged
47
+ to track, not fixed here. Reading them is no longer a freeze; capping their growth comes next.
@@ -0,0 +1,43 @@
1
+ # JobRunHistory incremental cache — event-loop freeze fix — ELI16
2
+
3
+ ## What this is
4
+
5
+ Your agent's server runs everything on one main thread. If any single operation makes that thread
6
+ wait, *everything* waits — the dashboard, your messages, every background check. That's a "freeze,"
7
+ and a long-enough freeze drops the dashboard's live connection (the "Disconnected" flapping).
8
+
9
+ This fixes the THIRD distinct freeze behind that flapping. (The first was a slow shared terminal
10
+ manager; the second was reading the macOS keychain the blocking way — both already fixed.) This one:
11
+ the agent keeps a log of every background job it has ever run, in a file called `job-runs.jsonl`. That
12
+ file has grown to about **13 megabytes**. And every time a job finished or started — which happens
13
+ about every minute — the server read the WHOLE 13MB file from disk and re-parsed all of it, on the
14
+ main thread. That single operation froze the server for **13 to 16 seconds**, every minute or so. It
15
+ even looked to the agent like the laptop had gone to sleep (a blocked main thread uses almost no CPU,
16
+ so the sleep-detector misreads it).
17
+
18
+ ## What already exists
19
+
20
+ The agent already uses this "keep a cached copy instead of re-reading from scratch" pattern elsewhere
21
+ (the cartographer code map serves a cached snapshot instead of recomputing live). This job-history
22
+ file just never got that treatment — it re-read the whole thing every single time.
23
+
24
+ ## What's new
25
+
26
+ The job-run history now keeps a **smart in-memory cache**. It remembers the file's size and
27
+ last-modified time:
28
+ - If nothing changed since last time → it returns the cached copy instantly, reading nothing from
29
+ disk.
30
+ - If a new line was appended (the normal case) → it reads ONLY the new bit at the end, not the whole
31
+ 13MB.
32
+ - If the file was compacted (shrunk) → it does one full re-read to stay correct.
33
+
34
+ So the per-call full-file read+parse is gone from every hot path. The freeze it caused is eliminated.
35
+ All the existing behavior (how it dedupes, recovers from a corrupt file, etc.) is preserved exactly,
36
+ proven by the test suite.
37
+
38
+ ## What you need to decide
39
+
40
+ Nothing. It's a self-contained, low-risk read-path cache, fully covered by tests. One honest note:
41
+ the `job-runs.jsonl` file (and a separate 91MB cartographer file) keep growing without a size limit —
42
+ the cache makes reading them cheap regardless, but capping their growth is a separate cleanup item
43
+ I've flagged to track, not fixed here.
@@ -0,0 +1,96 @@
1
+ # Side-Effects Review — bounded tail-reads for the 12MB telegram-log + 14MB feedback-store (event-loop freeze fix)
2
+
3
+ **Slug:** `eventloop-bounded-jsonl-reads` · **Tier:** 1 (focused low-risk bounded-read class fix, no
4
+ spec; live-profiler root-cause). Parent principles: **Structure beats Willpower** — never block the
5
+ event loop (the THIRD + FOURTH distinct event-loop-blocker class behind the dashboard "disconnected"
6
+ flapping, after sync tmux + sync keychain + the JobRunHistory 13MB re-read); and **Bounded
7
+ Accumulation** (the size of an append-only log must never determine the cost of reading it).
8
+
9
+ ## Summary of the change
10
+
11
+ FOUR callers each did `fs.readFileSync(messageLogPath, 'utf-8')` on the ~12MB
12
+ `.instar/telegram-messages.jsonl` just to inspect the last 50–200 lines:
13
+ - `CommitmentSentinel.parseRecentMessages` (`src/monitoring/CommitmentSentinel.ts`) — 5-minute timer,
14
+ only ever looks at the last `maxMessagesPerScan*10` lines.
15
+ - `CoherenceMonitor` output-sanity check (`src/monitoring/CoherenceMonitor.ts`) — 5-minute timer,
16
+ only ever looks at the last 50 agent messages.
17
+ - `checkLogForAgentResponse` (PresenceProxy ack path, `src/commands/server.ts`) — event-driven on
18
+ every PresenceProxy tier verdict, only ever looks at the last 50 lines.
19
+ - `TelegramAdapter.getMessageLog` (`src/messaging/TelegramAdapter.ts`) — analysis route, only ever
20
+ returns the last `limit` (default 100) entries.
21
+
22
+ A 12MB synchronous read + `.split('\n')` on a 5-minute timer froze the event loop for up to ~20s per
23
+ pass — the same ~0-CPU false-sleep signature the JobRunHistory fix removed. PLUS `JsonlFeedbackStore`
24
+ (`src/feedback-factory/store/JsonlFeedbackStore.ts`) re-read + re-`JSON.parse`d the entire ~14MB
25
+ `feedback.jsonl` at the start of every processing pass even when nothing had changed.
26
+
27
+ The fix is two behavior-preserving mechanisms:
28
+
29
+ **(A) A shared bounded tail-read utility** — `src/utils/jsonl-tail.ts` (`readJsonlTailLines` /
30
+ `readJsonlTailLastLines`). It `statSync`s the file (O(1)), `openSync` + `readSync`s only the trailing
31
+ `maxBytes` (default 512KB ≈ ~2,600 recent lines, far more than any caller needs) into a fixed buffer,
32
+ drops the first partial line when the window started mid-file, and returns the trailing lines in file
33
+ order. It NEVER loads the whole file and NEVER throws (a read failure returns an empty result,
34
+ matching the `@silent-fallback-ok` behavior of every former full-file caller). It mirrors
35
+ `CoherenceJournal.readTailTolerant`, generalized off that journal's typed-entry shape. The four
36
+ telegram-log readers above now call it instead of `readFileSync`.
37
+
38
+ **(B) A `(size, mtimeMs)` load cache in `JsonlFeedbackStore.loadJsonl`** — when the on-disk file is
39
+ byte-for-byte the one last folded, it serves a clone of the prior parse with ZERO IO/parse; any
40
+ change (append, shrink, delete) re-reads from disk. Mirrors the JobRunHistory cache pattern. The
41
+ clone-on-serve guarantees a caller mutating the returned Map can never poison the cache.
42
+
43
+ ## 1. Correctness / behavioral equivalence
44
+
45
+ The telegram-log readers each consumed only a fixed tail (last 50 / last 200 / last `limit`) of the
46
+ log, so the 512KB window (≈ 2,600 lines) is strictly a superset of every caller's need — the parsed +
47
+ filtered result is identical to the old full-split-then-slice. The partial-first-line drop guarantees
48
+ a truncated record at the window boundary is never mis-parsed. The feedback store's load semantics
49
+ (per-line parse, dedup-last-wins-by-id, torn-line skip, corrupt-file recovery) are preserved exactly;
50
+ the cache is only consulted when the fingerprint matches and is invalidated on any change. Verified:
51
+ 48/48 across the four touched test files (`jsonl-tail` 9, `CoherenceMonitor-bounded-read` 2,
52
+ `CommitmentSentinel` 26, `jsonl-feedback-store` 11) plus the cross-impact suites; tsc exit 0; lints
53
+ green. The two new regression tests spy on `fs.readFileSync` to FAIL if the whole telegram log is
54
+ read, AND prove a bad pattern in a RECENT agent message at the END of a multi-MB log is still
55
+ detected via the tail.
56
+
57
+ ## 2. Cache-coherence / multi-writer safety
58
+
59
+ The telegram-log tail-read holds no state — every call re-reads the live tail, so a concurrent
60
+ appender is always reflected. The feedback-store `(size, mtimeMs)` key invalidates on any byte/mtime
61
+ change (append, compaction rewrite, delete-then-recreate), so an unchanged file is the only cache-hit
62
+ case and a stale fold can never be served. The cache is per-process in-memory (not shared) — no
63
+ cross-process coherence concern (each process reads its own view, same as before). `loadCache` is
64
+ keyed per absolute path and dropped on `existsSync` failure.
65
+
66
+ ## 3. Fail-safe
67
+
68
+ `jsonl-tail.ts` never throws: missing file, stat failure, open/read failure each return an empty
69
+ result — exactly the prior `@silent-fallback-ok` degradation of the full-file callers (an
70
+ observability/housekeeping read must never endanger the observed operation). The feedback store's
71
+ stat-failure path falls through to a full read (`/* fall through to a full read — never let stat
72
+ failure skip the load */`), so a stat error degrades to the correct (slow) result, never wrong data.
73
+
74
+ ## 4. Blast radius
75
+
76
+ One new utility module + its test; four read-site one-liner swaps (CommitmentSentinel, CoherenceMonitor,
77
+ server.ts PresenceProxy ack, TelegramAdapter.getMessageLog); one read-path cache in JsonlFeedbackStore
78
+ + a test-only cache-clear export. No API/route change, no schema change, no write-path semantics
79
+ change, no new external surface.
80
+
81
+ ## 5. Residual (tracked, NOT fixed here — out of scope)
82
+
83
+ The existing `lint-no-wholefile-sync-read` ratchet only catches LITERAL path basenames in the read
84
+ call, but these four sites read VARIABLE paths (`this.messageLogPath`, a `logPath` param), so the
85
+ lint could never have flagged them — which is why they survived the earlier sweeps. The durable
86
+ structural prevention is an **accessor funnel** (route all append-log reads through a single bounded
87
+ accessor that the lint can enforce), which is **Bounded Accumulation Increment 2** — a tracked
88
+ follow-up named here so it is not silently dropped. Separately, the telegram-messages.jsonl and
89
+ feedback.jsonl files themselves still grow UNBOUNDED — a size/retention cap is the same Bounded
90
+ Accumulation track. This change makes READS cheap regardless of size; it does not cap the files.
91
+
92
+ ## 6. Rollback
93
+
94
+ Revert the source files. The change is purely read-path (a bounded tail-read + a fingerprint cache);
95
+ reverting restores the (slow but correct) full-read behavior. No data migration, no on-disk format
96
+ change to undo.
@@ -0,0 +1,61 @@
1
+ # Side-Effects Review — JobRunHistory incremental cache (event-loop freeze fix)
2
+
3
+ **Slug:** `jobrunhistory-incremental-cache` · **Tier:** 1 (focused bug fix, no spec; live-profiler
4
+ root-cause). Parent principle: **Structure beats Willpower** — never block the event loop (the third
5
+ distinct event-loop blocker behind the dashboard "disconnected" flapping, after sync tmux + sync
6
+ keychain reads).
7
+
8
+ ## Summary of the change
9
+
10
+ `JobRunHistory.readLines()` (`src/scheduler/JobRunHistory.ts`) did `fs.readFileSync` + per-line
11
+ `JSON.parse` of the ENTIRE `.instar/ledger/job-runs.jsonl` (~13MB) on EVERY call — and it is on the
12
+ hot path of `findRun` / `recordCompletion` / `recordReflection` / `recordHandoff` / `query` / `stats`
13
+ / `allStats` / `getLastHandoff`, all driven by the scheduler every job completion/spawn (~60s
14
+ cadence: health-check, commitment-detection, mentor jobs). A live `/usr/bin/sample` caught the main
15
+ thread in `ReadFileUtf8 → ParseJson → WriteFileUtf8` — a 13–16s event-loop freeze misreported by
16
+ SleepWakeDetector as a ~15s "wake" (the ~0-CPU false-sleep signature). This adds an **incremental
17
+ in-memory cache** of parsed runs keyed on the file's `(size, mtimeMs)`: an unchanged file returns the
18
+ cache with ZERO IO/parse; an append-only growth reads ONLY the appended tail via `fs.readSync` from
19
+ the last offset (O(delta), not O(13MB)); a shrink/rewrite (compaction) triggers one full re-read.
20
+
21
+ ## 1. Correctness / behavioral equivalence
22
+
23
+ Existing semantics preserved exactly: append-only, dedup-last-wins, torn-line skip, corrupt-file
24
+ recovery. `appendLine` keeps the cache coherent so the read after a completion is also free;
25
+ `compact()` re-seeds the cache after its rewrite. Verified: 30 original `JobRunHistory` tests + 4 new
26
+ regression tests (cache-hit does zero full-file reads across 75 ops; external append picked up via
27
+ tail-read; torn-line skipped; full re-read on shrink); the full scheduler suite (incl. MigrationLedger,
28
+ reaper, run-record) = 325; 346 total green.
29
+
30
+ ## 2. Cache-coherence / multi-writer safety
31
+
32
+ The file has two in-process append writers (this instance + `MigrationLedger.appendMigrationEvent`),
33
+ both append-only. The `(size, mtimeMs)` key + intact-prefix tail-read handles an external append
34
+ correctly (picks up appended rows via tail-read). A shrink (only `compact()` rewrites) forces a full
35
+ re-read, so a compaction can never serve a stale cache. The cache is per-process in-memory (not
36
+ shared) — no cross-process coherence concern (each process reads its own view, same as before).
37
+
38
+ ## 3. Fail-safe
39
+
40
+ The new fail paths (cache read, tail-read, stat) fall back to a full re-read or to the prior
41
+ behavior; each genuinely fail-safe catch is tagged `@silent-fallback-ok`. A stat/read error degrades
42
+ to a full re-read (correct result, just not the fast path) — never wrong data, never a crash.
43
+
44
+ ## 4. Blast radius
45
+
46
+ One module (`JobRunHistory.ts`) + its test. No API/route change, no schema change, no write-path
47
+ semantics change (`compact()` still rewrites on its existing cadence — see the residual note). No
48
+ new external surface.
49
+
50
+ ## 5. Residual (tracked, NOT fixed here — out of scope)
51
+
52
+ `job-runs.jsonl` grows UNBOUNDED to 13MB (and the cartographer `index.json` is 91MB) — a genuine
53
+ Bounded-Accumulation issue. The cache makes READS cheap regardless of size, but `compact()` still
54
+ does a full synchronous ~13MB WRITE on startup, and the file keeps growing. A retention/rotation cap
55
+ on `job-runs.jsonl` (and the cartographer index) is a separate Bounded-Accumulation follow-up worth
56
+ filing — named here so it is not silently dropped.
57
+
58
+ ## 6. Rollback
59
+
60
+ Revert the one source file. The change is purely a read-path cache; reverting restores the (slow but
61
+ correct) full-read behavior.