@zhushanwen/subagent-core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原子写原语(sink 设计 U6a / B6):tmp+rename 的单一实现。
|
|
3
|
+
*
|
|
4
|
+
* 背景:core 内部散布 6-7 份 tmp+rename 写点(manifest-store / sessions-index /
|
|
5
|
+
* worktree-registry / engine-discovery / zcode preparer / zcode appserver-home),
|
|
6
|
+
* tmp 命名各异(`.tmp.<pid>` / `.tmp_<pid>_<rand>` / `.tmp-<pid>-<ts>`)、失败路径
|
|
7
|
+
* 清理纪律不齐(worktree-registry 失败时漏清残留 tmp)。本模块给出统一原语供全
|
|
8
|
+
* 部写点收敛(调用点迁移归 u-wire 单元,本单元只建原语)。
|
|
9
|
+
*
|
|
10
|
+
* **统一 tmp 命名约定**:`<最终路径>.tmp.<pid>.<seq>-<rand>`。
|
|
11
|
+
* - `.tmp.` 标记向后兼容 manifest-store 既有扫描(`x.json` 的 tmp 名为
|
|
12
|
+
* `x.json.tmp.…`,命中其 `.json.tmp.` 模式);
|
|
13
|
+
* - pid 防两进程共用同一 tmp,seq+rand 防同进程内并发写同目标共用同一 tmp
|
|
14
|
+
* (对齐 sessions-index tmp 后缀的双重防撞设计)。
|
|
15
|
+
*
|
|
16
|
+
* **失败清理语义**:写入或 rename 失败 → 尽力 unlink 自身 tmp(失败仅 debug
|
|
17
|
+
* 记录)→ 原错误原样上抛(不掩盖、不包装)。rename 成功后 tmp 已不存在,无需
|
|
18
|
+
* 清理。跨进程/跨历史的陈旧残留 tmp 不由写入路径处理——统一走
|
|
19
|
+
* listStaleTmpFiles / cleanupStaleTmpFiles 扫描入口(崩溃残留恢复语义的单点,
|
|
20
|
+
* 见 sink 设计 §4 S6)。
|
|
21
|
+
*
|
|
22
|
+
* **两种耐久档位**(对齐现存两族写点的生产模式):
|
|
23
|
+
* - sync(writeAtomicFileSync):writeFileSync + renameSync,无 fsync——对齐
|
|
24
|
+
* worktree-registry / engine-discovery / zcode preparer / appserver-home 四处
|
|
25
|
+
* 同步写点现状(prep/注册表类,进程崩溃窗口可容忍);
|
|
26
|
+
* - async(writeAtomicFile):fsync 文件 → rename → 尽力 fsync 目录——对齐
|
|
27
|
+
* manifest-store / sessions-index 的生产耐久模式(掉电也不丢已确认写入)。
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
31
|
+
import * as fsPromises from "node:fs/promises";
|
|
32
|
+
import * as path from "node:path";
|
|
33
|
+
|
|
34
|
+
import { getLogger } from "../core/logger.ts";
|
|
35
|
+
|
|
36
|
+
const logger = getLogger("subagents");
|
|
37
|
+
|
|
38
|
+
/** tmp 命名标记:`${最终路径}.tmp.${pid}.${uniq}`。 */
|
|
39
|
+
const TMP_MARKER = ".tmp.";
|
|
40
|
+
|
|
41
|
+
/** 解析约定 tmp 名:贪婪目标段 + `.tmp.` + pid + `.` + uniq(alnum/-)。 */
|
|
42
|
+
const TMP_NAME_PATTERN = /^(.+)\.tmp\.(\d+)\.[0-9A-Za-z-]+$/;
|
|
43
|
+
|
|
44
|
+
/** 同进程内 tmp 单调序号(与随机段联合防并发写同目标撞名)。 */
|
|
45
|
+
let tmpSeq = 0;
|
|
46
|
+
|
|
47
|
+
/** 随机段生成参数:base36 编码去掉「0.x」前缀后取 6 字符。 */
|
|
48
|
+
const RAND_RADIX = 36;
|
|
49
|
+
const RAND_SLICE_START = 2;
|
|
50
|
+
const RAND_SLICE_END = 8;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 目标文件的原子写 tmp 路径(统一约定:`<最终路径>.tmp.<pid>.<seq>-<rand>`)。
|
|
54
|
+
*
|
|
55
|
+
* 独立导出供调用方预告 tmp 名(如崩溃恢复扫描按约定反查)与测试锚定命名形态;
|
|
56
|
+
* 两个 write 原语内部各自调用(每次写独立 tmp,并发写同目标互不串写)。
|
|
57
|
+
*/
|
|
58
|
+
export function atomicTmpPathFor(filePath: string): string {
|
|
59
|
+
tmpSeq += 1;
|
|
60
|
+
const rand = Math.random().toString(RAND_RADIX).slice(RAND_SLICE_START, RAND_SLICE_END);
|
|
61
|
+
return `${filePath}${TMP_MARKER}${process.pid}.${tmpSeq}-${rand}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 约定 tmp 路径的解析视图。 */
|
|
65
|
+
export interface AtomicTmpRef {
|
|
66
|
+
/** tmp 文件自身完整路径。 */
|
|
67
|
+
tmpPath: string;
|
|
68
|
+
/** 该 tmp 写入的目标路径(约定前缀还原)。 */
|
|
69
|
+
targetPath: string;
|
|
70
|
+
/** 创建该 tmp 的进程 pid(扫描方可据此实现存活过滤等策略)。 */
|
|
71
|
+
pid: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 解析约定 tmp 路径为目标视图;非约定形态返回 null。
|
|
76
|
+
*
|
|
77
|
+
* 注意贪婪匹配方向:目标名自身含 `.tmp.` 时(如 `foo.tmp.json.tmp.1-x`)
|
|
78
|
+
* 前缀段正确还原为 `foo.tmp.json`。反之,恰以 `.tmp.<数字>.<uniq>` 结尾的
|
|
79
|
+
* 用户文件会被误认——清理入口只应在受管目录(runtime 数据目录)内使用。
|
|
80
|
+
*/
|
|
81
|
+
export function parseAtomicTmpPath(tmpPath: string): AtomicTmpRef | null {
|
|
82
|
+
const match = TMP_NAME_PATTERN.exec(tmpPath);
|
|
83
|
+
if (match === null) return null;
|
|
84
|
+
return { tmpPath, targetPath: match[1], pid: Number(match[2]) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── 写入原语 ─────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export interface AtomicWriteOptions {
|
|
90
|
+
/** 字符串内容的编码(默认 "utf8";Uint8Array 内容忽略此项)。 */
|
|
91
|
+
encoding?: BufferEncoding;
|
|
92
|
+
/** 目标父目录缺失时递归创建(默认 true,对齐四处同步写点的 mkdir 纪律)。 */
|
|
93
|
+
ensureDir?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AtomicWriteFileOptions extends AtomicWriteOptions {
|
|
97
|
+
/**
|
|
98
|
+
* rename 成功后尽力 fsync 目标目录(默认 true)。POSIX 不要求;失败不否定
|
|
99
|
+
* 已成功的 rename(对齐 manifest-store/sessions-index 的 best-effort 目录
|
|
100
|
+
* fsync)。设 false 跳过(省两次目录句柄开销,弱耐久场景)。
|
|
101
|
+
*/
|
|
102
|
+
fsyncDir?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const DEFAULT_ENCODING: BufferEncoding = "utf8";
|
|
106
|
+
|
|
107
|
+
/** 失败路径清理自身 tmp:尽力 unlink,失败仅 debug(不掩盖原错误)。 */
|
|
108
|
+
function removeTmpBestEffortSync(tmpPath: string): void {
|
|
109
|
+
try {
|
|
110
|
+
unlinkSync(tmpPath);
|
|
111
|
+
} catch (cleanupErr) {
|
|
112
|
+
// tmp 可能已被 rename 消费或从未创建;原错误由调用方上抛
|
|
113
|
+
logger.debug("[subagent-core] atomic-write cleanup tmp failed", {
|
|
114
|
+
detail: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr),
|
|
115
|
+
tmpPath,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 同步原子写(writeFileSync + renameSync,无 fsync)。
|
|
122
|
+
*
|
|
123
|
+
* 读者要么看到旧版完整内容、要么看到新版完整内容,绝无半成品(rename 原子性)。
|
|
124
|
+
* 失败语义见模块头。适用 prep/注册表类高频小文件;需掉电耐久用 writeAtomicFile。
|
|
125
|
+
*/
|
|
126
|
+
export function writeAtomicFileSync(
|
|
127
|
+
filePath: string,
|
|
128
|
+
content: string | Uint8Array,
|
|
129
|
+
options: AtomicWriteOptions = {},
|
|
130
|
+
): void {
|
|
131
|
+
const encoding = options.encoding ?? DEFAULT_ENCODING;
|
|
132
|
+
if (options.ensureDir !== false) {
|
|
133
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
const tmpPath = atomicTmpPathFor(filePath);
|
|
136
|
+
try {
|
|
137
|
+
writeFileSync(tmpPath, content, encoding);
|
|
138
|
+
renameSync(tmpPath, filePath);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// rename 未成功 → 清理残留 tmp(尽力,不掩盖原错误)
|
|
141
|
+
removeTmpBestEffortSync(tmpPath);
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 异步原子写(fsync 文件 → rename → 尽力 fsync 目录)。
|
|
148
|
+
*
|
|
149
|
+
* manifest-store.writeManifest / sessions-index.saveIndex 生产耐久模式的统一
|
|
150
|
+
* 实现。失败语义见模块头。真异步(fs.promises,不阻塞 event loop)。
|
|
151
|
+
*/
|
|
152
|
+
export async function writeAtomicFile(
|
|
153
|
+
filePath: string,
|
|
154
|
+
content: string | Uint8Array,
|
|
155
|
+
options: AtomicWriteFileOptions = {},
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const encoding = options.encoding ?? DEFAULT_ENCODING;
|
|
158
|
+
const fsyncDir = options.fsyncDir ?? true;
|
|
159
|
+
if (options.ensureDir !== false) {
|
|
160
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
const tmpPath = atomicTmpPathFor(filePath);
|
|
163
|
+
const dirPath = path.dirname(filePath);
|
|
164
|
+
|
|
165
|
+
let renamed = false;
|
|
166
|
+
try {
|
|
167
|
+
// 1. 写 tmp → fsync 文件
|
|
168
|
+
const fh = await fsPromises.open(tmpPath, "w");
|
|
169
|
+
try {
|
|
170
|
+
await fh.writeFile(content, encoding);
|
|
171
|
+
await fh.sync();
|
|
172
|
+
} finally {
|
|
173
|
+
await fh.close();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 2. rename tmp → final(放在 try 内:失败时 catch 清理 tmp)
|
|
177
|
+
await fsPromises.rename(tmpPath, filePath);
|
|
178
|
+
renamed = true;
|
|
179
|
+
|
|
180
|
+
// 3. fsync 目录(尽力:POSIX 不要求,失败不否定已成功的 rename)
|
|
181
|
+
if (fsyncDir) {
|
|
182
|
+
try {
|
|
183
|
+
const dirFh = await fsPromises.open(dirPath, "r");
|
|
184
|
+
try {
|
|
185
|
+
await dirFh.sync();
|
|
186
|
+
} finally {
|
|
187
|
+
await dirFh.close();
|
|
188
|
+
}
|
|
189
|
+
} catch (dirSyncErr) {
|
|
190
|
+
logger.debug("[subagent-core] atomic-write fsync dir failed", {
|
|
191
|
+
detail: dirSyncErr instanceof Error ? dirSyncErr.message : String(dirSyncErr),
|
|
192
|
+
dirPath,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// rename 未成功 → 清理残留 tmp(尽力,不掩盖原错误)
|
|
198
|
+
if (!renamed) {
|
|
199
|
+
try {
|
|
200
|
+
await fsPromises.unlink(tmpPath);
|
|
201
|
+
} catch (cleanupErr) {
|
|
202
|
+
logger.debug("[subagent-core] atomic-write cleanup tmp failed", {
|
|
203
|
+
detail: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr),
|
|
204
|
+
tmpPath,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── 崩溃残留扫描 / 清理(S6 统一恢复语义的单点)──────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 扫描目录内全部约定形态的 tmp 文件(不做删除)。
|
|
216
|
+
*
|
|
217
|
+
* 供两类消费方:
|
|
218
|
+
* - cleanupStaleTmpFiles 的内部步骤;
|
|
219
|
+
* - 需要按域校验内容再决定「删 or 提升为正式文件」的宿主恢复逻辑
|
|
220
|
+
* (manifest-store.recoverTmpFiles 模式:tmp 合法且目标缺失 → rename 提升)。
|
|
221
|
+
*
|
|
222
|
+
* 目录不存在 → 返回空数组(恢复扫描对未初始化布局宽容)。非约定形态文件
|
|
223
|
+
* 一律不认(用户数据零误伤边界见 parseAtomicTmpPath 注释)。
|
|
224
|
+
*/
|
|
225
|
+
export function listStaleTmpFiles(dir: string): AtomicTmpRef[] {
|
|
226
|
+
let names: string[];
|
|
227
|
+
try {
|
|
228
|
+
names = readdirSync(dir);
|
|
229
|
+
} catch (readdirErr) {
|
|
230
|
+
if (typeof (readdirErr as NodeJS.ErrnoException).code === "string" &&
|
|
231
|
+
(readdirErr as NodeJS.ErrnoException).code === "ENOENT") {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
throw readdirErr;
|
|
235
|
+
}
|
|
236
|
+
const refs: AtomicTmpRef[] = [];
|
|
237
|
+
for (const name of names) {
|
|
238
|
+
// 以 join 后完整路径解析:targetPath 需带目录前缀(恢复提升 rename 用)
|
|
239
|
+
const ref = parseAtomicTmpPath(path.join(dir, name));
|
|
240
|
+
if (ref !== null) {
|
|
241
|
+
refs.push(ref);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return refs;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface CleanupStaleTmpOptions {
|
|
248
|
+
/**
|
|
249
|
+
* 只清理 mtime 早于 now - maxAgeMs 的残留(进程内在途写入的兜底保护窗口)。
|
|
250
|
+
* 缺省 = 全清(启动期恢复场景,目录归本进程管)。需按 pid 存活过滤的调用方
|
|
251
|
+
* 改用 listStaleTmpFiles 自行实现策略(ref.pid 可判活)。
|
|
252
|
+
*/
|
|
253
|
+
maxAgeMs?: number;
|
|
254
|
+
/** maxAgeMs 的基准时刻(缺省 Date.now();测试注入确定性时钟)。 */
|
|
255
|
+
now?: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface CleanupStaleTmpResult {
|
|
259
|
+
/** 已删除(含扫描与删除之间已消失的——幂等终态)。 */
|
|
260
|
+
removed: string[];
|
|
261
|
+
/** 因 maxAgeMs 窗口内被保留的(疑似他方在途写入)。 */
|
|
262
|
+
kept: string[];
|
|
263
|
+
/** unlink 失败的(尽力语义,错误已 debug 记录)。 */
|
|
264
|
+
failed: string[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 清理目录内约定形态的 tmp 残留(单条失败不阻断其余条目,逐条结果回传)。
|
|
269
|
+
*
|
|
270
|
+
* 恢复语义(对齐 manifest-store.recoverTmpFiles 的删除分支泛化):本函数只做
|
|
271
|
+
* 「删除」级恢复;「校验后提升为正式文件」需域知识(manifest 记录合法性),
|
|
272
|
+
* 由调用方基于 listStaleTmpFiles + parseAtomicTmpPath().targetPath 自行实现。
|
|
273
|
+
*/
|
|
274
|
+
export function cleanupStaleTmpFiles(
|
|
275
|
+
dir: string,
|
|
276
|
+
options: CleanupStaleTmpOptions = {},
|
|
277
|
+
): CleanupStaleTmpResult {
|
|
278
|
+
const now = options.now ?? Date.now();
|
|
279
|
+
const result: CleanupStaleTmpResult = { removed: [], kept: [], failed: [] };
|
|
280
|
+
for (const ref of listStaleTmpFiles(dir)) {
|
|
281
|
+
if (options.maxAgeMs !== undefined) {
|
|
282
|
+
try {
|
|
283
|
+
const mtimeMs = statSync(ref.tmpPath).mtimeMs;
|
|
284
|
+
if (now - mtimeMs < options.maxAgeMs) {
|
|
285
|
+
result.kept.push(ref.tmpPath);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
} catch (statErr) {
|
|
289
|
+
if (typeof (statErr as NodeJS.ErrnoException).code === "string" &&
|
|
290
|
+
(statErr as NodeJS.ErrnoException).code === "ENOENT") {
|
|
291
|
+
// 扫描与 stat 之间已消失(他方收尾)——按已删除的幂等终态记账
|
|
292
|
+
result.removed.push(ref.tmpPath);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
logger.debug("[subagent-core] cleanupStaleTmpFiles stat failed", {
|
|
296
|
+
detail: statErr instanceof Error ? statErr.message : String(statErr),
|
|
297
|
+
tmpPath: ref.tmpPath,
|
|
298
|
+
});
|
|
299
|
+
result.failed.push(ref.tmpPath);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
unlinkSync(ref.tmpPath);
|
|
305
|
+
result.removed.push(ref.tmpPath);
|
|
306
|
+
} catch (unlinkErr) {
|
|
307
|
+
if (typeof (unlinkErr as NodeJS.ErrnoException).code === "string" &&
|
|
308
|
+
(unlinkErr as NodeJS.ErrnoException).code === "ENOENT") {
|
|
309
|
+
result.removed.push(ref.tmpPath);
|
|
310
|
+
} else {
|
|
311
|
+
logger.debug("[subagent-core] cleanupStaleTmpFiles unlink failed", {
|
|
312
|
+
detail: unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr),
|
|
313
|
+
tmpPath: ref.tmpPath,
|
|
314
|
+
});
|
|
315
|
+
result.failed.push(ref.tmpPath);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bounded JSON pretty 序列化原语(sink 设计 U6a / B7)。
|
|
3
|
+
*
|
|
4
|
+
* 来源:自 pi-sw(extensions/universal/subagent-workflow)interface/helpers.ts
|
|
5
|
+
* 的同名私有函数**逐字平移**(函数体逐字符一致,仅补 export)——输出与该实现
|
|
6
|
+
* 字节级一致,由 `__tests__/bounded-serialize.test.ts` 的等价锚定 + 硬编码字节
|
|
7
|
+
* 快照断言守护。pi-sw 本地实现删除与消费切换归 u-sw-misc 单元(设计 U11)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ── bounded pretty 序列化(IF13/#19,TC5/ES5)────────────────
|
|
11
|
+
|
|
12
|
+
const JSON_INDENT = 2;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* bounded JSON pretty 序列化:只生成会被保留的前缀(≤8000 输出与
|
|
16
|
+
* JSON.stringify(value, null, JSON_INDENT) 逐字节一致;>8000 输出与
|
|
17
|
+
* 全量序列化后 .slice(0, budget) + "\n... (truncated)" 逐字节一致——
|
|
18
|
+
* 等价测试锚定,见 __tests__/bounded-serialize.test.ts)。
|
|
19
|
+
*
|
|
20
|
+
* 现状成本:全量 stringify 产生数 MB 中间串再丢弃 99%;本函数在输出字符流
|
|
21
|
+
* 越过 budget 后停止一切生成。保真策略:原语(string/number/boolean/null/bigint)
|
|
22
|
+
* 逐值复用 JSON.stringify(转义/Unicode/数字格式原生一致,零重实现),仅结构
|
|
23
|
+
* 拼装(2 空格缩进/逗号/括号)自实现。
|
|
24
|
+
*
|
|
25
|
+
* 特殊值语义(TC5 a-d,与原生 JSON.stringify 全值域对齐):
|
|
26
|
+
* (a) 含 toJSON 的对象 → 该子树整体走一次 JSON.stringify(subtree)(原生会先调
|
|
27
|
+
* toJSON,如 Date 输出带引号序列化串;bounded 拼装展开会与原生不同)。注意
|
|
28
|
+
* 边界:toJSON 返回对象时原生 pretty 会按缩进展开,本实现按原语串接其
|
|
29
|
+
* compact 形态——设计 TC5 锚定面为 Date 型(返回字符串)toJSON。
|
|
30
|
+
* (b) 任何 stringify 抛出(BigInt → TypeError)→ 整体回退 String(value)(对齐
|
|
31
|
+
* 旧实现整体 try/catch 的整串回退;禁止逐节点回退——输出形态与整串回退不同)。
|
|
32
|
+
* (c) 对象属性值为 undefined/function/symbol → 拼装时跳过(原生省略)。
|
|
33
|
+
* (d) 数组元素为 undefined/function/symbol → 序列化为 "null"(原生行为)。
|
|
34
|
+
*
|
|
35
|
+
* 截断边界(输出字符流层级,与对完整串 slice 逐字节等价):逐段 append 时本段
|
|
36
|
+
* 会使总长越过 budget → 只 append 本段前 (budget - 已累积) 字符(恰好 budget,
|
|
37
|
+
* 可切在转义序列/括号中间,不补任何结构闭合);越过(exceeded)才追加
|
|
38
|
+
* "\n... (truncated)" 标记——恰好 ===budget 不加(> 判定)。
|
|
39
|
+
*
|
|
40
|
+
* 祖先 Set 循环引用守卫:命中 → 整体回退 String(value)(同 (b) 整串回退语义)。
|
|
41
|
+
*/
|
|
42
|
+
export function boundedPrettySerialize(value: unknown, budget: number): string {
|
|
43
|
+
const ancestors = new Set<object>();
|
|
44
|
+
let out = "";
|
|
45
|
+
let exceeded = false;
|
|
46
|
+
|
|
47
|
+
/** 逐段追加:越过 budget 时截到恰好 budget 并置 exceeded(后续生成全部剪枝)。 */
|
|
48
|
+
function append(s: string): void {
|
|
49
|
+
if (exceeded) return;
|
|
50
|
+
if (out.length + s.length <= budget) {
|
|
51
|
+
out += s;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
out += s.slice(0, budget - out.length);
|
|
55
|
+
exceeded = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 数组分支:元素逐个缩进序列化((d) undefined/function/symbol → "null")。 */
|
|
59
|
+
function serializeArray(
|
|
60
|
+
arr: unknown[],
|
|
61
|
+
depth: number,
|
|
62
|
+
childIndent: string,
|
|
63
|
+
closeIndent: string,
|
|
64
|
+
): void {
|
|
65
|
+
if (arr.length === 0) {
|
|
66
|
+
append("[]");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
append("[\n");
|
|
70
|
+
for (let i = 0; i < arr.length && !exceeded; i++) {
|
|
71
|
+
if (i > 0) append(",\n");
|
|
72
|
+
append(childIndent);
|
|
73
|
+
const el = arr[i];
|
|
74
|
+
// (d) 数组元素 undefined/function/symbol → "null"
|
|
75
|
+
if (el === undefined || typeof el === "function" || typeof el === "symbol") {
|
|
76
|
+
append("null");
|
|
77
|
+
} else {
|
|
78
|
+
serialize(el, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
append("\n" + closeIndent + "]");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 对象分支:(c) 属性 undefined/function/symbol 跳过后逐对序列化。 */
|
|
85
|
+
function serializeObject(
|
|
86
|
+
obj: object,
|
|
87
|
+
depth: number,
|
|
88
|
+
childIndent: string,
|
|
89
|
+
closeIndent: string,
|
|
90
|
+
): void {
|
|
91
|
+
// (c) 对象属性 undefined/function/symbol 跳过(Object.entries 同原生:
|
|
92
|
+
// 只取 own enumerable,symbol 键天然不出现;getter 求值语义与原生一致)
|
|
93
|
+
const entries = Object.entries(obj).filter(
|
|
94
|
+
([, val]) => val !== undefined && typeof val !== "function" && typeof val !== "symbol",
|
|
95
|
+
);
|
|
96
|
+
if (entries.length === 0) {
|
|
97
|
+
append("{}");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
append("{\n");
|
|
101
|
+
for (let i = 0; i < entries.length && !exceeded; i++) {
|
|
102
|
+
if (i > 0) append(",\n");
|
|
103
|
+
append(childIndent + JSON.stringify(entries[i][0]) + ": ");
|
|
104
|
+
serialize(entries[i][1], depth + 1);
|
|
105
|
+
}
|
|
106
|
+
append("\n" + closeIndent + "}");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function serialize(v: unknown, depth: number): void {
|
|
110
|
+
if (exceeded) return;
|
|
111
|
+
const t = typeof v;
|
|
112
|
+
// 原语逐值复用原生序列化;bigint 的 JSON.stringify 抛 TypeError → 顶层 catch
|
|
113
|
+
// 整体回退 (b);NaN/Infinity → "null"(原生行为)
|
|
114
|
+
if (v === null || t === "string" || t === "number" || t === "boolean" || t === "bigint") {
|
|
115
|
+
append(JSON.stringify(v));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (t === "object" && v !== null) {
|
|
119
|
+
const obj = v as object;
|
|
120
|
+
if (ancestors.has(obj)) {
|
|
121
|
+
// 循环引用 → 整体回退 String(value)(旧实现同款整串回退)
|
|
122
|
+
throw new TypeError("circular reference");
|
|
123
|
+
}
|
|
124
|
+
// (a) toJSON 子树整体 stringify(Reflect.get 取属性,避免不安全断言)
|
|
125
|
+
if (typeof Reflect.get(obj, "toJSON") === "function") {
|
|
126
|
+
append(JSON.stringify(obj));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
ancestors.add(obj);
|
|
130
|
+
const childIndent = " ".repeat(JSON_INDENT * (depth + 1));
|
|
131
|
+
const closeIndent = " ".repeat(JSON_INDENT * depth);
|
|
132
|
+
if (Array.isArray(obj)) {
|
|
133
|
+
serializeArray(obj as unknown[], depth, childIndent, closeIndent);
|
|
134
|
+
} else {
|
|
135
|
+
serializeObject(obj, depth, childIndent, closeIndent);
|
|
136
|
+
}
|
|
137
|
+
ancestors.delete(obj);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// 顶层 undefined/function/symbol(调用方守卫后不可达;防御兜底对齐原生 "null")
|
|
141
|
+
append(JSON.stringify(v) ?? "null");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
serialize(value, 0);
|
|
146
|
+
return exceeded ? out + "\n... (truncated)" : out;
|
|
147
|
+
} catch {
|
|
148
|
+
// (b) 整体回退:String(value)(超 budget 仍截断 + 标记,与旧实现截断路径一致)
|
|
149
|
+
const fallback = String(value);
|
|
150
|
+
return fallback.length > budget
|
|
151
|
+
? fallback.slice(0, budget) + "\n... (truncated)"
|
|
152
|
+
: fallback;
|
|
153
|
+
}
|
|
154
|
+
}
|