@zhushanwen/subagent-core 0.2.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 +114 -0
- package/dist/chunk-3VOERJPJ.js +22 -0
- package/dist/chunk-A4IVZWVX.js +31 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
- package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
- package/dist/execution/engine/engines/zcode/constants.js +22 -0
- package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
- package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
- package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
- package/dist/execution/engine/engines/zcode/reader.js +239 -0
- package/dist/execution/engine/paths.cjs +55 -0
- package/dist/execution/engine/paths.d.cts +8 -0
- package/dist/execution/engine/paths.d.ts +8 -0
- package/dist/execution/engine/paths.js +26 -0
- package/dist/execution/relay-env.cjs +62 -0
- package/dist/execution/relay-env.d.cts +33 -0
- package/dist/execution/relay-env.d.ts +33 -0
- package/dist/execution/relay-env.js +20 -0
- package/dist/index.cjs +1852 -0
- package/dist/index.d.cts +1184 -0
- package/dist/index.d.ts +1184 -0
- package/dist/index.js +1810 -0
- package/dist/types-BxyAidGf.d.cts +875 -0
- package/dist/types-BxyAidGf.d.ts +875 -0
- package/package.json +99 -0
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
- package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
- package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
- package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
- package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
- package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
- package/src/__tests__/m2-append-content-probe.test.ts +73 -0
- package/src/__tests__/manifest-store.test.ts +368 -0
- package/src/__tests__/record-store-cache.test.ts +335 -0
- package/src/__tests__/record-store-index.test.ts +560 -0
- package/src/__tests__/record-store-last-line.test.ts +129 -0
- package/src/__tests__/review-fix-loop-script.test.ts +288 -0
- package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
- package/src/__tests__/robustness-low-batch1.test.ts +286 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
- package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
- package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
- package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
- package/src/__tests__/session-runner.test.ts +69 -0
- package/src/__tests__/smoke.test.ts +9 -0
- package/src/__tests__/stream-sink.test.ts +203 -0
- package/src/core/__tests__/host-services.test.ts +144 -0
- package/src/core/__tests__/logger.test.ts +78 -0
- package/src/core/__tests__/notify-ports.test.ts +104 -0
- package/src/core/host-services.ts +97 -0
- package/src/core/logger.ts +47 -0
- package/src/core/notify-ports.ts +126 -0
- package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
- package/src/execution/__tests__/config.test.ts +302 -0
- package/src/execution/__tests__/delivery-methods.test.ts +424 -0
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
- package/src/execution/__tests__/execute-nesting.test.ts +323 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
- package/src/execution/__tests__/execution-record.test.ts +1394 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
- package/src/execution/__tests__/finalize-record.test.ts +367 -0
- package/src/execution/__tests__/finalized-marker.test.ts +104 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
- package/src/execution/__tests__/model-resolver.test.ts +465 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/output-collector.test.ts +358 -0
- package/src/execution/__tests__/path-encoding.test.ts +104 -0
- package/src/execution/__tests__/pi-invocation.test.ts +134 -0
- package/src/execution/__tests__/record-store.test.ts +1057 -0
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
- package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
- package/src/execution/__tests__/relay-env.test.ts +42 -0
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +293 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
- package/src/execution/__tests__/spawn-args.test.ts +445 -0
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
- package/src/execution/__tests__/stdin-writer.test.ts +462 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +788 -0
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +615 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +633 -0
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +199 -0
- package/src/execution/agent-registry.ts +199 -0
- package/src/execution/agent-result-mapper.ts +90 -0
- package/src/execution/alive-store.ts +119 -0
- package/src/execution/argv-mirror.ts +113 -0
- package/src/execution/best-effort.ts +38 -0
- package/src/execution/channel-registry-access.ts +144 -0
- package/src/execution/concurrency-pool.ts +116 -0
- package/src/execution/config.ts +165 -0
- package/src/execution/dialog-queue.ts +330 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
- package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
- package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
- package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
- package/src/execution/engine/__tests__/paths.test.ts +39 -0
- package/src/execution/engine/__tests__/registry.test.ts +120 -0
- package/src/execution/engine/__tests__/routing.test.ts +231 -0
- package/src/execution/engine/common/data-dir.ts +66 -0
- package/src/execution/engine/common/errors.ts +183 -0
- package/src/execution/engine/common/event-journal.ts +254 -0
- package/src/execution/engine/common/journal-replay.ts +62 -0
- package/src/execution/engine/common/kill-chain.ts +221 -0
- package/src/execution/engine/common/nesting-guard.ts +50 -0
- package/src/execution/engine/common/persona-router.ts +108 -0
- package/src/execution/engine/common/pool-manager.ts +226 -0
- package/src/execution/engine/common/schema-emulation.ts +189 -0
- package/src/execution/engine/common/session-view-projection.ts +51 -0
- package/src/execution/engine/engine-discovery.ts +65 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
- package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
- package/src/execution/engine/engines/pi/reader.ts +48 -0
- package/src/execution/engine/engines/pi/registration.ts +35 -0
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
- package/src/execution/engine/engines/zcode/constants.ts +43 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
- package/src/execution/engine/engines/zcode/launcher.ts +161 -0
- package/src/execution/engine/engines/zcode/parser.ts +436 -0
- package/src/execution/engine/engines/zcode/preparer.ts +363 -0
- package/src/execution/engine/engines/zcode/reader.ts +381 -0
- package/src/execution/engine/engines/zcode/registration.ts +37 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
- package/src/execution/engine/host-task-spec.ts +47 -0
- package/src/execution/engine/model-prompt.ts +158 -0
- package/src/execution/engine/paths.ts +42 -0
- package/src/execution/engine/port.ts +153 -0
- package/src/execution/engine/registry.ts +133 -0
- package/src/execution/engine/routing.ts +218 -0
- package/src/execution/engine/types.ts +309 -0
- package/src/execution/execute-options-mapper.ts +115 -0
- package/src/execution/execution-record.ts +984 -0
- package/src/execution/finalize-record.ts +254 -0
- package/src/execution/finalized-marker.ts +70 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +56 -0
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +513 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +256 -0
- package/src/execution/model-config-service.ts +250 -0
- package/src/execution/model-resolver.ts +248 -0
- package/src/execution/notifier.ts +372 -0
- package/src/execution/notify-ledger.ts +580 -0
- package/src/execution/output-collector.ts +228 -0
- package/src/execution/path-encoding.ts +60 -0
- package/src/execution/pi-invocation.ts +120 -0
- package/src/execution/record-entry.ts +132 -0
- package/src/execution/record-store.ts +1265 -0
- package/src/execution/relay-env.ts +37 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +120 -0
- package/src/execution/session-pending.ts +170 -0
- package/src/execution/session-reconstructor.ts +678 -0
- package/src/execution/session-runner.ts +1789 -0
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/spawn-event-adapter.ts +363 -0
- package/src/execution/stdin-writer.ts +198 -0
- package/src/execution/stream-sink.ts +126 -0
- package/src/execution/subagent-service.ts +2268 -0
- package/src/execution/subprocess-agent-runner.ts +317 -0
- package/src/execution/temp-prompt.ts +62 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +102 -0
- package/src/execution/types.ts +1023 -0
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +243 -0
- package/src/execution/ui-request-observability.ts +81 -0
- package/src/execution/ui-request-queue.ts +184 -0
- package/src/execution/worktree-manager.ts +688 -0
- package/src/execution/worktree-registry.ts +279 -0
- package/src/index.ts +87 -0
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
- package/src/orchestration/__tests__/args-validator.test.ts +143 -0
- package/src/orchestration/__tests__/config-loader.test.ts +503 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
- package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
- package/src/orchestration/__tests__/script-lint.test.ts +831 -0
- package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
- package/src/orchestration/__tests__/worker-host.test.ts +120 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/agent-opts-resolver.ts +174 -0
- package/src/orchestration/args-validator.ts +127 -0
- package/src/orchestration/config-loader.ts +316 -0
- package/src/orchestration/error-recovery.ts +1018 -0
- package/src/orchestration/execute-agent-call.ts +256 -0
- package/src/orchestration/launcher.ts +455 -0
- package/src/orchestration/lifecycle.ts +399 -0
- package/src/orchestration/models/__tests__/budget.test.ts +307 -0
- package/src/orchestration/models/agent-call.ts +83 -0
- package/src/orchestration/models/budget.ts +118 -0
- package/src/orchestration/models/ports.ts +164 -0
- package/src/orchestration/models/run-runtime.ts +104 -0
- package/src/orchestration/models/run-spec.ts +83 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +183 -0
- package/src/orchestration/models/types.ts +301 -0
- package/src/orchestration/models/workflow-run.ts +254 -0
- package/src/orchestration/models/workflow-script-registry.ts +35 -0
- package/src/orchestration/models/workflow-script.ts +117 -0
- package/src/orchestration/script-lint.ts +766 -0
- package/src/orchestration/skill-discovery.ts +135 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +98 -0
- package/src/orchestration/worker-script-builder.ts +421 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +133 -0
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/meta-parser.test.ts +304 -0
- package/src/shared/__tests__/model-ref.test.ts +306 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
- package/src/shared/__tests__/resource-discovery.test.ts +749 -0
- package/src/shared/__tests__/resource-meta.test.ts +51 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/__tests__/timer-delay.test.ts +61 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/agent-ref.ts +57 -0
- package/src/shared/meta-parser.ts +261 -0
- package/src/shared/model-ref.ts +286 -0
- package/src/shared/resource-discovery.ts +835 -0
- package/src/shared/resource-meta.ts +65 -0
- package/src/shared/schema-env.ts +44 -0
- package/src/shared/schema-jsonify.ts +58 -0
- package/src/shared/timer-delay.ts +54 -0
- package/src/shared/xml-injection.ts +35 -0
- package/workflows/README.md +81 -0
- package/workflows/_shared/agent-refs.cjs +40 -0
- package/workflows/chain.js +137 -0
- package/workflows/map-reduce.js +180 -0
- package/workflows/parallel.js +154 -0
- package/workflows/review-fix-loop-utils.cjs +1542 -0
- package/workflows/review-fix-loop.js +1605 -0
- package/workflows/scatter-gather.js +184 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// src/execution/sessions-index.ts
|
|
2
|
+
//
|
|
3
|
+
// [perf L-1] sessions-index.json —— identity 探测结果的磁盘种子。
|
|
4
|
+
//
|
|
5
|
+
// 背景:RecordStore 冷启动首扫需逐个探测 sessions/*.jsonl 的 identity(头部 64KB →
|
|
6
|
+
// 尾部 64KB → 全文 fallback)。session 数量大时该探测是冷扫描的主要成本。本模块把
|
|
7
|
+
// 「本目录的探测结论」(per-file identity 或负标记)持久化到 sessionsDir 的兄弟位置
|
|
8
|
+
// (<enc> 段内),下次进程/实例首扫直接用索引条目构造缓存条目(零内容读取)。
|
|
9
|
+
//
|
|
10
|
+
// 语义:
|
|
11
|
+
// - 读侧(loadIndex)永不抛:文件缺失/JSON 损坏/结构不符/版本低 → 空索引(= 今天的
|
|
12
|
+
// 全量行为,下轮 dirty 重写自愈);版本高于自身 → 空索引 + higherVersion 标志
|
|
13
|
+
// (整体忽略不消费,RecordStore 据此抑制本轮及后续落盘,防 v1/v2 last-writer-wins
|
|
14
|
+
// 覆盖振荡)。
|
|
15
|
+
// - 写侧(saveIndex):tmp(pid+seq)+fsync+rename+目录 fsync 原子写(逐环复刻
|
|
16
|
+
// ManifestStore.writeManifest 的生产模式)。失败本身向上抛;fire-and-forget 的
|
|
17
|
+
// .catch 兜底在 RecordStore 侧。
|
|
18
|
+
// - 损坏/降级走 logger.debug(PI_EXT_DEBUG=1 可见,默认 no-op),不 console.error
|
|
19
|
+
// ——索引是纯性能缓存,降级自愈不应告警。
|
|
20
|
+
//
|
|
21
|
+
// 无状态纯函数模块:不依赖 RecordStore 任何内部状态。
|
|
22
|
+
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as fsPromises from "node:fs/promises";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
|
|
27
|
+
import { getLogger } from "../core/logger.ts";
|
|
28
|
+
|
|
29
|
+
import { bestEffort } from "./best-effort.ts";
|
|
30
|
+
import type { ExecutionMode } from "./types.ts";
|
|
31
|
+
|
|
32
|
+
const logger = getLogger("subagents");
|
|
33
|
+
|
|
34
|
+
// ============================================================
|
|
35
|
+
// 常量(测试锚点 SSOT:RecordStore 与测试从同一来源引用,禁止内联字面量)
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
/** 索引文件名(落 sessionsDir 兄弟位置——写索引不改 sessionsDir 目录 mtime,不击穿 L0 快路径)。 */
|
|
39
|
+
export const INDEX_FILENAME = "sessions-index.json";
|
|
40
|
+
|
|
41
|
+
/** 索引格式版本。schema 变更必须递增:低版本文件整体丢弃(空索引,下轮 dirty 重写自愈);高版本整体忽略(higherVersion,不重写)。 */
|
|
42
|
+
export const INDEX_VERSION = 1;
|
|
43
|
+
|
|
44
|
+
/** 两次成功落盘的最小墙钟间隔(节流:overlay 打开期间的高频扫描不放大磁盘写)。 */
|
|
45
|
+
export const INDEX_WRITE_MIN_INTERVAL_MS = 60_000;
|
|
46
|
+
|
|
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
|
+
// ============================================================
|
|
55
|
+
// 类型(DM1 磁盘顶层 + DM2 条目)
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 正索引条目:「该文件有 identity」的身份字段 + stat 戳。
|
|
60
|
+
*
|
|
61
|
+
* 不含 sessionFile(加载侧由 sessionsDir + basename 重构,绝对路径因 agentDir 迁移
|
|
62
|
+
* 整体失效);不含 forkDepth/chatMode(投影源 SubagentRecord 无此二字段,buildRecord
|
|
63
|
+
* 构造 light 时不读——存了也无消费方;未来需要时升 INDEX_VERSION)。
|
|
64
|
+
*/
|
|
65
|
+
export interface SessionsIndexEntry {
|
|
66
|
+
/** tagged union 判别(负条目为 true)。显式声明 false 供 TS narrowing(与 record-store.ts FileCacheEntry 同款)。 */
|
|
67
|
+
negative?: false;
|
|
68
|
+
/** jsonl stat 戳(mtime+size)——不匹配 = 内容变化,调用方回退重探测。 */
|
|
69
|
+
mtimeMs: number;
|
|
70
|
+
size: number;
|
|
71
|
+
id: string;
|
|
72
|
+
agent: string;
|
|
73
|
+
/** 执行模式。运行时校验宽容历史 "sync" 值(白名单 sync|background,镜像 isIdentityData);类型层由 ExecutionMode 收窄。 */
|
|
74
|
+
mode: ExecutionMode;
|
|
75
|
+
task: string;
|
|
76
|
+
slug: string;
|
|
77
|
+
startedAt: number;
|
|
78
|
+
/** undefined 表示缺失(旧文件/顶层)。JSON 序列化时被丢弃,往返保持 undefined。 */
|
|
79
|
+
rootSessionId: string | undefined;
|
|
80
|
+
parentRecordId: string | undefined;
|
|
81
|
+
depth: number;
|
|
82
|
+
/** 空串合法:尾部探测(readIdentityTail)拿不到 model 时的合法结果,不当损坏。 */
|
|
83
|
+
model: string;
|
|
84
|
+
thinkingLevel: string | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 负索引条目:「该文件确认无 identity」(junk/异构文件)。与内存负缓存
|
|
89
|
+
* NegativeFileEntry 语义对齐——把「没有」这一事实持久化,跨实例零重探测。
|
|
90
|
+
*/
|
|
91
|
+
export interface SessionsIndexNegativeEntry {
|
|
92
|
+
negative: true;
|
|
93
|
+
mtimeMs: number;
|
|
94
|
+
size: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 磁盘 JSON 顶层结构(key = jsonl basename 不含路径)。 */
|
|
98
|
+
export interface SessionsIndexFile {
|
|
99
|
+
version: 1;
|
|
100
|
+
pid: number;
|
|
101
|
+
entries: Record<string, SessionsIndexEntry | SessionsIndexNegativeEntry>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** saveIndex 输入(内存形态)。 */
|
|
105
|
+
export interface SessionsIndexData {
|
|
106
|
+
entries: Map<string, SessionsIndexEntry | SessionsIndexNegativeEntry>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** loadIndex 输出。恒返回该形态对象(不抛、不返回 null)。 */
|
|
110
|
+
export interface LoadedSessionsIndex {
|
|
111
|
+
entries: Map<string, SessionsIndexEntry | SessionsIndexNegativeEntry>;
|
|
112
|
+
/** true = 磁盘版本高于自身:条目整体不消费,且本进程及后续不得写盘(防 v1/v2 互相覆盖振荡)。 */
|
|
113
|
+
higherVersion: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================
|
|
117
|
+
// 条目级校验(ES3:单条目损坏仅丢弃该条目,不放大为整体失效)
|
|
118
|
+
// ============================================================
|
|
119
|
+
|
|
120
|
+
/** 正条目类型谓词:镜像 isIdentityData(session-reconstructor.ts:244-253)的字段检查 + 索引特有戳/形态字段。 */
|
|
121
|
+
function isPositiveIndexEntry(raw: unknown): raw is SessionsIndexEntry {
|
|
122
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
123
|
+
const v = raw as Record<string, unknown>;
|
|
124
|
+
return (
|
|
125
|
+
typeof v.mtimeMs === "number" &&
|
|
126
|
+
typeof v.size === "number" &&
|
|
127
|
+
typeof v.id === "string" &&
|
|
128
|
+
typeof v.agent === "string" &&
|
|
129
|
+
// 宽容历史 "sync" 值(ExecutionMode 现值仅 "background","sync" 是旧数据合法值,放行不丢弃)
|
|
130
|
+
(v.mode === "sync" || v.mode === "background") &&
|
|
131
|
+
typeof v.task === "string" &&
|
|
132
|
+
typeof v.slug === "string" &&
|
|
133
|
+
typeof v.startedAt === "number" &&
|
|
134
|
+
(v.rootSessionId === undefined || typeof v.rootSessionId === "string") &&
|
|
135
|
+
(v.parentRecordId === undefined || typeof v.parentRecordId === "string") &&
|
|
136
|
+
typeof v.depth === "number" &&
|
|
137
|
+
typeof v.model === "string" && // 空串合法(DS4)
|
|
138
|
+
(v.thinkingLevel === undefined || typeof v.thinkingLevel === "string")
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 负条目类型谓词(tagged union 形态检查)。 */
|
|
143
|
+
function isNegativeIndexEntry(raw: unknown): raw is SessionsIndexNegativeEntry {
|
|
144
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
145
|
+
const v = raw as Record<string, unknown>;
|
|
146
|
+
return v.negative === true && typeof v.mtimeMs === "number" && typeof v.size === "number";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 校验单条索引条目。返回 undefined = 条目损坏,调用方仅丢弃该条目
|
|
151
|
+
* (该文件本轮回退探测,其余条目正常保留/命中)。
|
|
152
|
+
*/
|
|
153
|
+
export function validateIndexEntry(
|
|
154
|
+
raw: unknown,
|
|
155
|
+
): SessionsIndexEntry | SessionsIndexNegativeEntry | undefined {
|
|
156
|
+
if (!isPositiveIndexEntry(raw) && !isNegativeIndexEntry(raw)) return undefined;
|
|
157
|
+
return raw;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================
|
|
161
|
+
// 读侧(IF1:永不抛)
|
|
162
|
+
// ============================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 读取指定目录(<enc> 段)的 sessions-index.json。永不抛:
|
|
166
|
+
* - 文件不存在/读失败/JSON.parse 失败/顶层结构不符/版本低于自身
|
|
167
|
+
* → { entries: 空 Map, higherVersion: false }(空索引 = 今天的全量行为)
|
|
168
|
+
* - 版本高于自身 → { entries: 空 Map, higherVersion: true }
|
|
169
|
+
* (整体忽略——即使 entries 合法也不消费,防陈旧 schema 灌入;RecordStore 据此抑制落盘)
|
|
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);
|
|
177
|
+
|
|
178
|
+
let raw: string;
|
|
179
|
+
try {
|
|
180
|
+
raw = fs.readFileSync(indexPath, "utf-8");
|
|
181
|
+
} catch (err) {
|
|
182
|
+
// ENOENT = 正常首跑,保持静默;其余读失败(EACCES 等长期权限异常)留 debug 线索
|
|
183
|
+
// ——空索引回退本身可自愈,但权限类异常不会自己消失,需可诊断。
|
|
184
|
+
const code = err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : undefined;
|
|
185
|
+
if (code !== "ENOENT") {
|
|
186
|
+
logger.debug("[subagents] sessions-index read failed, fallback to empty", {
|
|
187
|
+
detail: { dir: encDir, code },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return empty;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let parsed: unknown;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(raw);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// 损坏(截断/外部编辑)走 debug:可降级自愈场景,不 console.error
|
|
198
|
+
logger.debug("[subagents] sessions-index corrupted JSON, fallback to empty", {
|
|
199
|
+
detail: { path: indexPath, error: err instanceof Error ? err.message : String(err) },
|
|
200
|
+
});
|
|
201
|
+
return empty;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
205
|
+
logger.debug("[subagents] sessions-index invalid top-level shape, fallback to empty", {
|
|
206
|
+
detail: { path: indexPath },
|
|
207
|
+
});
|
|
208
|
+
return empty;
|
|
209
|
+
}
|
|
210
|
+
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
|
+
) {
|
|
217
|
+
logger.debug("[subagents] sessions-index invalid header fields, fallback to empty", {
|
|
218
|
+
detail: { path: indexPath },
|
|
219
|
+
});
|
|
220
|
+
return empty;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (top.version > INDEX_VERSION) {
|
|
224
|
+
// 高版本:整体忽略(entries 即使合法也不消费);调用方据 higherVersion 抑制写盘
|
|
225
|
+
return { entries: new Map(), higherVersion: true };
|
|
226
|
+
}
|
|
227
|
+
if (top.version < INDEX_VERSION) {
|
|
228
|
+
// 低版本:整体丢弃 → 空索引;本轮全扫 dirty 后重写自愈
|
|
229
|
+
logger.debug("[subagents] sessions-index stale version, discarded", {
|
|
230
|
+
detail: { path: indexPath, version: top.version, expected: INDEX_VERSION },
|
|
231
|
+
});
|
|
232
|
+
return empty;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const entries = new Map<string, SessionsIndexEntry | SessionsIndexNegativeEntry>();
|
|
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 };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============================================================
|
|
244
|
+
// 写侧(IF2:原子写;失败向上抛——fire-and-forget 的 .catch 兜底在调用方)
|
|
245
|
+
// ============================================================
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 原子写索引(tmp(pid+seq) → fsync → rename → fsync 目录;逐环复刻
|
|
249
|
+
* ManifestStore.writeManifest 的生产模式)。tmp 带 pid+单调序号双后缀:pid 防两进程
|
|
250
|
+
* 共用同一 tmp,seq 防同进程内并发 saveIndex(节流基准在写成功后才推进,W1 在途时
|
|
251
|
+
* 新一轮过窗扫描可 dispatch W2)共用同一 tmp;rename 原子性保证读侧看到旧版或完整
|
|
252
|
+
* 新版,绝无半成品。
|
|
253
|
+
*
|
|
254
|
+
* 失败向上抛——RecordStore 的 flushIndexAfterScan 以 fire-and-forget .catch 消费
|
|
255
|
+
* (写失败不影响任何扫描结果,恢复 dirty 待下轮过窗重试)。
|
|
256
|
+
*/
|
|
257
|
+
export async function saveIndex(encDir: string, data: SessionsIndexData): Promise<void> {
|
|
258
|
+
const filePath = path.join(encDir, INDEX_FILENAME);
|
|
259
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${++tmpSeq}`;
|
|
260
|
+
const file: SessionsIndexFile = {
|
|
261
|
+
version: INDEX_VERSION,
|
|
262
|
+
pid: process.pid,
|
|
263
|
+
entries: Object.fromEntries(data.entries),
|
|
264
|
+
};
|
|
265
|
+
const content = JSON.stringify(file);
|
|
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
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// src/core/spawn-event-adapter.ts
|
|
2
|
+
//
|
|
3
|
+
// pi 子进程 stdout JSON 事件流的解析器。Core 叶子原语(仅依赖 types.ts)。
|
|
4
|
+
//
|
|
5
|
+
// spawn 改造的基座模块。session-runner runSpawn 用 `pi --mode rpc` spawn 子进程。
|
|
6
|
+
// RPC mode 不向 stdout 输出 header 行(只有 json/print mode 才输出),故 runSpawn 额外
|
|
7
|
+
// 通过 get_state RPC 握手回填 sessionFile/sessionId。两种 stdout 行形态本模块统一解析:
|
|
8
|
+
// 1. header 行(json/print mode 首行):{ type: "session", id, timestamp, cwd, ... }
|
|
9
|
+
// —— session 元信息,含 session id(RPC mode 不发,靠 get_state 握手替代)
|
|
10
|
+
// 2. 事件行:{ type: "tool_execution_start" | "message_end" | ..., ... }
|
|
11
|
+
// —— 与 in-process session.subscribe 收到的 SdkEvent 同源同构
|
|
12
|
+
//
|
|
13
|
+
// 本模块只做「行 → 分类事件对象」的纯解析,不做累积/翻译(那是 runSpawn +
|
|
14
|
+
// handleSdkEvent 的职责)。刻意保持薄,便于单测。
|
|
15
|
+
//
|
|
16
|
+
// 与 session-runner.handleSdkEvent 的契约:parseSpawnLine 返回 kind:"event" 的
|
|
17
|
+
// event 字段即为 SdkEvent,可直接喂给 handleSdkEvent(事件 type schema 一致,
|
|
18
|
+
// 由 print-mode.ts:106 JSON.stringify(event) 保证同源)。
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
|
|
23
|
+
import type { SdkEvent } from "./types.ts";
|
|
24
|
+
|
|
25
|
+
/** pi stdout header 行(session 元信息)。type 固定为 "session"。 */
|
|
26
|
+
export interface SpawnSessionHeader {
|
|
27
|
+
readonly type: "session";
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly timestamp: string;
|
|
30
|
+
readonly cwd: string;
|
|
31
|
+
readonly parentSession?: string;
|
|
32
|
+
readonly version?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pi 原生 extension_ui_request 的方法特定字段(按 method 平铺)。
|
|
36
|
+
* 与 Pi rpc-types.ts L230-265 的 RpcExtensionUIRequest 1:1 对应。
|
|
37
|
+
* method 是判别字段;每个变体仅列出该 method 的已知字段(可选字段保持可选)。
|
|
38
|
+
* 未知 method 走 string fallback(保留 raw 字段,避免协议演进时丢字段)。 */
|
|
39
|
+
export type ExtensionUiRequest =
|
|
40
|
+
| { method: "select"; title: string; options: string[]; timeout?: number }
|
|
41
|
+
| { method: "confirm"; title: string; message: string; timeout?: number }
|
|
42
|
+
| { method: "input"; title: string; placeholder?: string; timeout?: number }
|
|
43
|
+
| { method: "editor"; title: string; prefill?: string }
|
|
44
|
+
| { method: "notify"; message: string; notifyType?: "info" | "warning" | "error" }
|
|
45
|
+
| { method: "setStatus"; statusKey: string; statusText: string | undefined }
|
|
46
|
+
| {
|
|
47
|
+
method: "setWidget";
|
|
48
|
+
widgetKey: string;
|
|
49
|
+
widgetLines: string[] | undefined;
|
|
50
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
51
|
+
}
|
|
52
|
+
| { method: "setTitle"; title: string }
|
|
53
|
+
| { method: "set_editor_text"; text: string }
|
|
54
|
+
// 未知 method fallback:保留原始字段,避免协议演进时丢信息
|
|
55
|
+
| { method: string; raw: Record<string, unknown> };
|
|
56
|
+
|
|
57
|
+
/** 解析后的 extension_ui_request 顶层形状(type 守卫用)。
|
|
58
|
+
* id 和 method 在顶层,method 特定字段平铺(与 Pi 原生格式一致)。 */
|
|
59
|
+
interface ExtensionUiRequestEnvelope {
|
|
60
|
+
type: "extension_ui_request";
|
|
61
|
+
id: string;
|
|
62
|
+
method: string;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Pi 原生 RPC response 顶层形状(type 守卫用)。
|
|
67
|
+
* 与 Pi rpc-types.ts 的 RpcResponse 一致:type:"response" + command + success。 */
|
|
68
|
+
interface RpcResponseEnvelope {
|
|
69
|
+
type: "response";
|
|
70
|
+
command: string;
|
|
71
|
+
success: boolean;
|
|
72
|
+
id?: string;
|
|
73
|
+
data?: unknown;
|
|
74
|
+
error?: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** parseSpawnLine 的分类结果。
|
|
79
|
+
*
|
|
80
|
+
* 关键改动(W1 协议层重写):
|
|
81
|
+
* - extension_ui_request 分支:从 {id, params:Record} 改为 {id, request: ExtensionUiRequest}
|
|
82
|
+
* (request 按 method 平铺,与 Pi rpc-types.ts 1:1)
|
|
83
|
+
* - response 分支:从 {id, result, error} 改为 {id?, command, success, data?, error?}
|
|
84
|
+
* (Pi 原生 RpcResponse 格式,SR-1 根因 1b) */
|
|
85
|
+
export type ParsedSpawnLine =
|
|
86
|
+
| { kind: "header"; header: SpawnSessionHeader }
|
|
87
|
+
| { kind: "event"; event: SdkEvent }
|
|
88
|
+
| {
|
|
89
|
+
kind: "response";
|
|
90
|
+
id?: string;
|
|
91
|
+
command: string;
|
|
92
|
+
success: boolean;
|
|
93
|
+
data?: unknown;
|
|
94
|
+
error?: string;
|
|
95
|
+
}
|
|
96
|
+
| { kind: "extension_ui_request"; id: string; request: ExtensionUiRequest }
|
|
97
|
+
| { kind: "invalid"; raw: string; error: string };
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 判断解析出的 JSON 是否为 header 行(type === "session")。
|
|
101
|
+
* 校验所有必需字段(id/timestamp/cwd),缺任一则不收窄——避免下游
|
|
102
|
+
* deriveSessionFilePath 访问 undefined 字段时抛 TypeError。
|
|
103
|
+
*/
|
|
104
|
+
function isSessionHeader(obj: unknown): obj is SpawnSessionHeader {
|
|
105
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
106
|
+
const r = obj as Record<string, unknown>;
|
|
107
|
+
return (
|
|
108
|
+
r.type === "session" &&
|
|
109
|
+
typeof r.id === "string" &&
|
|
110
|
+
typeof r.timestamp === "string" &&
|
|
111
|
+
typeof r.cwd === "string"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 判断解析出的 JSON 是否为 Pi 原生 RPC response。
|
|
117
|
+
*
|
|
118
|
+
* SR-1 根因 1b 修复:旧守卫判 JSON-RPC 2.0(jsonrpc + id + result/error),
|
|
119
|
+
* 但 Pi 实际发 {type:"response", command, success, data?, error?}。
|
|
120
|
+
* 新守卫只认 Pi 原生格式:
|
|
121
|
+
* - type === "response"
|
|
122
|
+
* - command: string(调用的命令名,如 "run_tool")
|
|
123
|
+
* - success: boolean
|
|
124
|
+
* id 可选(通知型 response 无 id)。
|
|
125
|
+
*
|
|
126
|
+
* 旧 JSON-RPC 2.0 response({jsonrpc, id, result})不再被识别 → 落 invalid 分支。
|
|
127
|
+
*/
|
|
128
|
+
function isRpcResponse(obj: unknown): obj is RpcResponseEnvelope {
|
|
129
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
130
|
+
const r = obj as Record<string, unknown>;
|
|
131
|
+
return (
|
|
132
|
+
r.type === "response" &&
|
|
133
|
+
typeof r.command === "string" &&
|
|
134
|
+
typeof r.success === "boolean"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 判断解析出的 JSON 是否为 Pi 原生 extension_ui_request。
|
|
140
|
+
*
|
|
141
|
+
* 关键改动(W1):旧守卫判 JSON-RPC 2.0(jsonrpc + method:"extension_ui_request" + params),
|
|
142
|
+
* 但 Pi 实际发平铺格式 {type:"extension_ui_request", id, method, ...method特定字段}。
|
|
143
|
+
* 新守卫:
|
|
144
|
+
* - type === "extension_ui_request"(顶层 type 字段,非 method 字段值)
|
|
145
|
+
* - id: string
|
|
146
|
+
* - method: string(select/confirm/.../set_editor_text 等具体方法名)
|
|
147
|
+
*
|
|
148
|
+
* 删掉 jsonrpc 守卫(Pi 不发 JSON-RPC 2.0 envelope)和 params 守卫(字段平铺,无 params 包裹)。
|
|
149
|
+
* 旧 JSON-RPC 2.0 格式({jsonrpc, method:"extension_ui_request", params})不再被识别。
|
|
150
|
+
*/
|
|
151
|
+
function isExtensionUiRequest(obj: unknown): obj is ExtensionUiRequestEnvelope {
|
|
152
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
153
|
+
const r = obj as Record<string, unknown>;
|
|
154
|
+
return (
|
|
155
|
+
r.type === "extension_ui_request" &&
|
|
156
|
+
typeof r.id === "string" &&
|
|
157
|
+
typeof r.method === "string"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 从已通过 isExtensionUiRequest 守卫的 envelope 构造 ExtensionUiRequest 变体。
|
|
163
|
+
*
|
|
164
|
+
* 按 method 平铺提取字段(与 Pi rpc-types.ts L230-265 1:1)。已知 method
|
|
165
|
+
* 走对应变体;未知 method 走 string fallback(保留 raw 字段全量字段)。
|
|
166
|
+
*
|
|
167
|
+
* 字段类型容错:协议字段类型不符(如 options 非数组)时,该字段降级为空数组/undefined
|
|
168
|
+
*(数组类字段做元素类型过滤,剔除非字符串元素),仍归类为已知 method(不丢 method 信息)。
|
|
169
|
+
*/
|
|
170
|
+
function buildExtensionUiRequest(env: ExtensionUiRequestEnvelope): ExtensionUiRequest {
|
|
171
|
+
const r: Record<string, unknown> = env;
|
|
172
|
+
switch (env.method) {
|
|
173
|
+
case "select":
|
|
174
|
+
return {
|
|
175
|
+
method: "select",
|
|
176
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
177
|
+
options: Array.isArray(r.options)
|
|
178
|
+
? r.options.filter((x): x is string => typeof x === "string")
|
|
179
|
+
: [],
|
|
180
|
+
...(typeof r.timeout === "number" ? { timeout: r.timeout } : {}),
|
|
181
|
+
};
|
|
182
|
+
case "confirm":
|
|
183
|
+
return {
|
|
184
|
+
method: "confirm",
|
|
185
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
186
|
+
message: typeof r.message === "string" ? r.message : "",
|
|
187
|
+
...(typeof r.timeout === "number" ? { timeout: r.timeout } : {}),
|
|
188
|
+
};
|
|
189
|
+
case "input":
|
|
190
|
+
return {
|
|
191
|
+
method: "input",
|
|
192
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
193
|
+
...(typeof r.placeholder === "string" ? { placeholder: r.placeholder } : {}),
|
|
194
|
+
...(typeof r.timeout === "number" ? { timeout: r.timeout } : {}),
|
|
195
|
+
};
|
|
196
|
+
case "editor":
|
|
197
|
+
return {
|
|
198
|
+
method: "editor",
|
|
199
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
200
|
+
...(typeof r.prefill === "string" ? { prefill: r.prefill } : {}),
|
|
201
|
+
};
|
|
202
|
+
case "notify":
|
|
203
|
+
return {
|
|
204
|
+
method: "notify",
|
|
205
|
+
message: typeof r.message === "string" ? r.message : "",
|
|
206
|
+
...(r.notifyType === "info" || r.notifyType === "warning" || r.notifyType === "error"
|
|
207
|
+
? { notifyType: r.notifyType }
|
|
208
|
+
: {}),
|
|
209
|
+
};
|
|
210
|
+
case "setStatus":
|
|
211
|
+
return {
|
|
212
|
+
method: "setStatus",
|
|
213
|
+
statusKey: typeof r.statusKey === "string" ? r.statusKey : "",
|
|
214
|
+
statusText: typeof r.statusText === "string" ? r.statusText : undefined,
|
|
215
|
+
};
|
|
216
|
+
case "setWidget": {
|
|
217
|
+
const placement = r.widgetPlacement;
|
|
218
|
+
const widgetLines = Array.isArray(r.widgetLines)
|
|
219
|
+
? r.widgetLines.filter((x): x is string => typeof x === "string")
|
|
220
|
+
: undefined;
|
|
221
|
+
return {
|
|
222
|
+
method: "setWidget",
|
|
223
|
+
widgetKey: typeof r.widgetKey === "string" ? r.widgetKey : "",
|
|
224
|
+
widgetLines,
|
|
225
|
+
...(placement === "aboveEditor" || placement === "belowEditor"
|
|
226
|
+
? { widgetPlacement: placement }
|
|
227
|
+
: {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "setTitle":
|
|
231
|
+
return {
|
|
232
|
+
method: "setTitle",
|
|
233
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
234
|
+
};
|
|
235
|
+
case "set_editor_text":
|
|
236
|
+
return {
|
|
237
|
+
method: "set_editor_text",
|
|
238
|
+
text: typeof r.text === "string" ? r.text : "",
|
|
239
|
+
};
|
|
240
|
+
default:
|
|
241
|
+
// 未知 method:保留全部原始字段,避免协议演进时丢信息
|
|
242
|
+
return { method: env.method, raw: r };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 解析 pi stdout 的一行。
|
|
248
|
+
*
|
|
249
|
+
* @param line stdout 的一行(不含换行符;空行返回 null)
|
|
250
|
+
* @returns 分类结果,或 null(空行/仅空白)
|
|
251
|
+
*
|
|
252
|
+
* 分类规则(判定顺序关键,见 W1 bug 修复):
|
|
253
|
+
* - 空白行 → null(pi 可能输出空行,跳过)
|
|
254
|
+
* - 合法 JSON + type:"session" + 必需字段 → header
|
|
255
|
+
* - 合法 JSON + type:"extension_ui_request" + id + method → extension_ui_request
|
|
256
|
+
* (必须在 event 分支之前,否则被 typeof obj.type===string 吞为 event)
|
|
257
|
+
* - 合法 JSON + type:"response" + command + success → response
|
|
258
|
+
* - 合法 JSON + 有 type 字段 → event(SdkEvent,type schema 由调用方校验)
|
|
259
|
+
* - 合法 JSON 但无 type → invalid
|
|
260
|
+
* - 非法 JSON → invalid(记录 error,不抛——单行损坏不应中断整个流)
|
|
261
|
+
*
|
|
262
|
+
* 容错原则:stdout 是流式输出,任何单行解析失败都不应中断进程。invalid 归类
|
|
263
|
+
* 由 runSpawn 决定是否记录/忽略,本函数不丢弃信息。
|
|
264
|
+
*/
|
|
265
|
+
export function parseSpawnLine(line: string): ParsedSpawnLine | null {
|
|
266
|
+
const trimmed = line.trim();
|
|
267
|
+
if (trimmed === "") return null;
|
|
268
|
+
|
|
269
|
+
let obj: unknown;
|
|
270
|
+
try {
|
|
271
|
+
obj = JSON.parse(trimmed);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return {
|
|
274
|
+
kind: "invalid",
|
|
275
|
+
raw: trimmed,
|
|
276
|
+
error: err instanceof Error ? err.message : String(err),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (isSessionHeader(obj)) {
|
|
281
|
+
return { kind: "header", header: obj };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// extension_ui_request:必须在 event 分支之前判定(W1 判定顺序 bug 修复)。
|
|
285
|
+
// 原因:extension_ui_request 也有 type 字段,若 event 分支(typeof obj.type===string)
|
|
286
|
+
// 在前,会被当 event 静默吞掉。现在先于 event 判定,命中后按 method 构造 request。
|
|
287
|
+
if (isExtensionUiRequest(obj)) {
|
|
288
|
+
return { kind: "extension_ui_request", id: obj.id, request: buildExtensionUiRequest(obj) };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// RPC response:Pi 原生格式 {type:"response", command, success, data?, error?}
|
|
292
|
+
if (isRpcResponse(obj)) {
|
|
293
|
+
return {
|
|
294
|
+
kind: "response",
|
|
295
|
+
...(typeof obj.id === "string" ? { id: obj.id } : {}),
|
|
296
|
+
command: obj.command,
|
|
297
|
+
success: obj.success,
|
|
298
|
+
...(obj.data !== undefined ? { data: obj.data } : {}),
|
|
299
|
+
...(typeof obj.error === "string" ? { error: obj.error } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 事件行:必须有 type 字段(SdkEvent 契约)
|
|
304
|
+
if (
|
|
305
|
+
typeof obj === "object" &&
|
|
306
|
+
obj !== null &&
|
|
307
|
+
typeof (obj as Record<string, unknown>).type === "string"
|
|
308
|
+
) {
|
|
309
|
+
return { kind: "event", event: obj as SdkEvent };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
kind: "invalid",
|
|
314
|
+
raw: trimmed,
|
|
315
|
+
error: "JSON missing string 'type' field",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 从已收集的 header + 事件流推导子进程的 session 文件路径。
|
|
321
|
+
*
|
|
322
|
+
* pi session 文件命名规则(session-manager.ts:846):
|
|
323
|
+
* `${fileTimestamp}_${sessionId}.jsonl`
|
|
324
|
+
* 其中 fileTimestamp = header.timestamp.replace(/[:.]/g, "-")
|
|
325
|
+
* (ISO 时间里的冒号和点替换为连字符,如 2026-07-03T12-34-56-789Z)。
|
|
326
|
+
*
|
|
327
|
+
* @param header spawn 解析出的 session header
|
|
328
|
+
* @param sessionDir 指定的 session 目录(spawn 时通过 --session-dir 传入)
|
|
329
|
+
* @returns 推导的 session 文件绝对路径(不保证文件存在——子进程可能仍在写入)
|
|
330
|
+
*
|
|
331
|
+
* 注意:这是基于 pi 命名规则的推导。runSpawn 会在进程结束后校验文件实际存在,
|
|
332
|
+
* 不存在时用 sessionId 后缀匹配兜底。
|
|
333
|
+
*/
|
|
334
|
+
export function deriveSessionFilePath(
|
|
335
|
+
header: SpawnSessionHeader,
|
|
336
|
+
sessionDir: string,
|
|
337
|
+
): string {
|
|
338
|
+
const fileTimestamp = header.timestamp.replace(/[:.]/g, "-");
|
|
339
|
+
return `${sessionDir}/${fileTimestamp}_${header.id}.jsonl`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 在 sessionDir 中按 sessionId 后缀匹配查找实际存在的 session 文件。
|
|
344
|
+
*
|
|
345
|
+
* 兜底机制:deriveSessionFilePath 基于命名规则推导,若 pi 命名规则变化导致
|
|
346
|
+
* 推导路径不存在,用 sessionId 作为后缀匹配实际文件。sessionId 是 header
|
|
347
|
+
* 的稳定标识,文件名必含它,匹配可靠。
|
|
348
|
+
*
|
|
349
|
+
* @returns 匹配到的文件绝对路径,或 undefined(无匹配)
|
|
350
|
+
*/
|
|
351
|
+
export function findSessionFileByHeaderId(
|
|
352
|
+
sessionDir: string,
|
|
353
|
+
sessionId: string,
|
|
354
|
+
): string | undefined {
|
|
355
|
+
try {
|
|
356
|
+
const files = fs.readdirSync(sessionDir);
|
|
357
|
+
// 文件名格式:<fileTimestamp>_<sessionId>.jsonl,sessionId 是后缀(去掉 .jsonl)
|
|
358
|
+
const match = files.find((f) => f.endsWith(`_${sessionId}.jsonl`));
|
|
359
|
+
return match ? path.join(sessionDir, match) : undefined;
|
|
360
|
+
} catch {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}
|