@akasha-book/sleep 1.5.4 → 1.7.1

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/README.md CHANGED
@@ -1,56 +1,29 @@
1
1
  # @akasha-book/sleep
2
2
 
3
- > 1.5.4 起新增**称呼行**(`akasha:address`,order 133):每回合 runtime context 注入「对用户称 Master;用户对你是『大肥鱼』」+ 执行点要求——
4
- > 成因见 `docs/observation-ledger.md` 与 `canon-address-drift-fix-20261010`:**规则躺在会话开头的注入里、执行点看不到 ⇒ 长会话中期必漂移**;
5
- > 修法=接进每回合必然经过的面。
3
+ 阿卡夏之书睡眠器 v1.5.5 —— **调度与注入**(蒸馏逻辑归核心库 `<AKASHA_DIR>/sleep.mjs`;手动触发 `node akasha.mjs sleep [--dry]` 与之共用同一水位线)。
6
4
 
7
- > 阿卡夏之书(Akasha Book)· 睡眠器插件
5
+ - **v1.5.5(2026-10-07 复查修复)**:移除死代码 `source==='compact'` 早退(官方 `agent/created` 的 source 只有 startup/resume;压缩信号是 session 事件 `compaction/*`);子代理守卫沿用官方 `header.origin/delegationDepth`(已核真实字段)。
8
6
 
9
- 阿卡夏之书睡眠器 v1.5.1 —— **调度与注入**(蒸馏逻辑归核心库 `../../core/sleep.mjs`;手动触发 `node akasha.mjs sleep [--dry]` 与之共用同一水位线)。
7
+ - **触发**:`agent/status → idle`,去抖 `minIntervalHours`(默认 6);另一路定时兜底 `ctx.interval`(`timerCheckMs`,默认 1h——timer 属混入服务,必须 `inject:['timer']` 才能访问 `ctx.interval`)。跳过也留痕(`sleep-skip`);首次激活即跑一次(`activate`)。
8
+ - **蒸馏(v1.4 起在核心库 `akasha\sleep.mjs`)**:`akasha\logs\hooks.jsonl` 增量(水位线 `processedLines`)→ turns / tools / errors / denials 统计 + 失败备注 + 待办(`buildTodos`:审计警告 → review;工具失败 / 门控拦截 / agent 错误 → 孤案候选);报告 `sleep-<date>.json`(**同日重复跑加时间戳后缀,不覆盖**);有待办时追加 `akasha\logs\inbox.jsonl`。
9
+ - **条子一(每回合)**:`systemPrompt.context`(`akasha:sleep`,order 130)——每个新回合的 runtime context 自动带上「最近一次睡眠」一行(含待办数;渲染自核心库)。
10
+ - **条子二(新会话·唤醒条)**:`agent/created` → 经 `agent.inject()` 把「起床包」摘要递进 next-step inbox(**不唤醒、不打断**;仅当 无睡眠记录 / 有待办 / 审计警告 才发;子代理与 compact 不打扰;消息 `source.kind = "akasha-wake"`)。
11
+ - **条子三(每回合·库脉搏 + 回查)**:`akasha:pulse`(order 132)——六库当前计数 + 最近写入(**写→可见通道**:任何会话 / 脚本写入库后,活跃会话下一回合即见);**v1.5 起**,窗口内(默认 30 分钟)有工具 / agent 失败时,追一行「回查提示」(错误钩子的注入级动作——只提示、不拦截;渲染幂等,同窗口重复渲染结果一致)。
12
+ - **降级(v1.4 切分)**:核心库不可用 → `sleep-error` 观测线 + 条子提示文案;不炸、不失活(插件不再内置蒸馏逻辑,拒绝双份实现)。
13
+ - 观测线:`sleep-armed` / `sleep-done` / `sleep-skip` / `sleep-error` / `wake-note` / `wake-note-error` / `wake-note-llm-error` / `sleep-report-error` / `sleep-state-error` / `sleep-inbox-error` 全部写进 `hooks.jsonl`(报告 / 状态 / inbox 三条由核心库发出)。
14
+ - 自测:`node selftest.mjs`(纯函数直测 `../../core/sleep.mjs`——含 v1.5 `renderRecallLine`;桩 ctx 端到端(含失败→回查行)+ 写失败线 + 无核心降级;临时目录,不碰真库水位线)。
10
15
 
