@zhushanwen/pi-subagent-workflow 9.3.0 → 9.4.1

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.
@@ -67,16 +67,20 @@ import {
67
67
  noteRebuiltSettlement,
68
68
  settlementRecordOfRunSettledFrame,
69
69
  type AgentResult,
70
+ type Budget,
70
71
  type ExecutionTraceNode,
71
72
  type RunEventLineIssue,
72
73
  type RunOutcome,
74
+ type RunSpec,
73
75
  type RunStore,
76
+ type WorkflowModelOverride,
74
77
  type WorkflowRecordRegisteredEntryData,
75
78
  type WorkflowRecordSettledEntryData,
76
79
  type WorkflowRunEvent,
80
+ type WorkerLogEntry,
77
81
  } from "@zhushanwen/subagent-core";
78
82
  import { WorkflowRun } from "@zhushanwen/subagent-core";
79
- import { errorLogsFromEvents, rebuildBudget, runAccountingFromEvents } from "@zhushanwen/subagent-core";
83
+ import { errorLogsFromEvents, latestModelOverride, rebuildBudget, runAccountingFromEvents } from "@zhushanwen/subagent-core";
80
84
  import { guardStaleCtx, isEnoentError, toErrorMessage } from "@zhushanwen/pi-ext-guards";
81
85
 
82
86
  // ── [W1 / D1] v2 条目读面(注册定界 + 终态条目抑制)────────────────
