# Upgrade Guide — vNEXT

<!-- assembled-by: assemble-next-md -->
<!-- bump: minor -->

## What Changed

One additive line in the mentor autoloop prompt (`src/scheduler/MentorAutonomousGuardian.ts`), locked by three new unit-test assertions. Earned from PR #792's no-silent-fallbacks CI failure, where a spec's "best-effort, never throws" wording invited the swallowed catch the ratchet counts.

The dashboard sessions list now shows EVERY session across ALL machines in the
multi-machine pool, and every session row states which machine it runs on
(a machine-nickname badge). Sessions on the machine you're viewing stay fully
interactive (live terminal, close button); sessions on other machines appear
as informational rows with a tooltip pointing at the owning machine's
dashboard.

Under the hood: `GET /sessions` self-tags sessions with `machineId` /
`machineNickname` when the pool is wired, and a new `GET /sessions?scope=pool`
aggregates every reachable peer's session list (existing cross-machine Bearer
auth, 5s timeout per peer). A dead or slow peer degrades to a `pool.failed`
entry — the page never breaks. The plain `/sessions` route keeps its
back-compatible array shape.

Added the ReviewExchange protocol — the structured way two mandate-named agents complete
a mutual code-review sign-off without the operator relaying messages between them. One
exchange tracks one review package (content-addressed by checksum, fixed at creation)
through a strict linear lifecycle: proposed, delivered, verdict recorded, complete — or
changes-requested, which is terminal so a stale approval can never be replayed onto
reworked code. Both sign-offs — the reviewing peer's authenticated approval and the
owner's countersignature — are checked against the operator's coordination mandate
before acceptance, and every accepted signature stores the audit reference of the gate
decision that authorized it. With no mandate issued, every sign-off refuses: the
protocol inherits deny-by-default and ships inert.

A submitted Secret Drop is now persisted store-first to the durable
AES-256-GCM encrypted SecretStore the instant it is received, instead of
living only in the server's in-memory `received` map. The retrieve route
transparently falls back to the durable copy when the in-memory one is gone
(server restart, compaction, cross-machine handoff), and a successful consume
deletes both copies — one-time semantics are preserved. Opt out with
`secrets.persistDrops: false` in `.instar/config.json`.

This closes the recurring "the user dropped a secret and the agent lost it"
failure: with the fleet shipping many releases a day, every release restarted
the server and evaporated any un-consumed submission.

Also: the SecretStore master-key manager now forces file-key mode during test
runs (`VITEST` / `NODE_ENV=test`). The OS keychain entry is machine-global —
a test constructing a SecretStore against a fresh stateDir used to generate a
new master key and silently overwrite that shared entry, making every real
agent vault on the machine undecryptable for keychain-resolving readers. No
test can touch the real keychain anymore. A `secrets.forceFileKey` config
plumb is also available for operators who want to pin the key source.

The CLAUDE.md template's Secret Drop "Security" bullet is rewritten (the old
"in-memory only, never written to disk" claim is no longer true), and an
idempotent migration updates existing agents' CLAUDE.md in place.

Two fixes for the 2026-06-05 "move this to the laptop" noise incident, where
one user message produced contradictory "Moving to Laptop" / "I can't move
this right now (rate-limited)" replies and executed multiple times.

1. **Idempotent topic transfer.** The transfer planner now recognizes a move
   request targeting the machine the topic is already on — or already pinned
   to — BEFORE the rate limit. Duplicates of an already-satisfied move answer
   "This conversation is already running on X — nothing to move" instead of a
   rate-limit rejection. The rate limit keeps its real job: damping rapid
   flip-flops between DIFFERENT machines.

2. **Exactly-once ingress turns on with the pool.** The exactly-once inbound
   message gate (the dedupe ledger that stops retried/replayed messages from
   re-executing commands) shipped default-OFF — even on machines whose
   multi-machine session pool was live. The default is now stage-aware: a pool
   at `live-transfer` or `rebalance` gets `exactlyOnceIngress` ON
   automatically. An explicit `multiMachine.exactlyOnceIngress: false` still
   wins.

## What to Tell Your User

Nothing user-visible. The autonomous mentor loop's instructions now include how to satisfy the repo's ratchet CI gates, so mentor-driven fixes stop tripping no-silent-fallbacks.

