@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.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 +39 -12
- package/agents/analyst.md +61 -0
- package/agents/coder.md +70 -0
- package/agents/debugger.md +67 -0
- package/agents/doc-reviewer.md +3 -3
- package/agents/explorer.md +50 -18
- package/agents/general-purpose.md +19 -8
- package/agents/orchestrator.md +37 -32
- package/agents/planner.md +45 -11
- package/agents/researcher.md +53 -11
- package/agents/reviewer.md +74 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-registry.test.ts +13 -11
- 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 +170 -14
- 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 +25 -7
- 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 +158 -52
- 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 +598 -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 +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/host-mode.ts +1 -1
- package/src/execution/lifecycle-manager.ts +484 -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-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +290 -75
- 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 +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +204 -22
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- 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 +18 -5
- package/src/interface/__tests__/tool-render.test.ts +15 -13
- 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 +44 -17
- 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 +82 -17
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -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 +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -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 +1 -1
- 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 +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- 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 +29 -10
- 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 +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
- package/workflows/README.md +4 -4
- package/agents/code-reviewer.md +0 -47
- package/agents/context-builder.md +0 -21
- package/agents/oracle.md +0 -34
- package/agents/worker.md +0 -20
|
@@ -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,9 +132,9 @@ 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
|
// ── 用例 ──
|
|
@@ -142,7 +143,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
142
143
|
it("正向:spawn 返回后注册表 pid 同步补全,活 worktree 超宽限不被 scan 误清", async () => {
|
|
143
144
|
// 0. 长驻脚本(子进程 90s 内不退出,模拟长跑子 agent)
|
|
144
145
|
scriptHolder.script = LONG_RUNNING_SCRIPT; // 1. 真实创建 worktree(pid=0 占位)
|
|
145
|
-
handle = wtm.create(repoDir, "rec-1");
|
|
146
|
+
handle = await wtm.create(repoDir, "rec-1");
|
|
146
147
|
expect(readEntry(handle.branch)).toMatchObject({ pid: 0 });
|
|
147
148
|
|
|
148
149
|
// 2. runSpawn 挂后台(不 await——长驻子进程 close 不触发,await 会挂死),
|
|
@@ -170,7 +171,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
170
171
|
vi.setSystemTime(Date.now() + SPAWN_GRACE_MS + 1000);
|
|
171
172
|
|
|
172
173
|
// 5. scan():活 worktree 必须不被清(修复前:pid=0 超宽限 → 误删 → 红)
|
|
173
|
-
wtm.scan();
|
|
174
|
+
await wtm.scan();
|
|
174
175
|
expect(fs.existsSync(handle.path)).toBe(true);
|
|
175
176
|
|
|
176
177
|
// 6. 收尾:真实时钟恢复 + kill 子进程让 runPromise settle
|
|
@@ -183,7 +184,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
183
184
|
await runPromise;
|
|
184
185
|
|
|
185
186
|
// 7. 反向:进程死后 scan 回收真孤儿
|
|
186
|
-
wtm.scan();
|
|
187
|
+
await wtm.scan();
|
|
187
188
|
expect(fs.existsSync(handle.path)).toBe(false);
|
|
188
189
|
const entryAfter = readEntry(handle.branch);
|
|
189
190
|
expect(entryAfter).toBeUndefined();
|
|
@@ -193,7 +194,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
193
194
|
// 0. 短命脚本(子进程立即退出,模拟快速完成的子 agent)
|
|
194
195
|
scriptHolder.script = SHORT_LIVED_SCRIPT;
|
|
195
196
|
// 1. 真实创建 worktree
|
|
196
|
-
handle = wtm.create(repoDir, "rec-2");
|
|
197
|
+
handle = await wtm.create(repoDir, "rec-2");
|
|
197
198
|
|
|
198
199
|
// 2. 短命脚本子进程:spawn 后同步补 pid(修复前 pid=0,且未超宽限 → 不回收 → 红)
|
|
199
200
|
const ctx = makeCtx({
|
|
@@ -218,7 +219,7 @@ describe("worktree pid 注册链路(真实 spawn 集成)", () => {
|
|
|
218
219
|
spawnedPid = entry!.pid;
|
|
219
220
|
|
|
220
221
|
// 4. scan:pid>0 且进程死 → 立即判孤儿回收(无需等宽限)
|
|
221
|
-
wtm.scan();
|
|
222
|
+
await wtm.scan();
|
|
222
223
|
expect(fs.existsSync(handle.path)).toBe(false);
|
|
223
224
|
expect(readEntry(handle.branch)).toBeUndefined();
|
|
224
225
|
}, 15000);
|
|
@@ -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
|
+
* xyz-system-prompt-extension.js 带进每个 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
|
}
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
AgentResult,
|
|
21
21
|
AgentUsage,
|
|
22
22
|
AgentUsageTotal,
|
|
23
|
+
ClosedReason,
|
|
23
24
|
DisplayItem,
|
|
24
25
|
ExecutionMode,
|
|
25
26
|
ExecutionRecord,
|
|
@@ -156,6 +157,10 @@ export function createRecord(
|
|
|
156
157
|
parentRecordId?: string;
|
|
157
158
|
/** subagent 递归深度。顶层=0。 */
|
|
158
159
|
depth?: number;
|
|
160
|
+
/** 对话模式标志(true = 可持续对话,轮次完成进 idle)。默认 undefined/false = 一次性。 */
|
|
161
|
+
chatMode?: boolean;
|
|
162
|
+
/** 空闲超时毫秒数(仅 chatMode 有意义)。覆盖默认 5min。 */
|
|
163
|
+
idleTimeoutMs?: number;
|
|
159
164
|
controller?: AbortController;
|
|
160
165
|
},
|
|
161
166
|
): ExecutionRecord {
|
|
@@ -171,6 +176,8 @@ export function createRecord(
|
|
|
171
176
|
rootSessionId: identity.rootSessionId,
|
|
172
177
|
parentRecordId: identity.parentRecordId,
|
|
173
178
|
depth: identity.depth ?? 0,
|
|
179
|
+
chatMode: identity.chatMode,
|
|
180
|
+
idleTimeoutMs: identity.idleTimeoutMs,
|
|
174
181
|
|
|
175
182
|
// 状态(实时更新)
|
|
176
183
|
status: "running",
|
|
@@ -180,6 +187,8 @@ export function createRecord(
|
|
|
180
187
|
turnCount: 0,
|
|
181
188
|
totalTokens: 0,
|
|
182
189
|
lastError: undefined,
|
|
190
|
+
// 对话轮次计数(首轮 = 0,每完成一轮 finalizeRoundToIdle +1)。非 chatMode 不自增。
|
|
191
|
+
round: 0,
|
|
183
192
|
|
|
184
193
|
// 完成(completeRecord 唯一写点)
|
|
185
194
|
endedAt: undefined,
|
|
@@ -233,6 +242,30 @@ function findRunningToolCall(
|
|
|
233
242
|
return undefined;
|
|
234
243
|
}
|
|
235
244
|
|
|
245
|
+
/**
|
|
246
|
+
* [perf] running toolCall 倒序索引:tool_start push 位置入索引,tool_end 弹尾定位
|
|
247
|
+
*(尾部 = 最后 push 的同名项,与 findRunningToolCall 倒序全扫的语义等价),把每次
|
|
248
|
+
* tool_end 的 O(所有 turns × toolCalls) 扫描降为 O(1)。WeakMap 按 record 实例隔离
|
|
249
|
+
*(createRecord 新实例从空索引开始,不影响旧实例)。
|
|
250
|
+
* 索引 miss(重建 record 的历史 running toolCall / 外部注入工具无 tool_start)
|
|
251
|
+
* 回退 findRunningToolCall 全扫兜底——正确性不依赖索引完整性。
|
|
252
|
+
*/
|
|
253
|
+
const runningToolIndex = new WeakMap<ExecutionRecord, Map<string, Array<{ turn: Turn; idx: number }>>>();
|
|
254
|
+
|
|
255
|
+
function indexToolStart(record: ExecutionRecord, turn: Turn, toolName: string): void {
|
|
256
|
+
let byName = runningToolIndex.get(record);
|
|
257
|
+
if (byName === undefined) {
|
|
258
|
+
byName = new Map();
|
|
259
|
+
runningToolIndex.set(record, byName);
|
|
260
|
+
}
|
|
261
|
+
const arr = byName.get(toolName);
|
|
262
|
+
if (arr === undefined) {
|
|
263
|
+
byName.set(toolName, [{ turn, idx: turn.toolCalls.length - 1 }]);
|
|
264
|
+
} else {
|
|
265
|
+
arr.push({ turn, idx: turn.toolCalls.length - 1 });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
236
269
|
/**
|
|
237
270
|
* 从 AgentEvent 更新 record。所有数据收口进 record.turns[]。
|
|
238
271
|
* - text/thinking:流式累积进 currentTurn()(完整内容,非切片)
|
|
@@ -271,13 +304,30 @@ export function updateFromEvent(record: ExecutionRecord, event: AgentEvent): voi
|
|
|
271
304
|
_status: "running",
|
|
272
305
|
startedTs: Date.now(),
|
|
273
306
|
};
|
|
274
|
-
currentTurn(record)
|
|
307
|
+
const turn = currentTurn(record);
|
|
308
|
+
turn.toolCalls.push(tc);
|
|
309
|
+
indexToolStart(record, turn, event.toolName);
|
|
275
310
|
return;
|
|
276
311
|
}
|
|
277
312
|
|
|
278
|
-
// ── tool_end
|
|
313
|
+
// ── tool_end:索引弹尾 O(1) 定位 running 同名 toolCall,miss 回退全扫兜底 ──
|
|
279
314
|
case "tool_end": {
|
|
280
|
-
|
|
315
|
+
let matched: readonly [Turn, number] | undefined;
|
|
316
|
+
const byName = runningToolIndex.get(record);
|
|
317
|
+
const arr = byName?.get(event.toolName);
|
|
318
|
+
if (arr !== undefined && arr.length > 0) {
|
|
319
|
+
const item = arr[arr.length - 1];
|
|
320
|
+
arr.pop();
|
|
321
|
+
const tc = item.turn.toolCalls[item.idx];
|
|
322
|
+
if (tc !== undefined && tc._status === "running") {
|
|
323
|
+
matched = [item.turn, item.idx] as const;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (matched === undefined) {
|
|
327
|
+
// 兜底:重建 record 的历史 running toolCall(索引未覆盖)、索引项被外部路径
|
|
328
|
+
// 置非 running 等场景——保持与旧实现一致的跨 turn 倒序全扫。
|
|
329
|
+
matched = findRunningToolCall(record, event.toolName);
|
|
330
|
+
}
|
|
281
331
|
if (matched !== undefined) {
|
|
282
332
|
const [turn, i] = matched;
|
|
283
333
|
const tc = turn.toolCalls[i]!;
|
|
@@ -488,6 +538,51 @@ export function getFullText(record: ExecutionRecord): string {
|
|
|
488
538
|
.join("\n\n");
|
|
489
539
|
}
|
|
490
540
|
|
|
541
|
+
/**
|
|
542
|
+
* [增量通知] 从 fromTurnIndex 起聚合 turn 文本(getFullText 的增量视图)。
|
|
543
|
+
*
|
|
544
|
+
* 语义:record.turns.slice(Math.max(0, fromTurnIndex)) 的 text 数组
|
|
545
|
+
* filter(text => text.length > 0)(判据与 getFullText 逐字对齐——空白串 " " 按非空
|
|
546
|
+
* 处理,不发散)后 join("\n\n")。
|
|
547
|
+
*
|
|
548
|
+
* 不变式(单测锁死):
|
|
549
|
+
* - fromTurnIndex = 0 与 getFullText(record) 逐字节等价(one-shot/首轮回退零成本)
|
|
550
|
+
* - fromTurnIndex >= turns.length 返回 ""(不抛错、不回绕)
|
|
551
|
+
* - fromTurnIndex < 0 规范化为 0(防御,Math.max 不产生 slice 负偏移语义)
|
|
552
|
+
*
|
|
553
|
+
* 纯函数只读;getFullText 本体与既有调用方(collectResult 等)零改动。
|
|
554
|
+
* 唯一调用点 onRoundSettled 的增量派生(record.roundBaseTurnIndex ?? 0)。
|
|
555
|
+
*/
|
|
556
|
+
export function getFullTextFrom(record: ExecutionRecord, fromTurnIndex: number): string {
|
|
557
|
+
return record.turns
|
|
558
|
+
.slice(Math.max(0, fromTurnIndex))
|
|
559
|
+
.map((t) => t.text)
|
|
560
|
+
.filter((text) => text.length > 0)
|
|
561
|
+
.join("\n\n");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* [增量通知] 轮次边界推进公式(D1):返回下一轮增量的 turns[] 起始下标。
|
|
566
|
+
*
|
|
567
|
+
* record.turns.length - (末 turn 存在且 !closed 且 text.length === 0 ? 1 : 0)
|
|
568
|
+
*
|
|
569
|
+
* - 末 turn 已闭合(pi 现序常态:带 usage 的 message_end 恒先于 turn_end,settle 时
|
|
570
|
+
* 全闭合)→ 返回 length,下一轮从新 turn 起。
|
|
571
|
+
* - 末 turn 是滞后 message_end 开出的空 turn(防御分支,pi 现序不可达)→ 不计入边界
|
|
572
|
+
* (-1),留在下一轮增量内:新轮首个 text_delta 经 currentTurn 复用该空 turn,复用
|
|
573
|
+
* 累积被 slice(from) 覆盖;若直用 turns.length 会把这段文本挤出 slice 范围静默丢失(D1)。
|
|
574
|
+
* - 末 turn 未闭合且 text 非空(防御形态,pi 现序不可达)→ 计入本轮返回 length(该形态
|
|
575
|
+
* 即 onRoundSettled 推进前观测哨 logger.warn 的触发条件)。
|
|
576
|
+
*
|
|
577
|
+
* 纯函数只读不写 record;唯一调用点 onRoundSettled 第 5 步(notify 之后推进)。
|
|
578
|
+
*/
|
|
579
|
+
export function nextRoundBaseTurnIndex(record: ExecutionRecord): number {
|
|
580
|
+
const last = record.turns[record.turns.length - 1];
|
|
581
|
+
const trailingOpenEmpty =
|
|
582
|
+
last !== undefined && !last.closed && last.text.length === 0 ? 1 : 0;
|
|
583
|
+
return record.turns.length - trailingOpenEmpty;
|
|
584
|
+
}
|
|
585
|
+
|
|
491
586
|
/**
|
|
492
587
|
* 聚合所有 turn 的 toolCalls(扁平化),并 strip InternalToolCall 的内部字段。
|
|
493
588
|
* 供 collectResult / schema enforcement 读,替代旧闭包 toolCalls 旁路。
|
|
@@ -499,6 +594,17 @@ export function getAllToolCalls(record: ExecutionRecord): ToolCall[] {
|
|
|
499
594
|
return record.turns.flatMap((t) => t.toolCalls.map(stripInternal));
|
|
500
595
|
}
|
|
501
596
|
|
|
597
|
+
/**
|
|
598
|
+
* 聚合所有 turn 的 toolCalls 总数(免克隆计数)。
|
|
599
|
+
*
|
|
600
|
+
* getAllToolCalls 会 flatMap + strip 克隆出完整数组——只需计数的调用方(渲染签名
|
|
601
|
+
* 每 200ms tick 调用一次)用它是纯浪费;本函数 reduce 累加各 turn 的 length,零分配。
|
|
602
|
+
* 与 getAllToolCalls(...).length 恒等(同一 turns 源)。
|
|
603
|
+
*/
|
|
604
|
+
export function countAllToolCalls(record: ExecutionRecord): number {
|
|
605
|
+
return record.turns.reduce((sum, t) => sum + t.toolCalls.length, 0);
|
|
606
|
+
}
|
|
607
|
+
|
|
502
608
|
/** 把 InternalToolCall 映射回纯净的 ToolCall(丢弃 _status / startedTs)。 */
|
|
503
609
|
function stripInternal(tc: InternalToolCall): ToolCall {
|
|
504
610
|
return {
|
|
@@ -540,21 +646,26 @@ export function getTotalUsage(record: ExecutionRecord): AgentUsageTotal | undefi
|
|
|
540
646
|
/**
|
|
541
647
|
* status 状态机的 CAS 互斥锁。仅当 `record.status === "running"` 时改为 target
|
|
542
648
|
* 并返回 true,否则返回 false。**status 状态机本身就是互斥锁**——终态
|
|
543
|
-
* (
|
|
649
|
+
* (closed/cancelled)不可逆,check-then-set 在 JS 单线程事件循环里天然原子。
|
|
544
650
|
*
|
|
545
651
|
* 用途:executor 的收尾竞争。cancelBackground 与 background detached 完成回调
|
|
546
652
|
* 都调 tryTransition 抢锁:抢到负责完整收尾,没抢到闭嘴不做事。
|
|
547
653
|
*
|
|
548
|
-
* target
|
|
549
|
-
*
|
|
550
|
-
*
|
|
654
|
+
* target 仅限正常执行流的两终态(closed + cancelled)。
|
|
655
|
+
* closed 携带 closedReason(L2 原因子枚举),由重建路径或正常执行流写入。
|
|
656
|
+
* 重建路径也可用 markReconstructedStatus 直接赋值(跳过 CAS)。
|
|
657
|
+
*
|
|
658
|
+
* @param closedReason closed 终态的 L2 关闭原因。仅 target="closed" 时有意义;
|
|
659
|
+
* target="cancelled" 时忽略。缺省 "gc"(通用完成/失败)。
|
|
551
660
|
*/
|
|
552
661
|
export function tryTransition(
|
|
553
662
|
record: ExecutionRecord,
|
|
554
|
-
target: "
|
|
663
|
+
target: "closed",
|
|
664
|
+
closedReason?: ClosedReason,
|
|
555
665
|
): boolean {
|
|
556
666
|
if (record.status !== "running") return false;
|
|
557
667
|
record.status = target;
|
|
668
|
+
record.closedReason = closedReason ?? "gc";
|
|
558
669
|
return true;
|
|
559
670
|
}
|
|
560
671
|
|
|
@@ -577,13 +688,18 @@ export function markReconstructedStatus(
|
|
|
577
688
|
* 不修改 turns/totalTokens——已由 updateFromEvent 累积,completeRecord 只读不重置。
|
|
578
689
|
*
|
|
579
690
|
* ⚠ 前置条件:调用方必须先通过 tryTransition 抢到锁(status 已被 CAS 设为 target)。
|
|
691
|
+
*
|
|
692
|
+
* @param closedReason closed 终态的 L2 关闭原因。仅 status="closed" 时写入;
|
|
693
|
+
* "cancelled" 时忽略。
|
|
580
694
|
*/
|
|
581
695
|
export function completeRecord(
|
|
582
696
|
record: ExecutionRecord,
|
|
583
697
|
result: AgentResult,
|
|
584
|
-
status: "
|
|
698
|
+
status: "closed",
|
|
699
|
+
closedReason?: ClosedReason,
|
|
585
700
|
): void {
|
|
586
701
|
record.status = status;
|
|
702
|
+
record.closedReason = closedReason ?? "gc";
|
|
587
703
|
record.endedAt = Date.now();
|
|
588
704
|
record.agentResult = result;
|
|
589
705
|
record.result = result.text;
|
|
@@ -663,6 +779,7 @@ export function snapshot(record: ExecutionRecord): RecordSnapshot {
|
|
|
663
779
|
task: record.task,
|
|
664
780
|
slug: record.slug,
|
|
665
781
|
status: record.status,
|
|
782
|
+
chatMode: record.chatMode,
|
|
666
783
|
turns: record.turnCount,
|
|
667
784
|
totalTokens: record.totalTokens,
|
|
668
785
|
startedAt: record.startedAt,
|
|
@@ -27,7 +27,7 @@ import type { ModelConfigService } from "./model-config-service.ts";
|
|
|
27
27
|
import { getSubagentSessionDir } from "./path-encoding.ts";
|
|
28
28
|
import type { RecordStore } from "./record-store.ts";
|
|
29
29
|
import { writeCancelledTombstone } from "./tombstone-store.ts";
|
|
30
|
-
import type { AgentResult, ExecutionRecord } from "./types.ts";
|
|
30
|
+
import type { AgentResult, ClosedReason, ExecutionRecord } from "./types.ts";
|
|
31
31
|
import type { WorktreeManager } from "./worktree-manager.ts";
|
|
32
32
|
|
|
33
33
|
const logger = getLogger("subagents");
|
|
@@ -40,10 +40,10 @@ export interface FinalizeDeps {
|
|
|
40
40
|
modelService: ModelConfigService;
|
|
41
41
|
/** Pi ExtensionAPI(仅用 appendEntry 记录 manifest 写失败事件)。null 在 dispose 后。 */
|
|
42
42
|
pi: { appendEntry?: (type: string, data: unknown) => void } | null;
|
|
43
|
-
/** 清节流状态(防 trailing timer 在 record 归档后误发陈旧 onUpdate)。 */
|
|
44
|
-
clearThrottle(id: string): void;
|
|
45
43
|
/** pending-notifications 终态注销(绑定 pi.events.emit,由调用方闭包提供)。 */
|
|
46
44
|
emitUnregister(id: string, status: string): void;
|
|
45
|
+
// [review 修复] 已删除 redeliverPending 回调(MF-1 消费确认制补投):pendingMessages
|
|
46
|
+
// 三段消费链随 deliverToRunning 一并移除(无生产调用方,死机制)。
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -57,11 +57,9 @@ export async function doFinalizeRecord(
|
|
|
57
57
|
deps: FinalizeDeps,
|
|
58
58
|
record: ExecutionRecord,
|
|
59
59
|
result: AgentResult,
|
|
60
|
-
status: "
|
|
60
|
+
status: "closed",
|
|
61
|
+
closedReason?: ClosedReason,
|
|
61
62
|
): Promise<void> {
|
|
62
|
-
// 终态清节流状态:防 trailing timer 在 record 归档后误发陈旧 onUpdate
|
|
63
|
-
deps.clearThrottle(record.id);
|
|
64
|
-
|
|
65
63
|
// ── Step 0: collectPatch(best-effort)──
|
|
66
64
|
// [MF#3] patchFile 写到 worktree 之外(sessionsDir/<branch>.patch),避免被 cleanup 删除;
|
|
67
65
|
// 路径回填 record.patchFile,供调用方(tool result / /subagents list)应用。
|
|
@@ -73,7 +71,7 @@ export async function doFinalizeRecord(
|
|
|
73
71
|
);
|
|
74
72
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
75
73
|
const patchFile = path.join(sessionsDir, `${record.worktreeHandle.branch}.patch`);
|
|
76
|
-
const patch = deps.worktreeManager.collectPatch(record.worktreeHandle, patchFile);
|
|
74
|
+
const patch = await deps.worktreeManager.collectPatch(record.worktreeHandle, patchFile);
|
|
77
75
|
if (patch.written) record.patchFile = patchFile;
|
|
78
76
|
} catch (pe: unknown) {
|
|
79
77
|
bestEffort(pe, "collectPatch (finalizeRecord Step0)");
|
|
@@ -82,7 +80,7 @@ export async function doFinalizeRecord(
|
|
|
82
80
|
|
|
83
81
|
// ── Step 1: completeRecord(B9: 抛错→后续仍执行)──
|
|
84
82
|
try {
|
|
85
|
-
completeRecord(record, result, status);
|
|
83
|
+
completeRecord(record, result, status, closedReason);
|
|
86
84
|
} catch (err) {
|
|
87
85
|
bestEffort(err, "completeRecord (finalizeRecord B9)", "error");
|
|
88
86
|
}
|
|
@@ -100,8 +98,8 @@ export async function doFinalizeRecord(
|
|
|
100
98
|
// (否则 worktree 泄漏)。各件独立 try/catch,互不阻断。
|
|
101
99
|
if (record.sessionFile) {
|
|
102
100
|
try {
|
|
103
|
-
// MF-1 fix: cancelled
|
|
104
|
-
if (
|
|
101
|
+
// MF-1 fix / v4 B-1: cancelled(closedReason='cancelled')写 tombstone 而非 finalized,防重建丢失 cancelled
|
|
102
|
+
if (closedReason === "cancelled") {
|
|
105
103
|
writeCancelledTombstone(record.sessionFile, {
|
|
106
104
|
id: record.id,
|
|
107
105
|
status: "cancelled",
|
|
@@ -118,7 +116,7 @@ export async function doFinalizeRecord(
|
|
|
118
116
|
}
|
|
119
117
|
if (record.worktreeHandle) {
|
|
120
118
|
try {
|
|
121
|
-
deps.worktreeManager.cleanup(record.worktreeHandle);
|
|
119
|
+
await deps.worktreeManager.cleanup(record.worktreeHandle);
|
|
122
120
|
} catch (err) {
|
|
123
121
|
bestEffort(err, "worktree cleanup (finalizeRecord Step3)");
|
|
124
122
|
}
|
|
@@ -146,7 +144,8 @@ export async function doFinalizeRecord(
|
|
|
146
144
|
rootSessionId: record.rootSessionId ?? "",
|
|
147
145
|
parentRecordId: record.parentRecordId,
|
|
148
146
|
agentName: record.agent,
|
|
149
|
-
|
|
147
|
+
// v4 B-1: manifest status 统一为 closed(cancelled 折入 closed,区分靠 tombstone sidecar)
|
|
148
|
+
status: "closed",
|
|
150
149
|
createdAt: record.startedAt,
|
|
151
150
|
completedAt: record.endedAt ?? Date.now(),
|
|
152
151
|
sessionFile: record.sessionFile,
|
|
@@ -163,3 +162,81 @@ export async function doFinalizeRecord(
|
|
|
163
162
|
});
|
|
164
163
|
}
|
|
165
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 对话模式轮次完成收尾:record 进 idle 态(非终态化,等待续聊)。
|
|
168
|
+
*
|
|
169
|
+
* 与 doFinalizeRecord 的关键区别(M2-A idle 语义):
|
|
170
|
+
* - 不调 completeRecord(record 不冻结,保留 turns[] 等运行时状态供续聊累积)
|
|
171
|
+
* - 不调 store.archive(record 留内存,getMutable 可查、list 可见)
|
|
172
|
+
* - 不 cleanup worktree(保留对话模式工作目录)
|
|
173
|
+
* - 不写 manifest(idle 非终态,manifest 是终态诊断辅助)
|
|
174
|
+
* - 删 .alive marker(进程已 SIGTERM 回收,不再是活进程)
|
|
175
|
+
* - emitUnregister(进程已死,从 pending 活跃后代差集移除;record 留内存不 archive)
|
|
176
|
+
*
|
|
177
|
+
* MF-2:设 record.result = result.text(否则 notifier idle 回复正文恒为 "(empty)",
|
|
178
|
+
* G1/G2 多轮回复送达不成立)。失败轮次(result.success=false,MF-6 回退 idle 路径)
|
|
179
|
+
* 的 result.text 可能为空,用 result.error 兜底让 notify 可读。
|
|
180
|
+
*
|
|
181
|
+
* 状态:record.status = "idle"(覆盖 tryTransition 设的 done/failed),record.round += 1。
|
|
182
|
+
* 各步骤 best-effort 互不阻断(参照 doFinalizeRecord 的 bestEffort 用法)。
|
|
183
|
+
*
|
|
184
|
+
* @param deps 与 doFinalizeRecord 同源(从 SubagentService 注入)
|
|
185
|
+
* @param result 本轮 AgentResult(MF-2:result.text 写入 record.result 供 notifier idle 回复)
|
|
186
|
+
*/
|
|
187
|
+
export async function doFinalizeRoundToIdle(
|
|
188
|
+
deps: FinalizeDeps,
|
|
189
|
+
record: ExecutionRecord,
|
|
190
|
+
result: AgentResult,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
// MF-2:设 record.result 供 notifier idle 回复正文(否则恒 "(empty)",G1/G2 不成立)。
|
|
193
|
+
// MF-6 兜底:失败轮次(success=false)的 result.text 可能为空,用 error 让 notify 可读。
|
|
194
|
+
// [增量 G2] 第三分支 chatMode 条件占位:本写点被 one-shot 成功路径共用(subagent-service
|
|
195
|
+
// runAndFinalize 的 `!record.chatMode && !aborted && result.success` 分支 → finalizeRoundToIdle),
|
|
196
|
+
// 非 chatMode 必须保持 `|| record.result` 现状——one-shot 空文本成功完成(collectResult
|
|
197
|
+
// getFullText 返回 ""、success=true、真实可达)时 result 前值 undefined → notifier
|
|
198
|
+
// buildLlmContent 的 `record.result ?? "(empty)"` 兜底确定性产出 "(empty)",通知逐字节
|
|
199
|
+
// 不变(G4);无条件替换会把文案漂移为占位。chatMode 空增量轮 → 固定占位
|
|
200
|
+
// "(no output this round)"(D5:增量语义下沿用旧 record.result = 上一轮增量,本轮通知
|
|
201
|
+
// 正文 = 上一轮内容,父 agent 误读为原样重复回复)。
|
|
202
|
+
let nextResult: string | undefined;
|
|
203
|
+
if (result.text) {
|
|
204
|
+
nextResult = result.text;
|
|
205
|
+
} else if (result.error) {
|
|
206
|
+
nextResult = `round did not complete: ${result.error}`;
|
|
207
|
+
} else if (record.chatMode) {
|
|
208
|
+
nextResult = "(no output this round)";
|
|
209
|
+
} else {
|
|
210
|
+
nextResult = record.result;
|
|
211
|
+
}
|
|
212
|
+
record.result = nextResult;
|
|
213
|
+
|
|
214
|
+
// 删 .alive marker(进程已 SIGTERM 回收)。
|
|
215
|
+
// sessionFile 窗口期可能 undefined(极少——对话模式轮次完成意味着 session 已跑过),
|
|
216
|
+
// 缺失时跳过但仍设内存 idle(重启后磁盘重建会落到 crashed,边界可接受)。
|
|
217
|
+
if (record.sessionFile) {
|
|
218
|
+
try {
|
|
219
|
+
removeAliveMarker(record.sessionFile);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
bestEffort(err, "removeAliveMarker (doFinalizeRoundToIdle)");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// pending-notifications:进程已死,从活跃后代差集移除(record 留内存不 archive,
|
|
226
|
+
// v4 B-1:record 现为 running-resumable,但 pending 注册的是进程活跃性,进程死了需注销)。
|
|
227
|
+
deps.emitUnregister(record.id, "running");
|
|
228
|
+
|
|
229
|
+
// 状态机(v4 B-1):record 保持 running(旧 idle 折入 running,覆盖 tryTransition 设的 closed,
|
|
230
|
+
// 可冷路径 resume),轮次计数 +1。idleSince 时间戳独立保留供 GC 判据。
|
|
231
|
+
// [S10] closedReason 同步清除:调用方(runAndFinalize catch / MF-6 分支)先 tryTransition
|
|
232
|
+
// 设了 closed+closedReason 再回退 running——不清则 "gc"/"cancelled" 残留在 running record 上,
|
|
233
|
+
// 泄漏进 list 投影与后续 notify 载荷(toNotifyRecord 透传 record.closedReason),让一个
|
|
234
|
+
// 活跃 record 看起来像已被某原因关闭过。
|
|
235
|
+
record.status = "running";
|
|
236
|
+
record.closedReason = undefined;
|
|
237
|
+
record.round = (record.round ?? 0) + 1;
|
|
238
|
+
record.idleSince = Date.now();
|
|
239
|
+
|
|
240
|
+
// [review 修复] 已删除残留 pendingMessages 的 redeliverPending 补投段(MF-1 消费
|
|
241
|
+
// 确认安全网):三段消费链随 deliverToRunning 一并移除,本段不可达。
|
|
242
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// handler 工厂分流、stdio 选择等消费点统一调用。
|
|
8
8
|
//
|
|
9
9
|
// 判定依据见 AGENTS.md「运行时环境区分」章节 +
|
|
10
|
-
// docs/pi-tui-development-guide.md 第四部分第 8 节。
|
|
10
|
+
// docs/extensions/pi-tui-development-guide.md 第四部分第 8 节。
|
|
11
11
|
// ExtensionMode 来自 Pi 源码 packages/coding-agent/src/core/extensions/types.ts:299
|
|
12
12
|
// (dist 中 core/extensions/types.d.ts:207)。
|
|
13
13
|
//
|