Skip to content

fix(tmux): exit copy-mode before forwarding input - #5

Merged
deepcoldy merged 1 commit into
deepcoldy:masterfrom
1330482928:fix/tmux-copy-mode-input
May 14, 2026
Merged

fix(tmux): exit copy-mode before forwarding input#5
deepcoldy merged 1 commit into
deepcoldy:masterfrom
1330482928:fix/tmux-copy-mode-input

Conversation

@1330482928

@1330482928 1330482928 commented May 14, 2026

Copy link
Copy Markdown
Collaborator

issue:当用户在 Web 终端/显示输出/滚动输出后,可能会让 tmux 进入一种“我现在在看历史/选择文本/滚动”的模式,tmux pane 可能停留在 copy-mode。此时 botmux 后续通过 tmux send-keys 写入用户消息时,按键不会进入 Codex/Claude/Gemini 等真实 CLI 输入框,而是被 tmux copy-mode 处理掉。表现为飞书卡片显示“等待输入”,随后提示“发给 Codex 后没能确认提交”。

fix:在发送文本、特殊按键或粘贴缓冲区内容之前,先检查 pane_in_mode 状态,并在必要时退出复制模式。这一保护机制同时适用于受管理的 tmux 会话和被接管(adopted)的 tmux 面板。

When a pane is left in tmux copy-mode, send-keys is handled by tmux instead of the CLI running in the pane. This makes botmux accept a chat message but fail to submit it to Codex/Claude/Gemini.

Before sending text, special keys, or paste-buffer input, check pane_in_mode and cancel copy-mode if needed. Apply the guard to both managed tmux sessions and adopted tmux panes.
@1330482928
1330482928 force-pushed the fix/tmux-copy-mode-input branch from 6689480 to 559ff0f Compare May 14, 2026 10:58
@deepcoldy

Copy link
Copy Markdown
Owner

稍等我review下

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Reviewed jointly with Codex via botmux.

Summary

  • Fix direction is correct: tmux copy-mode swallows send-keys input; probing #{pane_in_mode} and issuing send-keys -X cancel is the idiomatic tmux-safe approach.
  • Both TmuxBackend (managed) and TmuxPipeBackend (adopted) are covered, addressing managed and pipe-attached panes alike.
  • Guard is invoked from sendText / sendSpecialKeys / pasteText, covering all input paths.
  • try/catch fall-through is reasonable for pane-gone / tmux-restarting cases.
  • Test coverage is good: active / inactive / probe-failure cases all asserted with correct call ordering.

Verification (run by Codex)

  • Targeted tests: 34 passing
  • tsc --noEmit passing
  • Full pnpm test has some pre-existing/environmental failures unrelated to this PR

Non-blocking nits (optional)

  1. exitCopyModeIfNeeded() is duplicated near-verbatim between the two backends (only differs by cmdTarget vs paneTarget). Could be extracted to a shared util in src/adapters/backend/.
  2. The catch block silently swallows errors — at least a logger.debug would help diagnose the "waiting for input" symptom if tmux itself fails.
  3. Each input now adds one extra tmux display-message IPC (~negligible, but worth noting in dense topic threads).

None of the above blocks the merge.