11
- - **触发**:`agent/status → idle`,去抖 `minIntervalHours`(默认 6);另一路定时兜底 `ctx.interval`(默认 1h——timer 属混入服务,需 `inject:['timer']`)。跳过也留痕(`sleep-skip`);首次激活即跑一次(`activate`)。
12
- - **蒸馏**:`hooks.jsonl` 增量(水位线 `processedLines`)→ 统计 + 失败备注 + 待办(审计警告 → review;工具失败 / 拦截 / agent 错误 → 孤案候选;**失败前未查库的回合 → review `recall-miss`(召回复盘,带会话样本;不自动转孤案),wave1.1**);报告 `sleep-<date>.json`(**同日重复跑加时间戳后缀,不覆盖**);有待办时追加 `logs/inbox.jsonl`。
13
- - **条子一(每回合)**:`akasha:sleep`(order 130)——最近一次睡眠摘要(含待办数)。
14
- - **条子二(新会话·唤醒条)**:`agent/created` → 经 `agent.inject()` 递「起床包」摘要(不唤醒、不打断;仅当 无睡眠记录 / 有待办 / 审计警告 才发)。
15
- - **条子三(每回合·库脉搏 + 回查)**:`akasha:pulse`(order 132)——六库计数 + 最近写入(**写→可见通道**);窗口内(默认 30 分钟)有失败时追一行**回查提示**(注入级,不拦截)。
16
- - **配置**:`akashaDir` / `log` / `stateFile` / `inboxFile` / `appModulesDir`(唤醒条用的宿主模块目录;**留空则跳过并留痕 `wake-note-llm-skip`**)/ `minIntervalHours` / `timerCheckMs` / `contextOrder` / `pulseOrder`。
17
- - 观测线:`sleep-armed` / `sleep-done` / `sleep-skip` / `sleep-error` / `wake-note` / `wake-note-error` / `wake-note-llm-error` / `wake-note-llm-skip` / `sleep-report-error` / `sleep-state-error` / `sleep-inbox-error`。
18
- - 自测:`node selftest.mjs`(纯函数 + 桩 ctx 端到端 + 写失败线 + 无核心降级;不碰真实水位线)。
16
+ ## 版本与重启
19
17
 
20
- ## ⚠️ 版本纪律
21
- 替换已安装包的 JS **无法进程内热更**(模块代缓存)——升级后需重启宿主载入。
18
+ - v1.1.0 事故:访问 `ctx.setInterval` 未 `inject` → 属性访问本身抛错、**apply 整段失败且零日志**(诊断靠 `plugin_manager` 的返回或 `*-armed` 观测线)。v1.1.2 修复 + 入口改 `lib/index.js`。
19
+ - **替换已装包的 JS 无法进程内热更**(toggle / remove+reinstall 都无效)——v1.1.2 / v1.1.1 / v1.2.0 / v1.3.0 / v1.3.1 / v1.4.0 / **v1.5.0** 均需**重启**载入。
20
+ - v1.3.0:库脉搏(写→可见通道);v1.3.1:写路径静默失败补观测线;v1.4.0:逻辑归库(`akasha\sleep.mjs`)+ 手动触发;**v1.5.0:回查提示(失败窗口 → 脉搏条追一行)**。
21
+ - ⚠️ 注意:`ctx.on('tools/result')` 与 `ctx.on('agent/error')` 的监听是 v1.5 新增——宿主 Event 名以 `cordis_inspect_query`(host Event 目录)为准,两个事件均为 emit 语义(失败不打断宿主)。
22
22
 
23
- ## 打包 / 安装(DSH 宿主)
23
+ ## 打包 / 安装
24
24
 
25
- ```bash
25
+ ```powershell
26
+ cd <LOCAL_PATH>
26
27
  npm pack
