@nredd/pi-session-kit 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ - Make the portable stats CLI import-safe: importing `dist/pi-stats-export.mjs` no longer runs an operation; direct Node execution still runs it
6
+
7
+ - Replace `/mon`'s ambiguous non-free label with `total-free` (`T-F` compact); accept additive native App+Wired+Compressed and swap metrics without silently substituting free-page subtraction
8
+ - Accept native macOS interval CPU and footprint; add `f` to toggle the labelled byte column between `RSS` and `FOOT`, with failed measurements shown as `--`
9
+ - Explain RSS versus footprint, compressed pages, executable names (`pi` as `node`), strong native identity and unavailable-CPU fallback in `?`; native metrics require the subsequent published sampler dependency bump
10
+
11
+ - Raise the `monitor_start` `timeout_s` cap from 24h to 7 days (`604800`) so monitors can outlive long jobs
12
+
13
+ ## 0.8.0
14
+
15
+ - Add `/mon`: a read-only TUI dashboard of host CPU history, non-free memory, load and sampled processes, on by default (`mon.enabled`)
16
+ - Depend on the exact published `@nredd/pi-resource-sampler@0.1.1` (executable names, never argv or process titles; Bun host CPU measured), locked as a nested registry install so tests never borrow the workspace copy; one sampler per open view, disposed (children reaped, no timers) on close, session shutdown or replacement
17
+ - Map warming/unavailable metrics to `--`, never zero; split warnings (budget-limited lists, unavailable host CPU) from muted platform hints (Linux kernel-thread skips, unknown names, macOS process CPU)
18
+ - Render unreadable names as `(unknown)` and mark macOS names that may be cut at 16 bytes with `…`
19
+ - Header shows collector state with the last successful collection's local time; every resolved sample counts as live, separate from host CPU `warming`/`unavailable`
20
+ - Side-by-side cards from 72 columns with a two-row CPU history at the observed monotonic interval; process table `PID CPU% RSS MEM% NAME` with a selection marker and overflow scrollbar
21
+ - Pack `docs/mon-dashboard.md`, which the README links
22
+
5
23
  ## 0.7.0
6
24
 
7
25
  - Add a `/stats` **Machines** tab (Tab/Shift-Tab): each known machine's label and opaque UUID, local scan status, and remote producer `exportedAt` UTC instant plus age at opening
package/README.md CHANGED
@@ -3,20 +3,27 @@
3
3
  Published to npm as `@nredd/pi-session-kit` (public). From `0.4.0`, each version is cut by the monorepo `Publish` workflow from a signed `@nredd/pi-session-kit@<version>` tag, with npm provenance and a GitHub release carrying the tarball, checksums and manifest (`docs/releases.md`); earlier versions were published manually without provenance. `0.4.0` adds `createSessionKit(options)` (explicit `config`, `agentDir`, `monitorRoot`, injectable `clock` and `spawnMonitor`; the default export is unchanged) and fixes leaderless orphan-group sweeps at `ps` start-time resolution. `0.5.0` redesigns `/stats` around the full pane (pinned header, aligned stat cells, a Models table on wide panes) and gives every chart a clock- or calendar-aligned time axis, including the 5h and 24h hourly views.
