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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +160 -6
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +29 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CredentialIdentityOracle.d.ts +3 -2
- package/dist/core/CredentialIdentityOracle.d.ts.map +1 -1
- package/dist/core/CredentialIdentityOracle.js +10 -4
- package/dist/core/CredentialIdentityOracle.js.map +1 -1
- package/dist/core/InFlightSyncOpMarker.d.ts +29 -0
- package/dist/core/InFlightSyncOpMarker.d.ts.map +1 -0
- package/dist/core/InFlightSyncOpMarker.js +149 -0
- package/dist/core/InFlightSyncOpMarker.js.map +1 -0
- package/dist/core/OAuthRefresher.d.ts +16 -0
- package/dist/core/OAuthRefresher.d.ts.map +1 -1
- package/dist/core/OAuthRefresher.js +64 -5
- package/dist/core/OAuthRefresher.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +16 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +56 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/QuotaPoller.d.ts.map +1 -1
- package/dist/core/QuotaPoller.js +2 -0
- package/dist/core/QuotaPoller.js.map +1 -1
- package/dist/core/SessionManager.d.ts +146 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +498 -144
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SleepWakeDetector.d.ts +33 -0
- package/dist/core/SleepWakeDetector.d.ts.map +1 -1
- package/dist/core/SleepWakeDetector.js +53 -0
- package/dist/core/SleepWakeDetector.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +23 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/types.d.ts +52 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/lifeline/ServerSupervisor.d.ts +42 -0
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
- package/dist/lifeline/ServerSupervisor.js +79 -4
- package/dist/lifeline/ServerSupervisor.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +8 -0
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/monitoring/DegradedTmuxGuard.d.ts +161 -0
- package/dist/monitoring/DegradedTmuxGuard.d.ts.map +1 -0
- package/dist/monitoring/DegradedTmuxGuard.js +277 -0
- package/dist/monitoring/DegradedTmuxGuard.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +18 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +13 -5
- package/dist/server/routes.js.map +1 -1
- package/package.json +2 -2
- package/scripts/lint-no-direct-destructive.js +14 -0
- package/scripts/lint-sync-subprocess-chokepoint.js +208 -0
- package/scripts/sync-subprocess-chokepoint-baseline.json +148 -0
- package/src/data/builtin-manifest.json +65 -65
- package/upgrades/1.3.643.md +66 -0
- package/upgrades/1.3.644.md +45 -0
- package/upgrades/eli16/credential-keychain-async-read.md +44 -0
- package/upgrades/side-effects/credential-keychain-async-read.md +60 -0
- 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.
|