@zhushanwen/pi-subagent-workflow 8.11.0 → 8.12.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.
@@ -12,8 +12,9 @@
12
12
 
13
13
  import { Key, matchesKey } from "@earendil-works/pi-tui";
14
14
 
15
- import { getAllToolCalls, projectLiveProgress } from "@zhushanwen/subagent-core";
15
+ import { computeElapsedSeconds } from "@zhushanwen/subagent-core";
16
16
  import type { AgentEventLogEntry } from "@zhushanwen/subagent-core";
17
+ import type { SubagentRecord } from "@zhushanwen/subagent-core";
17
18
  import type { ExecutionTraceNode } from "@zhushanwen/subagent-core";
18
19
  import type { WorkflowRun } from "@zhushanwen/subagent-core";
19
20
  import {
@@ -41,6 +42,49 @@ import {
41
42
  /** 探测宽度:足够大避免截断折行影响行数统计(对齐 subagents DETAIL_LEN_PROBE_WIDTH)。 */
42
43
  const DETAIL_LEN_PROBE_WIDTH = 9999;
43
44
 
45
+ // ── [H2 W3] store record 的 live 进度投影(设计 D2:进度源从 node.live 切 store)──
46
+
47
+ /**
48
+ * running record 的实时进度投影——消费面七字段口径与旧 projectLiveProgress(node.live)
49
+ * 逐字段对齐(totalTokens / 工具计数 / elapsedSeconds / turns / eventLog /
50
+ * currentActivity / lastError)。
51
+ */
52
+ export interface LiveProgressView {
53
+ totalTokens: number;
54
+ toolCallCount: number;
55
+ elapsedSeconds: number;
56
+ turns: number;
57
+ eventLog: AgentEventLogEntry[];
58
+ currentActivity?: { type: "tool" | "text" | "thinking"; label: string };
59
+ lastError?: string;
60
+ }
61
+
62
+ /**
63
+ * SubagentRecord(store 查询投影)→ LiveProgressView。与旧路径
64
+ * projectLiveProgress(node.live) 的字段等价性(构造性论证,S1 等价表依据):
65
+ * - totalTokens / turns:recordToSubagent 直读 record.totalTokens / turnCount,同源;
66
+ * - toolCallCount:eventLog 里 tool_start 计数——getEventLog 对每个 toolCall 恰产
67
+ * 一条 tool_start,与 getAllToolCalls(record).length 恒等;
68
+ * - elapsedSeconds:computeElapsedSeconds 同一函数(startedAt/endedAt 同字段);
69
+ * - eventLog / currentActivity:recordToSubagent 内部即调 getEventLog /
70
+ * getCurrentActivity(同源 API,仅内存 running record 有 currentActivity);
71
+ * - lastError:eventLog 末条 type==="error" 的 label——getEventLog 仅当
72
+ * record.lastError 非空时追加该条且必在末尾,存在性 ⟺ 非空,label 即原文。
73
+ */
74
+ export function projectRecordProgress(rec: SubagentRecord): LiveProgressView {
75
+ const eventLog = rec.eventLog;
76
+ const last = eventLog[eventLog.length - 1];
77
+ return {
78
+ totalTokens: rec.totalTokens,
79
+ toolCallCount: eventLog.filter((e) => e.type === "tool_start").length,
80
+ elapsedSeconds: computeElapsedSeconds(rec),
81
+ turns: rec.turns,
82
+ eventLog,
83
+ currentActivity: rec.currentActivity,
84
+ lastError: last !== undefined && last.type === "error" ? last.label : undefined,
85
+ };
86
+ }
87
+
44
88
  /** status → 语义色标签(L2 detail 头用)。 */
45
89
  export function statusLabel(status: string, theme: ThemeLike): string {
46
90
  switch (status) {
@@ -57,7 +101,9 @@ export function statusLabel(status: string, theme: ThemeLike): string {
57
101
  * 构建 L2 右侧详情的完整内容行。
58
102
  *
59
103
  * 纯函数:无 Pi runtime、无副作用,可单测。入参 promptExpanded 用结构化类型,
60
- * 不依赖完整 ViewState(便于测试构造)。
104
+ * 不依赖完整 ViewState(便于测试构造)。liveView = 该节点配对的 running record 投影
105
+ * ([H2 W3] 从 trace.live 切 store 订阅,经 WorkflowsView.collectNodeLiveProgress
106
+ * 配对注入;缺省走终态 result 路径)。
61
107
  */
62
108
  export function buildDetailContent(
63
109
  node: ExecutionTraceNode,
@@ -66,6 +112,7 @@ export function buildDetailContent(
66
112
  theme: ThemeLike,
67
113
  mainWidth: number,
68
114
  now: number,
115
+ liveView?: LiveProgressView,
69
116
  ): string[] {
70
117
  const rightLines: string[] = [];
71
118
  const elapsed = formatElapsed(
@@ -75,20 +122,18 @@ export function buildDetailContent(
75
122
  rightLines.push(theme.fg("muted", "Detail"));
76
123
  rightLines.push("─".repeat(mainWidth));
77
124
  rightLines.push(`${statusDotStr(node.status, theme)} ${statusLabel(node.status, theme)} · ${node.model}`);
78
- // Live 路径优先:运行中用 node.live 的实时 usage/toolCalls/elapsed;否则用终态 result。
79
- if (node.live) {
80
- const live = projectLiveProgress(node.live);
81
- const tokK = live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "0 tok";
82
- const tcCount = getAllToolCalls(node.live).length;
83
- rightLines.push(theme.fg("dim", `${tokK} · ${tcCount} tool calls · ${formatElapsedSeconds(live.elapsedSeconds)}`));
125
+ // Live 路径优先:运行中用 store record 投影的实时 usage/toolCalls/elapsed;否则用终态 result。
126
+ if (liveView) {
127
+ const tokK = liveView.totalTokens > 0 ? `${Math.round(liveView.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "0 tok";
128
+ rightLines.push(theme.fg("dim", `${tokK} · ${liveView.toolCallCount} tool calls · ${formatElapsedSeconds(liveView.elapsedSeconds)}`));
84
129
  } else {
85
130
  rightLines.push(theme.fg("dim", formatTokenStat(node.result?.usage, node.result?.toolCalls, elapsed)));
86
131
  }
87
132
  rightLines.push("");
88
133
  renderWorkerLogSection(rightLines, run, mainWidth, theme);
89
134
  renderPromptSection(rightLines, node, state, theme);
90
- renderActivitySection(rightLines, node, mainWidth, theme);
91
- renderOutcomeSection(rightLines, node, mainWidth, theme);
135
+ renderActivitySection(rightLines, node, liveView, mainWidth, theme);
136
+ renderOutcomeSection(rightLines, node, liveView, mainWidth, theme);
92
137
  renderSessionSection(rightLines, node, mainWidth, theme);
93
138
  return rightLines;
94
139
  }
@@ -99,8 +144,9 @@ export function detailContentLength(
99
144
  state: { promptExpanded: boolean },
100
145
  run: WorkflowRun,
101
146
  theme: ThemeLike,
147
+ live?: LiveProgressView,
102
148
  ): number {
103
- return buildDetailContent(node, state, run, theme, DETAIL_LEN_PROBE_WIDTH, Date.now()).length;
149
+ return buildDetailContent(node, state, run, theme, DETAIL_LEN_PROBE_WIDTH, Date.now(), live).length;
104
150
  }
105
151
 
106
152
  // ── L2 详情滚动按键(纯函数,对齐 subagents processKey)─────────
@@ -214,20 +260,20 @@ function renderPromptSection(
214
260
  function renderActivitySection(
215
261
  rightLines: string[],
216
262
  node: ExecutionTraceNode,
263
+ liveView: LiveProgressView | undefined,
217
264
  mainWidth: number,
218
265
  theme: ThemeLike,
219
266
  ): void {
220
- // Live 路径:agent 运行中,从 node.live 派生实时 eventLog + currentActivity。
267
+ // Live 路径:agent 运行中,从 store record 投影派生实时 eventLog + currentActivity。
221
268
  // 与 subagents TUI 一致:当前活动行 + 最近 N 条离散事件(tool/turn_end/error)。
222
- if (node.live) {
223
- const live = projectLiveProgress(node.live);
224
- const eventLog = live.eventLog.filter((e) => e.type !== "turn_end");
225
- const totalCount = getAllToolCalls(node.live).length;
226
- const label = `Activity · ${totalCount} tool call${totalCount !== 1 ? "s" : ""} · ${live.turns} turn${live.turns !== 1 ? "s" : ""}`;
269
+ if (liveView) {
270
+ const eventLog = liveView.eventLog.filter((e) => e.type !== "turn_end");
271
+ const totalCount = liveView.toolCallCount;
272
+ const label = `Activity · ${totalCount} tool call${totalCount !== 1 ? "s" : ""} · ${liveView.turns} turn${liveView.turns !== 1 ? "s" : ""}`;
227
273
  rightLines.push(theme.fg("muted", label));
228
274
  // 当前活动行(running tool / thinking / text)
229
- if (live.currentActivity) {
230
- rightLines.push(theme.fg("accent", ` ⎿ ${live.currentActivity.type}: ${live.currentActivity.label}`.slice(0, mainWidth - BOX_BORDER_CHARS)));
275
+ if (liveView.currentActivity) {
276
+ rightLines.push(theme.fg("accent", ` ⎿ ${liveView.currentActivity.type}: ${liveView.currentActivity.label}`.slice(0, mainWidth - BOX_BORDER_CHARS)));
231
277
  }
232
278
  // 最近 N 条事件
233
279
  const showCount = Math.min(MAX_TOOL_CALLS_DISPLAY, eventLog.length);
@@ -236,7 +282,7 @@ function renderActivitySection(
236
282
  const entry = eventLog[i] as AgentEventLogEntry;
237
283
  rightLines.push(theme.fg("dim", ` ${formatTraceEventLine(entry, theme)}`.slice(0, mainWidth - BOX_BORDER_CHARS)));
238
284
  }
239
- if (totalCount === 0 && !live.currentActivity) {
285
+ if (totalCount === 0 && !liveView.currentActivity) {
240
286
  rightLines.push(theme.fg("dim", " (starting...)"));
241
287
  }
242
288
  rightLines.push("");
@@ -267,17 +313,17 @@ function renderActivitySection(
267
313
  function renderOutcomeSection(
268
314
  rightLines: string[],
269
315
  node: ExecutionTraceNode,
316
+ liveView: LiveProgressView | undefined,
270
317
  mainWidth: number,
271
318
  theme: ThemeLike,
272
319
  ): void {
273
320
  rightLines.push(theme.fg("muted", "Outcome"));
274
- if (node.status === "running" && node.live) {
321
+ if (node.status === "running" && liveView) {
275
322
  // 运行中:显示实时指标(elapsed/tokens/turns)替代空荡的 "Still running..."
276
- const live = projectLiveProgress(node.live);
277
- const tokK = live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "0 tok";
278
- rightLines.push(theme.fg("dim", ` Running · ${formatElapsedSeconds(live.elapsedSeconds)} · ${tokK} · ${live.turns} turn${live.turns !== 1 ? "s" : ""}`));
279
- if (live.lastError) {
280
- rightLines.push(theme.fg("warning", ` ⚠ ${live.lastError.slice(0, mainWidth - BOX_BORDER_CHARS)}`));
323
+ const tokK = liveView.totalTokens > 0 ? `${Math.round(liveView.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "0 tok";
324
+ rightLines.push(theme.fg("dim", ` Running · ${formatElapsedSeconds(liveView.elapsedSeconds)} · ${tokK} · ${liveView.turns} turn${liveView.turns !== 1 ? "s" : ""}`));
325
+ if (liveView.lastError) {
326
+ rightLines.push(theme.fg("warning", ` ⚠ ${liveView.lastError.slice(0, mainWidth - BOX_BORDER_CHARS)}`));
281
327
  }
282
328
  } else if (node.status === "running") {
283
329
  rightLines.push(theme.fg("dim", " Still running..."));
@@ -50,7 +50,7 @@
50
50
  * - pi 侧版本不匹配静默跳过语义保持(D-5);「缺 v 宽容」是 core FileRunStore
51
51
  * 侧的存量预处理职责,不内聚进 codec(D4 裁决②),故本侧零改动即保持。
52
52
  *
53
- * [S3 查证结论] pi 0.84.1 实装(node_modules/@earendil-works/pi-coding-agent/dist,
53
+ * [S3 查证结论] pi 0.84.4 实装(node_modules/@earendil-works/pi-coding-agent/dist,
54
54
  * core/session-manager.js,PS-19)的 session 生命周期管理不含自动 GC:
55
55
  * listSessionsFromDir 只做只读扫描(readdir + `.jsonl` 过滤 + header 解析,:548-571,
56
56
  * 非递归——`<sessionDir>/workflow-state/` 子目录完全不在 pi 的任何扫描/清理范围内),
@@ -8,7 +8,7 @@
8
8
  * 1. identity env→appendEntry 重建(类型 13 字段含 1 个 @deprecated,写入 12)
9
9
  * 2. notify ledger host 装配 + 重启恢复
10
10
  * 3. 双 Service 装配 + initSession(createOrReuseServices 封装,单例语义 D8)
11
- * 4. GC / manifest tmp / worktree 恢复
11
+ * 4. GC / manifest tmp 清扫([U4c/D6] promote 退役)/ 索引重建([U4c/G1] boot 全量腿)/ worktree 恢复
12
12
  * 5. per-session run store + kill-9 恢复循环 + evictDoneRunsBeyondCap
13
13
  * 6. SAR + engine 基线(经 SessionLifecycleResult 返回,sessionState 写入留在组合根)
14
14
  *
@@ -408,17 +408,33 @@ async function runProcessLevelMaintenance(
408
408
  });
409
409
  }
410
410
 
411
- // ADR-035 启动恢复:扫描 manifest tmp 残留(崩溃打断的 writeManifest 留下,promote/unlink)。
411
+ // ADR-035 启动清扫:manifest tmp 残留(崩溃打断的 writeManifest 留下)。
412
+ // [U4c / D6] tmp 恢复退役为静默删除——manifest 已是可丢可重建缓存(权威 =
413
+ // `.state`,重建走下方 rebuildIndexes 钩子),promote 半写 tmp 的恢复语义失效。
412
414
  // 扫描属进程级维护——oncePerProcess 守卫防双跑(u-audit-fix);第二派发重放首次
413
- // Promise(结果缓存语义),recovered 计数日志可能重打,无文件副作用。
415
+ // Promise(结果缓存语义),deleted 计数日志可能重打,无文件副作用。
414
416
  try {
415
- const recovered = await oncePerProcess("subagent-workflow:recover-manifest-tmp-files", () =>
417
+ const swept = await oncePerProcess("subagent-workflow:sweep-manifest-tmp-files", () =>
416
418
  service.recoverManifestTmpFiles());
417
- if (recovered.recovered > 0 || recovered.deleted > 0) {
418
- logger.warn(`[subagents] manifest tmp recovery: ${recovered.recovered} promoted, ${recovered.deleted} deleted`);
419
+ if (swept.deleted > 0) {
420
+ logger.warn(`[subagents] manifest tmp sweep: ${swept.deleted} stale tmp file(s) removed (manifest is rebuildable cache)`);
419
421
  }
420
422
  } catch (err) {
421
- logger.warn("[subagents] manifest tmp recovery failed", {
423
+ logger.warn("[subagents] manifest tmp sweep failed", {
424
+ reason: toErrorMessage(err),
425
+ });
426
+ }
427
+
428
+ // [U4c / G1] 缓存降级重建(boot 全量腿):boot revive 已完成(createOrReuseServices
429
+ // 内 initSession 的孤儿恢复/可重连 entry 重物化先于本 helper),此处全量重建可丢
430
+ // 缓存——manifest 幂等补缺 + sessions-index 经首扫自愈重写。进程级维护(磁盘域
431
+ // 全量),oncePerProcess 守卫防双跑;/resume /fork 同进程复用实例时跳过(查询面
432
+ // 惰性通道兜底,且终态写点本身维持 manifest 就位)。
433
+ try {
434
+ oncePerProcess("subagent-workflow:rebuild-record-indexes", () =>
435
+ service.rebuildIndexes());
436
+ } catch (err) {
437
+ logger.warn("[subagents] record index rebuild failed", {
422
438
  reason: toErrorMessage(err),
423
439
  });
424
440
  }