instar 1.3.985 → 1.3.986
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/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.3.986.md +153 -0
- package/upgrades/side-effects/booting-pane-read-as-ready.md +222 -0
|
@@ -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.
|