@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
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
finalizedMarkerModule,
|
|
28
28
|
fsSyncModule,
|
|
29
29
|
manifestStoreModule,
|
|
30
|
-
tempPromptModule,
|
|
31
30
|
} from "./helpers/subagent-service-mocks.ts";
|
|
32
31
|
|
|
33
32
|
// ── mock modules(同 execute-nesting.test.ts)──
|
|
@@ -37,11 +36,11 @@ vi.mock("node:fs", async (importOriginal) => fsSyncModule(await importOriginal<t
|
|
|
37
36
|
vi.mock("../alive-store.ts", async (importOriginal) => aliveStoreModule(await importOriginal<typeof import("../alive-store.ts")>()));
|
|
38
37
|
vi.mock("../finalized-marker.ts", () => finalizedMarkerModule());
|
|
39
38
|
vi.mock("../manifest-store.ts", () => manifestStoreModule());
|
|
40
|
-
vi.mock("../engine/engines/pi/temp-prompt.ts", () => tempPromptModule());
|
|
41
39
|
|
|
42
40
|
import { spawn } from "node:child_process";
|
|
43
41
|
|
|
44
|
-
import {
|
|
42
|
+
import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
|
|
43
|
+
import { clearEngines } from "../engine/registry.ts";
|
|
45
44
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
46
45
|
import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
|
|
47
46
|
import { ManifestStore } from "../manifest-store.ts";
|
|
@@ -62,6 +61,13 @@ function getLastSpawnEnv(): Record<string, string | undefined> {
|
|
|
62
61
|
return (mockSpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string | undefined>) ?? {};
|
|
63
62
|
}
|
|
64
63
|
|
|
64
|
+
/** [W3] 协议替身:execute → engine.run 挂起不 settle——record 保持 running 在 store
|
|
65
|
+
* 可观察(原 FakeChild 挂起不推进的等价形态)。 */
|
|
66
|
+
function registerHangingEngine(): FakePiEnginePort {
|
|
67
|
+
clearEngines();
|
|
68
|
+
return registerFakePiEngine();
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
// ── 辅助:service 构造 ──
|
|
66
72
|
|
|
67
73
|
function makeEmptyRegistry(): ModelRegistryLike {
|
|
@@ -122,6 +128,7 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
|
|
|
122
128
|
process.env[ENV_DEPTH] = "1";
|
|
123
129
|
|
|
124
130
|
const { service, store } = setup({});
|
|
131
|
+
registerHangingEngine();
|
|
125
132
|
|
|
126
133
|
// 不在 execCtxAls.run 内调用(模拟 ALS 断裂:事件回调上下文读不到 store)
|
|
127
134
|
const handle = await service.execute({ task: "child of parent", slug: "test", ctxModel });
|
|
@@ -143,6 +150,7 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
|
|
|
143
150
|
|
|
144
151
|
it("[顶层] 无 env(根进程):parentRecordId undefined / depth 0", async () => {
|
|
145
152
|
const { service, store } = setup({});
|
|
153
|
+
registerHangingEngine();
|
|
146
154
|
|
|
147
155
|
const handle = await service.execute({ task: "top level", slug: "test", ctxModel });
|
|
148
156
|
|
|
@@ -158,6 +166,7 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
|
|
|
158
166
|
process.env[ENV_DEPTH] = "0";
|
|
159
167
|
|
|
160
168
|
const { service, store } = setup({});
|
|
169
|
+
registerHangingEngine();
|
|
161
170
|
const execNesting = Reflect.get(service, "execNesting") as ExecCtxAls;
|
|
162
171
|
|
|
163
172
|
const handle = await execNesting.run({ recordId: "sa-inline-parent", depth: 3 }, () =>
|
|
@@ -170,24 +179,22 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
|
|
|
170
179
|
expect(rec!.depth).toBe(4);
|
|
171
180
|
});
|
|
172
181
|
|
|
173
|
-
it("[forkDepth 基线] env PI_SUBAGENT_FORK_DEPTH=1
|
|
182
|
+
it("[forkDepth 基线] env PI_SUBAGENT_FORK_DEPTH=1 → initSession 建立基线(读点兜底基线生效)", async () => {
|
|
183
|
+
// [W3 改写] spawn env 注入(PI_SUBAGENT_FORK_DEPTH 递增写子 env)属 inproc
|
|
184
|
+
// runSpawn 机制,随 inproc pi 引擎目录 删除迁入引擎包(pi-subagent-cli spawn 链);core
|
|
185
|
+
// 侧存留语义 = initSession 从 env 建立基线(ALS 断裂时的读点兜底),本用例锁定它。
|
|
174
186
|
process.env[ENV_ROOT_SESSION_ID] = "root-main";
|
|
175
187
|
process.env[ENV_SELF_RECORD_ID] = "sa-fork-parent";
|
|
176
188
|
process.env[ENV_DEPTH] = "0";
|
|
177
189
|
process.env[ENV_FORK_DEPTH] = "1";
|
|
178
190
|
|
|
179
191
|
const { service } = setup({});
|
|
192
|
+
registerHangingEngine();
|
|
180
193
|
|
|
181
|
-
|
|
182
|
-
await waitForSpawn(mockSpawn);
|
|
183
|
-
const childEnv = getLastSpawnEnv();
|
|
184
|
-
|
|
185
|
-
// 742 行 parentDepth = forkDepthAls.getStore() ?? forkDepthBaseline —— ALS 断裂时基线=1,+1 → 2
|
|
186
|
-
expect(childEnv.PI_SUBAGENT_FORK_DEPTH).toBe("2");
|
|
194
|
+
expect(Reflect.get(service, "forkDepthBaseline")).toBe(1);
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
await execPromise;
|
|
196
|
+
// fork 请求照常派发(引擎侧承载深度传递)——不因基线读取抛错
|
|
197
|
+
await expect(service.execute({ task: "fork child", slug: "test", ctxModel, fork: true })).resolves.toBeDefined();
|
|
191
198
|
});
|
|
192
199
|
|
|
193
200
|
it("[嵌套护栏] 基线 depth 参与 execute 入口嵌套护栏(depth>MAX 拒绝)", async () => {
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// src/execution/__tests__/round-supervisor.test.ts
|
|
2
|
+
//
|
|
3
|
+
// [W4] 轮次活性监督器单测:三态判定(该等/该唤醒/该放弃)、纳管模型(死亡事件
|
|
4
|
+
// 纳管、重建不解管——镜像置死只作触发信号不作持续判据)、boot 分区(重认领 vs
|
|
5
|
+
// conversation 豁免 vs in-flight 跳过)、通知对账两级启发式(高置信撤销改送终止
|
|
6
|
+
// 通知 / 低置信豁免声明)。timer 用 fake timers;deps 全部替身注入(不触真实
|
|
7
|
+
// 数据目录——监督器对 fs 零依赖)。
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
import type { ExecutionRecord } from "../types.ts";
|
|
12
|
+
import { RoundSupervisor, ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS, type RoundSupervisorDeps, type SupervisorCandidateRecord, type SupervisorRecordView } from "../round-supervisor/index.ts";
|
|
13
|
+
|
|
14
|
+
function makeDeps(overrides: Partial<RoundSupervisorDeps> = {}): RoundSupervisorDeps & {
|
|
15
|
+
notices: string[];
|
|
16
|
+
guidances: Array<{ record: SupervisorRecordView; exemptionDisclaimer: boolean }>;
|
|
17
|
+
replaced: Array<{ record: SupervisorRecordView; replacementId: string }>;
|
|
18
|
+
givenUp: Array<{ recordId: string; kind: string; detail: { replacementId?: string } }>;
|
|
19
|
+
views: Map<string, SupervisorRecordView>;
|
|
20
|
+
candidates: SupervisorCandidateRecord[];
|
|
21
|
+
live: Set<string>;
|
|
22
|
+
} {
|
|
23
|
+
const views = new Map<string, SupervisorRecordView>();
|
|
24
|
+
const candidates: SupervisorCandidateRecord[] = [];
|
|
25
|
+
const live = new Set<string>();
|
|
26
|
+
const notices: string[] = [];
|
|
27
|
+
const guidances: Array<{ record: SupervisorRecordView; exemptionDisclaimer: boolean }> = [];
|
|
28
|
+
const replaced: Array<{ record: SupervisorRecordView; replacementId: string }> = [];
|
|
29
|
+
const givenUp: Array<{ recordId: string; kind: string; detail: { replacementId?: string } }> = [];
|
|
30
|
+
const deps: RoundSupervisorDeps = {
|
|
31
|
+
now: () => Date.now(),
|
|
32
|
+
getRecordView: (id) => views.get(id),
|
|
33
|
+
listCandidateRecords: () => candidates,
|
|
34
|
+
hasLiveProcess: (recordId) => live.has(recordId),
|
|
35
|
+
sendMergedFailureNotice: (record) => notices.push(`merged:${record.id}`),
|
|
36
|
+
sendDecisionGuidance: (record, opts) => guidances.push({ record, exemptionDisclaimer: opts.exemptionDisclaimer }),
|
|
37
|
+
sendReplacedNotice: (record, replacementId) => replaced.push({ record, replacementId }),
|
|
38
|
+
giveUp: (recordId, kind, detail) => givenUp.push({ recordId, kind, detail }),
|
|
39
|
+
};
|
|
40
|
+
return Object.assign(deps, { notices, guidances, replaced, givenUp, views, candidates, live });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 内存 record 替身(adoptOnProcessDeath 入参形态——字段访问面最小化)。 */
|
|
44
|
+
function makeRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
|
|
45
|
+
return {
|
|
46
|
+
id: "bg-1",
|
|
47
|
+
agent: "worker",
|
|
48
|
+
model: "m",
|
|
49
|
+
mode: "background",
|
|
50
|
+
task: "t",
|
|
51
|
+
slug: "fix-bug",
|
|
52
|
+
startedAt: Date.now() - 1000,
|
|
53
|
+
status: "running",
|
|
54
|
+
resumable: true,
|
|
55
|
+
rootSessionId: "sess-root",
|
|
56
|
+
turnCount: 0,
|
|
57
|
+
...overrides,
|
|
58
|
+
} as ExecutionRecord;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("RoundSupervisor 三态判定", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.useFakeTimers();
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
vi.useRealTimers();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("该等:有活进程驱动 → 不干预(无指引、无放弃)", () => {
|
|
70
|
+
const deps = makeDeps();
|
|
71
|
+
const supervisor = new RoundSupervisor(deps);
|
|
72
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
73
|
+
deps.live.add("bg-1");
|
|
74
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "boom");
|
|
75
|
+
expect(deps.guidances).toHaveLength(0);
|
|
76
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("该等:在途 run 记账(noteRunStarted)→ 不干预;noteRunEnded 后重评估", () => {
|
|
80
|
+
const deps = makeDeps();
|
|
81
|
+
const supervisor = new RoundSupervisor(deps);
|
|
82
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
83
|
+
supervisor.noteRunStarted("bg-1");
|
|
84
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "boom");
|
|
85
|
+
expect(deps.guidances).toHaveLength(0);
|
|
86
|
+
supervisor.noteRunEnded("bg-1");
|
|
87
|
+
expect(deps.guidances).toHaveLength(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("该唤醒:resumable 未终态 且 无在途 run/无进程驱动 → 决策指引(一窗一次)+ 看门狗 armed", () => {
|
|
91
|
+
const deps = makeDeps();
|
|
92
|
+
const supervisor = new RoundSupervisor(deps);
|
|
93
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
94
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
|
|
95
|
+
expect(deps.notices).toEqual(["merged:bg-1"]); // 合并单条通知(failed 如实 + 接管契约)
|
|
96
|
+
expect(deps.guidances).toHaveLength(1);
|
|
97
|
+
expect(deps.guidances[0].exemptionDisclaimer).toBe(false);
|
|
98
|
+
// 重复死亡事件不重复通知、不重复指引(幂等)
|
|
99
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "again");
|
|
100
|
+
expect(deps.notices).toHaveLength(1);
|
|
101
|
+
expect(deps.guidances).toHaveLength(1);
|
|
102
|
+
expect(supervisor.supervisedIds()).toEqual(["bg-1"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("[重建不解管] 镜像重填再置死(引擎被动重建)不翻转判定:指引仍只送一次、纳管持续", () => {
|
|
106
|
+
const deps = makeDeps();
|
|
107
|
+
const supervisor = new RoundSupervisor(deps);
|
|
108
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
109
|
+
// 死亡事件纳管时镜像已置死(hasLiveProcess=false)→ 该唤醒。
|
|
110
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
|
|
111
|
+
expect(deps.guidances).toHaveLength(1);
|
|
112
|
+
// 模拟引擎被动重建:任一会话的下个请求触发 ensureConnected → 镜像重填(live=true)
|
|
113
|
+
deps.live.add("bg-1");
|
|
114
|
+
supervisor.noteRunEnded("bg-1"); // 重建后的重评估——镜像判「该等」仅解除看门狗
|
|
115
|
+
expect(deps.guidances).toHaveLength(1); // 指引不重发(已在窗内)
|
|
116
|
+
expect(supervisor.supervisedIds()).toEqual(["bg-1"]); // 纳管未解除(重建不解管)
|
|
117
|
+
// 镜像再置死(重建的进程又死):record 级状态未变 → 监督器继续管辖(不重复指引)
|
|
118
|
+
deps.live.delete("bg-1");
|
|
119
|
+
supervisor.noteRunEnded("bg-1");
|
|
120
|
+
expect(deps.guidances).toHaveLength(1);
|
|
121
|
+
expect(supervisor.supervisedIds()).toEqual(["bg-1"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("已有完成产出(SP-5 upgrade 等待态)→ 不唤醒", () => {
|
|
125
|
+
const deps = makeDeps();
|
|
126
|
+
const supervisor = new RoundSupervisor(deps);
|
|
127
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: true, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
128
|
+
supervisor.adoptOnProcessDeath(makeRecord({ result: "done text" }), "x");
|
|
129
|
+
expect(deps.guidances).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("conversation 形态(chatMode)豁免:不纳管不通知", () => {
|
|
133
|
+
const deps = makeDeps();
|
|
134
|
+
const supervisor = new RoundSupervisor(deps);
|
|
135
|
+
supervisor.adoptOnProcessDeath(makeRecord({ chatMode: true, resumable: true }), "x");
|
|
136
|
+
expect(deps.notices).toHaveLength(0);
|
|
137
|
+
expect(supervisor.supervisedIds()).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("该放弃:决策看门狗到期 → giveUp(watchdog-expired) 并解除纳管;record 已终态则跳过", async () => {
|
|
141
|
+
const deps = makeDeps();
|
|
142
|
+
const supervisor = new RoundSupervisor(deps);
|
|
143
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
144
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
145
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
146
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
147
|
+
expect(deps.givenUp).toEqual([{ recordId: "bg-1", kind: "watchdog-expired", detail: {} }]);
|
|
148
|
+
expect(supervisor.supervisedIds()).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("看门狗等待期内 run 恢复(决策收敛)→ 解除看门狗,不放弃", async () => {
|
|
152
|
+
const deps = makeDeps();
|
|
153
|
+
const supervisor = new RoundSupervisor(deps);
|
|
154
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
155
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
156
|
+
supervisor.noteRunStarted("bg-1"); // 主 agent resume = 决策收敛
|
|
157
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
158
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("[F5] 挂账转完成(SP-5 result 回填)→ 解除看门狗:到期不误 giveUp 已完成挂账 record", async () => {
|
|
162
|
+
const deps = makeDeps();
|
|
163
|
+
const supervisor = new RoundSupervisor(deps);
|
|
164
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
165
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x"); // 该唤醒 → 指引 + 看门狗 armed
|
|
166
|
+
expect(deps.guidances).toHaveLength(1);
|
|
167
|
+
// upgrade 完成 → result 回填(挂账态),重评估触发(noteRunEnded 携带视图更新)
|
|
168
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: true, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
169
|
+
supervisor.noteRunEnded("bg-1");
|
|
170
|
+
// armed 的看门狗必须已解除——2h 到期不得把已完成挂账 record 判死
|
|
171
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
|
|
172
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
173
|
+
expect(supervisor.supervisedIds()).toEqual(["bg-1"]); // 挂账态保持纳管(归 idle-gc 收口)
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("env ≤0 关闭该放弃路径:只指引永不放弃(回收层 opt-out)", async () => {
|
|
177
|
+
vi.stubEnv("XYZ_SUBAGENT_ROUND_SUPERVISOR_WATCHDOG_MS", "0");
|
|
178
|
+
RoundSupervisor._resetEnvCacheForTest();
|
|
179
|
+
try {
|
|
180
|
+
const deps = makeDeps();
|
|
181
|
+
const supervisor = new RoundSupervisor(deps);
|
|
182
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
183
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
184
|
+
expect(deps.guidances).toHaveLength(1);
|
|
185
|
+
await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS * 3);
|
|
186
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
187
|
+
} finally {
|
|
188
|
+
vi.unstubAllEnvs();
|
|
189
|
+
RoundSupervisor._resetEnvCacheForTest();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("RoundSupervisor boot 分区", () => {
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
vi.useFakeTimers();
|
|
197
|
+
});
|
|
198
|
+
afterEach(() => {
|
|
199
|
+
vi.useRealTimers();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("already-resumable-idle(非 conversation)→ 重认领接管 + 送达指引", () => {
|
|
203
|
+
const deps = makeDeps();
|
|
204
|
+
const supervisor = new RoundSupervisor(deps);
|
|
205
|
+
deps.candidates.push({ id: "bg-1", rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1 });
|
|
206
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
207
|
+
const { readopted } = supervisor.bootPartition();
|
|
208
|
+
expect(readopted).toEqual(["bg-1"]);
|
|
209
|
+
expect(deps.guidances).toHaveLength(1);
|
|
210
|
+
expect(supervisor.supervisedIds()).toEqual(["bg-1"]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("in-flight 形态(无 resumable 信号)→ 跳过(直断归孤儿恢复,防双收尾)", () => {
|
|
214
|
+
const deps = makeDeps();
|
|
215
|
+
const supervisor = new RoundSupervisor(deps);
|
|
216
|
+
deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1 });
|
|
217
|
+
deps.views.set("bg-2", { id: "bg-2", status: "running", resumable: false, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
|
|
218
|
+
const { readopted } = supervisor.bootPartition();
|
|
219
|
+
expect(readopted).toEqual([]);
|
|
220
|
+
expect(deps.guidances).toHaveLength(0);
|
|
221
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("conversation 形态 → 豁免(现状机制管辖)", () => {
|
|
225
|
+
const deps = makeDeps();
|
|
226
|
+
const supervisor = new RoundSupervisor(deps);
|
|
227
|
+
deps.candidates.push({ id: "bg-3", rootSessionId: "r", agent: "chat", slug: "s", startedAt: 1 });
|
|
228
|
+
deps.views.set("bg-3", { id: "bg-3", status: "running", resumable: true, hasResult: false, chatMode: true, rootSessionId: "r", agent: "chat", slug: "s", startedAt: 1, closedReason: undefined });
|
|
229
|
+
const { readopted } = supervisor.bootPartition();
|
|
230
|
+
expect(readopted).toEqual([]);
|
|
231
|
+
expect(supervisor.supervisedIds()).toEqual([]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("RoundSupervisor 通知对账(送指引前查替代)", () => {
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
vi.useFakeTimers();
|
|
238
|
+
});
|
|
239
|
+
afterEach(() => {
|
|
240
|
+
vi.useRealTimers();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
function setupDeadRecord(): { deps: ReturnType<typeof makeDeps>; supervisor: RoundSupervisor } {
|
|
244
|
+
const deps = makeDeps();
|
|
245
|
+
const supervisor = new RoundSupervisor(deps);
|
|
246
|
+
deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "fix-bug", startedAt: 1, closedReason: undefined });
|
|
247
|
+
return { deps, supervisor };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
it("高置信(同 root + 同 agent + 同 slug + 看门狗窗内新建)→ 撤销指引改送替代终止通知 + 该放弃", () => {
|
|
251
|
+
const { deps, supervisor } = setupDeadRecord();
|
|
252
|
+
deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 1000 });
|
|
253
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
254
|
+
expect(deps.guidances).toHaveLength(0); // 指引撤销
|
|
255
|
+
expect(deps.replaced).toHaveLength(1);
|
|
256
|
+
expect(deps.replaced[0].replacementId).toBe("bg-2");
|
|
257
|
+
expect(deps.givenUp).toEqual([{ recordId: "bg-1", kind: "superseded", detail: { replacementId: "bg-2" } }]);
|
|
258
|
+
expect(supervisor.supervisedIds()).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("低置信(仅 agent 同名)→ 不撤销指引,指引自带豁免声明", () => {
|
|
262
|
+
const { deps, supervisor } = setupDeadRecord();
|
|
263
|
+
deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "other-task", startedAt: Date.now() - 1000 });
|
|
264
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
265
|
+
expect(deps.guidances).toHaveLength(1);
|
|
266
|
+
expect(deps.guidances[0].exemptionDisclaimer).toBe(true);
|
|
267
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("同名但超出看门狗窗(无时序关联)→ 无替代命中,指引不带豁免声明", () => {
|
|
271
|
+
const { deps, supervisor } = setupDeadRecord();
|
|
272
|
+
deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "fix-bug", startedAt: Date.now() - ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS - 10_000 });
|
|
273
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
274
|
+
expect(deps.guidances).toHaveLength(1);
|
|
275
|
+
expect(deps.guidances[0].exemptionDisclaimer).toBe(false);
|
|
276
|
+
expect(deps.givenUp).toHaveLength(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("无候选 → 指引不带豁免声明", () => {
|
|
280
|
+
const { deps, supervisor } = setupDeadRecord();
|
|
281
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
282
|
+
expect(deps.guidances).toHaveLength(1);
|
|
283
|
+
expect(deps.guidances[0].exemptionDisclaimer).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("高置信对账按 slug 改写漏判 → 落入低置信(残余双执行风险面,豁免声明兜住)", () => {
|
|
287
|
+
const { deps, supervisor } = setupDeadRecord();
|
|
288
|
+
deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "rewritten-slug", startedAt: Date.now() - 1000 });
|
|
289
|
+
supervisor.adoptOnProcessDeath(makeRecord(), "x");
|
|
290
|
+
expect(deps.guidances).toHaveLength(1);
|
|
291
|
+
expect(deps.guidances[0].exemptionDisclaimer).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -27,9 +27,11 @@ import * as path from "node:path";
|
|
|
27
27
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
28
28
|
import { configureNotifyDomain, resetNotifyDomainForTests } from "../../core/notify-ports.ts";
|
|
29
29
|
|
|
30
|
+
// [D5 消亡] listActivePendingFromSessionFile(后代补杀清单口径)随 inproc pi 引擎目录
|
|
31
|
+
// 删除消亡(消费方全在待删 session-runner;止损语义由 W4 监督器该放弃承接)——
|
|
32
|
+
// 本文件覆盖收敛为 count 口径 + 游标剪枝原语。
|
|
30
33
|
import {
|
|
31
34
|
clearPendingCursors,
|
|
32
|
-
listActivePendingFromSessionFile,
|
|
33
35
|
prunePendingCursor,
|
|
34
36
|
readActivePendingFromSessionFile,
|
|
35
37
|
} from "../session-pending.ts";
|
|
@@ -175,7 +177,10 @@ describe("readActivePendingFromSessionFile", () => {
|
|
|
175
177
|
});
|
|
176
178
|
|
|
177
179
|
it("fork 继承的主 session register 已被 expired unregister 抵消 → 不干扰", () => {
|
|
178
|
-
// 模拟 P fork 主 session:继承 register
|
|
180
|
+
// 模拟 P fork 主 session:继承 register 且文件里已有对应 unregister 抵消行
|
|
181
|
+
// (历史 session 档 U4 产物)。翻 process 档后 U4 不再补写抵消行,fork 残留的
|
|
182
|
+
// 现行收口 = 读侧过滤②(见文末 [F1] describe)——本用例锁「文件内抵消行仍生效」
|
|
183
|
+
// 的差集语义本身,不锁清理机制。
|
|
179
184
|
const file = makeTmpSessionFile([
|
|
180
185
|
mkRegister("parent-bg"),
|
|
181
186
|
mkUnregister("parent-bg", "expired"),
|
|
@@ -319,9 +324,6 @@ describe("[LC-6] 差集计数语义等值", () => {
|
|
|
319
324
|
expect(r.count).toBe(1); // 2 register − 1 unregister
|
|
320
325
|
expect(counter.mock.calls[0]?.[0]).toHaveLength(1); // 端口收到差集(非全量 3 行)
|
|
321
326
|
expect((counter.mock.calls[0]?.[0] as unknown[])[0]).toMatchObject({ customType: "pending:register" });
|
|
322
|
-
|
|
323
|
-
const list = listActivePendingFromSessionFile(f);
|
|
324
|
-
expect(list.items.map((i) => i.id)).toEqual(["bg-2"]);
|
|
325
327
|
});
|
|
326
328
|
|
|
327
329
|
it("同 id 重 register(未 unregister)覆盖 → 仍 1 个活跃", () => {
|
|
@@ -330,9 +332,7 @@ describe("[LC-6] 差集计数语义等值", () => {
|
|
|
330
332
|
fs.writeFileSync(f, entryLine("pending:register", registerData("bg-1", "sess-1")));
|
|
331
333
|
fs.appendFileSync(f, entryLine("pending:register", registerData("bg-1", "sess-1-moved")));
|
|
332
334
|
|
|
333
|
-
|
|
334
|
-
expect(list.items).toHaveLength(1);
|
|
335
|
-
expect(list.items[0]?.sessionId).toBe("sess-1-moved");
|
|
335
|
+
expect(readActivePendingFromSessionFile(f).count).toBe(1);
|
|
336
336
|
});
|
|
337
337
|
|
|
338
338
|
it("unregister 后同 id 再 register → 恢复活跃", () => {
|
|
@@ -345,9 +345,6 @@ describe("[LC-6] 差集计数语义等值", () => {
|
|
|
345
345
|
entryLine("pending:register", registerData("bg-1", "sess-2")),
|
|
346
346
|
);
|
|
347
347
|
|
|
348
|
-
const list = listActivePendingFromSessionFile(f);
|
|
349
|
-
expect(list.items.map((i) => i.id)).toEqual(["bg-1"]);
|
|
350
|
-
expect(list.items[0]?.sessionId).toBe("sess-2");
|
|
351
348
|
expect(readActivePendingFromSessionFile(f).count).toBe(1);
|
|
352
349
|
});
|
|
353
350
|
|
|
@@ -382,8 +379,7 @@ describe("[LC-6] 差集计数语义等值", () => {
|
|
|
382
379
|
entryLine("pending:register", registerData("bg-2", "sess-2")),
|
|
383
380
|
);
|
|
384
381
|
|
|
385
|
-
|
|
386
|
-
expect(list.items.map((i) => i.id)).toEqual(["bg-2"]);
|
|
382
|
+
expect(readActivePendingFromSessionFile(f).count).toBe(1); // 畸形行丢弃,好行入差集
|
|
387
383
|
});
|
|
388
384
|
});
|
|
389
385
|
|
|
@@ -410,8 +406,7 @@ describe("[LC-6] 游标剪枝", () => {
|
|
|
410
406
|
expect(Buffer.byteLength(gen2, "utf-8")).toBeGreaterThan(150);
|
|
411
407
|
fs.writeFileSync(f, gen2);
|
|
412
408
|
|
|
413
|
-
|
|
414
|
-
expect(list.items.map((i) => i.id)).toEqual(["bg-x"]); // 剪枝生效:从头全量读
|
|
409
|
+
expect(readActivePendingFromSessionFile(f).count).toBe(1); // 剪枝生效:从头全量读
|
|
415
410
|
});
|
|
416
411
|
|
|
417
412
|
it("prunePendingCursor:回收后重新判定等值(全量重读差集不变)", () => {
|
|
@@ -429,7 +424,6 @@ describe("[LC-6] 游标剪枝", () => {
|
|
|
429
424
|
fs.appendFileSync(f, entryLine("pending:unregister", { id: "bg-1" }));
|
|
430
425
|
const r = readActivePendingFromSessionFile(f);
|
|
431
426
|
expect(r.count).toBe(1); // 重建后差集正确(无重复无丢失)
|
|
432
|
-
expect(listActivePendingFromSessionFile(f).items.map((i) => i.id)).toEqual(["bg-2"]);
|
|
433
427
|
});
|
|
434
428
|
|
|
435
429
|
it("prunePendingCursor:prune 后覆写文件(size ≥ 旧 offset)→ 新内容完整可见", () => {
|
|
@@ -446,7 +440,7 @@ describe("[LC-6] 游标剪枝", () => {
|
|
|
446
440
|
fs.writeFileSync(f, gen2); // 覆写(模拟文件替换),size > 旧 offset
|
|
447
441
|
prunePendingCursor(f);
|
|
448
442
|
|
|
449
|
-
expect(
|
|
443
|
+
expect(readActivePendingFromSessionFile(f).count).toBe(1);
|
|
450
444
|
});
|
|
451
445
|
|
|
452
446
|
it("prunePendingCursor 不存在的条目为 no-op(不抛错)", () => {
|
|
@@ -469,3 +463,111 @@ describe("[LC-6] 游标剪枝", () => {
|
|
|
469
463
|
expect(readActivePendingFromSessionFile(f).count).toBe(1);
|
|
470
464
|
});
|
|
471
465
|
});
|
|
466
|
+
|
|
467
|
+
// ─── [F1 读侧过滤②] 跨 session 残留基准 = 被读文件所属 session ───
|
|
468
|
+
//
|
|
469
|
+
// fork 继承的父级 register 残留(翻 process 档后 U4 补注销不再中性化,永久留存)
|
|
470
|
+
// 由读侧过滤收口:本文件读侧以「被读文件所属 session id」(pi session 文件首行
|
|
471
|
+
// SessionHeader.id)为基准经端口第二参传入,端口实现按 entry sessionId ≠ 基准
|
|
472
|
+
// 跳过——后代判定差集不含跨 session 残留,层主不被残留误判「尚有活跃后代」。
|
|
473
|
+
|
|
474
|
+
describe("[F1 读侧过滤②] 后代判定差集的跨 session 残留基准", () => {
|
|
475
|
+
const tmpFiles: string[] = [];
|
|
476
|
+
|
|
477
|
+
afterEach(() => {
|
|
478
|
+
for (const f of tmpFiles.splice(0)) {
|
|
479
|
+
try {
|
|
480
|
+
fs.rmSync(path.dirname(f), { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
481
|
+
} catch {
|
|
482
|
+
// 清理失败不影响断言
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
/** pi session 文件首行 SessionHeader(session-manager.js newSession 形态)。 */
|
|
488
|
+
const mkSessionHeader = (sessionId: string) =>
|
|
489
|
+
JSON.stringify({ type: "session", version: 3, id: sessionId, timestamp: "2026-09-01T00:00:00.000Z", cwd: "/tmp" });
|
|
490
|
+
|
|
491
|
+
/** 带 sessionId 的 register(pending-notifications listener 落盘形态)。 */
|
|
492
|
+
const mkRegisterIn = (id: string, sessionId: string) =>
|
|
493
|
+
JSON.stringify({ type: "custom", customType: "pending:register", data: { id, type: "subagent", name: id, registeredAt: 1, sessionId } });
|
|
494
|
+
|
|
495
|
+
/** 端口等价实现(消费基准):按 currentSessionId 过滤后直数——pending-notifications
|
|
496
|
+
* filterActiveRegisters 的最小转写(缺 sessionId 的旧形态条目视为本 session,不过滤)。 */
|
|
497
|
+
function filteredCountingPort(): ReturnType<typeof vi.fn<(entries: unknown[], opts?: { currentSessionId?: string }) => number>> {
|
|
498
|
+
const counter = vi.fn((entries: unknown[], opts?: { currentSessionId?: string }) => {
|
|
499
|
+
const seen = new Set<string>();
|
|
500
|
+
let count = 0;
|
|
501
|
+
for (const raw of entries) {
|
|
502
|
+
if (!raw || typeof raw !== "object") continue;
|
|
503
|
+
const data = (raw as { data?: { id?: unknown; sessionId?: unknown } }).data ?? {};
|
|
504
|
+
if (typeof data.id !== "string" || seen.has(data.id)) continue;
|
|
505
|
+
seen.add(data.id);
|
|
506
|
+
if (
|
|
507
|
+
opts?.currentSessionId !== undefined &&
|
|
508
|
+
typeof data.sessionId === "string" &&
|
|
509
|
+
data.sessionId !== opts.currentSessionId
|
|
510
|
+
) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
count += 1;
|
|
514
|
+
}
|
|
515
|
+
return count;
|
|
516
|
+
}) as ReturnType<typeof vi.fn<(entries: unknown[], opts?: { currentSessionId?: string }) => number>>;
|
|
517
|
+
configureNotifyDomain({ countActiveFromEntries: counter });
|
|
518
|
+
return counter;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
it("带 session header 的文件 → 端口收到的基准 = header.id(被读文件所属 session)", () => {
|
|
522
|
+
const counter = filteredCountingPort();
|
|
523
|
+
const file = makeTmpSessionFile([
|
|
524
|
+
mkSessionHeader("sess-child"),
|
|
525
|
+
mkRegisterIn("bg-own", "sess-child"),
|
|
526
|
+
]);
|
|
527
|
+
tmpFiles.push(file);
|
|
528
|
+
expect(readActivePendingFromSessionFile(file).count).toBe(1);
|
|
529
|
+
const call = counter.mock.calls[0] as unknown[];
|
|
530
|
+
expect(call[1]).toEqual({ currentSessionId: "sess-child" });
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("端到端:fork 继承的父级 register 残留(sessionId = 父 session)不进后代判定差集", () => {
|
|
534
|
+
filteredCountingPort();
|
|
535
|
+
// 子 session 文件 = header(sess-child) + fork 继承的父级注册残留 + 子进程自己的注册
|
|
536
|
+
const file = makeTmpSessionFile([
|
|
537
|
+
mkSessionHeader("sess-child"),
|
|
538
|
+
mkRegisterIn("bg-parent", "sess-parent"),
|
|
539
|
+
mkRegisterIn("wf-parent", "sess-parent"),
|
|
540
|
+
mkRegisterIn("bg-own", "sess-child"),
|
|
541
|
+
]);
|
|
542
|
+
tmpFiles.push(file);
|
|
543
|
+
// 无读侧过滤时差集为 3(层主误判「尚有活跃后代」空等);过滤后只数本 session = 1
|
|
544
|
+
expect(readActivePendingFromSessionFile(file)).toEqual({ count: 1, recentUnregister: false });
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("无 session header(旧 fixture 形态)→ 基准 undefined = 不过滤(向后兼容)", () => {
|
|
548
|
+
const counter = filteredCountingPort();
|
|
549
|
+
const file = makeTmpSessionFile([
|
|
550
|
+
mkRegisterIn("bg-parent", "sess-parent"),
|
|
551
|
+
mkRegisterIn("bg-own", "sess-child"),
|
|
552
|
+
]);
|
|
553
|
+
tmpFiles.push(file);
|
|
554
|
+
expect(readActivePendingFromSessionFile(file).count).toBe(2);
|
|
555
|
+
const call = counter.mock.calls[0] as unknown[];
|
|
556
|
+
expect(call[1]).toBeUndefined();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("增量读(cursor 已热)时基准仍从 cursor 缓存透传(不依赖重读首行)", () => {
|
|
560
|
+
const counter = filteredCountingPort();
|
|
561
|
+
const file = makeTmpSessionFile([
|
|
562
|
+
mkSessionHeader("sess-child"),
|
|
563
|
+
mkRegisterIn("bg-own", "sess-child"),
|
|
564
|
+
]);
|
|
565
|
+
tmpFiles.push(file);
|
|
566
|
+
expect(readActivePendingFromSessionFile(file).count).toBe(1); // 首读建 cursor
|
|
567
|
+
// 追加新 register 后增量读——offset > 0 不重读首行,基准取 cursor 缓存
|
|
568
|
+
fs.appendFileSync(file, mkRegisterIn("bg-own-2", "sess-child") + "\n", "utf-8");
|
|
569
|
+
expect(readActivePendingFromSessionFile(file).count).toBe(2);
|
|
570
|
+
const lastCall = counter.mock.calls[counter.mock.calls.length - 1] as unknown[];
|
|
571
|
+
expect(lastCall[1]).toEqual({ currentSessionId: "sess-child" });
|
|
572
|
+
});
|
|
573
|
+
});
|
|
@@ -103,14 +103,6 @@ vi.mock("../manifest-store.ts", () => {
|
|
|
103
103
|
return { ManifestStore: vi.fn(function (_recordsDir: string) { return new FakeManifestStore(); }) };
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
|
|
107
|
-
writePromptToTempFile: vi.fn(async (agent: string) => {
|
|
108
|
-
const safeName = agent.replace(/[^\w.-]+/g, "_");
|
|
109
|
-
return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
|
|
110
|
-
}),
|
|
111
|
-
cleanupTempPrompt: vi.fn(async () => {}),
|
|
112
|
-
}));
|
|
113
|
-
|
|
114
106
|
import { spawn } from "node:child_process";
|
|
115
107
|
|
|
116
108
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
@@ -233,6 +225,11 @@ describe("退役步骤 2 判定层:createBackgroundStream", () => {
|
|
|
233
225
|
});
|
|
234
226
|
|
|
235
227
|
// ── 接线层:service → 工厂的参数透传(mode 传错 = 判定失效)──
|
|
228
|
+
// [W3 改写] 驱动方式从 inproc spawn(FakeChild close)改为协议 seam
|
|
229
|
+
//(registerFakePiEngine 替身)——kickOffChatRound 的 stream 创建先于 pool acquire,
|
|
230
|
+
// 替身 run 挂起即可观察工厂入参。
|
|
231
|
+
import { registerFakePiEngine } from "./helpers/fake-engine-port.ts";
|
|
232
|
+
import { clearEngines } from "../engine/registry.ts";
|
|
236
233
|
|
|
237
234
|
describe("退役步骤 2 接线层:kickOffChatRound 透传 mode/sink 给工厂", () => {
|
|
238
235
|
beforeEach(() => {
|
|
@@ -242,6 +239,7 @@ describe("退役步骤 2 接线层:kickOffChatRound 透传 mode/sink 给工厂
|
|
|
242
239
|
|
|
243
240
|
afterEach(() => {
|
|
244
241
|
for (const key of RELAY_ENV_KEYS) delete process.env[key];
|
|
242
|
+
clearEngines();
|
|
245
243
|
vi.restoreAllMocks();
|
|
246
244
|
});
|
|
247
245
|
|
|
@@ -250,9 +248,11 @@ describe("退役步骤 2 接线层:kickOffChatRound 透传 mode/sink 给工厂
|
|
|
250
248
|
["tui", "tui"],
|
|
251
249
|
["undefined(headless)", undefined],
|
|
252
250
|
])("mode=%s:工厂收到 initSession 注入的 mode 与 streamSink", async (_label, mode) => {
|
|
251
|
+
clearEngines();
|
|
252
|
+
registerFakePiEngine();
|
|
253
253
|
const service = setupService(mode);
|
|
254
|
-
await
|
|
255
|
-
expect(mockCreateStream).toHaveBeenCalledTimes(1);
|
|
254
|
+
await service.execute({ task: "retire test", slug: "test", ctxModel, conversation: true });
|
|
255
|
+
await vi.waitFor(() => expect(mockCreateStream).toHaveBeenCalledTimes(1));
|
|
256
256
|
const [recordId, sinkArg, modeArg] = mockCreateStream.mock.calls[0]!;
|
|
257
257
|
expect(typeof recordId).toBe("string");
|
|
258
258
|
expect(sinkArg).toHaveProperty("setWidget");
|
|
@@ -21,8 +21,7 @@ const { loggerMock } = vi.hoisted(() => ({
|
|
|
21
21
|
}));
|
|
22
22
|
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
23
23
|
|
|
24
|
-
const {
|
|
25
|
-
killChildSpy: vi.fn(),
|
|
24
|
+
const { foreignLiveSpy } = vi.hoisted(() => ({
|
|
26
25
|
foreignLiveSpy: vi.fn<() => { pid: number; id: string; startedAt: number } | undefined>(),
|
|
27
26
|
}));
|
|
28
27
|
vi.mock("../alive-store.ts", () => ({
|
|
@@ -34,15 +33,6 @@ vi.mock("../alive-store.ts", () => ({
|
|
|
34
33
|
findForeignLiveInstance: foreignLiveSpy,
|
|
35
34
|
}));
|
|
36
35
|
|
|
37
|
-
vi.mock("../engine/engines/pi/session-runner.ts", () => ({
|
|
38
|
-
runSpawn: vi.fn(),
|
|
39
|
-
killAllSpawnedChildren: vi.fn(),
|
|
40
|
-
getChildByRecord: vi.fn(() => undefined),
|
|
41
|
-
registerSpawnedChildForRecord: vi.fn(),
|
|
42
|
-
spawnedChildren: new Map(),
|
|
43
|
-
killRecordChildWithEscalation: killChildSpy,
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
36
|
import { ModelConfigService } from "../model-config-service.ts";
|
|
47
37
|
import type { RecordStore } from "../record-store.ts";
|
|
48
38
|
import { SubagentService } from "../subagent-service.ts";
|