instar 1.3.642 → 1.3.644

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 (66) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +160 -6
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +29 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/CredentialIdentityOracle.d.ts +3 -2
  8. package/dist/core/CredentialIdentityOracle.d.ts.map +1 -1
  9. package/dist/core/CredentialIdentityOracle.js +10 -4
  10. package/dist/core/CredentialIdentityOracle.js.map +1 -1
  11. package/dist/core/InFlightSyncOpMarker.d.ts +29 -0
  12. package/dist/core/InFlightSyncOpMarker.d.ts.map +1 -0
  13. package/dist/core/InFlightSyncOpMarker.js +149 -0
  14. package/dist/core/InFlightSyncOpMarker.js.map +1 -0
  15. package/dist/core/OAuthRefresher.d.ts +16 -0
  16. package/dist/core/OAuthRefresher.d.ts.map +1 -1
  17. package/dist/core/OAuthRefresher.js +64 -5
  18. package/dist/core/OAuthRefresher.js.map +1 -1
  19. package/dist/core/PostUpdateMigrator.d.ts +16 -0
  20. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  21. package/dist/core/PostUpdateMigrator.js +56 -0
  22. package/dist/core/PostUpdateMigrator.js.map +1 -1
  23. package/dist/core/QuotaPoller.d.ts.map +1 -1
  24. package/dist/core/QuotaPoller.js +2 -0
  25. package/dist/core/QuotaPoller.js.map +1 -1
  26. package/dist/core/SessionManager.d.ts +146 -1
  27. package/dist/core/SessionManager.d.ts.map +1 -1
  28. package/dist/core/SessionManager.js +498 -144
  29. package/dist/core/SessionManager.js.map +1 -1
  30. package/dist/core/SleepWakeDetector.d.ts +33 -0
  31. package/dist/core/SleepWakeDetector.d.ts.map +1 -1
  32. package/dist/core/SleepWakeDetector.js +53 -0
  33. package/dist/core/SleepWakeDetector.js.map +1 -1
  34. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  35. package/dist/core/devGatedFeatures.js +23 -0
  36. package/dist/core/devGatedFeatures.js.map +1 -1
  37. package/dist/core/types.d.ts +52 -0
  38. package/dist/core/types.d.ts.map +1 -1
  39. package/dist/core/types.js.map +1 -1
  40. package/dist/lifeline/ServerSupervisor.d.ts +42 -0
  41. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  42. package/dist/lifeline/ServerSupervisor.js +79 -4
  43. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  44. package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
  45. package/dist/lifeline/TelegramLifeline.js +8 -0
  46. package/dist/lifeline/TelegramLifeline.js.map +1 -1
  47. package/dist/monitoring/DegradedTmuxGuard.d.ts +161 -0
  48. package/dist/monitoring/DegradedTmuxGuard.d.ts.map +1 -0
  49. package/dist/monitoring/DegradedTmuxGuard.js +277 -0
  50. package/dist/monitoring/DegradedTmuxGuard.js.map +1 -0
  51. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  52. package/dist/monitoring/guardManifest.js +18 -0
  53. package/dist/monitoring/guardManifest.js.map +1 -1
  54. package/dist/server/routes.d.ts.map +1 -1
  55. package/dist/server/routes.js +13 -5
  56. package/dist/server/routes.js.map +1 -1
  57. package/package.json +2 -2
  58. package/scripts/lint-no-direct-destructive.js +14 -0
  59. package/scripts/lint-sync-subprocess-chokepoint.js +208 -0
  60. package/scripts/sync-subprocess-chokepoint-baseline.json +148 -0
  61. package/src/data/builtin-manifest.json +65 -65
  62. package/upgrades/1.3.643.md +66 -0
  63. package/upgrades/1.3.644.md +45 -0
  64. package/upgrades/eli16/credential-keychain-async-read.md +44 -0
  65. package/upgrades/side-effects/credential-keychain-async-read.md +60 -0
  66. package/upgrades/side-effects/tmux-event-loop-resilience.md +86 -0
