@shgroup/dsh-serenity-hooks 1.87.6 → 1.88.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/dsh.plugin.json +1 -1
- package/lib/{ccc-roots-CvgsUnFW.js → ccc-roots-WfktvbN8.js} +1 -1
- package/lib/{cleanup-DxX78__Z.js → cleanup-CPl1SSaK.js} +16 -3
- package/lib/cro/log.d.ts +15 -0
- package/lib/im/channels.d.ts +40 -0
- package/lib/index.js +167 -26
- package/lib/{message-source-Cq4PsuES.js → message-source-BAMwXu6i.js} +3 -325
- package/lib/{profile-DGt_e-Nx.js → profile-zxakCNXy.js} +359 -1
- package/lib/{session-Ca5UYzF3.js → session-Bb4GSSFJ.js} +3 -3
- package/lib/{settings-BI3NRHgC.js → settings-DAorSseV.js} +1 -1
- package/lib/trajectory/cleanup.d.ts +14 -1
- package/lib/trajectory/interrupted.d.ts +34 -3
- package/lib/trajectory/policy/profile.d.ts +24 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { r as cccRootForCwd } from "./ccc-roots-
|
|
1
|
+
import { r as cccRootForCwd } from "./ccc-roots-WfktvbN8.js";
|
|
2
2
|
import { _ as resolveInside, g as readUtf8 } from "./ccc-CyYJL4F2.js";
|
|
3
|
-
import { D as
|
|
3
|
+
import { D as getLastActiveSessionInfo, E as getActiveSessionInfo, I as sessionEvents, L as sessionsRoot, M as parseTrajectoryCode, _ as readBindDeclaration, g as listBindDeclarations, h as isBindUsable, m as claimBinding } from "./profile-zxakCNXy.js";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { existsSync,
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
//#region src/kernel/constants.ts
|
|
8
8
|
/** 常量(纯模块,零 DSH 依赖) */
|
|
@@ -260,328 +260,6 @@ function buildTrajectoryBasePrompt(code) {
|
|
|
260
260
|
].join("\n");
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
|
-
//#region src/trajectory/bind-file.ts
|
|
264
|
-
/**
|
|
265
|
-
* trajectory/bind-file.ts — **当前载体绑定 = 认知轨迹目录里的自我交代**(判决 D172,owner 2026-10-20)
|
|
266
|
-
*
|
|
267
|
-
* ## owner 的原话(这是本模块的全部设计依据)
|
|
268
|
-
*
|
|
269
|
-
* > 「**认知轨迹自己交代永远是最好的交代**」
|
|
270
|
-
* > 「绑定表是没有意义的,绑定关系必须**运行时按照规则探测**,绑定表本质就搞成**第二真相源**了」
|
|
271
|
-
* > 「依赖文件系统,定义一个 `.dsh_session_bind` 后缀(加入 gitignore);每个认知轨迹目录内放一个
|
|
272
|
-
* > `current.dsh_session_bind`,**以这个为绑定真相源**」
|
|
273
|
-
*
|
|
274
|
-
* ## 为什么这是对的(而不是又一张表)
|
|
275
|
-
*
|
|
276
|
-
* 旧的 `.bindings.json` + 宿主存储域是**旁路索引**:它描述的对象(轨迹目录)与它自己**分离**,
|
|
277
|
-
* ⇒ 一旦两者不一致就必须写代码去对账,而**对账失败没有任何机械信号**(本轨反复踩的形状)。
|
|
278
|
-
*
|
|
279
|
-
* 新形态把这条信息**放回它描述的对象的内部**:
|
|
280
|
-
*
|
|
281
|
-
* ```text
|
|
282
|
-
* <轨迹目录>/
|
|
283
|
-
* SESSION.md ← 身体
|
|
284
|
-
* PROMPT.md ← 章程
|
|
285
|
-
* trajectory.json ← 声明(约束面 / uid / cro.host)
|
|
286
|
-
* current.dsh_session_bind ← 🆕 **此刻谁在承载它**(单个文件 ⇒ 结构上不可能有两个)
|
|
287
|
-
* ```
|
|
288
|
-
*
|
|
289
|
-
* ## 🔴 两条不变量由**形态本身**保证,不靠记账
|
|
290
|
-
*
|
|
291
|
-
* | 不变量 | 保证方式 |
|
|
292
|
-
* |---|---|
|
|
293
|
-
* | 一条轨迹**至多一个**当前载体 | 它是**一个文件**,写入即整体覆盖(原子 rename) |
|
|
294
|
-
* | 一个载体**至多一条**轨迹 | 写入前扫全部轨迹的 bind 文件,**该会话 id 已出现在别处 ⇒ 拒绝** |
|
|
295
|
-
*
|
|
296
|
-
* ⚠️ 旧形态靠 `supersededAt` 标记"退掉",而**标记是可以忘的**;覆盖不会忘。
|
|
297
|
-
*
|
|
298
|
-
* ## 零依赖纪律(同 `policy/files.ts`)
|
|
299
|
-
* 只用 `node:fs` / `node:path` ⇒ 谁 import 它都不会成环。每个读函数**永不抛**。
|
|
300
|
-
*
|
|
301
|
-
* ## 🔴 第三条不变量(owner 2026-10-20 追加):「文件里写的 sessionId 可能已经不存在了
|
|
302
|
-
* ——这个情况下就**视同没绑定**,就正确了」
|
|
303
|
-
*
|
|
304
|
-
* 这一条让整个设计**自洽**:**绑定的有效性由「那个会话真实存在」决定,不由「文件存在」决定。**
|
|
305
|
-
*
|
|
306
|
-
* ```text
|
|
307
|
-
* 文件在 + 会话在 ⇒ 有效绑定
|
|
308
|
-
* 文件在 + 会话没了 ⇒ **视同没绑定**(过期文件自动失效,不需要任何人来清理)
|
|
309
|
-
* 文件不在 ⇒ 没绑定
|
|
310
|
-
* ```
|
|
311
|
-
*
|
|
312
|
-
* ⇒ 因此**不需要**任何"清理陈旧绑定"的机制,也不需要 `reconcile` 去对账 —— 对账这件事本身消失。
|
|
313
|
-
* 代价是**每次取用都要验一次会话是否还在**(`ctx.agents.get` 或 `sessionController.resolveAgent`
|
|
314
|
-
* 或宿主会话清单),**这是运行时探测**,正是 owner 要的那种判据。
|
|
315
|
-
*
|
|
316
|
-
* ⚠️ 本模块**只负责读写文件**;"会话是否还在"必须由**调用方**(持有宿主上下文)来验 ——
|
|
317
|
-
* 把那条判断放在这里会迫使本模块拿到 `ctx`,从而破坏零依赖与可单测性。
|
|
318
|
-
* 调用方正确用法见 {@link isBindUsable}。
|
|
319
|
-
*
|
|
320
|
-
* ## 🔴 第四条:**完整生命周期**(owner 2026-10-20 追加:「调用 use 会覆盖掉这个文件,
|
|
321
|
-
* 完整生命周期要实现,请注意」)
|
|
322
|
-
*
|
|
323
|
-
* 文件是**一个槽位**,不是追加日志。⇒ 每次改变归属都是一次**状态迁移**,
|
|
324
|
-
* 四个动作必须**成对**出现,否则槽位会留下无法解释的残留:
|
|
325
|
-
*
|
|
326
|
-
* | 迁移 | 何时 | 必须做什么 |
|
|
327
|
-
* |---|---|---|
|
|
328
|
-
* | **acquire** | 会话首次认领轨迹 | 写槽位(该会话此前不持有任何轨迹) |
|
|
329
|
-
* | **switch** | 会话从轨迹 A **改到** B | 🔴 **先清 A 的槽位,再写 B** —— 只写 B 会让 A 永远指着一个不再属于它的会话 |
|
|
330
|
-
* | **release** | 会话主动解绑 / 会话将死 | **清掉槽位**(不是写空串,是删文件) |
|
|
331
|
-
* | **takeover** | 载体换代(rebuild / 崩溃后重建) | 覆盖写:旧会话退位,新会话入位(**必须显式声明**) |
|
|
332
|
-
*
|
|
333
|
-
* 🔴 **最容易漏的是 switch 的"先清 A"**:旧实现(`.bindings.json`)天然支持"一个会话多条记录",
|
|
334
|
-
* 所以从来没暴露过这个问题;新形态是**单槽位**,不清 A 就会得到
|
|
335
|
-
* 「A 与 B 都说这个会话属于自己」—— 正是 owner 要禁的"一个会话绑两条轨迹",
|
|
336
|
-
* 只不过这次是**残留**而不是**主动绑定**,更难发现。
|
|
337
|
-
*
|
|
338
|
-
* ⇒ 因此**不要**直接调 {@link writeBindDeclaration} 来改归属;一律走
|
|
339
|
-
* {@link transitionBinding}(它把四条迁移的配对关系写死在一处)。
|
|
340
|
-
*/
|
|
341
|
-
/** 绑定文件的固定名(**owner 指定**:`current.dsh_session_bind`) */
|
|
342
|
-
const BIND_FILENAME = "current.dsh_session_bind";
|
|
343
|
-
/**
|
|
344
|
-
* 绑定文件的**族**(认族不认名,D167 通用纪律)。
|
|
345
|
-
*
|
|
346
|
-
* 目前只有一代。留成数组是**为了下一次改名不重演历史**:本仓已在
|
|
347
|
-
* `skiff.json → trajectory-constraint.json → trajectory.json` 上栽过两次
|
|
348
|
-
* (上游改名、下游写死旧名 ⇒ 症状与真因在读数上完全不像)。
|
|
349
|
-
*/
|
|
350
|
-
const BIND_FILENAME_FAMILY = [BIND_FILENAME];
|
|
351
|
-
/** 轨迹归档区的目录名(**自持一份,不从 `core.ts` import**)。 */
|
|
352
|
-
const ARCHIVED_DIR_NAME = "_archived";
|
|
353
|
-
/** 校验一条声明(容忍手改 / 部分字段缺失) */
|
|
354
|
-
function asDeclaration(value) {
|
|
355
|
-
const d = value;
|
|
356
|
-
if (!d || typeof d !== "object") return null;
|
|
357
|
-
if (typeof d.sessionId !== "string" || d.sessionId === "") return null;
|
|
358
|
-
return {
|
|
359
|
-
sessionId: d.sessionId,
|
|
360
|
-
at: typeof d.at === "number" ? d.at : 0,
|
|
361
|
-
action: typeof d.action === "string" && d.action !== "" ? d.action : "activate",
|
|
362
|
-
...typeof d.note === "string" && d.note !== "" ? { note: d.note } : {},
|
|
363
|
-
...d.createdAsUser === true ? { createdAsUser: true } : {}
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
/** 某轨迹目录里绑定文件的绝对路径 */
|
|
367
|
-
function bindFilePath(trajectoryDir) {
|
|
368
|
-
return join(trajectoryDir, BIND_FILENAME);
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* 读一条轨迹的**当前载体声明**。
|
|
372
|
-
*
|
|
373
|
-
* @param trajectoryDir 轨迹目录绝对路径
|
|
374
|
-
* @returns 声明;文件不存在 / 坏 JSON / 形状不符 ⇒ `null`(**永不抛**)
|
|
375
|
-
*/
|
|
376
|
-
function readBindDeclaration(trajectoryDir) {
|
|
377
|
-
for (const name of BIND_FILENAME_FAMILY) {
|
|
378
|
-
const p = join(trajectoryDir, name);
|
|
379
|
-
try {
|
|
380
|
-
if (!existsSync(p)) continue;
|
|
381
|
-
return asDeclaration(JSON.parse(readFileSync(p, "utf-8")));
|
|
382
|
-
} catch {}
|
|
383
|
-
}
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* 写一条轨迹的**当前载体声明**(原子:同目录 tmp + rename)。
|
|
388
|
-
*
|
|
389
|
-
* 🔴 **不做唯一性检查** —— 那是 `claimBinding` 的职责。本函数只负责"这份文件长成什么样"。
|
|
390
|
-
* 分开的理由:唯一性检查需要**扫全部轨迹**,而写只需要一个目录 ⇒ 让不扫盘的地方能便宜地写。
|
|
391
|
-
*/
|
|
392
|
-
function writeBindDeclaration(trajectoryDir, decl) {
|
|
393
|
-
const path = bindFilePath(trajectoryDir);
|
|
394
|
-
const tmp = `${path}.tmp`;
|
|
395
|
-
try {
|
|
396
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
397
|
-
writeFileSync(tmp, `${JSON.stringify(decl, null, 2)}\n`, "utf-8");
|
|
398
|
-
renameSync(tmp, path);
|
|
399
|
-
return true;
|
|
400
|
-
} catch {
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* 清掉一条轨迹的当前载体声明(解绑)。
|
|
406
|
-
* @returns 文件原本存在且已删除 ⇒ true
|
|
407
|
-
*/
|
|
408
|
-
function clearBindDeclaration(trajectoryDir) {
|
|
409
|
-
try {
|
|
410
|
-
const path = bindFilePath(trajectoryDir);
|
|
411
|
-
if (!existsSync(path)) return false;
|
|
412
|
-
renameSync(path, `${path}.del`);
|
|
413
|
-
return true;
|
|
414
|
-
} catch {
|
|
415
|
-
return false;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* 列出某 CCC 下**全部轨迹的当前载体声明**(`dirName → 声明`)。
|
|
420
|
-
*
|
|
421
|
-
* 这是**反向查询**(会话 → 轨迹 / 轨迹 → 会话)的唯一入口,取代旧的旁路表扫描。
|
|
422
|
-
* 跳过 `_` / `.` 前缀目录(归档区与运行时区不是轨迹,判据同 `activeTrajectoryDirNames`)。
|
|
423
|
-
*
|
|
424
|
-
* 🔴 **2026-10-20(owner 裁决「死包含归档判断」配套):`_archived/` 必须**再扫一层**。**
|
|
425
|
-
*
|
|
426
|
-
* 修之前:归档目录整体被跳过 ⇒ **已归档轨迹的绑定声明对唯一性检查完全隐形**。
|
|
427
|
-
* 后果:承载过它的 dsh 会话看起来"无归属",可以被 `use` 到另一条轨迹上 ——
|
|
428
|
-
* 这正是 IMMUTABLE 1「一个会话终身只属于一条轨迹」要禁的事,而且是**静默**发生的。
|
|
429
|
-
*
|
|
430
|
-
* 为什么只扫归档区、不扫别的 `_` 目录:归档区里的**确实是轨迹**(只是不再活跃),
|
|
431
|
-
* 而 `_轨迹约束-logs` / `_weixin-logs` / `_corpus` 是日志与数据区,它们里面不会有
|
|
432
|
-
* `current.dsh_session_bind`(真有一个也只是垃圾,读到会被 `asDeclaration` 挡掉)。
|
|
433
|
-
* ⇒ 只多付一次 `_archived` 的 readdir,不改变"活跃清单"的语义(那是 `readAllSessions` 的事)。
|
|
434
|
-
*
|
|
435
|
-
* @param sessionsRoot `<CCC>/AGENT_SESSIONS`
|
|
436
|
-
* @returns 有交代的轨迹;**永不抛**(目录读不到 ⇒ 空表)
|
|
437
|
-
*/
|
|
438
|
-
function listBindDeclarations(sessionsRoot) {
|
|
439
|
-
const out = [];
|
|
440
|
-
const collect = (dir, into) => {
|
|
441
|
-
let names;
|
|
442
|
-
try {
|
|
443
|
-
names = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name);
|
|
444
|
-
} catch {
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
for (const dirName of names) {
|
|
448
|
-
const decl = readBindDeclaration(join(dir, dirName));
|
|
449
|
-
if (decl) into(dirName, decl);
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
collect(sessionsRoot, (dirName, decl) => out.push({
|
|
453
|
-
dirName,
|
|
454
|
-
decl
|
|
455
|
-
}));
|
|
456
|
-
collect(join(sessionsRoot, ARCHIVED_DIR_NAME), (dirName, decl) => {
|
|
457
|
-
if (!out.some((r) => r.dirName === dirName)) out.push({
|
|
458
|
-
dirName,
|
|
459
|
-
decl
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
return out;
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* **这条绑定还作数吗**(第三条不变量的机械形态)。
|
|
466
|
-
*
|
|
467
|
-
* 判据(owner 原话:「文件里写的 sessionId 可能已经不存在了,这个情况下就视同没绑定」):
|
|
468
|
-
*
|
|
469
|
-
* | 文件 | 会话存在? | 结论 |
|
|
470
|
-
* |---|---|---|
|
|
471
|
-
* | 无 | — | `absent` |
|
|
472
|
-
* | 有 | 是 | **`usable`** |
|
|
473
|
-
* | 有 | 否 | **`stale`** ⇒ **视同没绑定** |
|
|
474
|
-
*
|
|
475
|
-
* 🔴 **为什么"会话存在"必须由调用方给**:判断它要问宿主(`ctx.agents.get` /
|
|
476
|
-
* `sessionController.resolveAgent` / 会话清单),而本模块是零依赖层。
|
|
477
|
-
* ⇒ 这里只收一个 **`alive: (sessionId) => boolean`** 探针,把判断留在外面,本函数仍是纯逻辑、可单测。
|
|
478
|
-
*
|
|
479
|
-
* ⚠️ **`stale` 不删文件**:删除是写操作,而"视同没绑定"已经足够。
|
|
480
|
-
* 留着过期文件反而有用 —— 它记录了"这条轨迹上一次由谁承载"(沿革不丢,同旧 `supersededAt` 的意图)。
|
|
481
|
-
*
|
|
482
|
-
* @param decl {@link readBindDeclaration} 的结果
|
|
483
|
-
* @param alive 探针:该会话 id 此刻是否真实存在(**由调用方用宿主事实实现**)
|
|
484
|
-
* @returns `absent` / `usable` / `stale`,以及(usable 时的)声明
|
|
485
|
-
*/
|
|
486
|
-
function isBindUsable(decl, alive) {
|
|
487
|
-
if (decl === null) return { state: "absent" };
|
|
488
|
-
try {
|
|
489
|
-
return alive(decl.sessionId) ? {
|
|
490
|
-
state: "usable",
|
|
491
|
-
decl
|
|
492
|
-
} : {
|
|
493
|
-
state: "stale",
|
|
494
|
-
decl
|
|
495
|
-
};
|
|
496
|
-
} catch {
|
|
497
|
-
return {
|
|
498
|
-
state: "stale",
|
|
499
|
-
decl
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* 找出**已经占用**某个 dsh 会话 id 的那条轨迹(唯一性判据)。
|
|
505
|
-
*
|
|
506
|
-
* @param sessionsRoot `<CCC>/AGENT_SESSIONS`
|
|
507
|
-
* @param sessionId 待检查的会话 id
|
|
508
|
-
* @returns 占用它的轨迹目录名;无人占用 ⇒ `null`
|
|
509
|
-
*/
|
|
510
|
-
function findTrajectoryHolding(sessionsRoot, sessionId) {
|
|
511
|
-
for (const { dirName, decl } of listBindDeclarations(sessionsRoot)) if (decl.sessionId === sessionId) return dirName;
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* **认领**:把某条轨迹的当前载体设成 `sessionId`,并**当场执行两条不变量**。
|
|
516
|
-
*
|
|
517
|
-
* 这是全仓**唯一**允许改变绑定关系的写入口:`appendBound` 与迁移脚本都走这里。
|
|
518
|
-
* 集中成一处的理由 = 不变量只能有一个实现,否则迟早出现"某条路径忘了检查"(本仓已多次踩)。
|
|
519
|
-
*
|
|
520
|
-
* ## 判据(任一不成立即**拒绝**,不写)
|
|
521
|
-
*
|
|
522
|
-
* 1. **载体唯一**:`sessionId` 已安家在**另一条**轨迹 ⇒ 拒绝(一个会话不能绑两条轨迹)。
|
|
523
|
-
* ⚠️ 若它已安家在**本条**轨迹 ⇒ 幂等通过(重复 `use` 同一条轨迹是合法重入)。
|
|
524
|
-
* 2. **轨迹唯一**:本条轨迹已由**另一个** `sessionId` 承载 ⇒ **拒绝**,除非 `takeover`。
|
|
525
|
-
* `takeover` = 载体换代(`rebuild` / 崩溃后新建),由调用方**显式**声明;
|
|
526
|
-
* 绝不默认允许 —— 静默换代正是"一条轨迹长出两个载体"的成因。
|
|
527
|
-
*
|
|
528
|
-
* 🔴 **2026-10-20(owner 裁决,S196 实测逼出):第 ② 条只在"旧载体还活着"时成立。**
|
|
529
|
-
*
|
|
530
|
-
* 修之前:`claimBinding` **完全不问存活**,只要文件里有别的 `sessionId` 就拒。
|
|
531
|
-
* 而读侧(`listBoundSessionIds(…, alive)`)却按 IMMUTABLE 3 把过期声明当"没绑定"
|
|
532
|
-
* ⇒ **同一条轨迹,两个函数给出相反的答案**。S196 就是撞上这一格:
|
|
533
|
-
* 它的绑定文件指着 `session-52d680ae`(**已在宿主归档集里** = 死),
|
|
534
|
-
* 而 `use` 被 `TRAJECTORY_OCCUPIED` 拒掉,回执还说"成功"(另有一条 D186 的静默问题)。
|
|
535
|
-
*
|
|
536
|
-
* ⇒ 判据(第三条不变量的**写侧形态**):**旧载体会死 ⇒ 本条轨迹算空槽,直接写**。
|
|
537
|
-
* `alive` 不传时**沿用"文件在就算占用"**的旧语义(保守、向后兼容)——
|
|
538
|
-
* ⚠️ 那是"读不到宿主事实"的降级,**不是**"它一定活着";
|
|
539
|
-
* 生产调用点必须显式传 {@link hostAliveProbe}。
|
|
540
|
-
*
|
|
541
|
-
* @param sessionsRoot `<CCC>/AGENT_SESSIONS`
|
|
542
|
-
* @param dirName 目标轨迹目录名
|
|
543
|
-
* @param decl 要写入的声明
|
|
544
|
-
* @param opts.takeover 允许顶掉本条轨迹现有的另一个载体
|
|
545
|
-
* @param opts.alive 探针:**旧载体**是否还活着(缺省 ⇒ 一律当活 = 旧语义)
|
|
546
|
-
* @returns `{ ok: true, replaced?, released? }` 或 `{ ok: false, code, detail }`(**不抛**,由调用方决定如何呈现)
|
|
547
|
-
*
|
|
548
|
-
* 🔴 **2026-10-20 追加 `replaced`(由一条 skiff 轨迹的实测逼出)**:`released` 只覆盖
|
|
549
|
-
* 「这个会话此前安家在**另一条**轨迹」那一格(跨轨迹清理);而**同一轨迹换载体**时被顶掉的那个
|
|
550
|
-
* 会话 id 此前**没有任何出口** ⇒ 调用方无法在回执里说出"你顶掉了谁"。
|
|
551
|
-
* 静默换代正是"一条轨迹感觉有两个载体"的成因 ⇒ 顶掉谁必须能被读出来。
|
|
552
|
-
*/
|
|
553
|
-
function claimBinding(sessionsRoot, dirName, decl, opts = {}) {
|
|
554
|
-
const holder = findTrajectoryHolding(sessionsRoot, decl.sessionId);
|
|
555
|
-
if (holder !== null && holder !== dirName) return {
|
|
556
|
-
ok: false,
|
|
557
|
-
code: "SESSION_TAKEN",
|
|
558
|
-
detail: `会话 ${decl.sessionId} 已经承载轨迹「${holder}」,不能同时再承载「${dirName}」。一个会话只能属于一条认知轨迹 —— 要给「${dirName}」开一个载体,请新建一条会话。`
|
|
559
|
-
};
|
|
560
|
-
const cur = readBindDeclaration(join(sessionsRoot, dirName));
|
|
561
|
-
if (cur !== null && cur.sessionId !== decl.sessionId && opts.takeover !== true) {
|
|
562
|
-
if (opts.alive === void 0 ? true : opts.alive(cur.sessionId)) return {
|
|
563
|
-
ok: false,
|
|
564
|
-
code: "TRAJECTORY_OCCUPIED",
|
|
565
|
-
detail: `轨迹「${dirName}」当前已由会话 ${cur.sessionId} 承载(${cur.action},${new Date(cur.at).toISOString()})。如需换代(rebuild / 重建载体),调用方必须显式声明 takeover —— 静默换代会让一条轨迹出现两个载体。`
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
let released;
|
|
569
|
-
if (holder !== null && holder !== dirName) {
|
|
570
|
-
if (clearBindDeclaration(join(sessionsRoot, holder))) released = holder;
|
|
571
|
-
}
|
|
572
|
-
if (!writeBindDeclaration(join(sessionsRoot, dirName), decl)) return {
|
|
573
|
-
ok: false,
|
|
574
|
-
code: "WRITE_FAILED",
|
|
575
|
-
detail: `写 ${BIND_FILENAME} 失败:${join(sessionsRoot, dirName)}`
|
|
576
|
-
};
|
|
577
|
-
const replaced = cur !== null && cur.sessionId !== decl.sessionId ? cur.sessionId : void 0;
|
|
578
|
-
return {
|
|
579
|
-
ok: true,
|
|
580
|
-
...replaced === void 0 ? {} : { replaced },
|
|
581
|
-
...released === void 0 ? {} : { released }
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
//#endregion
|
|
585
263
|
//#region src/trajectory/bound.ts
|
|
586
264
|
/**
|
|
587
265
|
* trajectory/bound.ts — 「会话 ↔ 认知轨迹」绑定(**2026-10-20 重做:归属 = 轨迹自己的交代**)
|