@zhushanwen/subagent-core 0.8.0 → 0.9.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/dist/{chunk-OL5BK4VN.js → chunk-LZJOEDGQ.js} +455 -203
- package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
- package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
- package/dist/execution/engine/engine-discovery-scan.cjs +216 -155
- package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
- package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
- package/dist/execution/engine/engine-discovery-scan.js +1 -1
- package/dist/execution/relay-env.js +5 -4
- package/dist/index.cjs +7686 -5894
- package/dist/index.d.cts +1876 -1360
- package/dist/index.d.ts +1876 -1360
- package/dist/index.js +7048 -5493
- package/dist.bundle/index.cjs +7747 -5957
- package/package.json +4 -4
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- package/src/execution/__tests__/alive-store.test.ts +117 -1
- package/src/execution/__tests__/batch-finalized.test.ts +206 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
- package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
- package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
- package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
- package/src/execution/__tests__/delivery-methods.test.ts +90 -176
- package/src/execution/__tests__/dialog-queue.test.ts +70 -3
- package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
- package/src/execution/__tests__/execute-nesting.test.ts +8 -5
- package/src/execution/__tests__/execution-record.test.ts +8 -148
- package/src/execution/__tests__/finalize-record.test.ts +295 -129
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
- package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
- package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
- package/src/execution/__tests__/idle-gc.test.ts +60 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
- package/src/execution/__tests__/model-config-service.test.ts +111 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
- package/src/execution/__tests__/notify-ledger.test.ts +42 -1
- package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
- package/src/execution/__tests__/record-binding.test.ts +613 -0
- package/src/execution/__tests__/record-origin.test.ts +318 -0
- package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
- package/src/execution/__tests__/record-store.test.ts +72 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
- package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
- package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
- package/src/execution/__tests__/run-orchestration.test.ts +199 -0
- package/src/execution/__tests__/session-file-gc.test.ts +35 -4
- package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
- package/src/execution/__tests__/state-marker.test.ts +288 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
- package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
- package/src/execution/__tests__/subagent-service.test.ts +17 -215
- package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
- package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
- package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
- package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
- package/src/execution/alive-store.ts +37 -28
- package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
- package/src/execution/config.ts +1 -1
- package/src/execution/conversation-continuation.ts +541 -0
- package/src/execution/dialog-queue.ts +60 -12
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
- package/src/execution/engine/__tests__/registry.test.ts +0 -1
- package/src/execution/engine/__tests__/routing.test.ts +0 -1
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
- package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
- package/src/execution/engine/client/engine-client.ts +81 -25
- package/src/execution/engine/client/mirror.ts +9 -0
- package/src/execution/engine/client/remote-engine.ts +52 -34
- package/src/execution/engine/client/reverse-router.ts +6 -27
- package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
- package/src/execution/engine/common/capability-gate.ts +27 -0
- package/src/execution/engine/common/data-dir.ts +3 -2
- package/src/execution/engine/common/errors.ts +1 -1
- package/src/execution/engine/common/journal-wiring.ts +18 -9
- package/src/execution/engine/common/run-signals.ts +83 -0
- package/src/execution/engine/d8-compat.ts +1 -1
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
- package/src/execution/engine/host/host-bridge.ts +19 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/port.ts +34 -54
- package/src/execution/engine/types.ts +11 -14
- package/src/execution/execution-record.ts +15 -47
- package/src/execution/finalize-record.ts +94 -197
- package/src/execution/idle-gc.ts +9 -3
- package/src/execution/lifecycle-manager.ts +25 -361
- package/src/execution/lifecycle-predicates.ts +22 -19
- package/src/execution/manifest-store.ts +40 -42
- package/src/execution/model-config-service.ts +1 -1
- package/src/execution/notifier.ts +39 -1
- package/src/execution/notify-host.ts +13 -4
- package/src/execution/record-entry.ts +20 -2
- package/src/execution/record-store.ts +925 -160
- package/src/execution/relay-env.ts +8 -3
- package/src/execution/round-supervisor/domain.ts +17 -5
- package/src/execution/round-supervisor/service-binding.test.ts +89 -53
- package/src/execution/round-supervisor/service-binding.ts +71 -21
- package/src/execution/round-supervisor/supervisor.ts +23 -9
- package/src/execution/service/record-access.ts +547 -0
- package/src/execution/service/record-lifecycle.ts +542 -0
- package/src/execution/service/run-orchestration.ts +1647 -0
- package/src/execution/service/service-bootstrap.ts +111 -0
- package/src/execution/service/service-constants.ts +27 -0
- package/src/execution/service/session-baselines.ts +401 -0
- package/src/execution/service/sync-collect-domain.ts +442 -0
- package/src/execution/service/workflow-dispatch.ts +567 -0
- package/src/execution/session-file-gc.ts +11 -8
- package/src/execution/session-reconstructor.ts +44 -1
- package/src/execution/sessions-index.ts +20 -3
- package/src/execution/settled-watchdog.ts +86 -43
- package/src/execution/state-marker.ts +516 -0
- package/src/execution/stream-sink.ts +1 -1
- package/src/execution/subagent-actions-core.ts +53 -31
- package/src/execution/subagent-service.ts +643 -2980
- package/src/execution/subprocess-agent-runner.ts +46 -323
- package/src/execution/types.ts +60 -43
- package/src/execution/ui-request-handler-factory.ts +5 -0
- package/src/execution/worktree-manager.ts +1 -1
- package/src/execution/worktree-registry.ts +1 -1
- package/src/index.ts +6 -5
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
- package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
- package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
- package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
- package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +1 -1
- package/src/orchestration/models/ports.ts +20 -13
- package/src/orchestration/models/types.ts +11 -13
- package/src/orchestration/run-snapshot.ts +18 -22
- package/src/orchestration/worker-message-pump.ts +33 -57
- package/src/shared/atomic-write.ts +10 -8
- package/dist/chunk-T2SYBW3I.js +0 -24
- package/dist/engine-discovery-scan-BXLA8y-z.d.cts +0 -1624
- package/dist/engine-discovery-scan-BXLA8y-z.d.ts +0 -1624
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
- package/src/execution/__tests__/finalized-marker.test.ts +0 -104
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
- package/src/execution/finalized-marker.ts +0 -70
- package/src/execution/tombstone-store.ts +0 -72
|
@@ -2,30 +2,30 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Subagent 持续对话 V2 — 进程生命周期管理(§5.2 模块 1)。
|
|
4
4
|
//
|
|
5
|
-
// 本模块是
|
|
6
|
-
//
|
|
7
|
-
// ChildProcess
|
|
5
|
+
// 本模块是 subagent 进程生命周期管理器(现存唯一职责 = idle timer),以**模块级
|
|
6
|
+
// 单例**持有全局进程状态(per-record idle timer)。它**不直接持有
|
|
7
|
+
// ChildProcess 句柄**——句柄在 engine/host/spawned-children.ts——因此
|
|
8
8
|
// 所有「副作用」能力(kill / 探活 / 超时回调)都由调用方经回调/参数注入,本模块
|
|
9
9
|
// 只管状态记账 + 调度顺序。这让模块可独立编译 + 单测,无需拉起真实子进程。
|
|
10
10
|
//
|
|
11
|
-
//
|
|
11
|
+
// 唯一职责(V2 §5.2 五项职责的存留):
|
|
12
12
|
// 1. idle timer —— agent_settled arm / 新 turn disarm / 超时触发 onTimeout(决策 4)
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
13
|
+
// 【[H1 U6 后现状] arm 链已失活:旧「subagent-service.ts chat 域 arm/disarm」接线
|
|
14
|
+
// 随 chat 域退役删除,armIdleTimer 唯一剩余接线在 createHostBridge
|
|
15
|
+
// (host-bridge.ts)——而后者全仓无生产调用点。运行时 timer 永不 armed,
|
|
16
|
+
// disarmIdleTimer 仅剩 record-lifecycle 终态化路径的幂等清扫(对永不 armed
|
|
17
|
+
// 的 timer 为 no-op,保持终态清扫完整性)。模块与函数保留 = 既有判据形态
|
|
18
|
+
// (lifecycle-predicates.isIdle 消费 hasIdleTimer)与 env/API 面
|
|
19
|
+
// (ExecuteOptions.idleTimeoutMs 校验文案引用 DEFAULT_IDLE_TIMEOUT_MS)不删,
|
|
20
|
+
// 语义变化/显式 idle 状态设计属独立议题(impl-plan Gate B 收口 backlog)】
|
|
21
|
+
// 其余四项已删除:职责 2 全局 ceiling / 职责 3 shutdown 收割 / 职责 4 孤儿扫描自
|
|
22
|
+
// 落地起无生产接线;职责 5 activate 互斥的历史接线点(冷路径 resume 前)随协议化
|
|
23
|
+
// 重构消失、仅余自持单测。未来需要时按
|
|
24
|
+
// docs/design/v2-defense-ii-iii-resolution.md 重新设计。
|
|
23
25
|
//
|
|
24
|
-
//
|
|
25
|
-
// index.ts,由后续步骤接入;本模块不 import 它们,避免循环依赖。
|
|
26
|
+
// 本模块不 import subagent-service 等 execution 编排层,避免循环依赖。
|
|
26
27
|
//
|
|
27
|
-
// 设计参考:
|
|
28
|
-
// 触发条件重构);spawnedChildren Map 的模块级单例模式。
|
|
28
|
+
// 设计参考:setTimeout→SIGTERM 骨架(复用 timer 形态);模块级单例 Map 记账模式。
|
|
29
29
|
|
|
30
30
|
import { getLogger } from "../core/logger.ts";
|
|
31
31
|
import { assertSafeTimerDelay } from "../shared/timer-delay.ts";
|
|
@@ -48,6 +48,12 @@ const SECONDS_PER_MINUTE = 60;
|
|
|
48
48
|
*
|
|
49
49
|
* V2 §5.4 / 决策 4:初拟 ≤ prompt cache TTL(~5min)——超出 cacheTTL 的活进程白占
|
|
50
50
|
* 内存(续聊仍 cache miss),小于则 kill 丢热 cache。实测定(P-timeout)。
|
|
51
|
+
*
|
|
52
|
+
* [H1 U6 后现状] 「5min 超时回收保活进程」语义已无对象(每轮 = 新进程,轮末随
|
|
53
|
+
* agent_settled 回收,无长驻进程可超时)。常量存活消费 = assertIdleTimeoutMsSafe
|
|
54
|
+
* 错误文案基准(run-orchestration.ts)+ env XYZ_SUBAGENT_IDLE_TIMEOUT_MS 非法值
|
|
55
|
+
* 回落默认 + ExecuteOptions.idleTimeoutMs API 校验域——属 API 面常量,非活性
|
|
56
|
+
* timer 语义。
|
|
51
57
|
*/
|
|
52
58
|
const IDLE_TIMEOUT_MINUTES = 5;
|
|
53
59
|
export const DEFAULT_IDLE_TIMEOUT_MS = IDLE_TIMEOUT_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND;
|
|
@@ -80,19 +86,6 @@ function getEnvIdleTimeoutMs(): number | undefined {
|
|
|
80
86
|
return parsed;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
/**
|
|
84
|
-
* 默认全局活进程上限(ceiling)。
|
|
85
|
-
*
|
|
86
|
-
* V2 §5.4 / 决策 4:防「N 个 subagent 于 timeout 窗口内高频复用」场景的内存高水位
|
|
87
|
-
* 无上界。候选 8-10,实测定(P-ceiling)。
|
|
88
|
-
*
|
|
89
|
-
* 注意:挤出候选**仅限空闲进程**(busy 进程不做候选,V2 决策 4 硬约束)。本模块不
|
|
90
|
-
* 感知 busy(进程句柄在 session-runner),调用方接入 evictIfOverCeiling 前应确保
|
|
91
|
-
* 已把 busy 进程移出活进程集合(unregisterActiveProcess),或由后续步骤扩展注入
|
|
92
|
-
* isBusy 谓词。
|
|
93
|
-
*/
|
|
94
|
-
export const DEFAULT_MAX_ALIVE_PROCESSES = 8;
|
|
95
|
-
|
|
96
89
|
// ============================================================
|
|
97
90
|
// 职责 1:idle timer(per-record)
|
|
98
91
|
// ============================================================
|
|
@@ -194,347 +187,18 @@ export function hasIdleTimer(recordId: string): boolean {
|
|
|
194
187
|
return idleTimers.has(recordId);
|
|
195
188
|
}
|
|
196
189
|
|
|
197
|
-
// ============================================================
|
|
198
|
-
// 职责 2:全局 ceiling(活进程上限 + LRU)
|
|
199
|
-
//
|
|
200
|
-
// [状态:未接线(deferred)] register/touch/unregister/evictIfOverCeiling 均无生产
|
|
201
|
-
// 调用方(仅本模块单测引用)——「N 个 chatMode 长驻进程内存高水位」防护当前**未生效**。
|
|
202
|
-
// 接线条件:spawn/activate 完成处 registerActiveProcess + 事件驱动或周期调
|
|
203
|
-
// evictIfOverCeiling(注入 SIGTERM 回调),busy 进程按 evictIfOverCeiling docstring
|
|
204
|
-
// 约束移出集合。保留理由:activeProcesses 记账同时是职责 4 孤儿判定(scanOrphanProcesses
|
|
205
|
-
// 的「不在当前活进程集合」检查)的数据通路,与职责 4 一并待接入,不宜单删。
|
|
206
|
-
// ============================================================
|
|
207
|
-
|
|
208
|
-
interface ActiveProcessEntry {
|
|
209
|
-
/** 最近活动时间戳(Date.now())。LRU 挤出时取最小者。 */
|
|
210
|
-
lastTouched: number;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* recordId → 活进程记账项。register 时加入,unregister/挤出/reap 时移除。
|
|
215
|
-
*
|
|
216
|
-
* 该集合**不是** ChildProcess 句柄表(那在 spawnedChildren),只是 lifecycle-manager
|
|
217
|
-
* 用于 ceiling 判定 + orphan 判定 + reap 枚举的记账结构。
|
|
218
|
-
*/
|
|
219
|
-
const activeProcesses = new Map<string, ActiveProcessEntry>();
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 登记一个活进程(activate/spawn 完成时调)。
|
|
223
|
-
*
|
|
224
|
-
* 重复 register 同一 recordId 视为刷新(更新 lastTouched),不叠加。
|
|
225
|
-
*/
|
|
226
|
-
export function registerActiveProcess(recordId: string): void {
|
|
227
|
-
activeProcesses.set(recordId, { lastTouched: Date.now() });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 续聊/新 turn 时 touch 某 record,更新 LRU 时间戳(让它不被 LRU 挤出)。
|
|
232
|
-
*
|
|
233
|
-
* 不存在时 no-op(保守:避免隐式创建掩盖状态不一致;调用方应先 register)。
|
|
234
|
-
*/
|
|
235
|
-
export function touchActiveProcess(recordId: string): void {
|
|
236
|
-
const entry = activeProcesses.get(recordId);
|
|
237
|
-
if (!entry) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
entry.lastTouched = Date.now();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* 注销一个活进程(进程退出/终态化/已被 reap 时调)。
|
|
245
|
-
*
|
|
246
|
-
* cascade:同时 disarm 该 record 的 idle timer——进程都没了,残留 timer 触发只会
|
|
247
|
-
* 操作已清理的 record(V2 决策 4 一致性)。不存在时 no-op。
|
|
248
|
-
*/
|
|
249
|
-
export function unregisterActiveProcess(recordId: string): void {
|
|
250
|
-
activeProcesses.delete(recordId);
|
|
251
|
-
disarmIdleTimer(recordId);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* 超过 ceiling 时按 LRU 挤出最久空闲的进程,直到活进程数 ≤ ceiling。
|
|
256
|
-
*
|
|
257
|
-
* 挤出语义(V2 决策 4 passivate):对每个被挤出者调 onEvict(调用方注入:SIGTERM
|
|
258
|
-
* 回收),并从活进程集合移除 + disarm 其 idle timer(passivate 后进程死,timer 无意义)。
|
|
259
|
-
*
|
|
260
|
-
* **busy 不做挤出候选**(V2 决策 4 硬约束):本模块不感知 busy,调用方接入前负责把
|
|
261
|
-
* busy 进程 unregister 出集合(或后续步骤注入 isBusy 谓词),否则 busy 进程可能被误杀。
|
|
262
|
-
*
|
|
263
|
-
* @param onEvict 挤出回调(recordId)——调用方 SIGTERM 该进程
|
|
264
|
-
* @param maxAlive 可选上限覆盖(默认 DEFAULT_MAX_ALIVE_PROCESSES);注入便于测试
|
|
265
|
-
*/
|
|
266
|
-
export function evictIfOverCeiling(
|
|
267
|
-
onEvict: (recordId: string) => void,
|
|
268
|
-
maxAlive: number = DEFAULT_MAX_ALIVE_PROCESSES,
|
|
269
|
-
): void {
|
|
270
|
-
while (activeProcesses.size > maxAlive) {
|
|
271
|
-
// 找 lastTouched 最小的(最久未活动)。Map 迭代顺序 = 插入顺序,最小值扫描稳定。
|
|
272
|
-
let oldestId: string | undefined;
|
|
273
|
-
let oldestTouched = Infinity;
|
|
274
|
-
for (const [id, entry] of activeProcesses) {
|
|
275
|
-
if (entry.lastTouched < oldestTouched) {
|
|
276
|
-
oldestTouched = entry.lastTouched;
|
|
277
|
-
oldestId = id;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (oldestId === undefined) {
|
|
281
|
-
break; // 防御:集合为空但 size 判定异常时退出
|
|
282
|
-
}
|
|
283
|
-
// 先移除 + disarm,再回调。onEvict 是 passivate 语义(SIGTERM 回收),
|
|
284
|
-
// 不应在其中重新 register 同一 record——否则 size 不减会导致 while 死循环。
|
|
285
|
-
activeProcesses.delete(oldestId);
|
|
286
|
-
disarmIdleTimer(oldestId);
|
|
287
|
-
onEvict(oldestId);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* 查询当前活进程数(诊断/接入期断言用)。
|
|
293
|
-
*/
|
|
294
|
-
export function getActiveProcessCount(): number {
|
|
295
|
-
return activeProcesses.size;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ============================================================
|
|
299
|
-
// 职责 3:shutdown 收割(防线 i)
|
|
300
|
-
//
|
|
301
|
-
// [状态:未接线,无等价实现([F-7 纠偏])] reapAllAliveProcesses 无生产调用方。旧注
|
|
302
|
-
// 「shutdown 收割已由 reapSpawnedChildrenOnShutdown 等价覆盖」失实:该 hook 调
|
|
303
|
-
// killAllSpawnedChildren,而后者协议化后(engines/pi inproc 删除、子进程活在引擎
|
|
304
|
-
// 进程内)仅做 core 侧镜像置死,不发任何 SIGTERM(_signal 参数被忽略)——引擎进程
|
|
305
|
-
// 实际回收走 stdin-EOF 自灭链,本职责的显式收割仍无接线。本函数保留作未来接入面;
|
|
306
|
-
// 若未来接入须先保证 register/unregister 记账完整,否则收割列表不全。
|
|
307
|
-
// ============================================================
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* 收割当前全部活进程(父进程 shutdown 时调)。
|
|
311
|
-
*
|
|
312
|
-
* 遍历活进程集合,对每个调 killFn(调用方注入:child.kill("SIGTERM")),并 disarm 对应
|
|
313
|
-
* idle timer 避免 timer 泄漏(V2 决策 7 防线 i:shutdown hook 显式收割)。
|
|
314
|
-
*
|
|
315
|
-
* @param killFn 收割回调(recordId)——调用方对 spawnedChildren 中的句柄发 SIGTERM
|
|
316
|
-
* @returns 被收割的 recordId 列表(按集合迭代序)
|
|
317
|
-
*/
|
|
318
|
-
export function reapAllAliveProcesses(killFn: (recordId: string) => void): string[] {
|
|
319
|
-
const reaped: string[] = [];
|
|
320
|
-
// 拷贝 keys 再遍历——killFn 的副作用可能间接触发 unregister,避免迭代中改集合。
|
|
321
|
-
for (const recordId of [...activeProcesses.keys()]) {
|
|
322
|
-
killFn(recordId);
|
|
323
|
-
reaped.push(recordId);
|
|
324
|
-
}
|
|
325
|
-
// 全部收割后清空记账 + disarm 所有 timer。
|
|
326
|
-
for (const entry of idleTimers.values()) {
|
|
327
|
-
clearTimeout(entry.timer);
|
|
328
|
-
}
|
|
329
|
-
idleTimers.clear();
|
|
330
|
-
activeProcesses.clear();
|
|
331
|
-
return reaped;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ============================================================
|
|
335
|
-
// 职责 4:孤儿扫描(防线 ii)
|
|
336
|
-
// ============================================================
|
|
337
|
-
|
|
338
|
-
/** 孤儿扫描候选:来自持久化 record 的 pid 信息。 */
|
|
339
|
-
export interface OrphanCandidate {
|
|
340
|
-
readonly id: string;
|
|
341
|
-
/** 持久化的进程 pid(缺失表示上次未持久化,无法判定)。 */
|
|
342
|
-
readonly pid?: number;
|
|
343
|
-
/** session 文件路径(调用方二次校验命令行时可用,本函数不消费)。 */
|
|
344
|
-
readonly sessionFile?: string;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* 扫描孤儿进程候选(父进程启动时调)。
|
|
349
|
-
*
|
|
350
|
-
* 判定:pid 存在 && isProcessAlive(pid) && 该 record **不在**当前活进程集合 =
|
|
351
|
-
* 上次崩溃/异常退出遗留的孤儿(V2 决策 7 防线 ii)。
|
|
352
|
-
*
|
|
353
|
-
* **PID 复用风险**(V2 §5.4):仅靠 pid 存活不够——pid 可能被 OS 复用给无关进程。
|
|
354
|
-
* 本函数只按 pid 判定并返回候选列表,**调用方收割前必须二次校验**进程命令行含
|
|
355
|
-
* `pi --mode rpc`(确保是 subagent 残留而非被复用的无关进程)。该校验在调用方,不在本函数。
|
|
356
|
-
*
|
|
357
|
-
* 本函数只读,不改任何状态(孤儿收割由调用方对 killFn 执行)。
|
|
358
|
-
*
|
|
359
|
-
* @param records 持久化的 record 列表(含 pid)
|
|
360
|
-
* @param isProcessAlive pid 探活谓词(调用方注入,对齐 alive-store.isProcessAlive)
|
|
361
|
-
* @returns 孤儿候选 recordId 列表
|
|
362
|
-
*
|
|
363
|
-
* **状态:deferred(defense-in-depth,当前 spawn 配置下不触发)**。本函数骨架 + 单测已就绪,
|
|
364
|
-
* 数据通路(`.alive` sidecar 写 pid / `reconstructAll` 分支 3 读 alive.pid)也已落地,但
|
|
365
|
-
* **不接入 session_start**。理由 + 接入设计草图见
|
|
366
|
-
* `docs/design/v2-defense-ii-iii-resolution.md`(防线 ii 章节)。要点:当前 spawn 用 piped
|
|
367
|
-
* stdio(非 detach),父进程死亡 → stdin EOF → 子进程自杀(F10)覆盖全部正常崩溃路径,
|
|
368
|
-
* 孤儿近乎不可能泄漏;安全收割需 PID 复用校验(跨平台进程命令行读取,防 OS 复用 pid 误杀),
|
|
369
|
-
* 属低频场景的中等复杂度工作,待 spawn 改 detach 时再接入。
|
|
370
|
-
*/
|
|
371
|
-
export function scanOrphanProcesses(
|
|
372
|
-
records: OrphanCandidate[],
|
|
373
|
-
isProcessAlive: (pid: number) => boolean,
|
|
374
|
-
): string[] {
|
|
375
|
-
const orphans: string[] = [];
|
|
376
|
-
for (const record of records) {
|
|
377
|
-
if (record.pid === undefined) {
|
|
378
|
-
continue; // 无 pid 无法判定,不视为孤儿
|
|
379
|
-
}
|
|
380
|
-
if (activeProcesses.has(record.id)) {
|
|
381
|
-
continue; // 已在本进程 activation 集合内,非孤儿
|
|
382
|
-
}
|
|
383
|
-
if (isProcessAlive(record.pid)) {
|
|
384
|
-
orphans.push(record.id);
|
|
385
|
-
}
|
|
386
|
-
// pid 不存活 → 进程已退出,非孤儿
|
|
387
|
-
}
|
|
388
|
-
return orphans;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ============================================================
|
|
392
|
-
// 职责 5:activate 互斥(防线 iii,防双写者)
|
|
393
|
-
// ============================================================
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* recordId → 该 record 当前 activate 链尾的 Promise。
|
|
397
|
-
*
|
|
398
|
-
* 同一 recordId 的第二次 acquireActivateLock 会 await 链尾,直到前者 release 才 resolve,
|
|
399
|
-
* 从而串行化并发 activate,保证「同一 recordId 全局最多一个活进程」不变量(V2 决策 7
|
|
400
|
-
* 防线 iii:双写者交错 append 会写坏整个 session 文件,比脏 entry 致命一个量级)。
|
|
401
|
-
*
|
|
402
|
-
* **tail-identity 自清**(防长进程 recordId 条目泄漏):release 后经 queueMicrotask
|
|
403
|
-
* 异步检查——Map 链尾仍是本链尾(identity 匹配)时 delete 回收;有 waiter 排队时其
|
|
404
|
-
* acquire 已用新链尾覆盖 Map(identity 不匹配)→ 不删,条目由最后释放者回收。waiter
|
|
405
|
-
* 持 acquire 时刻捕获的 prev Promise 引用而非 Map 查询,delete 不影响其等待。30s
|
|
406
|
-
* 超时兜底与 _resetLifecycleState(全量 clear)语义不变。
|
|
407
|
-
*/
|
|
408
|
-
const activateLockTails = new Map<string, Promise<void>>();
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* 获取某 record 的 activate 串行锁。
|
|
412
|
-
*
|
|
413
|
-
* - 同 recordId 首次 acquire:立即 resolve,返回 release 函数。
|
|
414
|
-
* - 同 recordId 第二次 acquire(前者未 release):pending,直到前者 release 才 resolve。
|
|
415
|
-
* - 不同 recordId 互不阻塞(各自独立链)。
|
|
416
|
-
*
|
|
417
|
-
* @returns release 函数——获得锁后**必须**调用它释放(finally 块),否则同 recordId
|
|
418
|
-
* 的后续 acquire 永久挂起。
|
|
419
|
-
*
|
|
420
|
-
* **状态:已接入(双保险,D3)**。`subagent-service.ts:894` 冷路径 resume 前调
|
|
421
|
-
* `acquireActivateLock(record.id)`,作为 idle CAS 守卫之外的结构化防护层:idle CAS
|
|
422
|
-
*(`status !== "idle"` 检查与 `status = "running"` 翻转间无 await)仍是一级守卫,
|
|
423
|
-
* reject 并发 message;锁把冷路径 resume spawn 的双写者交错升级为串行排队,防坏 session。
|
|
424
|
-
* 超时兜底见下方 `ACTIVATE_LOCK_TIMEOUT_MS`(V3 D3 / v4-lifecycle-convergence.md A-2)。
|
|
425
|
-
*/
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* acquireActivateLock 等待前序锁释放的超时(v4 A-2)。
|
|
429
|
-
*
|
|
430
|
-
* 前 holder 崩溃/死锁导致 release 永不触发时,waiter 不无限挂起;30s 超时后抛含恢复指引
|
|
431
|
-
* 的错误(调用方可用 message action 重试)。30s 远超正常冷路径 resume spawn 耗时(~ms 级),
|
|
432
|
-
* 留足异常恢复余量而不误伤正常排队。
|
|
433
|
-
*/
|
|
434
|
-
const ACTIVATE_LOCK_TIMEOUT_SECONDS = 30;
|
|
435
|
-
const ACTIVATE_LOCK_TIMEOUT_MS = ACTIVATE_LOCK_TIMEOUT_SECONDS * MS_PER_SECOND;
|
|
436
|
-
|
|
437
|
-
export function acquireActivateLock(recordId: string): Promise<() => void> {
|
|
438
|
-
const prev = activateLockTails.get(recordId) ?? Promise.resolve();
|
|
439
|
-
let releaseFn!: () => void;
|
|
440
|
-
// [review 修复] 超时放行句柄:超时者从未持有锁(race 已 reject),releaseFn 不会被
|
|
441
|
-
// 调用方触发,current 将永久 pending → tail(= prev.then(() => current))永久
|
|
442
|
-
// pending → 后续同 recordId 的 acquire 只能靠 30s 超时出队(锁链瘫痪,只能重启
|
|
443
|
-
// 恢复)。超时回调显式 resolve current 放行链尾(见下方 timeoutPromise)。
|
|
444
|
-
let settleCurrent!: () => void;
|
|
445
|
-
const current = new Promise<void>((resolve) => {
|
|
446
|
-
settleCurrent = resolve;
|
|
447
|
-
// tail-identity 自清(ES7/LOCK_TAIL_GC_RACE):resolve 后经 microtask 异步回收——
|
|
448
|
-
// 仅当 Map 链尾仍是本链尾(tail)时 delete。自清检查只读 Map 引用比较、不依赖
|
|
449
|
-
// 链尾 Promise 是否已 settle——release() 同步 resolve 后 microtask 执行时,无论
|
|
450
|
-
// then 链推进到哪一步,identity 比较结果一致;同 recordId 快速 acquire→release→
|
|
451
|
-
// acquire 序下,后继 acquire 的 set 已覆盖 Map,先行 release 的自清检查
|
|
452
|
-
// get !== 旧 tail → 不删(正确保留新链)。
|
|
453
|
-
releaseFn = () => {
|
|
454
|
-
resolve();
|
|
455
|
-
queueMicrotask(() => {
|
|
456
|
-
if (activateLockTails.get(recordId) === tail) {
|
|
457
|
-
activateLockTails.delete(recordId);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
};
|
|
461
|
-
});
|
|
462
|
-
// 链尾 = 等 prev 完成后挂 current;current 在 releaseFn 调用前保持 pending,
|
|
463
|
-
// 让下一次 acquire 的 prev 等到本次 release。tail 引用同时作为自清的 identity key。
|
|
464
|
-
const tail = prev.then(() => current);
|
|
465
|
-
activateLockTails.set(recordId, tail);
|
|
466
|
-
|
|
467
|
-
// 30s 超时兜底(v4 A-2):前序 holder 长期不 release(崩溃/死锁)时,waiter 不无限挂起,
|
|
468
|
-
// 超时抛含恢复指引的错误(调用方可用 message action 重试)。正常拿到锁时 clearTimeout
|
|
469
|
-
// 取消未触发的 timer 防泄漏。超时只 reject 本次 acquire 的返回 promise,不改 release 语义——
|
|
470
|
-
// 原 holder release 仍 resolve current,链尾照常推进,后续 waiter 各受同样 30s 保护。
|
|
471
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
472
|
-
// acquire 已赢标记:prev 的 then 回调(微任务)恒先于 timer 回调(宏任务)执行,
|
|
473
|
-
// 标记位防「prev 恰在 30s 边界 settle、clearTimeout 未赶上已入队 timer」的窗口——
|
|
474
|
-
// 那时调用方已(将)持有锁,若再放行链尾会让后续 waiter 提前获锁形成双写者。
|
|
475
|
-
let acquired = false;
|
|
476
|
-
const acquirePromise = prev.then(() => {
|
|
477
|
-
acquired = true;
|
|
478
|
-
clearTimeout(timeoutId);
|
|
479
|
-
return releaseFn;
|
|
480
|
-
});
|
|
481
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
482
|
-
timeoutId = setTimeout(() => {
|
|
483
|
-
if (acquired) return;
|
|
484
|
-
// [review 修复] 超时放行链尾:resolve 自身 current,保证前序 holder release 后
|
|
485
|
-
// tail 可 settle(不绕过前序等待——prev 未 settle 时 tail 仍等 prev,互斥保持),
|
|
486
|
-
// 后续 acquire 不被本超时者的永久 pending tail 卡死。
|
|
487
|
-
settleCurrent();
|
|
488
|
-
// [review 修复] 超时者对称自清(round2 INFO 残留):releaseFn 永不被调用(race
|
|
489
|
-
// 已 reject),tail-identity 自清 microtask 不排队,条目滞留 Map 直到下次同
|
|
490
|
-
// recordId acquire 覆盖。此处挂 tail 尾部做同款 identity 自清——只有 tail
|
|
491
|
-
// 真正 settle(prev 也已 release)才回收:不能直接 queueMicrotask 删除——前序
|
|
492
|
-
// 仍持锁(tail pending)时删条目会让后续 acquire 不再排队等前序 release,
|
|
493
|
-
// 破坏互斥(超时放行的是链尾 settle,不是锁获取)。tail 永不 settle(前序
|
|
494
|
-
// 崩溃)时回调不执行,条目与现状一致由 _resetLifecycleState 兜底清理。
|
|
495
|
-
tail.then(() => {
|
|
496
|
-
if (activateLockTails.get(recordId) === tail) {
|
|
497
|
-
activateLockTails.delete(recordId);
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
reject(
|
|
501
|
-
new Error(
|
|
502
|
-
`subagent ${recordId} activation timed out; retry action: message`,
|
|
503
|
-
),
|
|
504
|
-
);
|
|
505
|
-
}, ACTIVATE_LOCK_TIMEOUT_MS);
|
|
506
|
-
});
|
|
507
|
-
return Promise.race([acquirePromise, timeoutPromise]);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
190
|
// ============================================================
|
|
511
191
|
// 测试钩子(模块级单例状态隔离)
|
|
512
192
|
// ============================================================
|
|
513
193
|
|
|
514
194
|
/**
|
|
515
|
-
* 清空全部模块级状态(idle timer
|
|
195
|
+
* 清空全部模块级状态(idle timer)。
|
|
516
196
|
*
|
|
517
197
|
* 仅用于单测的 beforeEach 隔离——clearTimeout 所有 armed timer 防止跨用例泄漏。
|
|
518
|
-
*
|
|
519
|
-
* 注意:不会 resolve 已 acquire 但未 release 的锁 Promise(那些 pending holder 由测试
|
|
520
|
-
* 自律 release;reset 后它们的链尾引用被 Map 丢弃,不再阻塞后续 acquire)。
|
|
521
198
|
*/
|
|
522
199
|
export function _resetLifecycleState(): void {
|
|
523
200
|
for (const entry of idleTimers.values()) {
|
|
524
201
|
clearTimeout(entry.timer);
|
|
525
202
|
}
|
|
526
203
|
idleTimers.clear();
|
|
527
|
-
activeProcesses.clear();
|
|
528
|
-
activateLockTails.clear();
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* 测试钩子:返回 activateLockTails 当前条目数。
|
|
533
|
-
*
|
|
534
|
-
* activateLockTails 是模块私有 const,自清语义(tail-identity 回收)的断言需要
|
|
535
|
-
* 观察点——本导出仅测试使用,命名对齐 _resetLifecycleState(本文件)与
|
|
536
|
-
* _resetProcessShutdownGuardForTest(index.ts)先例。
|
|
537
|
-
*/
|
|
538
|
-
export function _getActivateLockTailCountForTest(): number {
|
|
539
|
-
return activateLockTails.size;
|
|
540
204
|
}
|
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
// lifecycle-manager.hasIdleTimer(idle timer 是否 armed)与 core 侧 spawnedChildren
|
|
8
8
|
// 状态镜像读点(engine/host/spawned-children.ts,活进程句柄是否存在),不新增状态记账。
|
|
9
9
|
//
|
|
10
|
-
// 两种 running 子态(v4 B-1
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// 两种 running 子态(v4 B-1;[H1 U6 后现状] 长驻保活形态已退役——每轮 = 新进程,
|
|
11
|
+
// 轮末进程随 agent_settled 回收,权威注释见 run-orchestration.ts [H1 U6] 段):
|
|
12
|
+
// - 等待续聊(旧 idle):轮完成后进程已回收 → isResumable=true(无活进程),
|
|
13
|
+
// 续聊走 deliverMessage 冷路径 resume(续写原 session 文件)。旧设计另有
|
|
14
|
+
// 「agent_settled arm idle timer 保活进程待热路径 prompt」一路——该 arm 链随
|
|
15
|
+
// H1 U6 长驻退役失活:armIdleTimer 唯一生产接线在 createHostBridge
|
|
16
|
+
// (host-bridge.ts,全仓无生产调用点),运行时 hasIdleTimer 恒 false,
|
|
17
|
+
// 故 isIdle 生产恒 false,notify 守卫(notify-host.ts)实际生效的放行谓词 =
|
|
18
|
+
// closed 或 isResumable。isIdle 谓词保留 = 既有判据形态不删(语义变化/显式
|
|
19
|
+
// idle 状态设计属独立议题,见 impl-plan Gate B 收口登记的 backlog)。
|
|
16
20
|
// - 正在执行:isIdle=false、isResumable=false(有活进程)。
|
|
17
|
-
//
|
|
18
|
-
// 时序保证(session-runner.ts:670-686):agent_settled handler 内 armIdleTimer 在
|
|
19
|
-
// onRoundSettled(notify)与 resolveRun(runAndFinalize early-return 检查)之前执行,
|
|
20
|
-
// 故 isIdle 在这两个检查点恒为 true——notify 守卫与 early-return 判据可靠。
|
|
21
21
|
|
|
22
22
|
import { hasIdleTimer } from "./lifecycle-manager.ts";
|
|
23
23
|
// [W6 拆依赖] 活进程句柄读点改经 core 侧 spawnedChildren 状态镜像公共面
|
|
@@ -31,7 +31,8 @@ import type { ExecutionRecord } from "./types.ts";
|
|
|
31
31
|
*
|
|
32
32
|
* [W6] 改读 core 侧状态镜像(engine/host/spawned-children.ts 的 hasLiveProcessHandleCore:
|
|
33
33
|
* 镜像项存在且未 killed = 有活进程句柄;迁移期内建 pi 的 inproc 权威 map 并读兜底)。
|
|
34
|
-
*
|
|
34
|
+
* 轮末进程随 agent_settled 回收 / 被 kill 后镜像项置死或移除 = 无活进程
|
|
35
|
+
* (等待续聊态 / 跨重启重建同理)。
|
|
35
36
|
*/
|
|
36
37
|
export function hasLiveProcessHandle(recordId: string): boolean {
|
|
37
38
|
return hasLiveProcessHandleCore(recordId);
|
|
@@ -40,11 +41,12 @@ export function hasLiveProcessHandle(recordId: string): boolean {
|
|
|
40
41
|
/**
|
|
41
42
|
* 对话模式等待续聊态(旧 idle 收敛后的派生谓词)。
|
|
42
43
|
*
|
|
43
|
-
* 判据:该 record 有 armed idle timer(lifecycle-manager.hasIdleTimer)。
|
|
44
|
-
* 时 session-runner arm timer(V2 决策 4);新 turn / 终态化时 disarm。
|
|
44
|
+
* 判据:该 record 有 armed idle timer(lifecycle-manager.hasIdleTimer)。
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* [H1 U6 后现状] arm 链随长驻形态退役失活(armIdleTimer 唯一生产接线在
|
|
47
|
+
* createHostBridge,全仓无生产调用点)——运行时本谓词恒 false,notify 守卫的
|
|
48
|
+
* 轮次完成放行实际由 {@link isResumable} 承担。谓词保留为既有判据形态
|
|
49
|
+
* (notify 合批 hasRunningBackground / 守卫散点消费),删除属语义变更待独立议题。
|
|
48
50
|
*/
|
|
49
51
|
export function isIdle(record: ExecutionRecord): boolean {
|
|
50
52
|
return hasIdleTimer(record.id);
|
|
@@ -53,11 +55,12 @@ export function isIdle(record: ExecutionRecord): boolean {
|
|
|
53
55
|
/**
|
|
54
56
|
* 可冷路径 resume(running 且无活进程句柄)。
|
|
55
57
|
*
|
|
56
|
-
*
|
|
57
|
-
* close action
|
|
58
|
+
* 等待续聊态的现行判据([H1 U6] 每轮 = 新进程、轮末即回收,完成的轮次天然无活
|
|
59
|
+
* 进程句柄;跨重启重建同理)。deliverMessage 冷路径、GC、close action 的
|
|
60
|
+
* 「无活进程立即终态化」分支据此判定。
|
|
58
61
|
*
|
|
59
|
-
* 与 {@link isIdle} 的关系:
|
|
60
|
-
*
|
|
62
|
+
* 与 {@link isIdle} 的关系:isIdle 生产恒 false(见其 JSDoc),本谓词是
|
|
63
|
+
* 「等待续聊」唯一运行时生效的派生判据。
|
|
61
64
|
*
|
|
62
65
|
* [v4 A-6] 签名泛化为 Pick<"id"|"status">:ExecutionRecord(活态)与 SubagentRecord
|
|
63
66
|
* (list 快照)均结构兼容——recordToListItem 据此为 list 输出派生 resumable 字段。
|
|
@@ -6,6 +6,7 @@ import { getLogger } from "../core/logger.ts";
|
|
|
6
6
|
|
|
7
7
|
import { bestEffort } from "./best-effort.ts";
|
|
8
8
|
import { writeAtomicFile } from "../shared/atomic-write.ts";
|
|
9
|
+
import type { ClosedReason, ExecutionStatus } from "./types.ts";
|
|
9
10
|
|
|
10
11
|
const logger = getLogger("subagents");
|
|
11
12
|
|
|
@@ -23,6 +24,23 @@ export interface ManifestRecord {
|
|
|
23
24
|
* 历史 "error"/"completed"/"failed" 值由读侧 mapManifestStatus 向后兼容映射。
|
|
24
25
|
*/
|
|
25
26
|
status: "running" | "closed" | "cancelled";
|
|
27
|
+
/**
|
|
28
|
+
* [U4c / D5 词汇双写过渡] 内部权威状态词汇(ExecutionStatus 二态)。
|
|
29
|
+
* 与上方旧 status 三态投影**永久双写**——无版本磁盘 schema 不做破坏性变更;
|
|
30
|
+
* session-reader(独立 npm 包独立进程)直读旧 status 做 identity 富字段投影
|
|
31
|
+
* 与孤儿判定,删字段 = 外部消费方富字段降级。旧 status 只降权威地位不删字段。
|
|
32
|
+
* 宿主内消费方不读本字段(终态判定走 `.state` 权威,D1);本字段是词汇收口
|
|
33
|
+
* (全景① ExecutionStatus+ClosedReason)在 manifest 写面的过渡锚。
|
|
34
|
+
*/
|
|
35
|
+
executionStatus?: ExecutionStatus;
|
|
36
|
+
/**
|
|
37
|
+
* [M2 Gate B] closed 终态的 L2 关闭原因(status="closed" 时有意义)。旧 manifest 无
|
|
38
|
+
* 此字段(undefined = 死因不可考,读侧守卫归一 undefined)。缺失时 manifest 源重建
|
|
39
|
+
* 的快照丢 closedReason,endedMessageGuard 把 user-close/cancelled 误分流进
|
|
40
|
+
* 「reconnectable/fork-from」分支——本字段是 manifest 源快照三分流的唯一依据
|
|
41
|
+
* (磁盘 sidecar 源由 .state reason 承载,不经本字段)。
|
|
42
|
+
*/
|
|
43
|
+
closedReason?: ClosedReason;
|
|
26
44
|
createdAt: number;
|
|
27
45
|
completedAt?: number;
|
|
28
46
|
sessionFile?: string;
|
|
@@ -99,7 +117,9 @@ export class ManifestStore {
|
|
|
99
117
|
* 不阻塞 event loop)。
|
|
100
118
|
*
|
|
101
119
|
* 失败时原语尽力清理残留 tmp(debug 记录,不掩盖原错误)并原样上抛——
|
|
102
|
-
* 调用方(
|
|
120
|
+
* 调用方(RecordStore 写面:writeManifestPersisted 缺省异步分支 / 批写 barrier /
|
|
121
|
+
* rebuildIndexes 重建降级(debug 留痕)/ rematerializeManifest(warn 语义))
|
|
122
|
+
* 决定降级策略——各面降级策略分化见各调用点。
|
|
103
123
|
*/
|
|
104
124
|
async writeManifest(record: ManifestRecord): Promise<void> {
|
|
105
125
|
const filePath = path.join(this.dir, `${record.id}.json`);
|
|
@@ -177,20 +197,23 @@ export class ManifestStore {
|
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* 1. manifest 已存在 → 删 tmp(陈旧)
|
|
183
|
-
* 2. tmp 合法 + manifest 缺失 → rename tmp 为 manifest
|
|
184
|
-
* 3. tmp 非法 + manifest 缺失 → 删 tmp
|
|
200
|
+
* 启动时清扫 tmp 残留([U4c / D6] tmp 恢复退役后的语义——[H4/U5 收口] 更名
|
|
201
|
+
* recoverTmpFiles → sweepTmpFiles,名实对齐「静默删除」)。
|
|
185
202
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
203
|
+
* 旧语义(ADR-035 三分支:manifest 已存在删 tmp / tmp 合法且 manifest 缺失
|
|
204
|
+
* promote / tmp 非法删)已随缓存降级退役——manifest 现为可丢可重建缓存
|
|
205
|
+
* (权威 = `.state`,重建 = RecordStore.rebuildIndexes,D5),promote 半写 tmp
|
|
206
|
+
* 只会把陈旧快照复活成「看似权威」的索引,语义失效;统一**静默删除**全部
|
|
207
|
+
* tmp(含 0 字节/半写形态——D8 停机窗残留由本清扫顺带清理)。
|
|
208
|
+
*
|
|
209
|
+
* [T5④ / PS-13] per-file 容错保留:单个 tmp 删除失败(ENOENT——并发回收/外部
|
|
210
|
+
* 清理抢先、EACCES 等)只 warn + 跳过该文件,不再中断整轮。promote 退役后
|
|
211
|
+
* 无恢复形态,返回值简化为删除计数。
|
|
212
|
+
*
|
|
213
|
+
* @returns 删除的 tmp 文件数。
|
|
190
214
|
*/
|
|
191
|
-
async
|
|
215
|
+
async sweepTmpFiles(): Promise<number> {
|
|
192
216
|
let deleted = 0;
|
|
193
|
-
let recovered = 0;
|
|
194
217
|
let failed = 0;
|
|
195
218
|
|
|
196
219
|
const files = fs.readdirSync(this.dir);
|
|
@@ -198,39 +221,14 @@ export class ManifestStore {
|
|
|
198
221
|
|
|
199
222
|
for (const tmpFile of tmpFiles) {
|
|
200
223
|
const tmpPath = path.join(this.dir, tmpFile);
|
|
201
|
-
const manifestId = tmpFile.split(".json.tmp.")[0];
|
|
202
|
-
const manifestPath = path.join(this.dir, `${manifestId}.json`);
|
|
203
|
-
|
|
204
224
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
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 失败,删
|
|
225
|
-
fs.unlinkSync(tmpPath);
|
|
226
|
-
deleted++;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
225
|
+
fs.unlinkSync(tmpPath);
|
|
226
|
+
deleted++;
|
|
229
227
|
} catch (fileErr) {
|
|
230
228
|
// [T5④/PS-13] 单文件失败不中断整轮:warn 留痕(含文件名与原因)后继续处理
|
|
231
229
|
// 剩余 tmp。常见于 tmp 已被并发回收/外部清理删除(ENOENT)——自愈场景不再放大。
|
|
232
230
|
failed++;
|
|
233
|
-
logger.warn(`[subagents]
|
|
231
|
+
logger.warn(`[subagents] sweepTmpFiles: failed to remove ${tmpFile}, skipping (leftovers retry on next startup)`, {
|
|
234
232
|
detail: fileErr instanceof Error ? fileErr.message : String(fileErr),
|
|
235
233
|
});
|
|
236
234
|
}
|
|
@@ -238,10 +236,10 @@ export class ManifestStore {
|
|
|
238
236
|
|
|
239
237
|
if (failed > 0) {
|
|
240
238
|
logger.warn(
|
|
241
|
-
`[subagents]
|
|
239
|
+
`[subagents] sweepTmpFiles: ${failed} of ${tmpFiles.length} tmp file(s) could not be removed`,
|
|
242
240
|
);
|
|
243
241
|
}
|
|
244
242
|
|
|
245
|
-
return
|
|
243
|
+
return deleted;
|
|
246
244
|
}
|
|
247
245
|
}
|