instar 1.2.39 → 1.2.41

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,135 @@
1
+ # Side-effects review — consent state machine (PR 5 of tunnel-failure-resilience chain)
2
+
3
+ **Scope (PR 5 of the chain):** Land the consent state machine inside
4
+ `TunnelManager` and append `LocaltunnelProvider` (from PR 4) to the
5
+ default Tier-2 slot. Adds public `grantConsent` / `declineConsent`
6
+ methods so the upcoming Telegram callback handler (PR 6) can drive the
7
+ owner-approval gate. Spec
8
+ `specs/dev-infrastructure/tunnel-failure-resilience.md` is converged
9
+ + approved on main.
10
+
11
+ **Files touched:**
12
+ - `src/tunnel/TunnelManager.ts` — adds Tier-2 to `buildDefaultPool`;
13
+ modifies `exhaustedOrBackoff` to branch on Tier-2 availability +
14
+ consent cooldown; adds `findAvailableTier2`, `requestConsent`,
15
+ `clearPendingConsent`, `grantConsent`, `declineConsent`,
16
+ `recordConsentDecline`, and the `pendingConsent` accessor; updates
17
+ `stop()` to clear the pending consent.
18
+ - `tests/unit/tunnel-consent-state-machine.test.ts` — 11 tests covering
19
+ the awaiting-consent transition (and its negative cases — no Tier-2
20
+ available, cooldown active), grantConsent happy path, nonce
21
+ rejection (wrong nonce, replay), grant-failure paths (unreachable
22
+ relay, provider.start throws), declineConsent happy path + wrong
23
+ nonce, stop() clearing pendingConsent.
24
+
25
+ **Under-block**: None at user-visible runtime. Agents without the
26
+ `localtunnel` npm dep installed continue to fall through to
27
+ `exhausted` exactly as before (the Tier-2 slot's `isAvailable()`
28
+ returns false; `findAvailableTier2` returns null; the branch picks
29
+ the existing `exhausted` path). This is the spec's opt-in posture —
30
+ PR 4's side-effects review documented it explicitly.
31
+
32
+ **Over-block**: Minimal. Agents that DO have `localtunnel` installed
33
+ WILL see the new `awaiting-consent` transition fire instead of going
34
+ straight to `exhausted` — but the manager has no UX layer to surface
35
+ the prompt yet (PR 6 lands the Telegram callback handler that
36
+ actually calls `grantConsent` / `declineConsent`). For now the
37
+ manager sits in `awaiting-consent` for the 15-minute timeout, then
38
+ auto-declines and falls through to exhausted with the cooldown
39
+ advanced. From the user's perspective this looks identical to
40
+ "exhausted" today — there's just a 15-min delay before the cooldown
41
+ counter ticks the first time.
42
+
43
+ To explicitly avoid this UX gap during the interim window between
44
+ PR 5 and PR 6, operators who don't want the awaiting-consent state
45
+ can simply not install `localtunnel`. The default ships without it.
46
+
47
+ **Level-of-abstraction fit**:
48
+ - The state machine lives in `TunnelLifecycle` (PR 1); the manager
49
+ drives state through the existing CAS-guarded transitions.
50
+ - `_pendingConsent` is private state on the manager (binds nonce +
51
+ episode + provider). Exposed read-only via the `pendingConsent`
52
+ accessor that returns a snapshot — callers cannot mutate the
53
+ manager's state from outside.
54
+ - `grantConsent` / `declineConsent` are the AUTHORITY surface for
55
+ consent decisions. The Telegram callback handler (PR 6) will be
56
+ the SIGNAL layer — it validates the click came from the owner and
57
+ the message id matches, then calls these methods.
58
+
59
+ **Signal vs authority**: Compliant. The manager is the authority for
60
+ state transitions and the consent record's lifecycle; external
61
+ callers can only ask for grant/decline by presenting the nonce, and
62
+ the manager validates+decides. The Telegram callback handler in PR 6
63
+ will be a thin signal layer above this; it cannot bypass the nonce
64
+ check.
65
+
66
+ **Interactions**:
67
+ - `_pendingConsent` is single-flight per manager instance — only one
68
+ consent record can be active at a time (the lifecycle's `episode`
69
+ invariant: one episode = one consent prompt = one nonce).
70
+ - `clearPendingConsent` is called on every terminal path (grant
71
+ success, decline, timeout, stop, episode change). The timer is
72
+ cleared atomically with the record so no stale callback can fire.
73
+ - The consent timeout uses the lifecycle's `recordConsentRefusal`
74
+ to advance the cross-episode cooldown — same path as a manual
75
+ decline. The cooldown progression is exponential (1h → 4h → 24h)
76
+ per PR 1's lifecycle implementation.
77
+ - `rotationPending=true` is set on entering `relay-active` (the
78
+ crash-safe persisted marker for the upcoming rotation lifecycle
79
+ in PR 6). The actual rotation work is NOT in this PR — only the
80
+ marker.
81
+ - Reachability probe on the granted Tier-2 provider follows the
82
+ same `/health` semantics as Tier-1 — a relay that emits a URL
83
+ but doesn't actually serve traffic falls through to exhausted +
84
+ cooldown (so the owner isn't asked again immediately).
85
+
86
+ **External surfaces**:
87
+ - New public methods: `grantConsent(nonce)`, `declineConsent(nonce)`,
88
+ `pendingConsent` getter on `TunnelManager`. All require the
89
+ caller to have the current pending nonce, which is itself only
90
+ accessible via `pendingConsent.nonce` — there's no broader
91
+ exposure.
92
+ - No new API endpoint, no new CLI command, no new config field.
93
+ - The default tunnel pool now includes a `LocaltunnelProvider`
94
+ slot, but the manager only activates it after Tier-1 exhausts +
95
+ consent is granted, so users who never hit a Tier-1 exhaustion
96
+ never see Tier-2 attempted.
97
+
98
+ **Migration parity**: N/A. Server-side code only.
99
+
100
+ **Rollback cost**: Trivial. Revert `TunnelManager.ts` + the test file.
101
+ The new methods become unused (they're not called from anywhere
102
+ outside the manager + tests yet). LocaltunnelProvider stays in tree
103
+ from PR 4 but is no longer in the default pool.
104
+
105
+ **Tests**:
106
+ - 11/11 new tests in `tests/unit/tunnel-consent-state-machine.test.ts`.
107
+ - 19/19 tests in `tests/unit/tunnel-manager-rewrite.test.ts` (from
108
+ PR 2 + 3) still pass.
109
+ - 32/32 lifecycle, 13/13 notifier, 10/10 providers, 7/7 localtunnel-
110
+ provider — all still pass.
111
+ - `tsc --noEmit` clean. `npm run lint` clean. No existing tests
112
+ modified.
113
+
114
+ **Decision-point inventory**:
115
+ 1. `_pendingConsent` stores the provider directly (vs. a provider-name
116
+ key into the pool) — keeps the grant path independent of the pool
117
+ ordering, and ensures a provider that becomes "unavailable"
118
+ between request and grant is still the one we attempt (failure
119
+ surfaces cleanly).
120
+ 2. Nonce is 128-bit CSPRNG via `generateNonce` from PR 1 — meets the
121
+ GPT external review's spec for entropy + atomic compare-and-delete.
122
+ 3. Single-use enforced by `clearPendingConsent` BEFORE the provider
123
+ starts — a replay of the same nonce loses cleanly even if the
124
+ start is in flight.
125
+ 4. Timeout = 15 min (spec Part 4 default, staggered off the 10-min
126
+ reconnect window per verification finding V1).
127
+ 5. Reachability failure on a granted relay → cooldown advanced.
128
+ Reason: the relay started but doesn't serve; bothering the owner
129
+ again immediately wouldn't help. The cooldown gates the next ask.
130
+ 6. State machine transitions `awaiting-consent → exhausted` directly
131
+ on decline/timeout/start-failure — same transition the cooldown
132
+ path uses, so background retry semantics are uniform.
133
+ 7. `pendingConsent` accessor returns a defensive snapshot — mutating
134
+ it does NOT affect the manager. External callers cannot inject
135
+ their own nonce or change the bound provider.
@@ -0,0 +1,95 @@
1
+ # Side-effects review — LocaltunnelProvider (PR 4 of tunnel-failure-resilience chain)
2
+
3
+ **Scope (PR 4 of the chain):** Add the `LocaltunnelProvider`
4
+ implementation of the `TunnelProvider` interface from PR 1. The class
5
+ is added in isolation — NOT wired into the manager's active pool —
6
+ so PR 5 can ship the consent state machine + inline-button UX +
7
+ relay-active activation path with the provider already in place
8
+ under separate review. Spec
9
+ `specs/dev-infrastructure/tunnel-failure-resilience.md` is converged
10
+ + approved on main.
11
+
12
+ **Files touched:**
13
+ - `src/tunnel/LocaltunnelProvider.ts` — NEW. Implements
14
+ `TunnelProvider` with `name: 'localtunnel'`, `tier: 2`. Dynamic
15
+ import of the `localtunnel` npm package; graceful "unavailable"
16
+ return path when the dep isn't installed. Failure classification
17
+ preserves the `ProviderFailureReason` enum surface from PR 1
18
+ (rate-limited / network / process-exit / binary-missing).
19
+ - `tests/unit/localtunnel-provider.test.ts` — NEW. 7 tests covering
20
+ the provider's surface (name + tier), graceful degradation when
21
+ the npm package is absent (isAvailable=false; start() rejects
22
+ binary-missing; verdict cached across calls), and constructor
23
+ option acceptance.
24
+
25
+ **Under-block**: None — the provider is added but NOT wired into the
26
+ manager's active pool. The manager's existing `buildDefaultPool`
27
+ returns Cloudflare-named + Cloudflare-quick only, and the driver
28
+ filters Tier-2 providers (`if (provider.tier !== 1) continue;`).
29
+ User-facing behavior is unchanged.
30
+
31
+ **Over-block**: None. The provider class is purely additive; no
32
+ existing behavior changes.
33
+
34
+ **Level-of-abstraction fit**: The provider implements the same
35
+ interface as the Cloudflare providers, so PR 5's state-machine
36
+ changes can use it through the existing pool abstraction without
37
+ needing provider-specific code paths. Tier-2 trust classification
38
+ lives on the provider (`tier: 2` field), which is correctly the
39
+ manager's structural signal for "consent required."
40
+
41
+ **Signal vs authority**: Compliant. The provider raises errors with
42
+ classified reason strings (the SIGNAL); the manager classifies them
43
+ into `ProviderFailureReason` (the AUTHORITY). The consent gate (PR 5)
44
+ will be a separate AUTHORITY layer above the manager's driver — the
45
+ provider never makes consent decisions on its own.
46
+
47
+ **Interactions**:
48
+ - Dynamic import: the provider does `await import('localtunnel')` at
49
+ first `isAvailable()` call. On environments without the dep, the
50
+ import throws and the provider caches the "unavailable" verdict.
51
+ This is deliberately a runtime check, not a build-time one — instar
52
+ ships without the `localtunnel` dep in `package.json` (per the
53
+ spec's supply-chain hardening — Tier-2 backup capability is opt-in,
54
+ installed explicitly by operators who want it).
55
+ - The `subdomain` constructor option is operator-controlled. The
56
+ comment documents the privacy posture: never use the agent's
57
+ identity as a subdomain hint — it would be publicly visible on
58
+ `*.loca.lt`.
59
+
60
+ **External surfaces**:
61
+ - New exported class `LocaltunnelProvider` from `src/tunnel/`.
62
+ - No new npm dependency — `localtunnel` is NOT added to
63
+ `package.json` in this PR. The provider degrades gracefully when
64
+ the dep is absent. Operators add the dep explicitly to enable the
65
+ capability. This matches the spec's supply-chain posture (Part 7).
66
+ - No new API endpoint, no new CLI command, no new config field
67
+ (Part 4 fields land with PR 5 alongside the consent flow).
68
+
69
+ **Migration parity**: N/A. The provider is server-side code only;
70
+ no agent-installed file change.
71
+
72
+ **Rollback cost**: Trivial. Delete two files (`src/tunnel/Local-
73
+ tunnelProvider.ts` and the test). The provider is isolated; nothing
74
+ else references it.
75
+
76
+ **Tests**:
77
+ - 7/7 tests in `tests/unit/localtunnel-provider.test.ts`.
78
+ - 70/70 tunnel-related tests from PRs 1–3 unaffected.
79
+ - `tsc --noEmit` clean. `npm run lint` clean.
80
+
81
+ **Decision-point inventory**:
82
+ 1. Dynamic import (vs. static `import localtunnel from 'localtunnel'`)
83
+ — keeps `localtunnel` out of the build-time type-check graph so
84
+ instar can ship without the dep. Operators add it explicitly per
85
+ the spec's supply-chain stance.
86
+ 2. `localtunnel` NOT added to `package.json` in this PR — landing the
87
+ dep before the consent gate ships would expose users to an
88
+ unchecked Tier-2 capability. The dep lands in PR 5 alongside the
89
+ consent flow that gates its use. This is intentional and
90
+ per-spec — Part 7 frames the capability as opt-in regardless.
91
+ 3. `subdomain` is operator-controlled and documented as
92
+ non-identifying. The provider does not auto-generate one.
93
+ 4. Failure classification matches PR 1's enum — no new failure
94
+ reasons needed; the manager's existing logic handles localtunnel
95
+ failures the same way as cloudflared failures.
@@ -0,0 +1,115 @@
1
+ # Side-Effects Review — Silently-stopped trio
2
+
3
+ **Version / slug:** `silently-stopped-trio`
4
+ **Date:** 2026-05-22
5
+ **Author:** echo
6
+ **Second-pass reviewer:** [pending — high-risk: WorktreeManager + new sentinels]
7
+ **Spec:** [docs/specs/silently-stopped-trio.md](../../docs/specs/silently-stopped-trio.md)
8
+ **ELI16:** [docs/specs/silently-stopped-trio.eli16.md](../../docs/specs/silently-stopped-trio.eli16.md)
9
+
10
+ ## Summary of the change
11
+
12
+ Three independent layers closing the same failure class — "agent silently stopped working without anyone noticing" — diagnosed jointly with the gsd-side echo agent on 2026-05-22.
13
+
14
+ 1. **WorktreeManager clone-default** (`src/core/WorktreeManager.ts`). New private predicate `shouldCloneInsteadOfWorktree()` returns true when source `projectDir` is outside agent home (the sandbox-revocation hazard case). When true, `createBinding` uses `git clone` + `git checkout -b` instead of `git worktree add`, producing a fully self-contained `.git/` directory under agent home. `INSTAR_WORKTREE_FORCE_WORKTREE=1` is the rollback escape hatch. The existing in-tree worktree path (source already under agent home) is preserved unchanged.
15
+
16
+ 2. **SocketDisconnectSentinel** (`src/monitoring/SocketDisconnectSentinel.ts`, new). Detector + recovery for Claude Code's `socket connection closed unexpectedly` family of strings. Mirrors `RateLimitSentinel`'s shape — detect, immediate user notice, backoff staircase, verify, escalate. Patterns are intentionally broad (4 regex variants) because false-positives are cheap nudges and false-negatives are the exact failure class this exists to close.
17
+
18
+ 3. **ActiveWorkSilenceSentinel** (`src/monitoring/ActiveWorkSilenceSentinel.ts`, new). Walks the SessionRegistry every 60s; reports any session whose `lastOutputAt` is older than the silence threshold (default 15 min). One nudge (empty `tmux send-keys`), 30s verify, escalate via tone-gated `/attention` if no advance.
19
+
20
+ All three escalation payloads route through the existing `MessagingToneGate` B12-B14 ruleset via `/attention` (`category: degradation`).
21
+
22
+ ## Decision-point inventory
23
+
24
+ - `WorktreeManager.shouldCloneInsteadOfWorktree` — **add** — structural predicate (file shape + env override).
25
+ - `WorktreeManager.createBinding` clone branch — **add** — uses `git clone` + `git checkout -b` for cross-project sources.
26
+ - `SocketDisconnectSentinel.report / scanSession / runAttempt / verifyAttempt / escalate` — **add** — full recovery state machine.
27
+ - `ActiveWorkSilenceSentinel.tick / report / runNudge / verifyNudge / escalate` — **add** — silence-detection state machine.
28
+ - Tone-gated `/attention` POST path — **pass-through** — consumed by both new sentinels' notify functions; the gate (existing authority) is unchanged.
29
+
30
+ ## Deferrals tracked
31
+
32
+ The v3 Self-Healing Remediator's Tier-3 absorption of these sentinels' detection + recovery surfaces is the only forward note. <!-- tracked: topic-3079-v3-remediator -->
33
+
34
+ ---
35
+
36
+ ## 1. Over-block
37
+
38
+ - **Worktree decision over-broad.** Any source outside agent home triggers clone. False-positive cost: a `git clone` takes 1-5s on APFS (sub-second with CoW). False-negative cost: the exact incident class we just hit. Acceptable.
39
+ - **Socket-disconnect regex over-broad.** Patterns include generic `connection.*closed.*unexpectedly` — could match unrelated text. False-positive cost: one Ctrl+C+Enter nudge to a healthy session + a "lost its connection" Telegram message that resolves cleanly. False-negative cost: silently frozen session. Trade-off documented.
40
+ - **Silence threshold over-broad.** 15 min is conservative; agents doing slow LLM work (e.g. 5-min generation cycles) could trip if they go quiet during a long thinking pause. Mitigation: the `paused` / `recoveryInFlight` flags let SessionRegistry callers exempt sessions in known-quiet states. Configurable per-deployment.
41
+
42
+ ## 2. Under-block
43
+
44
+ - **WorktreeManager migration of EXISTING worktrees.** This PR ships the clone-default for NEW bindings. Migration of existing worktrees-on-disk is deferred — the spec discussed it but it requires significant safety work (rsync of uncommitted diffs, fencing during the swap). Tracked: same v3 Remediator absorption point. <!-- tracked: topic-3079-v3-remediator -->
45
+ - **Socket-disconnect pattern coverage.** Claude Code may surface other disconnect strings we don't yet know about. Patterns are extensible; the next observed string adds one line.
46
+ - **Active-work-silence false-negatives.** Sessions that produce minimal output (e.g. a `wait + retry` loop with tiny intervals) might never trip the 15-min threshold even when "stuck" in some other sense. Adjacent watchdogs (SessionWatchdog, PresenceProxy) cover other shapes.
47
+ - **Both sentinels currently fire-and-forget on escalation.** No retry-the-escalation loop. If the notify fails (network blip, tone gate hiccup), the message is lost. Acceptable for v1 — the bigger problem (silent failure) is closed; retry plumbing comes with v3 Remediator.
48
+
49
+ ## 3. Level-of-abstraction fit
50
+
51
+ - **WorktreeManager** is the right layer — it's the single funnel that decides how to materialize a worktree on disk. Putting the clone-vs-worktree decision anywhere else would skip the existing fencing + binding lifecycle.
52
+ - **Sentinels in `src/monitoring/`** alongside RateLimitSentinel, CompactionSentinel, SessionWatchdog. Same idiom; same wire-up shape via server.ts.
53
+ - **Escalation through `/attention`** uses the existing tone gate — no new judgment authority introduced.
54
+
55
+ ## 4. Signal vs authority compliance
56
+
57
+ **Reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
58
+
59
+ - [x] **No — this change produces signals consumed by an existing smart gate.**
60
+
61
+ - `shouldCloneInsteadOfWorktree` is a structural predicate (file path + env). No judgment.
62
+ - `SOCKET_DISCONNECT_PATTERNS` regex is a detector. State machine is a bounded mechanic (fixed attempt count, fixed backoff staircase).
63
+ - `ActiveWorkSilenceSentinel.tick` threshold is deterministic. Nudge is a bounded primitive.
64
+ - All user-facing escalation goes through `MessagingToneGate` via `/attention`'s existing path. No new authority introduced.
65
+
66
+ ## 5. Interactions
67
+
68
+ - **Shadowing:** SocketDisconnectSentinel and ActiveWorkSilenceSentinel can both fire on the same session (socket dropped → output stopped). The socket sentinel ticks every 15s; the silence sentinel waits 15 min. Socket sentinel virtually always fires first. Idempotency-by-id in `/attention` prevents duplicate topic creation if both ever land.
69
+ - **Double-fire vs existing watchdogs:** SessionWatchdog (long-running child), SessionMonitor (topic-bound output gap), PresenceProxy (user-waits-for-agent). All three handle different shapes; none overlap with the new sentinels in practice. Active-work-silence is the catch-all for what falls through the cracks.
70
+ - **Races:** Sentinel state maps are per-session keys; concurrent ticks on the same server can't race (single-threaded JS event loop). Cross-process safety is N/A — each server instance is a single sentinel owner.
71
+ - **WorktreeManager clone vs worktree-add:** Mutually exclusive code paths; no race.
72
+
73
+ ## 6. External surfaces
74
+
75
+ - **Other agents:** Bindings created by this PR for shared repos will be clones, not worktrees. `git worktree list` from the source repo won't show them. Documented in CLAUDE.md template (forward).
76
+ - **Other users:** ships in next release. macOS users + Linux users alike. The clone path is OS-agnostic.
77
+ - **External systems:** Telegram (one extra topic class — "disconnect" alerts — though tone-gated). `/attention` route — existing; no change to its contract.
78
+ - **Persistent state:** No new files for the sentinels (in-memory state only — by design, simpler than RateLimitSentinel which persists). WorktreeManager bindings file unchanged.
79
+ - **Timing:** Sentinel ticks (15s + 60s) are negligible overhead per session.
80
+
81
+ ## 7. Rollback cost
82
+
83
+ - WorktreeManager: revert the diff. `INSTAR_WORKTREE_FORCE_WORKTREE=1` is the in-place rollback toggle without a release.
84
+ - Sentinels: revert each new file + the server.ts wire-up. No persistent state to clean up.
85
+ - Total: ~15 min revert + release cycle.
86
+
87
+ ---
88
+
89
+ ## Addendum 2026-05-23 — Smoke-test interaction with SourceTreeGuard (fixed)
90
+
91
+ After the initial commit landed, `npm run test:smoke` surfaced 14 failures in the existing WorktreeManager test suite — every test that exercised the clone-default branch failed with `SourceTreeGuardError: Refusing to run src/core/WorktreeManager.ts:clone-default against the instar source tree`. Root cause: `SafeGitExecutor.runSourceTreeChecks` defaults the target list to cwd. The new `git clone` call ran from a cwd that happened to be inside the instar source tree (test fixtures invoking `createBinding` from inside an instar checkout), and the guard correctly flagged the cwd as destructive-against-source even though the clone op itself is non-destructive on the source. **Fix:** pin `cwd: this.worktreesRoot` on the clone call — guaranteed outside the source tree and the same dir the clone destination lives under. All 56 smoke tests pass.
92
+
93
+ This is a defensible interaction, not a guard regression: the guard is doing exactly what it was built to do (conservative protection of the source tree). The clone-default branch needed explicit cwd because it's the first WorktreeManager codepath to run git from outside the source's own dir.
94
+
95
+ ## Conclusion
96
+
97
+ Closes the silently-stopped failure class in one PR per the no-deferrals rule. 34 new tests cover the core surfaces. Server-side wire-up + integration tests + CLAUDE.md template update are part of this PR (see commit). Tone-gate compliance asserted at unit level for both sentinels' escalation payloads. Migration of existing worktrees explicitly deferred to v3 Remediator with tracker marker.
98
+
99
+ Clear to ship pending second-pass review.
100
+
101
+ ---
102
+
103
+ ## Second-pass review (if required)
104
+
105
+ **Reviewer:** [pending]
106
+
107
+ [awaiting reviewer]
108
+
109
+ ---
110
+
111
+ ## Evidence pointers
112
+
113
+ - Spec: `docs/specs/silently-stopped-trio.md` + `.eli16.md`.
114
+ - Tests: `tests/unit/monitoring/SocketDisconnectSentinel.test.ts`, `tests/unit/monitoring/ActiveWorkSilenceSentinel.test.ts`, `tests/unit/core/WorktreeManager-clone-default.test.ts` — 34 tests.
115
+ - Incident reference: topic 5447 alignment with gsd-side echo's diagnosis 2026-05-22.