@shgroup/dsh-serenity-hooks 1.34.2 → 1.35.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.
@@ -1,230 +0,0 @@
1
- /**
2
- * autopilot-trajectory.ts — Autopilot Trajectory(自动巡航轨迹,正式版 v1.27.4)
3
- *
4
- * 前身:autotrajectory(v1.26.12~17 实验验证机制)。用户拍板正式化:
5
- * 命名 = Autopilot Trajectory(autotrajectory 是技术代号——"auto" 弱前缀 + "trajectory"
6
- * 内部术语;Autopilot 契合宁静号 Ship 隐喻:设定目标 → 自主巡航 → 驾驶舱可见 → 人类可接管,
7
- * 加 Trajectory 明确对象)。成熟度分级:L1 时钟唤起 → L2 焦点+偏见 → **L3 质量反馈环(下一轮)**。
8
- *
9
- * 依据:serenity-acc-specs/docs/self-sustaining-trajectory-hypothesis.md(v0.1 实验提案;
10
- * S142 2026-08-30 用户提出猜想 + 三项裁决:① 时钟驱动 ② 先验偏见自生+随机(随机充分引入)
11
- * ③ 人类=反馈来源(不直接参与))。
12
- *
13
- * 形态(用户拍板 v0.5 + 正式化 v1.27.4):
14
- * · **前台运行**:时钟唤起 = 向活跃会话注入唤起消息(agent.steer,v1.22.5 自动继续同款
15
- * 通道)→ 模型自动继续 → 用户全程可见、随时可介入(人类反馈天然并入——同会话)。
16
- * · **偏见内容提供者 = CCC 根目录下脚本**(biasProvider;缺省 autopilot-bias.ts,
17
- * 旧默认 autotrajectory-bias.ts 回退——pangu 等已配置 CCC 兼容):tool 直接运行取
18
- * stdout 作为偏见内容;脚本缺失 → **报错要求实现**(偏见内容归 CCC)。
19
- * · **会话标志 = 目录名后缀 --auto**:AGENT_SESSIONS/<date>--<desc>--auto/ → 自主形态。
20
- * · **轨迹焦点 topPrompt = CCC 定义**:每次唤起最先注入的顶层提示词(稳定焦点锚定防漂移)。
21
- * · **唤起窗口避开北京时间 8~18 点**(用量峰谷省钱)——avoidWakeHours 可覆盖。
22
- * · **多 CCC 独立(v1.27.4,用户"4个CCC能各自有autotrajectory吗")**:配置层本就 CCC 级
23
- * (enabled/intervalHours/session/biasProvider/topPrompt/窗口);正式版时钟遍历所有
24
- * live+enabled CCC 各自评估唤起(collectAutopilotCccs),running 守卫 per-CCC + 全局
25
- * 串行化(防模型并发挤兑)。
26
- * · **默认关**:serenity.json autopilotTrajectory.enabled=false → 定时器不启动,零资源占用。
27
- */
28
- import type { Context } from 'cordis';
29
- import type { AutopilotTrajectorySettings } from './ccc.js';
30
- import { type WakeRecord } from './autopilot-core.js';
31
- import { type ClockRuntime } from './clock-runtime.js';
32
- import type { WakeChainFacts } from './autopilot-chain.js';
33
- /**
34
- * 判据原语(`readAutopilotSettings` / `beijingHour` / `inAllowedWakeWindow` /
35
- * `isAutopilotSession` / `resolveTargetMd` / `judgeWake` / `shouldWake` /
36
- * `readSelfGeneratedMotivation` / `fetchBiasContent` / 常量)**已提取至
37
- * `autopilot-core.ts`**(S142 §12.13 ⑥ C6a)——本文件是它们的**组合方之一**
38
- * (tick + 面板 + agent 定位),不再自持判据副本。
39
- * 提取理由与"三处消费方共用一份判据"的约束见该文件头部注释。
40
- */
41
- /** 审计历史 ring 上限(每 CCC 保留最近 N 条唤起记录) */
42
- export declare const AUDIT_HISTORY_MAX = 50;
43
- /** 唤起消息(四段式:轨迹焦点[CCC 定义,稳定] / 身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
44
- export declare function buildWakeMessage(opts: {
45
- sessionName: string;
46
- mdPath: string;
47
- intervalHours: number;
48
- topPrompt: string | null;
49
- motivation: string | null;
50
- biasContent: string | null;
51
- }): string;
52
- /** 唤起结果(tick 日志 / 面板显示 / 审计记录共用) */
53
- interface WakeResult {
54
- ok: boolean;
55
- detail: string;
56
- }
57
- /** 追加审计记录(ring 截断) */
58
- export declare function recordWake(root: string, rec: WakeRecord): void;
59
- /** 读审计历史(root 的最近记录;无 → []) */
60
- export declare function wakeHistoryFor(root: string): WakeRecord[];
61
- /** 测试辅助:清空审计(生产零调用) */
62
- export declare function resetWakeHistory(): void;
63
- /**
64
- * 执行一次唤起(时钟 tick 与面板「立即唤起」共用):
65
- * - force=false(时钟):完整校验——enabled / 目标命中 / --auto 标志 / 窗口 / 间隔 / 偏见脚本
66
- * - force=true(手动调试,用户在场):跳过窗口/间隔,仍校验 enabled / 目标命中 / --auto / 偏见脚本
67
- * (v1.27.12 移除每日唤起预算——force 无预算可跳,时钟也不受限)
68
- * 成功 → 注入前台会话(agent.steer,用户可见);返回结果供调用方(tick 打日志 / 面板显示 / 审计)。
69
- */
70
- export declare function performAutopilotWake(ctx: Context, root: string, settings: AutopilotTrajectorySettings, opts?: {
71
- force?: boolean;
72
- }): Promise<WakeResult>;
73
- /**
74
- * **周期自唤醒全局闸**(v1.34 更名收窄,原 `autopilotEnabled`):关 = 定时器不武装 + tick 不唤起。
75
- *
76
- * 语义边界(用户 2026-09-15 裁决 S-1):**只管周期自唤醒** —— 一次性唤醒(`container_trajectory wake-later`)
77
- * 归 `wakeSchedulerEnabled`,两者**互不连带**(旧实现共用一闸是缺陷,见 wake-scheduler 注释)。
78
- *
79
- * 读取顺序(迁移期,不回写):`autopilotWakeEnabled`(新键)→ `autopilotEnabled`(旧键)→ false。
80
- * 用 `??` 而非 `||` 的理由:**新键显式 false 必须能覆盖旧键 true**(否则"关不掉")。
81
- * 抽成模块级函数是为了让 {@link autopilotClockState}(诊断)与 tick 读**同一判据**。
82
- *
83
- * v1.34.1(⑥ C6a):**导出**——CCC 工具面(`container_admin autopilot status`)与条件链
84
- * ({@link autopilotRuntimeFacts})必须读**这一份**闸值。旧独立脚本读不到它 ⇒ 闸关着也印
85
- * 「✅ 唤起条件全部满足」(8 项分歧之首,见 SESSION §12.13 与现状稿 §4.4)。
86
- */
87
- export declare function autopilotGloballyEnabled(): boolean;
88
- /**
89
- * autopilot **进程态**类型(诊断用,模块级 = 进程级)。
90
- *
91
- * C6b(P2):字段由 {@link ClockRuntime}(**工厂那份**)提供,**字段名与语义一个字未改**。
92
- * 与 {@link wakeSchedulerState} 的差别只有一项:本钟**没有** `lastTickLog`(结果进下方
93
- * `wakeHistory` 审计 ring,不走 tick 日志)——故 `ClockRuntime.lastTickLog` 在工厂里是可选的。
94
- */
95
- export type AutopilotClockRuntime = ClockRuntime;
96
- declare const clock: import("./clock-runtime.js").Clock;
97
- /**
98
- * autopilot **进程态**快照(只读;`enabled` = 全局闸**当前**值)。
99
- * ⚠️ 与 {@link wakeSchedulerState} 同规格,但**本钟无 `lastTickLog`**(见 {@link ClockOptions.logFrom})。
100
- */
101
- export declare function autopilotClockState(): ReturnType<typeof clock.snapshot>;
102
- /** 测试用:复位进程态(含 per-CCC 重入守卫,避免用例间串味) */
103
- export declare function __resetAutopilotClockStateForTest(): void;
104
- /** 该 CCC 是否有唤起轮正在进行中(tick 的 per-CCC 防重入判据;诊断读同一份) */
105
- export declare function autopilotWakeInFlight(root: string): boolean;
106
- /**
107
- * 装配(index.ts apply 调用)。时钟唤起(v1.26.14 修复 + v1.27.4 多 CCC 独立):
108
- *
109
- * v1.26.14 根因:旧实现启动时一次性解析 root——web 进程启动时 live 会话往往为空 → 定时器不启动。
110
- * 修复:① 每次 tick 动态解析 root+settings ② 监听 session/created 启动定时器 ③ 优先实验 CCC。
111
- *
112
- * **v1.27.4 多 CCC 独立(用户"4个CCC能各自有autotrajectory吗")**:
113
- * - 单定时器保留(TICK_MS 5min),每次 tick **遍历所有 live+enabled CCC**(collectAutopilotCccs)
114
- * 各自评估 shouldWake + 各自唤起——每 CCC 的 interval/session/bias/topPrompt/窗口独立
115
- * - `running` 守卫 **per-CCC**(不同 CCC 唤起互不阻塞)
116
- * - **全局串行化**(wakeChain:同 tick 多 CCC 到点 → 依次唤起,防模型并发挤兑)
117
- *
118
- * 零资源占用语义保留:无 enabled CCC → tick 内直接 return(定时器存在但每 5min 一次空检查,
119
- * unref 不阻塞进程退出)。
120
- */
121
- export declare function registerAutopilot(ctx: Context): void;
122
- /**
123
- * 面板状态(GET /serenity/trajectory 数据源;纯逻辑,可单测)——
124
- * WebUI 设置面板「Autopilot Trajectory」只读区块展示的完整状态:配置摘要 + 目标会话
125
- * 命中/标志/空闲时长 + 当前窗口/可唤起判定 + 审计(最近唤起)。
126
- * 不运行偏见脚本(只报脚本是否就绪——运行验证走 trajectory random)。
127
- */
128
- interface AutopilotTrajectoryStatus {
129
- /** 是否配置了 trajectory.autopilot 段(.opencode/serenity.json;旧键 autopilotTrajectory / autotrajectory 回退) */
130
- configured: boolean;
131
- /** 总开关(缺省 false——未开零资源占用) */
132
- enabled: boolean;
133
- /** 无人类活动 N 小时后自动唤起(缺省 12) */
134
- intervalHours: number;
135
- /** 偏见内容提供者脚本(相对 CCC 根;缺省 autopilot-bias.ts) */
136
- biasProvider: string;
137
- /** 顶层提示词(每次唤起最先注入的稳定指令;未配置 → null) */
138
- topPrompt: string | null;
139
- /** 目标会话(S###/目录名;未配置 = 不唤起) */
140
- session: string | null;
141
- /** 避开唤起的高峰时段(北京时间) */
142
- avoidWakeHours: {
143
- start: number;
144
- end: number;
145
- };
146
- /** 目标会话状态(未配置/未命中 → null) */
147
- target: {
148
- /** 目录名(含 --auto 后缀时为自主形态) */
149
- dirName: string;
150
- /** 目录名是否带 --auto 标志 */
151
- autoFlag: boolean;
152
- /** 距上次轨迹活动(小时) */
153
- idleHours: number;
154
- /** 当前时刻是否满足唤起条件(标志+间隔+窗口,未运行中;v1.27.12 移除每日预算) */
155
- wakeable: boolean;
156
- } | null;
157
- /** 当前北京时间小时 */
158
- beijingHour: number;
159
- /** 当前是否在唤起窗口内(避开高峰之外) */
160
- windowAllowed: boolean;
161
- /** 审计:最近唤起记录(最多 10 条展示;正式版 v1.27.4) */
162
- recentWakes: WakeRecord[];
163
- }
164
- export declare function getAutopilotStatus(root: string): AutopilotTrajectoryStatus;
165
- /** live 会话条目(诊断/面板解析用;标题从 events 读) */
166
- interface LiveSessionEntry {
167
- id: string;
168
- cwd: string | null;
169
- cccRoot: string | null;
170
- title: string | null;
171
- }
172
- /**
173
- * live 会话清单(诊断/面板解析用;标题从 events 读)——**只枚举 live 会话本身**。
174
- *
175
- * C2:CCC 枚举("本机有哪些 CCC")**不在这里**,归 `ccc-roots.listCccs`(并集)。
176
- * 本函数保留的是它**独有**的那部分:逐条 live 会话 + cwd 归属 + 标题 + id
177
- * (诊断页需要"哪个会话属于哪个 CCC",那是会话维度、不是 CCC 维度)。
178
- * @param ctx 插件上下文
179
- * @returns 逐条 live 会话(`cccRoot` = 该会话 cwd 的 CCC 归属,无 → null)
180
- */
181
- export declare function listLiveSessions(ctx: Context): LiveSessionEntry[];
182
- /**
183
- * 收集所有「配置了 Autopilot Trajectory 的 live CCC」——**v1.27.4 多 CCC 独立**:
184
- * 返回 enabled 的 CCC 列表(时钟 tick 遍历目标);无 enabled → []。
185
- * (原 resolveAutoTrajectoryCcc 单目标语义被 collect 取代——面板/诊断用 enableOnly=false 取全量)
186
- */
187
- export declare function collectAutopilotCccs(ctx: Context, opts?: {
188
- includeDisabled?: boolean;
189
- }): string[];
190
- /**
191
- * 进程内诊断(trajectory diag-live 数据源;用户"排查访问不到 pangu 写个 msm")——
192
- * 输出当前实例 live 会话清单(id/cwd/ccc/标题)+ 每个配置了 Autopilot 的 CCC
193
- * 状态(配置摘要/目标命中/可唤起)+ 目标 agent 定位结果。脚本 diag 看不到运行时,
194
- * 本函数在插件进程内运行(能读 sessions/agents)。
195
- */
196
- export interface DiagLiveReport {
197
- processCwd: string;
198
- processCcc: string | null;
199
- liveSessions: LiveSessionEntry[];
200
- autopilotCccs: Array<{
201
- root: string;
202
- enabled: boolean;
203
- session: string | null;
204
- target: AutopilotTrajectoryStatus['target'];
205
- agentResolved: boolean;
206
- agentDiagnosis: string | null;
207
- }>;
208
- panelResolved: string | null;
209
- }
210
- export declare function diagLive(ctx: Context): DiagLiveReport;
211
- /**
212
- * **条件链的运行态事实**(⑥ C6a 的核心出口)——把只有插件进程能看到的判据交给
213
- * 条件链({@link buildWakeChain} 的输入):全局闸 / 重入守卫 / live 运行态。
214
- *
215
- * 为什么必须有(这是 C6 的动机本身):旧独立脚本只能读文件 ⇒ 结构性地看不到这三样,
216
- * 于是逐条件报告**必然**与实际唤起行为分歧(8 项,见 `acc-component-relations-current-state.md` §4.4)。
217
- * 搬进进程内之后,`container_admin autopilot status` 与 `acc-diag` ④ 段读的就是**同一份事实**。
218
- *
219
- * `ctx` 可传 `null`(**不是**"三项全不可知"):全局闸(`readSimpleSettings`)与重入守卫
220
- * (模块级 `runningByRoot`)**都不需要 ctx**,无 ctx 时也应读出真值——只有 live 运行态
221
- * (live 会话 / agent 可解析)依赖 ctx,那才是真正的"不可知"。
222
- * 这条区分由 `autopilot-ops.test.ts` 的全局闸可见性用例逼出来(首版把三者一起降级为不可知,
223
- * 使"闸关"在无 ctx 路径下隐形——正是本轮要消灭的那类假报告)。
224
- *
225
- * @param ctx 插件上下文(读 live 会话 + agent);null = 无进程上下文(live 标不可知)
226
- * @param root 目标 CCC 根
227
- * @returns 条件链事实(`live` 为 null 表示"live 运行态不可知")
228
- */
229
- export declare function autopilotRuntimeFacts(ctx: Context | null, root: string): WakeChainFacts;
230
- export {};
@@ -1,190 +0,0 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-Bd_SjEs7.js";
2
- import { c as getLastActiveSessionInfo, p as sessionEvents, s as getActiveSessionInfo } from "./trajectory-ops-mrXK0H0I.js";
3
- import { dirname, join } from "node:path";
4
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
- //#region src/trajectory-bound.ts
6
- /**
7
- * trajectory-bound.ts — SESSION 绑定持久化(CCC 内 `AGENT_SESSIONS/.bindings.json`)
8
- *
9
- * 目标(S142 用户拍板,方案 v1.0):让「dsh 会话(载体)↔ SESSION(宁静号轨迹)」
10
- * 的绑定坚固——LLM 不能因智力因素(幻觉/理解偏差)在过程中或 container_trajectory rebuild 后
11
- * 自行更换 SESSION。
12
- *
13
- * 存储形态(v1.30.6 起,S142 review F-01):绑定**不再写入 dsh 会话日志**。
14
- * 原因(宿主契约实证):dsp 的自定义事件 `serenity/bound` 既不在宿主的
15
- * `KNOWN_SESSION_EVENT_TYPES`(生成静态集,明确说明 out-of-repo 插件事件不在其中),
16
- * 而 `Session.append` 构造事件时只写 `{type,seq,time,data,surfaceOp?,sourceEventSeqs?}`
17
- * ——**没有写入 envelope `ignorable` 标记的通道**;宿主的 `assertEventsSupported`
18
- * (session-persistence/coordinator.ts:1248-1252,调用点含恢复/HMR 路径)会拒绝任何
19
- * "未知且非 ignorable" 的事件 → 含该事件的会话在冷加载时可能抛
20
- * `SessionFormatUnsupportedError`。v1.29.1~v1.30.5 的设计文档虽写明要带
21
- * `ignorable: true`(docs/session-binding-hardening-research.md:53),但类型层声明
22
- * 不等于持久层标记,实际从未落盘。
23
- *
24
- * 现形态:每 CCC 一个 JSON 文件 `AGENT_SESSIONS/.bindings.json`(原子写:tmp + rename),
25
- * 以 dsh 会话 id 为键,latest-wins 覆盖。**向后兼容**:读取时若文件中无该会话记录,
26
- * 仍回落到扫描会话日志中的旧 `serenity/bound` 事件(老会话的绑定不丢)。
27
- *
28
- * 编码无关(U4):绑定锚 = **完整 AGENT_SESSIONS 目录名**(磁盘唯一存在),
29
- * sessionId(S###/apaas-xxx/自定义)仅是派生展示字段——绝不假设 S 前缀。
30
- */
31
- /** 旧形态事件类型(v1.29.1~v1.30.5 写入会话日志;v1.30.6 起只读不写——兼容存量绑定) */
32
- const SESSION_BOUND_EVENT = "serenity/bound";
33
- /** 绑定文件相对 CCC 根的路径 */
34
- const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
35
- const BINDINGS_VERSION = 1;
36
- function sessionHeader(session) {
37
- const header = session?.header;
38
- if (!header || typeof header !== "object") return null;
39
- return header;
40
- }
41
- /** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
42
- function bindingsPathFor(session) {
43
- const header = sessionHeader(session);
44
- if (typeof header?.cwd !== "string") return null;
45
- const root = cccRootForCwd(header.cwd);
46
- if (!root) return null;
47
- return join(root, BINDINGS_REL_PATH);
48
- }
49
- function readBindingsFile(path) {
50
- if (!existsSync(path)) return {
51
- version: BINDINGS_VERSION,
52
- sessions: {}
53
- };
54
- try {
55
- const sessions = JSON.parse(readFileSync(path, "utf-8")).sessions;
56
- if (!sessions || typeof sessions !== "object") return {
57
- version: BINDINGS_VERSION,
58
- sessions: {}
59
- };
60
- return {
61
- version: BINDINGS_VERSION,
62
- sessions
63
- };
64
- } catch {
65
- return {
66
- version: BINDINGS_VERSION,
67
- sessions: {}
68
- };
69
- }
70
- }
71
- /** 原子写:同目录 tmp + rename(避免半写文件被后续读取) */
72
- function writeBindingsFile(path, data) {
73
- mkdirSync(dirname(path), { recursive: true });
74
- const tmp = `${path}.tmp`;
75
- writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
76
- renameSync(tmp, path);
77
- }
78
- /** 记录形状校验(容忍手改/旧数据) */
79
- function asBoundRecord(value) {
80
- const r = value;
81
- if (!r || typeof r.dirName !== "string" || typeof r.mdPath !== "string") return null;
82
- return r;
83
- }
84
- /** 旧形态:从会话日志扫最后一条 `serenity/bound`(兼容 v1.30.5 及更早写入的绑定) */
85
- function readLegacyBoundFromEvents(session) {
86
- const events = sessionEvents(session);
87
- for (let i = events.length - 1; i >= 0; i--) {
88
- const ev = events[i];
89
- if (!ev || ev.type !== SESSION_BOUND_EVENT) continue;
90
- const rec = asBoundRecord(ev.data);
91
- if (rec) return rec;
92
- }
93
- return null;
94
- }
95
- /**
96
- * 读取会话当前绑定(权威,latest-wins):文件记录优先,无则回落旧事件形态。
97
- * 无绑定返回 null。
98
- */
99
- function readLastBound(session) {
100
- const path = bindingsPathFor(session);
101
- const id = sessionHeader(session)?.id;
102
- if (path && typeof id === "string") {
103
- const rec = asBoundRecord(readBindingsFile(path).sessions[id]);
104
- if (rec) return rec;
105
- }
106
- return readLegacyBoundFromEvents(session);
107
- }
108
- /** 目录名 → 轨迹标签(`--S###--` 命中取 `S###`;未命中原样返回——编码无关 U4) */
109
- function trajectoryLabel(dirName) {
110
- const m = dirName.match(/--S(\d{3,})--/);
111
- return m ? `S${m[1]}` : dirName;
112
- }
113
- /**
114
- * **调用方自身**所属 trajectory 的标签(审计字段 `createdBy` 的唯一正解)。
115
- *
116
- * 三级来源,逐级回退(R↓:为什么不用单一来源):
117
- * 1. `readLastBound(session)` —— **持久 bound**,权威(重启存活、不受别的会话激活污染)
118
- * 2. `getActiveSessionInfo(scope)` —— 本 dsh 会话的**内存**激活(按 scope 隔离)
119
- * 3. `getLastActiveSessionInfo()` —— **全局**最近激活指针(无绑定的会话兼容回退)
120
- *
121
- * ⚠️ 为何单列成本函数(S142 §30.12.1 实测缺陷):唤醒注册表的 `createdBy` 原先直接取第 3 级
122
- * 全局指针,而它记录的是"**进程内最近一次被激活的 trajectory**"——多会话并发时必然张冠李戴
123
- * (实测:S142 登记唤醒被写成 `by=S060`)。`createdBy` 是 D60「以可审计替代入口栅」的**唯一凭证**,
124
- * 失真即替代控制失效 ⇒ 必须以"调用方自己是谁"为准,全局指针只作最后兜底。
125
- *
126
- * @param session 调用方 dsh 会话对象(需 `header.id` + `header.cwd`)
127
- * @param scope 调用方 dsh 会话 id(取不到传空串)
128
- * @returns 轨迹标签(`S###` 或完整目录名);三级全落空 → 空串(渲染为 `?`)
129
- */
130
- function resolveSessionTrajectoryLabel(session, scope) {
131
- const bound = session ? readLastBound(session) : null;
132
- if (bound?.dirName) return trajectoryLabel(bound.dirName);
133
- const scoped = scope !== "" ? getActiveSessionInfo(scope) : null;
134
- if (scoped) return scoped.sessionId;
135
- return getLastActiveSessionInfo()?.sessionId ?? "";
136
- }
137
- /**
138
- * 反向查:绑定到某 trajectory 目录名的 dsh 会话 id(**按绑定时间倒序,最新在前**)。
139
- *
140
- * 用途(wake-scheduler 冷唤醒):.bindings.json 以 **sessionId 为键**,而唤醒条目的
141
- * target 是**目录名**——需要值侧扫描做反向索引。冷会话被唤醒时必须先知道"哪个 dsh
142
- * 会话承载这条轨迹",这是唯一的权威来源(标题只作回退,见 v1.29.2 R2)。
143
- *
144
- * @param root CCC 根
145
- * @param dirName AGENT_SESSIONS 目录名(完整名,如 `2026-09-01--S151--x--auto`)
146
- * @returns 会话 id 列表(最新绑定在前);无绑定 → `[]`
147
- */
148
- function listBoundSessionIds(root, dirName) {
149
- const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
150
- const hits = [];
151
- for (const [id, raw] of Object.entries(file.sessions)) {
152
- const rec = asBoundRecord(raw);
153
- if (!rec || rec.dirName !== dirName) continue;
154
- hits.push({
155
- id,
156
- at: typeof rec.at === "number" ? rec.at : 0
157
- });
158
- }
159
- return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
160
- }
161
- /**
162
- * 写入绑定(latest-wins 覆盖该会话记录)。
163
- *
164
- * 绑定持久化尽力而为:无法定位 CCC 根 / 无会话 id / 写盘失败 → 返回 false,不阻断主流程。
165
- * @param session 目标 dsh 会话(需 `header.id` + `header.cwd` 可定位 CCC 根)
166
- * @param action 绑定动作
167
- * @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
168
- */
169
- function appendBound(session, action, rec) {
170
- const path = bindingsPathFor(session);
171
- const id = sessionHeader(session)?.id;
172
- if (!path || typeof id !== "string") return false;
173
- try {
174
- const file = readBindingsFile(path);
175
- file.sessions[id] = {
176
- dirName: rec.dirName,
177
- mdPath: rec.mdPath,
178
- ...rec.sessionId ? { sessionId: rec.sessionId } : {},
179
- action,
180
- at: Date.now(),
181
- ...rec.note ? { note: rec.note } : {}
182
- };
183
- writeBindingsFile(path, file);
184
- return true;
185
- } catch {
186
- return false;
187
- }
188
- }
189
- //#endregion
190
- export { resolveSessionTrajectoryLabel as i, listBoundSessionIds as n, readLastBound as r, appendBound as t };