@zhushanwen/pi-subagent-workflow 8.14.3 → 8.14.4

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.
@@ -65,8 +65,6 @@
65
65
  * 是 opt-out 通道);内存侧由 evictDoneRunsBeyondCap 淘汰。W17 后 state 文件
66
66
  * 已降级为纯性能缓存(权威数据在 session JSONL 的 workflow-record entry),随 session
67
67
  * 文件被用户删除时一并消失。
68
- *
69
- * 参考:domain-models.md §Ports(RunStore 定义)、clarification.md D-5。
70
68
  */
71
69
 
72
70
  import * as fs from "node:fs";
@@ -90,7 +88,7 @@ import {
90
88
  pruneStateFilesBeyondCap,
91
89
  type RunSnapshot,
92
90
  } from "@zhushanwen/subagent-core";
93
- import { isEnoentError, toErrorMessage } from "@zhushanwen/pi-ext-guards";
91
+ import { guardStaleCtx, isEnoentError, toErrorMessage } from "@zhushanwen/pi-ext-guards";
94
92
 
95
93
  // ── Workflow-record self-describing entry (W17, D4) ─────────
96
94
 
@@ -297,8 +295,13 @@ interface JsonlRunStoreOptions {
297
295
 
298
296
  export class JsonlRunStore {
299
297
  private readonly sessionDir: string;
300
- private readonly pi?: ExtensionAPI;
301
- private readonly ctx?: ExtensionContext;
298
+ /**
299
+ * workflow-record entry 的 appendEntry 源。store 对 pi 的唯一消费面是 doFlush 的
300
+ * appendEntry(W17 权威 entry 写入),类型收窄为该面——[skill-reload D3] rebind
301
+ * 时换入 stale-guarded 包装(见 {@link rebind}),构造时为裸 pi 原引用。
302
+ */
303
+ private pi?: Pick<ExtensionAPI, "appendEntry">;
304
+ private ctx?: ExtensionContext;
302
305
  private readonly saveDebounceMs: number;
303
306
  /** workflow-record entry append 节流最小间隔(ms),0 = 禁用。 */
304
307
  private readonly entryAppendMinIntervalMs: number;
@@ -592,6 +595,63 @@ export class JsonlRunStore {
592
595
  await Promise.allSettled(Array.from(this.chains.values()));
593
596
  }
594
597
 
598
+ /**
599
+ * [skill-reload D3] adoption 就地重绑:post-reload session_start 接管(adoption)时
600
+ * 原地改写 entry 写入源与 ctx——store 实例跨 reload 存活(D2 槽),在飞去抖批与
601
+ * per-runId 串行 flush 链持有 this,原地改写对后续 flush 天然可见(不遍历对象图
602
+ * 重绑:闭包引用不可枚举,漏一处 = 恢复后随机 assertActive 抛——设计被否项)。
603
+ * writtenOnce / lastEntryAppendAt / pending / chains 全部保留(接管而非重建)。
604
+ *
605
+ * [skill-reload D5] 换入的 appendEntry 源包 guardStaleCtx:下一次 reload 窗口
606
+ * (invalidate → adoption rebind 完成之间,通常 <1s)in-flight flush 触碰已 stale
607
+ * 的本 pi 时统一 debug 丢弃(不分中间态/终态——appendEntry 是同步 void,stale
608
+ * 表现为同步 assertActive 抛错,不包会把窗口内 flush 打成 IO 错误路径:settlers
609
+ * reject + writtenOnce 回滚)。终态保全不依赖窗口内写入,由 adoption 快照重发
610
+ * ({@link resendSnapshots})承担:窗口内终态的 run 在 adoption 时刻内存对象已
611
+ * 是终态,快照重发追加的就是终态 entry。
612
+ */
613
+ rebind(pi: ExtensionAPI, ctx: ExtensionContext): void {
614
+ this.pi = {
615
+ appendEntry: (customType: string, data?: unknown) => {
616
+ guardStaleCtx(() => pi.appendEntry(customType, data), {
617
+ label: "subagent-workflow:jsonl-run-store.appendEntry",
618
+ // 「统一 stale → debug 丢弃」:窗口内丢弃是设计内降级,debug 留痕可归因
619
+ onStale: (error) =>
620
+ logger.debug(
621
+ "[subagent-workflow] workflow-record entry append skipped (stale ctx)",
622
+ { reason: toErrorMessage(error) },
623
+ ),
624
+ });
625
+ },
626
+ };
627
+ this.ctx = ctx;
628
+ }
629
+
630
+ /**
631
+ * [skill-reload D4] adoption 快照重发:把 runs 内全部 run 的当前快照经 per-runId
632
+ * 串行 flush 链(enqueueFlush → doFlush)重发一条权威 workflow-record entry。
633
+ *
634
+ * 设计红线:必须经本链而非裸 pi.appendEntry——doFlush 的 await writeFile 与
635
+ * appendEntry 之间存在事件循环间隙(W17 补充事实),绕链直接 append 会与
636
+ * in-flight 中间态 flush 物理乱序(终态在前中间态在后,last-ways 读回 running →
637
+ * 崩溃恢复误判);走串行链后同 runId 的 entry 顺序由链内闭合保证。
638
+ *
639
+ * 节流语义自然继承 doFlush:终态 flush 永不节流(最终状态必进 pi 权威文件);
640
+ * running 中间态受既有 entryAppendMinIntervalMs(缺省 60s)节流约束可跳过——
641
+ * 与常规 flush 同源(pi 文件最后一条 entry 最多落后真实状态一个窗口)。测试
642
+ * 断言因此按终态/首写路径构造,不依赖中间态重发必落。
643
+ *
644
+ * rollbackFirstWrite=false:重发不是新文件首写(不触发磁盘保留裁剪);失败不
645
+ * 回滚 writtenOnce——重发失败向上抛,由 adoption 失败处置整体兜底(G3 可见)。
646
+ */
647
+ async resendSnapshots(runs: Map<string, WorkflowRun>): Promise<void> {
648
+ for (const run of runs.values()) {
649
+ // 串行 await:adoption 是一次性路径,跨 runId 顺序无语义,但全部重发完成
650
+ //(或首个失败上抛)后才返回,调用方据此判定接管完成。
651
+ await this.enqueueFlush(run.runId, run, [], false);
652
+ }
653
+ }
654
+
595
655
  /**
596
656
  * Reconstruct all runs(W17 [D4] 读序 = workflow-record entry > state 文件 > 空)。
597
657
  *
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * session-lifecycle — 会话生命周期装配 seam(bootstrap seam)。
3
3
  *
4
- * 设计锚点:docs/design/subagent-post-convergence-architecture.md §3.1(D1/D2/D8)。
5
4
  * 随迁内容 = 原组合根 index.ts session_start handler(:336-613)的六职责,原样搬移
6
5
  * (D2 纪律:本文件不改行为;行为变更点——守卫合一 / lazyDeps getter 化(10 成员
7
6
  * 守卫触发对象,偏差 #10)——留在 index.ts,各自独立成条):
@@ -19,7 +18,7 @@
19
18
  import * as fs from "node:fs";
20
19
  import * as path from "node:path";
21
20
 
22
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
21
+ import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
23
22
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
24
23
  import { getLogger } from "@zhushanwen/pi-extension-logger";
25
24
  import { guardStaleCtx, oncePerProcess, toErrorMessage } from "@zhushanwen/pi-ext-guards";
@@ -156,10 +155,33 @@ export interface SessionLifecycleDeps {
156
155
  worktreeManager?: Pick<WorktreeManager, "scan">;
157
156
  /**
158
157
  * per-session run store 工厂(随迁块 5)。默认 = new JsonlRunStore({ sessionDir, pi, ctx })
159
- * (per-session 新建为现状设计:store 生命周期与 session 等同,D-008/F-4)。
158
+ * (per-session 新建为现状设计:store 生命周期与 session 等同,D-008/F-4)。
160
159
  * 测试注入 fake 以控制 loadAll 行为(kill-9 恢复分支)。
161
160
  */
162
161
  createRunStore?: (sessionDir: string, pi: ExtensionAPI, ctx: ExtensionContext) => JsonlRunStore;
162
+ /**
163
+ * [skill-reload D4] adoption 失败处置回调(组合根注入):terminate adopted running
164
+ * runs(notifyDone: true——用户可见)+ 移除 sessionState 条目。terminate 依赖的
165
+ * LauncherDeps 完整形态(workerHost / onRunDone 通知链 / notifiedRunIds 去重窗口)
166
+ * 与 sessionState Map 归 workflow 域闭包(workflow-events.ts)持有,本 seam 无访问
167
+ * 通道,经此注入;未注入时失败处置仅完成 rebind-first + 日志(测试可观察调用)。
168
+ */
169
+ onAdoptionFailed?: (existing: SessionLifecycleResult, reason: string) => Promise<void>;
170
+ }
171
+
172
+ /** setupSessionLifecycle 的 adoption 分流入参([skill-reload D4])。 */
173
+ export interface SessionStartOptions {
174
+ /**
175
+ * session_start 事件 reason(pi SessionStartEvent.reason,SDK 锚定)。缺省视为
176
+ * 非 reload——现状全量装配路径(向后兼容,现有调用/测试不传即走原行为)。
177
+ */
178
+ reason?: SessionStartEvent["reason"];
179
+ /**
180
+ * adoption 候选(既有 per-session 条目)。调用点在 reason==='reload' 时从
181
+ * sessionState 取;条目缺失(reload 落在首次装配 await 链中)传 undefined →
182
+ * 全量装配(唯一差异 = 恢复门控已跳过)。
183
+ */
184
+ existing?: SessionLifecycleResult;
163
185
  }
164
186
 
165
187
  /** setupSessionLifecycle 装配结果——组合根据此写入 per-session sessionState。 */
@@ -372,8 +394,8 @@ export function bindLedgerHostAndRecover(pi: ExtensionAPI, ctx: ExtensionContext
372
394
  /**
373
395
  * 随迁块 4 的进程级维护三连(各 try-catch「失败记日志不阻断」,设计 §3.4):
374
396
  * 过期 session 文件清理 / ADR-035 manifest tmp 恢复 / ADR-035 worktree reaper 扫描。
375
- * ([modeless 波5] 原 [E1] sync 批崩溃恢复接线已摘除——collectMode 记录态消亡后
376
- * core 侧 recoverSyncCollectBatch 已是 accepted-no-op,调用点随之退役。)
397
+ * ([modeless 波5] 原 [E1] sync 批崩溃恢复接线已摘除;[collect 退役] core 侧
398
+ * recoverSyncCollectBatch 方法本体已删——sync 批机制不存在,无恢复面可接线。)
377
399
  */
378
400
  async function runProcessLevelMaintenance(
379
401
  agentDir: string,
@@ -461,6 +483,7 @@ async function createSessionRunState(
461
483
  pi: ExtensionAPI,
462
484
  ctx: ExtensionContext,
463
485
  deps: SessionLifecycleDeps,
486
+ opts: { skipRecovery: boolean },
464
487
  ): Promise<SessionRunState> {
465
488
  const store = deps.createRunStore
466
489
  ? deps.createRunStore(sessionDir, pi, ctx)
@@ -476,34 +499,135 @@ async function createSessionRunState(
476
499
  // M2 修正:workflow 域 resolveAgentOpts 不再消费 agentRegistry(agent ref 交
477
500
  // resolveIdentity),无需经 state 透传——modelService 是唯一 registry 源。
478
501
  let storeHealthy = true;
479
- try {
480
- // 崩溃恢复 loadAll 扫 cwd 共享 sessionDir(同 cwd 跨 session 共享)并把 running run
481
- // failed 落盘——写非本 session 的 run state 文件属跨 session 副作用,oncePerProcess
482
- // 守卫防双跑(u-audit-fix)。第二派发重放首次 Promise:不再落盘、不再 emit。
483
- await oncePerProcess(
484
- "subagent-workflow:recover-crashed-runs",
485
- () =>
486
- recoverCrashedRuns(
487
- store,
488
- runs,
489
- "Process killed (kill-9 or crash recovery)",
490
- {
491
- onRunRecovered: (payload) => {
492
- pi.events.emit("pending:unregister", payload);
502
+ // [skill-reload D4] 恢复门控:session_start(reason==='reload') 全程不跑
503
+ // recoverCrashedRuns(无论条目有无)。暗礁(设计 §2.4):recoverCrashedRuns
504
+ // 「crashed」只看磁盘快照 status=running、内存活 run 不参与判定且会被重建对象
505
+ // 覆盖——契约前提是「拥有这些 run 的进程已死」,而 reload 恰恰证明进程没死,
506
+ // 跑恢复即误杀窗口内存活的 run。条目缺失场景同理门控:磁盘可能有本 session 的
507
+ // running entry(前一轮 adoption 未完成又 reload 的窗口),由下一次**非 reload**
508
+ // 的 session_start(真重启/切换)按既有 kill-9 语义收编。跳过 loadAll 时无从
509
+ // 证伪健康度:storeHealthy 保持 true(workflow 域可用,可派发新 run)。门控放
510
+ // 调用点先于条目判断(setupSessionLifecycle 分流处),不进 oncePerProcess 守卫
511
+ // 内——守卫 Map 是模块级状态 reload 后归零(D9 不提权),靠守卫判 reason 形同虚设。
512
+ if (!opts.skipRecovery) {
513
+ try {
514
+ // 崩溃恢复 loadAll 扫 cwd 共享 sessionDir(同 cwd 跨 session 共享)并把 running run
515
+ // 转 failed 落盘——写非本 session 的 run state 文件属跨 session 副作用,oncePerProcess
516
+ // 守卫防双跑(u-audit-fix)。第二派发重放首次 Promise:不再落盘、不再 emit。
517
+ await oncePerProcess(
518
+ "subagent-workflow:recover-crashed-runs",
519
+ () =>
520
+ recoverCrashedRuns(
521
+ store,
522
+ runs,
523
+ "Process killed (kill-9 or crash recovery)",
524
+ {
525
+ onRunRecovered: (payload) => {
526
+ pi.events.emit("pending:unregister", payload);
527
+ },
493
528
  },
494
- },
495
- ),
496
- );
497
- } catch (err) {
498
- // QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
499
- logger.error("[subagent-workflow] store.loadAll failed, workflow domain uninitialized", {
500
- reason: toErrorMessage(err),
501
- });
502
- storeHealthy = false;
529
+ ),
530
+ );
531
+ } catch (err) {
532
+ // QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
533
+ logger.error("[subagent-workflow] store.loadAll failed, workflow domain uninitialized", {
534
+ reason: toErrorMessage(err),
535
+ });
536
+ storeHealthy = false;
537
+ }
503
538
  }
504
539
  return { store, runs, storeHealthy };
505
540
  }
506
541
 
542
+ // ── [skill-reload D4] post-reload adoption(接管而非重建) ─────────────────────
543
+
544
+ /**
545
+ * adoption 主体:接管既有条目(同引用原地改写)。成功返回原 SessionLifecycleResult
546
+ * (sessionState.get(sid) 与 reload 前同一引用——探针红线,store/runs 不换实例);
547
+ * 失败(健康检查不过 / rebind / 快照重发抛错)走 {@link failAdoption} 后返回
548
+ * undefined(调用方落到全量装配)。
549
+ */
550
+ async function tryAdoptExistingSession(
551
+ pi: ExtensionAPI,
552
+ ctx: ExtensionContext,
553
+ deps: SessionLifecycleDeps,
554
+ existing: SessionLifecycleResult,
555
+ lastEngine: string | undefined,
556
+ ): Promise<SessionLifecycleResult | undefined> {
557
+ // 健康检查:上一轮装配时 loadAll 失败(storeHealthy=false)的 store 不具备承载
558
+ // 接管的写入可靠性 → 失败处置(G3 用户可见),不接管。
559
+ if (!existing.storeHealthy) {
560
+ await failAdoption(pi, ctx, deps, existing, "store unhealthy (loadAll failed in previous session_start)");
561
+ return undefined;
562
+ }
563
+ try {
564
+ // D3 rebind:在飞去抖批与串行 flush 链持有 store(this),原地改写 .pi/.ctx
565
+ // 对后续 flush 天然可见;换入的 appendEntry 源带 stale guard(D5)。
566
+ existing.store.rebind(pi, ctx);
567
+ // D4 快照重发:经 store 既有 per-runId 串行 flush 链重发当前快照权威 entry
568
+ //(设计红线:禁止绕链直接 pi.appendEntry——物理乱序会让 last-ways 读回
569
+ // running → 崩溃恢复误判)。任何 IO 失败上抛 → 失败处置。
570
+ await existing.store.resendSnapshots(existing.runs);
571
+ } catch (err) {
572
+ await failAdoption(pi, ctx, deps, existing, toErrorMessage(err));
573
+ return undefined;
574
+ }
575
+ // 接管:同引用原地改写(ctx 换新——旧 ctx 已被 invalidate;lastEngine 按当前
576
+ // config 重置基线)+ runner 刷新 ctxModel。跳过 store/runner 重建(幂等 last-wins)。
577
+ existing.ctx = ctx;
578
+ existing.lastEngine = lastEngine;
579
+ existing.runner.updateCtxModel(ctx.model ?? undefined);
580
+ logger.debug(
581
+ `[subagent-workflow] adoption ok (sessionId=${existing.sessionId}, runs=${existing.runs.size})`,
582
+ );
583
+ return existing;
584
+ }
585
+
586
+ /**
587
+ * adoption 失败处置(顺序敏感,设计 D4/r4):
588
+ * ① 先无条件 store.rebind(newPi, newCtx)——失败若发生在 rebind 之前,terminate
589
+ * 的终态 flush 走未 rebind 的旧 pi 且 stale guard 未装 → 终态 failed entry 不落
590
+ * 权威 JSONL,run 从 session 历史消失;先 rebind 让终态 flush 走新 pi,G3 可见性
591
+ * 与权威记录同时兑现。rebind 自身再失败则跳过并日志登记终态丢失面。
592
+ * ②+③ 经组合根注入回调:terminateRunningRuns(notifyDone: true,用户可见)+
593
+ * 移除 sessionState 条目(不残留半接管状态)。
594
+ * ④ adoption=failed 归因日志(G4:可从日志直接读出因果)。
595
+ */
596
+ async function failAdoption(
597
+ pi: ExtensionAPI,
598
+ ctx: ExtensionContext,
599
+ deps: SessionLifecycleDeps,
600
+ existing: SessionLifecycleResult,
601
+ reason: string,
602
+ ): Promise<void> {
603
+ try {
604
+ existing.store.rebind(pi, ctx);
605
+ } catch (rebindErr) {
606
+ logger.warn(
607
+ "[subagent-workflow] adoption failure rebind also failed (terminal entries may be lost)",
608
+ { sessionId: existing.sessionId, reason: toErrorMessage(rebindErr) },
609
+ );
610
+ }
611
+ if (!deps.onAdoptionFailed) {
612
+ logger.warn(
613
+ "[subagent-workflow] adoption failure cleanup callback not injected (runs not terminated, entry not removed)",
614
+ { sessionId: existing.sessionId },
615
+ );
616
+ } else {
617
+ try {
618
+ await deps.onAdoptionFailed(existing, reason);
619
+ } catch (err) {
620
+ logger.error("[subagent-workflow] adoption failure cleanup failed", {
621
+ sessionId: existing.sessionId,
622
+ reason: toErrorMessage(err),
623
+ });
624
+ }
625
+ }
626
+ logger.error(
627
+ `[subagent-workflow] adoption=failed sessionId=${existing.sessionId} reason=${reason}`,
628
+ );
629
+ }
630
+
507
631
  // ── 单一装配入口 ─────────────────────────────────────────────────────────────────
508
632
 
509
633
  /**
@@ -518,6 +642,7 @@ export async function setupSessionLifecycle(
518
642
  pi: ExtensionAPI,
519
643
  ctx: ExtensionContext,
520
644
  deps: SessionLifecycleDeps,
645
+ options?: SessionStartOptions,
521
646
  ): Promise<SessionLifecycleResult> {
522
647
  const agentDir = getAgentDir();
523
648
  const sessionId = ctx.sessionManager.getSessionId();
@@ -529,26 +654,54 @@ export async function setupSessionLifecycle(
529
654
 
530
655
  // skill 路径两级缓存 session 级失效:pi 同进程可能有多个 session(TUI /new、/fork),
531
656
  // 运行中安装的 skill 需对新 session 可见(含曾 miss 缓存的 undefined 条目与 npm 新装
532
- // 包的候选目录)。session 内复用收益不变(IF8/DM3 消重发生在同 session 的重复调用)。
657
+ // 包的候选目录)。session 内复用收益不变(IF8/DM3 重读发生在同 session 的重复调用)。
533
658
  clearSkillPathCache();
534
659
 
535
660
  // ── [M4] identity 子进程写入(随迁块 1)──
536
661
  appendSubagentIdentityEntry(pi);
537
662
 
538
663
  // ── [U2] 通知账本装配 + 重启恢复(随迁块 2)──
664
+ // [skill-reload D4] 两分支都保留:ledger re-bind 到新 pi/ctx(getBoundNotifyLedger()
665
+ // 现读方自动看到新绑定)。
539
666
  bindLedgerHostAndRecover(pi, ctx);
540
667
 
541
668
  // ── subagents 域:双 Service 装配(随迁块 3,经 deps 可注入)──
669
+ // [skill-reload D4] 两分支都保留:initSession 复活链 + 新 ctx 注入(SubagentService
670
+ // 跨 reload 存活,其 stale 面 _pi/_streamSink/_isIdleFn 由 initSession 重注入覆盖)。
542
671
  const { service, modelService } = deps.createServices
543
672
  ? deps.createServices(pi, ctx)
544
673
  : createOrReuseServices(pi, ctx);
545
674
 
546
675
  // ── GC / manifest tmp / worktree 恢复(随迁块 4)──
676
+ // [skill-reload D4] 两分支都保留:进程级维护幂等重跑无害(oncePerProcess 守卫 Map
677
+ // 是模块级状态,reload 后归零属预期——D9)。
547
678
  await runProcessLevelMaintenance(agentDir, ctx, service, deps);
548
679
 
680
+ // [engine-awareness D1b] lastEngine 基线重算提前到 adoption 分流前(两分支共用):
681
+ // 构造性同源——单次 reloadGlobalConfig 读取同时刷新 Service 路由缓存与 lastEngine
682
+ // 基准,消灭 initModel 与本处两次独立读取间的分叉窗口。ok/absent → 归一后的当前
683
+ // 引擎;failed → undefined(首 turn 检测静默基线化兜底)。
684
+ const engineRead = modelService.reloadGlobalConfig();
685
+ const lastEngine =
686
+ engineRead.status === "failed" ? undefined : normalizeEngineId(engineRead.config.defaultEngine);
687
+
688
+ // ── [skill-reload D4] adoption 分流(post-reload session_start(reason==='reload'))──
689
+ // 恢复门控已由 isReload 承载(先于条目判断);条目存在 → 接管(同引用原地改写,
690
+ // store/runner 不重建);条目缺失(reload 落在首次装配 await 链中)→ 落到下方
691
+ // 全量装配,唯一差异 = 恢复门控已跳过。
692
+ const isReload = options?.reason === "reload";
693
+ if (isReload && options.existing) {
694
+ const adopted = await tryAdoptExistingSession(pi, ctx, deps, options.existing, lastEngine);
695
+ if (adopted) return adopted;
696
+ // adoption 失败处置已完成(rebind-first + terminate + 条目移除 + 日志)→
697
+ // 落到下方全量装配(session 继续可用:新建 store/runs/runner,恢复仍被门控跳过)。
698
+ }
699
+
549
700
  // ── workflow 域:per-session store + runs + kill-9 恢复(随迁块 5)──
550
701
  const sessionDir = resolveSessionDir();
551
- const { store, runs, storeHealthy } = await createSessionRunState(sessionDir, pi, ctx, deps);
702
+ const { store, runs, storeHealthy } = await createSessionRunState(sessionDir, pi, ctx, deps, {
703
+ skipRecovery: isReload,
704
+ });
552
705
 
553
706
  // D-008: per-session SAR(需要 ctxModel 填底 + subagentService 委托目标)。
554
707
  // old: const runner = new SubprocessAgentRunner()(module-level singleton,无 deps)
@@ -558,14 +711,6 @@ export async function setupSessionLifecycle(
558
711
  ctxModel: ctx.model ?? undefined,
559
712
  });
560
713
 
561
- // [engine-awareness D1b] lastEngine 初始化:构造性同源——单次 reloadGlobalConfig
562
- // 读取同时刷新 Service 路由缓存与 lastEngine 基准,消灭 initModel 与本处两次独立
563
- // 读取间的分叉窗口(两读值不一致时检测走 unchanged 分支不 reload,状态段/路由
564
- // 永停旧值且永不通知)。ok/absent → 归一后的当前引擎;failed → undefined(首 turn
565
- // 检测静默基线化兜底,此时缓存亦保持不动)。/resume、/fork 同样走 session_start
566
- // (SR-3),基线天然覆盖。
567
- const engineRead = modelService.reloadGlobalConfig();
568
-
569
714
  return {
570
715
  sessionId,
571
716
  store,
@@ -574,7 +719,6 @@ export async function setupSessionLifecycle(
574
719
  runner,
575
720
  ctx,
576
721
  storeHealthy,
577
- lastEngine:
578
- engineRead.status === "failed" ? undefined : normalizeEngineId(engineRead.config.defaultEngine),
722
+ lastEngine,
579
723
  };
580
724
  }