@shgroup/dsh-serenity-hooks 1.39.2 → 1.40.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/host/storage-domain.d.ts +115 -0
- package/lib/index.js +191 -4
- package/lib/{session-cleanup-B6IHZgww.js → session-cleanup-BtkSmry8.js} +64 -1
- package/lib/session-cleanup.d.ts +35 -0
- package/lib/{trajectory-bound-OyuNkn6e.js → trajectory-bound-CQTh86vq.js} +242 -14
- package/lib/trajectory-bound.d.ts +78 -1
- package/lib/{wake-scheduler-D0HDjEO8.js → wake-scheduler-UTh4t14k.js} +12 -3
- package/lib/wake-scheduler.d.ts +15 -1
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag;后两个按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2(deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host/storage-domain.ts — 宿主存储域上的「会话 ↔ 轨迹」绑定表(§0L,S142 2026-09-19)
|
|
3
|
+
*
|
|
4
|
+
* 为什么存在(R↓):绑定的载体原本是 CCC 内的 `AGENT_SESSIONS/.bindings.json`。
|
|
5
|
+
* owner 裁定(「放 CCC 不合适」)⇒ 改存**宿主自己的存储域**
|
|
6
|
+
* (`~/.dsh/storages/`,与宿主 `workspace.json` 同处、同类结构、同一套校验)。
|
|
7
|
+
*
|
|
8
|
+
* 能力与边界:
|
|
9
|
+
* - `openBindingDomain(ctx)`:从插件 ctx 取 `storageDomain`(**经 `hostService` = `ctx.get`**,
|
|
10
|
+
* **不是**属性直读——dsp 的 `inject` 不含它,属性直读在真实 cordis 下抛
|
|
11
|
+
* `cannot get property "storageDomain" without inject`,同 v1.31.4 的 subagents 陷阱);
|
|
12
|
+
* 开域成功返回句柄,任何环节失败返回 `null`(**本模块永不抛错**:宿主对插件 apply 抛错
|
|
13
|
+
* = 整个 dsh 启动失败,见 `host/access.ts` 的同类契约)。
|
|
14
|
+
* - 句柄的**读是同步的**(`get` / `entries` / `keys` / `size` 走内存),**写才是异步的**
|
|
15
|
+
* (`put` / `delete` 等落盘后 resolve)——**这是 §0L 能"向前兼容"的技术前提**:
|
|
16
|
+
* 既有 10 个调用点全是同步签名,故调用方拿到句柄后读法完全不变。
|
|
17
|
+
*
|
|
18
|
+
* 域名约束(实测,`tests/host/storage-domain.test.ts` 钉住):
|
|
19
|
+
* `UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/` ⇒ **`serenity_bindings`(下划线)**;
|
|
20
|
+
* 连字符形如 `serenity-bindings` **非法**(会在 `defineDomain` 模块加载期抛错)。
|
|
21
|
+
*
|
|
22
|
+
* 记录形状:与旧 `.bindings.json` **同名字段**(`dirName` / `mdPath` / `sessionId` / `action`
|
|
23
|
+
* / `at` / `note` / `supersededAt`)——同形可显著降低迁移风险,也让"双读兜底"易于对账。
|
|
24
|
+
* 硬锚仍是 **`dirName`**(完整目录名,U4:绝不解析编号前缀)。
|
|
25
|
+
*/
|
|
26
|
+
/** 域名称(下划线;连字符非法——见文件头) */
|
|
27
|
+
export declare const BINDING_DOMAIN_NAME = "serenity_bindings";
|
|
28
|
+
/** 表名 */
|
|
29
|
+
export declare const BINDING_TABLE_NAME = "bindings";
|
|
30
|
+
/** 域格式版本(`DomainSpec.version`;非负整数) */
|
|
31
|
+
export declare const BINDING_DOMAIN_VERSION = 1;
|
|
32
|
+
/**
|
|
33
|
+
* 一条绑定记录。字段与旧 `.bindings.json` 的 `SessionBoundRecord` **同名同义**
|
|
34
|
+
* (刻意不重命名:迁移对账不需要映射表,降低出错面)。
|
|
35
|
+
*/
|
|
36
|
+
export interface BindingRecord {
|
|
37
|
+
/** 🔴 硬锚:完整 `AGENT_SESSIONS` 目录名(不解析编号格式,U4) */
|
|
38
|
+
dirName: string;
|
|
39
|
+
/** `SESSION.md` 绝对路径(展示/定位用;**不作锚**——绝对路径绑死机器) */
|
|
40
|
+
mdPath: string;
|
|
41
|
+
/** 展示码(`S###` 或任意 CCC 自定义格式;**仅展示**,绝不用于识别) */
|
|
42
|
+
sessionId?: string;
|
|
43
|
+
action: string;
|
|
44
|
+
at: number;
|
|
45
|
+
note?: string;
|
|
46
|
+
/** 已被取代(D69:同轨迹多会话只留一条;记录保留、退出"当前") */
|
|
47
|
+
supersededAt?: number;
|
|
48
|
+
}
|
|
49
|
+
/** 域内的表句柄(宿主 `KvTable` 的**结构子集**——只取本模块用到的成员,便于替身注入) */
|
|
50
|
+
export interface BindingTableLike {
|
|
51
|
+
/** 同步读一条(内存) */
|
|
52
|
+
get(key: string): BindingRecord | undefined;
|
|
53
|
+
/** 同步快照遍历 */
|
|
54
|
+
entries(): IterableIterator<[string, BindingRecord]>;
|
|
55
|
+
/** 当前条数 */
|
|
56
|
+
readonly size: number;
|
|
57
|
+
/** 异步落盘写一条 */
|
|
58
|
+
put(key: string, value: BindingRecord): Promise<void>;
|
|
59
|
+
/** 异步落盘删一条 */
|
|
60
|
+
delete(key: string): Promise<boolean>;
|
|
61
|
+
}
|
|
62
|
+
/** 域句柄(宿主 `Domain` 的结构子集) */
|
|
63
|
+
export interface BindingDomainLike {
|
|
64
|
+
/** 取表句柄(重复调用返回同一实例) */
|
|
65
|
+
table(name: string): BindingTableLike;
|
|
66
|
+
}
|
|
67
|
+
/** `storageDomain` 设施的结构子集(宿主 `DomainFacility`) */
|
|
68
|
+
export interface DomainFacilityLike {
|
|
69
|
+
open(spec: unknown): Promise<BindingDomainLike>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 构造域规格(宿主 `defineDomain` 的入参形状)。
|
|
73
|
+
*
|
|
74
|
+
* 为什么不引 `@deepseek-ai/dsh-storage-domain` 的 `defineDomain`:
|
|
75
|
+
* 它是**宿主 peer 依赖**(本仓 peer-only 打包,不装它)⇒ 直接 import 会让插件在
|
|
76
|
+
* 未装该包的运行时**模块加载即失败**。域规格是**纯数据**,手写等价对象即可
|
|
77
|
+
* (`tests/host/storage-domain.test.ts` 用真实 `defineDomain` 验证本对象被接受)。
|
|
78
|
+
*/
|
|
79
|
+
export declare function bindingDomainSpec(): unknown;
|
|
80
|
+
/**
|
|
81
|
+
* 从插件 ctx 打开绑定域。**永不抛错**;不可用一律返回 `null`(调用方决定降级)。
|
|
82
|
+
*
|
|
83
|
+
* 失败面(全部返回 `null`,调用方回落旧 `.bindings.json`):
|
|
84
|
+
* - 宿主未提供 `storageDomain`(未装载 storage-domain 插件 / 老宿主);
|
|
85
|
+
* - `open` 抛错(域名非法 / 后端缺失 `backend-not-found` / 版本不符 / 记录损坏);
|
|
86
|
+
* - 拿到的服务形状不对(无 `open` 函数)。
|
|
87
|
+
*
|
|
88
|
+
* @param ctx 插件上下文(真实 cordis Context;测试可传结构化替身)
|
|
89
|
+
* @returns 域句柄,或 `null`
|
|
90
|
+
*/
|
|
91
|
+
export declare function openBindingDomain(ctx: unknown): Promise<BindingDomainLike | null>;
|
|
92
|
+
/**
|
|
93
|
+
* 绑定表的**同步读**封装(句柄缺失时全部返回空,语义 = "域里没有")。
|
|
94
|
+
*
|
|
95
|
+
* 为什么单独包一层(而不是让调用方直接拿 table):
|
|
96
|
+
* ① 句柄可能为 `null`(域不可用)⇒ 让 `null` 检查集中在一处;
|
|
97
|
+
* ② 读侧要统一做形状容忍(`asBindingRecord`)。
|
|
98
|
+
*/
|
|
99
|
+
export declare class BindingStore {
|
|
100
|
+
private readonly domain;
|
|
101
|
+
constructor(domain: BindingDomainLike | null);
|
|
102
|
+
/** 域是否可用(`false` ⇒ 调用方应回落旧文件) */
|
|
103
|
+
get available(): boolean;
|
|
104
|
+
private table;
|
|
105
|
+
/** 同步读一条(域不可用 → `undefined`) */
|
|
106
|
+
get(sessionId: string): BindingRecord | undefined;
|
|
107
|
+
/** 同步遍历全部 `[会话id, 记录]`(域不可用 → 空) */
|
|
108
|
+
entries(): Array<[string, BindingRecord]>;
|
|
109
|
+
/** 异步写一条(域不可用 / 落盘失败 → `false`;**不抛**) */
|
|
110
|
+
put(sessionId: string, rec: BindingRecord): Promise<boolean>;
|
|
111
|
+
/** 异步删一条(域不可用 / 失败 → `false`;**不抛**) */
|
|
112
|
+
delete(sessionId: string): Promise<boolean>;
|
|
113
|
+
}
|
|
114
|
+
/** 由域句柄造 store(`null` 句柄 → 不可用 store,读全空、写全 false) */
|
|
115
|
+
export declare function bindingStore(domain: BindingDomainLike | null): BindingStore;
|
package/lib/index.js
CHANGED
|
@@ -2,11 +2,12 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
|
2
2
|
import { a as hostSettings, c as hostWebServer, i as hostSessions, n as hostInjected, o as hostSubagents, r as hostService, s as hostWeb, t as hostAgents } from "./access-fiehjxV6.js";
|
|
3
3
|
import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$1, f as readExclusiveTools, h as resolveInside, i as findGitRoot, l as pathInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readHandymanConfig, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-NlLr_sxy.js";
|
|
4
4
|
import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-Bd_SjEs7.js";
|
|
5
|
-
import { C as localFileStamp,
|
|
5
|
+
import { C as showSession, D as localFileStamp, E as isoLocal, S as setActiveSessionInfo, T as useSession, _ as parseSessionContextFromEvents, a as readLastBound, b as sessionEvents$1, c as supersedeOtherBindings, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as pruneMissingBindings, l as DEFAULT_SESSION_SCOPE, m as findSession, o as resolveSessionTrajectoryLabel, p as extractSessionMdPathFromText, r as migrateBindingsToDomain, s as setBindingStore, t as appendBound, u as TRAJECTORY_ACTIONS, v as readActiveSessionMd, w as summarize, x as sessionsRoot, y as resolveSessionByTitle } from "./trajectory-bound-CQTh86vq.js";
|
|
6
6
|
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-B2ajFypI.js";
|
|
7
7
|
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-CEHL5cek.js";
|
|
8
8
|
import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-FTjoWQTQ.js";
|
|
9
|
-
import {
|
|
9
|
+
import { r as sessionsRootDir, t as hasSessionLogById } from "./session-cleanup-BtkSmry8.js";
|
|
10
|
+
import { a as WAKE_CATCH_UP_MS, i as registerDisposer, n as sendToTrajectory, o as addWake, r as wakeSchedulerState, s as listWakes, t as registerWakeScheduler } from "./wake-scheduler-UTh4t14k.js";
|
|
10
11
|
import { a as FACE_PORTS, c as MECHANISM_PORTS, i as ACP_HTTP_PORT, l as SKIFF_DEBUG_PORT, n as registerSettingsSection, o as GATEWAY_PORT, s as MAIN_WEB_PORT, t as readSimpleSettings, u as WEIXIN_SEND_PORT } from "./settings-section-DMDUoQum.js";
|
|
11
12
|
import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-CtFYm53S.js";
|
|
12
13
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -3285,6 +3286,13 @@ function createTrajectoryTool(ctx) {
|
|
|
3285
3286
|
context: active.context
|
|
3286
3287
|
};
|
|
3287
3288
|
if (advisory) out.advisory = advisory;
|
|
3289
|
+
if (dsh) {
|
|
3290
|
+
const selfId = String(dsh.header?.id ?? "");
|
|
3291
|
+
const superseded = supersedeOtherBindings(root, info?.dirName ?? targetDirName, selfId === "" ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([selfId]));
|
|
3292
|
+
if (superseded.length > 0) out.supersededBindings = superseded;
|
|
3293
|
+
const pruned = pruneMissingBindings(root, (id) => !hasSessionLogById(sessionsRootDir(), id));
|
|
3294
|
+
if (pruned.length > 0) out.prunedBindings = pruned;
|
|
3295
|
+
}
|
|
3288
3296
|
return out;
|
|
3289
3297
|
}
|
|
3290
3298
|
case "rebuild": {
|
|
@@ -3340,7 +3348,7 @@ function createTrajectoryTool(ctx) {
|
|
|
3340
3348
|
"✓ 已即时投递",
|
|
3341
3349
|
` · ${res.detail}`,
|
|
3342
3350
|
" · 语义:**不排队** —— 目标正在跑轮次则**当场注入当前轮**(steer),空闲则立即起一轮(followup)。",
|
|
3343
|
-
" · ⚠️
|
|
3351
|
+
" · ⚠️ 回执只到「已注入 / 已起轮」——**不表示**目标已执行或已答复。判\"目标真的动了\"须看它自己的 SESSION.md(文件级判据)。"
|
|
3344
3352
|
].join("\n")
|
|
3345
3353
|
};
|
|
3346
3354
|
}
|
|
@@ -7029,7 +7037,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7029
7037
|
const id = s.id ?? s.header?.id;
|
|
7030
7038
|
if (id) liveIds.add(id);
|
|
7031
7039
|
}
|
|
7032
|
-
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./session-cleanup-
|
|
7040
|
+
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./session-cleanup-BtkSmry8.js").then((n) => n.n);
|
|
7033
7041
|
const root = sessionsRootDir();
|
|
7034
7042
|
if (req.method === "GET") {
|
|
7035
7043
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
@@ -7512,6 +7520,17 @@ const HOST_SERVICES = [
|
|
|
7512
7520
|
}],
|
|
7513
7521
|
impact: "双端口网关无法换取 dsh cookie → 外部反代 401",
|
|
7514
7522
|
required: false
|
|
7523
|
+
},
|
|
7524
|
+
{
|
|
7525
|
+
id: "storageDomain",
|
|
7526
|
+
name: "storageDomain",
|
|
7527
|
+
access: "lazy",
|
|
7528
|
+
members: [{
|
|
7529
|
+
name: "open",
|
|
7530
|
+
kind: "function"
|
|
7531
|
+
}],
|
|
7532
|
+
impact: "轨迹绑定无处可存 → §0L 整体不可用(须退回\"写会话日志\"备选方案)",
|
|
7533
|
+
required: false
|
|
7515
7534
|
}
|
|
7516
7535
|
];
|
|
7517
7536
|
/** dsp 被验证过的宿主版本范围(与 package.json peerDependencies 同源,单一真相源) */
|
|
@@ -14436,6 +14455,161 @@ function createAccDiagTool(ctx) {
|
|
|
14436
14455
|
});
|
|
14437
14456
|
}
|
|
14438
14457
|
//#endregion
|
|
14458
|
+
//#region src/host/storage-domain.ts
|
|
14459
|
+
/**
|
|
14460
|
+
* host/storage-domain.ts — 宿主存储域上的「会话 ↔ 轨迹」绑定表(§0L,S142 2026-09-19)
|
|
14461
|
+
*
|
|
14462
|
+
* 为什么存在(R↓):绑定的载体原本是 CCC 内的 `AGENT_SESSIONS/.bindings.json`。
|
|
14463
|
+
* owner 裁定(「放 CCC 不合适」)⇒ 改存**宿主自己的存储域**
|
|
14464
|
+
* (`~/.dsh/storages/`,与宿主 `workspace.json` 同处、同类结构、同一套校验)。
|
|
14465
|
+
*
|
|
14466
|
+
* 能力与边界:
|
|
14467
|
+
* - `openBindingDomain(ctx)`:从插件 ctx 取 `storageDomain`(**经 `hostService` = `ctx.get`**,
|
|
14468
|
+
* **不是**属性直读——dsp 的 `inject` 不含它,属性直读在真实 cordis 下抛
|
|
14469
|
+
* `cannot get property "storageDomain" without inject`,同 v1.31.4 的 subagents 陷阱);
|
|
14470
|
+
* 开域成功返回句柄,任何环节失败返回 `null`(**本模块永不抛错**:宿主对插件 apply 抛错
|
|
14471
|
+
* = 整个 dsh 启动失败,见 `host/access.ts` 的同类契约)。
|
|
14472
|
+
* - 句柄的**读是同步的**(`get` / `entries` / `keys` / `size` 走内存),**写才是异步的**
|
|
14473
|
+
* (`put` / `delete` 等落盘后 resolve)——**这是 §0L 能"向前兼容"的技术前提**:
|
|
14474
|
+
* 既有 10 个调用点全是同步签名,故调用方拿到句柄后读法完全不变。
|
|
14475
|
+
*
|
|
14476
|
+
* 域名约束(实测,`tests/host/storage-domain.test.ts` 钉住):
|
|
14477
|
+
* `UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/` ⇒ **`serenity_bindings`(下划线)**;
|
|
14478
|
+
* 连字符形如 `serenity-bindings` **非法**(会在 `defineDomain` 模块加载期抛错)。
|
|
14479
|
+
*
|
|
14480
|
+
* 记录形状:与旧 `.bindings.json` **同名字段**(`dirName` / `mdPath` / `sessionId` / `action`
|
|
14481
|
+
* / `at` / `note` / `supersededAt`)——同形可显著降低迁移风险,也让"双读兜底"易于对账。
|
|
14482
|
+
* 硬锚仍是 **`dirName`**(完整目录名,U4:绝不解析编号前缀)。
|
|
14483
|
+
*/
|
|
14484
|
+
/** 域名称(下划线;连字符非法——见文件头) */
|
|
14485
|
+
const BINDING_DOMAIN_NAME = "serenity_bindings";
|
|
14486
|
+
/** 表名 */
|
|
14487
|
+
const BINDING_TABLE_NAME = "bindings";
|
|
14488
|
+
const passthroughSchema = {
|
|
14489
|
+
parse: (v) => v,
|
|
14490
|
+
safeParse: (v) => ({
|
|
14491
|
+
success: true,
|
|
14492
|
+
data: v
|
|
14493
|
+
})
|
|
14494
|
+
};
|
|
14495
|
+
/**
|
|
14496
|
+
* 构造域规格(宿主 `defineDomain` 的入参形状)。
|
|
14497
|
+
*
|
|
14498
|
+
* 为什么不引 `@deepseek-ai/dsh-storage-domain` 的 `defineDomain`:
|
|
14499
|
+
* 它是**宿主 peer 依赖**(本仓 peer-only 打包,不装它)⇒ 直接 import 会让插件在
|
|
14500
|
+
* 未装该包的运行时**模块加载即失败**。域规格是**纯数据**,手写等价对象即可
|
|
14501
|
+
* (`tests/host/storage-domain.test.ts` 用真实 `defineDomain` 验证本对象被接受)。
|
|
14502
|
+
*/
|
|
14503
|
+
function bindingDomainSpec() {
|
|
14504
|
+
return {
|
|
14505
|
+
name: BINDING_DOMAIN_NAME,
|
|
14506
|
+
version: 1,
|
|
14507
|
+
tables: { [BINDING_TABLE_NAME]: { valueSchema: passthroughSchema } }
|
|
14508
|
+
};
|
|
14509
|
+
}
|
|
14510
|
+
/** 读侧形状容忍(手改 / 旧数据 / 未来字段):只要求 `dirName` 是字符串 */
|
|
14511
|
+
function asBindingRecord(value) {
|
|
14512
|
+
const r = value;
|
|
14513
|
+
if (!r || typeof r !== "object" || typeof r.dirName !== "string") return null;
|
|
14514
|
+
return r;
|
|
14515
|
+
}
|
|
14516
|
+
/**
|
|
14517
|
+
* 从插件 ctx 打开绑定域。**永不抛错**;不可用一律返回 `null`(调用方决定降级)。
|
|
14518
|
+
*
|
|
14519
|
+
* 失败面(全部返回 `null`,调用方回落旧 `.bindings.json`):
|
|
14520
|
+
* - 宿主未提供 `storageDomain`(未装载 storage-domain 插件 / 老宿主);
|
|
14521
|
+
* - `open` 抛错(域名非法 / 后端缺失 `backend-not-found` / 版本不符 / 记录损坏);
|
|
14522
|
+
* - 拿到的服务形状不对(无 `open` 函数)。
|
|
14523
|
+
*
|
|
14524
|
+
* @param ctx 插件上下文(真实 cordis Context;测试可传结构化替身)
|
|
14525
|
+
* @returns 域句柄,或 `null`
|
|
14526
|
+
*/
|
|
14527
|
+
async function openBindingDomain(ctx) {
|
|
14528
|
+
const facility = hostService(ctx, "storageDomain");
|
|
14529
|
+
if (!facility || typeof facility.open !== "function") return null;
|
|
14530
|
+
try {
|
|
14531
|
+
const domain = await facility.open(bindingDomainSpec());
|
|
14532
|
+
if (!domain || typeof domain.table !== "function") return null;
|
|
14533
|
+
const table = domain.table(BINDING_TABLE_NAME);
|
|
14534
|
+
if (!table || typeof table.get !== "function") return null;
|
|
14535
|
+
return domain;
|
|
14536
|
+
} catch {
|
|
14537
|
+
return null;
|
|
14538
|
+
}
|
|
14539
|
+
}
|
|
14540
|
+
/**
|
|
14541
|
+
* 绑定表的**同步读**封装(句柄缺失时全部返回空,语义 = "域里没有")。
|
|
14542
|
+
*
|
|
14543
|
+
* 为什么单独包一层(而不是让调用方直接拿 table):
|
|
14544
|
+
* ① 句柄可能为 `null`(域不可用)⇒ 让 `null` 检查集中在一处;
|
|
14545
|
+
* ② 读侧要统一做形状容忍(`asBindingRecord`)。
|
|
14546
|
+
*/
|
|
14547
|
+
var BindingStore = class {
|
|
14548
|
+
domain;
|
|
14549
|
+
constructor(domain) {
|
|
14550
|
+
this.domain = domain;
|
|
14551
|
+
}
|
|
14552
|
+
/** 域是否可用(`false` ⇒ 调用方应回落旧文件) */
|
|
14553
|
+
get available() {
|
|
14554
|
+
return this.domain !== null;
|
|
14555
|
+
}
|
|
14556
|
+
table() {
|
|
14557
|
+
if (!this.domain) return null;
|
|
14558
|
+
try {
|
|
14559
|
+
return this.domain.table(BINDING_TABLE_NAME);
|
|
14560
|
+
} catch {
|
|
14561
|
+
return null;
|
|
14562
|
+
}
|
|
14563
|
+
}
|
|
14564
|
+
/** 同步读一条(域不可用 → `undefined`) */
|
|
14565
|
+
get(sessionId) {
|
|
14566
|
+
try {
|
|
14567
|
+
const rec = this.table()?.get(sessionId);
|
|
14568
|
+
return rec ? asBindingRecord(rec) ?? void 0 : void 0;
|
|
14569
|
+
} catch {
|
|
14570
|
+
return;
|
|
14571
|
+
}
|
|
14572
|
+
}
|
|
14573
|
+
/** 同步遍历全部 `[会话id, 记录]`(域不可用 → 空) */
|
|
14574
|
+
entries() {
|
|
14575
|
+
const out = [];
|
|
14576
|
+
try {
|
|
14577
|
+
const t = this.table();
|
|
14578
|
+
if (!t) return out;
|
|
14579
|
+
for (const [k, v] of t.entries()) {
|
|
14580
|
+
const rec = asBindingRecord(v);
|
|
14581
|
+
if (rec) out.push([k, rec]);
|
|
14582
|
+
}
|
|
14583
|
+
} catch {}
|
|
14584
|
+
return out;
|
|
14585
|
+
}
|
|
14586
|
+
/** 异步写一条(域不可用 / 落盘失败 → `false`;**不抛**) */
|
|
14587
|
+
async put(sessionId, rec) {
|
|
14588
|
+
try {
|
|
14589
|
+
const t = this.table();
|
|
14590
|
+
if (!t) return false;
|
|
14591
|
+
await t.put(sessionId, rec);
|
|
14592
|
+
return true;
|
|
14593
|
+
} catch {
|
|
14594
|
+
return false;
|
|
14595
|
+
}
|
|
14596
|
+
}
|
|
14597
|
+
/** 异步删一条(域不可用 / 失败 → `false`;**不抛**) */
|
|
14598
|
+
async delete(sessionId) {
|
|
14599
|
+
try {
|
|
14600
|
+
const t = this.table();
|
|
14601
|
+
if (!t) return false;
|
|
14602
|
+
return await t.delete(sessionId);
|
|
14603
|
+
} catch {
|
|
14604
|
+
return false;
|
|
14605
|
+
}
|
|
14606
|
+
}
|
|
14607
|
+
};
|
|
14608
|
+
/** 由域句柄造 store(`null` 句柄 → 不可用 store,读全空、写全 false) */
|
|
14609
|
+
function bindingStore(domain) {
|
|
14610
|
+
return new BindingStore(domain);
|
|
14611
|
+
}
|
|
14612
|
+
//#endregion
|
|
14439
14613
|
//#region src/index.ts
|
|
14440
14614
|
const name = "dsh-serenity-hooks";
|
|
14441
14615
|
/** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在
|
|
@@ -14487,6 +14661,19 @@ function apply(ctx, config) {
|
|
|
14487
14661
|
if (report !== null && report.issues.length > 0) console.warn(`[serenity-hooks] ${summarizeHostContract(report)}`);
|
|
14488
14662
|
} catch {}
|
|
14489
14663
|
registerImChannel(weixinChannel);
|
|
14664
|
+
(async () => {
|
|
14665
|
+
try {
|
|
14666
|
+
const domain = await openBindingDomain(ctx);
|
|
14667
|
+
setBindingStore(bindingStore(domain));
|
|
14668
|
+
if (domain) {
|
|
14669
|
+
const root = process.cwd();
|
|
14670
|
+
const stats = migrateBindingsToDomain(root);
|
|
14671
|
+
if (stats.migrated > 0) console.log(`[serenity-hooks] §0L 绑定已迁移入宿主存储域:${stats.migrated} 条(跳过 ${stats.skipped})`);
|
|
14672
|
+
} else console.log("[serenity-hooks] §0L 存储域不可用 → 绑定继续走 CCC 内 .bindings.json(向前兼容回落)");
|
|
14673
|
+
} catch {
|
|
14674
|
+
setBindingStore(null);
|
|
14675
|
+
}
|
|
14676
|
+
})();
|
|
14490
14677
|
if (config.tools) {
|
|
14491
14678
|
ctx.tools.register(ccFsTool);
|
|
14492
14679
|
ctx.tools.register(createTrajectoryTool(ctx));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
4
|
//#region src/session-cleanup.ts
|
|
@@ -19,6 +20,16 @@ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
|
19
20
|
*
|
|
20
21
|
* 本模块纯逻辑(零 ctx 依赖):传入 sessionsRoot + liveIds,可单测。
|
|
21
22
|
*/
|
|
23
|
+
var session_cleanup_exports = /* @__PURE__ */ __exportAll({
|
|
24
|
+
collectEligibleSessions: () => collectEligibleSessions,
|
|
25
|
+
cutoffDaysAgo: () => cutoffDaysAgo,
|
|
26
|
+
findSessionLog: () => findSessionLog,
|
|
27
|
+
hasSessionLogById: () => hasSessionLogById,
|
|
28
|
+
performCleanup: () => performCleanup,
|
|
29
|
+
sessionLogArtifacts: () => sessionLogArtifacts,
|
|
30
|
+
sessionLogBytesById: () => sessionLogBytesById,
|
|
31
|
+
sessionsRootDir: () => sessionsRootDir
|
|
32
|
+
});
|
|
22
33
|
/** DSH 会话 root:env DSH_HOME → ~/.dsh(config-ops globalConfigPath 同款推导) */
|
|
23
34
|
function sessionsRootDir() {
|
|
24
35
|
const dshHome = process.env.DSH_HOME ?? join(process.env.HOME ?? "", ".dsh");
|
|
@@ -142,9 +153,61 @@ function performCleanup(root, cutoffMs, liveIds = /* @__PURE__ */ new Set(), opt
|
|
|
142
153
|
function dirOf(logPath) {
|
|
143
154
|
return logPath.slice(0, logPath.lastIndexOf("/")) || logPath;
|
|
144
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* 某一 dsh 会话(目录名 / 会话 id)的**当前世代日志体积**(字节)。
|
|
158
|
+
*
|
|
159
|
+
* 为什么要它(S142 §0y「旧会话累积 ⇒ dsh 内存崩溃」,所有者令):
|
|
160
|
+
* · 宿主恢复/继续一条会话必须**重放全部事件**(`scanLog` → `materializeAppendBatch`)
|
|
161
|
+
* ⇒ **代价 ∝ 日志体积**;
|
|
162
|
+
* · 实测(`_tmp/measure-session-load.mjs`,照宿主 `scanZstdFrames` 复刻):
|
|
163
|
+
* 一条 **72.2 MB**(zstd)的日志 = 35 559 个 frame / 252.6 MB 文本 / 62 680 条事件,
|
|
164
|
+
* **光是读一遍峰值 RSS +1 175 MB**(探针什么都不保留);
|
|
165
|
+
* · 而宿主的解析缓存 `COLD_LOG_MEMO_MAX_ENTRIES = 2` 是**按条数限、不按字节限**的
|
|
166
|
+
* (`dsh-session-persistence-jsonl/lib/index.js:2175`)⇒ 两条巨日志 = 上限内。
|
|
167
|
+
* 🔴 **在本函数之前,ACC 侧没有任何地方知道"绑定的这条会话日志多大"——这个信号根本不存在。**
|
|
168
|
+
* 这是"靠 agent 自觉 rebuild 不够"的机制层原因(keeper 的 K 量的是**模型上下文**,压缩后回落;
|
|
169
|
+
* 而**日志文件只增不减** ⇒ rebuild 提醒永远不会因为"日志 72 MB"触发)。
|
|
170
|
+
*
|
|
171
|
+
* 只认会话目录(含任意世代日志);返回**最高世代**的日志(= 宿主 `findLog` 选中的当前世代)。
|
|
172
|
+
* 未命中 / 不可读 → null(非错误,调用方按"未知"处理)。
|
|
173
|
+
* @param root - DSH 会话 root({@link sessionsRootDir})
|
|
174
|
+
* @param sessionId - 会话目录名(`.bindings.json` 的键,含 `session-` 前缀)或裸 id
|
|
175
|
+
* @returns 字节数 + 日志绝对路径;未命中 → null
|
|
176
|
+
*/
|
|
177
|
+
function sessionLogBytesById(root, sessionId) {
|
|
178
|
+
if (sessionId === "" || !existsSync(root)) return null;
|
|
179
|
+
const dirName = sessionId.startsWith("session-") ? sessionId : `session-${sessionId}`;
|
|
180
|
+
for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
181
|
+
if (!project.isDirectory()) continue;
|
|
182
|
+
const log = findSessionLog(join(root, project.name, dirName));
|
|
183
|
+
if (log === null) continue;
|
|
184
|
+
try {
|
|
185
|
+
return {
|
|
186
|
+
bytes: statSync(log).size,
|
|
187
|
+
path: log
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* **(c) 绑定一致性兜底**的判据:某会话的日志**是否仍在磁盘上**。
|
|
197
|
+
*
|
|
198
|
+
* 🔴 **fail-closed**:sessions root 本身不存在时**一律返回 true(当作"在")**——
|
|
199
|
+
* 否则换 `DSH_HOME` / 换机器会让"全部绑定看起来都悬空",进而误清整张绑定表。
|
|
200
|
+
* 只有"root 在、而这个会话目录确实不在"才算悬空。
|
|
201
|
+
* @param sessionRoot - DSH 会话 root({@link sessionsRootDir})
|
|
202
|
+
* @param sessionId - 会话目录名(`.bindings.json` 的键)或裸 id
|
|
203
|
+
*/
|
|
204
|
+
function hasSessionLogById(sessionRoot, sessionId) {
|
|
205
|
+
if (!existsSync(sessionRoot)) return true;
|
|
206
|
+
return sessionLogBytesById(sessionRoot, sessionId) !== null;
|
|
207
|
+
}
|
|
145
208
|
/** 便捷:默认 N 天前为 cutoff(供 API/调用方) */
|
|
146
209
|
function cutoffDaysAgo(days, nowMs = Date.now()) {
|
|
147
210
|
return nowMs - days * DAY_MS;
|
|
148
211
|
}
|
|
149
212
|
//#endregion
|
|
150
|
-
export {
|
|
213
|
+
export { session_cleanup_exports as n, sessionsRootDir as r, hasSessionLogById as t };
|
package/lib/session-cleanup.d.ts
CHANGED
|
@@ -73,6 +73,41 @@ export declare function performCleanup(root: string, cutoffMs: number, liveIds?:
|
|
|
73
73
|
candidates: CandidateSession[];
|
|
74
74
|
result: CleanupResult | null;
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* 某一 dsh 会话(目录名 / 会话 id)的**当前世代日志体积**(字节)。
|
|
78
|
+
*
|
|
79
|
+
* 为什么要它(S142 §0y「旧会话累积 ⇒ dsh 内存崩溃」,所有者令):
|
|
80
|
+
* · 宿主恢复/继续一条会话必须**重放全部事件**(`scanLog` → `materializeAppendBatch`)
|
|
81
|
+
* ⇒ **代价 ∝ 日志体积**;
|
|
82
|
+
* · 实测(`_tmp/measure-session-load.mjs`,照宿主 `scanZstdFrames` 复刻):
|
|
83
|
+
* 一条 **72.2 MB**(zstd)的日志 = 35 559 个 frame / 252.6 MB 文本 / 62 680 条事件,
|
|
84
|
+
* **光是读一遍峰值 RSS +1 175 MB**(探针什么都不保留);
|
|
85
|
+
* · 而宿主的解析缓存 `COLD_LOG_MEMO_MAX_ENTRIES = 2` 是**按条数限、不按字节限**的
|
|
86
|
+
* (`dsh-session-persistence-jsonl/lib/index.js:2175`)⇒ 两条巨日志 = 上限内。
|
|
87
|
+
* 🔴 **在本函数之前,ACC 侧没有任何地方知道"绑定的这条会话日志多大"——这个信号根本不存在。**
|
|
88
|
+
* 这是"靠 agent 自觉 rebuild 不够"的机制层原因(keeper 的 K 量的是**模型上下文**,压缩后回落;
|
|
89
|
+
* 而**日志文件只增不减** ⇒ rebuild 提醒永远不会因为"日志 72 MB"触发)。
|
|
90
|
+
*
|
|
91
|
+
* 只认会话目录(含任意世代日志);返回**最高世代**的日志(= 宿主 `findLog` 选中的当前世代)。
|
|
92
|
+
* 未命中 / 不可读 → null(非错误,调用方按"未知"处理)。
|
|
93
|
+
* @param root - DSH 会话 root({@link sessionsRootDir})
|
|
94
|
+
* @param sessionId - 会话目录名(`.bindings.json` 的键,含 `session-` 前缀)或裸 id
|
|
95
|
+
* @returns 字节数 + 日志绝对路径;未命中 → null
|
|
96
|
+
*/
|
|
97
|
+
export declare function sessionLogBytesById(root: string, sessionId: string): {
|
|
98
|
+
bytes: number;
|
|
99
|
+
path: string;
|
|
100
|
+
} | null;
|
|
101
|
+
/**
|
|
102
|
+
* **(c) 绑定一致性兜底**的判据:某会话的日志**是否仍在磁盘上**。
|
|
103
|
+
*
|
|
104
|
+
* 🔴 **fail-closed**:sessions root 本身不存在时**一律返回 true(当作"在")**——
|
|
105
|
+
* 否则换 `DSH_HOME` / 换机器会让"全部绑定看起来都悬空",进而误清整张绑定表。
|
|
106
|
+
* 只有"root 在、而这个会话目录确实不在"才算悬空。
|
|
107
|
+
* @param sessionRoot - DSH 会话 root({@link sessionsRootDir})
|
|
108
|
+
* @param sessionId - 会话目录名(`.bindings.json` 的键)或裸 id
|
|
109
|
+
*/
|
|
110
|
+
export declare function hasSessionLogById(sessionRoot: string, sessionId: string): boolean;
|
|
76
111
|
/** 便捷:默认 N 天前为 cutoff(供 API/调用方) */
|
|
77
112
|
export declare function cutoffDaysAgo(days: number, nowMs?: number): number;
|
|
78
113
|
export {};
|
|
@@ -526,6 +526,107 @@ const SESSION_BOUND_EVENT = "serenity/bound";
|
|
|
526
526
|
/** 绑定文件相对 CCC 根的路径 */
|
|
527
527
|
const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
|
|
528
528
|
const BINDINGS_VERSION = 1;
|
|
529
|
+
let domainStore = null;
|
|
530
|
+
/**
|
|
531
|
+
* 注入域句柄(装载期调用一次)。传 `null` = 域不可用 ⇒ 全部退回旧文件行为。
|
|
532
|
+
* 幂等:重复调用以后者为准(HMR / 重载场景)。
|
|
533
|
+
*/
|
|
534
|
+
function setBindingStore(store) {
|
|
535
|
+
domainStore = store;
|
|
536
|
+
}
|
|
537
|
+
/** 域记录 → 本模块记录形状(同名字段,无需映射表;见 host/storage-domain.ts 头注) */
|
|
538
|
+
function fromDomainRecord(rec) {
|
|
539
|
+
return {
|
|
540
|
+
dirName: rec.dirName,
|
|
541
|
+
mdPath: rec.mdPath,
|
|
542
|
+
...rec.sessionId ? { sessionId: rec.sessionId } : {},
|
|
543
|
+
action: rec.action ?? "activate",
|
|
544
|
+
at: typeof rec.at === "number" ? rec.at : 0,
|
|
545
|
+
...rec.note ? { note: rec.note } : {},
|
|
546
|
+
...rec.supersededAt !== void 0 ? { supersededAt: rec.supersededAt } : {}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/** 本模块记录 → 域记录形状 */
|
|
550
|
+
function toDomainRecord(rec) {
|
|
551
|
+
return {
|
|
552
|
+
dirName: rec.dirName,
|
|
553
|
+
mdPath: rec.mdPath,
|
|
554
|
+
...rec.sessionId ? { sessionId: rec.sessionId } : {},
|
|
555
|
+
action: rec.action,
|
|
556
|
+
at: rec.at,
|
|
557
|
+
...rec.note ? { note: rec.note } : {},
|
|
558
|
+
...rec.supersededAt !== void 0 ? { supersededAt: rec.supersededAt } : {}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* §0L **一次性迁移**:把某个 CCC 的旧 `.bindings.json` 全量灌入宿主存储域。
|
|
563
|
+
*
|
|
564
|
+
* 设计要点(R↓):
|
|
565
|
+
* - **幂等**:只写域里**尚不存在**的键(`skip existing`)。⇒ 重复调用(每次启动都调)
|
|
566
|
+
* 不会覆盖域里的新状态,也不会因并发而互相踩踏。
|
|
567
|
+
* - **不删旧文件**:迁移只读不写旧表(owner「向前兼容」的第二道保险仍在)。
|
|
568
|
+
* - **不丢绑定**:逐条迁移,单条失败不影响其余(返回计数供诊断)。
|
|
569
|
+
* - **失败静默**:域写不可用 ⇒ 返回 `{migrated:0, skipped:0, failed:0}`,不抛错
|
|
570
|
+
* (装载期调用,不能成为启动单点)。
|
|
571
|
+
*
|
|
572
|
+
* @param root CCC 根(`AGENT_SESSIONS/.bindings.json` 所在)
|
|
573
|
+
* @returns 迁移计数(诊断用;`migrated` = 本次真正灌入的条数)
|
|
574
|
+
*/
|
|
575
|
+
function migrateBindingsToDomain(root) {
|
|
576
|
+
const out = {
|
|
577
|
+
migrated: 0,
|
|
578
|
+
skipped: 0,
|
|
579
|
+
failed: 0
|
|
580
|
+
};
|
|
581
|
+
if (!domainStore?.available) return out;
|
|
582
|
+
let file;
|
|
583
|
+
try {
|
|
584
|
+
file = readBindingsFile(join(root, BINDINGS_REL_PATH));
|
|
585
|
+
} catch {
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
for (const [id, raw] of Object.entries(file.sessions)) {
|
|
589
|
+
const rec = asBoundRecord(raw);
|
|
590
|
+
if (!rec) continue;
|
|
591
|
+
if (domainStore.get(id)) {
|
|
592
|
+
out.skipped += 1;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
domainStore.put(id, toDomainRecord(rec)).catch(() => {});
|
|
597
|
+
out.migrated += 1;
|
|
598
|
+
} catch {
|
|
599
|
+
out.failed += 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* 域内读一条(同步)。域不可用 → `undefined`(调用方据此回落文件)。
|
|
606
|
+
* @param sessionId dsh 会话 id
|
|
607
|
+
*/
|
|
608
|
+
function readFromDomain(sessionId) {
|
|
609
|
+
if (!domainStore?.available) return void 0;
|
|
610
|
+
const rec = domainStore.get(sessionId);
|
|
611
|
+
return rec ? fromDomainRecord(rec) : void 0;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* 域内取**某轨迹的全部绑定**(同步;已排除 superseded —— 与 `listBoundSessionIds` 同判据)。
|
|
615
|
+
* 域不可用 → `null`(**注意与"空结果"区分**:null = 回落文件,[] = 域里确实没有)。
|
|
616
|
+
*/
|
|
617
|
+
function listFromDomain(dirName) {
|
|
618
|
+
if (!domainStore?.available) return null;
|
|
619
|
+
const out = [];
|
|
620
|
+
for (const [id, raw] of domainStore.entries()) {
|
|
621
|
+
const rec = fromDomainRecord(raw);
|
|
622
|
+
if (rec.dirName !== dirName) continue;
|
|
623
|
+
out.push({
|
|
624
|
+
id,
|
|
625
|
+
rec
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
529
630
|
function sessionHeader(session) {
|
|
530
631
|
const header = session?.header;
|
|
531
632
|
if (!header || typeof header !== "object") return null;
|
|
@@ -586,12 +687,21 @@ function readLegacyBoundFromEvents(session) {
|
|
|
586
687
|
return null;
|
|
587
688
|
}
|
|
588
689
|
/**
|
|
589
|
-
* 读取会话当前绑定(权威,latest-wins
|
|
690
|
+
* 读取会话当前绑定(权威,latest-wins)。
|
|
691
|
+
*
|
|
692
|
+
* 读取顺序(§0L,S142 2026-09-19):
|
|
693
|
+
* 1. **宿主存储域**(域可用时的权威来源);
|
|
694
|
+
* 2. **旧文件** `.bindings.json`(域不可用 ⇒ 零回归回落;域可用但该会话未迁移 ⇒ 兜底);
|
|
695
|
+
* 3. **旧事件形态** `serenity/bound`(v1.30.5 及更早的存量)。
|
|
590
696
|
* 无绑定返回 null。
|
|
591
697
|
*/
|
|
592
698
|
function readLastBound(session) {
|
|
593
|
-
const path = bindingsPathFor(session);
|
|
594
699
|
const id = sessionHeader(session)?.id;
|
|
700
|
+
if (typeof id === "string") {
|
|
701
|
+
const fromDomain = readFromDomain(id);
|
|
702
|
+
if (fromDomain) return fromDomain;
|
|
703
|
+
}
|
|
704
|
+
const path = bindingsPathFor(session);
|
|
595
705
|
if (path && typeof id === "string") {
|
|
596
706
|
const rec = asBoundRecord(readBindingsFile(path).sessions[id]);
|
|
597
707
|
if (rec) return rec;
|
|
@@ -639,11 +749,24 @@ function resolveSessionTrajectoryLabel(session, scope) {
|
|
|
639
749
|
* @returns 会话 id 列表(最新绑定在前);无绑定 → `[]`
|
|
640
750
|
*/
|
|
641
751
|
function listBoundSessionIds(root, dirName) {
|
|
642
|
-
const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
|
|
643
752
|
const hits = [];
|
|
753
|
+
const fromDomain = listFromDomain(dirName);
|
|
754
|
+
if (fromDomain !== null) {
|
|
755
|
+
for (const { id, rec } of fromDomain) {
|
|
756
|
+
if (rec.supersededAt !== void 0) continue;
|
|
757
|
+
hits.push({
|
|
758
|
+
id,
|
|
759
|
+
at: typeof rec.at === "number" ? rec.at : 0
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
if (hits.length > 0) return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
|
|
763
|
+
}
|
|
764
|
+
const file = readBindingsFile(join(root, BINDINGS_REL_PATH));
|
|
644
765
|
for (const [id, raw] of Object.entries(file.sessions)) {
|
|
645
766
|
const rec = asBoundRecord(raw);
|
|
646
767
|
if (!rec || rec.dirName !== dirName) continue;
|
|
768
|
+
if (rec.supersededAt !== void 0) continue;
|
|
769
|
+
if (hits.some((h) => h.id === id)) continue;
|
|
647
770
|
hits.push({
|
|
648
771
|
id,
|
|
649
772
|
at: typeof rec.at === "number" ? rec.at : 0
|
|
@@ -652,6 +775,108 @@ function listBoundSessionIds(root, dirName) {
|
|
|
652
775
|
return hits.sort((a, b) => b.at - a.at).map((h) => h.id);
|
|
653
776
|
}
|
|
654
777
|
/**
|
|
778
|
+
* **(b) 同一 trajectory 多 live 会话的机械守卫**(S142 §0y,所有者 2026-09-18 裁「b 做」)。
|
|
779
|
+
*
|
|
780
|
+
* 把绑定到 `dirName` 的**其它**会话 id 标记为 `supersededAt` —— **保留记录**(沿革/正向查询不丢),
|
|
781
|
+
* 只切断"**唤醒还会找到它**"这条路。语义上即"解绑",且**不动那个会话本身**(它仍可被人工使用)。
|
|
782
|
+
*
|
|
783
|
+
* 为什么必须有机制(R↓):`.bindings.json` 的语义是"latest-wins 覆盖**本条会话记录**",
|
|
784
|
+
* 对**同一 trajectory 的其它会话**没有任何约束 ⇒ 多代会话自然堆积(实测 S142 4 代 / S185 2 代),
|
|
785
|
+
* 而 `acquireWakeAgent` 是"遍历全部绑定 id、任一条 live 就用它"——**两条 live 都能被选中**。
|
|
786
|
+
*
|
|
787
|
+
* @param root CCC 根
|
|
788
|
+
* @param dirName 目标 trajectory 目录名
|
|
789
|
+
* @param keepIds 保留(不标记)的会话 id 集合——通常是"当前正在 use 的这条"
|
|
790
|
+
* @returns **实际被标记**的会话 id(升序稳定;供调用方如实报告,未标记任何一条 → `[]`)
|
|
791
|
+
*/
|
|
792
|
+
function supersedeOtherBindings(root, dirName, keepIds) {
|
|
793
|
+
const path = join(root, BINDINGS_REL_PATH);
|
|
794
|
+
const file = readBindingsFile(path);
|
|
795
|
+
const at = Date.now();
|
|
796
|
+
/**
|
|
797
|
+
* 需要退休的会话 id 集合。
|
|
798
|
+
*
|
|
799
|
+
* 🔴 必须取**域 ∪ 文件的并集**(实测缺陷,2026-09-19):
|
|
800
|
+
* 首版实现只在**文件循环**里 `marked.push(id)`,于是"记录只在域里"时
|
|
801
|
+
* 返回值恒为 `[]` —— 而调用方(`tools/trajectory.ts` 的 `use`)**靠这个返回值**决定
|
|
802
|
+
* 是否回报 `supersededBindings`,并且早先用 `marked.length === 0` 短路**跳过了写盘**。
|
|
803
|
+
* ⇒ 症状是"D69 只留一条**静默不生效**"(记录没被标记,返回也看不出异常)。
|
|
804
|
+
* 由 `tests/trajectory-bound.test.ts` 的 D69 域路径用例抓到。
|
|
805
|
+
*/
|
|
806
|
+
const toMark = /* @__PURE__ */ new Set();
|
|
807
|
+
if (domainStore?.available) for (const [id, raw] of domainStore.entries()) {
|
|
808
|
+
const rec = fromDomainRecord(raw);
|
|
809
|
+
if (rec.dirName !== dirName) continue;
|
|
810
|
+
if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
|
|
811
|
+
toMark.add(id);
|
|
812
|
+
}
|
|
813
|
+
for (const [id, raw] of Object.entries(file.sessions)) {
|
|
814
|
+
const rec = asBoundRecord(raw);
|
|
815
|
+
if (!rec || rec.dirName !== dirName) continue;
|
|
816
|
+
if (keepIds.has(id) || rec.supersededAt !== void 0) continue;
|
|
817
|
+
toMark.add(id);
|
|
818
|
+
}
|
|
819
|
+
if (toMark.size === 0) return [];
|
|
820
|
+
if (domainStore?.available) for (const id of toMark) {
|
|
821
|
+
const raw = domainStore.get(id);
|
|
822
|
+
if (!raw) continue;
|
|
823
|
+
domainStore.put(id, toDomainRecord({
|
|
824
|
+
...fromDomainRecord(raw),
|
|
825
|
+
supersededAt: at
|
|
826
|
+
})).catch(() => {});
|
|
827
|
+
}
|
|
828
|
+
for (const id of toMark) {
|
|
829
|
+
const rec = asBoundRecord(file.sessions[id]);
|
|
830
|
+
if (!rec) continue;
|
|
831
|
+
rec.supersededAt = at;
|
|
832
|
+
file.sessions[id] = rec;
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
writeBindingsFile(path, file);
|
|
836
|
+
} catch {}
|
|
837
|
+
return [...toMark].sort();
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* **(c) 绑定一致性兜底**(S142 §0y,所有者 2026-09-18 裁「c 也算个兜底」):
|
|
841
|
+
* 删除指向**已不存在**的 dsh 会话的绑定记录。
|
|
842
|
+
*
|
|
843
|
+
* 为什么需要:`.bindings.json` **只增不清**,而 DSH 会话目录会被
|
|
844
|
+
* `session-cleanup`(本仓既有机制)物理删除 ⇒ 表里必然残留**悬空 id**。
|
|
845
|
+
* 悬空记录本身无害,但它会**污染反向索引**(`listBoundSessionIds` 是唤醒选人的唯一入口),
|
|
846
|
+
* 让"这条轨迹到底还有没有可用会话"变得不可判。
|
|
847
|
+
*
|
|
848
|
+
* 🔴 **安全性由调用方的判据负责**(本函数是纯函数,不认识文件系统):
|
|
849
|
+
* `isMissing` 必须是 **fail-closed** 的(读不到 ⇒ 返回 false = "不当作缺失"),
|
|
850
|
+
* 见 `hasSessionLogById`。判据写错会**清掉整张表**,故此处刻意不提供默认实现。
|
|
851
|
+
*
|
|
852
|
+
* @param root CCC 根
|
|
853
|
+
* @param isMissing 判据:该会话 id 是否**确认**已不存在(false = 保留)
|
|
854
|
+
* @returns 被删除的会话 id(升序稳定;未删任何一条 → `[]`)
|
|
855
|
+
*/
|
|
856
|
+
function pruneMissingBindings(root, isMissing) {
|
|
857
|
+
const path = join(root, BINDINGS_REL_PATH);
|
|
858
|
+
const file = readBindingsFile(path);
|
|
859
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
860
|
+
const judge = (id) => {
|
|
861
|
+
try {
|
|
862
|
+
return isMissing(id);
|
|
863
|
+
} catch {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
if (domainStore?.available) {
|
|
868
|
+
for (const [id] of domainStore.entries()) if (judge(id)) toRemove.add(id);
|
|
869
|
+
}
|
|
870
|
+
for (const id of Object.keys(file.sessions)) if (judge(id)) toRemove.add(id);
|
|
871
|
+
if (toRemove.size === 0) return [];
|
|
872
|
+
if (domainStore?.available) for (const id of toRemove) domainStore.delete(id).catch(() => {});
|
|
873
|
+
for (const id of toRemove) delete file.sessions[id];
|
|
874
|
+
try {
|
|
875
|
+
writeBindingsFile(path, file);
|
|
876
|
+
} catch {}
|
|
877
|
+
return [...toRemove].sort();
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
655
880
|
* 写入绑定(latest-wins 覆盖该会话记录)。
|
|
656
881
|
*
|
|
657
882
|
* 绑定持久化尽力而为:无法定位 CCC 根 / 无会话 id / 写盘失败 → 返回 false,不阻断主流程。
|
|
@@ -662,22 +887,25 @@ function listBoundSessionIds(root, dirName) {
|
|
|
662
887
|
function appendBound(session, action, rec) {
|
|
663
888
|
const path = bindingsPathFor(session);
|
|
664
889
|
const id = sessionHeader(session)?.id;
|
|
665
|
-
if (
|
|
890
|
+
if (typeof id !== "string") return false;
|
|
891
|
+
const record = {
|
|
892
|
+
dirName: rec.dirName,
|
|
893
|
+
mdPath: rec.mdPath,
|
|
894
|
+
...rec.sessionId ? { sessionId: rec.sessionId } : {},
|
|
895
|
+
action,
|
|
896
|
+
at: Date.now(),
|
|
897
|
+
...rec.note ? { note: rec.note } : {}
|
|
898
|
+
};
|
|
899
|
+
if (domainStore?.available) domainStore.put(id, toDomainRecord(record)).catch(() => {});
|
|
900
|
+
if (!path) return domainStore?.available === true;
|
|
666
901
|
try {
|
|
667
902
|
const file = readBindingsFile(path);
|
|
668
|
-
file.sessions[id] =
|
|
669
|
-
dirName: rec.dirName,
|
|
670
|
-
mdPath: rec.mdPath,
|
|
671
|
-
...rec.sessionId ? { sessionId: rec.sessionId } : {},
|
|
672
|
-
action,
|
|
673
|
-
at: Date.now(),
|
|
674
|
-
...rec.note ? { note: rec.note } : {}
|
|
675
|
-
};
|
|
903
|
+
file.sessions[id] = record;
|
|
676
904
|
writeBindingsFile(path, file);
|
|
677
905
|
return true;
|
|
678
906
|
} catch {
|
|
679
|
-
return
|
|
907
|
+
return domainStore?.available === true;
|
|
680
908
|
}
|
|
681
909
|
}
|
|
682
910
|
//#endregion
|
|
683
|
-
export {
|
|
911
|
+
export { showSession as C, localFileStamp as D, isoLocal as E, localHuman as O, setActiveSessionInfo as S, useSession as T, parseSessionContextFromEvents as _, readLastBound as a, sessionEvents as b, supersedeOtherBindings as c, clearActiveSessionInfo as d, createSession as f, listSessions as g, getActiveSessionInfo as h, pruneMissingBindings as i, localIdStamp as k, DEFAULT_SESSION_SCOPE as l, findSession as m, listBoundSessionIds as n, resolveSessionTrajectoryLabel as o, extractSessionMdPathFromText as p, migrateBindingsToDomain as r, setBindingStore as s, appendBound as t, TRAJECTORY_ACTIONS as u, readActiveSessionMd as v, summarize as w, sessionsRoot as x, resolveSessionByTitle as y };
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* 编码无关(U4):绑定锚 = **完整 AGENT_SESSIONS 目录名**(磁盘唯一存在),
|
|
24
24
|
* sessionId(S###/apaas-xxx/自定义)仅是派生展示字段——绝不假设 S 前缀。
|
|
25
25
|
*/
|
|
26
|
+
import { type BindingStore } from './host/storage-domain.js';
|
|
26
27
|
type SessionBoundAction = 'activate' | 'switch' | 'create' | 'rebuild' | 'reconcile' | 'release';
|
|
27
28
|
interface SessionBoundRecord {
|
|
28
29
|
dirName: string;
|
|
@@ -31,13 +32,55 @@ interface SessionBoundRecord {
|
|
|
31
32
|
action: SessionBoundAction;
|
|
32
33
|
at: number;
|
|
33
34
|
note?: string;
|
|
35
|
+
/**
|
|
36
|
+
* **(b) 已被取代标记**(S142 §0y,所有者 2026-09-18 裁「b 做」)。
|
|
37
|
+
*
|
|
38
|
+
* 语义:这条绑定**仍然存在**(沿革不丢、正向 `readLastBound` 仍能查到"我曾属于哪条轨迹"),
|
|
39
|
+
* 但**不再作为唤醒候选**({@link listBoundSessionIds} 过滤掉)——这正是"解绑"的机械形态。
|
|
40
|
+
*
|
|
41
|
+
* 为什么需要(实证):`.bindings.json` 只增不清——实测 S142 名下挂 **4 代**、S185 挂 **2 代**,
|
|
42
|
+
* 同一 trajectory 因此可以有**两条 live 会话**:各持一份完整上下文(内存翻倍)
|
|
43
|
+
* 且**都在写同一批文件**(双写者)。
|
|
44
|
+
*/
|
|
45
|
+
supersededAt?: number;
|
|
34
46
|
}
|
|
35
47
|
/** 绑定文件相对 CCC 根的路径 */
|
|
36
48
|
export declare const BINDINGS_REL_PATH = "AGENT_SESSIONS/.bindings.json";
|
|
49
|
+
/**
|
|
50
|
+
* 注入域句柄(装载期调用一次)。传 `null` = 域不可用 ⇒ 全部退回旧文件行为。
|
|
51
|
+
* 幂等:重复调用以后者为准(HMR / 重载场景)。
|
|
52
|
+
*/
|
|
53
|
+
export declare function setBindingStore(store: BindingStore | null): void;
|
|
54
|
+
/** 当前域句柄(诊断用;`null` = 未接线/不可用) */
|
|
55
|
+
export declare function getBindingStore(): BindingStore | null;
|
|
56
|
+
/**
|
|
57
|
+
* §0L **一次性迁移**:把某个 CCC 的旧 `.bindings.json` 全量灌入宿主存储域。
|
|
58
|
+
*
|
|
59
|
+
* 设计要点(R↓):
|
|
60
|
+
* - **幂等**:只写域里**尚不存在**的键(`skip existing`)。⇒ 重复调用(每次启动都调)
|
|
61
|
+
* 不会覆盖域里的新状态,也不会因并发而互相踩踏。
|
|
62
|
+
* - **不删旧文件**:迁移只读不写旧表(owner「向前兼容」的第二道保险仍在)。
|
|
63
|
+
* - **不丢绑定**:逐条迁移,单条失败不影响其余(返回计数供诊断)。
|
|
64
|
+
* - **失败静默**:域写不可用 ⇒ 返回 `{migrated:0, skipped:0, failed:0}`,不抛错
|
|
65
|
+
* (装载期调用,不能成为启动单点)。
|
|
66
|
+
*
|
|
67
|
+
* @param root CCC 根(`AGENT_SESSIONS/.bindings.json` 所在)
|
|
68
|
+
* @returns 迁移计数(诊断用;`migrated` = 本次真正灌入的条数)
|
|
69
|
+
*/
|
|
70
|
+
export declare function migrateBindingsToDomain(root: string): {
|
|
71
|
+
migrated: number;
|
|
72
|
+
skipped: number;
|
|
73
|
+
failed: number;
|
|
74
|
+
};
|
|
37
75
|
/** 绑定文件绝对路径(由会话 cwd 上溯 .serenity 定位 CCC 根);无法定位返回 null */
|
|
38
76
|
export declare function bindingsPathFor(session: unknown): string | null;
|
|
39
77
|
/**
|
|
40
|
-
* 读取会话当前绑定(权威,latest-wins
|
|
78
|
+
* 读取会话当前绑定(权威,latest-wins)。
|
|
79
|
+
*
|
|
80
|
+
* 读取顺序(§0L,S142 2026-09-19):
|
|
81
|
+
* 1. **宿主存储域**(域可用时的权威来源);
|
|
82
|
+
* 2. **旧文件** `.bindings.json`(域不可用 ⇒ 零回归回落;域可用但该会话未迁移 ⇒ 兜底);
|
|
83
|
+
* 3. **旧事件形态** `serenity/bound`(v1.30.5 及更早的存量)。
|
|
41
84
|
* 无绑定返回 null。
|
|
42
85
|
*/
|
|
43
86
|
export declare function readLastBound(session: unknown): SessionBoundRecord | null;
|
|
@@ -73,6 +116,40 @@ export declare function resolveSessionTrajectoryLabel(session: unknown, scope: s
|
|
|
73
116
|
* @returns 会话 id 列表(最新绑定在前);无绑定 → `[]`
|
|
74
117
|
*/
|
|
75
118
|
export declare function listBoundSessionIds(root: string, dirName: string): string[];
|
|
119
|
+
/**
|
|
120
|
+
* **(b) 同一 trajectory 多 live 会话的机械守卫**(S142 §0y,所有者 2026-09-18 裁「b 做」)。
|
|
121
|
+
*
|
|
122
|
+
* 把绑定到 `dirName` 的**其它**会话 id 标记为 `supersededAt` —— **保留记录**(沿革/正向查询不丢),
|
|
123
|
+
* 只切断"**唤醒还会找到它**"这条路。语义上即"解绑",且**不动那个会话本身**(它仍可被人工使用)。
|
|
124
|
+
*
|
|
125
|
+
* 为什么必须有机制(R↓):`.bindings.json` 的语义是"latest-wins 覆盖**本条会话记录**",
|
|
126
|
+
* 对**同一 trajectory 的其它会话**没有任何约束 ⇒ 多代会话自然堆积(实测 S142 4 代 / S185 2 代),
|
|
127
|
+
* 而 `acquireWakeAgent` 是"遍历全部绑定 id、任一条 live 就用它"——**两条 live 都能被选中**。
|
|
128
|
+
*
|
|
129
|
+
* @param root CCC 根
|
|
130
|
+
* @param dirName 目标 trajectory 目录名
|
|
131
|
+
* @param keepIds 保留(不标记)的会话 id 集合——通常是"当前正在 use 的这条"
|
|
132
|
+
* @returns **实际被标记**的会话 id(升序稳定;供调用方如实报告,未标记任何一条 → `[]`)
|
|
133
|
+
*/
|
|
134
|
+
export declare function supersedeOtherBindings(root: string, dirName: string, keepIds: ReadonlySet<string>): string[];
|
|
135
|
+
/**
|
|
136
|
+
* **(c) 绑定一致性兜底**(S142 §0y,所有者 2026-09-18 裁「c 也算个兜底」):
|
|
137
|
+
* 删除指向**已不存在**的 dsh 会话的绑定记录。
|
|
138
|
+
*
|
|
139
|
+
* 为什么需要:`.bindings.json` **只增不清**,而 DSH 会话目录会被
|
|
140
|
+
* `session-cleanup`(本仓既有机制)物理删除 ⇒ 表里必然残留**悬空 id**。
|
|
141
|
+
* 悬空记录本身无害,但它会**污染反向索引**(`listBoundSessionIds` 是唤醒选人的唯一入口),
|
|
142
|
+
* 让"这条轨迹到底还有没有可用会话"变得不可判。
|
|
143
|
+
*
|
|
144
|
+
* 🔴 **安全性由调用方的判据负责**(本函数是纯函数,不认识文件系统):
|
|
145
|
+
* `isMissing` 必须是 **fail-closed** 的(读不到 ⇒ 返回 false = "不当作缺失"),
|
|
146
|
+
* 见 `hasSessionLogById`。判据写错会**清掉整张表**,故此处刻意不提供默认实现。
|
|
147
|
+
*
|
|
148
|
+
* @param root CCC 根
|
|
149
|
+
* @param isMissing 判据:该会话 id 是否**确认**已不存在(false = 保留)
|
|
150
|
+
* @returns 被删除的会话 id(升序稳定;未删任何一条 → `[]`)
|
|
151
|
+
*/
|
|
152
|
+
export declare function pruneMissingBindings(root: string, isMissing: (sessionId: string) => boolean): string[];
|
|
76
153
|
/**
|
|
77
154
|
* 写入绑定(latest-wins 覆盖该会话记录)。
|
|
78
155
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as hostService, t as hostAgents } from "./access-fiehjxV6.js";
|
|
2
2
|
import { o as listCccs } from "./ccc-roots-Bd_SjEs7.js";
|
|
3
|
-
import {
|
|
3
|
+
import { E as isoLocal, O as localHuman, k as localIdStamp, m as findSession, n as listBoundSessionIds, x as sessionsRoot } from "./trajectory-bound-CQTh86vq.js";
|
|
4
4
|
import { t as readSimpleSettings } from "./settings-section-DMDUoQum.js";
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -572,6 +572,7 @@ function wakeSchedulerState() {
|
|
|
572
572
|
* @param root CCC 根
|
|
573
573
|
* @param dirName 目标 trajectory 目录名
|
|
574
574
|
* @returns 命中返回 agent 与取得方式(how 进 lastResult,便于诊断);未命中返回原因
|
|
575
|
+
* (原因带 `notReady` = **环境未就绪**而非投递失败,调用方**不得**据此记一次失败)
|
|
575
576
|
*/
|
|
576
577
|
async function acquireWakeAgent(ctx, root, dirName) {
|
|
577
578
|
const agents = hostAgents(ctx);
|
|
@@ -586,7 +587,10 @@ async function acquireWakeAgent(ctx, root, dirName) {
|
|
|
586
587
|
if (ids.length > 0) {
|
|
587
588
|
const sc = hostService(ctx, "sessionController");
|
|
588
589
|
const resolved = sc?.resolveAgent;
|
|
589
|
-
if (typeof resolved !== "function") return {
|
|
590
|
+
if (typeof resolved !== "function") return {
|
|
591
|
+
error: `宿主尚未就绪:目标会话未加载,且 sessionController 服务此刻不可用(重启后的启动窗口,或本进程确实无该服务)——绑定 id: ${ids[0]}`,
|
|
592
|
+
notReady: true
|
|
593
|
+
};
|
|
590
594
|
const id = ids[0];
|
|
591
595
|
try {
|
|
592
596
|
const out = await resolved.call(sc, id);
|
|
@@ -631,7 +635,8 @@ async function deliverWake(ctx, root, entry) {
|
|
|
631
635
|
const acquired = await acquireWakeAgent(ctx, root, target.dirName);
|
|
632
636
|
if ("error" in acquired) return {
|
|
633
637
|
ok: false,
|
|
634
|
-
detail: acquired.error
|
|
638
|
+
detail: acquired.error,
|
|
639
|
+
notReady: acquired.notReady === true
|
|
635
640
|
};
|
|
636
641
|
try {
|
|
637
642
|
acquired.agent.followup(createUserMessage({
|
|
@@ -811,6 +816,10 @@ async function runWakeTick(ctx) {
|
|
|
811
816
|
}
|
|
812
817
|
for (const e of due) {
|
|
813
818
|
const res = await deliverWake(ctx, root, e);
|
|
819
|
+
if (res.notReady) {
|
|
820
|
+
log.push(`· ${root}: ${e.id} → 跳过(宿主服务未就绪,下个 tick 再试;不计失败)`);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
814
823
|
updateWake(root, e.id, {
|
|
815
824
|
state: res.ok ? "delivered" : "pending",
|
|
816
825
|
attempts: e.attempts + 1,
|
package/lib/wake-scheduler.d.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* → 回退:live 会话标题匹配
|
|
15
15
|
* 3. agent:**live 优先**(`ctx.agents.get`)→ 冷会话走 `ctx.sessionController.resolveAgent`
|
|
16
16
|
* (宿主实现:live 优先 + resume 去重 + 由会话元数据恢复 preset)
|
|
17
|
-
* → `sessionController`
|
|
17
|
+
* → `sessionController` 缺席 ⇒ 仅投递 live 目标,**不静默**(v1.39.3:**归为"环境未就绪"**
|
|
18
|
+
* 而非"投递失败",见 `WakeDeliveryResult.notReady` —— 启动那一拍早于懒服务就绪是常态)
|
|
18
19
|
* 4. 投递:`agent.followup(...)`("Queue an ordinary follow-up turn and wake the driver")
|
|
19
20
|
*
|
|
20
21
|
* 守卫(I-4 默认 + v1.34 S-1 解耦):全局 `wakeSchedulerEnabled`(**缺省开**;**不再**回退旧键
|
|
@@ -28,6 +29,19 @@ import { type WakeEntry } from './wake-registry.js';
|
|
|
28
29
|
interface WakeDeliveryResult {
|
|
29
30
|
ok: boolean;
|
|
30
31
|
detail: string;
|
|
32
|
+
/**
|
|
33
|
+
* 🔴 **环境未就绪**(不是投递失败)——只有一种情形:目标需要**冷载入**,而宿主此刻**还没把
|
|
34
|
+
* 懒服务 `sessionController` 供上来**(典型 = `dsh web` 刚重启:时钟武装时**立刻跑一次 tick**,
|
|
35
|
+
* 那一拍早于懒服务就绪、也早于会话恢复)。
|
|
36
|
+
*
|
|
37
|
+
* 为什么要把它和"真失败"分开(2026-09-17 实测,S142 §0t):
|
|
38
|
+
* 旧实现把这一情形当成投递失败写进条目 ⇒ ① `attempts` 无端 +1 ② `lastResult` 留下一条
|
|
39
|
+
* **误导性错误**(原文猜"headless profile?",而真因是**重启窗口**,属**假陈述**)
|
|
40
|
+
* ③ 注册表/面板里出现"失败的投递",**每次 restart 都必然复现**——一个**假告警发生器**
|
|
41
|
+
* (所有者 2026-09-17 就是被它引来的)。而消息**从未丢失**:下一个 tick 就投成功了。
|
|
42
|
+
* ⇒ 判据:**"宿主服务还没起来"不是投递结果**,不得计入条目。
|
|
43
|
+
*/
|
|
44
|
+
notReady?: boolean;
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* 调度器**进程态**(诊断用,模块级 = 进程级,正是诊断对象)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|