@shgroup/dsh-serenity-hooks 1.32.0 → 1.34.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.
Files changed (100) hide show
  1. package/README.md +12 -11
  2. package/dsh.plugin.json +5 -5
  3. package/lib/acp-core.d.ts +2 -22
  4. package/lib/acp-http.d.ts +8 -0
  5. package/lib/api.d.ts +12 -4
  6. package/lib/autopilot-chain.d.ts +76 -0
  7. package/lib/autopilot-core-DCRkiWEe.js +229 -0
  8. package/lib/autopilot-core.d.ts +132 -0
  9. package/lib/autopilot-ops.d.ts +48 -0
  10. package/lib/autopilot-trajectory-D9Znxo75.js +782 -0
  11. package/lib/autopilot-trajectory.d.ts +79 -83
  12. package/lib/{ccc-DAsSHsub.js → ccc-NlLr_sxy.js} +18 -2
  13. package/lib/ccc-roots-Bd_SjEs7.js +152 -0
  14. package/lib/ccc-roots.d.ts +105 -0
  15. package/lib/ccc.d.ts +23 -6
  16. package/lib/client.js +30 -10
  17. package/lib/clock-runtime.d.ts +197 -0
  18. package/lib/config-ops.d.ts +9 -14
  19. package/lib/constants.d.ts +0 -2
  20. package/lib/container-status.d.ts +183 -0
  21. package/lib/diag-ops.d.ts +76 -0
  22. package/lib/face-host.d.ts +110 -0
  23. package/lib/fs-ops.d.ts +3 -2
  24. package/lib/gateway-auth.d.ts +2 -5
  25. package/lib/gateway.d.ts +0 -34
  26. package/lib/git-ops.d.ts +3 -2
  27. package/lib/handyman-ops.d.ts +3 -2
  28. package/lib/handyman-preset-inherit.d.ts +2 -1
  29. package/lib/host/access.d.ts +8 -7
  30. package/lib/host/contract.d.ts +17 -6
  31. package/lib/host/type-contract.d.ts +1 -2
  32. package/lib/im-bridge.d.ts +6 -5
  33. package/lib/index.d.ts +1 -1
  34. package/lib/index.js +11053 -8305
  35. package/lib/invariant.d.ts +5 -16
  36. package/lib/invariant.js +8 -4
  37. package/lib/json.d.ts +1 -1
  38. package/lib/kit-ops.d.ts +11 -31
  39. package/lib/localstore-ops.d.ts +3 -17
  40. package/lib/msm-ops.d.ts +34 -52
  41. package/lib/opencode-provider.d.ts +4 -5
  42. package/lib/output-guard-seam.d.ts +1 -4
  43. package/lib/output-guard.d.ts +12 -3
  44. package/lib/ports.d.ts +59 -0
  45. package/lib/rebuild.d.ts +5 -4
  46. package/lib/seams/bootstrap.d.ts +4 -3
  47. package/lib/seams/compact.d.ts +2 -1
  48. package/lib/seams/context.d.ts +2 -4
  49. package/lib/seams/env.d.ts +2 -1
  50. package/lib/seams/guards.d.ts +22 -34
  51. package/lib/seams/keeper.d.ts +4 -4
  52. package/lib/seams/system-prompt.d.ts +1 -5
  53. package/lib/session-cleanup.d.ts +3 -2
  54. package/lib/{settings-section-Bm-xG-NR.js → settings-section-DTYZjwUs.js} +70 -15
  55. package/lib/settings-section.d.ts +14 -35
  56. package/lib/skiff-core.d.ts +5 -10
  57. package/lib/skiff-debug.d.ts +15 -21
  58. package/lib/skiff-registry-FTjoWQTQ.js +25 -0
  59. package/lib/skiff-registry.d.ts +2 -1
  60. package/lib/{skiff-role-BYvNnwv1.js → skiff-role-CEHL5cek.js} +1 -1
  61. package/lib/skiff-role.d.ts +2 -2
  62. package/lib/skills/opencode-scan.d.ts +3 -2
  63. package/lib/skills-discovery.d.ts +15 -2
  64. package/lib/status.d.ts +2 -1
  65. package/lib/tools/acc-diag.d.ts +22 -0
  66. package/lib/tools/cce.d.ts +1 -2
  67. package/lib/tools/container-admin.d.ts +7 -3
  68. package/lib/tools/eap.d.ts +2 -3
  69. package/lib/tools/handyman.d.ts +0 -29
  70. package/lib/tools/msm.d.ts +0 -4
  71. package/lib/tools/neat.d.ts +1 -2
  72. package/lib/tools/praxis.d.ts +0 -1
  73. package/lib/tools/skiff-admin.d.ts +2 -2
  74. package/lib/tools/{session.d.ts → trajectory.d.ts} +25 -17
  75. package/lib/totp.d.ts +0 -4
  76. package/lib/trajectory-assistant.d.ts +2 -1
  77. package/lib/trajectory-bound-Cax1p9ut.js +190 -0
  78. package/lib/{session-bound.d.ts → trajectory-bound.d.ts} +5 -6
  79. package/lib/trajectory-ops-mrXK0H0I.js +410 -0
  80. package/lib/{session-ops.d.ts → trajectory-ops.d.ts} +27 -36
  81. package/lib/trajectory-skills.d.ts +71 -0
  82. package/lib/wake-registry-CYxIb484.js +257 -0
  83. package/lib/wake-registry.d.ts +3 -11
  84. package/lib/wake-scheduler.d.ts +23 -20
  85. package/lib/web-fetch-provider.d.ts +0 -21
  86. package/lib/{weixin-api-PjEhmjlZ.js → weixin-api-CtFYm53S.js} +3 -9
  87. package/lib/weixin-api.d.ts +6 -37
  88. package/lib/weixin-bridge.d.ts +5 -24
  89. package/lib/weixin-hook.d.ts +6 -12
  90. package/lib/weixin-output-guard.d.ts +2 -1
  91. package/lib/{weixin-route-wWk4AIwV.js → weixin-route-B2ajFypI.js} +4 -5
  92. package/lib/weixin-route.d.ts +0 -2
  93. package/lib/weixin-send-api.d.ts +15 -6
  94. package/package.json +3 -5
  95. package/experiments/autopilot-trajectory/SKILL.md +0 -115
  96. package/experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts +0 -522
  97. package/lib/session-bound-NHozeLFc.js +0 -822
  98. package/lib/skiff-debug-K2k1JnGj.js +0 -2867
  99. package/lib/tools/autopilot-trajectory.d.ts +0 -25
  100. package/lib/{session-cleanup-CMT4mHWt.js → session-cleanup-B6IHZgww.js} +1 -1
