@shgroup/dsh-serenity-hooks 1.87.6 → 1.87.7
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/{cleanup-DxX78__Z.js → cleanup-CPl1SSaK.js} +16 -3
- package/lib/cro/log.d.ts +15 -0
- package/lib/index.js +101 -15
- package/lib/trajectory/cleanup.d.ts +14 -1
- package/lib/trajectory/interrupted.d.ts +34 -3
- 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.7",
|
|
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": {
|
|
@@ -168,16 +168,29 @@ function dirOf(logPath) {
|
|
|
168
168
|
* 这是"靠 agent 自觉 rebuild 不够"的机制层原因(keeper 的 K 量的是**模型上下文**,压缩后回落;
|
|
169
169
|
* 而**日志文件只增不减** ⇒ rebuild 提醒永远不会因为"日志 72 MB"触发)。
|
|
170
170
|
*
|
|
171
|
+
* 🔴 **会话 id 有两种形态,目录名不能靠补前缀猜**(2026-10-23 owner 报「载体明明在,却报不在了」):
|
|
172
|
+
* · 普通会话 = `session-<uuid>`,目录名与 id **逐字相同**;
|
|
173
|
+
* · skiff 会话 = `skiff-*`,同样**逐字相同**。
|
|
174
|
+
* 旧实现无条件 `session-${id}` ⇒ 对 `skiff-weixin-…` 拼出一个**从不存在的目录**
|
|
175
|
+
* ⇒ `hasSessionLogById` 判 false ⇒ 一条**活得好好的**载体被判成「曾经有过、此刻不在了」
|
|
176
|
+
* ⇒ 调用方(`send-now` / CRO 自举闸门)据此**拒绝投递并拒绝建载体**,
|
|
177
|
+
* 而错误文案说「把那个会话恢复」—— 一个不存在的问题。
|
|
178
|
+
*
|
|
179
|
+
* ⇒ 判据改为:**先按本名找,找不到再试 `session-` 前缀形态**。
|
|
180
|
+
* 两个方向都试的理由 = 我们**不知道**调用方手里的串是 id 还是目录名;
|
|
181
|
+
* 而 `stat` 一个不存在的目录成本可忽略,误判的代价却是一整条轨迹被锁死。
|
|
182
|
+
* ⚠️ 仍找不到 ⇒ `null`("这条会话的日志不在"),与"读不到"同口径,由调用方决定降级。
|
|
183
|
+
*
|
|
171
184
|
* 只认会话目录(含任意世代日志);返回**最高世代**的日志(= 宿主 `findLog` 选中的当前世代)。
|
|
172
185
|
* 未命中 / 不可读 → null(非错误,调用方按"未知"处理)。
|
|
173
186
|
* @param root - DSH 会话 root({@link sessionsRootDir})
|
|
174
|
-
* @param sessionId -
|
|
187
|
+
* @param sessionId - 会话 id 或会话目录名(`session-*` / `skiff-*` 都按本名找)
|
|
175
188
|
* @returns 字节数 + 日志绝对路径;未命中 → null
|
|
176
189
|
*/
|
|
177
190
|
function sessionLogBytesById(root, sessionId) {
|
|
178
191
|
if (sessionId === "" || !existsSync(root)) return null;
|
|
179
|
-
const
|
|
180
|
-
for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
192
|
+
const candidates = sessionId.startsWith("session-") ? [sessionId] : [sessionId, `session-${sessionId}`];
|
|
193
|
+
for (const dirName of candidates) for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
181
194
|
if (!project.isDirectory()) continue;
|
|
182
195
|
const log = findSessionLog(join(root, project.name, dirName));
|
|
183
196
|
if (log === null) continue;
|
package/lib/cro/log.d.ts
CHANGED
|
@@ -91,6 +91,19 @@ export interface CroWakeLogEntry {
|
|
|
91
91
|
at: string;
|
|
92
92
|
/** 投递是否成功 */
|
|
93
93
|
ok: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* 🔴 **"没送成、但这不算失败"**(`ok=false` 时才有意义)—— 2026-10-23 guard 实测逼出。
|
|
96
|
+
*
|
|
97
|
+
* 现状:宿主重启窗口里 `sessionController` 还没就绪 ⇒ 投递取不到载体 ⇒ `notReady`。
|
|
98
|
+
* 那不是"投递失败"(下个 tick 会重试,且**每次都真的重试**),
|
|
99
|
+
* 但也**不是"没发生"** —— 原先整条不落档 ⇒ 该轨迹的流水里**一个字都没有**
|
|
100
|
+
* ⇒ 读日志的人无法区分「CRO 从没工作过」与「CRO 工作了、投递被挡住」。
|
|
101
|
+
*
|
|
102
|
+
* 🔴 **判据(D186 的又一次应用)**:可以不算失败,**但不许不出声**。
|
|
103
|
+
* 三态因此显式分开:`ok` / `fail` / `not-ready`(`ok:false` + `notReady:true`)。
|
|
104
|
+
* ⛔ 不要把它折回 `ok:true`(那是假成功),也不要折成 `fail`(那是假失败,见 `:171` 语义)。
|
|
105
|
+
*/
|
|
106
|
+
notReady?: true;
|
|
94
107
|
/** 投递方式(`已投递(live)` / `已投递(冷载入)`)或失败原因 */
|
|
95
108
|
detail: string;
|
|
96
109
|
/** 调度器 tick 计数(便于与 `acc-diag` ①b 对齐) */
|
|
@@ -111,6 +124,8 @@ export interface CroWakeLog {
|
|
|
111
124
|
/** 追加所需的输入(`at` 由本模块按当地时区生成,不劳调用方) */
|
|
112
125
|
export interface CroWakeLogInput {
|
|
113
126
|
ok: boolean;
|
|
127
|
+
/** 🔴 `true` ⇒ 记作 `not-ready`(没送成,但**不算失败**)。见 {@link CroWakeLogEntry.notReady} */
|
|
128
|
+
notReady?: boolean;
|
|
114
129
|
detail: string;
|
|
115
130
|
tick: number;
|
|
116
131
|
reason: string | null;
|
package/lib/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { A as resolveSessionByTitle, B as readTrajectoryFile, C as listActiveTra
|
|
|
5
5
|
import { n as checkLocalstoreGitCompliance, o as readStore, s as runLocalStore, t as LOCALSTORE_SCOPES } from "./service-DDmHSglV.js";
|
|
6
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
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-
|
|
8
|
+
import { i as sessionsRootDir, n as findSessionLog, r as hasSessionLogById } from "./cleanup-CPl1SSaK.js";
|
|
9
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";
|
|
@@ -8631,7 +8631,7 @@ function formatCroLogLine(entry) {
|
|
|
8631
8631
|
const head = oneline(entry.promptDigest.head);
|
|
8632
8632
|
return [
|
|
8633
8633
|
oneline(entry.at),
|
|
8634
|
-
entry.ok ? "ok" : "fail",
|
|
8634
|
+
entry.ok ? "ok" : entry.notReady === true ? "not-ready" : "fail",
|
|
8635
8635
|
`tick=${entry.tick}`,
|
|
8636
8636
|
oneline(entry.detail),
|
|
8637
8637
|
`reason=${reason}`,
|
|
@@ -8652,7 +8652,7 @@ function parseCroLogLine(line) {
|
|
|
8652
8652
|
const at = parts[0];
|
|
8653
8653
|
if (Number.isNaN(Date.parse(at))) return null;
|
|
8654
8654
|
const mark = parts[1];
|
|
8655
|
-
if (mark !== "ok" && mark !== "fail") return null;
|
|
8655
|
+
if (mark !== "ok" && mark !== "fail" && mark !== "not-ready") return null;
|
|
8656
8656
|
const tickM = /^tick=(\d+)$/.exec(parts[2]);
|
|
8657
8657
|
if (!tickM) return null;
|
|
8658
8658
|
const reasonRaw = parts[4].startsWith("reason=") ? parts[4].slice(7) : "";
|
|
@@ -8660,6 +8660,7 @@ function parseCroLogLine(line) {
|
|
|
8660
8660
|
return {
|
|
8661
8661
|
at,
|
|
8662
8662
|
ok: mark === "ok",
|
|
8663
|
+
...mark === "not-ready" ? { notReady: true } : {},
|
|
8663
8664
|
detail: parts[3],
|
|
8664
8665
|
tick: Number(tickM[1]),
|
|
8665
8666
|
reason: reasonRaw === "" ? null : reasonRaw,
|
|
@@ -8818,6 +8819,7 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
|
|
|
8818
8819
|
const entry = {
|
|
8819
8820
|
at: isoLocal(nowMs),
|
|
8820
8821
|
ok: input.ok,
|
|
8822
|
+
...input.notReady === true ? { notReady: true } : {},
|
|
8821
8823
|
detail: input.detail,
|
|
8822
8824
|
tick: input.tick,
|
|
8823
8825
|
reason: input.reason,
|
|
@@ -9191,7 +9193,12 @@ async function acquireWakeAgent(ctx, root, dirName) {
|
|
|
9191
9193
|
}
|
|
9192
9194
|
}
|
|
9193
9195
|
return {
|
|
9194
|
-
|
|
9196
|
+
/**
|
|
9197
|
+
* 🔴 **文案改指真相源(2026-10-23,guard 实测报障)**:原写「`AGENT_SESSIONS/.bindings.json` 中无 X」,
|
|
9198
|
+
* 而 D172 之后绑定真相源 = **`<轨迹目录>/current.dsh_session_bind`**,`.bindings.json`
|
|
9199
|
+
* **已退役且不在盘上**(实测不存在)⇒ 照旧文案排查的人会去翻一个**不存在的文件**。
|
|
9200
|
+
*/
|
|
9201
|
+
error: `无绑定会话记录(AGENT_SESSIONS/${dirName}/current.dsh_session_bind 不存在或无有效会话)——目标轨迹需先被 cognitive_trajectory use 激活过,或由 send-now 自动创建载体`,
|
|
9195
9202
|
noBinding: true
|
|
9196
9203
|
};
|
|
9197
9204
|
}
|
|
@@ -9664,7 +9671,52 @@ function buildCroWakeText(dirName, mdPath, prompt, reason) {
|
|
|
9664
9671
|
*/
|
|
9665
9672
|
async function deliverCroWake(ctx, root, dirName, prompt, reason) {
|
|
9666
9673
|
const mdPath = join(sessionsRoot(root), dirName, "SESSION.md");
|
|
9667
|
-
|
|
9674
|
+
let acquired = await acquireWakeAgent(ctx, root, dirName);
|
|
9675
|
+
/**
|
|
9676
|
+
* 🔴 **载体自举(2026-10-23,guard 实测报障后补)**
|
|
9677
|
+
*
|
|
9678
|
+
* ## 缺陷(真实代价)
|
|
9679
|
+
*
|
|
9680
|
+
* 一条「**生来就是为了被自己的 CRO 叫醒**」的轨迹,**第一次自唤醒在物理上不可能**:
|
|
9681
|
+
* 它是程序化 `create` 出来的 ⇒ 没有载体 ⇒ `noBinding` ⇒ 投递被拒
|
|
9682
|
+
* ⇒ **恰恰在最需要被叫醒的那一刻没有载体可注入**。实测证据:
|
|
9683
|
+
* `cro-wake-log.log` 首行 `fail ... 无绑定会话记录`,而**那一轮的 CRO 判定完全正确**。
|
|
9684
|
+
*
|
|
9685
|
+
* ## 为什么可以自举,以及它与 `send-now` 的分支差在哪
|
|
9686
|
+
*
|
|
9687
|
+
* `send-now` 的自举理由是「完成一次**人/agent 的**意图」;CRO 是**每 5min 自动重判**,
|
|
9688
|
+
* 若照搬会有「**载体爆炸**」风险。故本路径**多一道闸**:
|
|
9689
|
+
*
|
|
9690
|
+
* - 只认 `carrierState === 'never'`(**从未有过载体**)⇒ 建一次;
|
|
9691
|
+
* - `occupied`(有过但那个会话不在了)⇒ **一律不建**,如实报错。
|
|
9692
|
+
*
|
|
9693
|
+
* ⇒ 自举**至多发生一次**(建完就有 `current.dsh_session_bind`,此后 `carrierState` 不再是 `never`)。
|
|
9694
|
+
* 这正是 `carrierState` 那道守卫的语义,与 `send-now` 共用同一条判据(不另立)。
|
|
9695
|
+
*
|
|
9696
|
+
* ⚠️ 只处理 `noBinding`;`notReady`(启动窗口)与冷载入失败**照原样返回** ——
|
|
9697
|
+
* 那些不是"没载体",建新载体解决不了且可能制造第二条。
|
|
9698
|
+
*/
|
|
9699
|
+
if ("error" in acquired && acquired.noBinding === true) {
|
|
9700
|
+
const carrier = carrierState(root, dirName, hostAliveProbe(ctx));
|
|
9701
|
+
if (carrier.state === "never") {
|
|
9702
|
+
const created = await createAndBindTarget(ctx, root, dirName);
|
|
9703
|
+
if (!("error" in created)) {
|
|
9704
|
+
acquired = created;
|
|
9705
|
+
console.log(`[serenity-hooks] CRO 载体自举:${dirName} 从未有过载体 ⇒ 已自动创建并绑定(${created.how})`);
|
|
9706
|
+
}
|
|
9707
|
+
} else
|
|
9708
|
+
/**
|
|
9709
|
+
* 🔴 **拒绝自举时必须指名是哪一半**(D186:守卫可以拒绝,但不许不出声)。
|
|
9710
|
+
*
|
|
9711
|
+
* 若照原样把 `acquireWakeAgent` 的 `noBinding` 文案透传出去,`cro-wake-log` 里
|
|
9712
|
+
* 「`current.dsh_session_bind` 不存在或无有效会话」**两种状态长得一模一样**:
|
|
9713
|
+
* · `never` —— 那条轨迹**生来就没有载体**(本次已自举,不会走到这里);
|
|
9714
|
+
* · `occupied` —— **有过载体、那个会话现在不在了**(本分支拒绝自建)。
|
|
9715
|
+
* ⇒ 读日志的人分不出"该去救那个会话"还是"该去查为什么没建起来",
|
|
9716
|
+
* 而这正是本仓反复付代价的那类缺陷(D131:不靠一句话给状态命名,但**状态不同必须说出来**)。
|
|
9717
|
+
*/
|
|
9718
|
+
acquired = { error: carrier.state === "occupied" ? `CRO 唤起未投递:${dirName} 曾经由会话 ${carrier.sessionId} 承载,而它此刻不在了 ⇒ **拒绝自动新建载体**(一条轨迹至多一个载体,而 CRO 每 5min 重判一次,自建会让载体反复膨胀)。\n · 要恢复:把那个会话救回来,或用 cognitive_trajectory rebuild 显式换代(takeover)。` : `CRO 唤起未投递:${dirName} 的载体声明指向会话 ${carrier.sessionId},它按探针**仍可用**,而选人路径却报"无绑定" ⇒ **两处读数打架,不新建载体**(先查绑定文件与归档集)。` };
|
|
9719
|
+
}
|
|
9668
9720
|
if ("error" in acquired) return {
|
|
9669
9721
|
ok: false,
|
|
9670
9722
|
detail: acquired.error,
|
|
@@ -9760,9 +9812,10 @@ async function runCroPhase(ctx, root, registry, log) {
|
|
|
9760
9812
|
wakes += 1;
|
|
9761
9813
|
const res = await deliverCroWake(ctx, root, dirName, outcome.prompt, outcome.decision.reason);
|
|
9762
9814
|
log.push(`· ${root}: ${renderCroOutcome(dirName, outcome)} → ${res.ok ? "已投递" : `投递未成功:${res.detail}`}`);
|
|
9763
|
-
|
|
9815
|
+
{
|
|
9764
9816
|
const rec = appendCroWakeLog(root, dirName, {
|
|
9765
9817
|
ok: res.ok,
|
|
9818
|
+
...res.notReady === true ? { notReady: true } : {},
|
|
9766
9819
|
detail: res.detail,
|
|
9767
9820
|
tick: s.ticks,
|
|
9768
9821
|
reason: outcome.decision.reason ?? null,
|
|
@@ -10289,13 +10342,18 @@ function diagLive(ctx) {
|
|
|
10289
10342
|
*
|
|
10290
10343
|
* ## 判据(三条同时成立,缺一不可)
|
|
10291
10344
|
*
|
|
10292
|
-
* 1.
|
|
10293
|
-
* 2.
|
|
10294
|
-
* 3. `session/end-seed
|
|
10345
|
+
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind === 'interrupted'`;
|
|
10346
|
+
* 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则已被后续轮次接续);
|
|
10347
|
+
* 3. ⛔ **不要求**末条是 `session/end-seed`(2026-10-23 放宽,原因见 {@link detectInterruptedEnd})。
|
|
10295
10348
|
*
|
|
10296
10349
|
* ⚠️ **为什么不能只看"存在过 interrupted"**:一个长会话历史上可能中断几十次
|
|
10297
10350
|
* (本容器实测单会话 33 次)。只看"曾经中断"会把早已恢复的会话全部当成待续 ⇒ 灾难。
|
|
10298
10351
|
*
|
|
10352
|
+
* 🔴 **为什么原判据要求"末条 = end-seed"是错的**(实测 26 条里漏 2 条):
|
|
10353
|
+
* 重启后宿主会在 end-seed **之后**追加 `agent/inbox/spliced`(把待办消息重新塞进收件箱)
|
|
10354
|
+
* ⇒ "末条必须 end-seed"**在恢复动作发生之后就恒不成立**,
|
|
10355
|
+
* 而"被恢复过又再次被打断"恰恰是守护最该覆盖的形态。
|
|
10356
|
+
*
|
|
10299
10357
|
* ## 为什么不整文件解压
|
|
10300
10358
|
*
|
|
10301
10359
|
* DSH 日志是 **zstd 多帧追加**(实测单会话 24705 帧 / 47 MB)。原生
|
|
@@ -10382,16 +10440,44 @@ function readTailEvents(logPath, want) {
|
|
|
10382
10440
|
/**
|
|
10383
10441
|
* 判定一组尾部事件是否「以 interrupted 结尾」。
|
|
10384
10442
|
* 不命中 → null。
|
|
10443
|
+
*
|
|
10444
|
+
* ## 🔴 2026-10-23 判据放宽(owner 实测:「不是挂在工具调用上的,也要能被 interrupted 筛选到并唤醒」)
|
|
10445
|
+
*
|
|
10446
|
+
* ### 原判据(v1.87.4 起)为什么漏
|
|
10447
|
+
* 原来只认 **末条事件 = `session/end-seed`**。实测(本机 3337 条会话全扫):
|
|
10448
|
+
* · 末轮 `turn/end[interrupted]` 的会话 **26** 条;
|
|
10449
|
+
* · 其中满足原判据的只有 **24** 条 ⇒ **漏 2 条**,形态逐字为:
|
|
10450
|
+
* `… turn/end[interrupted] -> session/end-seed -> agent/inbox/spliced -> agent/inbox/spliced`
|
|
10451
|
+
* ⇒ 重启后宿主把这**两条**追加在 end-seed 之后(它们是"恢复时把待办消息重新塞进收件箱"),
|
|
10452
|
+
* 于是"末条必须 end-seed"这条**在恢复动作发生之后就恒不成立**。
|
|
10453
|
+
* ⚠️ 讽刺的是:**正是那些被恢复过、又再次被打断的会话**最容易长成这样
|
|
10454
|
+
* ⇒ 原判据恰好对"反复中断"的会话失明,而它们才是最需要守护的。
|
|
10455
|
+
*
|
|
10456
|
+
* ### 新判据(三条,缺一不可)
|
|
10457
|
+
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind === 'interrupted'`;
|
|
10458
|
+
* 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则说明它已被后续轮次接续);
|
|
10459
|
+
* 3. ⛔ **不再要求末条是 `end-seed`**。
|
|
10460
|
+
*
|
|
10461
|
+
* ### 为什么不改成"末条必须是某个白名单"
|
|
10462
|
+
* 那就是原缺陷换个白名单再犯一次(`agent/inbox/spliced` 之外还会有别的)。
|
|
10463
|
+
* **判据要问的是"最后一轮的结束原因是什么、之后有没有新轮开始"**,
|
|
10464
|
+
* 而不是"日志最后一个 token 长什么样"——后者是**载体形态**,前者是**状态**。
|
|
10465
|
+
*
|
|
10466
|
+
* ### 为什么不会误报"曾经中断但已恢复"
|
|
10467
|
+
* 恢复一定伴随 `turn/start`(新一轮)或新的 `turn/end`(`completed` 等)⇒ 判据 1/2 各自挡住。
|
|
10468
|
+
* 实测该放宽**不新增误报**:原 24 条全含于新判据(`curOnly = 0`),新增 2 条全是真漏。
|
|
10385
10469
|
*/
|
|
10386
10470
|
function detectInterruptedEnd(events) {
|
|
10387
10471
|
if (events.length === 0) return null;
|
|
10388
|
-
|
|
10389
|
-
let
|
|
10390
|
-
|
|
10391
|
-
turnEnd = events[i];
|
|
10472
|
+
let idx = -1;
|
|
10473
|
+
for (let i = events.length - 1; i >= 0; i--) if (events[i]?.type === "turn/end") {
|
|
10474
|
+
idx = i;
|
|
10392
10475
|
break;
|
|
10393
10476
|
}
|
|
10394
|
-
if (
|
|
10477
|
+
if (idx < 0) return null;
|
|
10478
|
+
const turnEnd = events[idx];
|
|
10479
|
+
if (turnEnd?.data?.reason?.kind !== "interrupted") return null;
|
|
10480
|
+
for (let i = idx + 1; i < events.length; i++) if (events[i]?.type === "turn/start") return null;
|
|
10395
10481
|
const turn = typeof turnEnd.data?.turn === "number" ? turnEnd.data.turn : null;
|
|
10396
10482
|
return {
|
|
10397
10483
|
turn,
|
|
@@ -15175,7 +15261,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15175
15261
|
const id = s.id ?? s.header?.id;
|
|
15176
15262
|
if (id) liveIds.add(id);
|
|
15177
15263
|
}
|
|
15178
|
-
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-
|
|
15264
|
+
const { sessionsRootDir, collectEligibleSessions, performCleanup, cutoffDaysAgo } = await import("./cleanup-CPl1SSaK.js").then((n) => n.t);
|
|
15179
15265
|
const root = sessionsRootDir();
|
|
15180
15266
|
if (req.method === "GET") {
|
|
15181
15267
|
const { candidates } = performCleanup(root, cutoffDaysAgo(days), liveIds, { dryRun: true });
|
|
@@ -88,10 +88,23 @@ export declare function performCleanup(root: string, cutoffMs: number, liveIds?:
|
|
|
88
88
|
* 这是"靠 agent 自觉 rebuild 不够"的机制层原因(keeper 的 K 量的是**模型上下文**,压缩后回落;
|
|
89
89
|
* 而**日志文件只增不减** ⇒ rebuild 提醒永远不会因为"日志 72 MB"触发)。
|
|
90
90
|
*
|
|
91
|
+
* 🔴 **会话 id 有两种形态,目录名不能靠补前缀猜**(2026-10-23 owner 报「载体明明在,却报不在了」):
|
|
92
|
+
* · 普通会话 = `session-<uuid>`,目录名与 id **逐字相同**;
|
|
93
|
+
* · skiff 会话 = `skiff-*`,同样**逐字相同**。
|
|
94
|
+
* 旧实现无条件 `session-${id}` ⇒ 对 `skiff-weixin-…` 拼出一个**从不存在的目录**
|
|
95
|
+
* ⇒ `hasSessionLogById` 判 false ⇒ 一条**活得好好的**载体被判成「曾经有过、此刻不在了」
|
|
96
|
+
* ⇒ 调用方(`send-now` / CRO 自举闸门)据此**拒绝投递并拒绝建载体**,
|
|
97
|
+
* 而错误文案说「把那个会话恢复」—— 一个不存在的问题。
|
|
98
|
+
*
|
|
99
|
+
* ⇒ 判据改为:**先按本名找,找不到再试 `session-` 前缀形态**。
|
|
100
|
+
* 两个方向都试的理由 = 我们**不知道**调用方手里的串是 id 还是目录名;
|
|
101
|
+
* 而 `stat` 一个不存在的目录成本可忽略,误判的代价却是一整条轨迹被锁死。
|
|
102
|
+
* ⚠️ 仍找不到 ⇒ `null`("这条会话的日志不在"),与"读不到"同口径,由调用方决定降级。
|
|
103
|
+
*
|
|
91
104
|
* 只认会话目录(含任意世代日志);返回**最高世代**的日志(= 宿主 `findLog` 选中的当前世代)。
|
|
92
105
|
* 未命中 / 不可读 → null(非错误,调用方按"未知"处理)。
|
|
93
106
|
* @param root - DSH 会话 root({@link sessionsRootDir})
|
|
94
|
-
* @param sessionId -
|
|
107
|
+
* @param sessionId - 会话 id 或会话目录名(`session-*` / `skiff-*` 都按本名找)
|
|
95
108
|
* @returns 字节数 + 日志绝对路径;未命中 → null
|
|
96
109
|
*/
|
|
97
110
|
export declare function sessionLogBytesById(root: string, sessionId: string): {
|
|
@@ -17,13 +17,18 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ## 判据(三条同时成立,缺一不可)
|
|
19
19
|
*
|
|
20
|
-
* 1.
|
|
21
|
-
* 2.
|
|
22
|
-
* 3. `session/end-seed
|
|
20
|
+
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind === 'interrupted'`;
|
|
21
|
+
* 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则已被后续轮次接续);
|
|
22
|
+
* 3. ⛔ **不要求**末条是 `session/end-seed`(2026-10-23 放宽,原因见 {@link detectInterruptedEnd})。
|
|
23
23
|
*
|
|
24
24
|
* ⚠️ **为什么不能只看"存在过 interrupted"**:一个长会话历史上可能中断几十次
|
|
25
25
|
* (本容器实测单会话 33 次)。只看"曾经中断"会把早已恢复的会话全部当成待续 ⇒ 灾难。
|
|
26
26
|
*
|
|
27
|
+
* 🔴 **为什么原判据要求"末条 = end-seed"是错的**(实测 26 条里漏 2 条):
|
|
28
|
+
* 重启后宿主会在 end-seed **之后**追加 `agent/inbox/spliced`(把待办消息重新塞进收件箱)
|
|
29
|
+
* ⇒ "末条必须 end-seed"**在恢复动作发生之后就恒不成立**,
|
|
30
|
+
* 而"被恢复过又再次被打断"恰恰是守护最该覆盖的形态。
|
|
31
|
+
*
|
|
27
32
|
* ## 为什么不整文件解压
|
|
28
33
|
*
|
|
29
34
|
* DSH 日志是 **zstd 多帧追加**(实测单会话 24705 帧 / 47 MB)。原生
|
|
@@ -93,6 +98,32 @@ export interface ScanResult {
|
|
|
93
98
|
/**
|
|
94
99
|
* 判定一组尾部事件是否「以 interrupted 结尾」。
|
|
95
100
|
* 不命中 → null。
|
|
101
|
+
*
|
|
102
|
+
* ## 🔴 2026-10-23 判据放宽(owner 实测:「不是挂在工具调用上的,也要能被 interrupted 筛选到并唤醒」)
|
|
103
|
+
*
|
|
104
|
+
* ### 原判据(v1.87.4 起)为什么漏
|
|
105
|
+
* 原来只认 **末条事件 = `session/end-seed`**。实测(本机 3337 条会话全扫):
|
|
106
|
+
* · 末轮 `turn/end[interrupted]` 的会话 **26** 条;
|
|
107
|
+
* · 其中满足原判据的只有 **24** 条 ⇒ **漏 2 条**,形态逐字为:
|
|
108
|
+
* `… turn/end[interrupted] -> session/end-seed -> agent/inbox/spliced -> agent/inbox/spliced`
|
|
109
|
+
* ⇒ 重启后宿主把这**两条**追加在 end-seed 之后(它们是"恢复时把待办消息重新塞进收件箱"),
|
|
110
|
+
* 于是"末条必须 end-seed"这条**在恢复动作发生之后就恒不成立**。
|
|
111
|
+
* ⚠️ 讽刺的是:**正是那些被恢复过、又再次被打断的会话**最容易长成这样
|
|
112
|
+
* ⇒ 原判据恰好对"反复中断"的会话失明,而它们才是最需要守护的。
|
|
113
|
+
*
|
|
114
|
+
* ### 新判据(三条,缺一不可)
|
|
115
|
+
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind === 'interrupted'`;
|
|
116
|
+
* 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则说明它已被后续轮次接续);
|
|
117
|
+
* 3. ⛔ **不再要求末条是 `end-seed`**。
|
|
118
|
+
*
|
|
119
|
+
* ### 为什么不改成"末条必须是某个白名单"
|
|
120
|
+
* 那就是原缺陷换个白名单再犯一次(`agent/inbox/spliced` 之外还会有别的)。
|
|
121
|
+
* **判据要问的是"最后一轮的结束原因是什么、之后有没有新轮开始"**,
|
|
122
|
+
* 而不是"日志最后一个 token 长什么样"——后者是**载体形态**,前者是**状态**。
|
|
123
|
+
*
|
|
124
|
+
* ### 为什么不会误报"曾经中断但已恢复"
|
|
125
|
+
* 恢复一定伴随 `turn/start`(新一轮)或新的 `turn/end`(`completed` 等)⇒ 判据 1/2 各自挡住。
|
|
126
|
+
* 实测该放宽**不新增误报**:原 24 条全含于新判据(`curOnly = 0`),新增 2 条全是真漏。
|
|
96
127
|
*/
|
|
97
128
|
export declare function detectInterruptedEnd(events: SessionEvent[]): {
|
|
98
129
|
turn: number | null;
|
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.7",
|
|
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": {
|