@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
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
// src/execution/state-marker.ts
|
|
2
|
+
//
|
|
3
|
+
// 终态 sidecar 单一入口(`.state`):subagent 收尾时宿主侧写,collectRecords 磁盘
|
|
4
|
+
// 重建时读——判「正常结束过」与死因。
|
|
5
|
+
//
|
|
6
|
+
// 背景(L4 合并):原有两个互斥 sidecar 是同一概念(宿主侧终态标记)的两份形态——
|
|
7
|
+
// `.finalized`(内容 = 可选关闭原因字符串,v8.5 起)与 `.cancelled`(JSON tombstone,
|
|
8
|
+
// 带 id/agent/startedAt/endedAt)。按设计「一个 session 要么 finalized 要么 cancelled」,
|
|
9
|
+
// 形态分叉只因历史写入点不同。现统一为 `<session>.state`:
|
|
10
|
+
//
|
|
11
|
+
// { "status": "finalized" | "cancelled", "reason"?: string, "endedAt"?: number }
|
|
12
|
+
//
|
|
13
|
+
// - finalized:reason = 关闭原因(旧格式空内容 → 空串 = 死因不可考 → 重建兜底
|
|
14
|
+
// closedReason=disconnected,不再误导为 gc);endedAt 不携带(重建用 jsonl 末
|
|
15
|
+
// entry ts / mtime,既有语义不变)。
|
|
16
|
+
// - cancelled:endedAt = 精确结束时间(原 tombstone.endedAt,判定分支消费它);
|
|
17
|
+
// reason 不携带(原 tombstone 其余字段 id/agent/startedAt 无消费方,随合并删除)。
|
|
18
|
+
//
|
|
19
|
+
// 读侧**兼容旧两名**(存量文件不迁移重写):`.state` 优先,缺失时回退 `.finalized`
|
|
20
|
+
// /`.cancelled` 并归一为同一 StateMarker。`.alive`(跨进程写权声明,D3 v7——acquire/
|
|
21
|
+
// release 归口 RecordStore 意图原语)不并入。
|
|
22
|
+
//
|
|
23
|
+
// 写侧语义(record 持久化收敛 §3.4 / D8 v7,U1 改造):**权威同步写 + 响亮重试**——
|
|
24
|
+
// `.state` 是终态判定权威(D1),写失败不再静默:重试 3 次指数退避(100ms 起),
|
|
25
|
+
// 仍失败 logger.error 响亮暴露并返回 false(不抛出,收尾主流程可继续;调用方据
|
|
26
|
+
// 返回值保持 record 不翻终态——record 留 running,boot 孤儿恢复终态化承接)。
|
|
27
|
+
// 迁移已完成(D7):写函数唯一生产调用方 = record-store 内部(意图原语消费返回
|
|
28
|
+
// 值,失败面从旧 best-effort 静默吞错变为响亮留痕 + 显式处置)。
|
|
29
|
+
//
|
|
30
|
+
// [UF-1] record 绑定 sidecar(`.record-binding`)同挂本载体族:宿主侧在 record.sessionFile
|
|
31
|
+
// 回填点写「record id → session 文件」映射(engine-CLI 化后子 session 文件无身份 entry,
|
|
32
|
+
// 旧 PI_SUBAGENT_SELF_RECORD_ID 注入链消失,collectRecords/findLightById 失去 id→file
|
|
33
|
+
// 工件——本 sidecar 是该映射的宿主侧落盘权威)。与终态 sidecar 的关系:
|
|
34
|
+
// - 读侧消费(record-store scanFile):仅当子文件无 identity entry 时用绑定重建身份,
|
|
35
|
+
// status 判定仍走 buildRecord 四分支矩阵——`.state`(终态)优先级天然高于绑定的
|
|
36
|
+
// running 形态,二者并存无冲突(终态后绑定保留:resurrect 回边删 .state 后绑定
|
|
37
|
+
// 仍在,再崩溃仍可恢复);
|
|
38
|
+
// - GC:session-file-gc 的 sidecar 名单已含本扩展名(孤儿绑定随 TTL 清理;jsonl
|
|
39
|
+
// 删除链同步同删——I-16 已销账,GC 领地批次落地)。
|
|
40
|
+
|
|
41
|
+
import * as fs from "node:fs";
|
|
42
|
+
|
|
43
|
+
import { getLogger } from "../core/logger.ts";
|
|
44
|
+
|
|
45
|
+
import type { RecordOrigin } from "./types.ts";
|
|
46
|
+
|
|
47
|
+
const logger = getLogger("subagents");
|
|
48
|
+
|
|
49
|
+
/** 终态 sidecar 扩展名(写侧新名 + 读侧兼容旧名;GC 清理名单与此同源语义)。 */
|
|
50
|
+
export const STATE_SIDECAR_EXT = ".state";
|
|
51
|
+
|
|
52
|
+
/** record 绑定 sidecar 扩展名(UF-1:宿主侧 id→file 映射载体)。 */
|
|
53
|
+
export const RECORD_BINDING_SIDECAR_EXT = ".record-binding";
|
|
54
|
+
const LEGACY_FINALIZED_EXT = ".finalized";
|
|
55
|
+
const LEGACY_CANCELLED_EXT = ".cancelled";
|
|
56
|
+
|
|
57
|
+
// ============================================================
|
|
58
|
+
// 写侧重试参数(§3.4 错误规格)
|
|
59
|
+
// ============================================================
|
|
60
|
+
|
|
61
|
+
/** 写失败重试次数(初始尝试之外再试 3 次)。 */
|
|
62
|
+
const STATE_WRITE_RETRY_COUNT = 3;
|
|
63
|
+
/** 指数退避基准:100ms → 200ms → 400ms(磁盘满/权限错通常是暂时状态,立即放弃
|
|
64
|
+
* 会让「同步写必落」在可恢复故障上静默失效)。 */
|
|
65
|
+
const STATE_WRITE_RETRY_BASE_DELAY_MS = 100;
|
|
66
|
+
|
|
67
|
+
/** 同步退避等待的 SharedArrayBuffer 字长(Atomics.wait 最小载体,单 int32 字)。 */
|
|
68
|
+
const SLEEP_WAIT_INT32_WORDS = 1;
|
|
69
|
+
/** int32 每字 4 字节(Atomics 载体分配换算常数)。 */
|
|
70
|
+
const INT32_BYTES_PER_WORD = 4;
|
|
71
|
+
/** 指数退避底数(100ms → 200ms → 400ms 的倍率来源)。 */
|
|
72
|
+
const BACKOFF_EXPONENT_BASE = 2;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 同步 sleep(重试退避用)。Atomics.wait 是 Node 侧标准同步等待原语:不烧 CPU、
|
|
76
|
+
* 不依赖 event loop——写函数运行在同步收尾路径(disposeAllRecords 等同步链),
|
|
77
|
+
* 无法 await。测试经 _setStateMarkerSleepForTest 注入替身(免真实 700ms 等待,
|
|
78
|
+
* 形态对齐 settled-watchdog `_resetSettledWatchdogsForTest` 模块级测试钩子先例)。
|
|
79
|
+
*/
|
|
80
|
+
function defaultRetrySleep(ms: number): void {
|
|
81
|
+
Atomics.wait(
|
|
82
|
+
new Int32Array(new SharedArrayBuffer(SLEEP_WAIT_INT32_WORDS * INT32_BYTES_PER_WORD)),
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
ms,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let retrySleep: (ms: number) => void = defaultRetrySleep;
|
|
90
|
+
|
|
91
|
+
/** 测试钩子:注入退避替身(fn=undefined 恢复实装)。 */
|
|
92
|
+
export function _setStateMarkerSleepForTest(fn: ((ms: number) => void) | undefined): void {
|
|
93
|
+
retrySleep = fn ?? defaultRetrySleep;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================
|
|
97
|
+
// 类型
|
|
98
|
+
// ============================================================
|
|
99
|
+
|
|
100
|
+
/** 终态二态(互斥)。 */
|
|
101
|
+
export type TerminalState = "finalized" | "cancelled";
|
|
102
|
+
|
|
103
|
+
/** 终态 sidecar 归一形态(新名直读 / 旧名兼容读出共用)。 */
|
|
104
|
+
export interface StateMarker {
|
|
105
|
+
status: TerminalState;
|
|
106
|
+
/** finalized 的关闭原因;空串 = 旧格式空文件(死因不可考);cancelled 恒 undefined。 */
|
|
107
|
+
reason?: string;
|
|
108
|
+
/** cancelled 的精确结束时间;finalized 恒 undefined(重建走 jsonl 末 entry ts)。 */
|
|
109
|
+
endedAt?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** sidecar stat 戳(结构对齐 record-store 的 Stamp——缓存校验用,避免跨模块类型耦合)。 */
|
|
113
|
+
export interface SidecarStat {
|
|
114
|
+
mtimeMs: number;
|
|
115
|
+
size: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// 写侧(唯一入口:两终态共用 .state;权威同步写 + 响亮重试)
|
|
120
|
+
// ============================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 写 finalized 终态 sidecar(权威同步写,§3.4)。
|
|
124
|
+
* 失败重试 3 次指数退避(100ms 起);仍失败 logger.error 响亮暴露并返回 false——
|
|
125
|
+
* 调用方(RecordStore.markFinalized 意图原语)据此保持 record 不翻终态(record 留
|
|
126
|
+
* running,boot 孤儿恢复终态化承接)。不抛出:失败处置经返回值交意图原语编排。
|
|
127
|
+
*
|
|
128
|
+
* @param sessionFile session.jsonl 绝对路径
|
|
129
|
+
* @param reason 可选的关闭原因(磁盘重建用它还原 closedReason)。传 undefined =
|
|
130
|
+
* 空串(死因不可考,重建兜底 disconnected)。
|
|
131
|
+
* @returns true = 已落盘;false = 重试耗尽仍未落(错误已 error 级留痕)。
|
|
132
|
+
*/
|
|
133
|
+
export function writeFinalizedState(sessionFile: string, reason?: string): boolean {
|
|
134
|
+
return writeStateMarker(sessionFile, reason === undefined ? { status: "finalized" } : { status: "finalized", reason });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 写 cancelled 终态 sidecar(权威同步写 + tombstone endedAt,§3.4)。
|
|
139
|
+
* 失败重试语义同 writeFinalizedState(响亮重试,终写失败返回 false 不抛出)。
|
|
140
|
+
*
|
|
141
|
+
* @param endedAt 精确结束时间(重建判定分支消费;调用方传 record.endedAt ?? Date.now())
|
|
142
|
+
* @returns true = 已落盘;false = 重试耗尽仍未落(错误已 error 级留痕)。
|
|
143
|
+
*/
|
|
144
|
+
export function writeCancelledState(sessionFile: string, endedAt: number): boolean {
|
|
145
|
+
return writeStateMarker(sessionFile, { status: "cancelled", endedAt });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* .state 写入 + 旧名清理(互斥由单文件单状态字段构造性保证)。
|
|
150
|
+
* 响亮重试(§3.4):初始尝试 + 3 次指数退避重试(100/200/400ms);仍失败
|
|
151
|
+
* logger.error(终态权威未落必须可见)并返回 false——**不抛出**(收尾路径同步链
|
|
152
|
+
* 不因重试耗尽中断,record 留 running 的处置由意图原语按返回值编排)。
|
|
153
|
+
*/
|
|
154
|
+
function writeStateMarker(sessionFile: string, marker: StateMarker): boolean {
|
|
155
|
+
let lastError: unknown;
|
|
156
|
+
for (let attempt = 0; attempt <= STATE_WRITE_RETRY_COUNT; attempt++) {
|
|
157
|
+
if (attempt > 0) {
|
|
158
|
+
// 指数退避:attempt=1 → 100ms、2 → 200ms、3 → 400ms。
|
|
159
|
+
retrySleep(STATE_WRITE_RETRY_BASE_DELAY_MS * BACKOFF_EXPONENT_BASE ** (attempt - 1));
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
// 旧名清理放在写成功之后(S12 修复):读侧 .state 优先,旧名残留无害(仅多一次
|
|
163
|
+
// stat 与「旧名在 .state 缺失/损坏时充当兼容读序兜底」),删除只是 stat 优化非正确性
|
|
164
|
+
// 依赖——若在写前删而写失败(重试耗尽),存量终态标记已被删而新标记未落,
|
|
165
|
+
// .cancelled tombstone 静默降级为无终态形态(重建回落 running,死因/时间丢失)。
|
|
166
|
+
fs.writeFileSync(`${sessionFile}${STATE_SIDECAR_EXT}`, JSON.stringify(marker), "utf-8");
|
|
167
|
+
// force:true 静默 ENOENT(未写过旧名的 session 正常路径)。
|
|
168
|
+
fs.rmSync(`${sessionFile}${LEGACY_FINALIZED_EXT}`, { force: true });
|
|
169
|
+
fs.rmSync(`${sessionFile}${LEGACY_CANCELLED_EXT}`, { force: true });
|
|
170
|
+
return true;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
lastError = err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
logger.error(
|
|
176
|
+
"[subagents] terminal state marker write failed after retries; record stays running " +
|
|
177
|
+
"(boot orphan recovery will re-finalize). Recovery: free disk/permissions and retry the finalize action.",
|
|
178
|
+
{
|
|
179
|
+
detail: {
|
|
180
|
+
sessionFile,
|
|
181
|
+
markerStatus: marker.status,
|
|
182
|
+
attempts: STATE_WRITE_RETRY_COUNT + 1,
|
|
183
|
+
error: lastError instanceof Error ? lastError.message : String(lastError),
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================
|
|
191
|
+
// 读侧(.state 优先;旧名兼容归一)
|
|
192
|
+
// ============================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 读终态 sidecar(.state 优先,缺失回退旧名)。
|
|
196
|
+
*
|
|
197
|
+
* 旧名回退顺序 **.cancelled → .finalized**:与合并前判定分支的优先级逐点一致
|
|
198
|
+
* (原实现分支 1 = tombstone,分支 2 = finalized;两者共存时 cancelled 胜出——
|
|
199
|
+
* 存量残留文件仍可能出现该形态)。
|
|
200
|
+
*
|
|
201
|
+
* 返回 undefined:三者皆无 / 内容无法判读(旧 .cancelled 损坏时不认 cancelled——
|
|
202
|
+
* 对齐旧语义,避免把损坏残留误判成终态)。
|
|
203
|
+
*
|
|
204
|
+
* 边界对齐(与合并前逐点等价):
|
|
205
|
+
* - `.state` 存在但内容损坏 → {status:"finalized"}(对齐旧 .finalized「存在性即
|
|
206
|
+
* 信号」的宽语义;不误判 cancelled);
|
|
207
|
+
* - 旧 `.finalized` 存在 → reason = 内容 trim(读失败 → undefined → 重建 disconnected);
|
|
208
|
+
* - 旧 `.cancelled` 存在且结构合法 → {status:"cancelled", endedAt}。
|
|
209
|
+
*/
|
|
210
|
+
export function readStateMarker(sessionFile: string): StateMarker | undefined {
|
|
211
|
+
const fromNew = readNewStateMarker(sessionFile);
|
|
212
|
+
if (fromNew !== undefined) return fromNew;
|
|
213
|
+
const fromCancelled = readLegacyCancelledMarker(sessionFile);
|
|
214
|
+
if (fromCancelled !== undefined) return fromCancelled;
|
|
215
|
+
return readLegacyFinalizedMarker(sessionFile);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readNewStateMarker(sessionFile: string): StateMarker | undefined {
|
|
219
|
+
let raw: string;
|
|
220
|
+
try {
|
|
221
|
+
raw = fs.readFileSync(`${sessionFile}${STATE_SIDECAR_EXT}`, "utf-8");
|
|
222
|
+
} catch {
|
|
223
|
+
return undefined; // sidecar 不存在(正常——未终态化的 record 无 sidecar)。
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(raw) as Partial<StateMarker>;
|
|
227
|
+
if (parsed.status === "cancelled") {
|
|
228
|
+
return typeof parsed.endedAt === "number"
|
|
229
|
+
? { status: "cancelled", endedAt: parsed.endedAt }
|
|
230
|
+
: { status: "cancelled" };
|
|
231
|
+
}
|
|
232
|
+
if (parsed.status === "finalized") {
|
|
233
|
+
return typeof parsed.reason === "string"
|
|
234
|
+
? { status: "finalized", reason: parsed.reason }
|
|
235
|
+
: { status: "finalized" };
|
|
236
|
+
}
|
|
237
|
+
return { status: "finalized" }; // 结构不合法 → 存在性信号(降级,不误判 cancelled)。
|
|
238
|
+
} catch {
|
|
239
|
+
return { status: "finalized" }; // JSON 损坏 → 同上。
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function readLegacyFinalizedMarker(sessionFile: string): StateMarker | undefined {
|
|
244
|
+
try {
|
|
245
|
+
const content = fs.readFileSync(`${sessionFile}${LEGACY_FINALIZED_EXT}`, "utf-8").trim();
|
|
246
|
+
return { status: "finalized", reason: content };
|
|
247
|
+
} catch {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function readLegacyCancelledMarker(sessionFile: string): StateMarker | undefined {
|
|
253
|
+
let raw: string;
|
|
254
|
+
try {
|
|
255
|
+
raw = fs.readFileSync(`${sessionFile}${LEGACY_CANCELLED_EXT}`, "utf-8");
|
|
256
|
+
} catch {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const parsed = JSON.parse(raw) as { status?: unknown; endedAt?: unknown };
|
|
261
|
+
if (parsed.status !== "cancelled") return undefined; // 结构不合法 → 降级(对齐旧 readCancelledTombstone)。
|
|
262
|
+
return typeof parsed.endedAt === "number"
|
|
263
|
+
? { status: "cancelled", endedAt: parsed.endedAt }
|
|
264
|
+
: { status: "cancelled" };
|
|
265
|
+
} catch {
|
|
266
|
+
return undefined; // JSON 损坏 → 降级。
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================
|
|
271
|
+
// stat 戳(缓存校验)
|
|
272
|
+
// ============================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 终态 sidecar 的合并 stat 戳(.state + 两个旧名一起取)。
|
|
276
|
+
*
|
|
277
|
+
* 为什么合并:record-store 的 per-file 缓存按戳校验失效——读侧要兼容旧名,若戳只盯
|
|
278
|
+
* `.state`,旧 sidecar 被 GC 删除/内容变化时缓存不失效(读到过期终态)。合并 = 任一
|
|
279
|
+
* 相关文件出现/消失/变化都改变戳(mtime 取最大、size 求和;旧名不再新增,碰撞面仅
|
|
280
|
+
* 限「等量增删」的极端重叠,可接受)。
|
|
281
|
+
*
|
|
282
|
+
* 返回 null:三者皆不存在(未终态化)。
|
|
283
|
+
*/
|
|
284
|
+
export function statStateStamp(sessionFile: string): SidecarStat | null {
|
|
285
|
+
let found = false;
|
|
286
|
+
let mtimeMs = 0;
|
|
287
|
+
let size = 0;
|
|
288
|
+
for (const ext of [STATE_SIDECAR_EXT, LEGACY_FINALIZED_EXT, LEGACY_CANCELLED_EXT]) {
|
|
289
|
+
try {
|
|
290
|
+
const st = fs.statSync(`${sessionFile}${ext}`);
|
|
291
|
+
found = true;
|
|
292
|
+
mtimeMs = Math.max(mtimeMs, st.mtimeMs);
|
|
293
|
+
size += st.size;
|
|
294
|
+
} catch (_e) {
|
|
295
|
+
void _e; // 该扩展名不存在——继续收集其余。
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return found ? { mtimeMs, size } : null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============================================================
|
|
302
|
+
// record 绑定 sidecar(UF-1:宿主侧 id→file 映射的写读)
|
|
303
|
+
// ============================================================
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 绑定 sidecar 载荷(v1)。字段集 = record-store 磁盘重建 light record 所需的
|
|
307
|
+
* 全部身份域(IdentityHeaderRecon 投影源)+ 对话形态域(chatMode/round)。
|
|
308
|
+
* undefined 字段经 JSON.stringify 自然缺省(读侧守卫归一)。
|
|
309
|
+
*/
|
|
310
|
+
export interface RecordBinding {
|
|
311
|
+
/** schema 版本。消费方按 v 判别解析,不认识的版本跳过而非猜测。 */
|
|
312
|
+
v: 1;
|
|
313
|
+
recordId: string;
|
|
314
|
+
/** 根 Pi session id(session 隔离过滤与归属校验用;initSession 前的异常窗口 undefined)。 */
|
|
315
|
+
rootSessionId?: string;
|
|
316
|
+
/** 直接父 record id(跨层归属校验用;顶层 undefined)。 */
|
|
317
|
+
parentRecordId?: string;
|
|
318
|
+
/** 递归深度(顶层 0)。 */
|
|
319
|
+
depth: number;
|
|
320
|
+
agent: string;
|
|
321
|
+
task: string;
|
|
322
|
+
slug: string;
|
|
323
|
+
/** 执行模式(窄字面量跟随 types.ts ExecutionMode 现值;扩展时随 schema v2)。 */
|
|
324
|
+
mode: "background";
|
|
325
|
+
startedAt: number;
|
|
326
|
+
/** 对话形态标志(message 链恢复语义的分流域)。 */
|
|
327
|
+
chatMode: boolean;
|
|
328
|
+
/** 已完成对话轮数(绑定写时点快照;回填点早于轮终 +1,恢复值可滞后一拍)。 */
|
|
329
|
+
round?: number;
|
|
330
|
+
model: string;
|
|
331
|
+
thinkingLevel?: string;
|
|
332
|
+
/** 创建时启用 worktree 隔离(重建面 hadWorktree 恢复源)。 */
|
|
333
|
+
worktree: boolean;
|
|
334
|
+
/**
|
|
335
|
+
* 来源身份(H2 S3 修复:引擎子文件身份面 origin 透传)。undefined(存量 binding)
|
|
336
|
+
* = "tool" 语义,消费方零迁移——engine-CLI 化后子 session 文件无 identity entry,
|
|
337
|
+
* 本 sidecar 是磁盘重建面 origin 过滤(subagents list / TUI overlay)的唯一承载,
|
|
338
|
+
* 漏本字段则归档/重启后 workflow record 逃过投影过滤(Gate B S3 FAIL 根因)。
|
|
339
|
+
*/
|
|
340
|
+
origin?: RecordOrigin;
|
|
341
|
+
/**
|
|
342
|
+
* origin="workflow" 时所属 workflow run id;tool 来源恒缺省。W2/W3 run 视图按
|
|
343
|
+
* collectRecordsByParentRunId 从本字段回查本 run 的 record 集。
|
|
344
|
+
*/
|
|
345
|
+
parentRunId?: string;
|
|
346
|
+
/**
|
|
347
|
+
* 终态 usage 快照([H2 A3],终态写点 Step3a 随 .state 同步更新 binding):
|
|
348
|
+
* totalTokens/turns/endedAt 三字段的 record 终值。light 列表面据此恢复 usage
|
|
349
|
+
* (子文件无 identity entry,全量重建面不可用;round 补投影同款先例)。
|
|
350
|
+
* undefined(存量 binding / 非终态写点)= 不投影(读侧守卫归一)。
|
|
351
|
+
*/
|
|
352
|
+
totalTokens?: number;
|
|
353
|
+
/** 终态 turn 计数快照(见 totalTokens 注)。 */
|
|
354
|
+
turns?: number;
|
|
355
|
+
/** 终态结束时间快照 ms(精确值,优于 light 路径的 jsonl mtime 近似)。 */
|
|
356
|
+
endedAt?: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 写 record 绑定 sidecar(原子写:独占创建 tmp → rename 覆盖目标)。
|
|
361
|
+
*
|
|
362
|
+
* best-effort 记账面:任何 I/O 失败只 warn 不抛——绑定写发生在派发/应答主路径上,
|
|
363
|
+
* 绑定缺失只影响跨重启恢复能力,不得影响当前进程的派发推进。
|
|
364
|
+
*
|
|
365
|
+
* @param sessionFile 子 session.jsonl 绝对路径(绑定目标 = `<sessionFile>.record-binding`)
|
|
366
|
+
*/
|
|
367
|
+
export function writeRecordBinding(sessionFile: string, binding: RecordBinding): void {
|
|
368
|
+
const target = `${sessionFile}${RECORD_BINDING_SIDECAR_EXT}`;
|
|
369
|
+
// tmp 名带 pid:多进程共享 sessionsDir 时互不覆盖;wx 独占创建防同进程残留碰撞。
|
|
370
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
371
|
+
try {
|
|
372
|
+
fs.writeFileSync(tmp, JSON.stringify(binding), { encoding: "utf-8", flag: "wx" });
|
|
373
|
+
fs.renameSync(tmp, target);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
try {
|
|
376
|
+
fs.rmSync(tmp, { force: true });
|
|
377
|
+
} catch (_e) {
|
|
378
|
+
void _e; // tmp 清理失败不追加处理(同为目标目录 IO 故障域)
|
|
379
|
+
}
|
|
380
|
+
logger.warn("[subagents] record binding write failed (best-effort bookkeeping; dispatch unaffected)", {
|
|
381
|
+
detail: {
|
|
382
|
+
sessionFile,
|
|
383
|
+
error: err instanceof Error ? err.message : String(err),
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 读 record 绑定 sidecar。
|
|
391
|
+
*
|
|
392
|
+
* 返回 undefined:文件缺失 / JSON 损坏 / 版本不识别 / 关键身份域缺失或类型非法
|
|
393
|
+
* (recordId/agent/task/mode/startedAt 是重建 light record 的最低要求,残缺载荷
|
|
394
|
+
* 拒绝重建——与 rebuildEntryRecord 的损坏 entry 跳过语义同向,不把损坏残留误判成
|
|
395
|
+
* 可恢复身份)。可选域(rootSessionId/parentRecordId/round/thinkingLevel)类型非法
|
|
396
|
+
* 时归一 undefined,不影响整体判读。
|
|
397
|
+
*/
|
|
398
|
+
export function readRecordBinding(sessionFile: string): RecordBinding | undefined {
|
|
399
|
+
let raw: string;
|
|
400
|
+
try {
|
|
401
|
+
raw = fs.readFileSync(`${sessionFile}${RECORD_BINDING_SIDECAR_EXT}`, "utf-8");
|
|
402
|
+
} catch {
|
|
403
|
+
return undefined; // sidecar 不存在(正常——未回填 sessionFile 的 record 无绑定)
|
|
404
|
+
}
|
|
405
|
+
let parsed: Partial<RecordBinding>;
|
|
406
|
+
try {
|
|
407
|
+
parsed = JSON.parse(raw) as Partial<RecordBinding>;
|
|
408
|
+
} catch {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
const identity = projectValidatedIdentityFields(parsed);
|
|
412
|
+
if (identity === undefined) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
const optional = normalizeOptionalBindingFields(parsed);
|
|
416
|
+
return {
|
|
417
|
+
v: 1,
|
|
418
|
+
...identity,
|
|
419
|
+
rootSessionId: optional.rootSessionId,
|
|
420
|
+
parentRecordId: optional.parentRecordId,
|
|
421
|
+
depth: optional.depth,
|
|
422
|
+
slug: optional.slug,
|
|
423
|
+
chatMode: parsed.chatMode === true,
|
|
424
|
+
round: optional.round,
|
|
425
|
+
model: optional.model,
|
|
426
|
+
thinkingLevel: optional.thinkingLevel,
|
|
427
|
+
worktree: parsed.worktree === true,
|
|
428
|
+
origin: optional.origin,
|
|
429
|
+
parentRunId: optional.parentRunId,
|
|
430
|
+
totalTokens: optional.totalTokens,
|
|
431
|
+
turns: optional.turns,
|
|
432
|
+
endedAt: optional.endedAt,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 身份必填域守卫 + 投影(recordId/agent/task/mode/startedAt 是重建 light record
|
|
438
|
+
* 的最低要求,残缺载荷拒绝重建)。守卫表达式自 readRecordBinding 原样搬移:短路
|
|
439
|
+
* 求值顺序与判读结果逐字节等价(C-data-20 解析权威);校验通过后在本函数内完成
|
|
440
|
+
* 必填域收窄投影(抽成纯布尔守卫会丢失调用方的 TS 属性收窄)。
|
|
441
|
+
*/
|
|
442
|
+
function projectValidatedIdentityFields(
|
|
443
|
+
parsed: Partial<RecordBinding>,
|
|
444
|
+
): Pick<RecordBinding, "recordId" | "agent" | "task" | "mode" | "startedAt"> | undefined {
|
|
445
|
+
if (
|
|
446
|
+
parsed.v !== 1 ||
|
|
447
|
+
typeof parsed.recordId !== "string" ||
|
|
448
|
+
parsed.recordId === "" ||
|
|
449
|
+
typeof parsed.agent !== "string" ||
|
|
450
|
+
typeof parsed.task !== "string" ||
|
|
451
|
+
(parsed.mode !== "background") ||
|
|
452
|
+
typeof parsed.startedAt !== "number"
|
|
453
|
+
) {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
recordId: parsed.recordId,
|
|
458
|
+
agent: parsed.agent,
|
|
459
|
+
task: parsed.task,
|
|
460
|
+
mode: parsed.mode,
|
|
461
|
+
startedAt: parsed.startedAt,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 可选域类型守卫归一(非法/缺省 → 各自缺省值,不影响整体判读)。各守卫表达式自
|
|
467
|
+
* readRecordBinding 原样搬移,逐字段等价。
|
|
468
|
+
*/
|
|
469
|
+
function normalizeOptionalBindingFields(
|
|
470
|
+
parsed: Partial<RecordBinding>,
|
|
471
|
+
): Pick<
|
|
472
|
+
RecordBinding,
|
|
473
|
+
| "rootSessionId"
|
|
474
|
+
| "parentRecordId"
|
|
475
|
+
| "depth"
|
|
476
|
+
| "slug"
|
|
477
|
+
| "round"
|
|
478
|
+
| "model"
|
|
479
|
+
| "thinkingLevel"
|
|
480
|
+
| "origin"
|
|
481
|
+
| "parentRunId"
|
|
482
|
+
| "totalTokens"
|
|
483
|
+
| "turns"
|
|
484
|
+
| "endedAt"
|
|
485
|
+
> {
|
|
486
|
+
return {
|
|
487
|
+
rootSessionId: typeof parsed.rootSessionId === "string" ? parsed.rootSessionId : undefined,
|
|
488
|
+
parentRecordId: typeof parsed.parentRecordId === "string" ? parsed.parentRecordId : undefined,
|
|
489
|
+
depth: typeof parsed.depth === "number" ? parsed.depth : 0,
|
|
490
|
+
slug: typeof parsed.slug === "string" ? parsed.slug : "",
|
|
491
|
+
round: typeof parsed.round === "number" ? parsed.round : undefined,
|
|
492
|
+
model: typeof parsed.model === "string" ? parsed.model : "",
|
|
493
|
+
thinkingLevel: typeof parsed.thinkingLevel === "string" ? parsed.thinkingLevel : undefined,
|
|
494
|
+
// 来源身份两字段(H2 S3):字面量守卫归一(非法/缺省 → undefined = "tool" 语义),
|
|
495
|
+
// 对齐 record-store.readEntryOriginFields 主 entry 重建侧的同名守卫。
|
|
496
|
+
origin:
|
|
497
|
+
parsed.origin === "workflow" || parsed.origin === "tool" ? parsed.origin : undefined,
|
|
498
|
+
parentRunId: typeof parsed.parentRunId === "string" ? parsed.parentRunId : undefined,
|
|
499
|
+
// 终态 usage 快照三字段(H2 A3):number 守卫(非法/缺省 → undefined = 不投影)。
|
|
500
|
+
totalTokens: typeof parsed.totalTokens === "number" ? parsed.totalTokens : undefined,
|
|
501
|
+
turns: typeof parsed.turns === "number" ? parsed.turns : undefined,
|
|
502
|
+
endedAt: typeof parsed.endedAt === "number" ? parsed.endedAt : undefined,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* merge 更新 record 绑定 sidecar([H2 A3] 终态写点专用):读现有 binding → 合并
|
|
508
|
+
* patch → 原子重写。现有 binding 缺失/损坏时**跳过不造新**(updateRecordBinding 不
|
|
509
|
+
* 承担身份创建职责——binding 缺失 = 回填点也未跑过的异常窗口,用部分字段造 binding
|
|
510
|
+
* 会产出残缺身份;writeRecordBinding 才是创建入口)。best-effort 语义同写侧。
|
|
511
|
+
*/
|
|
512
|
+
export function updateRecordBinding(sessionFile: string, patch: Partial<RecordBinding>): void {
|
|
513
|
+
const existing = readRecordBinding(sessionFile);
|
|
514
|
+
if (existing === undefined) return;
|
|
515
|
+
writeRecordBinding(sessionFile, { ...existing, ...patch });
|
|
516
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { ExtensionMode } from "./host-mode.ts";
|
|
20
20
|
import { resolveHostMode } from "./host-mode.ts";
|
|
21
|
-
import { isRelayActive } from "
|
|
21
|
+
import { isRelayActive } from "@zhushanwen/subagent-engine-sdk";
|
|
22
22
|
|
|
23
23
|
/** UI streaming sink 的最小接口(ctx.ui.setWidget 的 duck-typed 子集)。
|
|
24
24
|
*
|