work-ally 0.2.0-alpha.1

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.
Files changed (172) hide show
  1. package/AGENTS.md +110 -0
  2. package/DASHBOARD.md +160 -0
  3. package/PRODUCT.md +113 -0
  4. package/README.md +403 -0
  5. package/ally.sh +171 -0
  6. package/bridge/src/approval-rules.ts +360 -0
  7. package/bridge/src/channel-delivery.ts +207 -0
  8. package/bridge/src/channel-types.ts +22 -0
  9. package/bridge/src/channels/fake/adapter.ts +31 -0
  10. package/bridge/src/channels/feishu/adapter.ts +411 -0
  11. package/bridge/src/channels/feishu/approvals.ts +6 -0
  12. package/bridge/src/channels/feishu/formatter.ts +276 -0
  13. package/bridge/src/channels/feishu/normalize.ts +368 -0
  14. package/bridge/src/codex-config.ts +52 -0
  15. package/bridge/src/config.ts +240 -0
  16. package/bridge/src/fake-runtime-client.ts +505 -0
  17. package/bridge/src/handoff-service.ts +494 -0
  18. package/bridge/src/logger.ts +194 -0
  19. package/bridge/src/memory-digest.ts +186 -0
  20. package/bridge/src/receiver-approval-autonomy.ts +158 -0
  21. package/bridge/src/receiver-control-core.ts +140 -0
  22. package/bridge/src/receiver-control-work-session.ts +218 -0
  23. package/bridge/src/receiver-control.ts +83 -0
  24. package/bridge/src/receiver-delivery.ts +136 -0
  25. package/bridge/src/receiver-helpers.ts +96 -0
  26. package/bridge/src/receiver-human-gate.ts +333 -0
  27. package/bridge/src/receiver-inbound-preflight.ts +162 -0
  28. package/bridge/src/receiver-recovery.ts +236 -0
  29. package/bridge/src/receiver-runtime-callbacks.ts +367 -0
  30. package/bridge/src/receiver-runtime-policy.ts +132 -0
  31. package/bridge/src/receiver-runtime-state.ts +124 -0
  32. package/bridge/src/receiver-support-actions.ts +189 -0
  33. package/bridge/src/receiver-thread-start.ts +57 -0
  34. package/bridge/src/receiver-turn-coordination.ts +94 -0
  35. package/bridge/src/receiver-turn-execution.ts +257 -0
  36. package/bridge/src/receiver-turn-failure.ts +143 -0
  37. package/bridge/src/receiver-turn-result.ts +185 -0
  38. package/bridge/src/receiver-turn-steer.ts +70 -0
  39. package/bridge/src/receiver-work-session.ts +76 -0
  40. package/bridge/src/receiver.ts +329 -0
  41. package/bridge/src/router.ts +62 -0
  42. package/bridge/src/runtime-client-agent-messages.ts +150 -0
  43. package/bridge/src/runtime-client-message-dispatch.ts +176 -0
  44. package/bridge/src/runtime-client-protocol.ts +411 -0
  45. package/bridge/src/runtime-client-request-ops.ts +56 -0
  46. package/bridge/src/runtime-client-run-turn.ts +158 -0
  47. package/bridge/src/runtime-client-thread-ops.ts +270 -0
  48. package/bridge/src/runtime-client-transport.ts +309 -0
  49. package/bridge/src/runtime-client-turn-poll.ts +224 -0
  50. package/bridge/src/runtime-client-turn-read.ts +185 -0
  51. package/bridge/src/runtime-client-turn-state.ts +105 -0
  52. package/bridge/src/runtime-client.ts +344 -0
  53. package/bridge/src/runtime-user-input.ts +403 -0
  54. package/bridge/src/scheduler.ts +239 -0
  55. package/bridge/src/server-handoff-command.ts +364 -0
  56. package/bridge/src/server-main.ts +80 -0
  57. package/bridge/src/server-routine-command.ts +60 -0
  58. package/bridge/src/server-routine-execution.ts +222 -0
  59. package/bridge/src/server-runtime-app-support.ts +107 -0
  60. package/bridge/src/server-runtime-app.ts +238 -0
  61. package/bridge/src/server-thread-sync-command.ts +63 -0
  62. package/bridge/src/server.ts +17 -0
  63. package/bridge/src/session-store-delivery.ts +220 -0
  64. package/bridge/src/session-store-human-gate.ts +380 -0
  65. package/bridge/src/session-store-inbound-acceptance.ts +66 -0
  66. package/bridge/src/session-store-meta.ts +134 -0
  67. package/bridge/src/session-store-turn-ledger.ts +272 -0
  68. package/bridge/src/session-store.ts +380 -0
  69. package/bridge/src/system-notify.ts +220 -0
  70. package/bridge/src/thread-sync.ts +200 -0
  71. package/bridge/src/translator.ts +494 -0
  72. package/bridge/src/types.ts +289 -0
  73. package/bridge/src/utils.ts +104 -0
  74. package/bridge/src/work-session-store.ts +471 -0
  75. package/docs/.gitkeep +0 -0
  76. package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
  77. package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
  78. package/docs/completed/README.md +21 -0
  79. package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
  80. package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
  81. package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
  82. package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
  83. package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
  84. package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
  85. package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
  86. package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
  87. package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
  88. package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
  89. package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
  90. package/docs/developer-workflow.md +49 -0
  91. package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
  92. package/docs/implementation/test-coverage-map.md +363 -0
  93. package/docs/implementation/work-ally-implementation-guide.md +790 -0
  94. package/docs/issues/README.md +10 -0
  95. package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
  96. package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
  97. package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
  98. package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
  99. package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
  100. package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
  101. package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
  102. package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
  103. package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
  104. package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
  105. package/docs/manual-acceptance.md +127 -0
  106. package/docs/ops-runbook.md +44 -0
  107. package/docs/planning/FEATURE-memory-system.md +748 -0
  108. package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
  109. package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
  110. package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
  111. package/docs/planning/SPEC-assistant-rename.md +610 -0
  112. package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
  113. package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
  114. package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
  115. package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
  116. package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
  117. package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
  118. package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
  119. package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
  120. package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
  121. package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
  122. package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
  123. package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
  124. package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
  125. package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
  126. package/docs/planning/SPEC-stable-archive-contract.md +456 -0
  127. package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
  128. package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
  129. package/docs/planning/ally-next.md +1278 -0
  130. package/docs/planning/assistant-workbench-spec.md +725 -0
  131. package/docs/planning/product-workbench.md +283 -0
  132. package/docs/product-onboarding.md +227 -0
  133. package/docs/product-spec-standard.md +528 -0
  134. package/docs/troubleshooting.md +45 -0
  135. package/docs/user-quickstart.md +46 -0
  136. package/internal/dispatch.sh +95 -0
  137. package/internal/lib/common.sh +1450 -0
  138. package/internal/modules/assistant/manage.sh +1312 -0
  139. package/internal/modules/bootstrap/setup.sh +144 -0
  140. package/internal/modules/config/init-env.sh +10 -0
  141. package/internal/modules/global/manage.sh +154 -0
  142. package/internal/modules/handoff/manage.sh +54 -0
  143. package/internal/modules/mcp/manage.sh +83 -0
  144. package/internal/modules/ops/logs.sh +76 -0
  145. package/internal/modules/routines/manage.sh +55 -0
  146. package/internal/modules/runtime/assistant-autosave.sh +26 -0
  147. package/internal/modules/runtime/restart.sh +6 -0
  148. package/internal/modules/runtime/start.sh +283 -0
  149. package/internal/modules/runtime/status.sh +194 -0
  150. package/internal/modules/runtime/stop.sh +55 -0
  151. package/internal/modules/runtime/supervisor.sh +216 -0
  152. package/internal/modules/runtime/update.sh +26 -0
  153. package/package.json +41 -0
  154. package/runtime/config/.gitkeep +0 -0
  155. package/runtime/host/.gitkeep +0 -0
  156. package/runtime/host/healthcheck-codex-app-server.ts +22 -0
  157. package/runtime/host/ping-pong-codex-app-server.ts +66 -0
  158. package/runtime/host/probe-codex-app-server.ts +115 -0
  159. package/skills/archive-reader/SKILL.md +9 -0
  160. package/skills/feishu-production-debug/SKILL.md +37 -0
  161. package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
  162. package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
  163. package/skills/issue-to-spec-triage/SKILL.md +44 -0
  164. package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
  165. package/skills/memory-digest/SKILL.md +9 -0
  166. package/skills/post-implementation-closure/SKILL.md +39 -0
  167. package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
  168. package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
  169. package/skills/product-spec/SKILL.md +244 -0
  170. package/templates/env.example +5 -0
  171. package/templates/routines/nightly-memory-digest.yaml +10 -0
  172. package/templates/workspace/AGENTS.md +26 -0
