dsh-vibe-math 2.8.2 → 2.8.3

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.
@@ -121,6 +121,10 @@
121
121
  **所办必须先与全所交流、商讨、优化、审查**:至少 1 条所办消息(`vibe_v5_message`)**且**至少 1 次会议
122
122
  (`vibe_v5_meeting`),结论写进定稿说明,然后调用 `vibe_v5_finalize_paper`。
123
123
  两个计数记在 `paper.meta.json`;缺一即 `V5_PAPER_CONSULT_REQUIRED`(不是静默跳过)。
124
+ - **`force` = 一张新论文**:`/vN paper … force` **重写**已定稿的论文 ⇒ 新的 `paper` 状态以
125
+ `consult = {messages: 0, meetings: 0}` 开始,**重启前的消息与会议不再计入**(这是设计,不是丢状态 ✗)。
126
+ 因此 `paperEditor="office"` 在 `force` 之后必须**重新**征询;`V5_PAPER_CONSULT_REQUIRED` 的响应会**具名**说明
127
+ 这一点(`restarted: true` + `forceReason`),不再让人靠猜。
124
128
 
125
129
  ## 8. LaTeX 检测、编译与降级
126
130
 
@@ -8,6 +8,7 @@ Chinese counterpart of this index is [`README.md`](./README.md) (中文索引).
8
8
 
9
9
  | Version | Key change | Notes |
10
10
  |---|---|---|
11
+ | 2.8.3 | Patch: a speech arriving **after the meeting closed** is no longer dropped silently (it becomes a named "late note" in the minutes, which stay append-only); an office finalisation refused after a `force` restart now **explains why** (`restarted: true` + `forceReason`, stating that the counters restarted with the new paper and that a fresh consultation is required); the docs now state "`force` = a new paper". | [中文](./RELEASE-NOTES-2.8.3.md) · [English](./RELEASE-NOTES-2.8.3.en.md) |
11
12
  | 2.8.2 | Patch: an explicit `paperLatexCommand` is now probed **alone**, so `triedPaths` no longer reports impossible paths of the form "known TeX root + absolute path"; v5's delivered `paper.md` / `paper.tex` now also **each state** the compile outcome (no engine detected / compilation failed), exactly once and without duplication on re-entry — matching v4 since 2.8.1. Both have real-host reproductions and named-red guards. | [中文](./RELEASE-NOTES-2.8.2.md) · [English](./RELEASE-NOTES-2.8.2.en.md) |
12
13
  | 2.8.1 | Patch: v4's delivered `paper.md` and `paper.tex` now each state the compile outcome (no engine detected → tex+md only, or PDF compilation failed with tex/md kept), **exactly once** and idempotently — resolving known limitation #3 of the 2.8.0 notes; added `scripts/release-check.mjs`, which runs the pre-release checks as one command (version triple, section order of both notes, manifest registration, declared-but-missing = 0, repository-only files not shipped, no CRLF in the package). | [中文](./RELEASE-NOTES-2.8.1.md) · [English](./RELEASE-NOTES-2.8.1.en.md) |
13
14
  | 2.8.0 | Feature: an explicit `paperLatexCommand` now means "use only this" (v4/v5 no longer fall back silently), the missing-engine warning is actionable and `triedPaths` is recorded, and all four presets share one byte-identical missing-engine guidance block; the test gate is steadier (default concurrency 2, stale scratch swept at start-up, read-only warning for suspected leftover processes); repository line endings are unified on LF (byte-reproducible npm package). | [中文](./RELEASE-NOTES-2.8.0.md) · [English](./RELEASE-NOTES-2.8.0.en.md) |
@@ -7,6 +7,7 @@
7
7
 
8
8
  | 版本 | 关键变化 | 说明 |
9
9
  |---|---|---|
