@zhushanwen/pi-subagent-workflow 9.0.1 → 9.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/package.json +10 -10
- package/skills/subagent-ext-config/SKILL.md +9 -11
- package/src/host/__tests__/pi-host.test.ts +0 -1
- package/src/index.ts +7 -7
- package/src/injectors/subagent-list-injector.ts +1 -1
- package/src/interface/command/__tests__/commands-open.test.ts +278 -0
- package/src/interface/{commands.ts → command/commands.ts} +15 -7
- package/src/interface/{subagents.ts → command/subagents.ts} +4 -4
- package/src/interface/format/__tests__/display-state.test.ts +146 -0
- package/src/interface/format/display-state.ts +112 -0
- package/src/interface/{format.ts → format/format.ts} +20 -14
- package/src/interface/{id-preview.ts → format/id-preview.ts} +1 -1
- package/src/interface/{bg-notify-render.ts → gui/bg-notify-render.ts} +60 -35
- package/src/interface/gui/gui-mappers.ts +74 -0
- package/src/interface/{subagent-actions.ts → gui/subagent-actions.ts} +3 -3
- package/src/interface/tool/__tests__/subagent-tool-actions.test.ts +238 -0
- package/src/interface/{__tests__ → tool/__tests__}/subagent-tool-path-guard.test.ts +6 -5
- package/src/interface/{__tests__ → tool/__tests__}/tool-subagents.test.ts +4 -4
- package/src/interface/tool/__tests__/tool-workflow-resume.test.ts +161 -0
- package/src/interface/{__tests__ → tool/__tests__}/tool-workflow.test.ts +33 -9
- package/src/interface/{subagent-tool-schema.ts → tool/subagent-tool-schema.ts} +1 -1
- package/src/interface/{subagent-tool.ts → tool/subagent-tool.ts} +4 -4
- package/src/interface/{tool-render.ts → tool/tool-render.ts} +3 -3
- package/src/interface/{tool-shared.ts → tool/tool-shared.ts} +1 -1
- package/src/interface/{tool-subagents.ts → tool/tool-subagents.ts} +12 -11
- package/src/interface/{tool-workflow-script.ts → tool/tool-workflow-script.ts} +1 -1
- package/src/interface/{tool-workflow.ts → tool/tool-workflow.ts} +20 -165
- package/src/interface/tui/__tests__/list-view.test.ts +188 -0
- package/src/interface/{list-component.ts → tui/list-component.ts} +2 -2
- package/src/interface/{list-shared.ts → tui/list-shared.ts} +1 -1
- package/src/interface/{list-view.ts → tui/list-view.ts} +2 -2
- package/src/interface/{tui-kit.ts → tui/tui-kit.ts} +1 -1
- package/src/interface/{views → tui/views}/WorkflowsView.ts +10 -6
- package/src/interface/tui/views/__tests__/WorkflowsView.driver.test.ts +595 -0
- package/src/interface/{views → tui/views}/__tests__/WorkflowsView.test.ts +10 -3
- package/src/interface/tui/views/__tests__/detail-content.test.ts +195 -0
- package/src/interface/{views → tui/views}/detail-content.ts +8 -3
- package/src/interface/{views → tui/views}/view-constants.ts +2 -2
- package/src/jsonl-run-store.ts +133 -81
- package/src/session-lifecycle.ts +2 -1
- package/src/workflow-events.ts +21 -3
- package/src/workflow-notify.ts +7 -6
- package/src/interface/__tests__/tool-workflow-resume.test.ts +0 -227
- package/src/interface/gui-mappers.ts +0 -81
- /package/src/interface/{__tests__ → command/__tests__}/commands-resume.test.ts +0 -0
- /package/src/interface/{command-actions.ts → command/command-actions.ts} +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/capture-tool.ts +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/tool-prompt-contract.test.ts +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/tool-render.test.ts +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/tool-workflow-script.test.ts +0 -0
- /package/src/interface/{reentry-guard.ts → tool/reentry-guard.ts} +0 -0
- /package/src/interface/{tool-result.ts → tool/tool-result.ts} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detail-content 纯函数直测——与 WorkflowsView.driver.test.ts(经 view 驱动覆盖)
|
|
3
|
+
* 互补:本文件直接单测 view 驱动不易逐一命中的分支面。
|
|
4
|
+
*
|
|
5
|
+
* 覆盖面:
|
|
6
|
+
* - statusLabel 四分支(入参收窄联合的编译期补齐运行时对照)
|
|
7
|
+
* - projectRecordProgress 投影口径(S1 等价表:toolCallCount = tool_start 计数、
|
|
8
|
+
* lastError = eventLog 末条 error 的 label、elapsed/turns/token 直读)
|
|
9
|
+
* - processDetailKey 全键位(PgUp/PgDn/Home/End)+ followTail 语义 + 视口 0 兜底
|
|
10
|
+
* - buildDetailContent 大正文截断(>OUTPUT_TRUNCATE_BYTES → 末 5 行 + (truncated))
|
|
11
|
+
* 与 worker diagnostics 封顶标签(>20 条 → last 20 of N)
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
|
|
15
|
+
import type { AgentEventLogEntry, SubagentRecord, WorkflowRun } from "@zhushanwen/subagent-core";
|
|
16
|
+
import type { ExecutionTraceNode } from "@zhushanwen/subagent-core";
|
|
17
|
+
|
|
18
|
+
import type { ThemeLike } from "../../../format/format.ts";
|
|
19
|
+
import {
|
|
20
|
+
buildDetailContent,
|
|
21
|
+
detailContentLength,
|
|
22
|
+
processDetailKey,
|
|
23
|
+
projectRecordProgress,
|
|
24
|
+
statusLabel,
|
|
25
|
+
} from "../detail-content.ts";
|
|
26
|
+
|
|
27
|
+
const T0 = 1_700_000_000_000;
|
|
28
|
+
|
|
29
|
+
/** 记录色 token 的 marking theme(断言色档而非文本形态)。 */
|
|
30
|
+
const markingTheme: ThemeLike = {
|
|
31
|
+
bg: (_color: string, text: string) => text,
|
|
32
|
+
fg: (tag: string, text: string) => `${tag}(${text})`,
|
|
33
|
+
bold: (text: string) => text,
|
|
34
|
+
underline: (text: string) => text,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 无色 theme(内容断言用)。 */
|
|
38
|
+
const plainTheme: ThemeLike = {
|
|
39
|
+
bg: (_color: string, text: string) => text,
|
|
40
|
+
fg: (_tag: string, text: string) => text,
|
|
41
|
+
bold: (text: string) => text,
|
|
42
|
+
underline: (text: string) => text,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function makeRun(errorLogs: Array<{ level: string; message: string }> = []): WorkflowRun {
|
|
46
|
+
return {
|
|
47
|
+
state: { errorLogs },
|
|
48
|
+
} as unknown as WorkflowRun;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeNode(over: Partial<ExecutionTraceNode> = {}): ExecutionTraceNode {
|
|
52
|
+
return {
|
|
53
|
+
stepIndex: 0,
|
|
54
|
+
agent: "worker",
|
|
55
|
+
task: "do",
|
|
56
|
+
model: "test-model",
|
|
57
|
+
status: "running",
|
|
58
|
+
startedAt: new Date(T0).toISOString(),
|
|
59
|
+
...over,
|
|
60
|
+
} as ExecutionTraceNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeSub(over: Partial<SubagentRecord> = {}): SubagentRecord {
|
|
64
|
+
return {
|
|
65
|
+
id: "sa-1",
|
|
66
|
+
task: "do",
|
|
67
|
+
status: "running",
|
|
68
|
+
startedAt: T0,
|
|
69
|
+
turns: 1,
|
|
70
|
+
totalTokens: 500,
|
|
71
|
+
eventLog: [],
|
|
72
|
+
...over,
|
|
73
|
+
} as SubagentRecord;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("statusLabel 色档四分支", () => {
|
|
77
|
+
it("completed→success / running→warning / failed→error / 其他→muted", () => {
|
|
78
|
+
expect(statusLabel("completed", markingTheme)).toBe("success(completed)");
|
|
79
|
+
expect(statusLabel("running", markingTheme)).toBe("warning(running)");
|
|
80
|
+
expect(statusLabel("failed", markingTheme)).toBe("error(failed)");
|
|
81
|
+
expect(statusLabel("pending" as ExecutionTraceNode["status"], markingTheme)).toBe("muted(pending)");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("projectRecordProgress 投影口径(S1 等价表消费面)", () => {
|
|
86
|
+
it("toolCallCount = tool_start 计数;lastError = 末条 error 的 label;字段直读", () => {
|
|
87
|
+
const rec = makeSub({
|
|
88
|
+
totalTokens: 1234,
|
|
89
|
+
turns: 3,
|
|
90
|
+
endedAt: T0 + 5000,
|
|
91
|
+
currentActivity: { type: "tool", label: "bash" },
|
|
92
|
+
eventLog: [
|
|
93
|
+
{ type: "tool_start", label: "read", ts: T0 },
|
|
94
|
+
{ type: "tool_end", label: "read", ts: T0 + 10, status: "done" },
|
|
95
|
+
{ type: "tool_start", label: "write", ts: T0 + 20 },
|
|
96
|
+
{ type: "error", label: "EPIPE: broken pipe", ts: T0 + 30 },
|
|
97
|
+
] as AgentEventLogEntry[],
|
|
98
|
+
});
|
|
99
|
+
const view = projectRecordProgress(rec);
|
|
100
|
+
expect(view.totalTokens).toBe(1234);
|
|
101
|
+
expect(view.toolCallCount).toBe(2); // 两条 tool_start
|
|
102
|
+
expect(view.turns).toBe(3);
|
|
103
|
+
expect(view.elapsedSeconds).toBe(5); // endedAt - startedAt
|
|
104
|
+
expect(view.currentActivity).toEqual({ type: "tool", label: "bash" });
|
|
105
|
+
expect(view.lastError).toBe("EPIPE: broken pipe");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("末条非 error → lastError undefined;空 eventLog → 零计数", () => {
|
|
109
|
+
const view = projectRecordProgress(makeSub({ eventLog: [{ type: "tool_end", label: "read", ts: T0, status: "done" } as AgentEventLogEntry] }));
|
|
110
|
+
expect(view.lastError).toBeUndefined();
|
|
111
|
+
expect(view.toolCallCount).toBe(0);
|
|
112
|
+
const empty = projectRecordProgress(makeSub({ eventLog: [] }));
|
|
113
|
+
expect(empty.lastError).toBeUndefined();
|
|
114
|
+
expect(empty.currentActivity).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("processDetailKey 全键位(followTail 语义 + 边界 clamp)", () => {
|
|
119
|
+
const ctx = { viewportHeight: 10, contentLines: 35, isRunning: true };
|
|
120
|
+
|
|
121
|
+
it("PgUp:上翻一屏 + 脱离跟随;不越顶(clamp 0)", () => {
|
|
122
|
+
expect(processDetailKey("\x1b[5~", { scrollOffset: 12, followTail: true }, ctx))
|
|
123
|
+
.toEqual({ scrollOffset: 2, followTail: false, handled: true });
|
|
124
|
+
expect(processDetailKey("\x1b[5~", { scrollOffset: 3, followTail: true }, ctx))
|
|
125
|
+
.toEqual({ scrollOffset: 0, followTail: false, handled: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("PgDn:下翻一屏 + 到底恢复跟随;中途未到底保持脱离", () => {
|
|
129
|
+
expect(processDetailKey("\x1b[6~", { scrollOffset: 0, followTail: false }, ctx))
|
|
130
|
+
.toEqual({ scrollOffset: 10, followTail: false, handled: true });
|
|
131
|
+
// max = 35 - 10 = 25;10+10=20 < 25 → 仍脱离
|
|
132
|
+
expect(processDetailKey("\x1b[6~", { scrollOffset: 10, followTail: false }, ctx))
|
|
133
|
+
.toEqual({ scrollOffset: 20, followTail: false, handled: true });
|
|
134
|
+
// 20+10=30 > 25 → clamp 到底 → 恢复跟随
|
|
135
|
+
expect(processDetailKey("\x1b[6~", { scrollOffset: 20, followTail: false }, ctx))
|
|
136
|
+
.toEqual({ scrollOffset: 25, followTail: true, handled: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("Home 回顶脱离跟随;End 回底恢复跟随;未命中键 handled=false 原值透传", () => {
|
|
140
|
+
expect(processDetailKey("\x1b[H", { scrollOffset: 9, followTail: true }, ctx))
|
|
141
|
+
.toEqual({ scrollOffset: 0, followTail: false, handled: true });
|
|
142
|
+
expect(processDetailKey("\x1b[F", { scrollOffset: 0, followTail: false }, ctx))
|
|
143
|
+
.toEqual({ scrollOffset: 25, followTail: true, handled: true });
|
|
144
|
+
expect(processDetailKey("x", { scrollOffset: 7, followTail: false }, ctx))
|
|
145
|
+
.toEqual({ scrollOffset: 7, followTail: false, handled: false });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("视口 0 → PgUp/PgDn 走 PAGE_SCROLL_DEFAULT 步长;空内容 max=0", () => {
|
|
149
|
+
expect(processDetailKey("\x1b[5~", { scrollOffset: 30, followTail: true }, { viewportHeight: 0, contentLines: 0, isRunning: false }))
|
|
150
|
+
.toEqual({ scrollOffset: 20, followTail: false, handled: true });
|
|
151
|
+
expect(processDetailKey("\x1b[6~", { scrollOffset: 5, followTail: false }, { viewportHeight: 0, contentLines: 0, isRunning: false }))
|
|
152
|
+
.toEqual({ scrollOffset: 0, followTail: true, handled: true }); // max=0 → next=min(0,15)=0 → clamp 到底恢复跟随
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("buildDetailContent 区段分支(view 驱动外的边角)", () => {
|
|
157
|
+
it("超预算正文(>100KB):保留头部 100KB 后渲染末 5 行 + (truncated) 标记", () => {
|
|
158
|
+
const big = `${Array.from({ length: 10 }, (_, i) => `L${i}-` + "x".repeat(20_000)).join("\n")}`;
|
|
159
|
+
const lines = buildDetailContent(makeNode({ status: "completed", result: { content: big } }), { promptExpanded: false }, makeRun(), plainTheme, 120, T0);
|
|
160
|
+
const joined = lines.join("\n");
|
|
161
|
+
expect(joined).toContain("(truncated)");
|
|
162
|
+
// 头部 100KB ≈ 前 5 行(L0..L4,L4 中途截断)→ 末 5 行窗口 = L0..L4;L5..L9 不出现
|
|
163
|
+
expect(joined).toContain("L0-");
|
|
164
|
+
expect(joined).toContain("L4-");
|
|
165
|
+
expect(joined).not.toContain("L5-");
|
|
166
|
+
expect(joined).not.toContain("L9-");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("worker diagnostics 封顶:>20 条显示 last 20 of N 标签,只渲染末 20 条", () => {
|
|
170
|
+
const logs = Array.from({ length: 21 }, (_, i) => ({ level: "error", message: `E-${i}` }));
|
|
171
|
+
const lines = buildDetailContent(makeNode({ status: "completed", result: { content: "ok" } }), { promptExpanded: false }, makeRun(logs), plainTheme, 200, T0);
|
|
172
|
+
const joined = lines.join("\n");
|
|
173
|
+
expect(joined).toContain("Worker diagnostics · last 20 of 21");
|
|
174
|
+
expect(joined).toContain("[error] E-20");
|
|
175
|
+
expect(joined).not.toContain("[error] E-0\n");
|
|
176
|
+
// 单条 → "1 entry" 单数形态
|
|
177
|
+
const single = buildDetailContent(makeNode({ status: "completed" }), { promptExpanded: false }, makeRun([{ level: "warn", message: "w" }]), plainTheme, 200, T0);
|
|
178
|
+
expect(single.join("\n")).toContain("Worker diagnostics · 1 entry");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("终态无工具调用(completed)→ (no activity recorded);detailContentLength 与内容行数一致", () => {
|
|
182
|
+
const node = makeNode({ status: "completed", result: { content: "done" } });
|
|
183
|
+
const lines = buildDetailContent(node, { promptExpanded: false }, makeRun(), plainTheme, 120, T0);
|
|
184
|
+
expect(lines.join("\n")).toContain("(no activity recorded)");
|
|
185
|
+
expect(detailContentLength(node, { promptExpanded: false }, makeRun(), plainTheme)).toBe(lines.length);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("live 路径零活动 + running 无 live → Still running...", () => {
|
|
189
|
+
const live = projectRecordProgress(makeSub({ eventLog: [], turns: 0, totalTokens: 0 }));
|
|
190
|
+
const lines = buildDetailContent(makeNode(), { promptExpanded: false }, makeRun(), plainTheme, 120, T0, live);
|
|
191
|
+
expect(lines.join("\n")).toContain("(starting...)");
|
|
192
|
+
const noLive = buildDetailContent(makeNode(), { promptExpanded: false }, makeRun(), plainTheme, 120, T0);
|
|
193
|
+
expect(noLive.join("\n")).toContain("Still running...");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
formatTraceEventLine,
|
|
27
27
|
statusDotStr,
|
|
28
28
|
type ThemeLike,
|
|
29
|
-
} from "
|
|
29
|
+
} from "../../format/format.ts";
|
|
30
30
|
import { PAGE_SCROLL_DEFAULT } from "../tui-kit.ts";
|
|
31
31
|
import {
|
|
32
32
|
BOX_BORDER_CHARS,
|
|
@@ -83,8 +83,13 @@ export function projectRecordProgress(rec: SubagentRecord): LiveProgressView {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* status → 语义色标签(L2 detail 头用)。
|
|
88
|
+
*
|
|
89
|
+
* [§2.2 入参收窄] 此前签名是裸 string,任何词表都能静默落 muted(与 formatStatusBadge
|
|
90
|
+
* 同批状态处理不一致);收窄到 trace 节点状态联合后,新增节点状态会在编译期强制补齐。
|
|
91
|
+
*/
|
|
92
|
+
export function statusLabel(status: ExecutionTraceNode["status"], theme: ThemeLike): string {
|
|
88
93
|
switch (status) {
|
|
89
94
|
case "completed": return theme.fg("success", status);
|
|
90
95
|
case "running": return theme.fg("warning", status);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/interface/views/view-constants.ts
|
|
1
|
+
// src/interface/tui/views/view-constants.ts
|
|
2
2
|
//
|
|
3
3
|
// workflow 全屏视图族专属布局常量(WorkflowsView + detail-content 消费)。
|
|
4
|
-
// 自 interface/format.ts 沉回 views/(post-convergence C4:format.ts 只保留
|
|
4
|
+
// 自 interface/format/format.ts 沉回 views/(post-convergence C4:format.ts 只保留
|
|
5
5
|
// 跨视图族共享的格式化函数,views 专属常量就近落位消费方目录)。
|
|
6
6
|
// ELLIPSIS 留在 format.ts(formatActivityLine/formatPhaseLine 亦消费,非 views 专属)。
|
|
7
7
|
|
package/src/jsonl-run-store.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* 的改名波及大于语义收益),语义已收敛为 record store 单模式。
|
|
20
20
|
* - **旧格式两件套(旧 journal `.events.jsonl` + state 快照 `<runId>.jsonl`)不读、
|
|
21
21
|
* 不写、不主动删**(用户裁决 2026-09-28:不做存量迁移):全部读取路径只认
|
|
22
|
-
* record 后缀;注册条目的
|
|
22
|
+
* record 后缀;注册条目的 recordPath 锚点后缀天然区分新旧实体——旧后缀锚点
|
|
23
23
|
* = 历史 run,跳过不重建(从壳侧读取面消失即 D1 历史数据处置的预期行为)。
|
|
24
24
|
* - **loadAll = record 流折叠重建**:v2 注册条目定界(本会话有哪些实体)→
|
|
25
25
|
* record 流全量读 → fold 重建(终局 ⟸ run-settled 帧);「有注册、无终局帧」
|
|
@@ -49,19 +49,26 @@ import type { CustomEntry, ExtensionAPI, ExtensionContext, SessionEntry } from "
|
|
|
49
49
|
// 全部经 core barrel 消费(生产消费纪律:extensions 源码不深路径 import core)。
|
|
50
50
|
import {
|
|
51
51
|
AgentCall,
|
|
52
|
-
|
|
53
|
-
Budget,
|
|
54
|
-
RUN_EVENT_TYPES,
|
|
55
|
-
RUN_EVENT_JOURNAL_SUFFIX,
|
|
52
|
+
RUN_EVENTS_SUFFIX,
|
|
56
53
|
STATE_DIR_NAME,
|
|
57
54
|
Trace,
|
|
58
55
|
WORKFLOW_RECORD_CUSTOM_TYPE,
|
|
59
56
|
buildWorkflowRecordSettledEntryData,
|
|
60
57
|
classifyWorkflowRecordEntryData,
|
|
61
58
|
getLogger,
|
|
59
|
+
// [§3.2] 单行坏行判定规则单源在 core(requireSeq:false = 兼容档,存量无 seq 行放行);
|
|
60
|
+
// 壳侧只保留自己的错误文案与 ENOENT 分流。本地词表投影(RUN_EVENT_TYPE_SET /
|
|
61
|
+
// hasEventEnvelope)与 outcome 判据副本已删。
|
|
62
|
+
parseRecordStreamLine,
|
|
63
|
+
parseLegacyArgsSummary as parseLegacyArgsSummaryCore,
|
|
62
64
|
runSettledOutcomeToDoneReason,
|
|
65
|
+
// [D6(a) 第 1 步] 恢复路径重建的终局事实注入(fold 出 run-settled 帧 → 终局记录
|
|
66
|
+
// 注册表),与 core isRunSettled / runSummary 的判据同源。
|
|
67
|
+
noteRebuiltSettlement,
|
|
68
|
+
settlementRecordOfRunSettledFrame,
|
|
63
69
|
type AgentResult,
|
|
64
70
|
type ExecutionTraceNode,
|
|
71
|
+
type RunEventLineIssue,
|
|
65
72
|
type RunOutcome,
|
|
66
73
|
type RunStore,
|
|
67
74
|
type WorkflowRecordRegisteredEntryData,
|
|
@@ -69,13 +76,14 @@ import {
|
|
|
69
76
|
type WorkflowRunEvent,
|
|
70
77
|
} from "@zhushanwen/subagent-core";
|
|
71
78
|
import { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
79
|
+
import { errorLogsFromEvents, rebuildBudget, runAccountingFromEvents } from "@zhushanwen/subagent-core";
|
|
72
80
|
import { guardStaleCtx, isEnoentError, toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
73
81
|
|
|
74
82
|
// ── [W1 / D1] v2 条目读面(注册定界 + 终态条目抑制)────────────────
|
|
75
83
|
|
|
76
84
|
/** loadAll 的 entry 扫描产物。 */
|
|
77
85
|
interface EntrySources {
|
|
78
|
-
/** v2 注册条目(runId → 注册载荷;record 重建的定界源 +
|
|
86
|
+
/** v2 注册条目(runId → 注册载荷;record 重建的定界源 + recordPath 锚点)。 */
|
|
79
87
|
registered: Map<string, WorkflowRecordRegisteredEntryData>;
|
|
80
88
|
/**
|
|
81
89
|
* 已有 v2 终态条目的 run(runId → 终态载荷,后写覆盖 = last-wins)。唯一用途 =
|
|
@@ -96,9 +104,9 @@ function collectV2RecordEntry(entry: CustomEntry, entryIndex: number, sources: E
|
|
|
96
104
|
if (classification.ok || classification.reason !== "v2") return;
|
|
97
105
|
const v2 = classification.entry;
|
|
98
106
|
if (v2.kind === "registered") {
|
|
99
|
-
if (typeof v2.runId !== "string" || v2.runId === "" || typeof v2.
|
|
107
|
+
if (typeof v2.runId !== "string" || v2.runId === "" || typeof v2.recordPath !== "string") {
|
|
100
108
|
logger.warn(
|
|
101
|
-
`[subagent-workflow] workflow-record v2 registered entry #${entryIndex} malformed (runId/
|
|
109
|
+
`[subagent-workflow] workflow-record v2 registered entry #${entryIndex} malformed (runId/recordPath missing), skipped`,
|
|
102
110
|
);
|
|
103
111
|
return;
|
|
104
112
|
}
|
|
@@ -116,8 +124,9 @@ function collectV2RecordEntry(entry: CustomEntry, entryIndex: number, sources: E
|
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
/** loadAll 的 entry 扫描:主 session entries → v2 注册定界 + 终态条目抑制(唯一发现
|
|
119
|
-
* 通道)。历史形态 entry(v1 全量快照 / 旧 workflow-state-link
|
|
120
|
-
*
|
|
127
|
+
* 通道)。历史形态 entry(v1 全量快照 / 旧 workflow-state-link 指针)静默消失——
|
|
128
|
+
* 不识别、不拒读、不报错,是设计预期([ADR-0095] 同 record 侧 v1 处置:项目未上线
|
|
129
|
+
* 无历史数据,不迁移不兼容);未知 entry 的容忍面不受影响。 */
|
|
121
130
|
function collectEntrySources(entries: SessionEntry[]): EntrySources {
|
|
122
131
|
const sources: EntrySources = {
|
|
123
132
|
registered: new Map<string, WorkflowRecordRegisteredEntryData>(),
|
|
@@ -172,22 +181,17 @@ export class RecordStreamCorruptionError extends Error {
|
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
175
|
-
/** record 事件词表集合([D2] 守卫判定的本地只读投影;词表 SSOT = run-events RUN_EVENT_TYPES)。 */
|
|
176
|
-
const RUN_EVENT_TYPE_SET: ReadonlySet<string> = new Set<string>(RUN_EVENT_TYPES);
|
|
177
|
-
|
|
178
|
-
/** 事件信封守卫(taste/no-unsafe-cast:结构断言改类型守卫——type/ts 可用性在守卫内收窄)。 */
|
|
179
|
-
function hasEventEnvelope(v: object): v is { type: string; ts: number } {
|
|
180
|
-
const rec = v as Record<string, unknown>;
|
|
181
|
-
return typeof rec["type"] === "string" && rec["type"] !== "" && typeof rec["ts"] === "number" && Number.isFinite(rec["ts"]);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
184
|
/**
|
|
185
185
|
* record 流全量读(严格解析):合法事件行按写入序返回;坏行(JSON 解析失败 /
|
|
186
|
-
* 非对象 / 缺 type/ts
|
|
186
|
+
* 非对象 / 缺 type/ts 信封 / 词表外 type 或 outcome / agent-settled 帧缺 result 全文)
|
|
187
187
|
* 抛 {@link RecordStreamCorruptionError}——不静默跳过(场景 18 坏行停摆语义,
|
|
188
188
|
* 与 core journal scan 活体投影的宽容跳过语义刻意分层:活体 fold 不能因单帧
|
|
189
189
|
* 全停,恢复读面不能对损坏装瞎)。
|
|
190
190
|
*
|
|
191
|
+
* [§3.2] 判据经 core 单源原语 parseRecordStreamLine(requireSeq=false——本读面
|
|
192
|
+
* 兼容 W1 前无 seq 的存量行)消费;core 恢复读面用同一原语的严格档。两侧真差异只剩
|
|
193
|
+
* 错误文案与 ENOENT 分流。
|
|
194
|
+
*
|
|
191
195
|
* 文件不存在(ENOENT)原样上抛交调用方分流(新形态实体早期崩溃 vs 历史实体)。
|
|
192
196
|
*/
|
|
193
197
|
function readRecordStream(recordPath: string): WorkflowRunEvent[] {
|
|
@@ -203,41 +207,36 @@ function readRecordStream(recordPath: string): WorkflowRunEvent[] {
|
|
|
203
207
|
"record 流是 run 域唯一事实源(D1),坏行意味着截断/篡改/写入器缺陷。恢复:检查该文件是否被外部编辑" +
|
|
204
208
|
"或写入器版本与载荷契约不符(dispatchRunCreated/dispatchAskSettled);无法修复时接受该 run 不可续跑,勿手工删行。",
|
|
205
209
|
);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} catch {
|
|
210
|
-
throw corruption("非法 JSON(半截行/截断写入)");
|
|
211
|
-
}
|
|
212
|
-
if (typeof parsed !== "object" || parsed === null || !hasEventEnvelope(parsed)) {
|
|
213
|
-
throw corruption("缺事件信封(type/ts)或非对象行");
|
|
214
|
-
}
|
|
215
|
-
const event = parsed as WorkflowRunEvent;
|
|
216
|
-
// [D2] 词表/词值守卫(对齐 core isWorkflowRunEventLine 判定面):type 落词表 +
|
|
217
|
-
// outcome(携带时)落词表——历史形态帧(旧词表成员/旧 outcome 值)按损坏拒绝
|
|
218
|
-
// ([D1] 历史数据处置:旧词表行不进解析路径;本 strict 读面不静默跳过坏行,
|
|
219
|
-
// 拒绝语义同半截行——调用方 storeHealthy=false fail-fast)。
|
|
220
|
-
if (!RUN_EVENT_TYPE_SET.has(event.type)) {
|
|
221
|
-
throw corruption(`词表外事件 type=${JSON.stringify(event.type)}(旧词表历史行——[D1] 不读旧两件)`);
|
|
222
|
-
}
|
|
223
|
-
if (event.type === "run-settled" || event.type === "agent-settled") {
|
|
224
|
-
// event 已窄化为 RunSettledEvent | AgentSettledEvent(两成员 outcome 均必填);
|
|
225
|
-
// !== undefined 防御保留——旧格式历史行可能缺字段(读取面放行,词表判据兜底)。
|
|
226
|
-
const outcome = event.outcome;
|
|
227
|
-
if (outcome !== undefined && !(ALL_RUN_OUTCOMES as readonly string[]).includes(outcome as string)) {
|
|
228
|
-
throw corruption(`词表外 outcome=${JSON.stringify(outcome)}([D2] 词表重构后的历史形态——interrupted 已入 lifecycle)`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (event.type === "agent-settled" && event.result === undefined) {
|
|
232
|
-
throw corruption(
|
|
233
|
-
"agent-settled 帧缺 result 全文(record 单源后为非法形态——流被篡改或写入器未携带全文,场景 18)",
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
events.push(event);
|
|
210
|
+
const result = parseRecordStreamLine(line, { requireSeq: false });
|
|
211
|
+
if (!result.ok) throw corruption(describeRecordStreamIssue(result.issue));
|
|
212
|
+
events.push(result.event);
|
|
237
213
|
}
|
|
238
214
|
return events;
|
|
239
215
|
}
|
|
240
216
|
|
|
217
|
+
/** 单行问题 → 壳 strict 读面的中文文案(core 恢复读面同问题出英文文案——本函数不共享)。 */
|
|
218
|
+
function describeRecordStreamIssue(issue: RunEventLineIssue): string {
|
|
219
|
+
switch (issue.kind) {
|
|
220
|
+
case "invalid-json":
|
|
221
|
+
return "非法 JSON(半截行/截断写入)";
|
|
222
|
+
case "not-object":
|
|
223
|
+
return "非对象行(JSON 顶层不是对象)";
|
|
224
|
+
case "type-envelope":
|
|
225
|
+
return "缺事件信封(type 非字符串或为空)";
|
|
226
|
+
case "type-outside-vocabulary":
|
|
227
|
+
return `词表外事件 type=${JSON.stringify(issue.value)}(旧词表历史行——[D1] 不读旧两件)`;
|
|
228
|
+
case "ts-envelope":
|
|
229
|
+
return "缺事件信封 ts(非有限数值)";
|
|
230
|
+
case "seq-envelope":
|
|
231
|
+
// 兼容档(requireSeq=false)不校验 seq——本分支仅防御性保留(规则档位改动时文案已在)。
|
|
232
|
+
return `seq 信封非法 seq=${JSON.stringify(issue.value)}`;
|
|
233
|
+
case "outcome-outside-vocabulary":
|
|
234
|
+
return `词表外 outcome=${JSON.stringify(issue.value)}([D2] 词表重构后的历史形态——interrupted 已入 lifecycle)`;
|
|
235
|
+
case "agent-settled-missing-result":
|
|
236
|
+
return "agent-settled 帧缺 result 全文(record 单源后为非法形态——流被篡改或写入器未携带全文,场景 18)";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
241
240
|
/** 事件流尾向扫描取最后一帧 run-settled(单终局不变量下的防御性读取)。 */
|
|
242
241
|
function lastRunSettledEvent(
|
|
243
242
|
events: readonly WorkflowRunEvent[],
|
|
@@ -308,20 +307,20 @@ interface CallDraft {
|
|
|
308
307
|
* 语义限制,回落处置对齐 core 侧「尽力恢复」——core 同款分支 warn 留证,非静默)。
|
|
309
308
|
*/
|
|
310
309
|
function parseLegacyArgsSummary(argsSummary: string | undefined): Record<string, unknown> {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return parsed as Record<string, unknown>;
|
|
316
|
-
}
|
|
317
|
-
} catch (err) {
|
|
318
|
-
// 回落 {} 不变($ARGS 语义限制),warn 留证旧格式数据异常(对齐 core
|
|
319
|
-
// parseArgsSummary 同分支的日志级别)
|
|
310
|
+
// [§3.2] 恢复规则单源(core parseLegacyArgsSummary,与 resume-run 的旧格式回落同一实现);
|
|
311
|
+
// 本包装只补壳侧日志文案——截断分支此前壳侧静默、core 侧 warn,现两侧同留证。
|
|
312
|
+
const { args, issue } = parseLegacyArgsSummaryCore(argsSummary);
|
|
313
|
+
if (issue === "truncated-summary") {
|
|
320
314
|
logger.warn(
|
|
321
|
-
|
|
315
|
+
"[subagent-workflow] legacy run-created argsSummary is truncated — $ARGS restored as {} " +
|
|
316
|
+
"(legacy record stream predates the full-args payload; rerun with a fresh run if the script needs exact args)",
|
|
322
317
|
);
|
|
318
|
+
} else if (issue === "not-parseable") {
|
|
319
|
+
logger.warn("[subagent-workflow] legacy run-created argsSummary is not parseable JSON — $ARGS restored as {}");
|
|
320
|
+
} else if (issue === "not-object") {
|
|
321
|
+
logger.warn("[subagent-workflow] legacy run-created argsSummary is not a JSON object — $ARGS restored as {}");
|
|
323
322
|
}
|
|
324
|
-
return
|
|
323
|
+
return args;
|
|
325
324
|
}
|
|
326
325
|
|
|
327
326
|
/** [foldRecordStreamToRun 拆分] 事件流 → call 重建中间形态(per taskIndex 聚合
|
|
@@ -418,21 +417,60 @@ function draftsToAgentCalls(
|
|
|
418
417
|
}
|
|
419
418
|
|
|
420
419
|
|
|
420
|
+
/**
|
|
421
|
+
* spec 重建预算的读取面三档回落(与 core resume-run.assertResumeEligibility 等价,
|
|
422
|
+
* 少 options 档——resume 的显式覆盖已由 core 写进 run-resumed 帧,本折叠只表达
|
|
423
|
+
* record 事实):最近一条 run-resumed 的生效值 > run-created 的创建预算;两者
|
|
424
|
+
* 皆无/<=0 = 不限制。取流尾最近一条 run-resumed 而非「最近一条带字段」——更晚的
|
|
425
|
+
* 「不限时复活」(0/负值不落字段)须回落 created,而非错误地沿用更早的覆盖值。
|
|
426
|
+
* 回落按**逐字段 `??`**(两轴独立):run-resumed 帧可只载一轴(另一轴沿 created
|
|
427
|
+
* 继承),与 core 侧 `lastResumed?.budgetTimeMs ?? created.budgetTimeMs` 同构。
|
|
428
|
+
* 双预算轴(budgetTimeMs / budgetTokens)同遍历;`findLast` 属 ES2023 lib
|
|
429
|
+
* (本包 target ES2022)——从尾向头手写。
|
|
430
|
+
*/
|
|
431
|
+
function resolveSpecBudget(
|
|
432
|
+
created: Extract<WorkflowRunEvent, { type: "run-created" }> | undefined,
|
|
433
|
+
events: readonly WorkflowRunEvent[],
|
|
434
|
+
): { budgetTimeMs?: number; budgetTokens?: number } {
|
|
435
|
+
let lastResumed: Extract<WorkflowRunEvent, { type: "run-resumed" }> | undefined;
|
|
436
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
437
|
+
const event = events[i]!;
|
|
438
|
+
if (event.type === "run-resumed") {
|
|
439
|
+
lastResumed = event;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const budgetTimeMs = lastResumed?.budgetTimeMs ?? created?.budgetTimeMs;
|
|
444
|
+
const budgetTokens = lastResumed?.budgetTokens ?? created?.budgetTokens;
|
|
445
|
+
return {
|
|
446
|
+
...(budgetTimeMs !== undefined ? { budgetTimeMs } : {}),
|
|
447
|
+
...(budgetTokens !== undefined ? { budgetTokens } : {}),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
421
451
|
/** [foldRecordStreamToRun 拆分] run spec 重建(run-created 帧优先,注册条目兜底):
|
|
422
452
|
* args 全文优先(设计 §3.1 载荷表 run-created 行「args」);旧格式帧回落
|
|
423
453
|
* argsSummary 尽力恢复(未截断可完整恢复,截断回落 {}——core parseArgsSummary
|
|
424
454
|
* 同款语义;两侧行为等价由 record-mode 测试锁定);scriptPath 锚定恢复(core
|
|
425
455
|
* rebuildRunFromRecord 同款):worker 沙箱 eval 模式无 __dirname,模板脚本靠
|
|
426
|
-
* scriptPath 定位 _shared 族共享件;旧格式帧缺失回落空串。
|
|
456
|
+
* scriptPath 定位 _shared 族共享件;旧格式帧缺失回落空串。budgetTimeMs/budgetTokens
|
|
457
|
+
* 恢复(core 同款三档回落的 record 侧:最近一条 run-resumed 的生效值 > run-created
|
|
458
|
+
* 的创建预算,仅 > 0 落 spec):引擎/展示投影按 run 生效预算读(缺字段 = 旧格式/
|
|
459
|
+
* 未设预算 = 不限制)。 */
|
|
427
460
|
function rebuildRunSpecFromEntries(
|
|
428
461
|
created: Extract<WorkflowRunEvent, { type: "run-created" }> | undefined,
|
|
429
462
|
reg: WorkflowRecordRegisteredEntryData,
|
|
463
|
+
budgets: { budgetTimeMs?: number; budgetTokens?: number },
|
|
430
464
|
) {
|
|
431
465
|
return {
|
|
432
466
|
scriptSource: created?.scriptSource ?? "",
|
|
433
467
|
args: created?.args ?? parseLegacyArgsSummary(created?.argsSummary),
|
|
434
468
|
scriptName: reg.scriptName,
|
|
435
469
|
scriptPath: created?.scriptPath ?? "",
|
|
470
|
+
// 条件式与 core rebuildRunFromRecord 等价(> 0 才落字段)——旧格式帧/0/负值
|
|
471
|
+
// 一律不限时/不限制,两侧折叠结果同形
|
|
472
|
+
...(budgets.budgetTimeMs !== undefined && budgets.budgetTimeMs > 0 ? { budgetTimeMs: budgets.budgetTimeMs } : {}),
|
|
473
|
+
...(budgets.budgetTokens !== undefined && budgets.budgetTokens > 0 ? { budgetTokens: budgets.budgetTokens } : {}),
|
|
436
474
|
...(reg.slug !== undefined ? { slug: reg.slug } : {}),
|
|
437
475
|
};
|
|
438
476
|
}
|
|
@@ -441,6 +479,7 @@ function foldRecordStreamToRun(
|
|
|
441
479
|
runId: string,
|
|
442
480
|
reg: WorkflowRecordRegisteredEntryData,
|
|
443
481
|
events: readonly WorkflowRunEvent[],
|
|
482
|
+
settled?: WorkflowRecordSettledEntryData,
|
|
444
483
|
): WorkflowRun {
|
|
445
484
|
const created = events.find(
|
|
446
485
|
(e): e is Extract<WorkflowRunEvent, { type: "run-created" }> => e.type === "run-created",
|
|
@@ -448,7 +487,13 @@ function foldRecordStreamToRun(
|
|
|
448
487
|
const startedAtMs =
|
|
449
488
|
created?.ts ?? (Number.isFinite(reg.startedAt) ? reg.startedAt : Date.now());
|
|
450
489
|
const startedAtIso = new Date(startedAtMs).toISOString();
|
|
451
|
-
const spec = rebuildRunSpecFromEntries(created, reg);
|
|
490
|
+
const spec = rebuildRunSpecFromEntries(created, reg, resolveSpecBudget(created, events));
|
|
491
|
+
// [§2.1b] 会计重建:v2 终态条目带活体口径 usedTokens/callCount → 真值优先;条目缺席
|
|
492
|
+
// 回落 agent-settled.result.usage 的同一加权口径(下界近似,含 usedCost)。
|
|
493
|
+
const budget = rebuildBudget(settled, events);
|
|
494
|
+
// [§2.1 errorLogs 持久化 / ADR-0093] 诊断日志重建:worker-log 帧 → errorLogs(与活体
|
|
495
|
+
// 写入同语义:按序 + 尾部上限裁剪)。此前无持久面、重启即空。
|
|
496
|
+
const errorLogs = errorLogsFromEvents(events);
|
|
452
497
|
|
|
453
498
|
const drafts = collectRunCallDrafts(events);
|
|
454
499
|
|
|
@@ -467,18 +512,17 @@ function foldRecordStreamToRun(
|
|
|
467
512
|
runId,
|
|
468
513
|
spec,
|
|
469
514
|
{
|
|
470
|
-
|
|
471
|
-
budget: new Budget(),
|
|
515
|
+
budget,
|
|
472
516
|
calls,
|
|
473
517
|
trace,
|
|
474
|
-
errorLogs
|
|
518
|
+
errorLogs,
|
|
475
519
|
},
|
|
476
520
|
{
|
|
477
521
|
startedAt: startedAtIso,
|
|
478
|
-
// 中断标记([D2]
|
|
479
|
-
// run-
|
|
480
|
-
|
|
481
|
-
//
|
|
522
|
+
// 中断标记([D2] 中断态经 meta 投影表达):末次 run-interrupted 后无
|
|
523
|
+
// run-resumed 复活 → meta.interruptedAt 置位,runSummary 投影 'interrupted'
|
|
524
|
+
//(CLI/TUI 不显示僵尸「运行中」;resume 资格判据在 core fold lifecycle,
|
|
525
|
+
// 不受本投影影响)。
|
|
482
526
|
...(interruptedAt !== undefined ? { interruptedAt } : {}),
|
|
483
527
|
},
|
|
484
528
|
);
|
|
@@ -488,12 +532,11 @@ function foldRecordStreamToRun(
|
|
|
488
532
|
runId,
|
|
489
533
|
spec,
|
|
490
534
|
{
|
|
491
|
-
status: "done",
|
|
492
535
|
reason,
|
|
493
|
-
budget
|
|
536
|
+
budget,
|
|
494
537
|
calls,
|
|
495
538
|
trace,
|
|
496
|
-
errorLogs
|
|
539
|
+
errorLogs,
|
|
497
540
|
// 成功终局无 error;失败终局带帧 reason 文本(不落 generic 恢复文案)
|
|
498
541
|
...(reason !== "completed" && settledEvent.reason !== undefined ? { error: settledEvent.reason } : {}),
|
|
499
542
|
},
|
|
@@ -555,7 +598,7 @@ export class JsonlRunStore implements RunStore {
|
|
|
555
598
|
|
|
556
599
|
/** record 流路径 for a given runId(唯一持久件)。 */
|
|
557
600
|
private recordPathFor(runId: string): string {
|
|
558
|
-
return path.join(this.stateDir, `${runId}${
|
|
601
|
+
return path.join(this.stateDir, `${runId}${RUN_EVENTS_SUFFIX}`);
|
|
559
602
|
}
|
|
560
603
|
|
|
561
604
|
/**
|
|
@@ -616,7 +659,7 @@ export class JsonlRunStore implements RunStore {
|
|
|
616
659
|
* 保持 running 交恢复链收编);已终局且终态条目缺失 → 幂等补写(条目投影锚
|
|
617
660
|
* 的收编半边;经 rebind 后的 appendEntry 面,stale guard 内置)。
|
|
618
661
|
*
|
|
619
|
-
* **历史实体分流(D1 历史数据处置)**:注册条目
|
|
662
|
+
* **历史实体分流(D1 历史数据处置)**:注册条目 recordPath 锚点后缀非
|
|
620
663
|
* record 后缀 = 旧形态实体(旧 journal 锚点)→ 跳过不重建(历史 run 从壳侧
|
|
621
664
|
* 读取面消失 = 预期行为,不读旧两件套)。
|
|
622
665
|
*
|
|
@@ -665,8 +708,8 @@ export class JsonlRunStore implements RunStore {
|
|
|
665
708
|
): WorkflowRun[] {
|
|
666
709
|
const runs: WorkflowRun[] = [];
|
|
667
710
|
for (const [runId, reg] of registered) {
|
|
668
|
-
const recordPath = reg.
|
|
669
|
-
if (!recordPath.endsWith(
|
|
711
|
+
const recordPath = reg.recordPath;
|
|
712
|
+
if (!recordPath.endsWith(RUN_EVENTS_SUFFIX)) {
|
|
670
713
|
// 旧形态锚点(旧 journal .events.jsonl)= 历史 run:不读旧两件套
|
|
671
714
|
//(D1 历史数据处置——历史 run 从壳侧读取面消失,resume 一律拒绝)。
|
|
672
715
|
logger.debug(
|
|
@@ -695,7 +738,7 @@ export class JsonlRunStore implements RunStore {
|
|
|
695
738
|
logger.warn(
|
|
696
739
|
`[subagent-workflow] record store: record stream missing, degraded rebuild for interruption adoption (runId=${runId}, path=${recordPath})`,
|
|
697
740
|
);
|
|
698
|
-
runs.push(foldRecordStreamToRun(runId, reg, []));
|
|
741
|
+
runs.push(foldRecordStreamToRun(runId, reg, [], settledEntries.get(runId)));
|
|
699
742
|
continue;
|
|
700
743
|
}
|
|
701
744
|
// 损坏 / 真实 IO 错误:拒绝(穿透 → 宿主 fail-fast),不静默降级。
|
|
@@ -711,8 +754,15 @@ export class JsonlRunStore implements RunStore {
|
|
|
711
754
|
);
|
|
712
755
|
continue;
|
|
713
756
|
}
|
|
714
|
-
const run = foldRecordStreamToRun(runId, reg, events);
|
|
757
|
+
const run = foldRecordStreamToRun(runId, reg, events, settledEntries.get(runId));
|
|
715
758
|
runs.push(run);
|
|
759
|
+
// [D6(a) 第 1 步] 终局事实随重建产物带到消费面:fold 判定出「该 run 的 record
|
|
760
|
+
// 流里有 run-settled 帧」后,把帧载荷登记进 core 终局记录注册表——重启后
|
|
761
|
+
// runSummary 投影(展示仍为 done)与 isRunSettled / evictDoneRunsBeyondCap
|
|
762
|
+
// (done run 内存淘汰白名单)据此判定,不再绕道聚合 status 字段读回。
|
|
763
|
+
if (settledEvent !== undefined) {
|
|
764
|
+
noteRebuiltSettlement(runId, settlementRecordOfRunSettledFrame(settledEvent));
|
|
765
|
+
}
|
|
716
766
|
// 终态条目幂等补写:record 已终局而主 session 终态条目缺失(终局 coda 的
|
|
717
767
|
// 条目半边写失败 / 旧版本写点形态)→ 补写;条目已在 → 跳过(双重启不重复
|
|
718
768
|
// 追加的构造性保证)。无 pi(测试/非 Pi 环境)跳过。
|
|
@@ -743,7 +793,9 @@ export class JsonlRunStore implements RunStore {
|
|
|
743
793
|
...(settledEvent.errorCode !== undefined ? { errorCode: settledEvent.errorCode } : {}),
|
|
744
794
|
settledAt: settledEvent.ts,
|
|
745
795
|
callCount: events.filter((e) => e.type === "agent-settled").length,
|
|
746
|
-
|
|
796
|
+
// [§2.1b] 由 agent-settled.result.usage 推导(core 单源加权口径;此前硬编码 0,
|
|
797
|
+
// 展示层把补写条目显示成零消耗)
|
|
798
|
+
usedTokens: runAccountingFromEvents(events).usedTokens,
|
|
747
799
|
});
|
|
748
800
|
try {
|
|
749
801
|
this.pi.appendEntry(WORKFLOW_RECORD_CUSTOM_TYPE, data);
|
package/src/session-lifecycle.ts
CHANGED
|
@@ -75,6 +75,7 @@ import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
|
75
75
|
// 「导入面折叠直连」语义)
|
|
76
76
|
import { normalizeEngineId } from "@zhushanwen/subagent-core";
|
|
77
77
|
import { JsonlRunStore } from "./jsonl-run-store.ts";
|
|
78
|
+
import { GLOBAL_SLOT_KEYS } from "@zhushanwen/subagent-core";
|
|
78
79
|
|
|
79
80
|
// 模块级 logger(与原 index.ts 同 component 名;setPiHandle 注入后自动走 appendEntry)
|
|
80
81
|
const logger = getLogger("subagents");
|
|
@@ -288,7 +289,7 @@ async function collectAliveWorkflowRunReferences(
|
|
|
288
289
|
// dialog queue 仍为本模块私有单例——消费方 = 本文件 createOrReuseServices +
|
|
289
290
|
// index.ts session_shutdown(flush L2 pending dialog)。
|
|
290
291
|
|
|
291
|
-
const DIALOG_QUEUE_KEY = Symbol.for(
|
|
292
|
+
const DIALOG_QUEUE_KEY = Symbol.for(GLOBAL_SLOT_KEYS.dialogQueue);
|
|
292
293
|
|
|
293
294
|
/** 获取或创建进程级 dialog queue 单例。
|
|
294
295
|
* L2 跨子进程串行队列——所有子进程的 dialog 类请求共享同一队列实例。 */
|