@shgroup/dsh-serenity-hooks 1.87.3 → 1.87.4

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.3",
3
+ "version": "1.87.4",
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-vHheCJiU.js").then((n) => n.a);
230
+ const { listTrajectoryConstraints } = await import("./profile-CQWuuopQ.js").then((n) => n.a);
231
231
  rolesOf = (root) => {
232
232
  try {
233
233
  return listTrajectoryConstraints(root).map((p) => p.code);
@@ -210,4 +210,4 @@ function cutoffDaysAgo(days, nowMs = Date.now()) {
210
210
  return nowMs - days * DAY_MS;
211
211
  }
212
212
  //#endregion
213
- export { hasSessionLogById as n, sessionsRootDir as r, cleanup_exports as t };
213
+ export { sessionsRootDir as i, findSessionLog as n, hasSessionLogById as r, cleanup_exports as t };
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-BI_4UVcj.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-CR1IWLDJ.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-vHheCJiU.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-CQWuuopQ.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-DRkQwfgh.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-DZXSLLEx.js";
8
- import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
9
- import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-H7TVDayZ.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-HLXLQuvR.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-CksIqzii.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-D14eA2MR.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";
@@ -15,7 +15,7 @@ import { createRequire } from "node:module";
15
15
  import z from "@deepseek-ai/schemastery";
16
16
  import { defineTool } from "@deepseek-ai/dsh-tools";
17
17
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
18
- import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
18
+ import { appendFileSync, chmodSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
19
19
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
20
20
  import { homedir, hostname, platform } from "node:os";
21
21
  import { promisify } from "node:util";
@@ -24,8 +24,8 @@ import { fileURLToPath } from "node:url";
24
24
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
25
25
  import { createHash, randomBytes } from "node:crypto";
26
26
  import { Resvg, initWasm } from "@resvg/resvg-wasm";
27
+ import * as nodeZlib from "node:zlib";
27
28
  import { createServer, request } from "node:http";
28
- import * as zlib from "node:zlib";
29
29
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
30
30
  import { lookup } from "node:dns/promises";
31
31
  import { isIP } from "node:net";
@@ -10190,6 +10190,322 @@ function listResident(root) {
10190
10190
  return lines.join("\n");
10191
10191
  }
10192
10192
  //#endregion
10193
+ //#region src/trajectory/live.ts
10194
+ /**
10195
+ * trajectory/live.ts — 进程内 live 会话取数 + 精简诊断
10196
+ *
10197
+ * ── 为什么单独成模块(R↓,2026-09-15 S142「ACC 侧 autopilot 退场」S1 搬迁)──
10198
+ * 这两个函数原先住在 `autopilot-trajectory.ts`,但它们的**主语不是 autopilot**:
10199
+ * `listLiveSessions` 服务 `acc-diag` ① 段的"**哪个会话属于哪个 CCC**"(会话维度,
10200
+ * 不是 CCC 维度),而"**独立进程看不到运行时**"正是 `acc-diag` 存在的理由。
10201
+ * autopilot 退场后它们必须活下去 ⇒ 独立成模块,不再挂着已死机制的名字。
10202
+ *
10203
+ * ── 迁移边界(不搬什么,以及为什么)──────────────────────────────────────
10204
+ * 原 `diagLive` 还含两段,**均不搬**(其主语是 autopilot,随机制退场):
10205
+ * · `autopilotCccs`(逐 CCC 目标命中 / agent 定位诊断)—— 依赖 `readAutopilotSettings` /
10206
+ * `getAutopilotStatus` / `resolveTargetAgent`
10207
+ * · `panelResolved`(无参面板请求落到哪个 CCC)—— 面板区块随机制退场
10208
+ * 留下的 = **进程 cwd/CCC + live 会话清单**(`acc-diag` ① 段仍需要)。
10209
+ *
10210
+ * 判据纪律:本模块**不得**反向依赖 autopilot 判据层(那会让退场半途而废)。
10211
+ *
10212
+ * ── 🔴 2026-09-25 I2 收敛(约束文档 §2.7 违反清单,L1 违反之一)────────────────
10213
+ * **本模块的 `ctx` 参数类型从 `Context` 改为 `unknown`** —— 消除本文件唯一的 `W2` 接触
10214
+ * (直接 import 宿主 `cordis` 的类型)。
10215
+ *
10216
+ * **为什么这是"真解耦"而不是"把类型藏起来"(判据):** `ctx` 在本模块**从不被解引用** ——
10217
+ * 它只被**原样透传**给 `hostSessions(ctx)`,而后者(`host/access.ts`,**L0**)的参数
10218
+ * 本就声明为 `ctx: unknown` ⇒ `Context` 这个 import **在语义上是多余的**,
10219
+ * 它没有参与任何类型检查,只贡献了一处 L1→宿主 的接触点。
10220
+ * 🔵 **证据**:grep 本文件 `ctx` 全部 5 处命中 —— 3 处在参数/调用位、2 处在注释,
10221
+ * **零处**属性访问(`ctx.xxx`)。
10222
+ */
10223
+ /**
10224
+ * 从 dsh 会话 log 读取标题(latest-wins `session/title` 事件)——
10225
+ * **标题不在 `sessions.list()` 条目上**(wire/对象均无 title 字段;F3 命名经
10226
+ * `sessionTitle.rename` 写进 session log),必须从 events 提取(rebuild 同款读取模式)。
10227
+ */
10228
+ function readSessionTitle(session) {
10229
+ try {
10230
+ const events = sessionEvents(session);
10231
+ for (let i = events.length - 1; i >= 0; i--) {
10232
+ const e = events[i];
10233
+ if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
10234
+ }
10235
+ } catch {}
10236
+ return null;
10237
+ }
10238
+ /**
10239
+ * 逐条 live 会话(`cccRoot` = 该会话 cwd 的 CCC 归属,无 → null)。
10240
+ *
10241
+ * CCC 枚举("本机有哪些 CCC")**不在这里**,归 `ccc-roots.listCccs`(并集语义);
10242
+ * 本函数保留的是它**独有**的那部分:会话维度 + cwd 归属 + 标题 + id。
10243
+ *
10244
+ * @param ctx 插件上下文(**只透传**给 L0 取数口,本模块不解引用 ⇒ 类型为 `unknown`)
10245
+ * @returns 逐条 live 会话
10246
+ */
10247
+ function listLiveSessions(ctx) {
10248
+ const out = [];
10249
+ try {
10250
+ const sessions = hostSessions(ctx);
10251
+ for (const s of sessions?.list?.() ?? []) {
10252
+ const cwd = s?.header?.cwd ?? null;
10253
+ out.push({
10254
+ id: s?.id ?? "",
10255
+ cwd,
10256
+ cccRoot: cwd ? cccRootForCwd(cwd) : null,
10257
+ title: readSessionTitle(s)
10258
+ });
10259
+ }
10260
+ } catch {}
10261
+ return out;
10262
+ }
10263
+ function diagLive(ctx) {
10264
+ const processCwd = process.cwd();
10265
+ return {
10266
+ processCwd,
10267
+ processCcc: cccRootForCwd(processCwd),
10268
+ liveSessions: listLiveSessions(ctx)
10269
+ };
10270
+ }
10271
+ //#endregion
10272
+ //#region src/trajectory/interrupted.ts
10273
+ /**
10274
+ * 中断轮次发现(`interrupted` 会话扫描)
10275
+ *
10276
+ * ## 这个模块解决什么
10277
+ *
10278
+ * DSH 的会话日志里,**进程在某一轮完成前退出**(被 kill/崩溃/关机超时)时,
10279
+ * 宿主重放时会为该轮补上收尾事件:
10280
+ *
10281
+ * ```jsonl
10282
+ * turn/end {"turn":66,"reason":{"kind":"interrupted"}}
10283
+ * step/end {"turn":66,"step":85}
10284
+ * session/end-seed {}
10285
+ * ```
10286
+ *
10287
+ * 宿主**只补收尾,不续跑** —— 那一轮的工作就此停在那里。本模块负责把这些会话找出来,
10288
+ * 交给上层(认知轨迹)决定要不要续。
10289
+ *
10290
+ * ## 判据(三条同时成立,缺一不可)
10291
+ *
10292
+ * 1. 日志**最后一条事件是 `session/end-seed`** —— 这是"上一进程已结束、本进程重放"的标记;
10293
+ * 2. 该标记**之前最近的 `turn/end`** 的 `reason.kind === 'interrupted'`;
10294
+ * 3. `session/end-seed` 之后**没有新事件**(由判据 1 保证)。
10295
+ *
10296
+ * ⚠️ **为什么不能只看"存在过 interrupted"**:一个长会话历史上可能中断几十次
10297
+ * (本容器实测单会话 33 次)。只看"曾经中断"会把早已恢复的会话全部当成待续 ⇒ 灾难。
10298
+ *
10299
+ * ## 为什么不整文件解压
10300
+ *
10301
+ * DSH 日志是 **zstd 多帧追加**(实测单会话 24705 帧 / 47 MB)。原生
10302
+ * `zlib.zstdDecompressSync` 只解**第一帧**,对多帧直接报 `Unknown frame descriptor`。
10303
+ * ⇒ 采用**尾部窗口 + 倒序逐帧解压**:只读文件末尾 N 字节,从后往前找帧魔数,
10304
+ * 解到凑够所需事件数即停。实测 8h 窗口 6 个会话 **20 ms**。
10305
+ *
10306
+ * ## 世代识别复用 `cleanup.ts`
10307
+ *
10308
+ * 日志文件名有多个世代(`session.jsonl` / `session.v3.jsonl.zstd` / `session.v4.jsonl.zstd`…),
10309
+ * 宿主按世代写且**迁移保留源文件**。⇒ 本模块**不硬编码文件名**,走
10310
+ * {@link findSessionLog}(与清理/体积统计同一判据),避免"新世代对本模块不可见"。
10311
+ *
10312
+ * ## 边界
10313
+ *
10314
+ * - **只读**:不改任何会话日志。
10315
+ * - **不判周期**:何时扫、扫完做什么归调用方(CCC 的认知轨迹)。本模块只回答
10316
+ * 「此刻哪些会话以 interrupted 结尾」。
10317
+ */
10318
+ /**
10319
+ * zstd 解压(**运行时有、类型声明没有**)。
10320
+ *
10321
+ * 🔴 为什么不能用 `import { zstdDecompressSync } from 'node:zlib'`:
10322
+ * 本仓 `@types/node@^20` 的声明里**没有** zstd 系列(Node 22.15+ 才进声明)。
10323
+ * 而运行时本机 v22.22.1 **确实有**(实测 `typeof require('zlib').zstdDecompressSync === 'function'`)。
10324
+ * ⇒ 用命名空间导入 + 局部断言,并**在运行时校验**;缺失时走明确降级(返回空,不静默瞎猜)。
10325
+ */
10326
+ const zstdDecompressSync = nodeZlib.zstdDecompressSync;
10327
+ /** zstd 帧魔数(小端 `28 B5 2F FD`) */
10328
+ const ZSTD_MAGIC = Buffer.from([
10329
+ 40,
10330
+ 181,
10331
+ 47,
10332
+ 253
10333
+ ]);
10334
+ /** 默认回看窗口:8 小时(owner 2026-10-22 定) */
10335
+ const DEFAULT_LOOKBACK_MS = 288e5;
10336
+ /** 每次从文件尾部读取的字节数(够覆盖数百个末尾事件) */
10337
+ const TAIL_WINDOW_BYTES = 1 << 20;
10338
+ /** 需要凑够的末尾事件数(够跨过 turn/end + step/end + end-seed 即可,留余量) */
10339
+ const TAIL_EVENT_TARGET = 60;
10340
+ /**
10341
+ * 从文件尾部倒序解压,凑够 `want` 个事件。
10342
+ *
10343
+ * 多帧 zstd 必须**按帧边界**切:从后往前找魔数,每帧用下一帧起始位置当结束边界。
10344
+ * 任何一帧解压失败就停下(返回已拿到的)—— 再往前更可能不完整。
10345
+ */
10346
+ function readTailEvents(logPath, want) {
10347
+ if (zstdDecompressSync === void 0) return [];
10348
+ const st = statSync(logPath);
10349
+ const windowBytes = Math.min(TAIL_WINDOW_BYTES, st.size);
10350
+ if (windowBytes < 8) return [];
10351
+ const buf = Buffer.alloc(windowBytes);
10352
+ const fd = openSync(logPath, "r");
10353
+ try {
10354
+ readSync(fd, buf, 0, windowBytes, st.size - windowBytes);
10355
+ } finally {
10356
+ closeSync(fd);
10357
+ }
10358
+ const offsets = [];
10359
+ for (let i = 0; i <= windowBytes - ZSTD_MAGIC.length; i++) if (buf[i] === ZSTD_MAGIC[0] && buf[i + 1] === ZSTD_MAGIC[1] && buf[i + 2] === ZSTD_MAGIC[2] && buf[i + 3] === ZSTD_MAGIC[3]) offsets.push(i);
10360
+ if (offsets.length === 0) return [];
10361
+ const events = [];
10362
+ for (let k = offsets.length - 1; k >= 0 && events.length < want; k--) {
10363
+ const start = offsets[k];
10364
+ const end = k + 1 < offsets.length ? offsets[k + 1] : windowBytes;
10365
+ let text;
10366
+ try {
10367
+ text = zstdDecompressSync(buf.subarray(start, end)).toString("utf8");
10368
+ } catch {
10369
+ break;
10370
+ }
10371
+ const batch = [];
10372
+ for (const line of text.split("\n")) {
10373
+ if (!line.trim()) continue;
10374
+ try {
10375
+ batch.push(JSON.parse(line));
10376
+ } catch {}
10377
+ }
10378
+ events.unshift(...batch);
10379
+ }
10380
+ return events.slice(-want);
10381
+ }
10382
+ /**
10383
+ * 判定一组尾部事件是否「以 interrupted 结尾」。
10384
+ * 不命中 → null。
10385
+ */
10386
+ function detectInterruptedEnd(events) {
10387
+ 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];
10392
+ break;
10393
+ }
10394
+ if (!turnEnd || turnEnd.data?.reason?.kind !== "interrupted") return null;
10395
+ const turn = typeof turnEnd.data?.turn === "number" ? turnEnd.data.turn : null;
10396
+ return {
10397
+ turn,
10398
+ pendingTools: findPendingTools(events, turn)
10399
+ };
10400
+ }
10401
+ /**
10402
+ * 找出末轮里发起了但没有结果的工具调用名。
10403
+ *
10404
+ * ⚠️ **尽力而为**,非权威:尾部窗口可能截断更早的调用。上层应把它当
10405
+ * "**可能**未完成的副作用"清单,而不是"确定未完成"。
10406
+ */
10407
+ function findPendingTools(events, turn) {
10408
+ if (turn === null) return [];
10409
+ const called = /* @__PURE__ */ new Map();
10410
+ const resolved = /* @__PURE__ */ new Set();
10411
+ for (const e of events) {
10412
+ const data = e.data;
10413
+ if (!data || data.turn !== turn) continue;
10414
+ if (e.type === "tool/call" && typeof data.callId === "string" && typeof data.name === "string") called.set(data.callId, data.name);
10415
+ else if (e.type === "tool/result" && typeof data.callId === "string") resolved.add(data.callId);
10416
+ }
10417
+ return [...called].filter(([callId]) => !resolved.has(callId)).map(([, name]) => name);
10418
+ }
10419
+ /**
10420
+ * 扫描一个 **project 目录**(形如 `<sessions-root>/--home-yh-home-home-serenity--/`)。
10421
+ *
10422
+ * @param projectDir DSH 项目目录(其下每个子目录是一个会话)
10423
+ * @param options.lookbackMs 回看窗口(缺省 8h)
10424
+ * @param options.now 注入"当前时刻"(测试用)
10425
+ * @param options.liveIds 需要跳过的会话 id(正在跑的会话不该被判为待续)
10426
+ */
10427
+ function scanInterruptedSessions(projectDir, options = {}) {
10428
+ const t0 = Date.now();
10429
+ const lookbackMs = options.lookbackMs ?? 288e5;
10430
+ const cutoff = (options.now ?? Date.now()) - lookbackMs;
10431
+ const liveIds = options.liveIds ?? /* @__PURE__ */ new Set();
10432
+ const result = {
10433
+ sessions: [],
10434
+ scanned: 0,
10435
+ skipped: 0,
10436
+ errors: [],
10437
+ elapsedMs: 0
10438
+ };
10439
+ if (!existsSync(projectDir)) {
10440
+ result.elapsedMs = Date.now() - t0;
10441
+ return result;
10442
+ }
10443
+ let entries;
10444
+ try {
10445
+ entries = readdirSync(projectDir);
10446
+ } catch (err) {
10447
+ result.errors.push({
10448
+ sessionId: "*",
10449
+ message: err instanceof Error ? err.message : String(err)
10450
+ });
10451
+ result.elapsedMs = Date.now() - t0;
10452
+ return result;
10453
+ }
10454
+ for (const sessionId of entries) {
10455
+ if (liveIds.has(sessionId)) continue;
10456
+ const dirPath = join(projectDir, sessionId);
10457
+ const logPath = findSessionLog(dirPath);
10458
+ if (logPath === null) continue;
10459
+ let mtimeMs;
10460
+ try {
10461
+ mtimeMs = statSync(logPath).mtimeMs;
10462
+ } catch (err) {
10463
+ result.errors.push({
10464
+ sessionId,
10465
+ message: err instanceof Error ? err.message : String(err)
10466
+ });
10467
+ continue;
10468
+ }
10469
+ if (mtimeMs < cutoff) continue;
10470
+ result.scanned++;
10471
+ try {
10472
+ const events = readTailEvents(logPath, TAIL_EVENT_TARGET);
10473
+ const hit = detectInterruptedEnd(events);
10474
+ if (!hit) {
10475
+ result.skipped++;
10476
+ continue;
10477
+ }
10478
+ const tailTime = events[events.length - 1]?.time;
10479
+ result.sessions.push({
10480
+ sessionId,
10481
+ logPath,
10482
+ turn: hit.turn,
10483
+ lastEventAt: typeof tailTime === "number" ? tailTime : null,
10484
+ mtimeMs,
10485
+ pendingTools: hit.pendingTools
10486
+ });
10487
+ } catch (err) {
10488
+ result.errors.push({
10489
+ sessionId,
10490
+ message: err instanceof Error ? err.message : String(err)
10491
+ });
10492
+ }
10493
+ }
10494
+ result.sessions.sort((a, b) => (b.lastEventAt ?? b.mtimeMs) - (a.lastEventAt ?? a.mtimeMs));
10495
+ result.elapsedMs = Date.now() - t0;
10496
+ return result;
10497
+ }
10498
+ /**
10499
+ * 由 CCC 根推导它的 DSH 项目目录名(`--home-yh-home-home-serenity--`)。
10500
+ *
10501
+ * 规则取自宿主:把绝对路径的 `/` 换成 `-`,并在前后各加 `--`。
10502
+ * ⚠️ 这是**推导**,不是真相源 —— 实际目录名以磁盘为准;调用方应在不存在时
10503
+ * 回退到"扫整个 sessions root"或直接报未找到。
10504
+ */
10505
+ function projectDirNameForCwd(cwd) {
10506
+ return `--${cwd.replace(/\/+$/, "").split("/").filter(Boolean).join("-")}--`;
10507
+ }
10508
+ //#endregion
10193
10509
  //#region src/tools/cognitive-trajectory.ts
10194
10510
  /**
10195
10511
  * trajectory.ts — `cognitive_trajectory` 真实 DSH 工具定义(defineTool)
@@ -10230,6 +10546,57 @@ function listResident(root) {
10230
10546
  * 保留 osp 对齐语义的部分:create 的 `--desc`/`--issue` 二选一、use 激活即重命名 dsh 会话标题(F3)。
10231
10547
  * 活跃会话跟踪:内存 Map(按 dsh 会话 id 隔离)+ 从 events 恢复(S134)。
10232
10548
  */
10549
+ /**
10550
+ * 当前宿主里**正在跑**的 dsh 会话 id(用于把 live 会话排除出"待续"名单)。
10551
+ *
10552
+ * 🔴 **为什么必须排除**:一个正在跑的会话,其日志末条**不是** `session/end-seed`
10553
+ * (它还在写),所以按判据本来也不会命中;但排除是**双保险** ——
10554
+ * 万一某轮刚被中断、宿主还没来得及继续,把 live 会话当"待续"会导致**重复投递**。
10555
+ * 拿不到 live 列表时退化为空集(不阻断扫描,只少一层保护)。
10556
+ */
10557
+ function collectLiveSessionIds() {
10558
+ const out = /* @__PURE__ */ new Set();
10559
+ try {
10560
+ for (const s of listLiveSessions(void 0)) if (typeof s.id === "string" && s.id) out.add(s.id);
10561
+ } catch {}
10562
+ return out;
10563
+ }
10564
+ /**
10565
+ * 渲染 `interrupted` 扫描结果(人读)。
10566
+ *
10567
+ * 显示口径:**先给"有没有",再给每一条的定位信息,最后给边界说明**。
10568
+ * ⚠️ 明确写出"这些会话是否续跑由调用方决定" —— 否则读者会以为本动作已经处理了。
10569
+ */
10570
+ function renderInterruptedScan(input) {
10571
+ const { projectDir, lookbackHours, scan } = input;
10572
+ const lines = [];
10573
+ lines.push(`═══ 以 interrupted 结尾的 dsh 会话(回看 ${lookbackHours}h)═══`);
10574
+ lines.push(` 扫描域: ${projectDir}`);
10575
+ lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped} · 耗时 ${scan.elapsedMs}ms${scan.errors.length > 0 ? ` · 读取失败 ${scan.errors.length}` : ""}`);
10576
+ if (scan.sessions.length === 0) {
10577
+ lines.push("");
10578
+ lines.push("✓ 没有会话以 interrupted 结尾 —— 无需处理。");
10579
+ return lines.join("\n");
10580
+ }
10581
+ lines.push("");
10582
+ for (const s of scan.sessions) {
10583
+ const at = s.lastEventAt !== null ? new Date(s.lastEventAt).toISOString() : "(时间未知)";
10584
+ lines.push(` · ${s.sessionId}`);
10585
+ lines.push(` 轮次 turn=${s.turn ?? "?"} · 末事件 ${at}`);
10586
+ if (s.pendingTools.length > 0) lines.push(` ⚠️ 有调用无结果的工具(副作用未知): ${s.pendingTools.join(", ")}`);
10587
+ }
10588
+ lines.push("");
10589
+ lines.push("判据: 日志末条 = session/end-seed,且其前最近的 turn/end 的 reason.kind = interrupted。");
10590
+ lines.push("🔴 本动作**只读** —— 是否续跑、续哪条、几点续,由调用方(你的认知轨迹)决定。");
10591
+ lines.push("⚠️ 未完成的工具调用「副作用未知」≠「没执行」:它们可能已写文件 / 已发消息。");
10592
+ lines.push(" 续跑前应先核对副作用,而不是把那一轮整段重放。");
10593
+ if (scan.errors.length > 0) {
10594
+ lines.push("");
10595
+ lines.push("读取失败(诊断):");
10596
+ for (const e of scan.errors.slice(0, 10)) lines.push(` · ${e.sessionId}: ${e.message}`);
10597
+ }
10598
+ return lines.join("\n");
10599
+ }
10233
10600
  /** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
10234
10601
  function agentScope$1(exec) {
10235
10602
  return exec.agent?.session?.id ?? "default";
@@ -10349,6 +10716,7 @@ function createCognitiveTrajectoryTool(ctx) {
10349
10716
  "list (inventory + stats) / show (read one SESSION.md) / create (make a NEW trajectory) / use (bind THIS conversation) / rebuild (clear-and-rebuild THIS conversation in place) / archive (move a trajectory out of the active list; one-way, never overwrites).",
10350
10717
  "(B) DELIVERY — sends ONE message to ANOTHER trajectory; your own binding is unchanged: send-now (right now) / send-later (one future instant).",
10351
10718
  "(C) REFERENCE — cro-guide: how to give a trajectory its own program that decides when it should be woken.",
10719
+ "(D) INSPECTION — interrupted: list dsh sessions whose LAST turn ended with reason \"interrupted\" (the process died before that turn completed). PURE READ: no periodic scheduling, no auto-continue, no log mutation — deciding IF/WHEN/WHICH to resume belongs to the calling trajectory (this is the \"entry point only\" contract). The criterion is EXACTLY \"ends with interrupted\", NOT \"was ever interrupted\": one long session may have been interrupted dozens of times historically, and treating those as pending would resurrect sessions that already recovered. Each hit reports the turn number and the tool names that had a call but no result (best-effort — those side effects are UNKNOWN, so re-running them blindly is dangerous).",
10352
10720
  "HOW TO CHOOSE (a rule, not a guess):",
10353
10721
  "not yet bound + starting new work ⇒ create (with no prior binding it activates immediately, so the new trajectory has a carrier at once).",
10354
10722
  "already bound + recording a SEPARATE future line ⇒ create (it does NOT steal your binding; activate the new one later by send-now).",
@@ -10388,7 +10756,8 @@ function createCognitiveTrajectoryTool(ctx) {
10388
10756
  "send-now (deliver RIGHT NOW; the target needs no prior setup — if it has no live session, one is created and bound automatically) /",
10389
10757
  "send-later (one future instant + one message; fire-and-forget, and it does NOT auto-create a target).",
10390
10758
  "REFERENCE: cro-guide (no parameters: prints the CRO programming guide, including a ready-to-use sample snapshot) /",
10391
- "resident (READ-ONLY: the roster of trajectories that declared themselves RESIDENT — \"I am here, this is what I am good at, come bother me\". It lists each code with its own `serves` sentence, plus any code that wrote `resident` without `serves` (half a declaration is worse than none: people ask, then find they asked the wrong one). ⚠️ Residency is NOT a resource state — it does not mean \"never sleeps\" or \"never destroyed\"; a trajectory permanently exists either way. It governs BEING FOUND, not being kept alive, and it never touches the scheduler)."
10759
+ "resident (READ-ONLY: the roster of trajectories that declared themselves RESIDENT — \"I am here, this is what I am good at, come bother me\". It lists each code with its own `serves` sentence, plus any code that wrote `resident` without `serves` (half a declaration is worse than none: people ask, then find they asked the wrong one). ⚠️ Residency is NOT a resource state — it does not mean \"never sleeps\" or \"never destroyed\"; a trajectory permanently exists either way. It governs BEING FOUND, not being kept alive, and it never touches the scheduler).",
10760
+ "INSPECTION: interrupted (READ-ONLY: dsh sessions whose LAST turn ended with reason \"interrupted\" — the process died mid-turn. Accepts an optional lookbackHours (default 8). Deciding whether/when to resume is YOUR job — this action only answers \"which sessions are in that state right now\")."
10392
10761
  ].join(" ")
10393
10762
  },
10394
10763
  name: {
@@ -10442,6 +10811,10 @@ function createCognitiveTrajectoryTool(ctx) {
10442
10811
  message: {
10443
10812
  type: "string",
10444
10813
  description: "send-later/send-now: the single message delivered to the target cognitive trajectory. send-now may create and bind a carrier ONLY when the target has never had one — acceptable only for a delivery the user actually wants made (user-intent-driven), never for speculatively starting work."
10814
+ },
10815
+ lookbackHours: {
10816
+ type: "number",
10817
+ description: "interrupted: how far back to look, in hours (default 8). Sessions whose current-generation log was last written earlier than this are not examined at all."
10445
10818
  }
10446
10819
  },
10447
10820
  output: {
@@ -10636,6 +11009,34 @@ function createCognitiveTrajectoryTool(ctx) {
10636
11009
  * 🔴 **不进系统提示词**(owner 裁决 Q2:说明类信息按需取,不占每轮 token)。
10637
11010
  */
10638
11011
  return listResident(root);
11012
+ case "interrupted": {
11013
+ /**
11014
+ * 以 `interrupted` 结尾的 dsh 会话(owner 2026-10-22 令:**只出入口**)。
11015
+ *
11016
+ * 🔴 **边界(owner 原话:「我们 ACC 不关心周期」)**:本动作**只回答**"此刻哪些会话
11017
+ * 的最后一轮以 interrupted 结束",**不判周期、不自动续跑、不改日志**。
11018
+ * 何时扫、续谁、怎么续 = 调用方(CCC 的认知轨迹)的事。
11019
+ *
11020
+ * 🔴 **为什么不是"曾经中断过"**:长会话历史上可中断几十次(实测单会话 33 次);
11021
+ * 按"曾经"判会把早已恢复的会话全部当成待续 ⇒ 灾难。判据在
11022
+ * `trajectory/interrupted.ts` 的 `detectInterruptedEnd`:**末条 = end-seed
11023
+ * 且其前最近的 turn/end 是 interrupted**。
11024
+ *
11025
+ * 🔴 **正在跑的会话要排除**:live 会话不该被判成"待续"(它正在跑,不是中断)。
11026
+ */
11027
+ const lookbackHours = typeof args.lookbackHours === "number" && args.lookbackHours > 0 ? args.lookbackHours : DEFAULT_LOOKBACK_MS / 36e5;
11028
+ const sessionsRootPath = sessionsRootDir();
11029
+ const cwd = exec.agent?.session?.header?.cwd ?? root;
11030
+ const projectDir = join(sessionsRootPath, projectDirNameForCwd(cwd));
11031
+ return renderInterruptedScan({
11032
+ projectDir,
11033
+ lookbackHours,
11034
+ scan: scanInterruptedSessions(projectDir, {
11035
+ lookbackMs: lookbackHours * 36e5,
11036
+ liveIds: collectLiveSessionIds()
11037
+ })
11038
+ });
11039
+ }
10639
11040
  default: throw new Error(`Unknown action: ${args.action}`);
10640
11041
  }
10641
11042
  }
@@ -14395,7 +14796,7 @@ function registerStatusApi(ctx, opts = {}) {
14395
14796
  });
14396
14797
  }
14397
14798
  const { saveWeixinRoutes } = await import("./route-3OzOAjec.js").then((n) => n.l);
14398
- const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
14799
+ const { findDirNameByCode } = await import("./profile-CQWuuopQ.js").then((n) => n.a);
14399
14800
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
14400
14801
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
14401
14802
  return;
@@ -14543,7 +14944,7 @@ function registerStatusApi(ctx, opts = {}) {
14543
14944
  const id = s.id ?? s.header?.id;
14544
14945
  if (id) liveIds.add(id);
14545
14946
  }
14546
- const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-C-92qnDs.js").then((n) => n.t);
14947
+ const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-DxX78__Z.js").then((n) => n.t);
14547
14948
  const root = sessionsRootDir();
14548
14949
  if (req.method === "GET") {
14549
14950
  const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
@@ -15908,9 +16309,9 @@ function transformHtmlForProxy(raw, upstreamHeaders) {
15908
16309
  const enc = String(upstreamHeaders["content-encoding"] ?? "").toLowerCase().trim();
15909
16310
  const outHeaders = { ...upstreamHeaders };
15910
16311
  if (enc === "gzip" || enc === "x-gzip" || enc === "deflate" || enc === "br") try {
15911
- if (enc === "br") body = zlib.brotliDecompressSync(body);
15912
- else if (enc === "deflate") body = zlib.inflateSync(body);
15913
- else body = zlib.gunzipSync(body);
16312
+ if (enc === "br") body = nodeZlib.brotliDecompressSync(body);
16313
+ else if (enc === "deflate") body = nodeZlib.inflateSync(body);
16314
+ else body = nodeZlib.gunzipSync(body);
15914
16315
  delete outHeaders["content-encoding"];
15915
16316
  } catch {
15916
16317
  return null;
@@ -19438,7 +19839,7 @@ const MEDIA_MAX_BYTES = 20971520;
19438
19839
  */
19439
19840
  async function resolveCccRootByAlias(ctx, alias) {
19440
19841
  try {
19441
- const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
19842
+ const { listCccs } = await import("./ccc-roots-CR1IWLDJ.js").then((n) => n.a);
19442
19843
  const entries = await listCccs(ctx);
19443
19844
  const byName = entries.find((e) => e.name === alias);
19444
19845
  if (byName) return byName.root;
@@ -19506,7 +19907,7 @@ async function resolveInboundTarget(ctx, input) {
19506
19907
  });
19507
19908
  return null;
19508
19909
  }
19509
- const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
19910
+ const { findDirNameByCode } = await import("./profile-CQWuuopQ.js").then((n) => n.a);
19510
19911
  if (findDirNameByCode(root, target.role) === null) {
19511
19912
  const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
19512
19913
  console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
@@ -20460,7 +20861,7 @@ function syncHumanChannel(ctx) {
20460
20861
  (async () => {
20461
20862
  try {
20462
20863
  stopMachineLoops();
20463
- const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
20864
+ const { listCccs } = await import("./ccc-roots-CR1IWLDJ.js").then((n) => n.a);
20464
20865
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
20465
20866
  } catch {}
20466
20867
  })();
@@ -20753,7 +21154,7 @@ function matchCcc(input, candidates) {
20753
21154
  }
20754
21155
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
20755
21156
  async function collectCandidates(ctx) {
20756
- const { listCccs } = await import("./ccc-roots-BI_4UVcj.js").then((n) => n.a);
21157
+ const { listCccs } = await import("./ccc-roots-CR1IWLDJ.js").then((n) => n.a);
20757
21158
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
20758
21159
  const seen = /* @__PURE__ */ new Set();
20759
21160
  const out = [];
@@ -22431,85 +22832,6 @@ function createImBridgeTool() {
22431
22832
  });
22432
22833
  }
22433
22834
  //#endregion
22434
- //#region src/trajectory/live.ts
22435
- /**
22436
- * trajectory/live.ts — 进程内 live 会话取数 + 精简诊断
22437
- *
22438
- * ── 为什么单独成模块(R↓,2026-09-15 S142「ACC 侧 autopilot 退场」S1 搬迁)──
22439
- * 这两个函数原先住在 `autopilot-trajectory.ts`,但它们的**主语不是 autopilot**:
22440
- * `listLiveSessions` 服务 `acc-diag` ① 段的"**哪个会话属于哪个 CCC**"(会话维度,
22441
- * 不是 CCC 维度),而"**独立进程看不到运行时**"正是 `acc-diag` 存在的理由。
22442
- * autopilot 退场后它们必须活下去 ⇒ 独立成模块,不再挂着已死机制的名字。
22443
- *
22444
- * ── 迁移边界(不搬什么,以及为什么)──────────────────────────────────────
22445
- * 原 `diagLive` 还含两段,**均不搬**(其主语是 autopilot,随机制退场):
22446
- * · `autopilotCccs`(逐 CCC 目标命中 / agent 定位诊断)—— 依赖 `readAutopilotSettings` /
22447
- * `getAutopilotStatus` / `resolveTargetAgent`
22448
- * · `panelResolved`(无参面板请求落到哪个 CCC)—— 面板区块随机制退场
22449
- * 留下的 = **进程 cwd/CCC + live 会话清单**(`acc-diag` ① 段仍需要)。
22450
- *
22451
- * 判据纪律:本模块**不得**反向依赖 autopilot 判据层(那会让退场半途而废)。
22452
- *
22453
- * ── 🔴 2026-09-25 I2 收敛(约束文档 §2.7 违反清单,L1 违反之一)────────────────
22454
- * **本模块的 `ctx` 参数类型从 `Context` 改为 `unknown`** —— 消除本文件唯一的 `W2` 接触
22455
- * (直接 import 宿主 `cordis` 的类型)。
22456
- *
22457
- * **为什么这是"真解耦"而不是"把类型藏起来"(判据):** `ctx` 在本模块**从不被解引用** ——
22458
- * 它只被**原样透传**给 `hostSessions(ctx)`,而后者(`host/access.ts`,**L0**)的参数
22459
- * 本就声明为 `ctx: unknown` ⇒ `Context` 这个 import **在语义上是多余的**,
22460
- * 它没有参与任何类型检查,只贡献了一处 L1→宿主 的接触点。
22461
- * 🔵 **证据**:grep 本文件 `ctx` 全部 5 处命中 —— 3 处在参数/调用位、2 处在注释,
22462
- * **零处**属性访问(`ctx.xxx`)。
22463
- */
22464
- /**
22465
- * 从 dsh 会话 log 读取标题(latest-wins `session/title` 事件)——
22466
- * **标题不在 `sessions.list()` 条目上**(wire/对象均无 title 字段;F3 命名经
22467
- * `sessionTitle.rename` 写进 session log),必须从 events 提取(rebuild 同款读取模式)。
22468
- */
22469
- function readSessionTitle(session) {
22470
- try {
22471
- const events = sessionEvents(session);
22472
- for (let i = events.length - 1; i >= 0; i--) {
22473
- const e = events[i];
22474
- if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
22475
- }
22476
- } catch {}
22477
- return null;
22478
- }
22479
- /**
22480
- * 逐条 live 会话(`cccRoot` = 该会话 cwd 的 CCC 归属,无 → null)。
22481
- *
22482
- * CCC 枚举("本机有哪些 CCC")**不在这里**,归 `ccc-roots.listCccs`(并集语义);
22483
- * 本函数保留的是它**独有**的那部分:会话维度 + cwd 归属 + 标题 + id。
22484
- *
22485
- * @param ctx 插件上下文(**只透传**给 L0 取数口,本模块不解引用 ⇒ 类型为 `unknown`)
22486
- * @returns 逐条 live 会话
22487
- */
22488
- function listLiveSessions(ctx) {
22489
- const out = [];
22490
- try {
22491
- const sessions = hostSessions(ctx);
22492
- for (const s of sessions?.list?.() ?? []) {
22493
- const cwd = s?.header?.cwd ?? null;
22494
- out.push({
22495
- id: s?.id ?? "",
22496
- cwd,
22497
- cccRoot: cwd ? cccRootForCwd(cwd) : null,
22498
- title: readSessionTitle(s)
22499
- });
22500
- }
22501
- } catch {}
22502
- return out;
22503
- }
22504
- function diagLive(ctx) {
22505
- const processCwd = process.cwd();
22506
- return {
22507
- processCwd,
22508
- processCcc: cccRootForCwd(processCwd),
22509
- liveSessions: listLiveSessions(ctx)
22510
- };
22511
- }
22512
- //#endregion
22513
22835
  //#region src/surface/observation.ts
22514
22836
  /**
22515
22837
  * surface/observation.ts — 容器状态模型的**取数层唯一出口**(C5「观察面归一」,S142 2026-09-15)
@@ -1,6 +1,6 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-BI_4UVcj.js";
1
+ import { r as cccRootForCwd } from "./ccc-roots-CR1IWLDJ.js";
2
2
  import { _ as resolveInside, g as readUtf8 } from "./ccc-CyYJL4F2.js";
3
- import { D as parseTrajectoryCode, M as sessionsRoot, S as getLastActiveSessionInfo, j as sessionEvents, x as getActiveSessionInfo } from "./profile-vHheCJiU.js";
3
+ import { D as parseTrajectoryCode, M as sessionsRoot, S as getLastActiveSessionInfo, j as sessionEvents, x as getActiveSessionInfo } from "./profile-CQWuuopQ.js";
4
4
  import { dirname, join } from "node:path";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
@@ -244,7 +244,8 @@ const COGNITIVE_TRAJECTORY_ACTIONS = [
244
244
  "send-now",
245
245
  "send-later",
246
246
  "cro-guide",
247
- "resident"
247
+ "resident",
248
+ "interrupted"
248
249
  ];
249
250
  /**
250
251
  * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
@@ -1,8 +1,8 @@
1
- import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-BI_4UVcj.js";
1
+ import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-CR1IWLDJ.js";
2
2
  import { g as readUtf8, o as isSafeModeOn, p as readCognitiveTrajectorySkills, u as readBlacklist } from "./ccc-CyYJL4F2.js";
3
- import { M as sessionsRoot, O as readActiveSessionMd, V as isoLocal, d as trajectoryDirNameForSession, f as unregisterTrajectorySession$1, l as trajectoryConstraintForSession, m as DEFAULT_SESSION_SCOPE, o as readTrajectoryConstraintAt, u as registerTrajectorySession$1 } from "./profile-vHheCJiU.js";
3
+ import { M as sessionsRoot, O as readActiveSessionMd, V as isoLocal, d as trajectoryDirNameForSession, f as unregisterTrajectorySession$1, l as trajectoryConstraintForSession, m as DEFAULT_SESSION_SCOPE, o as readTrajectoryConstraintAt, u as registerTrajectorySession$1 } from "./profile-CQWuuopQ.js";
4
4
  import { a as readGitTrack, i as localstorePath } from "./service-DDmHSglV.js";
5
- import { d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt, v as ACC_VERSION } from "./message-source-DRkQwfgh.js";
5
+ import { d as constraintCognitiveTrajectory, m as profileMsmWhitelist, n as PLUGIN_SOURCE, o as readLastBound, p as createProfilePromptReader, u as buildTrajectoryBasePrompt, v as ACC_VERSION } from "./message-source-HLXLQuvR.js";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -1,4 +1,4 @@
1
- import { f as hostSettings } from "./ccc-roots-BI_4UVcj.js";
1
+ import { f as hostSettings } from "./ccc-roots-CR1IWLDJ.js";
2
2
  //#region src/surface/settings.ts
3
3
  /**
4
4
  * 设置面板命名空间 = **profile 条目 id**(v0.1.7 起;与旧 `settings.yaml` 段名逐字相同)。
@@ -62,8 +62,15 @@
62
62
  * 2026-10-20(D173 兑现,owner 令):「常驻」= 轨迹的**自我声明**,落地为 **`resident`** 只读动作。
63
63
  * 判据:它管的是**被找到**,不是**不被销毁** ⇒ 纯读、不碰生命周期、不进系统提示词。
64
64
  * ⚠️ 这也修正了 v1.77.1 的一处说明面失真(手册曾描述一个**不存在**的动作,D179(a))。
65
+ * 2026-10-22(owner 令「在 trajectory 加上触发入口即可,剩下的 CCC 搞定」):新增
66
+ * **`interrupted`** —— 列出**以 `interrupted` 结尾**的 dsh 会话(进程在轮次完成前退出)。
67
+ * 判据(R↓,为何 ACC 只出**入口**不管周期):**周期性归属调用方的认知轨迹**,
68
+ * ACC 只回答"此刻哪些会话以 interrupted 结尾"这一个问题。⇒ 本动作**纯读**、
69
+ * **不判周期、不自动续跑、不改任何会话日志**(是否续、续谁、几点续 = CCC 的事)。
70
+ * ⚠️ 判据的锋利处:**只认"以 interrupted 结尾"**,不认"曾经中断过"—— 长会话历史上
71
+ * 可中断几十次(实测单会话 33 次),按"曾经"判会把早已恢复的会话全部当成待续。
65
72
  */
66
- type CognitiveTrajectoryAction = 'list' | 'show' | 'create' | 'use' | 'rebuild' | 'archive' | 'uid' | 'send-now' | 'send-later' | 'cro-guide' | 'resident';
73
+ type CognitiveTrajectoryAction = 'list' | 'show' | 'create' | 'use' | 'rebuild' | 'archive' | 'uid' | 'send-now' | 'send-later' | 'cro-guide' | 'resident' | 'interrupted';
67
74
  export declare const COGNITIVE_TRAJECTORY_ACTIONS: readonly CognitiveTrajectoryAction[];
68
75
  /**
69
76
  * 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
@@ -0,0 +1,122 @@
1
+ /**
2
+ * 中断轮次发现(`interrupted` 会话扫描)
3
+ *
4
+ * ## 这个模块解决什么
5
+ *
6
+ * DSH 的会话日志里,**进程在某一轮完成前退出**(被 kill/崩溃/关机超时)时,
7
+ * 宿主重放时会为该轮补上收尾事件:
8
+ *
9
+ * ```jsonl
10
+ * turn/end {"turn":66,"reason":{"kind":"interrupted"}}
11
+ * step/end {"turn":66,"step":85}
12
+ * session/end-seed {}
13
+ * ```
14
+ *
15
+ * 宿主**只补收尾,不续跑** —— 那一轮的工作就此停在那里。本模块负责把这些会话找出来,
16
+ * 交给上层(认知轨迹)决定要不要续。
17
+ *
18
+ * ## 判据(三条同时成立,缺一不可)
19
+ *
20
+ * 1. 日志**最后一条事件是 `session/end-seed`** —— 这是"上一进程已结束、本进程重放"的标记;
21
+ * 2. 该标记**之前最近的 `turn/end`** 的 `reason.kind === 'interrupted'`;
22
+ * 3. `session/end-seed` 之后**没有新事件**(由判据 1 保证)。
23
+ *
24
+ * ⚠️ **为什么不能只看"存在过 interrupted"**:一个长会话历史上可能中断几十次
25
+ * (本容器实测单会话 33 次)。只看"曾经中断"会把早已恢复的会话全部当成待续 ⇒ 灾难。
26
+ *
27
+ * ## 为什么不整文件解压
28
+ *
29
+ * DSH 日志是 **zstd 多帧追加**(实测单会话 24705 帧 / 47 MB)。原生
30
+ * `zlib.zstdDecompressSync` 只解**第一帧**,对多帧直接报 `Unknown frame descriptor`。
31
+ * ⇒ 采用**尾部窗口 + 倒序逐帧解压**:只读文件末尾 N 字节,从后往前找帧魔数,
32
+ * 解到凑够所需事件数即停。实测 8h 窗口 6 个会话 **20 ms**。
33
+ *
34
+ * ## 世代识别复用 `cleanup.ts`
35
+ *
36
+ * 日志文件名有多个世代(`session.jsonl` / `session.v3.jsonl.zstd` / `session.v4.jsonl.zstd`…),
37
+ * 宿主按世代写且**迁移保留源文件**。⇒ 本模块**不硬编码文件名**,走
38
+ * {@link findSessionLog}(与清理/体积统计同一判据),避免"新世代对本模块不可见"。
39
+ *
40
+ * ## 边界
41
+ *
42
+ * - **只读**:不改任何会话日志。
43
+ * - **不判周期**:何时扫、扫完做什么归调用方(CCC 的认知轨迹)。本模块只回答
44
+ * 「此刻哪些会话以 interrupted 结尾」。
45
+ */
46
+ /** zstd 是否可用(不可用时扫描会返回"读不到事件",而不是假装成功) */
47
+ export declare const ZSTD_AVAILABLE: boolean;
48
+ /** 默认回看窗口:8 小时(owner 2026-10-22 定) */
49
+ export declare const DEFAULT_LOOKBACK_MS: number;
50
+ /** 日志里被我们识别的事件形状(只取用得到的字段) */
51
+ interface SessionEvent {
52
+ type?: string;
53
+ time?: number;
54
+ data?: {
55
+ turn?: number;
56
+ step?: number;
57
+ reason?: {
58
+ kind?: string;
59
+ };
60
+ };
61
+ }
62
+ /** 一个"以 interrupted 结尾"的会话 */
63
+ export interface InterruptedSession {
64
+ /** 会话目录名(= dsh 会话 id) */
65
+ sessionId: string;
66
+ /** 日志绝对路径(当前世代) */
67
+ logPath: string;
68
+ /** 被中断的轮次号 */
69
+ turn: number | null;
70
+ /** 日志最后一条事件的时间(epoch ms,取不到则 null) */
71
+ lastEventAt: number | null;
72
+ /** 日志文件 mtime(epoch ms) */
73
+ mtimeMs: number;
74
+ /** 该轮里"有 call 无 result"的工具名(**尽力而为**,可能因尾部窗口截断而不全) */
75
+ pendingTools: string[];
76
+ }
77
+ /** 扫描结果 */
78
+ export interface ScanResult {
79
+ /** 以 interrupted 结尾的会话(最新在前) */
80
+ sessions: InterruptedSession[];
81
+ /** 窗口内实际检查的会话数 */
82
+ scanned: number;
83
+ /** 窗口内但未命中(含正常结束)的会话数 */
84
+ skipped: number;
85
+ /** 读取失败条目(诊断用,不阻断) */
86
+ errors: Array<{
87
+ sessionId: string;
88
+ message: string;
89
+ }>;
90
+ /** 扫描耗时(ms) */
91
+ elapsedMs: number;
92
+ }
93
+ /**
94
+ * 判定一组尾部事件是否「以 interrupted 结尾」。
95
+ * 不命中 → null。
96
+ */
97
+ export declare function detectInterruptedEnd(events: SessionEvent[]): {
98
+ turn: number | null;
99
+ pendingTools: string[];
100
+ } | null;
101
+ /**
102
+ * 扫描一个 **project 目录**(形如 `<sessions-root>/--home-yh-home-home-serenity--/`)。
103
+ *
104
+ * @param projectDir DSH 项目目录(其下每个子目录是一个会话)
105
+ * @param options.lookbackMs 回看窗口(缺省 8h)
106
+ * @param options.now 注入"当前时刻"(测试用)
107
+ * @param options.liveIds 需要跳过的会话 id(正在跑的会话不该被判为待续)
108
+ */
109
+ export declare function scanInterruptedSessions(projectDir: string, options?: {
110
+ lookbackMs?: number;
111
+ now?: number;
112
+ liveIds?: ReadonlySet<string>;
113
+ }): ScanResult;
114
+ /**
115
+ * 由 CCC 根推导它的 DSH 项目目录名(`--home-yh-home-home-serenity--`)。
116
+ *
117
+ * 规则取自宿主:把绝对路径的 `/` 换成 `-`,并在前后各加 `--`。
118
+ * ⚠️ 这是**推导**,不是真相源 —— 实际目录名以磁盘为准;调用方应在不存在时
119
+ * 回退到"扫整个 sessions root"或直接报未找到。
120
+ */
121
+ export declare function projectDirNameForCwd(cwd: string): string;
122
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.87.3",
3
+ "version": "1.87.4",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「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(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {