@zhushanwen/pi-subagent-workflow 8.14.2 → 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.
- package/package.json +11 -10
- package/relay/relay.mjs +1 -1
- package/skills/subagent-ext-config/SKILL.md +1 -1
- package/src/host/__tests__/inflight-reporter.test.ts +1 -1
- package/src/host/__tests__/pi-host.test.ts +2 -2
- package/src/host/inflight-reporter.ts +1 -1
- package/src/host/pi-host.ts +4 -4
- package/src/index.ts +46 -410
- package/src/injectors/workflow-list-injector.ts +6 -1
- package/src/interface/__tests__/tool-subagents.test.ts +494 -0
- package/src/interface/bg-notify-render.ts +4 -27
- package/src/interface/commands.ts +3 -5
- package/src/interface/format.ts +3 -3
- package/src/interface/gui-mappers.ts +3 -3
- package/src/interface/helpers.ts +87 -29
- package/src/interface/id-preview.ts +12 -0
- package/src/interface/subagent-actions.ts +4 -6
- package/src/interface/subagent-tool-schema.ts +7 -17
- package/src/interface/subagent-tool.ts +5 -13
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-result.ts +40 -0
- package/src/interface/tool-shared.ts +109 -0
- package/src/interface/tool-subagents.ts +310 -0
- package/src/interface/tool-workflow-script.ts +6 -10
- package/src/interface/tool-workflow.ts +41 -60
- package/src/jsonl-run-store.ts +65 -5
- package/src/session-lifecycle.ts +184 -40
- package/src/workflow-events.ts +576 -0
package/src/jsonl-run-store.ts
CHANGED
|
@@ -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
|
-
|
|
301
|
-
|
|
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
|
*
|
package/src/session-lifecycle.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
376
|
-
*
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
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
|
}
|