27
- # 然后:plugin_manager install_bundle <tgz 绝对路径>(新 bundle 热生效;替换已装包需重启宿主)
28
+ # 然后:plugin_manager install_bundle <tgz 绝对路径>(首次安装会 HMR 热生效;替换需重启)
28
29
  ```
29
-
30
- 依赖:DSH 宿主(`systemPrompt` / `timer` 服务与事件面);核心库 `../../core/sleep.mjs` + `lib.mjs`(经 `akashaDir` 动态加载)。
31
-
32
- <!-- store-status -->
33
- ## 兼容与状态(DSH STORE 口径 · 2026-10-10)
34
-
35
- **固定源(immutable source)**:本版本的源码冻结于 Commit `d311ad54d1dc2cd2697c7b9eeac57eaa6466fc04`(对应 `plugins/akasha-sleep/package.json` 的 version)。
36
- 兼容声明见 manifest 的 `dsh.compatibility.dshReleases`:**0.2.0-rc.2 = compatible**(本机实测运行),
37
- `0.2.1-alpha.1` / `0.2.1-alpha.2` 尚未实测 ⇒ 按契约如实写 `unknown`(不猜、不吹)。
38
-
39
- | 证据层 | 状态 | 依据 |
40
- |---|---|---|
41
- | 静态契约(manifest / Bundle Patch / 许可证 / 入口 ID) | **verified** | 官方 `build-dsh-plugin` 审计:静态分见本包审计输出;入口 ID 为插件自有,不 disable/replace 任何 `@deepseek-ai/*` |
42
- | 单元与边界测试(`npm test`) | **verified** | `node selftest.test.mjs` 全绿;覆盖 malformed 输入、并发/节流、replay 一致性;**含"用户消息形状"契约测试**(与宿主 `createUserMessage` 逐字段比对,见 [`docs/upstream-contracts.md`](../../docs/upstream-contracts.md) C1) |
43
- | 一次性 Profile 安装·启动·卸载(E3) | **verified** | 2026-10-10 一次性 DSH_HOME 实跑:install → cold start(HTTP 就绪)→ stop → uninstall → `--dump-config` 逐字回到基线;dsh 0.2.0-rc.2;证据见同目录 `EVIDENCE.json` |
44
- | 真实 Profile 运行 | **verified(本机)** | 桌面端 0.2.0-rc.2 实跑;**他人机器 unverified**(未做外部验收);**称呼行 `akasha:address`(1.5.4,order 133)已活体可见**——每回合 runtime context 带「对用户称 Master / 用户称你大肥鱼」并写明执行点(交付 / 验收 / 复盘开口句) |
45
- | 独立安全审计 / 公开分发(E5) | **unverified** | 未做独立审计;分发前应重评 |
46
-
47
- **下一道门(next gate)**:① 在 0.2.1-alpha.x 上按同一套用例复测(一次性 DSH_HOME),把 `unknown` 改为精确结论(`compatible` 或 `incompatible`);
48
- ② 提交 DSH STORE 上架申请(monorepo 子路径:`tree/main/plugins/<name>`)并跟进机器人预检。
49
- 若任一版本复测失败,该版本标注为 `blocked`(不兼容)并保持其余版本声明不动。
50
-
51
- **权限 / 非目标 / 边界(permissions · non-goals · boundaries)**
52
- - 读取:`logs/hooks.jsonl`(蒸馏输入);写入:**仅在触发时**写睡眠报告、`logs/inbox.jsonl`(候选待办)与水位线 `sleep-state.json`;
53
- 失败路径**不推进水位线**(回滚语义,见测试断言);注入失败只降级显示,不抛。
54
- - **统一非目标(non-goals)**:不修改 DSH 核心与官方包;不替换、禁用或遮蔽任何官方组件;不写真实 Profile(测试一律用系统临时目录);
55
- 不在日志/输出里暴露凭据、完整用户文件或注入上下文。
56
- - **测试隔离**:所有自测只使用 `os.tmpdir()` 下的临时目录;不读写真实库(`~/.akasha`)与真实会话档案。
package/cordis.patch.yml CHANGED
@@ -2,7 +2,11 @@
2
2
  - id: akasha-sleep
3
3
  name: '@akasha-book/sleep'
4
4
  config:
5
- # 路径默认 ~/.akasha(见 lib/index.js 的 DEFAULTS);调度参数如下
5
+ akashaDir: '<AKASHA_DIR>'
6
+ log: '<AKASHA_DIR>/logs\hooks.jsonl'
7
+ stateFile: '<AKASHA_DIR>/logs\sleep-state.json'
8
+ inboxFile: '<AKASHA_DIR>/logs\inbox.jsonl'
9
+ reportDir: '<AKASHA_DIR>/reports'
6
10
  minIntervalHours: 6
7
11
  timerCheckMs: 3600000
8
12
  contextOrder: 130
package/lib/index.js CHANGED
@@ -43,7 +43,9 @@ const DEFAULTS = {
43
43
  timerCheckMs: 3600000,
44
44
  contextOrder: 130,
45
45
  pulseOrder: 132,
46
- addressOrder: 133
46
+ addressOrder: 133,
47
+ preflightOrder: 135, // C1|易错点前置
48
+ statefulOrder: 134 // C3|断言前把关(order 134 < 135 ⇒ 先渲染,命中时 C1 让位)
47
49
  };
48
50
 
49
51
  /** 唤醒条是否值得推(有内容才说话):无睡眠记录 / 有待办 / 审计有警告。 */
@@ -64,6 +66,13 @@ export function buildWakeNote(k) {
64
66
  }
65
67
 
