@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
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
import { getLogger } from "../core/logger.ts";
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DialogGlobalQueue,
|
|
25
|
+
isValidDialogTimeout,
|
|
26
|
+
type UiRequest,
|
|
27
|
+
type UiRequestHandler,
|
|
28
|
+
type UiResponse,
|
|
29
|
+
} from "./dialog-queue.ts";
|
|
24
30
|
import { type HostMode, resolveHostMode } from "./host-mode.ts";
|
|
25
31
|
import type { UiChannelRegistry } from "./ui-channels.ts";
|
|
26
32
|
import { isDialogMethod } from "./ui-interaction-model.ts";
|
|
@@ -157,10 +163,27 @@ function coerceUiResponse(raw: unknown, reqId: string): UiResponse {
|
|
|
157
163
|
* input(title: string, placeholder?: string, opts?): Promise<string | undefined>
|
|
158
164
|
* editor(title: string, prefill?: string): Promise<string | undefined>
|
|
159
165
|
* 注意:SDK 是位置参数(非对象参数);返回 undefined 表示用户取消。
|
|
166
|
+
* opts 为 ExtensionUIDialogOptions(signal?/timeout?,timeout 毫秒,SDK auto-dismiss
|
|
167
|
+
* 带倒计时展示)——LC-3/T2⑦ 协同:请求方显式传 timeout 时透传给 SDK,SDK 层超时
|
|
168
|
+
* (正常路径,用户可见倒计时)先于队列级兜底 settle;队列级上界(dialog-queue,
|
|
169
|
+
* 未传挂 30min 默认)兜住 SDK timeout 未生效的形态(host 实现不完整 / channel handler
|
|
170
|
+
* 路径不经 SDK)。两层取先到,settle 恰一次由 DialogGlobalQueue 的 settled 标志保证。
|
|
171
|
+
* editor 的 SDK 签名无 opts,不透传(其上界只由队列级兜底承担)。
|
|
160
172
|
*
|
|
161
173
|
* Stage 4 风险点:TUI 下 ask_user channel 未注册时,select 会以普通列表渲染
|
|
162
174
|
* (title 可能含 marker)。channel 注册后由 createRealHandler 优先走 channel handler,
|
|
163
175
|
* 不进这里。editor 不可用/抛错时降级 cancelled + warn,不卡队列。 */
|
|
176
|
+
/** select/confirm/input 的 SDK 第三参(opts):请求方传了**合法** timeout(>0 且有限,
|
|
177
|
+
* 判定源 = dialog-queue 的 isValidDialogTimeout,与队列层上界同一权威)才透传——
|
|
178
|
+
* 未传或非法(0/负数/NaN/±Infinity)时不加第三参,参数个数与旧调用完全一致(严格
|
|
179
|
+
* additive)。非法值透传会让 SDK 收到语义反转的 opts(<=0 可能立即 auto-dismiss),
|
|
180
|
+
* 且与队列层「非法回落默认上界」的判定不一致(A2-3);不透传时数组为空,SDK 无界
|
|
181
|
+
* 由队列层默认上界(30min)兜底。
|
|
182
|
+
* 返回元组联合以支持 spread 到固定签名(TS2556)。 */
|
|
183
|
+
function sdkTimeoutArg(req: UiRequest): [] | [{ timeout: number }] {
|
|
184
|
+
return isValidDialogTimeout(req.timeout) ? [{ timeout: req.timeout }] : [];
|
|
185
|
+
}
|
|
186
|
+
|
|
164
187
|
async function defaultDialogForward(
|
|
165
188
|
req: UiRequest,
|
|
166
189
|
ctx: HostUIContext,
|
|
@@ -168,16 +191,16 @@ async function defaultDialogForward(
|
|
|
168
191
|
const ui = ctx.ui;
|
|
169
192
|
switch (req.method) {
|
|
170
193
|
case "select": {
|
|
171
|
-
const selected = await ui.select(req.title ?? "", req.options ?? []);
|
|
194
|
+
const selected = await ui.select(req.title ?? "", req.options ?? [], ...sdkTimeoutArg(req));
|
|
172
195
|
return selected === undefined ? { cancelled: true } : { value: selected };
|
|
173
196
|
}
|
|
174
197
|
case "confirm": {
|
|
175
198
|
// SDK confirm 必传 message(req.message 缺失时降级空串,不报错阻塞)
|
|
176
|
-
const confirmed = await ui.confirm(req.title ?? "", req.message ?? "");
|
|
199
|
+
const confirmed = await ui.confirm(req.title ?? "", req.message ?? "", ...sdkTimeoutArg(req));
|
|
177
200
|
return { confirmed };
|
|
178
201
|
}
|
|
179
202
|
case "input": {
|
|
180
|
-
const text = await ui.input(req.title ?? "", req.placeholder);
|
|
203
|
+
const text = await ui.input(req.title ?? "", req.placeholder, ...sdkTimeoutArg(req));
|
|
181
204
|
return text === undefined ? { cancelled: true } : { value: text };
|
|
182
205
|
}
|
|
183
206
|
case "editor": {
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// src/execution/worktree-git-ops.ts
|
|
2
|
+
//
|
|
3
|
+
// worktree git 内核纯函数(sink 设计 U5 / D5 裁决全量)。
|
|
4
|
+
// 设计权威源:docs/design/subagent-core-sink-design.md §3.3 D5 + 错误规格表
|
|
5
|
+
// collectWorktreePatch 两行 + §5.4 ⛔3。
|
|
6
|
+
//
|
|
7
|
+
// 提取源:execution/worktree-manager.ts(gitRunAsync / GitRunError / SAFE_ID_RE /
|
|
8
|
+
// collectPatch 的 add+diff 基线机制 / cleanup 三步容错 / create 的脏树校验)。
|
|
9
|
+
// 本模块是 git 语义新单源;worktree-manager.ts 的同语义内联实现待其收缩单元切换
|
|
10
|
+
// 消费(本单元领地约束不改提取源,两处短暂并存由各自测试锚定等价)。
|
|
11
|
+
//
|
|
12
|
+
// [D5 基线锚点抽象] collectWorktreePatch 的基线二选一注入:
|
|
13
|
+
// - { kind: "commit" } 内存 baseCommit(core WorktreeHandle.baseCommit,本就
|
|
14
|
+
// 持久化于 worktrees.json 注册表——worktree-manager.ts create() 注册路径)
|
|
15
|
+
// - { kind: "anchor-file" } 宿主持久锚点文件(zsw sidecar 语义,内容 = base commit)
|
|
16
|
+
//
|
|
17
|
+
// [⛔3 两条降级路径显式化](不做 fail-fast——任务已完成的执行工作不应因 patch
|
|
18
|
+
// 收集作废;也不做纯静默——静默丢已提交改动使上层拿到残缺 patch 无法察觉):
|
|
19
|
+
// ① 锚点缺失/损坏 → 显著 warn + 降级裸 diff(仅未提交改动)+ patchIncomplete: true
|
|
20
|
+
// ② add 步骤失败 → 非致命继续 diff(裸 diff)+ warn + patchIncomplete: true
|
|
21
|
+
// 降级裸 diff 以 HEAD 为基线:较 zsw 现状 `diff <base>` 多丢已提交改动,损失面差异
|
|
22
|
+
// 由 patchIncomplete 留痕判断(D5 原文裁决,非等值平移)。
|
|
23
|
+
//
|
|
24
|
+
// 目录布局与孤儿判定策略留宿主(D5);per-repo 写命令串行队列是宿主/manager 编排
|
|
25
|
+
// 职责,不在纯内核复刻——git index.lock 冲突瞬时态正好落降级路径②,而非内核故障。
|
|
26
|
+
|
|
27
|
+
import { execFile } from "node:child_process";
|
|
28
|
+
import * as fs from "node:fs";
|
|
29
|
+
|
|
30
|
+
import { getLogger } from "../core/logger.ts";
|
|
31
|
+
import { bestEffort } from "./best-effort.ts";
|
|
32
|
+
import { DirtyWorktreeError } from "./types.ts";
|
|
33
|
+
|
|
34
|
+
const logger = getLogger("subagents");
|
|
35
|
+
|
|
36
|
+
// 默认 git 命令超时(ms),对齐提取源 worktree-manager.ts
|
|
37
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* gitRun 的包装错误:message 格式与提取源 gitRunAsync 逐字一致;exitCode/stderr/
|
|
41
|
+
* timedOut 为诊断属性(P-errshape 实测 Node 24:execFile 的 err.stderr 为
|
|
42
|
+
* undefined——stderr 在 callback 第三参;退出码在 err.code,数字时)。
|
|
43
|
+
*/
|
|
44
|
+
export class GitRunError extends Error {
|
|
45
|
+
readonly exitCode?: number;
|
|
46
|
+
readonly stderr?: string;
|
|
47
|
+
readonly timedOut?: boolean;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
message: string,
|
|
51
|
+
props: { exitCode?: number; stderr?: string; timedOut?: boolean },
|
|
52
|
+
) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "GitRunError";
|
|
55
|
+
this.exitCode = props.exitCode;
|
|
56
|
+
this.stderr = props.stderr;
|
|
57
|
+
this.timedOut = props.timedOut;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// recordId 白名单:字母数字下划线短横线(防路径注入;与提取源 SAFE_ID_RE 同式)
|
|
62
|
+
export const SAFE_ID_RE = /^[\w-]+$/;
|
|
63
|
+
|
|
64
|
+
/** recordId 是否匹配安全白名单 `^[\w-]+$`。 */
|
|
65
|
+
export function isSafeId(id: string): boolean {
|
|
66
|
+
return SAFE_ID_RE.test(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 断言 recordId 匹配安全白名单,不合法抛 DirtyWorktreeError(与提取源 create()
|
|
71
|
+
* 的拒绝语义一致,供 manager 收缩后无缝切换)。
|
|
72
|
+
*
|
|
73
|
+
* @throws DirtyWorktreeError(消息含白名单说明,可操作)
|
|
74
|
+
*/
|
|
75
|
+
export function assertSafeId(id: string, label = "recordId"): void {
|
|
76
|
+
if (!SAFE_ID_RE.test(id)) {
|
|
77
|
+
throw new DirtyWorktreeError(
|
|
78
|
+
`${label} contains unsafe characters: "${id}" (must match ^[\\w-]+$)`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* dirty 谓词:`git status --porcelain` 输出 trim 后非空即脏树。
|
|
85
|
+
* 提取源 create() 的内联判定(「消费点自行 trim」的干净文本消费点之一)。
|
|
86
|
+
*/
|
|
87
|
+
export function isTreeDirty(statusPorcelain: string): boolean {
|
|
88
|
+
return statusPorcelain.trim().length > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* git 命令执行器。stdout 保真返回(不 trim):diff 输出原样落盘为 patch 文件,
|
|
93
|
+
* 裁掉尾换行会产出 `git apply` 拒绝的 corrupt patch(worktree-manager.ts 头注释
|
|
94
|
+
* 2026-08-16 门 4 实测)。需要干净文本的消费点自行 trim。
|
|
95
|
+
*
|
|
96
|
+
* maxBuffer(execFile stdout 上限):不传 = Node execFile 缺省 1MB(1024 * 1024),
|
|
97
|
+
* 超限以 ERR_CHILD_PROCESS_STDIO_MAXBUFFER 失败(reject GitRunError)。大输出命令
|
|
98
|
+
* (如批量重构的大 diff)由调用方显式提高,宿主建议 32 * 1024 * 1024(对齐旧 zsw
|
|
99
|
+
* 宿主 GIT_MAX_BUFFER)。以条件展开实现而非 `maxBuffer: opts.maxBuffer` 直透:
|
|
100
|
+
* Node 先铺缺省再展开 options,显式 undefined 会覆盖缺省为无界(实测探针),
|
|
101
|
+
* 与「不传即 1MB 缺省」相悖。
|
|
102
|
+
*
|
|
103
|
+
* 失败 reject GitRunError。无 per-repo 写串行(编排职责留宿主,见文件头注释)。
|
|
104
|
+
*/
|
|
105
|
+
export function gitRun(
|
|
106
|
+
args: string[],
|
|
107
|
+
opts: { cwd: string; timeout?: number; maxBuffer?: number },
|
|
108
|
+
): Promise<string> {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
execFile(
|
|
111
|
+
"git",
|
|
112
|
+
args,
|
|
113
|
+
{
|
|
114
|
+
cwd: opts.cwd,
|
|
115
|
+
timeout: opts.timeout ?? GIT_TIMEOUT_MS,
|
|
116
|
+
...(opts.maxBuffer !== undefined ? { maxBuffer: opts.maxBuffer } : {}),
|
|
117
|
+
encoding: "utf-8",
|
|
118
|
+
},
|
|
119
|
+
(err, stdout, stderr) => {
|
|
120
|
+
if (err) {
|
|
121
|
+
const execErr = err as Error & { code?: unknown; killed?: boolean; signal?: string };
|
|
122
|
+
reject(
|
|
123
|
+
new GitRunError(`git ${args[0]} failed: ${execErr.message}`, {
|
|
124
|
+
exitCode: typeof execErr.code === "number" ? execErr.code : undefined,
|
|
125
|
+
stderr: typeof stderr === "string" ? stderr : undefined,
|
|
126
|
+
timedOut: execErr.killed === true && execErr.signal === "SIGTERM",
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
resolve(stdout);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// collectWorktreePatch(D5 统一 add + diff 基线机制)
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
/** patch 基线锚点抽象(D5):内存 baseCommit 或宿主持久锚点文件,二选一注入。 */
|
|
142
|
+
export type PatchBaselineAnchor =
|
|
143
|
+
| { readonly kind: "commit"; readonly baseCommit: string }
|
|
144
|
+
| { readonly kind: "anchor-file"; readonly path: string };
|
|
145
|
+
|
|
146
|
+
export interface CollectWorktreePatchOptions {
|
|
147
|
+
/** worktree checkout 目录(add / diff 的 cwd)。 */
|
|
148
|
+
readonly worktreePath: string;
|
|
149
|
+
/** patch 输出绝对路径(须在 worktree 之外——cleanup 不会删除)。 */
|
|
150
|
+
readonly patchFile: string;
|
|
151
|
+
/**
|
|
152
|
+
* 基线锚点。anchor-file 形态的 path 须在 worktree 之外(同 patchFile 约束):
|
|
153
|
+
* 落在 worktree 内会被本机制的 `git add -A` 一并暂存、混入 diff 产物——patch
|
|
154
|
+
* 被锚点文件自身污染(路径与内容进入 patch),且该形态无任何 warn 或
|
|
155
|
+
* patchIncomplete 留痕(不在 ⛔3 降级规格内,属静默污染)。
|
|
156
|
+
*/
|
|
157
|
+
readonly anchor: PatchBaselineAnchor;
|
|
158
|
+
/** git 命令超时(ms),缺省 30_000。 */
|
|
159
|
+
readonly timeout?: number;
|
|
160
|
+
/**
|
|
161
|
+
* execFile maxBuffer(字节)= diff 输出上限,缺省 1MB(1024 * 1024,Node execFile
|
|
162
|
+
* 缺省)。超限即 GitRunError(先经 ⛔3① git 层触发降级 warn,降级裸 diff 同样
|
|
163
|
+
* 超限后原样上抛——实测形态,见 worktree-git-ops.test.ts 大 diff 用例)。批量
|
|
164
|
+
* 重构等大 diff 场景宿主建议 32 * 1024 * 1024(对齐旧 zsw 宿主 GIT_MAX_BUFFER)。
|
|
165
|
+
* 仅透传至产生大输出的 diff 调用(`diff --cached` 与降级裸 diff);`add -A`
|
|
166
|
+
* 等小输出命令不透传。
|
|
167
|
+
*/
|
|
168
|
+
readonly maxBuffer?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* patch 收集结果。`patchIncomplete` 即 ⛔3 留痕载体:true 表示 patch 相对完整
|
|
173
|
+
* 机制有已知损失(丢已提交改动 / 丢新文件),宿主透传至 record/summary 的责任
|
|
174
|
+
* 在姊妹文档 V6。缺省(undefined)= 完整机制产物。
|
|
175
|
+
*/
|
|
176
|
+
export interface WorktreePatchResult {
|
|
177
|
+
readonly patchFile: string;
|
|
178
|
+
/** true = diff 非空且写盘成功;false = 空 diff(不写文件,避免悬空路径)。 */
|
|
179
|
+
readonly written: boolean;
|
|
180
|
+
readonly patchIncomplete?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 收集 worktree 改动为 patch(统一 add + diff 基线机制)。
|
|
185
|
+
*
|
|
186
|
+
* 完整形态:`git add -A`(暂存全部改动,含未跟踪新文件)→ `git diff --cached
|
|
187
|
+
* <baseline>`(暂存区 vs 基线锚点)。旧形态 `git diff HEAD <base>` 是树 vs 树对比:
|
|
188
|
+
* worktree HEAD 初始即 baseCommit,子 agent 不提交时 diff 恒空 → 改动丢失(提取源
|
|
189
|
+
* collectPatch [MF#2] 注释)。
|
|
190
|
+
*
|
|
191
|
+
* ⛔3 降级路径(均非致命、显著 warn、patchIncomplete 留痕):
|
|
192
|
+
* ① 锚点缺失(anchor-file 不存在/不可读)、损坏(内容空白,或内容不被 git 认)
|
|
193
|
+
* → 裸 diff HEAD(仅未提交改动,丢已提交改动)。
|
|
194
|
+
* ② add 失败(如索引锁冲突瞬时态)→ 裸 diff HEAD(untracked 新文件不进 patch)。
|
|
195
|
+
* 按 D5 裁决以 HEAD 为基线重定义降级形态(非 zsw `diff <base>` 等值平移),
|
|
196
|
+
* 损失面差异由 patchIncomplete 留痕判断。
|
|
197
|
+
*
|
|
198
|
+
* 写盘失败(磁盘满/权限)不在 ⛔3 降级规格内:环境级故障不与「空 diff written=false」
|
|
199
|
+
* 混淆(否则宿主把磁盘故障当无改动,patch 静默丢失),原样上抛由宿主处置。
|
|
200
|
+
*/
|
|
201
|
+
export async function collectWorktreePatch(
|
|
202
|
+
opts: CollectWorktreePatchOptions,
|
|
203
|
+
): Promise<WorktreePatchResult> {
|
|
204
|
+
const { worktreePath, patchFile, anchor } = opts;
|
|
205
|
+
let patchIncomplete = false;
|
|
206
|
+
|
|
207
|
+
// ── 基线锚点解析(⛔3 路径①的读取层:缺失 / 不可读 / 空白)──
|
|
208
|
+
let baseline: string | undefined;
|
|
209
|
+
if (anchor.kind === "commit") {
|
|
210
|
+
const commit = anchor.baseCommit.trim();
|
|
211
|
+
if (commit.length === 0) {
|
|
212
|
+
patchIncomplete = true;
|
|
213
|
+
logger.warn(
|
|
214
|
+
"[worktree-git-ops] patch baseline anchor commit is empty, degrading to bare diff (uncommitted changes only); patch marked incomplete",
|
|
215
|
+
{ worktreePath },
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
baseline = commit;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
try {
|
|
222
|
+
const commit = fs.readFileSync(anchor.path, "utf-8").trim();
|
|
223
|
+
if (commit.length === 0) {
|
|
224
|
+
patchIncomplete = true;
|
|
225
|
+
logger.warn(
|
|
226
|
+
"[worktree-git-ops] patch baseline anchor file is empty or blank, degrading to bare diff (uncommitted changes only); patch marked incomplete",
|
|
227
|
+
{ worktreePath, anchorFile: anchor.path },
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
baseline = commit;
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
patchIncomplete = true;
|
|
234
|
+
logger.warn(
|
|
235
|
+
"[worktree-git-ops] patch baseline anchor file missing or unreadable, degrading to bare diff (uncommitted changes only); patch marked incomplete",
|
|
236
|
+
{
|
|
237
|
+
worktreePath,
|
|
238
|
+
anchorFile: anchor.path,
|
|
239
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── git add -A(统一机制第一步;⛔3 路径②:失败非致命,继续 diff)──
|
|
246
|
+
let addFailed = false;
|
|
247
|
+
try {
|
|
248
|
+
await gitRun(["add", "-A"], { cwd: worktreePath, timeout: opts.timeout });
|
|
249
|
+
} catch (err) {
|
|
250
|
+
addFailed = true;
|
|
251
|
+
patchIncomplete = true;
|
|
252
|
+
logger.warn(
|
|
253
|
+
"[worktree-git-ops] git add -A failed, continuing with bare diff (tracked uncommitted changes only); patch marked incomplete",
|
|
254
|
+
{
|
|
255
|
+
worktreePath,
|
|
256
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── diff 基线机制 ──
|
|
262
|
+
if (baseline !== undefined && !addFailed) {
|
|
263
|
+
let diff: string;
|
|
264
|
+
try {
|
|
265
|
+
diff = await gitRun(["diff", "--cached", baseline], {
|
|
266
|
+
cwd: worktreePath,
|
|
267
|
+
timeout: opts.timeout,
|
|
268
|
+
maxBuffer: opts.maxBuffer,
|
|
269
|
+
});
|
|
270
|
+
} catch (err) {
|
|
271
|
+
// ⛔3 路径①的 git 层:锚点内容不被 git 认(损坏形态二:文件在、内容非合法
|
|
272
|
+
// commit)→ 同样降级裸 diff。裸 diff 再失败 = 真 git 故障,原样上抛。
|
|
273
|
+
patchIncomplete = true;
|
|
274
|
+
logger.warn(
|
|
275
|
+
"[worktree-git-ops] patch baseline anchor rejected by git (corrupted?), degrading to bare diff (uncommitted changes only); patch marked incomplete",
|
|
276
|
+
{
|
|
277
|
+
worktreePath,
|
|
278
|
+
baseline,
|
|
279
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
diff = await gitRun(["diff", "HEAD"], {
|
|
283
|
+
cwd: worktreePath,
|
|
284
|
+
timeout: opts.timeout,
|
|
285
|
+
maxBuffer: opts.maxBuffer,
|
|
286
|
+
});
|
|
287
|
+
return finishPatch(diff, patchFile, patchIncomplete);
|
|
288
|
+
}
|
|
289
|
+
return finishPatch(diff, patchFile, patchIncomplete);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 降级形态(⛔3①/②):裸 diff HEAD,仅未提交改动(add 成功时含已暂存新文件;
|
|
293
|
+
// add 失败时 untracked 新文件不进 patch——两条路径损失面不同,留痕同一标记)。
|
|
294
|
+
const diff = await gitRun(["diff", "HEAD"], {
|
|
295
|
+
cwd: worktreePath,
|
|
296
|
+
timeout: opts.timeout,
|
|
297
|
+
maxBuffer: opts.maxBuffer,
|
|
298
|
+
});
|
|
299
|
+
return finishPatch(diff, patchFile, patchIncomplete);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** diff 出口收敛:空 diff 不写文件(written=false,避免悬空路径);写盘失败上抛。 */
|
|
303
|
+
function finishPatch(
|
|
304
|
+
diff: string,
|
|
305
|
+
patchFile: string,
|
|
306
|
+
patchIncomplete: boolean,
|
|
307
|
+
): WorktreePatchResult {
|
|
308
|
+
if (diff.length === 0) {
|
|
309
|
+
return {
|
|
310
|
+
patchFile,
|
|
311
|
+
written: false,
|
|
312
|
+
...(patchIncomplete ? { patchIncomplete: true } : {}),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
fs.writeFileSync(patchFile, diff, "utf-8");
|
|
316
|
+
return {
|
|
317
|
+
patchFile,
|
|
318
|
+
written: true,
|
|
319
|
+
...(patchIncomplete ? { patchIncomplete: true } : {}),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ============================================================
|
|
324
|
+
// cleanupWorktree(三步容错清理)
|
|
325
|
+
// ============================================================
|
|
326
|
+
|
|
327
|
+
export interface CleanupWorktreeOptions {
|
|
328
|
+
/** 主仓库根目录(git -C 目标)。 */
|
|
329
|
+
readonly repo: string;
|
|
330
|
+
/** worktree checkout 绝对路径。 */
|
|
331
|
+
readonly worktreePath: string;
|
|
332
|
+
/** 分支名。 */
|
|
333
|
+
readonly branch: string;
|
|
334
|
+
/** 第三步宿主钩子(如 worktrees.json 注册表移除)。注册表归宿主(D5 目录布局
|
|
335
|
+
* 与孤儿判定留宿主),内核经回调解耦;抛错仅 warn 不阻断(三步各自容错)。 */
|
|
336
|
+
readonly onRemoved?: () => Promise<void> | void;
|
|
337
|
+
/** git 命令超时(ms),缺省 30_000。 */
|
|
338
|
+
readonly timeout?: number;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 清理 worktree:worktree remove --force → branch -D → onRemoved 宿主钩子。
|
|
343
|
+
* 三步各自独立容错——任一步失败不阻断其余(如 remove 失败仍尝试 branch -D +
|
|
344
|
+
* 宿主钩子),避免单步失败导致后续资源泄漏(提取源 cleanup 的容错结构)。
|
|
345
|
+
* 前两步失败 debug 留痕(best-effort 惯例);宿主钩子失败 warn(宿主态漂移值得
|
|
346
|
+
* 显著,孤儿收敛兜底在宿主 reaper/对账)。本函数永不 reject。
|
|
347
|
+
*/
|
|
348
|
+
export async function cleanupWorktree(opts: CleanupWorktreeOptions): Promise<void> {
|
|
349
|
+
try {
|
|
350
|
+
await gitRun(["worktree", "remove", "--force", opts.worktreePath], {
|
|
351
|
+
cwd: opts.repo,
|
|
352
|
+
timeout: opts.timeout,
|
|
353
|
+
});
|
|
354
|
+
} catch (err) {
|
|
355
|
+
bestEffort(err, "worktree remove (cleanup)");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await gitRun(["branch", "-D", opts.branch], {
|
|
360
|
+
cwd: opts.repo,
|
|
361
|
+
timeout: opts.timeout,
|
|
362
|
+
});
|
|
363
|
+
} catch (err) {
|
|
364
|
+
bestEffort(err, "branch delete (cleanup)");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (opts.onRemoved) {
|
|
368
|
+
try {
|
|
369
|
+
await opts.onRemoved();
|
|
370
|
+
} catch (err) {
|
|
371
|
+
logger.warn("[worktree-git-ops] cleanup onRemoved host hook failed (worktree/branch already cleaned)", {
|
|
372
|
+
branch: opts.branch,
|
|
373
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ============================================================
|
|
380
|
+
// listWorktreePorcelain(原始形态输出)
|
|
381
|
+
// ============================================================
|
|
382
|
+
|
|
383
|
+
export interface ListWorktreePorcelainOptions {
|
|
384
|
+
/** 主仓库根目录(git -C 目标)。 */
|
|
385
|
+
readonly repo: string;
|
|
386
|
+
/** git 命令超时(ms),缺省 30_000。 */
|
|
387
|
+
readonly timeout?: number;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* `git worktree list --porcelain` 原始 stdout 保真返回(不 trim / 不 split / 不
|
|
392
|
+
* 逐行加工)。宿主 realpath 对账依赖原始行文(zsw 的 /var→/private/var 归账)。
|
|
393
|
+
* 失败 reject GitRunError(读类命令失败 = 真故障,处置策略归宿主)。
|
|
394
|
+
*/
|
|
395
|
+
export async function listWorktreePorcelain(opts: ListWorktreePorcelainOptions): Promise<string> {
|
|
396
|
+
return gitRun(["worktree", "list", "--porcelain"], { cwd: opts.repo, timeout: opts.timeout });
|
|
397
|
+
}
|
|
@@ -49,6 +49,15 @@ const WORKTREE_TMP_ROOT = "pi-subagents";
|
|
|
49
49
|
/** 分支名前缀(create() 生成 `pi-sub-<recordId>`)。 */
|
|
50
50
|
const BRANCH_PREFIX = "pi-sub-";
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 对账「歧义跳过」的老化升级阈值(PS-12 措施⑤):同一物理残留连续 N 轮 scan
|
|
54
|
+
* 仍无法建立 branch↔pid 对应 → 升级为含磁盘路径与人工清理指引的强 warn,
|
|
55
|
+
* 终结「仅周期性低信息 warn、有界资源永无终局」的滞留形态。导出供测试与
|
|
56
|
+
* 运维文档对齐阈值。N 是周期数不是绝对时长——scan 由 session_start 触发,
|
|
57
|
+
* 频率随使用节奏,按周期计数避免与时钟假设耦合。
|
|
58
|
+
*/
|
|
59
|
+
export const RECONCILE_SKIP_ESCALATION_CYCLES = 4;
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* 物理面发现的 worktree(tmpdir checkout 目录存在,无论注册表是否登记)。
|
|
54
63
|
* repo 从 checkout/.git 指针文件推导(普通 repo 与 bare+worktree 均覆盖);
|
|
@@ -132,6 +141,12 @@ export class WorktreeManager {
|
|
|
132
141
|
// 不继承前驱错误(否则 1 个 worktree add 失败会传染同 repo 后续全部写命令,
|
|
133
142
|
// 替代旧同步版单线程天然全局串行的「各命令独立失败」语义)。
|
|
134
143
|
private readonly writeQueues = new Map<string, Promise<void>>();
|
|
144
|
+
// [PS-12 措施⑤] 对账歧义跳过的 per-checkout 连续计数(老化判据)。
|
|
145
|
+
// key = checkout 绝对路径;每轮 reconcileWithPhysical 末尾收敛为「本轮实际
|
|
146
|
+
// 进入歧义分支」的路径集(自愈/判死清理/注册表收编/物理消失的一律清零,
|
|
147
|
+
// 见该方法尾部),条目数 ≤ 单轮歧义残留数,无泄漏面。内存态:进程重启后
|
|
148
|
+
// 重新起数——老化是人工介入的提醒信号,不值得为此引入持久化状态。
|
|
149
|
+
private readonly ambiguousSkipCycles = new Map<string, number>();
|
|
135
150
|
|
|
136
151
|
constructor(agentDir: string) {
|
|
137
152
|
this.agentDir = agentDir;
|
|
@@ -373,7 +388,14 @@ export class WorktreeManager {
|
|
|
373
388
|
|
|
374
389
|
// ── 方向二:物理有 → 注册无 ──
|
|
375
390
|
const orphans = physical.filter((pt) => !registeredBranches.has(pt.branch));
|
|
376
|
-
await this.reconcileUnregisteredWorktrees(orphans);
|
|
391
|
+
const ambiguousNow = await this.reconcileUnregisteredWorktrees(orphans);
|
|
392
|
+
|
|
393
|
+
// [PS-12 措施⑤] 老化计数收敛(「出现对应后清零」):本轮未进入歧义跳过的
|
|
394
|
+
// 路径——自愈补写 / 判死清理 / 注册表收编 / 物理消失——计数删除,重现时
|
|
395
|
+
// 重新起数;升级 warn 因此只在「连续无对应」时出现,不因历史陈账误触发。
|
|
396
|
+
for (const checkout of [...this.ambiguousSkipCycles.keys()]) {
|
|
397
|
+
if (!ambiguousNow.has(checkout)) this.ambiguousSkipCycles.delete(checkout);
|
|
398
|
+
}
|
|
377
399
|
}
|
|
378
400
|
|
|
379
401
|
/** 对账方向一(注册有 → 物理无):条目的分支与 checkout 目录都已不存在 → 条目指向
|
|
@@ -408,9 +430,13 @@ export class WorktreeManager {
|
|
|
408
430
|
* 并发覆盖丢条目场景,补写后回归标准 pid 判据路径);
|
|
409
431
|
* - 多活 pid 或多残留无法建立 branch↔pid 对应:跳过 + warn——宁延迟勿误删;
|
|
410
432
|
* 活体自身 cleanup 路径正常(registry.remove 幂等),死体等活 pid 全灭后
|
|
411
|
-
* 下一周期收敛。
|
|
412
|
-
|
|
433
|
+
* 下一周期收敛。
|
|
434
|
+
*
|
|
435
|
+
* 返回本轮实际进入「歧义跳过」的 checkout 路径集(PS-12 老化计数收敛依据,
|
|
436
|
+
* 见 reconcileWithPhysical 尾部)。 */
|
|
437
|
+
private async reconcileUnregisteredWorktrees(orphans: PhysicalWorktree[]): Promise<Set<string>> {
|
|
413
438
|
// 按 enc 段聚合处理(活信号以 enc 段为粒度——.alive 在 <enc>/sessions/ 下)
|
|
439
|
+
const ambiguous = new Set<string>();
|
|
414
440
|
const orphansByEnc = new Map<string, PhysicalWorktree[]>();
|
|
415
441
|
for (const pt of orphans) {
|
|
416
442
|
const list = orphansByEnc.get(pt.enc) ?? [];
|
|
@@ -418,12 +444,17 @@ export class WorktreeManager {
|
|
|
418
444
|
orphansByEnc.set(pt.enc, list);
|
|
419
445
|
}
|
|
420
446
|
for (const [enc, list] of orphansByEnc) {
|
|
421
|
-
await this.reconcileEncSegment(enc, list);
|
|
447
|
+
await this.reconcileEncSegment(enc, list, ambiguous);
|
|
422
448
|
}
|
|
449
|
+
return ambiguous;
|
|
423
450
|
}
|
|
424
451
|
|
|
425
452
|
/** 单 enc 段的残留处置三分支:无活 pid 判死清理 / 唯一对应自愈补写 / 多对应保守跳过。 */
|
|
426
|
-
private async reconcileEncSegment(
|
|
453
|
+
private async reconcileEncSegment(
|
|
454
|
+
enc: string,
|
|
455
|
+
list: PhysicalWorktree[],
|
|
456
|
+
ambiguous: Set<string>,
|
|
457
|
+
): Promise<void> {
|
|
427
458
|
const alivePids = this.collectAlivePids(enc);
|
|
428
459
|
if (alivePids.length === 0) {
|
|
429
460
|
await this.cleanupDeadSegment(list);
|
|
@@ -449,11 +480,42 @@ export class WorktreeManager {
|
|
|
449
480
|
return;
|
|
450
481
|
}
|
|
451
482
|
// 多活 pid / 多残留:无法建立 branch↔pid 对应,保守跳过待下周期。
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
483
|
+
// [PS-12 措施⑤] 保守跳过加老化:同一 checkout 连续 {@link
|
|
484
|
+
// RECONCILE_SKIP_ESCALATION_CYCLES} 轮仍无对应 → 升级为含磁盘路径与人工
|
|
485
|
+
// 清理指引的强 warn(有界资源泄漏不能永远停在低信息周期 warn 上)。计数
|
|
486
|
+
// 清零由 reconcileWithPhysical 统一编排——凡本轮未进入本分支的路径一律重置。
|
|
487
|
+
let escalated = 0;
|
|
488
|
+
for (const pt of list) {
|
|
489
|
+
const cycles = (this.ambiguousSkipCycles.get(pt.checkout) ?? 0) + 1;
|
|
490
|
+
this.ambiguousSkipCycles.set(pt.checkout, cycles);
|
|
491
|
+
ambiguous.add(pt.checkout); // 本轮实际进入歧义跳过(老化收敛依据,见调用方)
|
|
492
|
+
if (cycles < RECONCILE_SKIP_ESCALATION_CYCLES) continue;
|
|
493
|
+
escalated++;
|
|
494
|
+
// repo 未知(无主残留)时指引里用 <main-repo> 占位,data.repo 同步标注
|
|
495
|
+
const repoHint = pt.repo ?? "<main-repo>";
|
|
496
|
+
logger.warn(
|
|
497
|
+
`[worktree] reconcile: unregistered physical worktree skipped for ${cycles} consecutive cycles ` +
|
|
498
|
+
`(alive-pid mapping still ambiguous) — manual cleanup may be needed. ` +
|
|
499
|
+
`Inspect: git -C ${repoHint} worktree list. ` +
|
|
500
|
+
`If no live process owns it: git -C ${repoHint} worktree remove --force ${pt.checkout} && git -C ${repoHint} branch -D ${pt.branch}. ` +
|
|
501
|
+
`Ownerless checkout (repo unknown, delete the directory directly): rm -rf ${pt.checkout}`,
|
|
502
|
+
{
|
|
503
|
+
branch: pt.branch,
|
|
504
|
+
checkout: pt.checkout,
|
|
505
|
+
repo: pt.repo,
|
|
506
|
+
skippedCycles: cycles,
|
|
507
|
+
},
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
// 未达升级阈值的残留维持既有低信息聚合 warn(数量只含本周期未升级部分;
|
|
511
|
+
// 全部升级时不再发本条,避免每周期双份噪音)
|
|
512
|
+
if (escalated < list.length) {
|
|
513
|
+
logger.warn("[worktree] reconcile: unregistered physical worktrees present but alive-pid mapping ambiguous, skipping this cycle", {
|
|
514
|
+
enc,
|
|
515
|
+
orphans: list.length - escalated,
|
|
516
|
+
alivePids: alivePids.length,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
457
519
|
}
|
|
458
520
|
|
|
459
521
|
/** 无活 pid 段:残留判死清理——checkout mtime 超 SPAWN_GRACE_MS 才清(防误清另一
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
// 双向 diff,见 reconcileWithPhysical)。
|
|
19
19
|
// - 同步 IO(readFileSync/writeFileSync)持锁执行:锁内临界区毫秒级,
|
|
20
20
|
// async 锁 + sync IO 组合在单线程 event loop 内无 interleaving。
|
|
21
|
-
// -
|
|
21
|
+
// - 原子写:tmp+rename 统一走 shared/atomic-write(U6b),防写一半崩溃产生损坏 JSON。
|
|
22
22
|
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
24
|
import * as path from "node:path";
|
|
25
25
|
|
|
26
26
|
import { getLogger } from "../core/logger.ts";
|
|
27
|
+
import { writeAtomicFileSync } from "../shared/atomic-write.ts";
|
|
27
28
|
import lockfile from "proper-lockfile";
|
|
28
29
|
|
|
29
30
|
import { bestEffort } from "./best-effort.ts";
|
|
@@ -36,11 +37,6 @@ export const SPAWN_GRACE_MS = 60_000;
|
|
|
36
37
|
/** JSON 缩进空格数(可读性 + diff 友好)。 */
|
|
37
38
|
const JSON_INDENT = 2;
|
|
38
39
|
|
|
39
|
-
/** tmp 随机段:36 进制取 8 字符(跳过 "0." 前缀),与 pid 组合保证并发唯一。 */
|
|
40
|
-
const TMP_RANDOM_BASE = 36;
|
|
41
|
-
const TMP_RANDOM_SLICE_START = 2; // 跳过 Math.random 字符串的 "0." 前缀
|
|
42
|
-
const TMP_RANDOM_SLICE_END = 10;
|
|
43
|
-
|
|
44
40
|
/**
|
|
45
41
|
* 锁参数:逐项对齐 extensions/shared/file-lock/src/file-lock.ts 的 withFileLock
|
|
46
42
|
* 包装默认值(stale 30s / retries 10)。两侧参数漂移会破坏「同一把
|
|
@@ -254,18 +250,16 @@ export class WorktreeRegistry {
|
|
|
254
250
|
}
|
|
255
251
|
|
|
256
252
|
/**
|
|
257
|
-
*
|
|
253
|
+
* 原子写入全部条目(shared/atomic-write 统一原语,U6b 迁移——sync 档
|
|
254
|
+
* writeAtomicFileSync;写失败时原语清理残留 tmp,消除旧实现「write 成功但
|
|
255
|
+
* rename 失败漏清 tmp」缺陷)。
|
|
258
256
|
* best-effort:写入失败不阻断主流程(create/cleanup 的 git 操作已执行,
|
|
259
257
|
* 注册表与 git 状态的短暂不一致靠下次 reaper 对账收敛)。
|
|
260
258
|
* 写盘失败时 warn 日志(带 branch/pid 上下文,补全失败可观测闭环)。
|
|
261
259
|
*/
|
|
262
260
|
private save(entries: WorktreeEntry[], context?: { branch?: string; pid?: number }): void {
|
|
263
261
|
try {
|
|
264
|
-
|
|
265
|
-
// tmp 名带 pid + 随机段:锁降级(无锁并发 RMW)时多进程 tmp 互不覆盖
|
|
266
|
-
const tmp = `${this.filePath}.tmp_${process.pid}_${Math.random().toString(TMP_RANDOM_BASE).slice(TMP_RANDOM_SLICE_START, TMP_RANDOM_SLICE_END)}`;
|
|
267
|
-
fs.writeFileSync(tmp, JSON.stringify({ entries }, null, JSON_INDENT), "utf-8");
|
|
268
|
-
fs.renameSync(tmp, this.filePath);
|
|
262
|
+
writeAtomicFileSync(this.filePath, JSON.stringify({ entries }, null, JSON_INDENT));
|
|
269
263
|
} catch (err) {
|
|
270
264
|
bestEffort(err, "worktree registry save");
|
|
271
265
|
// [worktree-reaper-fix] 补全写盘失败静默吞错时,条目 pid 恒 0、60s 后被 reaper 误删
|