Open the dashboard on any of your machines and you'll see all your sessions in
one list — each with a badge saying which machine it's running on. No more
opening one dashboard per machine to find a session.

Your agent can now run a real code review with a partner agent under the permission
slip you issue — no more relaying review messages between them yourself. The agents
deliver the code package, record the reviewer's verdict, and counter-sign, and every
step is checked against your permission slip and written to the same tamper-evident
log. If you revoke the slip mid-review, the review stops on the spot. Until you issue
a slip, nothing changes — every review step refuses by default.

If you hand me a secret through a drop link, it now survives my server
restarting, updating, or the conversation moving between machines. You should
never have to re-send a secret because "I lost it" again. Secrets are stored
encrypted and are deleted as soon as they're consumed.

Asking me to move a conversation to a machine it's already on (or repeating
the request) now gets a calm "already running there" instead of a confusing
"rate-limited" error. And if my internals retry a message under load, the
command runs once — not four times.

## Summary of New Capabilities

- `buildAutoloopGoal` includes a gate-compliance paragraph: intentional fail-open catches must report via DegradationReporter or carry an inline `@silent-fallback-ok` justification — never a ratchet baseline bump.
- The mentor is instructed to spell out these gate notes in any task brief or spec it authors for another agent.

- Dashboard sessions list shows every session across the pool with a machine badge per row.
- `GET /sessions?scope=pool` → `{ sessions, pool: { peersOk, failed, ... } }` (agents can answer "what's running across my machines?").
- `GET /sessions` sessions carry `machineId`/`machineNickname` on pooled installs.
- CLAUDE.md template bullet + idempotent migration so existing agents learn the capability.

- Create a review exchange for an artifact under a mandate, content-addressed so the
  signatures bind to exactly the code that was reviewed.
- Record delivery, the peer's authenticated verdict, and the owner's countersignature —
  with both sign-offs evaluated through the mandate gate and refused on any deny.
- Inspect exchanges and their signatures, each carrying the audit reference of the
  gate decision that authorized it.
- Maturity: stable engine and routes; ships inert (deny-by-default) until the operator
  issues a mandate with the sign-code-review authority.

- Secret Drop submissions persist store-first to the encrypted SecretStore and survive server churn.
- Retrieval transparently falls back to the durable copy; consume deletes both copies.
- `secrets.persistDrops: false` opts out (default ON).
- Tests can never pollute the machine-global SecretStore keychain entry (structural guard).
- `secrets.forceFileKey` pins the master-key source to the per-agent file key.

- Duplicate "move to X" requests no-op as "already there" — never "rate-limited."
- The transfer planner accepts an optional `currentPinOf` hook (pin-aware idempotency).
- `exactlyOnceIngress` defaults ON whenever the session pool routes real traffic.

## Evidence

Three tiers green: `tests/unit/dashboard-sessionMachineBadge.test.ts` (8),
`tests/unit/PostUpdateMigrator-poolSessionsVisibility.test.ts` (3),
`tests/integration/sessions-pool-scope.test.ts` (5 — including a REAL second
HTTP server as the peer and a dead-peer degradation case), and
`tests/e2e/sessions-pool-scope-lifecycle.test.ts` (3 — real AgentServer,
feature alive single-machine). tsc + lint clean.

`tests/integration/secret-drop-store-first.test.ts` (5 route-level tests, both
sides of every boundary), `tests/unit/secret-store.test.ts` (keychain
pollution guard), `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts`
(+3 migration tests), and the Tier-3 e2e
`tests/e2e/secret-drop-store-first-lifecycle.test.ts` — which boots two REAL
AgentServers on one stateDir and proves a secret submitted to process 1 is
retrievable from process 2 after a true restart. tsc + lint clean.

`tests/unit/TransferByNickname.test.ts` (+5, including the verbatim incident
case: already-on-target inside the rate window → noop) and
`tests/unit/seamlessnessConfig.test.ts` (+4, stage coupling both ways +
explicit-override both ways). tsc + lint clean. Production logs
(2026-06-05T04:20Z double pin, zero `exactly-once` lines on a live-transfer
pool) are the incident evidence.
