@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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/README.md +6 -5
- package/package.json +11 -7
- package/relay/relay.mjs +45 -2
- package/skills/workflow-script-format/SKILL.md +2 -56
- package/src/host/__tests__/inflight-reporter.test.ts +26 -1
- package/src/host/__tests__/pi-host.test.ts +418 -4
- package/src/host/inflight-reporter.ts +13 -10
- package/src/host/pi-host.ts +205 -17
- package/src/index.ts +21 -72
- package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
- package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
- package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
- package/src/injectors/resource-list-injector.ts +103 -32
- package/src/injectors/subagent-list-injector.ts +4 -28
- package/src/injectors/workflow-list-injector.ts +8 -4
- package/src/interface/__tests__/capture-tool.ts +56 -0
- package/src/interface/__tests__/commands-resume.test.ts +103 -0
- package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
- package/src/interface/__tests__/tool-render.test.ts +105 -2
- package/src/interface/__tests__/tool-subagents.test.ts +24 -26
- package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
- package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
- package/src/interface/__tests__/tool-workflow.test.ts +884 -0
- package/src/interface/bg-notify-render.ts +18 -9
- package/src/interface/command-actions.ts +22 -17
- package/src/interface/commands.ts +58 -12
- package/src/interface/format.ts +50 -16
- package/src/interface/id-preview.ts +4 -4
- package/src/interface/subagent-actions.ts +18 -50
- package/src/interface/subagent-tool.ts +26 -18
- package/src/interface/subagents.ts +5 -11
- package/src/interface/tool-render.ts +14 -30
- package/src/interface/tool-shared.ts +55 -12
- package/src/interface/tool-subagents.ts +21 -11
- package/src/interface/tool-workflow-script.ts +52 -72
- package/src/interface/tool-workflow.ts +345 -139
- package/src/interface/views/WorkflowsView.ts +10 -7
- package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
- package/src/interface/views/detail-content.ts +3 -5
- package/src/jsonl-run-store.ts +656 -544
- package/src/model-events.ts +35 -0
- package/src/session-lifecycle.ts +376 -85
- package/src/subagents-events.ts +56 -0
- package/src/workflow-events.ts +285 -200
- package/src/workflow-notify.ts +456 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
- package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
- package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
- package/src/interface/__tests__/detectors.test.ts +0 -161
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
- package/src/interface/helpers.ts +0 -272
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
- package/src/interface/views/__tests__/record-progress.test.ts +0 -235
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-events — model 域事件 handler(主 agent 模型切换的缓存刷新)。
|
|
3
|
+
*
|
|
4
|
+
* 原内联在 workflow-events.ts 装配 seam(跨域 handler 迁出,原样搬移——行为与
|
|
5
|
+
* 日志文案逐字保留);ModelConfigService 缓存是 model 域状态,与 workflow 域
|
|
6
|
+
* 装配零数据耦合。由 setupWorkflowDomain 在原注册位置调用 setupModelEvents
|
|
7
|
+
* (pi.on 注册顺序逐位不变,workflow-events.test.ts 锁定)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
12
|
+
import { getModelConfigService } from "@zhushanwen/subagent-core";
|
|
13
|
+
|
|
14
|
+
// 模块级 logger(与 index.ts 同 component 名;setPiHandle 注入后自动走 appendEntry)
|
|
15
|
+
const logger = getLogger("subagents");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 注册 model 域事件:用户切换 model 时刷新 ModelConfigService 缓存。
|
|
19
|
+
*/
|
|
20
|
+
export function setupModelEvents(pi: ExtensionAPI): void {
|
|
21
|
+
pi.on("model_select", (event) => {
|
|
22
|
+
const service = getModelConfigService();
|
|
23
|
+
if (service && typeof service.setCtxModel === "function") {
|
|
24
|
+
service.setCtxModel(event.model);
|
|
25
|
+
} else {
|
|
26
|
+
// [C2] 不再静默:service 缺席(session_start 装配链失败)时模型切换缓存未
|
|
27
|
+
// 刷新,后续 resolveModel 会用旧模型——warn 留痕接通「现象 → 根因」链路。
|
|
28
|
+
// model_select 是低频用户操作,不会刷屏。
|
|
29
|
+
logger.warn(
|
|
30
|
+
"[subagent-workflow] model_select ignored: model config service not initialized (session_start assembly failed) — model switch will not take effect for new subagents until session reload",
|
|
31
|
+
{ model: event.model },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/session-lifecycle.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 1. identity env→appendEntry 重建(类型 13 字段含 1 个 @deprecated,写入 12)
|
|
8
8
|
* 2. notify ledger host 装配 + 重启恢复
|
|
9
9
|
* 3. 双 Service 装配 + initSession(createOrReuseServices 封装,单例语义 D8)
|
|
10
|
-
* 4. GC / manifest tmp 清扫([U4c/D6] promote 退役)/ 索引重建([U4c/G1] boot 全量腿)/ worktree 恢复
|
|
10
|
+
* 4. GC / manifest tmp 清扫([U4c/D6] promote 退役)/ 索引重建([U4c/G1] boot 全量腿)/ worktree 恢复 / 保留维护轮兜底([W1/D5] session_start 触发)
|
|
11
11
|
* 5. per-session run store + kill-9 恢复循环 + evictDoneRunsBeyondCap
|
|
12
12
|
* 6. SAR + engine 基线(经 SessionLifecycleResult 返回,sessionState 写入留在组合根)
|
|
13
13
|
*
|
|
@@ -22,6 +22,9 @@ import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendi
|
|
|
22
22
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
24
24
|
import { guardStaleCtx, oncePerProcess, toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
25
|
+
// [W2/V4 D6] 恢复链注销直落消费的 protocol SSOT(customType + status 映射单点——
|
|
26
|
+
// 与 finalizeRun 直落 / reconcile-sweep 补注销同一函数,零新增定义点)。
|
|
27
|
+
import { mapReasonToStatus, PENDING_UNREGISTER_ENTRY_TYPE } from "@zhushanwen/extension-protocol";
|
|
25
28
|
|
|
26
29
|
// ═══ core 宿主端口消费(随迁块的依赖;production 默认实现住本文件) ═══
|
|
27
30
|
import { getOrCreateChannelRegistry } from "@zhushanwen/subagent-core";
|
|
@@ -37,6 +40,20 @@ import { bindNotifyLedgerHost, type NotifyLedgerHost } from "@zhushanwen/subagen
|
|
|
37
40
|
import { IDENTITY_CUSTOM_TYPE, type SubagentIdentityData } from "@zhushanwen/subagent-core";
|
|
38
41
|
import type { ExecutionMode } from "@zhushanwen/subagent-core";
|
|
39
42
|
import { maybeCleanupExpiredSessionFiles } from "@zhushanwen/subagent-core";
|
|
43
|
+
import { resolvePiSessionScopedDir } from "@zhushanwen/subagent-core";
|
|
44
|
+
// [W1 / D5] session_start 兜底触发点的统一保留维护轮:入口 + record 域目录锚
|
|
45
|
+
// (getSubagentRecordsDir 布局 + ENV_ROOT_CWD 贯穿 env 名单源)+ state 目录分量
|
|
46
|
+
// 单源 STATE_DIR_NAME——全部经 barrel 消费(壳生产消费纪律)。
|
|
47
|
+
import { reapOrphanRuns, runRetentionMaintenanceRound } from "@zhushanwen/subagent-core";
|
|
48
|
+
import { getSubagentRecordsDir } from "@zhushanwen/subagent-core";
|
|
49
|
+
import { ENV_ROOT_CWD } from "@zhushanwen/subagent-core";
|
|
50
|
+
import { STATE_DIR_NAME } from "@zhushanwen/subagent-core";
|
|
51
|
+
// workflow 族 customType 词表单源(workflow-record 现役 + workflow-state-link legacy
|
|
52
|
+
// 指针):引用集三代解析的判别字面量经 barrel 消费,勿手抄。
|
|
53
|
+
import {
|
|
54
|
+
WORKFLOW_RECORD_CUSTOM_TYPE,
|
|
55
|
+
WORKFLOW_STATE_LINK_CUSTOM_TYPE,
|
|
56
|
+
} from "@zhushanwen/subagent-core";
|
|
40
57
|
import {
|
|
41
58
|
getSubagentService,
|
|
42
59
|
setSubagentService,
|
|
@@ -80,6 +97,13 @@ function getCachedMainSessionFile(): string | undefined {
|
|
|
80
97
|
* 时(AGENTS.md 规则 6:首条 assistant 消息前可能不存在)返回 undefined,调用方
|
|
81
98
|
* (fork 解析 / 孤儿恢复)对该场景本就无 entry 可读。
|
|
82
99
|
*
|
|
100
|
+
* 布局对齐 pi 实装 getSessionsDir(config.js):session 文件落
|
|
101
|
+
* `<agentDir>/sessions/<encoded-cwd>/` 子目录(session-manager.js——`--<cwd 编码>--`
|
|
102
|
+
* 目录内 `<ts>_<sessionId>.jsonl`;与 collectAliveWorkflowRunReferences 同一布局
|
|
103
|
+
* 锚点)。子目录枚举 + 文件名后缀段 `<ts>_<sessionId>.jsonl` 匹配,slug 目录名不
|
|
104
|
+
* 解析(pi 编码规则演进不影响按 id 命中)。任一层 readdir 失败(sessions 根不存在
|
|
105
|
+
* 等)返回 undefined,调用方回落 getSessionFile()。
|
|
106
|
+
*
|
|
83
107
|
* [pi 锚点 ADR-0063 I4] getSessionFile attach 语义:返回 `this.sessionFile` 字段
|
|
84
108
|
* (pi-mono coding-agent/src/core/session-manager.ts :1011-1013),该字段仅在
|
|
85
109
|
* _setSessionFile(:884/:895-896,constructor 显式路径或 setSessionFile)与
|
|
@@ -87,23 +111,172 @@ function getCachedMainSessionFile(): string | undefined {
|
|
|
87
111
|
* extension ctx 持有的 sessionManager 若尚未重绑到 root session,getSessionFile
|
|
88
112
|
* 仍回旧值(与 E2E 实测一致)。clone v0.84.2 核对,实装 0.84.4。
|
|
89
113
|
*/
|
|
114
|
+
/** sessions 根下的 encoded-cwd slug 子目录绝对路径列表(布局锚点 = `<agentDir>/sessions`
|
|
115
|
+
* 对齐 pi 实装 getSessionsDir,详注见 resolveMainSessionFileById 与
|
|
116
|
+
* collectAliveWorkflowRunReferences)。readdir 失败原样上抛——错误分通道
|
|
117
|
+
* (catch-all 回落 / ENOENT 空态分流)归各调用方。 */
|
|
118
|
+
function listSessionSlugDirs(agentDir: string): string[] {
|
|
119
|
+
const sessionsRoot = path.join(agentDir, "sessions");
|
|
120
|
+
return fs
|
|
121
|
+
.readdirSync(sessionsRoot, { withFileTypes: true })
|
|
122
|
+
.filter((ent) => ent.isDirectory())
|
|
123
|
+
.map((ent) => path.join(sessionsRoot, ent.name));
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
function resolveMainSessionFileById(sessionId: string): string | undefined {
|
|
91
|
-
|
|
127
|
+
let slugDirs: string[];
|
|
92
128
|
try {
|
|
93
|
-
|
|
94
|
-
return match === undefined ? undefined : path.join(sessionsDir, match);
|
|
129
|
+
slugDirs = listSessionSlugDirs(getAgentDir());
|
|
95
130
|
} catch {
|
|
96
131
|
return undefined;
|
|
97
132
|
}
|
|
133
|
+
const suffix = `_${sessionId}.jsonl`;
|
|
134
|
+
for (const dir of slugDirs) {
|
|
135
|
+
try {
|
|
136
|
+
const match = fs.readdirSync(dir).find((f) => f.endsWith(suffix));
|
|
137
|
+
if (match !== undefined) return path.join(dir, match);
|
|
138
|
+
} catch {
|
|
139
|
+
continue; // 单子目录读失败继续其余(宁回落 getSessionFile,不中断装配)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
98
143
|
}
|
|
99
144
|
|
|
100
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* workflow 域 per-session state 目录探测(随迁为 module 私有,唯一消费方是随迁块)。
|
|
147
|
+
*
|
|
148
|
+
* [已知限制·登记] slug 锚 process.cwd()(进程 cwd)而非 session cwd:pi CLI 在
|
|
149
|
+
* 目录 B resume cwd 为 A 的 session 时,新 run 的 record 事件流落 B 的 slug
|
|
150
|
+
* 目录、旧 run 的在 A——GC/retention sweep(core 同源推导)读不到旧 run 的磁盘
|
|
151
|
+
* 足迹,兜底失效。主数据不受影响(权威 entry 在 session 文件里,跨 cwd 可重建)。
|
|
152
|
+
* 布局单源在 core resolvePiSessionScopedDir(workflow-state-root.ts)——修复改锚
|
|
153
|
+
* 只动 core 单点,本薄消费自动跟随。taiji 桌面场景(spawn cwd 恒等于 session
|
|
154
|
+
* cwd)不触发,仅裸 pi CLI 的跨目录 resume 触发,故登记不改。
|
|
155
|
+
*/
|
|
101
156
|
function resolveSessionDir(): string {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
// F2:agentDir 走 pi SDK 活源注入(实例隔离);slug + 探测布局单源在 core。
|
|
158
|
+
return resolvePiSessionScopedDir({ agentDir: getAgentDir() });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── [裁决点 7] 存活 session 引用集采集(壳侧注入面,core 不 import pi SDK)──────
|
|
162
|
+
//
|
|
163
|
+
// 全池 session 文件流式扫描(sessions 根 + 全 slug 子目录的 *.jsonl),逐文件
|
|
164
|
+
// 提 workflow run 的注册引用并集——三代形态都解析(v2 注册条目 / v1 全量快照
|
|
165
|
+
// 条目 / pre-W17 link 指针):
|
|
166
|
+
// - v2:workflow-record custom entry,data.kind === "registered" → data.runId;
|
|
167
|
+
// - v1:data.v === 1 且 data.snapshot.runId 为 string → snapshot.runId;
|
|
168
|
+
// - link:workflow-state-link custom entry → data.runId。
|
|
169
|
+
// 只认 v2 会把存量 run 首轮误判无主并不可逆删除(设计裁决点 7「引用集三代形态」)。
|
|
170
|
+
//
|
|
171
|
+
// 行预过滤(customType 子串)+ 逐行 JSON.parse 的宽容扫描:单文件解析失败跳过
|
|
172
|
+
// (坏文件不阻断整轮——引用集缺侧 = 宁保留方向:少采集到的 run 引用会使其
|
|
173
|
+
// 进观察期而非直接删除,宽限窗兜底)。成本量级 = 全池文件流式扫描,秒级~
|
|
174
|
+
// 十秒级(设计「实现落点」登记的实测预期——每 session_start 一次,oncePerProcess
|
|
175
|
+
// 守卫下进程内单跑)。
|
|
176
|
+
|
|
177
|
+
/** Node fs 错误 code 判定(ENOENT = 路径不存在;core shared/fs-error 的 errorCodeOf
|
|
178
|
+
* 未进 barrel——本地等价 helper,判据与 pi-host-run-store 的读错分通道同款)。 */
|
|
179
|
+
function isEnoent(err: unknown): boolean {
|
|
180
|
+
return typeof err === "object" && err !== null && (err as NodeJS.ErrnoException).code === "ENOENT";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** workflow custom entry 的最小消费视图(宽容扫描用):customType 保持 unknown
|
|
184
|
+
* (值域分流在调用方),data 已过对象校验(字段级读取仍走 unknown 判型)。 */
|
|
185
|
+
interface WorkflowCustomEntryView {
|
|
186
|
+
customType: unknown;
|
|
187
|
+
data: Record<string, unknown>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** [extractRunReferencesFromLine 守卫] 是否为 data 载荷成形的 workflow custom
|
|
191
|
+
* entry(type=custom 且 data 为对象;customType 免判型——分流在调用方)。 */
|
|
192
|
+
function isWorkflowCustomEntry(v: unknown): v is WorkflowCustomEntryView {
|
|
193
|
+
if (typeof v !== "object" || v === null) return false;
|
|
194
|
+
const rec = v as Record<string, unknown>;
|
|
195
|
+
return rec.type === "custom" && typeof rec.data === "object" && rec.data !== null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractRunReferencesFromLine(line: string, out: Set<string>): void {
|
|
199
|
+
const trimmed = line.trim();
|
|
200
|
+
if (trimmed === "" || !trimmed.includes("workflow")) return; // 行级预过滤
|
|
201
|
+
let parsed: unknown;
|
|
202
|
+
try {
|
|
203
|
+
parsed = JSON.parse(trimmed);
|
|
204
|
+
} catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!isWorkflowCustomEntry(parsed)) return;
|
|
208
|
+
const d: Record<string, unknown> = parsed.data;
|
|
209
|
+
if (parsed.customType === WORKFLOW_RECORD_CUSTOM_TYPE) {
|
|
210
|
+
extractWorkflowRecordRunReference(d, out);
|
|
211
|
+
} else if (parsed.customType === WORKFLOW_STATE_LINK_CUSTOM_TYPE) {
|
|
212
|
+
if (typeof d.runId === "string" && d.runId !== "") out.add(d.runId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** [extractRunReferencesFromLine 拆分] workflow-record 条目两代引用提取
|
|
217
|
+
* (v2 registered 的 runId / v1 快照内 runId)。 */
|
|
218
|
+
function extractWorkflowRecordRunReference(
|
|
219
|
+
d: Record<string, unknown>,
|
|
220
|
+
out: Set<string>,
|
|
221
|
+
): void {
|
|
222
|
+
if (d.kind === "registered" && typeof d.runId === "string" && d.runId !== "") {
|
|
223
|
+
out.add(d.runId);
|
|
224
|
+
} else if (d.v === 1 && typeof d.snapshot === "object" && d.snapshot !== null) {
|
|
225
|
+
const snapRunId: unknown = (d.snapshot as Record<string, unknown>).runId;
|
|
226
|
+
if (typeof snapRunId === "string" && snapRunId !== "") out.add(snapRunId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* [裁决点 7] 采集全部存活 session 的 workflow run 引用并集(agentDir 活源)。
|
|
232
|
+
*
|
|
233
|
+
* sessions 布局对齐 pi 实装 getSessionsDir(config.js):`<agentDir>/sessions`
|
|
234
|
+
* 下的 encoded-cwd 子目录(session-manager.js——`--<cwd 编码>--` 目录内
|
|
235
|
+
* `<ts>_<sessionId>.jsonl`)。指向错误层级会静默扫出空集,裁决点 7 的引用
|
|
236
|
+
* 保护整体落空(活跃 run 误判无主)——布局锚点以 pi dist 实装为准。
|
|
237
|
+
*/
|
|
238
|
+
async function collectAliveWorkflowRunReferences(
|
|
239
|
+
agentDir: string,
|
|
240
|
+
): Promise<ReadonlySet<string>> {
|
|
241
|
+
const refs = new Set<string>();
|
|
242
|
+
let slugDirs: string[];
|
|
243
|
+
try {
|
|
244
|
+
slugDirs = listSessionSlugDirs(agentDir);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
// 读错分通道(对齐 core pi-host-run-store 同款纪律):ENOENT = 从未落盘的
|
|
247
|
+
// 正常空态,空集返回(core reapOrphanRuns 按「无引用」正常判定);非 ENOENT
|
|
248
|
+
// (EACCES/EIO 等真 IO 故障)上抛——空集会把「引用状态不可知」折叠成「无任何
|
|
249
|
+
// 引用」的成功返回,core 的「采集失败 = 整轮跳过(宁保留)」防御(reapOrphan
|
|
250
|
+
// Runs 的 collectAliveRunReferences catch)在生产路径将不可达,持续 IO 权限
|
|
251
|
+
// 故障下被存活 session 引用的 run 会在宽限窗后被不可逆删除。
|
|
252
|
+
if (isEnoent(err)) return refs;
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
const readOpts = { encoding: "utf8" as const };
|
|
256
|
+
for (const dir of slugDirs) {
|
|
257
|
+
let files: string[];
|
|
258
|
+
try {
|
|
259
|
+
files = fs.readdirSync(dir);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
// 同款分通道:ENOENT = 该 slug 目录被并发清走(会话删除竞态),跳过;真 IO
|
|
262
|
+
// 故障上抛整轮跳过(该目录下 session 的引用集缺席只延后删除——但「缺席的
|
|
263
|
+
// 原因不可知」时按失败处置,不冒充空集)。
|
|
264
|
+
if (!isEnoent(err)) throw err;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
for (const file of files) {
|
|
268
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
269
|
+
try {
|
|
270
|
+
const content = fs.readFileSync(path.join(dir, file), readOpts);
|
|
271
|
+
for (const line of content.split("\n")) {
|
|
272
|
+
extractRunReferencesFromLine(line, refs);
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
continue; // 单文件读失败跳过(宁保留——该文件的引用缺席只延后删除)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return refs;
|
|
107
280
|
}
|
|
108
281
|
|
|
109
282
|
// ── 进程级单例(dialog queue;原 index.ts module 级随域搬移) ────────────────────
|
|
@@ -190,7 +363,7 @@ export interface SessionLifecycleResult {
|
|
|
190
363
|
store: JsonlRunStore;
|
|
191
364
|
runs: Map<string, WorkflowRun>;
|
|
192
365
|
sessionDir: string;
|
|
193
|
-
/** D-008 per-session SAR
|
|
366
|
+
/** D-008 per-session SAR(subagentService 委托目标) */
|
|
194
367
|
runner: SubprocessAgentRunner;
|
|
195
368
|
/** session 上下文(notifyDone 需要 GuiContext) */
|
|
196
369
|
ctx: ExtensionContext;
|
|
@@ -278,10 +451,6 @@ function createOrReuseServices(pi: ExtensionAPI, ctx: ExtensionContext): Service
|
|
|
278
451
|
setSubagentService(service);
|
|
279
452
|
}
|
|
280
453
|
|
|
281
|
-
// S-2: 启动 idle record GC 定时器(30 天 TTL,每小时检查一次)。
|
|
282
|
-
// 注册 setInterval 属进程级副作用——oncePerProcess 守卫防双跑(u-audit-fix)。
|
|
283
|
-
oncePerProcess("subagent-workflow:start-gc-timer", () => service.startGcTimer());
|
|
284
|
-
|
|
285
454
|
return { service, modelService, reused };
|
|
286
455
|
}
|
|
287
456
|
|
|
@@ -350,45 +519,69 @@ function appendSubagentIdentityEntry(pi: ExtensionAPI): void {
|
|
|
350
519
|
* 返回 undefined。
|
|
351
520
|
*/
|
|
352
521
|
export function bindLedgerHostAndRecover(pi: ExtensionAPI, ctx: ExtensionContext): NotifyLedgerHost | undefined {
|
|
522
|
+
const ledgerHost: NotifyLedgerHost = {
|
|
523
|
+
appendLedgerEntry: (customType, data) => {
|
|
524
|
+
pi.appendEntry(customType, data);
|
|
525
|
+
},
|
|
526
|
+
readSessionEntries: () => ctx.sessionManager.getEntries(),
|
|
527
|
+
isIdle: () => ctx.isIdle(),
|
|
528
|
+
onAgentSettled: (handler) => {
|
|
529
|
+
pi.on("agent_settled", handler);
|
|
530
|
+
},
|
|
531
|
+
sendDelivery: (message) => {
|
|
532
|
+
// D5 单通道:唯一发送形态 = sendCustomMessage({triggerTurn:true}),
|
|
533
|
+
// courier 已在发送前二次复查 isIdle,多通道投递选项已删(D5)。
|
|
534
|
+
// stale ctx 防御(crash-resilience D1 / ext-guards 审计 §7 blockers#1 收口):
|
|
535
|
+
// sendDelivery 经 settled 边沿 / 看门狗 / 恢复重放异步触发——session 替换窗口
|
|
536
|
+
// 触碰 stale pi 命中 assertActive(PS-30)即无人接 rejection(E1 同机制)。
|
|
537
|
+
// stale 静默降级(本条通知不投递,attemptDeliver 按已受理标 sentAt——session
|
|
538
|
+
// 替换后通知对旧 session 已无意义,与守卫前「留 pending 反复撞 stale 直到账本
|
|
539
|
+
// 重绑」终局一致),非 stale 错误原样上抛(attemptDeliver 既有 catch 走
|
|
540
|
+
// settleRejected 留账重试语义不变)。
|
|
541
|
+
guardStaleCtx(() => pi.sendMessage(message, { triggerTurn: true }), {
|
|
542
|
+
label: "subagent-workflow:sendDelivery",
|
|
543
|
+
onStale: (error) =>
|
|
544
|
+
logger.warn("notify delivery skipped (stale ctx)", {
|
|
545
|
+
error: toErrorMessage(error),
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
548
|
+
},
|
|
549
|
+
// abandon 对会话补显形(T4③ 放弃终态):不唤醒的 display 消息(无 triggerTurn),
|
|
550
|
+
// 让主 agent/用户在会话里看到「通知已放弃」线索。
|
|
551
|
+
// 与 sendDelivery(triggerTurn 唤醒)分工,通道不复用。
|
|
552
|
+
sendDisplayMessage: (message) => {
|
|
553
|
+
guardStaleCtx(() => pi.sendMessage(message), {
|
|
554
|
+
label: "subagent-workflow:sendDisplayMessage",
|
|
555
|
+
onStale: (error) =>
|
|
556
|
+
logger.warn("notify abandon display skipped (stale ctx)", {
|
|
557
|
+
error: toErrorMessage(error),
|
|
558
|
+
}),
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
// bind 与 recover 拆独立 try(失败归因不同):
|
|
563
|
+
// - bind 失败:槽上无 ledger,消费方(getBoundNotifyLedger)退回内核直发路径;
|
|
564
|
+
// - recover 失败:bind 已成功、槽上 ledger 仍在,消费方照常走账本路径(重启重放
|
|
565
|
+
// 缺席,边沿/看门狗仍投新通知)——不得共用 "bind failed" 文案误报。
|
|
566
|
+
let ledger: ReturnType<typeof bindNotifyLedgerHost>;
|
|
353
567
|
try {
|
|
354
|
-
|
|
355
|
-
appendLedgerEntry: (customType, data) => {
|
|
356
|
-
pi.appendEntry(customType, data);
|
|
357
|
-
},
|
|
358
|
-
readSessionEntries: () => ctx.sessionManager.getEntries(),
|
|
359
|
-
isIdle: () => ctx.isIdle(),
|
|
360
|
-
onAgentSettled: (handler) => {
|
|
361
|
-
pi.on("agent_settled", handler);
|
|
362
|
-
},
|
|
363
|
-
sendDelivery: (message) => {
|
|
364
|
-
// D5 单通道:唯一发送形态 = sendCustomMessage({triggerTurn:true}),
|
|
365
|
-
// courier 已在发送前二次复查 isIdle,多通道投递选项已删(D5)。
|
|
366
|
-
// stale ctx 防御(crash-resilience D1 / ext-guards 审计 §7 blockers#1 收口):
|
|
367
|
-
// sendDelivery 经 settled 边沿 / 看门狗 / 恢复重放异步触发——session 替换窗口
|
|
368
|
-
// 触碰 stale pi 命中 assertActive(PS-30)即无人接 rejection(E1 同机制)。
|
|
369
|
-
// stale 静默降级(本条通知不投递,attemptDeliver 按已受理标 sentAt——session
|
|
370
|
-
// 替换后通知对旧 session 已无意义,与守卫前「留 pending 反复撞 stale 直到账本
|
|
371
|
-
// 重绑」终局一致),非 stale 错误原样上抛(attemptDeliver 既有 catch 走
|
|
372
|
-
// settleRejected 留账重试语义不变)。
|
|
373
|
-
guardStaleCtx(() => pi.sendMessage(message, { triggerTurn: true }), {
|
|
374
|
-
label: "subagent-workflow:sendDelivery",
|
|
375
|
-
onStale: (error) =>
|
|
376
|
-
logger.warn("notify delivery skipped (stale ctx)", {
|
|
377
|
-
error: toErrorMessage(error),
|
|
378
|
-
}),
|
|
379
|
-
});
|
|
380
|
-
},
|
|
381
|
-
};
|
|
382
|
-
// U4:重放观测已内聚到 ledger 分桶日志(recoveryReplays 桶经 extensionLogger
|
|
383
|
-
// 通道落盘),此处不再重复打日志。
|
|
384
|
-
bindNotifyLedgerHost(ledgerHost).recoverFromSession();
|
|
385
|
-
return ledgerHost;
|
|
568
|
+
ledger = bindNotifyLedgerHost(ledgerHost);
|
|
386
569
|
} catch (err) {
|
|
387
570
|
logger.warn("[subagents] notify ledger bind failed", {
|
|
388
571
|
reason: toErrorMessage(err),
|
|
389
572
|
});
|
|
390
573
|
return undefined;
|
|
391
574
|
}
|
|
575
|
+
try {
|
|
576
|
+
// U4:重放观测已内聚到 ledger 分桶日志(recoveryReplays 桶经 extensionLogger
|
|
577
|
+
// 通道落盘),此处不再重复打日志。
|
|
578
|
+
ledger.recoverFromSession();
|
|
579
|
+
} catch (err) {
|
|
580
|
+
logger.warn("[subagents] notify ledger recoverFromSession failed (ledger stays bound)", {
|
|
581
|
+
reason: toErrorMessage(err),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return ledgerHost;
|
|
392
585
|
}
|
|
393
586
|
|
|
394
587
|
/**
|
|
@@ -458,6 +651,85 @@ async function runProcessLevelMaintenance(
|
|
|
458
651
|
reason: toErrorMessage(err),
|
|
459
652
|
});
|
|
460
653
|
}
|
|
654
|
+
|
|
655
|
+
// [W1 / D5] session_start 兜底触发:统一保留维护轮(冷启动即清——进程首启
|
|
656
|
+
// 无任何新写时的清理机会,覆盖「崩溃后重开的会话」历史遗留)。run 域目录锚
|
|
657
|
+
// = resolveSessionDir 同源(与 JsonlRunStore 落盘同布局);record 域目录锚 =
|
|
658
|
+
// getSubagentRecordsDir(agentDir, rootCwd),rootCwd 推导与 SubagentService 构造点
|
|
659
|
+
// (SessionBaselines)同式:env 贯穿 ?? ctx.cwd 兜底,env 名单源 ENV_ROOT_CWD——
|
|
660
|
+
// enc 段不漂移(sessions 与 records 两目录同源不变量)。进程级维护幂等,
|
|
661
|
+
// oncePerProcess 守卫防双跑;/resume /fork 同进程复用时跳过(record-only 会话
|
|
662
|
+
// 的持续累积由 record 首写触发点 RecordStore.register 覆盖)。
|
|
663
|
+
try {
|
|
664
|
+
await oncePerProcess("subagent-workflow:retention-maintenance-round", async () => {
|
|
665
|
+
const envRootCwd = process.env[ENV_ROOT_CWD];
|
|
666
|
+
const rootCwd = envRootCwd && envRootCwd !== "" ? envRootCwd : ctx.cwd;
|
|
667
|
+
await runRetentionMaintenanceRound(
|
|
668
|
+
{
|
|
669
|
+
stateDir: path.join(resolveSessionDir(), STATE_DIR_NAME),
|
|
670
|
+
recordsDir: getSubagentRecordsDir(agentDir, rootCwd),
|
|
671
|
+
},
|
|
672
|
+
{},
|
|
673
|
+
{
|
|
674
|
+
warn: (msg) => logger.warn(`[subagent-workflow] ${msg}`),
|
|
675
|
+
debug: (msg) => logger.debug(`[subagent-workflow] ${msg}`),
|
|
676
|
+
toMsg: (err: unknown) => toErrorMessage(err),
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
// [裁决点 7](workflow-run-resume-revision)孤儿 run 对账清理:引用集采集 =
|
|
680
|
+
// 壳侧注入面(core 不 import pi SDK——全池 session 文件流式扫描,v2 注册
|
|
681
|
+
// 条目 ∪ v1 快照条目 ∪ pre-W17 link 指针三代解析的并集)。同维护轮触发点、
|
|
682
|
+
// 同 oncePerProcess 守卫(幂等整轮);宽限窗缺省 7 天(env 可调)。
|
|
683
|
+
// 触发面 = 全部 state 目录(对齐 core pi-host-run-store 枚举口径:agentDir
|
|
684
|
+
// 根回退 + sessions/<slug>/workflow-state 全目录 + 本 session 锚定目录)——
|
|
685
|
+
// 「引用 session 全删的目录」的残留 run 对本 session 维护轮结构性可达,判定
|
|
686
|
+
// 与删除判据归 core reapOrphanRuns,本处只做目录枚举。引用集预采集一次复用
|
|
687
|
+
// 全部目录(同轮同快照,逐目录重复全池扫描无增益);登记文件
|
|
688
|
+
// (orphan-run-reap.json)落各自 stateDir,多目录天然隔离。
|
|
689
|
+
const stateDirs = new Set<string>([
|
|
690
|
+
path.join(resolveSessionDir(), STATE_DIR_NAME),
|
|
691
|
+
path.join(agentDir, STATE_DIR_NAME),
|
|
692
|
+
]);
|
|
693
|
+
try {
|
|
694
|
+
for (const ent of fs.readdirSync(path.join(agentDir, "sessions"), { withFileTypes: true })) {
|
|
695
|
+
if (ent.isDirectory()) {
|
|
696
|
+
stateDirs.add(path.join(agentDir, "sessions", ent.name, STATE_DIR_NAME));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch (err) {
|
|
700
|
+
// sessions 根不可枚举:ENOENT = 从未落盘(首次运行形态),仅保留上方两
|
|
701
|
+
// 目录锚;非 ENOENT(EACCES/EIO 等真 IO 故障)debug 留痕不放大——枚举
|
|
702
|
+
// 缺侧 = 少扫目录(宁保留方向,宽限窗兜底),对齐读错分通道纪律。
|
|
703
|
+
if (!isEnoent(err)) {
|
|
704
|
+
logger.debug(
|
|
705
|
+
`[subagent-workflow] sessions 根目录枚举失败,state 目录触发面缺侧:${toErrorMessage(err)}`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// 引用集惰性单次采集(同轮同快照,逐目录重复全池扫描无增益):promise 在
|
|
710
|
+
// core reapOrphanRuns 的 await 表达式内才创建——采集器上抛(真 IO 故障,
|
|
711
|
+
// 读错分通道)时拒绝被该 await 的 try/catch 即时接住,不产生提前创建导致的
|
|
712
|
+
// 未处理拒绝告警;登记文件(orphan-run-reap.json)落各自 stateDir,多目录天然隔离。
|
|
713
|
+
let aliveRefsPromise: Promise<ReadonlySet<string>> | undefined;
|
|
714
|
+
const collectAliveRefsOnce = (): Promise<ReadonlySet<string>> => {
|
|
715
|
+
aliveRefsPromise ??= collectAliveWorkflowRunReferences(agentDir);
|
|
716
|
+
return aliveRefsPromise;
|
|
717
|
+
};
|
|
718
|
+
const reapDeps = {
|
|
719
|
+
collectAliveRunReferences: collectAliveRefsOnce,
|
|
720
|
+
warn: (msg: string) => logger.warn(`[subagent-workflow] ${msg}`),
|
|
721
|
+
debug: (msg: string) => logger.debug(`[subagent-workflow] ${msg}`),
|
|
722
|
+
toMsg: (err: unknown) => toErrorMessage(err),
|
|
723
|
+
};
|
|
724
|
+
for (const dir of stateDirs) {
|
|
725
|
+
await reapOrphanRuns({ stateDir: dir }, reapDeps);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
} catch (err) {
|
|
729
|
+
logger.warn("[subagents] retention maintenance round failed", {
|
|
730
|
+
reason: toErrorMessage(err),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
461
733
|
}
|
|
462
734
|
|
|
463
735
|
/** createSessionRunState 返回值(随迁块 5 的装配产物)。 */
|
|
@@ -473,7 +745,8 @@ interface SessionRunState {
|
|
|
473
745
|
*
|
|
474
746
|
* MF-1: store 健康度跟踪。loadAll 失败 → storeHealthy=false,workflow 域启动时 fail-fast。
|
|
475
747
|
* 崩溃恢复四步(loadAll → failed → save → evict)收口到 core recoverCrashedRuns(D8:
|
|
476
|
-
* 宿主各写一遍正是 failure-mode-B);pending:unregister 经 hooks
|
|
748
|
+
* 宿主各写一遍正是 failure-mode-B);pending:unregister 经 hooks 外置回调在本面
|
|
749
|
+
* appendEntry 直落([W2/V4 D6] 与 reload-closeout D4 定案对齐——不经 emit;位置在
|
|
477
750
|
* transition 后、save 前,对齐原内联实现);save 走 store 冷路径(done 绕过去抖)——
|
|
478
751
|
* 冷路径语义在 JsonlRunStore.save 内,不随循环归属转移。loadAll 失败的 fail-fast
|
|
479
752
|
* (storeHealthy=false 停初始化)是宿主职责,core 原样上抛、这里 catch 兜住。
|
|
@@ -501,32 +774,55 @@ async function createSessionRunState(
|
|
|
501
774
|
let storeHealthy = true;
|
|
502
775
|
// [skill-reload D4] 恢复门控:session_start(reason==='reload') 全程不跑
|
|
503
776
|
// recoverCrashedRuns(无论条目有无)。暗礁(设计 §2.4):recoverCrashedRuns 判
|
|
504
|
-
// 「crashed
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
777
|
+
// 「crashed」只看 record fold 出的 running 态(loadAll 产物;runs Map 内存活
|
|
778
|
+
// run 不参与判定)——loadAll 折叠重建出 running 态即经 interruptRun(终局编排
|
|
779
|
+
// 单一入口)收编,事件流不含进程存活信息、窗口内存活 run 的流同样是 running
|
|
780
|
+
// 形态——契约前提是「拥有这些 run 的进程已死」,而 reload 恰恰证明进程没死,
|
|
781
|
+
// 跑恢复即误杀窗口内存活的 run。条目缺失场景同理
|
|
782
|
+
// 门控:磁盘可能有本 session 的 running entry(前一轮 adoption 未完成又
|
|
783
|
+
// reload 的窗口),由下一次**非 reload** 的 session_start(真重启/切换)按既有
|
|
784
|
+
// kill-9 语义收编。跳过 loadAll 时无从证伪健康度:storeHealthy 保持 true
|
|
785
|
+
// (workflow 域可用,可派发新 run)。
|
|
512
786
|
if (!opts.skipRecovery) {
|
|
513
787
|
try {
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
788
|
+
// [B1 修复] 恢复不挂 oncePerProcess:loadAll 只认本 session 的 v2 注册
|
|
789
|
+
// 条目(record 流折叠重建,W17 定界的现行形态)、收编即向本 run 的 record
|
|
790
|
+
// 流追加 run-interrupted 转移事件(不覆盖任何文件,天然幂等)——恢复是
|
|
791
|
+
// session 级幂等操作,挂进程级守卫会让同进程的后续 session_start(如 /new
|
|
792
|
+
// 后 /resume 一个上次崩溃退出的 session)重放首次 Promise、跳过 loadAll,
|
|
793
|
+
// 该 session 的 running 残留既不收编也不进 run 列表。reload 的防误杀由上方
|
|
794
|
+
// opts.skipRecovery(分流处按 reason 判定)承担,无需进程级守卫。
|
|
795
|
+
const { loaded, recovered } = await recoverCrashedRuns(
|
|
796
|
+
store,
|
|
797
|
+
runs,
|
|
798
|
+
"Process killed (kill-9 or crash recovery)",
|
|
799
|
+
{
|
|
800
|
+
onRunRecovered: (payload) => {
|
|
801
|
+
// [W2/V4 D6] 注销直落权威面:appendEntry 直接落盘,不经 emit。
|
|
802
|
+
// [reload-closeout D4] 定案「emit 链在 reload 转换窗失效、appendEntry
|
|
803
|
+
// 是唯一可靠通道」在崩溃恢复链同样适用(session_start 装配窗可能仍在
|
|
804
|
+
// reload 转换内);pending 域消费方全部从持久化 entries 现算,直落对其
|
|
805
|
+
// 即时生效。status 经 protocol mapReasonToStatus 单点(与 finalizeRun
|
|
806
|
+
// 直落 / reconcile-sweep 补注销同一函数);抛错由 core 侧 hook 围栏
|
|
807
|
+
// warn 留痕后恢复循环继续,差集残留交 reconcile-sweep 下次收口。
|
|
808
|
+
pi.appendEntry(PENDING_UNREGISTER_ENTRY_TYPE, {
|
|
809
|
+
id: payload.id,
|
|
810
|
+
reason: payload.reason,
|
|
811
|
+
status: mapReasonToStatus(payload.reason),
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
// [W1 / D4] 收编终态条目补写(恢复链收编的条目半边):core 在 journal
|
|
815
|
+
// run-settled 落账后回调本面,v2 终态条目经当前 pi appendEntry 落主
|
|
816
|
+
// session(注册 + 终态两条 v2 小条目的收编补齐;本 store loadAll 对
|
|
817
|
+
// 「journal 已终局而条目缺失」形态的幂等补写与此同构)。围栏在 core 消费
|
|
818
|
+
// 侧(同步抛错 warn 后恢复循环继续,lifecycle.ts hooks 契约)。
|
|
819
|
+
appendSettledEntry: (customType, data) => {
|
|
820
|
+
pi.appendEntry(customType, data);
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
);
|
|
824
|
+
logger.debug(
|
|
825
|
+
`[subagent-workflow] recoverCrashedRuns: loaded=${loaded} recovered=${recovered}`,
|
|
530
826
|
);
|
|
531
827
|
} catch (err) {
|
|
532
828
|
// QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
|
|
@@ -544,7 +840,7 @@ async function createSessionRunState(
|
|
|
544
840
|
/**
|
|
545
841
|
* adoption 主体:接管既有条目(同引用原地改写)。成功返回原 SessionLifecycleResult
|
|
546
842
|
* (sessionState.get(sid) 与 reload 前同一引用——探针红线,store/runs 不换实例);
|
|
547
|
-
* 失败(健康检查不过 / rebind
|
|
843
|
+
* 失败(健康检查不过 / rebind 抛错)走 {@link failAdoption} 后返回
|
|
548
844
|
* undefined(调用方落到全量装配)。
|
|
549
845
|
*/
|
|
550
846
|
async function tryAdoptExistingSession(
|
|
@@ -561,22 +857,19 @@ async function tryAdoptExistingSession(
|
|
|
561
857
|
return undefined;
|
|
562
858
|
}
|
|
563
859
|
try {
|
|
564
|
-
// D3 rebind
|
|
565
|
-
//
|
|
860
|
+
// D3 rebind:store 实例跨 reload 存活(this 引用不变),原地改写 .pi/.ctx;
|
|
861
|
+
// 换入的 appendEntry 源带 stale guard(D5)。[D1] record 单源后 store 无投影
|
|
862
|
+
// 物化面(原 D4 快照重发随 state 快照删除而退役)——接管动作收敛为 rebind,
|
|
863
|
+
// 后续 v2 终态条目补写自动走新 pi。
|
|
566
864
|
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
865
|
} catch (err) {
|
|
572
866
|
await failAdoption(pi, ctx, deps, existing, toErrorMessage(err));
|
|
573
867
|
return undefined;
|
|
574
868
|
}
|
|
575
869
|
// 接管:同引用原地改写(ctx 换新——旧 ctx 已被 invalidate;lastEngine 按当前
|
|
576
|
-
// config
|
|
870
|
+
// config 重置基线)。跳过 store/runner 重建(幂等 last-wins)。
|
|
577
871
|
existing.ctx = ctx;
|
|
578
872
|
existing.lastEngine = lastEngine;
|
|
579
|
-
existing.runner.updateCtxModel(ctx.model ?? undefined);
|
|
580
873
|
logger.debug(
|
|
581
874
|
`[subagent-workflow] adoption ok (sessionId=${existing.sessionId}, runs=${existing.runs.size})`,
|
|
582
875
|
);
|
|
@@ -703,12 +996,10 @@ export async function setupSessionLifecycle(
|
|
|
703
996
|
skipRecovery: isReload,
|
|
704
997
|
});
|
|
705
998
|
|
|
706
|
-
// D-008: per-session SAR
|
|
707
|
-
//
|
|
708
|
-
// new: per-session session_start 时创建,经 SessionLifecycleResult 传给组合根。
|
|
999
|
+
// D-008: per-session SAR(subagentService 委托目标)——per-session session_start
|
|
1000
|
+
// 时创建,经 SessionLifecycleResult 传给组合根。
|
|
709
1001
|
const runner = new SubprocessAgentRunner({
|
|
710
1002
|
subagentService: service,
|
|
711
|
-
ctxModel: ctx.model ?? undefined,
|
|
712
1003
|
});
|
|
713
1004
|
|
|
714
1005
|
return {
|