@shgroup/dsh-serenity-hooks 1.87.3 → 1.87.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dsh.plugin.json +1 -1
- package/lib/{ccc-roots-BI_4UVcj.js → ccc-roots-CvgsUnFW.js} +1 -1
- package/lib/{cleanup-C-92qnDs.js → cleanup-DxX78__Z.js} +1 -1
- package/lib/index.js +652 -98
- package/lib/{message-source-DRkQwfgh.js → message-source-Cq4PsuES.js} +2 -2
- package/lib/{profile-vHheCJiU.js → profile-DGt_e-Nx.js} +3 -1
- package/lib/{session-DZXSLLEx.js → session-Ca5UYzF3.js} +3 -3
- package/lib/{settings-H7TVDayZ.js → settings-BI3NRHgC.js} +1 -1
- package/lib/trajectory/core.d.ts +15 -1
- package/lib/trajectory/interrupted.d.ts +171 -0
- 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.87.
|
|
3
|
+
"version": "1.87.5",
|
|
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-
|
|
230
|
+
const { listTrajectoryConstraints } = await import("./profile-DGt_e-Nx.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 {
|
|
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-
|
|
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";
|
|
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-
|
|
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";
|
|
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-
|
|
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-
|
|
8
|
-
import { n as
|
|
9
|
-
import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-
|
|
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";
|
|
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,449 @@ 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
|
+
/**
|
|
10509
|
+
* 续跑正文(**简单、稳定、不含版本号**)。
|
|
10510
|
+
*
|
|
10511
|
+
* 判据(为什么这么短):本动作的用途是**把被打断的会话推回工作状态**,
|
|
10512
|
+
* 不是替它重述任务 —— 被打断那一轮的上下文就在它自己的日志里。
|
|
10513
|
+
* ⇒ 正文只做三件事:① 说明发生了什么;② 提示**先核对副作用**;③ 让它接着做。
|
|
10514
|
+
*
|
|
10515
|
+
* 🔴 **为什么要写"先核对副作用"**:中断时可能有「有 call 无 result」的工具调用,
|
|
10516
|
+
* 它们的副作用**未知**(可能已写文件 / 已发消息)。若模型把那一轮整段重放,
|
|
10517
|
+
* 会**重复执行**。这句话是给模型的护栏,不是客套。
|
|
10518
|
+
*/
|
|
10519
|
+
function buildContinueText(sessionId, turn, pendingTools) {
|
|
10520
|
+
const lines = [];
|
|
10521
|
+
lines.push("[中断续跑] 上一条进程在本轮完成前退出,该轮已被中止。现在继续。");
|
|
10522
|
+
lines.push("");
|
|
10523
|
+
lines.push(`会话: ${sessionId}${turn === null ? "" : ` / 被中断的轮次: turn ${turn}`}`);
|
|
10524
|
+
if (pendingTools.length > 0) {
|
|
10525
|
+
lines.push("");
|
|
10526
|
+
lines.push(`⚠️ 中断时这些工具「有调用、无结果」(副作用未知,**可能已经执行**): ${pendingTools.join(", ")}`);
|
|
10527
|
+
lines.push(" ⇒ 先核对它们是否已生效(文件是否已写、消息是否已发),再决定要不要重做。");
|
|
10528
|
+
lines.push(" ⛔ 不要把被中断的那一轮整段重放。");
|
|
10529
|
+
}
|
|
10530
|
+
lines.push("");
|
|
10531
|
+
lines.push("请接着完成该轮未完成的工作;完成后把进展写入本轨迹的 SESSION.md。");
|
|
10532
|
+
return lines.join("\n");
|
|
10533
|
+
}
|
|
10534
|
+
/**
|
|
10535
|
+
* 把一个 agent 唤醒(注入一条续跑消息)。
|
|
10536
|
+
*
|
|
10537
|
+
* 两条路径(与 `wake/scheduler.ts` 的 `sendToCognitiveTrajectory` 同源判据):
|
|
10538
|
+
* - **live**:`agents.get(id)` 拿得到 ⇒ 目标在跑(或被加载)⇒ `steer` 当场注入当前轮;
|
|
10539
|
+
* `steer` 不可用则退回 `followup`(宁可排队,不可静默丢)。
|
|
10540
|
+
* - **cold-resume**:`sessionController.resolveAgent(id)` 冷载入后同样注入。
|
|
10541
|
+
*
|
|
10542
|
+
* ⚠️ 冷会话载入**会真起一个 agent**(消耗真实算力)⇒ 调用方必须自己决定要不要,
|
|
10543
|
+
* 本函数不做数量限制(那是策略,归调用者)。
|
|
10544
|
+
*/
|
|
10545
|
+
function createResumeOne(deps) {
|
|
10546
|
+
return async (hit) => {
|
|
10547
|
+
const { sessionId } = hit;
|
|
10548
|
+
const text = buildContinueText(sessionId, hit.turn, hit.pendingTools);
|
|
10549
|
+
let agent;
|
|
10550
|
+
let how;
|
|
10551
|
+
const live = deps.agents?.get?.(sessionId);
|
|
10552
|
+
if (live) {
|
|
10553
|
+
agent = live;
|
|
10554
|
+
how = "live";
|
|
10555
|
+
} else {
|
|
10556
|
+
const sc = deps.sessionController;
|
|
10557
|
+
const resolve = sc?.resolveAgent;
|
|
10558
|
+
if (typeof resolve !== "function" || sc === void 0) return {
|
|
10559
|
+
sessionId,
|
|
10560
|
+
ok: false,
|
|
10561
|
+
error: "宿主 sessionController 不可用(重启启动窗口?)—— 稍后重试"
|
|
10562
|
+
};
|
|
10563
|
+
try {
|
|
10564
|
+
const out = await resolve.call(sc, sessionId);
|
|
10565
|
+
if (!out?.agent) return {
|
|
10566
|
+
sessionId,
|
|
10567
|
+
ok: false,
|
|
10568
|
+
error: `冷载入未返回 agent: ${String(out?.error ?? "未知原因")}`
|
|
10569
|
+
};
|
|
10570
|
+
agent = out.agent;
|
|
10571
|
+
how = "cold-resume";
|
|
10572
|
+
} catch (err) {
|
|
10573
|
+
return {
|
|
10574
|
+
sessionId,
|
|
10575
|
+
ok: false,
|
|
10576
|
+
error: `冷载入失败: ${err instanceof Error ? err.message : String(err)}`
|
|
10577
|
+
};
|
|
10578
|
+
}
|
|
10579
|
+
}
|
|
10580
|
+
const a = agent;
|
|
10581
|
+
try {
|
|
10582
|
+
if (typeof a.steer === "function" && a.status === "running") {
|
|
10583
|
+
a.steer(deps.makeMessage(text));
|
|
10584
|
+
return {
|
|
10585
|
+
sessionId,
|
|
10586
|
+
ok: true,
|
|
10587
|
+
how: `${how}+steer`
|
|
10588
|
+
};
|
|
10589
|
+
}
|
|
10590
|
+
if (typeof a.followup === "function") {
|
|
10591
|
+
a.followup(deps.makeMessage(text));
|
|
10592
|
+
return {
|
|
10593
|
+
sessionId,
|
|
10594
|
+
ok: true,
|
|
10595
|
+
how: `${how}+followup`
|
|
10596
|
+
};
|
|
10597
|
+
}
|
|
10598
|
+
if (typeof a.steer === "function") {
|
|
10599
|
+
a.steer(deps.makeMessage(text));
|
|
10600
|
+
return {
|
|
10601
|
+
sessionId,
|
|
10602
|
+
ok: true,
|
|
10603
|
+
how: `${how}+steer`
|
|
10604
|
+
};
|
|
10605
|
+
}
|
|
10606
|
+
return {
|
|
10607
|
+
sessionId,
|
|
10608
|
+
ok: false,
|
|
10609
|
+
how,
|
|
10610
|
+
error: "agent 既无 steer 也无 followup(宿主契约漂移)"
|
|
10611
|
+
};
|
|
10612
|
+
} catch (err) {
|
|
10613
|
+
if (typeof a.followup === "function") try {
|
|
10614
|
+
a.followup(deps.makeMessage(text));
|
|
10615
|
+
return {
|
|
10616
|
+
sessionId,
|
|
10617
|
+
ok: true,
|
|
10618
|
+
how: `${how}+followup(steer 失败后退回)`
|
|
10619
|
+
};
|
|
10620
|
+
} catch (err2) {
|
|
10621
|
+
return {
|
|
10622
|
+
sessionId,
|
|
10623
|
+
ok: false,
|
|
10624
|
+
error: `steer 与 followup 均失败: ${String(err2?.message ?? err2)}`
|
|
10625
|
+
};
|
|
10626
|
+
}
|
|
10627
|
+
return {
|
|
10628
|
+
sessionId,
|
|
10629
|
+
ok: false,
|
|
10630
|
+
error: `注入失败: ${err instanceof Error ? err.message : String(err)}`
|
|
10631
|
+
};
|
|
10632
|
+
}
|
|
10633
|
+
};
|
|
10634
|
+
}
|
|
10635
|
+
//#endregion
|
|
10193
10636
|
//#region src/tools/cognitive-trajectory.ts
|
|
10194
10637
|
/**
|
|
10195
10638
|
* trajectory.ts — `cognitive_trajectory` 真实 DSH 工具定义(defineTool)
|
|
@@ -10230,6 +10673,102 @@ function listResident(root) {
|
|
|
10230
10673
|
* 保留 osp 对齐语义的部分:create 的 `--desc`/`--issue` 二选一、use 激活即重命名 dsh 会话标题(F3)。
|
|
10231
10674
|
* 活跃会话跟踪:内存 Map(按 dsh 会话 id 隔离)+ 从 events 恢复(S134)。
|
|
10232
10675
|
*/
|
|
10676
|
+
/**
|
|
10677
|
+
* 当前宿主里**正在跑**的 dsh 会话 id(用于把 live 会话排除出"待续"名单)。
|
|
10678
|
+
*
|
|
10679
|
+
* 🔴 **为什么必须排除**:一个正在跑的会话,其日志末条**不是** `session/end-seed`
|
|
10680
|
+
* (它还在写),所以按判据本来也不会命中;但排除是**双保险** ——
|
|
10681
|
+
* 万一某轮刚被中断、宿主还没来得及继续,把 live 会话当"待续"会导致**重复投递**。
|
|
10682
|
+
* 拿不到 live 列表时退化为空集(不阻断扫描,只少一层保护)。
|
|
10683
|
+
*/
|
|
10684
|
+
function collectLiveSessionIds() {
|
|
10685
|
+
const out = /* @__PURE__ */ new Set();
|
|
10686
|
+
try {
|
|
10687
|
+
for (const s of listLiveSessions(void 0)) if (typeof s.id === "string" && s.id) out.add(s.id);
|
|
10688
|
+
} catch {}
|
|
10689
|
+
return out;
|
|
10690
|
+
}
|
|
10691
|
+
/**
|
|
10692
|
+
* 渲染 `interrupted` 扫描结果(人读)。
|
|
10693
|
+
*
|
|
10694
|
+
* 显示口径:**先给"有没有",再给每一条的定位信息,最后给边界说明**。
|
|
10695
|
+
* ⚠️ 明确写出"这些会话是否续跑由调用方决定" —— 否则读者会以为本动作已经处理了。
|
|
10696
|
+
*/
|
|
10697
|
+
function renderInterruptedScan(input) {
|
|
10698
|
+
const { projectDir, lookbackHours, scan } = input;
|
|
10699
|
+
const lines = [];
|
|
10700
|
+
lines.push(`═══ 以 interrupted 结尾的 dsh 会话(回看 ${lookbackHours}h)═══`);
|
|
10701
|
+
lines.push(` 扫描域: ${projectDir}`);
|
|
10702
|
+
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped} · 耗时 ${scan.elapsedMs}ms${scan.errors.length > 0 ? ` · 读取失败 ${scan.errors.length}` : ""}`);
|
|
10703
|
+
if (scan.sessions.length === 0) {
|
|
10704
|
+
lines.push("");
|
|
10705
|
+
lines.push("✓ 没有会话以 interrupted 结尾 —— 无需处理。");
|
|
10706
|
+
return lines.join("\n");
|
|
10707
|
+
}
|
|
10708
|
+
lines.push("");
|
|
10709
|
+
for (const s of scan.sessions) {
|
|
10710
|
+
const at = s.lastEventAt !== null ? new Date(s.lastEventAt).toISOString() : "(时间未知)";
|
|
10711
|
+
lines.push(` · ${s.sessionId}`);
|
|
10712
|
+
lines.push(` 轮次 turn=${s.turn ?? "?"} · 末事件 ${at}`);
|
|
10713
|
+
if (s.pendingTools.length > 0) lines.push(` ⚠️ 有调用无结果的工具(副作用未知): ${s.pendingTools.join(", ")}`);
|
|
10714
|
+
}
|
|
10715
|
+
lines.push("");
|
|
10716
|
+
lines.push("判据: 日志末条 = session/end-seed,且其前最近的 turn/end 的 reason.kind = interrupted。");
|
|
10717
|
+
lines.push("🔴 本动作**只读** —— 是否续跑、续哪条、几点续,由调用方(你的认知轨迹)决定。");
|
|
10718
|
+
lines.push("⚠️ 未完成的工具调用「副作用未知」≠「没执行」:它们可能已写文件 / 已发消息。");
|
|
10719
|
+
lines.push(" 续跑前应先核对副作用,而不是把那一轮整段重放。");
|
|
10720
|
+
if (scan.errors.length > 0) {
|
|
10721
|
+
lines.push("");
|
|
10722
|
+
lines.push("读取失败(诊断):");
|
|
10723
|
+
for (const e of scan.errors.slice(0, 10)) lines.push(` · ${e.sessionId}: ${e.message}`);
|
|
10724
|
+
}
|
|
10725
|
+
return lines.join("\n");
|
|
10726
|
+
}
|
|
10727
|
+
/**
|
|
10728
|
+
* 渲染 `continue-all-interrupted` 的结果(人读)。
|
|
10729
|
+
*
|
|
10730
|
+
* 三种形态必须能一眼分清:**dryRun(只预告)** / **有命中且全成功** / **有失败**。
|
|
10731
|
+
* ⚠️ 明确写出"已注入 ≠ 已跑完" —— 与 `send-now` 的回执口径一致(D-族:回执只到注入)。
|
|
10732
|
+
*/
|
|
10733
|
+
function renderContinueResult(input) {
|
|
10734
|
+
const { projectDir, lookbackHours, scan, outcomes, dryRun } = input;
|
|
10735
|
+
const lines = [];
|
|
10736
|
+
const head = dryRun ? "续跑预告(dryRun,未注入任何消息)" : "中断续跑结果";
|
|
10737
|
+
lines.push(`═══ ${head}(回看 ${lookbackHours}h)═══`);
|
|
10738
|
+
lines.push(` 扫描域: ${projectDir}`);
|
|
10739
|
+
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped} · 耗时 ${scan.elapsedMs}ms`);
|
|
10740
|
+
if (scan.sessions.length === 0) {
|
|
10741
|
+
lines.push("");
|
|
10742
|
+
lines.push("✓ 没有会话以 interrupted 结尾 —— 无需续跑。");
|
|
10743
|
+
return lines.join("\n");
|
|
10744
|
+
}
|
|
10745
|
+
lines.push("");
|
|
10746
|
+
if (dryRun) {
|
|
10747
|
+
lines.push("将对这些会话注入续跑消息:");
|
|
10748
|
+
for (const s of scan.sessions) {
|
|
10749
|
+
const at = s.lastEventAt !== null ? new Date(s.lastEventAt).toISOString() : "(时间未知)";
|
|
10750
|
+
lines.push(` · ${s.sessionId} turn=${s.turn ?? "?"} 末事件 ${at}`);
|
|
10751
|
+
if (s.pendingTools.length > 0) lines.push(` 副作用未知: ${s.pendingTools.join(", ")}`);
|
|
10752
|
+
}
|
|
10753
|
+
lines.push("");
|
|
10754
|
+
lines.push("要真的执行,去掉 dryRun 再调用一次。");
|
|
10755
|
+
return lines.join("\n");
|
|
10756
|
+
}
|
|
10757
|
+
const okCount = (outcomes ?? []).filter((o) => o.ok).length;
|
|
10758
|
+
const failCount = (outcomes ?? []).length - okCount;
|
|
10759
|
+
lines.push(`结果:已注入 ${okCount} / 失败 ${failCount}`);
|
|
10760
|
+
lines.push("");
|
|
10761
|
+
for (const o of outcomes ?? []) lines.push(o.ok ? ` ✓ ${o.sessionId} (${o.how ?? "已注入"})` : ` ✗ ${o.sessionId} ${o.error ?? "未知原因"}`);
|
|
10762
|
+
lines.push("");
|
|
10763
|
+
lines.push("⚠️ 回执只到「消息已注入」——**不表示**目标已经跑完或已答复。");
|
|
10764
|
+
lines.push(" 要确认它真的动了,去看该会话的日志 / 它自己的 SESSION.md。");
|
|
10765
|
+
if (scan.errors.length > 0) {
|
|
10766
|
+
lines.push("");
|
|
10767
|
+
lines.push("扫描期读取失败(诊断):");
|
|
10768
|
+
for (const e of scan.errors.slice(0, 10)) lines.push(` · ${e.sessionId}: ${e.message}`);
|
|
10769
|
+
}
|
|
10770
|
+
return lines.join("\n");
|
|
10771
|
+
}
|
|
10233
10772
|
/** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
|
|
10234
10773
|
function agentScope$1(exec) {
|
|
10235
10774
|
return exec.agent?.session?.id ?? "default";
|
|
@@ -10349,6 +10888,7 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
10349
10888
|
"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
10889
|
"(B) DELIVERY — sends ONE message to ANOTHER trajectory; your own binding is unchanged: send-now (right now) / send-later (one future instant).",
|
|
10351
10890
|
"(C) REFERENCE — cro-guide: how to give a trajectory its own program that decides when it should be woken.",
|
|
10891
|
+
"(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
10892
|
"HOW TO CHOOSE (a rule, not a guess):",
|
|
10353
10893
|
"not yet bound + starting new work ⇒ create (with no prior binding it activates immediately, so the new trajectory has a carrier at once).",
|
|
10354
10894
|
"already bound + recording a SEPARATE future line ⇒ create (it does NOT steal your binding; activate the new one later by send-now).",
|
|
@@ -10388,7 +10928,9 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
10388
10928
|
"send-now (deliver RIGHT NOW; the target needs no prior setup — if it has no live session, one is created and bound automatically) /",
|
|
10389
10929
|
"send-later (one future instant + one message; fire-and-forget, and it does NOT auto-create a target).",
|
|
10390
10930
|
"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)."
|
|
10931
|
+
"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).",
|
|
10932
|
+
"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\") /",
|
|
10933
|
+
"continue-all-interrupted (ACTION: same detection as above, but it then INJECTS a short continue message into every hit, waking them. Serial delivery. Supports dryRun to preview without injecting. ⚠️ This really starts agents and spends real compute — use dryRun first if unsure. The receipt means \"injected\", NOT \"the target finished or replied\")."
|
|
10392
10934
|
].join(" ")
|
|
10393
10935
|
},
|
|
10394
10936
|
name: {
|
|
@@ -10429,7 +10971,7 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
10429
10971
|
},
|
|
10430
10972
|
dryRun: {
|
|
10431
10973
|
type: "boolean",
|
|
10432
|
-
description: "create: preview mode (no actual changes)"
|
|
10974
|
+
description: "create: preview mode (no actual changes). continue-all-interrupted: preview which sessions WOULD be resumed, injecting nothing."
|
|
10433
10975
|
},
|
|
10434
10976
|
target: {
|
|
10435
10977
|
type: "string",
|
|
@@ -10442,6 +10984,10 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
10442
10984
|
message: {
|
|
10443
10985
|
type: "string",
|
|
10444
10986
|
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."
|
|
10987
|
+
},
|
|
10988
|
+
lookbackHours: {
|
|
10989
|
+
type: "number",
|
|
10990
|
+
description: "interrupted / continue-all-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
10991
|
}
|
|
10446
10992
|
},
|
|
10447
10993
|
output: {
|
|
@@ -10636,6 +11182,93 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
10636
11182
|
* 🔴 **不进系统提示词**(owner 裁决 Q2:说明类信息按需取,不占每轮 token)。
|
|
10637
11183
|
*/
|
|
10638
11184
|
return listResident(root);
|
|
11185
|
+
case "interrupted": {
|
|
11186
|
+
/**
|
|
11187
|
+
* 以 `interrupted` 结尾的 dsh 会话(owner 2026-10-22 令:**只出入口**)。
|
|
11188
|
+
*
|
|
11189
|
+
* 🔴 **边界(owner 原话:「我们 ACC 不关心周期」)**:本动作**只回答**"此刻哪些会话
|
|
11190
|
+
* 的最后一轮以 interrupted 结束",**不判周期、不自动续跑、不改日志**。
|
|
11191
|
+
* 何时扫、续谁、怎么续 = 调用方(CCC 的认知轨迹)的事。
|
|
11192
|
+
*
|
|
11193
|
+
* 🔴 **为什么不是"曾经中断过"**:长会话历史上可中断几十次(实测单会话 33 次);
|
|
11194
|
+
* 按"曾经"判会把早已恢复的会话全部当成待续 ⇒ 灾难。判据在
|
|
11195
|
+
* `trajectory/interrupted.ts` 的 `detectInterruptedEnd`:**末条 = end-seed
|
|
11196
|
+
* 且其前最近的 turn/end 是 interrupted**。
|
|
11197
|
+
*
|
|
11198
|
+
* 🔴 **正在跑的会话要排除**:live 会话不该被判成"待续"(它正在跑,不是中断)。
|
|
11199
|
+
*/
|
|
11200
|
+
const lookbackHours = typeof args.lookbackHours === "number" && args.lookbackHours > 0 ? args.lookbackHours : DEFAULT_LOOKBACK_MS / 36e5;
|
|
11201
|
+
const sessionsRootPath = sessionsRootDir();
|
|
11202
|
+
const cwd = exec.agent?.session?.header?.cwd ?? root;
|
|
11203
|
+
const projectDir = join(sessionsRootPath, projectDirNameForCwd(cwd));
|
|
11204
|
+
return renderInterruptedScan({
|
|
11205
|
+
projectDir,
|
|
11206
|
+
lookbackHours,
|
|
11207
|
+
scan: scanInterruptedSessions(projectDir, {
|
|
11208
|
+
lookbackMs: lookbackHours * 36e5,
|
|
11209
|
+
liveIds: collectLiveSessionIds()
|
|
11210
|
+
})
|
|
11211
|
+
});
|
|
11212
|
+
}
|
|
11213
|
+
case "continue-all-interrupted": {
|
|
11214
|
+
/**
|
|
11215
|
+
* 扫 + 逐条注入续跑消息(owner 2026-10-22 追加令:
|
|
11216
|
+
* 「我希望实现的是 continue-all-interrupted 方法,调用后自动找到 8h 内所有 dsh 会话
|
|
11217
|
+
* 注入简单的 continue 进行唤醒,这样 CCC 就不用动脑子了」)。
|
|
11218
|
+
*
|
|
11219
|
+
* 🔴 **与 `interrupted` 分立而不合并**:前者是**观察**(只读、零副作用),
|
|
11220
|
+
* 后者是**动作**(会真起 agent、真烧算力)。想看一眼清单却误触发全量续跑
|
|
11221
|
+
* 是不可接受的 ⇒ 两个动作各自可调用。
|
|
11222
|
+
*
|
|
11223
|
+
* 🔴 **边界仍是"不判周期"**:何时调用归调用方(CCC 的定时轨迹)。
|
|
11224
|
+
* 本动作只保证"**调用一次就把该做的做完**"。
|
|
11225
|
+
*/
|
|
11226
|
+
const lookbackHours = typeof args.lookbackHours === "number" && args.lookbackHours > 0 ? args.lookbackHours : DEFAULT_LOOKBACK_MS / 36e5;
|
|
11227
|
+
const dryRun = args.dryRun === true;
|
|
11228
|
+
const sessionsRootPath = sessionsRootDir();
|
|
11229
|
+
const cwdRead = exec.agent?.session?.header?.cwd ?? root;
|
|
11230
|
+
const projectDir = join(sessionsRootPath, projectDirNameForCwd(cwdRead));
|
|
11231
|
+
const scan = scanInterruptedSessions(projectDir, {
|
|
11232
|
+
lookbackMs: lookbackHours * 36e5,
|
|
11233
|
+
liveIds: collectLiveSessionIds()
|
|
11234
|
+
});
|
|
11235
|
+
if (dryRun) return renderContinueResult({
|
|
11236
|
+
projectDir,
|
|
11237
|
+
lookbackHours,
|
|
11238
|
+
scan,
|
|
11239
|
+
outcomes: null,
|
|
11240
|
+
dryRun: true
|
|
11241
|
+
});
|
|
11242
|
+
const hostCtx = exec;
|
|
11243
|
+
const resumeOne = createResumeOne({
|
|
11244
|
+
agents: hostAgents(hostCtx),
|
|
11245
|
+
sessionController: hostService(hostCtx, "sessionController"),
|
|
11246
|
+
makeMessage: (text) => createUserMessage({
|
|
11247
|
+
content: [{
|
|
11248
|
+
type: "text",
|
|
11249
|
+
text
|
|
11250
|
+
}],
|
|
11251
|
+
source: PLUGIN_SOURCE
|
|
11252
|
+
})
|
|
11253
|
+
});
|
|
11254
|
+
const outcomes = [];
|
|
11255
|
+
for (const hit of scan.sessions) try {
|
|
11256
|
+
outcomes.push(await resumeOne(hit));
|
|
11257
|
+
} catch (err) {
|
|
11258
|
+
outcomes.push({
|
|
11259
|
+
sessionId: hit.sessionId,
|
|
11260
|
+
ok: false,
|
|
11261
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11262
|
+
});
|
|
11263
|
+
}
|
|
11264
|
+
return renderContinueResult({
|
|
11265
|
+
projectDir,
|
|
11266
|
+
lookbackHours,
|
|
11267
|
+
scan,
|
|
11268
|
+
outcomes,
|
|
11269
|
+
dryRun: false
|
|
11270
|
+
});
|
|
11271
|
+
}
|
|
10639
11272
|
default: throw new Error(`Unknown action: ${args.action}`);
|
|
10640
11273
|
}
|
|
10641
11274
|
}
|
|
@@ -14395,7 +15028,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14395
15028
|
});
|
|
14396
15029
|
}
|
|
14397
15030
|
const { saveWeixinRoutes } = await import("./route-3OzOAjec.js").then((n) => n.l);
|
|
14398
|
-
const { findDirNameByCode } = await import("./profile-
|
|
15031
|
+
const { findDirNameByCode } = await import("./profile-DGt_e-Nx.js").then((n) => n.a);
|
|
14399
15032
|
for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
|
|
14400
15033
|
sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
|
|
14401
15034
|
return;
|
|
@@ -14543,7 +15176,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
14543
15176
|
const id = s.id ?? s.header?.id;
|
|
14544
15177
|
if (id) liveIds.add(id);
|
|
14545
15178
|
}
|
|
14546
|
-
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-
|
|
15179
|
+
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-DxX78__Z.js").then((n) => n.t);
|
|
14547
15180
|
const root = sessionsRootDir();
|
|
14548
15181
|
if (req.method === "GET") {
|
|
14549
15182
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
@@ -15908,9 +16541,9 @@ function transformHtmlForProxy(raw, upstreamHeaders) {
|
|
|
15908
16541
|
const enc = String(upstreamHeaders["content-encoding"] ?? "").toLowerCase().trim();
|
|
15909
16542
|
const outHeaders = { ...upstreamHeaders };
|
|
15910
16543
|
if (enc === "gzip" || enc === "x-gzip" || enc === "deflate" || enc === "br") try {
|
|
15911
|
-
if (enc === "br") body =
|
|
15912
|
-
else if (enc === "deflate") body =
|
|
15913
|
-
else body =
|
|
16544
|
+
if (enc === "br") body = nodeZlib.brotliDecompressSync(body);
|
|
16545
|
+
else if (enc === "deflate") body = nodeZlib.inflateSync(body);
|
|
16546
|
+
else body = nodeZlib.gunzipSync(body);
|
|
15914
16547
|
delete outHeaders["content-encoding"];
|
|
15915
16548
|
} catch {
|
|
15916
16549
|
return null;
|
|
@@ -19438,7 +20071,7 @@ const MEDIA_MAX_BYTES = 20971520;
|
|
|
19438
20071
|
*/
|
|
19439
20072
|
async function resolveCccRootByAlias(ctx, alias) {
|
|
19440
20073
|
try {
|
|
19441
|
-
const { listCccs } = await import("./ccc-roots-
|
|
20074
|
+
const { listCccs } = await import("./ccc-roots-CvgsUnFW.js").then((n) => n.a);
|
|
19442
20075
|
const entries = await listCccs(ctx);
|
|
19443
20076
|
const byName = entries.find((e) => e.name === alias);
|
|
19444
20077
|
if (byName) return byName.root;
|
|
@@ -19506,7 +20139,7 @@ async function resolveInboundTarget(ctx, input) {
|
|
|
19506
20139
|
});
|
|
19507
20140
|
return null;
|
|
19508
20141
|
}
|
|
19509
|
-
const { findDirNameByCode } = await import("./profile-
|
|
20142
|
+
const { findDirNameByCode } = await import("./profile-DGt_e-Nx.js").then((n) => n.a);
|
|
19510
20143
|
if (findDirNameByCode(root, target.role) === null) {
|
|
19511
20144
|
const detail = `目标 CCC "${target.ccc}" 内没有编码为 "${target.role}" 的认知轨迹`;
|
|
19512
20145
|
console.error(`[serenity-hooks] ✗ human-channel: ${detail}(account=${input.account} user=${input.user})—— 该消息**未投递**,请改订阅表里的目标编码`);
|
|
@@ -20460,7 +21093,7 @@ function syncHumanChannel(ctx) {
|
|
|
20460
21093
|
(async () => {
|
|
20461
21094
|
try {
|
|
20462
21095
|
stopMachineLoops();
|
|
20463
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21096
|
+
const { listCccs } = await import("./ccc-roots-CvgsUnFW.js").then((n) => n.a);
|
|
20464
21097
|
for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
|
|
20465
21098
|
} catch {}
|
|
20466
21099
|
})();
|
|
@@ -20753,7 +21386,7 @@ function matchCcc(input, candidates) {
|
|
|
20753
21386
|
}
|
|
20754
21387
|
/** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
|
|
20755
21388
|
async function collectCandidates(ctx) {
|
|
20756
|
-
const { listCccs } = await import("./ccc-roots-
|
|
21389
|
+
const { listCccs } = await import("./ccc-roots-CvgsUnFW.js").then((n) => n.a);
|
|
20757
21390
|
const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
|
|
20758
21391
|
const seen = /* @__PURE__ */ new Set();
|
|
20759
21392
|
const out = [];
|
|
@@ -22431,85 +23064,6 @@ function createImBridgeTool() {
|
|
|
22431
23064
|
});
|
|
22432
23065
|
}
|
|
22433
23066
|
//#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
23067
|
//#region src/surface/observation.ts
|
|
22514
23068
|
/**
|
|
22515
23069
|
* surface/observation.ts — 容器状态模型的**取数层唯一出口**(C5「观察面归一」,S142 2026-09-15)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { r as cccRootForCwd } from "./ccc-roots-
|
|
1
|
+
import { r as cccRootForCwd } from "./ccc-roots-CvgsUnFW.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-
|
|
3
|
+
import { D as parseTrajectoryCode, M as sessionsRoot, S as getLastActiveSessionInfo, j as sessionEvents, x as getActiveSessionInfo } from "./profile-DGt_e-Nx.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,9 @@ const COGNITIVE_TRAJECTORY_ACTIONS = [
|
|
|
244
244
|
"send-now",
|
|
245
245
|
"send-later",
|
|
246
246
|
"cro-guide",
|
|
247
|
-
"resident"
|
|
247
|
+
"resident",
|
|
248
|
+
"interrupted",
|
|
249
|
+
"continue-all-interrupted"
|
|
248
250
|
];
|
|
249
251
|
/**
|
|
250
252
|
* 读取 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-
|
|
1
|
+
import { n as agentCwdFor, r as cccRootForCwd, s as hostAgents } from "./ccc-roots-CvgsUnFW.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-
|
|
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-DGt_e-Nx.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-
|
|
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-Cq4PsuES.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";
|
package/lib/trajectory/core.d.ts
CHANGED
|
@@ -62,8 +62,22 @@
|
|
|
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 次),按"曾经"判会把早已恢复的会话全部当成待续。
|
|
72
|
+
* 2026-10-22(owner 追加令「我希望实现的是 continue-all-interrupted 方法,调用后自动找到
|
|
73
|
+
* 8h 内所有 dsh 会话注入简单的 continue 进行唤醒,这样 CCC 就不用动脑子了」):
|
|
74
|
+
* 新增 **`continue-all-interrupted`** = `interrupted` 的**动作版**(扫 → 逐条注入续跑消息)。
|
|
75
|
+
* 判据(R↓,两者为何并存而不合并):`interrupted` 是**观察**(只读,零副作用),
|
|
76
|
+
* `continue-all-interrupted` 是**动作**(会真起 agent、真消耗算力)。
|
|
77
|
+
* ⇒ 观察与动作**必须分开可调用** —— 想看一眼清单却误触发全量续跑,是不可接受的副作用。
|
|
78
|
+
* ⚠️ 边界不变:ACC 仍**不判周期**(何时调用归调用方),只是把"调用一次要做完什么"补齐。
|
|
65
79
|
*/
|
|
66
|
-
type CognitiveTrajectoryAction = 'list' | 'show' | 'create' | 'use' | 'rebuild' | 'archive' | 'uid' | 'send-now' | 'send-later' | 'cro-guide' | 'resident';
|
|
80
|
+
type CognitiveTrajectoryAction = 'list' | 'show' | 'create' | 'use' | 'rebuild' | 'archive' | 'uid' | 'send-now' | 'send-later' | 'cro-guide' | 'resident' | 'interrupted' | 'continue-all-interrupted';
|
|
67
81
|
export declare const COGNITIVE_TRAJECTORY_ACTIONS: readonly CognitiveTrajectoryAction[];
|
|
68
82
|
/**
|
|
69
83
|
* 读取 Session 事件序列(v1.28.1 适配 0.1.2-rc.1 补齐):rc.1 起官方 Session 类
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
/** 单条续跑结果 */
|
|
123
|
+
export interface ResumeOutcome {
|
|
124
|
+
sessionId: string;
|
|
125
|
+
/** 是否已把消息投进该会话 */
|
|
126
|
+
ok: boolean;
|
|
127
|
+
/** 投递路径:live = 已在跑(直接注入)|cold-resume = 冷载入后注入 */
|
|
128
|
+
how?: string;
|
|
129
|
+
/** 失败原因 */
|
|
130
|
+
error?: string;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 续跑正文(**简单、稳定、不含版本号**)。
|
|
134
|
+
*
|
|
135
|
+
* 判据(为什么这么短):本动作的用途是**把被打断的会话推回工作状态**,
|
|
136
|
+
* 不是替它重述任务 —— 被打断那一轮的上下文就在它自己的日志里。
|
|
137
|
+
* ⇒ 正文只做三件事:① 说明发生了什么;② 提示**先核对副作用**;③ 让它接着做。
|
|
138
|
+
*
|
|
139
|
+
* 🔴 **为什么要写"先核对副作用"**:中断时可能有「有 call 无 result」的工具调用,
|
|
140
|
+
* 它们的副作用**未知**(可能已写文件 / 已发消息)。若模型把那一轮整段重放,
|
|
141
|
+
* 会**重复执行**。这句话是给模型的护栏,不是客套。
|
|
142
|
+
*/
|
|
143
|
+
export declare function buildContinueText(sessionId: string, turn: number | null, pendingTools: string[]): string;
|
|
144
|
+
/**
|
|
145
|
+
* 把一个 agent 唤醒(注入一条续跑消息)。
|
|
146
|
+
*
|
|
147
|
+
* 两条路径(与 `wake/scheduler.ts` 的 `sendToCognitiveTrajectory` 同源判据):
|
|
148
|
+
* - **live**:`agents.get(id)` 拿得到 ⇒ 目标在跑(或被加载)⇒ `steer` 当场注入当前轮;
|
|
149
|
+
* `steer` 不可用则退回 `followup`(宁可排队,不可静默丢)。
|
|
150
|
+
* - **cold-resume**:`sessionController.resolveAgent(id)` 冷载入后同样注入。
|
|
151
|
+
*
|
|
152
|
+
* ⚠️ 冷会话载入**会真起一个 agent**(消耗真实算力)⇒ 调用方必须自己决定要不要,
|
|
153
|
+
* 本函数不做数量限制(那是策略,归调用者)。
|
|
154
|
+
*/
|
|
155
|
+
export declare function createResumeOne(deps: {
|
|
156
|
+
agents: {
|
|
157
|
+
get?: (id: string) => unknown;
|
|
158
|
+
} | undefined;
|
|
159
|
+
sessionController: {
|
|
160
|
+
resolveAgent?: (id: string) => Promise<{
|
|
161
|
+
agent?: unknown;
|
|
162
|
+
error?: string;
|
|
163
|
+
}>;
|
|
164
|
+
} | undefined;
|
|
165
|
+
makeMessage: (text: string) => unknown;
|
|
166
|
+
}): (hit: {
|
|
167
|
+
sessionId: string;
|
|
168
|
+
turn: number | null;
|
|
169
|
+
pendingTools: string[];
|
|
170
|
+
}) => Promise<ResumeOutcome>;
|
|
171
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.87.
|
|
3
|
+
"version": "1.87.5",
|
|
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": {
|