instar 1.2.34 → 1.2.36

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 (33) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +20 -67
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/tunnel/CloudflareNamedProvider.d.ts +51 -0
  5. package/dist/tunnel/CloudflareNamedProvider.d.ts.map +1 -0
  6. package/dist/tunnel/CloudflareNamedProvider.js +258 -0
  7. package/dist/tunnel/CloudflareNamedProvider.js.map +1 -0
  8. package/dist/tunnel/CloudflareQuickProvider.d.ts +48 -0
  9. package/dist/tunnel/CloudflareQuickProvider.d.ts.map +1 -0
  10. package/dist/tunnel/CloudflareQuickProvider.js +201 -0
  11. package/dist/tunnel/CloudflareQuickProvider.js.map +1 -0
  12. package/dist/tunnel/TunnelLifecycle.d.ts +190 -0
  13. package/dist/tunnel/TunnelLifecycle.d.ts.map +1 -0
  14. package/dist/tunnel/TunnelLifecycle.js +222 -0
  15. package/dist/tunnel/TunnelLifecycle.js.map +1 -0
  16. package/dist/tunnel/TunnelManager.d.ts +99 -62
  17. package/dist/tunnel/TunnelManager.d.ts.map +1 -1
  18. package/dist/tunnel/TunnelManager.js +340 -395
  19. package/dist/tunnel/TunnelManager.js.map +1 -1
  20. package/dist/tunnel/TunnelNotifier.d.ts +110 -0
  21. package/dist/tunnel/TunnelNotifier.d.ts.map +1 -0
  22. package/dist/tunnel/TunnelNotifier.js +268 -0
  23. package/dist/tunnel/TunnelNotifier.js.map +1 -0
  24. package/dist/tunnel/TunnelProvider.d.ts +80 -0
  25. package/dist/tunnel/TunnelProvider.d.ts.map +1 -0
  26. package/dist/tunnel/TunnelProvider.js +28 -0
  27. package/dist/tunnel/TunnelProvider.js.map +1 -0
  28. package/package.json +1 -1
  29. package/src/data/builtin-manifest.json +2 -2
  30. package/upgrades/1.2.35.md +96 -0
  31. package/upgrades/1.2.36.md +111 -0
  32. package/upgrades/side-effects/tunnel-failure-resilience.md +118 -0
  33. package/upgrades/side-effects/tunnel-manager-rewrite.md +129 -0
