@zhushanwen/subagent-core 0.8.0 → 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-OL5BK4VN.js → chunk-LZJOEDGQ.js} +455 -203
- 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 +216 -155
- 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 +7686 -5894
- package/dist/index.d.cts +1876 -1360
- package/dist/index.d.ts +1876 -1360
- package/dist/index.js +7048 -5493
- package/dist.bundle/index.cjs +7747 -5957
- package/package.json +4 -4
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- 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__/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__/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__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- 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 +81 -25
- 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 +19 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/port.ts +34 -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/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 -2980
- 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 +1 -1
- package/src/index.ts +6 -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-BXLA8y-z.d.cts +0 -1624
- package/dist/engine-discovery-scan-BXLA8y-z.d.ts +0 -1624
- 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
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// src/execution/__tests__/delivery-methods.test.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 会话形态续聊投递单元测试(M2-B1 投递基础设施 → H1 chat-run 统一定形)。
|
|
4
4
|
//
|
|
5
5
|
// [W3 改写] 投递链路协议化:deliverChatMessage → pi EnginePort(registry cli 形态
|
|
6
|
-
// port 的替身 registerFakePiEngine
|
|
7
|
-
//
|
|
6
|
+
// port 的替身 registerFakePiEngine)。
|
|
7
|
+
// [H1 U2/U6 改写] 每轮 = 新 run + resume 锚点(ctx.resume,键切换后唯一会话形态键),
|
|
8
|
+
// Continuation 单飞承接防双写者守卫(原 resumeColdRound resumesInFlight 守卫退役);
|
|
9
|
+
// 轮末分流 = run 应答驱动(onRunSettled,D7)——[H1 U6] 旧 interact 热路径/冷路径
|
|
10
|
+
// 分流与 settleChatRoundFromResponse 结算载体的断言段随载体退役删除。
|
|
8
11
|
// 原 inproc stdin 字节断言(sendPromptCommand/streamingBehavior/EPIPE 写后死检测)
|
|
9
|
-
// 随 inproc pi 引擎目录 删除归 pi-subagent-cli
|
|
12
|
+
// 随 inproc pi 引擎目录 删除归 pi-subagent-cli 包内测试。
|
|
10
13
|
//
|
|
11
|
-
//
|
|
14
|
+
// 本文件断言的编排语义(守卫/状态迁移/单飞隔离)与改线前逐点同构。
|
|
12
15
|
|
|
13
16
|
import * as fs from "node:fs";
|
|
14
17
|
import * as os from "node:os";
|
|
@@ -62,7 +65,7 @@ function makeIdleRecord(id = "sa-chat"): ExecutionRecord {
|
|
|
62
65
|
return record;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
describe("
|
|
68
|
+
describe("会话形态续聊投递(run + resume 锚点)", () => {
|
|
66
69
|
let agentDir: string;
|
|
67
70
|
let service: SubagentService;
|
|
68
71
|
let record: ExecutionRecord;
|
|
@@ -78,12 +81,9 @@ describe("冷路径续轮(M2-B1 idle 投递;engine_session_not_resumable →
|
|
|
78
81
|
record = makeIdleRecord();
|
|
79
82
|
// sessionFile 用 agentDir 下路径(finalizeRoundToIdle 写 .idle sidecar 不留 /tmp 垃圾)
|
|
80
83
|
record.sessionFile = path.join(agentDir, "fake-session.jsonl");
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
code: "engine_session_not_resumable",
|
|
85
|
-
message: "no live process (cold path). Recovery: dispatch a new run with ctx chat resume.",
|
|
86
|
-
};
|
|
84
|
+
// [U2a/B5] 轮终簿记归口 store.markRoundIdle(按 id 查内存)——record 须 register 进
|
|
85
|
+
// store(生产链路 getRecordForAction/run 流程的 record 恒在内存,测试补齐同形态)。
|
|
86
|
+
(service as unknown as { store: { register: (r: ExecutionRecord) => void } }).store.register(record);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
afterEach(() => {
|
|
@@ -92,20 +92,20 @@ describe("冷路径续轮(M2-B1 idle 投递;engine_session_not_resumable →
|
|
|
92
92
|
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
it("
|
|
95
|
+
it("续聊 message → run 收到 resume 锚点(ctx.resume 唯一会话形态键);轮终 round+1 保持 running", async () => {
|
|
96
96
|
const beforeRound = record.round;
|
|
97
97
|
|
|
98
|
-
await service.chatActions.deliverChatMessage(record, "next round msg"
|
|
98
|
+
await service.chatActions.deliverChatMessage(record, "next round msg");
|
|
99
99
|
|
|
100
|
-
//
|
|
100
|
+
// Continuation:锚点校验通过后派发(record 保持 running)
|
|
101
101
|
expect(record.status).toBe("running");
|
|
102
102
|
|
|
103
|
-
// detached:等协议 run
|
|
103
|
+
// detached:等协议 run 被调(续聊 = ctx.resume 锚点)
|
|
104
104
|
await vi.waitFor(() => expect(fake.runs.length).toBe(1));
|
|
105
105
|
const run = fake.runs[0]!;
|
|
106
106
|
expect(run.task.prompt).toBe("next round msg");
|
|
107
107
|
expect(run.task.conversation).toBe(true);
|
|
108
|
-
expect(run.ctx.
|
|
108
|
+
expect(run.ctx.resume).toEqual({
|
|
109
109
|
recordId: record.id,
|
|
110
110
|
resume: {
|
|
111
111
|
sessionRef: { recordId: record.id, sessionFile: record.sessionFile },
|
|
@@ -113,40 +113,44 @@ describe("冷路径续轮(M2-B1 idle 投递;engine_session_not_resumable →
|
|
|
113
113
|
},
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
//
|
|
117
|
-
run.emitLifecycle({ phase: "idle", anchor: { sessionRef: { recordId: record.id, sessionFile: record.sessionFile }, poolKey: "shared" } });
|
|
116
|
+
// 模拟引擎 agent_settled 应答(轮末分流 = run 应答驱动,D7)
|
|
118
117
|
run.settle({ content: "round text" });
|
|
119
118
|
|
|
120
|
-
// 等 detached 完成:
|
|
119
|
+
// 等 detached 完成:round 累加、record 保持 running-resumable(v4 B-1 idle 折入 running)
|
|
121
120
|
await vi.waitFor(() => expect(record.round).toBe(beforeRound! + 1));
|
|
122
121
|
expect(record.status).toBe("running");
|
|
123
122
|
});
|
|
124
123
|
|
|
125
|
-
it("终态 closed record → throw 行动语言(
|
|
124
|
+
it("终态 closed record → throw 行动语言(D4 表 closed 硬拒格),不派发 run", async () => {
|
|
126
125
|
record.status = "closed";
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
expect(
|
|
126
|
+
// [H1 U2] D4 表 closed 硬拒格:closedReason 非 可重连集(undefined/gc)→ 硬拒 +
|
|
127
|
+
// start 新的指引(Continuation reviveOrThrow 文案)
|
|
128
|
+
await expect(service.chatActions.deliverChatMessage(record, "msg")).rejects.toThrow(
|
|
129
|
+
/cannot be messaged or resumed/,
|
|
130
|
+
);
|
|
131
|
+
expect(fake.runs.length).toBe(0);
|
|
130
132
|
});
|
|
131
133
|
|
|
132
|
-
it("record 无 sessionFile →
|
|
134
|
+
it("record 无 sessionFile → 同步拒绝(D4 表锚点缺失格:no transcript anchor + re-dispatch 指引),不触发 kickOff", async () => {
|
|
133
135
|
record.sessionFile = undefined;
|
|
134
|
-
await expect(service.chatActions.deliverChatMessage(record, "msg"
|
|
136
|
+
await expect(service.chatActions.deliverChatMessage(record, "msg")).rejects.toThrow(
|
|
137
|
+
/no transcript anchor/,
|
|
138
|
+
);
|
|
135
139
|
expect(fake.runs.length).toBe(0);
|
|
136
140
|
});
|
|
137
141
|
|
|
138
|
-
it("record 无 controller →
|
|
142
|
+
it("record 无 controller → 投递 throw 行动语言(MF-4),不触发 kickOff", async () => {
|
|
139
143
|
record.controller = undefined;
|
|
140
|
-
await expect(service.chatActions.deliverChatMessage(record, "msg"
|
|
144
|
+
await expect(service.chatActions.deliverChatMessage(record, "msg")).rejects.toThrow(/not ready for a new message/);
|
|
141
145
|
expect(fake.runs.length).toBe(0);
|
|
142
146
|
});
|
|
143
147
|
});
|
|
144
148
|
|
|
145
149
|
// ============================================================
|
|
146
|
-
// deliverChatMessage(
|
|
150
|
+
// deliverChatMessage(chatMode 统一投递入口 → Continuation 编排)
|
|
147
151
|
// ============================================================
|
|
148
152
|
|
|
149
|
-
describe("deliverChatMessage
|
|
153
|
+
describe("deliverChatMessage(chatMode 统一投递 → Continuation 派发)", () => {
|
|
150
154
|
let agentDir: string;
|
|
151
155
|
let service: SubagentService;
|
|
152
156
|
let record: ExecutionRecord;
|
|
@@ -159,9 +163,11 @@ describe("deliverChatMessage (V2 决策 3 chatMode 统一投递;协议 interac
|
|
|
159
163
|
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
160
164
|
service = new SubagentService({ cwd: agentDir, modelService });
|
|
161
165
|
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
162
|
-
record = makeIdleRecord(); // chatMode:true,
|
|
163
|
-
// sessionFile
|
|
166
|
+
record = makeIdleRecord(); // chatMode:true, running, round=1
|
|
167
|
+
// sessionFile:续聊锚点需要
|
|
164
168
|
record.sessionFile = path.join(agentDir, "fake-session.jsonl");
|
|
169
|
+
// [U2a/B5] markRoundIdle 按 id 查 store 内存——record 须 register(见上 describe 注)。
|
|
170
|
+
(service as unknown as { store: { register: (r: ExecutionRecord) => void } }).store.register(record);
|
|
165
171
|
lifecycle._resetLifecycleState();
|
|
166
172
|
});
|
|
167
173
|
|
|
@@ -172,91 +178,49 @@ describe("deliverChatMessage (V2 决策 3 chatMode 统一投递;协议 interac
|
|
|
172
178
|
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
173
179
|
});
|
|
174
180
|
|
|
175
|
-
it("
|
|
181
|
+
it("轮间 message → Continuation 派发新轮 + 执行态信号清除([H1 U2] 承接原冷路径语义)", async () => {
|
|
176
182
|
record.result = "上一轮增量";
|
|
177
183
|
record.resumable = true;
|
|
178
184
|
|
|
179
|
-
await service.chatActions.deliverChatMessage(record, "after you finish"
|
|
185
|
+
await service.chatActions.deliverChatMessage(record, "after you finish");
|
|
180
186
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect(
|
|
185
|
-
|
|
187
|
+
// [H1 U2] 每轮 = 新 run + resume 锚点(§3.4 dispatchRound)
|
|
188
|
+
await vi.waitFor(() => expect(fake.runs.length).toBe(1));
|
|
189
|
+
const run = fake.runs[0]!;
|
|
190
|
+
expect(run.task.prompt).toBe("after you finish");
|
|
191
|
+
expect(run.ctx.resume?.recordId).toBe(record.id);
|
|
192
|
+
expect(run.ctx.resume?.resume?.sessionRef["sessionFile"]).toBe(record.sessionFile);
|
|
193
|
+
// 轮始执行态信号清除(§5.4 isStreaming 公式)+ 迁移上报
|
|
186
194
|
expect(record.status).toBe("running");
|
|
187
195
|
expect(record.result).toBeUndefined();
|
|
188
196
|
expect(record.resumable).toBeUndefined();
|
|
189
197
|
});
|
|
190
198
|
|
|
191
|
-
it("
|
|
192
|
-
await service.chatActions.deliverChatMessage(record, "
|
|
193
|
-
|
|
194
|
-
expect(fake.interacts[0]!.action).toEqual({ kind: "message", payload: "stop now", interrupt: true });
|
|
195
|
-
expect(record.status).toBe("running");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("冷路径:engine_session_not_resumable → 续轮 run chat + resume 锚点", async () => {
|
|
199
|
-
fake.interactMessageResult = {
|
|
200
|
-
ok: false,
|
|
201
|
-
code: "engine_session_not_resumable",
|
|
202
|
-
message: "no live process (cold path)",
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
await service.chatActions.deliverChatMessage(record, "resume msg", false);
|
|
206
|
-
|
|
199
|
+
it("settle 交棒 = run 应答驱动(D7):派发后挂中段守护,应答 settle 后轮终守护清空", async () => {
|
|
200
|
+
await service.chatActions.deliverChatMessage(record, "msg");
|
|
207
201
|
await vi.waitFor(() => expect(fake.runs.length).toBe(1));
|
|
208
|
-
const run = fake.runs[0]!;
|
|
209
|
-
expect(run.task.prompt).toBe("resume msg");
|
|
210
|
-
expect(run.ctx.chat?.recordId).toBe(record.id);
|
|
211
|
-
expect(run.ctx.chat?.resume?.sessionRef["sessionFile"]).toBe(record.sessionFile);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("业务拒绝(EPIPE 兜底耗尽等)→ 原样 throw(错误文本逐字节保持),不进冷路径", async () => {
|
|
215
|
-
fake.interactMessageResult = {
|
|
216
|
-
ok: false,
|
|
217
|
-
code: "engine_interact_failed",
|
|
218
|
-
message: `EPIPE fallback exhausted for ${record.id}: 3 consecutive failures.`,
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
await expect(service.chatActions.deliverChatMessage(record, "msg", false)).rejects.toThrow(
|
|
222
|
-
/EPIPE fallback exhausted/,
|
|
223
|
-
);
|
|
224
|
-
expect(fake.runs.length).toBe(0); // 不进冷路径
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("热路径 disarm idle timer:arm 后投递 → timer 清除(防 turn 期间误杀)", async () => {
|
|
228
|
-
// 先 arm idle timer(模拟首轮 agent_settled 后 armed)
|
|
229
|
-
lifecycle.armIdleTimer(record.id, () => {}, 10000);
|
|
230
|
-
expect(lifecycle.hasIdleTimer(record.id)).toBe(true);
|
|
231
|
-
|
|
232
|
-
await service.chatActions.deliverChatMessage(record, "msg", false);
|
|
233
|
-
|
|
234
|
-
// disarmIdleTimer 被调 → timer 清除(新 turn 不被 idle timer 误杀)
|
|
235
|
-
expect(lifecycle.hasIdleTimer(record.id)).toBe(false);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("受理后挂中段守护(settled-watchdog mid-round armed);settled 相位交棒收尾段", async () => {
|
|
239
|
-
await service.chatActions.deliverChatMessage(record, "msg", false);
|
|
240
202
|
|
|
241
203
|
const { hasSettledWatchdog, getSettledWatchdogPhase } = await import("../settled-watchdog.ts");
|
|
242
204
|
expect(hasSettledWatchdog(record.id)).toBe(true);
|
|
243
205
|
expect(getSettledWatchdogPhase(record.id)).toBe("mid-round");
|
|
244
206
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
207
|
+
// [H1 U2 / D7] settle 交棒 = run 应答驱动(onRunSettled 内 noteRoundSettledFromProtocol):
|
|
208
|
+
// 应答后轮终簿记完成,两段守护一并清(不残留 armed——收尾段 fire 会对已收敛轮误杀)
|
|
209
|
+
fake.runs[0]!.settle({ content: "round text" });
|
|
210
|
+
await vi.waitFor(() => expect(record.round).toBe(2));
|
|
211
|
+
expect(hasSettledWatchdog(record.id)).toBe(false);
|
|
248
212
|
});
|
|
249
213
|
});
|
|
250
214
|
|
|
251
215
|
// ============================================================
|
|
252
|
-
//
|
|
216
|
+
// 续聊并发守卫(review round2 MF1:同 turn 批量两条 message 双 kickOff 双 spawn)
|
|
253
217
|
// ============================================================
|
|
254
218
|
// 复现链(reviewer 探针实证):pi 对同一条 assistant message 的 tool calls 顺序执行,
|
|
255
|
-
// tool1
|
|
256
|
-
// 异步点);tool2 立即执行 →
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
describe("deliverChatMessage
|
|
219
|
+
// tool1 的投递在续聊返回即 resolve——早于协议 run 完成(pool.acquire await 等
|
|
220
|
+
// 异步点);tool2 立即执行 → 第二次 kickOff。v4 两态收敛后的 status 守卫对
|
|
221
|
+
// running-resumable record 恒放行 → 两次 kickOff → 两个 pi 子进程以 --session 同一
|
|
222
|
+
// JSONL 双写 + 第一个进程脱离记账成孤儿。
|
|
223
|
+
describe("deliverChatMessage 并发守卫(review round2 MF1)", () => {
|
|
260
224
|
let agentDir: string;
|
|
261
225
|
let service: SubagentService;
|
|
262
226
|
let record: ExecutionRecord;
|
|
@@ -266,16 +230,13 @@ describe("deliverChatMessage 冷路径并发守卫(review round2 MF1)", () =
|
|
|
266
230
|
agentDir = makeTmpAgentDir();
|
|
267
231
|
clearEngines();
|
|
268
232
|
fake = registerFakePiEngine();
|
|
269
|
-
fake.interactMessageResult = {
|
|
270
|
-
ok: false,
|
|
271
|
-
code: "engine_session_not_resumable",
|
|
272
|
-
message: "no live process (cold path)",
|
|
273
|
-
};
|
|
274
233
|
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
275
234
|
service = new SubagentService({ cwd: agentDir, modelService });
|
|
276
235
|
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
277
236
|
record = makeIdleRecord();
|
|
278
237
|
record.sessionFile = path.join(agentDir, "fake-session.jsonl");
|
|
238
|
+
// [U2a/B5] markRoundIdle 按 id 查 store 内存——record 须 register(见上 describe 注)。
|
|
239
|
+
(service as unknown as { store: { register: (r: ExecutionRecord) => void } }).store.register(record);
|
|
279
240
|
lifecycle._resetLifecycleState();
|
|
280
241
|
});
|
|
281
242
|
|
|
@@ -286,96 +247,49 @@ describe("deliverChatMessage 冷路径并发守卫(review round2 MF1)", () =
|
|
|
286
247
|
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
287
248
|
});
|
|
288
249
|
|
|
289
|
-
it("同 record
|
|
290
|
-
//
|
|
291
|
-
await expect(service.chatActions.deliverChatMessage(record, "first msg"
|
|
250
|
+
it("同 record 轮在途连续两条 message → 第二条入队不打断单飞(构造性单写者),轮终 drain 聚合派发", async () => {
|
|
251
|
+
// 第一条:派发新轮(run 挂起不 resolve——模拟在途轮)
|
|
252
|
+
await expect(service.chatActions.deliverChatMessage(record, "first msg")).resolves.toBeUndefined();
|
|
292
253
|
await vi.waitFor(() => expect(fake.runs.length).toBe(1));
|
|
293
254
|
|
|
294
|
-
// 第二条:run
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
await expect(service.chatActions.deliverChatMessage(record, "second msg", false)).rejects.toThrow(
|
|
298
|
-
/already starting a new round/,
|
|
299
|
-
);
|
|
255
|
+
// 第二条:run 在途 → [H1 U2 / D2 打断语义] abort 在途轮 signal + 入队(不打断单飞、
|
|
256
|
+
// 不二次派发——修复前双 kickOff 双写 session 的守卫由 Continuation 单飞构造性承接)
|
|
257
|
+
await expect(service.chatActions.deliverChatMessage(record, "second msg")).resolves.toBeUndefined();
|
|
300
258
|
expect(fake.runs.length).toBe(1);
|
|
301
|
-
|
|
302
|
-
|
|
259
|
+
const continuation = (
|
|
260
|
+
// [R4 深绑改写] continuations 队列已迁 RunOrchestration 聚合——读取路径改经
|
|
261
|
+
// 聚合实例(断言对象与强度不变)。
|
|
262
|
+
service as unknown as {
|
|
263
|
+
runOrchestration: { continuations: Map<string, { pendingCount: number }> };
|
|
264
|
+
}
|
|
265
|
+
).runOrchestration.continuations.get(record.id);
|
|
266
|
+
expect(continuation?.pendingCount).toBe(1);
|
|
267
|
+
|
|
268
|
+
// 轮终(应答收敛)→ drain → 队列消息派发为下一轮(单写者前置满足)
|
|
303
269
|
fake.runs[0]!.settle({ content: "done" });
|
|
304
|
-
await vi.waitFor(() =>
|
|
305
|
-
expect((service as unknown as { resumesInFlight: Set<string> }).resumesInFlight.has(record.id)).toBe(false),
|
|
306
|
-
);
|
|
307
|
-
await expect(service.chatActions.deliverChatMessage(record, "third msg", false)).resolves.toBeUndefined();
|
|
308
270
|
await vi.waitFor(() => expect(fake.runs.length).toBe(2));
|
|
309
|
-
expect(fake.runs[1]!.task.prompt).toBe("
|
|
271
|
+
expect(fake.runs[1]!.task.prompt).toBe("second msg");
|
|
310
272
|
});
|
|
311
273
|
|
|
312
|
-
it("守卫是 record 级:A
|
|
274
|
+
it("守卫是 record 级:A 在途轮不拦截 B 的 message", async () => {
|
|
313
275
|
const recordB = makeIdleRecord("sa-chat-b");
|
|
314
276
|
recordB.sessionFile = path.join(agentDir, "fake-session-b.jsonl");
|
|
277
|
+
// [U2a/B5] 同 beforeEach——recordB 须 register 进 store(markRoundIdle 内存查)。
|
|
278
|
+
(service as unknown as { store: { register: (r: ExecutionRecord) => void } }).store.register(recordB);
|
|
315
279
|
|
|
316
|
-
await service.chatActions.deliverChatMessage(record, "A msg"
|
|
280
|
+
await service.chatActions.deliverChatMessage(record, "A msg");
|
|
317
281
|
await vi.waitFor(() => expect(fake.runs.length).toBe(1));
|
|
318
282
|
|
|
319
|
-
// B
|
|
320
|
-
await expect(service.chatActions.deliverChatMessage(recordB, "B msg"
|
|
283
|
+
// B 的续聊轮不受 A 在途影响(Continuation per-record 实例)
|
|
284
|
+
await expect(service.chatActions.deliverChatMessage(recordB, "B msg")).resolves.toBeUndefined();
|
|
321
285
|
await vi.waitFor(() => expect(fake.runs.length).toBe(2));
|
|
322
|
-
expect(fake.runs[1]!.ctx.
|
|
286
|
+
expect(fake.runs[1]!.ctx.resume?.recordId).toBe(recordB.id);
|
|
323
287
|
|
|
324
288
|
fake.runs[0]!.settle({ content: "A done" });
|
|
325
289
|
fake.runs[1]!.settle({ content: "B done" });
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// ============================================================
|
|
333
|
-
// 本轮 settle(协议形态:run 应答 = 首轮 agent_settled)
|
|
334
|
-
// ============================================================
|
|
335
|
-
|
|
336
|
-
describe("settleChatRoundFromResponse(W3:协议形态的本轮结算)", () => {
|
|
337
|
-
let agentDir: string;
|
|
338
|
-
let service: SubagentService;
|
|
339
|
-
let record: ExecutionRecord;
|
|
340
|
-
|
|
341
|
-
beforeEach(() => {
|
|
342
|
-
agentDir = makeTmpAgentDir();
|
|
343
|
-
const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
|
|
344
|
-
service = new SubagentService({ cwd: agentDir, modelService });
|
|
345
|
-
service.initSession({ pi: makePi(), sessionId: "root-session" });
|
|
346
|
-
record = makeIdleRecord();
|
|
347
|
-
record.sessionFile = path.join(agentDir, "fake-session.jsonl");
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
afterEach(() => {
|
|
351
|
-
service.dispose();
|
|
352
|
-
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("应答到达 → round+1 + result=本轮内容 + 通知回注(W2 契约:outcome = 本轮内容)", () => {
|
|
356
|
-
const notifyRouted: string[] = [];
|
|
357
|
-
const coord = (service as unknown as { collectCoordinator: { route(r: ExecutionRecord): void } }).collectCoordinator;
|
|
358
|
-
const original = coord.route.bind(coord);
|
|
359
|
-
Object.assign(coord, { route: (r: ExecutionRecord) => { notifyRouted.push(r.id); original(r); } });
|
|
360
|
-
|
|
361
|
-
const settle = (service as unknown as {
|
|
362
|
-
settleChatRoundFromResponse(record: ExecutionRecord, outcome: { content: string }): void;
|
|
363
|
-
}).settleChatRoundFromResponse.bind(service);
|
|
364
|
-
settle(record, { content: "本轮回复文本" });
|
|
365
|
-
|
|
366
|
-
expect(record.round).toBe(2);
|
|
367
|
-
expect(record.result).toBe("本轮回复文本");
|
|
368
|
-
expect(notifyRouted).toEqual([record.id]);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("closeAfterRound 挂起 → 轮终兑现终态化(closed + user-close)", async () => {
|
|
372
|
-
record.closeAfterRound = true;
|
|
373
|
-
const settle = (service as unknown as {
|
|
374
|
-
settleChatRoundFromResponse(record: ExecutionRecord, outcome: { content: string }): void;
|
|
375
|
-
}).settleChatRoundFromResponse.bind(service);
|
|
376
|
-
settle(record, { content: "final" });
|
|
377
|
-
|
|
378
|
-
await vi.waitFor(() => expect(record.status).toBe("closed"));
|
|
379
|
-
expect(record.closeAfterRound).toBeUndefined();
|
|
290
|
+
// 轮终簿记完成(Continuation 无全局在途集合——终态守卫由 per-record 实例承载),
|
|
291
|
+
// 断言两轮各自 round+1 即单飞收口的可观察结果。
|
|
292
|
+
await vi.waitFor(() => expect(record.round).toBe(2));
|
|
293
|
+
await vi.waitFor(() => expect(recordB.round).toBe(2));
|
|
380
294
|
});
|
|
381
295
|
});
|
|
@@ -37,7 +37,15 @@ vi.mock("../../core/logger.ts", () => ({
|
|
|
37
37
|
getLogger: () => loggerMock,
|
|
38
38
|
}));
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_DIALOG_TIMEOUT_MS,
|
|
42
|
+
DialogGlobalQueue,
|
|
43
|
+
_getActiveDialogQueueForTest,
|
|
44
|
+
notifyChildProcessExited,
|
|
45
|
+
registerActiveDialogQueue,
|
|
46
|
+
type UiRequest,
|
|
47
|
+
type UiResponse,
|
|
48
|
+
} from "../dialog-queue.ts";
|
|
41
49
|
|
|
42
50
|
// ── 类型助手 ────────────────────────────────────────────────
|
|
43
51
|
// UiRequest 最小形状(method + id)。dialog 类:select/confirm/input/editor。
|
|
@@ -287,8 +295,9 @@ describe("DialogGlobalQueue — #19 单一推进点 + child close 集成语义",
|
|
|
287
295
|
});
|
|
288
296
|
|
|
289
297
|
it("child close 集成语义:queue 中同一 child 的多个 pending 被 rejectChildDialogs 一次全部 settle cancelled", async () => {
|
|
290
|
-
//
|
|
291
|
-
//
|
|
298
|
+
// 模拟子进程退出事件 → dialogQueue.rejectChildDialogs(child) 清理路径。
|
|
299
|
+
// 生产绑定点 = 宿主侧引擎镜像层(SpawnedChildrenMirror → notifyChildProcessExited,
|
|
300
|
+
// 见 mirror.test.ts 的接线用例);本用例聚焦 rejectChildDialogs 本身的批量语义。
|
|
292
301
|
// 用 blocker 占 current(另一 child)使被测 child 的项全排队,聚焦验证 queue 批量 cancel。
|
|
293
302
|
const queue = new DialogGlobalQueue();
|
|
294
303
|
const blocker = { pid: 70000 };
|
|
@@ -495,3 +504,61 @@ describe("DialogGlobalQueue — reject 抢先 settle 后超时 timer 清理(A2
|
|
|
495
504
|
expect(loggerMock.warn).not.toHaveBeenCalled();
|
|
496
505
|
});
|
|
497
506
|
});
|
|
507
|
+
|
|
508
|
+
describe("DialogGlobalQueue — 活跃队列登记 + 子进程退出通知(SR-4 接线)", () => {
|
|
509
|
+
beforeEach(() => {
|
|
510
|
+
vi.useFakeTimers();
|
|
511
|
+
});
|
|
512
|
+
afterEach(() => {
|
|
513
|
+
registerActiveDialogQueue(undefined);
|
|
514
|
+
vi.useRealTimers();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("notifyChildProcessExited:取消该 pid 的当前项与排队项(该 pid 的 pending 全 settle cancelled)", async () => {
|
|
518
|
+
const queue = new DialogGlobalQueue();
|
|
519
|
+
registerActiveDialogQueue(queue);
|
|
520
|
+
const handler = vi.fn((): Promise<UiResponse> => new Promise<UiResponse>(() => {}));
|
|
521
|
+
|
|
522
|
+
// 同 pid:1 项占住 current(永不 settle)+ 1 项排队
|
|
523
|
+
const current = queue.enqueue(dialogReq("r1"), handler, { child: { pid: 20001 } });
|
|
524
|
+
const queued = queue.enqueue(dialogReq("r2"), handler, { child: { pid: 20001 } });
|
|
525
|
+
expect(queue.size).toBe(1); // r2 仍在排队(enqueue 同步入队,current 立即置位)
|
|
526
|
+
|
|
527
|
+
notifyChildProcessExited(20001);
|
|
528
|
+
|
|
529
|
+
await expect(current).resolves.toEqual({ cancelled: true });
|
|
530
|
+
await expect(queued).resolves.toEqual({ cancelled: true });
|
|
531
|
+
expect(queue.size).toBe(0);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("notifyChildProcessExited:不影响其他 pid 的挂起项", async () => {
|
|
535
|
+
const queue = new DialogGlobalQueue();
|
|
536
|
+
registerActiveDialogQueue(queue);
|
|
537
|
+
const handler = vi.fn((): Promise<UiResponse> => new Promise<UiResponse>(() => {}));
|
|
538
|
+
|
|
539
|
+
const other = queue.enqueue(dialogReq("r1"), handler, { child: { pid: 30001 } });
|
|
540
|
+
|
|
541
|
+
notifyChildProcessExited(30002); // 另一个 pid 退出
|
|
542
|
+
|
|
543
|
+
let settled = false;
|
|
544
|
+
void other.then(() => {
|
|
545
|
+
settled = true;
|
|
546
|
+
});
|
|
547
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
548
|
+
expect(settled).toBe(false);
|
|
549
|
+
expect(queue.size).toBe(0); // 该项仍是 current,未被动过
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("未登记队列时 notifyChildProcessExited 为 no-op(不抛)", () => {
|
|
553
|
+
expect(_getActiveDialogQueueForTest()).toBeUndefined();
|
|
554
|
+
expect(() => notifyChildProcessExited(40001)).not.toThrow();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("登记后 _getActiveDialogQueueForTest 可读;注销后回落 undefined", () => {
|
|
558
|
+
const queue = new DialogGlobalQueue();
|
|
559
|
+
registerActiveDialogQueue(queue);
|
|
560
|
+
expect(_getActiveDialogQueueForTest()).toBe(queue);
|
|
561
|
+
registerActiveDialogQueue(undefined);
|
|
562
|
+
expect(_getActiveDialogQueueForTest()).toBeUndefined();
|
|
563
|
+
});
|
|
564
|
+
});
|