@@ -0,0 +1,45 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The macOS keychain credential read (`OAuthRefresher.defaultCredentialStore.read`) was a synchronous
9
+ `execFileSync('security', …)` with no timeout, run on the server's single event loop. The
10
+ credential-audit loop reads all of an agent's claude account slots sequentially through it, so when
11
+ several co-resident agents contend on one `securityd`, each read stalled seconds and the loop froze
12
+ 4–13s every ~30–65s — dropping the dashboard websocket (the "Disconnected" flapping) and false-firing
13
+ the SleepWakeDetector (a ~0-CPU I/O-wait block the CPU check can't see). This is the SECOND
14
+ event-loop blocker behind the dashboard flapping; the tmux Event-Loop Resilience fix (v1.3.643)
15
+ removed the first (the tmux calls), this removes the keychain one.
16
+
17
+ The read now has an async, off-loop variant (`readAsync`, promisified `execFile`, 3s timeout) used on
18
+ the audit hot path (`CredentialIdentityOracle.resolveSlotTenant` awaits it, so each slot read yields
19
+ the loop), plus a `timeout: 3000` on the remaining sync read/write as a bound for any non-hot-path
20
+ caller. The sibling `CredentialProvider.ts` already set a keychain timeout; OAuthRefresher was missed.
21
+
22
+ ## What to Tell Your User
23
+
24
+ If your dashboard kept showing "Disconnected" even after the tmux fix, this is the rest of the cause:
25
+ a slow shared macOS keychain was freezing the server loop. After this update the dashboard stays
26
+ connected under multi-agent load, and the agent stops occasionally misreporting itself as having gone
27
+ to sleep.
28
+
29
+ ## Summary of New Capabilities
30
+
31
+ - Async, timeout-bounded keychain credential read (`CredentialStore.readAsync`, optional + backward
32
+ compatible); the credential-audit hot path reads off the event loop.
33
+ - A 3s timeout on the remaining synchronous keychain read/write — an unbounded `securityd` stall can
34
+ no longer wedge the loop (a timeout maps to needs-reauth, retried next cycle).
35
+ - Removes the second event-loop blocker behind the dashboard "disconnected" flapping + the
36
+ SleepWakeDetector false-wakes.
37
+
38
+ ## Evidence
39
+
40
+ Root cause diagnosed by a live `/usr/bin/sample` of the running server during a freeze (1567/1567
41
+ main-thread samples in `SyncProcessRunner::Spawn → kevent` with `com.apple.security` loaded) +
42
+ correlation probes catching freezes ~64s apart. Fix covered by a new async-read test suite (parity
43
+ with the sync read, fallback-to-sync for stores without `readAsync`, and a deferred-promise test
44
+ proving `resolveSlotTenant` awaits the async path); 83/83 tests across the credential suites green,
45
+ tsc clean, no-silent-fallbacks ratchet + sync-subprocess chokepoint lint green.
@@ -0,0 +1,44 @@
1
+ # Credential keychain read — async + timeout-bounded — ELI16
2
+
3
+ ## What this is
4
+
5
+ Your agent's server has a single main thread that handles everything — answering the dashboard,
6
+ replying to messages, running every background check. If any one operation on that thread stops to
7
+ wait for something slow, *everything* waits. That's a "freeze."
8
+
9
+ This fixes a freeze. To know which account a credential belongs to, the server reads it from the
10
+ macOS keychain by running the `security` command — and it did that the BLOCKING way (`execFileSync`)
11
+ with no time limit. Normally a keychain read is instant. But you run several agents on one Mac, and
12
+ they all go through one macOS keychain service (`securityd`); when they pile up, each read can take
13
+ several seconds. The server checks all 5 of your Claude accounts one after another, so the freezes
14
+ added up to **4–13 seconds, roughly every 30–60 seconds**.
15
+
16
+ That's what was actually still breaking the dashboard. The earlier tmux fix (v1.3.643) correctly
17
+ removed the *tmux* version of this same problem, but there were TWO blocking calls, and this is the
18
+ second one. During each freeze the dashboard's live connection drops (you saw "Disconnected"), and
19
+ the freeze even looked to the agent like the laptop had gone to sleep, so it kept false-alarming a
20
+ "wake."
21
+
22
+ ## What already exists
23
+
24
+ The agent already has the right pattern in a sibling file (`CredentialProvider.ts`) — it reads the
25
+ keychain with a 10-second time limit. This read path just never got that treatment. And the
26
+ credential-audit loop that triggers the reads is already `async`, so switching it to a non-blocking
27
+ read is clean.
28
+
29
+ ## What's new
30
+
31
+ - The keychain read now has an **async, off-the-main-thread version** (`readAsync`) that the
32
+ audit loop uses — so while one account's keychain read is in flight, the main thread is free to
33
+ answer the dashboard, messages, and everything else. The freeze is gone, regardless of how slow
34
+ `securityd` gets or how many accounts there are.
35
+ - Both the old synchronous read and write now also carry a **3-second time limit**, so even a
36
+ caller that still reads synchronously can't wedge forever (it just falls back to "needs re-auth"
37
+ and retries next cycle — exactly what it did before for a missing entry).
38
+ - It's backward-compatible: the new async method is optional, so nothing else has to change.
39
+
40
+ ## What you need to decide
41
+
42
+ Nothing. It's a self-contained, low-risk fix on one read path, fully covered by tests. Once it ships
43
+ and your agent updates, the dashboard should stay connected under load instead of flapping, and the
44
+ spurious "wake" alarms stop.
@@ -0,0 +1,60 @@
1
+ # Side-Effects Review — Credential keychain read: async + timeout-bounded
2
+
3
+ **Slug:** `credential-keychain-async-read` · **Tier:** 1 (focused bug fix, no spec; rigorous
4
+ root-cause diagnosis via a live `/usr/bin/sample` of the running server). Parent principle:
5
+ **Structure beats Willpower** — the same "never block the event loop" guarantee the tmux
6
+ Event-Loop Resilience fix made structural, applied to the second blocking call site it didn't cover.
7
+
8
+ ## Summary of the change
9
+
10
+ The macOS keychain credential read in `OAuthRefresher.ts` (`defaultCredentialStore.read`) was a
11
+ SYNCHRONOUS `execFileSync('security', …)` with NO timeout, on the event loop. The credential-audit
12
+ hot path — `CredentialLocationLedger.auditIdentities()` → loops sequentially over all 5 claude
13
+ account slots → `await CredentialIdentityOracle.resolveSlotTenant()` → `readClaudeOauth()` → that
14
+ sync `security` spawn — froze the whole event loop 4–13s every ~30–65s under multi-agent `securityd`
15
+ contention, dropping the dashboard websocket (user-visible flapping) and false-firing the
16
+ SleepWakeDetector (~0-CPU I/O-wait). This adds an async `readAsync` (promisified `execFile`,
17
+ 3s-timeout) used on the hot path (`resolveSlotTenant` awaits it → each slot read yields the loop),
18
+ plus a `timeout: 3000` on the remaining sync `read`/`write` for any non-hot-path caller. The sibling
19
+ `monitoring/CredentialProvider.ts` already set `timeout:10000`; OAuthRefresher was simply missed.
20
+
21
+ ## 1. Behavioral equivalence / correctness
22
+
23
+ `readAsync` mirrors the sync read's args + null-on-error semantics exactly; `readClaudeOauthAsync`
24
+ reuses the identical parse and falls back to the sync `read` for any store without `readAsync`
25
+ (the interface method is OPTIONAL, so all existing `CredentialStore` mocks compile unchanged). The
26
+ only consumer switched to the async path is `resolveSlotTenant` (already an `async` method awaited by
27
+ the audit loop) — verified nothing after the read assumes synchrony (only `oauth?.accessToken` is
28
+ used). 83/83 tests across the credential suites green, including a deferred-promise test proving
29
+ `resolveSlotTenant` does not resolve until the async read resolves (it genuinely awaits the async
30
+ path, not the sync read).
31
+
32
+ ## 2. Failure modes / fail-safe
33
+
34
+ Every error path returns `null` (unreadable → caller falls to needs-reauth, retried next cycle) —
35
+ identical to the prior sync behavior. A 3s timeout now bounds a wedged `securityd` instead of an
36
+ unbounded block: a timeout maps to `null` (needs-reauth) exactly like a missing entry. The async
37
+ `execFile` buffers stdout/stderr (the `stdio` option is dropped on the promisified overload — stderr
38
+ is captured-then-ignored, matching the sync read's `stdio:['ignore','pipe','ignore']`).
39
+
40
+ ## 3. Blast radius
41
+
42
+ Two files of behavior (`OAuthRefresher.ts`, `CredentialIdentityOracle.ts`) + a one-line comment in
43
+ `QuotaPoller.ts`. `QuotaPoller.defaultTokenResolver` stays sync by design (a single periodic read,
44
+ now timeout-bounded — making it async would ripple through `SubscriptionAccount` token resolution
45
+ for no benefit). No write-path semantics change (only the timeout bound is added). No credential
46
+ VALUE ever leaves the funnel; no new external surface.
47
+
48
+ ## 4. Interactions
49
+
50
+ Complements the tmux Event-Loop Resilience fix (v1.3.643): that fix took the SYNC TMUX calls off the
51
+ loop; this takes the SYNC KEYCHAIN call off the loop. Together they remove the two periodic
52
+ event-loop blockers that caused the dashboard "disconnected" flapping. The async read also removes
53
+ the SleepWakeDetector false-wakes (no ~0-CPU block → no misread as sleep), so the spurious
54
+ wake-recovery cascade this triggered stops.
55
+
56
+ ## 5. Rollback
57
+
58
+ Revert the 2 source files. The change is additive (a new optional interface method + an async
59
+ function) plus a one-line switch in `resolveSlotTenant`; the sync `read` remains the fallback, so a
60
+ partial revert (keeping only the `timeout: 3000` bound) is also safe.
@@ -0,0 +1,86 @@
1
+ # Side-Effects Review — tmux Event-Loop Resilience (Increment 1)
2
+
3
+ **Slug:** `tmux-event-loop-resilience` · **Tier:** 2 (converged + approved spec
4
+ `docs/specs/tmux-event-loop-resilience.md`). Touches `PostUpdateMigrator` (a
5
+ config-strip migration), so the tier signal flags `belowFloor` — accepted: the
6
+ PostUpdateMigrator touch is a dev-gate cleanup strip (removes a stale persisted
7
+ `enabled:false` so the dev-gate resolves live-on-dev), fully reversible, no
8
+ external surface.
9
+
10
+ ## Summary of the change
11
+
12
+ The server's frequent synchronous `tmux` calls blocked the single event loop; a
13
+ slow shared tmux server froze the loop ~15s, dropping the dashboard websocket and
14
+ getting misread as host sleep. Increment 1: (A) an async, timeout-bounded tmux
15
+ hot path with cache-served dashboard/session reads; (B) an in-flight-sync-op
16
+ marker as the block-vs-sleep discriminator (in-process for SleepWakeDetector,
17
+ cross-process via a mirror file for ServerSupervisor); (C) a signal-only
18
+ `DegradedTmuxGuard` that surfaces a persistently slow tmux without ever killing
19
+ it. All three are dev-gated: LIVE on a development agent, DARK on the fleet.
20
+
21
+ ## 1. Over-block / false signal
22
+
23
+ `DegradedTmuxGuard` is signal-only — it raises ONE deduped attention item and
24
+ never kills, restarts, or reaps anything, so a false degradation signal costs at
25
+ most one surfaced notice. It is load-gated (suppressed when per-core load is high
26
+ — that is expected slowness, not a tmux fault) and requires N-cycle corroboration
27
+ (a single hiccup never raises an episode), with episode dedup + age-escalation.
28
+ The in-flight marker can only SUPPRESS a wake / DEFER a restart when affirmatively
29
+ present, in-flight, and non-stale — a false positive there fails toward the SAFE
30
+ direction (keep the session alive / let a genuinely-dead server restart).
31
+
32
+ ## 2. Under-block / missed signal
33
+
34
+ A real freeze the marker doesn't cover is still caught: SleepWakeDetector's
35
+ existing cpuBlockBusyRatio path (CPU-spin blocks) is unchanged, and the marker
36
+ adds the ~0-CPU I/O-wait case #1240 couldn't see. The marker self-heals via a
37
+ 2×timeout TTL so a leaked depth can never permanently blind sleep detection. The
38
+ supervisor defer is hard-capped (`starvationRestartThreshold`) so a stuck
39
+ in-flight marker can never wedge the restart path forever — it gives up loudly.
40
+
41
+ ## 3. Blast radius / fail-open
42
+
43
+ Every failure path fails toward SAFE and is non-silent (no-silent-fallbacks
44
+ ratchet enforced): a tmux call that times out maps to INDETERMINATE, never to
45
+ "absent" — `isSessionAliveAsync` returns `'indeterminate'` (never `false`) on a
46
+ timeout, so a slow tmux can NEVER cause a live session to be reaped (the
47
+ line-2352 regression the tri-state guards). The cross-process marker reader is
48
+ fail-OPEN: an absent/unparseable mirror ⇒ null ⇒ the supervisor proceeds to
49
+ restart a genuinely dead server. The mirror writer is best-effort: an unwritable
50
+ mirror never breaks a tmux call. `DegradedTmuxGuard` uses a fixed-capacity
51
+ modulo-write ring (Bounded Accumulation — 10,000-sample burst-invariant test
52
+ proves the ring length never exceeds windowSize), so it cannot grow memory under
53
+ a flood.
54
+
55
+ ## 4. Signal vs authority
56
+
57
+ The only blocking authority introduced is the supervisor restart-DEFER, and it is
58
+ bounded (hard cap + TTL + fail-open) and additive to the existing
59
+ `deferRestartForCpuStarvation` (the `||` keeps the CPU side-effect — verified by
60
+ test). The wake-handler amplifier guard short-circuits a marker-covered wake and
61
+ bounds the cascade's tmux re-validation (async 9000+SIGKILL); it never silences a
62
+ genuine recovery. `DegradedTmuxGuard` holds zero authority — it observes and
63
+ surfaces only.
64
+
65
+ ## 5. Interactions
66
+
67
+ Reuses the existing `'event-loop-block'` WakeSuppressionReason + StallEvent (no
68
+ new telemetry shape). The async hot-path twins coexist with the legacy sync
69
+ methods behind `tmuxAsyncEnabled` (off ⇒ byte-identical legacy behavior — the e2e
70
+ observable-equivalence test). `getCachedRunningSessions()` is the cache the
71
+ request routes (GET /status et al.) now read; the non-request consumers
72
+ (JobScheduler, WebSocketManager, AutoUpdater) intentionally keep the live read
73
+ (documented residual, Increment 2 territory). Per-agent socket isolation is
74
+ explicitly OUT of Increment 1 (Increment 2).
75
+
76
+ ## 6. Rollback
77
+
78
+ Each of the three flags is independently disable-able in `.instar/config.json`
79
+ (`monitoring.tmuxResilience.asyncHotPath.enabled`,
80
+ `monitoring.tmuxResilience.inFlightMarker.enabled`,
81
+ `monitoring.degradedTmuxGuard.enabled`) — set to `false` to revert that layer to
82
+ the pre-change behavior. The async hot path falls back to the sync path; the
83
+ marker branch goes inert; the guard stops observing. The migration strips only a
84
+ default-shaped persisted `false`, never an explicit operator `true`/`false`.
85
+ Config defaults OMIT `enabled` so the dev-gate (resolveDevAgentGate) governs:
86
+ fleet = dark, dev = live.