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,606 @@
1
+ # 分析报告:Codex App Server 断开语义、中间层可靠性边界与 pull-primary 主设计
2
+
3
+ 更新时间:2026-03-15
4
+ 状态:分析完成 / 已归档(主结论已吸收进 runtime recovery 专项)
5
+ 作者:Codex
6
+
7
+ > 归档说明:这份分析继续保留在 `docs/issues/` 作为问题演进痕迹。与当前实现直接相关、仍需推进的最小闭环,以 `docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md` 为准;其中 runtime abstraction 相关延展已移出本专项,统一以后续独立 spec 为准。
8
+
9
+ ## 0. 给架构师先看
10
+
11
+ ### 0.1 这是什么文档
12
+
13
+ 这是一份面向架构评审的分析报告,讨论 `work-ally` 作为 Feishu 与官方 `Codex app-server` 之间的中间层,为什么会在 runtime transport 抖动时暴露用户可见的不确定性,以及应如何从设计上治理。
14
+
15
+ 它不是代码修复说明,也不是某一次单点 bug 的 patch 设计,而是对中间层可靠性模型的重新定义。
16
+
17
+ ### 0.2 背景一句话
18
+
19
+ 当前系统一旦与 `Codex app-server` 的实时连接发生抖动,就可能无法稳定判断:
20
+
21
+ - 人类发出的消息是否已经真正进入 Codex thread
22
+ - Codex 已完成的回复是否已经成功回给 Feishu 用户
23
+ - 哪些结果需要补发,哪些结果绝不能重复发
24
+
25
+ ### 0.3 核心判断
26
+
27
+ 这类问题的本质不是“socket 偶尔会断”,而是:
28
+
29
+ > 中间层把产品正确性建立在一条实时连接的连续观察能力上;连接一抖动,系统就失去对会话事实与交付事实的确定性。
30
+
31
+ ### 0.4 最终建议
32
+
33
+ 主设计应升级为:
34
+
35
+ > **pull-primary, push-assisted, ledger-backed**
36
+
37
+ 含义是:
38
+
39
+ - 最终答复和恢复补偿,以主动拉取 Codex 已存储 thread/turn 事实为准
40
+ - 实时推送只承担体验增强和阻塞性交互承接
41
+ - 中间层自己维护 durable ledger,区分执行状态、交付状态和确认状态
42
+
43
+ ### 0.5 架构决策点
44
+
45
+ 如果只让读者记住四件事,应记住:
46
+
47
+ 1. `Codex App Server 已断开` 表示 transport connection 断了,不等于 thread 死亡。
48
+ 2. 官方协议同时支持“事件订阅”和“读取已存储 thread”,因此主动拉取补偿是成立的。
49
+ 3. 真正的一等问题不是 websocket 还是 stdio,而是有没有 reconciliation 和 delivery ledger。
50
+ 4. `stdio` 值得评估,但即使改成 `stdio`,也不能替代 pull-primary 主模型。
51
+
52
+ ### 0.6 术语表
53
+
54
+ - `transport`:bridge 与 `Codex app-server` 之间的通信通道,当前仓库使用本机 websocket;官方还支持 stdio。
55
+ - `thread`:Codex 侧的一条会话上下文,不等于一个操作系统进程。
56
+ - `turn`:thread 中的一轮请求-处理-完成过程。
57
+ - `item`:turn 内更细粒度的输出或请求单元,例如 agent message、plan、approval request 等。
58
+ - `pull-primary`:最终真相以主动读取已存储 thread/turn 状态为准,而不是以实时事件流为准。
59
+ - `push-assisted`:保留最小化实时事件通道,只承接审批、用户输入等阻塞性交互和必要状态变化。
60
+ - `ledger-backed`:中间层维护 durable ledger,用来判断哪些消息已执行、已交付、需补发、不可重放。
61
+
62
+ ## 1. 文档定位
63
+
64
+ 这份报告是第一份事故报告的上位分析,不再只回答“`Codex App Server 已断开` 到底是什么意思”,而是把整条论证链走完:
65
+
66
+ 1. 这里的“断开”在官方协议和当前实现里到底是什么。
67
+ 2. 为什么同机进程之间仍然会发生这类断连。
68
+ 3. 为什么这不自动等于 thread 死亡或 turn 失败。
69
+ 4. 为什么 `work-ally` 不能把产品正确性建立在一条实时连接上。
70
+ 5. 基于官方协议与当前产品诉求,什么才是更合理的主设计。
71
+
72
+ 这份报告与前一份文档的关系是:
73
+
74
+ - 第一份报告:聚焦 2026-03-14 21:47 的真实 incident、证据链、根因与现有缺口。
75
+ 文件:`docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md`
76
+ - 本报告:从 incident 提升到架构层,回答“这类问题在设计上应如何被治理”。
77
+
78
+ 本报告不提交任何代码实现,只给架构判断、协议边界和建议方向。
79
+
80
+ ## 2. 先给最终建议
81
+
82
+ 先写结论,避免读者在细节里迷路。
83
+
84
+ ### 2.1 最终建议
85
+
86
+ `work-ally` 应把与 `Codex app-server` 的桥接主设计升级为:
87
+
88
+ > **pull-primary, push-assisted, ledger-backed**
89
+
90
+ 具体含义是:
91
+
92
+ 1. **最终答复与会话正确性以主动拉取为准。**
93
+ 2. **实时推送只承担体验增强和阻塞性交互承接,不再承担正确性职责。**
94
+ 3. **中间层必须维护自己的 durable ledger,区分执行状态、交付状态与确认状态。**
95
+
96
+ ### 2.2 为什么这才是主设计
97
+
98
+ 因为用户真正需要的不是“实时流永不断”,而是:
99
+
100
+ - 飞书里的消息不要漏
101
+ - 不要重复发
102
+ - AI 已经完成的回复,即使 transport 抖动后也能补回来
103
+ - 人类已经发给 AI 的消息,不要因为桥接层抖动而神秘消失或重复进入 thread
104
+
105
+ 只要系统仍然把正确性压在一条实时连接上,那么无论底层用 websocket 还是 stdio,本质上都还不够稳。
106
+
107
+ ### 2.3 对 transport 的重新定位
108
+
109
+ 在这个新模型下:
110
+
111
+ - `stdio` / `websocket` 仍然重要,但它们的重要性从“正确性基石”降为“transport 稳定性与实现复杂度取舍”。
112
+ - 如果只看本机部署,`stdio` 大概率比当前 websocket 更稳。
113
+ - 但就算改成 `stdio`,如果没有 pull-based reconciliation 和 delivery ledger,这类问题仍然只能“少一点”,不能“从设计上收口”。
114
+
115
+ ## 3. 核心问题定义
116
+
117
+ 本报告要回答的核心问题,不是“某个 socket 为什么关了”这么窄,而是下面四个更底层的问题:
118
+
119
+ 1. “`Codex App Server 已断开 / 连接已恢复`”在协议上是什么意思。
120
+ 2. 它是否意味着 thread 或 turn 已经挂掉。
121
+ 3. 既然底层 thread 有自己的存储与状态,我们是否可以在实时链路不稳时,主动读取并完成补发。
122
+ 4. 如果可以,那中间层的主设计是否应该从“等着推”转向“以拉为准”。
123
+
124
+ 用户对产品的真实诉求可以压缩成一句话:
125
+
126
+ > 作为飞书与 Codex 之间的中间层,系统必须忠实地把 thread 的对话事实反映给人类,而不是把 transport 抖动放大成用户语义不确定。
127
+
128
+ ## 4. 官方协议给出的事实边界
129
+
130
+ 这部分只讲 OpenAI 官方 `Codex App Server` 文档中可直接得到的事实。
131
+
132
+ ### 4.1 app-server 支持两种 transport
133
+
134
+ 官方文档明确写明:
135
+
136
+ - `stdio`(`--listen stdio://`)是默认 transport,使用 JSONL。
137
+ - `websocket`(`--listen ws://IP:PORT`)是 experimental transport。
138
+ - 在 websocket 模式下,server 使用 bounded queues;当 ingress 满时,会拒绝新请求并返回 `Server overloaded; retry later.`,客户端应做指数退避重试。 citeturn1view0
139
+
140
+ 这件事的含义非常明确:
141
+
142
+ 1. 当前仓库用 websocket 不是唯一选择。
143
+ 2. 官方自己也没有把 websocket 定义成默认稳定基线。
144
+ 3. transport 本身就是一个有容量、排队与重试语义的工程层,而不是“天然可靠消息总线”。 citeturn1view0
145
+
146
+ ### 4.2 initialize 是 per-connection 握手
147
+
148
+ 官方文档明确写明:
149
+
150
+ - 每打开一条 transport connection,都必须先发一次 `initialize`。
151
+ - 然后发 `initialized`。
152
+ - 在这条连接上,如果未完成初始化就发送其他请求,server 会拒绝。
153
+ - 同一条连接重复 `initialize` 会返回 `Already initialized`。 citeturn2view0
154
+
155
+ 因此,“连接已恢复”的协议含义不是“旧连接回来了”,而是:
156
+
157
+ > 建立了一条新的 transport connection,并在这条新连接上重新完成了 app-server 协议握手。 citeturn2view0
158
+
159
+ ### 4.3 thread/start 与 thread/read 是两种不同能力
160
+
161
+ 官方文档明确区分了:
162
+
163
+ - `thread/start`:创建新 thread,并自动让当前连接订阅该 thread 的 turn/item 事件。
164
+ - `thread/resume`:继续已有 thread。
165
+ - `thread/read`:读取已存储 thread,不恢复订阅、不把 thread load 到内存、也不发 `thread/started`。
166
+ - `thread/read(includeTurns=true)` 可以返回 turn 历史。
167
+ - 返回的 `thread` 对象带 runtime `status`。 citeturn2view0turn2view1
168
+
169
+ 这意味着协议天然支持两种访问模式:
170
+
171
+ 1. **在线订阅模式**:等 server 往当前连接推事件。
172
+ 2. **存量读取模式**:客户端按需主动读取 thread 当前已存储的事实。 citeturn2view0turn2view1
173
+
174
+ ### 4.4 thread 的 runtime status 独立于 transport connection
175
+
176
+ 官方文档说明:`thread/read` 返回的 `thread.status` 可能是:
177
+
178
+ - `notLoaded`
179
+ - `idle`
180
+ - `systemError`
181
+ - `active`,并可带 `activeFlags`,例如 `waitingOnApproval`。 citeturn2view1turn2view4
182
+
183
+ 文档还给出:
184
+
185
+ - `thread/loaded/list` 返回当前内存中已加载的 thread ids。
186
+ - `thread/unsubscribe` 只是取消当前连接对某个 thread 的订阅;如果这是最后一个 subscriber,server 才会 unload thread,并发 `thread/closed`。 citeturn2view5turn2view7
187
+
188
+ 因此,transport connection、thread 是否 loaded、thread 是否 active / idle,是三个相关但不等价的概念。socket 断开并不自动等于 thread 死亡。 citeturn2view1turn2view5turn2view7
189
+
190
+ ### 4.5 turn/item 的权威终态来源
191
+
192
+ 官方文档明确写明:
193
+
194
+ - `turn/start` 响应会立即返回初始 `turn`,其中已经包含 `turn.id`,初始状态为 `inProgress`。
195
+ - `turn/completed` 是 turn 的终态通知,`turn.status` 可为 `completed`、`interrupted` 或 `failed`。
196
+ - `item/completed` 提供 item 的 final state,并且官方明确要求把它视为 authoritative state。
197
+ - `item/agentMessage/delta`、`plan` 增量等属于流式过程。 citeturn3view0turn2view2turn2view3
198
+
199
+ 因此一个成熟的中间层不应只依赖流式 delta,而应围绕:
200
+
201
+ - `thread.id`
202
+ - `turn.id`
203
+ - `item.id`
204
+ - `turn/completed`
205
+ - `item/completed`
206
+
207
+ 构造自己的 durable delivery model。 citeturn3view0turn2view2turn2view3
208
+
209
+ ### 4.6 审批与用户输入是 server-initiated request 语义
210
+
211
+ 官方文档明确写明:
212
+
213
+ - 命令审批、文件变更审批由 server 主动向 client 发 JSON-RPC request。
214
+ - client 响应后,server 通过 `serverRequest/resolved` 确认请求已被回答或被清理。
215
+ - `tool/requestUserInput`、动态工具调用也遵循类似的 server-initiated request 模型。 citeturn2view3turn2view6turn3view2
216
+
217
+ 这意味着:
218
+
219
+ - 有些语义适合靠主动拉取补偿,例如 turn 最终回复。
220
+ - 有些语义天然更依赖在线 request channel,例如审批 prompt、用户输入请求细节。 citeturn2view3turn2view6turn3view2
221
+
222
+ ## 5. 当前仓库的实现模型
223
+
224
+ 这一部分只回答“`work-ally` 现在到底怎么接 app-server”。
225
+
226
+ ### 5.1 app-server 现在是后台独立进程,不是 bridge 子进程
227
+
228
+ 仓库当前通过 shell 层以 `nohup` 方式启动:
229
+
230
+ - `codex app-server --listen "$CODEX_LISTEN"`
231
+
232
+ 然后 bridge 再根据配置里的 `listenUrl` 去连接该地址。证据位置:
233
+
234
+ - `internal/modules/runtime/start.sh`
235
+ - `internal/modules/runtime/supervisor.sh`
236
+ - `bridge/src/config.ts`
237
+ - `bridge/src/server.ts`
238
+
239
+ 所以当前运行模型是:
240
+
241
+ - 一个长期后台 app-server 进程
242
+ - 一个独立 bridge 进程
243
+ - 两者通过 loopback websocket transport 通信
244
+
245
+ 这不是“同进程内函数调用”,而是一条真正的本地 socket 会话。
246
+
247
+ ### 5.2 当前 bridge 如何定义“断开”
248
+
249
+ 在 `bridge/src/runtime-client.ts` 里:
250
+
251
+ - `connect()` 使用 `new WebSocket(this.listenUrl)` 建连。
252
+ - websocket 一旦 `close`,就进入 `handleSocketClosed('runtime websocket closed')`。
253
+ - `handleSocketClosed()` 会:
254
+ - 记录 `connect_closed`
255
+ - 清掉当前 socket 引用
256
+ - reject 所有 pending request
257
+ - reject 所有活动 turn
258
+ - 若不是手动断开,则安排自动重连
259
+
260
+ 因此当前实现里“已断开”的直接语义是:
261
+
262
+ > bridge 这边持有的那条 websocket 会话结束了,基于该连接等待中的请求与 turn 跟踪需要进入恢复逻辑。
263
+
264
+ ### 5.3 当前 bridge 如何定义“恢复”
265
+
266
+ 当前实现里,重连成功后会在新 socket 上重新执行 `initialize()`;若此前经历过 reconnect 流程,就发出 `reconnected` 事件。
267
+
268
+ 因此当前实现里的“连接已恢复”只表示:
269
+
270
+ > 新 transport 已连通,握手已完成,bridge 又可以继续和 app-server 通信了。
271
+
272
+ 它并不自动保证:
273
+
274
+ - 断线期间漏掉的事件已经重放
275
+ - 旧 turn 的最终结果已经被正确识别
276
+ - 飞书侧已经和 Codex thread 自动对齐
277
+
278
+ ## 6. 第一性原理:问题本质不是“断了”,而是“失去确定性”
279
+
280
+ 如果只从第一性原理看,这类问题的本质不是“socket 断了”本身,而是:
281
+
282
+ > 中间层把会话正确性建立在一条在线 transport 的连续观察能力上;一旦观察中断,系统就失去了对‘已经发生了什么、已经转达了什么、还缺什么’的确定性。
283
+
284
+ 把问题拆开,会发现至少有三层真相:
285
+
286
+ 1. **执行真相**:Codex thread / turn / item 实际发生了什么。
287
+ 2. **交付真相**:这些事实有没有成功传给飞书中的人。
288
+ 3. **确认真相**:飞书里的人发来的消息,有没有真正进入 Codex thread。
289
+
290
+ 当前问题之所以让用户尴尬,不是因为 transport 抖动本身不可接受,而是因为一旦抖动,系统就无法稳定回答下面这些问题:
291
+
292
+ - 这轮 turn 到底完成了吗?
293
+ - 已完成的回复到底有没有发到飞书?
294
+ - 飞书里的那条消息到底有没有成功送进 Codex?
295
+ - 如果没有送进,应该补送;如果已经送进,就绝不能重复送。
296
+
297
+ 所以这类问题的深层根因,不是“网络会断”,而是:
298
+
299
+ > 中间层还没有形成一套不依赖实时连接连续性的 durable truth model。
300
+
301
+ ## 7. 为什么“同一台电脑”仍然会断
302
+
303
+ 用户对这点的直觉疑惑是合理的,但工程上并不矛盾。
304
+
305
+ ### 7.1 同机不等于无连接状态机
306
+
307
+ 只要是:
308
+
309
+ - 两个独立进程
310
+ - 中间通过 `127.0.0.1:PORT` 上的 websocket 会话通信
311
+
312
+ 那它就是一个有完整生命周期的连接:
313
+
314
+ - 建连
315
+ - 握手
316
+ - 收发帧
317
+ - close
318
+ - 重连
319
+
320
+ “在一台机器上”只能减少外部网络因素,不会消灭 transport 自身的生命周期。
321
+
322
+ ### 7.2 当前架构里至少有四类断点
323
+
324
+ 当前模型下,以下任一层都可能导致“connection closed”:
325
+
326
+ 1. app-server 进程退出、重启或主动关闭连接
327
+ 2. bridge/client 侧超时、异常、清理或主动关闭
328
+ 3. runtime supervisor / allocator 重新分配监听地址
329
+ 4. websocket transport 自身的容量、队列与实验性实现边界
330
+
331
+ 因此,单靠一句 `runtime websocket closed` 现象日志,不能负责任地说一定是谁的锅。
332
+
333
+ ### 7.3 断开不自动等于 thread 挂掉
334
+
335
+ 按照官方协议,必须把以下几件事分开判断:
336
+
337
+ 1. app-server 进程是否还活着
338
+ 2. thread 是否仍 loaded
339
+ 3. turn 是否仍在执行或已经终态
340
+ 4. 当前 transport 是否还连着
341
+
342
+ 只有这样,才能避免把“连接断了”误判成“任务没了”。
343
+
344
+ ## 8. 为什么 push-only 模型不再成立
345
+
346
+ 当前桥接更接近一种 push-first 设计:
347
+
348
+ - transport 在线时,依赖 `turn/*`、`item/*`、approval request 等事件流推进状态
349
+ - 断线后,再用少量 `thread/read` 和本地内存状态做补救
350
+
351
+ 这个模型在“连接持续稳定”时可以工作,但它有两个结构性缺陷。
352
+
353
+ ### 8.1 push-only 把在线事件流误当成唯一真相
354
+
355
+ 一旦某个关键事件没到,系统就容易掉进歧义区:
356
+
357
+ - turn 没完成
358
+ - 还是完成了但通知没送到
359
+ - 还是已经完成但飞书没投递成功
360
+ - 还是其实结果已经送达,只是本地状态没记录下来
361
+
362
+ 如果没有独立 ledger,系统只能猜。
363
+
364
+ ### 8.2 push-only 让 transport 抖动直接伤害用户语义
365
+
366
+ 用户并不关心 websocket 是不是一度 closed;用户关心的是:
367
+
368
+ - “我刚才那句话到底有没有送到 AI?”
369
+ - “AI 刚才那轮到底有没有答完?”
370
+ - “如果答完了,为什么飞书里没看到?”
371
+
372
+ 只要系统无法用持久化事实回答这三个问题,产品就仍然是脆弱的。
373
+
374
+ ## 9. 为什么 pull-primary 是值得确立的主设计
375
+
376
+ 这是本报告的主结论。
377
+
378
+ ### 9.1 你们的产品目标不需要实时流承担正确性
379
+
380
+ 用户已经明确表达:产品不以极低延迟为核心目标,晚几秒可接受;真正重要的是不中断、不漏消息、不断言错误。
381
+
382
+ 在这种产品目标下,把“最终答复”主路径改成主动拉取,是非常自然的选择。
383
+
384
+ ### 9.2 官方协议天然支持“主动拉取最终真相”
385
+
386
+ 因为官方已经给出了:
387
+
388
+ - `turn/start` 返回初始 `turn.id`
389
+ - `thread/read(includeTurns=true)` 返回历史 turns
390
+ - `thread.status` 告诉你当前线程是 `active`、`idle`、`notLoaded` 还是 `systemError`
391
+ - `turn/completed` 和 `item/completed` 给出终态语义
392
+
393
+ 所以中间层完全可以这样工作:
394
+
395
+ 1. 收到飞书用户消息
396
+ 2. 启动 turn,拿到 `turn.id`
397
+ 3. 不把流式 delta 当最终依据
398
+ 4. 轮询 `thread/read(includeTurns=true)` 直到该 turn 进入终态
399
+ 5. 再根据 ledger 决定是否向飞书发送最终答复或失败状态
400
+
401
+ 在这个模型里,实时流断一下不再是致命问题,因为最终真相可以主动读取。
402
+
403
+ ### 9.3 pull-primary 更符合“忠实转达”的中间层定位
404
+
405
+ 如果站在“快递员”这个比喻上看:
406
+
407
+ - push-only 像是“边走边喊,喊漏了就容易说不清”
408
+ - pull-primary 像是“最终一定回仓库核对签收单,再决定哪些要补派、哪些已签收不能重派”
409
+
410
+ 对飞书这种异步人类入口来说,后者显然更符合产品责任。
411
+
412
+ ## 10. 为什么不是 pure pull
413
+
414
+ 虽然我建议把主设计切到 pull-primary,但我不建议把整个系统做成完全不依赖推送的 pure pull。
415
+
416
+ ### 10.1 最终答复适合拉
417
+
418
+ 以下内容很适合做成主动拉取驱动:
419
+
420
+ - turn 是否完成
421
+ - 最终回复文本
422
+ - turn 失败 / 中断终态
423
+ - thread 当前是否仍 active / idle / notLoaded
424
+ - 某个最终 item 的 authoritative state
425
+
426
+ 这些都属于稳定终态事实。
427
+
428
+ ### 10.2 阻塞性交互不适合纯拉
429
+
430
+ 审批、文件变更审批、`tool/requestUserInput`、动态工具调用这类 server-initiated request,本质上是“server 正在向 client 要一个交互决策”。
431
+
432
+ 目前官方文档并没有提供一个对等的“拉取所有待审批请求详情”的通用 inbox 接口;它提供的是:
433
+
434
+ - request 通过实时流发出
435
+ - `thread/status/changed` 可能告诉你正在 `waitingOnApproval` 或 `waitingOnUserInput`
436
+ - `serverRequest/resolved` 告诉你该请求已被回答或被清理
437
+
438
+ 因此,纯拉模型虽然能知道“线程卡住了”,但不一定能完整还原“该向用户展示什么审批 prompt、有哪些按钮、上下文是什么”。
439
+
440
+ ### 10.3 正确取舍是 push-assisted
441
+
442
+ 所以更合理的设计不是 pure pull,而是:
443
+
444
+ - **pull-primary**:最终答复、最终状态、恢复对账以主动拉取为准
445
+ - **push-assisted**:审批、用户输入请求、必要状态变更提示仍通过在线事件通道承接
446
+
447
+ 这样既不会把正确性押在实时流上,也不会把协议原生的交互能力做残。
448
+
449
+ ## 11. 为什么 ledger-backed 是不可跳过的一层
450
+
451
+ 这是比 transport 选择更关键的部分。
452
+
453
+ ### 11.1 没有 ledger,pull 只能“看见”,不能“对账”
454
+
455
+ 主动拉取只能让你看到 Codex 里发生了什么,但它不能自动告诉你:
456
+
457
+ - 哪条结果已经回给飞书了
458
+ - 哪条结果虽然完成了,但还没回给飞书
459
+ - 哪条飞书消息已经成功进入对应 turn 了
460
+ - 哪条飞书消息只是在 bridge 侧看见了,但从未真正启动 turn
461
+
462
+ 这些都必须由中间层自己持久化。
463
+
464
+ ### 11.2 至少需要两本账
465
+
466
+ 最小可用模型至少要有两本 durable ledger。
467
+
468
+ #### A. 入站确认账本
469
+
470
+ 把下面这些关系持久化下来:
471
+
472
+ - `feishu_inbound_message_id`
473
+ - `conversationRef`
474
+ - `threadId`
475
+ - `turnId`
476
+ - `accepted_at`
477
+ - `execution_status`
478
+
479
+ 它回答的问题是:
480
+
481
+ - 这条飞书消息到底有没有成功进入 Codex thread
482
+ - 如果没有进入,是否允许恢复后重放
483
+ - 如果已经绑定到某个 `turnId`,则绝不能重复重放
484
+
485
+ #### B. 出站交付账本
486
+
487
+ 把下面这些关系持久化下来:
488
+
489
+ - `threadId`
490
+ - `turnId`
491
+ - `itemId`(如果需要更细粒度)
492
+ - `result_kind`(final reply / failure / interrupted / approval prompt / user-input prompt)
493
+ - `feishu_outbound_message_id`
494
+ - `delivery_status`(pending / delivered / failed / superseded)
495
+
496
+ 它回答的问题是:
497
+
498
+ - Codex 已经产出的哪条结果还没有回给飞书
499
+ - 哪条已经回了,不应再补发
500
+ - 哪条因为飞书投递失败,需要恢复后重试
501
+
502
+ ### 11.3 这才是“忠实转达”的核心
503
+
504
+ 只有把执行状态和交付状态分开,中间层才真正称得上是在“忠实反映 thread”,而不是在“在线转发好运时就成功”。
505
+
506
+ ## 12. transport 在新模型下该怎么重新看
507
+
508
+ ### 12.1 websocket 现在的问题是什么
509
+
510
+ 当前 websocket 模型的问题,不只是它会断,更重要的是:
511
+
512
+ - 它被产品无意中提升成了正确性的唯一在线入口
513
+ - 一旦它抖动,系统就失去会话确定性
514
+ - 用户可见语义被迫跟着 transport 状态抖动
515
+
516
+ ### 12.2 stdio 值得认真评估,但它不再是“唯一主角”
517
+
518
+ 在 pull-primary + ledger-backed 的新模型下,`stdio` 依然值得认真评估,理由有三点:
519
+
520
+ 1. 官方把它定义为默认 transport。
521
+ 2. 它更适合本机单 bridge + 单 app-server 的父子进程模型。
522
+ 3. 它可以减少 loopback socket、端口与 websocket 生命周期的一层复杂度。 citeturn1view0
523
+
524
+ 但这里的关键变化是:
525
+
526
+ > 即使最终改成 stdio,它也只是让在线通道更稳,而不是替代 reconciliation 和 ledger。
527
+
528
+ 这意味着 transport 讨论被放回了正确位置:
529
+
530
+ - 它重要
531
+ - 但它不再是产品正确性的唯一基石
532
+
533
+ ### 12.3 一个务实判断
534
+
535
+ 如果后续架构只允许本机部署、bridge 与 app-server 一一对应,我会建议把 `stdio` 作为优先评估方向;如果保留当前 daemon 化模型或有远程接入诉求,websocket 仍可能有部署便利性。但无论选哪种,都不应再做 push-only。 citeturn1view0turn2view0
536
+
537
+ ## 13. 建议的系统形态
538
+
539
+ 如果把本报告的结论压缩成一个产品级运行模型,它应当长这样:
540
+
541
+ ### 13.1 主路径
542
+
543
+ 1. 飞书来消息。
544
+ 2. bridge 先把入站消息落到入站确认账本。
545
+ 3. bridge 成功启动 turn 后,拿到 `turnId` 并绑定到账本。
546
+ 4. 之后不以实时 delta 为最终真相,而是主动拉取 `thread/read(includeTurns=true)`。
547
+ 5. 一旦确认 turn 终态,先把结果落到出站交付账本,再向飞书发送。
548
+ 6. 若飞书发送失败,账本保留 `pending/failed`,恢复后继续补发。
549
+
550
+ ### 13.2 辅路径
551
+
552
+ 1. 保留最小化的 push 通道接收审批、用户输入请求、必要 status 变化。
553
+ 2. 这些 server-initiated request 仍按在线交互处理。
554
+ 3. 如果连接抖动,至少能根据 `thread.status.activeFlags` 判断线程是否卡在等待人工决策状态。
555
+ 4. 后续若官方补出可枚举的 pending request 读取接口,再评估是否进一步降低对 push 的依赖。 citeturn2view4turn2view6turn3view2
556
+
557
+ ### 13.3 用户语义
558
+
559
+ 在这个系统里,用户看到的文案与行为也应重构:
560
+
561
+ - 不再把“socket 断开”直接表达成“任务可能没了”
562
+ - 更准确地表达为“系统正在重新确认这一轮结果”
563
+ - 若 turn 终态已在 Codex 中落定但未发送到飞书,则自动补发
564
+ - 只有在无法证明消息已进入 turn、也无法自动恢复时,才要求用户重发
565
+
566
+ ## 14. 回答用户最关心的那个根问题
567
+
568
+ 把整份报告压缩成一句最重要的话:
569
+
570
+ > 这类问题真正要解决的,不是“让实时连接永远不断”,而是“即使实时连接断了,中间层仍然能靠主动拉取和 durable ledger,把 Codex thread 的事实忠实、去重、可补偿地反映给飞书里的用户”。
571
+
572
+ 换句话说:
573
+
574
+ - 如果 `work-ally` 继续做成“脆弱的实时转发器”,那它确实站在不稳的基石上。
575
+ - 如果 `work-ally` 升级成“以主动拉取确认最终真相、以账本保证不漏不重、以最小推送承接阻塞交互的可靠语义层”,那它就是有产品意义的,而且这正是它存在的价值。
576
+
577
+ ## 15. 最终建议清单
578
+
579
+ 最后给出可供架构师直接使用的结论清单。
580
+
581
+ 1. 不再把“实时推送不断”作为产品正确性的前提。
582
+ 2. 把最终答复与恢复补偿主路径切到 `pull-primary`。
583
+ 3. 保留最小化 `push-assisted` 通道,只承接审批、用户输入请求和必要状态变化。
584
+ 4. 明确建设两本 durable ledger:入站确认账本与出站交付账本。
585
+ 5. 把 websocket / stdio 的讨论放回 transport 基线问题,而不是正确性基石问题。
586
+ 6. 若后续部署形态允许,认真评估把本机默认 transport 从 websocket 调整为 stdio。
587
+ 7. 用户文案与恢复语义同步升级,减少“连接断开 = 任务失败”的误导表达。
588
+
589
+ ## 16. 参考材料
590
+
591
+ ### 16.1 官方文档
592
+
593
+ - OpenAI Developers: `Codex App Server`
594
+ - `https://developers.openai.com/codex/app-server`
595
+
596
+ ### 16.2 仓库实现证据
597
+
598
+ - `/Users/allenfeng/Development/Repositories/work-ally/internal/modules/runtime/start.sh`
599
+ - `/Users/allenfeng/Development/Repositories/work-ally/internal/modules/runtime/supervisor.sh`
600
+ - `/Users/allenfeng/Development/Repositories/work-ally/bridge/src/config.ts`
601
+ - `/Users/allenfeng/Development/Repositories/work-ally/bridge/src/server.ts`
602
+ - `/Users/allenfeng/Development/Repositories/work-ally/bridge/src/runtime-client.ts`
603
+
604
+ ### 16.3 前序 incident 报告
605
+
606
+ - `/Users/allenfeng/Development/Repositories/work-ally/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md`