@@ -0,0 +1,96 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+ <!-- patch = foundation layer for an upcoming feature; no user-visible behavior change yet -->
5
+
6
+ ## What Changed
7
+
8
+ **feat(tunnel): foundation modules for the tunnel-failure-resilience feature.**
9
+
10
+ This release lands the foundation layer that the upcoming tunnel-failure-
11
+ resilience feature is built on. Spec
12
+ `specs/dev-infrastructure/tunnel-failure-resilience.md` is converged +
13
+ approved (4 review iterations, 41 material findings folded in across
14
+ internal multi-angle review + GPT external review + GPT verification);
15
+ this PR is the first of a chain landing the implementation.
16
+
17
+ What this PR adds (additive only — no existing behavior changes yet):
18
+
19
+ 1. **`TunnelProvider` interface** (`src/tunnel/TunnelProvider.ts`) — a
20
+ provider abstraction that future tunnel providers (localtunnel, bore)
21
+ will implement alongside the two extracted Cloudflare providers.
22
+ Defines `ProviderTier`, `ProviderName`, `ProviderFailureReason`.
23
+
24
+ 2. **`CloudflareQuickProvider`** (`src/tunnel/CloudflareQuickProvider.ts`)
25
+ — Tier-1 zero-config Cloudflare quick-tunnel implementation
26
+ extracted from the original `TunnelManager.startQuickTunnel`. Owns
27
+ ONLY spawn + URL emission + teardown; retry/reconnect ownership
28
+ moves to the manager in a subsequent PR per the spec's single-owner
29
+ mandate. Forceful stop with SIGINT→SIGKILL escalation preserved.
30
+
31
+ 3. **`CloudflareNamedProvider`** (`src/tunnel/CloudflareNamedProvider.ts`)
32
+ — Tier-1 persistent Cloudflare named-tunnel implementation
33
+ (token-auth and config-file-auth modes). `isAvailable()` returns
34
+ false when neither token nor configFile is configured.
35
+
36
+ 4. **`TunnelLifecycle`** (`src/tunnel/TunnelLifecycle.ts`) — the single-
37
+ writer state machine that will own the tunnel lifecycle once the
38
+ manager is rewritten. Compare-and-swap `transition(expectedFrom, to)`
39
+ guard rejects losing concurrent writes (the error+exit double-handler
40
+ race fix). Monotonic transition epoch drives notification dedup.
41
+ Episode model + cross-episode consent cooldown + rotation-pending
42
+ flag (the boot-recovery mechanism for crash-safe credential
43
+ rotation) all present and tested.
44
+
45
+ 5. **`TunnelNotifier`** (`src/tunnel/TunnelNotifier.ts`) — two-channel
46
+ routing that, once wired in, will route group-topic status text
47
+ (no credentials, ever) separately from owner-DM credential delivery
48
+ (the only credential-bearing channel). Class-based throttling:
49
+ `action-required` (consent prompts) never throttled within an
50
+ episode; `state-change` keyed per `(state, channel)` within a 15-min
51
+ window; `noise` (flap collapse) at most once per episode.
52
+
53
+ The existing `TunnelManager.ts` and `server.ts` tunnel block are
54
+ UNTOUCHED in this PR — current tunnel behavior is identical. Wiring
55
+ the new modules into the manager (and retiring the legacy `server.ts`
56
+ retry ladders) is the next PR in this chain.
57
+
58
+ ## Evidence
59
+
60
+ 54 new unit tests pass:
61
+
62
+ - `tests/unit/tunnel-providers.test.ts` — 10 tests covering provider
63
+ interface assertions, `isAvailable()` semantics on token/configFile
64
+ configurations, start-rejection paths on missing prerequisites.
65
+ - `tests/unit/tunnel-lifecycle.test.ts` — 32 tests covering the CAS
66
+ guard (rejects losing transitions without mutating state),
67
+ monotonic-epoch increment, valid-transition map enforcement,
68
+ episode lifecycle, exponential consent-cooldown back-off,
69
+ rotation-pending persistence, failure-reason classification,
70
+ CSPRNG nonce generation.
71
+ - `tests/unit/tunnel-notifier.test.ts` — 12 tests covering the
72
+ channel-separation invariant (credentials never appear in group
73
+ messages), epoch dedup, action-required-never-throttled, state-
74
+ change per-(state, channel) throttling, flap-collapse emits
75
+ exactly once per episode, sink-error swallowing.
76
+
77
+ All existing tests still pass. `tsc --noEmit` clean. `npm run lint`
78
+ clean.
79
+
80
+ ## What to Tell Your User
81
+
82
+ This release adds internal scaffolding for a tunnel-resilience feature
83
+ coming over the next few releases. You will not notice any behavior
84
+ change yet — your dashboard link comes up the same way as before. The
85
+ upcoming releases will add failure notification to the Dashboard topic,
86
+ backup tunnel providers (asked about in DM before use), and automatic
87
+ recovery when Cloudflare comes back online. Each piece ships in its
88
+ own release in the chain that starts here.
89
+
90
+ ## Summary of New Capabilities
91
+
92
+ | Capability | How to Use |
93
+ |-----------|-----------|
94
+ | Provider abstraction internals | Internal — used by the upcoming manager rewrite |
95
+ | Single-writer state machine internals | Internal — used by the upcoming manager rewrite |
96
+ | Two-channel notifier internals | Internal — wired up in the next release |
@@ -0,0 +1,111 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+ <!-- patch = TunnelManager rewrite atop the foundation modules; no public API change -->
5
+
6
+ ## What Changed
7
+
8
+ **feat(tunnel): TunnelManager rewrite — drive providers + lifecycle + notifier through a single owner.**
9
+
10
+ This release lands PR 2 of the tunnel-failure-resilience chain
11
+ (PR 1 was the foundation: provider abstraction + state machine + notifier).
12
+ It rewrites `TunnelManager.ts` to drive the foundation modules and
13
+ retires the duplicate retry machinery that previously lived in
14
+ `server.ts`.
15
+
16
+ The public API surface is preserved exactly — `start()`, `stop()`,
17
+ `forceStop()`, `enableAutoReconnect()`, `disableAutoReconnect()`,
18
+ `getExternalUrl()`, plus the `url` / `isRunning` / `state` accessors
19
+ all still work as before. Callers (the SleepWake handler, the
20
+ `server.ts` tunnel wiring) don't need changes.
21
+
22
+ What's different inside:
23
+
24
+ - The manager builds a Tier-1 provider pool from config (the
25
+ `CloudflareNamedProvider` if a token or configFile is set, then the
26
+ `CloudflareQuickProvider`) and drives them in order.
27
+ - `start()` runs ONE round of provider attempts; if all fail, it
28
+ rejects (matching the legacy semantics) and the backoff retry runs
29
+ entirely in the background — `start()` does not block the caller
30
+ for the full exponential window.
31
+ - A post-start HTTP probe through the public URL confirms the link
32
+ actually serves traffic before the manager declares `active`. A
33
+ provider whose URL emits but does not respond is torn down and the
34
+ next provider tried.
35
+ - After the bounded startup-reconnect ladder exhausts (10 attempts
36
+ with exponential backoff up to 5 min), the manager schedules a
37
+ low-frequency 15-minute background probe until a provider connects.
38
+ Placeholder; replaced by the spec's N-consecutive-success
39
+ stability-gated probe in a later PR.
40
+ - The single-writer CAS-guarded lifecycle state machine from PR 1 is
41
+ the source of truth for tunnel state. The error+exit double-handler
42
+ race that the concurrency reviewer surfaced cannot cause a
43
+ double-advance through the provider pool.
44
+ - The `tunnel.json` state file now includes lifecycle snapshot fields
45
+ (`lastState`, `activeProvider`, `rotationPending`, `consentCooldown`,
46
+ `episode`) alongside the legacy `lastUrl`. Boot recovery reads the
47
+ snapshot to restore the rotation-pending flag and the cooldown
48
+ counter (the rotation lifecycle itself lands in a later PR).
49
+
50
+ What was removed:
51
+
52
+ - `server.ts` startup-retry ladder (the 5-attempt loop with 15-120s
53
+ exponential backoff). Deleted.
54
+ - `server.ts` background-retry scheduler (the 5/10/20-minute
55
+ `scheduleRetry` callbacks). Deleted.
56
+ - The single Lifeline failure message. Deleted. The user-facing
57
+ notification path will flow through the notifier once the sink is
58
+ wired up in the next PR.
59
+
60
+ What's not yet landed (deliberate, future PRs in this chain):
61
+
62
+ - Owner-DM channel + inline-button consent UX.
63
+ - Tier-2 relay providers (localtunnel) + consent flow.
64
+ - Auth token + PIN rotation on relay-episode end + boot recovery.
65
+ - Full N-consecutive-success self-heal stability gate.
66
+ - The auth-gated tunnel-state route.
67
+ - ConfigDefaults migration for the new tunnel config fields.
68
+
69
+ The lifecycle state machine already supports all of these; the manager
70
+ just doesn't transition into the relevant states yet.
71
+
72
+ ## Evidence
73
+
74
+ 16 new unit tests in `tests/unit/tunnel-manager-rewrite.test.ts`:
75
+
76
+ - Tier-1 happy path: drives the first available provider, skips
77
+ unavailable providers, persists the lifecycle snapshot to disk.
78
+ - Reachability probe: rejects a provider whose URL does not pass the
79
+ health check, tears it down, falls through to the next provider.
80
+ - Provider failure → next: classifies a rate-limit failure, records
81
+ the failed attempt against the current episode, advances to the
82
+ next provider.
83
+ - Stop, forceStop, getExternalUrl, enable/disableAutoReconnect:
84
+ back-compat surface preserved.
85
+ - Repeat start returns the same URL while running (mutex preserved).
86
+ - Notifier wiring: a transition into retrying after a failure emits
87
+ the couldn't-reach-Cloudflare group message via the injected sink.
88
+ - Persistence: rotation-pending flag and consent-cooldown counter
89
+ restored from tunnel.json; corrupted state file ignored.
90
+
91
+ All 70 tunnel-related unit tests pass (16 new plus 54 foundation
92
+ tests from PR 1). Typescript and lint clean. No existing tests
93
+ modified.
94
+
95
+ ## What to Tell Your User
96
+
97
+ You will not notice a behavior change yet. The internals shifted —
98
+ your agent now has a single tidy retry engine instead of two
99
+ overlapping ones — but your tunnel link comes up the same way it did
100
+ before. Failure handling is also unchanged from your perspective in
101
+ this release; the next release in the chain plugs the failure path
102
+ into a message that goes to your Dashboard topic so you can see what
103
+ happened when the link does not come up.
104
+
105
+ ## Summary of New Capabilities
106
+
107
+ | Capability | How to Use |
108
+ |-----------|-----------|
109
+ | Single tunnel-lifecycle owner | Internal — your tunnel link behavior is preserved |
110
+ | Post-start reachability probe | Automatic — prevents broadcasting a dead link |
111
+ | Indefinite low-frequency self-heal placeholder | Automatic — agent keeps trying to recover after exhaustion without a restart |
@@ -0,0 +1,118 @@
1
+ # Side-effects review — tunnel failure resilience (foundation checkpoint)
2
+
3
+ **Scope (this commit — checkpoint 1 of N):** Land the foundation layer
4
+ that the rest of the feature builds on. This checkpoint introduces
5
+ NEW modules in isolation; it does NOT yet rewire `TunnelManager.ts` or
6
+ `server.ts`, so existing tunnel behavior is unchanged. Subsequent
7
+ commits on this branch layer on (1) `TunnelManager` rewrite to drive
8
+ the new lifecycle, (2) localtunnel provider + consent flow, (3)
9
+ `TelegramAdapter.sendToOwnerDM` + inline-button consent UX, (4)
10
+ `authToken`/PIN rotation lifecycle + boot recovery, (5) self-heal probe,
11
+ (6) `/tunnel` route, (7) `ConfigDefaults` migration + CLAUDE.md template
12
+ update, (8) Tier-2/3 tests. The PR opens once the full feature lands.
13
+
14
+ **Files touched in this checkpoint:**
15
+ - `specs/dev-infrastructure/tunnel-failure-resilience.md` — fix
16
+ `eli16-overview` frontmatter pointer to the actual `.eli16.md` file
17
+ (was pointing at a section anchor in the convergence report, which
18
+ the gate cannot resolve).
19
+ - `specs/dev-infrastructure/tunnel-failure-resilience.eli16.md` —
20
+ recreated (the original was lost in a worktree rebase).
21
+ - `src/tunnel/TunnelProvider.ts` — NEW. Provider abstraction interface
22
+ + `ProviderName`/`ProviderTier`/`ProviderFailureReason` enums.
23
+ - `src/tunnel/CloudflareQuickProvider.ts` — NEW. Tier-1 quick-tunnel
24
+ provider extracted from the original `TunnelManager.startQuickTunnel`.
25
+ - `src/tunnel/CloudflareNamedProvider.ts` — NEW. Tier-1 named-tunnel
26
+ provider (token-auth + config-file-auth) extracted from the
27
+ original `TunnelManager.startNamedTunnel` + `startConfigFileTunnel`.
28
+ - `src/tunnel/TunnelLifecycle.ts` — NEW. Single-writer CAS-guarded
29
+ state machine for the tunnel lifecycle (idle → starting → active |
30
+ retrying → awaiting-consent → relay-active | self-healing |
31
+ exhausted). Episode model, cross-episode consent cooldown,
32
+ rotation-pending flag, monotonic transition epoch.
33
+ - `src/tunnel/TunnelNotifier.ts` — NEW. Two-channel routing (group
34
+ status text only; owner-DM credentials only) with class-based
35
+ throttling (action-required never throttled; state-change keyed per
36
+ (state, channel) within window; noise emits once per episode).
37
+ - `tests/unit/tunnel-providers.test.ts` — NEW. 10 tests covering
38
+ provider interface + `isAvailable` semantics + start-rejection paths.
39
+ - `tests/unit/tunnel-lifecycle.test.ts` — NEW. 32 tests covering CAS
40
+ guard, transition validity, episode lifecycle, consent cooldown
41
+ exponential backoff, rotation flag, classifyFailure, generators.
42
+ - `tests/unit/tunnel-notifier.test.ts` — NEW. 12 tests covering
43
+ channel separation (no credentials in group), epoch dedup, class
44
+ throttling, flap-collapse, sink-error swallowing.
45
+
46
+ **Under-block**: None introduced by this checkpoint. The new modules
47
+ are not yet wired into the live tunnel path — existing
48
+ `TunnelManager.ts` is untouched and behaves identically. The wire-up
49
+ happens in the next commit on this branch.
50
+
51
+ **Over-block**: None. The new modules are additive; no existing
52
+ control-flow is changed yet.
53
+
54
+ **Level-of-abstraction fit**:
55
+ - `TunnelProvider` is a pure interface; concrete providers are thin
56
+ wrappers around `cloudflared`.
57
+ - `TunnelLifecycle` is provider-agnostic and notification-agnostic; it
58
+ owns ONLY state.
59
+ - `TunnelNotifier` is lifecycle-aware (consumes transition events) but
60
+ channel-agnostic (delegates to a `NotifierSink` interface).
61
+ - Separation lets subsequent commits swap implementations (e.g., add
62
+ the localtunnel provider) without touching the lifecycle or notifier.
63
+
64
+ **Signal vs authority**: Compliant. Providers raise failure errors
65
+ (SIGNAL via Error.message classification); the lifecycle classifies
66
+ into `ProviderFailureReason` (AUTHORITY); the notifier consumes the
67
+ classified signal (READ-ONLY consumer). No new authority is introduced
68
+ at the wrong layer.
69
+
70
+ **Interactions**:
71
+ - New modules import only from each other and node:crypto / node:events
72
+ / cloudflared. No interaction with existing TelegramAdapter,
73
+ PostUpdateMigrator, or server.ts in this checkpoint.
74
+ - Notifier's `NotifierSink` interface is satisfied by the existing
75
+ `TelegramAdapter.sendToTopic` for the group channel; the owner-DM
76
+ send method (`sendOwnerDM`) does not exist yet and will be added in
77
+ a subsequent commit. Until then, tests use mock sinks.
78
+
79
+ **External surfaces**: None. No new API endpoint, no new CLI command,
80
+ no new config field user-facing. The new types are exported from
81
+ `src/tunnel/*` but no caller imports them in this checkpoint.
82
+
83
+ **Migration parity**: N/A for this checkpoint (no agent-installed
84
+ file changes). The migration story (ConfigDefaults entries, CLAUDE.md
85
+ template + migrator updates) lands in a later commit on this branch.
86
+
87
+ **Rollback cost**: Trivial. Revert these eight new files + the spec
88
+ frontmatter pointer fix; the eli16.md companion goes with them.
89
+ Nothing else changes.
90
+
91
+ **Tests**:
92
+ - 10/10 tests in `tests/unit/tunnel-providers.test.ts` pass.
93
+ - 32/32 tests in `tests/unit/tunnel-lifecycle.test.ts` pass.
94
+ - 12/12 tests in `tests/unit/tunnel-notifier.test.ts` pass.
95
+ - `tsc --noEmit` clean. `npm run lint` clean.
96
+ - No existing tests were modified; no regressions.
97
+
98
+ **Decision-point inventory**:
99
+ 1. Provider interface is named-provider-aware (`isAvailable()` returns
100
+ false when neither token nor configFile is configured) so the
101
+ manager naturally skips the named path on quick-only installs.
102
+ 2. Lifecycle uses CAS (`transition(expectedFrom, to)`) instead of
103
+ message-passing — Node single-threaded model makes the synchronous
104
+ compare-and-set sufficient; the alternative (a queue) was unnecessary
105
+ complexity.
106
+ 3. `noiseEmittedThisEpisode` is a per-episode boolean; the flap-
107
+ collapse message lands at most once per episode regardless of how
108
+ many flap cycles happen. Trades fidelity (which cycle triggered it)
109
+ for guaranteed non-flooding.
110
+ 4. Throttle key for state-change is `<state>::<channel>` — the group
111
+ pointer and owner-DM credential delivery for the same state-change
112
+ must both be allowed; channel-blind keying would suppress the DM
113
+ after the group pointer fired, which is exactly the bug the
114
+ unit tests caught during this checkpoint's build.
115
+ 5. `TunnelLifecycle.restoreFrom` does NOT resume the live state.
116
+ Every boot starts at `idle` and re-enters `starting` from scratch.
117
+ The persisted `rotationPending` flag is the only piece that
118
+ triggers a boot-time action (the rotation in checkpoint 4).
@@ -0,0 +1,129 @@
1
+ # Side-effects review — TunnelManager rewrite (PR 2 of tunnel-failure-resilience chain)
2
+
3
+ **Scope (this PR — second in the chain):** Rewrite `TunnelManager.ts`
4
+ to drive the foundation modules (provider abstraction + state machine
5
+ + notifier) landed in PR 1, and retire the duplicate retry machinery
6
+ in `server.ts`. Spec
7
+ `specs/dev-infrastructure/tunnel-failure-resilience.md` is converged
8
+ + approved on main. Public API surface of `TunnelManager` is preserved
9
+ exactly; behavior under the happy path is unchanged.
10
+
11
+ **Files touched:**
12
+ - `src/tunnel/TunnelManager.ts` — REWRITTEN. The legacy class (which
13
+ spawned cloudflared directly and owned its own reconnect loop) is
14
+ replaced with a manager that builds a Tier-1 provider pool, drives
15
+ it through `TunnelLifecycle`, plumbs transitions to the
16
+ `TunnelNotifier` (when a sink is injected), and persists the
17
+ lifecycle snapshot to `tunnel.json`. Public surface (`start()`,
18
+ `stop()`, `forceStop()`, `enableAutoReconnect()`,
19
+ `disableAutoReconnect()`, `getExternalUrl()`, `url` / `isRunning` /
20
+ `state`) preserved exactly.
21
+ - `src/commands/server.ts` — Removed the startup-retry ladder
22
+ (5-attempt loop with 15-120s exponential backoff), the
23
+ background-retry scheduler (the 5/10/20-minute `scheduleRetry`
24
+ callbacks), and the single Lifeline failure message
25
+ ("Tunnel failed after all retries..."). Replaced with a single
26
+ `await tunnel.start()` call; failure is non-fatal and the manager
27
+ handles retry internally.
28
+ - `tests/unit/tunnel-manager-rewrite.test.ts` — NEW. 16 tests covering
29
+ the happy path, reachability probe, provider-failure → next, stop
30
+ semantics, notifier wiring, and persistence restoration.
31
+
32
+ **Under-block**: None. The new manager preserves the legacy public
33
+ API surface; all existing callers (`SleepWake` handler, the
34
+ construction site in `server.ts`) keep working unchanged.
35
+
36
+ **Over-block**: Minor. The old `server.ts` background-retry scheduler
37
+ fired retries at 5/10/20-minute intervals after the inner ladder gave
38
+ up. The new manager's bounded startup-reconnect ladder is 10 attempts
39
+ with exponential backoff (max 5-min delay between rounds), then the
40
+ post-exhausted self-heal placeholder fires every 15 min indefinitely.
41
+ Net effect: under sustained Cloudflare outage, the recovery cadence is
42
+ similar but more predictable. **`start()` itself rejects after the
43
+ FIRST round of provider attempts fails** (matching the legacy
44
+ semantics) — the backoff retry runs entirely in the background so
45
+ `start()` does not block the caller. This was the fix to a
46
+ `tests/e2e/tunnel-private-view.test.ts` 60-second hook-timeout
47
+ regression observed on the initial PR 2 CI run; the original draft
48
+ had kept retrying synchronously inside `start()`.
49
+ server.ts is also defensive: failure on initial `start()` is
50
+ non-fatal and execution continues; the manager keeps trying in the
51
+ background.
52
+
53
+ **Level-of-abstraction fit**: The manager is the sole owner of
54
+ detect → attempt → fall-back → notify per the spec's single-owner
55
+ mandate. Provider implementations remain narrowly responsible for
56
+ spawn + URL emission + teardown. The lifecycle module remains
57
+ provider-agnostic and notification-agnostic; the notifier remains
58
+ channel-agnostic. No layering violation introduced.
59
+
60
+ **Signal vs authority**: Compliant. The manager classifies provider
61
+ failures into `ProviderFailureReason` and routes them to the
62
+ lifecycle's authoritative state machine (CAS-guarded). The notifier
63
+ remains a read-only consumer of transition events. No new authority
64
+ introduced at the wrong layer.
65
+
66
+ **Interactions**:
67
+ - `SleepWake` handler in `server.ts` (lines 5818-5848) is unchanged —
68
+ it calls `disableAutoReconnect()` / `forceStop()` /
69
+ `enableAutoReconnect()` / `start()` exactly as before. The new
70
+ manager's `forceStop()` is a thin wrapper over `stop()`, and the
71
+ providers' own `stop()` implementations escalate to SIGKILL — the
72
+ same forceful-teardown semantics the legacy `forceStop(5000)`
73
+ provided.
74
+ - The `Promise.race([restart, 15s-timeout])` in SleepWake interacts
75
+ cleanly with the new manager's internal retry: if `start()` doesn't
76
+ resolve in 15s, the timeout fires, the catch re-enables
77
+ auto-reconnect (already always-on in the new design), and the
78
+ manager continues retrying internally.
79
+ - The post-exhausted retry timer is registered with the manager's
80
+ own field tracking; `stop()` clears it. No timer leak across
81
+ episodes.
82
+
83
+ **External surfaces**: None. No new API endpoint, no new CLI command,
84
+ no new public config field, no change to the TunnelManager
85
+ constructor's required signature (the optional `injections` parameter
86
+ is purely additive for testability).
87
+
88
+ **Migration parity**:
89
+ - No agent-installed file change (the manager is server-side code).
90
+ - `tunnel.json` format change: the file now carries lifecycle snapshot
91
+ fields. The constructor is tolerant of an old-format file (corrupted
92
+ parse → starts fresh) and of an absent file (no-op). No migration
93
+ helper needed.
94
+
95
+ **Rollback cost**: Trivial. Revert two files (`TunnelManager.ts` +
96
+ `server.ts`) + the test file. The new modules from PR 1 stay
97
+ in tree but become unused; subsequent revert of PR 1 (also a clean
98
+ file-delete) would restore the legacy state entirely.
99
+
100
+ **Tests**:
101
+ - 16/16 new tests in `tests/unit/tunnel-manager-rewrite.test.ts`.
102
+ - 32/32 tests in `tests/unit/tunnel-lifecycle.test.ts` (foundation,
103
+ unchanged).
104
+ - 12/12 tests in `tests/unit/tunnel-notifier.test.ts` (foundation,
105
+ unchanged).
106
+ - 10/10 tests in `tests/unit/tunnel-providers.test.ts` (foundation,
107
+ unchanged).
108
+ - `tsc --noEmit` clean. `npm run lint` clean.
109
+
110
+ **Decision-point inventory**:
111
+ 1. Public API surface preserved exactly (vs. introducing a new class
112
+ name like `TunnelManagerV2` and migrating callers) — preserves the
113
+ "single-owner mandate" naturally and avoids a dual-implementation
114
+ transition window where both the legacy and new classes live in
115
+ tree.
116
+ 2. Reachability probe is owned by the manager (not by providers) per
117
+ the spec's separation of concerns. Providers cannot self-certify
118
+ reachability without violating the authority discipline.
119
+ 3. Post-exhausted retry placeholder lives in the manager (vs. living
120
+ in `server.ts` as a separate scheduler). One owner, one retry
121
+ engine, even for the long-tail recovery path. The placeholder will
122
+ be replaced by the spec's N-consecutive-success stability-gated
123
+ probe in a later PR; the placeholder's API is internal so the
124
+ replacement is non-breaking.
125
+ 4. `tunnel.json` schema extended additively (`version: 1` field
126
+ added, plus new fields alongside the legacy `lastUrl`). An older
127
+ file produces a graceful "start fresh"; a new file is unreadable
128
+ to a downgraded binary but the downgraded binary will simply
129
+ overwrite it with the legacy format — no permanent corruption.