@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,661 @@
|
|
|
1
|
+
// src/execution/round-supervisor/service-binding.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [MF-1 round1 test-coverage] service-binding 绑定面直测(审查基线覆盖 23.3%)。
|
|
4
|
+
//
|
|
5
|
+
// 覆盖面:
|
|
6
|
+
// 1. supervisorGiveUp(C-proc-13 注销发射点④「监督器放弃」唯一执行者):
|
|
7
|
+
// - 内存态 watchdog-expired:CAS 终态化 + finalizeClosed failed result +
|
|
8
|
+
// 终止通知(「safe to re-dispatch」用户语义,经 steer 通道);
|
|
9
|
+
// - CAS 抢锁失败跳过(他人已终态化时不重复放弃——同步链内不可达的防御
|
|
10
|
+
// 分支,替身 getMutable 受控注入驱动契约);
|
|
11
|
+
// - finalizeClosed 异常 best-effort(不向上抛、终止通知仍发);
|
|
12
|
+
// - superseded:errorText 带替代 id、不发终止通知;
|
|
13
|
+
// - 磁盘态(boot 重认领后看门狗到期):writeFinalized sidecar 落盘 +
|
|
14
|
+
// reportSubagentRecord 终态 entry(closed/gc/endedAt/error)、无 pi 通知;
|
|
15
|
+
// - 磁盘态无 sessionFile / writeFinalized 抛错(sidecar catch)/ 双 miss 收尾
|
|
16
|
+
// 竞争 / reportSubagentRecord 抛错(entry catch,须先经 boot 重认领武装
|
|
17
|
+
// 看门狗——无候选则 giveUp 不执行,用例空转)。
|
|
18
|
+
// 2. runPendingReconcileSweepForService 的 lookupRecordState 闭包 subagent 判据
|
|
19
|
+
// (workflow 判据回归钉在 workflow-state-root.test.ts,此处补 subagent 两方向:
|
|
20
|
+
// 活跃 record 不被 sweep 注销 + 终态/missing 差集补发注销)。
|
|
21
|
+
// 3. createRoundSupervisorForService 通知装配(merged failure / decision guidance
|
|
22
|
+
// + 豁免声明 / replaced notice 全走 steer 通道;candidates 接线;record 视图
|
|
23
|
+
// 磁盘兜底投影)。
|
|
24
|
+
//
|
|
25
|
+
// 测试纪律:fake timers(watchdog 推进);替身 RecordStore/PiLike 驱动(不触真实
|
|
26
|
+
// 数据目录);sidecar/register 写点全部 mkdtempSync 自建自删;PI_CODING_AGENT_DIR
|
|
27
|
+
// stub 指向 tmp(sweep 内 FileRunStore 构造读侧解析不探测真实 ~/.pi/agent)。
|
|
28
|
+
|
|
29
|
+
import * as fs from "node:fs";
|
|
30
|
+
import * as os from "node:os";
|
|
31
|
+
import * as path from "node:path";
|
|
32
|
+
|
|
33
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
34
|
+
|
|
35
|
+
import { COLD_LOOKUP_SCAN_LIMIT } from "../cold-resurrect.ts";
|
|
36
|
+
import * as finalizedMarker from "../finalized-marker.ts";
|
|
37
|
+
import type { PiLike } from "../notify-host.ts";
|
|
38
|
+
import type { RecordStore } from "../record-store.ts";
|
|
39
|
+
import type { ExecutionRecord, SubagentRecord } from "../types.ts";
|
|
40
|
+
import { RoundSupervisor, ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS, type SupervisorCandidateRecord } from "./index.ts";
|
|
41
|
+
import {
|
|
42
|
+
createRoundSupervisorForService,
|
|
43
|
+
runPendingReconcileSweepForService,
|
|
44
|
+
type RoundSupervisorBinding,
|
|
45
|
+
} from "./service-binding.ts";
|
|
46
|
+
|
|
47
|
+
// writeFinalized 实装自吞 IO 错(内部 try/catch,finalized-marker.ts),give-up
|
|
48
|
+
// sidecar 的 catch(service-binding.ts:164)是防御分支——真实 ENOENT 无法驱动,
|
|
49
|
+
// 仅本文件的 sidecar 抛错用例经模块替身注入抛错;默认委托真实实现,相邻用例行为
|
|
50
|
+
// 不变(vi.spyOn 对跨模块具名导入绑定不可拦截,必须走 vi.mock)。
|
|
51
|
+
vi.mock("../finalized-marker.ts", async (importOriginal) => {
|
|
52
|
+
const actual = await importOriginal<typeof import("../finalized-marker.ts")>();
|
|
53
|
+
return { ...actual, writeFinalized: vi.fn(actual.writeFinalized) };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// 替身
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
type SentMessage = {
|
|
61
|
+
message: { customType: string; content: string; display: boolean };
|
|
62
|
+
options: { deliverAs?: string } | undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type TestPi = PiLike & {
|
|
66
|
+
sent: SentMessage[];
|
|
67
|
+
appended: Array<{ customType: string; data: unknown }>;
|
|
68
|
+
emitted: Array<{ channel: string; data: unknown }>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function makePi(): TestPi {
|
|
72
|
+
const sent: SentMessage[] = [];
|
|
73
|
+
const appended: Array<{ customType: string; data: unknown }> = [];
|
|
74
|
+
const emitted: Array<{ channel: string; data: unknown }> = [];
|
|
75
|
+
return {
|
|
76
|
+
sendMessage: vi.fn((message: SentMessage["message"], options?: SentMessage["options"]) => {
|
|
77
|
+
sent.push({ message, options });
|
|
78
|
+
}),
|
|
79
|
+
appendEntry: vi.fn((customType: string, data?: unknown) => {
|
|
80
|
+
appended.push({ customType, data });
|
|
81
|
+
}),
|
|
82
|
+
events: {
|
|
83
|
+
emit: vi.fn((channel: string, data: unknown) => {
|
|
84
|
+
emitted.push({ channel, data });
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
sent,
|
|
88
|
+
appended,
|
|
89
|
+
emitted,
|
|
90
|
+
} as unknown as TestPi;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface StoreHarness {
|
|
94
|
+
store: RecordStore;
|
|
95
|
+
memory: Map<string, ExecutionRecord>;
|
|
96
|
+
disk: Map<string, SubagentRecord>;
|
|
97
|
+
/** collectRecords 返回的候选(替身投影;默认从 memory 生成)。 */
|
|
98
|
+
collectResult: SupervisorCandidateRecord[];
|
|
99
|
+
collectCalls: Array<{ limit: number; status: string; rootFilter: string | undefined }>;
|
|
100
|
+
reported: SubagentRecord[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** duck-typed RecordStore 替身(service-binding 只消费 getMutable / findLightById /
|
|
104
|
+
* collectRecords / reportSubagentRecord 四个读写点)。 */
|
|
105
|
+
function makeStore(): StoreHarness {
|
|
106
|
+
const memory = new Map<string, ExecutionRecord>();
|
|
107
|
+
const disk = new Map<string, SubagentRecord>();
|
|
108
|
+
const collectResult: SupervisorCandidateRecord[] = [];
|
|
109
|
+
const collectCalls: StoreHarness["collectCalls"] = [];
|
|
110
|
+
const reported: SubagentRecord[] = [];
|
|
111
|
+
const store = {
|
|
112
|
+
getMutable: (id: string) => memory.get(id),
|
|
113
|
+
findLightById: (id: string) => disk.get(id),
|
|
114
|
+
collectRecords: (limit: number, status: string, rootFilter: string | undefined) => {
|
|
115
|
+
collectCalls.push({ limit, status, rootFilter });
|
|
116
|
+
return collectResult as never;
|
|
117
|
+
},
|
|
118
|
+
reportSubagentRecord: (record: SubagentRecord) => {
|
|
119
|
+
reported.push(record);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
return { store: store as unknown as RecordStore, memory, disk, collectResult, collectCalls, reported };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface BindingHarness {
|
|
126
|
+
binding: RoundSupervisorBinding;
|
|
127
|
+
store: StoreHarness;
|
|
128
|
+
pi: ReturnType<typeof makePi> | null;
|
|
129
|
+
finalizeClosed: ReturnType<typeof vi.fn>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function makeBinding(overrides: {
|
|
133
|
+
store?: StoreHarness;
|
|
134
|
+
pi?: ReturnType<typeof makePi> | null;
|
|
135
|
+
rootId?: string | null;
|
|
136
|
+
sessionFile?: string;
|
|
137
|
+
} = {}): BindingHarness {
|
|
138
|
+
const storeHarness = overrides.store ?? makeStore();
|
|
139
|
+
const pi = overrides.pi === undefined ? makePi() : overrides.pi;
|
|
140
|
+
const finalizeClosed = vi.fn(async () => {});
|
|
141
|
+
const binding: RoundSupervisorBinding = {
|
|
142
|
+
getStore: () => storeHarness.store,
|
|
143
|
+
getPi: () => pi,
|
|
144
|
+
getSessionRootId: () => (overrides.rootId === undefined ? "root-1" : overrides.rootId),
|
|
145
|
+
getMainSessionFile: () => overrides.sessionFile,
|
|
146
|
+
finalizeClosed: finalizeClosed as unknown as (r: ExecutionRecord, res: unknown) => Promise<void>,
|
|
147
|
+
};
|
|
148
|
+
return { binding, store: storeHarness, pi, finalizeClosed };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 内存 ExecutionRecord 替身(字段访问面:id/agent/slug/status/resumable/result/
|
|
152
|
+
* chatMode/rootSessionId/startedAt/closedReason/turnCount)。 */
|
|
153
|
+
function makeRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
|
|
154
|
+
return {
|
|
155
|
+
id: "bg-1",
|
|
156
|
+
agent: "worker",
|
|
157
|
+
model: "m",
|
|
158
|
+
mode: "background",
|
|
159
|
+
task: "t",
|
|
160
|
+
slug: "fix-bug",
|
|
161
|
+
startedAt: Date.now() - 1000,
|
|
162
|
+
status: "running",
|
|
163
|
+
resumable: true,
|
|
164
|
+
rootSessionId: "root-1",
|
|
165
|
+
turnCount: 3,
|
|
166
|
+
...overrides,
|
|
167
|
+
} as ExecutionRecord;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 磁盘 SubagentRecord 替身(findLightById 返回形态,最小字段集)。 */
|
|
171
|
+
function makeDiskRecord(sessionFile: string | undefined, overrides: Partial<SubagentRecord> = {}): SubagentRecord {
|
|
172
|
+
return {
|
|
173
|
+
id: "bg-disk",
|
|
174
|
+
agent: "worker",
|
|
175
|
+
task: "t",
|
|
176
|
+
slug: "fix-bug",
|
|
177
|
+
status: "running",
|
|
178
|
+
mode: "background",
|
|
179
|
+
startedAt: Date.now() - 2000,
|
|
180
|
+
rootSessionId: "root-1",
|
|
181
|
+
parentRecordId: undefined,
|
|
182
|
+
depth: 0,
|
|
183
|
+
endedAt: undefined,
|
|
184
|
+
turns: 1,
|
|
185
|
+
totalTokens: 0,
|
|
186
|
+
model: "m",
|
|
187
|
+
eventLog: [],
|
|
188
|
+
displayItems: [],
|
|
189
|
+
resumable: true,
|
|
190
|
+
sessionFile,
|
|
191
|
+
...overrides,
|
|
192
|
+
} as unknown as SubagentRecord;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ============================================================
|
|
196
|
+
// 测试
|
|
197
|
+
// ============================================================
|
|
198
|
+
|
|
199
|
+
/** 终止通知文案锚(supervisorGiveUp watchdog-expired 分支独有;guidance/replaced/merged 均不含)。 */
|
|
200
|
+
const TERMINATION_NOTICE_MARK = "terminated after the decision watchdog";
|
|
201
|
+
|
|
202
|
+
let tmpDir: string;
|
|
203
|
+
let sessionFile: string;
|
|
204
|
+
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "svc-binding-"));
|
|
207
|
+
sessionFile = path.join(tmpDir, "main-session.jsonl");
|
|
208
|
+
vi.useFakeTimers();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
vi.useRealTimers();
|
|
213
|
+
vi.unstubAllEnvs();
|
|
214
|
+
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/** 纳管 + 看门狗到期 → giveUp("watchdog-expired")(异步 giveUp 编排排空微任务)。 */
|
|
218
|
+
async function adoptThenExpire(supervisor: RoundSupervisor, record: ExecutionRecord): Promise<void> {
|
|
219
|
+
supervisor.adoptOnProcessDeath(record, "engine crashed");
|
|
220
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
describe("supervisorGiveUp 内存态(watchdog-expired / superseded / CAS)", () => {
|
|
224
|
+
it("watchdog-expired:CAS 终态化 + finalizeClosed 收 failed result + 终止通知经 steer(safe to re-dispatch 语义)", async () => {
|
|
225
|
+
const h = makeBinding({ sessionFile });
|
|
226
|
+
h.store.memory.set("bg-1", makeRecord());
|
|
227
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
228
|
+
|
|
229
|
+
await adoptThenExpire(supervisor, makeRecord());
|
|
230
|
+
|
|
231
|
+
// CAS 终态化已落(closed + gc),finalizeClosed 收失败语义 result
|
|
232
|
+
expect(h.store.memory.get("bg-1")?.status).toBe("closed");
|
|
233
|
+
expect(h.store.memory.get("bg-1")?.closedReason).toBe("gc");
|
|
234
|
+
expect(h.finalizeClosed).toHaveBeenCalledTimes(1);
|
|
235
|
+
const [record, result] = (h.finalizeClosed.mock.calls[0] ?? []) as [ExecutionRecord, Record<string, unknown>];
|
|
236
|
+
expect(record.id).toBe("bg-1");
|
|
237
|
+
expect(result).toMatchObject({
|
|
238
|
+
text: "",
|
|
239
|
+
turns: 3,
|
|
240
|
+
success: false,
|
|
241
|
+
sessionId: "bg-1",
|
|
242
|
+
toolCalls: [],
|
|
243
|
+
durationMs: expect.any(Number),
|
|
244
|
+
error: expect.stringContaining("safe to re-dispatch"),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 终止通知:steer 通道、customType/display 契约、agent/slug 用户语义
|
|
248
|
+
// (sent = [merged failure, decision guidance, termination] 三条,钉最后一条)
|
|
249
|
+
expect(h.pi?.sent).toHaveLength(3);
|
|
250
|
+
const sent = h.pi!.sent[2]!;
|
|
251
|
+
expect(sent.options).toEqual({ deliverAs: "steer" });
|
|
252
|
+
expect(sent.message.customType).toBe("subagent-round-supervisor");
|
|
253
|
+
expect(sent.message.display).toBe(false);
|
|
254
|
+
expect(sent.message.content).toContain('Subagent "worker" (fix-bug)');
|
|
255
|
+
expect(sent.message.content).toContain("decision watchdog");
|
|
256
|
+
expect(sent.message.content).toContain("you can safely re-dispatch the task");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("CAS 抢锁失败(视图检查后 record 已被 cancel 终态化)→ 不 finalizeClosed、不通知、不重复放弃", async () => {
|
|
260
|
+
const h = makeBinding({ sessionFile });
|
|
261
|
+
const rec = makeRecord();
|
|
262
|
+
h.store.memory.set("bg-1", rec);
|
|
263
|
+
// 同步链内 view 检查与 CAS 之间不可插入并发终态化(防御分支)——替身受控注入:
|
|
264
|
+
// 第一次 getMutable(watchdog 回调的视图检查)返回 running,其后(giveUp 的
|
|
265
|
+
// memory 读取)返回已被 cancel 抢先终态化的同一 record。
|
|
266
|
+
let viewChecked = false;
|
|
267
|
+
h.store.store.getMutable = (id: string) => {
|
|
268
|
+
if (!viewChecked) {
|
|
269
|
+
viewChecked = true;
|
|
270
|
+
return rec;
|
|
271
|
+
}
|
|
272
|
+
return { ...rec, status: "closed", closedReason: "cancelled" } as ExecutionRecord;
|
|
273
|
+
};
|
|
274
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
275
|
+
|
|
276
|
+
await adoptThenExpire(supervisor, rec);
|
|
277
|
+
|
|
278
|
+
expect(h.finalizeClosed).not.toHaveBeenCalled();
|
|
279
|
+
// 纳管期通知(merged + guidance)照常,但终止通知不发生(不重复放弃)
|
|
280
|
+
expect(h.pi?.sent).toHaveLength(2);
|
|
281
|
+
expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
|
|
282
|
+
expect(h.store.reported).toHaveLength(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("finalizeClosed 抛错 → best-effort 吞掉不向上抛,终止通知仍发", async () => {
|
|
286
|
+
const h = makeBinding({ sessionFile });
|
|
287
|
+
h.finalizeClosed.mockImplementation(async () => {
|
|
288
|
+
throw new Error("finalize exploded");
|
|
289
|
+
});
|
|
290
|
+
h.store.memory.set("bg-1", makeRecord());
|
|
291
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
292
|
+
|
|
293
|
+
await expect(adoptThenExpire(supervisor, makeRecord())).resolves.toBeUndefined();
|
|
294
|
+
|
|
295
|
+
expect(h.finalizeClosed).toHaveBeenCalledTimes(1);
|
|
296
|
+
expect(h.pi?.sent).toHaveLength(3);
|
|
297
|
+
expect(h.pi?.sent[2]?.message.content).toContain("safely re-dispatch");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("superseded(高置信替代):errorText 带替代 id,不发终止通知(替代通知由监督器先行)", async () => {
|
|
301
|
+
const h = makeBinding({ sessionFile });
|
|
302
|
+
h.store.memory.set("bg-1", makeRecord());
|
|
303
|
+
h.store.collectResult.push({ id: "bg-2", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 1000 });
|
|
304
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
305
|
+
|
|
306
|
+
await adoptThenExpire(supervisor, makeRecord());
|
|
307
|
+
|
|
308
|
+
const [record, result] = (h.finalizeClosed.mock.calls[0] ?? []) as [ExecutionRecord, Record<string, unknown>];
|
|
309
|
+
expect(record.id).toBe("bg-1");
|
|
310
|
+
expect(result.error).toBe("round supervisor: superseded by replacement task bg-2");
|
|
311
|
+
// 终止通知不发;sent = [merged failure, replaced notice] 两条
|
|
312
|
+
expect(h.pi?.sent).toHaveLength(2);
|
|
313
|
+
expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
|
|
314
|
+
expect(h.pi?.sent[1]?.message.content).toContain("was already replaced by your new");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("pi 缺席(dispose 后 getPi null)→ 终态化照常、通知静默丢弃、不炸", async () => {
|
|
318
|
+
const h = makeBinding({ sessionFile, pi: null });
|
|
319
|
+
h.store.memory.set("bg-1", makeRecord());
|
|
320
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
321
|
+
|
|
322
|
+
await expect(adoptThenExpire(supervisor, makeRecord())).resolves.toBeUndefined();
|
|
323
|
+
|
|
324
|
+
expect(h.finalizeClosed).toHaveBeenCalledTimes(1);
|
|
325
|
+
expect(h.store.memory.get("bg-1")?.status).toBe("closed");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("supervisorGiveUp 磁盘态(boot 重认领后看门狗到期)", () => {
|
|
330
|
+
function makeDiskBinding(diskRecord: SubagentRecord): BindingHarness {
|
|
331
|
+
const h = makeBinding({ sessionFile });
|
|
332
|
+
h.store.disk.set(diskRecord.id, diskRecord);
|
|
333
|
+
// boot 分区候选(真实 collectRecords 的 running 投影形态)
|
|
334
|
+
h.store.collectResult.push({
|
|
335
|
+
id: diskRecord.id,
|
|
336
|
+
rootSessionId: diskRecord.rootSessionId,
|
|
337
|
+
agent: diskRecord.agent,
|
|
338
|
+
slug: diskRecord.slug,
|
|
339
|
+
startedAt: diskRecord.startedAt,
|
|
340
|
+
});
|
|
341
|
+
return h;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
it("writeFinalized sidecar 落盘 + reportSubagentRecord 终态 entry(closed/gc/endedAt/error);磁盘态不发终止通知", async () => {
|
|
345
|
+
const diskRecord = makeDiskRecord(sessionFile);
|
|
346
|
+
const h = makeDiskBinding(diskRecord);
|
|
347
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
348
|
+
|
|
349
|
+
// 纳管经磁盘兜底视图(内存 Map 空)——boot 分区重认领形态
|
|
350
|
+
supervisor.bootPartition();
|
|
351
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
352
|
+
|
|
353
|
+
// sidecar:sessionFile 旁 .finalized 内容 = "gc";finalizeClosed 不适用(无内存 record)
|
|
354
|
+
expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
|
|
355
|
+
expect(h.finalizeClosed).not.toHaveBeenCalled();
|
|
356
|
+
|
|
357
|
+
// 终态 entry 落盘(reportSubagentRecord):磁盘 record + 终态语义位
|
|
358
|
+
expect(h.store.reported).toHaveLength(1);
|
|
359
|
+
const reported = h.store.reported[0]!;
|
|
360
|
+
expect(reported).toMatchObject({
|
|
361
|
+
id: "bg-disk",
|
|
362
|
+
status: "closed",
|
|
363
|
+
closedReason: "gc",
|
|
364
|
+
endedAt: expect.any(Number),
|
|
365
|
+
error: expect.stringContaining("safe to re-dispatch"),
|
|
366
|
+
});
|
|
367
|
+
expect(reported.error).not.toContain("superseded");
|
|
368
|
+
|
|
369
|
+
// 磁盘态分支无终止通知(sent 仅 boot 重认领的 decision guidance 一条)
|
|
370
|
+
expect(h.pi?.sent).toHaveLength(1);
|
|
371
|
+
expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("磁盘态无 sessionFile → 跳过 sidecar,仍 reportSubagentRecord 终态化", async () => {
|
|
375
|
+
const diskRecord = makeDiskRecord(undefined);
|
|
376
|
+
const h = makeDiskBinding(diskRecord);
|
|
377
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
378
|
+
|
|
379
|
+
supervisor.bootPartition();
|
|
380
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
381
|
+
|
|
382
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
|
|
383
|
+
expect(h.store.reported).toHaveLength(1);
|
|
384
|
+
expect(h.store.reported[0]?.status).toBe("closed");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("reportSubagentRecord 抛错 → best-effort 吞掉不向上抛,sidecar 照常落盘", async () => {
|
|
388
|
+
// 候选入 collectResult → boot 重认领(readopted)武装看门狗——没有这一步
|
|
389
|
+
// listCandidateRecords 返回空、giveUp 永不执行、entry catch 不可达(MF-R2-2)。
|
|
390
|
+
const diskRecord = makeDiskRecord(sessionFile);
|
|
391
|
+
const h = makeDiskBinding(diskRecord);
|
|
392
|
+
h.store.store.reportSubagentRecord = () => {
|
|
393
|
+
throw new Error("entry write exploded");
|
|
394
|
+
};
|
|
395
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
396
|
+
|
|
397
|
+
const { readopted } = supervisor.bootPartition();
|
|
398
|
+
expect(readopted).toEqual([diskRecord.id]);
|
|
399
|
+
// 裸 await:giveUp 编排若有逃逸异常会以 unhandled rejection 判红本用例
|
|
400
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
401
|
+
|
|
402
|
+
// sidecar 与终态 entry 是两段独立 best-effort:entry 抛错不回滚已落 sidecar
|
|
403
|
+
expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
|
|
404
|
+
// 终态 entry 写入抛错被 give-up entry catch 吞掉 → reported 空
|
|
405
|
+
expect(h.store.reported).toHaveLength(0);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("writeFinalized 抛错 → best-effort 吞掉,终态 entry 照常落盘", async () => {
|
|
409
|
+
const diskRecord = makeDiskRecord(path.join(tmpDir, "gone-dir", "s.jsonl"));
|
|
410
|
+
const h = makeDiskBinding(diskRecord);
|
|
411
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
412
|
+
// 模块替身注入抛错(见文件头 vi.mock 注释);finally 恢复委托真实实现。
|
|
413
|
+
const actual = await vi.importActual<typeof import("../finalized-marker.ts")>(
|
|
414
|
+
"../finalized-marker.ts",
|
|
415
|
+
);
|
|
416
|
+
const writeSpy = vi.mocked(finalizedMarker.writeFinalized);
|
|
417
|
+
writeSpy.mockImplementation(() => {
|
|
418
|
+
throw new Error("sidecar write exploded");
|
|
419
|
+
});
|
|
420
|
+
try {
|
|
421
|
+
supervisor.bootPartition();
|
|
422
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
423
|
+
} finally {
|
|
424
|
+
writeSpy.mockImplementation(actual.writeFinalized);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 替身确实被 giveUp 消费(否则异常被 writeFinalized 实装自吞,用例假绿)
|
|
428
|
+
expect(writeSpy).toHaveBeenCalled();
|
|
429
|
+
expect(fs.existsSync(`${diskRecord.sessionFile}.finalized`)).toBe(false);
|
|
430
|
+
expect(h.store.reported).toHaveLength(1);
|
|
431
|
+
expect(h.store.reported[0]).toMatchObject({ id: diskRecord.id, status: "closed", closedReason: "gc" });
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("giveUp 执行时 record 已被 archive(内存移除 + 磁盘无 light)→ 双 miss 直接 return,零副作用", async () => {
|
|
435
|
+
const h = makeBinding({ sessionFile });
|
|
436
|
+
const rec = makeRecord();
|
|
437
|
+
h.store.memory.set("bg-1", rec);
|
|
438
|
+
// 视图检查(内存 running)之后、giveUp 磁盘兜底之前被 archive 的收尾竞争替身投影
|
|
439
|
+
let viewChecked = false;
|
|
440
|
+
h.store.store.getMutable = () => {
|
|
441
|
+
if (!viewChecked) {
|
|
442
|
+
viewChecked = true;
|
|
443
|
+
return rec;
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
};
|
|
447
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
448
|
+
|
|
449
|
+
await adoptThenExpire(supervisor, rec);
|
|
450
|
+
|
|
451
|
+
expect(h.finalizeClosed).not.toHaveBeenCalled();
|
|
452
|
+
expect(h.pi?.sent).toHaveLength(2); // 纳管期通知照常,终止通知不发(零通知放弃)
|
|
453
|
+
expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
|
|
454
|
+
expect(h.store.reported).toHaveLength(0);
|
|
455
|
+
expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("runPendingReconcileSweepForService 的 subagent 判据(lookupRecordState 闭包)", () => {
|
|
460
|
+
/** sweep harness:PI_CODING_AGENT_DIR stub(sweep 内 FileRunStore 读侧解析不触真实目录)。 */
|
|
461
|
+
function setup(): { agentDir: string } {
|
|
462
|
+
const agentDir = path.join(tmpDir, "agent");
|
|
463
|
+
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
|
|
464
|
+
return { agentDir };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function writeRegister(id: string, type: string): void {
|
|
468
|
+
fs.writeFileSync(
|
|
469
|
+
sessionFile,
|
|
470
|
+
JSON.stringify({ customType: "pending:register", data: { id, type, name: id } }) + "\n",
|
|
471
|
+
"utf-8",
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
it("isChildProcess=true → 空跑(不读 store、不读 session 文件)", () => {
|
|
476
|
+
setup();
|
|
477
|
+
const h = makeBinding({ sessionFile });
|
|
478
|
+
const getMutableSpy = vi.spyOn(h.store.store, "getMutable");
|
|
479
|
+
runPendingReconcileSweepForService(h.binding, true);
|
|
480
|
+
expect(getMutableSpy).not.toHaveBeenCalled();
|
|
481
|
+
expect(h.pi?.appended ?? []).toHaveLength(0);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("getMainSessionFile undefined → 空跑(差集输入源缺席,store 零读取)", () => {
|
|
485
|
+
setup();
|
|
486
|
+
const h = makeBinding({});
|
|
487
|
+
const getMutableSpy = vi.spyOn(h.store.store, "getMutable");
|
|
488
|
+
runPendingReconcileSweepForService(h.binding, false);
|
|
489
|
+
expect(getMutableSpy).not.toHaveBeenCalled();
|
|
490
|
+
expect(h.pi?.appended ?? []).toHaveLength(0);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("活跃 subagent record(内存 running)→ 不补注销(误注销活跃 record 的回归钉)", () => {
|
|
494
|
+
setup();
|
|
495
|
+
writeRegister("bg-live", "subagent");
|
|
496
|
+
const h = makeBinding({ sessionFile });
|
|
497
|
+
h.store.memory.set("bg-live", makeRecord({ id: "bg-live" }));
|
|
498
|
+
runPendingReconcileSweepForService(h.binding, false);
|
|
499
|
+
expect(h.pi?.appended ?? []).toHaveLength(0);
|
|
500
|
+
expect(h.pi?.emitted ?? []).toHaveLength(0);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("活跃 subagent record(仅磁盘 running,boot 重认领形态)→ 不补注销", () => {
|
|
504
|
+
setup();
|
|
505
|
+
writeRegister("bg-disk", "subagent");
|
|
506
|
+
const h = makeBinding({ sessionFile });
|
|
507
|
+
h.store.disk.set("bg-disk", makeDiskRecord(sessionFile, { id: "bg-disk" }));
|
|
508
|
+
runPendingReconcileSweepForService(h.binding, false);
|
|
509
|
+
expect(h.pi?.appended ?? []).toHaveLength(0);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("终态 subagent record(内存 closed,closedReason 有值)→ 差集补发注销(reason 直传 closedReason)+ 尽力 emit", () => {
|
|
513
|
+
setup();
|
|
514
|
+
writeRegister("bg-done", "subagent");
|
|
515
|
+
const h = makeBinding({ sessionFile });
|
|
516
|
+
h.store.memory.set("bg-done", makeRecord({ id: "bg-done", status: "closed", closedReason: "cancelled" }));
|
|
517
|
+
runPendingReconcileSweepForService(h.binding, false);
|
|
518
|
+
expect(h.pi?.appended).toEqual([
|
|
519
|
+
{ customType: "pending:unregister", data: { id: "bg-done", reason: "cancelled", status: "cancelled" } },
|
|
520
|
+
]);
|
|
521
|
+
expect(h.pi?.emitted).toEqual([{ channel: "pending:unregister", data: { id: "bg-done", reason: "cancelled" } }]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("终态 subagent record(磁盘 closed,closedReason 缺失)→ 补发注销 reason=completed 兜底", () => {
|
|
525
|
+
setup();
|
|
526
|
+
writeRegister("bg-old", "subagent");
|
|
527
|
+
const h = makeBinding({ sessionFile });
|
|
528
|
+
h.store.disk.set(
|
|
529
|
+
"bg-old",
|
|
530
|
+
makeDiskRecord(sessionFile, { id: "bg-old", status: "closed", resumable: false }),
|
|
531
|
+
);
|
|
532
|
+
runPendingReconcileSweepForService(h.binding, false);
|
|
533
|
+
expect(h.pi?.appended).toEqual([
|
|
534
|
+
{ customType: "pending:unregister", data: { id: "bg-old", reason: "completed", status: "completed" } },
|
|
535
|
+
]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("record 已归档/不存在(双 miss)→ 视同终态补注销(reason=expired)", () => {
|
|
539
|
+
setup();
|
|
540
|
+
writeRegister("bg-gone", "subagent");
|
|
541
|
+
const h = makeBinding({ sessionFile });
|
|
542
|
+
runPendingReconcileSweepForService(h.binding, false);
|
|
543
|
+
expect(h.pi?.appended).toEqual([
|
|
544
|
+
{ customType: "pending:unregister", data: { id: "bg-gone", reason: "expired", status: "expired" } },
|
|
545
|
+
]);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("pi 缺席(getPi null)→ 只判不写,不炸", () => {
|
|
549
|
+
setup();
|
|
550
|
+
writeRegister("bg-gone", "subagent");
|
|
551
|
+
const h = makeBinding({ sessionFile, pi: null });
|
|
552
|
+
expect(() => runPendingReconcileSweepForService(h.binding, false)).not.toThrow();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("getStore 抛错(store 未初始化形态)→ best-effort 吞掉不向上抛", () => {
|
|
556
|
+
setup();
|
|
557
|
+
// 差集非空是前置:无 register entry 时 collectActiveRegisterEntries 返回空、
|
|
558
|
+
// 循环零次、lookupRecordState(内含 getStore)不可达,用例空转(MF-R2-2)。
|
|
559
|
+
writeRegister("bg-gone", "subagent");
|
|
560
|
+
const h = makeBinding({ sessionFile });
|
|
561
|
+
let storeRequested = 0;
|
|
562
|
+
h.binding.getStore = () => {
|
|
563
|
+
storeRequested += 1;
|
|
564
|
+
throw new Error("store exploded");
|
|
565
|
+
};
|
|
566
|
+
expect(() => runPendingReconcileSweepForService(h.binding, false)).not.toThrow();
|
|
567
|
+
// 空转守卫:getStore 未被触达 = 判据链路没跑,异常根本没进 sweep 外层 catch
|
|
568
|
+
expect(storeRequested).toBeGreaterThanOrEqual(1);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe("createRoundSupervisorForService 通知装配", () => {
|
|
573
|
+
it("merged failure notice:死亡纳管单条通知(failed 如实 + 已接管契约)经 steer", () => {
|
|
574
|
+
const h = makeBinding({ sessionFile });
|
|
575
|
+
h.store.memory.set("bg-1", makeRecord());
|
|
576
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
577
|
+
|
|
578
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
|
|
579
|
+
|
|
580
|
+
const first = h.pi?.sent[0]!;
|
|
581
|
+
expect(first.options).toEqual({ deliverAs: "steer" });
|
|
582
|
+
expect(first.message.customType).toBe("subagent-round-supervisor");
|
|
583
|
+
expect(first.message.display).toBe(false);
|
|
584
|
+
expect(first.message.content).toContain('Subagent "worker" (fix-bug)');
|
|
585
|
+
expect(first.message.content).toContain("failed: the engine/child process driving it died (engine crashed)");
|
|
586
|
+
expect(first.message.content).toContain("NOT completed");
|
|
587
|
+
expect(first.message.content).toContain("Do NOT re-dispatch this task before that guidance arrives");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("decision guidance:无替代命中 → 指引不带豁免声明;slug 缺失回落 record id", () => {
|
|
591
|
+
const h = makeBinding({ sessionFile });
|
|
592
|
+
h.store.memory.set("bg-1", makeRecord({ slug: "" }));
|
|
593
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
594
|
+
|
|
595
|
+
supervisor.adoptOnProcessDeath(makeRecord({ slug: "" }), "engine crashed");
|
|
596
|
+
|
|
597
|
+
const guidance = h.pi?.sent[1]!;
|
|
598
|
+
expect(guidance.message.content).toContain('Subagent "worker" (bg-1, id=bg-1) died mid-task');
|
|
599
|
+
expect(guidance.message.content).toContain("Decide now");
|
|
600
|
+
expect(guidance.message.content).toContain("it will be terminated automatically when the decision watchdog expires");
|
|
601
|
+
expect(guidance.message.content).not.toContain("If you have already re-dispatched");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("低置信对账(仅 agent 同名)→ 指引自带豁免声明文案", () => {
|
|
605
|
+
const h = makeBinding({ sessionFile });
|
|
606
|
+
h.store.memory.set("bg-1", makeRecord());
|
|
607
|
+
h.store.collectResult.push({ id: "bg-2", rootSessionId: "root-1", agent: "worker", slug: "other-task", startedAt: Date.now() - 1000 });
|
|
608
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
609
|
+
|
|
610
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
|
|
611
|
+
|
|
612
|
+
const guidance = h.pi?.sent[1]!;
|
|
613
|
+
expect(guidance.message.content).toContain(
|
|
614
|
+
"If you have already re-dispatched or no longer need this task, ignore this guidance",
|
|
615
|
+
);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("高置信替代 → sendReplacedNotice 文案(原任务终止、无需 resume)", () => {
|
|
619
|
+
const h = makeBinding({ sessionFile });
|
|
620
|
+
h.store.memory.set("bg-1", makeRecord());
|
|
621
|
+
h.store.collectResult.push({ id: "bg-2", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 1000 });
|
|
622
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
623
|
+
|
|
624
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
|
|
625
|
+
|
|
626
|
+
const notice = h.pi?.sent[1]!;
|
|
627
|
+
expect(notice.message.content).toContain("was already replaced by your new task bg-2");
|
|
628
|
+
expect(notice.message.content).toContain("no resume is needed");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("candidates 接线:collectRecords 以 (SCAN_LIMIT, running, rootId) 调用;rootId null → undefined filter", () => {
|
|
632
|
+
const h = makeBinding({ sessionFile, rootId: "root-1" });
|
|
633
|
+
h.store.collectResult.push({ id: "bg-1", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: 1 });
|
|
634
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
635
|
+
supervisor.bootPartition();
|
|
636
|
+
expect(h.store.collectCalls).toEqual([
|
|
637
|
+
{ limit: COLD_LOOKUP_SCAN_LIMIT, status: "running", rootFilter: "root-1" },
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
const h2 = makeBinding({ sessionFile, rootId: null });
|
|
641
|
+
const supervisor2 = createRoundSupervisorForService(h2.binding);
|
|
642
|
+
supervisor2.bootPartition();
|
|
643
|
+
expect(h2.store.collectCalls).toEqual([
|
|
644
|
+
{ limit: COLD_LOOKUP_SCAN_LIMIT, status: "running", rootFilter: undefined },
|
|
645
|
+
]);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("record 视图磁盘兜底投影:内存 miss 时 boot 分区经 findLightById 读视图并可重认领", () => {
|
|
649
|
+
const h = makeBinding({ sessionFile });
|
|
650
|
+
h.store.disk.set("bg-disk", makeDiskRecord(sessionFile));
|
|
651
|
+
h.store.collectResult.push({ id: "bg-disk", rootSessionId: "root-1", agent: "worker", slug: "fix-bug", startedAt: 1 });
|
|
652
|
+
const supervisor = createRoundSupervisorForService(h.binding);
|
|
653
|
+
|
|
654
|
+
const { readopted } = supervisor.bootPartition();
|
|
655
|
+
|
|
656
|
+
expect(readopted).toEqual(["bg-disk"]);
|
|
657
|
+
// 磁盘投影视图驱动判定:resumable + 无完成产出 → 决策指引送达
|
|
658
|
+
expect(h.pi?.sent).toHaveLength(1);
|
|
659
|
+
expect(h.pi?.sent[0]?.message.content).toContain("died mid-task and stays resumable");
|
|
660
|
+
});
|
|
661
|
+
});
|