@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.87.6",
3
+ "version": "1.88.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -227,7 +227,7 @@ async function listCccs(ctx, opts = {}) {
227
227
  if (typeof defaultRoot === "string" && defaultRoot !== "" && !c.roots.includes(defaultRoot)) c.roots.unshift(defaultRoot);
228
228
  let rolesOf = null;
229
229
  if (opts.withRoles === true) {
230
- const { listTrajectoryConstraints } = await import("./profile-DGt_e-Nx.js").then((n) => n.a);
230
+ const { listTrajectoryConstraints } = await import("./profile-zxakCNXy.js").then((n) => n.a);
231
231
  rolesOf = (root) => {
232
232
  try {
233
233
  return listTrajectoryConstraints(root).map((p) => p.code);
@@ -168,16 +168,29 @@ function dirOf(logPath) {
168
168
  * 这是"靠 agent 自觉 rebuild 不够"的机制层原因(keeper 的 K 量的是**模型上下文**,压缩后回落;
169
169
  * 而**日志文件只增不减** ⇒ rebuild 提醒永远不会因为"日志 72 MB"触发)。
170
170
  *
171
+ * 🔴 **会话 id 有两种形态,目录名不能靠补前缀猜**(2026-10-23 owner 报「载体明明在,却报不在了」):
172
+ * · 普通会话 = `session-<uuid>`,目录名与 id **逐字相同**;
173
+ * · skiff 会话 = `skiff-*`,同样**逐字相同**。
174
+ * 旧实现无条件 `session-${id}` ⇒ 对 `skiff-weixin-…` 拼出一个**从不存在的目录**
175
+ * ⇒ `hasSessionLogById` 判 false ⇒ 一条**活得好好的**载体被判成「曾经有过、此刻不在了」
176
+ * ⇒ 调用方(`send-now` / CRO 自举闸门)据此**拒绝投递并拒绝建载体**,
177
+ * 而错误文案说「把那个会话恢复」—— 一个不存在的问题。
178
+ *
179
+ * ⇒ 判据改为:**先按本名找,找不到再试 `session-` 前缀形态**。
180
+ * 两个方向都试的理由 = 我们**不知道**调用方手里的串是 id 还是目录名;
181
+ * 而 `stat` 一个不存在的目录成本可忽略,误判的代价却是一整条轨迹被锁死。
182
+ * ⚠️ 仍找不到 ⇒ `null`("这条会话的日志不在"),与"读不到"同口径,由调用方决定降级。
183
+ *
171
184
  * 只认会话目录(含任意世代日志);返回**最高世代**的日志(= 宿主 `findLog` 选中的当前世代)。
172
185
  * 未命中 / 不可读 → null(非错误,调用方按"未知"处理)。
173
186
  * @param root - DSH 会话 root({@link sessionsRootDir})
174
- * @param sessionId - 会话目录名(`.bindings.json` 的键,含 `session-` 前缀)或裸 id
187
+ * @param sessionId - 会话 id 或会话目录名(`session-*` / `skiff-*` 都按本名找)
175
188
  * @returns 字节数 + 日志绝对路径;未命中 → null
176
189
  */
177
190
  function sessionLogBytesById(root, sessionId) {
178
191
  if (sessionId === "" || !existsSync(root)) return null;
179
- const dirName = sessionId.startsWith("session-") ? sessionId : `session-${sessionId}`;
180
- for (const project of readdirSync(root, { withFileTypes: true })) {
192
+ const candidates = sessionId.startsWith("session-") ? [sessionId] : [sessionId, `session-${sessionId}`];
193
+ for (const dirName of candidates) for (const project of readdirSync(root, { withFileTypes: true })) {
181
194
  if (!project.isDirectory()) continue;
182
195
  const log = findSessionLog(join(root, project.name, dirName));
183
196
  if (log === null) continue;
package/lib/cro/log.d.ts CHANGED
@@ -91,6 +91,19 @@ export interface CroWakeLogEntry {
91
91
  at: string;
92
92
  /** 投递是否成功 */
93
93
  ok: boolean;
94
+ /**
95
+ * 🔴 **"没送成、但这不算失败"**(`ok=false` 时才有意义)—— 2026-10-23 guard 实测逼出。
96
+ *
97
+ * 现状:宿主重启窗口里 `sessionController` 还没就绪 ⇒ 投递取不到载体 ⇒ `notReady`。
98
+ * 那不是"投递失败"(下个 tick 会重试,且**每次都真的重试**),
99
+ * 但也**不是"没发生"** —— 原先整条不落档 ⇒ 该轨迹的流水里**一个字都没有**
100
+ * ⇒ 读日志的人无法区分「CRO 从没工作过」与「CRO 工作了、投递被挡住」。
101
+ *
102
+ * 🔴 **判据(D186 的又一次应用)**:可以不算失败,**但不许不出声**。
103
+ * 三态因此显式分开:`ok` / `fail` / `not-ready`(`ok:false` + `notReady:true`)。
104
+ * ⛔ 不要把它折回 `ok:true`(那是假成功),也不要折成 `fail`(那是假失败,见 `:171` 语义)。
105
+ */
106
+ notReady?: true;
94
107
  /** 投递方式(`已投递(live)` / `已投递(冷载入)`)或失败原因 */
95
108
  detail: string;
96
109
  /** 调度器 tick 计数(便于与 `acc-diag` ①b 对齐) */
@@ -111,6 +124,8 @@ export interface CroWakeLog {
111
124
  /** 追加所需的输入(`at` 由本模块按当地时区生成,不劳调用方) */
112
125
  export interface CroWakeLogInput {
113
126
  ok: boolean;
127
+ /** 🔴 `true` ⇒ 记作 `not-ready`(没送成,但**不算失败**)。见 {@link CroWakeLogEntry.notReady} */
128
+ notReady?: boolean;
114
129
  detail: string;
115
130
  tick: number;
116
131
  reason: string | null;
@@ -154,6 +154,46 @@ export declare function imChannelIds(): string[];
154
154
  * @param root 本会话 CCC 根(由调用方解析)
155
155
  */
156
156
  export declare function hasEnabledImChannel(root: string): boolean;
157
+ /**
158
+ * 可见性判据(**轨迹级**,2026-10-23 owner 令):
159
+ * 这个 dsh 会话所属的认知轨迹,是否**被 IM 路由表点名**(= 它才是那个"接线人")。
160
+ *
161
+ * ## 它解决的问题(owner 原话的**去标识改写**,原文含私有角色名,按 D145 不入仓库)
162
+ *
163
+ * > 「很多轨迹发消息给我,但是我没空回,它们就会等待我,其实并不是,我希望他们继续工作;
164
+ * > 所以只有 IM 桥绑定了任意账户的认知轨迹才拥有 `im-bridge` 这个 tool,
165
+ * > 这样可以确保只有一个接线角色和我联系,其它轨迹和它联系即可,它会自行回复它们的」
166
+ *
167
+ * ⇒ 旧判据是 **CCC 级**({@link hasEnabledImChannel}:本 CCC 有任一启用账号 ⇒ 全体可见)。
168
+ * 它的后果不是"少一个工具",而是**一条错误的工作流**:每条轨迹都以为自己该直接对 owner 说话,
169
+ * 于是发完就**停下来等人回**——而 owner 根本没空逐条回。**工具在,就把等待也带上了。**
170
+ *
171
+ * ## 判据(两条,缺一不可)
172
+ *
173
+ * 1. 本 CCC 有可用账号({@link hasEnabledImChannel})—— 没有账号时路由表也没有意义;
174
+ * 2. 本会话**绑定的轨迹编码**出现在 `routes[].trajectory` 里。
175
+ *
176
+ * ⚠️ **为什么不看 `routes[].user`**:那是"对端是谁"(owner 的通道 id),
177
+ * 判"这条轨迹能不能说话"只跟**轨迹**有关,与哪个对端无关。
178
+ *
179
+ * ⚠️ **`*` 通配也算点名**:`{ user:"*", trajectory:"<某轨迹>" }` 这条路由的含义就是
180
+ * "**没人认领的消息都归那条轨迹**" ⇒ 它确实是一条被授权的接线轨迹,必须算可见。
181
+ * 这与 `matchWeixinRoute` 的通配兜底语义**同源**,不另立规则。
182
+ *
183
+ * ## 拿不到轨迹编码时怎么办(fail-closed,与 `exclusive` 同向)
184
+ *
185
+ * 会话 → 轨迹编码要读绑定。**取不到就判不可见**:
186
+ * - 与 `exclusive`(未声明 ⇒ 隐藏)同向;
187
+ * - 反向(取不到就放行)会让"绑定坏了"表现成"工具还在",
188
+ * 而那正是本判据要防的形态(D179:配置说它有、行为说它没有,且零报错)。
189
+ * ⚠️ 代价明示:绑定读取若短暂失败,该轮会少一个工具;下一轮绑定恢复即回。
190
+ * 这比"误给一条轨迹发消息的权限"便宜 —— 后者会让它**停下来等 owner**。
191
+ *
192
+ * @param root CCC 根
193
+ * @param trajectoryCode 本会话所属认知轨迹的编码(**由调用方解析**,见 `trajectory/policy/profile.ts`)
194
+ * @returns true = 该轨迹接线,`im-bridge` 可见
195
+ */
196
+ export declare function isImWiredTrajectory(root: string, trajectoryCode: string | null | undefined): boolean;
157
197
  /** 结果载荷(lossless JSON——工具返回值必须可 JSON 化) */
158
198
  export type ImJson = null | boolean | number | string | ImJson[] | {
159
199
  [key: string]: ImJson;
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { i as __toESM, n as __exportAll } from "./rolldown-runtime-B4iAMlE-.js";
2
- import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-CvgsUnFW.js";
2
+ import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-WfktvbN8.js";
3
3
  import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readSafeModeHiddenTools, i as findGitRoot, l as pathInside, m as readExclusiveTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-CyYJL4F2.js";
4
- import { A as resolveSessionByTitle, B as readTrajectoryFile, C as listActiveTrajectories, D as parseTrajectoryCode, E as parseSessionContextFromEvents, F as summarize, H as localFileStamp, I as trajectoryDescOf, L as useSession, M as sessionsRoot, N as setActiveSessionInfo, P as showSession, R as TRAJECTORY_FILENAME, T as namingTitleFor, U as localHuman, V as isoLocal, W as localIdStamp, _ as clearActiveSessionInfo, b as findSession, c as replacesDefaultIdentity, g as archiveSession, h as UID_FILENAME, i as listTrajectoryConstraints, j as sessionEvents, k as readTrajectoryUid, l as trajectoryConstraintForSession, n as findDirNameByCode, o as readTrajectoryConstraintAt, p as COGNITIVE_TRAJECTORY_ACTIONS, r as isTrajectoryConstrained, s as readTrajectoryConstraintByCode, t as TRAJECTORY_CONSTRAINT_FILENAME, v as createSession, w as listSessions, x as getActiveSessionInfo, y as extractSessionMdPathFromText, z as TRAJECTORY_LEGACY_FILENAMES } from "./profile-DGt_e-Nx.js";
4
+ import { A as namingTitleFor, B as summarize, C as createSession, E as getActiveSessionInfo, F as resolveSessionByTitle, G as readTrajectoryFile, H as useSession, I as sessionEvents, J as localHuman, K as isoLocal, L as sessionsRoot, M as parseTrajectoryCode, O as listActiveTrajectories, P as readTrajectoryUid, R as setActiveSessionInfo, S as clearActiveSessionInfo, T as findSession, U as TRAJECTORY_FILENAME, V as trajectoryDescOf, W as TRAJECTORY_LEGACY_FILENAMES, Y as localIdStamp, b as UID_FILENAME, c as replacesDefaultIdentity, i as listTrajectoryConstraints, j as parseSessionContextFromEvents, k as listSessions, l as trajectoryCodeForSession, n as findDirNameByCode, o as readTrajectoryConstraintAt, q as localFileStamp, r as isTrajectoryConstrained, s as readTrajectoryConstraintByCode, t as TRAJECTORY_CONSTRAINT_FILENAME, u as trajectoryConstraintForSession, v as COGNITIVE_TRAJECTORY_ACTIONS, w as extractSessionMdPathFromText, x as archiveSession, z as showSession } from "./profile-zxakCNXy.js";
5
5
  import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
6
- import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as profileWriteWhitelist, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as ACC_VERSION, y as findPackageRoot } from "./message-source-Cq4PsuES.js";
7
- import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-Ca5UYzF3.js";
8
- import { i as sessionsRootDir, n as findSessionLog, r as hasSessionLogById } from "./cleanup-DxX78__Z.js";
9
- import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-BI3NRHgC.js";
6
+ import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as constraintCognitiveTrajectory, f as constraintSystemPromptSource, g as profileWriteWhitelist, h as profileToolWhitelist, i as carrierState, l as TRAJECTORY_CONSTRAINT_HINT, n as PLUGIN_SOURCE, o as readLastBound, r as appendBound, s as resolveSessionCognitiveTrajectoryLabel, t as ACC_MESSAGE_KIND, v as ACC_VERSION, y as findPackageRoot } from "./message-source-BAMwXu6i.js";
7
+ import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-Bb4GSSFJ.js";
8
+ import { i as sessionsRootDir, n as findSessionLog, r as hasSessionLogById } from "./cleanup-CPl1SSaK.js";
9
+ import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-DAorSseV.js";
10
10
  import { a as readFeishuCredential, c as readWeixinSettings, d as weixinInboundDir, f as weixinSessionIdForTrajectory, i as matchWeixinRoute, n as extractWeixinText, o as readImSettings, r as hasVoiceItem, s as readWeixinCredential, t as extractWeixinMedia, u as sanitizeFileName$1 } from "./route-3OzOAjec.js";
11
11
  import { REGISTERED_TOOLS } from "./invariant.js";
12
12
  import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-CsZ-f64q.js";
@@ -8631,7 +8631,7 @@ function formatCroLogLine(entry) {
8631
8631
  const head = oneline(entry.promptDigest.head);
8632
8632
  return [
8633
8633
  oneline(entry.at),
8634
- entry.ok ? "ok" : "fail",
8634
+ entry.ok ? "ok" : entry.notReady === true ? "not-ready" : "fail",
8635
8635
  `tick=${entry.tick}`,
8636
8636
  oneline(entry.detail),
8637
8637
  `reason=${reason}`,
@@ -8652,7 +8652,7 @@ function parseCroLogLine(line) {
8652
8652
  const at = parts[0];
8653
8653
  if (Number.isNaN(Date.parse(at))) return null;
8654
8654
  const mark = parts[1];
8655
- if (mark !== "ok" && mark !== "fail") return null;
8655
+ if (mark !== "ok" && mark !== "fail" && mark !== "not-ready") return null;
8656
8656
  const tickM = /^tick=(\d+)$/.exec(parts[2]);
8657
8657
  if (!tickM) return null;
8658
8658
  const reasonRaw = parts[4].startsWith("reason=") ? parts[4].slice(7) : "";
@@ -8660,6 +8660,7 @@ function parseCroLogLine(line) {
8660
8660
  return {
8661
8661
  at,
8662
8662
  ok: mark === "ok",
8663
+ ...mark === "not-ready" ? { notReady: true } : {},
8663
8664
  detail: parts[3],
8664
8665
  tick: Number(tickM[1]),
8665
8666
  reason: reasonRaw === "" ? null : reasonRaw,
@@ -8818,6 +8819,7 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
8818
8819
  const entry = {
8819
8820
  at: isoLocal(nowMs),
8820
8821
  ok: input.ok,
8822
+ ...input.notReady === true ? { notReady: true } : {},
8821
8823
  detail: input.detail,
8822
8824
  tick: input.tick,
8823
8825
  reason: input.reason,
@@ -9191,7 +9193,12 @@ async function acquireWakeAgent(ctx, root, dirName) {
9191
9193
  }
9192
9194
  }
9193
9195
  return {
9194
- error: `无绑定会话记录(AGENT_SESSIONS/.bindings.json 中无 ${dirName})——目标轨迹需先被 cognitive_trajectory use 激活过`,
9196
+ /**
9197
+ * 🔴 **文案改指真相源(2026-10-23,guard 实测报障)**:原写「`AGENT_SESSIONS/.bindings.json` 中无 X」,
9198
+ * 而 D172 之后绑定真相源 = **`<轨迹目录>/current.dsh_session_bind`**,`.bindings.json`
9199
+ * **已退役且不在盘上**(实测不存在)⇒ 照旧文案排查的人会去翻一个**不存在的文件**。
9200
+ */
9201
+ error: `无绑定会话记录(AGENT_SESSIONS/${dirName}/current.dsh_session_bind 不存在或无有效会话)——目标轨迹需先被 cognitive_trajectory use 激活过,或由 send-now 自动创建载体`,
9195
9202
  noBinding: true
9196
9203
  };
9197
9204
  }
@@ -9664,7 +9671,52 @@ function buildCroWakeText(dirName, mdPath, prompt, reason) {
9664
9671
  */
9665
9672
  async function deliverCroWake(ctx, root, dirName, prompt, reason) {
9666
9673
  const mdPath = join(sessionsRoot(root), dirName, "SESSION.md");
9667
- const acquired = await acquireWakeAgent(ctx, root, dirName);
9674
+ let acquired = await acquireWakeAgent(ctx, root, dirName);
9675
+ /**
9676
+ * 🔴 **载体自举(2026-10-23,guard 实测报障后补)**
9677
+ *
9678
+ * ## 缺陷(真实代价)
9679
+ *
9680
+ * 一条「**生来就是为了被自己的 CRO 叫醒**」的轨迹,**第一次自唤醒在物理上不可能**:
9681
+ * 它是程序化 `create` 出来的 ⇒ 没有载体 ⇒ `noBinding` ⇒ 投递被拒
9682
+ * ⇒ **恰恰在最需要被叫醒的那一刻没有载体可注入**。实测证据:
9683
+ * `cro-wake-log.log` 首行 `fail ... 无绑定会话记录`,而**那一轮的 CRO 判定完全正确**。
9684
+ *
9685
+ * ## 为什么可以自举,以及它与 `send-now` 的分支差在哪
9686
+ *
9687
+ * `send-now` 的自举理由是「完成一次**人/agent 的**意图」;CRO 是**每 5min 自动重判**,
9688
+ * 若照搬会有「**载体爆炸**」风险。故本路径**多一道闸**:
9689
+ *
9690
+ * - 只认 `carrierState === 'never'`(**从未有过载体**)⇒ 建一次;
9691
+ * - `occupied`(有过但那个会话不在了)⇒ **一律不建**,如实报错。
9692
+ *
9693
+ * ⇒ 自举**至多发生一次**(建完就有 `current.dsh_session_bind`,此后 `carrierState` 不再是 `never`)。
9694
+ * 这正是 `carrierState` 那道守卫的语义,与 `send-now` 共用同一条判据(不另立)。
9695
+ *
9696
+ * ⚠️ 只处理 `noBinding`;`notReady`(启动窗口)与冷载入失败**照原样返回** ——
9697
+ * 那些不是"没载体",建新载体解决不了且可能制造第二条。
9698
+ */
9699
+ if ("error" in acquired && acquired.noBinding === true) {
9700
+ const carrier = carrierState(root, dirName, hostAliveProbe(ctx));
9701
+ if (carrier.state === "never") {
9702
+ const created = await createAndBindTarget(ctx, root, dirName);
9703
+ if (!("error" in created)) {
9704
+ acquired = created;
9705
+ console.log(`[serenity-hooks] CRO 载体自举:${dirName} 从未有过载体 ⇒ 已自动创建并绑定(${created.how})`);
9706
+ }
9707
+ } else
9708
+ /**
9709
+ * 🔴 **拒绝自举时必须指名是哪一半**(D186:守卫可以拒绝,但不许不出声)。
9710
+ *
9711
+ * 若照原样把 `acquireWakeAgent` 的 `noBinding` 文案透传出去,`cro-wake-log` 里
9712
+ * 「`current.dsh_session_bind` 不存在或无有效会话」**两种状态长得一模一样**:
9713
+ * · `never` —— 那条轨迹**生来就没有载体**(本次已自举,不会走到这里);
9714
+ * · `occupied` —— **有过载体、那个会话现在不在了**(本分支拒绝自建)。
9715
+ * ⇒ 读日志的人分不出"该去救那个会话"还是"该去查为什么没建起来",
9716
+ * 而这正是本仓反复付代价的那类缺陷(D131:不靠一句话给状态命名,但**状态不同必须说出来**)。
9717
+ */
9718
+ acquired = { error: carrier.state === "occupied" ? `CRO 唤起未投递:${dirName} 曾经由会话 ${carrier.sessionId} 承载,而它此刻不在了 ⇒ **拒绝自动新建载体**(一条轨迹至多一个载体,而 CRO 每 5min 重判一次,自建会让载体反复膨胀)。\n · 要恢复:把那个会话救回来,或用 cognitive_trajectory rebuild 显式换代(takeover)。` : `CRO 唤起未投递:${dirName} 的载体声明指向会话 ${carrier.sessionId},它按探针**仍可用**,而选人路径却报"无绑定" ⇒ **两处读数打架,不新建载体**(先查绑定文件与归档集)。` };
9719
+ }
9668
9720
  if ("error" in acquired) return {
9669
9721
  ok: false,
9670
9722
  detail: acquired.error,
@@ -9760,9 +9812,10 @@ async function runCroPhase(ctx, root, registry, log) {
9760
9812
  wakes += 1;
9761
9813
  const res = await deliverCroWake(ctx, root, dirName, outcome.prompt, outcome.decision.reason);
9762
9814
  log.push(`· ${root}: ${renderCroOutcome(dirName, outcome)} → ${res.ok ? "已投递" : `投递未成功:${res.detail}`}`);
9763
- if (!res.notReady) {
9815
+ {
9764
9816
  const rec = appendCroWakeLog(root, dirName, {
9765
9817
  ok: res.ok,
9818
+ ...res.notReady === true ? { notReady: true } : {},
9766
9819
  detail: res.detail,
9767
9820
  tick: s.ticks,
9768
9821
  reason: outcome.decision.reason ?? null,
@@ -10289,13 +10342,18 @@ function diagLive(ctx) {
10289
10342
  *
10290
10343
  * ## 判据(三条同时成立,缺一不可)
10291
10344
  *
10292
- * 1. 日志**最后一条事件是 `session/end-seed`** —— 这是"上一进程已结束、本进程重放"的标记;
10293
- * 2. 该标记**之前最近的 `turn/end`** 的 `reason.kind === 'interrupted'`;
10294
- * 3. `session/end-seed` 之后**没有新事件**(由判据 1 保证)。
10345
+ * 1. 日志里**最后一条 `turn/end`** 的 `reason.kind === 'interrupted'`;
10346
+ * 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则已被后续轮次接续);
10347
+ * 3. ⛔ **不要求**末条是 `session/end-seed`(2026-10-23 放宽,原因见 {@link detectInterruptedEnd})。
10295
10348
  *
10296
10349
  * ⚠️ **为什么不能只看"存在过 interrupted"**:一个长会话历史上可能中断几十次
10297
10350
  * (本容器实测单会话 33 次)。只看"曾经中断"会把早已恢复的会话全部当成待续 ⇒ 灾难。
10298
10351
  *
10352
+ * 🔴 **为什么原判据要求"末条 = end-seed"是错的**(实测 26 条里漏 2 条):
10353
+ * 重启后宿主会在 end-seed **之后**追加 `agent/inbox/spliced`(把待办消息重新塞进收件箱)
10354
+ * ⇒ "末条必须 end-seed"**在恢复动作发生之后就恒不成立**,
10355
+ * 而"被恢复过又再次被打断"恰恰是守护最该覆盖的形态。
10356
+ *
10299
10357
  * ## 为什么不整文件解压
10300
10358
  *
10301
10359
  * DSH 日志是 **zstd 多帧追加**(实测单会话 24705 帧 / 47 MB)。原生
@@ -10382,16 +10440,44 @@ function readTailEvents(logPath, want) {
10382
10440
  /**
10383
10441
  * 判定一组尾部事件是否「以 interrupted 结尾」。
10384
10442
  * 不命中 → null。
10443
+ *
10444
+ * ## 🔴 2026-10-23 判据放宽(owner 实测:「不是挂在工具调用上的,也要能被 interrupted 筛选到并唤醒」)
10445
+ *
10446
+ * ### 原判据(v1.87.4 起)为什么漏
10447
+ * 原来只认 **末条事件 = `session/end-seed`**。实测(本机 3337 条会话全扫):
10448
+ * · 末轮 `turn/end[interrupted]` 的会话 **26** 条;
10449
+ * · 其中满足原判据的只有 **24** 条 ⇒ **漏 2 条**,形态逐字为:
10450
+ * `… turn/end[interrupted] -> session/end-seed -> agent/inbox/spliced -> agent/inbox/spliced`
10451
+ * ⇒ 重启后宿主把这**两条**追加在 end-seed 之后(它们是"恢复时把待办消息重新塞进收件箱"),
10452
+ * 于是"末条必须 end-seed"这条**在恢复动作发生之后就恒不成立**。
10453
+ * ⚠️ 讽刺的是:**正是那些被恢复过、又再次被打断的会话**最容易长成这样
10454
+ * ⇒ 原判据恰好对"反复中断"的会话失明,而它们才是最需要守护的。
10455
+ *
10456
+ * ### 新判据(三条,缺一不可)
10457
+ * 1. 日志里**最后一条 `turn/end`** 的 `reason.kind === 'interrupted'`;
10458
+ * 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则说明它已被后续轮次接续);
10459
+ * 3. ⛔ **不再要求末条是 `end-seed`**。
10460
+ *
10461
+ * ### 为什么不改成"末条必须是某个白名单"
10462
+ * 那就是原缺陷换个白名单再犯一次(`agent/inbox/spliced` 之外还会有别的)。
10463
+ * **判据要问的是"最后一轮的结束原因是什么、之后有没有新轮开始"**,
10464
+ * 而不是"日志最后一个 token 长什么样"——后者是**载体形态**,前者是**状态**。
10465
+ *
10466
+ * ### 为什么不会误报"曾经中断但已恢复"
10467
+ * 恢复一定伴随 `turn/start`(新一轮)或新的 `turn/end`(`completed` 等)⇒ 判据 1/2 各自挡住。
10468
+ * 实测该放宽**不新增误报**:原 24 条全含于新判据(`curOnly = 0`),新增 2 条全是真漏。
10385
10469
  */
10386
10470
  function detectInterruptedEnd(events) {
10387
10471
  if (events.length === 0) return null;
10388
- if (events[events.length - 1]?.type !== "session/end-seed") return null;
10389
- let turnEnd;
10390
- for (let i = events.length - 2; i >= 0; i--) if (events[i]?.type === "turn/end") {
10391
- turnEnd = events[i];
10472
+ let idx = -1;
10473
+ for (let i = events.length - 1; i >= 0; i--) if (events[i]?.type === "turn/end") {
10474
+ idx = i;
10392
10475
  break;
10393
10476
  }
10394
- if (!turnEnd || turnEnd.data?.reason?.kind !== "interrupted") return null;
10477
+ if (idx < 0) return null;
10478
+ const turnEnd = events[idx];
10479
+ if (turnEnd?.data?.reason?.kind !== "interrupted") return null;
10480
+ for (let i = idx + 1; i < events.length; i++) if (events[i]?.type === "turn/start") return null;
10395
10481
  const turn = typeof turnEnd.data?.turn === "number" ? turnEnd.data.turn : null;
10396
10482
  return {
10397
10483
  turn,
@@ -12305,6 +12391,61 @@ function hasEnabledImChannel(root) {
12305
12391
  return (settings.accounts ?? []).some((a) => a.enabled !== false);
12306
12392
  }
12307
12393
  /**
12394
+ * 可见性判据(**轨迹级**,2026-10-23 owner 令):
12395
+ * 这个 dsh 会话所属的认知轨迹,是否**被 IM 路由表点名**(= 它才是那个"接线人")。
12396
+ *
12397
+ * ## 它解决的问题(owner 原话的**去标识改写**,原文含私有角色名,按 D145 不入仓库)
12398
+ *
12399
+ * > 「很多轨迹发消息给我,但是我没空回,它们就会等待我,其实并不是,我希望他们继续工作;
12400
+ * > 所以只有 IM 桥绑定了任意账户的认知轨迹才拥有 `im-bridge` 这个 tool,
12401
+ * > 这样可以确保只有一个接线角色和我联系,其它轨迹和它联系即可,它会自行回复它们的」
12402
+ *
12403
+ * ⇒ 旧判据是 **CCC 级**({@link hasEnabledImChannel}:本 CCC 有任一启用账号 ⇒ 全体可见)。
12404
+ * 它的后果不是"少一个工具",而是**一条错误的工作流**:每条轨迹都以为自己该直接对 owner 说话,
12405
+ * 于是发完就**停下来等人回**——而 owner 根本没空逐条回。**工具在,就把等待也带上了。**
12406
+ *
12407
+ * ## 判据(两条,缺一不可)
12408
+ *
12409
+ * 1. 本 CCC 有可用账号({@link hasEnabledImChannel})—— 没有账号时路由表也没有意义;
12410
+ * 2. 本会话**绑定的轨迹编码**出现在 `routes[].trajectory` 里。
12411
+ *
12412
+ * ⚠️ **为什么不看 `routes[].user`**:那是"对端是谁"(owner 的通道 id),
12413
+ * 判"这条轨迹能不能说话"只跟**轨迹**有关,与哪个对端无关。
12414
+ *
12415
+ * ⚠️ **`*` 通配也算点名**:`{ user:"*", trajectory:"<某轨迹>" }` 这条路由的含义就是
12416
+ * "**没人认领的消息都归那条轨迹**" ⇒ 它确实是一条被授权的接线轨迹,必须算可见。
12417
+ * 这与 `matchWeixinRoute` 的通配兜底语义**同源**,不另立规则。
12418
+ *
12419
+ * ## 拿不到轨迹编码时怎么办(fail-closed,与 `exclusive` 同向)
12420
+ *
12421
+ * 会话 → 轨迹编码要读绑定。**取不到就判不可见**:
12422
+ * - 与 `exclusive`(未声明 ⇒ 隐藏)同向;
12423
+ * - 反向(取不到就放行)会让"绑定坏了"表现成"工具还在",
12424
+ * 而那正是本判据要防的形态(D179:配置说它有、行为说它没有,且零报错)。
12425
+ * ⚠️ 代价明示:绑定读取若短暂失败,该轮会少一个工具;下一轮绑定恢复即回。
12426
+ * 这比"误给一条轨迹发消息的权限"便宜 —— 后者会让它**停下来等 owner**。
12427
+ *
12428
+ * @param root CCC 根
12429
+ * @param trajectoryCode 本会话所属认知轨迹的编码(**由调用方解析**,见 `trajectory/policy/profile.ts`)
12430
+ * @returns true = 该轨迹接线,`im-bridge` 可见
12431
+ */
12432
+ function isImWiredTrajectory(root, trajectoryCode) {
12433
+ if (typeof trajectoryCode !== "string" || trajectoryCode.trim() === "") return false;
12434
+ if (!hasEnabledImChannel(root)) return false;
12435
+ let settings;
12436
+ try {
12437
+ settings = readImSettings(root);
12438
+ } catch {
12439
+ return false;
12440
+ }
12441
+ const wanted = trajectoryCode.trim();
12442
+ for (const route of settings.routes ?? []) {
12443
+ const target = typeof route.trajectory === "string" ? route.trajectory.trim() : "";
12444
+ if (target !== "" && target === wanted) return true;
12445
+ }
12446
+ return false;
12447
+ }
12448
+ /**
12308
12449
  * 错误码 → 可行动提示(模型视角:说清怎么改)。
12309
12450
  *
12310
12451
  * 🔴 `CHANNEL_NOT_CONFIGURED` 的提示**不能写死某一个渠道的字段名**(2026-10-20 Q4 真机验收踩到):
@@ -12834,7 +12975,7 @@ function computeVisibilitySources(sessionId, root) {
12834
12975
  }
12835
12976
  return { deny: [.../* @__PURE__ */ new Set([...SAFE_MODE_DENY_TOOLS, ...declared])] };
12836
12977
  }));
12837
- out.push(sourceOf("imBridge", () => hasEnabledImChannel(root) ? { deny: [] } : { deny: [IM_BRIDGE_TOOL_NAME] }));
12978
+ out.push(sourceOf("imBridge", () => isImWiredTrajectory(root, trajectoryCodeForSession(root, sessionId)) ? { deny: [] } : { deny: [IM_BRIDGE_TOOL_NAME] }));
12838
12979
  out.push(sourceOf("exclusive", () => {
12839
12980
  const declared = readExclusiveTools(root);
12840
12981
  return { deny: EXCLUSIVE_TOOL_NAMES.filter((t) => !declared.includes(t)) };
@@ -15027,7 +15168,7 @@ function registerStatusApi(ctx, opts = {}) {
15027
15168
  });
15028
15169
  }
15029
15170
  const { saveWeixinRoutes } = await import("./route-3OzOAjec.js").then((n) => n.l);
15030
- const { findDirNameByCode } = await import("./profile-DGt_e-Nx.js").then((n) => n.a);
15171
+ const { findDirNameByCode } = await import("./profile-zxakCNXy.js").then((n) => n.a);
15031
15172
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
15032
15173
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
15033
15174
  return;
@@ -15175,7 +15316,7 @@ function registerStatusApi(ctx, opts = {}) {
15175
15316
  const id = s.id ?? s.header?.id;
15176
15317
  if (id) liveIds.add(id);
15177
15318
  }
15178
- const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-DxX78__Z.js").then((n) => n.t);
15319
+ const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-CPl1SSaK.js").then((n) => n.t);
15179
15320
  const root = sessionsRootDir();
15180
15321
  if (req.method === "GET") {
15181
15322
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
@@ -20070,7 +20211,7 @@ const MEDIA_MAX_BYTES = 20971520;
20070
20211
  */
20071
20212
  async function resolveCccRootByAlias(ctx, alias) {
20072
20213
  try {
20073
- const { listCccs } = await import("./ccc-roots-CvgsUnFW.js").then((n) => n.a);
20214
+ const { listCccs } = await import("./ccc-roots-WfktvbN8.js").then((n) => n.a);
20074
20215
  const entries = await listCccs(ctx);
20075
20216
  const byName = entries.find((e) => e.name === alias);
20076
20217
  if (byName) return byName.root;
@@ -20138,7 +20279,7 @@ async function resolveInboundTarget(ctx, input) {
20138
20279
  });
20139
20280
  return null;
20140
20281
  }
20141
- const { findDirNameByCode } = await import("./profile-DGt_e-Nx.js").then((n) => n.a);
20282
+ const { findDirNameByCode } = await import("./profile-zxakCNXy.js").then((n) => n.a);
20142
20283
  if (findDirNameByCode(root, target.role) === null) {
20143
20284
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
20144
20285
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -21092,7 +21233,7 @@ function syncHumanChannel(ctx) {
21092
21233
  (async () => {
21093
21234
  try {
21094
21235
  stopMachineLoops();
21095
- const { listCccs } = await import("./ccc-roots-CvgsUnFW.js").then((n) => n.a);
21236
+ const { listCccs } = await import("./ccc-roots-WfktvbN8.js").then((n) => n.a);
21096
21237
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
21097
21238
  } catch {}
21098
21239
  })();
@@ -21385,7 +21526,7 @@ function matchCcc(input, candidates) {
21385
21526
  }
21386
21527
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
21387
21528
  async function collectCandidates(ctx) {
21388
- const { listCccs } = await import("./ccc-roots-CvgsUnFW.js").then((n) => n.a);
21529
+ const { listCccs } = await import("./ccc-roots-WfktvbN8.js").then((n) => n.a);
21389
21530
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
21390
21531
  const seen = /* @__PURE__ */ new Set();
21391
21532
  const out = [];