instar 1.3.484 → 1.3.486

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 (65) hide show
  1. package/dashboard/index.html +4 -1
  2. package/dist/core/MessagingToneGate.d.ts +23 -2
  3. package/dist/core/MessagingToneGate.d.ts.map +1 -1
  4. package/dist/core/MessagingToneGate.js +15 -1
  5. package/dist/core/MessagingToneGate.js.map +1 -1
  6. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  7. package/dist/core/PostUpdateMigrator.js +21 -0
  8. package/dist/core/PostUpdateMigrator.js.map +1 -1
  9. package/dist/core/SessionManager.d.ts.map +1 -1
  10. package/dist/core/SessionManager.js +22 -0
  11. package/dist/core/SessionManager.js.map +1 -1
  12. package/dist/core/TelegramRelay.d.ts +1 -0
  13. package/dist/core/TelegramRelay.d.ts.map +1 -1
  14. package/dist/core/TelegramRelay.js +9 -1
  15. package/dist/core/TelegramRelay.js.map +1 -1
  16. package/dist/core/raw-file-path.d.ts +33 -0
  17. package/dist/core/raw-file-path.d.ts.map +1 -0
  18. package/dist/core/raw-file-path.js +105 -0
  19. package/dist/core/raw-file-path.js.map +1 -0
  20. package/dist/messaging/OutboundAdvisory.d.ts +152 -0
  21. package/dist/messaging/OutboundAdvisory.d.ts.map +1 -0
  22. package/dist/messaging/OutboundAdvisory.js +453 -0
  23. package/dist/messaging/OutboundAdvisory.js.map +1 -0
  24. package/dist/messaging/TelegramAdapter.d.ts +6 -0
  25. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  26. package/dist/messaging/TelegramAdapter.js +4 -1
  27. package/dist/messaging/TelegramAdapter.js.map +1 -1
  28. package/dist/messaging/pending-relay-store.d.ts +7 -0
  29. package/dist/messaging/pending-relay-store.d.ts.map +1 -1
  30. package/dist/messaging/pending-relay-store.js +9 -3
  31. package/dist/messaging/pending-relay-store.js.map +1 -1
  32. package/dist/monitoring/delivery-failure-sentinel.d.ts +6 -1
  33. package/dist/monitoring/delivery-failure-sentinel.d.ts.map +1 -1
  34. package/dist/monitoring/delivery-failure-sentinel.js +15 -3
  35. package/dist/monitoring/delivery-failure-sentinel.js.map +1 -1
  36. package/dist/scaffold/templates.d.ts.map +1 -1
  37. package/dist/scaffold/templates.js +7 -1
  38. package/dist/scaffold/templates.js.map +1 -1
  39. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  40. package/dist/scheduler/JobScheduler.js +8 -0
  41. package/dist/scheduler/JobScheduler.js.map +1 -1
  42. package/dist/server/AgentServer.d.ts +1 -0
  43. package/dist/server/AgentServer.d.ts.map +1 -1
  44. package/dist/server/AgentServer.js +22 -0
  45. package/dist/server/AgentServer.js.map +1 -1
  46. package/dist/server/PeerStreamProxy.d.ts +7 -0
  47. package/dist/server/PeerStreamProxy.d.ts.map +1 -1
  48. package/dist/server/PeerStreamProxy.js +11 -2
  49. package/dist/server/PeerStreamProxy.js.map +1 -1
  50. package/dist/server/WebSocketManager.d.ts +10 -1
  51. package/dist/server/WebSocketManager.d.ts.map +1 -1
  52. package/dist/server/WebSocketManager.js +27 -3
  53. package/dist/server/WebSocketManager.js.map +1 -1
  54. package/dist/server/routes.d.ts +12 -0
  55. package/dist/server/routes.d.ts.map +1 -1
  56. package/dist/server/routes.js +216 -3
  57. package/dist/server/routes.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/data/builtin-manifest.json +67 -67
  60. package/src/scaffold/templates.ts +7 -1
  61. package/src/templates/scripts/telegram-reply.sh +202 -10
  62. package/upgrades/1.3.485.md +31 -0
  63. package/upgrades/1.3.486.md +38 -0
  64. package/upgrades/side-effects/outbound-advisory-inform-only.md +252 -0
  65. package/upgrades/side-effects/pool-stream-cross-machine-fix.md +94 -0
