# Upgrade Guide — vNEXT

<!-- bump: patch -->
<!-- patch = consent state machine + grantConsent/declineConsent API; no Telegram wire-up yet -->

## What Changed

**feat(tunnel): consent state machine — Tier-1 exhaustion → awaiting-consent → relay-active.**

PR 5 of the tunnel-failure-resilience chain. Lands the consent state
machine inside `TunnelManager` so an exhausted Tier-1 path can offer
the Tier-2 relay (LocaltunnelProvider from PR 4) behind an
owner-approval gate.

What's new:

- `TunnelManager.buildDefaultPool` now appends a `LocaltunnelProvider`
  as the Tier-2 slot. `isAvailable()` returns false when the
  `localtunnel` npm package isn't installed (per the spec's opt-in
  posture), so the slot is silently skipped on agents without the
  dep — behavior is unchanged for them.
- `TunnelManager.exhaustedOrBackoff` now checks for an available
  Tier-2 provider AND that the cross-episode consent cooldown isn't
  active before deciding between `exhausted` and `awaiting-consent`.
  When both conditions hold, the manager transitions to
  `awaiting-consent` and generates a one-time CSPRNG nonce stored in
  a new `_pendingConsent` record.
- `TunnelManager.grantConsent(nonce)` — public API. Validates the
  nonce matches the pending record AND the state is still
  `awaiting-consent` (replay-safe single-use), starts the Tier-2
  provider, runs the post-start reachability probe, and transitions
  to `relay-active` with `rotationPending=true` (the persisted
  marker for the upcoming mandatory PIN+authToken rotation in PR 6).
  Returns true on success; false on any validation failure or
  start/probe failure.
- `TunnelManager.declineConsent(nonce)` — public API. Validates the
  nonce, advances the cross-episode cooldown counter, and
  transitions to `exhausted` + background retry.
- Consent timeout (15 min, per spec Part 4 default) — fires
  automatically; same effect as decline.
- `stop()` clears the pending consent + its timer so a torn-down
  manager can't have a stale timeout fire.
- `TunnelManager.pendingConsent` accessor — returns a defensive
  snapshot (episodeId, provider name, nonce, issuedAt) for the
  upcoming Telegram callback handler (PR 6) to embed the nonce in
  the inline-button payload.

What's NOT new yet (deliberate, future PRs in the chain):

- Telegram `callback_query` handler that invokes `grantConsent` /
  `declineConsent` on owner button clicks (PR 6).
- Inline-button payload in the consent prompt (PR 6).
- `authToken` / PIN rotation lifecycle on relay-episode end (PR 6+).
- Full N-consecutive-success self-heal stability gate (PR 7).
- The auth-gated `/tunnel` route + `ConfigDefaults` migration +
  CLAUDE.md template (PR 8).
- The `localtunnel` npm dep itself (PR 6, alongside the consent
  wire-up — keeps the capability opt-in until the gate is fully
  live).

Behavior change for users today: **none** at runtime. Agents without
the `localtunnel` dep installed continue exactly as before (Tier-2
slot is unavailable, manager falls through to exhausted). The
plumbing is in place; PR 6 makes it user-facing.

## Evidence

11 new unit tests in `tests/unit/tunnel-consent-state-machine.test.ts`:

- Tier-1 exhausts + Tier-2 available + cooldown inactive →
  `awaiting-consent` + pendingConsent populated with 128-bit hex nonce.
- Tier-1 exhausts + NO Tier-2 available → `exhausted` (baseline path).
- Tier-1 exhausts + consent cooldown active → `exhausted` (cooldown
  wins).
- `grantConsent` with matching nonce → `relay-active` + URL emitted +
  rotationPending=true + pendingConsent cleared (single-use).
- `grantConsent` with wrong nonce → returns false, state unchanged,
  provider.start NOT called.
- `grantConsent` replay of a used nonce → returns false, state
  unchanged.