@deepcoldy
deepcoldy merged commit 1617bf6 into deepcoldy:master May 14, 2026
deepcoldy added a commit that referenced this pull request May 21, 2026
#1 grant-command strip 前导 @bot,否则 /grant /revoke 永不触发(+测试)
#2 revoke 显式禁止撤 owner 本人
#3 would_open_bot 守卫移进 RMW 锁内(防并发删空)
#4 skip_repo 给 pendingRepo+会话发起人开例外
#5 adopt_select 下拉补 canOperate
#6 email→open_id 映射按请求 raw email 建 key(大小写归一)
#7 grant 卡 store 失败不再渲染成成功
#8 /help 文案标注需 @机器人
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
P1 commit #1 (spec v0.4.1 §7) — 类型层 + schema 字段铺底,**无产品逻辑改动**。
所有运行时测试都是 `it.todo` 占位,对应 product commit (#4 / deepcoldy#5) 才打开。

## 类型 / Schema

### src/services/chat-context-store.ts
- 新增 `export type TaskType = 'prd' | 'bug' | 'misc'`
- `ChatContext.taskType?: TaskType`(向后兼容:旧 JSON 不写此字段;
  caller 没传 → undefined → JSON.stringify 不写出 → 旧 reader 不知道新字段)
- `CreateOpts.taskType?: TaskType` + `create()` body 写入

### src/services/group-creator.ts
- `CreateGroupOpts.chatContext?: { taskType / relatedRefs / activeTodoRefs
  / rules / participants / parentDigest }` — types-only
- 注释明确:(a) participants 由 caller (Playbook) 推导后传入,
  group-creator 只透传不推导;(b) idempotencyKey 故意不加,幂等主责
  完全在 Playbook + spawn-idempotency-store(v0.4 妹妹 #2)
- **body 无任何改动**(commit #4 才接 chatContext → dispatchChatCreated)

### src/im/lark/chat-created-handler.ts
- `DispatchChatCreatedOpts` 加 rich fields(relatedRefs / activeTodoRefs
  / rules / parentDigest / taskType)— types-only
- **body 无任何改动**(commit deepcoldy#5 才写入 ChatContext)

## 测试

### test/group-creator-chatcontext.test.ts(新)
- 6 tests:C1 真跑(类型 backward compat) + C2/C3/C9/C10/C-PP-pass `it.todo`
- C4/C5/C6/C7/C7b/C8(v0.1-v0.3 老 case)按 v0.4 妹妹 #2 已挪到
  spawn-idempotency-store / Playbook 测试

### test/dispatch-chat-created-rich-context.test.ts(新)
- 5 tests:1 个类型断言真跑(rich opts 可编译) + D1/D2/D3/D-PP `it.todo`

## 验证

- `pnpm tsc --noEmit` ✅
- focused 3 文件 / 63 pass + 9 todo / 0 fail
- 全量 vitest 2579 pass / 6 fail(全部跟 commit #1 无关:3 个老 CLI binary
  缺失测试 + 3 个 EMFILE 系统 fd 耗尽,错误堆栈在 `src/workflows/fanout.ts:93`
  系统 watch() 直接抛,跟 schema 改动无关)
- **向后兼容回归 e2e**(真飞书建群):
  - `botmux create-group --bot 克劳德 --name "..."` → Lark 真群建成功
  - 写出 ChatContext.json 12 个旧字段全在 + `taskType` 字段缺席
    (caller 没传 → JSON 不写)= 老 reader 完全感知不到 schema 变化
  - dashboard `/api/topology` HTTP 200,新群被收录 (originType=bot_spawned)
  - 测试群已归档清理

## 边界遵守(妹妹 review v0.1-v0.4.1 后五版定下)

- group-creator 只建群 + dispatch,不知道 idempotency 概念
- Playbook 独占任务模板 + 权限校验 + 幂等(commit deepcoldy#6 实现)
- CLI 是 IPC 薄壳(commit deepcoldy#7 实现)
- session 真凭证从 session-store 反查,CLI 不能伪造(commit deepcoldy#6/deepcoldy#8 实现)
- BOTMUX_SESSION_ID env 由 worker-spawner 唯一注入(commit deepcoldy#8 实现)

下一步: commit #2 (config mainTopicChatId + 同步 ChatTopology.rootChatId)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
P1 commit #4 (spec v0.4.1 §7) — group-creator 接入 rich chatContext **透传**
(taskType / rules / relatedRefs / activeTodoRefs / participants / parentDigest)
给 dispatchChatCreated。**body 仅多 6 行透传**,不做任何派生 / 推导 / 幂等
逻辑(幂等主责在 commit deepcoldy#6 Playbook + commit #3 idempotency-store)。

## body 改动 (src/services/group-creator.ts)

只在 dispatchChatCreated() 调用点把 opts.chatContext.* 6 个字段透传过去:

```diff
   await dispatchChatCreated({
     chatId: r.chatId,
     larkAppId: opts.creatorLarkAppId,
     originType: 'bot_spawned',
     parentChatId: opts.sourceChatId ?? null,
     purpose: opts.purpose,
+    participants: opts.chatContext?.participants,
+    relatedRefs: opts.chatContext?.relatedRefs,
+    activeTodoRefs: opts.chatContext?.activeTodoRefs,
+    rules: opts.chatContext?.rules,
+    parentDigest: opts.chatContext?.parentDigest,
+    taskType: opts.chatContext?.taskType,
   });
```

dispatchChatCreated body 仍未实现持久化 — commit deepcoldy#5 才写进 ChatContext。
本 commit dispatchChatCreated 接到字段会立刻丢(types 在 #1 接好但 body 没动),
**但**调用契约已建立、spy 可以验证字段传递。

## 测试 (test/group-creator-chatcontext.test.ts)

打开 C2/C3/C9/C10/C-PP-pass(v0.4.1 §7 spec 列的 #4 范围),全部 真跑:

| Case | 验证 |
|---|---|
| C1 (2 cases) | 类型 backward compat + 不传 chatContext 时所有 rich 字段 undefined |
| C2 | taskType/rules/relatedRefs/activeTodoRefs 转发给 dispatchChatCreated (spy) |
| C3 | parentDigest + sourceChatId 一起转发 |
| C9 (2 cases) | 不传 transferOwnerTo 不调 transferChatOwner / 传了才调 |
| C10 (2 cases) | 不传 notifyOwnerOpenId 不调 sendMessage / 传了才调 |
| C-PP-pass | participants 数组(已 Playbook 推导好)verbatim 透传给 dispatchChatCreated |

mock 了 createChat / transferChatOwner / sendMessage / bindOncall /
dispatchChatCreated — 都 spy 函数,不打真 Lark。
`pnpm vitest run test/group-creator-chatcontext.test.ts` → 9/9 pass

## 不破回归

- 全 focused tests 仍过(C1 backward compat 已断言)
- 旧 caller (/group / dashboard create) 不传 chatContext → undefined 透传 →
  dispatchChatCreated 收到的 rich 字段全 undefined → 等价 commit #1 之前行为

下一步: commit deepcoldy#5 (dispatchChatCreated body 写入 ChatContext rich fields)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
P1 commit deepcoldy#5 (spec v0.4.1 §7 / D1-D3 + D-PP) — dispatchChatCreated body
把 commit #1 接好的 rich fields (relatedRefs / activeTodoRefs / rules /
parentDigest / taskType) 真正写进 ChatContext.create(),首张 welcome 卡
就是完整内容(no "send empty card, then update" anti-pattern)。

## body 改动 (src/im/lark/chat-created-handler.ts)

```diff
   create(opts.chatId, {
     purpose: opts.purpose ?? '(待 main-bot 自动推断)',
     originType: opts.originType,
     parentChatId: opts.parentChatId ?? null,
     participants: opts.participants ?? [],
+    relatedRefs: opts.relatedRefs,
+    activeTodoRefs: opts.activeTodoRefs,
+    rules: opts.rules,
+    parentDigest: opts.parentDigest,
+    taskType: opts.taskType,
   });
```

ChatContext.create() / CreateOpts 在 commit #1 已经有承载位,本 commit 只是
把 dispatch 入参真正绑过去。

## 测试 (test/dispatch-chat-created-rich-context.test.ts)

打开 D1/D2/D3/D-PP,全部真跑(temp dir 隔离 + sendContextCard mocked):

| Case | 验证 |
|---|---|
| 类型断言 | DispatchChatCreatedOpts 编译期接受 rich fields (compile-time) |
| D1 | dispatch 后 ChatContext.read() 含 taskType/rules/relatedRefs/activeTodoRefs/parentDigest 全字段 |
| D2 | 首张 welcome card 触发 sendContextCard 一次(mock 验证) |
| D3 | 同 chatId 第二次 dispatch idempotent — ChatContext.create first-writer-wins,card 不重发 |
| D-PP | participants=三 bot 数组 → ctx.participants length=3,三 openId 都在 |

5/5 pass. tsc clean.

## 不破回归

- 旧 caller(dispatch 不传 rich fields)→ undefined 透到 create() → 老
  ChatContext 行为不变(rules / activeTodoRefs / relatedRefs / taskType 全
  undefined,inheritedFrom.parentDigest 落空字符串)
- D3 验证 first-writer-wins 仍生效,bot 重新加入 chat 不重发 welcome 卡

下一步: commit deepcoldy#6 (MainBotPlaybook + authzCheck + IPC route + P-S1-11)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
P1 commit deepcoldy#10 (spec v0.4.1 §8 验收 9 步) — 端到端真 Lark 路径验过,全套
verified;+ dashboard `/api/topology` 节点 enrich `taskType` 让前端能
按任务类别过滤渲染。

## e2e 验收记录(真飞书拉群)

**配置阶段**
- `botmux config set-main-topic oc_3dabc5b37bca8301b12783ef684fc4a5` ✅
- `botmux config get-main-topic` returns chatId ✅

**spawnSubTask 真路径**
- POST `http://127.0.0.1:7892/api/spawn-subtask`
  body `{sessionId: <claude bot session>, purpose: "e2e P1 test build", taskType: "prd", relatedRefs: [...]}`
- daemon authzCheck 通过(session.chatId=mainTopic + session.larkAppId=Claude)
- ≤ 5 秒返回 200 OK:
  ```
  {ok:true, chatId:"oc_752f4e397b84280ed64f18ff5d111049",
   isNew:true, groupName:"📋 e2e P1 test build · 05-24 21:10",
   bots:["cli_a9771799e8bb5bc3","cli_a97448b83eb8dbd6","cli_aa9aab67157d5cb2"],
   idempotencyKey:"om_x100b6e226993d480c38a355406bae87-e2e-p1-test-build"}
  ```

**ChatContext.json 完整性**
- purpose / originType=bot_spawned / inheritedFrom.parentChatId=mainTopic
- **taskType="prd"** ✅ 持久化(commit #1 schema 字段 + commit deepcoldy#5 持久化)
- **rules** 含 3 条 PRD 模板(commit deepcoldy#6 Playbook 模板生成) ✅
- **participants** 含 3 bots 各自 role: main bot / reviewer/sister / scout
  (commit deepcoldy#6 Playbook 推导 + commit deepcoldy#5 dispatch 持久化) ✅
- **activeTodoRefs** = [真 rootMessageId](authzCheck 反查 session 真凭证,
  不接受 caller 伪造) ✅
- 不含松松 open_id(不拉松松进群,求助走 root inbox P2) ✅

**Idempotency 命中**
- 第二次同 sessionId+purpose 调 → 返 `isNew=false` + 同 chatId ✅
- createGroupWithBots 只调 1 次(per-key Promise lock + atomic file cache)

**Dashboard 渲染**
- `/api/topology` 返新群,含 originType=bot_spawned + status=active +
  archivedAt=null + taskType=prd ✅ (此 commit 加 taskType enrich)

## 本 commit 实际改动

### src/core/main-bot-playbook.ts
- 修 `resolveBotIdent(key)` 从 `~/.botmux/data/bots-info.json` (cross-app
  bot catalog) + `bot-openids-<thisAppId>.json` cross-ref 读取,而**不是**
  本 daemon 的 bot-registry — 单 daemon 只 register 自己的 bot,跨 bot
  open_id 必经文件 cross-ref
- 兜底:找不到 cross-ref → fall back 到 bot 自己 botOpenId

### src/dashboard.ts
- `/api/topology` enrich 节点加 `taskType` (合并 ChatContext.taskType)
- 让前端协作面板可以按任务类别过滤/显示 (P1 dashboard filtering 用)

## 清理

- 测试群 oc_752f4e397b84280ed64f18ff5d111049 已归档
- mainTopicChatId 清回 unset(避免影响其他测试 / 等松松正式配置)

## 全 P1 commit 链

| # | Commit | 主题 |
|---|---|---|
| 1 | 2341641 | 类型契约 + ChatContext.taskType + pending tests |
| 2 | deb6bf3 | config mainTopicChatId + 同步 rootChatId |
| 3 | f2ab5ed | spawn-idempotency-store |
| 4 | 708b336 | createGroupWithBots 透传 chatContext |
| 5 | 7c01e82 | dispatchChatCreated 持久化 rich fields |
| 6 | 8d4d9f1 | MainBotPlaybook + IPC route + P-S1~11 |
| 7 | adf7cbb | CLI subtask-create + CL-1~6 |
| 8 | a550e76 | prompt 注入 + worker BOTMUX_SESSION_ID env |
| 9 | ef48bef | dashboard drawer 设为主话题按钮 |
| 10 | (本 commit) | e2e 验收 + dashboard taskType enrich |

P1 落地完成。下一步: 等松松正式配主话题 + 实战触发 + 反馈 → P2 (RootInbox 汇总回流)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
…MessageId)

P2 commit #1 (design v0.3 §2 P2) — RootInbox 持久化 progress-item 通道
(dedup + update + close)。escalation-playbook (commit #2) 把 R1-R5 push
到此 store;commit #3 接 close 语义;commit deepcoldy#5 progress-report CLI 走
同通道。

## 设计要点 (妹妹 review v0.3 决定)

- 持久化:`~/.botmux/data/root-inbox.json` (atomic tmp+rename)
- dedup key = `buildId(kind, ruleId/subChatId/slug)` 决定性,同 key
  连续触发只 update 不 insert
- 'closed' is **terminal**:close 后同 id 再 upsert 不复活 (避免老问题
  自动 reopen),妹妹 review v0.3 #1 cooldown ≠ close 配套
- `rootCardMessageId` 字段记 Lark 主话题渲染出的卡片 messageId;
  commit #2 用 Lark `updateMessage(rootCardMessageId, ...)` 编辑原卡
  (不是 reply 追加,妹妹 review v0.3 #2)
- 三种 kind: escalation / progress / request_decision;
  daily_digest 走独立通道不进 RootInbox (妹妹 review v0.3 #4)

## API

```ts
buildId({kind, ruleId/slug, subChatId})  // 决定性 id
upsertOpen({id, kind, subChatId, subChatName, ruleId?, summary})
  → {item, inserted}              // 已 closed → no-op 返 existing
setRootCardMessageId(id, msgId)   // commit #2 用
close(id)                          // 幂等
listAll() / listOpen() / lookup(id)
__clearForTesting()
```

## 测试 (test/root-inbox-store.test.ts) — 16/16 pass

- buildId 三种 kind 决定性
- upsertOpen 首次 inserted=true / 二次 inserted=false updated count++
- firstSeenAt 保持 / lastUpdatedAt bump
- close 终态:再 upsert 不复活
- close 幂等
- setRootCardMessageId 写入 / missing id 返 null
- listOpen 排除 closed / listAll 按 lastUpdatedAt desc
- atomic file write 无 .tmp 残留

## 不破回归

- 全新文件 + 测试,零现有代码改动
- 不接 sink (escalation-playbook) 也不动 — commit #2 才接

下一步: commit #2 (escalation-playbook 接 RootInbox sink + Lark updateMessage 编辑原卡)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
P2 commit #4 (design v0.3 §2 close 语义条 3) — scout tick 跑完
runEscalationRules 后,对每个 open R1/R3/R5 root-inbox item 检查同
(ruleId, chatId) 是否还在 newEscalations 里;若不在 → 条件消失 → close。

R2 / R4 不参与自动 close(聚合 / count-based,false-positive 风险高)。

## 实现

### src/core/scout-spawner.ts
- 在 enqueueEscalation 循环后加 auto-close pass:
  - 构 stillFiring = Set("R1:chatId" | "R3:chatId" | "R5:chatId")
  - 遍历 root.listOpen():
    - kind != 'escalation' 跳
    - ruleId 不是 R1/R3/R5 跳
    - subChatId 已 archived 跳(commit #3 已关,defensive)
    - 不在 stillFiring → root.close(id) + 计数
  - log info "auto-closed N items"
  - 全程 try/catch,root-inbox 失败仅 log warn 不阻塞 scout

## 测试 (test/scout-spawner-bot-spawned-filter.test.ts +1 = 6/6 pass)

新 case "P2 #4: scout auto-closes root-inbox when escalation condition stops firing":
1. 节点 summary 含 "blocked" → R5 触发
2. 手动注入 open R5:oc_x root-inbox item
3. tick 1: R5 仍触发 → 不 close(仍 open)
4. summary 改成 "CI fixed" → R5 不再触发
5. tick 2: scout 检测 stillFiring 不含 "R5:oc_x" → 自动 close
   → root.lookup status='closed'

## 不破回归

- 关联测试全过 (114 + 1 = 115)
- R2 / R4 不动(避免聚合 escalation 误关)
- root-inbox 失败包 try/catch,不阻塞 scout 主路径

下一步: commit deepcoldy#5 (botmux progress-report CLI — 主 bot 在子群完成阶段
任务时主动汇报到 root-inbox)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
…sion

P2 commit deepcoldy#5 (design v0.3 §2 P2 第 3 条) — 主 bot 在子群完阶段进展 or
需要松松决断时主动调 `botmux progress-report` → daemon → root-inbox-
publisher → 写 RootInbox + 渲染主话题卡。

## 新增

### src/services/root-inbox-publisher.ts (新文件)
- `publishProgress(opts)` — kind='progress',✅ 卡
- `publishRequestDecision(opts)` — kind='request_decision',❓ 卡
- 共享 `publishGeneric(kind, ...)`:
  - dedup id = `<kind>:<subChatId>:<slug>`
  - upsertOpen 写 RootInbox
  - mainTopic 未配 → 跳过 Lark 发卡(仍写 store,方便 dashboard 看)
  - 首发 sendMessage 到 mainTopic(interactive 卡)+ 回写 rootCardMessageId
  - 同 id 再触发 → updateMessage 编辑原卡;失败 fallback fresh send
- 卡片 emoji 区分:progress=✅,request_decision=❓,updated=🔁,closed=✅

### src/cli/progress-report.ts (新文件)
- CLI 薄壳:--session-id (env BOTMUX_SESSION_ID 自动) / --summary /
  --slug / --kind progress|request_decision / --sub-chat-id / --sub-chat-name
- POST daemon `/api/progress-report`,stdout 透传

### src/daemon.ts
- 新增 `POST /api/progress-report` IPC route:
  - 解 body → authzCheck (session 存在 + larkAppId = Claude)
  - subChatId 默认从 session.chatId 取(兵分子群里调时自动用本群)
  - 调 publisher.publishProgress / publishRequestDecision
  - HttpError → 状态码,否则 200 + 结果

### src/cli.ts
- 注册 case 'progress-report'

## 测试 (test/root-inbox-publisher.test.ts) — 5/5 pass

- mainTopic 未配 → 写 RootInbox + 不发卡 + mainTopicConfigured=false
- mainTopic 配 + 首发 → sendMessage 调到 mainTopic + 回写 rootCardMessageId
- 同 slug 第二次 → updateMessage 编辑原卡(不刷屏)
- updateMessage 失败 → fallback fresh send
- publishRequestDecision 用 kind='request_decision' 前缀的 dedup id

## 不破回归

- 现有 escalation-playbook 不动(独立 sink,未来可考虑用 publisher 替换)
- daemon 新 IPC route 不影响旧 route
- CLI 新子命令 `progress-report` 跟 `subtask-create` 一样的薄壳模式

下一步: commit deepcoldy#6 (dashboard 主话题侧渲染 root-inbox 卡片 list + close 按钮)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
P3 commit #2 — 缇蕾每 15min 调 lark-cli 拉松松所有参与 chat 的消息流,
按 [start, end) 时间窗 + chat 排除 + tilly-message-store dedup 过滤,
返回 normalized list 供 commit #3 LLM 分析。

## 设计

- `fetchRecentMessages(opts)`: spawn `lark-cli im +messages-search
  --as user --start <iso> --end <iso> --page-all --format json`
- 解 JSON 中 `data.messages[]`,normalize 成 `TillyMessage` shape:
  messageId / chatId / chatName / chatType / senderId / senderType /
  msgType / content / createTime / threadId? / appLink?
- 按 `opts.excludeChatIds` 过滤
- 调 `tilly-message-store.filterUnscanned()` 过 dedup
- 返 fresh messages(caller 处理完才调 markScanned)
- 失败抛错 — daemon cron 接住 + log + skip tick

- `groupByChat(messages)`: chat → messages map + 按 createTime asc 排
  (chronological),便于 LLM prompt 按 chat 组织上下文

## 测试 (test/tilly-scout.test.ts) — 6/6 pass

mock 一个 shell fake-lark-cli 写 JSON 到 stdout(不需要真 lark):
- 正常 fetch + normalize 字段
- dedup 已 scanned 的 message
- excludeChatIds filter
- 空消息列表
- lark-cli 退码 non-zero 抛错
- groupByChat 排序

## 不依赖

- 不分析消息内容(commit #3 LLM 才做)
- 不 push 卡片(commit #4 publisher + cron 串)
- 不调度 cron(commit deepcoldy#5 daemon 才接)

下一步: commit #3 (缇蕾 LLM worker — spawn codex exec 跑 prompt 抽 4 类 + JSON 输出)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
…illy_digest'

P3 commit #4 — 缇蕾每日 cumulative digest 持久化 + 渲染主话题汇总卡。

## 新增 src/services/tilly-digest-store.ts

- `getCurrentDigest()` — 读今日 cumulative,跨日自动 archive + reset
- `mergeNewDigest(fresh)` — 每 15min tick 出来的 4 类 incoming merge 进
  当日 list,按 sourceMessageId dedup (first-write-wins)
- `getDateId()` — YYYY-MM-DD UTC
- `totalCount(d)` — 4 类总和
- 持久化 `tilly-digest-current.json` + 7 天 archive `tilly-digest-archive.json`
- 测试 6/6 pass (rollover / merge / dedup / 持久化 / totalCount)

## 新增 src/services/tilly-publisher.ts

- `renderTillyCardContent(d)` — 4 section markdown (📝 待办 / ✅ 进展 /
  🚧 卡点 / 💡 值得记) + 头部 today date + tick 数 + 最新 tick 时间
- `publishTillyDigest(digest, opts)` — upsertOpen `tilly_digest:<dateId>`
  + sendOrUpdateCard 给 mainTopic。同一天的 tick 共编辑同张卡(不刷
  屏),跨日自动新卡。
- allowReopen=true: 若手动 close 后下次 tick 仍创新 lifecycle
- mainTopic 未配 → 静默跳过 send(store still updated)

## src/services/root-inbox-store.ts + root-inbox-card-renderer.ts

- `RootInboxKind` 加 `'tilly_digest'` to union
- KIND_EMOJI / KIND_LABEL 加 🐶 / "缇蕾每日扫读"

## 不破回归

- 5 文件 root-inbox / tilly 子模块 共 55 tests 仍全过
- escalation / progress / request_decision 旧 kind 不变

下一步: commit deepcoldy#5 (daemon cron 15min trigger — fetch → analyze → merge → publish 端到端串)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
… publish

P3 commit deepcoldy#5 — daemon-0 cron 注册 tilly scout 15min interval,端到端串:
1. fetchRecentMessages([-15min, now]) via lark-cli user identity
2. analyzeMessages → codex exec 跑 prompt 出 4 类
3. mergeNewDigest 累积进当日 cumulative + dedup by sourceMessageId
4. publishTillyDigest → 主话题卡(同日同卡 updateMessage)
5. markScanned 持久化 dedup(仅 LLM 成功后才标,失败下次 retry)

## 改动 src/daemon.ts (+50 行 setInterval block)

加在原 main-bot/scout cron 注册之后,复用 botIndex=0 单 daemon 跑约束。
TILLY_TICK_INTERVAL_MS = 15 * 60 * 1000.

关键设计:
- fresh.length === 0 → 早退(节省 codex 启动)
- LLM digest.ok === false → **不**调 markScanned(下次 tick 重试)
- LLM digest.ok === true 但 publish 失败 → **照样** markScanned(digest
  已 merge 进 cumulative,下 tick 会重发完整 list)
- 全程 try/catch 兜底,cron 永不挂

## 完整 P3 流水线 (commit #1-deepcoldy#5)

```
daemon-0 cron 15min
  ├─ tilly-scout.fetchRecentMessages
  │   └─ lark-cli +messages-search --as user
  ├─ tilly-message-store.filterUnscanned (dedup)
  ├─ tilly-llm-analyzer.analyzeMessages
  │   └─ codex exec --output-schema --output-last-message
  ├─ tilly-digest-store.mergeNewDigest (按日 cumulative)
  └─ tilly-publisher.publishTillyDigest
      └─ root-inbox-store + root-inbox-card-renderer
          └─ lark sendMessage/updateMessage to mainTopic
```

## 不破回归

- 现有 main-bot/scout cron 不动(R1-R5 escalation + 自动 close 不受影响)
- 新 cron 独立,失败不影响其他
- tsc + build clean

下一步: commit deepcoldy#6 (sync + restart + 真实跑一轮 e2e 看主话题卡 + 报告松松)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 24, 2026
P3 v0.2 — 妹妹 review v0.1 6 条全修,加上 2 个 implementation 细节
建议(analyzedMessageIds 来自 kept set / 不靠 summary hack)。

## Blocker fixes

### #1 — prompt 加 messageId + 校验 sourceMessageId ∈ input set

bug: LLM 输出 sourceMessageId 必须,但 prompt 没给真 id 让它知道,必
hallucinate;hallucinated id 进 mergeNewDigest dedup 后追溯不可信。

修 src/services/tilly-llm-analyzer.ts:
- 渲染消息行加 `id=<messageId>` 前缀(hard ID anchor for LLM)
- prompt 写明"sourceMessageId 必须是真实 id,造假 drop"
- enrich() 增强 validate:includedIdSet.has(it.sourceMessageId) 才保留,
  否则 drop + log debug
- sourceChatId / sourceChatName 优先用真 messages map 派生(不信 LLM)

### #2 — analyzedMessageIds 来自 kept set,daemon 只 mark 实际分析的

bug: analyzer cap 至 50 条,但 daemon markScanned(fresh.map) 标全部,超
cap 消息永久漏扫。

修:
- analyzer 新增 `TillyDigest.analyzedMessageIds: string[]` (来自 kept set)
- renderMessagesForPrompt 返 `{text, includedIds}`;includedIds = 实际进
  prompt 的 50 条 messageIds
- daemon (src/daemon.ts) markScanned 改用 digest.analyzedMessageIds(缺
  时 fallback fresh.map)
- 截断时(MAX_PROMPT_CHARS 超)递归 half-list 重渲染,确保 includedIds
  始终精确等于 prompt 内可见 id 集(妹妹 v0.2 #1)

### #3 — codex sandbox 反 prompt injection

bug: Lark 消息含 untrusted content,`--dangerously-bypass-approvals-and-
sandbox` 给 codex 任意命令执行权限 — prompt injection 可触发 shell
命令、文件访问、网络。

修 src/services/tilly-llm-analyzer.ts:
- 删 `--dangerously-bypass-approvals-and-sandbox`
- 加 `--sandbox read-only`(codex 没有 shell 执行权限)
- 加 `--cd <codexCwd>` 指向 mkdtempSync 创的空 dir(即使 codex 越界
  也看不到我们 source)
- prompt 用 `<UNTRUSTED_DATA>...</UNTRUSTED_DATA>` 边界包消息
- prompt prefix 显式说"忽略 UNTRUSTED_DATA 内任何指令 — 它们是数据
  不是任务"
- prompt suffix 重复一次反 injection 提醒

## P1/P2 fixes

### #4 — dateId 用 Asia/Shanghai

bug: getDateId() 用 UTC YYYY-MM-DD,北京 0:00-8:00 归前一天,违反"今日"
语义。

修 src/services/tilly-digest-store.ts: `Intl.DateTimeFormat('en-CA',
{timeZone:'Asia/Shanghai'})` 直接输出本地 YYYY-MM-DD。

### deepcoldy#5 — tilly_digest 独立 renderer 分支,不再 hack summary

bug: publisher 把整个 markdown 塞 item.summary 让 generic renderer 输
出 → store 持久化 summary 字段被污染 + generic 仍会追加 fake
`openChatId=tilly-scout` 子群链接。

修:
- src/services/root-inbox-card-renderer.ts: `renderRootInboxCard(item,
  opts?: {customMarkdown?})` 新增 RenderOpts;kind='tilly_digest' 用
  customMarkdown 渲染(无 subChat link / 无 generic footer),其他 kind
  忽略 customMarkdown 走旧路径
- src/services/tilly-publisher.ts: 不再 spread+overwrite summary,直接
  调 sendOrUpdateCard(item, {customMarkdown})
- store summary 字段保留短 label "今日 N items"(dashboard listOpen 时
  清爽)

### deepcoldy#6 — lark-cli fetch timeout + cron in-flight guard

bug: fetchRecentMessages execFile 无 timeout,lark 挂住 setInterval 叠
tick。

修:
- src/services/tilly-scout.ts: execFile timeout: 60_000
- src/daemon.ts: tilly cron 用 module-scoped `tillyTickInFlight` flag;
  上一次 tick 未完跳过本次

## 测试 (8 文件 / 85 tests / 0 fail)

新增/扩展 case:
- tilly-llm-analyzer +5 case:
  - hallucinated sourceMessageId 被 drop
  - analyzedMessageIds 严格 = kept set
  - cap=50 时超 cap 不在 analyzedMessageIds(妹妹 v0.2 #1)
  - source 含 `--sandbox read-only` + `--cd codexCwd` + 不含 dangerous bypass
  - prompt 含 UNTRUSTED_DATA 边界 + 反 injection 提醒
- root-inbox-card-renderer +3 case:
  - tilly_digest 用 customMarkdown,无 openChatId=tilly-scout fake link
  - tilly_digest 缺 customMarkdown → fallback 提醒
  - 其他 kind 忽略 customMarkdown 走旧路径
- tilly-digest-store +1 case:
  - getDateId Asia/Shanghai:UTC 16:30 → 当地次日;UTC 23:59 → 当地次日

## 不破回归

- escalation-playbook / root-inbox-publisher / root-inbox-store 所有旧
  case 100% 仍过
- generic kind (escalation / progress / request_decision) 渲染逻辑不变
- daemon scout cron 不变

下一步: 等妹妹复核 → e2e 跑 1 轮看真效果

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 27, 2026
松松实拍严厉拷问:"这根本不是小宝发的啊,这不是你自己发的吗"——
我承认自欺欺人。grep 完代码看清楚:

- 缇蕾 = coco bot (cli_aa9aab67157d5cb2, openId ou_4141..)
- 但 daemon P3 commit deepcoldy#5 一直用 `claudeApp = resolveBotIdent('claude').larkAppId`
  发 publishTillyDigest / publishTillyAlert / notifyClaudeIfImportant
- 群里整天看到的"缇蕾扫读"卡 + alert + ping,sender 全是克劳德
- 缇蕾 bot identity 一次都没用过

bot-registry 设计上每个 daemon 进程都 loadBotConfigs() 加载所有 bot
配置 (含 appSecret),getBotClient(任意 larkAppId) 都拿得到 client。
所以 daemon-0 (claude bot daemon) 完全可以用 tillyApp 发——之前
偷懒选了 claudeApp 而已。

fix: daemon tilly cron 全部 send 路径换成 tillyApp。claudeIdent.openId
仍然用作 notify text 的 @ target (Flumy 主话题克劳德 session 被叫醒
消化扫读卡)。

效果(待下次 cron tick 验证):
- 扫读卡 sender = 缇蕾(不再借克劳德的脸)
- alert 卡 sender = 缇蕾
- @松松 + @克劳德 的 notify text sender = 缇蕾
- 之前 sender=克劳德 发的卡作为历史残留留在群里(卡 id 不变,update
  路径还在;如果想彻底切换可以手动 close 老卡,但 sender 元数据
  Lark 不允许改)

- src/daemon.ts: 新增 tillyApp + claudeIdent 变量;4 处 `larkAppId:
  claudeApp` → `larkAppId: tillyApp`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 27, 2026
…box dismiss/processed

按妹妹 v2 review #2 + #4 + deepcoldy#5:

## 新路由

1. **`GET /api/tilly-digest`** — cumulative 追溯入口
   - 读 `tilly-digest-store.getCurrentDigest()` + `listArchive()`
   - 返 `{ current: <今日 4 类完整>, archive: [...过去 7 天 days] }`
   - **不读** `main-bot-digest.json` (那是 R1-R5 escalation 线,妹妹 v2 #2 强调)

2. **`POST /api/scout-inbox/:id/dismiss`**
3. **`POST /api/scout-inbox/:id/processed`**
   - 调 `dispositionTillyHigh(id, {status, handledBy:'dashboard:user'})`
   - 仅作用于 `type==='tilly_digest_high'` (store 层 dispositionTillyHigh
     有类型守卫,escalation item 返 null → 路由返 404)
   - **不和 root-inbox.close 混** (RootInbox 是另一套 store,妹妹 v2 deepcoldy#5)
   - 404 message 说明 "id 不存在或不是 tilly_digest_high 类"

## 新 store API

- `tilly-digest-store.ts`: 新增 `listArchive()` export (内部 readArchive
  外露给 dashboard / future tests)

## 实拍

`curl /api/tilly-digest`:
```
keys: ['current', 'archive']
current.dateId: 2026-05-25
current totals: {'todos': 64, 'progress': 126, 'blockers': 76, 'noteworthy': 107}
archive days: 0  (今天首发跨日 archive 还没生)
```

build+restart 完。commit 5 (前端新 tab) 把这两个路由用起来。

- src/services/tilly-digest-store.ts: + listArchive()
- src/dashboard.ts: + GET /api/tilly-digest, POST /api/scout-inbox/:id/(dismiss|processed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request May 27, 2026
按 PRD v1.0 设计 + 妹妹 v2 review deepcoldy#5: cumulative 追溯完全归入 dashboard
新 tab,RootInbox 顶栏面板继续只做 action queue 不混。

## 实现

- ViewMode union 加 'tilly' 第三个 view
- view-switch 加第三按钮 "🐶 缇蕾扫读"
- renderTillyView() async fetch /api/tilly-digest + /api/scout-inbox 后渲染
  - 4 类 (todos/progress/blockers/noteworthy) 平铺,每条带 [prio] +
    sourceChatName + applink [→]
  - 顶部 "{n} pending high-prio" badge
  - 过去 7 天 archive 用 <details> 折叠
- **dismiss 按钮只挂在 pending scout-inbox tilly_digest_high item 对应
  的 cumulative item 上** (妹妹 v2 deepcoldy#5)
  - 实现:fetch /api/scout-inbox → 构造 pendingDismissMap{sourceMessageId→inboxId}
  - cumulative item 渲染时查 map 命中才显示 dismiss 按钮
  - 普通 (非 pending high-prio) cumulative item 只 applink,不 dismiss
- dismiss click → POST /api/scout-inbox/:id/dismiss → 成功后按钮移除
  (cumulative item 本身留着,因为 cumulative ≠ action queue)

## i18n

- topo.view.tilly / topo.tilly.{loading,title,meta,dismiss,archiveHeader,
  cat.{todos,progress,blockers,noteworthy},loadErr} (zh+en 各加 12 keys)

## style

- .topo-v2-tilly-pane + .tilly-{header,meta,pending-badge,category,count,
  item,prio*,chat,dismiss,archive,archive-day}
- pending-badge 黄色 + prio 高/中/低三色 + dismiss 绿小按钮

## 实拍

headless 截 `00_tilly_view.png`:
- Today · 2026-05-25 · 47x items · N ticks · 7 pending high-prio
- 📝 Todos 列表展开,每条 [high/med/low] 色块 + summary + 群名 + [→]

build+restart 完。

- src/dashboard/web/topology.ts: renderTillyView + ViewMode 'tilly'
- src/dashboard/web/i18n.ts: 12 + 12 keys
- src/dashboard/web/style.css: tilly tab 全套样式

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
deepcoldy added a commit that referenced this pull request May 27, 2026
#4 webhook 接入点搬进主 dashboard:
- 新增侧边栏「接入点」+ SPA 页 web/connectors.ts(#/connectors):列出接入点、
  重设计的创建表单(卡片式、与 dashboard 一致主题),支持启用/停用、删除、复制
  webhook URL、创建后一次性显示密钥。
- 后端 handleConnectorApi 早已挂在 dashboard token 网关后(/api/connectors*),
  无需改鉴权;新页直接调用。
- 表单按 投递模式 动态显隐(fixed→群 ID、dynamic/new-group→允许的群、
  new-group→去重/状态字段、workflow→工作流 ID)。

#5 去掉「来源类型」:
- ConnectorDefinition 移除 source{type,displayName}(人类标签用 promptEnvelope.sourceName);
  ConnectorSourceType 类型删除。
- connector-api 不再解析/校验 source.type(去掉 bad_source_type)。
- webhook 入站构造的 trigger envelope source.type='webhook'(投递渠道,与连接器无关)不受影响。
- 连接器测试 fixture 去掉 source 字段。

注:老 /team 页(含旧创建表单)将在下一步 #3 删除。connector/trigger/webhook/team
6 文件 49 测试全过、tsc+build 干净。
bdjasonzjs pushed a commit to bdjasonzjs/Dbotmux that referenced this pull request Jun 12, 2026
收尾自检遗留 deepcoldy#5:newDepth >= MAX_SUBTASK_DEPTH 时授 spawnable 无意义——
其子群再 spawn 必被 G2 拦,注入段还会出现「还能开 0 层」的误导文案。
create 入口直接 400(输入校验,非 fork-bomb 闸,主话题分支同样适用——
spawnable 是本特性新增字段,无存量行为可破坏)。不带 --spawnable 不受影响。

遗留 #1/#2(投递确认假阴性、finished 父群迟到消息)属 M0 传输层独立任务,
#4(G6 树级限速语义)待真实使用数据,均不在本任务顺手修——见任务文档 §十。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sensuossss added a commit that referenced this pull request Jun 22, 2026
…native 唤起)

- L2-5: send --chat-id 主群 → goal notify-parent(L1/L2 同 bot,send 是 self-message 被 guard 挡;notify-parent daemon-native 绕开),补 --summary-file/自动推断 session 说明
- L1-5: 描述改 notify-parent 唤起([goal-parent-notify] 前缀),强调通知只是唤醒、L1 仍须查账本/charter
- L1-4: goal supervise 加 --session-id 建议(填 prompt 顶 <session_id>),让 L2 完成时不论 L1 是话题/群级会话都能精准回定位
sensuossss added a commit that referenced this pull request Jun 24, 2026
按多 agent review 逐条修(合入 master 前):

High
- #1 runtime: revisit 后 D#001/D#002 同 node.id 并发,inFlight/nodeControllers/
  nodeAbortCleanups 改按 dispatchKey(instance) 存取 + 守卫删除;cancel 按 instance
  abort。原先无守卫 inFlight.delete(node.id) 会删掉活着的新实例 → 整 run 误崩。
- #2 host: architect 手搓 BotSnapshot 漏 disableCliBypass → 受限 bot 在 architect
  步骤悄悄提权。改用权威 botToSnapshot()。
- #3 architect: 复用 attempts/001 不清旧 manifest,watcher 见旧 manifest 直接
  finish('ok') → revise-dag 后修订不生效。dispatch 前 rmSync(attemptDir)。
- #4 test: 加 runWorkflow 集成测试,断言 restricted override 真的写进 worker 的
  req.botSnapshot.disableCliBypass(降权红线看门)。

Medium
- #5/#12 daemon-run: driveV3Run 顶部记 wasAlreadyTerminal,已终态短路,避免
  coalesce 重驱 / "/start" 重试重发 gate 卡与 done/failed 消息。
- #6 security: gate handler 的 waitId 未校验就 path-join → 加 isValidWaitId
  (含 # 给 A#001-gate 形态) 拦 ../ 逃逸。
- #7 dashboard: catalog 前缀路由用 startsWith 误吞 runId 以 catalog 开头的 v3 run
  详情页 → 改带边界正则。(本就是合并时我引入的)
- #8 daemon: 线程内 "/workflow new" grill fall-through 时 cmdContent 仍是字面量,
  被待回答 ask 拦截器当答案吞掉 → ask 拦截加 !threadGrill。
- #9 test: 补 manifest 软链逃逸(symlink→外部)的拒绝用例,防 realpath 被简化成 resolve。

Nit
- #16 v3-blocked handler: String(formValue) 把 object/array 强转成非空垃圾过校验
  → 改 typeof==='string'。
- #17 v3.ts: 抽 isTerminalRunStatus(),统一 poll/loop 的终态停轮判定(消死代码)。

校验:pnpm build 通过;v3/daemon/dashboard 相关单测全绿(含新增 2 条)。
sensuossss added a commit that referenced this pull request Jul 17, 2026
…native 唤起)

- L2-5: send --chat-id 主群 → goal notify-parent(L1/L2 同 bot,send 是 self-message 被 guard 挡;notify-parent daemon-native 绕开),补 --summary-file/自动推断 session 说明
- L1-5: 描述改 notify-parent 唤起([goal-parent-notify] 前缀),强调通知只是唤醒、L1 仍须查账本/charter
- L1-4: goal supervise 加 --session-id 建议(填 prompt 顶 <session_id>),让 L2 完成时不论 L1 是话题/群级会话都能精准回定位
sensuossss added a commit that referenced this pull request Jul 18, 2026
…native 唤起)

- L2-5: send --chat-id 主群 → goal notify-parent(L1/L2 同 bot,send 是 self-message 被 guard 挡;notify-parent daemon-native 绕开),补 --summary-file/自动推断 session 说明
- L1-5: 描述改 notify-parent 唤起([goal-parent-notify] 前缀),强调通知只是唤醒、L1 仍须查账本/charter
- L1-4: goal supervise 加 --session-id 建议(填 prompt 顶 <session_id>),让 L2 完成时不论 L1 是话题/群级会话都能精准回定位
deepcoldy added a commit that referenced this pull request Jul 26, 2026
旧 README 459 行、11 张内嵌图、功能全文铺开 + 使用指南塞进 Workflow/Goal/env 细节,
偏长杂乱。按 codex 定的骨架与约束重写:

顺序:Hero(一句话定位 + 4 badge + 文档/Quickstart/Showcase CTA + 1 张主 demo gif)
→ 3 个用户痛点/结果(它解决什么) → 3 条命令 5 分钟 Quickstart → 7 个核心场景(每条
一句价值 + 深链文档) → 支持的 CLI → 直接桥接 CLI 的设计理念 + 中性对比表 →
文档/社区/贡献/License/Star。

约束落地:
- 不写「30 秒」——扫码建应用客观是 5 分钟路径,标题即「5 分钟接入」。
- README 各 93 行(zh+en 共 186),落在 codex 的 180-220 区间;只留 1 张主 demo gif,
  细节全部深链文档站,不再内嵌 setup 截图。
- 支持的 CLI 清单以 registry 为事实源(24 个 adapter:claude-code/codex/codex-app/
  gemini/cursor/opencode/antigravity/copilot/grok/kimi/kiro-cli/pi/oh-my-pi/aiden/
  coco/traex/mtr/hermes/mira/mir/genius/seed/relay/riff),不再手工写会漂移的名单。
- README.en.md 同批对齐(同结构、同深链的 /en/ 版本)。

验证:自锚点(#5-分钟接入/#支持的-cli、#5-minute-setup/#supported-clis)与各 header
GFM slug 匹配;深链的文档站页(quickstart/cards/multi-bot/... 17 个)在 master
docs-site 均存在。

Co-Authored-By: Riff <noreply@riff.dev>
deepcoldy added a commit that referenced this pull request Aug 6, 2026
…locker)

codex 二轮 review(4878071011)7 blocker,按其拍定的 v3 设计重写。核心:lease 只管
「是否允许派发」,async-trigger-store 管「调用方看到的终态」——两者职责分离,不再靠
第三份 tombstone/index,也不靠 closeSession 成功来定义业务终态。

- #6(最核心,terminal 不接进 trigger-result):async-trigger-store 扩 status
  pending|completed|**failed**(failed 带 errorCode:no_output, reason:dispatch_unknown)。
  新增 recordFailedStrict(per-session withFileLockSync + atomicWriteFileSync durable + 抛错,
  与 recordCompleted 同锁串行,completed 更强证据恒胜)。resolveAsyncTriggerState 新增
  durable-failed 分支(优先级 completed > failed > closed > pending)——即使 reconcile 的
  closeSession 抛错、session 保持 open,trigger-result 也收敛 failed,不永久 running。
- #1(replace 非原子撕 tombstone):idempotency-store 全部改 atomicWriteFileSync(tmp+fsync
  +rename,失败保留旧文件),干掉 unlink→link。
- #2(takeover 非精确 CAS + 丢 won/existing):takeover 返回 {won|existing},锁内对完整
  immutable identity(owner+boot+session+trigger+requestHash+revision)精确校验;stale rev1
  不能覆盖 fresh winner rev1(新增回归测试)。lease 状态精简为 reserved|attempting(terminal
  移出到 async-store)。
- #3(reconcile 跨 bot):reconcileIdempotencyLeasesOnBoot(ownerLarkAppId, currentBootId)
  显式传 owner,读写/close 前 fail-closed 过滤 record.ownerLarkAppId,跳过 current boot。
- #4(reconcile 在 bind 之后):移到 setActiveSessionsRegistry 之后、startIpcServer 之前
  (daemon.ts)。返回 quarantine Set 传入 restoreActiveSessions,被 terminalize 的 session
  排除 re-attach(防状态/执行面分叉)。
- #5(本 boot 失败留坏 lease):barrier 前失败 compareAndRemove 释放 reserved(重试可全新);
  barrier 后 dispatch 同步 throw → recordFailedStrict + close(durable failed,不重派)。
- #7(HTTP 契约):trigger status mapper 加 idempotency_conflict→409;idempotent 的
  state:failed 视作 200(成功 HTTP 调用报终态,非请求错误)。
- 所有 claim/takeover/transition/compareAndRemove 走同一 per-key withFileLockSync(rename
  只原子替换≠CAS,必须锁内 read→校验→写)。withKeyLock/ensureDir 保证 .lock 父目录存在。

验证:pnpm build 通过。测试真穿状态机崩溃点——idempotency-store 16(含 stale-rev1 竞争 /
corrupt fail-closed / compareAndRemove CAS);trigger-session-idempotency 12(真 store:
attempting-orphan→async failed+close+quarantine / reserved-orphan→删+close / completed 留 /
current-boot 跳过 / **OTHER-owner 跨 bot 零触碰**);trigger-api 校验+范围拒绝;async-store/
state/api-only-wiring(readiness 序不变) 全绿。affected+shared-path 11 套件 327/327 绿。
docs-site build 绿。不带 key 的普通 trigger/webhook 行为零变化。

Co-Authored-By: Claude <noreply@anthropic.com>
deepcoldy added a commit that referenced this pull request Aug 7, 2026
…locker)

codex 二轮 review(4878071011)7 blocker,按其拍定的 v3 设计重写。核心:lease 只管
「是否允许派发」,async-trigger-store 管「调用方看到的终态」——两者职责分离,不再靠
第三份 tombstone/index,也不靠 closeSession 成功来定义业务终态。

- #6(最核心,terminal 不接进 trigger-result):async-trigger-store 扩 status
  pending|completed|**failed**(failed 带 errorCode:no_output, reason:dispatch_unknown)。
  新增 recordFailedStrict(per-session withFileLockSync + atomicWriteFileSync durable + 抛错,
  与 recordCompleted 同锁串行,completed 更强证据恒胜)。resolveAsyncTriggerState 新增
  durable-failed 分支(优先级 completed > failed > closed > pending)——即使 reconcile 的
  closeSession 抛错、session 保持 open,trigger-result 也收敛 failed,不永久 running。
- #1(replace 非原子撕 tombstone):idempotency-store 全部改 atomicWriteFileSync(tmp+fsync
  +rename,失败保留旧文件),干掉 unlink→link。
- #2(takeover 非精确 CAS + 丢 won/existing):takeover 返回 {won|existing},锁内对完整
  immutable identity(owner+boot+session+trigger+requestHash+revision)精确校验;stale rev1
  不能覆盖 fresh winner rev1(新增回归测试)。lease 状态精简为 reserved|attempting(terminal
  移出到 async-store)。
- #3(reconcile 跨 bot):reconcileIdempotencyLeasesOnBoot(ownerLarkAppId, currentBootId)
  显式传 owner,读写/close 前 fail-closed 过滤 record.ownerLarkAppId,跳过 current boot。
- #4(reconcile 在 bind 之后):移到 setActiveSessionsRegistry 之后、startIpcServer 之前
  (daemon.ts)。返回 quarantine Set 传入 restoreActiveSessions,被 terminalize 的 session
  排除 re-attach(防状态/执行面分叉)。
- #5(本 boot 失败留坏 lease):barrier 前失败 compareAndRemove 释放 reserved(重试可全新);
  barrier 后 dispatch 同步 throw → recordFailedStrict + close(durable failed,不重派)。
- #7(HTTP 契约):trigger status mapper 加 idempotency_conflict→409;idempotent 的
  state:failed 视作 200(成功 HTTP 调用报终态,非请求错误)。
- 所有 claim/takeover/transition/compareAndRemove 走同一 per-key withFileLockSync(rename
  只原子替换≠CAS,必须锁内 read→校验→写)。withKeyLock/ensureDir 保证 .lock 父目录存在。

验证:pnpm build 通过。测试真穿状态机崩溃点——idempotency-store 16(含 stale-rev1 竞争 /
corrupt fail-closed / compareAndRemove CAS);trigger-session-idempotency 12(真 store:
attempting-orphan→async failed+close+quarantine / reserved-orphan→删+close / completed 留 /
current-boot 跳过 / **OTHER-owner 跨 bot 零触碰**);trigger-api 校验+范围拒绝;async-store/
state/api-only-wiring(readiness 序不变) 全绿。affected+shared-path 11 套件 327/327 绿。
docs-site build 绿。不带 key 的普通 trigger/webhook 行为零变化。

Co-Authored-By: Claude <noreply@anthropic.com>
deepcoldy added a commit that referenced this pull request Aug 7, 2026
…locker)

codex 二轮 review(4878071011)7 blocker,按其拍定的 v3 设计重写。核心:lease 只管
「是否允许派发」,async-trigger-store 管「调用方看到的终态」——两者职责分离,不再靠
第三份 tombstone/index,也不靠 closeSession 成功来定义业务终态。

- #6(最核心,terminal 不接进 trigger-result):async-trigger-store 扩 status
  pending|completed|**failed**(failed 带 errorCode:no_output, reason:dispatch_unknown)。
  新增 recordFailedStrict(per-session withFileLockSync + atomicWriteFileSync durable + 抛错,
  与 recordCompleted 同锁串行,completed 更强证据恒胜)。resolveAsyncTriggerState 新增
  durable-failed 分支(优先级 completed > failed > closed > pending)——即使 reconcile 的
  closeSession 抛错、session 保持 open,trigger-result 也收敛 failed,不永久 running。
- #1(replace 非原子撕 tombstone):idempotency-store 全部改 atomicWriteFileSync(tmp+fsync
  +rename,失败保留旧文件),干掉 unlink→link。
- #2(takeover 非精确 CAS + 丢 won/existing):takeover 返回 {won|existing},锁内对完整
  immutable identity(owner+boot+session+trigger+requestHash+revision)精确校验;stale rev1
  不能覆盖 fresh winner rev1(新增回归测试)。lease 状态精简为 reserved|attempting(terminal
  移出到 async-store)。
- #3(reconcile 跨 bot):reconcileIdempotencyLeasesOnBoot(ownerLarkAppId, currentBootId)
  显式传 owner,读写/close 前 fail-closed 过滤 record.ownerLarkAppId,跳过 current boot。
- #4(reconcile 在 bind 之后):移到 setActiveSessionsRegistry 之后、startIpcServer 之前
  (daemon.ts)。返回 quarantine Set 传入 restoreActiveSessions,被 terminalize 的 session
  排除 re-attach(防状态/执行面分叉)。
- #5(本 boot 失败留坏 lease):barrier 前失败 compareAndRemove 释放 reserved(重试可全新);
  barrier 后 dispatch 同步 throw → recordFailedStrict + close(durable failed,不重派)。
- #7(HTTP 契约):trigger status mapper 加 idempotency_conflict→409;idempotent 的
  state:failed 视作 200(成功 HTTP 调用报终态,非请求错误)。
- 所有 claim/takeover/transition/compareAndRemove 走同一 per-key withFileLockSync(rename
  只原子替换≠CAS,必须锁内 read→校验→写)。withKeyLock/ensureDir 保证 .lock 父目录存在。

验证:pnpm build 通过。测试真穿状态机崩溃点——idempotency-store 16(含 stale-rev1 竞争 /
corrupt fail-closed / compareAndRemove CAS);trigger-session-idempotency 12(真 store:
attempting-orphan→async failed+close+quarantine / reserved-orphan→删+close / completed 留 /
current-boot 跳过 / **OTHER-owner 跨 bot 零触碰**);trigger-api 校验+范围拒绝;async-store/
state/api-only-wiring(readiness 序不变) 全绿。affected+shared-path 11 套件 327/327 绿。
docs-site build 绿。不带 key 的普通 trigger/webhook 行为零变化。

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants