- commit
- d1c9090
- parent
- d349be1
- author
- im_wower
- date
- 2026-03-25 23:35:11 +0800 CST
merge: land T-S006 and T-S008
21 files changed,
+361,
-60
+2,
-2
1@@ -6,7 +6,7 @@
2
3 - canonical local API: `http://100.71.210.78:4317`
4 - canonical public host: `https://conductor.makefile.so`
5-- `control-api.makefile.so`、Cloudflare Worker、D1 只算迁移期兼容件,不再作为默认控制面
6+- `control-api.makefile.so`、Cloudflare Worker、D1 只算迁移期 legacy 兼容件和残留依赖盘点目标,不再作为默认控制面
7 - `baa-hand` / `baa-shell` 只保留为接口参考,不再继续维护为主系统
8
9 主备切换、failover、switchback 和其它历史方案已经从当前主线移除;如需回溯,直接查看 tag `ha-failover-archive-2026-03-22`。
10@@ -106,7 +106,7 @@ docs/
11
12 legacy 兼容说明:
13
14-- `https://control-api.makefile.so` 只用于迁移期间兜底和识别残留依赖
15+- `https://control-api.makefile.so` 只用于迁移期间 legacy 兜底和识别残留依赖
16 - 业务查询和系统控制写入已经不再依赖 Cloudflare Worker / D1 真相源
17 - `apps/control-api-worker` 已从当前仓库移除;剩余 legacy 只保留在线上资产和历史文档背景里
18
+60,
-9
1@@ -21,8 +21,24 @@ export interface CodexdAppServerProcessLike {
2 on(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
3 }
4
5+export type CodexdAppServerTransportCloseSource =
6+ | "process.error"
7+ | "process.exit"
8+ | "stdout.end"
9+ | "stdout.error";
10+
11+export interface CodexdAppServerTransportCloseDiagnostic {
12+ exitCode?: number | null;
13+ flushedTrailingMessage: boolean;
14+ message: string;
15+ signal?: string | null;
16+ source: CodexdAppServerTransportCloseSource;
17+ trailingMessageLength: number;
18+}
19+
20 export interface CodexdAppServerStdioTransportConfig {
21 endStdinOnClose?: boolean;
22+ onCloseDiagnostic?: (diagnostic: CodexdAppServerTransportCloseDiagnostic) => void;
23 process: CodexdAppServerProcessLike;
24 }
25
26@@ -63,23 +79,47 @@ export function createCodexdAppServerStdioTransport(
27 }
28 };
29
30- const flushTrailingMessage = (): void => {
31+ const flushTrailingMessage = (): {
32+ flushedTrailingMessage: boolean;
33+ trailingMessageLength: number;
34+ } => {
35 const line = buffer.trim();
36 buffer = "";
37
38- if (line !== "") {
39- handlers?.onMessage(line);
40+ if (line === "") {
41+ return {
42+ flushedTrailingMessage: false,
43+ trailingMessageLength: 0
44+ };
45 }
46+
47+ handlers?.onMessage(line);
48+ return {
49+ flushedTrailingMessage: true,
50+ trailingMessageLength: line.length
51+ };
52 };
53
54- const closeTransport = (error: Error): void => {
55+ const closeTransport = (
56+ error: Error,
57+ diagnostic: Omit<
58+ CodexdAppServerTransportCloseDiagnostic,
59+ "flushedTrailingMessage" | "message" | "trailingMessageLength"
60+ >
61+ ): void => {
62 if (closed) {
63 return;
64 }
65
66- flushTrailingMessage();
67+ const trailingMessage = flushTrailingMessage();
68 closed = true;
69 connected = false;
70+ config.onCloseDiagnostic?.({
71+ ...diagnostic,
72+ flushedTrailingMessage: trailingMessage.flushedTrailingMessage,
73+ message: error.message,
74+ trailingMessageLength: trailingMessage.trailingMessageLength
75+ });
76 handlers?.onClose(error);
77 };
78
79@@ -109,19 +149,30 @@ export function createCodexdAppServerStdioTransport(
80 emitBufferedMessages();
81 });
82 stdout.on("end", () => {
83- closeTransport(new Error("Codex app-server stdio stdout ended."));
84+ closeTransport(new Error("Codex app-server stdio stdout ended."), {
85+ source: "stdout.end"
86+ });
87 });
88 stdout.on("error", (error) => {
89- closeTransport(error);
90+ closeTransport(error, {
91+ source: "stdout.error"
92+ });
93 });
94 config.process.on("error", (error) => {
95- closeTransport(error);
96+ closeTransport(error, {
97+ source: "process.error"
98+ });
99 });
100 config.process.on("exit", (code, signal) => {
101 closeTransport(
102 new Error(
103 `Codex app-server stdio child exited (code=${String(code)}, signal=${String(signal)}).`
104- )
105+ ),
106+ {
107+ exitCode: code,
108+ signal,
109+ source: "process.exit"
110+ }
111 );
112 });
113 },
+69,
-0
1@@ -33,6 +33,7 @@ import {
2 } from "../../../packages/codex-exec/src/index.js";
3 import {
4 createCodexdAppServerStdioTransport,
5+ type CodexdAppServerTransportCloseDiagnostic,
6 type CodexdAppServerProcessLike
7 } from "./app-server-transport.js";
8 import type {
9@@ -816,6 +817,71 @@ export class CodexdDaemon {
10 await this.stateStore.recordEvent(mapAppServerEventToRecentEvent(event));
11 }
12
13+ private async handleAppServerTransportClosed(
14+ diagnostic: CodexdAppServerTransportCloseDiagnostic
15+ ): Promise<void> {
16+ this.appServerClient = null;
17+ this.appServerInitializeResult = null;
18+ this.appServerInitializationPromise = null;
19+
20+ await this.stateStore.recordEvent({
21+ level: "warn",
22+ type: "app-server.transport.closed",
23+ message: `App-server transport closed via ${diagnostic.source}: ${diagnostic.message}`,
24+ detail: {
25+ exitCode: diagnostic.exitCode ?? null,
26+ flushedTrailingMessage: diagnostic.flushedTrailingMessage,
27+ signal: diagnostic.signal ?? null,
28+ source: diagnostic.source,
29+ trailingMessageLength: diagnostic.trailingMessageLength
30+ }
31+ });
32+
33+ const childState = this.stateStore.getChildState();
34+ const interruptedSessions = this.stateStore
35+ .listSessions()
36+ .filter(
37+ (session) =>
38+ session.status === "active" && session.threadId != null && session.currentTurnId != null
39+ );
40+
41+ for (const session of interruptedSessions) {
42+ const turnId = session.currentTurnId ?? session.lastTurnId;
43+
44+ if (turnId == null) {
45+ continue;
46+ }
47+
48+ await this.stateStore.upsertSession({
49+ ...session,
50+ currentTurnId: null,
51+ lastTurnId: turnId,
52+ lastTurnStatus: "failed",
53+ updatedAt: new Date().toISOString()
54+ });
55+ await this.stateStore.recordEvent({
56+ level: "error",
57+ type: "app-server.turn.completed.missing",
58+ message: `App-server transport closed before turn ${turnId} completed.`,
59+ detail: {
60+ childExitCode: diagnostic.exitCode ?? childState.exitCode,
61+ childPid: childState.pid,
62+ childSignal: diagnostic.signal ?? childState.signal,
63+ childStatus: childState.status,
64+ failureClass: "transport_closed_before_turn_completed",
65+ flushedTrailingMessage: diagnostic.flushedTrailingMessage,
66+ sessionId: session.sessionId,
67+ threadId: session.threadId,
68+ trailingMessageLength: diagnostic.trailingMessageLength,
69+ transportCloseMessage: diagnostic.message,
70+ transportCloseSource: diagnostic.source,
71+ turnId,
72+ turnStatusAtClose: session.lastTurnStatus
73+ }
74+ });
75+ }
76+ }
77+
78 private async patchSessionsByThreadId(
79 threadId: string,
80 update: (session: CodexdSessionRecord) => CodexdSessionRecord
81@@ -928,6 +994,9 @@ export class CodexdDaemon {
82 version: this.config.version ?? DEFAULT_APP_SERVER_VERSION
83 },
84 transport: createCodexdAppServerStdioTransport({
85+ onCloseDiagnostic: (diagnostic) => {
86+ void this.handleAppServerTransportClosed(diagnostic);
87+ },
88 process: context.child
89 })
90 });
+126,
-16
1@@ -134,6 +134,25 @@ class FakeRpcAppServerChild extends FakeChild {
2 this.stdout.emit("data", `${JSON.stringify(payload)}${trailingNewline ? "\n" : ""}`);
3 }
4
5+ finishRetryingTurn(session, turnId, completedTurn) {
6+ session.thread.turns = session.thread.turns.map((entry) =>
7+ entry.id === turnId ? completedTurn : entry
8+ );
9+ this.emitRpcMessage(
10+ {
11+ method: "turn/completed",
12+ params: {
13+ threadId: session.thread.id,
14+ turn: completedTurn
15+ }
16+ },
17+ {
18+ trailingNewline: false
19+ }
20+ );
21+ this.stdout.emit("end");
22+ }
23+
24 handleRequest(request) {
25 switch (request.method) {
26 case "initialize":
27@@ -263,22 +282,7 @@ class FakeRpcAppServerChild extends FakeChild {
28 }
29 });
30 setTimeout(() => {
31- session.thread.turns = session.thread.turns.map((entry) =>
32- entry.id === turnId ? completedTurn : entry
33- );
34- this.emitRpcMessage(
35- {
36- method: "turn/completed",
37- params: {
38- threadId: session.thread.id,
39- turn: completedTurn
40- }
41- },
42- {
43- trailingNewline: false
44- }
45- );
46- this.stdout.emit("end");
47+ this.finishRetryingTurn(session, turnId, completedTurn);
48 }, 25);
49 return;
50 }
51@@ -311,6 +315,12 @@ class FakeRpcAppServerChild extends FakeChild {
52 }
53 }
54
55+class FakeRpcAppServerDisconnectBeforeCompletedChild extends FakeRpcAppServerChild {
56+ finishRetryingTurn() {
57+ this.stdout.emit("end");
58+ }
59+}
60+
61 class FakeAppServerAdapter {
62 constructor(defaultCwd, options = {}) {
63 this.defaultCwd = defaultCwd;
64@@ -852,6 +862,106 @@ test("CodexdDaemon flushes the final stdio completion event so sequential sessio
65 }
66 });
67
68+test("CodexdDaemon classifies transport closes before a legal completion as a new failure", async () => {
69+ const repoRoot = mkdtempSync(join(tmpdir(), "codexd-stdio-disconnect-test-"));
70+ const config = resolveCodexdConfig({
71+ logsDir: join(repoRoot, "logs"),
72+ repoRoot,
73+ stateDir: join(repoRoot, "state")
74+ });
75+ const fakeChild = new FakeRpcAppServerDisconnectBeforeCompletedChild();
76+ const daemon = new CodexdDaemon(config, {
77+ env: {
78+ HOME: repoRoot
79+ },
80+ spawner: {
81+ spawn(command, args, options) {
82+ assert.equal(command, "codex");
83+ assert.deepEqual(args, ["app-server"]);
84+ assert.equal(options.cwd, repoRoot);
85+
86+ queueMicrotask(() => {
87+ fakeChild.emit("spawn");
88+ });
89+
90+ return fakeChild;
91+ }
92+ }
93+ });
94+
95+ await daemon.start();
96+
97+ try {
98+ const firstSession = await daemon.createSession({
99+ cwd: repoRoot,
100+ purpose: "duplex"
101+ });
102+ await daemon.createTurn({
103+ input: "First turn.",
104+ sessionId: firstSession.sessionId
105+ });
106+ await waitFor(() => {
107+ const current = daemon.getSession(firstSession.sessionId);
108+ return current?.lastTurnStatus === "completed" ? current : null;
109+ });
110+
111+ const secondSession = await daemon.createSession({
112+ cwd: repoRoot,
113+ purpose: "duplex"
114+ });
115+ const secondTurn = await daemon.createTurn({
116+ input: "Second turn.",
117+ sessionId: secondSession.sessionId
118+ });
119+
120+ const failedSecondSession = await waitFor(() => {
121+ const current = daemon.getSession(secondSession.sessionId);
122+ const missingCompletedEvent = daemon
123+ .getStatusSnapshot()
124+ .recentEvents.events.find(
125+ (event) =>
126+ event.type === "app-server.turn.completed.missing" &&
127+ event.detail?.turnId === secondTurn.turnId
128+ );
129+
130+ return current?.lastTurnStatus === "failed" && missingCompletedEvent != null
131+ ? {
132+ event: missingCompletedEvent,
133+ session: current
134+ }
135+ : null;
136+ });
137+ assert.equal(failedSecondSession.session.currentTurnId, null);
138+ assert.equal(failedSecondSession.session.lastTurnId, secondTurn.turnId);
139+ assert.equal(failedSecondSession.session.lastTurnStatus, "failed");
140+ assert.deepEqual(failedSecondSession.event.detail, {
141+ childExitCode: null,
142+ childPid: 4242,
143+ childSignal: null,
144+ childStatus: "running",
145+ failureClass: "transport_closed_before_turn_completed",
146+ flushedTrailingMessage: false,
147+ sessionId: secondSession.sessionId,
148+ threadId: secondSession.threadId,
149+ trailingMessageLength: 0,
150+ transportCloseMessage: "Codex app-server stdio stdout ended.",
151+ transportCloseSource: "stdout.end",
152+ turnId: secondTurn.turnId,
153+ turnStatusAtClose: "inProgress"
154+ });
155+ assert.equal(
156+ daemon.getStatusSnapshot().recentEvents.events.some(
157+ (event) =>
158+ event.type === "app-server.turn.completed" &&
159+ event.detail?.turnId === secondTurn.turnId
160+ ),
161+ false
162+ );
163+ } finally {
164+ await daemon.stop();
165+ }
166+});
167+
168 async function fetchJson(url, init) {
169 const response = await fetch(url, init);
170
1@@ -11,6 +11,14 @@
2 - session B 的最终 `turn/completed` 以无换行尾包到达
3 - codexd 仍然把 `lastTurnStatus` 正确收口到 `completed`
4
5+同时,`codexd` 现在会把“合法 `turn/completed` 根本没到,就先 transport close”的场景单独分类:
6+
7+- recent events 先记 `app-server.transport.closed`
8+- 若关闭时还有 `currentTurnId`,再记 `app-server.turn.completed.missing`
9+- `detail.failureClass = "transport_closed_before_turn_completed"`
10+
11+这类诊断不再归到 `BUG-008`。
12+
13 ## 现象
14
15 向 codexd 创建第一个 session 并发 turn,成功完成(turn.completed)。
16@@ -94,6 +102,20 @@ High —— 单次对话能通,但多轮/多 session 场景(即 duplex 核
17 - 第二次失败的 error detail: codexErrorInfo.responseStreamDisconnected.httpStatusCode = null,additionalDetails = "timeout waiting for child process to exit"
18 - child 进程(PID 20904)在失败后仍处于 running 状态,不是 crash
19
20+## Reopen 规则
21+
22+下面这些仍属于已修复范围,不 reopen `BUG-008`:
23+
24+- recent events 能看到该 turn 的 `app-server.turn.completed`
25+- 即使 completed 落在无换行尾包,session 最终仍收口到 `lastTurnStatus: completed`
26+- 中间出现 `Reconnecting... N/5` / `willRetry=true`,但最终 completed 到达
27+
28+下面这些应新开 bug,不算 `BUG-008` 复发:
29+
30+- `app-server.transport.closed` 出现后,该 turn 没有任何合法 `app-server.turn.completed`
31+- `codexd` 追加 `app-server.turn.completed.missing`
32+- 事件 detail 标明 `failureClass = "transport_closed_before_turn_completed"`
33+
34 ## 剩余风险
35
36-如果后续在线上再次复现“第二个 session 必然卡死”,且日志能证明 `turn/completed` 根本没有从 app-server 发出,那么应作为新的独立 bug 重新打开;当前代码与测试还没有看到这类证据。
37+当前代码与测试已经把“尾包无换行但 completed 实际发出”和“transport 提前断流导致 completed 缺失”拆成两类;后一类如果在线上再次出现,应作为新的 child / transport 故障单独跟踪。
1@@ -6,6 +6,13 @@
2
3 `apps/codexd/src/app-server-transport.ts` 现在会在 stdio 连接关闭前先冲刷尾部缓冲区,确保最后一条没有换行的 JSON-RPC 消息也会被交给 `CodexAppServerClient`。`apps/codexd/src/index.test.js` 新增了顺序两个 session 的回归测试,覆盖第二个 turn 先 `willRetry=true`、再以无换行尾包发出 `turn/completed` 的路径。
4
5+另外,`codexd` 现在会对“没有合法 completed 就先断流”给出独立诊断:
6+
7+- recent events 记录 `app-server.transport.closed`
8+- 若关闭时 turn 仍未完成,额外记录 `app-server.turn.completed.missing`
9+- 对应 session 会从 `inProgress` 明确收口到 `failed`
10+- 事件 detail 带 `failureClass = "transport_closed_before_turn_completed"`
11+
12 ## 现象
13
14 向 codexd 提交 turn 后,Codex 实际上已经产出完整回复(agentMessage 在 events.jsonl 中有记录),但 session 的 `lastTurnStatus` 始终停留在 `inProgress`,不更新为 `completed`。
15@@ -105,6 +112,21 @@ High —— 核心链路(发消息 → 得到完成状态)不通,调用方
16 - events.jsonl 里已确认的完整 agentMessage:「可以,我现在能稳定工作。」,对应 turn 019d1b95-b947-72c1-b6c2-02ee0a56202e
17 - lastTurnStatus 可能的值:inProgress / completed / failed;问题是 completed 从未出现
18
19+## Reopen 规则
20+
21+下面这些仍算 `BUG-010` 已修复范围,不 reopen:
22+
23+- `turn/completed` 的确发出了,只是恰好落在无换行尾包
24+- recent events 中能看到该 turn 的 `app-server.turn.completed`
25+- session 最终收口到 `lastTurnStatus: completed`
26+
27+下面这些应视为新的 bug:
28+
29+- transport / child 提前断流,recent events 出现 `app-server.transport.closed`
30+- 同一 turn 没有任何合法 `app-server.turn.completed`
31+- `codexd` 记录 `app-server.turn.completed.missing`
32+- 事件 detail 标明 `failureClass = "transport_closed_before_turn_completed"`
33+
34 ## 剩余风险
35
36-如果未来再次出现 `stdout` 提前结束,但最后根本没有发出合法的 `turn/completed` 消息,这会是新的 child / transport 故障,不属于这次修复覆盖的范围。
37+未来如果再次出现 `stdout` 提前结束,且最终没有合法 `turn/completed`,现在会被 codexd 明确归类为新的 child / transport 故障,不再混入本 bug。
+2,
-2
1@@ -8,9 +8,9 @@
2
3 按当前工作区状态,这 3 个 bug 都已完成修复,但仍保留文档作为根因与回归记录:
4
5-1. `BUG-008` — 已随 `BUG-010` 一并修复;如果未来再次出现“根本没有发出合法 turn/completed 就断流”,应作为新 bug reopen
6+1. `BUG-008` — 已随 `BUG-010` 一并修复;如果后续 recent events 出现 `app-server.turn.completed.missing` / `failureClass=transport_closed_before_turn_completed`,应作为新的 child / transport bug 记录,而不是 reopen 旧尾包问题
7 2. `BUG-009` — 已修复;如果后续测试绕开 `withRuntimeFixture(...)` 或关闭路径本身阻塞,仍需新开专项问题
8-3. `BUG-010` — 已修复;当前剩余风险与 child / transport 提前断流场景有关
9+3. `BUG-010` — 已修复;如果后续还有 `app-server.transport.closed` 且同一 turn 没有合法 `app-server.turn.completed`,应按新 bug 跟踪
10
11 修复任务:
12
+2,
-2
1@@ -21,7 +21,7 @@
2
3 - `conductor-daemon` 本地 API 是这些业务接口的真相源
4 - `status-api` 仍是只读状态视图
5-- Cloudflare Worker / D1 / `control-api.makefile.so` 都不再是这批业务接口的主路径
6+- Cloudflare Worker / D1 / `control-api.makefile.so` 都只剩 legacy 兼容或残留依赖盘点背景,不再是这批业务接口的主路径
7
8 ## 入口
9
10@@ -279,7 +279,7 @@ truth source:
11 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口;只有本地 `4317` 不可达时才需要显式改到公网
12 - `BAA_CONTROL_API_BASE` 只保留两个兼容点:`conductor-daemon` 仍读取这个历史变量名作为 upstream/public API base,`status-api` 只在手工或旧配置缺少 `BAA_CONDUCTOR_LOCAL_API` 时回退使用
13 - 默认 launchd 不再给 `status-api` 写入 `BAA_CONTROL_API_BASE`
14-- legacy `control-api.makefile.so` 不再是默认或 canonical truth source
15+- 如果旧文档或配置里还出现 legacy `control-api.makefile.so`,只能按迁移兼容 / 残留依赖盘点目标理解,绝不再是默认或 canonical truth source
16 - `status-api` 负责把该状态整理成 JSON 或 HTML
17
18 当前端点:
+1,
-1
1@@ -28,7 +28,7 @@
2
3 - 本地 `4317` 是当前主真相源
4 - `conductor.makefile.so` 是公网访问入口
5-- 不再提供单独 `control-api` 域名作为业务接口主入口
6+- 如果文档或旧配置还提到单独 `control-api` 域名,也只按 legacy 兼容 / 残留依赖盘点理解,不再作为业务接口主入口
7
8 ## 给 AI 的最小使用规则
9
+4,
-4
1@@ -40,7 +40,7 @@
2
3 - `baa-hand` / `baa-shell` 只作为参考实现
4 - `baa-conductor` 成为唯一主项目
5-- `control-api.makefile.so` 不再作为默认业务接口
6+- `control-api.makefile.so` 不再作为默认业务接口;如果线上仍保留同名域名,也只按 legacy 兼容背景理解
7
8 ## 3. 迁移边界
9
10@@ -195,7 +195,7 @@
11 - local: `http://100.71.210.78:4317`
12 - public: `https://conductor.makefile.so`
13
14-不再保留单独 `control-api.makefile.so` 作为兼容或默认目标。
15+不再把单独 `control-api.makefile.so` 写成默认或推荐目标;如果线上仍保留同名域名,也只按 legacy 兼容或残留依赖盘点目标理解。
16
17 ## 8. 实施顺序
18
19@@ -250,13 +250,13 @@
20 ### 第四批(当前主线已完成)
21
22 - 浏览器、CLI、运维脚本和文档默认目标已经切到 `conductor.makefile.so`
23-- `control-api.makefile.so`、Cloudflare Worker、D1 与 public `status-api` 不再承担默认控制面角色
24+- `control-api.makefile.so`、Cloudflare Worker、D1 与 public `status-api` 即使仍有线上残留,也不再承担默认控制面角色
25
26 ## 9. 删旧范围
27
28 cutover 完成后,优先删除或归档:
29
30-- `control-api.makefile.so` 作为主业务接口的所有描述
31+- 把 `control-api.makefile.so` 写成主业务接口的所有当前时态描述
32 - Cloudflare Worker / D1 的默认控制面角色
33 - `status-api` 的公网职责
34 - `baa-hand` / `baa-shell` 作为主系统的维护承诺
+1,
-1
1@@ -20,7 +20,7 @@
2 - canonical public host: `https://conductor.makefile.so`
3 - `status-api` `http://100.71.210.78:4318` 只作为本地只读观察面,默认回源 `BAA_CONDUCTOR_LOCAL_API`,当前 canonical 值是 `http://100.71.210.78:4317`
4 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口
5-- 默认 launchd 只把 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 只保留代码层兼容回退,不再把它当 canonical truth source
6+- 默认 launchd 只把 legacy 变量名 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 只保留代码层兼容回退,不再把它当 canonical truth source
7 - 推荐仓库路径:`/Users/george/code/baa-conductor`
8 - repo 内的 plist 只作为模板;真正加载的是脚本渲染出来的安装副本
9
+18,
-0
1@@ -112,6 +112,24 @@ BAA_CODEXD_LOCAL_API_BASE=http://127.0.0.1:4319
2 - 只有终态失败才把 session 的 `lastTurnStatus` 写成 `failed`
3 - stdio transport 在 child `stdout` 结束或进程退出前,会先冲刷最后一个未换行的 JSON-RPC 消息,再关闭连接;`turn/completed` 不会再因为尾包没有换行而丢失
4 - 顺序创建多个 session / thread 时,即使后一个 turn 先收到 `willRetry=true` 的错误,再在流尾收到最终完成事件,session 的 `lastTurnStatus` 也应收口到 `completed`
5+- 如果 stdio transport 提前关闭,recent events 会先记录 `app-server.transport.closed`
6+- 如果关闭时某个 session 仍有 `currentTurnId`,codexd 会把该 turn 收口为 `failed`,并追加 `app-server.turn.completed.missing`
7+- 这类事件会带 `detail.failureClass = "transport_closed_before_turn_completed"`,用来明确区分“合法 completed 缺失”与“completed 已发出但尾包无换行”的旧问题
8+
9+## Reopen 规则
10+
11+下面这些仍属于已修复的旧尾包问题范围:
12+
13+- recent events 能看到目标 turn 的 `app-server.turn.completed`
14+- session 最终收口到 `lastTurnStatus: completed`
15+- transport close 前冲刷了无换行尾包,completed 成功入库
16+
17+下面这些应按新的 child / transport 故障 reopen:
18+
19+- recent events 出现 `app-server.transport.closed`
20+- 同一 turn 没有任何合法 `app-server.turn.completed`
21+- codexd 额外记录 `app-server.turn.completed.missing`
22+- 事件 detail 标明 `failureClass = "transport_closed_before_turn_completed"`
23
24 ## 运行职责边界
25
+2,
-2
1@@ -9,7 +9,7 @@
2 - canonical public host: `https://conductor.makefile.so`
3 - local status view: `http://100.71.210.78:4318`
4 - `status-api` 默认真相源:`BAA_CONDUCTOR_LOCAL_API` -> `http://100.71.210.78:4317/v1/system/state`
5-- `BAA_CONTROL_API_BASE` 只剩两个保留点:`conductor` 运行时仍用这个历史变量名解析 upstream/public API base,`status-api` 只在手工兼容场景下回退读取
6+- legacy 变量名 `BAA_CONTROL_API_BASE` 只剩两个保留点:`conductor` 运行时仍用它解析 upstream/public API base,`status-api` 只在手工兼容场景下回退读取
7
8 ## 共享变量
9
10@@ -124,4 +124,4 @@ Firefox WS 派生规则:
11 --status-api-host 100.71.210.78
12 ```
13
14-默认 mini 安装不需要显式传 `--control-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
15+默认 mini 安装不需要显式传 `--control-api-base`;这个参数名本身也是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
+3,
-3
1@@ -40,13 +40,13 @@
2
3 - `~/.config/baa-conductor/runtime-secrets.env`
4
5-里提取 `BAA_SHARED_TOKEN` 并生成它;如果新的 env 文件不存在,还会回退读取 legacy `control-api-worker.secrets.env`。
6+里提取 `BAA_SHARED_TOKEN` 并生成它;如果新的 env 文件不存在,还会回退读取 legacy `control-api-worker.secrets.env` 这个迁移前文件名。
7
8 说明:
9
10 - `codexd` 独立安装时不需要共享 token
11 - `status-api` 默认真相源是 `BAA_CONDUCTOR_LOCAL_API`
12-- `--control-api-base` 仍然保留,但只影响 `conductor` 安装副本;默认值就是 `https://conductor.makefile.so`
13+- `--control-api-base` 仍然保留为 legacy 兼容参数名,但只影响 `conductor` 安装副本;默认值就是 `https://conductor.makefile.so`
14 - `--codexd-local-api-base` 会同时写给 `codexd` 和 `conductor`
15 - `codexd` 正式运行面只写入 `app-server` 会话链路所需默认值
16 - `codexd` 正式服务面只保留 `/healthz`、`/v1/codexd/status`、`/v1/codexd/sessions`、`/v1/codexd/turn`、`/v1/codexd/events`
17@@ -113,7 +113,7 @@
18 --status-api-host 100.71.210.78
19 ```
20
21-默认 mini 安装不需要显式传 `--control-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
22+默认 mini 安装不需要显式传 `--control-api-base`;这个参数名本身是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
23
24 单独安装 `codexd`:
25
+1,
-1
1@@ -28,7 +28,7 @@ npx --yes pnpm -r build
2
3 说明:
4
5-- 默认 mini 静态检查不需要显式传 `--control-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖
6+- 默认 mini 静态检查不需要显式传 `--control-api-base`;这个参数名本身是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖
7 - `status-api` 的有效默认真相源仍然是 `--local-api-base http://100.71.210.78:4317`
8 - `check-launchd.sh` 现在只校验 `conductor` 安装副本里的 `BAA_CONTROL_API_BASE`;其他服务要求该变量不存在
9 - `check-launchd.sh` 现在也会校验 `conductor` 安装副本里的 `BAA_CODEXD_LOCAL_API_BASE`
+4,
-4
1@@ -6,7 +6,7 @@
2
3 ## 当前代码基线
4
5-- 主线基线:`main@1078d2b`
6+- 主线基线:`main@d349be1`
7 - 任务文档已统一收口到 `tasks/`
8 - 当前活动任务见 `tasks/TASK_OVERVIEW.md`
9 - `T-S001` 到 `T-S004` 已经合入主线
10@@ -29,7 +29,7 @@
11 - `mini` 本地 conductor:`http://100.71.210.78:4317`
12 - `mini` 本地 status-api:`http://100.71.210.78:4318`,仅用于本地只读观察
13 - `mini` 本地 codexd:`http://127.0.0.1:4319`
14-- `https://control-api.makefile.so`,仅用于迁移期兼容与残留依赖盘点
15+- `https://control-api.makefile.so`,仅作为迁移期 legacy 兼容壳和残留依赖盘点目标,不再是当前入口
16
17 ## 当前保留内容
18
19@@ -49,17 +49,17 @@
20 - `T-S003`:`status-api`、launchd 模板和 runtime 文档已切到当前 `conductor` 主接口,`BAA_CONTROL_API_BASE` 只保留为兼容覆盖
21 - `T-S004`:`conductor-daemon` 测试已统一通过 `withRuntimeFixture(...)` 收口 cleanup,`BUG-009` 已修复
22 - `T-S005`:默认 launchd 现在只把 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 和 `worker-runner` 不再携带这个变量
23+- `T-S006`:`tasks/`、`plans/`、`README` 与 `docs/api/**` / `docs/runtime/**` 已把 `control-api` 收口为 legacy 背景,不再写成当前默认入口
24 - `T-S007`:`ConductorRuntime.stop()` 已补专项测试,覆盖 listener 释放、Firefox bridge client 关闭和重复 stop 的幂等行为
25
26 ## 下一步任务
27
28-- `T-S006`:清理历史任务 / 缺陷 / 文档里的 `control-api` 命名残留
29 - `T-S008`:给 codexd child / transport 提前断流补诊断与 reopen 规则
30
31 ## 当前仍需关注
32
33 - 如果后续再次出现 app-server 根本没有发出合法 `turn/completed` 就提前断流,这属于新的 child / transport 故障,应单独新开 bug
34 - `BAA_CONTROL_API_BASE` 仍保留在仓库里,定位是兼容入口,不是 canonical truth source
35-- 按任务边界没有完全清掉的 legacy 命名仍散落在部分历史任务文档、缺陷文档和兼容说明里
36+- 仍保留的 `control-api` 命名已经限定在历史任务卡、legacy 测试路径、兼容变量名和外部残留资产说明里;如果未来要继续删旧,需要单独评估文件名和兼容面
37 - 如果未来新增 runtime 测试绕开 `withRuntimeFixture(...)`,同类 listener 泄漏仍可能重新出现
38 - 这次没有改 `ConductorRuntime.stop()` 内部逻辑;如果未来关闭路径本身阻塞,还需要单独补运行时层测试
+2,
-0
1@@ -1,5 +1,7 @@
2 # Task T-S002:清理 control-api-worker 残留与坏测试
3
4+> 历史说明:本卡记录的是该任务开工时的状态;当前主线默认入口已是 `http://100.71.210.78:4317` / `https://conductor.makefile.so`,这里的 `control-api*` 只应按 legacy 背景理解。
5+
6 ## 直接给对话的提示词
7
8 读 `/Users/george/code/baa-conductor/tasks/T-S002.md` 任务文档,完成开发任务。
+2,
-0
1@@ -1,5 +1,7 @@
2 # Task T-S003:切换 status-api 到 conductor 主接口
3
4+> 历史说明:本卡记录的是该任务开工时的状态;当前主线默认入口已是 `http://100.71.210.78:4317` / `https://conductor.makefile.so`,`control-api.makefile.so` 和 `BAA_CONTROL_API_BASE` 只保留 legacy 兼容语义。
5+
6 ## 直接给对话的提示词
7
8 读 `/Users/george/code/baa-conductor/tasks/T-S003.md` 任务文档,完成开发任务。
+2,
-0
1@@ -1,5 +1,7 @@
2 # Task T-S005:收口 `BAA_CONTROL_API_BASE` 兼容入口
3
4+> 历史说明:本卡记录的是该任务开工时的状态;当前主线默认入口已是 `http://100.71.210.78:4317` / `https://conductor.makefile.so`,`BAA_CONTROL_API_BASE` 只保留 legacy 兼容语义。
5+
6 ## 直接给对话的提示词
7
8 读 `/Users/george/code/baa-conductor/tasks/T-S005.md` 任务文档,完成开发任务。
+1,
-1
1@@ -17,7 +17,7 @@
2
3 - 仓库:`/Users/george/code/baa-conductor`
4 - 分支:`main`
5-- 提交:`1078d2b`
6+- 提交:`d349be1`
7 - 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
8
9 ## 建议分支名
+13,
-10
1@@ -6,10 +6,10 @@
2
3 - canonical local API: `http://100.71.210.78:4317`
4 - canonical public host: `https://conductor.makefile.so`
5-- `control-api.makefile.so`、Cloudflare Worker、D1 只剩迁移期兼容残留
6+- `control-api.makefile.so`、Cloudflare Worker、D1 只剩迁移期 legacy 兼容残留和依赖盘点用途
7 - `baa-hand` / `baa-shell` 只保留为接口语义参考,不再作为主系统维护
8 - 当前任务卡都放在本目录
9-- 当前任务基线:`main@1078d2b`
10+- 当前任务基线:`main@d349be1`
11
12 ## 最近完成任务
13
14@@ -20,20 +20,23 @@
15 3. [`T-S003.md`](./T-S003.md):切换 `status-api` 到 `conductor` 主接口
16 4. [`T-S004.md`](./T-S004.md):修复 `conductor-daemon` 测试 listener 泄漏
17 5. [`T-S005.md`](./T-S005.md):收口 `BAA_CONTROL_API_BASE` 兼容入口
18-6. [`T-S007.md`](./T-S007.md):补 `ConductorRuntime.stop()` 关闭路径专项测试
19+6. [`T-S006.md`](./T-S006.md):清理历史 `control-api` 命名残留
20+7. [`T-S007.md`](./T-S007.md):补 `ConductorRuntime.stop()` 关闭路径专项测试
21+
22+说明:
23+
24+- 这些已完成任务卡里出现的 `control-api` / `BAA_CONTROL_API_BASE` 描述,默认都指任务开工时的历史背景;当前主线 canonical 口径仍是 `http://100.71.210.78:4317` 和 `https://conductor.makefile.so`
25
26 ## 当前活动任务
27
28-围绕剩余风险,当前建议继续推进这 2 张任务卡:
29+围绕剩余风险,当前建议继续推进这 1 张任务卡:
30
31-1. [`T-S006.md`](./T-S006.md):清理历史 `control-api` 命名残留
32-2. [`T-S008.md`](./T-S008.md):补 codexd child / transport 断流诊断与 reopen 规则
33+1. [`T-S008.md`](./T-S008.md):补 codexd child / transport 断流诊断与 reopen 规则
34
35-并行建议:
36+说明:
37
38-- `T-S006` 主要改文档和状态口径
39+- `T-S006` 已完成并回写当前文档口径
40 - `T-S008` 主要改 codexd 与 bug/runtime 文档
41-- 这两张卡写范围基本不重叠,可以并行
42
43 ## 任务文档约定
44
45@@ -54,7 +57,7 @@
46
47 - 直接在当前 `tasks/` 目录新建任务卡
48 - 所有新任务默认以 `100.71.210.78:4317` 和 `conductor.makefile.so` 为 canonical 接口面
49-- `control-api.makefile.so` 只允许作为删旧前的兼容背景出现
50+- `control-api.makefile.so` 只允许作为删旧前的 legacy 兼容背景或残留依赖盘点说明出现
51 - 当前任务编号继续使用 `T-S***`
52 - 能并行的任务优先拆开,并明确写清允许修改的目录
53 - 新任务文档结构参考 [`task-doc-template.md`](./task-doc-template.md)