@zhushanwen/subagent-core 0.7.1 → 0.9.0
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.
- package/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
- package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
- package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
- package/dist/execution/engine/engine-discovery-scan.cjs +325 -154
- package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
- package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
- package/dist/execution/engine/engine-discovery-scan.js +1 -1
- package/dist/execution/relay-env.js +5 -4
- package/dist/index.cjs +9799 -7954
- package/dist/index.d.cts +1896 -1360
- package/dist/index.d.ts +1896 -1360
- package/dist/index.js +8952 -7513
- package/dist.bundle/index.cjs +7897 -6054
- package/package.json +5 -5
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/record-store-last-line.test.ts +12 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- package/src/core/logger.ts +1 -1
- package/src/execution/__tests__/alive-store.test.ts +117 -1
- package/src/execution/__tests__/batch-finalized.test.ts +206 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
- package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
- package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
- package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
- package/src/execution/__tests__/delivery-methods.test.ts +90 -176
- package/src/execution/__tests__/dialog-queue.test.ts +70 -3
- package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
- package/src/execution/__tests__/execute-nesting.test.ts +8 -5
- package/src/execution/__tests__/execution-record.test.ts +8 -148
- package/src/execution/__tests__/finalize-record.test.ts +295 -129
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
- package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
- package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
- package/src/execution/__tests__/idle-gc.test.ts +60 -0
- package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
- package/src/execution/__tests__/model-config-service.test.ts +111 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
- package/src/execution/__tests__/nested-visibility.test.ts +13 -1
- package/src/execution/__tests__/notify-ledger.test.ts +42 -1
- package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
- package/src/execution/__tests__/record-binding.test.ts +613 -0
- package/src/execution/__tests__/record-origin.test.ts +318 -0
- package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
- package/src/execution/__tests__/record-store.test.ts +72 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
- package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
- package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
- package/src/execution/__tests__/run-orchestration.test.ts +199 -0
- package/src/execution/__tests__/session-file-gc.test.ts +35 -4
- package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
- package/src/execution/__tests__/state-marker.test.ts +288 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
- package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
- package/src/execution/__tests__/subagent-service.test.ts +17 -215
- package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
- package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
- package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
- package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
- package/src/execution/alive-store.ts +37 -28
- package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
- package/src/execution/config.ts +1 -1
- package/src/execution/conversation-continuation.ts +541 -0
- package/src/execution/dialog-queue.ts +60 -12
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
- package/src/execution/engine/__tests__/registry.test.ts +0 -1
- package/src/execution/engine/__tests__/routing.test.ts +0 -1
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
- package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
- package/src/execution/engine/client/engine-client.ts +111 -26
- package/src/execution/engine/client/mirror.ts +9 -0
- package/src/execution/engine/client/remote-engine.ts +52 -34
- package/src/execution/engine/client/reverse-router.ts +6 -27
- package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
- package/src/execution/engine/common/capability-gate.ts +27 -0
- package/src/execution/engine/common/data-dir.ts +3 -2
- package/src/execution/engine/common/errors.ts +1 -1
- package/src/execution/engine/common/journal-wiring.ts +18 -9
- package/src/execution/engine/common/run-signals.ts +83 -0
- package/src/execution/engine/d8-compat.ts +1 -1
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
- package/src/execution/engine/host/host-bridge.ts +26 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/host/spawned-children.ts +1 -1
- package/src/execution/engine/inflight-snapshot.ts +92 -0
- package/src/execution/engine/port.ts +52 -54
- package/src/execution/engine/types.ts +11 -14
- package/src/execution/execution-record.ts +15 -47
- package/src/execution/finalize-record.ts +94 -197
- package/src/execution/idle-gc.ts +9 -3
- package/src/execution/lifecycle-manager.ts +25 -361
- package/src/execution/lifecycle-predicates.ts +22 -19
- package/src/execution/manifest-store.ts +40 -42
- package/src/execution/model-config-service.ts +1 -1
- package/src/execution/notifier.ts +39 -1
- package/src/execution/notify-host.ts +13 -4
- package/src/execution/path-encoding.ts +6 -0
- package/src/execution/record-entry.ts +20 -2
- package/src/execution/record-store.ts +925 -160
- package/src/execution/relay-env.ts +8 -3
- package/src/execution/round-supervisor/domain.ts +17 -5
- package/src/execution/round-supervisor/service-binding.test.ts +89 -53
- package/src/execution/round-supervisor/service-binding.ts +71 -21
- package/src/execution/round-supervisor/supervisor.ts +23 -9
- package/src/execution/service/record-access.ts +547 -0
- package/src/execution/service/record-lifecycle.ts +542 -0
- package/src/execution/service/run-orchestration.ts +1647 -0
- package/src/execution/service/service-bootstrap.ts +111 -0
- package/src/execution/service/service-constants.ts +27 -0
- package/src/execution/service/session-baselines.ts +401 -0
- package/src/execution/service/sync-collect-domain.ts +442 -0
- package/src/execution/service/workflow-dispatch.ts +567 -0
- package/src/execution/session-file-gc.ts +11 -8
- package/src/execution/session-reconstructor.ts +44 -1
- package/src/execution/sessions-index.ts +20 -3
- package/src/execution/settled-watchdog.ts +86 -43
- package/src/execution/state-marker.ts +516 -0
- package/src/execution/stream-sink.ts +1 -1
- package/src/execution/subagent-actions-core.ts +53 -31
- package/src/execution/subagent-service.ts +643 -2955
- package/src/execution/subprocess-agent-runner.ts +46 -323
- package/src/execution/types.ts +60 -43
- package/src/execution/ui-request-handler-factory.ts +5 -0
- package/src/execution/worktree-manager.ts +1 -1
- package/src/execution/worktree-registry.ts +12 -8
- package/src/index.ts +16 -5
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
- package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
- package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
- package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
- package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +1 -1
- package/src/orchestration/models/ports.ts +20 -13
- package/src/orchestration/models/types.ts +11 -13
- package/src/orchestration/run-snapshot.ts +18 -22
- package/src/orchestration/worker-message-pump.ts +33 -57
- package/src/shared/atomic-write.ts +10 -8
- package/dist/chunk-T2SYBW3I.js +0 -24
- package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
- package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
- package/src/execution/__tests__/finalized-marker.test.ts +0 -104
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
- package/src/execution/finalized-marker.ts +0 -70
- package/src/execution/tombstone-store.ts +0 -72
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// src/execution/conversation-continuation.ts
|
|
2
|
+
//
|
|
3
|
+
// [H1 U2] ConversationContinuation——chat 域统一进 run 域的唯一新增组件(设计
|
|
4
|
+
// docs/design/subagent-chat-run-unification.md §3.4,伪码即实现契约)。
|
|
5
|
+
//
|
|
6
|
+
// 一个 chatMode record 一个实例:对话容器 = record,续聊轮 = 「新 run + resume 锚点」
|
|
7
|
+
//(pi --session 续写原 session 文件)。本类承载三面:
|
|
8
|
+
// - onMessage:D4 状态迁移表的 message 行(终态分流 / 在途轮打断入队 / 轮间直派);
|
|
9
|
+
// - dispatchRound:每轮派发(stale-child 兜底 → 载荷组装守卫 → 泛化派发主干);
|
|
10
|
+
// - onRunSettled:run 应答回调(resolve 时点 = agent_settled,D7)——终态守卫整体
|
|
11
|
+
// early-return → settle 段交棒 → 成功/失败轮末分流(D7)→ drain 队列。
|
|
12
|
+
//
|
|
13
|
+
// 红线(单写者不变量,两级声明)的实现归属:
|
|
14
|
+
// - cancel/正常路径 = 构造性保证:本类单飞——abort 收敛(进程退出 = run 应答收敛)
|
|
15
|
+
// 是 drain 派发下一轮的前置事件,编排上不存在并发派发(dispatchRoundGuarded
|
|
16
|
+
// 同步段占位 activeRunId,异步窗内重入被拒)。
|
|
17
|
+
// - 崩溃路径 = 引擎退出链收割(engine-client teardownProcess,红线①)+ 派发前
|
|
18
|
+
// stale-child 兜底(host.killStaleChild,红线②)+ 宿主重启单窗口(经验性,登记)。
|
|
19
|
+
//
|
|
20
|
+
// 编排能力经 ContinuationHost 注入(service 闭包)——本模块可独立编译与单测;
|
|
21
|
+
// 通知门(notifyGateAllowsDelivery)双闸消费:成功分支 route 前 + 失败分支独立
|
|
22
|
+
// 载荷发出前(B#19 投递门照迁移),与 onRunSettled 的 status 终态面正交双闸。
|
|
23
|
+
|
|
24
|
+
import type { AgentOutcome, ResumeAnchor } from "@zhushanwen/subagent-engine-sdk";
|
|
25
|
+
|
|
26
|
+
import { toErrorMessage } from "../core/error-message.ts";
|
|
27
|
+
import { getLogger } from "../core/logger.ts";
|
|
28
|
+
|
|
29
|
+
import { resurrectClosed } from "./execution-record.ts";
|
|
30
|
+
// 轮终 outcome 入参(权威定义在 finalize-record.ts,本文件 re-export 供 host 契约引用;
|
|
31
|
+
// finalize-record 不反向依赖本模块,无循环)。
|
|
32
|
+
import { type RoundSettlementOutcome } from "./finalize-record.ts";
|
|
33
|
+
export type { RoundSettlementOutcome };
|
|
34
|
+
import { engineConversationUpgradeUnsupportedError } from "./engine/common/capability-gate.ts";
|
|
35
|
+
import { PI_POOL_KEY } from "./engine/host/pi-host-binding.ts";
|
|
36
|
+
import { type BgNotifyRecord, notifyGateAllowsDelivery } from "./notifier.ts";
|
|
37
|
+
import {
|
|
38
|
+
type SettledWatchdogFireInfo,
|
|
39
|
+
disarmRoundFromProtocol,
|
|
40
|
+
noteRoundSettledFromProtocol,
|
|
41
|
+
} from "./settled-watchdog.ts";
|
|
42
|
+
import { isReconnectableFinalReason } from "./types.ts";
|
|
43
|
+
import type { ExecutionRecord } from "./types.ts";
|
|
44
|
+
|
|
45
|
+
const logger = getLogger("subagents");
|
|
46
|
+
|
|
47
|
+
/** 失败通知的恢复指引尾段([T2-③/LC-1] 可达性语义——失败原因 + 恢复指引必须可达宿主)。 */
|
|
48
|
+
const FAILURE_RECOVERY_TAIL =
|
|
49
|
+
"Recovery: re-send your message (action:'message') to continue — the conversation " +
|
|
50
|
+
"context is preserved (session file intact), or use action:'close' to discard it.";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 泛化派发主干的轮次回调面(service.kickOffChatRound 泛化参数,D6)。
|
|
54
|
+
* run resolve(= agent_settled)/ run reject(prepare 期失败)/ acquire 被打断
|
|
55
|
+
* (无 run 产生)三分,全部回流 Continuation 单点收口。
|
|
56
|
+
*/
|
|
57
|
+
export interface ContinuationRoundHandlers {
|
|
58
|
+
/** run 应答(resolve = agent_settled,D7 定案)。 */
|
|
59
|
+
onSettled(outcome: AgentOutcome): void;
|
|
60
|
+
/** run reject(prepare 期失败——进程创建前;转失败轮末分流)。 */
|
|
61
|
+
onRejected(err: unknown): void;
|
|
62
|
+
/** acquire 排队窗被打断/取消(无 run 产生):不终态化、不通知,直接 drain。 */
|
|
63
|
+
onAbandoned(): void;
|
|
64
|
+
/** settled-watchdog fire(中段无进展 / 收尾段上界):kill 在途轮,run 收敛后
|
|
65
|
+
* 经 onSettled/onRejected 走失败分支统一收口。 */
|
|
66
|
+
onWatchdogFire(fire: SettledWatchdogFireInfo): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** dispatchRound ④ 经泛化派发主干发起 run 的入参(每轮语义载荷)。 */
|
|
70
|
+
export interface ContinuationDispatchInput {
|
|
71
|
+
/** 本轮聚合消息正文(多条排队消息聚合为一轮输入,§3.1 打断路径)。 */
|
|
72
|
+
task: string;
|
|
73
|
+
/** resume 锚点(续聊轮恒带;首轮 undefined)。 */
|
|
74
|
+
resume?: ResumeAnchor;
|
|
75
|
+
/** 轮级 abort 通道(record controller 级联 + 打断通道,见 dispatchRound)。 */
|
|
76
|
+
signal: AbortSignal;
|
|
77
|
+
handlers: ContinuationRoundHandlers;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Continuation 对宿主编排能力的窄依赖(service 闭包注入;单测全 mock)。
|
|
82
|
+
*/
|
|
83
|
+
export interface ContinuationHost {
|
|
84
|
+
/** 泛化派发主干(D6:kickOffChatRound 共享部分——pool acquire / stream / chat 键
|
|
85
|
+
* 组装 / armMidRoundNoProgress),应答经 handlers 回流。 */
|
|
86
|
+
dispatchChatRound(record: ExecutionRecord, input: ContinuationDispatchInput): void;
|
|
87
|
+
/** 轮终簿记(doFinalizeRoundToIdle wrapper,D7 outcome 入参)。 */
|
|
88
|
+
finalizeRoundOutcome(record: ExecutionRecord, outcome: RoundSettlementOutcomeAlias): Promise<void>;
|
|
89
|
+
/** 成功通知路由(collectCoordinator.route——正文权威 = record.result)。 */
|
|
90
|
+
routeRecord(record: ExecutionRecord): void;
|
|
91
|
+
/** 失败通知直投(独立构造载荷——不经 route,正文不读 record.result)。 */
|
|
92
|
+
notifyRecord(record: BgNotifyRecord): void;
|
|
93
|
+
/** 红线②派发前兜底:镜像在途子进程活着 → kill 等退出(引擎存活期状态错配)。 */
|
|
94
|
+
killStaleChild(recordId: string): Promise<void>;
|
|
95
|
+
/** watchdog fire 的 kill 手段(kill + 协议 cancel——run 收敛由杀链驱动)。 */
|
|
96
|
+
killRoundChild(recordId: string, source: string): void;
|
|
97
|
+
/** D5 gate 判据:record 所属引擎 conversation 位(unsupported / 未注册 = false)。 */
|
|
98
|
+
upgradeGateAllows(record: ExecutionRecord): boolean;
|
|
99
|
+
/** D4 revive 格的宿主面:revive 后的 record register + 迁移上报(entry 落盘)。 */
|
|
100
|
+
reviveClosedRecord(record: ExecutionRecord): void;
|
|
101
|
+
/** 轮始簿记(store.markRoundStarted:status=running + result/resumable 清除 +
|
|
102
|
+
* 迁移上报 entry 落盘——[U2b 修复轮/D2] 归口原 dispatchRoundAsync 三行现场写)。 */
|
|
103
|
+
markRoundStarted(record: ExecutionRecord): void;
|
|
104
|
+
/** D4 close 行的立即终态化收口(closeChatIdle:doFinalizeRecord 语义 + notifyClosed)。 */
|
|
105
|
+
closeNow(record: ExecutionRecord): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** host 契约内的 outcome 形态(RoundSettlementOutcome,见顶部 import)。 */
|
|
109
|
+
type RoundSettlementOutcomeAlias = RoundSettlementOutcome;
|
|
110
|
+
|
|
111
|
+
/** Mutable 断言(chatMode readonly 字段的升级写点——actions-core 写点①同款形态)。 */
|
|
112
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* ConversationContinuation(每 chatMode record 一个实例;§3.4 全规格)。
|
|
116
|
+
*/
|
|
117
|
+
export class ConversationContinuation {
|
|
118
|
+
/** 在途轮标识(单飞守卫;undefined = 无在途轮。派发异步窗内为占位值)。 */
|
|
119
|
+
private activeRunId: string | undefined;
|
|
120
|
+
/** 在途轮打断通道(D2:abort 轮 signal,非 record 级 cancel——cancelBackground 会
|
|
121
|
+
* 终态化销毁 record,不可用于打断)。 */
|
|
122
|
+
private activeController: AbortController | undefined;
|
|
123
|
+
/** FIFO 待发消息(在途轮打断窗内到达的 message;abort 收敛后 drain 聚合为一轮)。 */
|
|
124
|
+
private readonly queue: string[] = [];
|
|
125
|
+
|
|
126
|
+
constructor(
|
|
127
|
+
private readonly record: ExecutionRecord,
|
|
128
|
+
private readonly host: ContinuationHost,
|
|
129
|
+
) {}
|
|
130
|
+
|
|
131
|
+
/** 当前绑定的 record(service 侧缓存刷新判定用——跨重启冷查重建新对象)。 */
|
|
132
|
+
get boundRecord(): ExecutionRecord {
|
|
133
|
+
return this.record;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get hasActiveRound(): boolean {
|
|
137
|
+
return this.activeRunId !== undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 待发队列长度(诊断/测试)。 */
|
|
141
|
+
get pendingCount(): number {
|
|
142
|
+
return this.queue.length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── 首轮派发(conversation:true start,§3.5 终态数据流入口)──────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 首轮派发:不经 D4 分流与锚点守卫(新 record 无 sessionFile——run 应答回填),
|
|
149
|
+
* 无 resume(新 session)。守卫 throw 直达调用方(executeViaEngine 同步段)。
|
|
150
|
+
*/
|
|
151
|
+
startFirstRound(task: string): void {
|
|
152
|
+
this.dispatchRoundGuarded([task], true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── D4 状态迁移表:message 行 ────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* message 入口(chatActions.deliverChatMessage 改写后的编排落点)。
|
|
159
|
+
*
|
|
160
|
+
* D4 表逐格:
|
|
161
|
+
* - record 已终态(user-close/cancelled)→ 硬拒(endedMessageGuard 同语义);
|
|
162
|
+
* - record closed 但属可重连终态(disconnected/parent-shutdown)→ revive +
|
|
163
|
+
* 非 chatMode 升级格(D4 revive 格:gate 放行 → 置位 chatMode=true 再续聊;
|
|
164
|
+
* gate 不过 → 硬拒 + fork/重派指引);
|
|
165
|
+
* - running(有在途轮)→ D2 打断:abort 在途轮 signal(record 不终态化)+
|
|
166
|
+
* 入队,abort 收敛后 drain;
|
|
167
|
+
* - running(轮间 idle)→ 直接派发新轮。
|
|
168
|
+
*
|
|
169
|
+
* @throws Error 终态硬拒 / 锚点缺失 / worktree 绑定丢失(同步拒绝,文案即指引)。
|
|
170
|
+
*/
|
|
171
|
+
onMessage(text: string): void {
|
|
172
|
+
const record = this.record;
|
|
173
|
+
if (record.status !== "running") {
|
|
174
|
+
this.reviveOrThrow();
|
|
175
|
+
}
|
|
176
|
+
if (this.activeRunId !== undefined) {
|
|
177
|
+
// D2 打断语义:消息即时生效,永不「忙」拒绝。占位窗(activeController 未建)
|
|
178
|
+
// 到达的消息仅入队——派发完成后轮终 drain 承接。
|
|
179
|
+
this.activeController?.abort();
|
|
180
|
+
this.queue.push(text);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.dispatchRoundGuarded([text], false);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── D4 状态迁移表:close 行(abort + 清队列;立即终态化由 host.closeNow 承接)──
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* close 的 Continuation 侧职责:abort 在途轮 + 清空队列(D4——chat 域不再置
|
|
190
|
+
* closeAfterRound 挂起标志,不等轮终)。终态化(closed/user-close + notifyClosed)
|
|
191
|
+
* 由 host.closeNow(closeChatIdle 收口序列)承接,二者由 service.closeSubagent 编排。
|
|
192
|
+
*/
|
|
193
|
+
abortAndClearQueue(): void {
|
|
194
|
+
this.activeController?.abort();
|
|
195
|
+
this.queue.length = 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── run 应答回调(resolve = agent_settled,D7)────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 轮末分流唯一入口(设计 §3.4 onRunSettled)。
|
|
202
|
+
*
|
|
203
|
+
* 【终态守卫 = 整体 early-return,先于一切分支】record 已终态化(close/cancel
|
|
204
|
+
* 抢先)→ 直接返回:不 roundIdle(否则 close 终态化被 doFinalizeRoundToIdle 的
|
|
205
|
+
* 「覆盖 closed 回滚为 running」机制回滚)、不通知、不 drain——S7 精确语义。
|
|
206
|
+
* notifyGate 门与此正交并存(门判 closedReason 编排面、守卫判 status 终态面,
|
|
207
|
+
* 两闸判据不同源不互替)。
|
|
208
|
+
*/
|
|
209
|
+
onRunSettled(outcome: AgentOutcome): void {
|
|
210
|
+
this.clearActiveRound();
|
|
211
|
+
if (this.record.status !== "running") {
|
|
212
|
+
this.queue.length = 0;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// settle 段交棒(run 应答驱动,D7 v5 轻形态——零协议扩展):先于轮终簿记,
|
|
216
|
+
// watchdog 停表早于状态写。resource 未挂载时幂等 no-op。
|
|
217
|
+
noteRoundSettledFromProtocol(this.record.id);
|
|
218
|
+
if (outcome.error !== undefined) {
|
|
219
|
+
void this.settleRoundFailed(outcome.error);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
void this.settleRoundSuccess(outcome);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** run reject(prepare 期失败——进程创建前):合成失败轮末分流。 */
|
|
226
|
+
onRoundRejected(err: unknown): void {
|
|
227
|
+
this.clearActiveRound();
|
|
228
|
+
if (this.record.status !== "running") return;
|
|
229
|
+
void this.settleRoundFailed(toErrorMessage(err));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** acquire 被打断/排队窗取消(无 run 产生):不终态化、不通知,直接 drain。 */
|
|
233
|
+
onRoundAbandoned(): void {
|
|
234
|
+
this.clearActiveRound();
|
|
235
|
+
if (this.record.status !== "running") return;
|
|
236
|
+
this.drain();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** settled-watchdog fire:kill 在途轮(协议 cancel + 镜像置死)+ abort 轮 signal
|
|
240
|
+
* ——run 收敛(进程退出)后经 onSettled/onRejected 走失败分支统一收口(含失败
|
|
241
|
+
* 通知),不在回调内直接簿记(单写者:收口只走 run 应答一条路)。 */
|
|
242
|
+
onWatchdogFire(fire: SettledWatchdogFireInfo): void {
|
|
243
|
+
this.host.killRoundChild(this.record.id, `settled watchdog (${fire.phase})`);
|
|
244
|
+
this.activeController?.abort();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── 派发(§3.4 dispatchRound)────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 同步守卫段 + 占位(构造性单飞):守卫 throw 直达调用方(tool 错误面——
|
|
251
|
+
* async 函数内的 throw 会变 rejected promise,`void` 调用丢传导,故守卫必须
|
|
252
|
+
* 在同步段完成);activeRunId 同步占位,dispatchRoundAsync 的 await 窗内
|
|
253
|
+
* 重入被拒(单写者构造性保证的实现面)。
|
|
254
|
+
*/
|
|
255
|
+
private dispatchRoundGuarded(msgs: string[], firstRound: boolean): void {
|
|
256
|
+
const record = this.record;
|
|
257
|
+
if (record.status !== "running") return;
|
|
258
|
+
if (this.activeRunId !== undefined) return; // 双保险(正常路径 onMessage 已分流)
|
|
259
|
+
if (!firstRound) {
|
|
260
|
+
if (!record.sessionFile) {
|
|
261
|
+
// §3.1 失败路径表:锚点缺失同步拒绝,文案即指引。
|
|
262
|
+
throw new Error(`no transcript anchor on record ${record.id}; re-dispatch (action:'start')`);
|
|
263
|
+
}
|
|
264
|
+
if (record.hadWorktree === true && !record.worktreeHandle) {
|
|
265
|
+
// 跨重启 worktree 绑定丢失守卫(承接 resumeColdRound 同款):防 resume 的
|
|
266
|
+
// spawn cwd 静默回落主 repo,子 agent 直接编辑主仓库。
|
|
267
|
+
throw new Error(
|
|
268
|
+
`subagent ${record.id} was created with worktree isolation, but that binding was lost when the parent process restarted; ` +
|
|
269
|
+
`resuming it now would run in the main repository and bypass the isolation. ` +
|
|
270
|
+
`Recovery: use action:'close' to release this subagent, then action:'start' a new one with worktree isolation.`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!record.controller) {
|
|
275
|
+
// chatMode background record 创建时一定有 controller;防御性检查(MF-4 行动语言)。
|
|
276
|
+
throw new Error(
|
|
277
|
+
`subagent ${record.id} is not ready for a new message (internal state error). ` +
|
|
278
|
+
`Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
// 占位(正式 roundId 在派发段定稿):单飞窗从本同步段开始。
|
|
282
|
+
this.activeRunId = `${record.id}#dispatching`;
|
|
283
|
+
void this.dispatchRoundAsync(msgs, firstRound);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async dispatchRoundAsync(msgs: string[], firstRound: boolean): Promise<void> {
|
|
287
|
+
const record = this.record;
|
|
288
|
+
// ① stale-child 兜底(红线第二级②):镜像在途子进程活着 → kill 等退出。
|
|
289
|
+
// 引擎已死场景的孤儿由引擎退出链收割兜底(红线第二级①,engine-client)。
|
|
290
|
+
try {
|
|
291
|
+
await this.host.killStaleChild(record.id);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
// 兜底失败不阻断派发(与 terminateChatSession 的 best-effort 同语义);失败
|
|
294
|
+
// 信号已在 host 侧留痕。残余双写窗由 S4 实测与红线③登记承接。
|
|
295
|
+
void err;
|
|
296
|
+
}
|
|
297
|
+
if (record.status !== "running") {
|
|
298
|
+
// 兜底窗内 close/cancel 抢先终态化 → 本轮作废(终态守卫同构语义)。
|
|
299
|
+
this.clearActiveRound();
|
|
300
|
+
this.queue.length = 0;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ② 载荷组装:轮级 signal(record controller 级联 + 打断通道)。
|
|
305
|
+
// model 身份重建 / resume 锚点 / chat 键组装 / sessionRootId 注入 / pool
|
|
306
|
+
// acquire / priority 在泛化派发主干(host.dispatchChatRound——归自
|
|
307
|
+
// resumeColdRound/kickOffChatRound 现有实现,D6)。
|
|
308
|
+
const controller = new AbortController();
|
|
309
|
+
const onRecordAbort = (): void => controller.abort();
|
|
310
|
+
const recordSignal = record.controller!.signal;
|
|
311
|
+
if (recordSignal.aborted) controller.abort();
|
|
312
|
+
else recordSignal.addEventListener("abort", onRecordAbort, { once: true });
|
|
313
|
+
|
|
314
|
+
const roundNo = (record.round ?? 0) + 1;
|
|
315
|
+
this.activeRunId = `${record.id}#${roundNo}`;
|
|
316
|
+
this.activeController = controller;
|
|
317
|
+
// 轮始簿记归口([U2b 修复轮/D2] store.markRoundStarted:status=running 重申 +
|
|
318
|
+
// 清上一轮 result 与 resumable——§5.4 isStreaming 公式要求 result undefined 才
|
|
319
|
+
// 显示 streaming,不清则续轮流仍显示 waiting、spinner 无法恢复;迁移上报 entry
|
|
320
|
+
// 落盘让 GUI 派生缓存失效、从 waiting 切回 spinner。U1 原语簿记为原三行现场写
|
|
321
|
+
// 的超集,多出 notifyChange 刷新)。record 不在 store 内存的形态 = false 旁路
|
|
322
|
+
// debug 留痕(生产链路 Continuation 绑定的 record 恒在册)。
|
|
323
|
+
this.host.markRoundStarted(record);
|
|
324
|
+
const concludeRound = (): void => {
|
|
325
|
+
recordSignal.removeEventListener("abort", onRecordAbort);
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
this.host.dispatchChatRound(record, {
|
|
329
|
+
// 多条聚合为一轮输入(§3.1:cancel 宽限窗内多条消息按序聚合)。
|
|
330
|
+
task: msgs.join("\n\n"),
|
|
331
|
+
resume: firstRound ? undefined : this.resumeAnchor(),
|
|
332
|
+
signal: controller.signal,
|
|
333
|
+
handlers: {
|
|
334
|
+
onSettled: (outcome) => {
|
|
335
|
+
concludeRound();
|
|
336
|
+
this.onRunSettled(outcome);
|
|
337
|
+
},
|
|
338
|
+
onRejected: (err) => {
|
|
339
|
+
concludeRound();
|
|
340
|
+
this.onRoundRejected(err);
|
|
341
|
+
},
|
|
342
|
+
onAbandoned: () => {
|
|
343
|
+
concludeRound();
|
|
344
|
+
this.onRoundAbandoned();
|
|
345
|
+
},
|
|
346
|
+
onWatchdogFire: (fire) => this.onWatchdogFire(fire),
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
// 主干同步段 throw(stream 创建 / 端口解析等):转失败轮末分流(与 run reject
|
|
351
|
+
// 同语义——record 保持 running-resumable,宿主经失败通知感知)。
|
|
352
|
+
concludeRound();
|
|
353
|
+
this.onRoundRejected(err);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** resume 锚点 = record identity(sessionFile 续写原文件;pi 无池化 poolKey 恒 'shared')。 */
|
|
358
|
+
private resumeAnchor(): ResumeAnchor {
|
|
359
|
+
return {
|
|
360
|
+
sessionRef: {
|
|
361
|
+
recordId: this.record.id,
|
|
362
|
+
...(this.record.sessionFile !== undefined ? { sessionFile: this.record.sessionFile } : {}),
|
|
363
|
+
},
|
|
364
|
+
poolKey: PI_POOL_KEY,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── 轮末分流(D7)────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
/** 轮终守护清理(旧 idle 相位帧 disarmRoundFromProtocol 语义的 run 应答驱动承接):
|
|
371
|
+
* 轮终簿记完成 = 本轮等待窗口终结,两段守护一并清(收尾段不残留 armed——fire 会对
|
|
372
|
+
* 已收敛轮误发 kill/cancel)。drain 派发下一轮时 armMidRoundNoProgress 重挂新窗。 */
|
|
373
|
+
private disarmRoundWatchdog(): void {
|
|
374
|
+
disarmRoundFromProtocol(this.record.id);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 成功分支:轮终簿记(success)→ notifyGate 门 → route(次序:route 晚于簿记)。 */
|
|
378
|
+
private async settleRoundSuccess(outcome: AgentOutcome): Promise<void> {
|
|
379
|
+
const record = this.record;
|
|
380
|
+
await this.host.finalizeRoundOutcome(record, { kind: "success", content: outcome.content });
|
|
381
|
+
this.disarmRoundWatchdog();
|
|
382
|
+
// 成功通知:「门 → route」双闸(v6 显式迁移自 settleChatRoundFromResponse——
|
|
383
|
+
// 门判 closedReason 编排面,拦 parent-new/parent-fork 编排性关闭的迟到应答与
|
|
384
|
+
// cancelled 的迟到帧;route 正文权威 = record.result = 本轮 content)。
|
|
385
|
+
if (notifyGateAllowsDelivery(record.closedReason)) {
|
|
386
|
+
this.host.routeRecord(record);
|
|
387
|
+
}
|
|
388
|
+
this.drain();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 失败/中断分支:lastError + 轮终簿记(failed——round 同样 +1,result = 前值 ??
|
|
393
|
+
* 失败摘要)+ 失败通知(独立构造载荷——不经 route(record):其正文恒读
|
|
394
|
+
* record.result = 前值,直接复用会以旧正文冒充失败通知;正文 = 失败摘要 + 恢复
|
|
395
|
+
* 指引,可达性迁移自 [T2-③/LC-1])+ record 保持 running-resumable(MF-6)。
|
|
396
|
+
* 发前过 notifyGate 门(B#19 投递门照迁移:拦 cancelled 竞态窗防双发、
|
|
397
|
+
* parent-new/parent-fork 竞态窗防僵尸回执注入已切换 session——守卫是入口一次性
|
|
398
|
+
* 判定,覆盖不了簿记 await 链内的中途关闭窗)。
|
|
399
|
+
*/
|
|
400
|
+
private async settleRoundFailed(reason: string): Promise<void> {
|
|
401
|
+
const record = this.record;
|
|
402
|
+
await this.host.finalizeRoundOutcome(record, { kind: "failed", reason });
|
|
403
|
+
this.disarmRoundWatchdog();
|
|
404
|
+
if (!notifyGateAllowsDelivery(record.closedReason)) {
|
|
405
|
+
this.drain();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// dedup key = record:round(notifier 65 行口径不变):round 已随簿记 +1,
|
|
409
|
+
// 失败轮通知与上一轮成功通知天然分离(60s 窗不吞)。
|
|
410
|
+
const notify: BgNotifyRecord = {
|
|
411
|
+
id: record.id,
|
|
412
|
+
// status:"closed" + outcome:"failed" 载荷 = buildLlmContent 的失败文案形态
|
|
413
|
+
// `Subagent "agent" (id) failed: <error>`——载荷只是通知文案载体,record 实态
|
|
414
|
+
// 保持 running-resumable(容器未被销毁,status 面 GUI 照常显示运行中)。
|
|
415
|
+
status: "closed",
|
|
416
|
+
closedReason: "gc",
|
|
417
|
+
outcome: "failed",
|
|
418
|
+
agent: record.agent,
|
|
419
|
+
...(record.model !== undefined ? { model: record.model } : {}),
|
|
420
|
+
error: `round did not complete: ${reason}. ${FAILURE_RECOVERY_TAIL}`,
|
|
421
|
+
startedAt: record.startedAt,
|
|
422
|
+
endedAt: Date.now(),
|
|
423
|
+
round: record.round,
|
|
424
|
+
...(record.sessionFile !== undefined ? { sessionFile: record.sessionFile } : {}),
|
|
425
|
+
};
|
|
426
|
+
this.host.notifyRecord(notify);
|
|
427
|
+
this.drain();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 队列 drain:abort 收敛(单写者前置满足)后按序派发;多条聚合为一轮输入。
|
|
432
|
+
*
|
|
433
|
+
* [A2] 内部续派路径的守卫 throw 必须就地转错误面:本方法运行在 settle 后续派
|
|
434
|
+
*(settleRoundSuccess/settleRoundFailed 尾部,async 函数体内的同步调用)与
|
|
435
|
+
* onRoundAbandoned 的 fire-and-forget 链上,dispatchRoundGuarded 的同步守卫 throw
|
|
436
|
+
*(锚点缺失 / worktree 绑定丢失 / controller 缺失)若逃逸即成 unhandled rejection
|
|
437
|
+
*(Node ≥15 默认崩宿主)且队列消息静默丢失。可达触发例:首轮在途时 message 打断
|
|
438
|
+
* 入队 → 首轮崩溃(合成 outcome 无 sessionFile)→ 失败 settle → drain 以
|
|
439
|
+
* firstRound=false 走锚点守卫 → throw。转换语义:失败通知(独立载荷过 notifyGate
|
|
440
|
+
* 门,与 settleRoundFailed 失败单发同构——record 保持 running-resumable,载荷
|
|
441
|
+
* closed+failed 仅是文案载体)+ 队列丢弃留痕(warn)。onMessage 同步入口的守卫
|
|
442
|
+
* throw 保留(设计 §3.1 失败路径表的工具错误面,不经本方法)。
|
|
443
|
+
*/
|
|
444
|
+
private drain(): void {
|
|
445
|
+
if (this.record.status !== "running") {
|
|
446
|
+
this.queue.length = 0;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (this.queue.length === 0) return;
|
|
450
|
+
const next = this.queue.splice(0);
|
|
451
|
+
try {
|
|
452
|
+
this.dispatchRoundGuarded(next, false);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
const reason = toErrorMessage(err);
|
|
455
|
+
logger.warn(
|
|
456
|
+
`[subagent] queued message dispatch rejected for ${this.record.id}: ${reason} — ` +
|
|
457
|
+
`${next.length} queued message(s) dropped`,
|
|
458
|
+
);
|
|
459
|
+
if (notifyGateAllowsDelivery(this.record.closedReason)) {
|
|
460
|
+
// dedup 身份必须独立于同轮失败通知(settleRoundFailed 缺省 key = `id:round`):
|
|
461
|
+
// 主可达场景「首轮崩溃 → 失败 settle(通知1 发出)→ drain 守卫 throw → 丢弃
|
|
462
|
+
// 通知(通知2)」中两通知同轮同 key,沿用缺省 key 会被 ledger/内核按 key 永久
|
|
463
|
+
// 去重吞掉(notifier dedupe 永久 + ledger 幂等)——「队列消息被丢」的显式反馈
|
|
464
|
+
// 永不可达。故经 dedupKey 传 `id:round:drain-drop`:
|
|
465
|
+
// - 带 round:不同轮的 drain 丢弃是不同事件(各轮 queue 的消息不同,每次都
|
|
466
|
+
// 应显式反馈),不带 round 会让本 bug 在「第二轮及以后」以同构路径复现;
|
|
467
|
+
// - 同轮重放同 key 合理:同轮 drain 丢弃是同一事件的反馈(queue 已 splice
|
|
468
|
+
// 清空,单飞守卫下同轮不会二次丢弃),at-least-once 幂等语义成立。
|
|
469
|
+
const dropDedupKey = this.record.round != null
|
|
470
|
+
? `${this.record.id}:${this.record.round}:drain-drop`
|
|
471
|
+
: `${this.record.id}:drain-drop`;
|
|
472
|
+
this.host.notifyRecord({
|
|
473
|
+
id: this.record.id,
|
|
474
|
+
// status:"closed" + outcome:"failed" 载荷 = 失败文案形态(settleRoundFailed
|
|
475
|
+
// 同构)——载荷只是通知文案载体,record 实态保持 running-resumable。
|
|
476
|
+
status: "closed",
|
|
477
|
+
closedReason: "gc",
|
|
478
|
+
outcome: "failed",
|
|
479
|
+
agent: this.record.agent,
|
|
480
|
+
...(this.record.model !== undefined ? { model: this.record.model } : {}),
|
|
481
|
+
error: `queued message could not be dispatched: ${reason}`,
|
|
482
|
+
startedAt: this.record.startedAt,
|
|
483
|
+
endedAt: Date.now(),
|
|
484
|
+
round: this.record.round,
|
|
485
|
+
...(this.record.sessionFile !== undefined ? { sessionFile: this.record.sessionFile } : {}),
|
|
486
|
+
dedupKey: dropDedupKey,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private clearActiveRound(): void {
|
|
493
|
+
this.activeRunId = undefined;
|
|
494
|
+
this.activeController = undefined;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── D4 revive 格(closed 可重连终态 → revive [+ 非 chatMode 升级] → 续聊)──
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* 终态分流(onMessage 入口的 record.status !== "running" 分支)。
|
|
501
|
+
*
|
|
502
|
+
* - user-close/cancelled → 硬拒(D4 表 closed 硬拒格;文案与 endedMessageGuard
|
|
503
|
+
* 的主动告别分支同语义);
|
|
504
|
+
* - 可重连终态(disconnected/parent-shutdown,RECONNECTABLE_FINAL_REASONS)→
|
|
505
|
+
* 非 chatMode 升级格(D4 revive 格 v4 显式化:水合保留持久化 chatMode 后,
|
|
506
|
+
* `chatMode !== true` 的 record 收到 message → 升级置位 chatMode=true 再续聊;
|
|
507
|
+
* 语义承接原 cold-resurrect 的无条件置位(U6 已删)——session shutdown 时被
|
|
508
|
+
* disposeAllRecords 关成 parent-shutdown 的在途 one-shot 正靠此路径保持可续)
|
|
509
|
+
* + D5 gate 前置(conversation 位检查,gate 不过 → 硬拒 + fork/重派指引);
|
|
510
|
+
* - 其余 closed(gc/parent-new/parent-fork)→ 按不可重连硬拒(fork-from 指引)。
|
|
511
|
+
*/
|
|
512
|
+
private reviveOrThrow(): void {
|
|
513
|
+
const record = this.record;
|
|
514
|
+
if (record.status !== "closed" || !isReconnectableFinalReason(record.closedReason)) {
|
|
515
|
+
const reasonDesc = record.closedReason !== undefined ? ` (closedReason: ${record.closedReason})` : "";
|
|
516
|
+
throw new Error(
|
|
517
|
+
`subagent ${record.id} was deliberately closed by user${reasonDesc} — ` +
|
|
518
|
+
`it cannot be messaged or resumed; nothing can reattach to it. ` +
|
|
519
|
+
`Recovery: start a new subagent (action:'start'); use action:'list' with includeFinished:true to review its final output (add includeWorkflow:true to also see workflow-dispatched subagents).`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (record.chatMode !== true) {
|
|
523
|
+
// D5 gate 前置(双写点②):gate 不过 → 硬拒 + fork/重派指引,防 unsupported
|
|
524
|
+
// 引擎升级后续聊行为悬空。
|
|
525
|
+
if (!this.host.upgradeGateAllows(record)) {
|
|
526
|
+
throw engineConversationUpgradeUnsupportedError(record.engine ?? "pi");
|
|
527
|
+
}
|
|
528
|
+
(record as Mutable<ExecutionRecord>).chatMode = true;
|
|
529
|
+
}
|
|
530
|
+
if (!resurrectClosed(record)) {
|
|
531
|
+
// closed → running 回边失败 = 竞态终态化(close/cancel 抢先)——按已终态硬拒。
|
|
532
|
+
throw new Error(
|
|
533
|
+
`subagent ${record.id} was closed while the message was being processed — it cannot be messaged. ` +
|
|
534
|
+
`Recovery: start a new subagent (action:'start').`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
// revive 宿主面:register(跨重启重建后不在内存的形态)+ 迁移上报(entry 落盘,
|
|
538
|
+
// live/reload 视图同步)。
|
|
539
|
+
this.host.reviveClosedRecord(record);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
@@ -249,22 +249,29 @@ export class DialogGlobalQueue {
|
|
|
249
249
|
* 不影响其他 child 的 pending dialog(TC-E4 case 2 子测试 2)。
|
|
250
250
|
*/
|
|
251
251
|
rejectChildDialogs(child: DialogChildRef): void {
|
|
252
|
-
//
|
|
252
|
+
// [清理顺序] 先把同 child 的排队项摘出队列,再 settle current——settleItem(current)
|
|
253
|
+
// 会推进队列(processNext 把队首提升为 current),若顺序反了,同 child 的排队项在
|
|
254
|
+
// 摘除前已被提升为 current 并逃过本次清理(残留到 30min 队列级超时才释放)。
|
|
255
|
+
const matched: QueueItem[] = [];
|
|
256
|
+
if (this.queue.length > 0) {
|
|
257
|
+
const remaining: QueueItem[] = [];
|
|
258
|
+
for (const item of this.queue) {
|
|
259
|
+
if (item.childPid === child.pid) {
|
|
260
|
+
matched.push(item);
|
|
261
|
+
} else {
|
|
262
|
+
remaining.push(item);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.queue = remaining;
|
|
266
|
+
}
|
|
267
|
+
// 正在处理的项(可能永不 settle,必须由这里解阻塞并推进下一个)
|
|
253
268
|
if (this.current && this.current.childPid === child.pid) {
|
|
254
269
|
this.settleItem(this.current, { cancelled: true });
|
|
255
270
|
}
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
for (const item of this.queue) {
|
|
260
|
-
if (item.childPid === child.pid) {
|
|
261
|
-
// settle 该 child 的 pending Promise 为 cancelled
|
|
262
|
-
this.settleItem(item, { cancelled: true });
|
|
263
|
-
} else {
|
|
264
|
-
remaining.push(item);
|
|
265
|
-
}
|
|
271
|
+
// 已摘除的排队项:settle Promise(非 current,不触发推进)
|
|
272
|
+
for (const item of matched) {
|
|
273
|
+
this.settleItem(item, { cancelled: true });
|
|
266
274
|
}
|
|
267
|
-
this.queue = remaining;
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
/**
|
|
@@ -359,3 +366,44 @@ export class DialogGlobalQueue {
|
|
|
359
366
|
return this.queue.length;
|
|
360
367
|
}
|
|
361
368
|
}
|
|
369
|
+
|
|
370
|
+
// ============================================================
|
|
371
|
+
// 活跃队列登记 + 子进程退出取消(SR-4 接线的宿主侧触达面)
|
|
372
|
+
// ============================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 进程级活跃 dialog 队列(生产单实例:宿主壳创建后经 createUiRequestHandlerForMode
|
|
376
|
+
* 传入,登记点在该工厂内——壳侧无需额外接线)。
|
|
377
|
+
*
|
|
378
|
+
* 为什么需要模块级登记:队列实例由宿主壳持有(它是跨子进程的全局串行队列),而
|
|
379
|
+
* 「子进程退出」信号从 core 的引擎镜像层(SpawnedChildrenMirror)到达——两者之间
|
|
380
|
+
* 没有实例引用链,模块级登记点是交界面(对齐 host-ui-endpoint 的
|
|
381
|
+
* setHostUiRequestEndpoint 先例)。
|
|
382
|
+
*/
|
|
383
|
+
let activeDialogQueue: DialogGlobalQueue | undefined;
|
|
384
|
+
|
|
385
|
+
/** 登记活跃队列(createUiRequestHandlerForMode 调用;undefined = 注销)。 */
|
|
386
|
+
export function registerActiveDialogQueue(queue: DialogGlobalQueue | undefined): void {
|
|
387
|
+
activeDialogQueue = queue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 子进程退出通知:取消该 pid 的全部挂起 dialog(current + 排队项,经
|
|
392
|
+
* rejectChildDialogs 的既有语义)。
|
|
393
|
+
*
|
|
394
|
+
* 调用点 = 引擎镜像层(SpawnedChildrenMirror 的 exited 分支与 killAll 整体置死)。
|
|
395
|
+
* 语义 = SR-4 原设计「child close → 取消其 pending」在协议化架构下的落点:宿主不再
|
|
396
|
+
* 持有 pi 子进程句柄,改由引擎经 host/childStateChanged 上报,本函数是消费端——
|
|
397
|
+
* 未接线的后果是「子进程已死、宿主仍占着队列串行位等应答」,直到队列级 30min 超时
|
|
398
|
+
* 才释放(延迟清理,非死锁)。
|
|
399
|
+
*
|
|
400
|
+
* 未登记队列(headless / 无 UI 通道宿主)或该 pid 无 pending 时 no-op。
|
|
401
|
+
*/
|
|
402
|
+
export function notifyChildProcessExited(pid: number): void {
|
|
403
|
+
activeDialogQueue?.rejectChildDialogs({ pid });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 测试钩子:读当前登记队列(undefined = 未登记)。 */
|
|
407
|
+
export function _getActiveDialogQueueForTest(): DialogGlobalQueue | undefined {
|
|
408
|
+
return activeDialogQueue;
|
|
409
|
+
}
|
|
@@ -94,7 +94,7 @@ describe("capability-gate(D3-④ 拦截矩阵)", () => {
|
|
|
94
94
|
const cases: Array<[TaskShapeForGate, RegExp]> = [
|
|
95
95
|
[{ fork: true }, /不支持 fork/],
|
|
96
96
|
[{ forkFromSessionFile: "/tmp/sess.jsonl" }, /fork-from 同为父 session 上下文继承/],
|
|
97
|
-
[{ conversation: true }, /不支持
|
|
97
|
+
[{ conversation: true }, /不支持 resume 续聊/],
|
|
98
98
|
[{ maxTurns: 10 }, /不支持 maxTurns/],
|
|
99
99
|
[{ worktree: true }, /不支持 worktree 隔离/],
|
|
100
100
|
[{ worktree: { path: "/tmp/wt" } }, /不支持 worktree 隔离/],
|
|
@@ -27,7 +27,7 @@ beforeEach(() => {
|
|
|
27
27
|
afterEach(() => {
|
|
28
28
|
if (PREV_DATA_DIR === undefined) delete process.env["XYZ_AGENT_DATA_DIR"];
|
|
29
29
|
else process.env["XYZ_AGENT_DATA_DIR"] = PREV_DATA_DIR;
|
|
30
|
-
fs.rmSync(dataRoot, { recursive: true, force: true });
|
|
30
|
+
fs.rmSync(dataRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
function makeHandle(): EngineHandle {
|
|
@@ -64,6 +64,21 @@ describe("wireEventJournal(D3-③ host helper 唯一实现)", () => {
|
|
|
64
64
|
expect(fs.existsSync(wiring.path)).toBe(true);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it("activity 豁免 journal.append:journal 文件无对应行,forwardEvents 仍收到(纯活性信号不进持久面)", async () => {
|
|
68
|
+
const forwarded: AgentEvent[] = [];
|
|
69
|
+
const wiring = wireEventJournal({ engineId: "zcode", taskId: "sa-6", forwardEvents: (e) => forwarded.push(e) });
|
|
70
|
+
wiring.onEvent({ type: "activity" });
|
|
71
|
+
wiring.onEvent({ type: "turn_end" });
|
|
72
|
+
wiring.onEvent({ type: "activity" });
|
|
73
|
+
await wiring.close();
|
|
74
|
+
// forwardEvents 照发(workflow liveRecord reducer no-op / 守护刷新面上游)
|
|
75
|
+
expect(forwarded.map((e) => e.type)).toEqual(["activity", "turn_end", "activity"]);
|
|
76
|
+
// journal 只落 turn_end 一行(activity 过滤在 append 前,无 seq 空洞)
|
|
77
|
+
const lines = fs.readFileSync(wiring.path, "utf8").trim().split("\n");
|
|
78
|
+
expect(lines).toHaveLength(1);
|
|
79
|
+
expect(JSON.parse(lines[0]).event.type).toBe("turn_end");
|
|
80
|
+
});
|
|
81
|
+
|
|
67
82
|
it("handle 回填:backfillHandle 写 retarget 后的终态路径(read ②级自描述定位符)", async () => {
|
|
68
83
|
const wiring = wireEventJournal({ engineId: "zcode", taskId: "sa-4" });
|
|
69
84
|
wiring.onPoolResolved("home-p-m1");
|