Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/features/F117-message-delivery-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,14 @@ stop hook 从这里注入即覆盖全部。和设计的出入与补充:
空闲超时,客户端的 `timeoutMs` 选项已无调用方,留待后续清理。
- **#774**:重试条件收窄为「未收到首帧」的启动超时(`isCliStartupTimeoutError`),保留 tmux 首事件看门狗在 resume
时的重试;成员开始输出之后的静默不再被吞掉重跑。
- **alpha 自测补丁(09-27)**:在 alpha(develop_base `900d87835`,`CLI_TIMEOUT_MS=60000`)里让 codex-sol 执行
`sleep 150`:它在距最后一次输出 120 秒时被停下,R 为 failed / `timeout`,诊断 `silenceDurationMs=120001`,之后同一只猫
照常应答。发现并修了一处:超时成员原本在串行时没有 done、在并行时 done 不带 errorCode,QueueProcessor 只能走抛异常的
兜底路径,于是对话底部多出一行「Error: 响应超时…」,还会推送「猫猫出错了」。现在超时成员以一个 errorCode 为 `timeout` 的
done 收尾,和 provider 失败的 done 带 `PROVIDER_EXECUTION_FAILED` 一样,由 QueueProcessor 直接记为失败。
- **CLI 接入实际按 2× 顺延**:同一次自测里,Codex CLI 空等时每 60 秒约有 400ms CPU,超过存活探针 50ms / 60s 的「忙」
门槛,所以 CLI 接入的成员实际要静默 2×`CLI_TIMEOUT_MS` 才会停。这是 F118 原有的门槛,旧的 cli-spawn 计时器也按它顺延;
这次不改,若要让顺延只给真正在算的进程,另议门槛。


### Phase K(路线 Phase 2b:Claude Agent SDK 接入,2026-09-25)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ import {
} from '../invocation/invocation-capacity-snapshot.js';
import { type InvocationParams, invokeSingleCat } from '../invocation/invoke-single-cat.js';
import { buildMcpCallbackInstructions, needsMcpInjection } from '../invocation/McpPromptInjector.js';
import { type MemberTimeoutEvent, memberTimeoutErrorText } from '../invocation/member-output-timeout.js';
import {
MEMBER_TIMEOUT_REASON,
type MemberTimeoutEvent,
memberTimeoutErrorText,
} from '../invocation/member-output-timeout.js';
import { getRichBlockBuffer } from '../invocation/RichBlockBuffer.js';
import { recordTurnOutputVerdict, requireTurnOutputAllowed } from '../invocation/response-draft-settlement.js';
import { resolveManagedSessionPolicySnapshot } from '../invocation/session-policy-snapshot.js';
Expand Down Expand Up @@ -2449,7 +2453,9 @@ export async function* routeParallel(
// identity / liveness wrongly attached to parent (instead of own turn).
const stampedDone = ownInvId && !msg.invocationId ? { ...msg, invocationId: ownInvId } : msg;
// F117 KD-22: the response is committed; report the timeout before the done, so the Queue
// counts this member failed (not cancelled) and releases its slot.
// counts this member failed (not cancelled) and releases its slot. The done names the
// timeout, as a failed provider turn's done names its failure, so the Queue settles the
// entry failed from it rather than throwing and broadcasting a second error.
if (memberTimeout) {
yield {
type: 'error' as const,
Expand All @@ -2463,6 +2469,7 @@ export async function* routeParallel(
yield projectLiveTurnExecution(
{
...stampedDone,
...(memberTimeout && stampedDone.errorCode === undefined ? { errorCode: MEMBER_TIMEOUT_REASON } : {}),
...(persistedDoneContent !== undefined ? { content: persistedDoneContent } : {}),
...(turnStoredMessageId ? { messageId: turnStoredMessageId } : {}),
isFinal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2284,7 +2284,9 @@ export async function* routeSerial(

// F117 KD-22: a member stopped by its output timeout failed, like a provider failure. The
// events it produced after the stop were dropped above, so its failure text and diagnostics
// come from what its timer kept before stopping it.
// come from what its timer kept before stopping it, and it ends with a done that names the
// timeout, as a failed provider turn's done names its failure: the Queue settles the entry
// failed from that done rather than throwing and broadcasting a second error.
if (stoppedByMemberTimeout(catSignal) && memberTimeout) {
hadError = true;
hadProviderError = true;
Expand All @@ -2293,6 +2295,7 @@ export async function* routeSerial(
...(persistedMetadata ?? { provider: '', model: '' }),
timeoutDiagnostics: memberTimeout.diagnostics,
};
doneMsg ??= { type: 'done', catId, errorCode: MEMBER_TIMEOUT_REASON, timestamp: Date.now() };
}

// F167 Phase S: this is the single route-side visibility barrier. The
Expand Down
7 changes: 7 additions & 0 deletions packages/api/test/1398-phase4-member-timeout-stop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ function assertTimedOutMember(result, catId) {
assert.ok(failure, `${catId}'s route reports the timeout`);
assert.match(failure.error, /响应超时/);
assert.ok(failure.metadata.timeoutDiagnostics.silenceDurationMs >= 1000, 'diagnostics kept from before the stop');
// Like a provider failure, the member ends with a done that names why, after its failure: the
// Queue settles the entry failed from it instead of throwing and broadcasting a second error row.
const events = result.events.filter((event) => event.catId === catId);
const done = events.findLast((event) => event.type === 'done');
assert.ok(done, `${catId}'s route ends it with a done`);
assert.equal(done.errorCode, MEMBER_TIMEOUT_REASON, 'the done names the timeout');
assert.ok(events.indexOf(failure) < events.indexOf(done), 'the failure is reported before the done');
const terminal = result.terminalOf(catId);
assert.equal(terminal.status, 'failed');
assert.equal(terminal.terminalReason, MEMBER_TIMEOUT_REASON);
Expand Down