@zhushanwen/pi-subagent-workflow 0.1.0 → 0.3.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 +56 -0
- package/agents/context-builder.md +1 -3
- package/agents/explorer.md +27 -0
- package/agents/oracle.md +2 -2
- package/agents/orchestrator.md +48 -0
- package/agents/planner.md +1 -3
- package/agents/researcher.md +0 -2
- package/agents/reviewer.md +2 -2
- package/agents/worker.md +0 -2
- package/package.json +5 -3
- package/skills/workflow-script-format/SKILL.md +6 -6
- package/src/execution/__tests__/agent-registry.test.ts +3 -3
- package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +484 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +233 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
- package/src/execution/__tests__/crash-recovery.test.ts +5 -1
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/execute-nesting.test.ts +1 -1
- package/src/execution/__tests__/execute-options-mapper.test.ts +41 -9
- package/src/execution/__tests__/finalize-record.test.ts +173 -0
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +59 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +209 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/index-session-start.test.ts +342 -0
- package/src/execution/__tests__/list-component.test.ts +1 -1
- package/src/execution/__tests__/notifier-flush.test.ts +78 -0
- package/src/execution/__tests__/path-encoding.test.ts +30 -1
- package/src/execution/__tests__/record-store.test.ts +86 -2
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +157 -153
- package/src/execution/__tests__/run-spawn-integration.test.ts +85 -151
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +193 -0
- package/src/execution/__tests__/sdk-contract.test.ts +5 -2
- package/src/execution/__tests__/session-file-gc.test.ts +46 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +7 -1
- package/src/execution/__tests__/spawn-args.test.ts +14 -19
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/stdin-writer.test.ts +353 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service.test.ts +73 -3
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
- package/src/execution/__tests__/tool-action.test.ts +27 -5
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +166 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +101 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +1 -1
- package/src/execution/agent-registry.ts +1 -1
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/channel-registry-access.ts +138 -0
- package/src/execution/concurrency-pool.ts +38 -6
- package/src/execution/dialog-queue.ts +329 -0
- package/src/execution/execute-options-mapper.ts +21 -4
- package/src/execution/execution-record.ts +5 -0
- package/src/execution/finalize-record.ts +160 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +52 -0
- package/src/execution/manifest-store.ts +206 -0
- package/src/execution/notifier.ts +5 -1
- package/src/execution/path-encoding.ts +18 -0
- package/src/execution/pi-invocation.ts +1 -1
- package/src/execution/record-store.ts +110 -2
- package/src/execution/session-file-gc.ts +25 -3
- package/src/execution/session-reconstructor.ts +11 -0
- package/src/execution/session-runner.ts +228 -32
- package/src/execution/spawn-event-adapter.ts +219 -6
- package/src/execution/stdin-writer.ts +106 -0
- package/src/execution/stream-sink.ts +83 -0
- package/src/execution/subagent-service.ts +230 -235
- package/src/execution/subprocess-agent-runner.ts +16 -4
- package/src/execution/types.ts +23 -3
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +175 -0
- package/src/execution/ui-request-observability.ts +77 -0
- package/src/execution/ui-request-queue.ts +168 -0
- package/src/index.ts +101 -4
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
- package/src/interface/command-actions.ts +77 -0
- package/src/interface/commands.ts +40 -4
- package/src/interface/format.ts +2 -0
- package/src/interface/gui-mappers.ts +83 -0
- package/src/interface/helpers.ts +52 -9
- package/src/interface/list-component.ts +3 -1
- package/src/interface/subagent-actions.ts +44 -24
- package/src/interface/subagent-tool.ts +56 -24
- package/src/interface/subagents.ts +45 -5
- package/src/interface/tool-render.ts +16 -5
- package/src/interface/tool-workflow-script.ts +113 -15
- package/src/interface/tool-workflow.ts +92 -34
- package/src/interface/views/WorkflowsView.ts +13 -4
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
- package/src/interface/views/detail-content.ts +20 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
- package/src/orchestration/agent-opts-resolver.ts +11 -2
- package/src/orchestration/error-recovery.ts +131 -23
- package/src/orchestration/execute-agent-call.ts +12 -3
- package/src/orchestration/jsonl-run-store.ts +10 -0
- package/src/orchestration/lifecycle.ts +1 -1
- package/src/orchestration/models/agent-call.ts +7 -0
- package/src/orchestration/models/ports.ts +15 -2
- package/src/orchestration/models/run-spec.ts +6 -0
- package/src/orchestration/models/trace.ts +1 -0
- package/src/orchestration/models/types.ts +19 -0
- package/src/orchestration/node-ops.ts +2 -0
- package/src/orchestration/worker-script-builder.ts +1 -0
- package/workflows/README.md +58 -0
- package/workflows/chain.js +107 -0
- package/workflows/map-reduce.js +142 -0
- package/workflows/parallel.js +131 -0
- package/workflows/scatter-gather.js +146 -0
- package/agents/scout.md +0 -17
- package/examples/README.md +0 -43
- package/examples/chain.example.js +0 -92
- package/examples/map-reduce.example.js +0 -99
- package/examples/parallel.example.js +0 -82
- package/examples/scatter-gather.example.js +0 -106
- package/src/interface/gui-adapter.ts +0 -136
|
@@ -12,6 +12,7 @@ import * as fs from "node:fs";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
|
|
14
14
|
import { getCurrentActivity, getDisplayItems, getEventLog, markReconstructedStatus, snapshot as toSnapshot } from "./execution-record.ts";
|
|
15
|
+
import type { ManifestRecord, ManifestStore } from "./manifest-store.ts";
|
|
15
16
|
import { reconstructFromFile } from "./session-reconstructor.ts";
|
|
16
17
|
import type {
|
|
17
18
|
ExecutionRecord,
|
|
@@ -37,7 +38,25 @@ const STATUS_PRIORITY: Record<ExecutionStatus, number> = {
|
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
/** .alive sidecar 的 24 小时软超时(超过此时间即使 pid 存活也判 crashed)。 */
|
|
40
|
-
const ALIVE_SOFT_TIMEOUT_MS =
|
|
41
|
+
const ALIVE_SOFT_TIMEOUT_MS = 3_600_000; // 1h in ms (reduced from 24h to minimize PID reuse window)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* manifest status → ExecutionStatus 运行时守卫映射。
|
|
45
|
+
*
|
|
46
|
+
* manifest 写 running/completed/failed/cancelled 四态(ManifestRecord.status union),但磁盘
|
|
47
|
+
* 文件可能陈旧(含历史 "error" 值、被外部篡改、或意外出现 crashed 值)。越界值返回 null——
|
|
48
|
+
* manifestToSubagent 据此返回 null,collectRecords 跳过损坏 record 并 console.warn,不因单个
|
|
49
|
+
* 坏文件崩溃,也不把损坏 record 错误降级为 failed(failed 会触发重试/告警,是误报)。
|
|
50
|
+
*
|
|
51
|
+
* 提取为纯函数:同时解决 PR#85 反射问题(三元 + `as ExecutionStatus` cast)。
|
|
52
|
+
*/
|
|
53
|
+
function mapManifestStatus(s: string): ExecutionStatus | null {
|
|
54
|
+
if (s === "completed") return "done";
|
|
55
|
+
if (s === "failed") return "failed";
|
|
56
|
+
if (s === "running") return "running";
|
|
57
|
+
if (s === "cancelled") return "cancelled";
|
|
58
|
+
return null; // 越界=数据损坏(含历史 "error"、意外 crashed 值),返回 null 让调用方跳过
|
|
59
|
+
}
|
|
41
60
|
|
|
42
61
|
/** store 变更监听器(返回取消订阅函数)。 */
|
|
43
62
|
export type ChangeListener = () => void;
|
|
@@ -45,6 +64,12 @@ export type ChangeListener = () => void;
|
|
|
45
64
|
/** status 过滤模式(collectRecords 的核心能力参数)。 */
|
|
46
65
|
export type StatusFilter = "running" | "all";
|
|
47
66
|
|
|
67
|
+
/** Pi ExtensionAPI 的最小子集(仅 collectRecords 跳过损坏 manifest 时上报用)。
|
|
68
|
+
* 解构为局部类型,避免与 subagent-service 的 PiLike 循环依赖。 */
|
|
69
|
+
export type RecordStorePi = {
|
|
70
|
+
appendEntry?: (customType: string, data: unknown) => void;
|
|
71
|
+
} | null | undefined;
|
|
72
|
+
|
|
48
73
|
// ============================================================
|
|
49
74
|
// RecordStore
|
|
50
75
|
// ============================================================
|
|
@@ -62,11 +87,31 @@ export class RecordStore {
|
|
|
62
87
|
private readonly records = new Map<string, ExecutionRecord>();
|
|
63
88
|
private readonly listeners = new Set<ChangeListener>();
|
|
64
89
|
private _disposed = false;
|
|
90
|
+
/** Pi handle(用于 appendEntry 上报损坏 manifest)。构造时可空,setPi() 后续注入。
|
|
91
|
+
* 显式存为字段而非构造参数 readonly:setPi 需要写权限。 */
|
|
92
|
+
private pi: RecordStorePi = null;
|
|
65
93
|
|
|
66
94
|
/** 重建缓存:sessionFile → SubagentRecord。notifyChange 时失效。 */
|
|
67
95
|
private reconCache: Map<string, SubagentRecord> | undefined;
|
|
68
96
|
|
|
69
|
-
constructor(
|
|
97
|
+
constructor(
|
|
98
|
+
private readonly sessionsDir: string,
|
|
99
|
+
private readonly manifestStore?: ManifestStore,
|
|
100
|
+
/** Pi 入口(注入 appendEntry 用于上报损坏 manifest)。
|
|
101
|
+
* SubagentService 构造时 this.pi 尚未注入(session_start 之前),传 undefined 兜底;
|
|
102
|
+
* 后续通过 setPi() 注入(见下)。允许 null = 兼容 PiLike 字段类型。 */
|
|
103
|
+
pi?: RecordStorePi,
|
|
104
|
+
) {
|
|
105
|
+
this.pi = pi ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** session_start 后由 SubagentService.initSession 调,注入真实 Pi handle。
|
|
109
|
+
* 设计为独立方法而非要求构造时必传——RecordStore 在 SubagentService 构造时即建
|
|
110
|
+
* (与 sessionsDir/manifestStore 一同初始化),但 this.pi 此时尚未注入。
|
|
111
|
+
* 后续构造期外的 appendEntry 上报才有意义。 */
|
|
112
|
+
setPi(pi: RecordStorePi): void {
|
|
113
|
+
this.pi = pi ?? null;
|
|
114
|
+
}
|
|
70
115
|
|
|
71
116
|
/** 注册新 record。触发 onChange。 */
|
|
72
117
|
register(record: ExecutionRecord): void {
|
|
@@ -154,6 +199,32 @@ export class RecordStore {
|
|
|
154
199
|
byId.set(rec.id, rec);
|
|
155
200
|
}
|
|
156
201
|
|
|
202
|
+
// 1.5 FR-8: manifest 源补充 orphan 记录。
|
|
203
|
+
// 优先级:内存 > 磁盘重建 > manifest。manifest 仅补充 session.jsonl 重建失败的记录。
|
|
204
|
+
if (this.manifestStore) {
|
|
205
|
+
for (const manifest of this.readManifestsSync()) {
|
|
206
|
+
if (byId.has(manifest.id)) continue; // 已被磁盘/内存源覆盖
|
|
207
|
+
if (rootSessionFilter !== undefined && manifest.rootSessionId !== rootSessionFilter) continue;
|
|
208
|
+
const rec = RecordStore.manifestToSubagent(manifest);
|
|
209
|
+
if (!rec) {
|
|
210
|
+
// manifest status 越界=数据损坏(含历史 "error"、意外 crashed 值):跳过而非降级 failed,
|
|
211
|
+
// 避免损坏 record 被误显示为 failed(触发错误重试/告警)。
|
|
212
|
+
// 双通道上报:console.warn 给开发者(终端调试);pi.appendEntry 给用户(session 内可见,
|
|
213
|
+
// 即使退出后也能从 session.jsonl 复盘事故原因)。SubagentService 构造时 pi 未注入
|
|
214
|
+
// (session_start 之前),appendEntry 走可选链安全降级。
|
|
215
|
+
console.warn("[subagents] skip manifest with invalid status:", manifest.id, manifest.status);
|
|
216
|
+
this.pi?.appendEntry?.("subagent:manifest-invalid-status", {
|
|
217
|
+
id: manifest.id,
|
|
218
|
+
status: manifest.status,
|
|
219
|
+
rootSessionId: manifest.rootSessionId,
|
|
220
|
+
agentName: manifest.agentName,
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
byId.set(rec.id, rec);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
157
228
|
// 2. 内存源覆盖(running record 优先——它是活态,比磁盘重建更新鲜)。同样按 session 过滤。
|
|
158
229
|
for (const r of this.records.values()) {
|
|
159
230
|
if (rootSessionFilter !== undefined && r.rootSessionId !== rootSessionFilter) continue;
|
|
@@ -250,6 +321,7 @@ export class RecordStore {
|
|
|
250
321
|
const rec: SubagentRecord = {
|
|
251
322
|
id: recon.id,
|
|
252
323
|
agent: recon.agent,
|
|
324
|
+
slug: recon.slug,
|
|
253
325
|
status: recon.status, // 临时值,各分支覆盖
|
|
254
326
|
mode: recon.mode,
|
|
255
327
|
startedAt: recon.startedAt,
|
|
@@ -322,6 +394,41 @@ export class RecordStore {
|
|
|
322
394
|
return b.startedAt - a.startedAt; // 新→旧
|
|
323
395
|
}
|
|
324
396
|
|
|
397
|
+
/** FR-8: 同步读取所有 manifest 记录(封装 ManifestStore.listAllSync,消除反射访问)。 */
|
|
398
|
+
private readManifestsSync(): readonly ManifestRecord[] {
|
|
399
|
+
return this.manifestStore?.listAllSync() ?? [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** FR-8: ManifestRecord → SubagentRecord(manifest 源投影)。
|
|
403
|
+
* task/slug/model 从 manifest 真实值投影(配合 writeManifest 补字段),缺失兜底空串。
|
|
404
|
+
* status 越界(mapManifestStatus 返回 null)时返回 null,由 collectRecords 跳过。 */
|
|
405
|
+
private static manifestToSubagent(m: ManifestRecord): SubagentRecord | null {
|
|
406
|
+
const status = mapManifestStatus(m.status);
|
|
407
|
+
if (status === null) return null;
|
|
408
|
+
return {
|
|
409
|
+
id: m.id,
|
|
410
|
+
agent: m.agentName,
|
|
411
|
+
task: m.task ?? "",
|
|
412
|
+
slug: m.slug ?? "",
|
|
413
|
+
status,
|
|
414
|
+
mode: "background" as const,
|
|
415
|
+
startedAt: m.createdAt,
|
|
416
|
+
rootSessionId: m.rootSessionId || undefined,
|
|
417
|
+
parentRecordId: undefined,
|
|
418
|
+
depth: 0,
|
|
419
|
+
endedAt: m.completedAt,
|
|
420
|
+
turns: 0,
|
|
421
|
+
totalTokens: 0,
|
|
422
|
+
model: m.model ?? "",
|
|
423
|
+
thinkingLevel: undefined,
|
|
424
|
+
eventLog: [],
|
|
425
|
+
displayItems: [],
|
|
426
|
+
result: undefined,
|
|
427
|
+
error: status === "failed" ? "manifest record" : undefined,
|
|
428
|
+
sessionFile: m.sessionFile,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
325
432
|
/** ExecutionRecord → SubagentRecord(内存源投影)。 */
|
|
326
433
|
private static recordToSubagent(r: ExecutionRecord): SubagentRecord {
|
|
327
434
|
return {
|
|
@@ -329,6 +436,7 @@ export class RecordStore {
|
|
|
329
436
|
agent: r.agent,
|
|
330
437
|
status: r.status,
|
|
331
438
|
mode: r.mode,
|
|
439
|
+
slug: r.slug,
|
|
332
440
|
startedAt: r.startedAt,
|
|
333
441
|
rootSessionId: r.rootSessionId,
|
|
334
442
|
parentRecordId: r.parentRecordId,
|
|
@@ -44,8 +44,11 @@ export function maybeCleanupExpiredSessionFiles(agentDir: string, cwd: string):
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** 递归扫描目录,unlink 超 TTL 的 .jsonl 文件及其 .cancelled sidecar。
|
|
48
|
-
|
|
47
|
+
/** 递归扫描目录,unlink 超 TTL 的 .jsonl 文件及其 .cancelled sidecar。
|
|
48
|
+
* [F2] 进入名为 records 的子目录时,额外清理超 TTL 的 manifest .json(跳过 .tmp.——
|
|
49
|
+
* recoverTmpFiles 同步处理)。allowManifestJson 仅由父调用按目录名开启,其他位置
|
|
50
|
+
* (如 subagents/worktrees.json)不匹配 .json,避免误删 worktree reaper 状态文件。 */
|
|
51
|
+
function walkAndClean(dir: string, now: number, allowManifestJson = false): void {
|
|
49
52
|
let entries: fs.Dirent[];
|
|
50
53
|
try {
|
|
51
54
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -56,7 +59,26 @@ function walkAndClean(dir: string, now: number): void {
|
|
|
56
59
|
for (const entry of entries) {
|
|
57
60
|
const full = path.join(dir, entry.name);
|
|
58
61
|
if (entry.isDirectory()) {
|
|
59
|
-
|
|
62
|
+
// 只在进入名为 records 的子目录时打开 manifest .json 清理。
|
|
63
|
+
// records 在 <enc>/records/ 下递归自动覆盖;其他位置(如 subagents/worktrees.json)
|
|
64
|
+
// 不能匹配 .json——否则会误删 worktree reaper 依赖的状态文件。
|
|
65
|
+
walkAndClean(full, now, entry.name === "records");
|
|
66
|
+
} else if (
|
|
67
|
+
allowManifestJson &&
|
|
68
|
+
entry.name.endsWith(".json") &&
|
|
69
|
+
// 跳过 .tmp.:recoverTmpFiles(session_start)同步处理 tmp,GC 不重复。
|
|
70
|
+
// 不校验内容——30 天 mtime 已是强 orphan 信号,扩展名 + 文件名足够。
|
|
71
|
+
!entry.name.includes(".tmp.")
|
|
72
|
+
) {
|
|
73
|
+
try {
|
|
74
|
+
const stat = fs.statSync(full);
|
|
75
|
+
if (now - stat.mtimeMs > TTL_MS) {
|
|
76
|
+
fs.unlinkSync(full);
|
|
77
|
+
}
|
|
78
|
+
} catch (_e) {
|
|
79
|
+
// 文件可能已被删除,忽略
|
|
80
|
+
void _e;
|
|
81
|
+
}
|
|
60
82
|
} else if (entry.name.endsWith(".jsonl")) {
|
|
61
83
|
try {
|
|
62
84
|
const stat = fs.statSync(full);
|
|
@@ -84,6 +84,11 @@ export interface SubagentIdentityData {
|
|
|
84
84
|
agent: string;
|
|
85
85
|
mode: ExecutionMode;
|
|
86
86
|
task: string;
|
|
87
|
+
/**
|
|
88
|
+
* 短标签(≤20 字符)。旧文件缺失→读取兜底空串(见 reconstructFromFile)。
|
|
89
|
+
* 写入时(session-runner)必填;读取时可选以兼容旧文件。
|
|
90
|
+
*/
|
|
91
|
+
slug?: string;
|
|
87
92
|
startedAt: number;
|
|
88
93
|
/** 根 Pi session ID(session 隔离过滤用)。旧文件可能缺失。 */
|
|
89
94
|
rootSessionId?: string;
|
|
@@ -126,6 +131,8 @@ export interface ReconstructedRecord {
|
|
|
126
131
|
agent: string;
|
|
127
132
|
mode: ExecutionMode;
|
|
128
133
|
task: string;
|
|
134
|
+
/** 短标签(≤20 字符)。旧文件缺失→兜底空串。 */
|
|
135
|
+
slug: string;
|
|
129
136
|
startedAt: number;
|
|
130
137
|
/** 根 Pi session ID(session 隔离过滤用)。旧文件可能缺失。 */
|
|
131
138
|
rootSessionId: string | undefined;
|
|
@@ -353,6 +360,7 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
|
|
|
353
360
|
const turn = emptyTurn();
|
|
354
361
|
turns.push(turn);
|
|
355
362
|
|
|
363
|
+
if (!Array.isArray(msg.content)) continue;
|
|
356
364
|
for (const block of msg.content) {
|
|
357
365
|
if (block.type === "text") {
|
|
358
366
|
turn.text += block.text;
|
|
@@ -428,8 +436,11 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
|
|
|
428
436
|
|
|
429
437
|
// rootSessionId 归一化:新文件读 rootSessionId,旧文件 fallback parentSessionId。
|
|
430
438
|
const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
|
|
439
|
+
// slug 兜底:旧 identity entry 无 slug 字段 → 空串(与 SubagentRecord.slug: string 契约一致)。
|
|
440
|
+
const slug = identity.slug ?? "";
|
|
431
441
|
return {
|
|
432
442
|
...identity,
|
|
443
|
+
slug,
|
|
433
444
|
rootSessionId,
|
|
434
445
|
parentRecordId: identity.parentRecordId,
|
|
435
446
|
depth: identity.depth ?? 0,
|