@@ -283,7 +287,7 @@ interface CallDraft {
283
287
  /**
284
288
  * record 事件流 → WorkflowRun 聚合重建(纯函数,零 IO)。
285
289
  *
286
- * fold 语义(词表 = run-events.ts [D4] 对齐后 9 事件):
290
+ * fold 语义(词表 = run-events.ts 事件词表,单源):
287
291
  * - `run-created`:spec.scriptSource(全文,[D1])+ args(全文,设计 §3.1 载荷表;
288
292
  * 旧格式帧回落 argsSummary 尽力恢复——parseLegacyArgsSummary)+ startedAt 锚点
289
293
  * (帧 ts 优先,回落注册条目 startedAt);
@@ -294,7 +298,10 @@ interface CallDraft {
294
298
  * 形态建占位行——同 journal fold 兜底);sessionFile/sessionId 从 result 透传
295
299
  * (对齐 finalizeCall 的 call 字段填充纪律);
296
300
  * - `run-settled`:终局(status=done + reason 映射 + completedAt=帧 ts);无帧 =
297
- * running(交恢复链收编)。
301
+ * running(交恢复链收编);
302
+ * - `model-override`:latestModelOverride 折叠 → meta.modelOverride(最新一条生效,
303
+ * 覆盖旧覆盖值不叠加;无覆盖不造键——详见 foldRecordStreamToRun 内
304
+ * [subagent-model-switch §6.6①/§7.4] 注释,不在此重复展开)。
298
305
  *
299
306
  * record 流不承载的字段(budget 计数/errorLogs/trace 完整面)按恢复语义最小形态
300
307
  * 缺省——步骤级详情的恢复读面 = record 流直读(session-reader 家族链),不经本聚合。
@@ -475,26 +482,32 @@ function rebuildRunSpecFromEntries(
475
482
  };
476
483
  }
477
484
 
478
- function foldRecordStreamToRun(
479
- runId: string,
480
- reg: WorkflowRecordRegisteredEntryData,
481
- events: readonly WorkflowRunEvent[],
482
- settled?: WorkflowRecordSettledEntryData,
483
- ): WorkflowRun {
484
- const created = events.find(
485
- (e): e is Extract<WorkflowRunEvent, { type: "run-created" }> => e.type === "run-created",
486
- );
487
- const startedAtMs =
488
- created?.ts ?? (Number.isFinite(reg.startedAt) ? reg.startedAt : Date.now());
489
- const startedAtIso = new Date(startedAtMs).toISOString();
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);
485
+ /**
486
+ * [foldRecordStreamToRun 拆分] 两分支 reconstruct 共用的 fold 产物(spec 之外的
487
+ * 快照/元数据公共字段)。
488
+ */
489
+ interface FoldCommonParts {
490
+ budget: Budget;
491
+ errorLogs: WorkerLogEntry[];
492
+ startedAt: string;
493
+ }
494
+
495
+ /**
496
+ * [foldRecordStreamToRun 拆分] 两分支 reconstruct 共用的聚合重建块(trace + calls +
497
+ * 覆盖记账——原 fold 主干的公共块提取)。
498
+ */
499
+ interface RebuiltRunAggregate {
500
+ calls: Map<number, AgentCall>;
501
+ trace: Trace;
502
+ /** 生效模型覆盖记账(无覆盖 undefined 不造键——语义见 rebuildRunAggregate 内注释)。 */
503
+ modelOverride: WorkflowModelOverride | undefined;
504
+ }
497
505
 
506
+ /**
507
+ * 聚合重建块:drafts → trace 节点 → Trace 副本 → call 集合(D-10 引用共享链)+
508
+ * 覆盖记账折叠。纯函数,零 IO。
509
+ */
510
+ function rebuildRunAggregate(events: readonly WorkflowRunEvent[]): RebuiltRunAggregate {
498
511
  const drafts = collectRunCallDrafts(events);
499
512
 
500
513
  // Trace 先重建:call 的 traceNode 回链到 Trace 副本(D-10 引用共享;匹配不到
@@ -505,48 +518,123 @@ function foldRecordStreamToRun(
505
518
 
506
519
  const calls = draftsToAgentCalls(drafts, sharedNodes, nodes);
507
520
 
508
- const settledEvent = lastRunSettledEvent(events);
509
- if (settledEvent === undefined) {
510
- const interruptedAt = lastInterruptedAt(events);
511
- return WorkflowRun.reconstruct(
512
- runId,
513
- spec,
514
- {
515
- budget,
516
- calls,
517
- trace,
518
- errorLogs,
519
- },
520
- {
521
- startedAt: startedAtIso,
522
- // 中断标记([D2] 中断态经 meta 投影表达):末次 run-interrupted 后无
523
- // run-resumed 复活 → meta.interruptedAt 置位,runSummary 投影 'interrupted'
524
- //(CLI/TUI 不显示僵尸「运行中」;resume 资格判据在 core fold lifecycle,
525
- // 不受本投影影响)。
526
- ...(interruptedAt !== undefined ? { interruptedAt } : {}),
527
- },
528
- );
529
- }
521
+ // [subagent-model-switch §6.6①/§7.4] 覆盖记账折叠(派生视图半边):生效覆盖值 =
522
+ // 最新一条(latestModelOverride core 单点——覆盖旧覆盖值不叠加,不变量 2);
523
+ // 无覆盖 undefined 不造键。fold 未跟时本行缺失 = 覆盖状态静默丢行——
524
+ // jsonl-run-store-loadall 的 fold 消费用例是此丢行风险的机器防线(P8 第五道)。
525
+ const modelOverride = latestModelOverride(events);
526
+ return { calls, trace, modelOverride };
527
+ }
528
+
529
+ /**
530
+ * 非终局分支 reconstruct:无 run-settled 帧 = running(交恢复链收编)+ 中断标记
531
+ * 投影([D2])。
532
+ */
533
+ function reconstructUnsettledRun(
534
+ runId: string,
535
+ spec: RunSpec,
536
+ aggregate: RebuiltRunAggregate,
537
+ parts: FoldCommonParts,
538
+ events: readonly WorkflowRunEvent[],
539
+ ): WorkflowRun {
540
+ const interruptedAt = lastInterruptedAt(events);
541
+ return WorkflowRun.reconstruct(
542
+ runId,
543
+ spec,
544
+ {
545
+ budget: parts.budget,
546
+ calls: aggregate.calls,
547
+ trace: aggregate.trace,
548
+ errorLogs: parts.errorLogs,
549
+ },
550
+ {
551
+ startedAt: parts.startedAt,
552
+ // 中断标记([D2] 中断态经 meta 投影表达):末次 run-interrupted 后无
553
+ // run-resumed 复活 → meta.interruptedAt 置位,runSummary 投影 'interrupted'
554
+ //(CLI/TUI 不显示僵尸「运行中」;resume 资格判据在 core fold lifecycle,
555
+ // 不受本投影影响)。
556
+ ...(interruptedAt !== undefined ? { interruptedAt } : {}),
557
+ ...(aggregate.modelOverride !== undefined ? { modelOverride: aggregate.modelOverride } : {}),
558
+ },
559
+ );
560
+ }
561
+
562
+ /**
563
+ * 终局分支 reconstruct:run-settled 帧派生 DoneReason + completedAt;失败终局带
564
+ * 帧 reason 文本(不落 generic 恢复文案)。
565
+ */
566
+ function reconstructSettledRun(
567
+ runId: string,
568
+ spec: RunSpec,
569
+ aggregate: RebuiltRunAggregate,
570
+ parts: FoldCommonParts,
571
+ settledEvent: RunSettledFrame,
572
+ ): WorkflowRun {
530
573
  const reason = runSettledOutcomeToDoneReason(settledEvent.outcome, settledEvent.errorCode);
531
574
  return WorkflowRun.reconstruct(
532
575
  runId,
533
576
  spec,
534
577
  {
535
578
  reason,
536
- budget,
537
- calls,
538
- trace,
539
- errorLogs,
579
+ budget: parts.budget,
580
+ calls: aggregate.calls,
581
+ trace: aggregate.trace,
582
+ errorLogs: parts.errorLogs,
540
583
  // 成功终局无 error;失败终局带帧 reason 文本(不落 generic 恢复文案)
541
584
  ...(reason !== "completed" && settledEvent.reason !== undefined ? { error: settledEvent.reason } : {}),
542
585
  },
543
586
  {
544
- startedAt: startedAtIso,
587
+ startedAt: parts.startedAt,
545
588
  completedAt: new Date(settledEvent.ts).toISOString(),
589
+ ...(aggregate.modelOverride !== undefined ? { modelOverride: aggregate.modelOverride } : {}),
546
590
  },
547
591
  );
548
592
  }
549
593
 
594
+ /**
595
+ * startedAt 毫秒锚:run-created 帧 ts 优先,回落注册条目 startedAt(非有限数值 =
596
+ * 兜底当前时刻,保 startedAt 恒有值)。
597
+ */
598
+ function resolveStartedAtMs(
599
+ created: Extract<WorkflowRunEvent, { type: "run-created" }> | undefined,
600
+ reg: WorkflowRecordRegisteredEntryData,
601
+ ): number {
602
+ return created?.ts ?? (Number.isFinite(reg.startedAt) ? reg.startedAt : Date.now());
603
+ }
604
+
605
+ /**
606
+ * record 事件流 → WorkflowRun 聚合(纯函数,零 IO;fold 语义词表说明见文件头
607
+ * 「record 流 fold 重建」节)。主干只做产物编排:公共 fold 产物(budget/errorLogs/
608
+ * startedAt/spec)+ 聚合重建块(rebuildRunAggregate)+ 按终局帧有无分流到
609
+ * reconstructUnsettledRun / reconstructSettledRun。
610
+ */
611
+ function foldRecordStreamToRun(
612
+ runId: string,
613
+ reg: WorkflowRecordRegisteredEntryData,
614
+ events: readonly WorkflowRunEvent[],
615
+ settled?: WorkflowRecordSettledEntryData,
616
+ ): WorkflowRun {
617
+ const created = events.find(
618
+ (e): e is Extract<WorkflowRunEvent, { type: "run-created" }> => e.type === "run-created",
619
+ );
620
+ const spec = rebuildRunSpecFromEntries(created, reg, resolveSpecBudget(created, events));
621
+ // [§2.1b] 会计重建:v2 终态条目带活体口径 usedTokens/callCount → 真值优先;条目缺席
622
+ // 回落 agent-settled.result.usage 的同一加权口径(下界近似,含 usedCost)。
623
+ // [§2.1 errorLogs 持久化 / ADR-0093] 诊断日志重建:worker-log 帧 → errorLogs(与活体
624
+ // 写入同语义:按序 + 尾部上限裁剪)。此前无持久面、重启即空。
625
+ const parts: FoldCommonParts = {
626
+ budget: rebuildBudget(settled, events),
627
+ errorLogs: errorLogsFromEvents(events),
628
+ startedAt: new Date(resolveStartedAtMs(created, reg)).toISOString(),
629
+ };
630
+ const aggregate = rebuildRunAggregate(events);
631
+ const settledEvent = lastRunSettledEvent(events);
632
+ if (settledEvent === undefined) {
633
+ return reconstructUnsettledRun(runId, spec, aggregate, parts, events);
634
+ }
635
+ return reconstructSettledRun(runId, spec, aggregate, parts, settledEvent);
636
+ }
637
+
550
638
  // ── JsonlRunStore ────────────────────────────────────────────
551
639
 
552
640
  const logger = getLogger("subagents");
@@ -42,7 +42,7 @@ import {
42
42
  import { bindNotifyLedgerHost, type NotifyLedgerHost } from "@zhushanwen/subagent-core";
43
43
  import { IDENTITY_CUSTOM_TYPE, type SubagentIdentityData } from "@zhushanwen/subagent-core";
44
44
  import type { ExecutionMode } from "@zhushanwen/subagent-core";
45
- import { maybeCleanupExpiredSessionFiles } from "@zhushanwen/subagent-core";
45
+ import { startEngineJournalGcTimer } from "@zhushanwen/subagent-core";
46
46
  import { resolvePiSessionScopedDir } from "@zhushanwen/subagent-core";
47
47
  // [W1 / D5] session_start 兜底触发点的统一保留维护轮:入口 + record 域目录锚
48
48
  // (getSubagentRecordsDir 布局 + ENV_ROOT_CWD 贯穿 env 名单源)+ state 目录分量
@@ -557,7 +557,7 @@ export function bindLedgerHostAndRecover(pi: ExtensionAPI, ctx: ExtensionContext
557
557
 
558
558
  /**
559
559
  * 随迁块 4 的进程级维护三连(各 try-catch「失败记日志不阻断」,设计 §3.4):
560
- * 过期 session 文件清理 / ADR-035 manifest tmp 恢复 / ADR-035 worktree reaper 扫描。
560
+ * 引擎 journal TTL 回收 / ADR-035 manifest tmp 恢复 / ADR-035 worktree reaper 扫描。
561
561
  * ([modeless 波5] 原 [E1] sync 批崩溃恢复接线已摘除;[collect 退役] core 侧
562
562
  * recoverSyncCollectBatch 方法本体已删——sync 批机制不存在,无恢复面可接线。)
563
563
  */
@@ -568,12 +568,16 @@ async function runProcessLevelMaintenance(
568
568
  deps: SessionLifecycleDeps,
569
569
  ): Promise<void> {
570
570
  try {
571
- // 递归扫描 <agentDir>/subagents + unlink 超 TTL 跨 session 文件属进程级维护
572
- // ——oncePerProcess 守卫防双跑(u-audit-fix)。
573
- oncePerProcess("subagent-workflow:cleanup-expired-session-files", () =>
574
- maybeCleanupExpiredSessionFiles(agentDir, ctx.cwd));
571
+ // 引擎 journal TTL 回收(30 天 mtime)+ worktree .patch 孤儿清扫(同 TTL,仅
572
+ // *.patch 两层枚举——按 branch 命名无会话锚,级联引用并集无锚可达,唯一 mtime
573
+ // 兜底通道)属进程级维护——oncePerProcess 守卫防双跑(u-audit-fix)。接线即扫
574
+ // 一次 + 24h 定时复扫(unref 不 hold 进程退出,对齐 main 侧 log-retention 形态)。
575
+ // 子代理会话文件(*.jsonl / sidecar / manifest / *.events)不在本维护面——回收归
576
+ // 删主会话级联(runtime session-content-cascade,2026-10-12 裁决)。
577
+ oncePerProcess("subagent-workflow:engine-journal-gc", () =>
578
+ startEngineJournalGcTimer(agentDir));
575
579
  } catch (err) {
576
- logger.warn("[subagents] expired session file cleanup failed", {
580
+ logger.warn("[subagents] engine journal gc failed", {
577
581
  reason: toErrorMessage(err),
578
582
  });
579
583
  }
@@ -402,6 +402,28 @@ export function setupWorkflowDomain(
402
402
  }
403
403
  return service.executeWorkflowAgent(opts, parentRunId, signal, undefined, undefined, stepIndex);
404
404
  },
405
+ // [F1-18 修复] resume 显式 model 落账后的宿主投影同步(生产接线——单例现读
406
+ // 同 workflowAgentDispatch 形态)。实现 = SubagentService.applyRunOverrideProjection
407
+ // (覆盖内存表写值 + 重建负缓存作废);throw 走 resume 侧回滚围栏(run 回
408
+ // interrupted 可重试),service 缺席文案同款 [C2] 闭环。
409
+ onResumeModelOverrideCommitted: (runId, override) => {
410
+ const service = getSubagentService();
411
+ if (!service) {
412
+ throw new Error(
413
+ "resume model override projection unavailable: subagent service not initialized " +
414
+ "(session_start assembly failed). Recovery: retry the resume once the session " +
415
+ "is initialized; the override frame is already persisted in the run journal.",
416
+ );
417
+ }
418
+ service.applyRunOverrideProjection(runId, override);
419
+ },
420
+ // [dmg-r2-5] 宿主会话锚现读口(run-created 帧 rootSessionId 载荷源):与
421
+ // workflowAgentDispatch 同款 service 单例现读形态——sessionRootId 根进程语义
422
+ // 与 record 域 rootSessionId 同源(根进程 = 本 session id,嵌套 = env ROOT)。
423
+ // 同 cwd 多会话共享 workflow-state 目录,runtime 网关 runId 路由靠该字段精确
424
+ // 判归属;service 缺席返回 null(帧不落字段,读侧旧格式行放行——不抛,创建
425
+ // 主链不因锚缺席失败)。
426
+ getSessionRootId: () => getSubagentService()?.getSessionRootId() ?? null,
405
427
  log,
406
428
  };
407
429
  return deps;
@@ -702,6 +724,20 @@ export function setupWorkflowDomain(
702
724
  // "sar-unattached" → armed 回执落账键错 → fold 出 created 态 → IllegalTransitionError
703
725
  // 让位,run journal 恒缺 armed 帧(真机 wf-1790034646281-w7tp4f 实证链,R2 裁决)。
704
726
  get workflowAgentDispatch() { return createLazy(resolveDeps, "workflowAgentDispatch"); },
727
+ // [dmg-r2-5] 宿主会话锚必须随 lazyDeps 转发(workflowAgentDispatch 同款先例):
728
+ // workflow tool / subagents tool 的 run action 以 lazyDeps 为 deps 启动 run,漏本
729
+ // 成员则 run-created 帧不落 rootSessionId(可选成员静默放行)——同 cwd 多会话下
730
+ // runtime 网关 runId 路由恒回落目录存在性弱锚,精确归属形同虚设。
731
+ get getSessionRootId() { return createLazy(resolveDeps, "getSessionRootId"); },
732
+ // [dmg-r1-1] resume 显式 model 落账回调必须随 lazyDeps 转发(getSessionRootId
733
+ // 同款先例,同族第三例):三个生产入口(registerWorkflowTool / registerSubagentsTool /
734
+ // registerWorkflowsCommand)都以 lazyDeps 为 deps 启动 run,resume with model 的
735
+ // 落账点(resume-run appendResumeModelOverride)落完 journal B 帧后经本回调同步
736
+ // 宿主投影——漏本成员则可选链静默短路(可选成员静默放行):覆盖内存表 /
737
+ // 负缓存 stale 不失效,重派被旧投影遮蔽(旧 model 命中或覆盖丢失按 run-created.model
738
+ // 解析),pi 仍 notify「resuming with model B」成功文案。转发清单对账测试防第四处
739
+ // 漏转发(workflow-events.test.ts lazyDeps 转发清单对账 describe)。
740
+ get onResumeModelOverrideCommitted() { return createLazy(resolveDeps, "onResumeModelOverrideCommitted"); },
705
741
  // scheduleTimeBudget / appendEntry 不可缺席(ports.ts D-12 regression fix 同族):
706
742
  // rebuildRuntime 重排 run 级墙钟预算计时器、finalizeRun 的 pending:unregister
707
743
  // 直落都经这两个成员消费——lazyDeps 缺席会让消费点拿到 undefined(可选属性