@@ -1,822 +0,0 @@
1
- import { a as findSerenityRoot } from "./ccc-DAsSHsub.js";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
4
- //#region src/session-ops.ts
5
- /**
6
- * session-ops.ts — session 工具纯操作层(零 DSH 依赖,可独立单测)
7
- *
8
- * 行为对齐 osp(opencode-serenity-plugin/src/session/lib.ts)——osp 是 ACC 工具 spec:
9
- * - create:--desc / --issue 二选一(互斥、缺省报错);issue 模式目录 YYYY-MM-DD--<issue>
10
- * (无 S###,sessionId=issue);desc 模式 YYYY-MM-DD--S###--<desc>;goal 写入目标段;dry-run 预览
11
- * - close:需 name + confirm=true;标记 [x] 已完成+已关闭 + 进度记录"关闭"
12
- * - archive:name 缺省 → 批量归档(completed + ≥7 天 → 移动 _archived/);单会话需 completed + grace
13
- * - list/show/health/qa/summary:文本输出格式与 osp 一致
14
- * 保留 dsp S134 活跃会话机制(内存 Map + events 恢复,不落盘)——osp 同为内存 active-state。
15
- */
16
- const SESSION_ACTIONS = [
17
- "list",
18
- "show",
19
- "create",
20
- "use",
21
- "close",
22
- "health",
23
- "qa",
24
- "archive",
25
- "summary",
26
- "hook-develop-guide",
27
- "rebuild"
28
- ];
29
- /**
30
- * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
31
- * 移除 `.events` 属性 → `snapshotEvents()` 方法(dsh-session/src/session.ts:
32
- * `snapshotEvents(fromSeq, toSeqExclusive)`)。插件早期代码多处裸读 `.events`
33
- * (经 `as unknown as { events? }` 断言绕过 typecheck),运行时静默 undefined——
34
- * 造成 first-anchor 每轮重插 / SESSION 激活恢复失效 / rebuild 定位错乱。
35
- * 统一收敛到本 helper:snapshotEvents() 优先(rc.1 真实形态),`.events` 兜底
36
- * (测试替身/旧运行时)。所有消费方一律经此读取,禁止再裸读 `.events`。
37
- * 泛型 T:调用方按需声明事件形状(如 `SessionEvent`),unknown 默认。
38
- */
39
- function sessionEvents(session) {
40
- const s = session;
41
- if (!s) return [];
42
- if (typeof s.snapshotEvents === "function") try {
43
- return s.snapshotEvents() ?? [];
44
- } catch {}
45
- return s.events ?? [];
46
- }
47
- const SESSION_MD = "SESSION.md";
48
- const ARCHIVE_DIR_NAME = "_archived";
49
- const HEALTH_STALE_DAYS = 7;
50
- const HEALTH_STALLED_PCT = 30;
51
- const HEALTH_STALLED_DAYS = 3;
52
- const HEALTH_GHOST_DAYS = 2;
53
- const DAY = 864e5;
54
- function sessionsRoot(root) {
55
- return join(root, "AGENT_SESSIONS");
56
- }
57
- /** 解析 SESSION.md 状态元数据(对齐 osp parseSessionMd) */
58
- function parseSessionMd(filePath) {
59
- try {
60
- const content = readFileSync(filePath, "utf-8");
61
- return {
62
- hasSessionMd: true,
63
- completed: /\[\s*x\s*\]/i.test(content),
64
- completedCount: (content.match(/\[\s*x\s*\]/gi) ?? []).length,
65
- pendingCount: (content.match(/\[\s*[ \t]\s*\]/g) ?? []).length,
66
- unresolvedCount: (content.match(/(未解决|open|question|TODO)/gi) ?? []).length
67
- };
68
- } catch {
69
- return {
70
- hasSessionMd: false,
71
- completed: false,
72
- completedCount: 0,
73
- pendingCount: 0,
74
- unresolvedCount: 0
75
- };
76
- }
77
- }
78
- function readSessionEntry(dirPath) {
79
- try {
80
- const st = statSync(dirPath);
81
- if (!st.isDirectory()) return null;
82
- const dirName = basename(dirPath);
83
- const mdPath = join(dirPath, SESSION_MD);
84
- const status = existsSync(mdPath) ? parseSessionMd(mdPath) : {
85
- hasSessionMd: false,
86
- completed: false,
87
- completedCount: 0,
88
- pendingCount: 0,
89
- unresolvedCount: 0
90
- };
91
- return {
92
- dirName,
93
- path: dirPath,
94
- mtime: st.mtime,
95
- status
96
- };
97
- } catch {
98
- return null;
99
- }
100
- }
101
- /** 读取 AGENT_SESSIONS 中所有会话,活跃(未完成)排前(对齐 osp readAllSessions) */
102
- function readAllSessions(sessionsDir) {
103
- try {
104
- return readdirSync(sessionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => readSessionEntry(join(sessionsDir, e.name))).filter((s) => s !== null).sort((a, b) => {
105
- if (!a.status.completed && b.status.completed) return -1;
106
- if (a.status.completed && !b.status.completed) return 1;
107
- return b.mtime.getTime() - a.mtime.getTime();
108
- });
109
- } catch {
110
- return [];
111
- }
112
- }
113
- /** 提取目录名中的会话 ID(S### 或 issue 名);无匹配返回 '' */
114
- function extractSessionId(dirName) {
115
- const m = dirName.match(/--S(\d{3,})--/);
116
- return m ? `S${m[1]}` : "";
117
- }
118
- /**
119
- * 根据 key 查找会话(对齐 osp findSession):
120
- * 精确目录名 → S### ID(允许 S31→031)→ 唯一模糊子串匹配(多个则报错)
121
- */
122
- function findSession(sessionsDir, key) {
123
- const all = readAllSessions(sessionsDir);
124
- const byName = all.find((s) => s.dirName === key);
125
- if (byName) return byName;
126
- const searchId = key.replace(/^S/, "").padStart(3, "0");
127
- const byId = all.find((s) => {
128
- const m = s.dirName.match(/--S(\d{3,})--/);
129
- return m && m[1] === searchId;
130
- });
131
- if (byId) return byId;
132
- const lower = key.toLowerCase();
133
- const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(lower));
134
- if (fuzzy.length === 1) return fuzzy[0] ?? null;
135
- if (fuzzy.length > 1) throw new Error(`Found ${fuzzy.length} sessions matching "${key}": ` + fuzzy.map((s) => s.dirName).join(", ") + ". Use a more specific query.");
136
- return null;
137
- }
138
- /** list 子命令(对齐 osp listSessions 文本格式 + active 标记) */
139
- function listSessions(root, activeId) {
140
- const sessions = readAllSessions(sessionsRoot(root));
141
- if (sessions.length === 0) return "(no sessions in AGENT_SESSIONS/)";
142
- const lines = sessions.map((s) => {
143
- const age = Math.floor((Date.now() - s.mtime.getTime()) / DAY);
144
- const sessionId = extractSessionId(s.dirName);
145
- return `${activeId !== void 0 && sessionId !== "" && sessionId === activeId ? "●" : s.status.completed ? "✓" : "○"} ${s.dirName} (${age}d ago)`;
146
- });
147
- return `AGENT_SESSIONS/ (${sessions.length} sessions)\n` + lines.join("\n");
148
- }
149
- /** show 子命令(对齐 osp showSession:`# dirName\n\n` + SESSION.md 内容) */
150
- function showSession(root, key) {
151
- const session = findSession(sessionsRoot(root), key);
152
- if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
153
- const mdPath = join(session.path, SESSION_MD);
154
- if (!existsSync(mdPath)) return `Session ${session.dirName} (no SESSION.md — directory exists but is empty)`;
155
- const content = readFileSync(mdPath, "utf-8");
156
- return `# ${session.dirName}\n\n${content}`;
157
- }
158
- /** 生成 SESSION.md 模板(对齐 osp:goal 写入目标段,时间戳 YYYY-MM-DD HH:mm) */
159
- function sessionMdTemplate(title, id, goal, now) {
160
- const ts = now.toISOString().slice(0, 16).replace("T", " ");
161
- return `# SESSION: ${title}\n- ID: ${id}\n\n## 目标\n${goal ?? "(待补充)"}\n\n## 状态\n- [ ] 进行中\n\n## 关键决策\n| # | 决策 | 理由 |\n|---|------|------|\n| 1 | | |\n\n## 进度记录\n- ${ts} — 创建\n\n## 产出物\n- \n\n## 未解决的问题\n- \n`;
162
- }
163
- /** create 子命令(对齐 osp createSession:--desc/--issue 二选一 + dry-run + 长度限制) */
164
- /** 目录名脱敏(Windows 审计问题 10):非法字符 → '-', 去尾点/空格, 保留名(CON/NUL 等)加前缀 */
165
- function sanitizeDirName(s) {
166
- const cleaned = s.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "");
167
- if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(cleaned)) return `_${cleaned}`;
168
- return cleaned;
169
- }
170
- function createSession(opts) {
171
- const { root, desc, issue, goal, dryRun } = opts;
172
- const sessionsDir = sessionsRoot(root);
173
- const now = /* @__PURE__ */ new Date();
174
- const datePrefix = now.toISOString().slice(0, 10);
175
- if (!desc && !issue) throw new Error("create requires either --desc or --issue");
176
- if (desc && issue) throw new Error("--desc and --issue are mutually exclusive");
177
- if (issue) {
178
- if (issue.length > 100) throw new Error(`issue too long: ${issue.length} chars (max 100)`);
179
- const dirName = `${datePrefix}--${sanitizeDirName(issue)}`;
180
- const sessionPath = join(sessionsDir, dirName);
181
- if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
182
- if (dryRun) return {
183
- message: `[dry-run] Would create: ${dirName}/`,
184
- dirName,
185
- sessionPath,
186
- sessionId: issue
187
- };
188
- mkdirSync(sessionPath, { recursive: true });
189
- writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(issue, issue, goal, now), "utf-8");
190
- return {
191
- message: `Created: ${dirName}/`,
192
- dirName,
193
- sessionPath,
194
- sessionId: issue
195
- };
196
- }
197
- if (!desc || desc.length === 0) throw new Error("description cannot be empty");
198
- if (desc.length > 200) throw new Error(`description too long: ${desc.length} chars (max 200)`);
199
- const sessions = readAllSessions(sessionsDir);
200
- let maxId = 0;
201
- for (const s of sessions) {
202
- const m = s.dirName.match(/--S(\d{3,})--/);
203
- if (m) {
204
- const num = parseInt(m[1], 10);
205
- if (num > maxId) maxId = num;
206
- }
207
- }
208
- const nextId = String(maxId + 1).padStart(3, "0");
209
- const dirName = `${datePrefix}--S${nextId}--${sanitizeDirName(desc)}`;
210
- const sessionPath = join(sessionsDir, dirName);
211
- if (!dryRun && existsSync(sessionPath)) throw new Error(`Session directory already exists: "${dirName}"`);
212
- if (dryRun) return {
213
- message: `[dry-run] Would create: ${dirName}/\n goal=${goal ?? "(none)"}`,
214
- dirName,
215
- sessionPath,
216
- sessionId: `S${nextId}`
217
- };
218
- mkdirSync(sessionPath, { recursive: true });
219
- writeFileSync(join(sessionPath, SESSION_MD), sessionMdTemplate(desc, `S${nextId}`, goal, now), "utf-8");
220
- return {
221
- message: `Created: ${dirName}/ (S${nextId})`,
222
- dirName,
223
- sessionPath,
224
- sessionId: `S${nextId}`
225
- };
226
- }
227
- /**
228
- * 活动会话跟踪(S134 v1.16.14 内存化,对齐 osp active-state):
229
- * **不落盘**——活跃会话状态在内存 Map(key = scope = dsh 会话 id),避免落盘标记
230
- * 文件累积与跨会话串台(落盘版 `.dsh/active-sessions/<scope>` 已被此方案取代)。
231
- * 进程重启恢复:从**当前会话历史(events)**解析 `[SESSION CONTEXT]` 标记(use 时注入),
232
- * 只扫自己会话——无全局扫描、无跨会话污染。
233
- */
234
- const DEFAULT_SESSION_SCOPE = "default";
235
- /** [SESSION CONTEXT] 恢复标记(use 时注入 events 历史;进程重启后从 events 解析) */
236
- const SESSION_CONTEXT_MARKER = "[SESSION CONTEXT] Activated:";
237
- /** 内存活跃会话:scope(dsh 会话 id)→ 会话信息(不落盘;并行多会话各自 key 隔离) */
238
- const activeStore = /* @__PURE__ */ new Map();
239
- /** 全局最近活跃(对齐 osp lastActive;供无 scope 上下文使用) */
240
- let lastActive = null;
241
- function getActiveSessionInfo(scope) {
242
- return activeStore.get(scope) ?? null;
243
- }
244
- function getLastActiveSessionInfo() {
245
- return lastActive;
246
- }
247
- function setActiveSessionInfo(scope, info) {
248
- activeStore.set(scope, info);
249
- lastActive = info;
250
- }
251
- function clearActiveSessionInfo(scope) {
252
- activeStore.delete(scope);
253
- if (lastActive && ![...activeStore.values()].some((v) => v === lastActive)) lastActive = null;
254
- }
255
- /** 当前 scope 的活跃会话 SESSION.md 绝对路径;无激活返回 null(读内存,不落盘) */
256
- function readActiveSessionMd(_root, scope = DEFAULT_SESSION_SCOPE) {
257
- return getActiveSessionInfo(scope)?.mdPath ?? null;
258
- }
259
- /**
260
- * use 子命令:激活会话(写内存 Map)+ 返回对齐 osp 的输出文本
261
- * (含 [SESSION CONTEXT] 标记 + todowrite 指令;标记随工具结果进 events 历史,
262
- * 进程重启后从当前会话 events 解析恢复)。
263
- */
264
- function useSession(root, key, scope = DEFAULT_SESSION_SCOPE) {
265
- const session = findSession(sessionsRoot(root), key);
266
- if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
267
- const mdPath = join(session.path, SESSION_MD);
268
- if (!existsSync(mdPath)) throw new Error(`Session "${session.dirName}" has no SESSION.md — nothing to load.`);
269
- const sessionId = extractSessionId(session.dirName) || basename(session.dirName);
270
- const dirName = session.dirName;
271
- const shortName = dirName.replace(/^\d{4}-\d{2}-\d{2}--/, "");
272
- setActiveSessionInfo(scope, {
273
- sessionId,
274
- dirName,
275
- mdPath
276
- });
277
- return {
278
- dir: dirName,
279
- mdPath,
280
- context: [
281
- `───────────────────────────────────────────────────────────────`,
282
- `${SESSION_CONTEXT_MARKER} ${dirName}`,
283
- `───────────────────────────────────────────────────────────────`,
284
- `Use "session show ${sessionId}" to view session details.`,
285
- `SESSION.md path: ${mdPath}`,
286
- ``,
287
- `→ All subsequent work should refer back to this session.`,
288
- ` Use "session show ${sessionId}" to check current progress.`,
289
- ` After advancing work, update the "进度记录" (progress) section in SESSION.md.`,
290
- ``,
291
- `→ BEFORE responding to the user, you MUST call todowrite immediately`,
292
- ` with the session todo list. The first item MUST be:`,
293
- ` content: "SESSION: ${sessionId} — ${shortName}"`,
294
- ` status: "completed", priority: "low"`,
295
- ` Follow with any tasks parsed from SESSION.md.`,
296
- `───────────────────────────────────────────────────────────────`
297
- ].join("\n")
298
- };
299
- }
300
- /**
301
- * close 子命令(对齐 osp closeSession):需 name + confirm=true;
302
- * 标记 SESSION.md 为 [x] 已完成 + [x] 已关闭 + 进度记录"关闭";清除该会话的活跃状态。
303
- */
304
- function closeSession(root, key, confirm, scope = DEFAULT_SESSION_SCOPE) {
305
- if (!confirm) return "⚠ Close requires explicit confirmation.\n Re-run with --confirm to confirm closing this session.";
306
- const session = findSession(sessionsRoot(root), key);
307
- if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
308
- if (session.status.completed) return `Session "${session.dirName}" is already completed.`;
309
- const mdPath = join(session.path, SESSION_MD);
310
- if (!existsSync(mdPath)) throw new Error(`Session "${session.dirName}" has no SESSION.md — nothing to close.`);
311
- let content = readFileSync(mdPath, "utf-8");
312
- content = content.replace(/\r\n/g, "\n");
313
- content = content.replace(/## 状态\n\n?- \[ \] 进行中/, "## 状态\n- [x] 已完成\n- [x] 已关闭");
314
- const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
315
- if (!content.includes("-- 关闭")) content = content.replace(/(## 进度记录\n)/, `$1- ${now} — 关闭\n`);
316
- writeFileSync(mdPath, content, "utf-8");
317
- clearActiveSessionInfo(scope);
318
- return `Session "${session.dirName}" closed and marked as completed.`;
319
- }
320
- /**
321
- * 从会话历史(events)解析会话身份(进程重启恢复;只扫**当前会话**自己的历史——无跨会话串台)。
322
- *
323
- * v1.24.11 稳固化(S142 用户需求:重建后新会话必须准确知道从哪个 SESSION 恢复):
324
- * **路径规范行即可,不再要求 [SESSION CONTEXT] 标记**——`session use` 上下文与重建锚点
325
- * (buildRebuildAnchor 的 `- Persistent trajectory — SESSION.md path: <rel>` 行)同格式,
326
- * 因此**仅靠重建锚点的会话(从未显式 use)同样可恢复**。从**尾到头**扫描,最后一条
327
- * 合法路径胜出(时间序最新);会话目录名/ID 从路径本身派生(单一真相源,不猜)。
328
- * 路径可为相对(重建锚点存 rel)——绝对化与存在性校验在调用方(知道 root)执行。
329
- */
330
- function parseSessionContextFromEvents(events) {
331
- for (let i = events.length - 1; i >= 0; i--) {
332
- const strs = [];
333
- collectStrings(events[i], strs);
334
- for (const s of strs) {
335
- const md = extractSessionMdPathFromText(s);
336
- if (!md) continue;
337
- let dirName;
338
- let filePath;
339
- if (basename(md) === SESSION_MD) {
340
- dirName = basename(dirname(md));
341
- filePath = md;
342
- } else if (isSessionDirName(basename(md))) {
343
- dirName = basename(md);
344
- filePath = join(md, SESSION_MD);
345
- } else continue;
346
- if (!isSessionDirName(dirName)) continue;
347
- const idMatch = dirName.match(/--S(\d{3,})--/);
348
- return {
349
- sessionId: idMatch ? `S${idMatch[1]}` : dirName,
350
- dirName,
351
- mdPath: filePath
352
- };
353
- }
354
- }
355
- return null;
356
- }
357
- /** 递归收集对象/数组/字符串中的全部字符串(保留原文,无 JSON 转义) */
358
- function collectStrings(v, out) {
359
- if (typeof v === "string") {
360
- out.push(v);
361
- return;
362
- }
363
- if (v && typeof v === "object") for (const val of Object.values(v)) collectStrings(val, out);
364
- }
365
- /** 规范行正则:`SESSION.md path: <路径>`(路径可含空格 → [^\r\n]+ 整行匹配) */
366
- const SESSION_MD_PATH_RE = /SESSION\.md path:\s*([^\r\n]+)/;
367
- /** 从文本提取 SESSION.md 路径(use 上下文 / 重建锚点规范行通用;无匹配返回 null)。
368
- * 同行已知尾注(如系统提示词 Session 块的 persistent-body 注释)剥除——规范行只取路径本体。 */
369
- function extractSessionMdPathFromText(text) {
370
- const m = text.match(SESSION_MD_PATH_RE);
371
- if (!m) return null;
372
- let p = m[1].trim();
373
- const suffix = p.search(/ \(the trajectory's persistent body/);
374
- if (suffix > 0) p = p.slice(0, suffix).trim();
375
- return p;
376
- }
377
- /** 会话目录名形态校验(createSession 恒带日期前缀 `YYYY-MM-DD--`) */
378
- function isSessionDirName(dirName) {
379
- return /^\d{4}-\d{2}-\d{2}--/.test(dirName) && dirName.length > 11;
380
- }
381
- /**
382
- * 从 dsh 会话标题解析 SESSION 目录(编码无关 best-match,U3/U4)——
383
- * 标题不假设 S### 前缀(CCC 可自定义编码:apaas-xxx / P### / 完整目录名等)。
384
- * 匹配优先级(全部对 AGENT_SESSIONS 现有目录 best-match,不猜):
385
- * ① 标题即完整目录名(含日期前缀 `YYYY-MM-DD--`)→ 精确命中
386
- * ② 标题含 `--<code>--` 段(如完整目录名被截断为 `<code>-日期-概括` 前段)→
387
- * 按 code 段匹配:取标题首 token(`-` 前),与各目录 `--<code>--`/`--<code>` 尾段比对
388
- * ③ 唯一模糊子串匹配(多个则返回 null 防误猜)
389
- * @param title dsh 会话标题(如 `S142-2026-08-24-概括` / `apaas-26116-…` / 完整目录名)
390
- * @param sessionsDir AGENT_SESSIONS 绝对路径
391
- * @returns 命中目录的绝对路径(SESSION.md);无/歧义返回 null
392
- */
393
- function resolveSessionByTitle(title, sessionsDir) {
394
- const t = (title ?? "").trim();
395
- if (!t) return null;
396
- const all = readAllSessions(sessionsDir);
397
- if (all.length === 0) return null;
398
- if (isSessionDirName(t)) {
399
- const exact = all.find((s) => s.dirName === t);
400
- if (exact) return join(exact.path, SESSION_MD);
401
- }
402
- const codeToken = t.split("-")[0]?.trim() ?? "";
403
- if (codeToken) {
404
- const byCode = all.filter((s) => {
405
- const m = s.dirName.match(/--([^--]+)--/);
406
- const code = m ? m[1] : null;
407
- const tailMatch = s.dirName.match(/--([^--]+)$/);
408
- const tailCode = tailMatch && !s.dirName.includes("--", s.dirName.lastIndexOf("--") + 3) ? tailMatch[1] : null;
409
- return code === codeToken || tailCode === codeToken || s.dirName === codeToken || s.dirName.includes(`--${codeToken}`);
410
- });
411
- if (byCode.length === 1) return join(byCode[0].path, SESSION_MD);
412
- if (byCode.length > 1) return null;
413
- }
414
- const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(t.toLowerCase()));
415
- if (fuzzy.length === 1) return join(fuzzy[0].path, SESSION_MD);
416
- return null;
417
- }
418
- /** health 子命令(对齐 osp healthCheck:stale/stalled/ghost/drift 四类检查,文本输出) */
419
- function healthCheck(root) {
420
- const sessions = readAllSessions(sessionsRoot(root));
421
- if (sessions.length === 0) return "No sessions found — nothing to check.";
422
- const now = Date.now();
423
- const issues = [];
424
- for (const s of sessions) {
425
- const ageDays = (now - s.mtime.getTime()) / DAY;
426
- const st = s.status;
427
- if (ageDays > HEALTH_STALE_DAYS && !st.completed) issues.push({
428
- dirName: s.dirName,
429
- issue: `No activity for ${Math.floor(ageDays)}d`,
430
- severity: "stale"
431
- });
432
- const totalTasks = st.completedCount + st.pendingCount;
433
- if (totalTasks > 0) {
434
- const pct = Math.round(st.completedCount / totalTasks * 100);
435
- if (pct < HEALTH_STALLED_PCT && ageDays > HEALTH_STALLED_DAYS && !st.completed) issues.push({
436
- dirName: s.dirName,
437
- issue: `Only ${pct}% done after ${Math.floor(ageDays)}d`,
438
- severity: "stalled"
439
- });
440
- }
441
- if (!st.hasSessionMd && ageDays > HEALTH_GHOST_DAYS) issues.push({
442
- dirName: s.dirName,
443
- issue: "No SESSION.md (ghost directory)",
444
- severity: "ghost"
445
- });
446
- if (st.unresolvedCount > 3 && !st.completed) issues.push({
447
- dirName: s.dirName,
448
- issue: `${st.unresolvedCount} unresolved items`,
449
- severity: "drift"
450
- });
451
- }
452
- if (issues.length === 0) return "All sessions healthy — no issues found.";
453
- const lines = issues.map((i) => `[${i.severity.toUpperCase()}] ${i.dirName}: ${i.issue}`);
454
- return `${issues.length} issue(s) found:\n` + lines.join("\n");
455
- }
456
- /** archive 子命令(对齐 osp archiveSessions:移动 _archived/;name 缺省批量) */
457
- function archiveSessions(root, opts) {
458
- const { name, dryRun } = opts;
459
- const sessionsDir = sessionsRoot(root);
460
- const now = Date.now();
461
- const archiveDir = join(sessionsDir, ARCHIVE_DIR_NAME);
462
- if (name) {
463
- const session = findSession(sessionsDir, name);
464
- if (!session) throw new Error(`Session not found: "${name}"`);
465
- if (!session.status.completed) return `Session "${session.dirName}" is not completed — skipping.`;
466
- const ageDays = (now - session.mtime.getTime()) / DAY;
467
- if (ageDays < 7) return `Session "${session.dirName}" completed ${Math.floor(ageDays)}d ago — needs ${7 - Math.floor(ageDays)} more days before archiving.`;
468
- if (dryRun) return `[dry-run] Would archive: ${session.dirName} → ${ARCHIVE_DIR_NAME}/`;
469
- if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
470
- renameSync(session.path, join(archiveDir, session.dirName));
471
- return `Archived: ${session.dirName} → _archived/`;
472
- }
473
- const toArchive = readAllSessions(sessionsDir).filter((s) => {
474
- if (!s.status.completed) return false;
475
- return (now - s.mtime.getTime()) / DAY >= 7;
476
- });
477
- if (toArchive.length === 0) return "No sessions eligible for archiving.";
478
- if (dryRun) return `[dry-run] Would archive ${toArchive.length} session(s):\n` + toArchive.map((s) => ` ${s.dirName}`).join("\n");
479
- if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
480
- let count = 0;
481
- for (const s of toArchive) {
482
- renameSync(s.path, join(archiveDir, s.dirName));
483
- count++;
484
- }
485
- return `Archived ${count} session(s) → _archived/`;
486
- }
487
- /** summary 子命令(对齐 osp sessionSummary 文本仪表盘) */
488
- function summarize(root) {
489
- const sessions = readAllSessions(sessionsRoot(root));
490
- if (sessions.length === 0) return "AGENT_SESSIONS/ is empty.";
491
- const now = Date.now();
492
- const completed = sessions.filter((s) => s.status.completed).length;
493
- const active = sessions.length - completed;
494
- const stale = sessions.filter((s) => !s.status.completed && (now - s.mtime.getTime()) / DAY > HEALTH_STALE_DAYS).length;
495
- const ghost = sessions.filter((s) => !s.status.hasSessionMd).length;
496
- const recent = sessions.slice(0, 5);
497
- const lines = [
498
- `AGENT_SESSIONS Summary`,
499
- `────────────────────────`,
500
- `Total: ${sessions.length}`,
501
- `Active: ${active}`,
502
- `Completed: ${completed}`,
503
- `Stale: ${stale}`,
504
- `Ghost: ${ghost}`,
505
- ``,
506
- `Recent activity (top 5):`,
507
- ...recent.map((s) => {
508
- const age = Math.floor((now - s.mtime.getTime()) / DAY);
509
- return ` ${s.status.completed ? "✓" : "○"} ${s.dirName} (${age}d ago)`;
510
- })
511
- ];
512
- if (stale > 0) lines.push("", "⚠ Warning: Stale sessions found — run \"session health\" for details.");
513
- return lines.join("\n");
514
- }
515
- /** 事实核对:SESSION.md 声明 vs 实际情况(结构/一致性/新鲜度/决策质量/产出物) */
516
- function qaCheck(root, key) {
517
- const session = findSession(sessionsRoot(root), key);
518
- if (!session) throw new Error(`Session not found: "${key}". Use "list" to see available sessions.`);
519
- const mdPath = join(session.path, SESSION_MD);
520
- if (!existsSync(mdPath)) return `[ERROR] Session "${session.dirName}" has no SESSION.md — nothing to verify.`;
521
- const content = readFileSync(mdPath, "utf-8");
522
- const issues = [];
523
- for (const section of [
524
- {
525
- heading: "目标",
526
- label: "目标 (goal)"
527
- },
528
- {
529
- heading: "状态",
530
- label: "状态 (status)"
531
- },
532
- {
533
- heading: "关键决策",
534
- label: "关键决策 (key decisions)"
535
- },
536
- {
537
- heading: "进度记录",
538
- label: "进度记录 (progress)"
539
- },
540
- {
541
- heading: "产出物",
542
- label: "产出物 (outputs)"
543
- },
544
- {
545
- heading: "未解决的问题",
546
- label: "未解决的问题 (unresolved)"
547
- }
548
- ]) {
549
- const headingRegex = new RegExp(`^##\\s*${section.heading}[\\s\\S]*?(?=^##|(?![\\s\\S]))`, "m");
550
- const match = content.match(headingRegex);
551
- if (!match) {
552
- issues.push({
553
- severity: "warning",
554
- category: "structure",
555
- message: `Missing section: ${section.label}`
556
- });
557
- continue;
558
- }
559
- const headingLineRegex = new RegExp(`^##\\s*${section.heading}\\s*$`, "m");
560
- const body = match[0].replace(headingLineRegex, "").trim();
561
- if (!body || /^[-*]\s*$/.test(body)) issues.push({
562
- severity: "warning",
563
- category: "structure",
564
- message: `Section "${section.label}" is empty (only placeholder)`
565
- });
566
- }
567
- const completedTasks = (content.match(/\[\s*x\s*\]/gi) ?? []).length;
568
- const pendingTasks = (content.match(/\[\s*[ \t]\s*\]/gi) ?? []).length;
569
- const statusSection = content.match(/^##\s*状态[\s\S]*?(?=^##|(?![^]))/im);
570
- const statusBody = statusSection ? statusSection[0].replace(/^##\s*状态.*$/m, "").trim() : "";
571
- const hasCompletionMark = statusBody ? /#+\s*(?:完成|done|completed|closed)\b/i.test(statusBody) || /(?:全部完成|已全部完成|所有.*任务.*完成|任务.*全部完成|已完成.*所有)/i.test(statusBody) : false;
572
- const unresolvedSection = content.match(/^##\s*未解决的问题[\s\S]*?(?=^##|(?![^]))/im);
573
- const unresolvedBody = unresolvedSection ? unresolvedSection[0].replace(/^##\s*未解决的问题.*$/m, "").trim() : "";
574
- const unresolvedCount = unresolvedBody ? (unresolvedBody.match(/(?:未解决|open|question|TODO)/gi) ?? []).length : 0;
575
- if (hasCompletionMark && pendingTasks > 0) issues.push({
576
- severity: "error",
577
- category: "consistency",
578
- message: `Session marked as completed but has ${pendingTasks} pending task(s)`
579
- });
580
- if (hasCompletionMark && unresolvedCount > 0) issues.push({
581
- severity: "warning",
582
- category: "consistency",
583
- message: `Session marked as completed but has ${unresolvedCount} unresolved item(s)`
584
- });
585
- if (completedTasks > 0 && pendingTasks === 0 && !hasCompletionMark) issues.push({
586
- severity: "info",
587
- category: "consistency",
588
- message: `All ${completedTasks} task(s) completed but session not marked complete`
589
- });
590
- const progressSection = content.match(/##\s*进度记录[\s\S]*?(?=^##|\z)/m);
591
- if (progressSection) {
592
- const dateMatches = progressSection[0].match(/\b(\d{4}-\d{2}-\d{2})\b/g);
593
- if (dateMatches && dateMatches.length > 0) {
594
- const lastDateStr = dateMatches[dateMatches.length - 1];
595
- const lastDate = new Date(lastDateStr);
596
- const daysSince = Math.floor((Date.now() - lastDate.getTime()) / DAY);
597
- if (daysSince > HEALTH_STALE_DAYS && pendingTasks > 0) issues.push({
598
- severity: "warning",
599
- category: "stale",
600
- message: `No progress entry for ${daysSince} days (last: ${lastDateStr}), session still has ${pendingTasks} pending task(s)`
601
- });
602
- }
603
- }
604
- const decisionSection = content.match(/##\s*关键决策[\s\S]*?(?=^##|\z)/m);
605
- if (decisionSection) {
606
- const decisionLines = decisionSection[0].split("\n").filter((l) => /^\|\s*\d+\s*\|/.test(l));
607
- if (decisionLines.length > 0) {
608
- const emptyDecisions = decisionLines.filter((l) => {
609
- const cells = l.split("|").map((c) => c.trim());
610
- return cells.length >= 4 && (!cells[2] || !cells[3] || cells[2] === "-" || cells[3] === "-");
611
- });
612
- if (emptyDecisions.length > 0) issues.push({
613
- severity: "info",
614
- category: "quality",
615
- message: `${emptyDecisions.length} decision(s) have empty reason — consider filling gaps`
616
- });
617
- } else if (!hasCompletionMark) issues.push({
618
- severity: "info",
619
- category: "quality",
620
- message: "No decisions recorded yet — add key decisions as the session progresses"
621
- });
622
- }
623
- const outputSection = content.match(/##\s*产出物[\s\S]*?(?=^##|\z)/m);
624
- if (outputSection) {
625
- const outputLines = outputSection[0].split("\n").filter((l) => /^\s*[-*]\s/.test(l));
626
- const fileRefs = [];
627
- for (const line of outputLines) {
628
- const refs = line.match(/`[^`]+`/g) ?? [];
629
- fileRefs.push(...refs.map((r) => r.replace(/`/g, "")));
630
- const inlineRefs = line.match(/\b[\w./-]+\.[a-zA-Z]{1,5}\b/g) ?? [];
631
- fileRefs.push(...inlineRefs.filter((r) => r.includes("/") || r.includes(".")));
632
- }
633
- if (fileRefs.length > 0) {
634
- const missing = fileRefs.filter((ref) => !existsSync(join(root, ref)));
635
- if (missing.length > 0 && completedTasks > 0) issues.push({
636
- severity: "warning",
637
- category: "outputs",
638
- message: `${missing.length} referenced file(s) not found: ${missing.slice(0, 3).join(", ")}${missing.length > 3 ? `... (+${missing.length - 3} more)` : ""}`
639
- });
640
- }
641
- }
642
- const errorCount = issues.filter((i) => i.severity === "error").length;
643
- const warningCount = issues.filter((i) => i.severity === "warning").length;
644
- const infoCount = issues.filter((i) => i.severity === "info").length;
645
- const verified = errorCount === 0 && warningCount === 0;
646
- const lines = [
647
- `QA Report: ${session.dirName}`,
648
- `────────────────${"─".repeat(session.dirName.length)}`,
649
- `Summary: ${issues.length} issue(s) found (${errorCount} error, ${warningCount} warning, ${infoCount} info)`,
650
- `Status: ${verified ? "✓ Verified" : "⚠ Issues found"}`
651
- ];
652
- if (issues.length > 0) {
653
- lines.push("");
654
- for (const issue of issues) {
655
- const tag = issue.severity === "error" ? "ERR" : issue.severity === "warning" ? "WRN" : "INF";
656
- lines.push(` [${tag}:${issue.category}] ${issue.message}`);
657
- }
658
- }
659
- lines.push("", "Recommendations:");
660
- if (errorCount > 0) lines.push(" • Fix errors before closing the session (status vs content mismatch)");
661
- if (warningCount > 0) lines.push(" • Review warnings — they may indicate incomplete or outdated information");
662
- if (verified) lines.push(" • Session looks clean — no issues detected");
663
- return lines.join("\n");
664
- }
665
- /** 绑定文件相对 CCC 根的路径 */
666
- const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
667
- const BINDINGS_VERSION = 1;
668
- function sessionHeader(session) {
669
- const header = session?.header;
670
- if (!header || typeof header !== "object") return null;
671
- return header;
672
- }
673
- /** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
674
- function bindingsPathFor(session) {
675
- const header = sessionHeader(session);
676
- if (typeof header?.cwd !== "string") return null;
677
- const root = findSerenityRoot(header.cwd);
678
- if (!root) return null;
679
- return join(root, BINDINGS_REL_PATH);
680
- }
681
- function readBindingsFile(path) {
682
- if (!existsSync(path)) return {
683
- version: BINDINGS_VERSION,
684
- sessions: {}
685
- };
686
- try {
687
- const sessions = JSON.parse(readFileSync(path, "utf-8")).sessions;
688
- if (!sessions || typeof sessions !== "object") return {
689
- version: BINDINGS_VERSION,
690
- sessions: {}
691
- };
692
- return {
693
- version: BINDINGS_VERSION,
694
- sessions
695
- };
696
- } catch {
697
- return {
698
- version: BINDINGS_VERSION,
699
- sessions: {}
700
- };
701
- }
702
- }
703
- /** 原子写:同目录 tmp + rename(避免半写文件被后续读取) */
704
- function writeBindingsFile(path, data) {
705
- mkdirSync(dirname(path), { recursive: true });
706
- const tmp = `${path}.tmp`;
707
- writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
708
- renameSync(tmp, path);
709
- }
710
- /** 记录形状校验(容忍手改/旧数据) */
711
- function asBoundRecord(value) {
712
- const r = value;
713
- if (!r || typeof r.dirName !== "string" || typeof r.mdPath !== "string") return null;
714
- return r;
715
- }
716
- /** 旧形态:从会话日志扫最后一条 `serenity/bound`(兼容 v1.30.5 及更早写入的绑定) */
717
- function readLegacyBoundFromEvents(session) {
718
- const events = sessionEvents(session);
719
- for (let i = events.length - 1; i >= 0; i--) {
720
- const ev = events[i];
721
- if (!ev || ev.type !== "serenity/bound") continue;
722
- const rec = asBoundRecord(ev.data);
723
- if (rec) return rec;
724
- }
725
- return null;
726
- }
727
- /**
728
- * 读取会话当前绑定(权威,latest-wins):文件记录优先,无则回落旧事件形态。
729
- * 无绑定返回 null。
730
- */
731
- function readLastBound(session) {
732
- const path = bindingsPathFor(session);
733
- const id = sessionHeader(session)?.id;
734
- if (path && typeof id === "string") {
735
- const rec = asBoundRecord(readBindingsFile(path).sessions[id]);
736
- if (rec) return rec;
737
- }
738
- return readLegacyBoundFromEvents(session);
739
- }
740
- /** 目录名 → 轨迹标签(`--S###--` 命中取 `S###`;未命中原样返回——编码无关 U4) */
741
- function trajectoryLabel(dirName) {
742
- const m = dirName.match(/--S(\d{3,})--/);
743
- return m ? `S${m[1]}` : dirName;
744
- }
745
- /**
746
- * **调用方自身**所属 trajectory 的标签(审计字段 `createdBy` 的唯一正解)。
747
- *
748
- * 三级来源,逐级回退(R↓:为什么不用单一来源):
749
- * 1. `readLastBound(session)` —— **持久 bound**,权威(重启存活、不受别的会话激活污染)
750
- * 2. `getActiveSessionInfo(scope)` —— 本 dsh 会话的**内存**激活(按 scope 隔离)
751
- * 3. `getLastActiveSessionInfo()` —— **全局**最近激活指针(无绑定的会话兼容回退)
752
- *
753
- * ⚠️ 为何单列成本函数(S142 §30.12.1 实测缺陷):唤醒注册表的 `createdBy` 原先直接取第 3 级
754
- * 全局指针,而它记录的是"**进程内最近一次被激活的 trajectory**"——多会话并发时必然张冠李戴
755
- * (实测:S142 登记唤醒被写成 `by=S060`)。`createdBy` 是 D60「以可审计替代入口栅」的**唯一凭证**,
756
- * 失真即替代控制失效 ⇒ 必须以"调用方自己是谁"为准,全局指针只作最后兜底。
757
- *
758
- * @param session 调用方 dsh 会话对象(需 `header.id` + `header.cwd`)
759
- * @param scope 调用方 dsh 会话 id(取不到传空串)
760
- * @returns 轨迹标签(`S###` 或完整目录名);三级全落空 → 空串(渲染为 `?`)
761
- */
762
- function resolveSessionTrajectoryLabel(session, scope) {
763
- const bound = session ? readLastBound(session) : null;
764
- if (bound?.dirName) return trajectoryLabel(bound.dirName);
765
- const scoped = scope !== "" ? getActiveSessionInfo(scope) : null;
766
- if (scoped) return scoped.sessionId;
767
- return getLastActiveSessionInfo()?.sessionId ?? "";
768
- }
769
- /**
770
- * 反向查:绑定到某 trajectory 目录名的 dsh 会话 id(**按绑定时间倒序,最新在前**)。
771
- *
772
- * 用途(wake-scheduler 冷唤醒):.bindings.json 以 **sessionId 为键**,而唤醒条目的
773
- * target 是**目录名**——需要值侧扫描做反向索引。冷会话被唤醒时必须先知道"哪个 dsh
774
- * 会话承载这条轨迹",这是唯一的权威来源(标题只作回退,见 v1.29.2 R2)。
775
- *
776
- * @param root CCC 根
777
- * @param dirName AGENT_SESSIONS 目录名(完整名,如 `2026-09-01--S151--x--auto`)
778
- * @returns 会话 id 列表(最新绑定在前);无绑定 → `[]`
779
- */
780
- function listBoundSessionIds(root, dirName) {
781
- const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
782
- const hits = [];
783
- for (const [id, raw] of Object.entries(file.sessions)) {
784
- const rec = asBoundRecord(raw);
785
- if (!rec || rec.dirName !== dirName) continue;
786
- hits.push({
787
- id,
788
- at: typeof rec.at === "number" ? rec.at : 0
789
- });
790
- }
791
- return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
792
- }
793
- /**
794
- * 写入绑定(latest-wins 覆盖该会话记录)。
795
- *
796
- * 绑定持久化尽力而为:无法定位 CCC 根 / 无会话 id / 写盘失败 → 返回 false,不阻断主流程。
797
- * @param session 目标 dsh 会话(需 `header.id` + `header.cwd` 可定位 CCC 根)
798
- * @param action 绑定动作
799
- * @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
800
- */
801
- function appendBound(session, action, rec) {
802
- const path = bindingsPathFor(session);
803
- const id = sessionHeader(session)?.id;
804
- if (!path || typeof id !== "string") return false;
805
- try {
806
- const file = readBindingsFile(path);
807
- file.sessions[id] = {
808
- dirName: rec.dirName,
809
- mdPath: rec.mdPath,
810
- ...rec.sessionId ? { sessionId: rec.sessionId } : {},
811
- action,
812
- at: Date.now(),
813
- ...rec.note ? { note: rec.note } : {}
814
- };
815
- writeBindingsFile(path, file);
816
- return true;
817
- } catch {
818
- return false;
819
- }
820
- }
821
- //#endregion
822
- export { showSession as C, setActiveSessionInfo as S, useSession as T, qaCheck as _, DEFAULT_SESSION_SCOPE as a, sessionEvents as b, clearActiveSessionInfo as c, extractSessionMdPathFromText as d, findSession as f, parseSessionContextFromEvents as g, listSessions as h, resolveSessionTrajectoryLabel as i, closeSession as l, healthCheck as m, listBoundSessionIds as n, SESSION_ACTIONS as o, getActiveSessionInfo as p, readLastBound as r, archiveSessions as s, appendBound as t, createSession as u, readActiveSessionMd as v, summarize as w, sessionsRoot as x, resolveSessionByTitle as y };