66
68
  export function apply(ctx, config = {}) {
69
+
70
+ // A/B 实验开关(2026-10-10 用户解冻,见 docs/ab-experiment-design.md §6):
71
+ // abOff=true ⇒ **关闭臂**——本插件不注册任何守卫/注入/工具,只记一行观测;库文件与日志照旧(便于事后复盘)。
72
+ if (config.abOff === true) {
73
+ try { (ctx.logger?.info ?? (() => {}))('sleep: abOff=true(A/B 关闭臂)——本插件不注册任何面'); } catch { /* 静默 */ }
74
+ return;
75
+ }
67
76
  const cfg = { ...DEFAULTS, ...config };
68
77
  const require_ = createRequire(import.meta.url);
69
78
  const reportDir = join(cfg.akashaDir, 'logs');
@@ -149,14 +158,45 @@ export function apply(ctx, config = {}) {
149
158
 
150
159
  // 回查提示(v1.5,错误钩子的注入级动作):进程内记录近期失败(工具 / agent),窗口内幂等渲染。
151
160
  const recentFailures = [];
161
+ const usedTools = new Set(); // 本会话用过哪些工具(首用预告的判据)
162
+ const announcedTraps = new Set(); // 已预告过的陷阱工具(每工具一次)
152
163
  const noteFailure = (tool) => {
153
164
  try {
154
165
  recentFailures.push({ ts: Date.now(), tool: String(tool ?? '?') });
155
166
  if (recentFailures.length > 20) recentFailures.splice(0, recentFailures.length - 20);
156
167
  } catch { /* 静默 */ }
157
168
  };
158
- ctx.on('tools/result', (exec, result) => { if (result?.isError === true) noteFailure(exec?.name); });
169
+ // 极限观测(2026-10-11 用户令):把「该查库了」变成**可见信号**,不靠记性——针对"200 轮后忘记查库"的观察。
170
+ const obs = { turn: 0, steps: 0, compactions: 0, usedToolsN: 0, lastRecallTurn: null, lastCompactionTurn: null };
171
+ const clip = (s) => (s.length <= 80 ? s : s.slice(0, 79) + '…'); // 行宽硬约束的统一裁剪(模块级,供多行共用)
172
+ const isRecallTool = (name) => /^mcp__akasha__/.test(String(name || ''));
173
+ ctx.on('tools/result', (exec, result) => {
174
+ const tool = String(exec?.name ?? '?');
175
+ usedTools.add(tool); // 首用预告用(2026-10-11)
176
+ obs.usedToolsN = usedTools.size;
177
+ if (isRecallTool(tool)) obs.lastRecallTurn = obs.turn; // MCP 召回 ⇒ 刷新"距上次查库"
178
+ else {
179
+ // CLI 查库也算召回(2026-10-11 实测:本会话大量查库走 `node akasha.mjs …`,只认 MCP 会显示"尚未查库"=自欺)
180
+ try {
181
+ const blob = JSON.stringify(exec?.args ?? exec?.input ?? exec?.params ?? '');
182
+ if (/akasha\.mjs|session\.mjs|mcp\.mjs/.test(blob)) obs.lastRecallTurn = obs.turn;
183
+ } catch { /* 忽略 */ }
184
+ }
185
+ if (result?.isError === true) noteFailure(exec?.name);
186
+ });
159
187
  ctx.on('agent/error', () => noteFailure('agent'));
188
+ // 轮次与压缩真值:能拿到就用真值,拿不到就保持 0(宁缺勿假)
189
+ try { ctx.on('agent/turn-stopping', (p) => { if (p && typeof p.turn === 'number') obs.turn = p.turn; }); } catch { /* 事件不存在 */ }
190
+ try { ctx.on('agent/turn-end', (p) => { if (p && typeof p.turn === 'number') obs.turn = Math.max(obs.turn, p.turn); }); } catch { /* 事件不存在 */ }
191
+ try {
192
+ ctx.on('session/event', (p) => {
193
+ const t = String(p?.type ?? p?.name ?? p?.eventType ?? '');
194
+ if (t.startsWith('compaction/end') || (t === 'compaction' && p?.phase === 'end')) {
195
+ obs.compactions++;
196
+ obs.lastCompactionTurn = obs.turn; // 脆弱窗判据(压缩补偿行用)
197
+ }
198
+ });
199
+ } catch { /* 事件不存在 */ }
160
200
 
161
201
  // 库脉搏(v1.3;v1.5 起附加回查行):写→可见通道——并行会话 / 脚本写入库后,下一回合即可见并命名新内容。
162
202
  ctx.systemPrompt.context({
@@ -191,6 +231,269 @@ export function apply(ctx, config = {}) {
191
231
  }
192
232
  });
193
233
 
234
+ // C3|断言前把关(stateful-fact gate,2026-10-10;评审 v2 定稿=触发改到提问侧)
235
+ // 为什么改触发:context 行在回合开始渲染、回复尚未产生;且本插件看不到用户消息事件 ⇒
236
+ // 取**会话层最近一条 intent 段**(跳过注入样式的段)做 6 类关键词判定。
237
+ // 只提醒不阻断;statefulGate:false ⇒ 提示与事件双停。
238
+ const gateState = { c3Cat: null, c3At: 0, c3Fired: false };
239
+ if (cfg.statefulGate !== false) {
240
+ const STATEFUL = [
241
+ { cat: "发布", re: /已发布|未发布|上架|没过审|已过审|待审/ },
242
+ { cat: "安装", re: /已安装|未安装|装好了|装上了/ },
243
+ { cat: "版本", re: /版本|version/i },
244
+ { cat: "开关/运行", re: /开关|启用|禁用|默认开|默认关|开着|关着|在跑|跑着|还在吗|开着吗|装着吗|有没有开/ },
245
+ { cat: "存在", re: /是否存在|有没有|在不在|还在吗/ },
246
+ ];
247
+ ctx.systemPrompt.context({
248
+ name: "akasha:stateful",
249
+ order: cfg.statefulOrder,
250
+ text: () => {
251
+ try {
252
+ const path = join(cfg.akashaDir, "data", "session.jsonl");
253
+ const lines = readFileSync(path, "utf8").split("\n").slice(-200);
254
+ let gist = null;
255
+ for (let i = lines.length - 1; i >= 0; i--) {
256
+ if (!lines[i].includes("\"kind\":\"intent\"")) continue;
257
+ let r; try { r = JSON.parse(lines[i]); } catch { continue; }
258
+ const g = String(r.gist || "");
259
+ if (!g) continue;
260
+ // 规则化跳过注入段(2026-10-10 生产核对发现:除 runtime context 外还有 openviking / hindsight 等注入块被记成 intent)
261
+ const injected = g.startsWith("<") || g.length > 800
262
+ || /Current runtime context|Relevant memory from|Hindsight|openviking-context|阿卡夏·/.test(g);
263
+ if (injected) continue;
264
+ gist = g; break;
265
+ }
266
+ if (!gist) return null;
267
+ const hit = STATEFUL.find((k) => k.re.test(gist));
268
+ if (!hit) return null;
269
+ const now = Date.now();
270
+ if (gateState.c3Cat === hit.cat && now - gateState.c3At < 30 * 60 * 1000) return null; // 同类 30 分钟去重
271
+ gateState.c3Cat = hit.cat; gateState.c3At = now; gateState.c3Fired = true;
272
+ try {
273
+ appendFileSync(join(cfg.akashaDir, "logs", "hooks.jsonl"),
274
+ JSON.stringify({ ts: new Date().toISOString(), kind: "stateful-reminder", cat: hit.cat }) + "\n", "utf8");
275
+ } catch { /* 事件失败不影响提示 */ }
276
+ return "[断言提醒] 状态性事实——先查再答(例:versions + dist-tags 两处核对)";
277
+ } catch { return null; }
278
+ }
279
+ });
280
+ }
281
+
282
+ // C1|易错点前置(pre-flight hint,2026-10-10 解冻后实现;评审 v2 定稿)
283
+ // 依据:可预防漏召率 91.2%(回合级)、会话级 100% ⇒ 缺的不是知识,是"动作前把库拉进来"。
284
+ // 形态:一行 ≤80 字,只提示不阻断;关(preflightHints:false)则**提示与事件双停**(不写假触发)。
285
+ // 近似说明(如实):context 行在**回合开始**渲染,此刻还不知道本回合会调用什么工具,
286
+ // 故用"最近失败的工具"作为**预提示**(近 30 分钟窗口 + 同工具去重);"本回合尚未查库"这一前置
287
+ // 在渲染时无法判定,改为"上一个回合没有查库动作时更倾向提示"的弱化版(v1 取"总是可提示",由去重与上限约束)。
288
+ const preflightState = { tool: null, at: 0 };
289
+ if (cfg.preflightHints !== false) {
290
+ ctx.systemPrompt.context({
291
+ name: 'akasha:preflight',
292
+ order: cfg.preflightOrder,
293
+ text: () => {
294
+ try {
295
+ if (gateState.c3Fired) return null; // C3 优先(同回合只出一条)
296
+ const lib = require_(join(cfg.akashaDir, 'lib.mjs'));
297
+ const now = Date.now();
298
+ const WINDOW = 30 * 60 * 1000;
299
+ const recent = (recentFailures || []).filter((f) => now - Number(f.ts ?? f.at) <= WINDOW); // 字段是 ts(见 noteFailure)
300
+ const map = lib.RECALL_TRAP_MAP || {};
301
+ // 2026-10-11 修复(用户裁定:反射要能随工程知识纠错与优化):
302
+ // **反射文本随条目走** —— 不再只认写死的表。
303
+ // ① 链感知解析(id 或 id-rN 取链尾);② 条目缺失/已退役 ⇒ **不打**(fail closed,不执行过期反射);
304
+ // ③ 优先用条目自带 `hint`(≤80 字反射文本:**改条目=改反射**);④ `hint_until` 过期 ⇒ 跳过;
305
+ // ⑤ 内置表降级为**过渡兜底**;⑥ 都取不到 ⇒ 不打(宁缺勿滥)。
306
+ // 该表仅作过渡:条目补上 hint 后即成为唯一来源。
307
+ const FALLBACK_ACTIONS = {
308
+ 'canon-trap-edit-context-mismatch': '先 read 原文再改、改完复核',
309
+ 'canon-trap-read-offset-range': '先用 grep 拿真实行号再读',
310
+ 'canon-trap-grep-exit2-path': '换浅范围/排除 node_modules 再 grep',
311
+ 'canon-trap-unknown-tool-name': '只从本会话可见工具面选名字',
312
+ 'canon-trap-provider-method-list': '先列方法清单再调用',
313
+ };
314
+ let canonRecs = [];
315
+ try { canonRecs = lib.currentRecords(lib.loadStore('canon').records); } catch { /* 读不到 ⇒ 只走兜底 */ }
316
+ const resolveEntry = (cid) => canonRecs.filter((r) => String(r.id) === cid || String(r.id).startsWith(cid + '-r')).pop() || null;
317
+ const isRetired = (rec) => !!rec && (/已退役|retired/i.test(String(rec.claim ?? '')) || String(rec.id).endsWith('-retired'));
318
+ const today = new Date().toISOString().slice(0, 10);
319
+ /** 某工具的首选反射:条目 `hint` 优先 → 兜底表;退役/过期/缺失一律跳过;取不到返回 null(fail closed)。
320
+ * ⚠ 口径:**库可读时,条目是唯一来源**——该条目没有 hint 也**不**回落兜底(否则"退役/过期"会被兜底表救活,
321
+ * 等于过期反射照打)。兜底表只在**库读不到**时生效。 */
322
+ const hintForTool = (tool) => {
323
+ const ids = map[tool];
324
+ if (!ids || !ids.length) return null;
325
+ for (const cand of ids) {
326
+ if (canonRecs.length) {
327
+ const rec = resolveEntry(cand);
328
+ if (!rec) continue; // 链上已无此条 ⇒ 跳过
329
+ if (isRetired(rec)) continue; // 已退役 ⇒ 不执行过期反射
330
+ if (rec.hint_until && String(rec.hint_until) < today) continue; // 反射有效期已过 ⇒ 跳过
331
+ if (typeof rec.hint === 'string' && rec.hint.trim()) return { id: cand, act: rec.hint.trim(), hintSource: 'entry' };
332
+ continue; // 有条目但无 hint ⇒ 该候选作罢,不回落
333
+ }
334
+ if (FALLBACK_ACTIONS[cand]) return { id: cand, act: FALLBACK_ACTIONS[cand], hintSource: 'fallback' };
335
+ }
336
+ return null;
337
+ };
338
+ const emitPreflight = (payload) => {
339
+ try { // 事件:关闭时走不到这里(双停)
340
+ appendFileSync(join(cfg.akashaDir, 'logs', 'hooks.jsonl'),
341
+ JSON.stringify({ ts: new Date().toISOString(), kind: 'preflight', ...payload }) + '\n', 'utf8');
342
+ } catch { /* 事件失败不影响提示 */ }
343
+ };
344
+ const trim80 = (s) => (s.length <= 80 ? s : s.slice(0, 79) + '…');
345
+
346
+ // ① 失败驱动(原逻辑):近 30 分钟内有过失败 ⇒ 给"那件工具"的反射
347
+ if (recent.length) {
348
+ const tool = String(recent[recent.length - 1].tool || '');
349
+ const fresh = !(preflightState.tool === tool && now - preflightState.at < WINDOW); // 同工具 30 分钟去重
350
+ if (fresh && map[tool]) {
351
+ const h = hintForTool(tool);
352
+ if (h) {
353
+ preflightState.tool = tool; preflightState.at = now;
354
+ emitPreflight({ tool, entry: h.id, hintSource: h.hintSource, mode: 'failure' });
355
+ return trim80('[坑前提示] ' + tool + ' 前:' + h.act + ' —— 条目 ' + h.id);
356
+ }
357
+ }
358
+ }
359
+
360
+ // ② 首用预告(2026-10-11 新增;补"漏召"的结构缺口):
361
+ // 渲染时**不知道本回合会用什么工具**,但知道**本会话还没用过哪些** ⇒ 逐个把"未用过的陷阱工具"预告一遍,
362
+ // 每个工具只预告一次(用过即止)= 把"事后回查"提前成"动手前预检",不依赖历史失败。
363
+ if (cfg.preflightFirstUse !== false) {
364
+ const nextTool = Object.keys(map).find((t) => !usedTools.has(t) && !announcedTraps.has(t));
365
+ if (nextTool) {
366
+ const h = hintForTool(nextTool);
367
+ if (h) {
368
+ announcedTraps.add(nextTool);
369
+ emitPreflight({ tool: nextTool, entry: h.id, hintSource: h.hintSource, mode: 'first-use' });
370
+ return trim80('[坑前提示·首用] ' + nextTool + ' 还没用过:' + h.act + ' —— 条目 ' + h.id);
371
+ }
372
+ }
373
+ }
374
+ return null;
375
+ } catch { return null; }
376
+ }
377
+ });
378
+ }
379
+
380
+ // 极限观测行(2026-10-11,用户令):轮次维度的机械钩子——把「该查库了」变成可见信号,不靠记性。
381
+ // 只在知道轮次时渲染(拿不到真值就保持静默,不伪造)。硬约束同 C1:**一行、≤80 字**。
382
+ if (cfg.extremeObs !== false) {
383
+ ctx.systemPrompt.context({
384
+ name: 'akasha:extreme',
385
+ order: (cfg.pulseOrder ?? 132) + 2,
386
+ text: () => {
387
+ try {
388
+ if (!obs.turn) return null;
389
+ const gap = obs.lastRecallTurn === null ? '(本进程尚未查库)' : '距今 ' + Math.max(0, obs.turn - obs.lastRecallTurn) + ' 轮';
390
+ // 口径标注(2026-10-11):轮次/压缩/查库均为**本进程**统计——重启即归零;不标会被读成"整场会话"
391
+ const line = '[极限观测·本进程] 轮=' + obs.turn + ' 压缩=' + obs.compactions + ' 上次查库=' + gap;
392
+ return line.length <= 80 ? line : line.slice(0, 79) + '…';
393
+ } catch { return null; }
394
+ }
395
+ });
396
+ }
397
+
398
+ // 压缩补偿行(2026-10-11 用户方向:"针对压缩轮次增加,逐步强行增加使用库查记忆进行补全")。
399
+ // 依据(当晚实测):① 压缩摘要只保话题、**丢约定**——四条核心约定存活率 0–21%,而**每回合注入的称呼层 100% 存活**;
400
+ // ② 「丢记忆」实例(turn 220)落在**压缩后 15 轮内**(压缩在 205)。
401
+ // ⇒ 分级补偿:**脆弱窗**(距上次压缩 ≤15 轮)要求"重要结论先落库";**压缩 ≥3** 且本回合尚未查库 ⇒ 要求先查;
402
+ // **压缩 ≥5** ⇒ 升级为强制;**查过即不再催**(闭环,避免噪音)。
403
+ const RECALL_WINDOW = 15;
404
+ if (cfg.compensate !== false) {
405
+ ctx.systemPrompt.context({
406
+ name: 'akasha:compensate',
407
+ order: (cfg.pulseOrder ?? 132) + 3,
408
+ text: () => {
409
+ try {
410
+ if (!obs.turn || !obs.compactions) return null; // 没压缩过 ⇒ 不打扰
411
+ const sinceComp = obs.lastCompactionTurn === null ? null : Math.max(0, obs.turn - obs.lastCompactionTurn);
412
+ const recalledRecently = obs.lastRecallTurn !== null && (obs.turn - obs.lastRecallTurn) <= 2;
413
+ const inWindow = sinceComp !== null && sinceComp <= RECALL_WINDOW;
414
+ if (obs.compactions >= 5 && !recalledRecently) {
415
+ return clip('[压缩补偿·强制] 本会话已压缩 ' + obs.compactions + ' 次:关键动作前先 akasha_lookup/brief 补全,再动手');
416
+ }
417
+ if (obs.compactions >= 3 && !recalledRecently) {
418
+ return clip('[压缩补偿] 已压缩 ' + obs.compactions + ' 次' + (inWindow ? '(脆弱窗 距上次 ' + sinceComp + ' 轮)' : '') + ':本回合先查库再答');
419
+ }
420
+ if (inWindow) {
421
+ return clip('[压缩补偿·脆弱窗] 距上次压缩 ' + sinceComp + ' 轮:重要结论先落库,动手前先查');
422
+ }
423
+ return null;
424
+ } catch { return null; }
425
+ }
426
+ });
427
+ }
428
+
429
+ // 核心约定常驻行(2026-10-11 用户裁定):**防目标漂移**——压缩丢的是"我们怎么做事"(实测五条约定存活 0–21%),
430
+ // 而每回合注入的称呼层 100% 存活 ⇒ 把核心约定也做成**常驻**。**库驱动**:渲染时读 canon 的
431
+ // `canon-core-conventions` 当前版本的 `line` 字段 ⇒ **改约定=改条目**(不碰代码);读不到 ⇒ 内置兜底(永不为空)。
432
+ // 行宽**破例放宽到 160 字**(其余行均为 80):它是漂移锚,值得多占一行。
433
+ if (cfg.conventions !== false) {
434
+ const CONV_ID = 'canon-core-conventions';
435
+ const FALLBACK_CONVENTIONS = '库无界·装载有界|来源态四标签|修订链:追加不改原文|零编造(红线)|称呼:Master/大肥鱼';
436
+ const clipLong = (s) => (s.length <= 160 ? s : s.slice(0, 159) + '…');
437
+ ctx.systemPrompt.context({
438
+ name: 'akasha:conventions',
439
+ order: (cfg.pulseOrder ?? 132) + 4,
440
+ text: () => {
441
+ try {
442
+ const lib = require_(join(cfg.akashaDir, 'lib.mjs'));
443
+ const recs = lib.currentRecords(lib.loadStore('canon').records);
444
+ const rec = recs.filter((r) => String(r.id) === CONV_ID || String(r.id).startsWith(CONV_ID + '-r')).pop();
445
+ const text = (rec && typeof rec.line === 'string' && rec.line.trim()) ? rec.line.trim() : FALLBACK_CONVENTIONS;
446
+ return clipLong('[约定] ' + text);
447
+ } catch { return clipLong('[约定] ' + FALLBACK_CONVENTIONS); }
448
+ }
449
+ });
450
+ }
451
+
452
+ // ① 语义钩子 + 显式钩子(2026-10-11 用户令「开搞」;补设计稿 §2.4 五类钩子中缺的两类)
453
+ // · **显式钩子**("上次那个"):读**本会话最近 intent 段**(会话层=用户消息的摘要)⇒ 检测指代词 ⇒ 检索六库 ⇒ **一行**指路。
454
+ // ⚠ 已知限制(如实):数据源是**摘要**而非原话,摘要可能丢掉指代词 ⇒ **宁可漏触发不误触发**;
455
+ // 若要原话级,需让 hooks 侧记录 user-message 行(待办)。
456
+ // · **语义钩子**("当前处境作为查询"):本会话近 30 分钟内有工具失败 ⇒ 用工具名检索六库 ⇒ **一行**指路。
457
+ // 两钩子**每回合最多出一行**,优先级 显式 > 语义;都无触发 ⇒ **静默**(不制造噪音);可关 `recallHook:false`。
458
+ if (cfg.recallHook !== false) {
459
+ const PRONOUN = /(上次|之前|上回|那天|那个|说过|记得|当时|前面说|前面提)/;
460
+ const recentIntentGist = () => {
461
+ try {
462
+ const lib = require_(join(cfg.akashaDir, 'lib.mjs'));
463
+ const rows = lib.loadStore('session').records.filter((r) => r.kind === 'intent');
464
+ const last = rows[rows.length - 1];
465
+ return last ? String(last.gist || '') : '';
466
+ } catch { return ''; }
467
+ };
468
+ const clip120 = (s) => (s.length <= 120 ? s : s.slice(0, 119) + '…');
469
+ ctx.systemPrompt.context({
470
+ name: 'akasha:recall',
471
+ order: (cfg.pulseOrder ?? 132) + 5,
472
+ text: () => {
473
+ try {
474
+ const lib = require_(join(cfg.akashaDir, 'lib.mjs'));
475
+ // 显式钩子(指代)
476
+ const gist = recentIntentGist();
477
+ if (gist && PRONOUN.test(gist)) {
478
+ // 去掉**所有**指代词(2026-10-11 修:replace 无 g 标志时只替一次,残留「那个」会污染查询)
479
+ const q = gist.replace(new RegExp(PRONOUN.source, 'g'), ' ').replace(/\s+/g, ' ').trim().slice(0, 60) || gist.slice(0, 60);
480
+ const r = lib.lookupDetailed(q, { limit: 2, contextBudget: false });
481
+ const ids = (r.hits || []).map((h) => String(h.id)).slice(0, 2).join(' · ');
482
+ if (ids) return clip120('[指代检索] 像是问旧事 → 先看:' + ids);
483
+ }
484
+ // 语义钩子(近失败)
485
+ const f = recentFailures[recentFailures.length - 1];
486
+ if (f && obs.turn && Date.now() - f.ts < 30 * 60 * 1000) {
487
+ const r = lib.lookupDetailed(String(f.tool || ''), { limit: 2, contextBudget: false });
488
+ const top = (r.hits || [])[0];
489
+ if (top) return clip120('[处境检索] ' + f.tool + ' 近 30 分钟内失败过 → 相关条目 ' + top.id + ':' + String(top.snippet || '').slice(0, 42));
490
+ }
491
+ return null;
492
+ } catch { return null; }
493
+ }
494
+ });
495
+ }
496
+
194
497
  // 空闲触发(事件失败开放)。
195
498
  ctx.on('agent/status', (payload) => {
196
499
  try { if (payload?.status === 'idle') maybeSleep('idle'); } catch { /* 静默 */ }
package/package.json CHANGED
@@ -1,24 +1,25 @@
1
1
  {
2
2
  "name": "@akasha-book/sleep",
3
- "version": "1.5.4",
4
- "repository": { "type": "git", "url": "https://github.com/Ersith/akasha-book.git", "directory": "plugins/akasha-sleep" },
5
- "engines": { "node": ">=18" },
6
- "license": "MIT",
3
+ "version": "1.7.1",
4
+ "engines": {
5
+ "node": ">=18"
6
+ },
7
7
  "type": "module",
8
- "description": "阿卡夏之书睡眠器 v1.5:调度与注入(蒸馏逻辑归核心库 sleep.mjs,可手动 node akasha.mjs sleep)——空闲/定时触发 + 每回合条子 + 新会话唤醒条 + 库脉搏(写→可见通道)+ 回查提示(窗口内失败追一行)",
9
- "exports": { ".": "./lib/index.js" },
10
- "files": ["lib/index.js", "cordis.patch.yml", "README.md"],
11
- "scripts": { "test": "node selftest.test.mjs" },
8
+ "description": "阿卡夏之书睡眠器 v1.5:调度与注入(蒸馏逻辑归核心库 akasha\\sleep.mjs,可手动 node akasha.mjs sleep)——空闲/定时触发 + 每回合条子 + 新会话唤醒条 + 库脉搏(写→可见通道)+ 回查提示(窗口内失败追一行,错误钩子注入级)",
9
+ "exports": {
10
+ ".": "./lib/index.js"
11
+ },
12
+ "files": [
13
+ "lib/index.js",
14
+ "cordis.patch.yml",
15
+ "README.md"
16
+ ],
12
17
  "dsh": {
13
18
  "bundle": {
14
19
  "patch": "./cordis.patch.yml"
15
- },
16
- "compatibility": {
17
- "dshReleases": {
18
- "0.2.0-rc.2": "compatible",
19
- "0.2.1-alpha.1": "compatible",
20
- "0.2.1-alpha.2": "compatible"
21
- }
22
20
  }
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
23
24
  }
24
25
  }