@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
|
@@ -1,13 +1,51 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/execution/record-store.ts
|
|
2
2
|
//
|
|
3
3
|
// Record 的统一容器。内存只留 running record;终态从 session.jsonl 重建。
|
|
4
4
|
//
|
|
5
5
|
// 职责:
|
|
6
6
|
// - 持有 running record(终态 record 在 archive 时立即从内存移除)
|
|
7
7
|
// - onChange 订阅(TUI widget/list 据此重渲)
|
|
8
|
-
// - collectRecords:内存(running)
|
|
8
|
+
// - collectRecords:内存(running) ∪ 磁盘(sessions/*.jsonl 重建) ∪ manifest(sessions-index.json 补充) 三源合并
|
|
9
9
|
// - 提供 snapshot() 只读视图给 TUI(永不返回可变引用)
|
|
10
10
|
//
|
|
11
|
+
// ════════════════════════════════════════════════════════════════════
|
|
12
|
+
// [U1 / record 持久化收敛 §3.1] 意图级操作 API 立面(store 唯一写入口)
|
|
13
|
+
// ════════════════════════════════════════════════════════════════════
|
|
14
|
+
// 调用方说「发生了什么」,不说「写哪个文件」——文件布局知识收在本类内部。
|
|
15
|
+
// 迁移已完成:全部写点经本 API,旧直写路径已删除(守卫见 D7)。
|
|
16
|
+
//
|
|
17
|
+
// | 意图原语 | 语义 | 内部写面 |
|
|
18
|
+
// |---------|------|---------|
|
|
19
|
+
// | register(record) | 创建入册(既有方法,意图语义补齐) | entry(best-effort)+ 缓存一致性(stat 戳自校验承接) |
|
|
20
|
+
// | appendEvent(id, event) | 事件追加(过程;turns 归约) | entry 变迁(best-effort) |
|
|
21
|
+
// | markRoundStarted(id) | 轮始重置(status=running + result/resumable 清除) | entry(best-effort) |
|
|
22
|
+
// | markRoundIdle(id, outcome) | 轮末收口(保持 running-resumable,非置 idle;簿记全集①-⑨见方法注释;簿记⑦ `.alive` 保留——写权声明跨轮延续,D3a) | entry + 注销发射点② |
|
|
23
|
+
// | markFinalized(record, reason) | 正常终态(含 disposeAllRecords 编排性关闭,D8 矩阵;副作用编排 abort/kill/disarm/CAS/promote 留调用方) | `.state` writeSync 先 → binding(updateRecordBinding)→ entry/archive → manifest writeSync → `.alive` 删(D8 v7 写序) |
|
|
24
|
+
// | markCancelled(record) | 取消终态(tombstone endedAt) | 同 markFinalized 写序(writeCancelledState) |
|
|
25
|
+
// | markBatchFinalized(records) | sync 批终态(barrier:manifest 落盘完成先于批通知写账——「通知可达 ⇒ 索引就位」构造性保证) | barrier + 批 entry + manifest |
|
|
26
|
+
// | adoptEngineDeath(id, {error}) | 引擎死亡收养(error/result/resumable 三写;监督器接管编排留调用方) | entry(best-effort) |
|
|
27
|
+
// | markResurrected(record, wasClosed) | 磁盘终态位翻回活态(acquire-first 三件套 + 内存翻回 + register,单 try 域原子收敛,任一步失败响亮抛错,D3c) | `.alive` 写(writeSync)先 → `.state`/`.finalized`/`.cancelled` 删 + 内存翻回 + register |
|
|
28
|
+
// | markIdleArchived(record) | idle-GC 归档(30 天 TTL 内存回收,非终态化——磁盘仍 running 可接管) | store.archive 先 → manifest(running 投影)→ `.alive` release 后(archive 抛错则整体失败 marker 必未删) |
|
|
29
|
+
// | acquireWriteLease(sessionFile, id) | store 内部 acquire 动作(writeAliveMarker 唯一包装;spawn 侧 sessionFile 回填挂钩用,D3a 时机①,U2b 消费) | `.alive` 写(失败响亮抛错) |
|
|
30
|
+
//
|
|
31
|
+
// ── 字段级写点全集 → 操作映射(设计 §3.1 v4 十字段逐一归口)──
|
|
32
|
+
// ① status —— 轮始重置→markRoundStarted;轮终保持 running→markRoundIdle;
|
|
33
|
+
// 终态→markFinalized/markCancelled(内存冻结由调用方 completeRecord/
|
|
34
|
+
// tryTransition 先行,store 收口持久化面)
|
|
35
|
+
// ② result —— 轮始清→markRoundStarted;轮终写→markRoundIdle(outcome);终态→
|
|
36
|
+
// markFinalized 序言(completeRecord 冻结后随 entry/manifest 投影)
|
|
37
|
+
// ③ round —— 轮终 +1→markRoundIdle
|
|
38
|
+
// ④ closedReason—— 终态→markFinalized/markCancelled;轮终清除→markRoundIdle([S10])
|
|
39
|
+
// ⑤ resumable —— 轮始清/轮终置 true→markRoundStarted/markRoundIdle;收养→
|
|
40
|
+
// adoptEngineDeath
|
|
41
|
+
// ⑥ idleSince —— 轮终刷新→markRoundIdle
|
|
42
|
+
// ⑦ sessionFile —— 回填族(run 应答/promote)归调用方内存回填 + register/
|
|
43
|
+
// markFinalized 序言随投影持久化;acquireWriteLease 在锚点确立时
|
|
44
|
+
// 声明写权(D3a 时机①)
|
|
45
|
+
// ⑧ turns —— 事件累积族→appendEvent(execution-record.updateFromEvent 归约)
|
|
46
|
+
// ⑨ lastError —— 轮终失败→markRoundIdle failed 载荷
|
|
47
|
+
// ⑩ error —— 收养/失败→adoptEngineDeath/markRoundIdle
|
|
48
|
+
//
|
|
11
49
|
// [perf] 两级读写设计(修复 /subagents 打开慢):
|
|
12
50
|
// 1. 列表扫描 = light:只读文件头部 identity(readIdentityHeader,64KB)+ sidecar
|
|
13
51
|
// 状态矩阵,不解析 message entries。列表/补全/hasRunning 只需身份与状态,
|
|
@@ -21,14 +59,23 @@
|
|
|
21
59
|
// 磁盘种子——冷启动首扫读一次(惰性装载),dirty 扫描后按 60s 节流落盘
|
|
22
60
|
// (fileCache 投影,fire-and-forget);运行期 L0/L1 语义不变,损坏/低版本
|
|
23
61
|
// 静默回退全扫,高版本忽略不重写。见 sessions-index.ts。
|
|
62
|
+
// 5. [U4c / D5 缓存降级] manifest 与 sessions-index 均为可丢缓存(权威 = `.state`):
|
|
63
|
+
// 重建双通道 = boot revive 完成后全量(rebuildIndexes,宿主 boot 钩子接线)+
|
|
64
|
+
// 查询面惰性(mergedRecords 对 manifest 缺员的磁盘重建 record 单点补建,每 id
|
|
65
|
+
// 每进程一次);重建失败静默降级(子 session 已 GC/损坏 → 该条跳过,整轮不抛)。
|
|
24
66
|
|
|
25
67
|
import * as fs from "node:fs";
|
|
26
68
|
import * as path from "node:path";
|
|
27
69
|
|
|
28
70
|
import { getLogger } from "../core/logger.ts";
|
|
29
71
|
|
|
30
|
-
import { getCurrentActivity, getDisplayItems, getEventLog, markReconstructedStatus, snapshot as toSnapshot } from "./execution-record.ts";
|
|
31
|
-
import {
|
|
72
|
+
import { getCurrentActivity, getDisplayItems, getEventLog, markReconstructedStatus, resurrectClosed, snapshot as toSnapshot, updateFromEvent } from "./execution-record.ts";
|
|
73
|
+
import { readStateMarker, statStateStamp, writeFinalizedState, writeCancelledState, updateRecordBinding, STATE_SIDECAR_EXT } from "./state-marker.ts";
|
|
74
|
+
import type { StateMarker } from "./state-marker.ts";
|
|
75
|
+
// [UF-1] record 绑定 sidecar:宿主侧 id→file 映射(engine-CLI 化后子文件无 identity
|
|
76
|
+
// entry 时代的身份载体)——scanFile 探测分支在 identity miss 时消费它重建 light record。
|
|
77
|
+
import { readRecordBinding, RECORD_BINDING_SIDECAR_EXT } from "./state-marker.ts";
|
|
78
|
+
import type { RecordBinding } from "./state-marker.ts";
|
|
32
79
|
import { toSubagentRecordEntry, SUBAGENT_RECORD_CUSTOM_TYPE } from "./record-entry.ts";
|
|
33
80
|
import type { ManifestRecord, ManifestStore } from "./manifest-store.ts";
|
|
34
81
|
import { INDEX_WRITE_MIN_INTERVAL_MS, loadIndex, saveIndex } from "./sessions-index.ts";
|
|
@@ -43,17 +90,19 @@ import {
|
|
|
43
90
|
reconstructFromFile,
|
|
44
91
|
} from "./session-reconstructor.ts";
|
|
45
92
|
import type {
|
|
46
|
-
|
|
93
|
+
AgentEvent,
|
|
47
94
|
ClosedReason,
|
|
48
95
|
ExecutionRecord,
|
|
49
96
|
ExecutionStatus,
|
|
50
97
|
RecordSnapshot,
|
|
51
98
|
SubagentRecord,
|
|
52
99
|
} from "./types.ts";
|
|
53
|
-
import type { CancelledTombstone } from "./tombstone-store.ts";
|
|
54
100
|
import { CLOSED_REASONS as CLOSED_REASON_LIST } from "./types.ts";
|
|
55
|
-
|
|
56
|
-
|
|
101
|
+
// [U4a / D3b (a″)] findForeignLiveInstance:孤儿恢复的活实例跳过判据——现查探针
|
|
102
|
+
// 替代重建时 externalInstance 缓存(pid 单判据 + self-pid 排除,比缓存更新鲜)。
|
|
103
|
+
import { writeAliveMarker, removeAliveMarker, findForeignLiveInstance } from "./alive-store.ts";
|
|
104
|
+
import type { RoundSettlementOutcome } from "./finalize-record.ts";
|
|
105
|
+
import { writeAtomicFileSync } from "../shared/atomic-write.ts";
|
|
57
106
|
|
|
58
107
|
const logger = getLogger("subagents");
|
|
59
108
|
|
|
@@ -69,7 +118,9 @@ const STATUS_PRIORITY: Record<ExecutionStatus, number> = {
|
|
|
69
118
|
closed: 3,
|
|
70
119
|
};
|
|
71
120
|
|
|
72
|
-
|
|
121
|
+
/** [D8 v7] manifest 同步写的 JSON 缩进空格数——与 ManifestStore.writeManifest 字节
|
|
122
|
+
* 形态一致(读写两侧格式互认,外部 session-reader 直读不感知差异)。 */
|
|
123
|
+
const MANIFEST_INDENT_SPACES = 2;
|
|
73
124
|
|
|
74
125
|
/**
|
|
75
126
|
* manifest status → ExecutionStatus 运行时守卫映射。
|
|
@@ -103,19 +154,16 @@ interface Stamp {
|
|
|
103
154
|
size: number;
|
|
104
155
|
}
|
|
105
156
|
|
|
106
|
-
/** sidecar 状态矩阵输入(buildLightRecord / getFullRecord 共享)。
|
|
157
|
+
/** sidecar 状态矩阵输入(buildLightRecord / getFullRecord 共享)。
|
|
158
|
+
* [U4a / D3b (a)] alive 维度已随 externalInstance 投影移除——非终态统一兜底 running,
|
|
159
|
+
* 探活读面由 (a′)(a″) 的 findForeignLiveInstance 现查探针承担(不在重建缓存)。 */
|
|
107
160
|
interface SidecarMatrix {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
/** [v8.5 A2] `.finalized` sidecar 内容携带的关闭原因(readFinalizedReason 读出)。
|
|
111
|
-
* 旧格式空文件 → 空串(死因不可考);sidecar 不存在 → undefined。 */
|
|
112
|
-
finalizedReason: string | undefined;
|
|
113
|
-
alive: AliveMarker | undefined;
|
|
161
|
+
/** 终态 sidecar(.state 优先,兼容旧 .finalized/.cancelled 归一)。undefined = 未终态化。 */
|
|
162
|
+
state: StateMarker | undefined;
|
|
114
163
|
/** jsonl mtime(light 分支 2 的 endedAt 近似——finalize 后文件不再变化)。 */
|
|
115
164
|
jsonlMtimeMs: number;
|
|
116
165
|
/** 全量重建可得的精确结束时间(最后 entry ts);light 传 undefined 回落 mtime。 */
|
|
117
166
|
fullEndedAt?: number;
|
|
118
|
-
now: number;
|
|
119
167
|
}
|
|
120
168
|
|
|
121
169
|
/**
|
|
@@ -126,7 +174,7 @@ interface SidecarMatrix {
|
|
|
126
174
|
* full === light 是哨兵(「已尝试但无详情可补」,如无 assistant message
|
|
127
175
|
* 的文件),避免重复全文重读;stat 戳变化时随 light 一起重置重试。
|
|
128
176
|
*
|
|
129
|
-
* 校验:jsonl +
|
|
177
|
+
* 校验:jsonl + 终态 sidecar + record 绑定([UF-1])的 stat 戳对比(终态戳为该组文件 .state/.finalized/.cancelled 的合并戳,见 state-marker.statStateStamp;[U4a / D3b (a)] alive 戳已随 externalInstance 投影移除——light 态不依赖 .alive,探活读面走现查探针)。任何写操作至少改变一个戳 →
|
|
130
178
|
* 只重建该文件,其余 N-1 个复用缓存(statSync 毫秒级,取代旧的整体失效重扫)。
|
|
131
179
|
*/
|
|
132
180
|
interface FileCacheEntry {
|
|
@@ -135,15 +183,11 @@ interface FileCacheEntry {
|
|
|
135
183
|
light: SubagentRecord;
|
|
136
184
|
full: SubagentRecord | undefined;
|
|
137
185
|
jsonl: Stamp;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
aliveData: AliveMarker | undefined;
|
|
144
|
-
/** [v8.5 A2] 最近一次重建时读到的 `.finalized` 内容 reason(同上,校验命中复用)。
|
|
145
|
-
* sidecar 不存在时恒 undefined(与「存在但空串」区分,后者是旧格式空文件)。 */
|
|
146
|
-
finalReason: string | undefined;
|
|
186
|
+
state: Stamp | null;
|
|
187
|
+
/** [UF-1] record 绑定 sidecar 戳(null = 无绑定文件)。 */
|
|
188
|
+
binding: Stamp | null;
|
|
189
|
+
/** 最近一次重建时读到的终态 sidecar 内容(校验命中路径复用,不重读文件)。 */
|
|
190
|
+
stateMarker: StateMarker | undefined;
|
|
147
191
|
}
|
|
148
192
|
|
|
149
193
|
/** 负缓存条目:确认无 identity 的文件(损坏/异构)。缓存「没有」这一事实,
|
|
@@ -151,27 +195,28 @@ interface FileCacheEntry {
|
|
|
151
195
|
interface NegativeFileEntry {
|
|
152
196
|
negative: true;
|
|
153
197
|
jsonl: Stamp;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
198
|
+
state: Stamp | null;
|
|
199
|
+
/** [UF-1] 绑定戳纳入负缓存:绑定文件后到(run 应答回填点落盘)改变戳,
|
|
200
|
+
* 打破负缓存触发重探测——「先扫描后绑定落盘」时序的恢复能力锚点。 */
|
|
201
|
+
binding: Stamp | null;
|
|
157
202
|
}
|
|
158
203
|
|
|
159
204
|
/** fileCache 值类型:正常条目或负缓存条目。 */
|
|
160
205
|
type FileCacheValue = FileCacheEntry | NegativeFileEntry;
|
|
161
206
|
|
|
162
|
-
/** scanFile 单文件本轮 stat 戳集合(jsonl +
|
|
207
|
+
/** scanFile 单文件本轮 stat 戳集合(jsonl + 终态 sidecar(含旧名合并戳)+ record 绑定;
|
|
208
|
+
* [U4a / D3b (a)] alive 戳退役——light 态不依赖 .alive,省去每文件一次 statSync)。 */
|
|
163
209
|
interface FileStamps {
|
|
164
210
|
jsonl: Stamp;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
alive: Stamp | null;
|
|
211
|
+
state: Stamp | null;
|
|
212
|
+
binding: Stamp | null;
|
|
168
213
|
}
|
|
169
214
|
|
|
170
215
|
/** sidecar payload 读取结果(索引命中与探测重建两分支共享的读点)。 */
|
|
171
216
|
interface SidecarPayloads {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
217
|
+
state: StateMarker | undefined;
|
|
218
|
+
/** [UF-1] record 绑定载荷(identity miss 时的身份重建源)。 */
|
|
219
|
+
binding: RecordBinding | undefined;
|
|
175
220
|
}
|
|
176
221
|
|
|
177
222
|
/** 孤儿判定的末行读取初始窗口(常规 entry 远小于此,避免全文件读)。 */
|
|
@@ -224,7 +269,7 @@ function readLastJsonlLine(sessionFile: string): { ok: true; line: string } | {
|
|
|
224
269
|
try {
|
|
225
270
|
fs.closeSync(fd);
|
|
226
271
|
} catch (_e) {
|
|
227
|
-
void _e; // 关闭失败不影响已读结果(对齐
|
|
272
|
+
void _e; // 关闭失败不影响已读结果(对齐 state-marker best-effort 模式)
|
|
228
273
|
}
|
|
229
274
|
}
|
|
230
275
|
}
|
|
@@ -269,6 +314,19 @@ function entryNum(d: Record<string, unknown>, k: string): number | undefined {
|
|
|
269
314
|
return typeof d[k] === "number" ? (d[k] as number) : undefined;
|
|
270
315
|
}
|
|
271
316
|
|
|
317
|
+
/**
|
|
318
|
+
* 来源域投影(H2 W1,设计 subagent-workflow-record-unification §3.3 D1):origin 经
|
|
319
|
+
* 字面量守卫(非法值/缺省 → undefined = "tool" 语义,存量 entry 零迁移);parentRunId
|
|
320
|
+
* 经安全 string 读取。缺省语义对齐 ExecutionRecord.origin 注释——消费面按
|
|
321
|
+
* `=== "workflow"` 负向判定,缺省(undefined)恒视为手动 tool 派发。
|
|
322
|
+
*/
|
|
323
|
+
function readEntryOriginFields(d: Record<string, unknown>): Pick<SubagentRecord, "origin" | "parentRunId"> {
|
|
324
|
+
return {
|
|
325
|
+
origin: d.origin === "workflow" || d.origin === "tool" ? d.origin : undefined,
|
|
326
|
+
parentRunId: entryStr(d, "parentRunId"),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
272
330
|
/** 终态域投影:status 只认 "closed" 字面量(其余含缺省 → "running",旧调用方行为不变);
|
|
273
331
|
* closedReason 经枚举守卫(非法/缺省 → undefined)。 */
|
|
274
332
|
function readEntryTerminalFields(
|
|
@@ -341,6 +399,9 @@ function rebuildEntryRecord(id: string, d: Record<string, unknown>): SubagentRec
|
|
|
341
399
|
rootSessionId: entryStr(d, "rootSessionId"),
|
|
342
400
|
parentRecordId: entryStr(d, "parentRecordId"),
|
|
343
401
|
depth: entryNum(d, "depth") ?? 0,
|
|
402
|
+
// [H2 W1] 来源域透传(origin/parentRunId):漏本行则主 session entry 重建路径
|
|
403
|
+
// 丢 origin,重启后 workflow record 逃过投影过滤(D1 ①-④ 全失效)。
|
|
404
|
+
...readEntryOriginFields(d),
|
|
344
405
|
endedAt: entryNum(d, "endedAt"),
|
|
345
406
|
turns: entryNum(d, "turns") ?? 0,
|
|
346
407
|
totalTokens: entryNum(d, "totalTokens") ?? 0,
|
|
@@ -413,13 +474,12 @@ function sameNullableStamp(a: Stamp | null, b: Stamp | null): boolean {
|
|
|
413
474
|
return sameStamp(a, b);
|
|
414
475
|
}
|
|
415
476
|
|
|
416
|
-
/** 缓存条目与本轮 stat 戳全同(jsonl +
|
|
477
|
+
/** 缓存条目与本轮 stat 戳全同(jsonl + 终态 sidecar + record 绑定,null 语义对齐)→ 零读取复用。 */
|
|
417
478
|
function isFreshCache(cached: FileCacheValue, stamps: FileStamps): boolean {
|
|
418
479
|
return (
|
|
419
480
|
sameStamp(cached.jsonl, stamps.jsonl) &&
|
|
420
|
-
sameNullableStamp(cached.
|
|
421
|
-
sameNullableStamp(cached.
|
|
422
|
-
sameNullableStamp(cached.alive, stamps.alive)
|
|
481
|
+
sameNullableStamp(cached.state, stamps.state) &&
|
|
482
|
+
sameNullableStamp(cached.binding, stamps.binding)
|
|
423
483
|
);
|
|
424
484
|
}
|
|
425
485
|
|
|
@@ -436,15 +496,14 @@ function detectIdentity(file: string, size: number): IdentityHeaderRecon | undef
|
|
|
436
496
|
}
|
|
437
497
|
|
|
438
498
|
/**
|
|
439
|
-
* sidecar payload
|
|
440
|
-
*
|
|
441
|
-
*
|
|
499
|
+
* sidecar payload 读取(读顺序:终态 marker → record 绑定)。
|
|
500
|
+
* 两者都是活态数据,调用方沿用每轮重读语义;终态 marker 静态数据仅在戳非空时读
|
|
501
|
+
* (文件小,成本可忽略)。
|
|
442
502
|
*/
|
|
443
503
|
function readSidecarPayloads(file: string, stamps: FileStamps): SidecarPayloads {
|
|
444
504
|
return {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
finalReason: stamps.finalized !== null ? readFinalizedReason(file) : undefined,
|
|
505
|
+
state: stamps.state !== null ? readStateMarker(file) : undefined,
|
|
506
|
+
binding: stamps.binding !== null ? readRecordBinding(file) : undefined,
|
|
448
507
|
};
|
|
449
508
|
}
|
|
450
509
|
|
|
@@ -504,6 +563,27 @@ export class RecordStore {
|
|
|
504
563
|
* 落盘(防 v1/v2 last-writer-wins 覆盖振荡),直至下次 loadIndex 重新评估。 */
|
|
505
564
|
private indexHigherVersion = false;
|
|
506
565
|
|
|
566
|
+
/** [U4c / G1] manifest 惰性重建的每 id 尝试守卫(mergedRecords 高频路径防磁盘写
|
|
567
|
+
* 放大:每 id 每进程至多一次;dispose/revive 随其余缓存态一并重置)。 */
|
|
568
|
+
private manifestRebuildTried = new Set<string>();
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* [D8 v7] manifest 同步写目录(与 manifestStore 同一 records 目录,由构造方提供——
|
|
572
|
+
* manifestStore.dir 私有,本类不经反射访问)。提供时终态原语(markFinalized /
|
|
573
|
+
* markCancelled / markBatchFinalized)走 writeAtomicFileSync 同步落盘——停机窗
|
|
574
|
+
* fire-and-forget 竞态构造性消灭(disposeAllRecords 同步链全链同步段内完成);
|
|
575
|
+
* 已接线(subagent-service.ts 构造点 recordsDir);缺省分支仅纯内存测试形态。
|
|
576
|
+
*/
|
|
577
|
+
private readonly manifestDir: string | undefined;
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* [§3.1 markRoundIdle 簿记⑧] pending-notifications 轮终注销(发射点②)的注入面。
|
|
581
|
+
* store 不直接依赖 pending 注册表(「零副作用编排」边界——文件布局收口 ≠ 通知
|
|
582
|
+
* 注册表依赖);由 SubagentService 构造点注入 emitPendingUnregister 闭包(U5 收口
|
|
583
|
+
* 接线),缺省 no-op(纯内存测试形态不发射)。
|
|
584
|
+
*/
|
|
585
|
+
private pendingUnregister: ((id: string, status: string) => void) | undefined;
|
|
586
|
+
|
|
507
587
|
constructor(
|
|
508
588
|
private readonly sessionsDir: string,
|
|
509
589
|
private readonly manifestStore?: ManifestStore,
|
|
@@ -511,8 +591,11 @@ export class RecordStore {
|
|
|
511
591
|
* SubagentService 构造时 this.pi 尚未注入(session_start 之前),传 undefined 兜底;
|
|
512
592
|
* 后续通过 setPi() 注入(见下)。允许 null = 兼容 PiLike 字段类型。 */
|
|
513
593
|
pi?: RecordStorePi,
|
|
594
|
+
/** [D8 v7] manifest 同步写目录(见 manifestDir 字段注释)。 */
|
|
595
|
+
manifestDir?: string,
|
|
514
596
|
) {
|
|
515
597
|
this.pi = pi ?? null;
|
|
598
|
+
this.manifestDir = manifestDir;
|
|
516
599
|
}
|
|
517
600
|
|
|
518
601
|
/** session_start 后由 SubagentService.initSession 调,注入真实 Pi handle。
|
|
@@ -535,7 +618,7 @@ export class RecordStore {
|
|
|
535
618
|
/**
|
|
536
619
|
* 归档:record 已被 completeRecord 设置了终态 status。
|
|
537
620
|
* 立即从内存移除(终态 record 下次读时从 session.jsonl 重建)。
|
|
538
|
-
* cancelled record
|
|
621
|
+
* cancelled record 由调用方先写终态 sidecar(cancel 路径),此处只负责移除。
|
|
539
622
|
*
|
|
540
623
|
* W16 [D4]:终态冻结字段(result/endedAt/closedReason)在 completeRecord 已就绪,
|
|
541
624
|
* 此处 append 的快照即完整终态记录(所有终态路径的必经锚点)。
|
|
@@ -558,6 +641,550 @@ export class RecordStore {
|
|
|
558
641
|
this.pi?.appendEntry?.("subagent-record", toSubagentRecordEntry(RecordStore.recordToSubagent(record)));
|
|
559
642
|
}
|
|
560
643
|
|
|
644
|
+
// ════════════════════════════════════════════════════════════
|
|
645
|
+
// [U1 / §3.1] 意图级操作 API 立面(store 唯一写入口;映射表见模块头)
|
|
646
|
+
// ════════════════════════════════════════════════════════════
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* 注入 pending-notifications 轮终注销闭包(markRoundIdle 簿记⑧发射点②,见字段
|
|
650
|
+
* pendingUnregister 注释)。装配点 = SubagentService 构造器(U5 收口接线)。
|
|
651
|
+
*/
|
|
652
|
+
setPendingUnregister(fn: ((id: string, status: string) => void) | undefined): void {
|
|
653
|
+
this.pendingUnregister = fn;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 意图原语:事件追加(过程记录)。turns/eventLog/totalTokens 经 execution-record
|
|
658
|
+
* 事件归约累积(字段⑧),随后 entry 变迁上报(best-effort——过程面可丢,重建由
|
|
659
|
+
* 子 session 文件承接)。调用方按事件粒度决定调用频率(高频 delta 逐事件上报会
|
|
660
|
+
* 放大 entry 写面,编排粒度属调用方职责)。
|
|
661
|
+
*
|
|
662
|
+
* @returns false = id 不在内存(未注册/已归档)——事件丢弃并 debug 留痕。
|
|
663
|
+
*/
|
|
664
|
+
appendEvent(id: string, event: AgentEvent): boolean {
|
|
665
|
+
const rec = this.records.get(id);
|
|
666
|
+
if (rec === undefined) {
|
|
667
|
+
logger.debug("[subagents] appendEvent: record not in memory (not registered / archived)", {
|
|
668
|
+
detail: { id, eventType: event.type },
|
|
669
|
+
});
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
updateFromEvent(rec, event);
|
|
673
|
+
this.reportRecordTransition(rec);
|
|
674
|
+
this.notifyChange();
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* 意图原语:轮始重置(字段①②⑤)。status=running + result/resumable 清除——
|
|
680
|
+
* §5.4 isStreaming 公式要求 result undefined 才显示 streaming,不清则续轮流仍显示
|
|
681
|
+
* waiting。归口写点:热路径轮始与冷启动 resume 续轮(subagent-service,U3 迁移)。
|
|
682
|
+
*
|
|
683
|
+
* @returns false = id 不在内存(debug 留痕,无副作用)。
|
|
684
|
+
*/
|
|
685
|
+
markRoundStarted(id: string): boolean {
|
|
686
|
+
const rec = this.records.get(id);
|
|
687
|
+
if (rec === undefined) {
|
|
688
|
+
logger.debug("[subagents] markRoundStarted: record not in memory", { detail: { id } });
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
rec.status = "running";
|
|
692
|
+
rec.result = undefined;
|
|
693
|
+
rec.resumable = undefined;
|
|
694
|
+
this.reportRecordTransition(rec);
|
|
695
|
+
this.notifyChange();
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* 意图原语:轮末收口——**保持 running-resumable**(名称沿用,非置 idle:status 写
|
|
701
|
+
* idle 会断 SP-5 升级链与 hasRunning 判据)。簿记全集(①-⑨,= doFinalizeRoundToIdle
|
|
702
|
+
* 现状簿记 + D3a 修订):
|
|
703
|
+
* ① status 保持 running;② result 按 outcome 写入(成功=content / 失败=前值??
|
|
704
|
+
* 失败摘要 + lastError);③ round+1;④ closedReason 清除([S10]);⑤ resumable=true
|
|
705
|
+
* (GUI waiting 判据);⑥ idleSince 刷新(idle-GC 判据);⑦ **`.alive` 保留**
|
|
706
|
+
* (D3a 跨轮延续——写权声明至 release 两出口[终态原语/idle-GC 归档],轮终
|
|
707
|
+
* record 仍 resumable 随时续聊 spawn 写同一 sessionFile,删则轮后跨进程防御
|
|
708
|
+
* 空窗);⑧ pending 注销发射点②(进程已死,从活跃后代差集移除——经
|
|
709
|
+
* setPendingUnregister 注入,未注入时跳过);⑨ reportRecordTransition(entry
|
|
710
|
+
* 携带新 round 与本轮 result)。
|
|
711
|
+
* worktree/通知等副作用编排留调用方。
|
|
712
|
+
*
|
|
713
|
+
* @param outcome 轮终结果(kind 判别:success=content / failed=reason)
|
|
714
|
+
* @returns false = id 不在内存(debug 留痕,无副作用)。
|
|
715
|
+
* @throws Error record 终态簿记已冻结(endedAt 已设——复活终态的调用即 bug,
|
|
716
|
+
* fail-fast,对齐 doFinalizeRoundToIdle A3 断言)。
|
|
717
|
+
*/
|
|
718
|
+
markRoundIdle(id: string, outcome: RoundSettlementOutcome): boolean {
|
|
719
|
+
const rec = this.records.get(id);
|
|
720
|
+
if (rec === undefined) {
|
|
721
|
+
logger.debug("[subagents] markRoundIdle: record not in memory", { detail: { id } });
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
if (rec.endedAt !== undefined) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`markRoundIdle(${id}): terminal bookkeeping already frozen ` +
|
|
727
|
+
`(status: ${rec.status}${rec.closedReason !== undefined ? `/${rec.closedReason}` : ""}, endedAt: ${rec.endedAt}) — ` +
|
|
728
|
+
`round-idle finalization would resurrect a finalized record. ` +
|
|
729
|
+
`Recovery: caller must gate on record.status === "running" before settling a round.`,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
// ② result 写入规则(D7):成功轮 = content(chat 空 content 兜底占位);失败轮 =
|
|
733
|
+
// 前值保真 ?? 失败摘要 + lastError 写失败原因(字段⑨)。
|
|
734
|
+
let nextResult: string | undefined;
|
|
735
|
+
if (outcome.kind === "failed") {
|
|
736
|
+
rec.lastError = outcome.reason;
|
|
737
|
+
nextResult = rec.result ?? `round did not complete: ${outcome.reason}`;
|
|
738
|
+
} else if (rec.chatMode) {
|
|
739
|
+
nextResult = outcome.content || "(no output this round)";
|
|
740
|
+
} else {
|
|
741
|
+
nextResult = outcome.content || rec.result || "(empty)";
|
|
742
|
+
}
|
|
743
|
+
rec.result = nextResult;
|
|
744
|
+
// ①③④⑤⑥:保持 running + 轮次推进 + 清残留死因 + 执行态信号 + idle 锚。
|
|
745
|
+
rec.status = "running";
|
|
746
|
+
rec.closedReason = undefined;
|
|
747
|
+
rec.round = (rec.round ?? 0) + 1;
|
|
748
|
+
rec.idleSince = Date.now();
|
|
749
|
+
rec.resumable = true;
|
|
750
|
+
// ⑦ `.alive` 保留——无删除动作(D3a 跨轮延续,见方法头)。
|
|
751
|
+
// ⑧ pending 注销发射点②(已接线 SubagentService 装配点;未注入时跳过——纯内存
|
|
752
|
+
// 测试形态 no-op)。
|
|
753
|
+
this.pendingUnregister?.(id, "running");
|
|
754
|
+
// ⑨ entry 上报(best-effort 过程面)。
|
|
755
|
+
this.reportRecordTransition(rec);
|
|
756
|
+
this.notifyChange();
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* 意图原语:正常终态(含 disposeAllRecords 编排性关闭,reason=parent-*,D8 矩阵)。
|
|
762
|
+
* 只吸收**持久化面**——collectPatch / worktree cleanup / pending 注销① / onFinalized
|
|
763
|
+
* 钩子留调用方编排(§3.1 副作用边界)。内存终态冻结(completeRecord/tryTransition)
|
|
764
|
+
* 亦留调用方——状态机操作非文件布局。
|
|
765
|
+
*
|
|
766
|
+
* 内部写序(D8 v7):`.state` writeSync **先**(终态权威优先落)→ entry/archive →
|
|
767
|
+
* manifest writeSync 后 → `.alive` 删除(release 出口①)。
|
|
768
|
+
*
|
|
769
|
+
* 失败语义(§3.4):`.state` 重试耗尽仍未落 → 返回 false,**零持久化副作用**(不
|
|
770
|
+
* archive / 不写 manifest / 不 release 写权声明)——record 留 running 形态(磁盘无
|
|
771
|
+
* 终态位,下次 boot 孤儿恢复终态化承接);错误已在 state-marker 层 error 级响亮暴露。
|
|
772
|
+
*
|
|
773
|
+
* @returns true = 持久化面完成;false = `.state` 未落(record 不应被视作已终态化)。
|
|
774
|
+
*/
|
|
775
|
+
markFinalized(record: ExecutionRecord, closedReason?: ClosedReason): boolean {
|
|
776
|
+
const reason = closedReason ?? record.closedReason ?? "gc";
|
|
777
|
+
if (record.sessionFile !== undefined) {
|
|
778
|
+
if (!writeFinalizedState(record.sessionFile, reason)) return false;
|
|
779
|
+
// 终态 usage 快照随 binding 落盘(维持 doFinalizeRecord Step3a 现状——light
|
|
780
|
+
// 列表面的唯一低成本 usage 源)。binding 内部 best-effort(缺失不造残缺身份)。
|
|
781
|
+
updateRecordBinding(record.sessionFile, {
|
|
782
|
+
totalTokens: record.totalTokens,
|
|
783
|
+
turns: record.turnCount,
|
|
784
|
+
endedAt: record.endedAt,
|
|
785
|
+
});
|
|
786
|
+
} else {
|
|
787
|
+
logger.warn("[subagents] markFinalized: no sessionFile anchor, .state face skipped", {
|
|
788
|
+
detail: { id: record.id },
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
this.archive(record);
|
|
792
|
+
this.writeTerminalManifest(record);
|
|
793
|
+
if (record.sessionFile !== undefined) removeAliveMarker(record.sessionFile);
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* 意图原语:取消终态(tombstone)。写序与失败语义同 markFinalized(D8 v7);区别
|
|
799
|
+
* 仅 `.state` 载荷 = {status:"cancelled", endedAt}(重建判定分支消费精确结束时间)。
|
|
800
|
+
* 归口写点:cancelBackground 终态写面(record-lifecycle,U2a 迁移)。
|
|
801
|
+
*/
|
|
802
|
+
markCancelled(record: ExecutionRecord): boolean {
|
|
803
|
+
if (record.sessionFile !== undefined) {
|
|
804
|
+
if (!writeCancelledState(record.sessionFile, record.endedAt ?? Date.now())) return false;
|
|
805
|
+
updateRecordBinding(record.sessionFile, {
|
|
806
|
+
totalTokens: record.totalTokens,
|
|
807
|
+
turns: record.turnCount,
|
|
808
|
+
endedAt: record.endedAt,
|
|
809
|
+
});
|
|
810
|
+
} else {
|
|
811
|
+
logger.warn("[subagents] markCancelled: no sessionFile anchor, .state face skipped", {
|
|
812
|
+
detail: { id: record.id },
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
this.archive(record);
|
|
816
|
+
this.writeTerminalManifest(record);
|
|
817
|
+
if (record.sessionFile !== undefined) removeAliveMarker(record.sessionFile);
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* 意图原语:sync 批终态(统一写点)。内部写序显式复刻 barrier:manifest 落盘
|
|
823
|
+
* **完成**先于批通知写账(batchFinalized 落标 entry)——「通知可达 ⇒ 索引就位」
|
|
824
|
+
* 构造性保证(session-reader 指针行反查依赖;缓存降级下 barrier 不可删,D4②/D5)。
|
|
825
|
+
* manifestDir 提供时为同步写(写完即返回);缺省降级 allSettled 异步屏障(原
|
|
826
|
+
* writeSyncBatchManifestBarrier,已随 U3 归口删除)。
|
|
827
|
+
*/
|
|
828
|
+
async markBatchFinalized(records: readonly SubagentRecord[]): Promise<void> {
|
|
829
|
+
if (this.manifestDir !== undefined) {
|
|
830
|
+
for (const rec of records) {
|
|
831
|
+
try {
|
|
832
|
+
writeAtomicFileSync(
|
|
833
|
+
path.join(this.manifestDir, `${rec.id}.json`),
|
|
834
|
+
JSON.stringify(RecordStore.batchManifestRecord(rec), null, MANIFEST_INDENT_SPACES),
|
|
835
|
+
);
|
|
836
|
+
} catch (err) {
|
|
837
|
+
logger.warn("[subagents] batch-finalized manifest sync write failed (pointer lookup index missing for this member)", {
|
|
838
|
+
detail: { id: rec.id, error: err instanceof Error ? err.message : String(err) },
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
} else if (this.manifestStore !== undefined) {
|
|
843
|
+
// 缺省降级分支(仅纯内存测试形态可达):异步屏障 allSettled(失败 warn 不
|
|
844
|
+
// 阻断写账——反查索引缺失只影响指针行反查,session-reader 错误文案已指引
|
|
845
|
+
// 绝对路径兜底)。
|
|
846
|
+
const results = await Promise.allSettled(
|
|
847
|
+
records.map((rec) => this.manifestStore!.writeManifest(RecordStore.batchManifestRecord(rec))),
|
|
848
|
+
);
|
|
849
|
+
for (let i = 0; i < results.length; i++) {
|
|
850
|
+
const result = results[i]!;
|
|
851
|
+
if (result.status === "rejected") {
|
|
852
|
+
logger.warn("[subagents] batch-finalized manifest write failed", {
|
|
853
|
+
detail: { id: records[i]!.id, error: result.reason instanceof Error ? result.reason.message : String(result.reason) },
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// 批通知写账(barrier 之后):显式覆写 collectMode/batchFinalized 落标——防非
|
|
859
|
+
// entry 源重建(getFullRecord sidecar/manifest 分支)丢标记(对齐
|
|
860
|
+
// appendBatchFinalizedEntry 现状)。
|
|
861
|
+
for (const rec of records) {
|
|
862
|
+
this.reportSubagentRecord({ ...rec, collectMode: "sync", batchFinalized: true });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* 意图原语:引擎死亡收养(字段⑤⑩——error/result/resumable 三写,record 保持
|
|
868
|
+
* resumable 交监督器接管,禁 completed 谎报 / closed 直接终局)。归口写点:
|
|
869
|
+
* adoptResumableAfterEngineDeath(run-orchestration——已随 U2b 修复轮迁移);
|
|
870
|
+
* 监督器 adoptOnProcessDeath 编排留调用方。
|
|
871
|
+
*
|
|
872
|
+
* @returns false = id 不在内存(debug 留痕,无副作用)。
|
|
873
|
+
*/
|
|
874
|
+
adoptEngineDeath(id: string, opts: { error: string }): boolean {
|
|
875
|
+
const rec = this.records.get(id);
|
|
876
|
+
if (rec === undefined) {
|
|
877
|
+
logger.debug("[subagents] adoptEngineDeath: record not in memory", { detail: { id } });
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
rec.error = opts.error;
|
|
881
|
+
rec.result = undefined;
|
|
882
|
+
rec.resumable = true;
|
|
883
|
+
this.reportRecordTransition(rec);
|
|
884
|
+
this.notifyChange();
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* 意图原语:磁盘终态位翻回活态(透明重生回边整体收编,D3c 规格)。
|
|
890
|
+
*
|
|
891
|
+
* acquire-first 顺序(单 try 域原子收敛):写 `.alive` 写权声明(acquire)→ 删
|
|
892
|
+
* `.state` → 删 `.finalized`/`.cancelled` legacy(读侧兼容回退认领旧名,残留未删
|
|
893
|
+
* 则重建仍读出终态),随后 resurrectClosed 内存翻回 + register——
|
|
894
|
+
* 任一步失败**响亮抛错**(禁止 best-effort 吞错续跑:acquire 失败 = 双写风险敞口;
|
|
895
|
+
* acquire-first 顺序保证失败时磁盘保持 closed 可读形态——极端形态下经 legacy
|
|
896
|
+
* 文件名回退仍读出终态,见 catch 文案)。reportTransition(entry 上报)留编排层
|
|
897
|
+
* (纯投递副作用,失败不破坏状态一致性)。
|
|
898
|
+
*
|
|
899
|
+
* 两种接管形态统一(wasClosed 判别):closed 候选 = 三件套全量;running 候选接管
|
|
900
|
+
* (跨重启磁盘重建)= 跳过删终态位(无 `.state` 可删)**仍 acquire marker**
|
|
901
|
+
* (「接管即声明」——现状此路径不写 marker 的双写窗随归口消灭)。
|
|
902
|
+
* 中间形态推演(G3 单点论证)见设计 D3c (i)(ii)(iii):三形态无卡死态、无双写窗。
|
|
903
|
+
*
|
|
904
|
+
* @param record 调用方重建的可变 record(createRecord 产物)
|
|
905
|
+
* @param wasClosed 磁盘候选是否为 closed 形态(cold-lookup 的 found.status 判定)
|
|
906
|
+
* @throws Error acquire 或终态位删除失败(含 sessionFile 缺失——无锚点无法声明写权)
|
|
907
|
+
*/
|
|
908
|
+
markResurrected(record: ExecutionRecord, wasClosed: boolean): void {
|
|
909
|
+
const { id, sessionFile } = record;
|
|
910
|
+
if (sessionFile === undefined) {
|
|
911
|
+
throw new Error(
|
|
912
|
+
`markResurrected(${id}): no sessionFile anchor — cannot acquire write lease; ` +
|
|
913
|
+
`resurrect aborted without touching disk or memory (no half-state).`,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
// acquire-first:先声明写权——失败即中止,终态位未删(D3c (i)/(ii) 形态锚)。
|
|
918
|
+
writeAliveMarker(sessionFile, { pid: process.pid, id, startedAt: Date.now() });
|
|
919
|
+
if (wasClosed) {
|
|
920
|
+
fs.rmSync(`${sessionFile}${STATE_SIDECAR_EXT}`, { force: true });
|
|
921
|
+
// 旧名两名全量清理(与 writeStateMarker 写侧清理对称):readStateMarker 在 .state
|
|
922
|
+
// 缺失时回退旧名——残留任一旧终态文件都会让重建读出 cancelled/finalized,破坏
|
|
923
|
+
// live ≡ reload。
|
|
924
|
+
fs.rmSync(`${sessionFile}.finalized`, { force: true });
|
|
925
|
+
fs.rmSync(`${sessionFile}.cancelled`, { force: true });
|
|
926
|
+
}
|
|
927
|
+
} catch (err) {
|
|
928
|
+
logger.error(
|
|
929
|
+
`[subagents] markResurrected(${id}) failed to acquire/flip terminal position; ` +
|
|
930
|
+
`resurrect aborted loudly (disk keeps a closed-readable shape (possibly via legacy filename), memory unregistered)`,
|
|
931
|
+
{ detail: { sessionFile, error: err instanceof Error ? err.message : String(err) } },
|
|
932
|
+
);
|
|
933
|
+
throw new Error(
|
|
934
|
+
`markResurrected(${id}): write-lease acquire/terminal-position flip failed for ${sessionFile} ` +
|
|
935
|
+
`(${err instanceof Error ? err.message : String(err)}). Recovery: inspect disk (permissions/full) and retry message; ` +
|
|
936
|
+
`terminal state remains closed (possibly via legacy filename), the record stays resurrectable.`,
|
|
937
|
+
{ cause: err },
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
resurrectClosed(record);
|
|
941
|
+
this.register(record);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* 意图原语:idle-GC 归档(30 天 TTL 内存回收,**非终态化**——record 磁盘仍 running
|
|
946
|
+
* 可接管;归档 ≠ 放弃可重连性,故不写 `.state`「gc」——那会把「可接管」变「不可
|
|
947
|
+
* 重连硬拒」,D3a 被否分支)。
|
|
948
|
+
*
|
|
949
|
+
* 写序(D3a/轮 5):store.archive **先**、`.alive` release **后**——archive 抛错则
|
|
950
|
+
* 原语整体失败、marker 必未删(持有与声明一致);release 失败 best-effort 留痕
|
|
951
|
+
* (removeAliveMarker 内部 warn——GC 为旁路维护路径不阻断 interval,泄漏窗 = 至
|
|
952
|
+
* 宿主退出,已接受)。归档 record 后续被接管时统一 acquireWriteLease 重新声明。
|
|
953
|
+
*
|
|
954
|
+
* [U4c / G2] 归档点补写 manifest(投影 running——磁盘确仍 running):record 离开
|
|
955
|
+
* 内存后,外部 session-reader 的 identity 富字段主路径只剩 manifest(子文件
|
|
956
|
+
* identity entry 随 30 天 GC 衰减),归档时不落盘则该 record 在 manifest 面长期
|
|
957
|
+
* 缺席。写失败走 writeTerminalManifest 同款响亮上报(终态写面共用通道)。
|
|
958
|
+
*/
|
|
959
|
+
markIdleArchived(record: ExecutionRecord): void {
|
|
960
|
+
this.archive(record);
|
|
961
|
+
// [U4c / G2] 归档点补写:经状态派生投影(running 如实投影——非终态化语义,
|
|
962
|
+
// terminalManifestRecord 的 closed 硬编码不适用),响亮失败通道同终态写面。
|
|
963
|
+
this.writeManifestPersisted(record.id, RecordStore.derivedManifestRecord(RecordStore.recordToSubagent(record)));
|
|
964
|
+
if (record.sessionFile !== undefined) removeAliveMarker(record.sessionFile);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* [A6 / D3a 时机①] store 内部 acquire 动作——writeAliveMarker 的唯一包装(G1 口径
|
|
969
|
+
* = store 内部写面;非新意图原语)。spawn 侧 sessionFile 锚点确立(run 应答回填 /
|
|
970
|
+
* 冷启动 resume 续轮)时由调用方挂钩(U2b),宿主开始往 session 文件写即声明写权。
|
|
971
|
+
*
|
|
972
|
+
* @throws Error 写失败原样上抛(acquire 失败 = 双写风险敞口,调用方响亮处理——
|
|
973
|
+
* 禁止 best-effort 吞错续跑,D3c 失败语义)。
|
|
974
|
+
*/
|
|
975
|
+
acquireWriteLease(sessionFile: string, recordId: string): void {
|
|
976
|
+
writeAliveMarker(sessionFile, { pid: process.pid, id: recordId, startedAt: Date.now() });
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** 终态 manifest 投影(markFinalized/markCancelled 共用;对齐 writeManifestBestEffort
|
|
980
|
+
* 现状投影——status 统一 closed,closedReason 随投影携带)。
|
|
981
|
+
* [U4c / G2 词汇双写] executionStatus(ExecutionStatus 二态)随终态写面双写——
|
|
982
|
+
* 旧 status 三态是 session-reader 直读的投影字段(永久保留),新字段是内部权威
|
|
983
|
+
* 词汇的写面过渡锚(D5 词汇全景①③并存)。 */
|
|
984
|
+
private static terminalManifestRecord(record: ExecutionRecord): ManifestRecord {
|
|
985
|
+
return {
|
|
986
|
+
id: record.id,
|
|
987
|
+
rootSessionId: record.rootSessionId ?? "",
|
|
988
|
+
parentRecordId: record.parentRecordId,
|
|
989
|
+
agentName: record.agent,
|
|
990
|
+
status: "closed",
|
|
991
|
+
executionStatus: "closed",
|
|
992
|
+
closedReason: record.closedReason,
|
|
993
|
+
createdAt: record.startedAt,
|
|
994
|
+
completedAt: record.endedAt ?? Date.now(),
|
|
995
|
+
sessionFile: record.sessionFile,
|
|
996
|
+
task: record.task,
|
|
997
|
+
slug: record.slug,
|
|
998
|
+
model: record.model,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** 批成员 manifest 投影(markBatchFinalized 用;原 writeBatchMemberManifest,
|
|
1003
|
+
* 已随 U3 归口删除——status 如实投影[成功成员此刻 running+resumable],后续
|
|
1004
|
+
* upgrade 终态时原子覆盖)。[U4c / G2] executionStatus/closedReason 双写同 terminal 投影。 */
|
|
1005
|
+
private static batchManifestRecord(rec: SubagentRecord): ManifestRecord {
|
|
1006
|
+
return {
|
|
1007
|
+
id: rec.id,
|
|
1008
|
+
rootSessionId: rec.rootSessionId ?? "",
|
|
1009
|
+
parentRecordId: rec.parentRecordId,
|
|
1010
|
+
agentName: rec.agent,
|
|
1011
|
+
status: rec.status,
|
|
1012
|
+
executionStatus: rec.status,
|
|
1013
|
+
closedReason: rec.closedReason,
|
|
1014
|
+
createdAt: rec.startedAt,
|
|
1015
|
+
completedAt: rec.endedAt,
|
|
1016
|
+
sessionFile: rec.sessionFile,
|
|
1017
|
+
task: rec.task,
|
|
1018
|
+
slug: rec.slug,
|
|
1019
|
+
model: rec.model,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* [U4c / G1+G2] 状态派生 manifest 投影(rebuildIndexes / 反查 miss 惰性通道 /
|
|
1025
|
+
* markIdleArchived 归档点共用)。数据源 = SubagentRecord 投影(identity entry/
|
|
1026
|
+
* binding + `.state` sidecar 矩阵,D1「.state 权威 + entry 尽力」)——词汇双写
|
|
1027
|
+
* 同终态写面。旧 status 三态从 closedReason 派生 cancelled(对齐 markCancelled
|
|
1028
|
+
* 的 `.state` cancelled 分支重建语义:buildRecord 分支 1 closedReason="cancelled")。
|
|
1029
|
+
*/
|
|
1030
|
+
private static derivedManifestRecord(rec: SubagentRecord): ManifestRecord {
|
|
1031
|
+
const legacyStatus: ManifestRecord["status"] =
|
|
1032
|
+
rec.status === "closed" ? (rec.closedReason === "cancelled" ? "cancelled" : "closed") : "running";
|
|
1033
|
+
return {
|
|
1034
|
+
id: rec.id,
|
|
1035
|
+
rootSessionId: rec.rootSessionId ?? "",
|
|
1036
|
+
parentRecordId: rec.parentRecordId,
|
|
1037
|
+
agentName: rec.agent,
|
|
1038
|
+
status: legacyStatus,
|
|
1039
|
+
executionStatus: rec.status,
|
|
1040
|
+
closedReason: rec.closedReason,
|
|
1041
|
+
createdAt: rec.startedAt,
|
|
1042
|
+
completedAt: rec.endedAt,
|
|
1043
|
+
sessionFile: rec.sessionFile,
|
|
1044
|
+
task: rec.task,
|
|
1045
|
+
slug: rec.slug,
|
|
1046
|
+
model: rec.model,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* [D8 v7] manifest 落盘统一通道:manifestDir 提供时 writeAtomicFileSync 同步写
|
|
1052
|
+
* (停机竞态构造性消灭;与 ManifestStore.writeManifest 字节形态一致——2 空格缩进
|
|
1053
|
+
* JSON,读侧/外部 session-reader 不感知差异);缺省 fire-and-forget 异步写(仅纯
|
|
1054
|
+
* 内存测试形态)。写失败响亮(error 日志 + 用户可见 entry,对齐 writeManifestBestEffort)。
|
|
1055
|
+
*/
|
|
1056
|
+
private writeManifestPersisted(id: string, manifest: ManifestRecord): void {
|
|
1057
|
+
if (this.manifestDir !== undefined) {
|
|
1058
|
+
try {
|
|
1059
|
+
writeAtomicFileSync(
|
|
1060
|
+
path.join(this.manifestDir, `${id}.json`),
|
|
1061
|
+
JSON.stringify(manifest, null, MANIFEST_INDENT_SPACES),
|
|
1062
|
+
);
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
RecordStore.reportManifestWriteFailure(id, err, this.pi);
|
|
1065
|
+
}
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (this.manifestStore !== undefined) {
|
|
1069
|
+
void this.manifestStore.writeManifest(manifest).catch((err: unknown) => {
|
|
1070
|
+
RecordStore.reportManifestWriteFailure(id, err, this.pi);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* [D8 v7] 终态 manifest 落盘(markFinalized/markCancelled 共用,投影 status 恒
|
|
1077
|
+
* closed)。同步性与失败语义见 writeManifestPersisted。
|
|
1078
|
+
*/
|
|
1079
|
+
private writeTerminalManifest(record: ExecutionRecord): void {
|
|
1080
|
+
this.writeManifestPersisted(record.id, RecordStore.terminalManifestRecord(record));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// ── [U4c / D5] 缓存降级重建通道 ──────────────────────────────
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* [U4c / G1] 全量重建可丢缓存(boot 通道:boot revive 完成后由宿主调用)。
|
|
1087
|
+
*
|
|
1088
|
+
* manifest 与 sessions-index 均为可丢缓存(D5):权威 = `.state`,重建源 =
|
|
1089
|
+
* `.state` + entry 尽力。本方法:
|
|
1090
|
+
* 1. 触发全量扫描(reconstructAll)——sessions-index 的重建隐式完成于既有机制
|
|
1091
|
+
* (loadIndex 空/损坏 → 探测 → dirty → saveIndex,「损坏静默回退全扫」同款
|
|
1092
|
+
* 先例),不另设第二条索引写路径;
|
|
1093
|
+
* 2. 对扫描重建出的每个 record,manifest 缺失时补写(幂等补缺,不覆写幸存
|
|
1094
|
+
* manifest——幸存者可能比重建源新鲜,覆写 = 用尽力数据降级权威快照)。
|
|
1095
|
+
*
|
|
1096
|
+
* 失败降级(D5 三要素):单 record 重建源不可用(子 session 已 GC/损坏 → 不在
|
|
1097
|
+
* 扫描集)或 manifest 写失败 → debug 留痕跳过该条,整轮不抛(S5「重建照常无错
|
|
1098
|
+
* 误静默吞」)——缓存补缺失败不构成宿主错误。
|
|
1099
|
+
*
|
|
1100
|
+
* @returns 补写的 manifest 数(诊断/日志用)。
|
|
1101
|
+
*/
|
|
1102
|
+
rebuildIndexes(): number {
|
|
1103
|
+
const records = this.reconstructAll(undefined);
|
|
1104
|
+
let rebuilt = 0;
|
|
1105
|
+
for (const rec of records) {
|
|
1106
|
+
if (this.rebuildManifestIfMissing(rec)) rebuilt++;
|
|
1107
|
+
}
|
|
1108
|
+
return rebuilt;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* [U4c / G1] 单 record 的 manifest 惰性补建(rebuildIndexes 全量轮 + mergedRecords
|
|
1113
|
+
* 惰性通道共用)。manifest 已存在 → 跳过;无 manifest 落点(manifestDir/manifestStore
|
|
1114
|
+
* 均缺省,纯内存测试形态)→ 跳过;每 id 每进程至多尝试一次(manifestRebuildTried
|
|
1115
|
+
* 守卫——boot 全量轮与惰性通道共享,防重复磁盘写)。写失败 debug 留痕不抛
|
|
1116
|
+
* (缓存面,§3.4「缓存损坏」行——无需动作)。
|
|
1117
|
+
*
|
|
1118
|
+
* @returns true = 本次实际补写。
|
|
1119
|
+
*/
|
|
1120
|
+
private rebuildManifestIfMissing(rec: SubagentRecord): boolean {
|
|
1121
|
+
if (this.manifestDir === undefined && this.manifestStore === undefined) return false;
|
|
1122
|
+
if (this.manifestRebuildTried.has(rec.id)) return false;
|
|
1123
|
+
this.manifestRebuildTried.add(rec.id);
|
|
1124
|
+
if (this.manifestDir !== undefined) {
|
|
1125
|
+
const manifestPath = path.join(this.manifestDir, `${rec.id}.json`);
|
|
1126
|
+
try {
|
|
1127
|
+
if (fs.existsSync(manifestPath)) return false;
|
|
1128
|
+
writeAtomicFileSync(manifestPath, JSON.stringify(RecordStore.derivedManifestRecord(rec), null, MANIFEST_INDENT_SPACES));
|
|
1129
|
+
return true;
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
logger.debug("[subagents] rebuildIndexes: manifest rebuild skipped (write failed)", {
|
|
1132
|
+
detail: { id: rec.id, error: err instanceof Error ? err.message : String(err) },
|
|
1133
|
+
});
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// 缺省降级形态(manifestDir 缺省、manifestStore 在,纯内存测试形态外的测试分支):
|
|
1138
|
+
// 异步写,失败同样静默降级。
|
|
1139
|
+
void this.manifestStore!.writeManifest(RecordStore.derivedManifestRecord(rec)).catch((err: unknown) => {
|
|
1140
|
+
logger.debug("[subagents] rebuildIndexes: manifest rebuild skipped (write failed)", {
|
|
1141
|
+
detail: { id: rec.id, error: err instanceof Error ? err.message : String(err) },
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** manifest 写失败的双通道上报(error 日志给开发者 + entry 给用户,对齐
|
|
1148
|
+
* writeManifestBestEffort 现状)。 */
|
|
1149
|
+
private static reportManifestWriteFailure(
|
|
1150
|
+
id: string,
|
|
1151
|
+
err: unknown,
|
|
1152
|
+
pi: RecordStorePi,
|
|
1153
|
+
): void {
|
|
1154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1155
|
+
logger.error(`[subagents] manifest write failed (record=${id}): ${msg}`);
|
|
1156
|
+
pi?.appendEntry?.("subagent:manifest-write-failed", { id, error: msg });
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* [H4 收口 / G1] entry 重物化腿的 manifest 投影补写(record-access 可重连终态
|
|
1161
|
+
* 重物化通道的唯一入口——store 外零 manifest 直写)。manifest 是可丢缓存(D5),
|
|
1162
|
+
* 本方法只做缺员补写:失败 warn 留痕不响亮(缓存补缺失败不构成宿主错误,对齐
|
|
1163
|
+
* U4c rebuildIndexes 的降级语义——区别于终态面 writeManifestPersisted 的响亮);
|
|
1164
|
+
* manifestDir 接线时为同步写(停机窗防护与终态面同源)。
|
|
1165
|
+
*/
|
|
1166
|
+
rematerializeManifest(manifest: ManifestRecord): void {
|
|
1167
|
+
const warnFailure = (err: unknown): void => {
|
|
1168
|
+
logger.warn("[subagents] rematerialize manifest write failed (cache backfill)", {
|
|
1169
|
+
detail: { id: manifest.id, error: err instanceof Error ? err.message : String(err) },
|
|
1170
|
+
});
|
|
1171
|
+
};
|
|
1172
|
+
if (this.manifestDir !== undefined) {
|
|
1173
|
+
try {
|
|
1174
|
+
writeAtomicFileSync(
|
|
1175
|
+
path.join(this.manifestDir, `${manifest.id}.json`),
|
|
1176
|
+
JSON.stringify(manifest, null, MANIFEST_INDENT_SPACES),
|
|
1177
|
+
);
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
warnFailure(err);
|
|
1180
|
+
}
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (this.manifestStore !== undefined) {
|
|
1184
|
+
void this.manifestStore.writeManifest(manifest).catch(warnFailure);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
561
1188
|
/** 按 id 查找。返回可变 record(仅 runtime 内部用)。 */
|
|
562
1189
|
getMutable(id: string): ExecutionRecord | undefined {
|
|
563
1190
|
return this.records.get(id);
|
|
@@ -566,7 +1193,7 @@ export class RecordStore {
|
|
|
566
1193
|
/**
|
|
567
1194
|
* abort 所有 running record 的 controller(background 子进程 SIGTERM)。
|
|
568
1195
|
*
|
|
569
|
-
* 仅在 SubagentService.dispose(进程退出路径)调用。不做 CAS
|
|
1196
|
+
* 仅在 SubagentService.dispose(进程退出路径)调用。不做 CAS/终态标记——dispose
|
|
570
1197
|
* 是终局,状态机收尾无意义;目的是让 background 子进程的 AbortSignal 触发 →
|
|
571
1198
|
* runSpawn 的 signal listener → child.kill("SIGTERM"),防止主进程退出后子进程成孤儿。
|
|
572
1199
|
*
|
|
@@ -606,7 +1233,7 @@ export class RecordStore {
|
|
|
606
1233
|
* ╔══════════════════════════════════════════════════════════════════╗
|
|
607
1234
|
* ║ 1. 磁盘源:扫 sessionsDir 的 .jsonl,逐个 scanFile([perf] 头部 ║
|
|
608
1235
|
* ║ identity 轻量重建 + stat 戳缓存命中零读取)。cancelled ║
|
|
609
|
-
* ║
|
|
1236
|
+
* ║ 终态 sidecar override status。详情字段(eventLog/result/turns) ║
|
|
610
1237
|
* ║ 缺省,由 getFullRecord(id) 懒加载 ║
|
|
611
1238
|
* ║ 2. 内存源覆盖(同 id 内存优先——running record 更新鲜) ║
|
|
612
1239
|
* ║ 3. session 过滤:只留 rootSessionId === rootSessionFilter 的 ║
|
|
@@ -624,12 +1251,64 @@ export class RecordStore {
|
|
|
624
1251
|
*
|
|
625
1252
|
* session 隔离:同一 cwd 下多个 Pi session 共享 sessionsDir,靠 rootSessionId
|
|
626
1253
|
* 区分。内存与磁盘源都按 rootSessionFilter 过滤后再 merge/sort/slice。
|
|
1254
|
+
*
|
|
1255
|
+
* [H2 W1] includeWorkflow(设计 subagent-workflow-record-unification §3.3 D1):
|
|
1256
|
+
* 缺省 false = 过滤 origin==="workflow" 的 record(subagents tool list / TUI
|
|
1257
|
+
* /subagents 等消费面默认不展示 workflow 派发的 record);true = 全量(排查通道)。
|
|
1258
|
+
* 过滤只在查询消费面——治理路径(recoverOrphanRecords / recoverEntryOnlyOrphans /
|
|
1259
|
+
* revive)与本参数无关,永远全量可见(D1 ⑥ 负向规格)。
|
|
627
1260
|
*/
|
|
628
1261
|
collectRecords(
|
|
629
1262
|
limit: number,
|
|
630
1263
|
statusFilter: StatusFilter = "all",
|
|
631
1264
|
rootSessionFilter?: string,
|
|
1265
|
+
includeWorkflow: boolean = false,
|
|
1266
|
+
): SubagentRecord[] {
|
|
1267
|
+
let result = [...this.mergedRecords(rootSessionFilter).values()];
|
|
1268
|
+
|
|
1269
|
+
// 3. statusFilter。
|
|
1270
|
+
if (statusFilter === "running") {
|
|
1271
|
+
result = result.filter((r) => r.status === "running");
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// 3.5 [H2 W1] origin 过滤(缺省排除 workflow 来源)。负向判定:origin 缺省
|
|
1275
|
+
// (undefined = "tool" 语义)与显式 "tool" 均保留。
|
|
1276
|
+
if (!includeWorkflow) {
|
|
1277
|
+
result = result.filter((r) => r.origin !== "workflow");
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// 4-5. 排序 + slice。
|
|
1281
|
+
return result
|
|
1282
|
+
.sort(RecordStore.compareRecords)
|
|
1283
|
+
.slice(0, limit);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* [H2 W1] 按 workflow run id 列 record(parentRunId 查询入口,W2 run 视图进度 /
|
|
1288
|
+
* W3 下钻消费)。查询域 = collectRecords 现状口径(内存 ∪ 磁盘重建 ∪ manifest 补充,
|
|
1289
|
+
* 设计 D2「查询域显式」)。不过滤 origin——按 parentRunId 显式查询即下钻/治理语义
|
|
1290
|
+
* (tool 来源 record parentRunId 恒 undefined,不会被误中)。
|
|
1291
|
+
*
|
|
1292
|
+
* 返回排序与 slice 口径同 collectRecords(STATUS_PRIORITY + startedAt desc + limit)。
|
|
1293
|
+
*/
|
|
1294
|
+
collectRecordsByParentRunId(
|
|
1295
|
+
parentRunId: string,
|
|
1296
|
+
limit: number,
|
|
1297
|
+
rootSessionFilter?: string,
|
|
632
1298
|
): SubagentRecord[] {
|
|
1299
|
+
return [...this.mergedRecords(rootSessionFilter).values()]
|
|
1300
|
+
.filter((r) => r.parentRunId === parentRunId)
|
|
1301
|
+
.sort(RecordStore.compareRecords)
|
|
1302
|
+
.slice(0, limit);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* collectRecords / collectRecordsByParentRunId 共用的三源合并(磁盘重建 ∪ manifest
|
|
1307
|
+
* 补充 ∪ 内存覆盖)。返回 byId Map(内存优先——running record 是活态比磁盘重建新)。
|
|
1308
|
+
* [D1 ⑤] 磁盘重建投影本身不做任何 origin 过滤——record 全量重建,过滤只在上层
|
|
1309
|
+
* 查询消费面按参数生效。
|
|
1310
|
+
*/
|
|
1311
|
+
private mergedRecords(rootSessionFilter?: string): Map<string, SubagentRecord> {
|
|
633
1312
|
const byId = new Map<string, SubagentRecord>();
|
|
634
1313
|
|
|
635
1314
|
// 1. 磁盘源(重建终态 record)。 reconstructAll 已按 rootSessionFilter 过滤。
|
|
@@ -671,16 +1350,19 @@ export class RecordStore {
|
|
|
671
1350
|
byId.set(r.id, RecordStore.recordToSubagent(r));
|
|
672
1351
|
}
|
|
673
1352
|
|
|
674
|
-
// 3.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1353
|
+
// 3. [U4c / G1 惰性通道] manifest 反查 miss 的惰性重建(D5 双通道的惰性腿):
|
|
1354
|
+
// 磁盘源已重建的 record 若在 manifest 索引缺员(缓存被删/boot 全量轮漏扫),
|
|
1355
|
+
// 此处补建——manifest 是外部 session-reader 的 identity 富字段主路径与指针
|
|
1356
|
+
// 行反查索引,缺员窗口不应等到下次 boot。在途 record(内存持有,本 host 的
|
|
1357
|
+
// 终态写点会落 manifest——「创建时不写」契约保持)不在补建集;每 id 每进程
|
|
1358
|
+
// 只尝试一次(rebuildManifestIfMissing 内 manifestRebuildTried 守卫,防高频
|
|
1359
|
+
// collectRecords 放大磁盘写)。
|
|
1360
|
+
for (const rec of byId.values()) {
|
|
1361
|
+
if (this.records.has(rec.id)) continue;
|
|
1362
|
+
this.rebuildManifestIfMissing(rec);
|
|
678
1363
|
}
|
|
679
1364
|
|
|
680
|
-
|
|
681
|
-
return result
|
|
682
|
-
.sort(RecordStore.compareRecords)
|
|
683
|
-
.slice(0, limit);
|
|
1365
|
+
return byId;
|
|
684
1366
|
}
|
|
685
1367
|
|
|
686
1368
|
// ── 孤儿终态恢复(residual-fixes 设计 §6.1.2)──────────────────
|
|
@@ -695,14 +1377,15 @@ export class RecordStore {
|
|
|
695
1377
|
}
|
|
696
1378
|
|
|
697
1379
|
/**
|
|
698
|
-
*
|
|
1380
|
+
* 孤儿终态恢复:对重建矩阵兜底分支(running 且无异宿主在持声明;编号沿用设计
|
|
1381
|
+
* D3b 文档的「分支 4」表述,分支 3 已随 externalInstance 投影移除)的 record
|
|
699
1382
|
* 判定真实终态并落 entry,消除「父扩展死后再无人写终态 → 侧栏永久 running」。
|
|
700
1383
|
*
|
|
701
1384
|
* 判定(residual-fixes §5.2 三判据 + chat 分流):
|
|
702
1385
|
* - chatMode = true → 不终态化(跨重启可续聊是产品语义,v4 B-1),落 resumable
|
|
703
1386
|
* entry 供侧栏 waiting 细分;
|
|
704
1387
|
* - 子 JSONL 末行完整 JSON.parse → closed(closedReason=gc,与分支 2 重建映射一致;
|
|
705
|
-
* done/failed 细分由 error 字段经 deriveClosedDisplay 派生)+ 写 .
|
|
1388
|
+
* done/failed 细分由 error 字段经 deriveClosedDisplay 派生)+ 写 .state sidecar
|
|
706
1389
|
* (防重锚——下次重建走分支 2 不再进判定);
|
|
707
1390
|
* - 末行截断 → closed + error(保守,错误方向安全)+ sidecar;
|
|
708
1391
|
* - 文件不可读(IO 错误,可能暂时)→ 不判终态,落 resumable entry(防御性路径,
|
|
@@ -721,8 +1404,15 @@ export class RecordStore {
|
|
|
721
1404
|
recoverOrphanRecords(rootSessionFilter?: string, mainSessionFile?: string): void {
|
|
722
1405
|
const lastById = new Map(this.scanLastRecordEntries(mainSessionFile).map((r) => [r.id, r]));
|
|
723
1406
|
for (const rec of this.reconstructAll(rootSessionFilter)) {
|
|
724
|
-
|
|
725
|
-
|
|
1407
|
+
if (rec.status !== "running") continue; // 分支 1/2 已 closed:无需恢复
|
|
1408
|
+
// [U4a / D3b (a″)] 活实例跳过换 findForeignLiveInstance 现查探针(pid 单判据):
|
|
1409
|
+
// marker pid 活 = 异宿主在持声明,不可清——boot 误终态化会击穿跨进程写权防御
|
|
1410
|
+
// (同 root 双宿主下宿主 A 会把宿主 B 持有中的 record 直断 gc)。比重建时缓存
|
|
1411
|
+
// 的 externalInstance 更新鲜(resurrect 回边可随时改写 marker)。self-pid 排除下,
|
|
1412
|
+
// pid 复用到本进程的残留 marker 同样放行清理——原持有者已死,record 确为孤儿。
|
|
1413
|
+
if (rec.sessionFile !== undefined && findForeignLiveInstance(rec.sessionFile) !== undefined) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
726
1416
|
if (this.orphanJudged.has(rec.id)) continue;
|
|
727
1417
|
this.orphanJudged.add(rec.id);
|
|
728
1418
|
this.finalizeOrphanRecord(rec, lastById.get(rec.id));
|
|
@@ -770,11 +1460,11 @@ export class RecordStore {
|
|
|
770
1460
|
} catch {
|
|
771
1461
|
parseOk = false;
|
|
772
1462
|
}
|
|
773
|
-
// .
|
|
1463
|
+
// .state sidecar:防重锚(同 doFinalizeRecord 终态路径的收尾标记)。
|
|
774
1464
|
// 显式携 reason="gc":孤儿判定「末行完整 = 自然完成」,与 doFinalizeRecord 写入的
|
|
775
1465
|
// 真实 reason 同层——否则无 reason sidecar 在磁盘重建时被兜底为 disconnected,
|
|
776
1466
|
// 把正常完成的记录误标成断联。
|
|
777
|
-
|
|
1467
|
+
writeFinalizedState(sessionFile, "gc");
|
|
778
1468
|
// [F3 boot 直断语义,设计表 3 行 2] 走到本分支的 record 分两类:
|
|
779
1469
|
// - resumable=true(上方分流保证此时 result 有值)= SP-5 one-shot 完成态——任务
|
|
780
1470
|
// 真实完成,直断 closed/gc 无 error,投影 completed 不变;
|
|
@@ -874,7 +1564,9 @@ export class RecordStore {
|
|
|
874
1564
|
}
|
|
875
1565
|
|
|
876
1566
|
/** entry-born 孤儿按无文件判据收敛落 entry:chatMode → resumable(分流语义一致);
|
|
877
|
-
* 否则 closed+gc+error
|
|
1567
|
+
* 否则 closed+gc+error(entry-born 是 zcode 引擎 record 的常态形态——无子 session 文件
|
|
1568
|
+
* 非异常,D8 第五行 entry-born 作用域注记;one-shot 直断 closed/gc,续聊对无 transcript
|
|
1569
|
+
* 形态结构性不成立,承接通道仅 start fresh)。 */
|
|
878
1570
|
private finalizeEntryOnlyOrphan(rec: SubagentRecord, chatMode: boolean): void {
|
|
879
1571
|
this.reportSubagentRecord({
|
|
880
1572
|
...rec,
|
|
@@ -884,7 +1576,7 @@ export class RecordStore {
|
|
|
884
1576
|
status: "closed" as const,
|
|
885
1577
|
closedReason: "gc" as const,
|
|
886
1578
|
endedAt: Date.now(),
|
|
887
|
-
error: "orphan recovery: no child session file
|
|
1579
|
+
error: "orphan recovery: closed by gc (entry-born form has no child session file to resume from; start a fresh subagent)",
|
|
888
1580
|
}),
|
|
889
1581
|
});
|
|
890
1582
|
}
|
|
@@ -947,13 +1639,14 @@ export class RecordStore {
|
|
|
947
1639
|
this.indexDirty = false;
|
|
948
1640
|
this.lastIndexWriteAt = 0;
|
|
949
1641
|
this.indexHigherVersion = false;
|
|
1642
|
+
this.manifestRebuildTried.clear();
|
|
950
1643
|
}
|
|
951
1644
|
|
|
952
1645
|
/**
|
|
953
1646
|
* /resume /fork /new 后复活(dispose 的逆操作)。
|
|
954
1647
|
*
|
|
955
1648
|
* [PS-10/T6④] 同步复位 orphanJudged 防重缓存:resumable 形态(IO-error 保守分支 /
|
|
956
|
-
* chatMode 分流)没有 .
|
|
1649
|
+
* chatMode 分流)没有 .state sidecar 锚,重判资格完全由本缓存承载——dispose 时
|
|
957
1650
|
* 有 clear(session 结束),但 revive 此前不复位,导致「同进程内曾经的 IO 失败记录
|
|
958
1651
|
* 永久停留 resumable」,与本文件 recoverOrphanRecords 注释承诺的「IO 恢复后重开可重判」
|
|
959
1652
|
* 不符。/new 复活正是「重开」语义:IO 已恢复的记录下次 recoverOrphanRecords 重新判定
|
|
@@ -962,18 +1655,25 @@ export class RecordStore {
|
|
|
962
1655
|
revive(): void {
|
|
963
1656
|
this._disposed = false;
|
|
964
1657
|
this.orphanJudged.clear();
|
|
1658
|
+
// [U4c / G1] /new /resume 重开后 manifest 态可能已变(外部删除/异宿主写入),
|
|
1659
|
+
// 惰性重建守卫随缓存态一并复位(对齐 orphanJudged 的「重开重判」语义)。
|
|
1660
|
+
this.manifestRebuildTried.clear();
|
|
965
1661
|
}
|
|
966
1662
|
|
|
967
1663
|
// ── 内部 ──────────────────────────────────────────────────
|
|
968
1664
|
|
|
969
1665
|
/**
|
|
970
|
-
*
|
|
1666
|
+
* 三分支状态矩阵重建(终态 marker / 兜底;[perf] light 版)。
|
|
971
1667
|
*
|
|
972
1668
|
* 优先级:
|
|
973
|
-
* 1.
|
|
974
|
-
* 2.
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1669
|
+
* 1. 终态 sidecar status=cancelled → closed(closedReason=cancelled)
|
|
1670
|
+
* 2. 终态 sidecar status=finalized → closed(closedReason=内容 reason;空/旧格式 → disconnected)
|
|
1671
|
+
* (旧名 .finalized/.cancelled 由 readStateMarker 归一,判定分支不区分来源)
|
|
1672
|
+
* 3. 兜底(其余一切,含 .alive 在持形态)→ running(v4 B-1 可续聊语义)
|
|
1673
|
+
*
|
|
1674
|
+
* [U4a / D3b (a)] 原分支 3(.alive + pid 活 → running + externalInstance 投影)已
|
|
1675
|
+
* 移除:探活缓存的读面角色由 (a′)(a″) 的 findForeignLiveInstance 现查探针替代,
|
|
1676
|
+
* status 判定与 .alive 解耦(终态判定由 `.state` 权威分支承接)。
|
|
977
1677
|
*
|
|
978
1678
|
* [perf]:逐文件 scanFile(stat 戳校验 + 头部 identity 轻量重建)。命中缓存的
|
|
979
1679
|
* 文件零文件读取;变化的文件只重建自身,其余 N-1 个复用缓存。
|
|
@@ -985,15 +1685,11 @@ export class RecordStore {
|
|
|
985
1685
|
// [perf] 目录 mtime 快路径:sessionsDir mtime 未变 ⇒ 文件集合与 sidecar 集合都未变
|
|
986
1686
|
//(任何文件新建/删除/重命名都改目录 mtime),且 jsonl append 不影响 light 态
|
|
987
1687
|
//(identity/status 由首行与 sidecar 决定,进度重数据走 getFullRecord 的独立 stat
|
|
988
|
-
//校验)→ 跳过 readdir + N×
|
|
989
|
-
//期间 250ms 动画 timer + 120ms debounce 双驱动高频扫描,快路径把 ~N×
|
|
990
|
-
// 1
|
|
1688
|
+
//校验)→ 跳过 readdir + N×3 statSync,直接复用缓存 light。/subagents overlay 打开
|
|
1689
|
+
//期间 250ms 动画 timer + 120ms debounce 双驱动高频扫描,快路径把 ~N×3 stat 降到
|
|
1690
|
+
// 1 次(目录本身)。
|
|
991
1691
|
// 已知局限(与 mtime 缓存同族):目录 mtime 粒度粗糙的文件系统(NFS/2s FAT)
|
|
992
|
-
// 可能漏判——APFS 微秒级可靠。
|
|
993
|
-
// 新建/删除/重命名**(这些操作必改目录 mtime);覆盖写已存在的 sidecar 不改目录
|
|
994
|
-
// mtime——`.alive` 覆盖写(resume spawn 后 pid 变化重写 marker)后快路径会复用旧
|
|
995
|
-
// aliveData(旧 pid/旧 startedAt),refreshAlive 探活与 1h 软超时判定可能滞后一拍
|
|
996
|
-
//(status 判定不受影响:分支 3 探活失败只清 externalInstance,不改 status)。
|
|
1692
|
+
// 可能漏判——APFS 微秒级可靠。
|
|
997
1693
|
let dirMtimeMs: number;
|
|
998
1694
|
try {
|
|
999
1695
|
dirMtimeMs = fs.statSync(this.sessionsDir).mtimeMs;
|
|
@@ -1009,11 +1705,9 @@ export class RecordStore {
|
|
|
1009
1705
|
this.indexHigherVersion = loaded.higherVersion;
|
|
1010
1706
|
}
|
|
1011
1707
|
if (this.dirStamp !== null && this.dirStamp.mtimeMs === dirMtimeMs) {
|
|
1012
|
-
const now = Date.now();
|
|
1013
1708
|
const out: SubagentRecord[] = [];
|
|
1014
1709
|
for (const entry of this.fileCache.values()) {
|
|
1015
1710
|
if (entry.negative) continue;
|
|
1016
|
-
RecordStore.refreshAlive(entry, now);
|
|
1017
1711
|
out.push(entry.light);
|
|
1018
1712
|
}
|
|
1019
1713
|
return rootSessionFilter === undefined
|
|
@@ -1043,10 +1737,9 @@ export class RecordStore {
|
|
|
1043
1737
|
}
|
|
1044
1738
|
}
|
|
1045
1739
|
|
|
1046
|
-
const now = Date.now();
|
|
1047
1740
|
const out: SubagentRecord[] = [];
|
|
1048
1741
|
for (const file of files) {
|
|
1049
|
-
const entry = this.scanFile(file
|
|
1742
|
+
const entry = this.scanFile(file);
|
|
1050
1743
|
if (entry) out.push(entry.light);
|
|
1051
1744
|
}
|
|
1052
1745
|
this.dirStamp = { mtimeMs: dirMtimeMs };
|
|
@@ -1056,13 +1749,14 @@ export class RecordStore {
|
|
|
1056
1749
|
}
|
|
1057
1750
|
|
|
1058
1751
|
/**
|
|
1059
|
-
* 扫描单文件:stat 戳(jsonl +
|
|
1060
|
-
*
|
|
1752
|
+
* 扫描单文件:stat 戳(jsonl + 终态 sidecar + record 绑定)校验,全同 →
|
|
1753
|
+
* 复用缓存(零文件读取,含负缓存直接返回 null);否则重建 light。
|
|
1061
1754
|
* identity 定位两级:头部 64KB(首轮会话)→ 全文 fallback(续聊场景 identity
|
|
1062
|
-
* append 在尾部);两级都找不到 →
|
|
1063
|
-
*
|
|
1755
|
+
* append 在尾部);两级都找不到 → [UF-1] record 绑定 sidecar 回退(宿主侧身份
|
|
1756
|
+
* 载荷重建 light)→ 仍无 → 写负缓存(防每轮全文重读)。
|
|
1757
|
+
* 返回 null:文件消失/读失败/无 identity 且无绑定 → 跳过。
|
|
1064
1758
|
*/
|
|
1065
|
-
private scanFile(file: string
|
|
1759
|
+
private scanFile(file: string): FileCacheEntry | null {
|
|
1066
1760
|
const jsonl = statStamp(file);
|
|
1067
1761
|
if (!jsonl) {
|
|
1068
1762
|
this.fileCache.delete(file);
|
|
@@ -1070,53 +1764,117 @@ export class RecordStore {
|
|
|
1070
1764
|
}
|
|
1071
1765
|
const stamps: FileStamps = {
|
|
1072
1766
|
jsonl,
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
alive: statStamp(`${file}.alive`),
|
|
1767
|
+
state: statStateStamp(file),
|
|
1768
|
+
binding: statStamp(`${file}${RECORD_BINDING_SIDECAR_EXT}`),
|
|
1076
1769
|
};
|
|
1077
1770
|
|
|
1078
1771
|
const cached = this.fileCache.get(file);
|
|
1079
1772
|
if (cached !== undefined && isFreshCache(cached, stamps)) {
|
|
1080
1773
|
if (cached.negative) return null; // 负缓存命中:确认无 identity,零读取跳过
|
|
1081
|
-
// pid 探活结果不落盘(进程死亡无 IO)——分支 3 的 running 项每扫重查,
|
|
1082
|
-
// 保留原语义(旧实现每次 collectRecords 都重新 isProcessAlive)。
|
|
1083
|
-
RecordStore.refreshAlive(cached, now);
|
|
1084
1774
|
return cached;
|
|
1085
1775
|
}
|
|
1086
1776
|
|
|
1087
1777
|
// [perf L-1] 磁盘索引查询(首扫惰性装载,miss/空索引时 get 恒 undefined = 无索引)。
|
|
1088
1778
|
// 条目戳匹配 jsonl 当前 stat → 零内容读取构造缓存条目。undefined = 未命中
|
|
1089
1779
|
// (落到下方探测),null = 负条目命中(零探测跳过)。
|
|
1090
|
-
|
|
1091
|
-
|
|
1780
|
+
// [UF-1] 绑定 sidecar 存在的文件跳过索引投影:SessionsIndexEntry 不含
|
|
1781
|
+
// chatMode/round(身份域子集),索引命中会把绑定承载的对话形态域抹成 undefined。
|
|
1782
|
+
if (stamps.binding === null) {
|
|
1783
|
+
const fromIndex = this.buildEntryFromIndex(file, stamps);
|
|
1784
|
+
if (fromIndex !== undefined) return fromIndex;
|
|
1785
|
+
}
|
|
1092
1786
|
|
|
1093
1787
|
// [perf L-1] 索引 miss/戳不匹配落到原三级探测:本轮探测结果必须进索引(含负探测)。
|
|
1094
1788
|
// 覆盖两种形态:首扫(映像已装载但 miss/不匹配)与后续轮次(映像已释放,凡进重建分支必是戳变化)。
|
|
1095
1789
|
this.indexDirty = true;
|
|
1096
1790
|
|
|
1791
|
+
const payloads = readSidecarPayloads(file, stamps);
|
|
1097
1792
|
const header = detectIdentity(file, jsonl.size);
|
|
1098
|
-
|
|
1099
|
-
|
|
1793
|
+
// [UF-1] 身份源两级:子文件 identity entry(历史权威,命中时绑定不参与)→
|
|
1794
|
+
// record 绑定 sidecar(engine-CLI 化后子文件无身份 entry,宿主在 sessionFile
|
|
1795
|
+
// 回填点落盘的 id→file 映射承担恢复能力)。两者皆缺 → 负缓存。
|
|
1796
|
+
const base = header ?? RecordStore.identityFromBinding(payloads.binding, file);
|
|
1797
|
+
if (!base) {
|
|
1798
|
+
// 负缓存:确认无 identity。后续扫描 stat 命中直接跳过;戳变化(文件补写 /
|
|
1799
|
+
// 绑定后到落盘)自动重试。
|
|
1100
1800
|
this.fileCache.set(file, { negative: true, ...stamps });
|
|
1101
1801
|
return null;
|
|
1102
1802
|
}
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1803
|
+
const entry = RecordStore.buildFileCacheEntry(base, file, stamps, payloads);
|
|
1804
|
+
RecordStore.projectBindingDerivedFields(entry, header, payloads);
|
|
1105
1805
|
this.fileCache.set(file, entry);
|
|
1106
|
-
this.idToFile.set(
|
|
1806
|
+
this.idToFile.set(base.id, file);
|
|
1107
1807
|
return entry;
|
|
1108
1808
|
}
|
|
1109
1809
|
|
|
1810
|
+
/**
|
|
1811
|
+
* [UF-1 / H2 A3] 绑定承载域补投影(仅 header === undefined 的绑定重建路径;
|
|
1812
|
+
* identity entry 命中时绑定不参与)。自 scanFile 原样搬移:赋值条件与顺序等价。
|
|
1813
|
+
*/
|
|
1814
|
+
private static projectBindingDerivedFields(
|
|
1815
|
+
entry: FileCacheEntry,
|
|
1816
|
+
header: IdentityHeaderRecon | undefined,
|
|
1817
|
+
payloads: SidecarPayloads,
|
|
1818
|
+
): void {
|
|
1819
|
+
// [UF-1] 绑定承载的对话形态域补投影:IdentityHeaderRecon 无 round 槽位
|
|
1820
|
+
//(既有语义:identity entry 磁盘重建不恢复 round),绑定路径在其上恢复——
|
|
1821
|
+
// 续聊轮数随绑定快照可滞后一拍(state-marker.RecordBinding.round 契约)。
|
|
1822
|
+
if (header === undefined && payloads.binding?.round !== undefined) {
|
|
1823
|
+
entry.light.round = payloads.binding.round;
|
|
1824
|
+
}
|
|
1825
|
+
// [H2 A3] 终态 usage 快照补投影(round 同款先例):binding 快照只在终态写点
|
|
1826
|
+
// (finalizeRecord Step3a)更新,存在即代表终值——light 列表面据此恢复
|
|
1827
|
+
// totalTokens/turns/endedAt,不再恒 0(list 与通知显示消耗真实值)。
|
|
1828
|
+
if (header === undefined && payloads.binding) {
|
|
1829
|
+
const b = payloads.binding;
|
|
1830
|
+
if (b.totalTokens !== undefined) entry.light.totalTokens = b.totalTokens;
|
|
1831
|
+
if (b.turns !== undefined) entry.light.turns = b.turns;
|
|
1832
|
+
if (b.endedAt !== undefined) entry.light.endedAt = b.endedAt;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* [UF-1] record 绑定载荷 → light 身份基底(IdentityHeaderRecon 同形投影)。
|
|
1838
|
+
* 绑定缺失/损坏返回 undefined(调用方落负缓存,不把损坏残留误判成身份)。
|
|
1839
|
+
* forkDepth/model_change/thinking_level_change 等头部途经信息绑定期不存在:
|
|
1840
|
+
* forkDepth 恒 undefined、model/thinkingLevel 取绑定快照值。
|
|
1841
|
+
*/
|
|
1842
|
+
private static identityFromBinding(binding: RecordBinding | undefined, file: string): IdentityHeaderRecon | undefined {
|
|
1843
|
+
if (binding === undefined) return undefined;
|
|
1844
|
+
return {
|
|
1845
|
+
id: binding.recordId,
|
|
1846
|
+
agent: binding.agent,
|
|
1847
|
+
mode: binding.mode,
|
|
1848
|
+
task: binding.task,
|
|
1849
|
+
slug: binding.slug,
|
|
1850
|
+
startedAt: binding.startedAt,
|
|
1851
|
+
rootSessionId: binding.rootSessionId,
|
|
1852
|
+
parentRecordId: binding.parentRecordId,
|
|
1853
|
+
depth: binding.depth,
|
|
1854
|
+
forkDepth: undefined,
|
|
1855
|
+
chatMode: binding.chatMode,
|
|
1856
|
+
worktree: binding.worktree,
|
|
1857
|
+
// [H2 S3] 来源域透传:漏本两行则引擎子文件身份面(binding sidecar)重建丢
|
|
1858
|
+
// origin,归档/重启后 workflow record 逃过 D1 投影过滤(Gate B S3 FAIL 根因)。
|
|
1859
|
+
// binding 读侧(readRecordBinding)已字面量守卫归一,此处直传。
|
|
1860
|
+
origin: binding.origin,
|
|
1861
|
+
parentRunId: binding.parentRunId,
|
|
1862
|
+
model: binding.model,
|
|
1863
|
+
thinkingLevel: binding.thinkingLevel,
|
|
1864
|
+
sessionFile: file,
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1110
1868
|
/**
|
|
1111
1869
|
* [perf L-1] 磁盘索引查询(首扫惰性装载,miss/空索引时 get 恒 undefined = 无索引)。
|
|
1112
|
-
* 条目戳匹配 jsonl 当前 stat → 零内容读取构造缓存条目。sidecar payload
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1870
|
+
* 条目戳匹配 jsonl 当前 stat → 零内容读取构造缓存条目。sidecar payload(终态 marker)
|
|
1871
|
+
* 是活态数据,沿用探测分支的每轮重读语义;终态 reason 静态数据仅在 sidecar 存在
|
|
1872
|
+
* 时读一次(文件小,成本可忽略)。
|
|
1115
1873
|
*
|
|
1116
1874
|
* 返回 undefined = 索引未命中/戳不匹配(调用方落到原三级探测);null = 负条目命中
|
|
1117
1875
|
* (「确认无 identity」跨实例持久,零探测跳过,与内存负缓存同款形态)。
|
|
1118
1876
|
*/
|
|
1119
|
-
private buildEntryFromIndex(file: string, stamps: FileStamps
|
|
1877
|
+
private buildEntryFromIndex(file: string, stamps: FileStamps): FileCacheEntry | null | undefined {
|
|
1120
1878
|
if (this.indexEntries === null) return undefined;
|
|
1121
1879
|
const hit = this.indexEntries.get(path.basename(file));
|
|
1122
1880
|
if (hit === undefined || hit.mtimeMs !== stamps.jsonl.mtimeMs || hit.size !== stamps.jsonl.size) {
|
|
@@ -1127,7 +1885,20 @@ export class RecordStore {
|
|
|
1127
1885
|
return null;
|
|
1128
1886
|
}
|
|
1129
1887
|
const payloads = readSidecarPayloads(file, stamps);
|
|
1130
|
-
const entry = RecordStore.buildFileCacheEntry(
|
|
1888
|
+
const entry = RecordStore.buildFileCacheEntry(
|
|
1889
|
+
{
|
|
1890
|
+
...hit,
|
|
1891
|
+
forkDepth: undefined,
|
|
1892
|
+
sessionFile: file,
|
|
1893
|
+
// [H2 S3] 显式归一(exactOptionalPropertyTypes:索引可选属性 → recon 必填);
|
|
1894
|
+
// 值已过 loadIndex 守卫(undefined/字面量白名单),直传即安全。
|
|
1895
|
+
origin: hit.origin,
|
|
1896
|
+
parentRunId: hit.parentRunId,
|
|
1897
|
+
},
|
|
1898
|
+
file,
|
|
1899
|
+
stamps,
|
|
1900
|
+
payloads,
|
|
1901
|
+
);
|
|
1131
1902
|
this.fileCache.set(file, entry);
|
|
1132
1903
|
this.idToFile.set(hit.id, file);
|
|
1133
1904
|
return entry;
|
|
@@ -1194,6 +1965,10 @@ export class RecordStore {
|
|
|
1194
1965
|
depth: cached.light.depth,
|
|
1195
1966
|
model: cached.light.model,
|
|
1196
1967
|
thinkingLevel: cached.light.thinkingLevel,
|
|
1968
|
+
// [H2 S3] 来源域随投影入索引:light→索引→重建往返闭合(origin 丢失面与
|
|
1969
|
+
// binding 缺失面互补,索引命中路径不再静默抹掉 workflow 身份)。
|
|
1970
|
+
origin: cached.light.origin,
|
|
1971
|
+
parentRunId: cached.light.parentRunId,
|
|
1197
1972
|
});
|
|
1198
1973
|
}
|
|
1199
1974
|
}
|
|
@@ -1204,12 +1979,13 @@ export class RecordStore {
|
|
|
1204
1979
|
* [perf] byId 索引直查 light record(单文件 stat 校验,不触发 getFullRecord 的
|
|
1205
1980
|
* 全量重建)。idToFile 未热(进程重启后尚未扫描过)时返回 undefined,调用方
|
|
1206
1981
|
* 自行兜底全目录扫描——用于把「跨重启后每条 message 一次 collectRecords 全扫」
|
|
1207
|
-
* 降为 O(1)
|
|
1982
|
+
* 降为 O(1) 索引命中。(反查 miss 的索引自愈不在此层——miss 契约被 cold-lookup
|
|
1983
|
+
* 链与 record-binding 测试钉死;manifest 惰性重建挂 mergedRecords,见其注释。)
|
|
1208
1984
|
*/
|
|
1209
1985
|
findLightById(id: string): SubagentRecord | undefined {
|
|
1210
1986
|
const file = this.idToFile.get(id);
|
|
1211
1987
|
if (!file) return undefined;
|
|
1212
|
-
return this.scanFile(file
|
|
1988
|
+
return this.scanFile(file)?.light;
|
|
1213
1989
|
}
|
|
1214
1990
|
|
|
1215
1991
|
/**
|
|
@@ -1228,19 +2004,15 @@ export class RecordStore {
|
|
|
1228
2004
|
|
|
1229
2005
|
const file = this.idToFile.get(id);
|
|
1230
2006
|
if (!file) return undefined;
|
|
1231
|
-
const entry = this.scanFile(file
|
|
2007
|
+
const entry = this.scanFile(file);
|
|
1232
2008
|
if (!entry) return undefined;
|
|
1233
2009
|
if (entry.full === undefined) {
|
|
1234
2010
|
const recon = reconstructFromFile(file);
|
|
1235
2011
|
if (recon) {
|
|
1236
2012
|
entry.full = RecordStore.buildRecord(recon, {
|
|
1237
|
-
|
|
1238
|
-
finalized: entry.finalized !== null,
|
|
1239
|
-
finalizedReason: entry.finalized !== null ? (entry.finalReason ?? "") : undefined,
|
|
1240
|
-
alive: entry.aliveData,
|
|
2013
|
+
state: entry.stateMarker,
|
|
1241
2014
|
jsonlMtimeMs: entry.jsonl.mtimeMs,
|
|
1242
2015
|
fullEndedAt: recon.endedAt,
|
|
1243
|
-
now: Date.now(),
|
|
1244
2016
|
});
|
|
1245
2017
|
} else {
|
|
1246
2018
|
entry.full = entry.light; // 哨兵:无详情可补,后续直取 light(戳变化时重置重试)
|
|
@@ -1249,46 +2021,27 @@ export class RecordStore {
|
|
|
1249
2021
|
return entry.full;
|
|
1250
2022
|
}
|
|
1251
2023
|
|
|
1252
|
-
/** alive 探活刷新(scanFile 缓存命中与 reconstructAll 快路径共用):
|
|
1253
|
-
* 分支 3 的 running + alive 条目每扫重查 pid(结果不落盘,进程死亡无 IO),
|
|
1254
|
-
* 保留旧实现「每次 collectRecords 重新 isProcessAlive」的语义。 */
|
|
1255
|
-
private static refreshAlive(entry: FileCacheEntry, now: number): void {
|
|
1256
|
-
if (entry.alive === null || entry.light.status !== "running") return;
|
|
1257
|
-
const marker = entry.aliveData;
|
|
1258
|
-
if (!marker) return;
|
|
1259
|
-
const live = isProcessAlive(marker.pid) && now - marker.startedAt < ALIVE_SOFT_TIMEOUT_MS;
|
|
1260
|
-
entry.light.externalInstance = live ? marker : undefined;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
2024
|
/** identity 基底 + sidecar 状态矩阵 → 缓存条目(索引命中与探测重建两分支的公共装配点)。 */
|
|
1264
2025
|
private static buildFileCacheEntry(
|
|
1265
2026
|
base: IdentityHeaderRecon,
|
|
1266
2027
|
file: string,
|
|
1267
2028
|
stamps: FileStamps,
|
|
1268
2029
|
payloads: SidecarPayloads,
|
|
1269
|
-
now: number,
|
|
1270
2030
|
): FileCacheEntry {
|
|
1271
2031
|
return {
|
|
1272
2032
|
light: RecordStore.buildRecord(base, {
|
|
1273
|
-
|
|
1274
|
-
finalized: stamps.finalized !== null,
|
|
1275
|
-
finalizedReason: payloads.finalReason,
|
|
1276
|
-
alive: payloads.aliveData,
|
|
2033
|
+
state: payloads.state,
|
|
1277
2034
|
jsonlMtimeMs: stamps.jsonl.mtimeMs,
|
|
1278
|
-
now,
|
|
1279
2035
|
}),
|
|
1280
2036
|
full: undefined,
|
|
1281
2037
|
jsonl: stamps.jsonl,
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
tomb: payloads.tomb,
|
|
1286
|
-
aliveData: payloads.aliveData,
|
|
1287
|
-
finalReason: payloads.finalReason,
|
|
2038
|
+
state: stamps.state,
|
|
2039
|
+
binding: stamps.binding,
|
|
2040
|
+
stateMarker: payloads.state,
|
|
1288
2041
|
};
|
|
1289
2042
|
}
|
|
1290
2043
|
|
|
1291
|
-
/** identity 基底(头部 light 或全量 recon)+
|
|
2044
|
+
/** identity 基底(头部 light 或全量 recon)+ sidecar 状态矩阵(终态 marker / 兜底两态)→ SubagentRecord。 */
|
|
1292
2045
|
private static buildRecord(
|
|
1293
2046
|
base: IdentityHeaderRecon | ReconstructedRecord,
|
|
1294
2047
|
m: SidecarMatrix,
|
|
@@ -1306,6 +2059,9 @@ export class RecordStore {
|
|
|
1306
2059
|
rootSessionId: base.rootSessionId,
|
|
1307
2060
|
parentRecordId: base.parentRecordId,
|
|
1308
2061
|
depth: base.depth,
|
|
2062
|
+
// [H2 S3] 来源域落位(identity 面已守卫归一):缺省 undefined = "tool" 语义。
|
|
2063
|
+
origin: base.origin,
|
|
2064
|
+
parentRunId: base.parentRunId,
|
|
1309
2065
|
endedAt: undefined,
|
|
1310
2066
|
turns: base.turnCount,
|
|
1311
2067
|
totalTokens: base.totalTokens,
|
|
@@ -1333,6 +2089,9 @@ export class RecordStore {
|
|
|
1333
2089
|
rootSessionId: base.rootSessionId,
|
|
1334
2090
|
parentRecordId: base.parentRecordId,
|
|
1335
2091
|
depth: base.depth,
|
|
2092
|
+
// [H2 S3] 来源域落位(同全量分支)。
|
|
2093
|
+
origin: base.origin,
|
|
2094
|
+
parentRunId: base.parentRunId,
|
|
1336
2095
|
endedAt: undefined,
|
|
1337
2096
|
turns: 0,
|
|
1338
2097
|
totalTokens: 0,
|
|
@@ -1350,39 +2109,35 @@ export class RecordStore {
|
|
|
1350
2109
|
};
|
|
1351
2110
|
}
|
|
1352
2111
|
|
|
1353
|
-
// ── 分支 1: .cancelled
|
|
1354
|
-
|
|
1355
|
-
|
|
2112
|
+
// ── 分支 1: 终态 sidecar(.state 优先,旧 .finalized/.cancelled 兼容归一)──
|
|
2113
|
+
if (m.state !== undefined && m.state.status === "cancelled") {
|
|
2114
|
+
// v4 B-1: cancelled 折入 closed(closedReason='cancelled' 保留 L2 区分)。
|
|
1356
2115
|
markReconstructedStatus(rec, "closed");
|
|
1357
2116
|
rec.closedReason = "cancelled";
|
|
1358
2117
|
rec.error = "cancelled by user";
|
|
1359
|
-
|
|
2118
|
+
// endedAt 用 sidecar 携带的精确值(原 tombstone.endedAt);缺失(新写侧恒携带,
|
|
2119
|
+
// 兼容手写残留)回落全量末 entry ts / light mtime。
|
|
2120
|
+
rec.endedAt = m.state.endedAt ?? m.fullEndedAt ?? m.jsonlMtimeMs;
|
|
1360
2121
|
}
|
|
1361
|
-
// ── 分支 2:
|
|
1362
|
-
else if (m.
|
|
2122
|
+
// ── 分支 2: finalized(同 sidecar,status=finalized)──
|
|
2123
|
+
else if (m.state !== undefined) {
|
|
1363
2124
|
// closed 统一终态:done/failed/crashed 合并为 closed。closedReason 优先用
|
|
1364
2125
|
// sidecar 内容携带的真实原因([v8.5 A2] doFinalizeRecord Step3 写入)。
|
|
1365
2126
|
// 空内容(旧格式空文件 / 未携 reason 的外部写入)→ disconnected 兜底:死因
|
|
1366
2127
|
// 不可考,但「正常结束过」信号仍在——替代旧的误导性 gc 兜底(自然完成 vs
|
|
1367
2128
|
// 断联不分)。非枚举值(外部损坏/手写垃圾内容)同 treated as unknown → disconnected。
|
|
1368
2129
|
markReconstructedStatus(rec, "closed");
|
|
1369
|
-
const reason = m.
|
|
2130
|
+
const reason = m.state.reason?.trim();
|
|
1370
2131
|
rec.closedReason = isValidClosedReason(reason) ? (reason as ClosedReason) : "disconnected";
|
|
1371
2132
|
// 全量路径用最后 entry ts(精确);light 路径用 jsonl mtime 近似(finalize 后
|
|
1372
2133
|
// 文件不再变化,误差 <1s),避免重建后耗时随墙钟无限增长。
|
|
1373
2134
|
rec.endedAt = m.fullEndedAt ?? m.jsonlMtimeMs;
|
|
1374
2135
|
}
|
|
1375
|
-
// ── 分支
|
|
1376
|
-
else if (
|
|
1377
|
-
m.alive !== undefined &&
|
|
1378
|
-
isProcessAlive(m.alive.pid) &&
|
|
1379
|
-
m.now - m.alive.startedAt < ALIVE_SOFT_TIMEOUT_MS
|
|
1380
|
-
) {
|
|
1381
|
-
markReconstructedStatus(rec, "running");
|
|
1382
|
-
rec.externalInstance = m.alive;
|
|
1383
|
-
}
|
|
1384
|
-
// ── 分支 4: 兜底(都无 / .alive 但 pid 死 / 超时)──
|
|
2136
|
+
// ── 分支 4: 兜底(其余一切——无 sidecar / .alive 在持 / pid 死)──
|
|
1385
2137
|
// v4 B-1:跨重启可续聊态落点 = running。endedAt 保持 undefined(非终态)。
|
|
2138
|
+
// [U4a / D3b (a)] 原分支 3(.alive + pid 活 → running + externalInstance 投影)
|
|
2139
|
+
// 已移除:非终态统一落此兜底,探活读面由 (a′)(a″) findForeignLiveInstance
|
|
2140
|
+
// 现查探针承担(防御保留、数据源换新——fork-from 守卫与孤儿恢复跳过)。
|
|
1386
2141
|
else {
|
|
1387
2142
|
markReconstructedStatus(rec, "running");
|
|
1388
2143
|
}
|
|
@@ -1407,7 +2162,10 @@ export class RecordStore {
|
|
|
1407
2162
|
|
|
1408
2163
|
/** FR-8: ManifestRecord → SubagentRecord(manifest 源投影)。
|
|
1409
2164
|
* task/slug/model 从 manifest 真实值投影(配合 writeManifest 补字段),缺失兜底空串。
|
|
1410
|
-
* status 越界(mapManifestStatus 返回 null)时返回 null,由 collectRecords 跳过。
|
|
2165
|
+
* status 越界(mapManifestStatus 返回 null)时返回 null,由 collectRecords 跳过。
|
|
2166
|
+
* [M2 Gate B] closedReason 投影(枚举守卫,同 mapManifestStatus 的越界容错口径):
|
|
2167
|
+
* 旧 manifest 无此字段 / 损坏值 → undefined。缺失曾让 manifest 源快照在
|
|
2168
|
+
* endedMessageGuard 丢失三分流依据(user-close/cancelled 误入 reconnectable 分支)。 */
|
|
1411
2169
|
private static manifestToSubagent(m: ManifestRecord): SubagentRecord | null {
|
|
1412
2170
|
const status = mapManifestStatus(m.status);
|
|
1413
2171
|
if (status === null) return null;
|
|
@@ -1417,6 +2175,7 @@ export class RecordStore {
|
|
|
1417
2175
|
task: m.task ?? "",
|
|
1418
2176
|
slug: m.slug ?? "",
|
|
1419
2177
|
status,
|
|
2178
|
+
closedReason: isValidClosedReason(m.closedReason) ? m.closedReason : undefined,
|
|
1420
2179
|
mode: "background" as const,
|
|
1421
2180
|
startedAt: m.createdAt,
|
|
1422
2181
|
rootSessionId: m.rootSessionId || undefined,
|
|
@@ -1474,10 +2233,16 @@ export class RecordStore {
|
|
|
1474
2233
|
// U2:engineHandle 经 entry 持久化(register/archive 双写点均经本投影),无则 undefined 自然省略
|
|
1475
2234
|
engineHandle: r.engineHandle,
|
|
1476
2235
|
// [U5 修复 U2 披露的投影缺口] 同步收集两字段随本投影持久化(register entry /
|
|
1477
|
-
// archive entry
|
|
1478
|
-
//
|
|
2236
|
+
// archive entry 双写点均经本投影),原缺失时闭合判定 flushBatch 重建、E1 重建扫描等
|
|
2237
|
+
// 消费方读不到原始值。undefined 经 JSON.stringify 自然缺省,旧 entry 零迁移。
|
|
1479
2238
|
collectMode: r.collectMode,
|
|
1480
2239
|
batchFinalized: r.batchFinalized,
|
|
2240
|
+
// [H2 W1] 来源身份两字段随本投影持久化(register/archive/reportRecordTransition
|
|
2241
|
+
// 全部写点均经本投影 → toSubagentRecordEntry)。漏投影则 entry 无 origin,重启后
|
|
2242
|
+
// 重建链拿不到来源、D1 投影过滤全失效(同型先例:H1 U5 缺字段事故)。
|
|
2243
|
+
// undefined 经 JSON.stringify 自然缺省,存量 record 序列化字节不变(零迁移)。
|
|
2244
|
+
origin: r.origin,
|
|
2245
|
+
parentRunId: r.parentRunId,
|
|
1481
2246
|
};
|
|
1482
2247
|
}
|
|
1483
2248
|
}
|