@zhushanwen/subagent-core 0.6.0 → 0.7.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/README.md +1 -1
- package/dist/chunk-KXVBIVOU.js +16 -0
- package/dist/chunk-T2SYBW3I.js +24 -0
- package/dist/chunk-VURUAGMM.js +2305 -0
- package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
- package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
- package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
- package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
- package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
- package/dist/execution/engine/engine-discovery-scan.js +20 -0
- package/dist/execution/engine/paths.cjs +6 -23
- package/dist/execution/engine/paths.d.cts +1 -8
- package/dist/execution/engine/paths.d.ts +1 -8
- package/dist/execution/engine/paths.js +1 -1
- package/dist/execution/relay-env.cjs +12 -28
- package/dist/execution/relay-env.d.cts +1 -33
- package/dist/execution/relay-env.d.ts +1 -33
- package/dist/execution/relay-env.js +5 -3
- package/dist/index.cjs +14045 -16059
- package/dist/index.d.cts +1035 -1579
- package/dist/index.d.ts +1035 -1579
- package/dist/index.js +9838 -13800
- package/dist.bundle/index.cjs +28875 -30323
- package/package.json +20 -26
- package/src/__tests__/record-store-last-line.test.ts +16 -7
- package/src/core/error-message.ts +7 -6
- package/src/core/host-services.ts +8 -2
- package/src/core/notify-ports.ts +17 -2
- package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
- package/src/execution/__tests__/delivery-methods.test.ts +179 -237
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -13
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
- package/src/execution/__tests__/gc-timer.test.ts +7 -22
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
- package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
- package/src/execution/__tests__/idle-gc.test.ts +200 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
- package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
- package/src/execution/__tests__/record-store.test.ts +60 -6
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
- package/src/execution/__tests__/round-supervisor.test.ts +293 -0
- package/src/execution/__tests__/session-pending.test.ts +119 -17
- package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
- package/src/execution/__tests__/subagent-service.test.ts +2 -2
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
- package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
- package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
- package/src/execution/agent-registry.ts +6 -2
- package/src/execution/dialog-queue.ts +6 -73
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
- package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
- package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
- package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
- package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
- package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
- package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
- package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
- package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
- package/src/execution/engine/__tests__/registry.test.ts +206 -1
- package/src/execution/engine/__tests__/routing.test.ts +97 -0
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
- package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
- package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
- package/src/execution/engine/client/client-options.ts +103 -0
- package/src/execution/engine/client/engine-client.ts +687 -0
- package/src/execution/engine/client/mirror.ts +167 -0
- package/src/execution/engine/client/pid-file.ts +392 -0
- package/src/execution/engine/client/reaper.ts +95 -0
- package/src/execution/engine/client/remote-engine.ts +441 -0
- package/src/execution/engine/client/reverse-router.ts +255 -0
- package/src/execution/engine/common/capability-gate.ts +109 -22
- package/src/execution/engine/common/errors.ts +9 -25
- package/src/execution/engine/common/event-journal.ts +4 -50
- package/src/execution/engine/common/journal-replay.ts +3 -1
- package/src/execution/engine/common/kill-chain.ts +18 -251
- package/src/execution/engine/common/nesting-guard.ts +18 -114
- package/src/execution/engine/common/pool-manager.ts +4 -0
- package/src/execution/engine/common/session-view-service.ts +56 -84
- package/src/execution/engine/config.ts +151 -0
- package/src/execution/engine/d8-compat.ts +346 -0
- package/src/execution/engine/engine-discovery-roots.ts +107 -0
- package/src/execution/engine/engine-discovery-scan.ts +269 -0
- package/src/execution/engine/engine-discovery.ts +30 -7
- package/src/execution/engine/engine-inspect-package.ts +282 -0
- package/src/execution/engine/engine-manifest.ts +244 -0
- package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
- package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
- package/src/execution/engine/host/host-bridge.ts +150 -0
- package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
- package/src/execution/engine/host/pi-host-binding.ts +91 -0
- package/src/execution/engine/host/spawned-children.ts +139 -0
- package/src/execution/engine/host-task-spec.ts +29 -2
- package/src/execution/engine/model-validation.ts +55 -2
- package/src/execution/engine/paths.ts +16 -41
- package/src/execution/engine/port.ts +48 -2
- package/src/execution/engine/registry.ts +153 -34
- package/src/execution/engine/routing.ts +225 -52
- package/src/execution/engine/types.ts +25 -200
- package/src/execution/finalize-record.ts +25 -0
- package/src/execution/idle-gc.ts +89 -14
- package/src/execution/lifecycle-manager.ts +10 -7
- package/src/execution/lifecycle-predicates.ts +10 -8
- package/src/execution/record-store.ts +29 -2
- package/src/execution/relay-env.ts +19 -36
- package/src/execution/round-supervisor/domain.ts +89 -0
- package/src/execution/round-supervisor/index.ts +33 -0
- package/src/execution/round-supervisor/notify-accounting.ts +87 -0
- package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
- package/src/execution/round-supervisor/service-binding.test.ts +661 -0
- package/src/execution/round-supervisor/service-binding.ts +273 -0
- package/src/execution/round-supervisor/supervisor.ts +426 -0
- package/src/execution/session-pending.ts +68 -74
- package/src/execution/settled-watchdog.ts +39 -0
- package/src/execution/subagent-service.ts +746 -400
- package/src/execution/subprocess-agent-runner.ts +16 -8
- package/src/execution/types.ts +41 -117
- package/src/execution/ui-channels.ts +13 -214
- package/src/execution/ui-request-handler-factory.ts +7 -4
- package/src/execution/ui-request-observability.ts +2 -2
- package/src/execution/workflow-state-root.ts +67 -0
- package/src/execution/worktree-git-ops.ts +5 -0
- package/src/execution/worktree-manager.ts +11 -1
- package/src/index.ts +55 -26
- package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +144 -59
- package/src/orchestration/models/types.ts +43 -42
- package/src/shared/zcode-model-ref.ts +31 -0
- package/dist/chunk-43ONBFZX.js +0 -240
- package/dist/chunk-A4IVZWVX.js +0 -31
- package/dist/chunk-APZY4IME.js +0 -27
- package/dist/chunk-V3VHZ2VX.js +0 -59
- package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
- package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
- package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
- package/dist/execution/engine/engines/zcode/constants.js +0 -54
- package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
- package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
- package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
- package/dist/execution/engine/engines/zcode/reader.js +0 -9
- package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
- package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
- package/src/__tests__/session-runner.test.ts +0 -69
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
- package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
- package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
- package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
- package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
- package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
- package/src/execution/__tests__/output-collector.test.ts +0 -469
- package/src/execution/__tests__/pi-invocation.test.ts +0 -166
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
- package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
- package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
- package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
- package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
- package/src/execution/__tests__/spawn-args.test.ts +0 -446
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
- package/src/execution/__tests__/spawned-children.test.ts +0 -236
- package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
- package/src/execution/__tests__/stdin-writer.test.ts +0 -464
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -616
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
- package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
- package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
- package/src/execution/engine/engines/pi/output-collector.ts +0 -317
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
- package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
- package/src/execution/engine/engines/pi/reader.ts +0 -59
- package/src/execution/engine/engines/pi/registration.ts +0 -37
- package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
- package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
- package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
- package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
- package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
- package/src/execution/engine/engines/zcode/connection.ts +0 -598
- package/src/execution/engine/engines/zcode/constants.ts +0 -176
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
- package/src/execution/engine/engines/zcode/parser.ts +0 -102
- package/src/execution/engine/engines/zcode/preparer.ts +0 -235
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
- package/src/execution/round-settlement.ts +0 -93
- package/src/execution/ui-request-queue.ts +0 -215
- package/src/shared/schema-env.ts +0 -44
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/execution/engine/client/mirror.ts
|
|
2
|
+
//
|
|
3
|
+
// spawnedChildren 状态镜像(W2 协议客户端,impl-plan §2.2「镜像失效语义」)。
|
|
4
|
+
//
|
|
5
|
+
// 协议化后引擎进程是子进程的持有方(spawn/杀链都发生在引擎侧),core 只持
|
|
6
|
+
// **状态镜像**:数据源 = 反向通道 `host/childSpawned` / `host/childStateChanged`。
|
|
7
|
+
// 消费者 = 生命周期谓词(W6 起 lifecycle-predicates 的 hasLiveProcessHandle /
|
|
8
|
+
// isResumable 改读镜像,同步读、不跨进程查询)与 notify 合并窗口 / idle GC。
|
|
9
|
+
//
|
|
10
|
+
// 失效语义(impl-plan §2.2 必写死):
|
|
11
|
+
// 1. 未收 `childSpawned` 前 = 无句柄(getChildByRecord undefined 等价);
|
|
12
|
+
// 2. 引擎进程 exit / 重建 / dispose / killAll 时把该引擎**全部镜像项整体置死**
|
|
13
|
+
// (killed=true + 状态广播)——否则 notify-host 60s 合并窗口挂住 / idle-gc TTL
|
|
14
|
+
// 不触发 / resumable 说谎 / 续聊被拒;
|
|
15
|
+
// 3. 置死广播后镜像清空,isResumable 回落「无句柄」。
|
|
16
|
+
//
|
|
17
|
+
// 载荷契约(SDK reverse-channels.ts):`killed` 必含,判据
|
|
18
|
+
// `child !== undefined && !child.killed`——本镜像的状态查询面与其同构。
|
|
19
|
+
|
|
20
|
+
import { getLogger } from "@zhushanwen/subagent-engine-sdk";
|
|
21
|
+
|
|
22
|
+
const logger = getLogger("subagents");
|
|
23
|
+
|
|
24
|
+
/** 镜像项(childSpawned/childStateChanged 载荷的落库形态)。 */
|
|
25
|
+
export interface MirrorEntry {
|
|
26
|
+
/** 引擎内一次性子进程 pid(镜像 key)。 */
|
|
27
|
+
readonly pid: number;
|
|
28
|
+
/** 归属 record id。 */
|
|
29
|
+
readonly recordId: string;
|
|
30
|
+
/** running = 在跑;exited = 自然/被杀退出。 */
|
|
31
|
+
state: "running" | "exited";
|
|
32
|
+
/** 必含(判据 `child !== undefined && !child.killed`);true = 已被杀/已终止。 */
|
|
33
|
+
killed: boolean;
|
|
34
|
+
exitCode?: number;
|
|
35
|
+
signal?: string;
|
|
36
|
+
/** 最近一次状态变更的墙钟时间戳(Date.now(),诊断面)。 */
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 镜像状态变更事件(广播载荷)。 */
|
|
41
|
+
export interface MirrorChangeEvent {
|
|
42
|
+
reason: "childSpawned" | "childStateChanged" | "killedAll";
|
|
43
|
+
/** killedAll = 整体置死(引擎 exit / 重建 / dispose / killAll)。 */
|
|
44
|
+
pid?: number;
|
|
45
|
+
recordId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type MirrorListener = (event: MirrorChangeEvent) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* spawnedChildren 状态镜像(per EngineClient 一份)。
|
|
52
|
+
*
|
|
53
|
+
* 镜像只是状态投影:本类不发信号、不杀进程(收割 = 引擎进程组级,见 EngineClient);
|
|
54
|
+
* 「整体置死」是对状态面的批量标记 + 广播,使宿主侧谓词/通知/GC 立即回落安全态。
|
|
55
|
+
*/
|
|
56
|
+
export class SpawnedChildrenMirror {
|
|
57
|
+
private readonly entries = new Map<number, MirrorEntry>();
|
|
58
|
+
private readonly listeners = new Set<MirrorListener>();
|
|
59
|
+
|
|
60
|
+
/** `host/childSpawned`:落项(此前该 record 无句柄——失效语义 1)。 */
|
|
61
|
+
recordSpawned(pid: number, recordId: string): void {
|
|
62
|
+
this.entries.set(pid, {
|
|
63
|
+
pid,
|
|
64
|
+
recordId,
|
|
65
|
+
state: "running",
|
|
66
|
+
killed: false,
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
});
|
|
69
|
+
this.emit({ reason: "childSpawned", pid, recordId });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `host/childStateChanged`:更新项(载荷 killed 必含——类型层 required)。 */
|
|
73
|
+
recordStateChanged(patch: {
|
|
74
|
+
pid: number;
|
|
75
|
+
recordId: string;
|
|
76
|
+
state: "running" | "exited";
|
|
77
|
+
killed: boolean;
|
|
78
|
+
exitCode?: number;
|
|
79
|
+
signal?: string;
|
|
80
|
+
}): void {
|
|
81
|
+
const existing = this.entries.get(patch.pid);
|
|
82
|
+
const entry: MirrorEntry = {
|
|
83
|
+
pid: patch.pid,
|
|
84
|
+
recordId: patch.recordId,
|
|
85
|
+
state: patch.state,
|
|
86
|
+
killed: patch.killed,
|
|
87
|
+
exitCode: patch.exitCode ?? existing?.exitCode,
|
|
88
|
+
signal: patch.signal ?? existing?.signal,
|
|
89
|
+
updatedAt: Date.now(),
|
|
90
|
+
};
|
|
91
|
+
this.entries.set(patch.pid, entry);
|
|
92
|
+
this.emit({ reason: "childStateChanged", pid: patch.pid, recordId: patch.recordId });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 失效语义 2 + 3:全部镜像项整体置死(killed=true + state=exited)→ 逐项广播 →
|
|
97
|
+
* 清空 Map。调用点 = 引擎进程 exit / 重建 / dispose / killAll(EngineClient 统一编排)。
|
|
98
|
+
*/
|
|
99
|
+
killAll(): void {
|
|
100
|
+
if (this.entries.size === 0) {
|
|
101
|
+
this.emit({ reason: "killedAll" });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of this.entries.values()) {
|
|
105
|
+
entry.killed = true;
|
|
106
|
+
entry.state = "exited";
|
|
107
|
+
entry.updatedAt = Date.now();
|
|
108
|
+
this.emit({ reason: "killedAll", pid: entry.pid, recordId: entry.recordId });
|
|
109
|
+
}
|
|
110
|
+
this.entries.clear();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 未收 childSpawned / 已被整体置死清空后 → undefined(无句柄等价,失效语义 1)。 */
|
|
114
|
+
getChildByRecord(recordId: string): MirrorEntry | undefined {
|
|
115
|
+
for (const entry of this.entries.values()) {
|
|
116
|
+
if (entry.recordId === recordId) return entry;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getEntry(pid: number): MirrorEntry | undefined {
|
|
122
|
+
return this.entries.get(pid);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 句柄存活谓词:镜像有项且未被置死(`child !== undefined && !child.killed`)。 */
|
|
126
|
+
hasLiveProcessHandle(recordId: string): boolean {
|
|
127
|
+
const entry = this.getChildByRecord(recordId);
|
|
128
|
+
return entry !== undefined && !entry.killed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** isResumable 的镜像面(W6 lifecycle-predicates 改读本方法;语义同 hasLiveProcessHandle)。 */
|
|
132
|
+
isResumable(recordId: string): boolean {
|
|
133
|
+
return this.hasLiveProcessHandle(recordId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 快照(诊断/测试)。 */
|
|
137
|
+
snapshot(): MirrorEntry[] {
|
|
138
|
+
return [...this.entries.values()];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get size(): number {
|
|
142
|
+
return this.entries.size;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 订阅状态广播(W6 notify/谓词接线点)。返回退订函数。 */
|
|
146
|
+
onChange(listener: MirrorListener): () => void {
|
|
147
|
+
this.listeners.add(listener);
|
|
148
|
+
return () => {
|
|
149
|
+
this.listeners.delete(listener);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private emit(event: MirrorChangeEvent): void {
|
|
154
|
+
for (const listener of this.listeners) {
|
|
155
|
+
try {
|
|
156
|
+
listener(event);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
// 广播监听器异常不阻断镜像主流程(监听器自身负责正式留痕)。
|
|
159
|
+
logger.debug(
|
|
160
|
+
`[spawned-children-mirror] listener threw for ${event.reason}: ${
|
|
161
|
+
err instanceof Error ? err.message : String(err)
|
|
162
|
+
}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// src/execution/engine/client/pid-file.ts
|
|
2
|
+
//
|
|
3
|
+
// 引擎实例 pidfile:原子写 + 启动期三条件清扫(W2,impl-plan §2.2「pidfile」+
|
|
4
|
+
// §7.2 R9-3 / R9-3b)。
|
|
5
|
+
//
|
|
6
|
+
// 命名(必写死):`<engineDataDir>/engines/<id>/engine.<hostKind>.<hostPid>.pid`
|
|
7
|
+
// —— pi 宿主与 runtime 各写自己的文件,**不得共用 `engine.pid`**(last-writer-wins
|
|
8
|
+
// 会误杀对方存活实例)。
|
|
9
|
+
//
|
|
10
|
+
// 写入时机 = spawn 成功后原子写(tmp + rename),内容含 enginePid / hostPid /
|
|
11
|
+
// engineStartTime(R9-3b:清扫时与目标 pid 实际启动时间比对,同 cmdline 的 pid
|
|
12
|
+
// 复用由此识别)。清理时机 = 引擎自灭 / dispose / 宿主正常退出。
|
|
13
|
+
//
|
|
14
|
+
// 三条件清扫(全部成立才杀):① 引擎 pid 存活;② cmdline 身份校验(复用
|
|
15
|
+
// session-runner readProcessCmdline 先例,防 pid 复用误杀);③ 该 pidfile 所属宿主
|
|
16
|
+
// pid 已死。任一**不确定**(ps 失败 / 宿主 pid 探测非 ESRCH 错误 / startTime 读不到)
|
|
17
|
+
// → 保守跳过不杀且保留文件(下轮再清);判定**不成立**(pid 已死 / cmdline 不符 /
|
|
18
|
+
// engineStartTime 不匹配 / 内容坏)→ 删除该 pidfile(陈旧清理通道,无数量上限——
|
|
19
|
+
// 否则宿主每次崩溃重启新增一个 pidfile 无限堆积)。
|
|
20
|
+
//
|
|
21
|
+
// Windows:无可移植启动时间判据 → 安全方向 = **不杀只删**(孤儿引擎由自灭
|
|
22
|
+
// stdio EOF 兜底;文件陈旧删除通道照走,防堆积)。
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { readdirSync, readFileSync, renameSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
|
|
28
|
+
import { getLogger, resolveEngineDir } from "@zhushanwen/subagent-engine-sdk";
|
|
29
|
+
|
|
30
|
+
const logger = getLogger("subagents");
|
|
31
|
+
|
|
32
|
+
/** ps 探测超时(处置路径一次性调用,防 ps 挂死拖住清扫;session-runner 先例同值)。 */
|
|
33
|
+
const CMDLINE_PROBE_TIMEOUT_MS = 3_000;
|
|
34
|
+
|
|
35
|
+
/** pidfile JSON 缩进(人类可排查形态;文件内容程序化消费,缩进仅为诊断友好)。 */
|
|
36
|
+
const PIDFILE_JSON_INDENT = 2;
|
|
37
|
+
|
|
38
|
+
/** pidfile 内容(JSON)。engineStartTime = OS 级进程启动时间串;读不到时 null。 */
|
|
39
|
+
export interface EnginePidfileContent {
|
|
40
|
+
enginePid: number;
|
|
41
|
+
hostPid: number;
|
|
42
|
+
engineStartTime: string | null;
|
|
43
|
+
engineId: string;
|
|
44
|
+
hostKind: string;
|
|
45
|
+
/** 写入时刻(ISO 8601,诊断面)。 */
|
|
46
|
+
writtenAt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function pidfilePath(
|
|
50
|
+
engineDataDir: string,
|
|
51
|
+
engineId: string,
|
|
52
|
+
hostKind: string,
|
|
53
|
+
hostPid: number,
|
|
54
|
+
): string {
|
|
55
|
+
return join(resolveEngineDir(engineDataDir, engineId), `engine.${hostKind}.${hostPid}.pid`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** spawn 成功后的原子写:同目录 tmp 文件 + rename(崩溃中断不落半截 JSON)。 */
|
|
59
|
+
export function writePidfileAtomic(path: string, content: EnginePidfileContent): void {
|
|
60
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
61
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
62
|
+
writeFileSync(tmp, `${JSON.stringify(content, null, PIDFILE_JSON_INDENT)}\n`, "utf-8");
|
|
63
|
+
renameSync(tmp, path);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 读 pidfile;缺失 / 坏 JSON → undefined(调用方按陈旧处理)。 */
|
|
67
|
+
export function readPidfile(path: string): EnginePidfileContent | undefined {
|
|
68
|
+
try {
|
|
69
|
+
const raw = readFileSync(path, "utf-8");
|
|
70
|
+
const parsed = JSON.parse(raw) as Partial<EnginePidfileContent>;
|
|
71
|
+
if (
|
|
72
|
+
typeof parsed.enginePid !== "number" ||
|
|
73
|
+
typeof parsed.hostPid !== "number" ||
|
|
74
|
+
typeof parsed.engineId !== "string" ||
|
|
75
|
+
typeof parsed.hostKind !== "string"
|
|
76
|
+
) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
enginePid: parsed.enginePid,
|
|
81
|
+
hostPid: parsed.hostPid,
|
|
82
|
+
engineStartTime:
|
|
83
|
+
parsed.engineStartTime === null || typeof parsed.engineStartTime === "string"
|
|
84
|
+
? parsed.engineStartTime
|
|
85
|
+
: null,
|
|
86
|
+
engineId: parsed.engineId,
|
|
87
|
+
hostKind: parsed.hostKind,
|
|
88
|
+
writtenAt: typeof parsed.writtenAt === "string" ? parsed.writtenAt : "",
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// 坏 JSON / 读取失败 = 陈旧文件的正常分支(调用方按 undefined → 删除处理),debug 留痕。
|
|
92
|
+
logger.debug(`[pid-file] readPidfile(${path}) failed, treating as stale: ${err instanceof Error ? err.message : String(err)}`);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function removePidfile(path: string): void {
|
|
98
|
+
try {
|
|
99
|
+
rmSync(path, { force: true });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// 已被并发清扫删除 = 预期终态,debug 留痕。
|
|
102
|
+
logger.debug(`[pid-file] removePidfile(${path}) failed (concurrent sweep = expected): ${err instanceof Error ? err.message : String(err)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================
|
|
107
|
+
// EngineClient 编排面(spawn 成功写入 + 启动清扫的一次式调用)
|
|
108
|
+
// ============================================================
|
|
109
|
+
|
|
110
|
+
export interface EnginePidfileRegistration {
|
|
111
|
+
engineDataDir: string;
|
|
112
|
+
engineId: string;
|
|
113
|
+
hostKind: string;
|
|
114
|
+
hostPid: number;
|
|
115
|
+
enginePid: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* spawn 成功后的实例 pidfile 登记一到式:读引擎 pid 的 OS 启动时间(R9-3b 清扫比对
|
|
120
|
+
* 数据源)→ 原子写。返回写入路径;写失败返回 undefined(清扫覆盖降级,warn 已留)。
|
|
121
|
+
*/
|
|
122
|
+
export function registerEnginePidfile(reg: EnginePidfileRegistration): string | undefined {
|
|
123
|
+
const path = pidfilePath(reg.engineDataDir, reg.engineId, reg.hostKind, reg.hostPid);
|
|
124
|
+
try {
|
|
125
|
+
writePidfileAtomic(path, {
|
|
126
|
+
enginePid: reg.enginePid,
|
|
127
|
+
hostPid: reg.hostPid,
|
|
128
|
+
engineStartTime: readProcessStartTime(reg.enginePid) ?? null,
|
|
129
|
+
engineId: reg.engineId,
|
|
130
|
+
hostKind: reg.hostKind,
|
|
131
|
+
writtenAt: new Date().toISOString(),
|
|
132
|
+
});
|
|
133
|
+
return path;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.warn(
|
|
136
|
+
`[pid-file] pidfile write failed for ${reg.engineId} (sweep coverage degraded): ${
|
|
137
|
+
err instanceof Error ? err.message : String(err)
|
|
138
|
+
}`,
|
|
139
|
+
);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 启动期一次性清扫的一到式包装(异常按保守跳过处理:清扫失败不阻断引擎启动,
|
|
146
|
+
* warn 留痕)。删除通道无数量上限(宿主 N 次崩溃重启的堆积一次全清)。
|
|
147
|
+
*/
|
|
148
|
+
export function sweepEnginePidfiles(args: {
|
|
149
|
+
engineDataDir: string;
|
|
150
|
+
engineId: string;
|
|
151
|
+
currentHostPid: number;
|
|
152
|
+
matchesEngineCmdline: (cmdline: string) => boolean;
|
|
153
|
+
}): PidfileSweepResult {
|
|
154
|
+
try {
|
|
155
|
+
return sweepStalePidfiles(args);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
logger.warn(
|
|
158
|
+
`[pid-file] pidfile sweep failed for ${args.engineId} (conservative skip): ${
|
|
159
|
+
err instanceof Error ? err.message : String(err)
|
|
160
|
+
}`,
|
|
161
|
+
);
|
|
162
|
+
return { killed: [], removed: [], skipped: [] };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 进程存活三态探测:true(存活)/ false(ESRCH = 已死)/ undefined(不确定,
|
|
168
|
+
* 如 EPERM——清扫方必须保守跳过)。
|
|
169
|
+
*/
|
|
170
|
+
export function isProcessAlive(pid: number): boolean | undefined {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, 0);
|
|
173
|
+
return true;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
176
|
+
if (code === "ESRCH") return false;
|
|
177
|
+
if (code === "EPERM") return undefined;
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 读目标 pid 的完整命令行(macOS + Linux 通用的 `ps -p <pid> -o command=`)。
|
|
184
|
+
* 返回 undefined:ps 失败 / 超时 / 空输出(进程刚死等)——调用方按「不确定」保守跳过。
|
|
185
|
+
* 复用 pi/session-runner.ts:595 readProcessCmdline 先例(同实现、同超时、同失败语义)。
|
|
186
|
+
*/
|
|
187
|
+
function readProcessCmdline(pid: number): string | undefined {
|
|
188
|
+
try {
|
|
189
|
+
const r = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
190
|
+
encoding: "utf-8",
|
|
191
|
+
timeout: CMDLINE_PROBE_TIMEOUT_MS,
|
|
192
|
+
});
|
|
193
|
+
if (r.error || r.status !== 0) return undefined;
|
|
194
|
+
const out = typeof r.stdout === "string" ? r.stdout.trim() : "";
|
|
195
|
+
return out.length > 0 ? out : undefined;
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 读目标 pid 的 OS 级启动时间(`ps -p <pid> -o lstart=`,macOS/Linux 通用文本形态,
|
|
203
|
+
* 如 "Mon Sep 8 21:14:32 2026")。R9-3b 的 pid 复用识别数据源:同 cmdline 的 pid
|
|
204
|
+
* 复用启动时间必不同。读不到 → undefined(保守跳过)。Windows 无可移植等价 →
|
|
205
|
+
* 调用方按平台分流(win32 不杀只删)。
|
|
206
|
+
*/
|
|
207
|
+
export function readProcessStartTime(pid: number): string | undefined {
|
|
208
|
+
try {
|
|
209
|
+
const r = spawnSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
210
|
+
encoding: "utf-8",
|
|
211
|
+
timeout: CMDLINE_PROBE_TIMEOUT_MS,
|
|
212
|
+
});
|
|
213
|
+
if (r.error || r.status !== 0) return undefined;
|
|
214
|
+
const out = typeof r.stdout === "string" ? r.stdout.trim() : "";
|
|
215
|
+
return out.length > 0 ? out : undefined;
|
|
216
|
+
} catch {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 清扫结果(诊断/测试可观测)。 */
|
|
222
|
+
export interface PidfileSweepResult {
|
|
223
|
+
/** 三条件全立被杀的引擎 pid(POSIX 组杀)。 */
|
|
224
|
+
killed: number[];
|
|
225
|
+
/** 判定不成立被删除的陈旧 pidfile(含 Windows 不杀只删)。 */
|
|
226
|
+
removed: Array<{ file: string; reason: string }>;
|
|
227
|
+
/** 保守跳过(任一不确定)——文件保留,下轮再清。 */
|
|
228
|
+
skipped: Array<{ file: string; reason: string }>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface SweepStalePidfilesOptions {
|
|
232
|
+
engineDataDir: string;
|
|
233
|
+
engineId: string;
|
|
234
|
+
/** 当前宿主 pid(跳过自己刚写的文件)。 */
|
|
235
|
+
currentHostPid: number;
|
|
236
|
+
/**
|
|
237
|
+
* 引擎 cmdline 身份谓词(防 pid 复用误杀的第二道校验)。由调用方按 spawn command
|
|
238
|
+
* 组装(如 cmdline 含引擎 CLI 入口的词形)。
|
|
239
|
+
*/
|
|
240
|
+
matchesEngineCmdline: (cmdline: string) => boolean;
|
|
241
|
+
/** 平台注入(缺省 process.platform;测试注入 win32 验证不杀只删分支)。 */
|
|
242
|
+
platform?: NodeJS.Platform;
|
|
243
|
+
/** 杀执行器注入(缺省 POSIX 负 pid 组杀;测试红线:绝不真杀无关进程)。 */
|
|
244
|
+
killEngine?: (enginePid: number) => void;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 启动期扫同 id 全部 pidfile 并按三条件清扫。
|
|
249
|
+
*
|
|
250
|
+
* 逐文件判定(顺序即优先级):
|
|
251
|
+
* 坏内容 → 删;自己 → 跳过;宿主 pid 存活 → 保留跳过(活实例或 pid 复用疑似);
|
|
252
|
+
* 宿主 pid 死 → win32:删(不杀);posix:引擎 pid 死 → 删;存活 → cmdline 校验
|
|
253
|
+
* 不符 → 删;engineStartTime 不匹配 → 删(pid 复用);全过 → 组杀 + 删。
|
|
254
|
+
* ps 失败 / 探测非 ESRCH 错误 → 保守跳过 + 保留。
|
|
255
|
+
*/
|
|
256
|
+
export function sweepStalePidfiles(opts: SweepStalePidfilesOptions): PidfileSweepResult {
|
|
257
|
+
const result: PidfileSweepResult = { killed: [], removed: [], skipped: [] };
|
|
258
|
+
const platform = opts.platform ?? process.platform;
|
|
259
|
+
const dir = resolveEngineDir(opts.engineDataDir, opts.engineId);
|
|
260
|
+
|
|
261
|
+
let files: string[];
|
|
262
|
+
try {
|
|
263
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".pid"));
|
|
264
|
+
} catch {
|
|
265
|
+
return result; // 目录不存在 = 首次启动,无可清扫
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
sweepOnePidfile(join(dir, file), file, opts, platform, result);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 单个 pidfile 的前置检查(顺序即优先级):坏内容 → 删;自己 → 跳过;宿主 pid
|
|
277
|
+
* 探测不确定/存活 → 保守跳过 + 保留(活实例或宿主 pid 复用疑似)。宿主已死才进
|
|
278
|
+
* 陈旧判定链(reapDeadHostEngine)。
|
|
279
|
+
*/
|
|
280
|
+
function sweepOnePidfile(
|
|
281
|
+
path: string,
|
|
282
|
+
file: string,
|
|
283
|
+
opts: SweepStalePidfilesOptions,
|
|
284
|
+
platform: NodeJS.Platform,
|
|
285
|
+
result: PidfileSweepResult,
|
|
286
|
+
): void {
|
|
287
|
+
const content = readPidfile(path);
|
|
288
|
+
if (content === undefined) {
|
|
289
|
+
removePidfile(path);
|
|
290
|
+
result.removed.push({ file, reason: "unreadable-or-corrupt" });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (content.hostPid === opts.currentHostPid) {
|
|
294
|
+
result.skipped.push({ file, reason: "own-pidfile" });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const hostAlive = isProcessAlive(content.hostPid);
|
|
299
|
+
if (hostAlive === undefined) {
|
|
300
|
+
result.skipped.push({ file, reason: "host-pid-probe-uncertain" });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (hostAlive) {
|
|
304
|
+
// 宿主 pid 存活:对方宿主实例还活着(或宿主 pid 被复用疑似)——保守跳过 +
|
|
305
|
+
// 保留文件(R9-3:宿主 pid 复用误判「已死」方向同样落这里保住)。
|
|
306
|
+
result.skipped.push({ file, reason: "host-pid-alive" });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
reapDeadHostEngine(path, file, content, opts, platform, result);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 宿主已死后的陈旧判定链(pidfile 必陈旧):win32 不杀只删;POSIX 按引擎 pid
|
|
315
|
+
* 存活 → cmdline 身份 → engineStartTime 三条件逐级校验,任一不确定保守跳过 +
|
|
316
|
+
* 保留,判定不成立删文件,全过 → 组杀 + 删。
|
|
317
|
+
*/
|
|
318
|
+
function reapDeadHostEngine(
|
|
319
|
+
path: string,
|
|
320
|
+
file: string,
|
|
321
|
+
content: EnginePidfileContent,
|
|
322
|
+
opts: SweepStalePidfilesOptions,
|
|
323
|
+
platform: NodeJS.Platform,
|
|
324
|
+
result: PidfileSweepResult,
|
|
325
|
+
): void {
|
|
326
|
+
// 宿主已死,pidfile 必陈旧。Windows:无可移植启动时间判据 → 不杀只删。
|
|
327
|
+
if (platform === "win32") {
|
|
328
|
+
removePidfile(path);
|
|
329
|
+
result.removed.push({ file, reason: "stale (host dead); win32 never kills" });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const engineAlive = isProcessAlive(content.enginePid);
|
|
334
|
+
if (engineAlive === undefined) {
|
|
335
|
+
result.skipped.push({ file, reason: "engine-pid-probe-uncertain" });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!engineAlive) {
|
|
339
|
+
removePidfile(path);
|
|
340
|
+
result.removed.push({ file, reason: "engine-pid-dead" });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const cmdline = readProcessCmdline(content.enginePid);
|
|
345
|
+
if (cmdline === undefined) {
|
|
346
|
+
result.skipped.push({ file, reason: "engine-cmdline-probe-uncertain" });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!opts.matchesEngineCmdline(cmdline)) {
|
|
350
|
+
// pid 已被复用给无关进程:判定不成立 → 删文件、绝不动那个无辜进程。
|
|
351
|
+
removePidfile(path);
|
|
352
|
+
result.removed.push({ file, reason: "cmdline-mismatch (pid reused by unrelated process)" });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const startTime = readProcessStartTime(content.enginePid);
|
|
357
|
+
if (startTime === undefined) {
|
|
358
|
+
result.skipped.push({ file, reason: "engine-start-time-probe-uncertain" });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (content.engineStartTime === null || content.engineStartTime !== startTime) {
|
|
362
|
+
// 同 cmdline 的 pid 复用(R9-3b):启动时间不同 → 判定不成立 → 删文件不杀。
|
|
363
|
+
removePidfile(path);
|
|
364
|
+
result.removed.push({ file, reason: "engine-start-time-mismatch (pid reused)" });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 三条件全立:确是宿主崩溃残留的引擎实例 → 组杀 + 删文件。
|
|
369
|
+
const killEngine =
|
|
370
|
+
opts.killEngine ?? defaultKillEngineProcessGroup;
|
|
371
|
+
killEngine(content.enginePid);
|
|
372
|
+
removePidfile(path);
|
|
373
|
+
result.killed.push(content.enginePid);
|
|
374
|
+
result.removed.push({ file, reason: "swept after kill (stale orphan)" });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 缺省杀执行器:POSIX 负 pid 组杀(引擎 CLI 由 EngineClient detached:true spawn = 组长),失败回落单杀。 */
|
|
378
|
+
function defaultKillEngineProcessGroup(enginePid: number): void {
|
|
379
|
+
try {
|
|
380
|
+
process.kill(-enginePid, "SIGKILL");
|
|
381
|
+
return;
|
|
382
|
+
} catch (groupErr) {
|
|
383
|
+
// 非组长 / 已死——回落单杀再兜一次。
|
|
384
|
+
logger.debug(`[pid-file] process-group kill(-${enginePid}) failed, falling back to single kill: ${groupErr instanceof Error ? groupErr.message : String(groupErr)}`);
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
process.kill(enginePid, "SIGKILL");
|
|
388
|
+
} catch (err) {
|
|
389
|
+
// 已死 = 预期终态。
|
|
390
|
+
logger.debug(`[pid-file] single kill(${enginePid}) failed (already dead = expected): ${err instanceof Error ? err.message : String(err)}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/execution/engine/client/reaper.ts
|
|
2
|
+
//
|
|
3
|
+
// 进程组收割(EngineClient 内部模块,从 engine-client.ts 拆出——max-lines 纪律)。
|
|
4
|
+
//
|
|
5
|
+
// 收割单一机制(impl-plan §2.2,v6 已删按 pid 补杀):POSIX 负 pid 组杀
|
|
6
|
+
// (SIGTERM → grace → SIGKILL 升级);Windows taskkill /PID <pid> /T /F(按 pid 树)。
|
|
7
|
+
// 「零残留」断言范围 = 一代子进程 + 组内后代(引擎自身 detached 后代不覆盖,
|
|
8
|
+
// §7.2 R9-1 / 设计 §3.9 已接受代价)。
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import type { ChildProcess } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
import { getLogger } from "@zhushanwen/subagent-engine-sdk";
|
|
14
|
+
|
|
15
|
+
const logger = getLogger("subagents");
|
|
16
|
+
|
|
17
|
+
/** 进程组 SIGTERM 优雅窗口(对齐 SDK kill-chain DEFAULT_KILL_GRACE_MS)。 */
|
|
18
|
+
const KILL_GRACE_MS = 5_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 组杀引擎 CLI:POSIX `process.kill(-pid)` 负 pid 组杀(引擎由 detached:true spawn =
|
|
22
|
+
* 组长,组内含引擎 + 其任务子进程 = 一代子进程 + 组内后代);Windows taskkill /T /F。
|
|
23
|
+
*/
|
|
24
|
+
export function killProcessTree(pid: number, reason: string, opts: { engineId: string }): void {
|
|
25
|
+
if (process.platform === "win32") {
|
|
26
|
+
const r = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { timeout: 10_000 });
|
|
27
|
+
if (r.error || r.status !== 0) {
|
|
28
|
+
logger.warn(
|
|
29
|
+
`[engine-client:${opts.engineId}] taskkill failed for pid ${pid} (${reason}): ${
|
|
30
|
+
r.error?.message ?? `exit ${r.status}`
|
|
31
|
+
}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const signalGroup = (signal: NodeJS.Signals): boolean => {
|
|
37
|
+
try {
|
|
38
|
+
process.kill(-pid, signal);
|
|
39
|
+
return true;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
logger.debug(
|
|
42
|
+
`[engine-client:${opts.engineId}] process.kill(-${pid}, ${signal}) failed: ${
|
|
43
|
+
err instanceof Error ? err.message : String(err)
|
|
44
|
+
}`,
|
|
45
|
+
);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
if (!signalGroup("SIGTERM")) {
|
|
50
|
+
// 组不存在(组长已死)→ 单进程兜底。
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, "SIGTERM");
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.debug(
|
|
55
|
+
`[engine-client:${opts.engineId}] single SIGTERM to ${pid} failed (already dead = expected): ${
|
|
56
|
+
err instanceof Error ? err.message : String(err)
|
|
57
|
+
}`,
|
|
58
|
+
);
|
|
59
|
+
return; // 已死
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const escalation = setTimeout(
|
|
63
|
+
() => {
|
|
64
|
+
if (!signalGroup("SIGKILL")) {
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, "SIGKILL");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// 已死 = 预期终态。
|
|
69
|
+
logger.debug(
|
|
70
|
+
`[engine-client:${opts.engineId}] SIGKILL to ${pid} failed (already dead = expected): ${
|
|
71
|
+
err instanceof Error ? err.message : String(err)
|
|
72
|
+
}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
KILL_GRACE_MS,
|
|
78
|
+
);
|
|
79
|
+
escalation.unref();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 等 child 退出(有界;超时不再等待——SIGKILL 后进程必死,收尸由 OS 完成)。 */
|
|
83
|
+
export function waitForChildExit(child: ChildProcess, timeoutMs: number): Promise<void> {
|
|
84
|
+
return new Promise<void>((resolve) => {
|
|
85
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
86
|
+
resolve();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const timer = setTimeout(() => resolve(), timeoutMs);
|
|
90
|
+
child.once("exit", () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|