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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SessionManager.d.ts +15 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +26 -15
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/claudeReadinessProbe.d.ts +137 -0
- package/dist/core/claudeReadinessProbe.d.ts.map +1 -0
- package/dist/core/claudeReadinessProbe.js +181 -0
- package/dist/core/claudeReadinessProbe.js.map +1 -0
- package/dist/threadline/ThreadlineBootstrap.d.ts +9 -0
- package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.js +14 -0
- package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
- package/dist/threadline/relayConnectionObserver.d.ts +76 -0
- package/dist/threadline/relayConnectionObserver.d.ts.map +1 -0
- package/dist/threadline/relayConnectionObserver.js +86 -0
- package/dist/threadline/relayConnectionObserver.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.3.986.md +153 -0
- package/upgrades/1.3.987.md +100 -0
- package/upgrades/side-effects/booting-pane-read-as-ready.md +222 -0
- package/upgrades/side-effects/relay-drop-was-unrecorded.md +160 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"instarVersion": "1.3.
|
|
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": "
|
|
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.
|