@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
|
@@ -25,18 +25,47 @@ const RECENT_UNREGISTER_WINDOW_MS = 60_000;
|
|
|
25
25
|
|
|
26
26
|
/** 判定结果:count > 0 = 有活跃后代(应保持进程等唤醒)。 */
|
|
27
27
|
export interface ActivePendingResult {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
count: number;
|
|
29
|
+
/** 最近窗口内(60s)有 pending:unregister——后代刚完成,唤醒通知可能在路上。 */
|
|
30
|
+
recentUnregister: boolean;
|
|
31
|
+
/** 读取/解析失败的原因(undefined = 成功)。调用方对 error 采取保守策略(不 kill)。 */
|
|
32
|
+
error?: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* [perf] per-file 增量游标:session 文件 append-only,同文件重复判定(层主被多个
|
|
37
|
+
* 后代唤醒 N 次 → N 次 agent_end)只需读上次 offset 之后的新增行,累计 entries
|
|
38
|
+
* 差集语义与全量读完全一致。fork 继承的大 session(数十 MB)从「每次整读」降为
|
|
39
|
+
* 「首读全量 + 后续增量」。
|
|
40
|
+
* truncate/重建防御:size < offset → 重置全读。EOF 半行(写入竞态)不入账,
|
|
41
|
+
* offset 只推进到完整行边界,下次补读。
|
|
42
|
+
* 内存量级:每 sessionFile 一条(offset + 后代 entries,均有限);测试隔离用
|
|
43
|
+
* clearPendingCursors()。
|
|
44
|
+
*/
|
|
45
|
+
interface PendingReadCursor {
|
|
46
|
+
offset: number;
|
|
47
|
+
entries: unknown[];
|
|
48
|
+
latestUnregisterMs: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const cursors = new Map<string, PendingReadCursor>();
|
|
52
|
+
|
|
53
|
+
/** 清空增量游标缓存(测试隔离用)。 */
|
|
54
|
+
export function clearPendingCursors(): void {
|
|
55
|
+
cursors.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** [taste/no-unsafe-cast] pending 行的最小结构守卫:非 null object 即可(字段访问
|
|
59
|
+
* 侧均做了 undefined 检查,全可选类型断言无校验意义,改守卫带运行时检查)。 */
|
|
60
|
+
function isPendingLineLike(v: unknown): v is { customType?: string; timestamp?: string } {
|
|
61
|
+
return typeof v === "object" && v !== null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 读 session 文件计算活跃 pending 数(增量读,见 PendingReadCursor)。
|
|
37
66
|
*
|
|
38
67
|
* 快速路径:行内含 pending 值(`"pending:register"` / `"pending:unregister"`)才解析,
|
|
39
|
-
*
|
|
68
|
+
* 大量 message 行只付 includes 扫描跳过 JSON.parse。
|
|
40
69
|
* [S-4] 按值匹配而非 `"customType":"pending:` 序列化格式——后者耦合 pi 的 JSON 序列化
|
|
41
70
|
* 空格习惯(冒号后无空格),pi 改 pretty-print 会导致全过滤 → count=0 → keep-alive
|
|
42
71
|
* 静默失效 → recursive tree 被杀、steer 丢失。值字符串本身不受序列化空格影响。
|
|
@@ -45,45 +74,87 @@ export interface ActivePendingResult {
|
|
|
45
74
|
* 坏行跳过(append 中途崩溃的截断行)。
|
|
46
75
|
*/
|
|
47
76
|
export function readActivePendingFromSessionFile(
|
|
48
|
-
|
|
77
|
+
sessionFile: string | undefined,
|
|
49
78
|
): ActivePendingResult {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
if (!sessionFile) {
|
|
80
|
+
return { count: 0, recentUnregister: false, error: "no sessionFile (handshake not settled)" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let size: number;
|
|
84
|
+
try {
|
|
85
|
+
size = fs.statSync(sessionFile).size;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
count: 0,
|
|
89
|
+
recentUnregister: false,
|
|
90
|
+
error: `session file unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let cursor = cursors.get(sessionFile);
|
|
95
|
+
if (cursor === undefined || size < cursor.offset) {
|
|
96
|
+
// 首次(全量)或文件被 truncate/重建(offset 越界)→ 重置从头读
|
|
97
|
+
cursor = { offset: 0, entries: [], latestUnregisterMs: 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let chunk: string;
|
|
101
|
+
try {
|
|
102
|
+
if (cursor.offset === 0) {
|
|
103
|
+
chunk = fs.readFileSync(sessionFile, "utf-8");
|
|
104
|
+
} else {
|
|
105
|
+
const fd = fs.openSync(sessionFile, "r");
|
|
106
|
+
try {
|
|
107
|
+
const len = size - cursor.offset;
|
|
108
|
+
const buf = Buffer.alloc(len);
|
|
109
|
+
let total = 0;
|
|
110
|
+
while (total < len) {
|
|
111
|
+
const n = fs.readSync(fd, buf, total, len - total, cursor.offset + total);
|
|
112
|
+
if (n <= 0) break;
|
|
113
|
+
total += n;
|
|
114
|
+
}
|
|
115
|
+
chunk = buf.toString("utf-8", 0, total);
|
|
116
|
+
} finally {
|
|
117
|
+
fs.closeSync(fd);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return {
|
|
122
|
+
count: 0,
|
|
123
|
+
recentUnregister: false,
|
|
124
|
+
error: `session file unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 只消费到最后一个完整行:EOF 半行(append 写入竞态)不入账,offset 不推进,
|
|
129
|
+
// 下次从该行起点重读(补全后正常入账)。
|
|
130
|
+
const lastNl = chunk.lastIndexOf("\n");
|
|
131
|
+
const complete = lastNl === -1 ? "" : chunk.slice(0, lastNl);
|
|
132
|
+
cursor.offset += Buffer.byteLength(complete, "utf-8");
|
|
133
|
+
cursors.set(sessionFile, cursor);
|
|
63
134
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
135
|
+
for (const line of complete.split("\n")) {
|
|
136
|
+
// [S-4] 按值匹配:countActiveFromEntries 只消费 register/unregister 两种 customType,
|
|
137
|
+
// 故只需检测这两个值字符串;冒号前后空格变化不影响(值始终是连续子串)。
|
|
138
|
+
if (!line.includes('"pending:register"') && !line.includes('"pending:unregister"')) continue;
|
|
139
|
+
try {
|
|
140
|
+
const entry: unknown = JSON.parse(line);
|
|
141
|
+
if (!isPendingLineLike(entry)) continue;
|
|
142
|
+
cursor.entries.push(entry);
|
|
143
|
+
if (entry.customType === "pending:unregister" && entry.timestamp) {
|
|
144
|
+
const ts = Date.parse(entry.timestamp);
|
|
145
|
+
if (Number.isFinite(ts) && ts > cursor.latestUnregisterMs) cursor.latestUnregisterMs = ts;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// 截断行/坏行跳过——不影响其余 entry 的差集判定(罕见:append 中途崩溃)
|
|
149
|
+
console.debug("[session-pending] skipped malformed pending line in", sessionFile);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
82
152
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
153
|
+
const active = countActiveFromEntries(cursor.entries);
|
|
154
|
+
return {
|
|
155
|
+
count: active.count,
|
|
156
|
+
recentUnregister:
|
|
157
|
+
cursor.latestUnregisterMs > 0 &&
|
|
158
|
+
Date.now() - cursor.latestUnregisterMs < RECENT_UNREGISTER_WINDOW_MS,
|
|
159
|
+
};
|
|
89
160
|
}
|
|
@@ -98,6 +98,17 @@ export interface SubagentIdentityData {
|
|
|
98
98
|
depth?: number;
|
|
99
99
|
/** [MF#4] 本 session 的 fork 深度(session-runner 写入 parentForkDepth+1)。旧文件可能缺失。 */
|
|
100
100
|
forkDepth?: number;
|
|
101
|
+
/**
|
|
102
|
+
* 对话模式标志(可持续对话 subagent)。旧文件缺失 → undefined(按一次性模式处理)。
|
|
103
|
+
* session-runner 写入;reconstructFromFile 经 ...identity 展开自动透传到 ReconstructedRecord。
|
|
104
|
+
*/
|
|
105
|
+
chatMode?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* [review round2] worktree 隔离标志(该 subagent 创建时 worktree:true)。旧文件缺失 →
|
|
108
|
+
* undefined。仅供跨重启重建路径判定「worktree 绑定已丢失」——WorktreeHandle 本身不可
|
|
109
|
+
* 序列化,跨重启后无法 reattach,续聊须拒绝(防 resume cwd 静默回落主 repo 破坏隔离)。
|
|
110
|
+
*/
|
|
111
|
+
worktree?: boolean;
|
|
101
112
|
/** @deprecated 兼容旧文件:旧 identity entry 写的是 parentSessionId,读取时 fallback 到 rootSessionId。 */
|
|
102
113
|
parentSessionId?: string;
|
|
103
114
|
}
|
|
@@ -142,9 +153,21 @@ export interface ReconstructedRecord {
|
|
|
142
153
|
depth: number;
|
|
143
154
|
/** [MF#4] 本 session 的 fork 深度(来自 identity custom entry;旧文件为 undefined)。 */
|
|
144
155
|
forkDepth: number | undefined;
|
|
156
|
+
/** 对话模式标志(来自 identity custom entry;旧文件为 undefined)。 */
|
|
157
|
+
chatMode?: boolean;
|
|
158
|
+
/** [review round2] worktree 隔离标志(见 SubagentIdentityData.worktree)。 */
|
|
159
|
+
worktree?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* 对话轮次计数(非 identity entry 字段,reconstructFromFile 不填)。
|
|
162
|
+
* V2 idle record 的 round 只在内存维护(doFinalizeRoundToIdle 递增),磁盘重建不恢复。
|
|
163
|
+
*/
|
|
164
|
+
round?: number;
|
|
145
165
|
sessionFile: string;
|
|
146
166
|
// ── 可变状态(来自 message entries)──
|
|
147
167
|
status: ExecutionStatus;
|
|
168
|
+
/** L2 关闭原因子枚举(仅 status="closed" 时有意义)。SP-1 新增。
|
|
169
|
+
* 从 stopReason 推导:error/aborted → gc;其余 → gc。 */
|
|
170
|
+
closedReason?: import("./types.ts").ClosedReason;
|
|
148
171
|
turns: Turn[];
|
|
149
172
|
turnCount: number;
|
|
150
173
|
totalTokens: number;
|
|
@@ -276,6 +299,9 @@ interface PendingToolCall {
|
|
|
276
299
|
*
|
|
277
300
|
* status 由最后一条 assistant message 的 stopReason 推导(error/aborted → failed,
|
|
278
301
|
* 其余 → done)。cancelled 由 tombstone override(record-store 层),本函数不感知。
|
|
302
|
+
*
|
|
303
|
+
* [perf] 本函数是重路径(全文读 + 全 entry 解析)。列表扫描用 readIdentityHeader
|
|
304
|
+
* (只读头部 identity),详情才走本函数(RecordStore.getFullRecord 懒加载)。
|
|
279
305
|
*/
|
|
280
306
|
export function reconstructFromFile(sessionFile: string): ReconstructedRecord | undefined {
|
|
281
307
|
// 读文件 + 逐行 JSON.parse(session.jsonl = newline-delimited JSON)。
|
|
@@ -338,8 +364,6 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
|
|
|
338
364
|
const pending: PendingToolCall[] = [];
|
|
339
365
|
let lastError: string | undefined;
|
|
340
366
|
let totalTokens = 0;
|
|
341
|
-
/** 最后一条 assistant message 的 stopReason(推导终态 status)。 */
|
|
342
|
-
let lastStopReason: string | undefined;
|
|
343
367
|
/** 最后一条 entry 的时间戳(ms),推导 endedAt(避免重建 record 耗时随墙钟无限增长)。 */
|
|
344
368
|
let lastEntryTsMs: number | undefined;
|
|
345
369
|
|
|
@@ -386,7 +410,7 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
|
|
|
386
410
|
// stopReason 驱动 lastError(与 updateFromEvent 一致):
|
|
387
411
|
// error/aborted → 设 lastError;stop(正常结束)→ 清 lastError(镜像 turn_end 的清除语义,
|
|
388
412
|
// 即前序 turn 的瞬态 error 在后续成功 turn 后恢复,不误判 success=false)。
|
|
389
|
-
lastStopReason
|
|
413
|
+
// [v4 B-1] status 恒 closed 后 lastStopReason 不再参与推导,死变量已删。
|
|
390
414
|
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
391
415
|
lastError = msg.errorMessage ?? msg.stopReason;
|
|
392
416
|
} else if (msg.stopReason === "stop") {
|
|
@@ -429,10 +453,10 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
|
|
|
429
453
|
const eventLog = deriveEventLog(turns, lastError, identity.startedAt);
|
|
430
454
|
|
|
431
455
|
// 终态 status:最后一条 assistant message 的 stopReason 推导(与 finalizeRecord 的判定一致)。
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
456
|
+
// SP-1:done/failed 合并为 closed,cancelled 由 tombstone override(record-store 层),本函数不感知。
|
|
457
|
+
const status: ExecutionStatus = "closed";
|
|
458
|
+
// closedReason 统 gc(通用完成/失败)。error/aborted 的区分由 error 字段保留。
|
|
459
|
+
const closedReason: import("./types.ts").ClosedReason = "gc";
|
|
436
460
|
|
|
437
461
|
// rootSessionId 归一化:新文件读 rootSessionId,旧文件 fallback parentSessionId。
|
|
438
462
|
const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
|
|
@@ -447,6 +471,7 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
|
|
|
447
471
|
forkDepth: identity.forkDepth,
|
|
448
472
|
sessionFile,
|
|
449
473
|
status,
|
|
474
|
+
closedReason,
|
|
450
475
|
turns,
|
|
451
476
|
turnCount,
|
|
452
477
|
totalTokens,
|
|
@@ -459,3 +484,195 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
|
|
|
459
484
|
eventLog,
|
|
460
485
|
};
|
|
461
486
|
}
|
|
487
|
+
|
|
488
|
+
// ============================================================
|
|
489
|
+
// 轻量头部读取(列表扫描用)
|
|
490
|
+
// ============================================================
|
|
491
|
+
|
|
492
|
+
/** 头部读取字节数。首轮会话 identity 在第 2-3 行;但续聊(resume)场景 session_start
|
|
493
|
+
* 再次触发,identity append 到文件尾部附近(实测真实目录 1186/1744 文件的 identity
|
|
494
|
+
* 不在头 64KB)。头部 miss 时由 RecordStore.scanFile 调 readIdentityAnywhere 全文 fallback。 */
|
|
495
|
+
export const IDENTITY_HEAD_BYTES = 65536;
|
|
496
|
+
|
|
497
|
+
/** 轻量重建产出:仅身份字段 + 头部可见的 model/thinkingLevel(详情字段一律缺省)。 */
|
|
498
|
+
export interface IdentityHeaderRecon {
|
|
499
|
+
id: string;
|
|
500
|
+
agent: string;
|
|
501
|
+
mode: ExecutionMode;
|
|
502
|
+
task: string;
|
|
503
|
+
slug: string;
|
|
504
|
+
startedAt: number;
|
|
505
|
+
rootSessionId: string | undefined;
|
|
506
|
+
parentRecordId: string | undefined;
|
|
507
|
+
depth: number;
|
|
508
|
+
forkDepth: number | undefined;
|
|
509
|
+
chatMode?: boolean;
|
|
510
|
+
/** [review round2] worktree 隔离标志(见 SubagentIdentityData.worktree)。 */
|
|
511
|
+
worktree?: boolean;
|
|
512
|
+
model: string;
|
|
513
|
+
thinkingLevel: string | undefined;
|
|
514
|
+
sessionFile: string;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 只读文件头部,解析 identity custom entry(+途经的 model/thinking_level change)。
|
|
519
|
+
*
|
|
520
|
+
* 列表扫描专用:单次 readSync 读头部 IDENTITY_HEAD_BYTES 字节,逐行解析到 identity
|
|
521
|
+
* 即停。不解析 message entries(turns/eventLog/result 等重数据由
|
|
522
|
+
* RecordStore.getFullRecord 懒加载 reconstructFromFile 补齐)。
|
|
523
|
+
*
|
|
524
|
+
* 与 reconstructFromFile 的行为差异(有意):本函数只要头部有 identity 就返回,
|
|
525
|
+
* 不要求存在 assistant message——pi 延迟写入策略下无 assistant 的文件几乎不存在,
|
|
526
|
+
* 若存在(崩溃前 flush)也按分支 4 running 呈现(v4 B-1 可续聊语义),
|
|
527
|
+
* 而非静默不可见。
|
|
528
|
+
*
|
|
529
|
+
* 返回 undefined:文件缺失/读失败/头部无 identity。不抛。
|
|
530
|
+
* 头部无 identity ≠ 文件无 identity(续聊场景 identity 在尾部附近)——需要覆盖全文件
|
|
531
|
+
* 时用 readIdentityAnywhere。
|
|
532
|
+
*/
|
|
533
|
+
export function readIdentityHeader(sessionFile: string): IdentityHeaderRecon | undefined {
|
|
534
|
+
let text: string;
|
|
535
|
+
try {
|
|
536
|
+
const fd = fs.openSync(sessionFile, "r");
|
|
537
|
+
try {
|
|
538
|
+
const buf = Buffer.alloc(IDENTITY_HEAD_BYTES);
|
|
539
|
+
let total = 0;
|
|
540
|
+
while (total < buf.length) {
|
|
541
|
+
const n = fs.readSync(fd, buf, total, buf.length - total, total);
|
|
542
|
+
if (n <= 0) break;
|
|
543
|
+
total += n;
|
|
544
|
+
}
|
|
545
|
+
text = buf.toString("utf-8", 0, total);
|
|
546
|
+
} finally {
|
|
547
|
+
fs.closeSync(fd);
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
return undefined; // 文件缺失/不可读 → 降级跳过
|
|
551
|
+
}
|
|
552
|
+
return parseIdentityFromText(text, sessionFile);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* 全文件找 identity:头部/尾部都 miss 后的最后 fallback(真实数据仅 ~0.2% 走到)。
|
|
557
|
+
*
|
|
558
|
+
* 全量 readFileSync + indexOf 预筛(只有含特征串的行才 JSON.parse),从后往前找
|
|
559
|
+
* (续聊轮次的 identity 靠尾部,最后一次的 identity 最新鲜)。读全文是成本主体
|
|
560
|
+
* (预筛/parse 可忽略)——调用方(RecordStore)应缓存结果避免重复读。
|
|
561
|
+
*/
|
|
562
|
+
export function readIdentityAnywhere(sessionFile: string): IdentityHeaderRecon | undefined {
|
|
563
|
+
let text: string;
|
|
564
|
+
try {
|
|
565
|
+
text = fs.readFileSync(sessionFile, "utf-8");
|
|
566
|
+
} catch {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
const lines = text.split("\n");
|
|
570
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
571
|
+
const s = lines[i];
|
|
572
|
+
if (!s.includes(IDENTITY_CUSTOM_TYPE)) continue; // 预筛:identity 行必含特征串
|
|
573
|
+
const recon = parseIdentityFromText(s, sessionFile);
|
|
574
|
+
if (recon) return recon;
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* 只读文件尾部找 identity(续聊场景主力命中点:最后一轮 session_start 追加的
|
|
581
|
+
* identity 离文件尾近,实测真实目录 65% 文件 identity 在尾 64KB)。
|
|
582
|
+
*
|
|
583
|
+
* 尾部块首行可能残缺(从行中间开始)——JSON.parse 失败自然跳过,不影响后续行。
|
|
584
|
+
* model/thinking_level 在尾部块中拿不到 identity 之前的途经 entry,返回空/undefined
|
|
585
|
+
* (best-effort,详情场景由全量重建补齐)。
|
|
586
|
+
*/
|
|
587
|
+
export function readIdentityTail(sessionFile: string): IdentityHeaderRecon | undefined {
|
|
588
|
+
let text: string;
|
|
589
|
+
try {
|
|
590
|
+
const { size } = fs.statSync(sessionFile);
|
|
591
|
+
const start = Math.max(0, size - IDENTITY_HEAD_BYTES);
|
|
592
|
+
const fd = fs.openSync(sessionFile, "r");
|
|
593
|
+
try {
|
|
594
|
+
const buf = Buffer.alloc(size - start);
|
|
595
|
+
let total = 0;
|
|
596
|
+
while (total < buf.length) {
|
|
597
|
+
const n = fs.readSync(fd, buf, total, buf.length - total, start + total);
|
|
598
|
+
if (n <= 0) break;
|
|
599
|
+
total += n;
|
|
600
|
+
}
|
|
601
|
+
text = buf.toString("utf-8", 0, total);
|
|
602
|
+
} finally {
|
|
603
|
+
fs.closeSync(fd);
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
const lines = text.split("\n");
|
|
609
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
610
|
+
const s = lines[i];
|
|
611
|
+
if (!s.includes(IDENTITY_CUSTOM_TYPE)) continue;
|
|
612
|
+
const recon = parseIdentityFromText(s, sessionFile);
|
|
613
|
+
if (recon) return recon;
|
|
614
|
+
}
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** 从文本片段解析 identity(头部/全文两入口共享)。解析到 identity 即停(返回);
|
|
619
|
+
* 找不到返回 undefined。model/thinking_level 仅在 identity 之前的途经 entry 中
|
|
620
|
+
* best-effort 提取,详情场景由全量重建补齐。 */
|
|
621
|
+
function parseIdentityFromText(text: string, sessionFile: string): IdentityHeaderRecon | undefined {
|
|
622
|
+
let identity: SubagentIdentityData | undefined;
|
|
623
|
+
let model = "";
|
|
624
|
+
let thinkingLevel: string | undefined;
|
|
625
|
+
for (const line of text.split("\n")) {
|
|
626
|
+
const s = line.trim();
|
|
627
|
+
if (!s) continue;
|
|
628
|
+
// 预筛([S-4] 同款值匹配):只有含三种目标 entry 值串的行才值得 JSON.parse——
|
|
629
|
+
// head 64KB 内大 toolResult 行(数十 KB/行)占多数,全量 parse 是冷扫描 CPU 大头。
|
|
630
|
+
// 值字符串在任意序列化格式下连续出现,不耦合 pi 的空格习惯。
|
|
631
|
+
if (
|
|
632
|
+
!s.includes(IDENTITY_CUSTOM_TYPE) &&
|
|
633
|
+
!s.includes('"model_change"') &&
|
|
634
|
+
!s.includes('"thinking_level_change"')
|
|
635
|
+
) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
let entry: JsonlEntry;
|
|
639
|
+
try {
|
|
640
|
+
entry = JSON.parse(s) as JsonlEntry;
|
|
641
|
+
} catch {
|
|
642
|
+
continue; // 损坏行/截断行(头部读取边界)跳过
|
|
643
|
+
}
|
|
644
|
+
if (entry.type === "custom" && entry.customType === IDENTITY_CUSTOM_TYPE) {
|
|
645
|
+
if (isIdentityData(entry.data)) {
|
|
646
|
+
identity = entry.data;
|
|
647
|
+
break; // 找到即停(后续行不再解析)
|
|
648
|
+
}
|
|
649
|
+
} else if (entry.type === "model_change") {
|
|
650
|
+
if (typeof entry.provider === "string" && typeof entry.modelId === "string") {
|
|
651
|
+
model = `${entry.provider}/${entry.modelId}`;
|
|
652
|
+
}
|
|
653
|
+
} else if (entry.type === "thinking_level_change") {
|
|
654
|
+
if (typeof entry.thinkingLevel === "string") thinkingLevel = entry.thinkingLevel;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (!identity) return undefined;
|
|
658
|
+
|
|
659
|
+
// 归一化与全量 recon 同源:rootSessionId fallback 旧字段、slug 兜底空串。
|
|
660
|
+
const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
|
|
661
|
+
return {
|
|
662
|
+
id: identity.id,
|
|
663
|
+
agent: identity.agent,
|
|
664
|
+
mode: identity.mode,
|
|
665
|
+
task: identity.task,
|
|
666
|
+
slug: identity.slug ?? "",
|
|
667
|
+
startedAt: identity.startedAt,
|
|
668
|
+
rootSessionId,
|
|
669
|
+
parentRecordId: identity.parentRecordId,
|
|
670
|
+
depth: identity.depth ?? 0,
|
|
671
|
+
forkDepth: identity.forkDepth,
|
|
672
|
+
chatMode: identity.chatMode,
|
|
673
|
+
worktree: identity.worktree,
|
|
674
|
+
model,
|
|
675
|
+
thinkingLevel,
|
|
676
|
+
sessionFile,
|
|
677
|
+
};
|
|
678
|
+
}
|