@zhushanwen/subagent-core 0.2.0 → 0.4.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 +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-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -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__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -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__/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__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- 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-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- 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 +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- 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 +389 -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.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- 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__/workflow-files.test.ts +186 -0
- 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 +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- 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 +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- 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__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- 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 +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -22,9 +22,9 @@ export function executeOptionsToEngineTaskSpec(opts: ExecuteOptions): AgentTaskS
|
|
|
22
22
|
const persona: PersonaSpec | undefined =
|
|
23
23
|
opts.skillPath !== undefined || opts.appendSystemPrompt !== undefined
|
|
24
24
|
? {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
...(opts.skillPath !== undefined ? { skillPath: opts.skillPath } : {}),
|
|
26
|
+
...(opts.appendSystemPrompt !== undefined ? { appendSystemPrompt: opts.appendSystemPrompt } : {}),
|
|
27
|
+
}
|
|
28
28
|
: undefined;
|
|
29
29
|
return {
|
|
30
30
|
task: opts.task,
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
// §3.3.5「EnginePort 完整签名」——本文件是可编码落地的契约层,后续 wave(公共降级层
|
|
5
5
|
// P2 / zcode 引擎 P3 / 配置路由 P4)以本接口为实现契约,字段级变更须先改设计文档。
|
|
6
6
|
//
|
|
7
|
+
// 字段级扩展登记(接上文纪律——先改设计文档再扩接口):
|
|
8
|
+
// - [R1 已实施 2026-08-30] EnginePort.dispose?()——引擎停机面。权威源:
|
|
9
|
+
// docs/design/zcode-engine-appserver-resident.md §3.3 D6 / §3.4 不变量 4。
|
|
10
|
+
// - [R4 已实施 2026-08-30] RunContext.onHandleReady——运行中句柄回填通道
|
|
11
|
+
// (同设计 §3.4 不变量 3:sessionRef 在 create 应答后经本回调送达编排层,
|
|
12
|
+
// 与 onPoolResolved 分立两个时点)。
|
|
13
|
+
//
|
|
7
14
|
// 四个能力面(D1):
|
|
8
15
|
// run —— 主语义:一次性 fire-to-completion 任务执行;
|
|
9
16
|
// interact —— 交互控制面(chatMode 的 message/close/cancel + idle,可选能力面,
|
|
@@ -22,6 +29,7 @@ import type {
|
|
|
22
29
|
AgentTaskSpec,
|
|
23
30
|
EngineCapabilities,
|
|
24
31
|
EngineHandle,
|
|
32
|
+
EngineHandleData,
|
|
25
33
|
InteractAction,
|
|
26
34
|
InteractResult,
|
|
27
35
|
ProbeReport,
|
|
@@ -49,7 +57,13 @@ export interface RunContext {
|
|
|
49
57
|
signal?: AbortSignal;
|
|
50
58
|
/** 事件流出口(host 消费后统一落 journal,D6 第②级)。 */
|
|
51
59
|
onEvent?: (event: AgentEvent) => void;
|
|
52
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* model 解析第三层兜底(现有 D-008 语义不变)——**pi 链路专属兜底**:经
|
|
62
|
+
* taskSpecToExecuteOptions → resolveModel 第三层消费(PiEngine 直通)。自带
|
|
63
|
+
* provider 体系与缺省模型的引擎(如 zcode:requested > 引擎缺省常量链)按自身
|
|
64
|
+
* 默认链解析,不消费本字段(zcode 侧在「ctx 有模型但被忽略」时出声留痕,
|
|
65
|
+
* zcode-engine.warnIgnoredCtxModel)。
|
|
66
|
+
*/
|
|
53
67
|
ctxModel?: ModelInfo;
|
|
54
68
|
/**
|
|
55
69
|
* text_delta streaming 通道(宿主侧 UI widget)。与 onEvent 平行的 text_delta 出口:
|
|
@@ -81,12 +95,27 @@ export interface RunContext {
|
|
|
81
95
|
* 未来流式引擎需在事件出口前调用)。
|
|
82
96
|
*/
|
|
83
97
|
onPoolResolved?: (poolKey: string) => void;
|
|
98
|
+
/**
|
|
99
|
+
* [R4 §3.4 不变量 3] 运行中句柄回填通道:引擎在「session/create 应答到达后」
|
|
100
|
+
* 立即回调(早于 run resolve——stream 引擎的 run 生命周期远长于会话建立)。
|
|
101
|
+
* 与 onPoolResolved 分立两个时点:poolKey 在 prepare 期(onPoolResolved,连接
|
|
102
|
+
* 建立前即可知),sessionRef 在 create 应答后(本回调)。编排层收到后立即回填
|
|
103
|
+
* record.engineHandle 并落 entry——运行中的 GUI 经 entry 重建 record 即拿到
|
|
104
|
+
* ①②级读取钥匙,不再等 run resolve 后的终态回填。可选回调:不支持运行中回填
|
|
105
|
+
* 的引擎(spawn 单轮、终态即回填)不调用,宿主语义不受影响。
|
|
106
|
+
*/
|
|
107
|
+
onHandleReady?: (partial: Pick<EngineHandleData, "sessionRef" | "poolKey">) => void;
|
|
84
108
|
/**
|
|
85
109
|
* [U0 D10] 引擎 spawn 的子进程句柄注册钩子(宿主终止链记账)。引擎在 spawn 成功后
|
|
86
110
|
* 同步回调(与 pi runSpawn 的 spawnedChildren.set 同构时机);宿主据此把 child 注册进
|
|
87
111
|
* session-runner 的 spawnedChildren Map(cancel SIGTERM / dispose 收割兜底 / killAll
|
|
88
112
|
* 全量清理对非 pi 引擎 record 生效)。close/error 后由宿主按句守卫移除。可选:引擎
|
|
89
113
|
* 内部不 spawn 进程(如未来常驻 driver host 实现)时不调用,宿主记账自然为空。
|
|
114
|
+
*
|
|
115
|
+
* 边界声明(R1 D6):本钩子只用于 per-record 一次性 spawn(一任务一进程模态)。
|
|
116
|
+
* 引擎持有的常驻进程(跨任务共享,如 app-server 常驻连接)不经本钩子注册、不进
|
|
117
|
+
* spawnedChildren Map——其生命周期完全归引擎 dispose 管理(防 per-record 重复
|
|
118
|
+
* SIGTERM / 单任务 abort 误杀共享进程)。
|
|
90
119
|
*/
|
|
91
120
|
onChildSpawned?: (child: ChildProcess) => void;
|
|
92
121
|
}
|
|
@@ -150,4 +179,15 @@ export interface EnginePort {
|
|
|
150
179
|
* 侧零改动。
|
|
151
180
|
*/
|
|
152
181
|
listModels?(): Array<{ id: string; name?: string }> | null;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* [R1 D6] 可选停机面:释放引擎持有的常驻资源(如 app-server 常驻进程 / 长连接)。
|
|
185
|
+
* 幂等契约(§3.4 不变量 4):重复调用无副作用;dispose 后首个 run 自动重建(与
|
|
186
|
+
* 「进程死后重建」同一代码路径)。可选成员保持向后兼容——无常驻资源的引擎(pi
|
|
187
|
+
* 现状 spawn 单轮)不必实现。等待策略(D6①「触发不等待」):宿主收割入口
|
|
188
|
+
* (registry disposeEngines → killAllSpawnedChildren)只同步调用拿 Promise 不
|
|
189
|
+
* await,引擎实现须自行保证同步面(立即 fire close 帧 + 同步 SIGTERM)在返回
|
|
190
|
+
* Promise 前完成;grace→SIGKILL 升级序列属异步面(promise 段)。
|
|
191
|
+
*/
|
|
192
|
+
dispose?(): Promise<void>;
|
|
153
193
|
}
|
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
//
|
|
11
11
|
// 依赖方向(设计 §3.3.1 贯穿纪律④):registry 只依赖 port.ts 的类型,不 import 任何
|
|
12
12
|
// 具体引擎——注册由组合根(index.ts)或引擎自己的 registration 模块完成,防循环依赖。
|
|
13
|
+
// (core/logger 是 facade 基础设施非引擎,dispose best-effort 记日志需要它,不违反本纪律。)
|
|
14
|
+
|
|
15
|
+
import { getLogger } from "../../core/logger.ts";
|
|
13
16
|
|
|
14
17
|
import type { EnginePort } from "./port.ts";
|
|
15
18
|
|
|
19
|
+
// core log facade(execution 层统一 "subagents" component,模块顶层缓存惯例)。
|
|
20
|
+
const logger = getLogger("subagents");
|
|
21
|
+
|
|
16
22
|
/** 引擎工厂:惰性创建引擎实例(getEngine 首次取用时执行)。 */
|
|
17
23
|
export type EngineFactory = () => EnginePort;
|
|
18
24
|
|
|
@@ -87,17 +93,64 @@ function getRegistrySlot(): EngineRegistrySlot {
|
|
|
87
93
|
return slot;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/**
|
|
97
|
+
* [R1 D6] 触发引擎 dispose:同步调用拿 Promise 不 await(「触发不等待」,D6①——
|
|
98
|
+
* dispose 的同步面〔fire close 帧 + 同步 SIGTERM〕由引擎实现保证在返回 Promise 前
|
|
99
|
+
* 完成,registry 不等待异步段)。同步 throw 与异步 reject 均记日志吞掉,绝不外溢
|
|
100
|
+
* 阻断调用方——重注册替换(D6②)与宿主收割(D6③)都是 best-effort 面,且 reject
|
|
101
|
+
* 无人接会成为 unhandledRejection 崩宿主。两条 dispose 路径共用本函数。
|
|
102
|
+
*/
|
|
103
|
+
function triggerEngineDispose(engine: EnginePort, source: string): void {
|
|
104
|
+
if (typeof engine.dispose !== "function") return;
|
|
105
|
+
try {
|
|
106
|
+
engine.dispose().then(undefined, (err: unknown) => {
|
|
107
|
+
logger.warn(
|
|
108
|
+
`[engine-registry] engine '${engine.id}' dispose rejected (${source}, best-effort continue): ${
|
|
109
|
+
err instanceof Error ? err.message : String(err)
|
|
110
|
+
}`,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger.warn(
|
|
115
|
+
`[engine-registry] engine '${engine.id}' dispose threw synchronously (${source}, best-effort continue): ${
|
|
116
|
+
err instanceof Error ? err.message : String(err)
|
|
117
|
+
}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
/**
|
|
91
123
|
* 登记引擎工厂。重复注册同一 id = 覆盖(组合根可能多次执行,如每次 session_start 重跑
|
|
92
124
|
* registerPiEngine——幂等覆盖保证不炸也不堆积),覆盖时丢弃缓存的旧单例,让下一次
|
|
93
125
|
* getEngine 用新工厂重建。
|
|
126
|
+
*
|
|
127
|
+
* [R1 D6②] 覆盖前对已实例化的旧单例触发 dispose(防泄漏——旧实例可能持有常驻进程/
|
|
128
|
+
* 长连接);触发不等待 + 失败不阻断(见 triggerEngineDispose),幂等覆盖语义不变。
|
|
94
129
|
*/
|
|
95
130
|
export function registerEngine(id: string, factory: EngineFactory): void {
|
|
96
131
|
const slot = getRegistrySlot();
|
|
132
|
+
const previous = slot.singletons.get(id);
|
|
133
|
+
if (previous) triggerEngineDispose(previous, `registerEngine('${id}') overwrite`);
|
|
97
134
|
slot.factories.set(id, factory);
|
|
98
135
|
slot.singletons.delete(id);
|
|
99
136
|
}
|
|
100
137
|
|
|
138
|
+
/**
|
|
139
|
+
* [R1 D6③] 对已实例化的引擎单例触发 dispose(触发不等待)。宿主唯一收割入口
|
|
140
|
+
* (session-runner killAllSpawnedChildren)在杀 per-record children 之前调用——
|
|
141
|
+
* 常驻进程的回收归引擎 dispose,本函数只负责按序触发。
|
|
142
|
+
*
|
|
143
|
+
* 只遍历 singletons:已实例化才可能持有常驻资源,绝不经 getEngine 实例化未用
|
|
144
|
+
* 引擎(停机路径反向创建资源违背停机语义)。dispose 后不删单例——幂等与
|
|
145
|
+
* 「dispose 后首个 run 自动重建」由引擎实现承诺(§3.4 不变量 4),registry
|
|
146
|
+
* 不越权管理引擎内部生命周期。
|
|
147
|
+
*/
|
|
148
|
+
export function disposeEngines(): void {
|
|
149
|
+
for (const engine of getRegistrySlot().singletons.values()) {
|
|
150
|
+
triggerEngineDispose(engine, "disposeEngines()");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
101
154
|
/** 未注册 id 抛 EngineNotFoundError(含已注册清单与配置指引)。 */
|
|
102
155
|
export function getEngine(id: string): EnginePort {
|
|
103
156
|
const slot = getRegistrySlot();
|
|
@@ -26,6 +26,7 @@ import type { ManifestStore } from "./manifest-store.ts";
|
|
|
26
26
|
import type { ModelConfigService } from "./model-config-service.ts";
|
|
27
27
|
import { getSubagentSessionDir } from "./path-encoding.ts";
|
|
28
28
|
import type { RecordStore } from "./record-store.ts";
|
|
29
|
+
import { readIdentityHeader, readIdentityTail } from "./session-reconstructor.ts";
|
|
29
30
|
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
30
31
|
import type { AgentResult, ClosedReason, ExecutionRecord } from "./types.ts";
|
|
31
32
|
import type { WorktreeManager } from "./worktree-manager.ts";
|
|
@@ -42,10 +43,48 @@ export interface FinalizeDeps {
|
|
|
42
43
|
pi: { appendEntry?: (type: string, data: unknown) => void } | null;
|
|
43
44
|
/** pending-notifications 终态注销(绑定 pi.events.emit,由调用方闭包提供)。 */
|
|
44
45
|
emitUnregister(id: string, status: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* [T1/PS-9] subagent sessionDir(getSubagentSessionDir(agentDir, rootCwd),调用方注入)。
|
|
48
|
+
*
|
|
49
|
+
* record.sessionFile 缺失(RC-1 握手失败 + LC-4 反查也未命中的残余形态)时用于磁盘
|
|
50
|
+
* 反查:按 identity(record.id)扫目录找真实 session 文件,作为 tombstone/finalized
|
|
51
|
+
* sidecar 与 removeAliveMarker 的依据——消除「sessionFile 缺失 → 终态原因丢失 +
|
|
52
|
+
* alive marker 残留」。undefined = 调用方无法提供(反查跳过,行为退回修复前)。
|
|
53
|
+
*/
|
|
54
|
+
sessionDir?: string;
|
|
45
55
|
// [review 修复] 已删除 redeliverPending 回调(MF-1 消费确认制补投):pendingMessages
|
|
46
56
|
// 三段消费链随 deliverToRunning 一并移除(无生产调用方,死机制)。
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
/**
|
|
60
|
+
* [T1/PS-9] 按 record 身份在 sessionDir 反查 session 文件(record.sessionFile 缺失时用)。
|
|
61
|
+
*
|
|
62
|
+
* 匹配依据:session JSONL 的 identity custom entry 携带 record id(子进程 session_start
|
|
63
|
+
* hook 写入),探测函数与 RecordStore 列表扫描同源(readIdentityHeader 头部 64KB →
|
|
64
|
+
* readIdentityTail 尾部 64KB;续聊场景 identity 靠尾)。readIdentityAnywhere 需全文读,
|
|
65
|
+
* 本反查是收尾路径的 best-effort,不做。
|
|
66
|
+
*
|
|
67
|
+
* @returns 匹配到的 session 文件绝对路径;目录不可读 / 无匹配返回 undefined(不抛)。
|
|
68
|
+
*/
|
|
69
|
+
function findSessionFileByRecordIdentity(
|
|
70
|
+
sessionDir: string,
|
|
71
|
+
recordId: string,
|
|
72
|
+
): string | undefined {
|
|
73
|
+
let names: string[];
|
|
74
|
+
try {
|
|
75
|
+
names = fs.readdirSync(sessionDir);
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined; // 目录缺失/不可读 → 反查跳过,行为退回无反查
|
|
78
|
+
}
|
|
79
|
+
for (const name of names) {
|
|
80
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
81
|
+
const full = path.join(sessionDir, name);
|
|
82
|
+
const recon = readIdentityHeader(full) ?? readIdentityTail(full);
|
|
83
|
+
if (recon?.id === recordId) return full;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
49
88
|
/**
|
|
50
89
|
* 时序收尾(D-017)。步骤 0→4 全部 best-effort 互不阻断(除 manifest 外都幂等)。
|
|
51
90
|
*
|
|
@@ -60,6 +99,21 @@ export async function doFinalizeRecord(
|
|
|
60
99
|
status: "closed",
|
|
61
100
|
closedReason?: ClosedReason,
|
|
62
101
|
): Promise<void> {
|
|
102
|
+
// [T1/PS-9] sessionFile 缺失时 sessionDir 反查(放在一切步骤前,让 archive 投影 /
|
|
103
|
+
// Step 3 marker / Step 4 manifest 统一受益)。RC-1(握手失败)+ LC-4(收尾反查未命中)
|
|
104
|
+
// 的残余形态下,磁盘上 session 文件仍可能真实存在(identity 携带 record.id)——
|
|
105
|
+
// 反查命中则 tombstone/finalized sidecar 与 removeAliveMarker 有了依据,终态原因
|
|
106
|
+
// 不再丢失、alive marker 不再残留;未命中则保持旧行为(best-effort 跳过)。
|
|
107
|
+
if (!record.sessionFile && deps.sessionDir) {
|
|
108
|
+
const resolved = findSessionFileByRecordIdentity(deps.sessionDir, record.id);
|
|
109
|
+
if (resolved) {
|
|
110
|
+
record.sessionFile = resolved;
|
|
111
|
+
logger.warn(
|
|
112
|
+
`[subagent] finalizeRecord: sessionFile was missing, resolved via sessionDir identity lookup: ${resolved}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
63
117
|
// ── Step 0: collectPatch(best-effort)──
|
|
64
118
|
// [MF#3] patchFile 写到 worktree 之外(sessionsDir/<branch>.patch),避免被 cleanup 删除;
|
|
65
119
|
// 路径回填 record.patchFile,供调用方(tool result / /subagents list)应用。
|
|
@@ -179,7 +233,9 @@ export async function doFinalizeRecord(
|
|
|
179
233
|
*
|
|
180
234
|
* MF-2:设 record.result = result.text(否则 notifier idle 回复正文恒为 "(empty)",
|
|
181
235
|
* G1/G2 多轮回复送达不成立)。失败轮次(result.success=false,MF-6 回退 idle 路径)
|
|
182
|
-
*
|
|
236
|
+
* error 优先于 text:collectResult 恒带 getFullText 全量正文,text 优先会把失败的
|
|
237
|
+
* 恢复指引覆盖成轮内旧正文(Gate B S-B-1 实测:settled watchdog kill 后宿主收到
|
|
238
|
+
* 成功形态通知,无从得知挂死与恢复方式)。
|
|
183
239
|
*
|
|
184
240
|
* 状态:record.status = "idle"(覆盖 tryTransition 设的 done/failed),record.round += 1。
|
|
185
241
|
* 各步骤 best-effort 互不阻断(参照 doFinalizeRecord 的 bestEffort 用法)。
|
|
@@ -193,7 +249,9 @@ export async function doFinalizeRoundToIdle(
|
|
|
193
249
|
result: AgentResult,
|
|
194
250
|
): Promise<void> {
|
|
195
251
|
// MF-2:设 record.result 供 notifier idle 回复正文(否则恒 "(empty)",G1/G2 不成立)。
|
|
196
|
-
//
|
|
252
|
+
// [T2-③ / LC-1] 失败轮 error 优先:collectResult 恒带 getFullText 全量正文(即使
|
|
253
|
+
// success=false),text 优先会把 watchdog 等失败的恢复指引覆盖成轮内正文(半成品 /
|
|
254
|
+
// 回补值),宿主无从得知失败与恢复方式(S-B-1 首轮 settled watchdog 实测)。
|
|
197
255
|
// [R2-1] 轮终写点恒写非空:one-shot 空文本成功完成(collectResult getFullText 返回 ""、
|
|
198
256
|
// success=true、真实可达,本写点被 subagent-service runAndFinalize 的成功分支共用)首轮
|
|
199
257
|
// result 前值 undefined,兜底补 "(empty)" 占位——措辞与 notifier buildLlmContent 的
|
|
@@ -205,10 +263,10 @@ export async function doFinalizeRoundToIdle(
|
|
|
205
263
|
// "(no output this round)"(D5:增量语义下沿用旧 record.result = 上一轮增量,本轮通知
|
|
206
264
|
// 正文 = 上一轮内容,父 agent 误读为原样重复回复)。
|
|
207
265
|
let nextResult: string | undefined;
|
|
208
|
-
if (result.text) {
|
|
209
|
-
nextResult = result.text;
|
|
210
|
-
} else if (result.error) {
|
|
266
|
+
if (result.error && (!result.success || !result.text)) {
|
|
211
267
|
nextResult = `round did not complete: ${result.error}`;
|
|
268
|
+
} else if (result.text) {
|
|
269
|
+
nextResult = result.text;
|
|
212
270
|
} else if (record.chatMode) {
|
|
213
271
|
nextResult = "(no output this round)";
|
|
214
272
|
} else {
|
|
@@ -30,6 +30,31 @@ export interface GetStateResult {
|
|
|
30
30
|
sessionId?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* get_state response 监听器注册函数形态(stdout pump / 测试注入)。
|
|
35
|
+
*
|
|
36
|
+
* 返回值:注销函数(从监听表移除该 resolver),可省略——performGetStateHandshake
|
|
37
|
+
* 忽略返回值(既有语义:迟到 response 靠 resolved 标志自弃,条目由 close 统一清);
|
|
38
|
+
* requestGetStateOnce 消费返回值做单次请求的自清理(不依赖 close 兜底)。
|
|
39
|
+
*/
|
|
40
|
+
export type AddGetStateResponseListener = (
|
|
41
|
+
id: string,
|
|
42
|
+
resolver: (data: unknown) => void,
|
|
43
|
+
) => void | (() => void);
|
|
44
|
+
|
|
45
|
+
/** 从 get_state response data 提取 sessionFile/sessionId(提取规则单一来源,握手与惰性重试共用)。 */
|
|
46
|
+
function extractGetStateFields(data: unknown, into: GetStateResult): void {
|
|
47
|
+
if (data && typeof data === "object") {
|
|
48
|
+
const d = data as Record<string, unknown>;
|
|
49
|
+
if (typeof d.sessionFile === "string" && d.sessionFile.length > 0) {
|
|
50
|
+
into.sessionFile = d.sessionFile;
|
|
51
|
+
}
|
|
52
|
+
if (typeof d.sessionId === "string" && d.sessionId.length > 0) {
|
|
53
|
+
into.sessionId = d.sessionId;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
/**
|
|
34
59
|
* FR-4: 通过 get_state RPC 查询子进程获取 sessionFile/sessionId。
|
|
35
60
|
*
|
|
@@ -43,7 +68,7 @@ export interface GetStateResult {
|
|
|
43
68
|
*/
|
|
44
69
|
export function performGetStateHandshake(
|
|
45
70
|
child: ChildProcess,
|
|
46
|
-
addResponseListener:
|
|
71
|
+
addResponseListener: AddGetStateResponseListener,
|
|
47
72
|
): Promise<GetStateResult> {
|
|
48
73
|
return new Promise<GetStateResult>((resolve) => {
|
|
49
74
|
const collected: GetStateResult = {};
|
|
@@ -81,15 +106,7 @@ export function performGetStateHandshake(
|
|
|
81
106
|
// [#15] 闭包清理本次 tryOnce 的 timer(2s 超时 + 排队中的 retry),不碰其他 reqId 的 timer。
|
|
82
107
|
clearTimeout(timer);
|
|
83
108
|
if (pendingRetry) clearTimeout(pendingRetry);
|
|
84
|
-
|
|
85
|
-
const d = data as Record<string, unknown>;
|
|
86
|
-
if (typeof d.sessionFile === "string" && d.sessionFile.length > 0) {
|
|
87
|
-
collected.sessionFile = d.sessionFile;
|
|
88
|
-
}
|
|
89
|
-
if (typeof d.sessionId === "string" && d.sessionId.length > 0) {
|
|
90
|
-
collected.sessionId = d.sessionId;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
109
|
+
extractGetStateFields(data, collected);
|
|
93
110
|
// sessionFile 已获取——立即 resolve(无需更多重试)
|
|
94
111
|
if (collected.sessionFile) {
|
|
95
112
|
resolved = true;
|
|
@@ -102,3 +119,61 @@ export function performGetStateHandshake(
|
|
|
102
119
|
tryOnce();
|
|
103
120
|
});
|
|
104
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* [T1/RC-1] 单次 get_state 请求(agent_end 决策点惰性回补专用)。
|
|
125
|
+
*
|
|
126
|
+
* 与 performGetStateHandshake 的关系:复用同一消息构造(sendGetStateCommand)与同一
|
|
127
|
+
* 字段提取(extractGetStateFields),但**不做重试循环、不 share 握手语义**——调用方
|
|
128
|
+
* (session-runner agent_end 处置)在子进程 idle 时现场补一次查询(探针 P-T1 实证
|
|
129
|
+
* 应答 0.3-0.4ms),超时/失败即放弃,由调用方走既有保守分支(行为不劣化)。
|
|
130
|
+
*
|
|
131
|
+
* 结果契约与 performGetStateHandshake 一致:resolve GetStateResult(可能为空对象),
|
|
132
|
+
* 永不 reject——stdin 已断(EPIPE/ERR_STREAM_DESTROYED)的同步写失败按「回补失败」
|
|
133
|
+
* 处理 resolve 空对象,调用方无需 try/catch。
|
|
134
|
+
*
|
|
135
|
+
* 自清理:超时或 response 到达后从监听表移除本请求的 resolver(消费注册器的返回值),
|
|
136
|
+
* 不留迟到条目;注册器不返回注销函数时退化为 no-op(与握手同形态,靠 close 统一清)。
|
|
137
|
+
*
|
|
138
|
+
* @param child 子进程(stdin 写入 get_state 命令)
|
|
139
|
+
* @param addResponseListener 注册 response 监听器的函数(stdout pump 中调用)
|
|
140
|
+
* @param timeoutMs 单次超时(决策点预算 1s 量级;P-T1 实测 idle 应答亚毫秒)
|
|
141
|
+
*/
|
|
142
|
+
export function requestGetStateOnce(
|
|
143
|
+
child: ChildProcess,
|
|
144
|
+
addResponseListener: AddGetStateResponseListener,
|
|
145
|
+
timeoutMs: number,
|
|
146
|
+
): Promise<GetStateResult> {
|
|
147
|
+
return new Promise<GetStateResult>((resolve) => {
|
|
148
|
+
let settled = false;
|
|
149
|
+
let removeListener: () => void = () => {};
|
|
150
|
+
|
|
151
|
+
const finish = (r: GetStateResult): void => {
|
|
152
|
+
if (settled) return;
|
|
153
|
+
settled = true;
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
removeListener();
|
|
156
|
+
resolve(r);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let reqId: string;
|
|
160
|
+
try {
|
|
161
|
+
reqId = sendGetStateCommand(child);
|
|
162
|
+
} catch {
|
|
163
|
+
// stdin 已断等同步写失败 = 回补失败,空结果走调用方保守分支(同超时语义)
|
|
164
|
+
resolve({});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
removeListener =
|
|
168
|
+
addResponseListener(reqId, (data: unknown) => {
|
|
169
|
+
const r: GetStateResult = {};
|
|
170
|
+
extractGetStateFields(data, r);
|
|
171
|
+
// response 已到达:无论是否含目标字段都不再等(单次语义,无重试)
|
|
172
|
+
finish(r);
|
|
173
|
+
}) ?? (() => {});
|
|
174
|
+
// [#15] 同款时序模式:timer 在 resolver 注册后创建,resolver/finish 闭包引用它——
|
|
175
|
+
// 两者都只在 timer 初始化之后才可能被调用(response 到达是异步事件)。
|
|
176
|
+
const timer: ReturnType<typeof setTimeout> = setTimeout(() => finish({}), timeoutMs);
|
|
177
|
+
timer.unref();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -24,8 +24,13 @@
|
|
|
24
24
|
// 设计参考:session-runner 的 MF-3/MF-4 setTimeout→SIGTERM 骨架(复用 timer 形态,
|
|
25
25
|
// 触发条件重构);spawnedChildren Map 的模块级单例模式。
|
|
26
26
|
|
|
27
|
+
import { getLogger } from "../core/logger.ts";
|
|
27
28
|
import { assertSafeTimerDelay } from "../shared/timer-delay.ts";
|
|
28
29
|
|
|
30
|
+
// core/logger 无本地状态依赖(不 import execution/orchestration 层),与本模块
|
|
31
|
+
// 「可独立编译 + 单测」约束兼容;不 import session-runner / subagent-service(循环依赖)。
|
|
32
|
+
const logger = getLogger("subagents");
|
|
33
|
+
|
|
29
34
|
// ============================================================
|
|
30
35
|
// 默认常量
|
|
31
36
|
// ============================================================
|
|
@@ -48,6 +53,10 @@ export const DEFAULT_IDLE_TIMEOUT_MS = IDLE_TIMEOUT_MINUTES * SECONDS_PER_MINUTE
|
|
|
48
53
|
* 从环境变量 XYZ_SUBAGENT_IDLE_TIMEOUT_MS 读取全局默认超时。
|
|
49
54
|
* 返回 undefined 表示 env 未设置或非法(调用方回落 DEFAULT_IDLE_TIMEOUT_MS)。
|
|
50
55
|
*
|
|
56
|
+
* [LC-7/T7①] env 已设但非法(非数字/<=0)回落默认值时必须 warn 留痕——「以为设了
|
|
57
|
+
* 极长保活、实际回落 5min」的静默语义漂移不可见(设计 §4.3 LC-7),生效行为必须
|
|
58
|
+
* 可见。禁用语义不认 env(禁用只能显式传参 <=0,见 armIdleTimer 注释)。
|
|
59
|
+
*
|
|
51
60
|
* [review 修复] 原 PI_ 前缀(PI_SUBAGENT_IDLE_TIMEOUT_MS)不在 ENV_WHITELIST_PREFIXES
|
|
52
61
|
* 白名单(packages/shared/src/constants.ts 只有 XYZ_ 等,无 PI_),xyz-agent 桌面
|
|
53
62
|
* spawn 链(safe-env 过滤)会丢弃该 env,配置口在桌面场景静默失效——已改名 XYZ_
|
|
@@ -59,7 +68,12 @@ function getEnvIdleTimeoutMs(): number | undefined {
|
|
|
59
68
|
const raw = process.env.XYZ_SUBAGENT_IDLE_TIMEOUT_MS;
|
|
60
69
|
if (!raw) return undefined;
|
|
61
70
|
const parsed = Number(raw);
|
|
62
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
71
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
72
|
+
logger.warn(
|
|
73
|
+
`[lifecycle-manager] XYZ_SUBAGENT_IDLE_TIMEOUT_MS="${raw}" is invalid (expected a positive millisecond number) — falling back to DEFAULT_IDLE_TIMEOUT_MS (${DEFAULT_IDLE_TIMEOUT_MS}ms); set a plain ms value (e.g. 1800000) to override`,
|
|
74
|
+
);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
63
77
|
return parsed;
|
|
64
78
|
}
|
|
65
79
|
|
|
@@ -134,8 +148,18 @@ export function armIdleTimer(
|
|
|
134
148
|
// 刷新:先清旧 timer,避免同一 record 叠加多个 armed timer。
|
|
135
149
|
disarmIdleTimer(recordId);
|
|
136
150
|
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
// [LC-5/T6①] 超时回调按值守卫(对齐同包 session-runner.ts removeChildRegistration
|
|
152
|
+
// 的按句守卫先例):clearTimeout 对「已到期、回调已入 macrotask 队列」的 timer 无效
|
|
153
|
+
//(Node 语义:fire 后不可撤销),若此刻同 recordId 发生 disarm + re-arm(agent_settled
|
|
154
|
+
// 刷新与旧 timer 到点同轮交错),旧回调无条件 delete(recordId) 会误删**新** timer 条目
|
|
155
|
+
// ——新 timer 脱管,后续 disarm 失效 → turn 中途被 idle GC 误杀。回调捕获自己的 timer
|
|
156
|
+
// 引用,仅当 Map 当前条目仍是自己(未被 re-arm 覆盖)才删除。设计 §11-2:fake-timer
|
|
157
|
+
// 模型内无法复现「旧回调迟到于 re-arm」的精确交错(sinon clock 同步 tick 消除了该
|
|
158
|
+
// 中间态),身份比对按防御性守卫保留(成本一行)。
|
|
159
|
+
const timer: NodeJS.Timeout = setTimeout(() => {
|
|
160
|
+
if (idleTimers.get(recordId)?.timer === timer) {
|
|
161
|
+
idleTimers.delete(recordId);
|
|
162
|
+
}
|
|
139
163
|
onTimeout();
|
|
140
164
|
}, resolved);
|
|
141
165
|
// node 默认 setTimeout 返回的 timer 会被事件循环 keep-alive;unref 让它不阻塞
|
|
@@ -2,7 +2,12 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as fsPromises from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { getLogger } from "../core/logger.ts";
|
|
6
|
+
|
|
5
7
|
import { bestEffort } from "./best-effort.ts";
|
|
8
|
+
import { writeAtomicFile } from "../shared/atomic-write.ts";
|
|
9
|
+
|
|
10
|
+
const logger = getLogger("subagents");
|
|
6
11
|
|
|
7
12
|
export interface ManifestRecord {
|
|
8
13
|
id: string;
|
|
@@ -89,54 +94,19 @@ export class ManifestStore {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
|
92
|
-
* 原子写:tmp → fsync → rename → fsync dir
|
|
97
|
+
* 原子写:tmp → fsync → rename → fsync dir(shared/atomic-write 统一原语,
|
|
98
|
+
* U6b 迁移——原逐行实现与 writeAtomicFile 逐环等值)。真异步(fs.promises,
|
|
99
|
+
* 不阻塞 event loop)。
|
|
93
100
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
101
|
+
* 失败时原语尽力清理残留 tmp(debug 记录,不掩盖原错误)并原样上抛——
|
|
102
|
+
* 调用方(finalizeRecord)决定降级策略。
|
|
96
103
|
*/
|
|
97
104
|
async writeManifest(record: ManifestRecord): Promise<void> {
|
|
98
105
|
const filePath = path.join(this.dir, `${record.id}.json`);
|
|
99
|
-
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
100
106
|
const content = JSON.stringify(record, null, MANIFEST_INDENT_SPACES);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// 1. 写 tmp → fsync 文件
|
|
105
|
-
const fh = await fsPromises.open(tmpPath, "w");
|
|
106
|
-
try {
|
|
107
|
-
await fh.writeFile(content, "utf-8");
|
|
108
|
-
await fh.sync();
|
|
109
|
-
} finally {
|
|
110
|
-
await fh.close();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 2. rename tmp → final(放入 try:失败时 catch 清理 tmp)
|
|
114
|
-
await fsPromises.rename(tmpPath, filePath);
|
|
115
|
-
renamed = true;
|
|
116
|
-
|
|
117
|
-
// 3. fsync 目录(best-effort:POSIX 不要求,失败不否定已成功的 rename)
|
|
118
|
-
try {
|
|
119
|
-
const dirFh = await fsPromises.open(this.dir, "r");
|
|
120
|
-
try {
|
|
121
|
-
await dirFh.sync();
|
|
122
|
-
} finally {
|
|
123
|
-
await dirFh.close();
|
|
124
|
-
}
|
|
125
|
-
} catch (dirSyncErr) {
|
|
126
|
-
bestEffort(dirSyncErr, "fsync dir (writeManifest)");
|
|
127
|
-
}
|
|
128
|
-
} catch (err) {
|
|
129
|
-
// rename 未成功 → 清理残留 tmp(best-effort,不掩盖原错误)
|
|
130
|
-
if (!renamed) {
|
|
131
|
-
try {
|
|
132
|
-
await fsPromises.unlink(tmpPath);
|
|
133
|
-
} catch (cleanupErr) {
|
|
134
|
-
// best-effort:tmp 可能已被 rename 消费或从未创建。不影响主错误(下面 re-throw err)
|
|
135
|
-
bestEffort(cleanupErr, "unlink tmp (writeManifest)");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
throw err;
|
|
139
|
-
}
|
|
107
|
+
// ensureDir:false:目录由构造函数负责创建(缺目录 = 外部删除的异常态,
|
|
108
|
+
// 维持旧实现的 fail-fast 上抛语义,不静默重建)
|
|
109
|
+
await writeAtomicFile(filePath, content, { ensureDir: false });
|
|
140
110
|
}
|
|
141
111
|
|
|
142
112
|
/**
|
|
@@ -212,10 +182,16 @@ export class ManifestStore {
|
|
|
212
182
|
* 1. manifest 已存在 → 删 tmp(陈旧)
|
|
213
183
|
* 2. tmp 合法 + manifest 缺失 → rename tmp 为 manifest
|
|
214
184
|
* 3. tmp 非法 + manifest 缺失 → 删 tmp
|
|
185
|
+
*
|
|
186
|
+
* [T5④ / PS-13] per-file 容错:单个 tmp 文件操作失败(ENOENT——并发回收/外部清理
|
|
187
|
+
* 抢先、EACCES 等)只 warn + 跳过该文件,不再中断整轮——旧实现单文件 ENOENT 即抛,
|
|
188
|
+
* 剩余 tmp 本轮不再处理,自愈但不可见(残留顺延下次启动)。跳过数经 warn 汇总留痕,
|
|
189
|
+
* 调用方返回值形态不变(跳过者不计数)。
|
|
215
190
|
*/
|
|
216
191
|
async recoverTmpFiles(): Promise<{ deleted: number; recovered: number }> {
|
|
217
192
|
let deleted = 0;
|
|
218
193
|
let recovered = 0;
|
|
194
|
+
let failed = 0;
|
|
219
195
|
|
|
220
196
|
const files = fs.readdirSync(this.dir);
|
|
221
197
|
const tmpFiles = files.filter((f) => f.includes(".json.tmp."));
|
|
@@ -225,32 +201,47 @@ export class ManifestStore {
|
|
|
225
201
|
const manifestId = tmpFile.split(".json.tmp.")[0];
|
|
226
202
|
const manifestPath = path.join(this.dir, `${manifestId}.json`);
|
|
227
203
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
204
|
+
try {
|
|
205
|
+
if (fs.existsSync(manifestPath)) {
|
|
206
|
+
// 分支 1: manifest 已存在,删 tmp
|
|
207
|
+
fs.unlinkSync(tmpPath);
|
|
208
|
+
deleted++;
|
|
209
|
+
} else {
|
|
210
|
+
// 试解析 tmp
|
|
211
|
+
try {
|
|
212
|
+
const content = fs.readFileSync(tmpPath, "utf-8");
|
|
213
|
+
const parsed: unknown = JSON.parse(content);
|
|
214
|
+
if (isValidManifest(parsed)) {
|
|
215
|
+
// 分支 2: tmp 是合法 manifest,rename 为正式文件
|
|
216
|
+
fs.renameSync(tmpPath, manifestPath);
|
|
217
|
+
recovered++;
|
|
218
|
+
} else {
|
|
219
|
+
// 分支 3b: 合法 JSON 但非合法 manifest(缺必填字段),删
|
|
220
|
+
fs.unlinkSync(tmpPath);
|
|
221
|
+
deleted++;
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// 分支 3a: JSON.parse 失败,删
|
|
243
225
|
fs.unlinkSync(tmpPath);
|
|
244
226
|
deleted++;
|
|
245
227
|
}
|
|
246
|
-
} catch {
|
|
247
|
-
// 分支 3a: JSON.parse 失败,删
|
|
248
|
-
fs.unlinkSync(tmpPath);
|
|
249
|
-
deleted++;
|
|
250
228
|
}
|
|
229
|
+
} catch (fileErr) {
|
|
230
|
+
// [T5④/PS-13] 单文件失败不中断整轮:warn 留痕(含文件名与原因)后继续处理
|
|
231
|
+
// 剩余 tmp。常见于 tmp 已被并发回收/外部清理删除(ENOENT)——自愈场景不再放大。
|
|
232
|
+
failed++;
|
|
233
|
+
logger.warn(`[subagents] recoverTmpFiles: failed to recover ${tmpFile}, skipping (leftovers retry on next startup)`, {
|
|
234
|
+
detail: fileErr instanceof Error ? fileErr.message : String(fileErr),
|
|
235
|
+
});
|
|
251
236
|
}
|
|
252
237
|
}
|
|
253
238
|
|
|
239
|
+
if (failed > 0) {
|
|
240
|
+
logger.warn(
|
|
241
|
+
`[subagents] recoverTmpFiles: ${failed} of ${tmpFiles.length} tmp file(s) could not be recovered`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
254
245
|
return { deleted, recovered };
|
|
255
246
|
}
|
|
256
247
|
}
|
|
@@ -95,9 +95,9 @@ export interface NotifierHost {
|
|
|
95
95
|
isIdle?: () => boolean;
|
|
96
96
|
/**
|
|
97
97
|
* 原生 settled 事件订阅能力(D8):注册 handler 监听主 agent settled 边沿。
|
|
98
|
-
* 无退订语义(pi 0.84.
|
|
98
|
+
* 无退订语义(pi 0.84.4 的 `pi.on(...)` 全部重载返回 void、无 off——实装锚点:
|
|
99
99
|
* node_modules `@earendil-works/pi-coding-agent` dist/core/extensions/types.d.ts
|
|
100
|
-
* `on()` 系列重载,0.84.
|
|
100
|
+
* `on()` 系列重载,0.84.4 实测)——退订由本模块的 disposed
|
|
101
101
|
* 标志包装兑现(见下方 port 装配)。可选:未注入时内核退化退避轮询(busy 消息
|
|
102
102
|
* 靠退避达上限强发,不走 settled 边沿驱动)。
|
|
103
103
|
*/
|
|
@@ -226,14 +226,14 @@ export function createNotifier(host: NotifierHost): BgNotifier {
|
|
|
226
226
|
host.onAgentSettled === undefined
|
|
227
227
|
? undefined
|
|
228
228
|
: (cb) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
229
|
+
let disposed = false;
|
|
230
|
+
host.onAgentSettled?.(() => {
|
|
231
|
+
if (!disposed) cb();
|
|
232
|
+
});
|
|
233
|
+
return () => {
|
|
234
|
+
disposed = true;
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
237
|
send: (msg, _intent) => {
|
|
238
238
|
// D5 单通道:投递意图唯一化——steer/followUp/nextTurn 多通道全部删除,
|
|
239
239
|
// 唯一发送形态 = sendCustomMessage({triggerTurn:true})。busy 场景由 ledger
|