10
+ | 2.8.3 | 补丁:会议**收束后到达的发言**不再静默丢弃(作为具名「会后补记」写入纪要、只增不覆盖);`force` 重启后 `paperEditor="office"` 的定稿拒绝**说明原因**(新增 `restarted:true` + `forceReason`,并写明"计数随新论文重置、需重新征询");文档明确「`force` = 一张新论文」。 | [中文](./RELEASE-NOTES-2.8.3.md) · [English](./RELEASE-NOTES-2.8.3.en.md) |
10
11
  | 2.8.2 | 补丁:显式 `paperLatexCommand` 改为**单独探测** ⇒ `triedPaths` 不再出现「已知 TeX 根 + 绝对路径」这类**不可能路径**;v5 交付的 `paper.md`/`paper.tex` 也**各自写明**编译结果(未检测到引擎/编译失败),恰好一次且重入不重复 ⇒ 与 2.8.1 的 v4 对齐。两处均有真机复现读数与具名红守卫。 | [中文](./RELEASE-NOTES-2.8.2.md) · [English](./RELEASE-NOTES-2.8.2.en.md) |
11
12
  | 2.8.1 | 补丁:v4 交付的 `paper.md`/`paper.tex` 现在各自写明编译结果(未检测到引擎 ⇒ 只产出 tex+md,或 PDF 编译失败并保留 tex/md),**恰好一次**且幂等 ⇒ 2.8.0 说明里的「已知限制」第 3 条已解决;新增 `scripts/release-check.mjs` 把发布前逐项核对做成一条命令(版本三处一致/两份说明章节顺序/清单登记/声明缺失=0/开发专用文件不随包/包内无 CRLF)。 | [中文](./RELEASE-NOTES-2.8.1.md) · [English](./RELEASE-NOTES-2.8.1.en.md) |
12
13
  | 2.8.0 | 功能版:显式 `paperLatexCommand` 改为「指定即只用它」(v4/v5 不再静默回落)、检测不到引擎的警告可行动并新增 `triedPaths`、四套预设新增逐字一致的引擎缺失指引;测试门禁更稳(默认并发 2、自动清扫陈旧临时目录、疑似遗留进程只读告警);仓库行尾统一为 LF(npm 包逐字节可复现)。 | [中文](./RELEASE-NOTES-2.8.0.md) · [English](./RELEASE-NOTES-2.8.0.en.md) |
