@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.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 +15 -0
- package/package.json +7 -4
- package/scripts/rfl.mjs +308 -0
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +197 -15
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +442 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +300 -90
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +72 -34
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/channel-registry-access.ts +3 -1
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +99 -13
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +491 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-entry.ts +118 -0
- package/src/execution/record-store.ts +844 -108
- package/src/execution/session-pending.ts +121 -49
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +713 -316
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +917 -138
- package/src/execution/temp-prompt.ts +8 -3
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +218 -19
- package/src/execution/worktree-manager.ts +449 -59
- package/src/execution/worktree-registry.ts +97 -29
- package/src/index.ts +318 -20
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +53 -20
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +21 -27
- package/src/interface/tool-workflow-script.ts +29 -33
- package/src/interface/tool-workflow.ts +67 -100
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/detail-content.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +137 -30
- package/src/orchestration/execute-agent-call.ts +44 -10
- package/src/orchestration/jsonl-run-store.ts +397 -71
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +145 -133
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +38 -11
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +79 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +22 -1
- package/src/shared/resource-discovery.ts +162 -59
- package/src/shared/schema-jsonify.ts +56 -0
- package/workflows/review-fix-loop-utils.cjs +542 -32
- package/workflows/review-fix-loop.js +462 -109
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
// 真实注册表文件,仅 mock ./pi-invocation.ts(把 pi 二进制替换为 node -e 脚本)。
|
|
16
16
|
//
|
|
17
17
|
// mock 最小化原则:
|
|
18
|
-
// - node:child_process 不 mock(真实 spawn /
|
|
18
|
+
// - node:child_process 不 mock(真实 spawn / spawnSync git)
|
|
19
19
|
// - node:fs 不 mock(真实目录/文件:worktree checkout、注册表 JSON)
|
|
20
20
|
// - alive-store 不 mock(真实 process.kill(pid, 0) 探活)
|
|
21
21
|
// - 仅 vi.mock("./pi-invocation.ts"):getPiInvocation 返回 node -e 脚本
|
|
22
22
|
// - fake timers 仅 toFake: ["Date"]:推进注册表宽限判定用,不干扰真实 I/O 事件
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
25
|
import * as fs from "node:fs";
|
|
26
26
|
import * as os from "node:os";
|
|
27
27
|
import * as path from "node:path";
|
|
@@ -67,7 +67,8 @@ let handle: WorktreeHandle | undefined;
|
|
|
67
67
|
let spawnedPid: number | undefined;
|
|
68
68
|
|
|
69
69
|
function git(args: string[], cwd: string): string {
|
|
70
|
-
|
|
70
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
71
|
+
return (r.stdout ?? "").trim();
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
/** 初始化临时 git repo(至少一个 commit,worktreeManager.create 需要 clean tree + HEAD)。 */
|
|
@@ -97,7 +98,7 @@ async function waitForPid(branch: string, timeoutMs = 5000): Promise<number> {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/** 清理:kill 子进程 + worktree cleanup + 删除临时目录。 */
|
|
100
|
-
function cleanup(): void {
|
|
101
|
+
async function cleanup(): Promise<void> {
|
|
101
102
|
if (spawnedPid) {
|
|
102
103
|
try {
|
|
103
104
|
process.kill(spawnedPid, "SIGKILL");
|
|
@@ -108,7 +109,7 @@ function cleanup(): void {
|
|
|
108
109
|
}
|
|
109
110
|
if (handle) {
|
|
110
111
|
try {
|
|
111
|
-
wtm.cleanup(handle);
|
|
112
|
+
await wtm.cleanup(handle);
|
|
112
113
|
} catch (err) {
|
|
113
114
|
// best-effort:git worktree remove 失败不阻断测试清理
|
|
114
115
|
// eslint-disable-next-line no-console
|
|
@@ -131,18 +132,21 @@ beforeEach(() => {
|
|
|
131
132
|
registry = new WorktreeRegistry(agentDir);
|
|
132
133
|
});
|
|
133
134
|
|
|
134
|
-
afterEach(() => {
|
|
135
|
+
afterEach(async () => {
|
|
135
136
|
vi.useRealTimers();
|
|
136
|
-
cleanup();
|
|
137
|
+
await cleanup();
|
|
137
138
|
});
|
|
138
139
|
|
|
139
140
|
// ── 用例 ──
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
// [HISTORICAL] 2026-08-20 PR #185:真实 spawn node 子进程 + 真实 git worktree + 注册表
|
|
143
|
+
// 文件锁集成用例显式超时——整包满并行 + 系统余载下真实子进程链路超 vitest 默认 5s
|
|
144
|
+
// testTimeout(对齐本包 worktree-registry D5a / process-shutdown-hook 口径)。
|
|
145
|
+
describe("worktree pid 注册链路(真实 spawn 集成)", { timeout: 30_000 }, () => {
|
|
142
146
|
it("正向:spawn 返回后注册表 pid 同步补全,活 worktree 超宽限不被 scan 误清", async () => {
|
|
143
147
|
// 0. 长驻脚本(子进程 90s 内不退出,模拟长跑子 agent)
|
|
144
148
|
scriptHolder.script = LONG_RUNNING_SCRIPT; // 1. 真实创建 worktree(pid=0 占位)
|
|
145
|
-
handle = wtm.create(repoDir, "rec-1");
|
|
149
|
+
handle = await wtm.create(repoDir, "rec-1");
|
|
146
150
|
expect(readEntry(handle.branch)).toMatchObject({ pid: 0 });
|
|
147
151
|
|
|
148
152
|
// 2. runSpawn 挂后台(不 await——长驻子进程 close 不触发,await 会挂死),
|
|
@@ -170,7 +174,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
170
174
|
vi.setSystemTime(Date.now() + SPAWN_GRACE_MS + 1000);
|
|
171
175
|
|
|
172
176
|
// 5. scan():活 worktree 必须不被清(修复前:pid=0 超宽限 → 误删 → 红)
|
|
173
|
-
wtm.scan();
|
|
177
|
+
await wtm.scan();
|
|
174
178
|
expect(fs.existsSync(handle.path)).toBe(true);
|
|
175
179
|
|
|
176
180
|
// 6. 收尾:真实时钟恢复 + kill 子进程让 runPromise settle
|
|
@@ -183,7 +187,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
183
187
|
await runPromise;
|
|
184
188
|
|
|
185
189
|
// 7. 反向:进程死后 scan 回收真孤儿
|
|
186
|
-
wtm.scan();
|
|
190
|
+
await wtm.scan();
|
|
187
191
|
expect(fs.existsSync(handle.path)).toBe(false);
|
|
188
192
|
const entryAfter = readEntry(handle.branch);
|
|
189
193
|
expect(entryAfter).toBeUndefined();
|
|
@@ -193,7 +197,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
193
197
|
// 0. 短命脚本(子进程立即退出,模拟快速完成的子 agent)
|
|
194
198
|
scriptHolder.script = SHORT_LIVED_SCRIPT;
|
|
195
199
|
// 1. 真实创建 worktree
|
|
196
|
-
handle = wtm.create(repoDir, "rec-2");
|
|
200
|
+
handle = await wtm.create(repoDir, "rec-2");
|
|
197
201
|
|
|
198
202
|
// 2. 短命脚本子进程:spawn 后同步补 pid(修复前 pid=0,且未超宽限 → 不回收 → 红)
|
|
199
203
|
const ctx = makeCtx({
|
|
@@ -218,7 +222,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
218
222
|
spawnedPid = entry!.pid;
|
|
219
223
|
|
|
220
224
|
// 4. scan:pid>0 且进程死 → 立即判孤儿回收(无需等宽限)
|
|
221
|
-
wtm.scan();
|
|
225
|
+
await wtm.scan();
|
|
222
226
|
expect(fs.existsSync(handle.path)).toBe(false);
|
|
223
227
|
expect(readEntry(handle.branch)).toBeUndefined();
|
|
224
228
|
}, 15000);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// src/__tests__/worktree-reconcile.integration.test.ts
|
|
2
|
+
//
|
|
3
|
+
// D5b 对账双向 diff 集成测试:真实 git repo + 真实 tmpdir(不 mock fs/execFile)。
|
|
4
|
+
//
|
|
5
|
+
// 隔离手段:TMPDIR env 重定向到测试私有目录——WorktreeManager 的物理面扫描
|
|
6
|
+
// 根 = os.tmpdir()/pi-subagents(跟随 TMPDIR),避免扫到本机其他进程的真实残留。
|
|
7
|
+
//
|
|
8
|
+
// 覆盖四个收敛方向(判据见 worktree-manager.ts reconcileWithPhysical):
|
|
9
|
+
// 1. 物理有 + 注册无 + 无活 pid + 超 grace → 清分支与目录
|
|
10
|
+
// 2. 物理有 + 注册无 + 恰好 1 活 pid + 1 残留 → 补写回注册表(自愈)
|
|
11
|
+
// 3. 注册有(pid 活)+ 物理无 → 移除幻影条目
|
|
12
|
+
// 4. 多活 pid / 多残留(对应歧义)→ 跳过不动(宁延迟勿误删)
|
|
13
|
+
|
|
14
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as os from "node:os";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
20
|
+
|
|
21
|
+
import { encodeCwd } from "../path-encoding.ts";
|
|
22
|
+
import { WorktreeRegistry } from "../worktree-registry.ts";
|
|
23
|
+
import { WorktreeManager } from "../worktree-manager.ts";
|
|
24
|
+
|
|
25
|
+
/** 原始 TMPDIR(beforeEach 重定向、afterEach 还原)。 */
|
|
26
|
+
const ORIG_TMPDIR = os.tmpdir();
|
|
27
|
+
|
|
28
|
+
/** git 辅助:repo 内执行(输出 trim)。 */
|
|
29
|
+
function git(repo: string, ...args: string[]): string {
|
|
30
|
+
return execFileSync("git", ["-C", repo, ...args], { encoding: "utf-8" }).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// [HISTORICAL] 2026-08-20 PR #185:真实 git 子进程 + 注册表文件锁集成用例显式超时——
|
|
34
|
+
// 每用例多次 git init/worktree add/branch -D 真实子进程调用,整包满并行 + 系统余载下
|
|
35
|
+
// 超 vitest 默认 5s testTimeout(对齐本包 worktree-registry D5a / process-shutdown-hook 口径)。
|
|
36
|
+
describe("WorktreeManager.scan 物理面对账(D5b)", { timeout: 30_000 }, () => {
|
|
37
|
+
let baseDir: string;
|
|
38
|
+
let agentDir: string;
|
|
39
|
+
let repo: string;
|
|
40
|
+
let enc: string;
|
|
41
|
+
let registry: WorktreeRegistry;
|
|
42
|
+
let mgr: WorktreeManager;
|
|
43
|
+
/** 真实系统 tmp 下的测试根(TMPDIR 重定向目标)。 */
|
|
44
|
+
let outerDir: string;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
outerDir = fs.mkdtempSync(path.join(ORIG_TMPDIR, "wt-reconcile-it-"));
|
|
48
|
+
// 物理面扫描根重定向:os.tmpdir() 跟随 TMPDIR env(darwin/linux 均如此)
|
|
49
|
+
process.env.TMPDIR = outerDir;
|
|
50
|
+
baseDir = outerDir;
|
|
51
|
+
|
|
52
|
+
agentDir = path.join(baseDir, "agent");
|
|
53
|
+
repo = path.join(baseDir, "repo");
|
|
54
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
55
|
+
git(repo, "init", "-q");
|
|
56
|
+
git(repo, "config", "user.email", "test@test.local");
|
|
57
|
+
git(repo, "config", "user.name", "test");
|
|
58
|
+
fs.writeFileSync(path.join(repo, "a.txt"), "init\n", "utf-8");
|
|
59
|
+
git(repo, "add", "-A");
|
|
60
|
+
git(repo, "commit", "-q", "-m", "init");
|
|
61
|
+
|
|
62
|
+
enc = encodeCwd(repo);
|
|
63
|
+
registry = new WorktreeRegistry(agentDir);
|
|
64
|
+
mgr = new WorktreeManager(agentDir);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
process.env.TMPDIR = ORIG_TMPDIR;
|
|
69
|
+
fs.rmSync(outerDir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** 建一个物理 worktree(按 create() 的真实路径布局,绕过 WorktreeManager.create 的脏树校验)。 */
|
|
73
|
+
function makePhysicalWorktree(branch: string): string {
|
|
74
|
+
const checkout = path.join(baseDir, "pi-subagents", enc, branch);
|
|
75
|
+
git(repo, "worktree", "add", "-q", "-b", branch, checkout, "HEAD");
|
|
76
|
+
return checkout;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 在 <agentDir>/subagents/<enc>/sessions/ 下写一个 .alive marker。 */
|
|
80
|
+
function writeAliveMarker(sessionId: string, pid: number): void {
|
|
81
|
+
const sessions = path.join(agentDir, "subagents", enc, "sessions");
|
|
82
|
+
fs.mkdirSync(sessions, { recursive: true });
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(sessions, `${sessionId}.jsonl.alive`),
|
|
85
|
+
JSON.stringify({ pid, id: sessionId, startedAt: Date.now() }),
|
|
86
|
+
"utf-8",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 把目录 mtime 设到 grace 之前(对账判据的 createdAt 近似)。 */
|
|
91
|
+
function ageBeyondGrace(dir: string): void {
|
|
92
|
+
const past = new Date(Date.now() - 120_000);
|
|
93
|
+
fs.utimesSync(dir, past, past);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it("物理有 + 注册无 + 无活 pid + 超 grace → 清分支与目录", async () => {
|
|
97
|
+
const checkout = makePhysicalWorktree("pi-sub-t1");
|
|
98
|
+
ageBeyondGrace(checkout);
|
|
99
|
+
|
|
100
|
+
await mgr.scan();
|
|
101
|
+
|
|
102
|
+
expect(fs.existsSync(checkout)).toBe(false);
|
|
103
|
+
expect(git(repo, "branch", "--list", "pi-sub-*")).toBe("");
|
|
104
|
+
expect(registry.load()).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("物理有 + 注册无 + 未超 grace(create 窗口)→ 跳过不删", async () => {
|
|
108
|
+
const checkout = makePhysicalWorktree("pi-sub-t2");
|
|
109
|
+
// mtime 保持「现在」——另一进程 worktree add 完成到 registry.add 落盘之间的窗口
|
|
110
|
+
|
|
111
|
+
await mgr.scan();
|
|
112
|
+
|
|
113
|
+
expect(fs.existsSync(checkout)).toBe(true);
|
|
114
|
+
expect(git(repo, "branch", "--list", "pi-sub-t2")).not.toBe("");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("物理有 + 注册无 + 恰好 1 活 pid + 1 残留 → 补写回注册表(自愈)", async () => {
|
|
118
|
+
const checkout = makePhysicalWorktree("pi-sub-t3");
|
|
119
|
+
// 活 pid 用本测试进程(isProcessAlive 真实调用必活)
|
|
120
|
+
writeAliveMarker("s-t3", process.pid);
|
|
121
|
+
|
|
122
|
+
await mgr.scan();
|
|
123
|
+
|
|
124
|
+
const entries = registry.load();
|
|
125
|
+
expect(entries).toHaveLength(1);
|
|
126
|
+
expect(entries[0].branch).toBe("pi-sub-t3");
|
|
127
|
+
expect(entries[0].pid).toBe(process.pid);
|
|
128
|
+
expect(entries[0].checkout).toBe(checkout);
|
|
129
|
+
// 物理资源未被删(自愈而非只删)
|
|
130
|
+
expect(fs.existsSync(checkout)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("注册有(pid 活)+ 物理无 → 移除幻影条目", async () => {
|
|
134
|
+
// pid 活(本进程)使阶段一孤儿清理跳过,隔离方向一的独立语义
|
|
135
|
+
await registry.add({
|
|
136
|
+
repo,
|
|
137
|
+
branch: "pi-sub-ghost",
|
|
138
|
+
checkout: path.join(baseDir, "pi-subagents", enc, "pi-sub-ghost"),
|
|
139
|
+
pid: process.pid,
|
|
140
|
+
createdAt: Date.now(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await mgr.scan();
|
|
144
|
+
|
|
145
|
+
expect(registry.load()).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("多残留 + 多活 pid(对应歧义)→ 跳过不动", async () => {
|
|
149
|
+
const c1 = makePhysicalWorktree("pi-sub-m1");
|
|
150
|
+
const c2 = makePhysicalWorktree("pi-sub-m2");
|
|
151
|
+
ageBeyondGrace(c1);
|
|
152
|
+
ageBeyondGrace(c2);
|
|
153
|
+
// 第二个「活 pid」用真实 sleep 子进程(与 process.pid 不同值)
|
|
154
|
+
const sleeper = spawn("sleep", ["60"]);
|
|
155
|
+
try {
|
|
156
|
+
expect(sleeper.pid).toBeTruthy();
|
|
157
|
+
writeAliveMarker("s-m1", process.pid);
|
|
158
|
+
writeAliveMarker("s-m2", sleeper.pid as number);
|
|
159
|
+
|
|
160
|
+
await mgr.scan();
|
|
161
|
+
|
|
162
|
+
// 歧义 → 本周期全部跳过(宁延迟勿误删),下周期活 pid 消失后收敛
|
|
163
|
+
expect(fs.existsSync(c1)).toBe(true);
|
|
164
|
+
expect(fs.existsSync(c2)).toBe(true);
|
|
165
|
+
expect(registry.load()).toEqual([]);
|
|
166
|
+
} finally {
|
|
167
|
+
sleeper.kill("SIGKILL");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("幂等:连续两次 scan 结果一致(无重复动作)", async () => {
|
|
172
|
+
const checkout = makePhysicalWorktree("pi-sub-idem");
|
|
173
|
+
ageBeyondGrace(checkout);
|
|
174
|
+
|
|
175
|
+
await mgr.scan();
|
|
176
|
+
await mgr.scan();
|
|
177
|
+
|
|
178
|
+
expect(fs.existsSync(checkout)).toBe(false);
|
|
179
|
+
expect(git(repo, "branch", "--list", "pi-sub-*")).toBe("");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -45,29 +45,29 @@ describe("WorktreeRegistry", () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe("add + load", () => {
|
|
48
|
-
it("add 后 load 能读到", () => {
|
|
48
|
+
it("add 后 load 能读到", async () => {
|
|
49
49
|
const entry = makeEntry();
|
|
50
|
-
registry.add(entry);
|
|
50
|
+
await registry.add(entry);
|
|
51
51
|
const loaded = registry.load();
|
|
52
52
|
expect(loaded).toHaveLength(1);
|
|
53
53
|
expect(loaded[0]).toEqual(entry);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it("多条目跨 repo 共存", () => {
|
|
56
|
+
it("多条目跨 repo 共存", async () => {
|
|
57
57
|
const entryA = makeEntry({ repo: REPO_A, branch: "pi-sub-bg-1" });
|
|
58
58
|
const entryB = makeEntry({ repo: REPO_B, branch: "pi-sub-bg-2" });
|
|
59
|
-
registry.add(entryA);
|
|
60
|
-
registry.add(entryB);
|
|
59
|
+
await registry.add(entryA);
|
|
60
|
+
await registry.add(entryB);
|
|
61
61
|
const loaded = registry.load();
|
|
62
62
|
expect(loaded).toHaveLength(2);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it("同 branch add 覆盖(去重)", () => {
|
|
65
|
+
it("同 branch add 覆盖(去重)", async () => {
|
|
66
66
|
const entry = makeEntry({ pid: 0 });
|
|
67
|
-
registry.add(entry);
|
|
67
|
+
await registry.add(entry);
|
|
68
68
|
// 同 branch 再次 add(覆盖)
|
|
69
69
|
const updated = makeEntry({ pid: 999 });
|
|
70
|
-
registry.add(updated);
|
|
70
|
+
await registry.add(updated);
|
|
71
71
|
const loaded = registry.load();
|
|
72
72
|
expect(loaded).toHaveLength(1);
|
|
73
73
|
expect(loaded[0].pid).toBe(999);
|
|
@@ -75,25 +75,25 @@ describe("WorktreeRegistry", () => {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
describe("updatePid", () => {
|
|
78
|
-
it("补全 pid(create 占位 → first header 补全)", () => {
|
|
79
|
-
registry.add(makeEntry({ branch: "pi-sub-bg-1", pid: 0 }));
|
|
80
|
-
registry.updatePid("pi-sub-bg-1", 12345);
|
|
78
|
+
it("补全 pid(create 占位 → first header 补全)", async () => {
|
|
79
|
+
await registry.add(makeEntry({ branch: "pi-sub-bg-1", pid: 0 }));
|
|
80
|
+
await registry.updatePid("pi-sub-bg-1", 12345);
|
|
81
81
|
const loaded = registry.load();
|
|
82
82
|
expect(loaded[0].pid).toBe(12345);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
it("branch 不存在时忽略(幂等)", () => {
|
|
86
|
-
registry.add(makeEntry({ branch: "pi-sub-bg-1" }));
|
|
87
|
-
registry.updatePid("pi-sub-nonexistent", 12345);
|
|
85
|
+
it("branch 不存在时忽略(幂等)", async () => {
|
|
86
|
+
await registry.add(makeEntry({ branch: "pi-sub-bg-1" }));
|
|
87
|
+
await registry.updatePid("pi-sub-nonexistent", 12345);
|
|
88
88
|
const loaded = registry.load();
|
|
89
89
|
expect(loaded).toHaveLength(1);
|
|
90
90
|
expect(loaded[0].pid).toBe(0); // 原条目未变
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
it("update 不改变其他字段", () => {
|
|
93
|
+
it("update 不改变其他字段", async () => {
|
|
94
94
|
const entry = makeEntry({ branch: "pi-sub-bg-1", repo: REPO_A, checkout: "/tmp/x" });
|
|
95
|
-
registry.add(entry);
|
|
96
|
-
registry.updatePid("pi-sub-bg-1", 999);
|
|
95
|
+
await registry.add(entry);
|
|
96
|
+
await registry.updatePid("pi-sub-bg-1", 999);
|
|
97
97
|
const loaded = registry.load();
|
|
98
98
|
expect(loaded[0].repo).toBe(REPO_A);
|
|
99
99
|
expect(loaded[0].checkout).toBe("/tmp/x");
|
|
@@ -102,60 +102,98 @@ describe("WorktreeRegistry", () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe("remove", () => {
|
|
105
|
-
it("移除指定 branch", () => {
|
|
106
|
-
registry.add(makeEntry({ branch: "pi-sub-bg-1" }));
|
|
107
|
-
registry.add(makeEntry({ branch: "pi-sub-bg-2" }));
|
|
108
|
-
registry.remove("pi-sub-bg-1");
|
|
105
|
+
it("移除指定 branch", async () => {
|
|
106
|
+
await registry.add(makeEntry({ branch: "pi-sub-bg-1" }));
|
|
107
|
+
await registry.add(makeEntry({ branch: "pi-sub-bg-2" }));
|
|
108
|
+
await registry.remove("pi-sub-bg-1");
|
|
109
109
|
const loaded = registry.load();
|
|
110
110
|
expect(loaded).toHaveLength(1);
|
|
111
111
|
expect(loaded[0].branch).toBe("pi-sub-bg-2");
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
it("branch 不存在时忽略(幂等)", () => {
|
|
115
|
-
registry.add(makeEntry({ branch: "pi-sub-bg-1" }));
|
|
116
|
-
registry.remove("pi-sub-nonexistent");
|
|
114
|
+
it("branch 不存在时忽略(幂等)", async () => {
|
|
115
|
+
await registry.add(makeEntry({ branch: "pi-sub-bg-1" }));
|
|
116
|
+
await registry.remove("pi-sub-nonexistent");
|
|
117
117
|
expect(registry.load()).toHaveLength(1);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("空注册表 remove 不报错", () => {
|
|
121
|
-
expect(
|
|
120
|
+
it("空注册表 remove 不报错", async () => {
|
|
121
|
+
await expect(registry.remove("pi-sub-anything")).resolves.not.toThrow();
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
describe("降级与健壮性", () => {
|
|
126
|
-
it("文件不存在时 load 返回空数组", () => {
|
|
126
|
+
it("文件不存在时 load 返回空数组", async () => {
|
|
127
127
|
expect(registry.load()).toEqual([]);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
it("损坏 JSON 时 load 返回空数组", () => {
|
|
130
|
+
it("损坏 JSON 时 load 返回空数组", async () => {
|
|
131
131
|
fs.mkdirSync(path.dirname(registryFile), { recursive: true });
|
|
132
132
|
fs.writeFileSync(registryFile, "{ not valid json }}}", "utf-8");
|
|
133
133
|
expect(registry.load()).toEqual([]);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
it("entries 字段缺失时 load 返回空数组", () => {
|
|
136
|
+
it("entries 字段缺失时 load 返回空数组", async () => {
|
|
137
137
|
fs.mkdirSync(path.dirname(registryFile), { recursive: true });
|
|
138
138
|
fs.writeFileSync(registryFile, '{"other": 123}', "utf-8");
|
|
139
139
|
expect(registry.load()).toEqual([]);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
it("save 创建不存在的父目录", () => {
|
|
142
|
+
it("save 创建不存在的父目录", async () => {
|
|
143
143
|
// registryFile 在 tmpAgentDir/subagents/ 下,subagents 目录不存在
|
|
144
144
|
expect(fs.existsSync(path.dirname(registryFile))).toBe(false);
|
|
145
|
-
registry.add(makeEntry());
|
|
145
|
+
await registry.add(makeEntry());
|
|
146
146
|
expect(fs.existsSync(registryFile)).toBe(true);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
it("save 后无残留 .tmp 文件(原子写)", () => {
|
|
150
|
-
registry.add(makeEntry());
|
|
149
|
+
it("save 后无残留 .tmp 文件(原子写)", async () => {
|
|
150
|
+
await registry.add(makeEntry());
|
|
151
151
|
const tmpFile = `${registryFile}.tmp`;
|
|
152
152
|
expect(fs.existsSync(tmpFile)).toBe(false);
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
describe("SPAWN_GRACE_MS 常量", () => {
|
|
157
|
-
it("值为 60s", () => {
|
|
157
|
+
it("值为 60s", async () => {
|
|
158
158
|
expect(SPAWN_GRACE_MS).toBe(60_000);
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
describe("D5a 锁互斥(两写方并发不丢条目)", () => {
|
|
163
|
+
// [HISTORICAL] 2026-08-20 PR #185:真实 proper-lockfile 文件锁 RMW 用例显式超时——
|
|
164
|
+
// 单跑 >1s,整包满并行 + 系统余载下 100/31 次持锁 load→mutate→save 超过 vitest
|
|
165
|
+
// 默认 5s testTimeout(隔离跑通过,纯环境饿死),对齐本包 record-store-index 口径。
|
|
166
|
+
it("两个 registry 实例(模拟两 pi 进程写方)并发各 add 50 条,零丢失", { timeout: 30_000 }, async () => {
|
|
167
|
+
// 失败模式 G 的复现形态:无锁时两实例 load→mutate→save 交错,后写者覆盖
|
|
168
|
+
// 先写者 → 条目丢失。锁内 RMW 应保证 100 条全部落盘。
|
|
169
|
+
const r1 = new WorktreeRegistry(tmpAgentDir);
|
|
170
|
+
const r2 = new WorktreeRegistry(tmpAgentDir);
|
|
171
|
+
const tasks: Promise<void>[] = [];
|
|
172
|
+
for (let i = 0; i < 50; i++) {
|
|
173
|
+
tasks.push(r1.add(makeEntry({ branch: `pi-sub-w1-${i}` })));
|
|
174
|
+
tasks.push(r2.add(makeEntry({ branch: `pi-sub-w2-${i}` })));
|
|
175
|
+
}
|
|
176
|
+
await Promise.all(tasks);
|
|
177
|
+
const loaded = r1.load();
|
|
178
|
+
expect(loaded).toHaveLength(100);
|
|
179
|
+
// 两写方的条目都在(非「后写者全量覆盖」形态)
|
|
180
|
+
expect(loaded.filter((e) => e.branch.startsWith("pi-sub-w1-"))).toHaveLength(50);
|
|
181
|
+
expect(loaded.filter((e) => e.branch.startsWith("pi-sub-w2-"))).toHaveLength(50);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("并发 add + remove 交错:remove 只删自己的 branch", { timeout: 30_000 }, async () => {
|
|
185
|
+
const r1 = new WorktreeRegistry(tmpAgentDir);
|
|
186
|
+
const r2 = new WorktreeRegistry(tmpAgentDir);
|
|
187
|
+
await r1.add(makeEntry({ branch: "pi-sub-keep" }));
|
|
188
|
+
const tasks: Promise<void>[] = [];
|
|
189
|
+
for (let i = 0; i < 30; i++) {
|
|
190
|
+
tasks.push(r2.add(makeEntry({ branch: `pi-sub-n-${i}` })));
|
|
191
|
+
}
|
|
192
|
+
tasks.push(r1.remove("pi-sub-keep"));
|
|
193
|
+
await Promise.all(tasks);
|
|
194
|
+
const loaded = r1.load();
|
|
195
|
+
expect(loaded).toHaveLength(30);
|
|
196
|
+
expect(loaded.find((e) => e.branch === "pi-sub-keep")).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
161
199
|
});
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import type { AgentResult as WorkflowAgentResult, AgentUsage as WorkflowAgentUsage, ToolCallEntry } from "../orchestration/models/types.ts";
|
|
9
9
|
import type { AgentResult as SubagentsAgentResult, AgentUsageTotal, ToolCall } from "./types.ts";
|
|
10
10
|
|
|
11
|
+
/** safeStringify 的 JSON 截断长度(字符),防大对象拼进 toolCalls 挤爆 trace。 */
|
|
12
|
+
const TOOL_ARGS_JSON_MAX_CHARS = 500;
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* D-A10: subagents AgentResult → workflow AgentResult 映射。
|
|
13
16
|
*
|
|
@@ -80,7 +83,7 @@ function mapToolCalls(calls: ToolCall[]): ToolCallEntry[] {
|
|
|
80
83
|
function safeStringify(value: unknown): string {
|
|
81
84
|
try {
|
|
82
85
|
const s = JSON.stringify(value);
|
|
83
|
-
return s.length >
|
|
86
|
+
return s.length > TOOL_ARGS_JSON_MAX_CHARS ? `${s.slice(0, TOOL_ARGS_JSON_MAX_CHARS)}...` : s;
|
|
84
87
|
} catch {
|
|
85
88
|
return String(value);
|
|
86
89
|
}
|
|
@@ -12,6 +12,7 @@ export interface MirrorFlags {
|
|
|
12
12
|
noExtensions: boolean;
|
|
13
13
|
approve: boolean;
|
|
14
14
|
extensionPaths: string[];
|
|
15
|
+
noContextFiles: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -31,7 +32,7 @@ const VALUED_FLAGS = new Set<string>([
|
|
|
31
32
|
const ARGV_FLAG_START = 2;
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
* 从主进程 argv 解析可镜像的 flag(--no-extensions/--approve/--extension)。
|
|
35
|
+
* 从主进程 argv 解析可镜像的 flag(--no-extensions/--approve/--extension/--no-context-files)。
|
|
35
36
|
*
|
|
36
37
|
* 数据源是主 pi 进程的 process.argv:子进程 spawn 的父就是主进程,
|
|
37
38
|
* 主进程 argv 完整保留启动时收到的全部 flag(已运行时验证)。这让子进程
|
|
@@ -39,14 +40,25 @@ const ARGV_FLAG_START = 2;
|
|
|
39
40
|
*
|
|
40
41
|
* 解析规则:
|
|
41
42
|
* - --no-extensions / -ne、--approve / -a:布尔 flag
|
|
43
|
+
* - --no-context-files / -nc:布尔 flag(镜像理由:--extension 镜像会把
|
|
44
|
+
* @zhushanwen/pi-system-prompt 带进每个 subagent,该扩展靠子进程自己的
|
|
45
|
+
* argv 检测此 flag 决定是否注入全局 AGENTS.md——不镜像则用户 opt-out
|
|
46
|
+
* 在 subagent 路径失效)
|
|
42
47
|
* - --extension / -e:支持 `--extension <path>`(空格)与 `--extension=<path>`(等号)
|
|
43
48
|
* - 其他 flag(在 VALUED_FLAGS 中)的值不被误当 extension 路径
|
|
44
49
|
* - positional 参数被忽略
|
|
45
50
|
* - argv[0]/argv[1] 是 bun/pi binary 路径,从 argv[2:] 开始扫
|
|
46
51
|
*/
|
|
52
|
+
// [perf] 进程内 process.argv 引用恒定(唯一调用点 session-runner 每次 spawn 传同一引用),
|
|
53
|
+
// 解析结果按 argv 引用相等 memo——每次 spawn 免重复扫描/正则。测试传新数组引用不命中。
|
|
54
|
+
let memoArgv: readonly string[] | undefined;
|
|
55
|
+
let memoResult: MirrorFlags | undefined;
|
|
56
|
+
|
|
47
57
|
export function mirrorMainProcessFlags(argv: readonly string[]): MirrorFlags {
|
|
58
|
+
if (memoResult !== undefined && memoArgv === argv) return memoResult;
|
|
48
59
|
let hasNoExtensions = false;
|
|
49
60
|
let hasApprove = false;
|
|
61
|
+
let hasNoContextFiles = false;
|
|
50
62
|
const extensionPaths: string[] = [];
|
|
51
63
|
|
|
52
64
|
// argv[0]=runtime(bun), argv[1]=pi binary 路径;flag 从 argv[2] 起
|
|
@@ -62,6 +74,10 @@ export function mirrorMainProcessFlags(argv: readonly string[]): MirrorFlags {
|
|
|
62
74
|
hasApprove = true;
|
|
63
75
|
continue;
|
|
64
76
|
}
|
|
77
|
+
if (tok === "--no-context-files" || tok === "-nc") {
|
|
78
|
+
hasNoContextFiles = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
65
81
|
// 等号形式 --extension=path / -e=path
|
|
66
82
|
const eqMatch = /^(--extension|-e)=(.*)$/.exec(tok);
|
|
67
83
|
if (eqMatch) {
|
|
@@ -86,5 +102,8 @@ export function mirrorMainProcessFlags(argv: readonly string[]): MirrorFlags {
|
|
|
86
102
|
// 其他情况(未知 flag、--flag=val 形式、positional)忽略
|
|
87
103
|
}
|
|
88
104
|
|
|
89
|
-
|
|
105
|
+
const result = { noExtensions: hasNoExtensions, approve: hasApprove, extensionPaths, noContextFiles: hasNoContextFiles };
|
|
106
|
+
memoArgv = argv;
|
|
107
|
+
memoResult = result;
|
|
108
|
+
return result;
|
|
90
109
|
}
|
|
@@ -76,7 +76,9 @@ function readHandshakeSlot(): ChannelRegistryHandshake | undefined {
|
|
|
76
76
|
);
|
|
77
77
|
return undefined;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// 断言目标含必填 version 字段(object/null 已守卫;version 值本身在下一行校验),
|
|
80
|
+
// 对齐 format.ts extractAgentName 的必填字段断言先例,避免全可选断言零校验。
|
|
81
|
+
const version = (slot as { version: unknown }).version;
|
|
80
82
|
if (version !== HANDSHAKE_VERSION) {
|
|
81
83
|
logger.warn(
|
|
82
84
|
`[pi-subagent-workflow] channel handshake version mismatch (got ${String(
|