@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
|
@@ -15,6 +15,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
15
15
|
import { EngineClient } from "../engine-client.ts";
|
|
16
16
|
import { RemoteEngine, type RemoteEngineManifestSnapshot } from "../remote-engine.ts";
|
|
17
17
|
import { SubagentStream } from "../../../stream-sink.ts";
|
|
18
|
+
import { getSubagentSessionDir } from "../../../path-encoding.ts";
|
|
18
19
|
import { isProcessAlive } from "../pid-file.ts";
|
|
19
20
|
import { getLogger, type UiRequest } from "@zhushanwen/subagent-engine-sdk";
|
|
20
21
|
|
|
@@ -253,6 +254,49 @@ describe("RemoteEngine run 帧映射", () => {
|
|
|
253
254
|
await cleanup();
|
|
254
255
|
});
|
|
255
256
|
|
|
257
|
+
it("ctx.sessionRootId → wire ctx.sessionRootId 逐字保真;缺省不传 → wire 无该键(F6 additive)", async () => {
|
|
258
|
+
// 正向:根 session id 上 wire(pi 引擎 relay 归属键 SESSION_ID 的权威来源)
|
|
259
|
+
const withRoot = makeEngine();
|
|
260
|
+
const { ctx: ctxRoot, events: eventsRoot } = makeCtx({ sessionRootId: "root-sess-f6" });
|
|
261
|
+
await withRoot.engine.run({ prompt: "p" }, ctxRoot);
|
|
262
|
+
expect(extractRunParams(eventsRoot).ctx.sessionRootId).toBe("root-sess-f6");
|
|
263
|
+
|
|
264
|
+
// 负向:缺省 → wire ctx 不出现该键(undefined 不上 wire,旧引擎 additive 兼容)
|
|
265
|
+
const bare = makeEngine();
|
|
266
|
+
const { ctx: ctxBare, events: eventsBare } = makeCtx();
|
|
267
|
+
await bare.engine.run({ prompt: "p" }, ctxBare);
|
|
268
|
+
expect(extractRunParams(eventsBare).ctx).not.toHaveProperty("sessionRootId");
|
|
269
|
+
|
|
270
|
+
await withRoot.cleanup();
|
|
271
|
+
await bare.cleanup();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("[Option C] wire ctx.sessionDir = getSubagentSessionDir(env 同源推导);ctx 显式注入优先", async () => {
|
|
275
|
+
// stub env 固定推导输入(测试进程可能已带这两键——stub 后期望值确定)
|
|
276
|
+
vi.stubEnv("PI_CODING_AGENT_DIR", join(dataDir, "agent-dir"));
|
|
277
|
+
vi.stubEnv("PI_SUBAGENT_ROOT_CWD", join(dataDir, "root-cwd"));
|
|
278
|
+
try {
|
|
279
|
+
// 正向:env 同源推导值上 wire(宿主单一权威 getSubagentSessionDir,引擎不自推导)
|
|
280
|
+
const derived = makeEngine();
|
|
281
|
+
const { ctx, events } = makeCtx();
|
|
282
|
+
await derived.engine.run({ prompt: "p" }, ctx);
|
|
283
|
+
expect(extractRunParams(events).ctx.sessionDir).toBe(
|
|
284
|
+
getSubagentSessionDir(join(dataDir, "agent-dir"), join(dataDir, "root-cwd")),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// 宿主编排层显式注入(ctx.sessionDir)优先于 env 推导
|
|
288
|
+
const injected = makeEngine();
|
|
289
|
+
const { ctx: ctxInj, events: eventsInj } = makeCtx({ sessionDir: "/explicit/session-dir" });
|
|
290
|
+
await injected.engine.run({ prompt: "p" }, ctxInj);
|
|
291
|
+
expect(extractRunParams(eventsInj).ctx.sessionDir).toBe("/explicit/session-dir");
|
|
292
|
+
|
|
293
|
+
await derived.cleanup();
|
|
294
|
+
await injected.cleanup();
|
|
295
|
+
} finally {
|
|
296
|
+
vi.unstubAllEnvs();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
256
300
|
it("poolResolved / handleReady → RunContext 回调(journal 路径权威 + 运行中句柄回填)", async () => {
|
|
257
301
|
const pools: string[] = [];
|
|
258
302
|
const readies: Array<{ sessionRef: Record<string, string>; poolKey: string }> = [];
|
|
@@ -393,7 +437,7 @@ describe("gate 位多声明 run 期判定(W3 契约⑤接线:manifest vs ini
|
|
|
393
437
|
});
|
|
394
438
|
});
|
|
395
439
|
|
|
396
|
-
describe("
|
|
440
|
+
describe("read / probe / dispose 门面([H1 U6] interact 断言随 interact 面退役删除)", () => {
|
|
397
441
|
it("read 协议帧携带 dataDir(必填:存量池时代相对 dbPath 定位)", async () => {
|
|
398
442
|
const { engine, cleanup } = makeEngine();
|
|
399
443
|
const view = await engine.read({
|
|
@@ -411,21 +455,11 @@ describe("interact / read / probe / dispose 门面", () => {
|
|
|
411
455
|
await cleanup();
|
|
412
456
|
});
|
|
413
457
|
|
|
414
|
-
it("probe → ProbeReport;
|
|
458
|
+
it("probe → ProbeReport;dispose → client 停机(幂等)", async () => {
|
|
415
459
|
const { engine, cleanup } = makeEngine();
|
|
416
460
|
const report = await engine.probe();
|
|
417
461
|
expect(report.ok).toBe(true);
|
|
418
462
|
expect(report.engineVersion).toBe("fake-1.0.0");
|
|
419
|
-
const interact = await engine.interact({
|
|
420
|
-
data: {
|
|
421
|
-
v: 1,
|
|
422
|
-
engineId: "fake",
|
|
423
|
-
sessionRef: { sessionId: "s-1" },
|
|
424
|
-
poolKey: "shared",
|
|
425
|
-
adapterVersion: "fake-adapter",
|
|
426
|
-
},
|
|
427
|
-
}, { kind: "message", payload: "hi" });
|
|
428
|
-
expect(interact).toEqual({ ok: true, delivered: true });
|
|
429
463
|
await engine.dispose();
|
|
430
464
|
await expect(engine.dispose()).resolves.toBeUndefined(); // 幂等
|
|
431
465
|
await cleanup();
|
|
@@ -104,8 +104,7 @@ export type EngineClientState =
|
|
|
104
104
|
/**
|
|
105
105
|
* run 作用域反向通知路由(RemoteEngine.run 注册,终态后注销)。
|
|
106
106
|
* 回调允许返回 Promise(数据面 10s 应答守卫的计时对象;同步回调不受守卫影响)。
|
|
107
|
-
* [
|
|
108
|
-
* recordId 键走 registerRecordRoute,D1-A 关联键分路)。
|
|
107
|
+
* [H1 U6] 轮次相位消费口(旧第 9 反向通道)已随 chat 域相位机退役删除。
|
|
109
108
|
*/
|
|
110
109
|
export interface RunRoute {
|
|
111
110
|
onEvent?: (event: unknown) => void | Promise<void>;
|
|
@@ -114,7 +113,6 @@ export interface RunRoute {
|
|
|
114
113
|
onHandleReady?: (
|
|
115
114
|
partial: Pick<EngineHandleData, "sessionRef" | "poolKey">,
|
|
116
115
|
) => void | Promise<void>;
|
|
117
|
-
onRoundLifecycle?: (phase: unknown) => void | Promise<void>;
|
|
118
116
|
}
|
|
119
117
|
|
|
120
118
|
/** spawn 引擎 CLI 平台形态(impl-plan §2.2 必写死的两行)。 */
|
|
@@ -163,9 +161,6 @@ export class EngineClient {
|
|
|
163
161
|
private nextRequestId = 1;
|
|
164
162
|
private readonly pending = new Map<number, PendingRequest>();
|
|
165
163
|
private readonly runRoutes = new Map<string, RunRoute>();
|
|
166
|
-
/** [W3 v1.x] chat 轮次路由(recordId 键,D1-A——续聊轮无 runId);注册经
|
|
167
|
-
* RemoteEngine.registerChatRoundRoute,record 终态注销。 */
|
|
168
|
-
private readonly recordRoutes = new Map<string, RunRoute>();
|
|
169
164
|
private stderrTail = "";
|
|
170
165
|
private unavailableReason: EngineSdkError | undefined;
|
|
171
166
|
private connectInFlight: Promise<void> | undefined;
|
|
@@ -186,7 +181,6 @@ export class EngineClient {
|
|
|
186
181
|
permissionHandler: opts.permissionHandler,
|
|
187
182
|
log: opts.log,
|
|
188
183
|
runRoutes: this.runRoutes,
|
|
189
|
-
recordRoutes: this.recordRoutes,
|
|
190
184
|
mirror: this.mirror,
|
|
191
185
|
setPartialHandle: (partial) => {
|
|
192
186
|
this.lastPartialHandle = partial;
|
|
@@ -560,15 +554,6 @@ export class EngineClient {
|
|
|
560
554
|
};
|
|
561
555
|
}
|
|
562
556
|
|
|
563
|
-
/** [W3 v1.x] 注册 chat 轮次路由(recordId 键);分发见 reverse-router.ts
|
|
564
|
-
* (与 runRoutes 同构,键空间互斥:runId=run 帧分配 / recordId=宿主铸造)。 */
|
|
565
|
-
registerRecordRoute(recordId: string, route: RunRoute): () => void {
|
|
566
|
-
this.recordRoutes.set(recordId, route);
|
|
567
|
-
return () => {
|
|
568
|
-
if (this.recordRoutes.get(recordId) === route) this.recordRoutes.delete(recordId);
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
557
|
/** 健康检查(ADR-0047:静默 ≠ 卡死,不据此杀任务)。 */
|
|
573
558
|
async ping(): Promise<void> {
|
|
574
559
|
await this.ensureConnected();
|
|
@@ -635,10 +620,12 @@ export class EngineClient {
|
|
|
635
620
|
}
|
|
636
621
|
|
|
637
622
|
/**
|
|
638
|
-
* 进程死亡 /
|
|
639
|
-
*
|
|
623
|
+
* 进程死亡 / 失败后的统一清理(收割/组杀分支各成子函数,此处只做顺序编排):
|
|
624
|
+
* 孤儿收割(镜像置死前)→ 镜像置死(失效语义 2+3)→ pidfile 删 → 在途请求
|
|
625
|
+
* engine_crashed(附 stderr 尾)→ run 路由清空 → 活引擎组杀兜底。
|
|
640
626
|
*/
|
|
641
627
|
private teardownProcess(detail: string): void {
|
|
628
|
+
this.reapOrphansAfterUnexpectedDeath(detail);
|
|
642
629
|
this.mirror.killAll();
|
|
643
630
|
if (this.pidfileWritten !== undefined) {
|
|
644
631
|
removePidfile(this.pidfileWritten);
|
|
@@ -651,23 +638,92 @@ export class EngineClient {
|
|
|
651
638
|
}
|
|
652
639
|
this.pending.clear();
|
|
653
640
|
this.runRoutes.clear();
|
|
654
|
-
this.recordRoutes.clear();
|
|
655
641
|
this.lastPartialHandle = undefined;
|
|
642
|
+
this.killLeakedAliveChild(detail);
|
|
643
|
+
if (this.state !== "unavailable" && this.state !== "disposed") {
|
|
644
|
+
this.state = "exited";
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* [H1 U2 / 红线①收割链] 非主动死亡且引擎已死的路径,在镜像置死**之前**补发收割
|
|
650
|
+
* 信号:孤儿真因 = 引擎意外死时宿主未发收割信号(原先 alreadyDead 分支直接跳过
|
|
651
|
+
* killProcessTree),任务子进程留在无主进程组继续写 session 文件。插入点约束:
|
|
652
|
+
* - 位置 = mirror.killAll() 置死清空之前——Windows 通道依赖镜像快照过滤活孤儿
|
|
653
|
+
* (置死后全量项 killed=true 不可过滤);
|
|
654
|
+
* - 条件 = !intentionalKill && alreadyDead——主动 dispose 链已有两层杀(引擎
|
|
655
|
+
* killAllActiveChildren SIGTERM+30s + 宿主 killAll 组杀 5s 升级),跳过即不叠加、
|
|
656
|
+
* 优雅窗零压缩、pi trap-flush 零截断;进程仍活的路径(握手失败重建)由
|
|
657
|
+
* killLeakedAliveChild 组杀兜底承接。
|
|
658
|
+
*/
|
|
659
|
+
private reapOrphansAfterUnexpectedDeath(detail: string): void {
|
|
660
|
+
if (!this.intentionalKill && this.child !== undefined) {
|
|
661
|
+
const dying = this.child;
|
|
662
|
+
const alreadyDead = dying.exitCode !== null || dying.signalCode !== null;
|
|
663
|
+
if (alreadyDead && dying.pid !== undefined) {
|
|
664
|
+
if (process.platform === "win32") {
|
|
665
|
+
// Windows:killProcessTree 的 taskkill 以引擎 pid 为树根,树根已死 → not
|
|
666
|
+
// found → 空操作(不可复用)——镜像通道收割(置死前快照,见下)。
|
|
667
|
+
this.reapOrphansViaMirrorSnapshot(detail);
|
|
668
|
+
} else {
|
|
669
|
+
// POSIX:复用 killProcessTree 负 pid 组杀(SIGTERM → 5s grace → SIGKILL
|
|
670
|
+
// 升级链内建,fire-and-forget 不阻塞同步 teardown)。组长死后进程组存活到
|
|
671
|
+
// 最后一员退出,组杀可达任务子进程(引擎 detached:true spawn = 组长,任务
|
|
672
|
+
// 子进程 detached:false 同组,组杀目标 -child.pid 不依赖镜像)。
|
|
673
|
+
// 无外层逐 pid 兜底(SIGTERM 同步返回后 5s 窗内孤儿必然仍活,同步校验恒
|
|
674
|
+
// 命中、立即兜底 = 优雅窗归零截断 trap-flush——升级链已覆盖残余职责)。
|
|
675
|
+
killProcessTree(
|
|
676
|
+
dying.pid,
|
|
677
|
+
`reap orphaned children after unexpected engine death (${detail})`,
|
|
678
|
+
{ engineId: this.engineId },
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* 握手失败路径(版本越界/握手超时/启动僵死)引擎进程仍活着:必须组杀,否则
|
|
687
|
+
* crash 重建循环每次 spawn 泄漏一个常驻孤儿(测试机实测积累 370+)。主动杀
|
|
688
|
+
* (intentionalKill=true)与进程已死(exitCode/signalCode 非空)跳过重复发信号。
|
|
689
|
+
*/
|
|
690
|
+
private killLeakedAliveChild(detail: string): void {
|
|
656
691
|
if (this.child !== undefined) {
|
|
657
692
|
const child = this.child;
|
|
658
693
|
this.child = undefined;
|
|
659
|
-
// 握手失败路径(版本越界 / 握手超时 / 启动僵死)引擎进程仍活着:必须组杀,
|
|
660
|
-
// 否则泄漏为常驻孤儿(crash 重建循环每次 spawn 泄漏一个;测试机实测积累
|
|
661
|
-
// 370+ 假引擎进程)。主动杀路径(killAll 已发信号,intentionalKill=true)与
|
|
662
|
-
// 进程已死路径(onEngineExit,exitCode/signalCode 非空)跳过重复发信号。
|
|
663
694
|
const alreadyDead = child.exitCode !== null || child.signalCode !== null;
|
|
664
695
|
if (!alreadyDead && !this.intentionalKill && child.pid !== undefined) {
|
|
665
696
|
killProcessTree(child.pid, detail, { engineId: this.engineId });
|
|
666
697
|
}
|
|
667
698
|
child.removeAllListeners();
|
|
668
699
|
}
|
|
669
|
-
|
|
670
|
-
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* [H1 U2 / 红线①Windows 通道] 镜像快照收割活孤儿:镜像快照同步抓清单(置死前,
|
|
704
|
+
* 全量项含引擎一生历史死 pid——镜像对已退出子进程只改 state 不删项、唯一清空点 =
|
|
705
|
+
* killAll)→ 过滤活孤儿(state === "running" && !killed,活孤儿 ≤ 并发数)→ 逐 pid
|
|
706
|
+
* **异步 spawn** taskkill /T /F fire-and-forget(树根 = 活着的任务子进程自身,树杀
|
|
707
|
+
* 有效;快照同步抓、杀动作异步发——与 POSIX fire-and-forget 对称;禁 spawnSync:
|
|
708
|
+
* 逐个同步 N×10s 上界会冻结 onEngineExit 同步回调、推迟 pending reject(Continuation
|
|
709
|
+
* 失败通知链源头)与宿主事件循环)。
|
|
710
|
+
*/
|
|
711
|
+
private reapOrphansViaMirrorSnapshot(detail: string): void {
|
|
712
|
+
const liveOrphans = this.mirror.snapshot().filter((e) => e.state === "running" && !e.killed);
|
|
713
|
+
if (liveOrphans.length === 0) return;
|
|
714
|
+
logger.warn(
|
|
715
|
+
`[engine-client:${this.engineId}] reaping ${liveOrphans.length} orphaned task child(ren) via mirror snapshot after unexpected engine death (${detail})`,
|
|
716
|
+
);
|
|
717
|
+
for (const entry of liveOrphans) {
|
|
718
|
+
const killer = spawn("taskkill", ["/PID", String(entry.pid), "/T", "/F"], { stdio: "ignore" });
|
|
719
|
+
killer.unref();
|
|
720
|
+
killer.on("error", (err: Error) => {
|
|
721
|
+
// spawn 失败(taskkill 缺失等)留 debug 痕迹——残余孤儿由红线③登记承接
|
|
722
|
+
//(下一轮派发前兜底 / 宿主重启窗口)。
|
|
723
|
+
logger.debug(
|
|
724
|
+
`[engine-client:${this.engineId}] taskkill spawn failed for orphan pid ${entry.pid} (${err.message})`,
|
|
725
|
+
);
|
|
726
|
+
});
|
|
671
727
|
}
|
|
672
728
|
}
|
|
673
729
|
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import { getLogger } from "@zhushanwen/subagent-engine-sdk";
|
|
21
21
|
|
|
22
|
+
import { notifyChildProcessExited } from "../../dialog-queue.ts";
|
|
23
|
+
|
|
22
24
|
const logger = getLogger("subagents");
|
|
23
25
|
|
|
24
26
|
/** 镜像项(childSpawned/childStateChanged 载荷的落库形态)。 */
|
|
@@ -90,6 +92,11 @@ export class SpawnedChildrenMirror {
|
|
|
90
92
|
};
|
|
91
93
|
this.entries.set(patch.pid, entry);
|
|
92
94
|
this.emit({ reason: "childStateChanged", pid: patch.pid, recordId: patch.recordId });
|
|
95
|
+
// [SR-4 接线] 子进程退出 → 取消其挂起 dialog:原绑定点(宿主 spawn pi 子进程时代
|
|
96
|
+
// 的 session-runner child close)随协议化消失(宿主不再持句柄),新落点 = 引擎经
|
|
97
|
+
// host/childStateChanged 上报 → 本镜像消费。未接线时该子进程的 pending dialog 要
|
|
98
|
+
// 占住队列串行位直到 30min 队列级超时(延迟清理,非死锁)。
|
|
99
|
+
if (patch.state === "exited") notifyChildProcessExited(patch.pid);
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
/**
|
|
@@ -106,6 +113,8 @@ export class SpawnedChildrenMirror {
|
|
|
106
113
|
entry.state = "exited";
|
|
107
114
|
entry.updatedAt = Date.now();
|
|
108
115
|
this.emit({ reason: "killedAll", pid: entry.pid, recordId: entry.recordId });
|
|
116
|
+
// [SR-4 接线] 引擎进程消亡 = 其全部子进程已死 → 逐 pid 取消挂起 dialog
|
|
117
|
+
notifyChildProcessExited(entry.pid);
|
|
109
118
|
}
|
|
110
119
|
this.entries.clear();
|
|
111
120
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/execution/engine/client/remote-engine.ts
|
|
2
2
|
//
|
|
3
3
|
// RemoteEngine:cli 形态 EnginePort 适配(W2,impl-plan §2.2「RemoteEngine 同步成员
|
|
4
|
-
//
|
|
4
|
+
// 形态映射」必写死;[H1 U6] 交互控制面与 recordId 键路由面已随 chat 域退役删除)。
|
|
5
|
+
// 把 core EnginePort 的成员映射到 EngineClient 协议请求;
|
|
5
6
|
// 同步成员(capabilities / listModels / validateModel)**只读 manifest 注册期快照**
|
|
6
7
|
// ——单源化原则(设计 §3.3「同步成员清单」v6 减法):无握手缓存、无失效时机,
|
|
7
8
|
// initialize 应答仅诊断(warn 由 EngineClient 留痕)。
|
|
@@ -12,31 +13,31 @@
|
|
|
12
13
|
//
|
|
13
14
|
// W3 消费契约:routing/registry 的 cli 形态 EnginePort 实例 = 本类(先写后读)。
|
|
14
15
|
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
15
19
|
import {
|
|
16
20
|
CANCEL_SETTLE_GRACE_MS,
|
|
17
21
|
EngineSdkError,
|
|
18
22
|
type AgentCallOpts as SdkAgentCallOpts,
|
|
19
23
|
type AgentOutcome as SdkAgentOutcome,
|
|
20
24
|
type EngineHandleData as SdkEngineHandleData,
|
|
21
|
-
type InteractAction as SdkInteractAction,
|
|
22
|
-
type InteractResult as SdkInteractResult,
|
|
23
25
|
type ModelCatalogEntry,
|
|
24
26
|
type ProbeReport as SdkProbeReport,
|
|
25
27
|
type SessionView as SdkSessionView,
|
|
26
28
|
} from "@zhushanwen/subagent-engine-sdk";
|
|
27
29
|
|
|
28
30
|
import type { AgentCallOpts } from "../../../orchestration/models/types.ts";
|
|
31
|
+
import { getSubagentSessionDir } from "../../path-encoding.ts";
|
|
29
32
|
import { assertGateCapabilitiesMatched } from "../common/capability-gate.ts";
|
|
30
33
|
import type {
|
|
31
34
|
EngineCapabilities,
|
|
32
35
|
EngineHandle,
|
|
33
36
|
EngineHandleData,
|
|
34
|
-
InteractAction,
|
|
35
|
-
InteractResult,
|
|
36
37
|
ProbeReport,
|
|
37
38
|
SessionView,
|
|
38
39
|
} from "../types.ts";
|
|
39
|
-
import type {
|
|
40
|
+
import type { EnginePort, EngineRunResult, RunContext } from "../port.ts";
|
|
40
41
|
import type { EngineClient, RunRoute } from "./engine-client.ts";
|
|
41
42
|
|
|
42
43
|
/** manifest 注册期快照(发现器/注册表读取,构造时注入——同步成员唯一源)。 */
|
|
@@ -91,7 +92,7 @@ export class RemoteEngine implements EnginePort {
|
|
|
91
92
|
// **不实现**(消费方 model-validation.ts:62 `typeof validateModel !== "function"`
|
|
92
93
|
// → 跳过校验恒放行)。实例 own property 置 undefined 遮蔽原型方法——
|
|
93
94
|
// typeof engine.validateModel === "undefined"。
|
|
94
|
-
(this as
|
|
95
|
+
(this as { validateModel?: unknown }).validateModel = undefined;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -232,24 +233,6 @@ export class RemoteEngine implements EnginePort {
|
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
async interact(handle: EngineHandle, action: InteractAction): Promise<InteractResult> {
|
|
236
|
-
await this.opts.client.ensureConnected();
|
|
237
|
-
const result = (await this.opts.client.request("interact", {
|
|
238
|
-
handle: handle.data,
|
|
239
|
-
action: action as SdkInteractAction,
|
|
240
|
-
})) as SdkInteractResult;
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* [W3 v1.x] chat 轮次反向通道路由注册(recordId 键,EnginePort 可选面实现):
|
|
246
|
-
* interact 续聊轮的 streamDelta / roundLifecycle 分发目标。薄委托 EngineClient
|
|
247
|
-
* 的 recordRoutes(键分发见 reverse-router.ts)。
|
|
248
|
-
*/
|
|
249
|
-
registerChatRoundRoute(recordId: string, route: ChatRoundRoute): () => void {
|
|
250
|
-
return this.opts.client.registerRecordRoute(recordId, route as RunRoute);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
236
|
/** 协议 read(dataDir 必填——引擎数据根,构造注入)。 */
|
|
254
237
|
async read(handle: EngineHandle): Promise<SessionView> {
|
|
255
238
|
await this.opts.client.ensureConnected();
|
|
@@ -308,16 +291,47 @@ interface WireRunParams {
|
|
|
308
291
|
ctxModel: string | undefined;
|
|
309
292
|
engineFallback: RunContext["engineFallback"];
|
|
310
293
|
streamMode: "stream" | undefined;
|
|
294
|
+
sessionRootId?: string;
|
|
295
|
+
/** [Option C] 恒有值(宿主注入 ?? 同源 env 推导)——与 sessionRootId 的
|
|
296
|
+
* "undefined 不上 wire" 不同,本字段派生恒产出字符串。 */
|
|
297
|
+
sessionDir: string;
|
|
311
298
|
};
|
|
312
|
-
|
|
299
|
+
resume?: NonNullable<RunContext["resume"]>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// pi 壳宿主进程内贯穿的两条 env(与 subagent-service / workflow-state-root 同源推导):
|
|
303
|
+
// - PI_CODING_AGENT_DIR:pi SDK getAgentDir 的 env 覆盖通道——xyz-agent 生产链路由
|
|
304
|
+
// runtime spawn pi 时显式注入(rpc-client buildPiOutboundEnv,经
|
|
305
|
+
// buildOutboundChildEnv 共享构建器出站);缺省 ~/.pi/agent 与 pi
|
|
306
|
+
// 实装版 dist config.js getAgentDir 逐字同构(锚定先例 workflow-state-root.ts)。
|
|
307
|
+
// - PI_SUBAGENT_ROOT_CWD:真 ROOT 的 cwd(MF-3 贯穿)——嵌套 subagent 场景宿主
|
|
308
|
+
// spawn 子进程时注入,与 subagent-service 构造处的 rootCwd 同 env 同值。
|
|
309
|
+
const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
310
|
+
const PI_ROOT_CWD_ENV = "PI_SUBAGENT_ROOT_CWD";
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* [Option C 协议化] 宿主权威 subagent session 目录(Gate B S6 修复):宿主进程内
|
|
314
|
+
* 同源 env 推导 agentDir/rootCwd 后调 getSubagentSessionDir(宿主单一权威推导,
|
|
315
|
+
* path-encoding.ts——引擎本地推导与宿主布局三处不等价,已降级 [LEGACY] fallback)。
|
|
316
|
+
* 每次调用重新解析(env 读取零成本,不缓存防测试/宿主切换读旧值,对齐
|
|
317
|
+
* common/data-dir.ts getEngineDataDir 惯例)。rootCwd 缺省 process.cwd():pi 壳
|
|
318
|
+
* ctx.cwd = pi 进程启动 cwd(session-lifecycle 侧同用进程 cwd 的既有锚定)。
|
|
319
|
+
*/
|
|
320
|
+
function deriveHostSubagentSessionDir(): string {
|
|
321
|
+
const agentDir = process.env[PI_AGENT_DIR_ENV];
|
|
322
|
+
const resolvedAgentDir =
|
|
323
|
+
agentDir !== undefined && agentDir !== "" ? agentDir : join(homedir(), ".pi", "agent");
|
|
324
|
+
const rootCwd = process.env[PI_ROOT_CWD_ENV];
|
|
325
|
+
const resolvedRootCwd = rootCwd !== undefined && rootCwd !== "" ? rootCwd : process.cwd();
|
|
326
|
+
return getSubagentSessionDir(resolvedAgentDir, resolvedRootCwd);
|
|
313
327
|
}
|
|
314
328
|
|
|
315
329
|
/**
|
|
316
330
|
* run 帧 wire 载荷构建。协议 ctx 承载(RunContext 字段映射表):cwd 取任务声明值
|
|
317
331
|
* (缺省进程 cwd);ctxModel 投影 canonical 词形(provider/id,ModelInfo 字段裁决)。
|
|
318
|
-
* [
|
|
319
|
-
* RunContext.
|
|
320
|
-
* ctx.
|
|
332
|
+
* [H1 U6] 会话形态参数直传(RunContext.resume → run.params.resume;结构由
|
|
333
|
+
* RunContext.resume 注释与 SDK RunResumeParams 的 implements 互证承载)。一次性轮
|
|
334
|
+
* ctx.resume === undefined → wire 上不出现该键(协议 additive 语义)。
|
|
321
335
|
*/
|
|
322
336
|
function buildRunParams(task: AgentCallOpts, ctx: RunContext, runId: string): WireRunParams {
|
|
323
337
|
const ctxModelRef = ctx.ctxModel ? `${ctx.ctxModel.provider}/${ctx.ctxModel.id}` : undefined;
|
|
@@ -332,21 +346,25 @@ function buildRunParams(task: AgentCallOpts, ctx: RunContext, runId: string): Wi
|
|
|
332
346
|
ctxModel: ctxModelRef,
|
|
333
347
|
engineFallback: ctx.engineFallback,
|
|
334
348
|
streamMode: ctx.stream !== undefined ? ("stream" as const) : undefined,
|
|
349
|
+
// [F6] 根 session id(relay 归属键 SESSION_ID 权威源)——undefined 不上 wire
|
|
350
|
+
//(additive 语义,与顶层 chat 参数同写法)。
|
|
351
|
+
...(ctx.sessionRootId !== undefined ? { sessionRootId: ctx.sessionRootId } : {}),
|
|
352
|
+
// [Option C 协议化] 权威 subagent session 目录(Gate B S6):宿主注入值优先,
|
|
353
|
+
// 缺省同源 env 推导(deriveHostSubagentSessionDir)——恒有值恒上 wire,引擎
|
|
354
|
+
// 据此组装 --session-dir 不自推导(引擎本地推导降级 [LEGACY] fallback)。
|
|
355
|
+
sessionDir: ctx.sessionDir ?? deriveHostSubagentSessionDir(),
|
|
335
356
|
},
|
|
336
|
-
...(ctx.
|
|
357
|
+
...(ctx.resume !== undefined ? { resume: ctx.resume } : {}),
|
|
337
358
|
};
|
|
338
359
|
}
|
|
339
360
|
|
|
340
|
-
/** run 作用域事件路由(event / streamDelta / poolResolved / handleReady
|
|
361
|
+
/** run 作用域事件路由(event / streamDelta / poolResolved / handleReady)。 */
|
|
341
362
|
function buildRunRouteHandlers(ctx: RunContext): RunRoute {
|
|
342
363
|
return {
|
|
343
364
|
onEvent: (event) => ctx.onEvent?.(event as Parameters<NonNullable<RunContext["onEvent"]>>[0]),
|
|
344
365
|
onStreamDelta: (delta) => ctx.stream?.onDelta(delta),
|
|
345
366
|
onPoolResolved: (poolKey) => ctx.onPoolResolved?.(poolKey),
|
|
346
367
|
onHandleReady: (partial) => ctx.onHandleReady?.(partial),
|
|
347
|
-
// [W3 v1.x] 首轮(run 会话形态)轮次生命周期帧(runId 键)→ 宿主消费口。
|
|
348
|
-
onRoundLifecycle: (phase) =>
|
|
349
|
-
ctx.onRoundLifecycle?.(phase as Parameters<NonNullable<RunContext["onRoundLifecycle"]>>[0]),
|
|
350
368
|
};
|
|
351
369
|
}
|
|
352
370
|
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
type HostLogParams,
|
|
25
25
|
type HostPermissionParams,
|
|
26
26
|
type HostPoolResolvedParams,
|
|
27
|
-
type HostRoundLifecycleParams,
|
|
28
27
|
type HostStreamDeltaParams,
|
|
29
28
|
type UiRequest,
|
|
30
29
|
} from "@zhushanwen/subagent-engine-sdk";
|
|
@@ -45,8 +44,6 @@ export interface ReverseRouterDeps {
|
|
|
45
44
|
log?: (params: HostLogParams) => void;
|
|
46
45
|
/** run 作用域通知路由表(EngineClient 持有,EngineClient 生命周期内同一引用)。 */
|
|
47
46
|
runRoutes: Map<string, RunRoute>;
|
|
48
|
-
/** [W3 v1.x] chat 轮次路由表(recordId 键;EngineClient 持有,同上)。 */
|
|
49
|
-
recordRoutes: Map<string, RunRoute>;
|
|
50
47
|
/** childSpawned/childStateChanged 的镜像落点。 */
|
|
51
48
|
mirror: SpawnedChildrenMirror;
|
|
52
49
|
/** handleReady 的 partial handle 回填(崩溃合成 handle 数据源)。 */
|
|
@@ -159,11 +156,11 @@ function handleDataPlaneRequest(deps: ReverseRouterDeps, frame: { id: string; me
|
|
|
159
156
|
/** 数据面单通道分发器(params 形态自证——发帧前引擎侧自检,分发层只做强转)。 */
|
|
160
157
|
type DataPlaneHandler = (deps: ReverseRouterDeps, params: unknown) => Promise<void> | void;
|
|
161
158
|
|
|
162
|
-
/** method → 分发器表(表外 method = 无操作;路由面准入已由 REVERSE_CHANNEL_TIMEOUT_CLASS 收敛为 data-plane)。
|
|
159
|
+
/** method → 分发器表(表外 method = 无操作;路由面准入已由 REVERSE_CHANNEL_TIMEOUT_CLASS 收敛为 data-plane)。
|
|
160
|
+
* [H1 U6] 旧轮次相位反向通道已随 chat 域相位机退役删除(交棒改 run 应答驱动)。 */
|
|
163
161
|
const DATA_PLANE_HANDLERS: Record<string, DataPlaneHandler> = {
|
|
164
162
|
"host/log": (deps, params) => dispatchLog(deps, params as HostLogParams),
|
|
165
163
|
"host/streamDelta": (deps, params) => dispatchStreamDelta(deps, params as HostStreamDeltaParams),
|
|
166
|
-
"host/roundLifecycle": (deps, params) => dispatchRoundLifecycle(deps, params as HostRoundLifecycleParams),
|
|
167
164
|
"host/poolResolved": (deps, params) => dispatchPoolResolved(deps, params as HostPoolResolvedParams),
|
|
168
165
|
"host/handleReady": (deps, params) => dispatchHandleReady(deps, params as HostHandleReadyParams),
|
|
169
166
|
"host/childSpawned": (deps, params) => dispatchChildSpawned(deps, params as HostChildSpawnedParams),
|
|
@@ -180,31 +177,13 @@ function dispatchLog(deps: ReverseRouterDeps, p: HostLogParams): void {
|
|
|
180
177
|
}
|
|
181
178
|
|
|
182
179
|
async function dispatchStreamDelta(deps: ReverseRouterDeps, p: HostStreamDeltaParams): Promise<void> {
|
|
183
|
-
// [
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// 引擎侧不重发;路由缺席非引擎故障)。
|
|
188
|
-
if (p.recordId !== undefined) {
|
|
189
|
-
await deps.recordRoutes.get(p.recordId)?.onStreamDelta?.(p.delta);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
180
|
+
// [H1 U6] 关联键收敛为 runId 单键(recordId 分路随 chat 续聊轮 interact 面退役——
|
|
181
|
+
// 每轮 = 新 run,delta 恒经 run 作用域路由)。无注册路由 = 该轮宿主消费面未挂
|
|
182
|
+
//(诊断形态)——静默丢弃(数据面已 ack,引擎侧不重发;路由缺席非引擎故障)。
|
|
183
|
+
if (p.runId === undefined) return;
|
|
192
184
|
await deps.runRoutes.get(p.runId)?.onStreamDelta?.(p.delta);
|
|
193
185
|
}
|
|
194
186
|
|
|
195
|
-
async function dispatchRoundLifecycle(deps: ReverseRouterDeps, p: HostRoundLifecycleParams): Promise<void> {
|
|
196
|
-
// [W3 v1.x] 第 9 反向通道消费(settled/idle/failed 三相位 × runId|recordId 键)。
|
|
197
|
-
// 相位语义(arm/disarm/交棒/失败分诊)归宿主编排层——本路由只做键分发,
|
|
198
|
-
// 消费方 = RunContext.onRoundLifecycle(首轮 runId 键)与 registerChatRoundRoute
|
|
199
|
-
// 路由(续聊轮 recordId 键)。载荷形状自证(isHostRoundLifecycleParams)归
|
|
200
|
-
// 引擎侧发帧前自检 + 消费方,分发层不重复判别(与 streamDelta 一致的薄分发)。
|
|
201
|
-
if (p.recordId !== undefined) {
|
|
202
|
-
await deps.recordRoutes.get(p.recordId)?.onRoundLifecycle?.(p);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
await deps.runRoutes.get(p.runId)?.onRoundLifecycle?.(p);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
187
|
async function dispatchPoolResolved(deps: ReverseRouterDeps, p: HostPoolResolvedParams): Promise<void> {
|
|
209
188
|
await deps.runRoutes.get(p.runId)?.onPoolResolved?.(p.poolKey);
|
|
210
189
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/execution/engine/common/__tests__/run-signals.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [H2 W4] mergeRunSignals 纯函数行为测试——自 subprocess-agent-runner-timeout.test.ts
|
|
4
|
+
// 迁移改写:原 mergeTimeoutSignal 薄包装(timeoutMs 无守护信号源时原样返回 external
|
|
5
|
+
// signal;有则返回 .signal 视图)随 SAR.run 掏空删除(生产零消费),断言面改直测
|
|
6
|
+
// 公共 helper 权威实现(engine/common/run-signals.ts,W2 迁移步⑥自 SAR 提取)。
|
|
7
|
+
// 原用例行为语义逐条保持(T3.6 / T3.17 编号沿用)。
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { mergeRunSignals } from "../run-signals.ts";
|
|
12
|
+
|
|
13
|
+
describe("mergeRunSignals (D-A9 / H2 W2 迁移步⑥)", () => {
|
|
14
|
+
it("T3.6 timeoutMs===undefined → 原样返回 external signal", () => {
|
|
15
|
+
const ctrl = new AbortController();
|
|
16
|
+
const handle = mergeRunSignals(ctrl.signal, undefined);
|
|
17
|
+
expect(handle.signal).toBe(ctrl.signal);
|
|
18
|
+
handle.dispose();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("T3.6 timeoutMs<=0 → 原样返回 external signal", () => {
|
|
22
|
+
const ctrl = new AbortController();
|
|
23
|
+
const handle = mergeRunSignals(ctrl.signal, 0);
|
|
24
|
+
expect(handle.signal).toBe(ctrl.signal);
|
|
25
|
+
handle.dispose();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("T3.6 timeoutMs>0 → 返回新 signal(合并外部+超时两路)", () => {
|
|
29
|
+
const ctrl = new AbortController();
|
|
30
|
+
const handle = mergeRunSignals(ctrl.signal, 50);
|
|
31
|
+
expect(handle.signal).not.toBe(ctrl.signal);
|
|
32
|
+
expect(handle.signal.aborted).toBe(false);
|
|
33
|
+
handle.dispose();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("T3.6 timeoutMs 到期 → merged signal abort", () => {
|
|
37
|
+
vi.useFakeTimers();
|
|
38
|
+
const ctrl = new AbortController();
|
|
39
|
+
const handle = mergeRunSignals(ctrl.signal, 50);
|
|
40
|
+
|
|
41
|
+
expect(handle.signal.aborted).toBe(false);
|
|
42
|
+
vi.advanceTimersByTime(51);
|
|
43
|
+
expect(handle.signal.aborted).toBe(true);
|
|
44
|
+
handle.dispose();
|
|
45
|
+
vi.useRealTimers();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("T3.6 外部 signal abort → merged signal abort", () => {
|
|
49
|
+
const ctrl = new AbortController();
|
|
50
|
+
const handle = mergeRunSignals(ctrl.signal, 5000);
|
|
51
|
+
ctrl.abort();
|
|
52
|
+
expect(handle.signal.aborted).toBe(true);
|
|
53
|
+
handle.dispose();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("T3.6 外部 signal 已 abort → 返回已 abort 的 signal", () => {
|
|
57
|
+
const ctrl = new AbortController();
|
|
58
|
+
ctrl.abort();
|
|
59
|
+
const handle = mergeRunSignals(ctrl.signal, 5000);
|
|
60
|
+
expect(handle.signal.aborted).toBe(true);
|
|
61
|
+
handle.dispose();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("T3.17 NFR: merged signal abort → timeout timer 清理", () => {
|
|
65
|
+
vi.useFakeTimers();
|
|
66
|
+
const ctrl = new AbortController();
|
|
67
|
+
const handle = mergeRunSignals(ctrl.signal, 50);
|
|
68
|
+
|
|
69
|
+
ctrl.abort(); // 外部 abort → merged 也 abort
|
|
70
|
+
expect(handle.signal.aborted).toBe(true);
|
|
71
|
+
|
|
72
|
+
// 推进时间,不应再有副作用
|
|
73
|
+
vi.advanceTimersByTime(100);
|
|
74
|
+
// timer 应被清理(通过 abort event listener)
|
|
75
|
+
// 无异常 = timer 已正确清理
|
|
76
|
+
handle.dispose();
|
|
77
|
+
vi.useRealTimers();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -168,6 +168,33 @@ export function assertGateCapabilitiesMatched(
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// ============================================================
|
|
172
|
+
// 能力位方向判定③:SP-5 升级 gate(H1 U2 / D5 双写点共用文案单源)
|
|
173
|
+
// ============================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* [H1 U2 / D5 双写点] SP-5 升级(one-shot → chatMode)被 conversation 位拒绝时的
|
|
177
|
+
* 统一错误构造(文案/错误码/恢复指引单一权威)。
|
|
178
|
+
*
|
|
179
|
+
* 两个写点共同消费(现场契约「改动这两处必须协同」的 gate 化落点):
|
|
180
|
+
* - 写点①进程内热升级:subagent-actions-core messageHandler;
|
|
181
|
+
* - 写点②跨重启冷升级(D4 revive 格):conversation-continuation(U6 已删
|
|
182
|
+
* cold-resurrect 无条件置位——语义迁入本格,磁盘重建只水合持久化 chatMode)。
|
|
183
|
+
*
|
|
184
|
+
* unsupported 引擎(zcode)的 one-shot 收到 message 不升级——升级后续聊行为悬空
|
|
185
|
+
* (Continuation 续聊轮依赖引擎 resume 能力),guard 文案指引 fork/重派。
|
|
186
|
+
*/
|
|
187
|
+
export function engineConversationUpgradeUnsupportedError(engineId: string): EngineError {
|
|
188
|
+
return new EngineError(
|
|
189
|
+
"engine_capability_unsupported",
|
|
190
|
+
`subagent completed as one-shot on engine '${engineId}' (capabilities.conversation = 'unsupported') — ` +
|
|
191
|
+
`it cannot be upgraded to a resumable conversation, so the message is rejected.`,
|
|
192
|
+
`Recovery: re-dispatch the follow-up as a new subagent (action:'start'), ` +
|
|
193
|
+
`or branch from its session history with action:'fork-from', ` +
|
|
194
|
+
`or pick an engine that declares the conversation capability.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
171
198
|
/** engine_capability_mismatch 的统一构造(manifest 权威 + 恢复指引 = 修 manifest / 升级引擎包)。 */
|
|
172
199
|
function gateMismatchError(engineId: string, cap: string, declared: string, answered: string): EngineError {
|
|
173
200
|
return new EngineError(
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// 顶层(journal / 隔离池的父目录),extension 写侧与 runtime 校验侧同源推导。
|
|
6
6
|
//
|
|
7
7
|
// 通道调查结论(2026-08-25,P2 实测证据链):
|
|
8
|
-
// - runtime spawn pi 主会话时 RpcClient
|
|
9
|
-
// XYZ_
|
|
8
|
+
// - runtime spawn pi 主会话时 RpcClient 出站 env 从 runtime process.env 继承
|
|
9
|
+
// XYZ_ 前缀白名单变量(调查当时经私有 buildSafeEnv,现已被 buildPiOutboundEnv
|
|
10
|
+
// → buildOutboundChildEnv 共享构建器取代),且显式设置 PI_CODING_AGENT_DIR;
|
|
10
11
|
// - dev 模式 Electron main 设置 XYZ_AGENT_DATA_DIR(apps/electron/main/main.ts:122),
|
|
11
12
|
// 经 process-control.ts:262 透传给 runtime → 透传链成立;
|
|
12
13
|
// - prod 默认路径(~/.xyz-agent 缺省)下 runtime 进程 env 中**没有**该变量
|
|
@@ -59,7 +59,7 @@ export class EngineError extends Error {
|
|
|
59
59
|
this.recovery = recovery;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** 结构化投影(
|
|
62
|
+
/** 结构化投影(code/message 与 GUI 警告条共用形态)。 */
|
|
63
63
|
toStructured(): { code: EngineErrorCode; message: string; recovery: string } {
|
|
64
64
|
return { code: this.code, message: this.message, recovery: this.recovery };
|
|
65
65
|
}
|