instar 1.2.33 → 1.2.35
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/core/MessagingToneGate.d.ts.map +1 -1
- package/dist/core/MessagingToneGate.js +30 -1
- package/dist/core/MessagingToneGate.js.map +1 -1
- package/dist/tunnel/CloudflareNamedProvider.d.ts +51 -0
- package/dist/tunnel/CloudflareNamedProvider.d.ts.map +1 -0
- package/dist/tunnel/CloudflareNamedProvider.js +258 -0
- package/dist/tunnel/CloudflareNamedProvider.js.map +1 -0
- package/dist/tunnel/CloudflareQuickProvider.d.ts +48 -0
- package/dist/tunnel/CloudflareQuickProvider.d.ts.map +1 -0
- package/dist/tunnel/CloudflareQuickProvider.js +201 -0
- package/dist/tunnel/CloudflareQuickProvider.js.map +1 -0
- package/dist/tunnel/TunnelLifecycle.d.ts +190 -0
- package/dist/tunnel/TunnelLifecycle.d.ts.map +1 -0
- package/dist/tunnel/TunnelLifecycle.js +222 -0
- package/dist/tunnel/TunnelLifecycle.js.map +1 -0
- package/dist/tunnel/TunnelNotifier.d.ts +110 -0
- package/dist/tunnel/TunnelNotifier.d.ts.map +1 -0
- package/dist/tunnel/TunnelNotifier.js +268 -0
- package/dist/tunnel/TunnelNotifier.js.map +1 -0
- package/dist/tunnel/TunnelProvider.d.ts +80 -0
- package/dist/tunnel/TunnelProvider.d.ts.map +1 -0
- package/dist/tunnel/TunnelProvider.js +28 -0
- package/dist/tunnel/TunnelProvider.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.2.34.md +84 -0
- package/upgrades/1.2.35.md +96 -0
- package/upgrades/side-effects/context-death-stop-hook.md +100 -0
- package/upgrades/side-effects/tunnel-failure-resilience.md +118 -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,100 @@
|
|
|
1
|
+
# Side-effects review — context-death stop hook (B15 tone-gate rule)
|
|
2
|
+
|
|
3
|
+
**Scope**: Add `B15_CONTEXT_DEATH_STOP` to `MessagingToneGate`. The
|
|
4
|
+
operator explicitly asked for a structural guard against the agent's
|
|
5
|
+
recurring "Context-Death Self-Stop" pattern (CLAUDE.md anti-pattern,
|
|
6
|
+
multiple memory entries) because documentation alone has not eliminated
|
|
7
|
+
the slip.
|
|
8
|
+
|
|
9
|
+
**Files touched**:
|
|
10
|
+
- `src/core/MessagingToneGate.ts` — adds `'B15_CONTEXT_DEATH_STOP'` to
|
|
11
|
+
`VALID_RULES`; adds a new `## SELF-STOP rule` section in `buildPrompt`
|
|
12
|
+
describing literal pattern markers + legitimate-stop carve-outs;
|
|
13
|
+
updates the response-format rule list to include B15.
|
|
14
|
+
- `tests/unit/messaging-tone-gate-b15.test.ts` — 9 new tests covering
|
|
15
|
+
prompt-render assertions, drift-detection acceptance of B15,
|
|
16
|
+
propagation of a valid B15 response, and pass-through of completion /
|
|
17
|
+
blocker / topic-split candidates.
|
|
18
|
+
- `specs/dev-infrastructure/context-death-stop-hook.md` — converged +
|
|
19
|
+
approved (single-iteration; design intent authored by operator).
|
|
20
|
+
- `specs/dev-infrastructure/context-death-stop-hook.eli16.md` — plain-
|
|
21
|
+
English companion.
|
|
22
|
+
- `docs/specs/reports/context-death-stop-hook-convergence.md` — report.
|
|
23
|
+
|
|
24
|
+
**Under-block**: Narrow. The rule keys on the candidate message itself.
|
|
25
|
+
If the agent slips into the pattern in *internal* reasoning (thought
|
|
26
|
+
traces, plan files) but produces an outbound message that doesn't carry
|
|
27
|
+
any of the literal markers, B15 does not fire — exactly the boundary the
|
|
28
|
+
authority is supposed to enforce. That's correct: this is the outbound-
|
|
29
|
+
message authority, not a thought-monitor.
|
|
30
|
+
|
|
31
|
+
**Over-block**: Minimal by design. The rule has explicit
|
|
32
|
+
legitimate-stop carve-outs (real question, real blocker, real error,
|
|
33
|
+
real completion) AND explicit non-applicability boundaries (discussion-
|
|
34
|
+
about-the-pattern, topic-split/move logistics, operator-initiated stop
|
|
35
|
+
messages). The LLM gate is the existing authority that combines literal
|
|
36
|
+
markers with conversational context. Failure-mode: an LLM call timeout
|
|
37
|
+
or parse failure → fail-open (gate's existing semantics), so a network
|
|
38
|
+
hiccup never silences a legitimate message.
|
|
39
|
+
|
|
40
|
+
**Level-of-abstraction fit**: B15 lives where B1–B14 already live. No
|
|
41
|
+
new authority, no new file, no new endpoint. The structural placement
|
|
42
|
+
is sibling to existing rules, and the same drift-detection (rule
|
|
43
|
+
citation must be in `VALID_RULES`) governs it.
|
|
44
|
+
|
|
45
|
+
**Signal vs authority**: Compliant. The literal pattern markers in the
|
|
46
|
+
prompt are the SIGNAL; the LLM gate (the existing authority) decides
|
|
47
|
+
block/pass after combining markers with the legitimate-stop carve-outs
|
|
48
|
+
and conversational context. No new authority is introduced; no signal
|
|
49
|
+
becomes a blocker on its own.
|
|
50
|
+
|
|
51
|
+
**Interactions**:
|
|
52
|
+
- B15 does not change B1–B14 behavior. The drift-detection (any rule
|
|
53
|
+
cited must be in `VALID_RULES`) gains one entry; existing rules
|
|
54
|
+
remain valid.
|
|
55
|
+
- The fail-open paths (`failedOpen: true`, `invalidRule: true`) are
|
|
56
|
+
unchanged.
|
|
57
|
+
- All paths that call `MessagingToneGate.review()` automatically benefit
|
|
58
|
+
from B15 — no caller changes required. Tested implicitly by the
|
|
59
|
+
existing health-alert + attention-route tests passing without
|
|
60
|
+
modification.
|
|
61
|
+
|
|
62
|
+
**External surfaces**: None. No new API endpoint, no new config field,
|
|
63
|
+
no new CLI command, no change to the tone-gate's public type contract
|
|
64
|
+
(`ToneReviewResult` shape unchanged). The new rule id is observable on
|
|
65
|
+
the `result.rule` string, which already carries B1..B14 ids today.
|
|
66
|
+
|
|
67
|
+
**Migration parity**:
|
|
68
|
+
- No agent-installed file change. The tone gate runs on the agent's
|
|
69
|
+
server; existing agents pick up B15 the next time they update + the
|
|
70
|
+
server restarts.
|
|
71
|
+
- CLAUDE.md template (`generateClaudeMd` and `migrateClaudeMd`) — not
|
|
72
|
+
touched in this PR. The rule fires at message-send time regardless of
|
|
73
|
+
what the agent reads at session-start; a future PR can add a one-line
|
|
74
|
+
awareness note to the template, but it isn't load-bearing here (the
|
|
75
|
+
point of the structural guard is that the agent doesn't need to
|
|
76
|
+
remember).
|
|
77
|
+
|
|
78
|
+
**Rollback cost**: Trivial. Revert three files (Source edit + test +
|
|
79
|
+
spec triplet). The tone-gate behavior collapses to its pre-B15 state.
|
|
80
|
+
|
|
81
|
+
**Tests**: 9/9 new unit tests pass; 27/27 existing `MessagingToneGate`
|
|
82
|
+
tests still pass; 8/8 `messaging-tone-gate-health-alerts.test.ts` still
|
|
83
|
+
pass. `tsc --noEmit` clean. `npm run lint` clean.
|
|
84
|
+
|
|
85
|
+
**Decision-point inventory**:
|
|
86
|
+
1. LLM-gate rule (vs. regex hook) — chosen for the same reason the
|
|
87
|
+
existing tone gate uses LLM judgment: literal patterns under-block
|
|
88
|
+
on paraphrase and over-block on context (e.g., "we should pick this
|
|
89
|
+
up later *after lunch*" is fine).
|
|
90
|
+
2. Single-iteration converge (vs. full 4-internal + 3-external) — the
|
|
91
|
+
change is a single-rule extension to a well-defined existing
|
|
92
|
+
authority with the operator-authored design intent in the topic-9984
|
|
93
|
+
conversation; the full converge ceremony is calibrated for new
|
|
94
|
+
subsystems. The rule is unit-testable in isolation and reversible
|
|
95
|
+
trivially; if the operator wants the full ceremony for parity it
|
|
96
|
+
can be a follow-up.
|
|
97
|
+
3. Drop "regex pre-filter" optimization — adding a regex pre-check
|
|
98
|
+
before the LLM call would skip cheap-pass cases but introduces a
|
|
99
|
+
second authority on the same decision, contradicting the gate's
|
|
100
|
+
signal-vs-authority discipline. Rejected.
|
|
@@ -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).
|