instar 1.3.985 → 1.3.987

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-26T17:59:55.648Z",
5
- "instarVersion": "1.3.985",
4
+ "generatedAt": "2026-07-26T19:26:36.562Z",
5
+ "instarVersion": "1.3.987",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -1538,7 +1538,7 @@
1538
1538
  "type": "subsystem",
1539
1539
  "domain": "sessions",
1540
1540
  "sourcePath": "src/core/SessionManager.ts",
1541
- "contentHash": "4fb46f8df9bd47f899f51c13d25057a158d21b00bacec8bbecd75b3414c64b95",
1541
+ "contentHash": "3f88cbed25892776b9eaa258ff68b7eb7b8b676713ea01280899ee8f5b282e96",
1542
1542
  "since": "2025-01-01"
1543
1543
  },
1544
1544
  "subsystem:auto-updater": {
@@ -0,0 +1,153 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ **A session's startup banner — and a session's startup QUESTION — were read as an input prompt, so
9
+ a message could be typed into a pane that could not receive it, and the delivery was logged as a
10
+ success.**
11
+
12
+ `SessionManager.detectClaudePrompt` decided a pane was accepting input if the last six
13
+ non-blank lines contained `❯`, `›`, `bypass permissions`, or a mention of `/effort`,
14
+ `/model` or `/fast`. That last clause matched a bare slash-command mention **anywhere, in
15
+ any context, including prose**.
16
+
17
+ Claude Code's startup banner advertises slash commands in prose. The pane that caused the
18
+ incident carried, verbatim:
19
+
20
+ > `…Fable 5 draws down usage faster than Opus 4.8. Run /model and`
21
+ > `select Fable to use it. Learn more: https://support.claude.com/…`
22
+
23
+ so the probe matched a promotional line and declared a still-painting pane ready.
24
+
25
+ **Observed (topic 29723, 2026-07-26).** Inbound message at `17:44:49Z`, session respawned,
26
+ pointer prompt injected at `17:45:00Z` — but that session's own start hook did not complete
27
+ until `17:45:15Z`. The paste landed fifteen seconds early, was swallowed by the TUI redraw,
28
+ and the injector wrote `Injected initial message into "…" (915 chars, after stabilization
29
+ delay)`. A success line for a delivery that did not happen. The message reached the agent
30
+ only because the start hook's unanswered-message backstop re-reads recent history — an
31
+ unrelated path that happens to cover this hole and cannot be relied on to.
32
+
33
+ **The slash-command clauses are deleted, not tuned.** A first attempt kept them and required
34
+ "status-bar shape" (line-start or directly after an interpunct). Second-pass review falsified that:
35
+ the banner uses the same shape — `· /memory to free up context` and `+1 more · /status` are BANNER
36
+ lines — so the discrimination was still vocabulary, and `· /model to opt in` matched outright. The
37
+ line-start branch also reinstated the very dependency it claimed to remove: at a fixed pane width, a
38
+ copy edit shifting the wrap by ~14 characters puts the command at column 0 and the defect returns. A
39
+ signal that cannot separate an ADVERT for a command from a STATUS BAR showing one carries no
40
+ readiness information. The structural markers were widened instead — `? for shortcuts`,
41
+ `shift+tab to cycle` and `auto-accept edits` join `bypass permissions`, so a session in auto-accept
42
+ mode no longer depends on the prompt glyph being visible.
43
+
44
+ **Second defect, found by the operator: a startup question read as ready.** Claude Code paints the
45
+ same `❯` on a menu's focused option that it uses for the input box. This is strictly worse than the
46
+ banner — text typed at a banner is lost, but Enter at a menu SELECTS AN OPTION, so an arriving
47
+ message can answer a permission question on the operator's behalf. A focused menu is now its own
48
+ state, never ready.
49
+
50
+ **The verdict is no longer a boolean.** Three consumers ask this question and one of them KILLS a
51
+ live session when the answer is not-ready. "Still painting" and "waiting on an answer" want opposite
52
+ responses from it, so the probe now names which state it saw and each caller applies its own policy;
53
+ the destructive caller leaves a menu alone, waits a bounded moment for the always-on auto-resolver to
54
+ clear it, and only then treats the session as stuck.
55
+
56
+ Classification moved into a small pure module so the incident's literal pane text is a
57
+ regression fixture rather than a paraphrase.
58
+
59
+ ## What to Tell Your User
60
+
61
+ If you ever messaged me, watched a session start, and then saw it sit there doing nothing with
62
+ your message apparently lost — this was one way that happened. I was deciding I was awake by
63
+ looking for certain command names on screen, and my own startup banner mentions those command
64
+ names while advertising a feature. So I read "still starting up" as "ready", typed your message
65
+ into a screen that was still drawing itself, and recorded it as delivered.
66
+
67
+ There is a second case, sharper than the first. Sessions sometimes start by asking a question,
68
+ and the marker drawn beside the selected answer is the same one drawn for the input box. So a
69
+ session waiting on a question also read as ready. At a banner, a message typed too early is
70
+ simply lost. At a question it is not lost — pressing Enter picks an answer, so an arriving
71
+ message could have answered a permission question on your behalf. That can no longer happen.
72
+
73
+ I now wait for the actual input box, or for a footer that only exists once the app is running,
74
+ and I treat a question on screen as somewhere not to type.
75
+
76
+ One honest limit: this makes typing too early far less likely, but I still report a message as
77
+ delivered on the basis of having typed it, not on the basis of it appearing. Confirming that
78
+ typed text actually arrived is a separate change, recorded and not bundled here.
79
+
80
+ ## Summary of New Capabilities
81
+
82
+ - A pane showing only a startup banner is no longer classified as ready for input, so a message
83
+ is not injected before the session can receive it.
84
+ - A focused selection menu is recognised as its own state and is never ready, so an arriving
85
+ message cannot select an option in a question meant for the operator.
86
+ - Slash-command mentions no longer count as a readiness signal in any form — the discrimination
87
+ moved to markers that only exist once the TUI is running.
88
+ - All three permission-mode footers are recognised (`bypass permissions`, `? for shortcuts`,
89
+ `auto-accept edits`, `shift+tab to cycle`), where previously only one was — so a session in
90
+ auto-accept mode no longer depends on the prompt glyph being visible.
91
+ - The readiness verdict names which state it saw rather than answering yes/no, so the caller that
92
+ kills a stuck session no longer kills one that is merely waiting on a question.
93
+ - The readiness decision is a pure, separately-testable function, so the pane text from a real
94
+ incident is carried as a regression fixture instead of being described.
95
+
96
+ ## Evidence
97
+
98
+ **Reproduction (before),** real pane text against the pre-fix clause:
99
+
100
+ | input | pre-fix verdict | correct verdict |
101
+ |---|---|---|
102
+ | the 2026-07-26 boot banner (contains `Run /model and`) | `ready` | not ready |
103
+ | `Do you want to proceed?` + `❯ 1. Yes` / `2. No` | `ready` | menu (never ready) |
104
+ | `Do you trust the files in this folder?` + two options | `ready` | menu (never ready) |
105
+ | `Run /fast to enable faster output on this plan.` | `ready` | not ready |
106
+
107
+ **Also falsified — the intermediate "status-bar shape" attempt,** kept here because it is the
108
+ reason the clause was deleted rather than narrowed:
109
+
110
+ | input | "shape" verdict | correct verdict |
111
+ |---|---|---|
112
+ | `⚠ Fable 5 promotional access ends soon · /model to opt in` | `ready` | not ready |
113
+ | ` · /model to switch between Opus and Fable` | `ready` | not ready |
114
+ | soft-wrapped banner putting `/model` at column 0 | `ready` | not ready |
115
+ | `opus 5 • medium • /effort` (bullet separator) | not ready | ready |
116
+
117
+ **Observed after:**
118
+
119
+ | input | before | after |
120
+ |---|---|---|
121
+ | the 2026-07-26 boot banner | ready | **not ready** |
122
+ | any prose or interpunct mention of `/model`, `/fast`, `/effort` | ready | **not ready** |
123
+ | a focused startup question (3 real shapes) | ready | **menu — never typed into** |
124
+ | ordinary output listing `1.` / `2.` above the input box | ready | ready (not mistaken for a menu) |
125
+ | a single numbered line beside the prompt | ready | ready |
126
+ | drawn input box (`❯`) / codex prompt (`›`) | ready | ready |
127
+ | `⏵⏵ bypass permissions on` | ready | ready |
128
+ | `? for shortcuts` / `auto-accept edits` / `shift+tab to cycle` | **not ready** | **ready** (newly recognised) |
129
+ | banner that has since grown an input box | ready | ready |
130
+ | empty / whitespace pane | not ready | not ready |
131
+
132
+ **Both guards refuse.** Restoring the old slash-command clause fails six assertions; removing the
133
+ menu classification fails three. Both include the discrimination guard, whose only job is to prove
134
+ the probe still tells its three verdicts apart:
135
+
136
+ ```
137
+ # restoring the slash-command clause
138
+ × REGRESSION: the 2026-07-26 startup banner does NOT read as ready
139
+ × REGRESSION: a banner line in interpunct "status-bar shape" does NOT read as ready
140
+ × REGRESSION: a soft-wrapped banner putting /model at line start does NOT read as ready
141
+ × an agent echoing a slash command in its own output does NOT read as ready
142
+ × the probe discriminates > produces all three verdicts and is not stuck on one
143
+ Tests 6 failed | 12 passed (18)
144
+
145
+ # removing the menu classification
146
+ × REGRESSION: a startup question does NOT read as ready
147
+ × a menu is not ready to be typed into
148
+ × the probe discriminates > produces all three verdicts and is not stuck on one
149
+ Tests 3 failed | 15 passed (18)
150
+ ```
151
+
152
+ **Scope run:** `npx tsc --noEmit` clean; every tree-scanning test under `tests/unit` plus every
153
+ test that mocks `capture-pane` (the fixtures that feed this probe) — counts in the PR body.
@@ -0,0 +1,100 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ **When the agent-to-agent relay connection dropped, nothing recorded it — so the startup line
9
+ saying it had connected stayed the last word forever, whatever happened afterwards.**
10
+
11
+ `RelayClient` emits two connection-loss events: `disconnected` (socket close; the client retries
12
+ with exponential backoff) and `displaced` (another connection claimed this identity — **terminal**,
13
+ because `case 'displaced': shouldReconnect = false` disarms retry for the life of the process).
14
+ `ThreadlineBootstrap` subscribed to `message`, `unknown-sender` and `auto-discovered` — and to
15
+ **neither** loss event. Only `listener-daemon.ts` handled them.
16
+
17
+ **Observed (2026-07-26).** The agent could not send to a peer; two send paths refused with
18
+ `Relay not connected and local delivery unavailable`. The peer's own server reported
19
+ `ready: true, relay.connected: true` and had been listening since the previous day — the peer was
20
+ healthy. This agent reported `ready: false, relay.connected: false`, while its last written word on
21
+ the subject was `Threadline: relay connected` at `18:21:27Z`, with no disconnect line anywhere.
22
+
23
+ **The property that made this worth fixing rather than noting: the defect concealed its own cause.**
24
+ A `displaced` frame would fully explain why backoff never recovered the connection, and
25
+ `ThreadlineBootstrap` documents a known displacement race between the server's client and the
26
+ standalone listener daemon. Whether that is what fired is **unknowable**, because nothing recorded
27
+ it — the absence of evidence was produced by the defect itself.
28
+
29
+ The server now subscribes to both and records each transition, appending to
30
+ `logs/threadline-relay-events.jsonl` and exposing the latest event on the bootstrap result.
31
+
32
+ **Ordering note.** This ships observability and *not* reconnection, reversing the author's first
33
+ plan. A reconnect fix built first would have been unverifiable — there would have been no way to
34
+ observe whether it held.
35
+
36
+ ## What to Tell Your User
37
+
38
+ If I have ever been unable to reach another agent and could not tell you why, this is one reason.
39
+ My link to the shared relay could go down and leave behind a note saying it was up — not merely
40
+ silent, but actively misleading, because that startup note was the only note that could ever exist.
41
+
42
+ Two things can end that connection. An ordinary drop, which retries by itself and usually recovers.
43
+ And being displaced, where another process takes the same identity — in which case retrying is
44
+ switched off deliberately and permanently, so the link is gone until a restart. Those demand
45
+ opposite reactions from anyone reading, and both were equally invisible.
46
+
47
+ Now a drop is reported calmly and says a retry is coming, and a displacement is reported as an error
48
+ that says plainly the agent cannot send or receive until it restarts.
49
+
50
+ Two honest limits. This does not reconnect anything — a restart still fixes it, as before, and
51
+ recovery is separate work. And it cannot explain the outage that prompted it, because that evidence
52
+ was never created; what changes is that the next one is diagnosable.
53
+
54
+ ## Summary of New Capabilities
55
+
56
+ - A relay disconnection is recorded durably instead of passing silently.
57
+ - A relay *displacement* is recorded, flagged terminal, and reported at error level with its
58
+ consequence stated — that retry is disarmed and the agent cannot send or receive until restart.
59
+ - The two are distinguishable, so a reader can tell "wait for the retry" from "this is over".
60
+ - Every occurrence is appended rather than overwriting the last, so a flapping connection is
61
+ visible — the shape a reconnection bug actually takes.
62
+ - The most recent loss event is exposed on the bootstrap result, so a status surface can report
63
+ *why* the relay is down rather than only that it is.
64
+ - A connection that has never dropped records nothing and reports no last event, keeping
65
+ "never dropped" distinct from "dropped, cause unknown".
66
+
67
+ ## Evidence
68
+
69
+ **Before**, on the affected agent:
70
+
71
+ | surface | reading |
72
+ |---|---|
73
+ | peer's own server | `ready: true`, `relay.connected: true`, listening since 2026-07-25 |
74
+ | this agent | `ready: false`, `relay.connected: false` |
75
+ | this agent's log, last relay line | `Threadline: relay connected` @ `18:21:27Z` |
76
+ | disconnect / displacement lines | none — no handler existed |
77
+
78
+ **After** — every row asserted by a test:
79
+
80
+ | event | recorded | terminal | reported as | says |
81
+ |---|---|---|---|---|
82
+ | `disconnected` | yes | `false` | log | will retry with backoff |
83
+ | `displaced` | yes | `true` | **error** | retry disarmed; cannot send/receive until restart |
84
+ | never dropped | nothing written | — | — | last event is `null`, not a falsy "unknown" |
85
+ | 5 consecutive drops | 5 rows appended | — | — | flapping stays visible |
86
+ | unwritable log dir | handler does not throw | — | error | console + in-memory state still correct |
87
+ | 5,000-char reason | clamped below 400 | — | — | untrusted text is not written whole |
88
+ | null fingerprint | `"unknown"` | — | — | recorded honestly, not as empty string |
89
+
90
+ **Three guards refuse.** Each regression was introduced deliberately and the suite re-run:
91
+
92
+ ```
93
+ remove the 'displaced' subscription → 4 failed | 6 passed (10)
94
+ remove the 'disconnected' subscription → 7 failed | 3 passed (10)
95
+ collapse terminal into one verdict (false) → 4 failed | 6 passed (10)
96
+ restored → 10 passed (10)
97
+ ```
98
+
99
+ **Scope run:** `npx tsc --noEmit` clean; tree-scanning tests plus every threadline test — counts in
100
+ the PR body.
@@ -0,0 +1,222 @@
1
+ # Side-Effects Review — a startup banner, and a startup menu, were read as an input prompt
2
+
3
+ **Version / slug:** `booting-pane-read-as-ready`
4
+ **Date:** `2026-07-26`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `independent reviewer subagent — VERDICT: CONCERN. Design changed in response; see Phase 5.`
7
+
8
+ ## Summary of the change
9
+
10
+ `SessionManager.detectClaudePrompt` decided a pane was accepting input if the last
11
+ six non-blank lines contained `❯`, `›`, `bypass permissions`, `/(effort|model|fast)`,
12
+ or `(low|medium|high) · /effort`. Two independent defects:
13
+
14
+ **1 — the banner.** The slash-command clause matched a bare mention **anywhere, in
15
+ any context, including prose**. Claude Code's startup banner advertises slash
16
+ commands in prose; the incident pane carried:
17
+
18
+ > `…Fable 5 draws down usage faster than Opus 4.8. Run /model and`
19
+ > `select Fable to use it. Learn more: https://support.claude.com/…`
20
+
21
+ **2 — the menu (found by the operator).** Claude Code paints the **same `❯`** on a
22
+ menu's focused option that it uses for the input box. A session sitting on a
23
+ startup question therefore read as READY. This is strictly worse: text typed at a
24
+ banner is lost, but **Enter at a menu SELECTS AN OPTION**, so an arriving message
25
+ can answer a permission question on the operator's behalf.
26
+
27
+ **Observed (topic 29723, 2026-07-26).** Inbound `17:44:49Z` → respawn → pointer
28
+ injected `17:45:00Z` → that session's start hook completed `17:45:15Z`. The paste
29
+ landed 15s early, was swallowed by the redraw, and the injector logged
30
+ `Injected initial message into "…" (915 chars, after stabilization delay)` — a
31
+ success line for a delivery that did not happen. The message reached the agent only
32
+ via the start hook's unanswered-message backstop, an unrelated path.
33
+
34
+ **The slash-command clauses are DELETED, not tuned.** The first attempt kept them
35
+ and required "status-bar shape" (line-start or post-interpunct). Second-pass review
36
+ falsified that, and I verified each claim before accepting it:
37
+
38
+ | falsifying input | verdict under the "shape" fix |
39
+ |---|---|
40
+ | `⚠ Fable 5 promotional access ends soon · /model to opt in` | `ready` (false positive) |
41
+ | ` · /model to switch between Opus and Fable` | `ready` (false positive) |
42
+ | soft-wrapped banner putting `/model` at column 0 | `ready` (false positive) |
43
+ | `opus 5 • medium • /effort` (bullet separator) | not ready (false negative) |
44
+
45
+ The artifact's own fixture contains `· /memory to free up context` and
46
+ `+1 more · /status` — **banner** lines in the exact shape it claimed prose never
47
+ uses. So discrimination was still vocabulary wearing structure's clothes, and the
48
+ line-start branch reinstated the Anthropic-copy dependency it claimed to remove
49
+ (at a fixed pane width, a copy edit shifting the wrap ~14 chars recreates the
50
+ original defect). A signal that cannot separate an *advert for* a command from a
51
+ *status bar showing* one carries no readiness information. Deleted, and the
52
+ genuinely structural markers widened instead.
53
+
54
+ **The answer is no longer a boolean.** See §1 — one of three consumers KILLS a live
55
+ session on not-ready. `classifyPaneReadiness` returns `ready | menu | not-ready`;
56
+ `isReadyPromptTail` is retained as the façade for callers that only want "can I
57
+ type now".
58
+
59
+ ## Decision-point inventory
60
+
61
+ | point | classification | note |
62
+ |---|---|---|
63
+ | `❯` / `›` present (and not on an option line) → ready | `invariant` | Deterministic substring test. |
64
+ | permission-mode / shortcut footer present → ready | `invariant` | Widened from one marker to four, sourced from the probes already trusted for this (`SessionReaper`, interactive-pool config) rather than invented here. |
65
+ | selector glyph ON a numbered option line + ≥2 options → menu | `invariant` | Deterministic shape test, mirroring `PermissionPromptAutoResolver`'s existing definition rather than a second divergent one. |
66
+ | slash-command mention → ready | **REMOVED** | Could not distinguish advert from status bar in any formulation tried. |
67
+ | consent-dialog auto-accept | `invariant` | Unchanged, and deliberately left in `SessionManager` — it has a side effect (`send-keys`), so it does not belong in a pure classifier. Verified still running BEFORE the classifier and over all 20 captured lines. |
68
+
69
+ No judgment points. No model call.
70
+
71
+ ## 1. Over-block
72
+
73
+ **The risk is not uniform across callers, and the first draft of this section was
74
+ wrong about that.** There are three consumers:
75
+
76
+ 1. `waitForClaudeReadyWithRetry` → `handleReadyAndInject` (spawn/inject). Failure
77
+ direction: waits, then the pre-existing extended-wait and blind-inject-if-alive
78
+ fallbacks apply. Safe.
79
+ 2. `waitForClaudeReady` direct, in the Slack stuck-session path (`server.ts:8395`).
80
+ **Failure direction: it KILLS the live session and respawns.** A tightened probe
81
+ that false-negatives here destroys a live conversation. `docs/signal-vs-authority.md`
82
+ names session lifecycle as explicitly high-risk.
83
+ 3. `classifyPaneState` (new), used by (2) to tell `menu` from `not-ready`.
84
+
85
+ **Mitigations, both required by the above:**
86
+ - The positive marker set was **widened**, not just narrowed: `? for shortcuts`,
87
+ `shift+tab to cycle`, `auto-accept edits` join `bypass permissions`. Previously
88
+ only one of Claude Code's three permission-mode footers was recognised, so a
89
+ session in auto-accept mode depended entirely on `❯` being visible. That
90
+ assumption is no longer load-bearing. All four are asserted absent from the
91
+ banner fixture, so the widening costs no false positives.
92
+ - The Slack path no longer kills on `menu`. It gives the always-on auto-resolver a
93
+ bounded second window and re-checks; a menu that never clears falls through to
94
+ the pre-existing stuck path, so no message is lost.
95
+
96
+ **Residual, stated honestly:** a status bar using a separator other than U+00B7 (`•`,
97
+ box-drawing) and showing none of the four footers and no `❯` would now wait. I have
98
+ not enumerated every Claude Code rendering across widths and themes. On caller (1)
99
+ that costs a delay; on caller (2) the menu carve-out plus the widened footers are
100
+ what keep it from costing a session.
101
+
102
+ ## 2. Under-block
103
+
104
+ **Injection still reports success on having TYPED, not on arrival.**
105
+ `verifyInjection` recovers a swallowed *submit*, not a swallowed *paste*. A pane
106
+ that becomes ready and then stalls mid-paste still produces a success log for a lost
107
+ message. Deliberately **not bundled** — it changes the injector's success criterion
108
+ at every inject callsite, and folding it in would make this PR's refusal evidence
109
+ ambiguous. Recorded as a candidate, not silently deferred.
110
+
111
+ Also untouched: the probe reads the last 6 of 20 captured non-blank lines. A banner
112
+ longer than that window pushing a real prompt out of view still reads not-ready.
113
+ Unchanged behaviour; fails toward waiting on callers (1) and (3).
114
+
115
+ The menu detector requires ≥2 numbered options. A single-option prompt, or a
116
+ free-text startup question with no numbered options, is not detected as a menu.
117
+
118
+ ## 3. Level-of-abstraction fit
119
+
120
+ One pure function, no state, no I/O, below `SessionManager`. The consent-dialog
121
+ branch stayed behind because it presses keys — a classifier that mutates the pane is
122
+ not a classifier.
123
+
124
+ **A smarter component already exists and was consulted rather than duplicated.**
125
+ `PermissionPromptAutoResolver` owns approval-prompt handling (matching, auto-answer,
126
+ audit). This probe deliberately reuses its notion of a menu (selector glyph on a
127
+ numbered option line) instead of inventing a second one, and does not attempt to
128
+ answer prompts — it only declines to call a menu an input surface.
129
+
130
+ Callers of the removed clause: exactly one, verified independently by grep. No
131
+ duplicate site left holding the old semantics.
132
+
133
+ ## 4. Signal vs authority compliance
134
+
135
+ **The first draft's answer here was materially incomplete and is corrected.** It
136
+ claimed the probe "cannot block a message, refuse an action, or reach a user". False
137
+ via §1(2): a not-ready verdict at `server.ts` refuses an action and kills a session.
138
+
139
+ The *principle* is not violated — `docs/signal-vs-authority.md` scopes it to
140
+ brittle logic making judgments about **meaning**, and exempts deterministic
141
+ invariants. This is mechanics (is a glyph on screen), not meaning. But the honest
142
+ statement is: **this probe feeds a destructive authority**, which is exactly why the
143
+ verdict was widened from a boolean to a named state, so that authority can
144
+ distinguish "wedged" from "waiting on an answer" instead of treating both as kill.
145
+
146
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
147
+
148
+ No judgment points introduced. The change moves *away* from a vocabulary match whose
149
+ behaviour drifts as Anthropic edits its banner copy, toward structural markers. Note
150
+ the correction to the first draft: the intermediate "status-bar shape" design did
151
+ **not** achieve that and claimed it anyway — the claim is retracted above rather
152
+ than quietly restated.
153
+
154
+ ## 5. Interactions
155
+
156
+ - **`waitForClaudeReadyWithRetry` / `handleReadyAndInject`** — unchanged; both now
157
+ consult the corrected probe. A pane that used to pass early on the banner now
158
+ passes on the input box instead: later, and correctly.
159
+ - **`server.ts` Slack stuck-session path** — changed, see §1.
160
+ - **`PermissionPromptAutoResolver`** — complementary, not shadowed. It clears
161
+ prompts; this declines to type at them. The Slack carve-out explicitly depends on
162
+ it running (it is an always-on floor with no enable flag).
163
+ - **`StuckInputSentinel`, `SessionWatchdog`, `PromptGate`, `PresenceProxy`,
164
+ `SessionReaper`, `TriageOrchestrator`, `anthropic-interactive-pool`** — each keeps
165
+ its own independent markers for its own question ("is it stuck?", "is it idle?").
166
+ None imports `detectClaudePrompt`; none carried the removed clause. Their marker
167
+ vocabulary is now the *source* for this probe's footers rather than a fourth
168
+ divergent set.
169
+ - **`ModelSwapService`** injects literal `/model <id>` into live panes — a real
170
+ in-pane source of the removed token. Another reason the clause had to go.
171
+ - **Codex / Gemini / pi panes** — the `›` clause is unchanged; they do not print the
172
+ Claude banner.
173
+
174
+ ## 6. External surfaces
175
+
176
+ None. No route, no config key, no CLI flag, no message text, no state file. Two new
177
+ exported functions and one new public method on `SessionManager`, all internal.
178
+
179
+ ## 6b. Operator-surface quality
180
+
181
+ One new log line when the Slack path declines to kill a session on a menu, naming
182
+ the session and the reason. The pre-existing failure lines are unchanged.
183
+
184
+ Worth recording for the follow-up: the *success* line still reports typing, not
185
+ arrival — the asymmetry that hid this incident for seven hours is untouched here.
186
+
187
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
188
+
189
+ Machine-local by construction — the probe reads a tmux pane on the machine that owns
190
+ it. No replication, no lease interaction, no shared state, no generated URL. Two
191
+ machines on different versions apply their own probe to their own panes; there is no
192
+ cross-machine invariant to violate.
193
+
194
+ ## 8. Rollback cost
195
+
196
+ Low. Delete the module, restore the inline clauses, revert one `server.ts` block. No
197
+ migration, no persisted state, no config default, nothing written to an agent home.
198
+ The test file would fail on revert, which is the desired property.
199
+
200
+ ## Phase 5 — Second-pass review (independent reviewer subagent)
201
+
202
+ **VERDICT: CONCERN** — one blocking, four should-fix, one note. Every finding was
203
+ independently verified before being accepted; all were real.
204
+
205
+ | # | finding | disposition |
206
+ |---|---|---|
207
+ | 1 (blocking) | Unenumerated third consumer at `server.ts:8395` kills a live session on not-ready, so §1's "the failure direction is waiting, which is safe" and §4's "cannot refuse an action" are false for it. | **Design changed.** Verdict widened to `ready \| menu \| not-ready`; Slack path given a menu carve-out + bounded re-check; §1 and §4 rewritten. |
208
+ | 2 (should-fix) | The `^` line-start branch reinstates the Anthropic-copy dependency via wrap geometry, and carries no test weight. | **Clause deleted entirely** (both branches). |
209
+ | 3 (should-fix) | "Shape, not vocabulary" is falsified by the artifact's own fixture; `· /model to opt in` matches. | **Verified and accepted.** Clause deleted; the claim is retracted in §4b rather than restated. |
210
+ | 4 (should-fix) | "A ready pane always shows `❯`" is contradicted by four sibling probes; only one of three permission-mode footers was covered. | **Marker set widened** to four footers, sourced from those siblings. This is also the mitigation for #1. |
211
+ | 5 (should-fix) | §4 Q4 materially incomplete. | **Rewritten** above. |
212
+ | 6 (note) | Interpunct is U+00B7 only; `•` and box-drawing are false negatives. `ModelSwapService` injects `/model` into live panes. | Moot for the separator (clause deleted); the `ModelSwapService` point is recorded in §5 as further justification. |
213
+
214
+ **Independently found by the operator, in parallel:** the menu case (§ Summary 2).
215
+ Verified against three realistic startup questions, all three of which read as ready
216
+ before this change.
217
+
218
+ **Reviewer concurrence on the revised design was not re-sought.** The revision was
219
+ driven by the reviewer's own findings plus a verified operator report, and every
220
+ change is pinned by a test asserting both sides of its boundary. That is a
221
+ disclosed reduction in independence for the second iteration, not a claim of
222
+ concurrence.
@@ -0,0 +1,160 @@
1
+ # Side-Effects Review — a relay that dropped left "connected" as the last word
2
+
3
+ **Version / slug:** `relay-drop-was-unrecorded`
4
+ **Date:** `2026-07-26`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `see Phase 5`
7
+
8
+ ## Summary of the change
9
+
10
+ `RelayClient` emits two connection-loss events: `disconnected` (socket close, client
11
+ retries with backoff) and `displaced` (another connection claimed this identity —
12
+ **terminal**, because `case 'displaced': shouldReconnect = false` disarms retry for
13
+ the life of the process). `ThreadlineBootstrap` subscribed to `message`,
14
+ `unknown-sender` and `auto-discovered` — and to **neither** loss event. Verified by
15
+ grep: both return empty in that file. Only `listener-daemon.ts` handles them.
16
+
17
+ So the server logged `Threadline: relay connected (fingerprint: …)` and could log
18
+ nothing afterwards, whatever happened.
19
+
20
+ **Observed (2026-07-26, topic 29723).** The agent could not send to peer
21
+ `instar-codey`; two send paths refused with `Relay not connected and local delivery
22
+ unavailable`. The peer's own server reported `ready:true, relay.connected:true`,
23
+ listening since 2026-07-25 — the peer was fine. This agent reported
24
+ `ready:false, relay.connected:false`, while its last written word on the subject was
25
+ `relay connected` at 18:21:27Z, with no disconnect line anywhere.
26
+
27
+ **The property that makes this worth fixing rather than noting: the defect conceals
28
+ its own cause.** `displaced` would fully explain why exponential backoff never
29
+ recovered the connection. Whether a `displaced` frame arrived is **unknowable**,
30
+ because nothing recorded it. The absence of evidence is produced by the defect.
31
+ `ThreadlineBootstrap` itself documents a known "displacement race" between the
32
+ server's client and the standalone listener daemon — a candidate, **not asserted**.
33
+
34
+ This change RECORDS transitions. It does not reconnect and does not alter client
35
+ behaviour. That ordering is deliberate and is a correction to my own first plan
36
+ ("reconnect, then observe"): a reconnect fix built first would have been
37
+ unverifiable, because there would be no way to see whether it held.
38
+
39
+ ## Decision-point inventory
40
+
41
+ | point | classification | note |
42
+ |---|---|---|
43
+ | `disconnected` → record, non-terminal | `invariant` | Deterministic event subscription. |
44
+ | `displaced` → record, terminal + error-level | `invariant` | Same. `terminal` is stored explicitly rather than inferred, because "will retry" vs "will never retry" is the distinction a reader needs and they are otherwise identical. |
45
+ | reason string clamped to 300 chars | `invariant` | The reason originates off-machine; it is treated as untrusted text, never structured data. |
46
+ | audit-write failure swallowed | `invariant` | An observer must not throw out of a handler on the path it observes. |
47
+
48
+ No judgment points. No model call. Nothing gates or blocks.
49
+
50
+ ## 1. Over-block
51
+
52
+ Nothing is blocked. The observer only appends and logs; it cannot refuse a
53
+ connection, a send, or a message. The strongest available failure is noise in the
54
+ log if the relay flaps — which is information, not a block, and is the signal a
55
+ future reconnect fix will be judged against.
56
+
57
+ ## 2. Under-block
58
+
59
+ **It does not fix the outage.** The relay is still down as of writing, and this
60
+ change does not reconnect it. Restarting the server restores the connection; that is
61
+ unchanged and untouched.
62
+
63
+ **It does not identify tonight's cause.** That evidence was destroyed before it
64
+ existed. The honest claim is narrower than it looks: the *next* occurrence becomes
65
+ diagnosable, this one does not become explicable.
66
+
67
+ **It observes only the server's client.** The MCP server and the listener daemon each
68
+ construct their own client. The daemon already handles both events; the MCP path is
69
+ untouched and remains unobserved. Recorded, not silently deferred.
70
+
71
+ **A record that is never read is only half of observability.** This writes a durable
72
+ JSONL and exposes the latest event through the bootstrap result. Surfacing it on
73
+ `/threadline/status` is the obvious next step and is deliberately not bundled — this
74
+ PR's refusal evidence is about recording, and mixing a route change in would blur it.
75
+
76
+ ## 3. Level-of-abstraction fit
77
+
78
+ A pure-ish module (`fs` only), below the bootstrap, testable with a fake
79
+ `EventEmitter` and no socket. The bootstrap owns wiring; the module owns recording.
80
+
81
+ **A smarter component already exists and was consulted, not duplicated.**
82
+ `listener-daemon.ts` handles both events and writes `listener-displaced-alert.json` —
83
+ a single-slot file. This deliberately appends instead: a one-slot file cannot show a
84
+ flapping connection, which is exactly the shape a reconnect bug takes.
85
+
86
+ ## 4. Signal vs authority compliance
87
+
88
+ Pure signal. It records and logs; it holds no authority over anything. It cannot
89
+ block a send, refuse a connection, or influence reconnection. `docs/signal-vs-authority.md`
90
+ is satisfied trivially — there is no decision to misplace.
91
+
92
+ The one risk an observer can carry is taking down what it watches. That is closed by
93
+ swallowing audit-write failures, asserted by a test.
94
+
95
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
96
+
97
+ None introduced. Every branch is a deterministic event subscription.
98
+
99
+ ## 5. Interactions
100
+
101
+ - **`listener-daemon.ts`** — already handles both events independently; unchanged.
102
+ Its single-slot alert file is left alone; this is a second, additive record.
103
+ - **`ThreadlineClient` / `RelayClient`** — unchanged. No new emissions, no altered
104
+ reconnect behaviour, no change to `shouldReconnect`.
105
+ - **The daemon-handles-relay branch** — when the daemon owns the relay, the server
106
+ never connects and the observer is never attached. Correct: nothing to observe.
107
+ - **`/threadline/status`** — unchanged in this PR (see §2).
108
+
109
+ ## 6. External surfaces
110
+
111
+ One new durable file, `logs/threadline-relay-events.jsonl`, and one optional field on
112
+ the bootstrap result. No route, no config key, no CLI flag, no message to any user.
113
+
114
+ **Content:** timestamps, event names, this agent's own fingerprint, and a clamped
115
+ reason string from the relay. No message bodies, no peer content.
116
+
117
+ ## 6b. Operator-surface quality
118
+
119
+ Two new log lines. The displacement line is error-level and states the consequence in
120
+ plain terms — that retry is disarmed and the agent cannot send or receive until
121
+ restart — rather than naming the event and leaving the reader to infer what it means.
122
+ The disconnect line is calm and says a retry is coming. That difference is the entire
123
+ point; a reader must be able to tell "wait" from "this is over".
124
+
125
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
126
+
127
+ Machine-local by design. Each machine observes its own relay client and writes its own
128
+ record; a relay connection is per-process and cannot be reasoned about remotely. No
129
+ replication, no lease interaction, no shared state, no generated URL.
130
+
131
+ ## 8. Rollback cost
132
+
133
+ Low. Delete the module, remove one import, one declaration, one call, one optional
134
+ result field. No migration, no persisted state anyone reads yet, no config default,
135
+ nothing installed into an agent home. The test would fail on revert, announcing it.
136
+
137
+ ## Phase 5 — Second-pass review
138
+
139
+ The change touches the session/dispatch family only in the sense that it observes it;
140
+ it holds no authority, gates nothing, and cannot alter connection behaviour, so the
141
+ high-risk trigger list (block/allow decisions, session lifecycle, trust) is not
142
+ engaged. Author-applied lenses, disclosed:
143
+
144
+ **Adversarial — "how would I make this useless?"** By recording to a single slot (a
145
+ flap would overwrite itself) or by letting a write failure kill the handler. Both are
146
+ closed and asserted by tests.
147
+
148
+ **"Did I fix the symptom or the cause?"** Neither — deliberately. This fixes the
149
+ *blindness*, which is a precondition for diagnosing the cause. Stated plainly in §2
150
+ rather than implied.
151
+
152
+ **"Would it have caught the incident?"** It would have recorded the transition and,
153
+ if a displacement occurred, said so at error level with the consequence spelled out.
154
+ It would not have prevented it.
155
+
156
+ **Weakest point:** §2's last item. A durable record that no status surface reads is
157
+ observability only for someone who knows the file exists. I judged the route change
158
+ worth separating to keep this PR's refusal evidence unambiguous, but a reader could
159
+ reasonably call that half a job — which is why it is written down here rather than
160
+ left for them to notice.