From eccf45526cfb42e2633d0febbb255045694a309e Mon Sep 17 00:00:00 2001 From: wine-fall <62830944+wine-fall@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:54:53 +0800 Subject: [PATCH] test(director): the boot stretch hands the keyboard over [issue #145] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Roadmap line 0 no longer reproduces under its own recorded conditions. Three runs on 2026-09-04 with the real brain, a seeded persona, --plain and piped stdin, one line typed after the second beat plus a 3s settle: the line echoed every time, and runs 2 and 3 were read at the deterministic seam — settings.json gained musicEnabled:false, and src/steer-tools.ts is the only writer of that key, so the line genuinely crossed the Director's steer path. This is cannot-reproduce, NOT fixed, and the difference is kept everywhere it matters. No cause was ever found: the `settled` guard predates the August 25 loss by a week and LineQueue's take/peek semantics have not changed since, so the guard is pinned by test, never credited with the cure. The new test is green on the base revision too — a characterization pin over the hand-over, not a red-to-green regression, which the test says of itself so a later reader cannot mistake it for proof the defect is dead. guide.test.ts already pins the reader's own guard on the quit and eof arms. What was missing is the hand-over: once a pre-broadcast read has settled, the next line typed is the Director's, on a real LineQueue. Removing `settled` turns it red with the steer brain never seeing the line — the reported symptom exactly. Line 0 leaves the roadmap because nothing about it is schedulable — no repro means no red test to write and no cause to fix — and its record stays: the contract and the history in spec 01 §3.3, the disposition under "Not on this roadmap". A first line going missing again reopens it with the new repro. Line 4's eval half (#98) is now read first among the P2s. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018nkjmN4XQdDbffSRnnAaEi --- ROADMAP.md | 100 +++++++---------------------------- specs/spec01/01-core-loop.md | 13 +++++ test/director-steer.test.ts | 31 +++++++++++ 3 files changed, 63 insertions(+), 81 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index cbd80db..d62f7c0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,16 +1,15 @@ # murmur — roadmap -_Where the radio goes next. Five lines; a line is deleted when it is done, not +_Where the radio goes next. Four lines; a line is deleted when it is done, not archived. Order is the **P** column, not the row number — the numbers are names, so a line keeps its own while the order moves. This is the **direction** layer: the current build focus lives in [`specs/STATUS.md`](specs/STATUS.md). -Where a line names issues, the evidence and close condition live there; line 0 -carries its own, having been folded in from a closed issue._ +Where a line names issues, the evidence and close condition live there._ -_P0_ blocks every judgement above it · _P1_ changes what murmur **is** · -_P2_ is reliability and quality · _P3_ is distribution and not rotting. +_P1_ changes what murmur **is** · _P2_ is reliability and quality · +_P3_ is distribution and not rotting. -_Last updated: 2026-09-04 (line 2 delivered and deleted)._ +_Last updated: 2026-09-04 (lines 2 and 0 delivered and deleted)._ **This round has two goals at once, and they do not conflict:** murmur should be good enough that its own author leaves it on, *and* runnable by someone who @@ -19,89 +18,18 @@ hosted voice stays. | # | Line | What it is | This round delivers | P | Tracked as | |---|---|---|---|---|---| -| 0 | Foundations | Stop losing the listener's first line | An input path that never drops a typed line | **P0** | §0 below (the reconciliation landed; the dropped line has not) | | 1 | Sound like a DJ | Talk and music actually interleave, instead of alternating at boundaries | A track gets a lead-in, not a label; the host can interject mid-track, not only at its edges | **P1** | the lead-in and the coda landed (#199, #200) and both already speak over the ducked track; what is missing is the autonomous mid-track beat, [#163](https://github.com/wine-fall/murmur/issues/163) | | 5 | Log in to the catalogue you already have | The catalogues murmur cannot reach are the ones behind a login | An opt-in, guided login for one auth-gated source — NetEase first | **P1** | new; §5 below | | 3 | Pick well, play reliably | Candidates come from sources worth trusting, not from keyword soup | Dead stream probes down; picks back under the spec-04 budget | **P2** | [#164](https://github.com/wine-fall/murmur/issues/164), [#149](https://github.com/wine-fall/murmur/issues/149) + new | | 4 | Others can run it, and it does not rot | A second brain backend, and an eval track under the stochastic behavior | murmur runs without a Claude Code login; prompt regressions get caught by a test | **P3** (its eval half, [#98](https://github.com/wine-fall/murmur/issues/98), is P2 now that line 2 has landed unguarded) | [#89](https://github.com/wine-fall/murmur/issues/89), [#98](https://github.com/wine-fall/murmur/issues/98), [#80](https://github.com/wine-fall/murmur/issues/80), [#153](https://github.com/wine-fall/murmur/issues/153), [#102](https://github.com/wine-fall/murmur/issues/102) | -Lines 1 and 5 are the ones that change what murmur *is*. Line 0 comes first -because every by-ear judgement above it is worthless while the first thing the -listener says disappears. +Lines 1 and 5 are the ones that change what murmur *is*. Line 4's eval half +([#98](https://github.com/wine-fall/murmur/issues/98)) is read first among the +P2s: it is the only line already overdue rather than upcoming, and every prompt +edit on the stochastic surface is unguarded until it lands. --- -## 0. Foundations - -**The backlog of parallel work is resolved — and not by merging all of it.** -The beat-grounding fix landed first (#165: the four-state music union and the -anti-fabrication rules). The clock enrichment followed in #191, which took the -weekday-and-date half of #162 and **deliberately left out** the on-air track's -played/remaining seconds: a beat is composed two deep ahead of air (spec 04 -§3.3), so a countdown stamped at compose time is already false when it is -spoken, and it works against the grounding rules #165 had just added. -`MusicState` carries no progress fields for that reason. Nothing is queued -behind those two. One PR is still open, -[#168](https://github.com/wine-fall/murmur/pull/168), a conflicting -STATUS.md-only chore that blocks nothing. - -**The first line a listener types in a session is silently dropped.** Carried -here from issue #145, which is closed in favour of this line. - -It never echoes, never reaches the steer turn, and leaves no dev-log trace. The -second line behaves normally. Reproduced 3/3 on 2026-08-25 against the real -brain, with a pre-seeded `persona.md` so first-run onboarding is skipped, lines -written to stdin after the second beat aired plus a 3 s settle: - -| run | line 1 | line 2 | result | -|---|---|---|---| -| A | "turn the music off please" | "actually, speak Japanese from now on" | only line 2 echoed and acted | -| B | "hey, can you turn the music off? just talk tonight" | — | nothing echoed; no effect | -| C | "turn the music off" | "turn the music off" | only line 2 echoed; `settings.json` got `musicEnabled: false` | - -Run C is the clean isolation — identical text, only the second one lands — so -the defect is **positional**, not content- or intent-dependent. - -Suspected, **not verified**: a pre-broadcast `lineReader` (`src/guide.ts:143`) -left legitimately pending from the boot stretch consumes the first queued line -and discards it. `LineQueue.peek()` (`src/host.ts:108`) memoizes one shared -waiting promise, so a stale reader's callback can win the race and `takeLine()` -before the Director's own boundary race sees it. The `settled` guard -(`src/guide.ts:149-156`) exists for this class of loss, but covers only a read -already resolved through EOF or the quit latch — not a reader still pending -when an unrelated line arrives. - -Not yet investigated: whether the TUI front-end is affected too (all three runs -were `--plain`), and whether a real TTY behaves differently from the piped -stdin used in the repro. - -Spec: `specs/spec01/01-core-loop.md` §3.3 and -`specs/spec03/03-03-guide-harness.md` §3. The contract violated: a typed line -either reaches the Director or is consumed by a reader that is actually asking -something — never dropped. - -Done when a regression test pins it (a line pushed while a pre-broadcast reader -is pending still reaches the Director's steer path, fakes only), and a real -plain-mode run shows the **first** typed line echoing and taking effect. - -**Checked 2026-09-04 — still neither fixed nor cleared.** No fix has landed: -the `settled` guard predates the repro (it came in on 2026-08-18/19, a week -before), and `LineQueue`'s take/peek semantics have not changed since (#187 -added `hasReader()` and reworked the `IpcHost` echo flow around it on 09-02, -which the plain-mode consumption path does not go through). A stub plain-mode run with a -pre-seeded persona echoed and acted on the **first** typed line — but that path -runs with no harness, so neither the crash-report offer nor the setup -conversation opens a pre-broadcast reader, and it therefore neither reproduces -the bug nor clears it. The suspected seam also reads clean today: every `read()` -in the first run, the crash offer and the setup flow is awaited, and `settled` -is set in the race's own `finally`, so a resolved read's stale callback returns -`''` rather than taking. So treat the suspected cause as **unconfirmed** and -start from the **August 25 boot state** — a real brain, a seeded persona, piped -stdin — not from that seam. Note that the crash-report offer did not exist -then (the sentinel landed 08-31, #169/#175), so it cannot have been the reader -that ate that line; it is a *new* pre-broadcast reader worth checking on its -own, not a way back to the original. - ## 1. Sound like a DJ Two halves of one behaviour; the first has landed. @@ -231,6 +159,16 @@ account sees no change at all. scheduled as separate work items. [#44](https://github.com/wine-fall/murmur/issues/44) now closes on #202's first box: spec 13 is its durable fix, and only an ear can say whether it worked. +- **The listener's dropped first line** (issue #145, formerly line 0) — retired + on 2026-09-04 as **cannot-reproduce, not fixed**: three real `--plain` runs + (real brain, seeded persona, piped stdin, one line after the second beat) + all landed the first line, verified at the `settings.json` seam. It is off + the roadmap because there is nothing to schedule — no repro means no red + test to write and no cause to fix — not because it was solved. The contract + and this history live in `specs/spec01/01-core-loop.md` §3.3, and the + hand-over it turns on is pinned in `test/director-steer.test.ts`. A listener + losing a first line again reopens this as a line, with the new repro. + - **Doc debt** ([#104](https://github.com/wine-fall/murmur/issues/104)) and **watch items** ([#83](https://github.com/wine-fall/murmur/issues/83)) — one edit each, taken when they are in the way. #104 grew a third claim to fix: diff --git a/specs/spec01/01-core-loop.md b/specs/spec01/01-core-loop.md index d687748..faf8a57 100644 --- a/specs/spec01/01-core-loop.md +++ b/specs/spec01/01-core-loop.md @@ -142,6 +142,19 @@ Two concurrent tasks over a shared state, single event loop: returns `Steer | None`, and the interrupt handling is consolidated in one Director method rather than duplicated per segment kind. + **The contract on that line**: a typed line either reaches the Director or is + consumed by a reader that is actually asking something — never dropped. A + session in August 2026 lost its FIRST line three times running (issue #145), + positionally: identical text, only the second one landed. It has not + reproduced since — three real `--plain` runs on 2026-09-04 with the real + brain, a seeded persona and piped stdin all echoed and acted on the first + line, read at the seam (`settings.json` gained `musicEnabled: false`, which + only `src/steer-tools.ts` writes). No fix was ever identified, so the cause + is unknown rather than closed: `lineReader`'s `settled` guard was already + present when the loss happened, and it is pinned by test, not credited with + the cure. A first line that goes missing again is that defect returning, not + a new one. + On a `talkback` steer, the interjection is **prepare-then-barge-in**, not cancel-then-compose: 1. **The current segment keeps playing** — no immediate `stop()`. Cutting to diff --git a/test/director-steer.test.ts b/test/director-steer.test.ts index 13922cd..c548b06 100644 --- a/test/director-steer.test.ts +++ b/test/director-steer.test.ts @@ -7,6 +7,7 @@ import { describe, expect, it } from 'vitest' import { EveryNCadence } from '../src/cadence.ts' import type { ContextPack, SteerActions, SteerBrain } from '../src/contracts.ts' import { Director, type DirectorDeps, steerFromLine } from '../src/director.ts' +import { escPulse, lineReader, quitLatch } from '../src/guide.ts' import { COMMANDS } from '../src/ipc.ts' import { InProcessMemoryStore } from '../src/memory.ts' import { @@ -475,3 +476,33 @@ describe('end_broadcast: two-phase shutdown (spec 11 §2.1)', () => { await run }) }) + +describe('the boot stretch hands the keyboard over (issue #145)', () => { + it('a line typed after a pre-broadcast reader settled reaches the steer path', async () => { + // A boot-stretch reader — the crash-report offer, the setup conversation — + // parks on the SAME LineQueue the Director races. Once its read has + // settled, its stale wake-up must not take the next line: that line is the + // Director's. guide.test.ts pins the reader's own guard on the quit and eof + // arms; this pins the hand-over across the seam, on a real LineQueue. + // + // What this does NOT prove: issue #145's dropped first line. That loss + // happened with the `settled` guard already present and its cause was never + // found (spec 01 §3.3), so this test is green on the base revision too — + // it is a characterization pin, not a red-to-green regression. Its power is + // over the guard: delete `settled` and the steer brain stops seeing the + // line, which is what the report described. + const steer = new FakeSteer(() => 'off it goes.') + const { director, host } = build(steer, { music: false }) + const esc = escPulse() + const pending = lineReader(host, quitLatch(), esc)() + await new Promise((r) => setImmediate(r)) // the read registers on a microtask + esc.fire() + expect(await pending).toBe('') + + host.type('turn the music off') // the listener's first line, now on air + await director.run(2) + + expect(steer.calls.map((c) => c.userText)).toEqual(['turn the music off']) + expect(host.radio).toContain('off it goes.') + }) +})