instar 1.3.229 → 1.3.230

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,66 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Adds `instar dev:profile-node [pid]` — a contributor/agent dev command that
9
+ CPU-profiles a **running** node process and prints its hottest JS functions
10
+ (function + `file:line` + self-time %).
11
+
12
+ **Why this exists (the technique that cracked a real bug).** macOS `sample` — and
13
+ lsof snapshots, empty `gh run --log`, port-grep — cannot symbolicate a node
14
+ process's **JS** frames; they show only native/V8 frames, so a busy agent server's
15
+ actual hot *function* stays invisible. The way to see it is the node process's own
16
+ introspection: `SIGUSR1` opens node's inspector on the running process, then a CDP
17
+ CPU profile over the inspector websocket reports the exact JS call frames. This is
18
+ literally how the systemic agent-server hot-loop was pinned — `StateManager.listSessions`
19
+ burning ~30% of CPU in `readFileUtf8`, invisible to every other tool. This command
20
+ turns that hard-won procedure into one reusable command.
21
+
22
+ **What it does:** resolves the pid (or auto-finds the hottest `node` process) →
23
+ `SIGUSR1` → probes `127.0.0.1:9229-9235` for the inspector target → captures a CPU
24
+ profile (default 5s, `--duration`) → prints the top frames by self-time (`--top`).
25
+ The biggest non-idle frame is the hot path. Read-only aside from the SIGUSR1, which
26
+ opens the inspector on localhost for the process's lifetime (it closes on the next
27
+ restart) — the command says so in its output.
28
+
29
+ Pairs with `dev:ci-failures` and `dev:preflight` as the contributor dev-loop
30
+ toolkit (friction → tooling).
31
+
32
+ ## What to Tell Your User
33
+
34
+ Nothing required — it is a developer/agent tool for working on Instar. When an
35
+ agent server is mysteriously burning CPU, the instar dev:profile-node command
36
+ prints the exact JS function responsible, which native profilers cannot show.
37
+
38
+ ## Summary of New Capabilities
39
+
40
+ - `instar dev:profile-node [pid] [--duration <sec>] [--top <n>]` — CPU-profile a
41
+ running node process (SIGUSR1 + inspector + CDP) and print its hottest JS frames.
42
+ No pid → profiles the hottest node process.
43
+
44
+ ## Evidence
45
+
46
+ - Built directly from the technique that pinned the real hot-loop: after `sample`,
47
+ lsof, grep, and log-diffing all failed, SIGUSR1 + a CDP CPU profile showed 30.8%
48
+ `readFileUtf8` + 7% `listSessions` (StateManager) — the JS frame nothing else
49
+ could see — leading to the cache fix.
50
+ - Live end-to-end smoke: profiling a spawned CPU-busy node correctly reported the
51
+ hot frame (`listOnTimeout`, where the busy loop ran).
52
+ - Tests: `tests/unit/devProfileNode.test.ts` — pure `aggregateHotFrames` (ranking,
53
+ file:line normalization, anonymous-frame fallback, empty profile, topN) +
54
+ `findInspectorTarget` (first-hit / none) + `runDevProfileNode` orchestration with
55
+ injected deps (happy path, no-pid→hottest, no-node→exit1, signal-fail→exit1,
56
+ no-inspector→exit1, capture-throw→exit1).
57
+
58
+ ## Also in this change
59
+
60
+ Fixes a real false-negative in `scripts/docs-coverage.mjs`: a command's coverage
61
+ was keyed on its camelCase **file** name (`devProfileNode`), so the needle
62
+ `instar devProfileNode` never matched the shipped colon-command
63
+ `instar dev:profile-node` — every `dev:*` command (ci-failures, preflight,
64
+ profile-node) read as undocumented. The matcher now also tries a kebab form and
65
+ treats `:`/`-` as interchangeable, so colon-commands count. (The new command
66
+ surfaced this — friction → structural fix.)
@@ -0,0 +1,42 @@
1
+ # A stethoscope for a node process that's running hot
2
+
3
+ ## The one-sentence version
4
+ When one of our programs is burning the CPU for no obvious reason, the usual tools can tell you *that* it's busy but not *what line of code* is doing it. This new command — `instar dev:profile-node` — presses a stethoscope to the running program and prints the exact function that's hogging the CPU.
5
+
6
+ ## The problem it solves
7
+ Picture a car idling in the driveway but somehow guzzling gas. You pop the hood with your usual gauge — it says "engine's working hard" but won't tell you *which part*. That's macOS's built-in profiler on a Node.js program: it sees the engine block (the low-level machinery) but can't read the actual code, so the real culprit stays hidden.
8
+
9
+ This bit us for real: three of our agent servers were each burning ~50–60% CPU, bouncing them never helped, and four different tools all came back "busy, but I can't tell you why."
10
+
11
+ ## The trick that worked (now a one-liner)
12
+ Node programs have a hidden superpower: send them a specific nudge (the SIGUSR1 signal) and they open a little diagnostic port on themselves. Connect to that port, record a few seconds of "what function is running right now," and it tells you — in plain code terms — exactly where the time goes.
13
+
14
+ That's how we finally caught it: **30% of the CPU was in `readFileUtf8`, inside `listSessions`** — a function re-reading every session file off the disk, over and over. Invisible to everything else; obvious once we looked through the right lens.
15
+
16
+ `instar dev:profile-node` bakes that whole trick into one command:
17
+ - Point it at a process (or let it auto-find the hottest one).
18
+ - It nudges the process, records ~5 seconds, and prints the hottest functions with their percentages.
19
+ - The biggest non-idle line is your culprit.
20
+
21
+ ## What it looks like
22
+ ```
23
+ Hottest JS frames (self-time) for pid 12673:
24
+ 87.6% listOnTimeout node:internal/timers:546
25
+ 9.9% (program)
26
+ 2.5% (garbage collector)
27
+ ```
28
+ (That's from a test process running a busy loop inside a timer — and it correctly fingered the timer callback.)
29
+
30
+ ## The safety notes
31
+ 1. The only thing it changes is opening that diagnostic port — and only on **localhost** (your own machine), only until the process next restarts. The command tells you it did this.
32
+ 2. It **samples** the process — it never kills it, restarts it, or edits anything.
33
+ 3. It's a developer tool, run on demand — nothing in the running system uses it.
34
+
35
+ ## Why it matters
36
+ The hard part of fixing a mystery slowdown isn't the fix — it's *finding* the cause. This turns a clever, hard-won debugging trick into a reusable one-liner, so the next time any node process runs hot, the answer is one command away instead of an afternoon of dead ends. (Same spirit as our `dev:ci-failures` tool: when a workaround saves the day, make it permanent.)
37
+
38
+ ---
39
+
40
+ **Rendered (verified HTTP 200):** https://echo.dawn-tunnel.dev/view/e5433071-bf6f-4cf0-b97a-5e16d9f7b160?sig=2c015aaeedc96ebb20d57b326f44d88eaa93dd2bcba0b4e8347a33d23f4d2b4b
41
+
42
+ *See also: the full success-story + meta-insights writeup for this run.*
@@ -0,0 +1,85 @@
1
+ # Side-Effects Review — `instar dev:profile-node`
2
+
3
+ **Version / slug:** `dev-profile-node-tool`
4
+ **Date:** `2026-06-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (dev-only CLI; read-only sampling; no server/runtime surface)`
7
+
8
+ ## Summary of the change
9
+
10
+ A new dev CLI command that CPU-profiles a running node process via SIGUSR1 + the
11
+ node inspector + a CDP CPU profile, and prints the hottest JS frames. New file
12
+ `src/commands/devProfileNode.ts` + a cli.ts registration + a manifest entry +
13
+ awareness lines. No server, route, config, or runtime-behavior change.
14
+
15
+ ## Decision-point inventory
16
+
17
+ 1. **pid resolution** — explicit `[pid]` digits win; otherwise auto-pick the
18
+ hottest `node` process (`ps -Aceo pcpu,pid,comm`). No node found → exit 1.
19
+ 2. **Inspector discovery** — probe ports 9229–9235 (node's default + increments
20
+ when the default is taken); first target with a `webSocketDebuggerUrl` wins;
21
+ none → exit 1 with a clear message.
22
+ 3. **Profile aggregation** — `aggregateHotFrames` sums per-node `hitCount` by call
23
+ frame, normalizes the path to `dist/…`/`src/…`, +1 to the 0-based line, and
24
+ returns top-N by self-time. Idle/native frames are kept on purpose (seeing
25
+ "48% idle, 30% readFileUtf8" is the signal).
26
+ 4. **Exit codes** — 0 on a successful print (even "no samples"); 1 only on an
27
+ operational failure (no node, signal failed, no inspector, capture threw).
28
+
29
+ ## 1. The one real side effect — SIGUSR1 opens the inspector
30
+
31
+ SIGUSR1 tells node to start its inspector on `127.0.0.1` (localhost-only) for the
32
+ life of the process. Implications, all bounded:
33
+ - **Localhost-only** — the inspector binds 127.0.0.1, not a public interface; it is
34
+ not remotely reachable.
35
+ - **Transient** — it closes when the process restarts; it does not persist.
36
+ - **Disclosed** — the command prints that it left the inspector open on the pid.
37
+ - **No state mutation** — profiling samples the process; it does not change its
38
+ behavior, memory, or files.
39
+
40
+ This is the same standard procedure node documents for live debugging; it is the
41
+ *only* way to symbolicate JS frames on a running process. The benefit (pinning a
42
+ hot loop that no other tool can see) outweighs a transient localhost debug port on
43
+ a dev box. Operators who object can simply not run the command.
44
+
45
+ ## 2. Over/under-block
46
+
47
+ Not applicable — it gates nothing and changes no behavior. A wrong pid or a
48
+ non-node target fails cleanly with exit 1; it never acts on the target beyond
49
+ SIGUSR1 + reading the profile.
50
+
51
+ ## 3. Blast radius
52
+
53
+ Zero runtime surface. The command runs only when a contributor/agent invokes it.
54
+ No server code path imports it; it is a leaf CLI command behind a dynamic import
55
+ (like `dev:ci-failures`). The `ws` dependency is already present (`^8.19.0`).
56
+
57
+ ## 4. Migration parity
58
+
59
+ CLAUDE.md template (`generateClaudeMd`) + `site/.../cli.md` + the builtin-manifest
60
+ (`cli:dev-profile-node`) are updated in lockstep so the awareness + discoverability
61
+ checks stay green. No config/hook/skill migration — it is a pure CLI addition
62
+ (same shape as `dev:ci-failures`, which needed none).
63
+
64
+ ## 5. Testability
65
+
66
+ `aggregateHotFrames` and `findInspectorTarget` are pure/dep-injected; the
67
+ SIGUSR1 / inspector-http / ws-CDP boundary is behind `ProfileNodeDeps` so the
68
+ orchestration is unit-tested without a real process. A live end-to-end smoke
69
+ (profiling a spawned busy node) confirmed the real path.
70
+
71
+ ## 6. What it does NOT do
72
+
73
+ - Does not kill, restart, or modify the target (only SIGUSR1 + sample).
74
+ - Does not run as part of the server or any job — invoked on demand only.
75
+ - Does not expose a remote/public debug port (inspector is localhost).
76
+
77
+ ## 7. Collateral fix — docs-coverage matcher
78
+
79
+ Adding the command surfaced a false-negative in `scripts/docs-coverage.mjs`: it
80
+ matched a command by its camelCase file name, so colon-commands (`dev:*`) never
81
+ matched their documented form and read as undocumented (the floor was sitting
82
+ right at the limit, so the new command tipped it). The matcher now also tries a
83
+ kebab form and normalizes `:`↔`-`. Audit-only script (the weekly docs-coverage
84
+ report); no runtime/behavior effect. It re-classifies the three `dev:*` commands
85
+ as documented, which is correct (they ARE in cli.md + the CLAUDE.md template).