@@ -0,0 +1,466 @@
1
+ # 分析报告:审批等待已对用户可见,但审批实体缺失导致会话被错误阻塞
2
+
3
+ 更新时间:2026-03-16
4
+ 状态:已修复并补门禁(2026-03-17)
5
+ 作者:Codex
6
+
7
+ ## 1. 这份报告回答什么
8
+
9
+ 这份报告聚焦一个非常具体、可复现且用户可感知的问题:
10
+
11
+ > 飞书前台已经收到“当前已暂停,等待你审批后继续”,但用户实际上看不到任何可操作的审批卡;此后用户继续追问或直接回复“同意”,中间层仍然把会话卡在“等待审批”状态,导致消息既没有转给 Codex,也没有真正完成审批。
12
+
13
+ 这份文档的目标不是立刻给 patch,而是把以下内容低上下文讲清楚:
14
+
15
+ 1. 真实现象是什么。
16
+ 2. 证据链在哪里。
17
+ 3. 已经确认的事实是什么。
18
+ 4. 尚未确认但不影响修复方向的点是什么。
19
+ 5. 根因属于文案问题、实现 bug,还是状态机/架构设计问题。
20
+ 6. 后续应按什么原则修改,才能把这类问题真正收口。
21
+ 7. 应补哪些自动化测试,避免以后同类问题再次漏掉。
22
+
23
+ ## 2. 事件背景
24
+
25
+ ### 2.1 排查对象
26
+
27
+ - 助理:`Ally`
28
+ - 办公桌:`/Users/allenfeng/.work-ally/assistants/Ally`
29
+ - 会话:`feishu--oc_1890ff7f9132a7d90b09765f27ff5bc8`
30
+ - 关注时间:2026-03-16 17:07 左右(Asia/Shanghai,对应 `2026-03-16T09:07Z`)
31
+ - 对应 turn:`019cf5e0-e31d-7081-b714-a2119e66dc74`
32
+ - 对应 thread:`019cebce-0481-7be3-98ce-38432074d5bd`
33
+
34
+ ### 2.2 用户可见现象
35
+
36
+ 用户在飞书前台看到的关键对话序列是:
37
+
38
+ 1. 助理先正常推进新任务,连续输出 commentary。
39
+ 2. 随后系统发出:`当前已暂停,等待你审批后继续。`
40
+ 3. 但用户前台没有看到任何审批卡,也没有任何审批编号、审批内容或操作入口。
41
+ 4. 用户发:`进展如何?`
42
+ 5. 系统回:`当前这轮已暂停,正在等你审批;你刚发的这条我还没转给 Codex。先处理审批,再继续发下一条。`
43
+ 6. 用户再发:`同意`
44
+ 7. 系统再次回:`当前这轮已暂停,正在等你审批;你刚发的这条我还没转给 Codex。先处理审批,再继续发下一条。`
45
+
46
+ 对用户而言,这里的直观体感是:
47
+
48
+ - 系统宣称“卡在审批”;
49
+ - 但人类看不到审批对象;
50
+ - 人类主动表态“同意”也没用;
51
+ - 中间层仍然持续阻塞会话。
52
+
53
+ 这已经不是“提示不够友好”,而是一个**真实的会话阻塞 bug**。
54
+
55
+ ## 3. 证据位置
56
+
57
+ ### 3.1 用户可读原材料
58
+
59
+ - archive:`/Users/allenfeng/.work-ally/assistants/Ally/.system/archive/2026/03/16/feishu--oc_1890ff7f9132a7d90b09765f27ff5bc8.ndjson`
60
+
61
+ ### 3.2 运行日志
62
+
63
+ - timeline:`/Users/allenfeng/.work-ally/assistants/Ally/.system/logs/timeline-2026-03-16.log`
64
+ - bridge:`/Users/allenfeng/.work-ally/assistants/Ally/.system/logs/bridge-2026-03-16.log`
65
+
66
+ ### 3.3 相关实现代码
67
+
68
+ - `bridge/src/receiver.ts`
69
+ - `bridge/src/router.ts`
70
+ - `bridge/src/runtime-client.ts`
71
+ - `bridge/src/channels/feishu/adapter.ts`
72
+ - 相关测试目录:`tests/integration/session/`
73
+
74
+ ## 4. 事实链
75
+
76
+ ### 4.1 新任务本身是正常启动的
77
+
78
+ 在本次问题发生前,turn `019cf5e0-e31d-7081-b714-a2119e66dc74` 已经正常启动,并持续向用户输出了多条 commentary,内容明确是在处理“新的群聊 feature/spec 需求”,不是旧 turn 重放。
79
+
80
+ 关键证据:
81
+
82
+ - `timeline-2026-03-16.log` 中 `09:01:22Z` 记录 `runtime.turn_started`
83
+ - `09:02:16Z`、`09:03:11Z`、`09:04:55Z`、`09:06:44Z`、`09:07:00Z` 均有同一 turn 的 commentary
84
+ - archive 中对应记录位于第 28-44 行
85
+
86
+ 这说明:
87
+
88
+ - 这不是消息没进 bridge;
89
+ - 也不是 turn 根本没跑起来;
90
+ - 问题发生在 turn 已经运行一段时间之后。
91
+
92
+ ### 4.2 17:07 左右 runtime 确实进入了 `waitingOnApproval`
93
+
94
+ `timeline-2026-03-16.log` 记录:
95
+
96
+ - `2026-03-16T09:07:10.174Z [DEBUG] [bridge] runtime.thread_status_changed {"status":{"type":"active","activeFlags":["waitingOnApproval"]}}`
97
+
98
+ 这说明:
99
+
100
+ - runtime 侧确实进入了“等待审批”的 thread 状态;
101
+ - 用户看到“等待审批”并非完全空穴来风。
102
+
103
+ ### 4.3 bridge 只把“等待审批”状态发给了用户,没有把审批实体发出来
104
+
105
+ archive 记录表明:
106
+
107
+ - 第 45 行:`09:07:12Z` 向飞书发出 `status_reply`,文案是 `当前已暂停,等待你审批后继续。`
108
+ - 但同一时间段内,archive 里没有任何 `approval_requested` 的 outbound 记录;
109
+ - timeline/bridge log 同一时间段也没有 `receiver.approval_requested` 或等价事件;
110
+ - 唯一可见的发送记录是普通状态消息对应的 `feishu.message_sent`。
111
+
112
+ 因此可以明确判断:
113
+
114
+ > 这次不是“审批卡发了但用户没看到”,而是**系统根本没有把审批实体稳定交付到飞书前台**。
115
+
116
+ ### 4.4 用户后续自然语言追问没有转给 Codex,而是被本地拦截了
117
+
118
+ archive 记录表明:
119
+
120
+ - 第 46-48 行:用户发 `进展如何?`,bridge 记录 `waiting_user_follow_up_intercepted`,并回了“你刚发的这条我还没转给 Codex”
121
+ - 第 49-51 行:用户发 `同意`,bridge 再次记录 `waiting_user_follow_up_intercepted`,仍未转给 Codex
122
+
123
+ timeline 也对应记录:
124
+
125
+ - `09:10:40.659Z receiver.waiting_user_follow_up_intercepted`
126
+ - `09:11:00.236Z receiver.waiting_user_follow_up_intercepted`
127
+
128
+ 所以可以确认:
129
+
130
+ - 用户的追问确实到达了 bridge;
131
+ - bridge 故意没有往 Codex 转发;
132
+ - 阻塞是 bridge 本地状态机造成的,不是飞书丢消息,也不是 Codex 没响应。
133
+
134
+ ### 4.5 “同意”没有生效,不是因为用户说错了,而是当前解析前提根本不成立
135
+
136
+ 当前实现里,审批解析分两类:
137
+
138
+ 1. 明确控制命令:`/approve <approvalId>` / `/deny <approvalId>`
139
+ 2. 对审批卡内容的自然语言回复:例如回复一张含“审批编号”的卡,然后说“同意”
140
+
141
+ `bridge/src/router.ts` 中,`parseApprovalReply()` 必须从 `replyToText` 里解析出审批编号;如果当前消息不是“回复那张审批卡”,就拿不到 `approvalId`,自然无法提交审批。
142
+
143
+ 而这次现场中:
144
+
145
+ - 用户根本没看到审批卡;
146
+ - 飞书入站记录里也没有 `reply_to_message_id` / `reply_to_text`;
147
+ - 所以“同意”这条消息不可能被解释成审批动作。
148
+
149
+ 这解释了为什么用户明明已经表态“同意”,系统却仍然卡住。
150
+
151
+ ## 5. 问题定义
152
+
153
+ 这个问题不能定义成“审批文案有点误导”,也不能定义成“偶发漏发一条卡片”。
154
+
155
+ 更准确的问题定义是:
156
+
157
+ > **bridge 把 `waitingOnApproval` 这个 runtime thread flag 直接升级成了对用户可见的“待审批阻塞事实”,但它并没有保证审批实体本身已经被成功持久化、成功发送并成功暴露给用户。**
158
+
159
+ 一旦审批实体缺失,当前状态机就会进入自相矛盾:
160
+
161
+ - 一方面告诉用户“正在等你审批”;
162
+ - 另一方面又不给用户任何可执行对象;
163
+ - 再进一步把用户后续所有自然语言都拦下来,不再发给 Codex。
164
+
165
+ ## 6. 5 Why 根因分析
166
+
167
+ ### Why 1:为什么用户看到了“等待审批”,却看不到审批卡?
168
+
169
+ 因为 bridge 在 `onThreadStatus` 路径里,只要看到 `waitingOnApproval`,就会立刻发送 `waitingApprovalStatusMessage()`,不要求审批实体已经存在。
170
+
171
+ 对应代码:`bridge/src/receiver.ts` 中 `onThreadStatus` 分支。
172
+
173
+ ### Why 2:为什么系统会把 thread flag 当成“用户已获得审批对象”的依据?
174
+
175
+ 因为当前设计把两个概念混成了一个:
176
+
177
+ 1. runtime 内部 thread 进入了等待审批状态;
178
+ 2. 用户侧已经拿到了可操作的审批对象。
179
+
180
+ 这两个概念在实现上被默认视为等价,但真实系统里并不等价。
181
+
182
+ ### Why 3:为什么这两个概念不等价?
183
+
184
+ 因为审批是一个独立实体,它至少有三个独立阶段:
185
+
186
+ 1. runtime 内部产生审批请求;
187
+ 2. bridge 收到并记录审批请求;
188
+ 3. bridge 成功把审批内容交付到飞书用户前台。
189
+
190
+ 只有第三步完成,才算“用户真的可以审批”。当前设计却在第一步甚至只看到 flag 时,就把会话推进到了等待用户审批的阻塞态。
191
+
192
+ ### Why 4:为什么用户后续消息会被拦截?
193
+
194
+ 因为 `handleInbound()` 会优先根据 `waitingStateFor()` 和当前 session 状态,把会话判定为 `waiting_user`;只要等待态是 `approval`,后续自然语言就会进入 `waiting_user_follow_up_intercepted` 路径,而不是继续发给 Codex。
195
+
196
+ 也就是说,阻塞是否生效,目前绑定的是“本地看到等待态”,而不是“审批对象已经对用户可见”。
197
+
198
+ ### Why 5:为什么用户发“同意”也无法自救?
199
+
200
+ 因为当前审批解析依赖审批卡上下文或显式 `/approve <id>`。当审批卡本身缺失时,用户没有审批编号、也没有 reply target,于是“同意”既不能被当作审批动作,也不会被转发给 Codex,只会再次被等待态拦截。
201
+
202
+ ### 根因收口
203
+
204
+ 根因不是单点漏日志,也不是文案问题,而是:
205
+
206
+ > **审批阻塞状态机建模错误:系统把“观察到等待审批状态”误当成了“审批实体已对用户可操作”,从而过早进入阻塞态,并吞掉后续用户输入。**
207
+
208
+ ## 7. 已知事实与未知点
209
+
210
+ ### 7.1 已知事实
211
+
212
+ 已经可以确定的事实有:
213
+
214
+ 1. runtime thread 真实进入了 `waitingOnApproval`。
215
+ 2. bridge 真实向用户发了“等待你审批”的状态提示。
216
+ 3. bridge 没有把对应审批实体稳定送到飞书前台。
217
+ 4. 用户的“进展如何?”和“同意”都到达了 bridge。
218
+ 5. bridge 明确没有把这两条消息转给 Codex,而是本地拦截了。
219
+ 6. 当前控制解析机制要求审批卡或审批编号;本次场景两者都不存在。
220
+
221
+ ### 7.2 仍待进一步确认、但不影响修复方向的点
222
+
223
+ 还没有最终钉死的子问题是:
224
+
225
+ > 这次审批实体为什么没有出现?
226
+
227
+ 目前有两种可能:
228
+
229
+ 1. runtime 在这次场景下只暴露了 thread flag,但没有把审批 request 事件送到 bridge;
230
+ 2. runtime 发了审批 request,但 `runtime-client -> receiver.onApproval` 这条链路在当前实现/当前时序下漏接了。
231
+
232
+ 无论是哪一种,这都不改变主修复方向,因为问题的关键并不是“审批事件到底丢在更上游还是更下游”,而是:
233
+
234
+ > **只要审批实体没有真正成功交付给用户,bridge 就不应该把会话切成“等待用户审批”的阻塞态。**
235
+
236
+ 也就是说,即使上游偶发不稳定,bridge 也应该把这种情况建模成“审批详情待同步”或“阻塞原因待确认”,而不是甩给用户一句“等你审批”。
237
+
238
+ ## 8. 影响范围
239
+
240
+ 这个问题的影响不只是一轮会话卡住,而是会损伤三件事:
241
+
242
+ ### 8.1 用户信任
243
+
244
+ 用户会认为:
245
+
246
+ - 系统把事情吞了;
247
+ - 自己明明被要求审批,却根本无从操作;
248
+ - 连“同意”都没有用;
249
+ - 中间层像是在自说自话。
250
+
251
+ ### 8.2 会话连续性
252
+
253
+ 因为后续自然语言被拦截,用户没法继续追问,也没法显式转为新的任务;整轮会话会被错误挂起。
254
+
255
+ ### 8.3 同类阻塞机制的普遍风险
256
+
257
+ 如果 `waitingOnApproval` 这里成立,`waitingOnUserInput` 也存在同类风险:
258
+
259
+ - 只看到 flag,没把真正的问题表单/elicitation 内容稳定送到用户前台;
260
+ - 却已经把会话切成“等你输入”的阻塞态。
261
+
262
+ 因此这不是审批专属小洞,而是**阻塞态与用户可见实体脱钩**这一类系统性问题。
263
+
264
+ ## 9. 修复原则
265
+
266
+ 这次不要按“补个 if”思路修,而要按下面的原则改。
267
+
268
+ ### 9.1 原则一:阻塞态必须绑定“用户已拿到可操作实体”
269
+
270
+ 只有当以下条件成立时,才能把会话切到真正的 `waiting_approval`:
271
+
272
+ 1. bridge 已收到明确的审批实体;
273
+ 2. bridge 已把审批实体持久化到 session/archive;
274
+ 3. bridge 已成功把审批卡发送到飞书;
275
+ 4. 发送结果被确认成功。
276
+
277
+ 换言之:
278
+
279
+ - `waitingOnApproval` flag 只能说明“可能卡在审批”;
280
+ - 不能直接说明“用户已经可以审批”。
281
+
282
+ ### 9.2 原则二:flag-only 场景要建成异常态,不要建成用户阻塞态
283
+
284
+ 如果 bridge 只观察到 `waitingOnApproval`,却没有拿到审批实体,那么应进入一种单独状态,例如:
285
+
286
+ - `approval_visibility_unconfirmed`
287
+ - 或“审批详情同步中”
288
+
289
+ 在这个状态下:
290
+
291
+ - 可以对用户透明说明“检测到当前轮可能进入审批阻塞,正在同步审批详情”;
292
+ - 但不能说“等你审批后继续”;
293
+ - 更不能把所有后续消息都拦下来当成未审批。
294
+
295
+ ### 9.3 原则三:后续消息拦截条件应收紧
296
+
297
+ 当前拦截 follow-up 的条件过宽。以后只有当“用户可见审批实体已存在”时,才允许把后续自然语言解释成“你还没处理审批”。
298
+
299
+ 如果审批实体缺失:
300
+
301
+ - 用户的后续消息要么继续转给 Codex;
302
+ - 要么进入“当前阻塞详情仍未同步完成”的异常提示;
303
+ - 但不能再假设“用户明明已经拿到审批对象却不处理”。
304
+
305
+ ### 9.4 原则四:用户补救路径要完整
306
+
307
+ 当系统处于审批阻塞态时,用户至少应始终拥有以下其一:
308
+
309
+ 1. 可直接操作的审批卡;
310
+ 2. 可引用的审批编号;
311
+ 3. 明确的错误说明:当前没有拿到审批对象,因此无法审批。
312
+
313
+ 不能再出现:
314
+
315
+ - 系统要求审批;
316
+ - 但又不给任何审批句柄。
317
+
318
+ ### 9.5 原则五:pull/reconcile 不能只关注最终回复,也要关注阻塞实体
319
+
320
+ 此前稳定性优化更偏向“最终结果补发”。这次问题说明:
321
+
322
+ - 审批请求、用户输入请求这类阻塞实体也必须纳入对账模型;
323
+ - 否则就可能出现“最终回复还没问题,但阻塞对象丢了”的前台假死。
324
+
325
+ ## 10. 建议实现方向
326
+
327
+ 这里只给实现方向,不直接给 patch。
328
+
329
+ ### 10.1 状态模型调整
330
+
331
+ 把当前单一的 `waiting_approval` 拆成至少两层:
332
+
333
+ 1. `approval_pending_visibility`
334
+ - 只看到了 runtime flag,或只确认当前轮疑似进入审批阻塞;
335
+ - 但还没有用户可见审批实体。
336
+ 2. `waiting_approval`
337
+ - 已有审批记录;
338
+ - 已成功把审批对象交给用户;
339
+ - 这时才允许把会话切成真正等待用户审批。
340
+
341
+ ### 10.2 发送顺序与幂等要求
342
+
343
+ 审批链路要调整为:
344
+
345
+ 1. 收到审批实体。
346
+ 2. 先落 session / archive durable record。
347
+ 3. 发送审批卡。
348
+ 4. 发送成功后,才把 turn execution status 标成真正 `waiting_approval`。
349
+ 5. 如果发送失败或审批实体未拿到,则停在异常态并继续 reconcile。
350
+
351
+ ### 10.3 用户输入拦截逻辑调整
352
+
353
+ `waiting_user_follow_up_intercepted` 这条路径要以“可见阻塞实体存在”为前提,而不是仅依赖 thread flag。
354
+
355
+ ### 10.4 审批答复容错增强(可选增强,不作为根修复替代)
356
+
357
+ 在保证审批实体稳定可见之后,可考虑额外增强:
358
+
359
+ - 当当前 session 恰好只有一个 pending approval 时,允许用户直接发“同意/拒绝”而不必强依赖 reply-to;
360
+ - 但这只能算兜底体验增强,不能替代“审批实体必须先送达”这个主修复。
361
+
362
+ ## 11. 必补测试
363
+
364
+ 这次暴露的是专项门禁缺口,后续至少要补下面这些自动化场景。
365
+
366
+ ### 11.1 flag-only waiting approval
367
+
368
+ 场景:
369
+
370
+ - runtime thread status 进入 `waitingOnApproval`
371
+ - 但 bridge 没有收到审批实体
372
+
373
+ 期望:
374
+
375
+ - 不发送“等待你审批后继续”
376
+ - 不进入真正的 `waiting_approval`
377
+ - 不拦截后续自然语言为“未审批 follow-up”
378
+
379
+ ### 11.2 approval entity arrives after waiting flag
380
+
381
+ 场景:
382
+
383
+ - 先收到 waiting flag
384
+ - 后收到审批实体
385
+
386
+ 期望:
387
+
388
+ - 只有在审批实体持久化并成功发给用户后,才进入真正的审批等待态
389
+ - 不重复发卡
390
+ - 不重复发状态
391
+
392
+ ### 11.3 approval entity send failure
393
+
394
+ 场景:
395
+
396
+ - bridge 收到了审批实体
397
+ - 但向飞书发送审批卡失败
398
+
399
+ 期望:
400
+
401
+ - 会话不能假装已经在等用户审批
402
+ - 要进入可见异常态
403
+ - 后续可以补发或重试
404
+
405
+ ### 11.4 follow-up while approval visibility unconfirmed
406
+
407
+ 场景:
408
+
409
+ - 用户在“只看到等待 flag、还没看到审批卡”的阶段发追问
410
+
411
+ 期望:
412
+
413
+ - 不要误报“你这条没转给 Codex,因为在等你审批”
414
+ - 要么继续转发给 Codex,要么给出准确的“审批详情仍未同步完成”提示
415
+
416
+ ### 11.5 plain approve text without visible card
417
+
418
+ 场景:
419
+
420
+ - 用户直接发“同意”
421
+ - 当前没有审批卡、没有审批编号
422
+
423
+ 期望:
424
+
425
+ - 不能静默吞掉
426
+ - 应明确告知“当前没有可确认的审批对象”或等价信息
427
+
428
+ ### 11.6 mirrored waitingOnUserInput
429
+
430
+ 场景:
431
+
432
+ - `waitingOnUserInput` 只有 flag,没有实体问题单/elicitation
433
+
434
+ 期望:
435
+
436
+ - 与审批场景同样处理,不得重复犯同类错误
437
+
438
+ ## 12. 验收标准
439
+
440
+ 这类问题修好后,前台体验必须满足下面三条:
441
+
442
+ 1. **只要系统说“等你审批”,用户就一定看得到可操作审批内容。**
443
+ 2. **如果审批内容还没真正送达,系统不能把后续自然语言拦截成“你还没审批”。**
444
+ 3. **用户发“同意/拒绝”时,要么能正确处理,要么明确告诉用户当前没有可确认的审批对象,不能再自相矛盾。**
445
+
446
+ ## 13. 一句话结论
447
+
448
+ 这次 bug 的本质不是“偶发漏发一张卡”,而是:
449
+
450
+ > **bridge 把 runtime 的等待审批状态过早解释成了“用户已经拥有审批对象”,从而把会话错误地切进了阻塞态。**
451
+
452
+ 后续修复必须围绕一个硬原则展开:
453
+
454
+ > **审批阻塞只有在审批实体已成功送达到用户前台时才成立;否则它应被建模为异常同步态,而不是用户待操作态。**
455
+
456
+ ## 10. 实现收口(2026-03-17)
457
+
458
+ 本问题已按“先实体可见,再进入真实 waiting_approval 阻塞态”收口:
459
+
460
+ 1. `receiver.onApproval` 先把 approval record 以 `visible: false` 落库。
461
+ 2. 只有 `approval_requested` 真正成功发到 channel 后,才调用 `markApprovalVisible()`,并把 session / ledger 切到真实 waiting approval。
462
+ 3. `onThreadStatus` 若只观察到 `waitingOnApproval` flag、但还没有可见审批实体,只会发“审批详情同步中”的非阻塞提示,不再把后续自然语言误拦截成 `waiting_user_follow_up_intercepted`。
463
+ 4. 针对这一条补了自动化用例:`tests/integration/session/approval-flow.test.mjs` 中 `waitingOnApproval flag without visible approval artifact does not block follow-up input`。
464
+
465
+ 因此,这个问题现在已经不是 pending 行为。后续若再出现,只应按新状态机视为 regression。
466
+