- `grantConsent` + reachability probe fails → `exhausted` + cooldown
  advanced.
- `grantConsent` + provider.start throws → `exhausted` + cooldown
  advanced.
- `declineConsent` with matching nonce → `exhausted` + cooldown
  advanced + pendingConsent cleared.
- `declineConsent` with wrong nonce → returns false, state unchanged,
  no cooldown change.
- `stop()` clears pendingConsent + the timeout timer.

All 92 tunnel-related unit tests pass (11 new + 81 from PRs 1–4).
`tsc --noEmit` clean. `npm run lint` clean.

## Also in this release — Silently-stopped trio

Closes three independent gaps that all let an agent silently stop working without anyone noticing — diagnosed jointly with the gsd-side echo agent on 2026-05-22 (topic 5447).

**A — WorktreeManager clone-default for cross-project worktrees.** The legacy git-worktree flow keeps per-worktree metadata inside the SOURCE repo's hidden git folder. When Claude Code's sandbox EPERM-blocks paths under the shared projects directory, every git command from the worktree dies (confirmed 2026-05-22, recovery cost ~20m). WorktreeManager now uses `git clone` for cross-project work — self-contained git directory under agent home, no shared-path dependency. Existing in-tree worktrees still use the cheaper worktree path; the `INSTAR_WORKTREE_FORCE_WORKTREE` env var is the rollback escape hatch.

**B — SocketDisconnectSentinel.** Mirrors the RateLimitSentinel pattern shipped earlier this week. Watches tracked sessions for Claude Code's "socket connection closed unexpectedly" message and related disconnect strings every 15 seconds. On detection: immediate plain-English Telegram notice, four-attempt staircase recovery, escalation if recovery fails. No existing detector covered this case.

**C — ActiveWorkSilenceSentinel.** Independent of topic binding. Walks the SessionRegistry every 60s for "had output recently, hasn't for N minutes" (default 15 min). One gentle nudge, 30-second verify, escalate via the tone-gated attention path if the session doesn't unstick. Covers the gsd-style sub-spawned worktree session pattern that slipped through SessionWatchdog, SessionMonitor, and PresenceProxy.

All three route user-facing alerts through the existing MessagingToneGate B12-B14 ruleset.

## What to Tell Your User

This release puts the security gate for backup tunnels into place.
Nothing visible to you yet — the gate needs a button to click, and
that's the next release. Once that lands, when Cloudflare goes fully
down, you'll get a private message from your agent asking permission
to use a backup. Until you approve, the agent stays link-less and
keeps retrying Cloudflare. If you decline or don't reply, the agent
won't ask again for a while (the wait window grows the more you
decline, so the agent stops bothering you about something you've
already decided).

If you want the backup capability to actually work when the next
release lands, install the localtunnel npm package alongside your
agent. Without it the backup option is silently skipped and the
agent continues with Cloudflare only — which is fine if that's your
preference.

## Summary of New Capabilities

| Capability | How to Use |
|-----------|-----------|
| Consent state machine (Tier-1 → awaiting-consent → relay-active) | Internal — exposed via grantConsent / declineConsent APIs that the upcoming Telegram callback handler will call |
| Single-use CSPRNG nonce binding | Automatic — pendingConsent.nonce embeds in the inline button in the next release |
| Cross-episode consent cooldown | Automatic — exponential back-off on decline/timeout (1h → 4h → 24h) |
| Mandatory rotation marker on relay-active entry | Automatic — rotationPending=true is the crash-safe flag the upcoming rotation lifecycle reads |
| WorktreeManager clone-default | Automatic for cross-project work; rollback via INSTAR_WORKTREE_FORCE_WORKTREE=1 |
| SocketDisconnectSentinel | Wired into server startup; configurable via config.monitoring.socketDisconnectSentinel |
| ActiveWorkSilenceSentinel | Wired into server startup; threshold configurable via config.monitoring.activeWorkSilenceSentinel.silenceThresholdMs |