@@ -0,0 +1,50 @@
1
+ # dsh-vibe-math 2.8.3 — Release Notes
2
+
3
+ > Previous release: 2.8.2. This is a **patch release** fixing two v5 issues around meetings and finalisation: a speech that arrives **after the meeting closed** is no longer dropped silently (it becomes a clearly-marked late note), and an office finalisation refused after a `force` restart now **explains why** (the counters restarted with the new paper, so a fresh consultation is required). Parameter names and defaults, data formats and the directory layout are **unchanged**, the DSH support window is unchanged, and **no migration is needed**.
4
+
5
+ ## Overview
6
+
7
+ - A member's meeting speech **no longer disappears silently**: even when it arrives **after** the meeting closed (the watchdog abandons a stalled meeting; the office may finalise early) it is written into the minutes as a clearly-marked "late note", with one named line in the group chat.
8
+ - Meeting minutes are **append-only**: earlier speakers are always preserved.
9
+ - A refusal after a `force` restart is now **explainable**: the response carries `restarted: true` plus the `forceReason`, and the message states that the counters restarted with the new paper, that earlier messages/meetings no longer count, and that a fresh consultation is required.
10
+ - The documentation now states it plainly: **`force` means a new paper** (both `consult` counters start at 0), so a restart requires a **new** consultation.
11
+
12
+ ## Added
13
+
14
+ - When `vibe_v5_finalize_paper` is refused for insufficient consultation, the response now carries **`restarted`** (boolean: was this paper started by a `force` restart?).
15
+
16
+ ## Changed
17
+
18
+ - Meeting minutes gained one kind of entry: `### <member>(会后补记:该发言到达时会议已收束)` — used for speeches that arrive **after** the meeting closed; identical text submitted again is recorded only once.
19
+ - The `V5_PAPER_CONSULT_REQUIRED` message appends the reset explanation when `restarted` (**behaviour is unchanged**; it only states the cause).
20
+ - `docs/final-paper.md` §7 gained the "`force` = a new paper" rule.
21
+
22
+ ## Fixed
23
+
24
+ - Symptom: a member's speech — on a real host, the **convener's own** speech — never appeared in the minutes: the header said `- 发言顺序: r-1 → acad` while the body contained only `### r-1`; the paper's `consult.meetings` stayed 0 and finalisation was refused repeatedly. Cause: a member's end-block `input` was only consumed when a meeting was **live**, and a stalled meeting is closed by the watchdog — speeches arriving later had nowhere to go and were **dropped silently**. Such speeches are now appended as "late notes" (named, de-duplicated, appended rather than overwritten). Guards: `tests/e2e-v5-round2.test.mjs` (`★ [real1004-minutes]`), `tests/v5-institute-fixes.mutants.mjs`.
25
+ - Symptom: after a `force` restart of the paper, the office's usual finalisation was refused by `V5_PAPER_CONSULT_REQUIRED` with **no hint** that the restart had reset the counters (on a real host: **11** consecutive refusals, leaving the members to infer it). Cause: that semantics existed **only in the code** — neither the docs nor the refusal message said it. The refusal now names the reset and asks for a fresh consultation, and the docs state "`force` = a new paper". Guards: `tests/e2e-v5-round2.test.mjs` (`★ [real1004-consult]`), `tests/v5-institute-fixes.mutants.mjs` (**18/18** named reds).
26
+
27
+ ## Compatibility & Migration
28
+
29
+ - Breaking changes: **none**.
30
+ - Additive only: the `restarted` response field; one new kind of minutes entry.
31
+ - Migration: none needed; existing `paper.meta.json` files, meeting minutes and the directory layout are unchanged.
32
+ - The DSH support window is unchanged (see `engines.dsh` in `package.json`).
33
+
34
+ ## Known Limitations
35
+
36
+ - **TeX installed in a non-standard location is still not auto-discovered**: the default probe covers PATH and the documented common install roots; use an absolute path in `paperLatexCommand` otherwise.
37
+ - The shipped suites **do not cover host-level behaviour** (for example "the caller is dropped after an interrupt"); that needs a real host.
38
+ - **A root-agent question blocks the whole institute** (operational note, product unchanged): nothing else proceeds until the answer arrives — state the problem and acceptance criteria up front for unattended runs. See [`slv-playbook.md`](./slv-playbook.md) §5.
39
+ - After a meeting is abandoned by the watchdog, the minutes keep the abandonment marker and **all earlier speeches**; speeches that arrive later land in the "late note" section.
40
+
41
+ ## Verification
42
+
43
+ - `node tests/run-tests.mjs` — all green (**106** items: 44 suites + 62 probes).
44
+ - `node scripts/release-check.mjs` — exits 0 (version triple, section order of both notes, `files[]` registration, no CRLF in the package); after publishing, add `--registry` to compare the registry shasum with the local tarball.
45
+ - The two fixes in this release: `node tests/e2e-v5-round2.test.mjs` (`★ [real1004-minutes]`, `★ [real1004-consult]`) and `node tests/v5-institute-fixes.mutants.mjs` (**18/18** named reds).
46
+ - Real-host re-check recipe: install the exact published version (`-Pin <version>`) and run a **trimmed** meeting + paper task (no research), then check that the minutes contain both speakers and that a refusal after a `force` restart is named.
47
+
48
+ ## Dependencies
49
+
50
+ - No new runtime dependencies; the DSH support window is unchanged.
@@ -0,0 +1,50 @@
1
+ # dsh-vibe-math 2.8.3 — 发布说明
2
+
3
+ > 上一版本:2.8.2。本版是**补丁版**,修两处 v5 的会议与定稿问题:**会议收束后到达的发言不再被静默丢弃**(改成具名的"会后补记");`force` 重启后 `paperEditor="office"` 的定稿拒绝会**说明原因**(计数已随新论文重置、需重新征询)。参数名与默认值、数据格式与目录结构**未变**,DSH 支撑窗口不变,**无需迁移**。
4
+
5
+ ## 概述
6
+
7
+ - 成员的会议发言**不会再无声消失**:即使发言在会议收束(看门狗放弃停滞会议,或所办提前定稿)**之后**才到达,也会作为「**会后补记**」写进会议纪要,并在群聊里具名说明一次。
8
+ - 会议纪要**只增不覆盖**:先前发言者的内容一律保留。
9
+ - `force` 重启后的定稿拒绝**可解释**:响应新增 `restarted: true` 并写明 `forceReason`,消息直接告诉你"计数随新论文重置、重启前的消息/会议不再计入、请重新征询"。
10
+ - 文档明确:**`force` = 一张新论文**(`consult` 的两个计数随之从 0 开始),因此重启后必须**重新**征询。
11
+
12
+ ## 新增
13
+
14
+ - `vibe_v5_finalize_paper` 在因征询不足被拒时,响应新增 **`restarted`** 字段(布尔;该论文是否由 `force` 重启而来)。
15
+
16
+ ## 变更
17
+
18
+ - 会议纪要新增一种条目:「会后补记」(`### <成员>(会后补记:该发言到达时会议已收束)`)——针对**会议已收束后**到达的发言;同一段文本重复提交只记一次。
19
+ - `V5_PAPER_CONSULT_REQUIRED` 的文案在 `restarted` 时追加重置说明(**行为逻辑不变**,只是把原因说清)。
20
+ - `docs/final-paper.md` §7 增补一条「`force` = 一张新论文」的口径。
21
+
22
+ ## 修复
23
+
24
+ - 症状:某位成员(真机实测中正是**会议的召集人**)的发言没有出现在会议纪要里——纪要在头部写着 `- 发言顺序: r-1 → acad`,正文却**只有 `### r-1`**;同一张论文的 `consult.meetings` 一直为 0,定稿被连续拒绝。原因:成员结束块里的 `input` 只认「**有活会议**」这一条路径,而会议一旦停滞会被看门狗收束,之后到达的发言**无处可去、静默丢弃**。现在这类发言会作为「会后补记」写入纪要(具名、去重、追加而非覆盖)。守卫:`tests/e2e-v5-round2.test.mjs`(`★ [real1004-minutes]`)、`tests/v5-institute-fixes.mutants.mjs`。
25
+ - 症状:`force` 重启论文后,所办按老流程定稿会被 `V5_PAPER_CONSULT_REQUIRED` 反复拒绝,而**提示里没有任何线索**说明"刚刚重启导致计数清零"(真机实测连续 **11** 次、只能靠成员自行推断)。原因:这一语义**只存在于代码里**,文档与拒绝文案都没写。现在拒绝会具名说明重置与"请重新征询",文档也把「`force` = 新论文」写清楚。守卫:`tests/e2e-v5-round2.test.mjs`(`★ [real1004-consult]`)、`tests/v5-institute-fixes.mutants.mjs`(**18/18** 具名红)。
26
+
27
+ ## 兼容性与迁移
28
+
29
+ - 破坏性变更:**无**。
30
+ - 纯新增:`restarted` 响应字段;纪要里一种新的条目类型。
31
+ - 迁移:无需迁移;既有 `paper.meta.json`、会议纪要与目录结构不变。
32
+ - DSH 支撑窗口不变(见 `package.json` 的 `engines.dsh`)。
33
+
34
+ ## 已知限制
35
+
36
+ - **非标准位置的 TeX 仍不会被自动发现**:默认探测只覆盖 PATH 与文档化的常见安装根;装在其它目录请用 `paperLatexCommand` 指绝对路径。
37
+ - 随包套件**不覆盖宿主层行为**(例如「中断后调用方被丢弃」),这类行为需要真机才能观察。
38
+ - **根代理提问会阻塞整所流程**(运维注意项,不改产品):答复到达前其余流程停摆;长跑时请把问题与验收条件一次说清。详见 [`slv-playbook.md`](./slv-playbook.md) §5。
39
+ - 会议「因长时间无新发言被看门狗放弃」之后,纪要里会留下放弃标记;**放弃前的发言仍保留**,之后到达的发言进「会后补记」。
40
+
41
+ ## 验证方式
42
+
43
+ - `node tests/run-tests.mjs` —— 全部通过(**106** 项:44 套件 + 62 探针)。
44
+ - `node scripts/release-check.mjs` —— 退出 0(版本三处一致/两份说明章节顺序/`files[]` 登记/包内无 CRLF);发布后加 `--registry` 比对线上 shasum 与本地 tarball。
45
+ - 本版两处修复:`node tests/e2e-v5-round2.test.mjs`(`★ [real1004-minutes]`、`★ [real1004-consult]`)与 `node tests/v5-institute-fixes.mutants.mjs`(**18/18** 具名红)。
46
+ - 真机复测方式:用 registry 上的精确版本(`-Pin <version>`)跑一个**精简**会议+论文任务(跳过研究),核对纪要是否含两位发言人、以及 force 重启后拒绝是否具名。
47
+
48
+ ## 依赖
49
+
50
+ - 无新增运行时依赖;DSH 支撑窗口不变。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-vibe-math",
3
3
  "description": "Multi-agent mathematical problem-solving & verification frameworks for DeepSeek Harness — FOUR agent presets in one install: vibe-math-v2 (probability-driven, classic: qs.json + Propos knowledge base + explorer→solver→review/debate verdict), vibe-math-v3 (THIRD-generation, classic: paper-style Markdown knowledge base with Problems/Progress/Propos/Methods/Verified + planner-agent scheduling that decides the next N actions + universal theory/method invention library + agents write their own Markdown directly via a per-file write lock), and vibe-math-v4 (FOURTH-generation: persistent self-organizing resident subagents that message & meet to decide all tasks, verify only by unanimous consensus, /compact at a context threshold, and stop only when all agree the problem is solved), and vibe-math-v5 (FIFTH-generation research institute: an academician as the organizational centre who decomposes and ASSIGNS work and chairs meetings; permanent researchers who hold the vote and may hire/fire their own temp workers; temp workers with no vote; a group chat and meetings; a durable per-recipient mailbox; a compare-and-set task DAG; and a boolean m-vote consensus rule where an object enters Verified/ only when at least m voting members agree AND every one of them returns exactly 1 or exactly 0). Installing this bundle auto-installs all four presets (v1 was removed at v2.0.0). Each preset also writes a FINAL PAPER by default when its run concludes — and does so BEFORE the run is marked complete: on v4/v5 the team co-authors its parts, merges them, cross-reviews, and the editor named by paperEditor finalises; the output is Paper/<run or institute id>/{paper.md,paper.tex,paper.pdf when a LaTeX engine is detected,paper.meta.json,paper.log.md} (params finalPaper/paperFormat/paperLanguage/paperCompilePdf/paperLatexCommand, plus paperEditor on v4/v5; see docs/final-paper.md).",
