@akasha-book/session 0.2.4 → 0.2.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/README.md +12 -12
- package/lib/index.js +48 -12
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# @akasha-book/session(小阿卡夏)
|
|
2
|
-
|
|
3
|
-
阿卡夏之书**会话层**插件 —— **自动增量索引 + 压缩事件行 + 节奏条 + 循环观测 P0(dry 干跑)**。
|
|
4
|
-
核心逻辑归 `core/session.mjs` 与 `core/loop-detect.mjs`(本插件只做调度与注入;降级只留痕,不炸宿主)。
|
|
5
|
-
|
|
6
|
-
- **触发**:`session/event → turn/end`(按会话去抖 `minIndexIntervalMs`,默认 30s)→ 增量索引(水位 + 指纹双幂等);`agent/status → idle` 追平;`ctx.interval` 兜底。
|
|
7
|
-
- **压缩事件行**:`session/event → compaction/end`(成功压缩)→
|
|
8
|
-
- **循环观测 P0**:**dry 干跑——只观测、只写观测线;绝不干预**。流环=`agent/assistant-stream` 帧按 attemptId 累积(每 480 字符对尾 8K 跑 `detectLoops`);turn 环=事件流缓存全文兜底;命中与帧类型探针写 `logs/hooks.jsonl`。汇总 CLI:`node akasha.mjs session loopwatch`。
|
|
9
|
-
- **节奏条**:`systemPrompt.context`(`akasha:session`,order 134)——段数 / 会话数 + 回看提示;无数据静默。
|
|
10
|
-
- **索引观测线**:`session-index`(added / skipped / parseFails / ms / **lagMs**=事件循环延迟采样)/ `session-index-skip` / `session-index-error` / **`session-index-slow`**(单次索引 >1.5s 告警,含 lagMs)。
|
|
11
|
-
- 配置:`akashaDir`(核心库位置)/ `sessionsRoot`(宿主会话档案目录)/ `storeFile` / `metaFile` / `log`;默认见 `lib/index.js` 的 DEFAULTS。
|
|
12
|
-
- 自测:`node selftest.mjs`(桩 ctx 端到端;临时目录,不碰真实数据;需与本包 `core/` 并列)。
|
|
1
|
+
# @akasha-book/session(小阿卡夏)
|
|
2
|
+
|
|
3
|
+
阿卡夏之书**会话层**插件 —— **自动增量索引 + 压缩事件行 + 节奏条 + 循环观测 P0(dry 干跑)**。
|
|
4
|
+
核心逻辑归 `core/session.mjs` 与 `core/loop-detect.mjs`(本插件只做调度与注入;降级只留痕,不炸宿主)。
|
|
5
|
+
|
|
6
|
+
- **触发**:`session/event → turn/end`(按会话去抖 `minIndexIntervalMs`,默认 30s)→ 增量索引(水位 + 指纹双幂等);`agent/status → idle` 追平;`ctx.interval` 兜底。
|
|
7
|
+
- **压缩事件行**:`session/event → compaction/end`(成功压缩)→ 先强制补索引(绕过去抖/背压)→ 观测线(带 `indexed`/`reason`)+ 节奏条点名。**只有补索引确认成功才说「已收入」**;失败时改说「可能未进会话层」并给回退(原始会话档案 / `session index`),同会话后续索引成功即改口(wave1 · B1)。
|
|
8
|
+
- **循环观测 P0**:**dry 干跑——只观测、只写观测线;绝不干预**。流环=`agent/assistant-stream` 帧按 attemptId 累积(每 480 字符对尾 8K 跑 `detectLoops`);turn 环=事件流缓存全文兜底;命中与帧类型探针写 `logs/hooks.jsonl`。汇总 CLI:`node akasha.mjs session loopwatch`。
|
|
9
|
+
- **节奏条**:`systemPrompt.context`(`akasha:session`,order 134)——段数 / 会话数 + 回看提示;无数据静默。
|
|
10
|
+
- **索引观测线**:`session-index`(added / skipped / parseFails / ms / **lagMs**=事件循环延迟采样)/ `session-index-skip` / `session-index-error` / **`session-index-slow`**(单次索引 >1.5s 告警,含 lagMs)。
|
|
11
|
+
- 配置:`akashaDir`(核心库位置)/ `sessionsRoot`(宿主会话档案目录)/ `storeFile` / `metaFile` / `log`;默认见 `lib/index.js` 的 DEFAULTS。
|
|
12
|
+
- 自测:`node selftest.mjs`(桩 ctx 端到端;临时目录,不碰真实数据;需与本包 `core/` 并列)。
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 阿卡夏之书会话层插件 v0.2(@akasha-book/
|
|
1
|
+
// 阿卡夏之书会话层插件 v0.2(@akasha-book/session)—— 自动增量索引 + 压缩事件行 + 节奏条 + 循环观测 P0(dry)。
|
|
2
2
|
// · 核心逻辑归 ~/.akasha/session.mjs(插件只做调度与注入;降级只留痕,不炸宿主)
|
|
3
3
|
// · 观测线(hooks.jsonl):session-armed / session-index / session-index-skip / session-index-error / session-compact
|
|
4
4
|
// + 循环观测(P0 干跑,2026-10-07):loop-watch / loop-watch-probe —— **只观测、绝不干预**(不 steer / 不 cancel / 不注入)
|
|
@@ -37,7 +37,19 @@ export function effectiveThrottle(size, baseMs) {
|
|
|
37
37
|
return size > 16 * 1024 * 1024 ? baseMs * 4 : baseMs;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
40
|
+
/** 压缩后补索引失败时的原因短语(给节奏条用;未知原因如实写「未确认」)。 */
|
|
41
|
+
const COMPACT_FAIL_REASON = {
|
|
42
|
+
'not-found': '未找到会话档案',
|
|
43
|
+
backoff: '近期索引失败、处于背压窗口',
|
|
44
|
+
error: '索引出错',
|
|
45
|
+
'no-session': '事件缺会话 id'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 节奏条渲染(纯函数):段数/会话数 + 回看提示;压缩窗口内追一行。无数据 → null(静默)。
|
|
50
|
+
* B1(2026-10 wave1):只有「最近一次压缩的补索引确认成功」(compacts[i].indexed === true)才可说「已收入」;
|
|
51
|
+
* 失败 / 未确认(含旧格式条目无 indexed 字段)一律改说「可能未进会话层」并给回退路径——提示行不得替索引作伪证。
|
|
52
|
+
*/
|
|
41
53
|
export function renderSessionLine(state, now) {
|
|
42
54
|
const sessions = state?.meta?.sessions ?? {};
|
|
43
55
|
const ids = Object.keys(sessions);
|
|
@@ -51,7 +63,13 @@ export function renderSessionLine(state, now) {
|
|
|
51
63
|
const last = compacts[compacts.length - 1];
|
|
52
64
|
const s = state?.meta?.sessions?.[last?.session];
|
|
53
65
|
const counts = s ? `(该会话已索引 ${Number(s.segments ?? 0)} 段 · 结论 ${Number(s.conclusions ?? 0)} 条)` : '';
|
|
54
|
-
|
|
66
|
+
if (last?.indexed === true) {
|
|
67
|
+
lines.push(`⚠ 近期发生压缩(${compacts.length} 次)——本段历史已收入会话层${counts};细节查 akasha_session_lookup`);
|
|
68
|
+
} else {
|
|
69
|
+
const why = COMPACT_FAIL_REASON[last?.reason] ?? '补索引未确认';
|
|
70
|
+
lines.push(`⚠ 近期发生压缩(${compacts.length} 次),但压缩后补索引未成功(${why})——这段历史可能未进会话层;` +
|
|
71
|
+
`勿凭会话层断言「查无此事」,回看请查原始会话档案,或手动 node akasha.mjs session index <会话id>`);
|
|
72
|
+
}
|
|
55
73
|
}
|
|
56
74
|
return lines.join('\n');
|
|
57
75
|
}
|
|
@@ -223,20 +241,30 @@ export function apply(ctx, config = {}) {
|
|
|
223
241
|
}
|
|
224
242
|
};
|
|
225
243
|
|
|
226
|
-
|
|
244
|
+
// 某会话索引成功 → 该会话窗口内「补索引未确认」的压缩条目改记为已收入(档案是全量的,后补成功即真收入)。
|
|
245
|
+
const settleCompacts = (sid, trigger) => {
|
|
246
|
+
for (const c of compacts) if (c.session === sid && c.indexed !== true) { c.indexed = true; c.reason = 'recovered'; c.recoveredBy = trigger; }
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 返回索引结论(B1):{ ok:true, reason:'indexed'|'unchanged' } = 档案内容已在会话层;
|
|
251
|
+
* { ok:false, reason:'no-session'|'backoff'|'not-found'|'error' } = 没收进去;
|
|
252
|
+
* { ok:null, reason:'debounced' } = 去抖跳过(不作结论)。opts.force(压缩路径)绕过去抖与失败背压。
|
|
253
|
+
*/
|
|
254
|
+
const indexNow = (sidIn, trigger, extra = {}, opts = {}) => {
|
|
227
255
|
const sid = String(sidIn ?? '').replace(/^session-/, ''); // 前缀归一:宿主事件常见 'session-<id>',目录 / 记账恒用裸 id
|
|
228
256
|
try {
|
|
229
|
-
if (!sid) return; // 空 id:静默跳过(正常路径不出现;见 README 降级节)
|
|
257
|
+
if (!sid) return { ok: false, reason: 'no-session' }; // 空 id:静默跳过(正常路径不出现;见 README 降级节)
|
|
230
258
|
const now = Date.now();
|
|
231
|
-
if (now - (lastFailAt.get(sid) ?? 0) < FAIL_BACKOFF_MS) return; // 失败背压:窗口内静默
|
|
259
|
+
if (!opts.force && now - (lastFailAt.get(sid) ?? 0) < FAIL_BACKOFF_MS) return { ok: false, reason: 'backoff' }; // 失败背压:窗口内静默
|
|
232
260
|
const file = core().resolveSessionFile(cfg.sessionsRoot, sid);
|
|
233
|
-
if (!file) { lastFailAt.set(sid, now); log({ kind: 'session-index-error', session: sid, trigger, message: '未找到会话档案' }); return; }
|
|
261
|
+
if (!file) { lastFailAt.set(sid, now); log({ kind: 'session-index-error', session: sid, trigger, message: '未找到会话档案' }); return { ok: false, reason: 'not-found' }; }
|
|
234
262
|
lastFailAt.delete(sid);
|
|
235
263
|
let st = null;
|
|
236
264
|
try { st = statSync(file); } catch { /* stat 失败 → 走全量 */ }
|
|
237
|
-
if (!shouldIndex(lastIndexedAt.get(sid) ?? 0, now, effectiveThrottle(st?.size ?? 0, cfg.minIndexIntervalMs))) {
|
|
265
|
+
if (!opts.force && !shouldIndex(lastIndexedAt.get(sid) ?? 0, now, effectiveThrottle(st?.size ?? 0, cfg.minIndexIntervalMs))) {
|
|
238
266
|
log({ kind: 'session-index-skip', session: sid, trigger, sinceMs: now - (lastIndexedAt.get(sid) ?? 0) });
|
|
239
|
-
return;
|
|
267
|
+
return { ok: null, reason: 'debounced' };
|
|
240
268
|
}
|
|
241
269
|
// 设计:去抖标记先置位——索引失败后 30s 内不重试(错误背压);统一失败窗口见 lastFailAt
|
|
242
270
|
lastIndexedAt.set(sid, now);
|
|
@@ -245,7 +273,8 @@ export function apply(ctx, config = {}) {
|
|
|
245
273
|
const prev = lastStat.get(sid);
|
|
246
274
|
if (prev && prev.size === st.size && prev.mtimeMs === st.mtimeMs) {
|
|
247
275
|
log({ kind: 'session-index-skip', session: sid, trigger, reason: 'unchanged' });
|
|
248
|
-
|
|
276
|
+
settleCompacts(sid, trigger);
|
|
277
|
+
return { ok: true, reason: 'unchanged' };
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
const t0 = Date.now();
|
|
@@ -254,11 +283,14 @@ export function apply(ctx, config = {}) {
|
|
|
254
283
|
const dur = Date.now() - t0;
|
|
255
284
|
log({ kind: 'session-index', session: sid, trigger, turn: extra.turn ?? null, added: r.added, skipped: r.skipped, parseFails: r.parseFails, frameFails: r.frameFails ?? 0, ms: dur, lagMs: loopLagMs });
|
|
256
285
|
if (dur > 1500) log({ kind: 'session-index-slow', session: sid, trigger, ms: dur, lagMs: loopLagMs, added: r.added }); // >1.5s 告警线(2026-10-07 补4)
|
|
286
|
+
settleCompacts(sid, trigger);
|
|
287
|
+
return { ok: true, reason: 'indexed', added: r.added };
|
|
257
288
|
} catch (error) {
|
|
258
289
|
const msg = String(error?.message ?? error);
|
|
259
290
|
const hint = /not yet fully loaded|does not provide an export named|Cannot find module/.test(msg) ? '(疑似宿主模块代缓存——重启桌面端后恢复)' : '';
|
|
260
291
|
lastFailAt.set(sid, Date.now());
|
|
261
292
|
log({ kind: 'session-index-error', session: sid, trigger, message: (msg + hint).slice(0, 300) });
|
|
293
|
+
return { ok: false, reason: 'error' };
|
|
262
294
|
}
|
|
263
295
|
};
|
|
264
296
|
|
|
@@ -268,9 +300,13 @@ export function apply(ctx, config = {}) {
|
|
|
268
300
|
if (event?.type === 'turn/end') indexNow(String(session?.id ?? ''), 'turn-end', { turn: event?.data?.turn ?? null });
|
|
269
301
|
else if (event?.type === 'compaction/end' && event?.data?.error === void 0) {
|
|
270
302
|
const sid = String(session?.id ?? '').replace(/^session-/, '');
|
|
271
|
-
|
|
303
|
+
// 先索引再宣称「已收入」(2026-10 复查);B1(wave1):宣称与否取决于索引结论——
|
|
304
|
+
// 压缩是稀有且关键的时刻,force 绕过去抖/背压强制补一次;失败如实记 indexed:false,节奏条不说「已收入」。
|
|
305
|
+
const res = indexNow(sid, 'compaction', {}, { force: true }) ?? { ok: false, reason: 'error' };
|
|
306
|
+
const indexed = res.ok === true;
|
|
307
|
+
compacts.push({ session: sid, ts: Date.now(), indexed, reason: res.reason });
|
|
272
308
|
if (compacts.length > 20) compacts.splice(0, compacts.length - 20);
|
|
273
|
-
log({ kind: 'session-compact', session: sid });
|
|
309
|
+
log({ kind: 'session-compact', session: sid, indexedFirst: true, indexed, reason: res.reason });
|
|
274
310
|
}
|
|
275
311
|
} catch { /* 静默 */ }
|
|
276
312
|
});
|
package/package.json
CHANGED