instar 1.3.414 → 1.3.416

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,73 @@
1
+ # Side-Effects Review — lint: ban blocking process scans on the runtime hot path
2
+
3
+ **Version / slug:** `lint-no-blocking-process-scans`
4
+ **Date:** `2026-06-07`
5
+ **Author:** `Echo`
6
+ **Tier:** 1 (a new CI lint + comment-only src annotations + a test; no behavior change, no API/route/config/migration)
7
+ **Second-pass reviewer:** `Echo (self) — Tier-1; preventive gate, no runtime behavior change`
8
+
9
+ ## Summary of the change
10
+
11
+ Adds `scripts/lint-no-blocking-process-scans.js` (wired into `npm run lint`): in
12
+ `src/monitoring/` and `src/server/`, a SYNCHRONOUS child-process call
13
+ (`spawnSync`/`execSync`/`execFileSync`) whose command literal is a process-
14
+ enumeration tool (`ps`/`pgrep`/`lsof`/`pkill`) now fails CI. This is post-mortem
15
+ standard #3 for the 2026-06-07 "server temporarily down" incident (topic 21816):
16
+ synchronous `ps`/`lsof` scans on a cadence blocked the event loop and starved
17
+ `/health` under load → the supervisor restarted an alive server → the loop.
18
+ #972 fixed SessionWatchdog; this lint stops the class from being re-introduced.
19
+
20
+ The two existing in-dir call sites are `lsof` one-shots, allowlisted with inline
21
+ `// lint-allow-blocking-scan:` justifications (comment-only edits):
22
+ - `SessionRecovery.ts` — targeted `lsof -p <pid>` (single process, 5s timeout),
23
+ runs once during a session's JSONL recovery, not on a cadence.
24
+ - `agentWorktreeGit.ts` — full-cwd `lsof` in AgentWorktreeReaper, which ships
25
+ dark + dry-run by default (not on any live agent's hot path); 15s timeout;
26
+ async conversion noted as a follow-up.
27
+
28
+ ## Decision-point inventory
29
+
30
+ - The only decision: which sync calls to fail CI on. Scoped to the documented
31
+ load-sensitive enumeration commands (`ps`/`pgrep`/`lsof`/`pkill`) in the two
32
+ runtime hot dirs. tmux/git calls are deliberately NOT covered (bounded, fast,
33
+ ubiquitous — converting them is a separate, bigger concern and not this
34
+ incident's cause).
35
+
36
+ ## 1. False positives (flagging a safe call)
37
+
38
+ A genuinely one-shot, bounded sync scan is excused by an inline
39
+ `// lint-allow-blocking-scan: <reason>` (scanned up to 6 lines above the call to
40
+ allow a multi-line justification). The escape hatch requires a written reason, so
41
+ the decision is reviewed, not silent. Two current sites use it.
42
+
43
+ ## 2. False negatives (missing a real one)
44
+
45
+ Static detection matches a string-literal command. A sync scan that passes the
46
+ command via a variable (e.g. `execFileSync(file, args)` in `mcpProcessReaperDeps`)
47
+ is not caught — accepted: the lint is a ratchet against the common, copy-pasted
48
+ literal form; the variable-indirection cases are pre-existing and tracked. tmux
49
+ is intentionally out of scope.
50
+
51
+ ## 3. Level-of-abstraction fit
52
+
53
+ Correct: a CI lint in the existing `lint-no-*` family, modeled on
54
+ `lint-no-unfunneled-headless-launch.js`. Structure > Willpower — a future periodic
55
+ `spawnSync('ps')` fails the build instead of being discovered as a stall in prod.
56
+
57
+ ## 4. Blast radius
58
+
59
+ CI-only. No runtime code changes (the two src edits are comments). Cannot affect a
60
+ running agent. Worst case of a bug in the lint = a spurious CI failure, fixed by an
61
+ allowlist comment or a lint tweak — never a production impact.
62
+
63
+ ## 5. Rollback
64
+
65
+ Remove the script + the one `package.json` chain entry; revert the two comment
66
+ annotations. No state/format change.
67
+
68
+ ## 6. Tests
69
+
70
+ `tests/unit/lint-no-blocking-process-scans.test.ts` (5): flags sync ps; flags
71
+ spawnSync pgrep + execSync lsof; honours the inline allow justification; ignores
72
+ comment-only mentions + async/tmux calls; the real runtime tree is clean. The lint
73
+ also self-validated against a synthetic violation. tsc clean.
@@ -0,0 +1,58 @@
1
+ # Side-Effects Review — per-account email on the Subscription Pool
2
+
3
+ ## Scope of change
4
+
5
+ - `src/core/SubscriptionPool.ts` — add optional `email` to `SubscriptionAccount`,
6
+ `AddAccountInput`, `UpdateAccountInput`; `add()`/`update()` store/patch it.
7
+ - `src/core/QuotaPoller.ts` — new exported `readAccountEmail(configHome)` (reads the
8
+ PUBLIC `oauthAccount.emailAddress` from the config home's `.claude.json`); `pollAll()`
9
+ auto-populates `account.email` from it on each poll.
10
+ - `src/server/routes.ts` — POST/PATCH `/subscription-pool` pass `email` through.
11
+ - `dashboard/subscriptions.js` + `dashboard/index.html` — render the email under the
12
+ account nickname. Tests (unit + render assertion).
13
+
14
+ ## Why
15
+
16
+ Operator requirement (Justin, topic 20905): accounts must be identified by nickname
17
+ AND email — e.g. "SageMind - Justin" vs "SageMind - Adriana" vs "SageMind - Dawn"
18
+ are the same org, so the email is the disambiguator. The pool previously stored only
19
+ nickname + login location.
20
+
21
+ ## The safety property (load-bearing)
22
+
23
+ The email is **auto-populated from the account's own login record** (`oauthAccount.
24
+ emailAddress` in the config home), not just operator-typed. So the stored email
25
+ always reflects WHICH account actually authenticated under that config home — a
26
+ login into the wrong account *surfaces* (the email won't match the nickname's intent)
27
+ instead of hiding. This directly serves the operator's stated worry about mislabeling
28
+ accounts. The email is a PUBLIC identifier — never a token/secret.
29
+
30
+ ## What does NOT change
31
+
32
+ - Additive-optional everywhere: `email` is optional; existing accounts and existing
33
+ add/update calls (no email) behave exactly as before (`email` stays undefined).
34
+ - No new route, no new authority, no behavior change to selection/swap/poll beyond
35
+ writing one extra public field. The credential-field guard is unaffected (`email`
36
+ is not a forbidden field name).
37
+
38
+ ## Framework generality
39
+
40
+ `readAccountEmail` reads claude-code's `oauthAccount.emailAddress` (the Claude config
41
+ layout), matching the standard's Claude-first scope. The `email` field itself is
42
+ framework-agnostic (any provider's account can carry one); only the auto-populate
43
+ reader is claude-code-specific today. A non-claude account simply gets no auto-filled
44
+ email (operators can still set one via the route) — no behavior regression, and the
45
+ reader extends per-framework when those logins are wired.
46
+
47
+ ## Failure modes considered
48
+
49
+ - Config home has no `.claude.json` / no `oauthAccount` → `readAccountEmail` returns
50
+ null → poll leaves `email` unchanged (no throw).
51
+ - Operator passes `email` at add time → stored as-is; a later poll overwrites it with
52
+ the real logged-in email (reality wins — the safety property).
53
+ - `update({ email: '' })` clears the email (explicit unset).
54
+
55
+ ## Migration / parity
56
+
57
+ Additive-optional field on an existing store — no migration needed (old records load
58
+ with `email` undefined; the next poll fills it). Ships via dist.