4
- "version": "2.8.2",
4
+ "version": "2.8.3",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0",
@@ -209,7 +209,9 @@
209
209
  "docs/release-notes/RELEASE-NOTES-2.8.1.md",
210
210
  "docs/release-notes/RELEASE-NOTES-2.8.1.en.md",
211
211
  "docs/release-notes/RELEASE-NOTES-2.8.2.md",
212
- "docs/release-notes/RELEASE-NOTES-2.8.2.en.md"
212
+ "docs/release-notes/RELEASE-NOTES-2.8.2.en.md",
213
+ "docs/release-notes/RELEASE-NOTES-2.8.3.md",
214
+ "docs/release-notes/RELEASE-NOTES-2.8.3.en.md"
213
215
  ],
214
216
  "license": "MIT",
215
217
  "keywords": [
@@ -239,7 +241,7 @@
239
241
  },
240
242
  "minVersion": "0.1.2-alpha.4",
241
243
  "testedVersion": "0.2.0-rc.2",
242
- "compatNote": "v2.8.2 补丁版:修 v5 的「诊断与交付物」两处。① 显式 paperLatexCommand 改为**单独探测** ⇒ paper.meta.json 的 compile.triedPaths 不再出现「已知 TeX 根 + 你的绝对路径」这类不可能路径(此前该值被当命令名去和已知安装根拼候选),真机实测:坏路径 ⇒ not-detected + triedPaths=[\"Z:/…\"];真引擎 ⇒ compiled + 真产出 paper.pdf。② v5 交付的 paper.md/paper.tex 现在各自写明编译结果(未检测到引擎 ⇒ 只产出 tex+md;编译失败 ⇒ 已保留 tex/md),恰好一次且定稿重入不重复 ⇒ 与 2.8.1 的 v4 对齐。参数名与默认值、数据格式与目录结构均未变;DSH 支撑窗口不变;无需迁移。",
244
+ "compatNote": "v2.8.3 补丁版:修 v5 的会议与定稿两处。① 会议收束后到达的成员发言不再静默丢弃 —— 看门狗放弃停滞会议(或所办提前定稿)后到达的 input 现在作为具名「会后补记」追加进纪要(按文本去重、先前发言只增不覆盖);真机实测原症状是纪要在头部写着「发言顺序: r-1 → acad」而正文只有 ### r-1。② force 重启后 paperEditor=\"office\" 的定稿拒绝现在说明原因:响应新增 restarted 字段并在文案里写明 forceReason 与「计数随新论文重置、重启前的消息/会议不再计入、请重新征询」;docs/final-paper.md §7 明确「force = 一张新论文」。参数名与默认值、数据格式与目录结构均未变;DSH 支撑窗口不变;无需迁移。",
243
245
  "compatibility": {
244
246
  "dshReleases": {
245
247
  "0.1.2-alpha.4": "compatible",
@@ -1350,6 +1350,20 @@ console.log('\n[25] paper idempotency: repeated triggers only fill missing artif
1350
1350
  const stF = await h.callTool('vibe_v5_status', {})
1351
1351
  assert(stF.paper.status === 'writing' && stF.paper.round === 1, 'the forced run restarted at round 1')
1352
1352
  }
1353
+ // real1004-consult (found on a real host with 2.8.2): `force` rewrites the paper, so the NEW paper starts with
1354
+ // consult={0,0} — intended — but the office was rejected ELEVEN times with no hint that its earlier meeting had
1355
+ // stopped counting (only the members could infer it). The refusal must NAME the reset and say what to do.
1356
+ {
1357
+ const hq = makeHost({ pluginModule })
1358
+ await hq.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1359
+ await hq.settleSpawns()
1360
+ const fq = await hq.callTool('vibe_v5_paper', { force: true, editor: 'office', reason: 'real1004 复测' })
1361
+ assert(fq.ok === true && fq.started === true, 'precondition: a forced office-editor paper starts (' + JSON.stringify(fq).slice(0, 130) + ')')
1362
+ await drivePaper(hq) // parts + cross-reviews land, so the office's finalisation reaches the CONSULT gate
1363
+ const rq = await hq.callTool('vibe_v5_finalize_paper', { decision: 'deliverable', note: 'real1004:未重新征询,预期被具名拒绝' })
1364
+ assert(rq.ok === false && rq.code === 'V5_PAPER_CONSULT_REQUIRED' && rq.restarted === true && /force/.test(String(rq.message)) && /重置/.test(String(rq.message)) && /重新/.test(String(rq.message)),
1365
+ '★ [real1004-consult] an office finalisation after a force restart NAMES the reset (restarted=true, message explains the required re-consultation): ' + JSON.stringify(rq).slice(0, 300))
1366
+ }
1353
1367
 
1354
1368
  // ---------- 26. fake LaTeX: the success path produces paper.pdf ----------
1355
1369
  console.log('\n[26] fake LaTeX compiler: success path produces paper.pdf')
@@ -1452,6 +1466,35 @@ console.log('\n[27] fake LaTeX compiler: repair path and persistent-failure degr
1452
1466
  assert(tp.length >= 1 && joined.length === 0,
1453
1467
  '★ [task-17/v5] an ABSOLUTE explicit paperLatexCommand is probed ALONE — triedPaths never joins it onto a known TeX root (triedPaths=' + JSON.stringify(tp) + ')')
1454
1468
  }
1469
+ // real1004-minutes (found on a real host with 2.8.2): a speech that arrives AFTER the meeting closed — the
1470
+ // watchdog abandons a stalled meeting and the office may finalise early — used to be dropped SILENTLY, so the
1471
+ // minutes declared a speaking order ("r-1 → acad") that never appeared in the file (only "### r-1"). It must
1472
+ // be appended as a clearly-marked late note, and the earlier speeches must survive (append-only).
1473
+ {
1474
+ const hm = makeHost({ pluginModule })
1475
+ await hm.callTool('vibe_v5_start', { problem: PROBLEM, researcherCount: 1 })
1476
+ await hm.settleSpawns()
1477
+ const mt = await hm.callTool('vibe_v5_meeting', { agenda: 'real1004 晚到发言:会议收束后仍有人发言', kind: 'sync' })
1478
+ const mid = mt && mt.meeting
1479
+ assert(!!mid, 'precondition: the meeting is convened (' + JSON.stringify(mt).slice(0, 120) + ')')
1480
+ await hm.drain(40) // everyone speaks -> the meeting finalises, meeting = null
1481
+ const mpath = join(hm.WS, 'VibeMath', 'Projects', 'default', 'Institutes', 'institute', 'Shared', 'Meetings', mid + '.md')
1482
+ const before = readFileSync(mpath, 'utf8')
1483
+ assert(/### /.test(before), 'precondition: the closed minutes already contain at least one speech: ' + before.slice(0, 140))
1484
+ const stc = await hm.callTool('vibe_v5_status', {})
1485
+ assert(!stc.meeting, 'precondition: the meeting is CLOSED (meeting=' + JSON.stringify(stc.meeting) + ') so a later speech has no live meeting')
1486
+ // A LATE speech: the member submits `input` on a LATER wake while no meeting is running (exactly what a
1487
+ // real host showed — the member kept submitting after the meeting was already closed).
1488
+ await hm.callTool('vibe_v5_message', { to: 'acad', content: 'real1004:请再确认一次你的意见。' })
1489
+ const wlate = await hm.peekWakeOf('acad', 8000)
1490
+ assert(!!wlate, 'precondition: a member is woken again after the meeting closed (so the end block is processed)')
1491
+ hm.fireEnd(wlate.childId, { input: 'real1004 晚到发言:会议收束之后我仍提交了意见。', contextPct: 20 })
1492
+ await sleep(200)
1493
+ const after = readFileSync(mpath, 'utf8')
1494
+ assert(after.includes('real1004 晚到发言') && after.includes('会后补记'),
1495
+ '★ [real1004-minutes] a speech arriving after the meeting closed is appended as a late note, never dropped silently')
1496
+ assert(after.includes('### '), 'the earlier speeches are still in the minutes (append-only, never clobbered)')
1497
+ }
1455
1498
 
1456
1499
  // ---------- 28. office-only surface + id normalisation ----------
1457
1500
  console.log('\n[28] the final-paper surface is office-only and its directory id cannot escape Paper/')
@@ -219,6 +219,22 @@ const FAMILIES = [
219
219
  to: ": ''",
220
220
  expect: /\[task-18\/v5\] the DELIVERED paper.md and paper.tex/,
221
221
  },
222
+ {
223
+ // real1004-minutes: a speech arriving after the meeting closed must be appended as a late note; dropping
224
+ // it silently is exactly what a real host showed (minutes declared "r-1 → acad" with only "### r-1").
225
+ name: 'real1004-minutes: a speech arriving after the meeting closed is dropped silently again',
226
+ from: "} else if (lastMeetingId && typeof p.input === 'string' && p.input.trim()) {",
227
+ to: '} else if (false) {',
228
+ expect: /\[real1004-minutes\] a speech arriving after the meeting closed/,
229
+ },
230
+ {
231
+ // real1004-consult: the office refusal must NAME the force reset (observed: 11 unnamed rejections on a real
232
+ // host). Dropping the clause must redden the guarding assertion by name.
233
+ name: 'real1004-consult: the office refusal stops naming the force restart',
234
+ from: 'const restarted = p.forced === true',
235
+ to: 'const restarted = false',
236
+ expect: /\[real1004-consult\] an office finalisation after a force restart NAMES the reset/,
237
+ },
222
238
  ]
223
239
  let red = 0
224
240
  for (const f of FAMILIES) { const ok = runFamily(f); if (ok) red++ }
@@ -990,6 +990,11 @@ export function apply(ctx) {
990
990
  const inflight = new Map() // childId -> turn token (dedupes duplicate subagent/end)
991
991
  let heartbeatDisposer = null
992
992
  let meeting = null // in-flight meeting round state
993
+ // real1004-minutes: the id of the MOST RECENT meeting. A speech can arrive AFTER the meeting was
994
+ // closed (the watchdog abandons a stalled meeting, and the office may finalise early); such a late
995
+ // submission used to be dropped SILENTLY, so the minutes claimed a speaking order that never appeared
996
+ // in the file. Keep the pointer so a late speech can still be appended as a clearly-marked note.
997
+ let lastMeetingId = ''
993
998
  let pendingMeeting = null // parked meeting (never preempts verification)
994
999
  let digestTimer = null
995
1000
  let lastProgressAt = now()
@@ -4971,6 +4976,7 @@ export function apply(ctx) {
4971
4976
  return idx
4972
4977
  } })
4973
4978
  const id = idx.id
4979
+ lastMeetingId = id // real1004-minutes: late speeches are appended to the most recent minutes
4974
4980
  meeting = {
4975
4981
  id, agenda: opts.agenda, kind: opts.kind || 'sync', target: opts.target || '',
4976
4982
  by: opts.by || 'office', order, inputs: {}, extras: {}, lastInputAt: now(), startedAt: now(),
@@ -5623,11 +5629,20 @@ export function apply(ctx) {
5623
5629
  }
5624
5630
  const consult = Object.assign({ messages: 0, meetings: 0 }, p.consult || {})
5625
5631
  if (!(Number(consult.messages) > 0) || !(Number(consult.meetings) > 0)) {
5632
+ // real1004-consult: `force` REWRITES the paper (docs/final-paper.md: "force 重写已定稿的论文"), so the
5633
+ // new paper starts with consult={0,0}. That is intended — but the refusal used to say nothing about it:
5634
+ // on a real host the office was rejected ELEVEN times and only the members could infer why their
5635
+ // earlier meeting had stopped counting. Name the reset, its reason, and what to do about it.
5636
+ const restarted = p.forced === true
5626
5637
  return {
5627
5638
  ok: false, code: 'V5_PAPER_CONSULT_REQUIRED',
5628
5639
  message: 'paperEditor="office" 要求所办先与全所交流、商讨、优化、审查:至少 1 条所办消息(vibe_v5_message)+ 至少 1 次会议(vibe_v5_meeting),然后才能定稿。当前:messages=' +
5629
- Number(consult.messages || 0) + ', meetings=' + Number(consult.meetings || 0),
5640
+ Number(consult.messages || 0) + ', meetings=' + Number(consult.meetings || 0) +
5641
+ (restarted
5642
+ ? '。注意:本论文由 force 重启(forceReason=' + String(p.forceReason || '') + ')⇒ consult 计数随新论文重置为 0,重启前的消息/会议不再计入,请**重新**征询(两个计数记录在 paper.meta.json)'
5643
+ : ''),
5630
5644
  consult: { messages: Number(consult.messages || 0), meetings: Number(consult.meetings || 0) },
5645
+ restarted,
5631
5646
  }
5632
5647
  }
5633
5648
  const fin = {
@@ -6808,6 +6823,20 @@ export function apply(ctx) {
6808
6823
  const rel = 'Shared/Meetings/' + meeting.id + '.md'
6809
6824
  const prev = (await readTextRel(rel)) || ('# 会议纪要|' + meeting.id + '\n\n')
6810
6825
  await writeTextRel(rel, prev + '### ' + member.id + '\n' + (text || '(无发言)') + '\n\n')
6826
+ } else if (lastMeetingId && typeof p.input === 'string' && p.input.trim()) {
6827
+ // real1004-minutes: no live meeting, yet the member submitted a speech (a real host showed exactly
6828
+ // this: `input` kept arriving after the meeting had been closed/abandoned). It used to be dropped
6829
+ // SILENTLY — the minutes then declared a speaking order that never appeared in the file (`- 发言顺序:
6830
+ // r-1 → acad` with only `### r-1` present). Append it as a clearly marked late note instead, keyed on
6831
+ // ANY wake (not only meeting wakes: the observed submissions arrived on later rounds), de-duplicated by
6832
+ // text so repeated submissions do not pile up.
6833
+ const late = String(p.input).trim()
6834
+ const rel = 'Shared/Meetings/' + lastMeetingId + '.md'
6835
+ const prev = (await readTextRel(rel)) || ('# 会议纪要|' + lastMeetingId + '\n\n')
6836
+ if (prev.indexOf(late) === -1) {
6837
+ await writeTextRel(rel, prev + '### ' + member.id + '(会后补记:该发言到达时会议已收束)\n' + late + '\n\n')
6838
+ await saveChatLine('【会议 ' + lastMeetingId + '】' + member.id + ' 的发言在会议收束后到达,已作为补记写入纪要(不再静默丢弃)。')
6839
+ }
6811
6840
  }
6812
6841
  if (p.solved !== undefined) {
6813
6842
  await writeTextRel('Shared/State-of-institute.md', [