@@ -0,0 +1,252 @@
1
+ <!-- DRAFT — completed in Phase 4 of /instar-dev. Phase 1 principle check + Phase 2 plan recorded below first. -->
2
+
3
+ # Side-Effects Review — Outbound advisory: jargon + raw-file-path gaps for automated senders (inform-only)
4
+
5
+ **Version / slug:** `outbound-advisory-inform-only`
6
+ **Date:** `2026-06-11`
7
+ **Author:** `echo (instar-dev agent)`
8
+ **Second-pass reviewer:** `required — outbound-messaging decision surface (Phase 5)`
9
+
10
+ ## Phase 1 — Principle check (recorded before any code)
11
+
12
+ **Does this change involve a decision point?** YES — it touches the outbound-messaging
13
+ pipeline, the single most sensitive decision surface in instar. The signal-vs-authority
14
+ principle applies directly, and the design (per the converged + approved spec
15
+ `docs/specs/outbound-jargon-filepath-gap.md`, r2) was shaped around it:
16
+
17
+ - `detectRawFilePath` (new) and `detectJargon` (re-scoped) are pure DETECTORS — regex,
18
+ cheap, fail-open, no blocking power. They feed the EXISTING authority
19
+ (`MessagingToneGate.review`) as signals. No new rule, no re-scoped block rule.
20
+ - The preflight advisory is honestly named in the spec as a "default-withhold the sender
21
+ always resolves" — the sending agent keeps final authority (fix or `--ack-advisory`,
22
+ both always deliver past the advisory layer). It never consults a judge, never
23
+ escalates against the sender, fails open on every error path.
24
+ - The repeated-ignore escalation informs the OPERATOR (one deduped Attention item),
25
+ gates nothing.
26
+ - Governing operator constraint (Justin, 2026-06-10): zero new blocking authority.
27
+ r1's deterministic 422 floor was DELETED in r2 for exactly this reason.
28
+
29
+ ## Phase 2 — Plan (recorded before any code)
30
+
31
+ **Build location:** fresh worktree `.worktrees/echo-outbound-advisory-build`, branch
32
+ `echo/outbound-advisory-build`, based off `JKHeadley/main` @ v1.3.484 (verified:
33
+ `git remote -v` = JKHeadley https, `package.json` 1.3.484, git identity
34
+ `Instar Agent (echo) <echo@instar.local>`).
35
+
36
+ **Implementation order:**
37
+ 1. `detectRawFilePath` detector (sibling of `localhost-link.ts`, same linear-regex discipline) + unit tests.
38
+ 2. `messageKind` union widening (`'automated'`) in all five cited sites + `renderMessageKind` branch.
39
+ 3. `checkOutboundMessage`: always-compute jargon for non-`reply` kinds (drop the `options.jargon` opt-in; fix the one vestigial caller); feed `signals.filePath`.
40
+ 4. `POST /messaging/preflight` + `GET /messaging/advisory-log` + server-side single-writer JSONL audit + in-memory escalation index + Attention escalation (fixed sourceContext).
41
+ 5. Scheduler env injection: BOTH SessionManager spawn env blocks (headless + rerouted-interactive) + JobScheduler `runScriptJob` env (`INSTAR_MESSAGE_KIND`, `INSTAR_JOB_SLUG`, `INSTAR_SENDER_CLASS`).
42
+ 6. `telegram-reply.sh` template: metadata forwarding in BOTH body builders, preflight loop + `--ack-advisory`, curl timeout ms→s clamp, queue metadata column in BOTH script queue writers (quote-safe).
43
+ 7. `PendingRelayStore` COLUMN_ADDS metadata column + `DeliveryFailureSentinel.postReply` threading + redrive exemption via `X-Instar-DeliveryId` validated against a real queue row.
44
+ 8. Relay-hop forwarding (3-layer widening) so kind/senderClass/ack survive the cross-machine hop.
45
+ 9. `/telegram/reply`: read `metadata.messageKind` → `checkOutboundMessage`; `acked` audit row; kindless-job-send + class-spoof breadcrumbs; senderClass validation against the job definition.
46
+ 10. Config plumbing via LiveConfig (live re-read; `?? true` defaults — rollback without restart).
47
+ 11. PostUpdateMigrator: current live telegram-reply SHA → allowlist; CLAUDE.md template section (add advisory awareness, remove hand-curl example); `migrateFrameworkShadowCapabilities` marker.
48
+ 12. All three test tiers per spec §6; release fragment; this artifact completed; trace; commit; PR; CI; merge (Phase 7 auto-merge on green).
49
+
50
+ **Decision points touched:** outbound `/telegram/reply` pipeline (signals only), new preflight route (advisory only), Attention queue (operator-inform only).
51
+ **Existing detectors/authorities interacted with:** `detectJargon`, `detectLocalhostLink`, `MessagingToneGate.review` (B2/B12 untouched in scope), grounding-before-messaging hook (independent layer, unchanged).
52
+ **Rollback path:** `messaging.outboundAdvisory.enabled:false` + `messaging.outboundFloor.jargonAlways:false` (live config, no restart); scheduler env injection inert without script/route consumers; queue column is nullable + idempotent-ALTER (legacy rows fine).
53
+
54
+ ## Summary of the change
55
+
56
+ Implements the converged + approved spec `docs/specs/outbound-jargon-filepath-gap.md` (r2): structural
57
+ `automated` message-kind stamping at job spawn (SessionManager both lanes + JobScheduler script env),
58
+ the jargon signal single-sourced for non-reply kinds, a new `detectRawFilePath` signal detector, an
59
+ inform-only preflight advisory in `telegram-reply.sh` backed by `POST /messaging/preflight` +
60
+ `GET /messaging/advisory-log`, a server-side single-writer audit with repeated-ignore escalation, kind
61
+ metadata threading through the durable relay queue / sentinel redrive / cross-machine relay hop, and
62
+ full migration parity (SHA allowlist + CLAUDE.md + shadow mirrors). Files: `src/core/raw-file-path.ts`
63
+ (new), `src/messaging/OutboundAdvisory.ts` (new), `src/core/MessagingToneGate.ts`,
64
+ `src/server/routes.ts`, `src/server/AgentServer.ts`, `src/core/SessionManager.ts`,
65
+ `src/scheduler/JobScheduler.ts`, `src/templates/scripts/telegram-reply.sh`,
66
+ `src/messaging/pending-relay-store.ts`, `src/monitoring/delivery-failure-sentinel.ts`,
67
+ `src/messaging/TelegramAdapter.ts`, `src/core/TelegramRelay.ts`, `src/core/PostUpdateMigrator.ts`,
68
+ `src/scaffold/templates.ts` + three test tiers.
69
+
70
+ ## Decision-point inventory
71
+
72
+ - `evaluateOutbound` / `checkOutboundMessage` (routes.ts) — **modify (signals only)** — two new SIGNALS (jargon for non-reply kinds, filePath for all kinds) feed the EXISTING authority; no decision logic changed; block/allow remains solely the authority's.
73
+ - `MessagingToneGate.review` — **pass-through** — receives the new kind + signals as context; rules untouched (no B12 re-scope, no new rule).
74
+ - `POST /messaging/preflight` (new) — **add (advisory only)** — deterministic detectors, returns advisories; CANNOT refuse a send (the server delivers regardless — proven by test).
75
+ - `telegram-reply.sh` preflight loop — **add (default-withhold the sender always resolves)** — honestly named per spec §2.4: the first flagged attempt does not deliver; resolution (fix or ack) belongs exclusively to the sender; fail-OPEN on every error path.
76
+ - Repeated-ignore escalation — **add (operator-inform only)** — one deduped Attention item; gates nothing.
77
+ - Observability breadcrumbs in `/telegram/reply` — **add (log lines only)** — never affect delivery; wrapped in a try/catch.
78
+
79
+ ---
80
+
81
+ ## 1. Over-block
82
+
83
+ The server holds NO new block surface — `/telegram/reply` delivers a raw-path automated message
84
+ exactly as before (test-proven). The script-side withhold can over-fire two ways:
85
+
86
+ - `detectRawFilePath` false positive (e.g. a legitimate user-actionable path in a power-user setup):
87
+ costs the sender one `--ack-advisory` re-run; the message still delivers unchanged. Bounded cost,
88
+ stated in the spec's risk table.
89
+ - `detectJargon` false positive on an automated send: same single-re-run cost. Jargon is deliberately
90
+ NOT computed for conversational replies, so the over-block tail the operator has repeatedly warned
91
+ about cannot grow on the main path.
92
+
93
+ A genuinely-flagged localhost link is the one case `--ack-advisory` cannot deliver — but that block
94
+ belongs to the PRE-EXISTING localhost guard (untouched); the advisory's static guidance states this
95
+ honestly rather than promising ack-delivery.
96
+
97
+ ## 2. Under-block
98
+
99
+ - A job that hand-curls `/telegram/reply` (bypassing the script) sends kindless and skips the
100
+ preflight entirely — named residual (§2.1/§2.5), bounded by the kindless-job-send breadcrumb and the
101
+ removal of the hand-curl example from the CLAUDE.md template. Accepted by design (sender sovereignty).
102
+ - `isProxy`/`isSystemTemplate`/`willRelay` system-composed sends skip `checkOutboundMessage` today and
103
+ are untouched — there is no sender to inform. Named residual.
104
+ - A one-shot sender that ignores its advisory drops that one message with no re-fire behind it; the
105
+ per-slug aggregate escalation is the bound. Accepted residual cost of inform-only, per the operator's
106
+ explicit constraint.
107
+ - `detectRawFilePath` misses path shapes outside its three alternatives (e.g. bare relative
108
+ `foo/bar.txt` with an unknown top dir, Windows `C:\` paths). Deliberate precision-first scope —
109
+ the LLM authority's B2 judgment still covers them on gated kinds.
110
+
111
+ ## 3. Level-of-abstraction fit
112
+
113
+ Correct layers throughout: the detector is a cheap deterministic sibling of `localhost-link.ts` in
114
+ `src/core/`; the signals feed the EXISTING authority instead of running parallel to it (the spec's
115
+ grounding correction explicitly rejected a new gate); the advisory loop lives in the mandated relay
116
+ script (the only place that can inform the sender BEFORE delivery without giving the server veto
117
+ power); the audit/escalation live server-side where the single-writer guarantee is enforceable. The
118
+ kind is stamped at the SPAWNER (the only layer that structurally knows a session is a job).
119
+
120
+ ## 4. Signal vs authority compliance
121
+
122
+ **Required reference:** docs/signal-vs-authority.md
123
+
124
+ - [x] No — this change produces signals consumed by an existing smart gate.
125
+
126
+ `detectRawFilePath` and the re-scoped `detectJargon` are pure detectors feeding
127
+ `MessagingToneGate.review`. The preflight advisory holds no blocking authority: its withhold is always
128
+ and only resolved by the sender, never escalates, never consults a judge, and fails open on every
129
+ error path — the spec names it honestly as a "default-withhold the sender always resolves" rather than
130
+ claiming "no authority at all". r1's deterministic 422 floor was DELETED per the operator's
131
+ inform-only constraint; no block rule was added or re-scoped anywhere in this change.
132
+
133
+ ## 5. Interactions
134
+
135
+ - **Shadowing:** the preflight runs BEFORE the send, script-side; the existing pipeline (localhost
136
+ guard, tone gate, dedup, length cap) runs unchanged server-side afterward. One send can legitimately
137
+ receive two differently-shaped interventions (grounding hook + advisory) — independent layers; the
138
+ advisory text never claims to be the only check.
139
+ - **Double-fire:** the `acked` audit row is written ONLY on successful delivery, so an
140
+ acked-then-queued send audits once, after the redrive — verified that the queue carries
141
+ `advisoryAck` whole, closing the escalation false-fire on transient delivery failures.
142
+ - **Races:** the escalation index is in-memory single-process (write-time, no poller); restart resets
143
+ are accepted, stated best-effort observability. JSONL appends are single `appendFileSync` calls
144
+ (O_APPEND).
145
+ - **Feedback loops:** the escalation raises Attention items through `createAttentionItem` with a
146
+ FIXED sourceContext and stable per-signature ids (id-dedup prevents re-raise loops). **Corrected
147
+ after the second-pass review:** HIGH/URGENT items are EXEMPT from the per-source topic budget, so
148
+ the original all-HIGH design would have been an un-budgeted topic-per-signature flood for a
149
+ topic-varying sender. Iterated: per-signature items are NORMAL (the budget genuinely binds — a
150
+ burst-invariant test feeds escalations through the REAL AttentionTopicGuard and asserts allowed
151
+ topics ≤ the budget); the per-slug aggregate is the single loud HIGH bound (one deduped item per
152
+ slug); once a slug's aggregate has fired, further per-signature items for that slug are suppressed.
153
+ - The sentinel's `postReply` signature widened with optional trailing params — all existing callers
154
+ and test doubles remain compatible (verified by the full suite).
155
+
156
+ ## 6. External surfaces
157
+
158
+ - **Other agents on the machine:** job sessions of every framework (claude/codex/gemini) get the new
159
+ env vars — inert unless the relay script consumes them. The CLAUDE.md awareness section reaches
160
+ Codex/Gemini via the shadow-capability mirror.
161
+ - **Install base:** the `telegram-reply.sh` re-deploy rides the SHA-gated migrator; user-modified
162
+ scripts are never stomped (`.new` + degradation event, existing behavior). The queue column arrives
163
+ via idempotent ALTER on every writer — legacy rows read null metadata and ride the delivery-id
164
+ breadcrumb exemption.
165
+ - **External systems:** no new egress. The preflight is a localhost call; detectors are local regex.
166
+ - **Persistent state:** new `logs/outbound-advisory.jsonl` (size-rotated, single `.1` rollover); new
167
+ nullable `message_metadata` column in the pending-relay SQLite (additive, no migration needed for
168
+ rollback).
169
+ - **Timing:** the preflight adds one localhost round-trip (~ms) + ~100ms of process spawns per
170
+ automated send; bounded by the clamped curl timeout; the conversational path is untouched.
171
+
172
+ ## 7. Rollback cost
173
+
174
+ - **Hot levers (live config, NO restart):** `messaging.outboundAdvisory.enabled:false` disables the
175
+ preflight + escalation; `messaging.outboundFloor.jargonAlways:false` reverts the jargon signal.
176
+ Both are read per-request through LiveConfig.
177
+ - **Code revert:** the scheduler env injection is inert without the script/route consumers, so a
178
+ partial rollback degrades safely in any order.
179
+ - **Data:** the JSONL audit and the nullable queue column need no cleanup on rollback (additive,
180
+ null-tolerant readers).
181
+ - **User visibility during rollback:** none — worst case, automated sends simply stop being advised.
182
+
183
+ ## Conclusion
184
+
185
+ The review confirms the design holds the operator's governing constraint structurally: zero new
186
+ blocking authority anywhere in the server; the only withhold lives script-side, is always resolvable
187
+ by the sender alone, and fails open on every error path (each proven by a test, not asserted). The
188
+ named residuals (hand-curl bypass, system-composed sends, one-shot drops) are bounded by breadcrumbs
189
+ and the escalation, and are the accepted cost of inform-only — the operator chose this trade
190
+ explicitly. One design change came out of testing: the fix-landing resolution threshold was tuned to
191
+ 0.4 Jaccard after measuring the founding incident shape (~0.47) vs the gaming heartbeat (~0.06).
192
+ Clear to ship pending the required second-pass review (outbound-messaging decision surface).
193
+
194
+ ---
195
+
196
+ ## Second-pass review (required — outbound messaging surface)
197
+
198
+ **Reviewer:** subagent (claude, independent second-pass)
199
+ **Independent read of the artifact: concern → resolved → concur (re-confirmed below)**
200
+
201
+ First-pass verdict (verbatim): the core inform-only claims all verified adversarially against the
202
+ code (no new server-side blocking authority; no silent drop — the NOT-SENT line precedes every
203
+ withhold; fail-open complete across curl failure/timeout/non-200/malformed-JSON/missing-python3/
204
+ disabled; secret-safety bounds hold at all four echo sites; queue/redrive/relay threading and
205
+ migration parity verified). Three concerns were raised:
206
+
207
+ 1. **(Load-bearing) The §5 flood-bound claim was FALSE as written:** per-signature escalations were
208
+ HIGH, and HIGH bypasses BOTH the per-source budget and the universal topic budget — a
209
+ topic-varying sender would have produced K un-budgeted HIGH topics (the 2026-06-05 flood shape);
210
+ the spec-required burst proof test was missing, which is exactly why the false claim survived.
211
+ **Resolution (implemented):** per-signature items demoted to NORMAL (budget-eligible); per-slug
212
+ aggregate kept as the single deduped HIGH; per-signature suppressed once the slug aggregate has
213
+ fired; burst-invariant test added driving 10 distinct misbehaving signatures through the REAL
214
+ `AttentionTopicGuard` and asserting allowed topics ≤ the budget, plus a suppression test.
215
+ 2. **Two §6 assertions were absent.** **Resolution (implemented):** non-telegram (slack) automated
216
+ send asserted to receive the jargon + filePath signals (with the slack route now threading
217
+ `metadata.messageKind`, mirroring telegram); the Tier-2-location nit (route tests under
218
+ `tests/unit/` driving a real express app) stands with this note.
219
+ 3. **(Disclosed, no action)** the 0.4 Jaccard fix-landing threshold can under-resolve a heavy
220
+ rewrite; bounded to NORMAL/budgeted items by resolution 1.
221
+
222
+ **Re-confirmation (independent, after the iteration):** concur — "All three resolutions verified in
223
+ code and tests against the real `AttentionTopicGuard` (35/35 green)"; the burst test was verified
224
+ non-vacuous (a revert to HIGH fails both assertions). The one cosmetic nit (a stale test-header
225
+ comment) was fixed in the same pass.
226
+
227
+ ---
228
+
229
+ ## Evidence pointers
230
+
231
+ - `tests/unit/telegram-reply-advisory-script.test.ts` — the REAL shipped script under bash + recording
232
+ curl shim: NOT-SENT literal/exit-0, ack annotation, fail-open ×2, legacy-body byte-shape, ms→s clamp,
233
+ queue metadata on HTTP-000.
234
+ - `tests/unit/raw-file-path.test.ts` (26) · `tests/unit/outbound-advisory.test.ts` (17) ·
235
+ `tests/unit/outbound-advisory-routes.test.ts` (15) · `tests/unit/pending-relay-metadata.test.ts` (4) ·
236
+ `tests/unit/relay-kind-forward.test.ts` (2) · spawn-env assertions in
237
+ `tests/unit/session-manager-behavioral.test.ts`.
238
+ - `tests/e2e/outbound-advisory-alive.test.ts` (5) — production-init alive + auth + on-disk audit +
239
+ live kill switch.
240
+
241
+ ---
242
+
243
+ ## Post-CI follow-up (same PR)
244
+
245
+ The no-silent-fallbacks ratchet (CI shard 3) correctly demanded that every spec-mandated fail-open
246
+ catch be a TRACKED decision: all are now annotated `@silent-fallback-ok` with the spec-section
247
+ justification (fail-open is the §0 operator constraint, not an accident), plus one documented
248
+ scanner-window false positive on a pre-existing 500-responder catch. No behavior change.
249
+
250
+ CI round 2: one source-inspection test (`slack-thread-session-wiring`) used a fixed 1600-char
251
+ extraction window that the slack route's new messageKind threading overflowed — window widened to
252
+ 2400 with a comment naming the cause. No behavior change.
@@ -0,0 +1,94 @@
1
+ # Side-Effects Review — Pool Dashboard Streaming: cross-machine history relay + closed-proxy recovery
2
+
3
+ **Version / slug:** `pool-stream-cross-machine-fix`
4
+ **Date:** `2026-06-11`
5
+ **Author:** `echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `not required` (no block/allow surface on messaging/dispatch, no session lifecycle, no sentinel/gate/watchdog logic — see Phase-5 assessment in Conclusion)
7
+
8
+ ## Summary of the change
9
+
10
+ Fixes the two live bugs found during 2026-06-08 live testing of Pool Dashboard Streaming (POOL-DASHBOARD-STREAM-SPEC, converged-approved 2026-06-06), which left a remote (Mac Mini) session tile connecting but rendering a blank terminal, with no recovery until server restart:
11
+
12
+ 1. **Closed-proxy eviction** (`src/server/WebSocketManager.ts` → `peerProxyFor`): a `PeerStreamProxy` that reached `closed` (idle-grace close after the last viewer left, or `machine-unreachable` after the bounded reconnect failed) stayed cached in `peerProxies` forever. A closed proxy ignores every further subscribe by design, while the subscribe handler still answered `subscribed` — so one upstream hiccup (or 60s of nobody watching) made that peer permanently unstreamable, silently. `peerProxyFor` now evicts a closed proxy and creates a fresh one; a fresh user-initiated subscribe is a fresh episode with its own bounded reconnect budget (P19 preserved).
13
+ 2. **Cross-machine history relay** (`src/server/WebSocketManager.ts` `history` case, `src/server/PeerStreamProxy.ts` `relayFrame`, `dashboard/index.html` `loadMoreHistory`): the scrollback fetch (`{type:'history'}`) had no remote branch — it always called local `captureOutput`, which can only return empty for a session owned by another machine (spec §2.2: capture happens ONLY on the owning machine). The handler now relays history upstream for a remote-subscribed session exactly like input/key, the proxy gained a read-only `relayFrame` (relayInput delegates to it), and the dashboard client sends `machineId` on history requests for remote sessions.
14
+ 3. **Relayable history miss** (same `history` case): the local "no output" reply was `{type:'error', message}` with NO session field — the peer fan-out drops sessionless frames, so the error could never travel. It now carries `session` + `code:'session-not-found'`, which the dashboard already renders honestly (§2.4).
15
+
16
+ Files: `src/server/WebSocketManager.ts`, `src/server/PeerStreamProxy.ts`, `dashboard/index.html`, `tests/unit/WebSocketManager.test.ts`, `tests/unit/PeerStreamProxy.test.ts`.
17
+
18
+ ## Decision-point inventory
19
+
20
+ - `WebSocketManager.peerProxyFor` — modify — get-or-create now evicts a `closed` proxy; routing decision (which proxy serves a peer), not a block/allow decision.
21
+ - `WebSocketManager.handleMessage 'history'` — modify — adds the remote-routing branch (same shape as the existing `input`/`key` remote branches); read-only data fetch, no authority.
22
+ - `PeerStreamProxy.relayFrame` — add — forwards a read-only frame while the link is active; drops otherwise (same drop semantics relayInput always had).
23
+ - `gateWrite` (remote input authority, `poolStreamAllowRemoteInput`) — **pass-through, untouched** — history is read-only and never reaches `gateWrite`; the input default-off security gate is not modified by this change.
24
+ - Stream ticket auth (`StreamTicketStore`, `/pool-stream` upgrade) — **pass-through, untouched**.
25
+
26
+ ---
27
+
28
+ ## 1. Over-block
29
+
30
+ **What legitimate inputs does this change reject that it shouldn't?**
31
+
32
+ No issue identified. The change adds no new rejection. The history remote branch only triggers when the client explicitly sent a `machineId` AND that client already holds a live remote subscription for exactly that `(machine, session)` pair (`client.remoteSubs.has(...)`) — a history request for a never-subscribed remote session falls through to the local path and answers `session-not-found`, which is the honest answer (you cannot scroll back a session you are not watching). Local history requests (no machineId) are byte-for-byte unchanged.
33
+
34
+ ---
35
+
36
+ ## 2. Under-block
37
+
38
+ **What failure modes does this still miss?**
39
+
40
+ - A history request relayed while the upstream is `connecting` (mid-reconnect) is dropped, not queued — the user's infinite-scroll simply doesn't load that batch and re-fires on the next scroll (the client re-requests on scroll position). Stale queued requests racing a reconnect would be worse. Accepted, documented in `relayFrame`'s contract.
41
+ - The history reply fans to EVERY local client subscribed to that (machine, session), not just the requester — same multiplexing behavior the spec defines for all frames (§2.2 "frames relay 1:1"); clients gate rendering on `activeSession`, so a non-requesting viewer at most refreshes its scrollback.
42
+ - The 2026-06-07 mint-timeout failure class (wedged ticket mint) is already handled by the explicit 10s timeout shipped in #970, upstream of this change.
43
+
44
+ ---
45
+
46
+ ## 3. Level-of-abstraction fit
47
+
48
+ **Is this at the right layer?**
49
+
50
+ Yes. The history relay is placed at exactly the layer the spec assigned it: the SUBSCRIBE-side routing chokepoint in `WebSocketManager.handleMessage`, parallel to the existing `input`/`key` remote branches — not a new parallel path. `relayFrame` lives in `PeerStreamProxy` because that class owns "is the upstream link usable" state; the manager owns "is this session remote for this client" state. The eviction lives in `peerProxyFor` (the single get-or-create chokepoint) rather than inside the proxy (e.g. a self-reopening proxy), keeping the proxy's one-shot bounded-reconnect state machine — and its P19 guarantee — untouched.
51
+
52
+ ---
53
+
54
+ ## 4. Signal vs authority compliance
55
+
56
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
57
+
58
+ - [x] No — this change has no block/allow surface.
59
+
60
+ The change is pure data-plane plumbing (routing read-only frames to the machine that owns the data, and replacing a dead connection object). The ONLY authority in this subsystem — the serving machine's `poolStreamAllowRemoteInput` gate on remote keystrokes — is not touched, weakened, or bypassed: history never reaches `gateWrite` because it is a read, and reads were already permitted to any authenticated peer link (the initial subscribe capture has always shipped full screen content to the peer). No new information becomes visible to any principal that could not already see it.
61
+
62
+ ---
63
+
64
+ ## 5. Interactions
65
+
66
+ - **Shadowing:** the remote history branch runs BEFORE the local capture path, gated on `client.remoteSubs` — the local path is unreachable only for sessions the client explicitly subscribed remotely, which is exactly the set local capture could never serve (it returned null for them). Nothing that previously worked is shadowed.
67
+ - **Double-fire:** none — a history request takes exactly one branch (remote XOR local). Eviction cannot double-open: `peerProxyFor` is synchronous, and only a `closed` proxy (no live transport, no pending timers except a fired/cleared one) is evicted, so no second live upstream to the same peer can exist.
68
+ - **Races:** a client disconnect between failAll and re-subscribe calls `dropRemoteSubsForClient`, which looks up the CURRENT map entry — if eviction already replaced the proxy, the unsubscribe lands on the fresh proxy where `unsubscribe()` of an unknown clientId is a documented no-op. An old evicted proxy holds no timers and no transport (failAll/idle-close tear both down), so it is plain garbage, not a leak.
69
+ - **Feedback loops:** none — the eviction does not auto-reconnect; it only acts on the next explicit user subscribe, so a permanently-down peer costs one bounded reconnect cycle per user click, never a storm.
70
+
71
+ ---
72
+
73
+ ## 6. External surfaces
74
+
75
+ - **Other machines:** the serving side of the protocol is UNCHANGED — an updated laptop streaming from a not-yet-updated Mini works (the Mini's serving side always answered history requests arriving on a peer link; the bug was the laptop never sending them). Mixed-version fleets degrade to exactly today's behavior at worst.
76
+ - **Protocol:** no new frame types; `history` was already in the documented protocol and in `UpstreamTransport.send`'s doc comment ("subscribe/unsubscribe/input/key/history"). The local history-miss error gains `code` + `session` fields (additive; the dashboard's existing `error` handler renders `code`-carrying frames and still `console.error`s unknown shapes).
77
+ - **Persistent state:** none touched. No config, no migrations, no ledgers.
78
+ - **Timing:** `relayFrame` depends on the upstream being `active` — bounded, observable via the existing `[pool-stream-connector]` log lines.
79
+
80
+ ---
81
+
82
+ ## 7. Rollback cost
83
+
84
+ Pure code change in three files with no persistent state: revert the commit, ship as the next patch. During a rollback window the symptom is exactly the pre-fix behavior (blank remote scrollback, no post-hiccup recovery) — degraded, not destructive. No data migration, no agent state repair, no template/hook migration (dashboard/index.html ships in the npm package and replaces wholesale on update).
85
+
86
+ ---
87
+
88
+ ## Conclusion
89
+
90
+ Review produced one design adjustment: the local history-miss error was originally left sessionless; the relay analysis (§5 fan-out drops sessionless frames) surfaced that it could never travel cross-machine, so it now carries `session` + `code` — that is also what makes the dashboard render it honestly instead of burying it in the console. Phase-5 second-pass assessment: the change touches none of the listed high-risk surfaces (no outbound/inbound messaging block decisions, no session spawn/restart/kill/recovery, no compaction, no coherence gates/idempotency/trust, no sentinel/guard/gate/watchdog logic — the one gate in this subsystem, remote-input default-off, is untouched pass-through), so a dedicated reviewer subagent is not required; the artifact stands on the three-tier test evidence. Clear to ship.
91
+
92
+ **Phase 1 principle check (recorded):** the change involves no new decision point that gates information flow or constrains behavior — it repairs the data plane of an approved feature to match its converged spec (§2.2 capture-on-owner, §2.4 failure honesty, P19 bounded reconnect). Signal-vs-authority applies as a constraint check only: confirmed no brittle logic gained blocking authority.
93
+
94
+ **Phase 2 plan (recorded):** built in a FRESH worktree `.worktrees/fix-pool-stream-cross-machine` off `JKHeadley/main` @ `60c4e3a3c` (v1.3.484), canonical remote verified (`JKHeadley → github.com/JKHeadley/instar.git`), per-worktree identity `Instar Agent (echo) <echo@instar.local>`. Acceptance criteria: (a) new unit tests fail on unfixed code for the stated reasons — verified (5 failures: missing relayFrame ×2, ignored re-subscribe ×2 → silent blank, sessionless error ×1); (b) all green with fix; (c) full suite green; (d) live verify on the real laptop+Mini after deploy. Rollback: revert commit (see §7).