@zhushanwen/subagent-core 0.2.0 → 0.5.1
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 +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-43ONBFZX.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-V3VHZ2VX.js +59 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +62 -9
- package/dist/execution/engine/engines/zcode/constants.d.cts +116 -12
- package/dist/execution/engine/engines/zcode/constants.d.ts +116 -12
- package/dist/execution/engine/engines/zcode/constants.js +39 -7
- package/dist/execution/engine/engines/zcode/reader.d.cts +2 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +2 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +18234 -1277
- package/dist/index.d.cts +5646 -673
- package/dist/index.d.ts +5646 -673
- package/dist/index.js +17828 -1277
- package/dist/{types-BxyAidGf.d.cts → types-Dv4QhSJ_.d.cts} +678 -112
- package/dist/{types-BxyAidGf.d.ts → types-Dv4QhSJ_.d.ts} +678 -112
- package/package.json +7 -4
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +0 -1
- package/src/__tests__/append-system-prompt-assembly.test.ts +8 -7
- package/src/__tests__/fr4-get-state-handshake.test.ts +2 -2
- package/src/__tests__/m2-append-content-probe.test.ts +0 -1
- package/src/__tests__/manifest-store.test.ts +10 -9
- package/src/__tests__/record-store-cache.test.ts +0 -2
- package/src/__tests__/record-store-index.test.ts +1 -2
- package/src/__tests__/review-fix-loop-script.test.ts +93 -3
- package/src/__tests__/review-fix-loop-utils.test.ts +31 -10
- package/src/__tests__/robustness-low-batch1.test.ts +6 -5
- package/src/__tests__/robustness-medium-batch1.test.ts +2 -2
- package/src/__tests__/robustness-medium-batch2.test.ts +17 -6
- package/src/__tests__/robustness-medium-batch4.test.ts +2 -2
- package/src/__tests__/session-runner.test.ts +1 -1
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/core/__tests__/host-services.test.ts +2 -2
- package/src/core/__tests__/logger.test.ts +1 -1
- package/src/core/error-message.ts +9 -0
- package/src/core/host-services.ts +21 -5
- package/src/core/notify-ports.ts +18 -5
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agent-registry.test.ts +370 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +18 -2
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +6 -6
- package/src/execution/__tests__/chat-engine-routing.test.ts +136 -30
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +5 -4
- package/src/execution/__tests__/cold-resurrect.test.ts +283 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +77 -62
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +220 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/engine-model-validation.test.ts +741 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +31 -24
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +21 -91
- package/src/execution/__tests__/execute-nesting.test.ts +34 -188
- package/src/execution/__tests__/execution-record.test.ts +217 -60
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +3 -3
- package/src/execution/__tests__/finalize-record.test.ts +278 -16
- package/src/execution/__tests__/gc-timer.test.ts +57 -2
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +14 -12
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +113 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +150 -3
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +137 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +280 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +1 -2
- package/src/execution/__tests__/manifest-parentid.test.ts +4 -4
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/model-resolver.test.ts +2 -2
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +23 -121
- package/src/execution/__tests__/nested-visibility.test.ts +2 -0
- package/src/execution/__tests__/notify-ledger.test.ts +1249 -0
- package/src/execution/__tests__/output-collector.test.ts +117 -6
- package/src/execution/__tests__/pi-invocation.test.ts +1 -1
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +89 -5
- package/src/execution/__tests__/records-cwd-isolation.test.ts +2 -2
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +29 -132
- package/src/execution/__tests__/recursive-visibility-env.test.ts +55 -179
- package/src/execution/__tests__/rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +387 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +19 -9
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +5 -4
- package/src/execution/__tests__/run-spawn-edges.test.ts +418 -26
- package/src/execution/__tests__/run-spawn-integration.test.ts +3 -3
- package/src/execution/__tests__/run-spawn-resume.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +2 -2
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +16 -8
- package/src/execution/__tests__/service-kill-escalation.test.ts +91 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +0 -2
- package/src/execution/__tests__/session-file-gc.test.ts +35 -0
- package/src/execution/__tests__/session-pending.test.ts +471 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +366 -15
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-dispatch.test.ts +489 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +4 -3
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +227 -0
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +290 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +7 -5
- package/src/execution/__tests__/settled-watchdog.test.ts +524 -0
- package/src/execution/__tests__/spawn-args.test.ts +11 -10
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +26 -23
- package/src/execution/__tests__/spawn-event-adapter.test.ts +1 -1
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +5 -4
- package/src/execution/__tests__/spawned-children.test.ts +155 -11
- package/src/execution/__tests__/start-sync-model-guard.test.ts +4 -4
- package/src/execution/__tests__/stdin-writer.test.ts +6 -4
- package/src/execution/__tests__/stream-sink-retirement.test.ts +6 -6
- package/src/execution/__tests__/subagent-actions-core.test.ts +1017 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +99 -35
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +271 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +33 -29
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +341 -0
- package/src/execution/__tests__/subagent-service.test.ts +37 -30
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +88 -12
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +75 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +22 -20
- package/src/execution/__tests__/temp-prompt.test.ts +1 -1
- package/src/execution/__tests__/timeout-integration.test.ts +13 -12
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +1 -1
- package/src/execution/__tests__/turn-limiter.test.ts +1 -1
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +107 -7
- package/src/execution/__tests__/ui-request-handler.test.ts +4 -3
- package/src/execution/__tests__/ui-request-queue.test.ts +167 -2
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-manager.test.ts +14 -5
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -3
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +298 -30
- package/src/execution/agent-result-mapper.ts +3 -0
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/cold-resurrect.ts +199 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +91 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +84 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +34 -7
- package/src/execution/engine/__tests__/common/persona-router.test.ts +4 -5
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +211 -51
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +149 -0
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +499 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +56 -99
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +88 -15
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +33 -7
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +75 -54
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +128 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +15 -3
- package/src/execution/engine/__tests__/model-prompt.test.ts +15 -3
- package/src/execution/engine/__tests__/registry.test.ts +94 -3
- package/src/execution/engine/__tests__/routing.test.ts +115 -6
- package/src/execution/engine/common/capability-gate.ts +92 -0
- package/src/execution/engine/common/journal-wiring.ts +97 -0
- package/src/execution/engine/common/kill-chain.ts +42 -10
- package/src/execution/engine/common/nesting-guard.ts +66 -0
- package/src/execution/engine/common/persona-router.ts +18 -8
- package/src/execution/engine/common/pool-manager.ts +332 -78
- package/src/execution/engine/common/session-view-service.ts +506 -0
- package/src/execution/engine/common/session-view-types.ts +125 -0
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +174 -28
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +257 -0
- package/src/execution/{argv-mirror.ts → engine/engines/pi/argv-mirror.ts} +43 -21
- package/src/execution/{get-state-handshake.ts → engine/engines/pi/get-state-handshake.ts} +86 -11
- package/src/execution/{output-collector.ts → engine/engines/pi/output-collector.ts} +108 -19
- package/src/execution/engine/engines/pi/pi-engine.ts +434 -38
- package/src/execution/{pi-invocation.ts → engine/engines/pi/pi-invocation.ts} +2 -2
- package/src/execution/engine/engines/pi/reader.ts +11 -0
- package/src/execution/engine/engines/pi/registration.ts +4 -2
- package/src/execution/engine/engines/pi/session-runner.ts +2840 -0
- package/src/execution/{spawn-event-adapter.ts → engine/engines/pi/spawn-event-adapter.ts} +75 -34
- package/src/execution/{stdin-writer.ts → engine/engines/pi/stdin-writer.ts} +10 -9
- package/src/execution/{temp-prompt.ts → engine/engines/pi/temp-prompt.ts} +2 -2
- package/src/execution/{turn-limiter.ts → engine/engines/pi/turn-limiter.ts} +1 -1
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +301 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +321 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +473 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +7 -210
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +6 -110
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +247 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +451 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +792 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +782 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +168 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +353 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +282 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +372 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +65 -47
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +167 -0
- package/src/execution/engine/engines/zcode/connection.ts +598 -0
- package/src/execution/engine/engines/zcode/constants.ts +147 -14
- package/src/execution/engine/engines/zcode/golden-sample.ts +42 -34
- package/src/execution/engine/engines/zcode/parser.ts +12 -346
- package/src/execution/engine/engines/zcode/preparer.ts +22 -150
- package/src/execution/engine/engines/zcode/session-channel.ts +968 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +1023 -250
- package/src/execution/engine/host-task-spec.ts +32 -36
- package/src/execution/engine/model-validation.ts +177 -0
- package/src/execution/engine/port.ts +65 -7
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/engine/routing.ts +75 -1
- package/src/execution/engine/types.ts +30 -103
- package/src/execution/execution-record.ts +222 -144
- package/src/execution/finalize-record.ts +181 -85
- package/src/execution/idle-gc.ts +28 -0
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/lifecycle-predicates.ts +1 -1
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-host.ts +212 -0
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +148 -88
- package/src/execution/round-settlement.ts +93 -0
- package/src/execution/session-file-gc.ts +96 -62
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-reconstructor.ts +256 -143
- package/src/execution/sessions-index.ts +127 -102
- package/src/execution/settled-watchdog.ts +311 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +1106 -907
- package/src/execution/subprocess-agent-runner.ts +144 -97
- package/src/execution/types.ts +9 -0
- package/src/execution/ui-request-handler-factory.ts +126 -75
- package/src/execution/ui-request-queue.ts +50 -19
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +542 -9
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +3 -5
- package/src/orchestration/__tests__/agent-call-stream.test.ts +2 -2
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/args-validator.test.ts +14 -0
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +384 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +151 -11
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +390 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +3 -3
- package/src/orchestration/__tests__/lifecycle.test.ts +84 -5
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/run-snapshot.test.ts +356 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/script-lint.test.ts +17 -0
- package/src/orchestration/__tests__/test-mocks.ts +2 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +7 -6
- package/src/orchestration/__tests__/worker-message-pump-finalize-run.test.ts +229 -0
- package/src/orchestration/__tests__/{error-recovery-handlers.test.ts → worker-message-pump-handlers.test.ts} +28 -27
- package/src/orchestration/__tests__/{error-recovery-postmessage-defense.test.ts → worker-message-pump-postmessage-defense.test.ts} +8 -8
- package/src/orchestration/__tests__/{error-recovery-serialize-failed-result.test.ts → worker-message-pump-serialize-failed-result.test.ts} +2 -2
- package/src/orchestration/__tests__/{error-recovery-workflow-call.test.ts → worker-message-pump-workflow-call.test.ts} +1 -1
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/worker-script-builder.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +1 -1
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +8 -8
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/args-validator.ts +53 -31
- package/src/orchestration/execute-agent-call.ts +25 -79
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +160 -104
- package/src/orchestration/lifecycle.ts +371 -97
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/ports.ts +4 -4
- package/src/orchestration/models/run-runtime.ts +15 -15
- package/src/orchestration/models/trace.ts +9 -9
- package/src/orchestration/models/types.ts +111 -22
- package/src/orchestration/models/workflow-run.ts +28 -28
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +123 -90
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/{error-recovery.ts → worker-message-pump.ts} +498 -221
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/meta-parser.test.ts +151 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +261 -217
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +323 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +182 -68
- package/src/shared/model-ref.ts +15 -2
- package/src/shared/resource-discovery.ts +97 -204
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/schema-jsonify.ts +1 -1
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +165 -92
- package/workflows/review-fix-loop-utils.d.cts +287 -0
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
- package/src/execution/__tests__/execute-options-mapper.test.ts +0 -200
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +0 -164
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +0 -100
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +0 -39
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +0 -150
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +0 -580
- package/src/execution/engine/engines/zcode/launcher.ts +0 -161
- package/src/execution/execute-options-mapper.ts +0 -115
- package/src/execution/session-runner.ts +0 -1789
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +0 -288
|
@@ -21,12 +21,11 @@
|
|
|
21
21
|
// 无状态纯函数模块:不依赖 RecordStore 任何内部状态。
|
|
22
22
|
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
|
-
import * as fsPromises from "node:fs/promises";
|
|
25
24
|
import * as path from "node:path";
|
|
26
25
|
|
|
27
26
|
import { getLogger } from "../core/logger.ts";
|
|
27
|
+
import { writeAtomicFile } from "../shared/atomic-write.ts";
|
|
28
28
|
|
|
29
|
-
import { bestEffort } from "./best-effort.ts";
|
|
30
29
|
import type { ExecutionMode } from "./types.ts";
|
|
31
30
|
|
|
32
31
|
const logger = getLogger("subagents");
|
|
@@ -44,13 +43,6 @@ export const INDEX_VERSION = 1;
|
|
|
44
43
|
/** 两次成功落盘的最小墙钟间隔(节流:overlay 打开期间的高频扫描不放大磁盘写)。 */
|
|
45
44
|
export const INDEX_WRITE_MIN_INTERVAL_MS = 60_000;
|
|
46
45
|
|
|
47
|
-
/** tmp 文件单调计数:同一进程内并发的 saveIndex 各用独立 tmp。节流基准只在写成功后
|
|
48
|
-
* 推进——W1 在途时新一轮过窗扫描可再 dispatch W2,共用同一 tmp 会交错(W2 truncate
|
|
49
|
-
* 落在 W1 write 与 rename 之间 → 半成品被 rename / rename 后失败 → 假失败日志)。
|
|
50
|
-
* pid+seq 双后缀保证 tmp 唯一:交错 rename 的终态必为某次完整快照(last-writer-wins,
|
|
51
|
-
* 与跨进程 pid 隔离同语义;陈旧快照胜出时下轮戳不匹配自愈)。 */
|
|
52
|
-
let tmpSeq = 0;
|
|
53
|
-
|
|
54
46
|
// ============================================================
|
|
55
47
|
// 类型(DM1 磁盘顶层 + DM2 条目)
|
|
56
48
|
// ============================================================
|
|
@@ -117,28 +109,59 @@ export interface LoadedSessionsIndex {
|
|
|
117
109
|
// 条目级校验(ES3:单条目损坏仅丢弃该条目,不放大为整体失效)
|
|
118
110
|
// ============================================================
|
|
119
111
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
112
|
+
// 以下字段组谓词按原 isPositiveIndexEntry 的检查顺序切分(stamp/identity → 会话描述
|
|
113
|
+
// → 可选 string → depth/model/thinkingLevel),组内与组间短路求值顺序不变。
|
|
114
|
+
|
|
115
|
+
/** 执行模式白名单:宽容历史 "sync" 值(ExecutionMode 现值仅 "background","sync" 是旧数据合法值,放行不丢弃)。 */
|
|
116
|
+
function isKnownIndexMode(mode: unknown): boolean {
|
|
117
|
+
return mode === "sync" || mode === "background";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 戳 + 身份字段组(mtimeMs/size/id/agent/mode)。 */
|
|
121
|
+
function hasStampAndIdentityFields(v: Record<string, unknown>): boolean {
|
|
124
122
|
return (
|
|
125
123
|
typeof v.mtimeMs === "number" &&
|
|
126
124
|
typeof v.size === "number" &&
|
|
127
125
|
typeof v.id === "string" &&
|
|
128
126
|
typeof v.agent === "string" &&
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
isKnownIndexMode(v.mode)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 会话描述字段组(task/slug/startedAt)。 */
|
|
132
|
+
function hasSessionDescFields(v: Record<string, unknown>): boolean {
|
|
133
|
+
return typeof v.task === "string" && typeof v.slug === "string" && typeof v.startedAt === "number";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 可选 string 字段组(rootSessionId/parentRecordId:undefined 表示缺失,合法)。 */
|
|
137
|
+
function hasOptionalStringFields(v: Record<string, unknown>): boolean {
|
|
138
|
+
return (
|
|
134
139
|
(v.rootSessionId === undefined || typeof v.rootSessionId === "string") &&
|
|
135
|
-
(v.parentRecordId === undefined || typeof v.parentRecordId === "string")
|
|
140
|
+
(v.parentRecordId === undefined || typeof v.parentRecordId === "string")
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 深度/模型/思考档字段组(model 空串合法——尾部探测拿不到 model 的合法结果,DS4)。 */
|
|
145
|
+
function hasModelFields(v: Record<string, unknown>): boolean {
|
|
146
|
+
return (
|
|
136
147
|
typeof v.depth === "number" &&
|
|
137
148
|
typeof v.model === "string" && // 空串合法(DS4)
|
|
138
149
|
(v.thinkingLevel === undefined || typeof v.thinkingLevel === "string")
|
|
139
150
|
);
|
|
140
151
|
}
|
|
141
152
|
|
|
153
|
+
/** 正条目类型谓词:镜像 isIdentityData(session-reconstructor.ts:244-253)的字段检查 + 索引特有戳/形态字段。 */
|
|
154
|
+
function isPositiveIndexEntry(raw: unknown): raw is SessionsIndexEntry {
|
|
155
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
156
|
+
const v = raw as Record<string, unknown>;
|
|
157
|
+
return (
|
|
158
|
+
hasStampAndIdentityFields(v) &&
|
|
159
|
+
hasSessionDescFields(v) &&
|
|
160
|
+
hasOptionalStringFields(v) &&
|
|
161
|
+
hasModelFields(v)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
/** 负条目类型谓词(tagged union 形态检查)。 */
|
|
143
166
|
function isNegativeIndexEntry(raw: unknown): raw is SessionsIndexNegativeEntry {
|
|
144
167
|
if (typeof raw !== "object" || raw === null) return false;
|
|
@@ -161,64 +184,108 @@ export function validateIndexEntry(
|
|
|
161
184
|
// 读侧(IF1:永不抛)
|
|
162
185
|
// ============================================================
|
|
163
186
|
|
|
164
|
-
/**
|
|
165
|
-
*
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* - 版本匹配 → 逐条 validateIndexEntry(坏条目丢弃,其余保留)
|
|
171
|
-
*
|
|
172
|
-
* 只读精确路径 INDEX_FILENAME:.tmp. 残留文件不匹配文件名,天然被忽略。
|
|
173
|
-
*/
|
|
174
|
-
export function loadIndex(encDir: string): LoadedSessionsIndex {
|
|
175
|
-
const empty: LoadedSessionsIndex = { entries: new Map(), higherVersion: false };
|
|
176
|
-
const indexPath = path.join(encDir, INDEX_FILENAME);
|
|
187
|
+
/** 磁盘顶层已通过形态校验后的窄化视图(version 数值 + entries 对象)。
|
|
188
|
+
* type alias(非 interface)——赋给 Record<string, unknown> 依赖隐式索引签名。 */
|
|
189
|
+
type ValidIndexTop = {
|
|
190
|
+
version: number;
|
|
191
|
+
entries: Record<string, unknown>;
|
|
192
|
+
};
|
|
177
193
|
|
|
178
|
-
|
|
194
|
+
/** unknown → Node fs 错误码(非 Error 或无 code → undefined)。 */
|
|
195
|
+
function errorCodeOf(err: unknown): string | undefined {
|
|
196
|
+
return err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 读索引文件文本。读失败 → null:ENOENT = 正常首跑保持静默;其余读失败(EACCES 等
|
|
200
|
+
* 长期权限异常)留 debug 线索——空索引回退本身可自愈,但权限类异常不会自己消失,需可诊断。 */
|
|
201
|
+
function readIndexFile(indexPath: string, encDir: string): string | null {
|
|
179
202
|
try {
|
|
180
|
-
|
|
203
|
+
return fs.readFileSync(indexPath, "utf-8");
|
|
181
204
|
} catch (err) {
|
|
182
|
-
|
|
183
|
-
// ——空索引回退本身可自愈,但权限类异常不会自己消失,需可诊断。
|
|
184
|
-
const code = err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : undefined;
|
|
205
|
+
const code = errorCodeOf(err);
|
|
185
206
|
if (code !== "ENOENT") {
|
|
186
207
|
logger.debug("[subagents] sessions-index read failed, fallback to empty", {
|
|
187
208
|
detail: { dir: encDir, code },
|
|
188
209
|
});
|
|
189
210
|
}
|
|
190
|
-
return
|
|
211
|
+
return null;
|
|
191
212
|
}
|
|
213
|
+
}
|
|
192
214
|
|
|
193
|
-
|
|
215
|
+
/** JSON.parse 索引内容。损坏(截断/外部编辑)→ null,走 debug:可降级自愈场景,不 console.error。 */
|
|
216
|
+
function parseIndexJson(raw: string, indexPath: string): unknown {
|
|
194
217
|
try {
|
|
195
|
-
|
|
218
|
+
return JSON.parse(raw);
|
|
196
219
|
} catch (err) {
|
|
197
|
-
// 损坏(截断/外部编辑)走 debug:可降级自愈场景,不 console.error
|
|
198
220
|
logger.debug("[subagents] sessions-index corrupted JSON, fallback to empty", {
|
|
199
221
|
detail: { path: indexPath, error: err instanceof Error ? err.message : String(err) },
|
|
200
222
|
});
|
|
201
|
-
return
|
|
223
|
+
return null;
|
|
202
224
|
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** DM1 头部字段形态守卫(version 数值 + entries 非空 object 非数组)。 */
|
|
228
|
+
function isIndexTopHeader(v: Record<string, unknown>): v is ValidIndexTop {
|
|
229
|
+
return (
|
|
230
|
+
typeof v.version === "number" &&
|
|
231
|
+
typeof v.entries === "object" &&
|
|
232
|
+
v.entries !== null &&
|
|
233
|
+
!Array.isArray(v.entries)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
203
236
|
|
|
237
|
+
/** 顶层结构校验(DM1)。不符 → null(两条 debug 文案与判定条件一一对应)。 */
|
|
238
|
+
function readTopLevel(parsed: unknown, indexPath: string): ValidIndexTop | null {
|
|
204
239
|
if (typeof parsed !== "object" || parsed === null) {
|
|
205
240
|
logger.debug("[subagents] sessions-index invalid top-level shape, fallback to empty", {
|
|
206
241
|
detail: { path: indexPath },
|
|
207
242
|
});
|
|
208
|
-
return
|
|
243
|
+
return null;
|
|
209
244
|
}
|
|
210
245
|
const top = parsed as Record<string, unknown>;
|
|
211
|
-
if (
|
|
212
|
-
typeof top.version !== "number" ||
|
|
213
|
-
typeof top.entries !== "object" ||
|
|
214
|
-
top.entries === null ||
|
|
215
|
-
Array.isArray(top.entries)
|
|
216
|
-
) {
|
|
246
|
+
if (!isIndexTopHeader(top)) {
|
|
217
247
|
logger.debug("[subagents] sessions-index invalid header fields, fallback to empty", {
|
|
218
248
|
detail: { path: indexPath },
|
|
219
249
|
});
|
|
220
|
-
return
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return top;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 逐条校验收集:坏条目仅丢弃该条目,其余保留(ES3 条目级降级)。 */
|
|
256
|
+
function collectValidEntries(
|
|
257
|
+
entriesObj: Record<string, unknown>,
|
|
258
|
+
): Map<string, SessionsIndexEntry | SessionsIndexNegativeEntry> {
|
|
259
|
+
const entries = new Map<string, SessionsIndexEntry | SessionsIndexNegativeEntry>();
|
|
260
|
+
for (const [key, value] of Object.entries(entriesObj)) {
|
|
261
|
+
const entry = validateIndexEntry(value);
|
|
262
|
+
if (entry !== undefined) entries.set(key, entry);
|
|
221
263
|
}
|
|
264
|
+
return entries;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 读取指定目录(<enc> 段)的 sessions-index.json。永不抛:
|
|
269
|
+
* - 文件不存在/读失败/JSON.parse 失败/顶层结构不符/版本低于自身
|
|
270
|
+
* → { entries: 空 Map, higherVersion: false }(空索引 = 今天的全量行为)
|
|
271
|
+
* - 版本高于自身 → { entries: 空 Map, higherVersion: true }
|
|
272
|
+
* (整体忽略——即使 entries 合法也不消费,防陈旧 schema 灌入;RecordStore 据此抑制落盘)
|
|
273
|
+
* - 版本匹配 → 逐条 validateIndexEntry(坏条目丢弃,其余保留)
|
|
274
|
+
*
|
|
275
|
+
* 只读精确路径 INDEX_FILENAME:.tmp. 残留文件不匹配文件名,天然被忽略。
|
|
276
|
+
*/
|
|
277
|
+
export function loadIndex(encDir: string): LoadedSessionsIndex {
|
|
278
|
+
const empty: LoadedSessionsIndex = { entries: new Map(), higherVersion: false };
|
|
279
|
+
const indexPath = path.join(encDir, INDEX_FILENAME);
|
|
280
|
+
|
|
281
|
+
const raw = readIndexFile(indexPath, encDir);
|
|
282
|
+
if (raw === null) return empty;
|
|
283
|
+
|
|
284
|
+
const parsed = parseIndexJson(raw, indexPath);
|
|
285
|
+
if (parsed === null) return empty;
|
|
286
|
+
|
|
287
|
+
const top = readTopLevel(parsed, indexPath);
|
|
288
|
+
if (top === null) return empty;
|
|
222
289
|
|
|
223
290
|
if (top.version > INDEX_VERSION) {
|
|
224
291
|
// 高版本:整体忽略(entries 即使合法也不消费);调用方据 higherVersion 抑制写盘
|
|
@@ -232,12 +299,7 @@ export function loadIndex(encDir: string): LoadedSessionsIndex {
|
|
|
232
299
|
return empty;
|
|
233
300
|
}
|
|
234
301
|
|
|
235
|
-
|
|
236
|
-
for (const [key, value] of Object.entries(top.entries)) {
|
|
237
|
-
const entry = validateIndexEntry(value);
|
|
238
|
-
if (entry !== undefined) entries.set(key, entry);
|
|
239
|
-
}
|
|
240
|
-
return { entries, higherVersion: false };
|
|
302
|
+
return { entries: collectValidEntries(top.entries), higherVersion: false };
|
|
241
303
|
}
|
|
242
304
|
|
|
243
305
|
// ============================================================
|
|
@@ -245,60 +307,23 @@ export function loadIndex(encDir: string): LoadedSessionsIndex {
|
|
|
245
307
|
// ============================================================
|
|
246
308
|
|
|
247
309
|
/**
|
|
248
|
-
* 原子写索引(tmp
|
|
249
|
-
* ManifestStore.writeManifest
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
310
|
+
* 原子写索引(shared/atomic-write 统一原语,U6b 迁移:tmp → fsync → rename →
|
|
311
|
+
* fsync 目录)。原逐行实现(逐环复刻 ManifestStore.writeManifest 的生产模式)
|
|
312
|
+
* 与 writeAtomicFile 逐环等值;tmp 带 pid+单调序号双后缀防撞的语义由原语的
|
|
313
|
+
* atomicTmpPathFor 接管(pid 防两进程共用同一 tmp,seq+rand 防同进程并发
|
|
314
|
+
* saveIndex 共用同一 tmp)。rename 原子性保证读侧看到旧版或完整新版,绝无半成品。
|
|
315
|
+
* ensureDir:false——旧实现无 mkdir(目录由 sessionsDir 布局保证),缺目录维持
|
|
316
|
+
* fail-fast 上抛(调用方 .catch 兜底),不静默重建。
|
|
253
317
|
*
|
|
254
318
|
* 失败向上抛——RecordStore 的 flushIndexAfterScan 以 fire-and-forget .catch 消费
|
|
255
|
-
|
|
319
|
+
*(写失败不影响任何扫描结果,恢复 dirty 待下轮过窗重试)。
|
|
256
320
|
*/
|
|
257
321
|
export async function saveIndex(encDir: string, data: SessionsIndexData): Promise<void> {
|
|
258
322
|
const filePath = path.join(encDir, INDEX_FILENAME);
|
|
259
|
-
const tmpPath = `${filePath}.tmp.${process.pid}.${++tmpSeq}`;
|
|
260
323
|
const file: SessionsIndexFile = {
|
|
261
324
|
version: INDEX_VERSION,
|
|
262
325
|
pid: process.pid,
|
|
263
326
|
entries: Object.fromEntries(data.entries),
|
|
264
327
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
let renamed = false;
|
|
268
|
-
try {
|
|
269
|
-
// 1. 写 tmp → fsync 文件
|
|
270
|
-
const fh = await fsPromises.open(tmpPath, "w");
|
|
271
|
-
try {
|
|
272
|
-
await fh.writeFile(content, "utf-8");
|
|
273
|
-
await fh.sync();
|
|
274
|
-
} finally {
|
|
275
|
-
await fh.close();
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// 2. rename tmp → final(放在 try 内:失败时 catch 清理 tmp)
|
|
279
|
-
await fsPromises.rename(tmpPath, filePath);
|
|
280
|
-
renamed = true;
|
|
281
|
-
|
|
282
|
-
// 3. fsync 目录(best-effort:POSIX 不要求,失败不否定已成功的 rename)
|
|
283
|
-
try {
|
|
284
|
-
const dirFh = await fsPromises.open(encDir, "r");
|
|
285
|
-
try {
|
|
286
|
-
await dirFh.sync();
|
|
287
|
-
} finally {
|
|
288
|
-
await dirFh.close();
|
|
289
|
-
}
|
|
290
|
-
} catch (dirSyncErr) {
|
|
291
|
-
bestEffort(dirSyncErr, "fsync dir (saveIndex)");
|
|
292
|
-
}
|
|
293
|
-
} catch (err) {
|
|
294
|
-
// rename 未成功 → 清理残留 tmp(best-effort,不掩盖原错误)
|
|
295
|
-
if (!renamed) {
|
|
296
|
-
try {
|
|
297
|
-
await fsPromises.unlink(tmpPath);
|
|
298
|
-
} catch (cleanupErr) {
|
|
299
|
-
bestEffort(cleanupErr, "unlink tmp (saveIndex)");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
throw err;
|
|
303
|
-
}
|
|
328
|
+
await writeAtomicFile(filePath, JSON.stringify(file), { ensureDir: false });
|
|
304
329
|
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// src/execution/settled-watchdog.ts
|
|
2
|
+
//
|
|
3
|
+
// [T2-③ / LC-1] chatMode settled 等待两段式守护(回收层「上界族 + 无进展检测」共享原语)。
|
|
4
|
+
//
|
|
5
|
+
// 设计:docs/design/timeout-zcode-turn-and-settled-watchdog.md §6-D9 / §7(两段式重锚定,
|
|
6
|
+
// P0-4 核心)。等待 agent_settled 的窗口拆两段,判定语义与被保护对象逐段匹配:
|
|
7
|
+
//
|
|
8
|
+
// 中段(prompt → agent_end):工作段,输出即进展——无进展检测
|
|
9
|
+
// (armMidRoundNoProgress,锚点:prompt 发出时 arm;有效协议事件行刷新;
|
|
10
|
+
// 连续静默 SETTLED_MID_ROUND_NO_PROGRESS_MS=30min → kill + 该轮失败终态化)。
|
|
11
|
+
// 对齐 keep-alive 无进展检测先例(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS 同构:
|
|
12
|
+
// 「仍在推进」由 stdout 有效事件定义而非总时长,修复旧 10min 全程固定窗对
|
|
13
|
+
// >10min 合法 chatMode 单轮的误杀)。
|
|
14
|
+
//
|
|
15
|
+
// 收尾段(agent_end → agent_settled):post-run 收尾(compact 检查等)——固定硬上限
|
|
16
|
+
// (armSettledWatchdog,锚点:agent_end 到达时经 handoverMidRoundToSettled 交棒;
|
|
17
|
+
// 事件不刷新:收尾段内的任何输出都不能证明 settled 终将到达,刷新会让
|
|
18
|
+
// 「wedged 但仍有周期性输出」的进程无限续命——LC-9 已证 stdout 可有调试行)。
|
|
19
|
+
// 定值 600s 依据 P-T2c 已执行探针(probe/p-t2c-report.md):收尾段 6 轮真实会话
|
|
20
|
+
// 全部 <2ms、compact 30 万 tokens 40.1s,按探针自身降级规则 P99×10=401s<600s 成立。
|
|
21
|
+
//
|
|
22
|
+
// 两段独立计时:交棒时中段已过时间不继承——收尾段从 agent_end 起满 600s 才 fire
|
|
23
|
+
// (A7 验收判据「收尾段上界只在 agent_end 后计时」)。
|
|
24
|
+
//
|
|
25
|
+
// LC-1 三 wedged 场景覆盖复核(D9):① pi 版本偏斜无事件(agent_end 永不到达)→
|
|
26
|
+
// 中段 30min 静默回收;② post-run compact 卡死(agent_end 已到、settled 不来)→
|
|
27
|
+
// 收尾段 600s 回收;③ stdout 行损坏(settled 行被丢)→ 收尾段 600s 回收。
|
|
28
|
+
//
|
|
29
|
+
// 用户通道(规则 19):env XYZ_SUBAGENT_SETTLED_WATCHDOG_MS——>0 覆盖收尾段定值,
|
|
30
|
+
// ≤0 关闭两段(关闭即回到「settled 不 arm 则 idle timer 不挂、runSpawn 已在首轮返回、
|
|
31
|
+
// spawn watchdog 默认关」的三无窗口,warn 明示后果),未设/非法 = 默认 600s。
|
|
32
|
+
// 中段阈值 v1 不开 env(减法:与 keep-alive 先例同为纯常量)。
|
|
33
|
+
//
|
|
34
|
+
// 双挂载点共用同一原语——同一组常量 + 同一组挂载/交棒/清除 helper,仅两个 prompt
|
|
35
|
+
// 发出点(设计 T2-③ 明示架构,两处各写一套恰是被否的「散布姿势」微缩复发):
|
|
36
|
+
// - 首轮:session-runner.ts runSpawn(prompt 发出后 arm 中段)
|
|
37
|
+
// - 后续轮次热路径:subagent-service.ts deliverMessage(发出新一轮 prompt 后 arm 中段)
|
|
38
|
+
// 事件侧接线(两路径共用同一 stdout pump 闭包,均在 session-runner.ts):
|
|
39
|
+
// - 有效协议事件行 → refreshMidRoundNoProgress(中段刷新)
|
|
40
|
+
// - agent_end(chatMode,!willRetry)→ handoverMidRoundToSettled(交棒收尾段)
|
|
41
|
+
// - agent_settled / close / 收尾 → disarmSettledWatchdog(两段一并清)
|
|
42
|
+
//
|
|
43
|
+
// 与 lifecycle-manager 的 idle timer 互补:idle timer 管 settled 已到达后的空闲
|
|
44
|
+
// 回收,本原语管 settled 永不到达的 wedged——两条正交通道不互相替代。
|
|
45
|
+
//
|
|
46
|
+
// 实现形态对齐 armIdleTimer(recordId → entry 的模块级记账):重复 arm 先清旧
|
|
47
|
+
// timer、到期回调先自删条目再执行——挂载/清除责任内聚在原语,调用方不管理句柄
|
|
48
|
+
// 覆盖(防「忘清旧窗」散布错误)。同一 record 任意时刻只有一段 armed(交棒清中段
|
|
49
|
+
// 才挂收尾段),entry 记录当前段 + 收尾段回调(交棒点无需知道挂载方是谁)。
|
|
50
|
+
|
|
51
|
+
import { getLogger } from "../core/logger.ts";
|
|
52
|
+
import { assertSafeTimerDelay } from "../shared/timer-delay.ts";
|
|
53
|
+
|
|
54
|
+
const logger = getLogger("subagents");
|
|
55
|
+
|
|
56
|
+
/** 时间单位换算常量(命名后供 watchdog 常量组合,对齐 session-runner 同名先例,消除裸乘法字面量)。 */
|
|
57
|
+
const MS_PER_SECOND = 1000;
|
|
58
|
+
const SECONDS_PER_MINUTE = 60;
|
|
59
|
+
|
|
60
|
+
/** 中段无进展阈值(分钟):对齐 KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS 的 30min 量级。 */
|
|
61
|
+
const MID_ROUND_MINUTES = 30;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 收尾段固定硬上限默认值(600s,值不变——锚点从 prompt 发出改挂 agent_end 之后)。
|
|
65
|
+
*
|
|
66
|
+
* [P-T2c 已执行探针定案] 真实 pi 会话 agent_end→agent_settled 间隔 6 轮全部 <2ms
|
|
67
|
+
*(两事件行同 chunk 到达);30 万 tokens 上下文显式 compaction 耗时 40.1s——按探针
|
|
68
|
+
* 自身降级规则 P99×10 = 401s < 600s,600s 成立(probe/p-t2c-report.md)。
|
|
69
|
+
* 可被 env XYZ_SUBAGENT_SETTLED_WATCHDOG_MS(>0)覆盖,仅此一处定义。
|
|
70
|
+
*/
|
|
71
|
+
export const SETTLED_WATCHDOG_TIMEOUT_MS = 600_000;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 中段(prompt → agent_end)无进展检测的连续静默阈值(30min)。
|
|
75
|
+
*
|
|
76
|
+
* 对齐 KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS 先例(30min 无进展检测,量级同源)。
|
|
77
|
+
* 刷新面严格限定 stdout pump 解析出的**有效协议事件行**(message_* 与 tool_* 与 turn_end
|
|
78
|
+
* 等 SdkEvent)——LC-9 的 invalid 行(非法 JSON / 缺 type 字段的调试噪音)不刷新,
|
|
79
|
+
* 防调试输出续命。v1 不开 env(设计 §6-D9 定案:与 keep-alive 同为纯常量)。
|
|
80
|
+
* [export] 测试可观测。
|
|
81
|
+
*/
|
|
82
|
+
export const SETTLED_MID_ROUND_NO_PROGRESS_MS = MID_ROUND_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 收尾段定值的用户覆盖 env(规则 19:用户显式指定才生效)。
|
|
86
|
+
*
|
|
87
|
+
* >0 覆盖收尾段(SETTLED_WATCHDOG_TIMEOUT_MS 默认);≤0 关闭两段(回到三无窗口,
|
|
88
|
+
* warn 明示后果);未设 = 默认 600s;非数字 = 非法回落默认 + warn 留痕(对齐
|
|
89
|
+
* XYZ_SUBAGENT_IDLE_TIMEOUT_MS 的 LC-7 教训:非法回落必须可见)。前缀用
|
|
90
|
+
* XYZ_SUBAGENT_*(ENV_WHITELIST_PREFIXES 白名单,PI_ 前缀在桌面 spawn 链被静默
|
|
91
|
+
* 丢弃——同 XYZ_SUBAGENT_IDLE_TIMEOUT_MS 改名教训)。
|
|
92
|
+
*/
|
|
93
|
+
export const SETTLED_WATCHDOG_ENV = "XYZ_SUBAGENT_SETTLED_WATCHDOG_MS";
|
|
94
|
+
|
|
95
|
+
/** watchdog fire 时传给回调的段信息(失败文案按段分叉:窗长语义不同)。 */
|
|
96
|
+
export interface SettledWatchdogFireInfo {
|
|
97
|
+
/** 触发段:mid-round = 中段无进展;settled = 收尾段固定上界。 */
|
|
98
|
+
phase: "mid-round" | "settled";
|
|
99
|
+
/** 本段实际等待窗长(ms)——中段为常量,收尾段为 env 覆盖后的生效值。 */
|
|
100
|
+
waitedMs: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 单段 armed 的记账条目。同一 record 任意时刻至多一段 armed(交棒先清后挂)。 */
|
|
104
|
+
interface SettledWatchdogEntry {
|
|
105
|
+
timer: NodeJS.Timeout;
|
|
106
|
+
phase: "mid-round" | "settled";
|
|
107
|
+
/** 中段回调:armMidRoundNoProgress 登记;armSettledWatchdog 直挂收尾段时无。 */
|
|
108
|
+
onMidTimeout?: (info: SettledWatchdogFireInfo) => void;
|
|
109
|
+
/** 收尾段回调:prompt 发出点随 armMidRoundNoProgress 登记,交棒点经此取用。 */
|
|
110
|
+
onSettleTimeout: (info: SettledWatchdogFireInfo) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** recordId → armed watchdog entry。仅在等待窗口内 armed(挂载/清除成对)。 */
|
|
114
|
+
const armedEntries = new Map<string, SettledWatchdogEntry>();
|
|
115
|
+
|
|
116
|
+
// ── env 解析(惰性首读定案 + 缓存;_resetSettledWatchdogsForTest 清缓存供测试改 env)──
|
|
117
|
+
|
|
118
|
+
let envCache: { disabled: boolean; overrideMs?: number } | undefined;
|
|
119
|
+
|
|
120
|
+
function resolveSettledWatchdogEnv(): { disabled: boolean; overrideMs?: number } {
|
|
121
|
+
if (envCache) return envCache;
|
|
122
|
+
envCache = { disabled: false };
|
|
123
|
+
const raw = process.env[SETTLED_WATCHDOG_ENV];
|
|
124
|
+
if (raw === undefined || raw.trim() === "") return envCache; // 未设 = 默认 600s
|
|
125
|
+
const parsed = Number(raw);
|
|
126
|
+
if (!Number.isFinite(parsed)) {
|
|
127
|
+
logger.warn(
|
|
128
|
+
`[settled-watchdog] ${SETTLED_WATCHDOG_ENV}="${raw}" is invalid (expected a millisecond number) — ` +
|
|
129
|
+
`falling back to default settled phase limit (${SETTLED_WATCHDOG_TIMEOUT_MS}ms); ` +
|
|
130
|
+
`set a plain ms value (e.g. 60000) to override, or 0 to disable both phases`,
|
|
131
|
+
);
|
|
132
|
+
return envCache;
|
|
133
|
+
}
|
|
134
|
+
if (parsed <= 0) {
|
|
135
|
+
logger.warn(
|
|
136
|
+
`[settled-watchdog] ${SETTLED_WATCHDOG_ENV}=${parsed} disables BOTH watchdog phases (mid-round ` +
|
|
137
|
+
`no-progress + settled phase limit). Consequence: a wedged chatMode round (no agent_end, or ` +
|
|
138
|
+
`agent_settled never arriving) has NO independent recovery timer — the process leaks until the ` +
|
|
139
|
+
`host exits (the "three-no-window" shape). Recovery: unset the env or set a positive ms value.`,
|
|
140
|
+
);
|
|
141
|
+
envCache.disabled = true;
|
|
142
|
+
return envCache;
|
|
143
|
+
}
|
|
144
|
+
envCache.overrideMs = parsed;
|
|
145
|
+
return envCache;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 收尾段生效窗长:env >0 覆盖,否则默认 600s(文案拼接 / 单测断言用)。 */
|
|
149
|
+
export function getSettledWatchdogTimeoutMs(): number {
|
|
150
|
+
return resolveSettledWatchdogEnv().overrideMs ?? SETTLED_WATCHDOG_TIMEOUT_MS;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 两段守护是否被 env ≤0 显式关闭(arm 时 no-op 的唯一来源)。 */
|
|
154
|
+
export function isSettledWatchdogDisabled(): boolean {
|
|
155
|
+
return resolveSettledWatchdogEnv().disabled;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 清除某 record 的 entry(settled 到达 / close / resolveRun / 终态化处置任一发生即清,
|
|
160
|
+
* 两段一并清)。不存在 armed entry 时 no-op。
|
|
161
|
+
*/
|
|
162
|
+
function clearEntry(recordId: string): void {
|
|
163
|
+
const entry = armedEntries.get(recordId);
|
|
164
|
+
if (!entry) return;
|
|
165
|
+
clearTimeout(entry.timer);
|
|
166
|
+
armedEntries.delete(recordId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 挂载(或按新窗口重挂)某 record 的**中段**无进展检测(prompt 发出点调用)。
|
|
171
|
+
*
|
|
172
|
+
* - 重复 arm(同 recordId:新一轮 prompt)先清旧 entry——每轮窗口独立计时,不叠加
|
|
173
|
+
* (对齐 armIdleTimer 刷新语义);旧收尾段未清的异常形态也被本覆盖清掉(新 prompt
|
|
174
|
+
* = 新等待窗)。
|
|
175
|
+
* - 收尾段回调 onSettleTimeout 随 entry 记账:agent_end 交棒点
|
|
176
|
+
* (handoverMidRoundToSettled)无需知道挂载方是谁(首轮 runSpawn / 热路径
|
|
177
|
+
* deliverMessage 各传各的处置闭包)。
|
|
178
|
+
* - timer unref:不阻止主进程退出(回收兜底不阻塞 shutdown,对齐 idle timer /
|
|
179
|
+
* spawn watchdog 的 unref 语义)。
|
|
180
|
+
* - 到期行为由调用方注入(kill 进程 + 该轮失败终态化)——本模块不持有
|
|
181
|
+
* ChildProcess / record,与 lifecycle-manager 同构(副作用能力由调用方注入,
|
|
182
|
+
* 本模块只管 timer 生命周期,可独立编译 + 单测)。
|
|
183
|
+
* - env 显式关闭(≤0)时 no-op(解析时已 warn 明示后果)。
|
|
184
|
+
*/
|
|
185
|
+
export function armMidRoundNoProgress(
|
|
186
|
+
recordId: string,
|
|
187
|
+
handlers: {
|
|
188
|
+
onMidTimeout: (info: SettledWatchdogFireInfo) => void;
|
|
189
|
+
onSettleTimeout: (info: SettledWatchdogFireInfo) => void;
|
|
190
|
+
},
|
|
191
|
+
): void {
|
|
192
|
+
if (isSettledWatchdogDisabled()) return;
|
|
193
|
+
clearEntry(recordId);
|
|
194
|
+
// 常量恒在安全域内;入口统一校验防未来值改可配置时静默引入 1ms 溢出语义反转。
|
|
195
|
+
assertSafeTimerDelay(SETTLED_MID_ROUND_NO_PROGRESS_MS, "settled watchdog (mid-round)");
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
armedEntries.delete(recordId);
|
|
198
|
+
logger.debug(
|
|
199
|
+
`[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`,
|
|
200
|
+
);
|
|
201
|
+
handlers.onMidTimeout({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
|
|
202
|
+
}, SETTLED_MID_ROUND_NO_PROGRESS_MS);
|
|
203
|
+
timer.unref();
|
|
204
|
+
armedEntries.set(recordId, {
|
|
205
|
+
timer,
|
|
206
|
+
phase: "mid-round",
|
|
207
|
+
onMidTimeout: handlers.onMidTimeout,
|
|
208
|
+
onSettleTimeout: handlers.onSettleTimeout,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 挂载(或按新窗口重挂)某 record 的**收尾段**固定硬上限(锚点:agent_end 之后)。
|
|
214
|
+
*
|
|
215
|
+
* 生产路径经 handoverMidRoundToSettled 调用(交棒语义内聚);导出保留供清理面
|
|
216
|
+
* 测试直接挂载(disarm 消费同一 entry,与段无关)与未来独立收尾场景。
|
|
217
|
+
* - 事件不刷新(收尾段语义:窗口内输出不能证明 settled 将到达——见头注释)。
|
|
218
|
+
* - env >0 覆盖窗长;到期回调先自删条目再执行(onTimeout 内重新 arm 不会被误删)。
|
|
219
|
+
* - env 显式关闭时 no-op。
|
|
220
|
+
*/
|
|
221
|
+
export function armSettledWatchdog(recordId: string, onTimeout: (info: SettledWatchdogFireInfo) => void): void {
|
|
222
|
+
if (isSettledWatchdogDisabled()) return;
|
|
223
|
+
clearEntry(recordId);
|
|
224
|
+
const windowMs = getSettledWatchdogTimeoutMs();
|
|
225
|
+
// env 可配置值:入口校验防越界值静默引入 1ms 溢出语义反转(非法值在 env 解析层
|
|
226
|
+
// 已回落,此处是纵深防御)。
|
|
227
|
+
assertSafeTimerDelay(windowMs, "settled watchdog");
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
armedEntries.delete(recordId);
|
|
230
|
+
logger.debug(
|
|
231
|
+
`[settled-watchdog] settled phase fired for ${recordId} after ${windowMs}ms without agent_settled`,
|
|
232
|
+
);
|
|
233
|
+
onTimeout({ phase: "settled", waitedMs: windowMs });
|
|
234
|
+
}, windowMs);
|
|
235
|
+
timer.unref();
|
|
236
|
+
armedEntries.set(recordId, { timer, phase: "settled", onSettleTimeout: onTimeout });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* [D9 交棒] agent_end 到达:中段让位收尾段。
|
|
241
|
+
*
|
|
242
|
+
* 清中段计时(已过时间不继承),用挂载点记账的 onSettleTimeout 挂收尾段(从
|
|
243
|
+
* agent_end 起独立计时)。未挂载(非 chatMode / env 关闭 / 已清)或已交棒 /
|
|
244
|
+
* 已 fire 时幂等 no-op——stdout pump 对每轮 agent_end 都调本函数,幂等由
|
|
245
|
+
* phase 判定保证。
|
|
246
|
+
*/
|
|
247
|
+
export function handoverMidRoundToSettled(recordId: string): void {
|
|
248
|
+
const entry = armedEntries.get(recordId);
|
|
249
|
+
if (!entry || entry.phase !== "mid-round") return;
|
|
250
|
+
const onSettleTimeout = entry.onSettleTimeout;
|
|
251
|
+
clearEntry(recordId);
|
|
252
|
+
armSettledWatchdog(recordId, onSettleTimeout);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* [D9 中段刷新] 有效协议事件行到达:刷新中段静默计时(重挂同窗,先清旧)。
|
|
257
|
+
*
|
|
258
|
+
* 未挂载 / 已交棒(收尾段不刷新——刷新会让收尾段失去唯一可收敛形态,D9 被否 (b)
|
|
259
|
+
* 方案的否决理由)/ 已 fire 时 no-op。刷新面由调用方限定在 stdout pump 的合法
|
|
260
|
+
* SdkEvent 行(invalid 行不调用本函数)。
|
|
261
|
+
*/
|
|
262
|
+
export function refreshMidRoundNoProgress(recordId: string): void {
|
|
263
|
+
const entry = armedEntries.get(recordId);
|
|
264
|
+
if (!entry || entry.phase !== "mid-round") return;
|
|
265
|
+
clearTimeout(entry.timer);
|
|
266
|
+
assertSafeTimerDelay(SETTLED_MID_ROUND_NO_PROGRESS_MS, "settled watchdog (mid-round refresh)");
|
|
267
|
+
const onMidTimeout = entry.onMidTimeout;
|
|
268
|
+
const timer = setTimeout(() => {
|
|
269
|
+
armedEntries.delete(recordId);
|
|
270
|
+
logger.debug(
|
|
271
|
+
`[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`,
|
|
272
|
+
);
|
|
273
|
+
onMidTimeout?.({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
|
|
274
|
+
}, SETTLED_MID_ROUND_NO_PROGRESS_MS);
|
|
275
|
+
timer.unref();
|
|
276
|
+
entry.timer = timer;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 查询某 record 是否有 armed watchdog entry(测试/接入期断言用;两段同账本)。
|
|
281
|
+
*/
|
|
282
|
+
export function hasSettledWatchdog(recordId: string): boolean {
|
|
283
|
+
return armedEntries.has(recordId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 查询某 record 当前 armed 段(测试断言「交棒切换」用)。未挂载返回 undefined。
|
|
288
|
+
*/
|
|
289
|
+
export function getSettledWatchdogPhase(recordId: string): "mid-round" | "settled" | undefined {
|
|
290
|
+
return armedEntries.get(recordId)?.phase;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 清除某 record 的 armed watchdog(settled 到达 / close / resolveRun 任一发生
|
|
295
|
+
* 即清,两段一并清)。不存在 armed entry 时 no-op。
|
|
296
|
+
*/
|
|
297
|
+
export function disarmSettledWatchdog(recordId: string): void {
|
|
298
|
+
clearEntry(recordId);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 清空全部 armed entry + env 解析缓存(测试隔离用,beforeEach 调;命名对齐
|
|
303
|
+
* _resetLifecycleState 先例。清缓存使测试内 vi.stubEnv 改 env 后重新解析生效)。
|
|
304
|
+
*/
|
|
305
|
+
export function _resetSettledWatchdogsForTest(): void {
|
|
306
|
+
for (const entry of armedEntries.values()) {
|
|
307
|
+
clearTimeout(entry.timer);
|
|
308
|
+
}
|
|
309
|
+
armedEntries.clear();
|
|
310
|
+
envCache = undefined;
|
|
311
|
+
}
|