4
4
  Use the [monorepo development commands](https://github.com/nredd/pi-harness/blob/main/README.md). The Git install and standalone harness
5
5
  references below describe the legacy deployment, not installation or candidate verification of
6
- this package. `0.6.0` keeps passive monitor output only in the monitor ring buffer rather than adding it to the transcript or model context; `monitor_read` is the explicit output surface, and match/exit/timeout wakes are compact summaries with a cursor. `0.7.0` adds the `/stats` Machines tab with per-machine producer export age, separate from the viewer's last import, and the collapsible monitors widget.
6
+ this package. `0.6.0` keeps passive monitor output only in the monitor ring buffer rather than adding it to the transcript or model context; `monitor_read` is the explicit output surface, and match/exit/timeout wakes are compact summaries with a cursor. `0.7.0` adds the `/stats` Machines tab with per-machine producer export age, separate from the viewer's last import, and the collapsible monitors widget. `0.8.0` adds `/mon`.
7
7
 
8
8
  ## Standalone use
9
9
 
10
- The packed tarball is self-contained: Pi host modules (`@earendil-works/pi-*`, `typebox`) are `*` peers supplied by Pi, there are no runtime dependencies and no sibling plugin is required. Verified by installing the tarball alone and running it in exact stock Pi 1.3.0 (see the repository's `docs/package-contracts.md`). Optional fork integrations (subagents, Plan mode) are absent in that setup; the exercised behavior does not need them.
10
+ The packed tarball is self-contained: Pi host modules (`@earendil-works/pi-*`, `typebox`) are `*` peers supplied by Pi, and no sibling plugin is required. Its only runtime dependency is the exact published `@nredd/pi-resource-sampler@0.1.1` library (no Pi imports), which npm installs from the registry for `/mon`. Verified by installing the tarball alone and running it in exact stock Pi 1.3.0 (see the repository's `docs/package-contracts.md`). Optional fork integrations (subagents, Plan mode) are absent in that setup; the exercised behavior does not need them.
11
11
 
12
12
  Session-scoped pi extensions for tmux users. No daemon, supervisor, core patch or cross-pane
13
13
  registry. Each runtime owns its goal, loops and monitors; persisted goal state follows the active
14
14
  session branch. Closing pi stops its work.
15
15
 
16
+ `/mon` opens a read-only, full-pane view of local CPU, memory, load and processes, sampled
17
+ every ~2s by the shared `@nredd/pi-resource-sampler` library only while it is open. Details and
18
+ limits (macOS native CPU/footprint require the upcoming sampler pin, executable names keep 16 bytes,
19
+ RSS excludes compressed pages, `f` toggles a labelled footprint column,
20
+ and memory labels state their measurement basis) are in
21
+ [docs/mon-dashboard.md](docs/mon-dashboard.md). It is not a second `/monitor` process runner.
22
+
16
23
  ## Install and config
17
24
 
18
25
  ```sh
19
- pi install npm:@nredd/pi-session-kit@0.7.0
26
+ pi install npm:@nredd/pi-session-kit@0.8.0
20
27
  ```
21
28
 
22
29
  Optional `~/.pi/agent/pi-session-kit.json` (respects `PI_CODING_AGENT_DIR`):
@@ -29,7 +36,8 @@ Optional `~/.pi/agent/pi-session-kit.json` (respects `PI_CODING_AGENT_DIR`):
29
36
  "loop": { "enabled": true },
30
37
  "monitor": { "enabled": true, "collapseKey": "ctrl+shift+m", "maxWidgetLines": 12 },
31
38
  "diff": { "enabled": true },
32
- "stats": { "enabled": true }
39
+ "stats": { "enabled": true },
40
+ "mon": { "enabled": true }
33
41
  }
34
42
  ```
35
43
 
@@ -63,13 +71,17 @@ persistent cron is installed. Deferred-tool configurations need `goal_update` in
63
71
  ## Monitor
64
72
 
65
73
  - `monitor_start({argv, name, match?, wake?, timeout_s?})`: run an argv command with stdin closed,
66
- no implicit shell. Five active monitors maximum, one-hour default timeout. Explicitly passing a
74
+ no implicit shell. Five active monitors maximum, one-hour default timeout, 7 day (`604800` s) maximum. Explicitly passing a
67
75
  shell remains arbitrary command execution; this is not a sandbox.
68
76
  - `monitor_read({id, since?})`: numbered output since a cursor. Ring buffers retain 2000 lines.
69
77
  - `monitor_stop({id})`, `/monitor list`, `/monitor stop <id|all>`: stop and inspect owned monitors.
70
78
  - Output batches every two seconds into the bounded ring buffer (with dropped-line reporting). Passive batches neither start turns nor create transcript/model-context entries; call `monitor_read` to fetch them. A regex match, natural exit, timeout or `wake: true` emits one compact visible wake with a cursor after the output the model last saw through a wake or read, not a replay of buffered output. A bounded batch preserves its first and latest dropped matching lines as wake evidence even if later output evicts them from the ring.
71
79
  - Children run in process groups: SIGTERM, then SIGKILL. Exit cleanup and pidfiles cover orderly quit
72
- and later recovery after SIGKILL. Files live at
80
+ and later recovery after SIGKILL. Sweeps reauthenticate the leader after enumerating members;
81
+ uncertain identities or guards never authorize signaling. Reap checks are bounded to 20 polls
82
+ after escalation; a timeout keeps authenticated recovery on disk rather than claiming cleanup.
83
+ Spawn failures after child creation kill the child; matcher initialization failures also remove
84
+ its pidfile. A spawn completing after session disposal is killed without registration. Files live at
73
85
  `$TMPDIR/pi-monitors-<uid>/<sessionId>.<ownerPid>/<monitorId>.json`. Startup checks dead-owner
74
86
  records, including a resumed session, and validates process identity before signalling. It never
75
87
  sweeps a live owner's monitors. Two panes resuming the same session have separate directories.
@@ -213,8 +225,17 @@ the kit does not append a second reminder or request another continuation.
213
225
  `test/scenarios/` drives the real kit through core's in-process session harness under Vitest and Bun,
214
226
  covering goals, file-backed restore, child rename/reload, read-rule injection, titles,
215
227
  monitor output/wakes/cursors/caps/timeouts and loop scheduling with same-cwd session isolation.
228
+ Command completion scenarios resolve public runner suggestions and execute the selected `/goal`,
229
+ `/loop` and `/monitor` commands, checking persisted state, timer removal and the selected child.
230
+ Loop resume starts from a provider-error pause: ticks stay quiet until the selected resume command
231
+ restores dispatch. Git setup uses child-only scratch HOME/config and empty hooks/templates, with
232
+ hostile config/template/hook controls; it never changes the parent process HOME.
233
+ Pidfile age-boundary fixtures pin file/directory mtimes to the injected sweep clock and check the
234
+ 24-hour threshold under both past and future wall clocks.
216
235
  The interactive host/VirtualTerminal covers `/diff`, `/stats` and real `/name`, including width/resize
217
- matrices, chart glyph ports/colours and file-backed titles. Stats histories use a fixed clock;
236
+ matrices, chart glyph ports/colours and file-backed titles. Mounted command callbacks also verify
237
+ live-theme invalidation, empty Models ranges, help page boundaries, source-failure reopening,
238
+ stale deferred factories and stats load-error cleanup without replacing the interactive host. Stats histories use a fixed clock;
218
239
  `createSessionKit({ agentDir, clock })` scopes both offline loading and displayed time ranges.
219
240
  Terminal fixtures route input chunks through pi-tui's real `StdinBuffer`, matching ProcessTerminal's
220
241
  keypress splitting (complete keys only; lone Escape is flushed immediately). Stats scroll-to-clamp
@@ -223,6 +244,18 @@ tabs and exact provider totals remain covered at every width. Each stats-view bo
223
244
  and disposes it in `finally`, so runner timeout teardown cannot race a still-running callback.
224
245
  Monitor scenarios use a shared fake clock and controlled children; native `test/monitor/` keeps real
225
246
  workers/processes, and process-group kill/orphan recovery stays in the assembled integration tier.
226
- `npm run check` runs Biome, TypeScript, the full Vitest suite and `bun test test/scenarios` (Bun 1.3.14).
247
+ Native stop/dispose checks assert direct children are reaped when the operation resolves, then
248
+ observe orphaned descendants disappearing with the existing bounded liveness check. Leader exit
249
+ and pipe closure do not synchronize PID 1's reaping; on Linux, `kill(pid, 0)` can still succeed
250
+ for a terminated zombie. No extra signal, fixed delay or larger timeout is needed.
251
+ The native diff descendant timeout fixture gates its fake deadline on real stdout readiness, so
252
+ slow process startup cannot race a fixed wall-clock deadline. Adapter fault tests cover monitor
253
+ startup/sweep/widget failures, post-spawn errors, worker startup timeout, rejected sampler cleanup,
254
+ first/latest dropped-match evidence, title-stream failure and real rule reinjection after tree
255
+ navigation. Synthetic PIDs are never passed to real signal capabilities. Native Promise construction
256
+ initializes the monitor exit resolver before child handlers register; synchronous close/error
257
+ registration fixtures lock that ordering. The overwritten no-op resolver initializer is removed,
258
+ not a reachable validation or lifecycle path.
259
+ `npm run check` runs Biome, TypeScript, the full Vitest suite and `bun test test/scenarios` (Bun 1.4.4).
227
260
  Cross-extension Plan/subagent/todo interactions and OS terminal boundaries remain in the integration tier;
228
261
  `docs/verification-rebuild.md` at the monorepo root tracks their destinations.
@@ -1,5 +1,7 @@
1
1
  // src/stats/cli.ts
2
+ import { realpathSync } from "node:fs";
2
3
  import { resolve as resolve2 } from "node:path";
4
+ import { pathToFileURL } from "node:url";
3
5
 
4
6
  // src/stats/load.ts
5
7
  import { createHash, randomUUID } from "node:crypto";
@@ -651,7 +653,23 @@ async function main() {
651
653
  "usage: export --agent-dir DIR --output OUT [--sessions-dir DIR] | import --agent-dir DIR --input REPO"
652
654
  );
653
655
  }
654
- main().catch((error) => {
655
- console.error(error instanceof Error ? error.message : "Stats operation failed");
656
- process.exitCode = 1;
657
- });
656
+ async function run() {
657
+ try {
658
+ await main();
659
+ } catch (error) {
660
+ console.error(error instanceof Error ? error.message : "Stats operation failed");
661
+ process.exitCode = 1;
662
+ }
663
+ }
664
+ function isEntrypoint() {
665
+ try {
666
+ return !!process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
667
+ } catch {
668
+ return false;
669
+ }
670
+ }
671
+ if (isEntrypoint()) void run();
672
+ export {
673
+ main,
674
+ run
675
+ };
@@ -0,0 +1,140 @@
1
+ # /mon resource dashboard
2
+
3
+ `/mon` is a read-only, interactive-TUI view of local host resources. It is on by default and
4
+ disabled with `{ "mon": { "enabled": false } }` in `pi-session-kit.json`. In RPC/print/JSON modes
5
+ it only warns; it never samples there.
6
+
7
+ ## Source
8
+
9
+ `src/mon/source.ts` is a thin mapping over the published `@nredd/pi-resource-sampler`, declared
10
+ as the exact runtime dependency `0.1.1` (`0.1.0` leaked argv-style process titles as macOS names
11
+ and could fake Bun host CPU). No collector lives in session-kit. Each open view creates
12
+ one sampler (its own CPU baseline) and disposes it on close; disposal aborts and awaits any active
13
+ sample, including its `ps` child on macOS. Mapping rules:
14
+
15
+ - Collector success and host CPU state are separate. Every resolved sample is `LIVE` and refreshes
16
+ the last-success time, even while host CPU is `warming` or `unavailable`, because its memory,
17
+ load and process data are fresh. The CPU readout then says `-- warming` or `-- unavailable`.
18
+ The header pairs the state (`● LIVE`, `○ WAITING`, `▲ STALE`) with the local wall time of the
19
+ last successful collection (`updated HH:MM:SS`, `last ok` when stale), so freshness is never
20
+ inferred from the CPU state.
21
+ - `warming` and `unavailable` metrics become `--`, never zero. The first observation is a warming
22
+ baseline; persistent warming shows the sampler's reason (e.g. `Host CPU warming: CPU counters
23
+ have not advanced`).
24
+ - Two diagnostic tones. A warning (`▲`, warning color) means a limit hides data the user would
25
+ expect: `Partial list: S of E seen (reasons)` for budget caps or collector failures (at most three
26
+ reasons, in words: `time budget`, `process cap`, `read budget`, `ps failed`, ...), unavailable host
27
+ CPU, persistent warming. A muted hint states an expected platform limit: Linux always skips kernel
28
+ threads (no RSS) and processes that exit mid-scan, so a scan whose only reason is
29
+ `process-unreadable-or-changed` is `N skipped: kernel threads, exited or unreadable`, not a
30
+ warning; `N names unknown (no access, e.g. other users)`; macOS process CPU. The process header
31
+ counts *sampled* rows, not host processes.
32
+ - Names are the sampler's executable basenames, never argv or process titles. `""` renders as a dim
33
+ `(unknown)` (Linux hides other users' `/proc/PID/exe` without ptrace access). macOS `p_comm` keeps
34
+ 16 bytes, so a macOS name of 16+ UTF-8 bytes ends in a dim `…` (`Google Chrome He…`): possibly cut,
35
+ never claimed complete. Names stay untrusted text: sanitized, never interpreted.
36
+ - Upcoming sampler source supplies macOS native interval CPU and footprint via builtin FFI and
37
+ libproc, with process-start absolute ticks plus microsecond identity. Inaccessible PIDs and
38
+ unsupported runtimes have unknown metrics. The existing published `0.1.1` remains entirely
39
+ unavailable for process CPU; `ps` whole-second start times alone never authenticate CPU. The
40
+ view orders by RSS only when every process CPU is unavailable, until the user picks a sort.
41
+ Linux first samples are warming, so CPU order stays stable.
42
+ - History spacing uses the sampler's monotonic `monotonicTimeMs`, so wall-clock steps (NTP, manual
43
+ changes) cannot invent or hide gaps; the wall `wallTimeMs` is only displayed (header clock and
44
+ last-success UTC).
45
+ Node's monotonic clock pauses during system sleep on macOS and Linux, so a suspend shows no gap.
46
+ `scan Nms` (80+ columns) is the sampler's collection duration. Missing/zero memory totals and non-finite values become unknown.
47
+
48
+ ## Presentation
49
+
50
+ - From 72 columns and 17 rows (one more per diagnostic line), CPU and memory/load cards sit side
51
+ by side; CPU history is two rows (16 levels) there. Narrower or shorter terminals get a
52
+ three-line summary (`CPU`, `RAM`, 1-minute load, one-row history); at six rows summary/chrome
53
+ yields to the selected process, but a compact partial-list or stopped/retry warning remains visible.
54
+ At eight rows, core CPU/RAM/load metrics take priority over optional history and platform hints.
55
+ Compact metrics tighten spacing when needed to keep CPU, RAM and the 1-minute load visible.
56
+ The last row reserves close/help. Below 60 columns the header
57
+ keeps the update clock over the tagline.
58
+ - Columns: `PID`, `CPU%` (100 = one core), `RSS`, `MEM%` (RSS over total RAM, 60+ columns),
59
+ `NAME` last so long names never push numbers off screen. `f` toggles the byte column between
60
+ `RSS` and `FOOT` (native physical footprint, including compressed memory); unavailable/failed
61
+ PIDs show `--`, never zero. `MEM%` and `m` sorting remain RSS-based in either view. `--` is dimmed. A `▸` marks the
62
+ selection; a theme scrollbar appears only when the list overflows. The memory card adds 1-minute
63
+ load per logical CPU.
64
+ - Host CPU severity colors the value (success under 50%, warning under 85%, error above) next to
65
+ the number itself. Stale CPU retains the last measured value, muted rather than live severity-colored.
66
+ Cards add a `last value` label; compact summaries rely on the `STALE` header instead, reserving
67
+ the metric row for CPU, RAM and load.
68
+ - Host CPU history keeps each capture timestamp, uses a fixed 0..100% scale and at most 120
69
+ samples, newest on the right. A column is the lower-median interval between captures, widened
70
+ only when the history is longer than the width, so completion-paced drift never invents gaps.
71
+ Columns with no CPU value after the first one are gaps (`·`), never zero or interpolation;
72
+ columns before it are blank, including the warming baseline. Stale snapshots add no point. The caption states the actual span. The latest sample in a bucket wins.
73
+ Stale views show the last successful capture's UTC timestamp (or `none`). Waiting/live/stale and
74
+ CPU warming/unavailable use semantic theme colors and text, so meaning does not depend on color.
75
+ - Memory labels its actual basis. Older samplers report **total-free** (`T-F` in the compact
76
+ summary), which includes cached files and is NOT Activity Monitor's Memory Used. Additive v1
77
+ `memory.usage` metrics instead render **used (A+W+C)** (`A+W+C` compact), an estimate of resident
78
+ App + Wired + Compressed physical RAM. A failed native metric stays `--`, never total-free under
79
+ a used label. Cards also show free pages and swap used/allocated; missing swap stays `--/--`.
80
+ `?` explains each basis, RSS excluding compressed pages versus Activity Monitor's footprint,
81
+ executable names (`pi` runs as `node`), and decaying `ps %cpu` versus interval CPU.
82
+ Load averages are not percentages. Process CPU is interval CPU, where 100% is one logical CPU and
83
+ multithreaded processes can exceed 100%.
84
+ - Release sequencing: the registry dependency remains `0.1.1`, which lacks native host-memory
85
+ metrics. This adapter accepts the new optional memory/footprint fields and tests them, but production
86
+ remains honestly labelled total-free with unavailable process CPU/footprint until a new sampler
87
+ is published, its common dependency pin
88
+ and nested registry lock entries are updated, and both consumer registry tests pass. Do NOT claim
89
+ the new native used/swap line is active with the old registry pin.
90
+ - The process viewport preserves selection by sampler identity, not PID or sorted row.
91
+ Exited identities choose a bounded neighboring row; PID reuse cannot inherit selection.
92
+ Unknown/non-finite measurements sort last with deterministic PID/identity tie breaks.
93
+ - `j/k` and arrows select; PageUp/PageDown page; `g/G` jump first/last; `f` toggles RSS/FOOT; `c/m/p` sort
94
+ CPU/RSS/PID; `s` cycles sort; `?` toggles help; `q`/Escape close. CPU/RSS descend,
95
+ PID ascends. Help has a bounded, wrapped viewport with a visible range and scroll hint;
96
+ `j/k`, arrows, PageUp/PageDown and `g/G` scroll help without moving the process selection.
97
+ No kill, locks, workload control, process arguments or environment display. Process names
98
+ are the sampler's sanitized display names and are never interpreted.
99
+
100
+ ## Ownership
101
+
102
+ `MonView` accepts presentation snapshots and never collects data. It caches rendered lines
103
+ by dimensions until update/input/theme invalidation. `MonPoll` owns one completion-paced probe
104
+ (~2s after the previous one completed), with no catch-up queue. Closing aborts collection,
105
+ cancels the timeout and awaits the sampler's disposal. Errors stop collection and retain
106
+ explicitly stale values; reopening is the retry mechanism. Late results are discarded.
107
+
108
+ `registerMon` opens a source only inside a TUI interaction and serializes replacement through
109
+ completed asynchronous disposal, including concurrent opens. Session shutdown/start invalidates
110
+ pending openers before they can create a source, even when Pi defers the UI factory. It registers
111
+ no model tool, persists no history, sends no model messages and does no network work.
112
+
113
+ ## Verification
114
+
115
+ - `npm run check` in `packages/session-kit`. The root lock pins a nested registry install
116
+ (`packages/session-kit/node_modules/@nredd/pi-resource-sampler`, exact `0.1.1` tarball and
117
+ integrity), so tests run the published bytes, not the same-version workspace that npm would
118
+ otherwise link. `source.test.ts` fails if resolution ever reaches the workspace.
119
+ - `test/mon/source.test.ts`: two actual host observations (warming, then measured), a real poll's
120
+ two updates, and mapping edge cases. In-flight close wraps the real sampler to hold its pending
121
+ sample, then requires that sample to reject with `AbortError`, no update, no child and no timer.
122
+ On macOS it first requires the sampler's live `ps` child; Linux reads `/proc` in process, so
123
+ there the `AbortError` is the only provider evidence and no child is claimed.
124
+ - `test/mon/theme.test.ts`: actual built-in dark and light themes over an actual observation at
125
+ 40x12, 60x20, 80x24 and 120x40.
126
+ - `test/scenarios/mon.test.ts` (Vitest and Bun): the real interactive Pi host, `createSessionKit`
127
+ and the real sampler; two observations, resize matrix, close/Escape/reopen, close before the
128
+ first sample and session shutdown all leave no kit timer or child process.
129
+ - `test/mon/{component,poll,index}.test.ts`: layout, identity, history, lifecycle interleavings
130
+ with fakes; `resource-fixture.ts` is type-checked against the package's `ResourceSnapshot`.
131
+ - `make check-package-contracts` packs session-kit, installs it alone (sampler from npm, exact
132
+ lockfile closure) and drives it through stock Pi.
133
+ - Real macOS terminal recordings exercise the packed, registry-backed view, resize, navigation,
134
+ close and reopen. Empty direct-child lists before and after close show only that no child was
135
+ observed at those instants; they do NOT prove an in-flight helper was cancelled or reaped.
136
+ In-flight cancellation/reaping evidence comes from the deterministic real-sampler tests above,
137
+ not those terminal snapshots. No Linux terminal recording is claimed.
138
+ - Manual fixture preview (no sampling): `bun test/mon/terminal.ts dark|light`. It is labelled
139
+ `FIXTURE DATA` and uses `fixture-*` names, so a capture can never pass for host data; it is not
140
+ evidence of real measurements.
@@ -3,7 +3,14 @@
3
3
  Build the committed `dist/pi-stats-export.mjs` with `npm ci && npm run build:stats-export`.
4
4
  The exact esbuild version and dependencies are locked; tests assert byte-identical rebuilds.
5
5
  Copy only that file. Runtime is Node 22+ with builtins only, no Pi install, extension, private
6
- repository, package install, auth or network access.
6
+ repository, package install, auth or network access. Importing the artifact is silent, even when
7
+ an importing process supplies a nonexistent entrypoint argument; direct and symlinked Node execution
8
+ still run the CLI. Packed-CLI tests explicitly launch `node`, including when their Vitest
9
+ host is Bun; this tests the artifact's declared runtime, not Bun's entry-module resolver.
10
+
11
+ Chart tick steps parse decimal powers directly rather than using implementation-approximated
12
+ exponentiation, keeping large finite ticks consistent between Node and Bun. Steps above the
13
+ numeric range retain infinite headroom.
7
14
 
8
15
  ```
9
16
  node ARTIFACT export --agent-dir DIR --output OUT [--sessions-dir ROOT]
@@ -36,10 +43,16 @@ Installation state, never synced:
36
43
  - `DIR/pi-session-kit/stats-cache.json`: local parser cache v2, including file paths and compact
37
44
  usage tuples. Version 1 entries for deleted files stay intact; existing files reparse even if
38
45
  their mtime/size match. Strong identities are not invented for inaccessible legacy files.
46
+ Failed stat/parse operations count as incomplete history; a failed reparse retains the last-good
47
+ cached record. Invalid cached identities and raw provider/model pairs are discarded. Persisted
48
+ child markers apply only to the matching session ID, surviving user-visible renames.
39
49
  - `DIR/pi-session-kit/stats-global.json`: one atomically replaced document containing validated
40
50
  last-good machine snapshots, last successful import time and coverage problems. Imports do not
41
51
  mutate local usage or installation identity. A wholly failed import does not advance the sync
42
- time; a partial import records valid updates and marks coverage incomplete.
52
+ time; a partial import records valid updates and marks coverage incomplete. The assembled cache
53
+ accepts up to 128 MiB; exceeding that limit fails before replacement and retains its last-good
54
+ bytes. Tests inject final-document byte accounting at the exact limit and one byte over, while
55
+ retaining real import validation and atomic publication.
43
56
 
44
57
  Snapshot version 1:
45
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nredd/pi-session-kit",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -12,6 +12,7 @@
12
12
  "src",
13
13
  "dist/pi-stats-export.mjs",
14
14
  "docs/stats-snapshots.md",
15
+ "docs/mon-dashboard.md",
15
16
  "CHANGELOG.md",
16
17
  "LICENSE",
17
18
  "README.md"
@@ -25,6 +26,9 @@
25
26
  "pi-package",
26
27
  "pi-extension"
27
28
  ],
29
+ "dependencies": {
30
+ "@nredd/pi-resource-sampler": "0.2.0"
31
+ },
28
32
  "peerDependencies": {
29
33
  "@earendil-works/pi-coding-agent": "*",
30
34
  "@earendil-works/pi-tui": "*",
@@ -35,15 +39,16 @@
35
39
  "format": "biome check --write src/ test/",
36
40
  "typecheck": "tsc --noEmit",
37
41
  "build:stats-export": "esbuild src/stats/cli.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/pi-stats-export.mjs",
38
- "test": "npm run build:stats-export && vitest run",
39
- "test:bun": "bun test test/scenarios",
42
+ "test": "npm run build:stats-export && vitest run --coverage",
43
+ "test:bun": "bun test test/scenarios --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage/bun",
40
44
  "check": "npm run lint && npm run typecheck && npm run test && npm run test:bun"
41
45
  },
42
46
  "devDependencies": {
43
47
  "@biomejs/biome": "2.5.15",
44
- "@earendil-works/pi-coding-agent": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.7/earendil-works-pi-coding-agent-1.3.0.tgz",
45
- "@earendil-works/pi-tui": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.7/earendil-works-pi-tui-1.3.0.tgz",
48
+ "@earendil-works/pi-coding-agent": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.8/earendil-works-pi-coding-agent-1.3.0.tgz",
49
+ "@earendil-works/pi-tui": "https://github.com/nredd/pi/releases/download/v1.3.0-nredd.8/earendil-works-pi-tui-1.3.0.tgz",
46
50
  "@types/node": "26.6.5",
51
+ "@vitest/coverage-v8": "5.0.3",
47
52
  "@xterm/headless": "5.5.0",
48
53
  "esbuild": "0.28.2",
49
54
  "typebox": "1.3.36",
package/src/config.ts CHANGED
@@ -9,7 +9,7 @@ import { readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
 
12
- export const FEATURES = ["titles", "rules", "goal", "loop", "monitor", "diff", "stats"] as const;
12
+ export const FEATURES = ["titles", "rules", "goal", "loop", "monitor", "diff", "stats", "mon"] as const;
13
13
  export type Feature = (typeof FEATURES)[number];
14
14
 
15
15
  export interface KitConfig {
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ import { configPath, type Feature, type KitConfig, loadConfig } from "./config.t
9
9
  import { register as registerDiff } from "./diff/index.ts";
10
10
  import { register as registerGoal } from "./goal/index.ts";
11
11
  import { register as registerLoop } from "./loop/index.ts";
12
+ import { registerMon } from "./mon/index.ts";
13
+ import { createSamplerSource } from "./mon/source.ts";
12
14
  import { register as registerMonitor } from "./monitor/index.ts";
13
15
  import type { MonitorProcess, SpawnOptions } from "./monitor/process.ts";
14
16
  import { register as registerRules } from "./rules/index.ts";
@@ -20,7 +22,7 @@ export interface SessionKitOptions {
20
22
  config?: KitConfig;
21
23
  agentDir?: string;
22
24
  monitorRoot?: string;
23
- /** Time for stats ranges, loops, goal wakes and monitor batching/timeouts. Defaults to real time. */
25
+ /** Time for stats ranges, loops, goal wakes, monitor batching/timeouts and /mon cadence. Defaults to real time. */
24
26
  clock?: Clock;
25
27
  /** Child boundary only; validation, matching, ring buffering and message delivery stay in the manager. */
26
28
  spawnMonitor?: (argv: readonly string[], options: SpawnOptions) => Promise<MonitorProcess>;
@@ -37,6 +39,7 @@ const MODULES: Record<Feature, Register> = {
37
39
  registerMonitor(pi, config, options.monitorRoot, { clock: options.clock, spawnMonitor: options.spawnMonitor }),
38
40
  diff: registerDiff,
39
41
  stats: (pi, config, options) => registerStats(pi, config, options),
42
+ mon: (pi, _config, options) => registerMon(pi, { createSource: () => createSamplerSource(), clock: options.clock }),
40
43
  };
41
44
 
42
45
  /** Create the real kit without requiring a machine-local agent configuration. */