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,348 @@
1
+ # 分析报告:fresh thread 过早终态判定与 `[object Object]` 错误外泄
2
+
3
+ 更新时间:2026-03-16
4
+ 状态:已修复并补门禁(2026-03-17)
5
+ 作者:Codex
6
+
7
+ ## 1. 这份报告回答什么
8
+
9
+ 这份报告不把 `Ally 22:43 只回了 [object Object]` 当成一个孤立的文案事故,而是把它上提为一个更值得警惕的问题:
10
+
11
+ > bridge 目前会把某些“暂时还没观测清楚 turn 终态”的运行态,过早升级成“本轮已经失败”,然后把这个本地判断直接外显给用户。
12
+
13
+ 这份文档要回答的是:
14
+
15
+ 1. 这次 `[object Object]` 现场到底发生了什么。
16
+ 2. 它为什么不是一个单纯的字符串格式化问题。
17
+ 3. 它暴露的是哪一条 turn 终态判定链路的设计缺陷。
18
+ 4. 它和之前“阻塞态对用户可见但无可操作实体”的问题,是否属于同一类上位问题。
19
+ 5. 后续应该按什么原则收口,避免继续东打一块补丁、西打一块补丁。
20
+
21
+ ## 2. 结论先说
22
+
23
+ 结论很明确:
24
+
25
+ - 这是一条真实 bug,不是飞书显示问题。
26
+ - 用户消息已经进入 bridge,也已经成功启动了新的 thread 和 turn。
27
+ - bridge 在 fresh thread 刚创建后,过早执行了 `thread/read(includeTurns=true)` 轮询。
28
+ - runtime 返回的是一个结构化暂态错误:thread 还没 materialize,当前还不支持 `includeTurns`。
29
+ - `runtime-client` 把这个结构化错误错误地转换成了 `Error(String(error))`,于是用户看到了字面量 `[object Object]`。
30
+ - 更深层的问题不在于字符串丑,而在于 bridge 把一次“暂态观测失败”误判成了“turn 已失败”,并且在真正的 `turn/completed` 后续到来前,就已经把错误结果发给了用户。
31
+
32
+ 一句话收口:
33
+
34
+ > **当前 turn 终态链路把“没有可靠读到终态”错误地等价成了“终态就是失败”。**
35
+
36
+ ## 3. 事件背景
37
+
38
+ ### 3.1 排查对象
39
+
40
+ - 助理:`Ally`
41
+ - 办公桌:`/Users/allenfeng/.work-ally/assistants/Ally`
42
+ - 会话:`feishu--oc_1890ff7f9132a7d90b09765f27ff5bc8`
43
+ - 关注时间:2026-03-16 22:43 左右(Asia/Shanghai,对应 `2026-03-16T14:43Z`)
44
+ - 对应 thread:`019cf719-de8c-79e2-9a60-10603069b15e`
45
+ - 对应 turn:`019cf719-e00e-7663-a831-2b29124a876f`
46
+
47
+ ### 3.2 用户可见现象
48
+
49
+ 用户发出一条正常需求消息后,只收到一条回复:
50
+
51
+ `[object Object]`
52
+
53
+ 这不是“回复内容不完整”,而是系统直接把内部错误对象以错误形式暴露给了用户。
54
+
55
+ ## 4. 证据链
56
+
57
+ ### 4.1 archive 原材料
58
+
59
+ - `/Users/allenfeng/.work-ally/assistants/Ally/.system/archive/2026/03/16/feishu--oc_1890ff7f9132a7d90b09765f27ff5bc8.ndjson`
60
+
61
+ 关键记录:
62
+
63
+ 1. 用户消息在 `2026-03-16T14:43:13.248Z` 进入 archive。
64
+ 2. `2026-03-16T14:43:14.105Z` 记录 `system_event: turn_failed`,其中 `error` 已经是 `[object Object]`。
65
+ 3. `2026-03-16T14:43:14.690Z` 记录 `error_reply`,正文就是 `[object Object]`。
66
+
67
+ 这说明:
68
+
69
+ - `[object Object]` 是 bridge 真正发给飞书用户的内容;
70
+ - 不是日志里才有,用户前台确实看到了。
71
+
72
+ ### 4.2 bridge / timeline 日志
73
+
74
+ - bridge:`/Users/allenfeng/.work-ally/assistants/Ally/.system/logs/bridge-2026-03-16.log`
75
+ - timeline:`/Users/allenfeng/.work-ally/assistants/Ally/.system/logs/timeline-2026-03-16.log`
76
+
77
+ 关键顺序如下:
78
+
79
+ 1. `14:43:13.638Z`:bridge 收到用户消息。
80
+ 2. `14:43:14.054Z`:thread 创建成功。
81
+ 3. `14:43:14.064Z`:turn 创建成功。
82
+ 4. `14:43:14.068Z`:bridge 立刻发起 `thread/read`。
83
+ 5. `14:43:14.102Z`:runtime 返回错误:
84
+
85
+ `thread ... is not materialized yet; includeTurns is unavailable before first user message`
86
+
87
+ 6. 同一时刻:`runtime.turn_final_state_poll_failed` 被记录为 `Error: [object Object]`。
88
+ 7. 随后:`receiver.turn_failed`,bridge 把这轮直接判成失败。
89
+ 8. `14:45:25.752Z`:两分钟后,runtime 其实发来了 `turn_completed_event`,状态是 `completed`。
90
+
91
+ 这条时间线说明:
92
+
93
+ - turn 并没有在 `14:43:14` 真正失败;
94
+ - bridge 是在真正终态到来之前,提前宣判了失败;
95
+ - 这不是“Codex 没工作”,而是 bridge 自己把暂态错误转成了用户可见终态。
96
+
97
+ ## 5. 直接原因
98
+
99
+ 这次事故的直接原因有两层。
100
+
101
+ ### 5.1 第一层:错误归一化坏了
102
+
103
+ `bridge/src/runtime-client.ts` 当前在 `pollTurnFinalState()` 的异常路径中使用:
104
+
105
+ - `new Error(String(error))`
106
+
107
+ 对应位置:
108
+
109
+ - `/Users/allenfeng/Development/Repositories/work-ally/bridge/src/runtime-client.ts:686`
110
+
111
+ 当 runtime 返回的是 JSON-RPC 结构化错误对象时,`String(error)` 会得到 `[object Object]`,于是:
112
+
113
+ - 日志里的错误变成 `[object Object]`
114
+ - 用户看到的错误也变成 `[object Object]`
115
+
116
+ ### 5.2 第二层:暂态观测失败被误判成终态失败
117
+
118
+ 更严重的问题在同一段逻辑里:
119
+
120
+ - `pollTurnFinalState()` 只要遇到“非 recoverable”读错误,就会 `rejectTurnResult()`;
121
+ - `receiver` 捕获到 reject 后,会沿 `turn_failed -> error_reply` 这条路径向用户回错;
122
+ - 但这次错误并不是 turn 真失败,而只是 fresh thread 尚未 materialize 时 `thread/read(includeTurns=true)` 暂时不可用。
123
+
124
+ 也就是说,系统当前把:
125
+
126
+ - “现在还不能可靠读取 turn 历史”
127
+
128
+ 错误地升级成了:
129
+
130
+ - “这一轮 turn 已经失败”
131
+
132
+ ## 6. 5 Why 根因分析
133
+
134
+ ### Why 1:为什么用户看到了 `[object Object]`?
135
+
136
+ 因为 runtime 返回了结构化错误对象,而 bridge 用 `String(error)` 包装它,最终把对象字符串化成了 `[object Object]`。
137
+
138
+ ### Why 2:为什么这个对象字符串最终会暴露给用户?
139
+
140
+ 因为这次错误没有被当成“观测层暂态错误”,而是被当成了“turn failure”,随后 receiver 直接走了 `error_reply` 用户出口。
141
+
142
+ ### Why 3:为什么一次 `thread/read` 失败会被当成 turn failure?
143
+
144
+ 因为当前终态轮询模型里,`pollTurnFinalState()` 承担了双重职责:
145
+
146
+ 1. 读取 runtime 已存储事实;
147
+ 2. 本地裁定 turn 是否结束。
148
+
149
+ 一旦读取事实这一步失败,当前实现会直接把它理解成“终态判定已经失败”。
150
+
151
+ ### Why 4:为什么 fresh thread 上会出现这种读取失败?
152
+
153
+ 因为 `turn/start` 成功后,bridge 立即执行 `thread/read(includeTurns=true)`;但 fresh thread 在 materialize 之前,并不总是支持带 `includeTurns` 的读取。这说明 bridge 当前默认假设:
154
+
155
+ > turn 刚启动后,thread 的完整可读视图一定已经就绪。
156
+
157
+ 这个假设并不成立。
158
+
159
+ ### Why 5:为什么这种暂态会升级成用户可见事故?
160
+
161
+ 因为当前状态机缺少一个关键分层:
162
+
163
+ - “尚未可靠观测到终态”
164
+ - “已可靠确认终态失败”
165
+
166
+ 这两种状态在实现里被混成了一类。于是任何不在 recoverable 正则名单里的读错误,都会过早地落到用户侧终态。
167
+
168
+ ### 根因收口
169
+
170
+ 根因不是一个字符串处理小失误,而是:
171
+
172
+ > **turn 终态状态机没有把“观测失败”与“业务终态失败”分层,导致 bridge 用本地读取异常覆盖了 runtime 的真实 turn 事实。**
173
+
174
+ ## 7. 这次事故反映的上位设计问题
175
+
176
+ 这次 `[object Object]` 事故暴露的不是单点 patch 位,而是 turn 终态链路上至少三条设计问题。
177
+
178
+ ### 7.1 问题一:终态判定过早绑定在观测链路上
179
+
180
+ 当前实现里:
181
+
182
+ - `turn/start` 后立刻开启 `pollTurnFinalState()`;
183
+ - poll 既负责“看事实”,又负责“下结论”;
184
+ - 一旦 `thread/read` 报错,系统很容易直接走失败出口。
185
+
186
+ 这意味着:
187
+
188
+ > 终态不是由 runtime 的 durable fact 驱动,而是被本地观测成功与否牵着走。
189
+
190
+ 这在设计上过于激进。
191
+
192
+ ### 7.2 问题二:event truth 和 poll truth 的优先级没有收干净
193
+
194
+ 当前代码里,`turn/completed` 事件已经被接住,并存在 `completedEventResult` 中;但 `pollTurnFinalState()` 仍可能在更早时刻先 reject 整个 turn。
195
+
196
+ 一旦 reject 发生:
197
+
198
+ - turn state 被 finalize;
199
+ - 后续真正到来的 `turn/completed` 即使存在,也已经来不及正常兑现给用户。
200
+
201
+ 也就是说,当前系统虽然在做 pull-primary,但还残留着:
202
+
203
+ > “只要本地某一步观察不顺,就先把这轮打死”
204
+
205
+ 的旧思路。
206
+
207
+ ### 7.3 问题三:用户出口没有经过“是否已确认终态”的把关
208
+
209
+ 用户最终看到的 `error_reply`,本质上应只来自两类情况:
210
+
211
+ 1. runtime 已明确给出失败终态;
212
+ 2. bridge 已经过完整补偿、对账和等待后,仍能确认无法恢复。
213
+
214
+ 但这次现场中:
215
+
216
+ - runtime 尚未给出终态;
217
+ - bridge 也没有完整进入补偿 / 继续拉取 / 等待 completed event 的流程;
218
+ - 用户却已经收到了失败消息。
219
+
220
+ 这说明当前用户出口缺少“终态已确认”这一层门禁。
221
+
222
+ ## 8. 和之前 pending 问题的关系
223
+
224
+ 它和下面两份 pending 报告不是同一个具体 bug,但属于同一个上位问题家族:
225
+
226
+ - `docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md`
227
+ - `docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md`
228
+
229
+ 共同点是:
230
+
231
+ > bridge 把“不完整的内部信号”过早升级成了“用户面前已经成立的事实”。
232
+
233
+ 前两份文档里,不完整的内部信号是:
234
+
235
+ - `waitingOnApproval`
236
+ - `waitingOnUserInput`
237
+
238
+ 这份文档里,不完整的内部信号是:
239
+
240
+ - fresh thread 上一次暂态 `thread/read` 失败
241
+
242
+ 三者的共性问题都是:
243
+
244
+ - 内部信号还不足以支撑对用户下结论;
245
+ - 但 bridge 已经先替用户做了结论化翻译;
246
+ - 结果导致用户看到错误状态,甚至后续真实结果被吞掉。
247
+
248
+ ## 9. 修复原则
249
+
250
+ 后续修复不应只停在“把 `[object Object]` 改得更好看”,而应按下面几条硬原则推进。
251
+
252
+ ### 9.1 原则一:观测失败不等于 turn failure
253
+
254
+ 任何 `thread/read` / transport / materialization 暂态错误,都只能先落在:
255
+
256
+ - 观测暂不可用
257
+ - 继续等待
258
+ - 继续拉取
259
+ - 等待 `turn/completed`
260
+
261
+ 除非 runtime 已明确给出 turn failed,或 bridge 已经过完整恢复窗口仍确认不可恢复,否则不能直接对用户宣称失败。
262
+
263
+ ### 9.2 原则二:fresh thread 需要显式 materialization 窗口
264
+
265
+ `turn/start` 成功后,不应假设 `thread/read(includeTurns=true)` 立即可用。
266
+
267
+ 要么:
268
+
269
+ - 对 “not materialized yet / includeTurns unavailable” 定义专门的 retryable 暂态;
270
+
271
+ 要么:
272
+
273
+ - 在 fresh thread 的早期阶段先读轻量状态,再延后拉取 turns。
274
+
275
+ 但无论采用哪种实现,结论都一样:
276
+
277
+ > fresh thread 初期的可读性,不能被当成稳定前提。
278
+
279
+ ### 9.3 原则三:用户可见错误必须建立在“终态已确认”之上
280
+
281
+ 以后用户出口的错误文案必须满足一条门槛:
282
+
283
+ - 这是 runtime 的真实终态;
284
+ - 或这是 bridge 经过恢复和对账后仍然确认的不可恢复状态。
285
+
286
+ 不能再把中间层自己的内部读取异常直接回显给用户。
287
+
288
+ ### 9.4 原则四:turn/completed 和 persisted turn fact 才是终态真相
289
+
290
+ 在架构上,最终应坚持:
291
+
292
+ - `turn/completed`
293
+ - `thread/read` 读到的 persisted turn status
294
+
295
+ 这两类 durable fact 才能决定 turn 的用户终态。
296
+
297
+ 轮询失败、暂态协议错误、短时 materialization 窗口,都只应影响“还要不要继续等 / 继续拉”,不应直接决定“用户该看到什么终态”。
298
+
299
+ ### 9.5 原则五:错误归一化要先于状态机判断
300
+
301
+ 任何 runtime 结构化错误都必须先规范化成:
302
+
303
+ - `code`
304
+ - `message`
305
+ - `retryable / transient / terminal`
306
+
307
+ 然后再进入状态机。
308
+
309
+ 如果连错误都还没被正确归类,就直接参与 turn 终态裁决,后续还会继续冒出别的“对象字符串化”或“误分类”事故。
310
+
311
+ ## 10. 建议测试门禁
312
+
313
+ 这次事故同时说明,自测仍缺少一组针对“fresh thread 初期终态判定”的用例。后续至少应补下列场景:
314
+
315
+ 1. `turn/start` 成功后首次 `thread/read(includeTurns=true)` 返回 `not materialized yet`。
316
+ 2. 上述错误是结构化 JSON-RPC object,而不是 `Error` 实例。
317
+ 3. 同一轮 turn 在暂态读错误后,随后正常收到 `turn/completed`。
318
+ 4. 暂态读错误期间,不得向用户发送 `error_reply`。
319
+ 5. 用户前台绝不能看到 `[object Object]`。
320
+ 6. archive 不得过早记录 `turn_failed`。
321
+ 7. 如果后来 turn 真 completed,结果必须能继续对账并交付。
322
+ 8. 如果最终确实失败,用户看到的也必须是规范化后的可解释错误,而不是对象字符串。
323
+
324
+ ## 11. 后续落地方向
325
+
326
+ 这份报告当前只做问题收敛,不直接给 patch 细节,但后续实现应优先围绕下面三块推进:
327
+
328
+ 1. `runtime-client` 的错误归一化:结构化 JSON-RPC error 不能再走 `String(error)`。
329
+ 2. `pollTurnFinalState()` 的状态机重收:fresh thread materialization 暂态不能直接 reject turn。
330
+ 3. `receiver` 的用户出口门禁:只有在 turn 终态已确认时,才允许向用户发送失败终态。
331
+
332
+ ## 12. 一句话结论
333
+
334
+ `[object Object]` 不是一次偶发字符串事故,而是 turn 终态架构里“把观测异常误当业务终态”的一次明确暴露;如果不按状态机分层去修,后面还会以别的文案继续冒出来。
335
+
336
+ ## 10. 实现收口(2026-03-17)
337
+
338
+ 本问题已按“观测失败 != 终态失败”收口:
339
+
340
+ 1. `runtime-client` 新增结构化 JSON-RPC 错误归一化,不再把对象错误透传成 `[object Object]`。
341
+ 2. `thread ... is not materialized yet; includeTurns is unavailable before first user message` 已纳入 recoverable transient。
342
+ 3. `pollTurnFinalState()` 对这类 fresh-thread materialization read failure 继续轮询,不再过早 `rejectTurnResult()`。
343
+ 4. `receiver` 的错误落日志与对用户文案也改为统一的错误提取逻辑,避免对象串化外泄。
344
+ 5. 对应门禁已补到 `tests/unit/runtime/runtime-client-pull-primary.test.mjs`:
345
+ - `runTurn treats fresh-thread materialization error as transient and never leaks object formatting`
346
+
347
+ 因此,这个问题现在已经从“pending 分析”进入“resolved behavior + regression guard”。
348
+