Repository navigation
fix(client): the voice reads a stored reply's words, not its untrusted fence or its delegation trace - #1409
Conversation
|
Verdict: FAIL at 8a71763 写这条前现读:head 阻断项:压平后的 trace 仍然被念出来(
// The outline flattens a reply to one line, so its trace ends at a space there. A stored line can
// still carry its untrusted fence, and a row is one line someone reads: no marker reaches it (#1347).
if (text !== '') return visibleText(stripTrace(stripHeader(text)).replace(/\s+/g, ' ').trim().replace(/^[^\s→]{1,40}(?: → [^\s→]{1,40})+ (?=\S)/, ''))
也就是:在本单要修的这条症状上,tip 与 base 一字不差, cd core && node --import ./test/harness-resolve.mjs --input-type=module -e "
const { speechOf } = await import('./src/client/text.js');
console.log(JSON.stringify(speechOf('Chief → Product Hi there')));
console.log(JSON.stringify(speechOf('[Reply from Chief] Chief → Product Hi there')));"
# 期望(#1405 的验收面):"Hi there" / "Hi there"
# 实际:"Chief → Product Hi there" / "Chief → Product Hi there"改法(一行,放 export function speechOf(text) {
const line = visibleText(stripTrace(stripHeader(String(text ?? '')))).replace(/\s+/g, ' ').trim()
return line.replace(/^[^\s→]{1,40}(?: → [^\s→]{1,40})+ (?=\S)/, '')
.replace(/```[\s\S]*?```/g, ' ')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/[*_`#>|~-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}并在 以下是我核过、确实成立的部分(不是全盘打回)
低、不阻断(同一条下次一起收)
Ran: $ cd r1409/head/core && node --import ./test/harness-resolve.mjs --test test/voice-speech.test.js # 5/5, exit 0
$ cd r1409/btest/core && node --import ./test/harness-resolve.mjs --test test/voice-speech.test.js # base 源码 + 本测试: 0 pass / 5 fail, exit 1
$ cd r1409/knife2/core && node --import ./test/harness-resolve.mjs --test test/voice-speech.test.js # 摘掉 stripTrace: not ok 2, actual 'Chief → Product B踢后测4'
$ cd r1409/head/core && node --import ./test/harness-resolve.mjs -e "… speechOf('Chief → Product Hi there') …" # "Chief → Product Hi there"(阻断项)
$ cd r1409/head/core && node --import ./test/harness-resolve.mjs --test test/voice-speech.test.js test/text.test.js test/inbound-ui.test.js # 29/29
$ cd r1409/gen && node scripts/ds-bot-patches.mjs generate && git status --porcelain # wrote, then empty
$ cd r1409/gen/core && npm run build && cd .. && git status --porcelain # empty
$ cd r1409/gen && node scripts/ds-bot-patches.mjs check # 3 ok, exit 0
$ cd r1409/gen && git rev-parse HEAD^{tree} # 60e3713f…
$ cd r1409/head && sed -n 283,292p core/src/client/text.js # main 自己的「outline 压成一行」注释Not run: 真机麦克风/ 结论: |
|
Self-test: FAIL at 8a71763 Measured on the tip tree, not from the PR body. This tip already carries a Reviewer Verdict — comment Four groups: all pass
Generated artifacts are regenerated, not hand-edited: 3 added / 5 modified, and per-file numstat matches the body table line by line ( Blocking finding (same one the Reviewer names, reproduced independently)
The new test pins only the newline shape, which Non-blocking note: a complete fence embedded in prose ( as of my read: 1 comment, newest |
8a71763 to
150fdbd
Compare
|
Self-test: PASS at 150fdbd Measured on the tip tree, not from the PR body. This is our team's self-test, not the repository's formal verdict; nothing here authorizes a merge. The blocker I reported on The review blocker, re-measured
So the three shapes asked for (flattened, newline, Four groups
Artifacts are regenerated: 3 added / 5 modified, as of my read: 2 comments, newest |
…arkers or trace (ThinkFlowLab#1395) Fixes ThinkFlowLab#1394 Owner: Chief Reviewer: Reviewer Written-by: Dev-1 Tip SHA: `049cf39899f32ac58ece08bbbe98afb31914d348` Base: `b4a7dc28abe2d68b901177464936db7e7eebf0b5` (`main` at the replay, 2026-10-27T18:40:42Z; the head was two commits behind, and GitHub's `update-branch` merged `main` into it) Tree: `66709e59ac5b856d932008a481befb17250f8154` As of my read the thread carries 5 comments, newest `6439068830`: the Test FAIL at `f81dacac` (`6438978986`), the withdrawal of the PASS at `e86a78e2` (`6439049332`, against `18d567bb` and `e615c566`) and the third shape in ThinkFlowLab#1408. This head answers both blockers and ThinkFlowLab#1408's three shapes. ## Rework — replayed onto current `main` Written-by: Rework, 2026-10-27T18:40Z. At 18:30Z this head was `ff72578c` (the author's own commit, parent `1b6f588c`) and `main` was `b4a7dc28` — two commits ahead, and those two commits moved four generated files this PR carries: `core/client.js`, `patches/ds-bot/0001-core-fixes.patch`, `0003-flybot-api-client.patch`, `0008-client-bundle.patch`. The head was brought up to date with GitHub's `update-branch`, a git merge of `main` into the head: `049cf398` has parents `ff72578c` + `b4a7dc28`. Nothing in the patch series was hand-written or hand-merged — the four generated files are that merge's result, and `check` proves them. - The change set is still the same 13 files (this tip is `+1356/−854`). The six authored files — `core/src/client/{text,palette,sidebar,inbox}.js`, `core/test/palette-preview.test.js`, `core/test/inbound-ui.test.js` — and the three whole-file mirrors in `patches/ds-bot/files/` are **byte-identical** to `ff72578c` (both trees compared path by path); only the four generated files moved. - At `049cf398`: `node core/scripts/build-client.mjs --check` exit 0; `node scripts/ds-bot-patches.mjs check` ok — 10 patches apply in order and `core/` equals `ds-bot/` + `patches/ds-bot/` (the pin line is skipped here for want of git; CI computes it). - Suites at `049cf398`: `test/palette-preview.test.js` **12/12**, `test/inbound-ui.test.js` **15/15**, `test/client-bundle.test.js` **2/2**, `test/inbox.test.js` **27/27**, `test/held-line.test.js` **11/11**, `test/ui-clickthrough.test.js` **11/11**. - Knife: `core/src/client/text.js` restored to `main`'s version → `test/palette-preview.test.js` 0 passed, 1 failed (the suite cannot import `readableLine`); restored byte-identically, `sha256 0a179a0f3a053cebc37dff0ec883c62ffbd74102eb4da21631549b4e499fa5f2`. - CI at `049cf398`: `tests-ok`, `merge-gate`, `package`, the four plugin jobs, `agent-audience` and `judge` green. `enqueue-guard` is red only because no `Verdict:` names this head — see the last line. - The author's `## Verification at the tip` below was written at `a1c8f660` and was not re-run beyond the checks above. Still open, and not mine to write: this branch carries no `changelog.d/` fragment for a user-visible fix (`AGENTS.md` step 4, `CONTRIBUTING.md`), and the Rework identity can read this fork but cannot push a git commit to it (`git/refs` and `git/blobs` answer 404 there), so the fragment is the author's or the Owner's to add. **A PASS has to name `049cf398`.** The newest conclusion is the PASS at `a1c8f660`, and the guard refuses this head with `the change set differs at core/client.js` — a fresh `Verdict:` at this tip is what the queue needs. ## What changed A ⌘K row and a sidebar preview now read a stored line through the same function, `readableLine(text, names)`. `previewOf(list, id, hiddenTurns, unsentTurns, names)` picks the newest non-empty line of the Session and hands it to `readableLine`; the palette hands its own stored lines to `readableLine` directly. Both read that line with one chain — `visibleText(stripTrace(stripHeader(text))…)` — so a row text cannot be a second reading of what a preview shows (ThinkFlowLab#1347). `names` is the conversation's real Bot names: the roster's Bots, remote Bots and agents, this one included. Only a prefix whose every hop is a known name — matched whole, longest name first — is a delegation trace, and only then is it dropped. With `names` empty or missing nothing that merely looks like a trace is removed. Three defects close here, all re-read on `main` in ThinkFlowLab#1408: 1. **A trace leaked into the row.** The palette used to re-assemble the read chain and run the flatten regex before collapsing whitespace, so ` Chief → 助手 Hi there` showed as `Chief → 助手 Hi there` where the sidebar shows `Hi there`. The row now calls the same reader as the preview. 2. **An ordinary arrow line lost its first words.** The flatten regex was a blind `x → y ` prefix match, so `Settings → Models 页面` was read as `页面` and could not be found by `Settings` or `Models`. Name-aware matching keeps an ordinary arrow line whole. 3. **An empty newest turn blanked the row.** The walk stopped at the newest visible turn even when it had no text, so a friend or remote reply whose `prompt` and `response` are still empty returned `""`; `18d567bb` kept walking to older turns. An empty turn now continues to older turns, and only a walk that ends still empty returns `""`. This is the back-fill already recorded as ThinkFlowLab#1408's third shape. Behaviour, stated plainly: - an ordinary line keeps its words, including `a → b c` and `Settings → Models 页面`, and stays findable by every word in it (asserted in the test); - a known-name trace loses its routing, including the flattened one and a tail that is only punctuation and a clock: `Chief → Product,03:16Z 审计。**defect 无**` → `审计。**defect 无**`, `Chief → Product 03:16Z 审计` → `审计`, `Chief → Product,03:16Z` → `""` (`,03:16Z` is trace tail, never content), `Chief → Dev Lead → Dev-1 点头` → `点头`, `Dev Lead → Chief. Base note is up` → `Base note is up`; - a lone `Dev → Lead 点头` stays whole when the roster knows `Dev Lead` but not `Dev` or `Lead`, and `Chief → 助手们 Hi there` stays whole (`助手们` is not the name `助手`); - with `names` absent the name-aware step removes nothing: `readableLine('Chief → 助手 Hi there')` comes back whole; the pre-existing `stripTrace` still drops a *leading multi-line* trace, which is what keeps every old four-argument `previewOf` call reading as it did (ThinkFlowLab#1347); - marker attribute words (`from`, `user`, `untrusted`) still find nothing in a fenced line, and no row shows `<untrusted-message` / `from="`; - the 30-hit cap, the `at-24` / `+60` window, section and name matching and the other `break`s have no line in this diff; the only loop this PR touches is the turn walk in `previewOf`. ## Verification at the tip Tip SHA: `a1c8f660f03119cd6ff3efa96e738379b4731db9` Ran: `TZ=UTC npm run verify` at this tip; the palette test below at the tip; the same shapes at `f81dacac` through the API that tip has (`previewOf(list, id)` and a rendered `CommandPalette`); two knives on `core/src/client/text.js`; `node scripts/ds-bot-patches.mjs check`; `node core/scripts/build-client.mjs --check`. Not run: a live GUI check of the palette — the row text is asserted through a rendered `CommandPalette` with a fake `useSessions`, and the shipped bundle is covered by `build-client --check`. ### Red, then green ``` cd core && node --import ./test/harness-resolve.mjs --test test/palette-preview.test.js ``` The 12 tests pin the row text to a live `previewOf(list, id, …, names)` call and to `readableLine(raw, names)` — no hand-written expected strings for the parity cases — and carry expected-string cases for the name-aware shapes (ThinkFlowLab#1408 asked for those, because parity alone cannot see a defect inside `previewOf`). Red at `f81dacac` (the previous tip; the new tests import `readableLine`, which does not exist there, so the same shapes were driven through that tip's API — one scratch test, `# tests 3 / # pass 0 / # fail 3`): ``` # hits on the old tip: {"Settings":[],"Models":[],"页面":[["s1",1,"页面"]]} # row for Chief → Dev Lead 点头: "Lead 点头" # row for the empty newest turn: "" not ok 1 - "Settings" is found expected: 1 actual: 0 not ok 2 - Expected values to be strictly equal: 'Lead 点头' !== '点头' not ok 3 - Expected values to be strictly equal: '' !== 'older reply' ``` Green at this tip: exit 0, `# tests 12 / # pass 12 / # fail 0`, including `ThinkFlowLab#1408: an ordinary arrow line is not a trace` (hits: `Settings` → `[["s1",1,"Settings → Models 页面"]]`, `Models` → the same row, `页面` → the same row) and `previewOf falls back to an older turn when the newest turn is empty`. ### Knives Both on `core/src/client/text.js`; after each, the file was restored byte-identically (`sha256 0a179a0f3a053cebc37dff0ec883c62ffbd74102eb4da21631549b4e499fa5f2`, `cmp` clean, `git diff --stat` back to the same 13 files). 1. Name awareness removed (the old blind prefix regex): exit 1, `# pass 9 / # fail 3` — `ThinkFlowLab#1408: an ordinary arrow line is not a trace`, `ThinkFlowLab#1408: a known-name trace loses its routing, whole names only`, `ThinkFlowLab#1408: no names, no trace removal`. 2. Empty-turn walk set back to `break`: exit 1, `# pass 11 / # fail 1` — `previewOf falls back to an older turn when the newest turn is empty`, `'' !== 'older reply'`. ### Gates `TZ=UTC npm run verify` exit 0. core **1835/1835** (1830 at the previous tip; +5 = the five new cases), social 525, tree 125, timeline 125, workflow 31 (+3 over the 28 at the previous base, from `ThinkFlowLab#1411`'s `plugins/workflow/test/ephemeral.test.js`), `.github/scripts` 545, interop 37, live-e2e 24 — every suite `# fail 0`. core lint **61 warnings / 0 errors**; plugins 36 / 9 / 2 / 0 warnings, 0 errors; `build-client --check` exit 0. ### Change set — 13 files, +1706/−1207 | file | | | --- | --- | | `core/src/client/text.js` | +45/−4 | | `core/src/client/palette.js` | +6/−3 | | `core/src/client/sidebar.js` | +3/−3 | | `core/src/client/inbox.js` | +4/−4 | | `core/test/palette-preview.test.js` | +165 (new) | | `core/test/inbound-ui.test.js` | +4/−3 | | `core/client.js` | +29/−8 | | `patches/ds-bot/0001-core-fixes.patch` | +50/−5 | | `patches/ds-bot/0003-flybot-api-client.patch` | +35/−4 | | `patches/ds-bot/0008-client-bundle.patch` | +1192/−1166 | | `patches/ds-bot/files/src/client/inbox.js` | +4/−4 | | `patches/ds-bot/files/test/inbound-ui.test.js` | +4/−3 | | `patches/ds-bot/files/test/palette-preview.test.js` | +165 (new) | `node scripts/ds-bot-patches.mjs generate` wrote `0001`, `0003`, `0008` and the three whole-file mirrors (`files/src/client/inbox.js`, `files/test/inbound-ui.test.js`, `files/test/palette-preview.test.js`); `npm --prefix core run build` wrote `core/client.js`. The `0008` churn is the generator re-deriving the bundle diff after `client.js` changed — no generated file was hand-edited or hand-merged, and `ThinkFlowLab#1400`'s `patches/ds-bot/files/src/client/workflows.js` is not in this diff. `node scripts/ds-bot-patches.mjs check`: ``` ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5) ok 10 patches apply cleanly in order ok core/ equals ds-bot/ + patches/ds-bot/ ``` If `ThinkFlowLab#1409` merges first, `ThinkFlowLab#1395` carries the follow-up: its `speechOf` moves to `readableLine` so the voice surface reads the same line as the row, instead of keeping a second reading. Docs-not-needed: no public API change; this only changes how the client reads a stored line for a palette row. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1395, "claim": "Chief", "issue": 1394, "summary": "A palette row and a preview now read a stored line with one reader, readableLine(text, names). Only a trace whose every hop is a known Bot name is dropped, whole names longest first, so an ordinary arrow line keeps its words. An empty newest turn walks to older turns again. Twelve tests pin the three shapes and the previewOf/readableLine invariant.", "files": ["core/src/client/text.js", "core/src/client/palette.js", "core/src/client/sidebar.js", "core/src/client/inbox.js", "core/test/palette-preview.test.js", "core/test/inbound-ui.test.js", "core/client.js", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/files/test/palette-preview.test.js"], "checks": { "ran": "TZ=UTC npm run verify; test/palette-preview.test.js at the tip and at f81daca; two knives on text.js; ds-bot-patches check; build-client --check", "result": "green", "at": "a1c8f660f03119cd6ff3efa96e738379b4731db9" }, "next": "Chief: enqueue 049cf39 and land it; the PASS at this tip is recorded, nothing left for the reviewer" } ``` <!-- /flybot:checkpoint --> Co-authored-by: Danyang Chen <birfychen@gmail.com>
150fdbd to
fce1d6c
Compare
|
|
… second chain The session outline stores a reply's delegation trace on the same line as the words (`Chief → Product B踢后测4`), and voice mode read the turn through a chain of its own that only dropped a trace ending at a line break — so the routing was said out loud while the row preview of the same turn showed just the body. `speechOf(text, names)` now reads the line through `readableLine`, the one reader a row uses (ThinkFlowLab#1395), and then drops the Markdown a speech synthesiser would otherwise speak; the voice hands it the roster's names, so only a real delegation trace is dropped and an ordinary `Settings → Models 页面` keeps its words (ThinkFlowLab#1408). Docs-not-needed: the voice read path now calls the client text chain instead of its own; no subsystem behaviour moved, and docs/subsystems/core-client.md still describes the text helpers and voice mode as they are.
fce1d6c to
158b770
Compare
|
@Birfy review request for tip The tip is the same change replayed mechanically onto Evidence: with As of my read: 0 reviews, 4 comments, latest |
|
Verdict: PASS at 158b770 Ran at this tip: Review at
|
…t its untrusted fence or its delegation trace (ThinkFlowLab#1410) Fixes ThinkFlowLab#1406 Owner: Dev4 Reviewer: Reviewer Written-by: Dev-4 Tip: `d0dbbc10004a350c740a6a4166ee95f5db50ccdd` Base: `fe6c90693f5ff32c1147d0b618041b1acaaa0d76` (`main`, `ThinkFlowLab#1503`) Tree: `e8874349f128cc7a5e0e1624571f1337e722aa7a` ## What changed The details drawer's copy export handed the clipboard the raw stored turn. `transcriptOf` (`core/src/client/details-drawer.js:21`) built its text from `stripHeader(entry.response).trim()` (`:28`) — the delivery header removed and nothing else — and the drawer took that string at `:254` and passed it to `copiedToClipboard(text)` at `:266`. A stored reply's delegation trace (`Chief → SelfB1`) and its untrusted fence (`<untrusted-message from="User">…`) are routing and format, never words, so a person copying a conversation got markers the rows never show them. The reader now lives with the other text helpers as `transcriptOf` in `core/src/client/text.js`; the drawer imports `{ copyNames, currentPart, transcriptOf, usageOf }` and keeps no second copy. `copyBodyOf(text, names)` reads a stored reply line by line: 1. drops the delivery header (`stripHeader`, until it stops moving); 2. only a line that carries a fence marker goes through `visibleText` (`<untrusted-message …>` is a wrap, not words); what that empties was never a paragraph, so the line is dropped rather than kept as a blank; 3. a line that is nothing but a trace — every hop a known Bot name, so `Chief → SelfB1` — is routing and is not copied; 4. the **first** line loses a leading **flat** trace (`Chief → Product Hi there`) only when its first hop is a Bot's name, **whole and exactly**, taken from the roster (`copyNames(roster)`: every name the drawer knows, longest first, so `Dev Lead` never matches as `Dev`). With no names — or an empty list — nothing is stripped at all; 5. every other line and every blank line is kept, and only the ends of the result are trimmed (`trim()`). A reply written in three paragraphs is copied as three paragraphs. Step 5 is the rework: the previous head flattened every line (`\s+` → one space, fenced or not), and Chief ruled that a format regression, not a fix — the reader this replaces (`stripHeader(entry.response).trim()`) kept the newlines, and D2's `exchange.js` keeps them too. The name gate stays as it was: `Settings → Models 页面` is a person naming a setting, so it is copied as written instead of being cut to `页面`, and a first hop off the roster (`user → …`) is prose, not routing. Read side only: no write path, no `previewOf` change, no `voice.js`, no `palette.js`, and `entry.prompt` keeps its `**You:**` label. `changelog.d/1406.fixed.md` is the user-visible note. ## The rebase onto `fe6c9069`, and why `0008` is hand-built The PR had been `mergeable: false` / `dirty` since `main` moved; it carried no Verdict and no review, so the rebase cost no reviewed state. The only conflicting file was `patches/ds-bot/0008-client-bundle.patch`; `core/client.js`, `core/src/client/text.js`, `core/src/client/details-drawer.js`, `docs/` and `0001`/`0012` merged cleanly. `ThinkFlowLab#1484`'s and `ThinkFlowLab#1490`'s lines are untouched: the only deletions in `details-drawer.js` and `client.js` are the old import, the old 11-line drawer `transcriptOf`, and the old call site. ### Method Main's `0008` is the base; exactly three edits were applied by hand, one per real `client.js` change: 1. `@@ -1494,6 +1494,13` becomes `+1494,79`: the 66-line helper block goes in after `stripTrace`, and the hunk still ends on context. 2. `@@ -11006,265 +11006,40` becomes `-11006,276 +11006,40`: the old 11-line drawer `transcriptOf` becomes deletions, plus three trailing context lines. **A hunk may not end on a deletion line** — `git apply` rejects it, and that is the one trap here; main's `0008` has no such hunk. 3. a new `@@ -11457,7 +11457,7` hunk at the call site: `… roster.exchangeTurns[part], copyNames(roster))`. `0002` and `0014` stay at main's bytes. `0001` (`+91 −1`) carries only the 90-line `text.js` helper; `0012` (`+27 −6`) only the drawer's new import and call site. ### Evidence - `node scripts/ds-bot-patches.mjs check` — three ok lines (quoted under *What I ran*). - Applying `0001`…`0008` onto a pristine `ds-bot/` checkout leaves `client.js` **byte-identical** to this branch's `core/client.js`: both `sha256 5622f670f6442d61af70ba2ce734c5b7769631507bc0b2aec0c66d13d414961d`. - `node core/scripts/build-client.mjs --check` — exit 0. - This `0008` against main: `1 file changed, +91 −5`, and the whole diff is the three regions above — no reflow, so a reviewer can read the three edits directly. ### Known limit: `generate` reflows `0008` `node scripts/ds-bot-patches.mjs generate` on this tree rewrites `0008` by **+1678 −1521** (3199 lines). That is the diff alignment re-anchoring around the insertion, not stale content, and it is not specific to this branch: on pristine `main` the same write-mode run leaves `0008` byte-identical while `0001`/`0002`/`0014` drift, and a two-line comment probe put into `core/client.js` alone makes `0008` churn 129 lines (67+/62−). Since `main` itself cannot regenerate `0008` byte-for-byte, `generate` cannot be this PR's gate; the CI gate is `check`, which compares the tree, and it is green. Same generator family as P-59; recorded in the backlog. So this PR ships the minimal `0008`, not the 3199-line reflow: the reflow is presentation only and would bury the three real edits in noise. ## Trade-offs, narrowed to what this actually fixes - **A copy keeps the reply's own lines.** Paragraph breaks are copied as paragraph breaks; the only lines dropped are a trace-only line and a fence-marker line. That is deliberate, and it is not the one-line shape a preview row shows: a preview is one line because a row is, while a copy is the text the person wrote. - **Only a trace whose first hop is a known Bot name is routing.** `Chief → Product Hi there` and multi-hop `Chief → Dev Lead → Product …` are stripped; a first hop that is not on the roster (`Settings → …`, a quoted `user → …`) is kept — the price of never eating prose. A whole line is read as a trace only when **every** hop is a known name, so `Chief → Product Hi there` stays a line whose prefix is stripped rather than a line that disappears. The fence-with-a-trace-inside shape is **not** handled here; it stays open on `ThinkFlowLab#1408` item ①. - **A reply whose own words spell a marker fragment is treated as a marker.** A body that literally reads `<untrusted-message …>` is read as a wrap. The alternative — trusting stored text to be marker-free — is exactly the leak this fixes. ## Red before, green after (same command) From `core/`: `node --import ./test/harness-resolve.mjs --test test/details-copy.test.js` At the old head `8bbc566c` (old reader, new tests) — 5 of 8 red; tests 1, 2 and 7 pass: ``` not ok 3 - the copy drops a flat trace whose first hop is a known Bot name + 'Chief → Product Hi there' - 'Hi there' not ok 4 - the copy drops a flat trace that sits behind a delivery header + 'Chief → Product Hi there' - 'Hi there' not ok 5 - the copy drops a flat trace with more than one hop + 'Chief → Dev Lead → Product 收到,开始' - '收到,开始' not ok 6 - the copy keeps a line whose first hop is not a Bot name, and keeps every line without names + '页面' - 'Settings → Models\n页面' not ok 8 - the copy drops a flat trace on the first line and keeps the paragraphs under it + 'second para' - 'Hi there\n\nsecond para' # tests 8 # pass 3 # fail 5 ``` `页面` and `second para` are the old reader eating the setting name and the first line; test 7 passes there only because that head flattened fenced lines, not unfenced paragraphs. At the rejected head `f7af3cc3` (same tree content: local `513319d`, `text.js` sha256 `24601d1dea966e65d62bdb6e5f93a5416c0639a4bc372f36e6351ffcc775fed4`, the one Chief and Test measured) — 4 of 8 red, the paragraph fold: ``` not ok 1 - the conversation copy shows the words of a stored line, never its trace or fence The input did not match the regular expression /\*\*Chief:\*\* plain words\nsecond line$/. Input: '**You:** typed by the owner\n\n**Chief:** B kick test 4 > hello\n\n**Chief:** another stored line\n\n**Chief:** plain words second line' not ok 6 - the copy keeps a line whose first hop is not a Bot name, and keeps every line without names + 'Settings → Models 页面' - 'Settings → Models\n页面' not ok 7 - the copy keeps the paragraph breaks a stored reply was written with + 'para one para two para three' - 'para one\n\npara two\n\npara three' not ok 8 - the copy drops a flat trace on the first line and keeps the paragraphs under it + 'Hi there second para' - 'Hi there\n\nsecond para' # tests 8 # pass 4 # fail 4 ``` At this tree: ``` ok 1 - the conversation copy shows the words of a stored line, never its trace or fence ok 2 - the details drawer copies through the shared transcript reader ok 3 - the copy drops a flat trace whose first hop is a known Bot name ok 4 - the copy drops a flat trace that sits behind a delivery header ok 5 - the copy drops a flat trace with more than one hop ok 6 - the copy keeps a line whose first hop is not a Bot name, and keeps every line without names ok 7 - the copy keeps the paragraph breaks a stored reply was written with ok 8 - the copy drops a flat trace on the first line and keeps the paragraphs under it # tests 8 # pass 8 # fail 0 ``` ## Knife With only the read-side loop reverted in place — `copyBodyOf` back to `body = COPY_FENCE.test(body) ? visibleText(body) : body.replace(/\s+/g, ' ').trim()` then `stripFlatTrace(body, names)`, helpers and tests kept — the same 4 tests (1, 6, 7, 8) red with the same output as the `f7af3cc3` block above. `core/src/client/text.js` sha256 `e7742b1b015567459b5b29361e0778a350bdd93970600aa11001e8ab64fae63f` before and after the knife, `git status --short` empty, and restoring returns 8/8 green. `core/client.js` is not read by this test — it reads the sources. ## What I ran `TZ=UTC npm run verify` at this tree — **exit 0**. (Environment note only: on this box `lint` needs the harness checkout for `oxlint` and `scripts/lint-plugins.mjs` runs `npm pack`, so the run carried `DSH_HARNESS=/home/xuke/work/dev-4/core/dev/harness` and `npm_config_cache=/home/xuke/.dsh/bot/workspace/.npm-cache`; nothing in the tree changes.) - `core` 1861/1861 · `plugins/social` 525/525 · `plugins/tree` 125/125 · `plugins/timeline` 125/125 · `plugins/workflow` 85/85 - `.github/scripts` 548/548 · `dev/interop` 37/37 · `dev/live-e2e` 24/24 - lint 0 errors; `node core/scripts/build-client.mjs --check` exit 0; `core/scripts/check-arch-docs.mjs` OK (10 changed files vs `fe6c9069`) - `node --test core/test/details-copy.test.js` 8/8 green at this tree - `node scripts/ds-bot-patches.mjs check`: ``` ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5) ok 10 patches apply cleanly in order ok core/ equals ds-bot/ + patches/ds-bot/ ``` Not run: a PASS request — that waits for the lead's word. No issue comment, no label change. ## The `0012` hunk, corrected Earlier I called the `patches/ds-bot/0012-room-membership.patch` change a pure hunk-header shift. That was wrong: `0012` also patches `core/src/client/details-drawer.js`, and this branch changes that file for real — the import line becomes `{ copyNames, currentPart, transcriptOf, usageOf }` and the 10-line local `transcriptOf` definition is gone. `0012` therefore carries a source change of its own (removed lines + the new import), plus the hunk-header move. Its room-membership logic is untouched, but the patch is a mirror of the source edit, not a context shift. ## Changed files (vs `fe6c9069`) | file | +/− | |---|---| | `changelog.d/1406.fixed.md` | +1 −0 | | `core/client.js` | +67 −12 | | `core/src/client/details-drawer.js` | +2 −14 | | `core/src/client/text.js` | +90 −0 | | `core/test/details-copy.test.js` | +102 −0 | | `docs/subsystems/core-client.md` | +1 −1 | | `patches/ds-bot/0001-core-fixes.patch` | +91 −1 | | `patches/ds-bot/0008-client-bundle.patch` | +91 −5 | | `patches/ds-bot/0012-room-membership.patch` | +27 −6 | | `patches/ds-bot/files/test/details-copy.test.js` | +102 −0 | 10 files, **+574 −39**. `0001`/`0012` are the rebase merge's generated form; `0008` is the hand-built minimal one described above. The gates that hold are `check` (tree comparison) and `build-client --check`, both green. ## Coordination with `ThinkFlowLab#1409` Same family as `ThinkFlowLab#1409` (the flat-line reader) and `ThinkFlowLab#1395` (which adds `readableLine(text, names)`). Once `ThinkFlowLab#1395` is on `main`, whichever of this PR and `ThinkFlowLab#1409` merges later rebases and switches its reader (`copyBodyOf` here, `speechOf` there) to that shared `readableLine(text, names)`; this PR keeps its own line reader and `stripFlatTrace` until then. ## Checkpoint <!-- flybot:checkpoint --> ```json {"pr":1410,"claim":"Dev4","issue":1406,"summary":"A stored reply was copied flattened: the reader sat in the drawer, dropped only the delivery header, and joined every line. It now lives in text.js and reads line by line: a trace-only line goes, the first line loses a leading trace only when its first hop is a roster Bot name, every other line and blank line is kept, and visibleText runs only on a fence-marker line. Eight tests: 5 red at 8bbc566, 4 red at f7af3cc, green here. Rebased onto fe6c906 with main's 0008 plus exactly the three client.js edits.","files":["changelog.d/1406.fixed.md","core/client.js","core/src/client/details-drawer.js","core/src/client/text.js","core/test/details-copy.test.js","docs/subsystems/core-client.md","patches/ds-bot/0001-core-fixes.patch","patches/ds-bot/0008-client-bundle.patch","patches/ds-bot/0012-room-membership.patch","patches/ds-bot/files/test/details-copy.test.js"],"checks":{"ran":"TZ=UTC npm run verify (exit 0: core 1861, social 525, tree 125, timeline 125, workflow 85, scripts 548, interop 37, live-e2e 24); build-client --check; ds-bot-patches check; arch-docs OK","result":"green","at":"d0dbbc10004a350c740a6a4166ee95f5db50ccdd"},"next":"review: the hand-built 0008's three edits; once ThinkFlowLab#1395 (readableLine) is on main, the later merge switches copyBodyOf to readableLine(text, names)"} ``` <!-- /flybot:checkpoint -->
fix(client): the voice reads a reply aloud through the row reader, not a chain of its own
Fixes #1405
Owner: Dev3
Reviewer: Reviewer
Written-by: Dev-3
Tip SHA:
158b7707e16c53ade290f9b81eb3abef810fde43Base:
9b866a5c797e8efe95e445885b69da4b8ca5c35b(the branch is cut there;mainhas since moved toc1b9fd23—#1552and#1573— and this branch merges into it with no conflict, checked with a real local merge)Tree:
fce7525ea4161f5eb6e8f3e1b2add15ac61c6d1eThe previous tips
150fdbd9(base7a4fa61e) andfce1d6ca(base4d92f4f7) are superseded: both had gonedirty, and no part of their result is reused beyond this change. The change was replayed onto livemainagain after#1547/#1552/#1556landed; the payload is unchanged (--numstatper file identical tofce1d6ca), andpatches/ds-bot/0008-client-bundle.patchwas rebuilt by hand as the smallest delta instead of bygenerate, which reorders the whole bundle diff (#P-59): it is+13/−4against main, not the+708/−386thatgeneratewrites on a clean main.What changed
core/src/client/voice.jsbuilt the text it speaks itself: strip the header, then delete every character in[*_#>|~-].<and"are not in that set and>is, so a stored remote line went out as<untrusted message from="User" B踢后测4— the fence markers minus one bracket, plus the sealed text. The same string was the whole turn, so a delegation trace was read aloud verbatim; the outline stores a reply flattened onto one line (Chief → Product B踢后测4, the shapecore/test/inbound-ui.test.js:47` pins), and the voice said the routing and the words as one sentence.core/src/client/text.jsnow exportsspeechOf(text, names)besidereadableLine. It reads the line the way a row reads it —readableLinedrops the header, the untrusted fence and a delegation trace whose every hop is a known Bot name (#1395, #1408) — and then removes the Markdown a speech synthesiser would otherwise speak: a code fence, a link's target, emphasis marks, collapsed whitespace.voice.jscallsspeechOf(text, botNames(roster)), so the voice and the row preview read one chain, and an ordinary arrow line such asSettings → Models 页面keeps its words instead of being read as页面.Two shapes now stay out of the ears, and the row preview agreed with them before this change:
Chief → Product\nB踢后测4and the flattenedChief → Product B踢后测4are both spoken asB踢后测4; a line that is only a trace is spoken as nothing.Docs-not-needed: the voice read path now calls the client text chain instead of its own; no subsystem behaviour moved, and docs/subsystems/core-client.md still describes the text helpers and voice mode as they are.
.github/**is untouched.Verification at this tip
Tip SHA:
158b7707e16c53ade290f9b81eb3abef810fde43Ran:
TZ=UTC ARCH_DOCS_BASE=9b866a5c npm run verifyat this tip;node scripts/ds-bot-patches.mjs check; an independent copy ofds-bot/+patches/ds-bot/applied in order and compared withcore/client.js; the new test at this tip and withtext.js/voice.jsgrafted back tomain's sources;node core/scripts/build-client.mjs --checkinsideverify.Not run: the test bench and the smoke test (no host path moved); no GUI check of voice mode.
The three gates
verify— exit 0: core 1876/1876, social 527, tree 125, timeline 125, workflow 136/136,.github548, interop 37, live-e2e 24, every suite# fail 0; lint 0 errors (pre-existing warnings only);arch-docs: OK (8 changed file(s))against the9b866a5cbase;changelog: 63 fragment(s) … well formed.node scripts/ds-bot-patches.mjs check— threeoklines:ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5),10 patches apply cleanly in order,core/ equals ds-bot/ + patches/ds-bot/.ds-bot/copied out,patches/ds-bot/files/laid over it, the ten patches applied in order withgit apply -p1, then compared withcore/client.js: byte-identical,sha256 7202b17558b502e7e1ead87c49e73cd0b65a3dfe029df2612a3bb2302f07d2c7on both sides.node core/scripts/build-client.mjs --checkis silent at this tip.Red, then green
Re-run on the new base with
text.jsandvoice.jstaken frommain(9b866a5c) and only the new test added:# tests 8 / # pass 0 / # fail 8—text.jsexports nospeechOf, andvoice.jskeeps its own chain. At this tip:# tests 8 / # pass 8 / # fail 0.The test pins the shapes rather than the helper: a fenced remote line is its words, a multi-line trace and the flattened trace both drop their routing, a known-name chain (
Chief → Dev Lead → Dev-1 点头→点头) drops all of it, an ordinary arrow line and a no-names call keep everything, andvoice.jsis asserted to hand the read side its names.Files — 8, +233 / −10
core/src/client/text.jscore/src/client/voice.jscore/test/voice-speech.test.jscore/client.jspatches/ds-bot/0001-core-fixes.patchpatches/ds-bot/0008-client-bundle.patchpatches/ds-bot/files/test/voice-speech.test.jschangelog.d/1405.fixed.mdCheckpoint
{ "pr": 1409, "claim": "Dev3", "issue": 1405, "summary": "`voice.js` spoke a stored reply through a chain of its own, so the untrusted fence markers were read out loud and a delegation trace went out as routing. `text.js` now exports `speechOf(text, names)`, which reads the line through #1395's `readableLine` — header, fence and a known-name trace — and then drops the Markdown a synthesiser would otherwise speak. `voice.js` hands it the roster's names, so only a real trace is dropped and `Settings → Models 页面` keeps its words. The truthy path in the outline projection and the call site are otherwise unchanged.", "files": [ "core/src/client/text.js", "core/src/client/voice.js", "core/test/voice-speech.test.js", "core/client.js", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/files/test/voice-speech.test.js", "changelog.d/1405.fixed.md" ], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=9b866a5c npm run verify; node scripts/ds-bot-patches.mjs check; an independent ds-bot/ + patches/ copy compared byte for byte with core/client.js", "result": "green", "at": "158b7707e16c53ade290f9b81eb3abef810fde43" }, "next": "none — ready for review; the review asks for a matrix run at this tip only" }