@akasha-book/session 0.0.0-stage → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/cordis.patch.yml +7 -0
- package/lib/index.js +314 -0
- package/package.json +18 -4
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @akasha-book/session(小阿卡夏)
|
|
2
2
|
|
|
3
|
-
|
|
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/` 并列)。
|
package/cordis.patch.yml
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// 阿卡夏之书会话层插件 v0.2(@akasha-book/akasha-session)—— 自动增量索引 + 压缩事件行 + 节奏条 + 循环观测 P0(dry)。
|
|
2
|
+
// · 核心逻辑归 ~/.akasha/session.mjs(插件只做调度与注入;降级只留痕,不炸宿主)
|
|
3
|
+
// · 观测线(hooks.jsonl):session-armed / session-index / session-index-skip / session-index-error / session-compact
|
|
4
|
+
// + 循环观测(P0 干跑,2026-10-07):loop-watch / loop-watch-probe —— **只观测、绝不干预**(不 steer / 不 cancel / 不注入)
|
|
5
|
+
// 实时面=`agent/assistant-stream` 帧(现役 v4 格式;`assistant/chunk` 为 V0 遗物,仅兼容保留)
|
|
6
|
+
// · 条子:systemPrompt.context(akasha:session,order 134);无内容静默
|
|
7
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const inject = ['systemPrompt', 'timer'];
|
|
13
|
+
|
|
14
|
+
// 默认值:家目录(~/.akasha);sessionsRoot 按你的宿主实际会话档案目录配置。
|
|
15
|
+
const AKASHA_HOME = join(homedir(), '.akasha');
|
|
16
|
+
const DEFAULTS = {
|
|
17
|
+
log: join(AKASHA_HOME, 'logs', 'hooks.jsonl'),
|
|
18
|
+
akashaDir: AKASHA_HOME,
|
|
19
|
+
sessionsRoot: join(homedir(), '.dsh', 'sessions'),
|
|
20
|
+
storeFile: join(AKASHA_HOME, 'data', 'session.jsonl'),
|
|
21
|
+
metaFile: join(AKASHA_HOME, 'data', 'session-meta.json'),
|
|
22
|
+
minIndexIntervalMs: 30000,
|
|
23
|
+
timerCheckMs: 240000,
|
|
24
|
+
contextOrder: 134,
|
|
25
|
+
loopWatch: true,
|
|
26
|
+
watchMinChars: 200,
|
|
27
|
+
watchCheckEvery: 480
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** 去抖判定(纯函数)。 */
|
|
31
|
+
export function shouldIndex(lastIndexedAt, now, minMs) {
|
|
32
|
+
return !Number.isFinite(lastIndexedAt) || lastIndexedAt <= 0 || now - lastIndexedAt >= minMs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 自适应节流(纯函数):档案 >16MB 时节流 ×4(防大会话增长后每次触发阻塞宿主)。 */
|
|
36
|
+
export function effectiveThrottle(size, baseMs) {
|
|
37
|
+
return size > 16 * 1024 * 1024 ? baseMs * 4 : baseMs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 节奏条渲染(纯函数):段数/会话数 + 回看提示;压缩窗口内追一行。无数据 → null(静默)。 */
|
|
41
|
+
export function renderSessionLine(state, now) {
|
|
42
|
+
const sessions = state?.meta?.sessions ?? {};
|
|
43
|
+
const ids = Object.keys(sessions);
|
|
44
|
+
if (!ids.length) return null;
|
|
45
|
+
let segments = 0;
|
|
46
|
+
for (const id of ids) segments += Number(sessions[id]?.segments ?? 0);
|
|
47
|
+
if (!segments) return null;
|
|
48
|
+
const lines = [`小阿卡夏·会话层:已索引 ${segments} 段(${ids.length} 会话);回看用 node akasha.mjs session lookup <词>`];
|
|
49
|
+
const compacts = (state?.compacts ?? []).filter((c) => Number.isFinite(c?.ts) && now - c.ts <= 10 * 60 * 1000);
|
|
50
|
+
if (compacts.length) {
|
|
51
|
+
const last = compacts[compacts.length - 1];
|
|
52
|
+
const s = state?.meta?.sessions?.[last?.session];
|
|
53
|
+
const counts = s ? `(该会话已索引 ${Number(s.segments ?? 0)} 段 · 结论 ${Number(s.conclusions ?? 0)} 条)` : '';
|
|
54
|
+
lines.push(`⚠ 近期发生压缩(${compacts.length} 次)——本段历史已收入会话层${counts};细节查 akasha_session_lookup`);
|
|
55
|
+
}
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 循环观测 P0(dry):chunk 归类——真值对 @deepseek-ai/dsh-llm StreamChunk(text-delta / reasoning-delta / tool-call-delta)。 */
|
|
60
|
+
export function classifyChunk(chunk) {
|
|
61
|
+
const t = chunk?.type;
|
|
62
|
+
if (t === 'reasoning-delta') return { stream: 'reasoning', text: String(chunk.text ?? '') };
|
|
63
|
+
if (t === 'text-delta') return { stream: 'text', text: String(chunk.text ?? '') };
|
|
64
|
+
if (t === 'tool-call-delta') return { stream: 'tool', name: String(chunk.name ?? '') };
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 流式节流(纯函数):累计 ≥ minChars 且距上次检查 ≥ every 才查。 */
|
|
69
|
+
export function shouldCheckStream(bufferedLen, lastCheckedLen, every, minChars) {
|
|
70
|
+
return bufferedLen >= minChars && bufferedLen - lastCheckedLen >= every;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 命中摘要(纯函数):exact > variant > punct 取最强未报类型;hitSet 去重;摘录 ≤60。 */
|
|
74
|
+
export function summarizeWatch(result, stream, atChars, hitSet, phase = 'stream') {
|
|
75
|
+
const order = [['exact', result?.exact?.[0]], ['variant', result?.variant?.[0]], ['punct', result?.punctRuns?.[0]]];
|
|
76
|
+
for (const [type, hit] of order) {
|
|
77
|
+
if (!hit) continue;
|
|
78
|
+
if (hitSet?.has(type)) continue;
|
|
79
|
+
if (hitSet) hitSet.add(type);
|
|
80
|
+
const excerpt = String(typeof hit === 'string' ? hit : hit.excerpt ?? '').slice(0, 60);
|
|
81
|
+
const extra = (typeof hit === 'object' && hit.count) ? { count: hit.count } : {};
|
|
82
|
+
return { phase, stream, atChars, hit: { type, excerpt, ...extra } };
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function apply(ctx, config = {}) {
|
|
88
|
+
const cfg = { ...DEFAULTS, ...config };
|
|
89
|
+
const require_ = createRequire(import.meta.url);
|
|
90
|
+
const core = () => require_(join(cfg.akashaDir, 'session.mjs'));
|
|
91
|
+
|
|
92
|
+
const log = (record) => {
|
|
93
|
+
try {
|
|
94
|
+
mkdirSync(dirname(cfg.log), { recursive: true });
|
|
95
|
+
appendFileSync(cfg.log, JSON.stringify({ ts: new Date().toISOString(), ...record }) + '\n', 'utf8');
|
|
96
|
+
} catch { /* 观测线写不了不拖垮宿主 */ }
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const readMeta = () => { try { return JSON.parse(readFileSync(cfg.metaFile, 'utf8')); } catch { return null; } };
|
|
100
|
+
|
|
101
|
+
// 事件循环延迟采样(2026-10-07,观测用;unref 不拖住进程):每秒 tick 偏差 ≈ 采样期阻塞。
|
|
102
|
+
let loopLastTick = Date.now();
|
|
103
|
+
let loopLagMs = 0;
|
|
104
|
+
const loopTimer = setInterval(() => {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
loopLagMs = Math.max(0, now - loopLastTick - 1000);
|
|
107
|
+
loopLastTick = now;
|
|
108
|
+
}, 1000);
|
|
109
|
+
if (typeof loopTimer.unref === 'function') loopTimer.unref();
|
|
110
|
+
|
|
111
|
+
// 每会话去抖(进程内;重启后首触发即补跑,不丢段)。
|
|
112
|
+
const lastIndexedAt = new Map();
|
|
113
|
+
// 档案快照(size + mtimeMs):两者都相同 ⇒ 没有新字节,跳过重读;**只在索引成功后记账**(失败可重试)。
|
|
114
|
+
const lastStat = new Map();
|
|
115
|
+
// 统一失败背压(未命中 / 链接失败 / 执行失败:窗口内静默,不重复扫 + 不重复报错;含宿主模块代缓存场景)。
|
|
116
|
+
const lastFailAt = new Map();
|
|
117
|
+
const FAIL_BACKOFF_MS = 5 * 60 * 1000;
|
|
118
|
+
|
|
119
|
+
// 压缩事件窗口(进程内近况;重启后清空——只影响提示行,不影响数据)。
|
|
120
|
+
const compacts = [];
|
|
121
|
+
|
|
122
|
+
// ---------- 循环防护 P0(dry 干跑观测,受控解冻) ----------
|
|
123
|
+
// 红线:只观测、只写 hooks 观测线;绝不 steer / cancel / 注入。
|
|
124
|
+
const watchEnabled = cfg.loopWatch !== false;
|
|
125
|
+
const watchBuf = new Map(); // `${sid}:${attempt}:${index}:${stream}` -> { text, seen, lastChecked, done, hit:Set }
|
|
126
|
+
const watchProbe = new Map(); // sid -> Set(帧/chunk 类型;前 8 个各记一行,防真值漂移)
|
|
127
|
+
const watchTurnDone = new Set(); // `${sid}:${turn}` 去重
|
|
128
|
+
const attemptTurns = new Map(); // attemptId -> {turn, step}(chunk 帧不带 turn/step,start 帧带——2026-10-07 复查)
|
|
129
|
+
const lastAssistant = new Map(); // sid -> {turn, message}(事件流缓存;官方 Session 无同步读者可用)
|
|
130
|
+
const watchLoopCore = () => require_(join(cfg.akashaDir, 'loop-detect.mjs'));
|
|
131
|
+
|
|
132
|
+
const probeType = (sid, tag) => {
|
|
133
|
+
const seen = watchProbe.get(sid) ?? new Set();
|
|
134
|
+
if (seen.has(tag) || seen.size >= 8) return;
|
|
135
|
+
seen.add(tag);
|
|
136
|
+
watchProbe.set(sid, seen);
|
|
137
|
+
log({ kind: 'loop-watch-probe', session: sid, type: tag });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const feedStreamChunk = (sid, turn, step, keyPrefix, cls) => {
|
|
141
|
+
const key = `${keyPrefix}:${cls.stream}`;
|
|
142
|
+
let buf = watchBuf.get(key);
|
|
143
|
+
if (!buf) {
|
|
144
|
+
buf = { text: '', seen: 0, lastChecked: 0, done: false, hit: new Set() };
|
|
145
|
+
watchBuf.set(key, buf);
|
|
146
|
+
if (watchBuf.size > 64) {
|
|
147
|
+
let victim = null;
|
|
148
|
+
for (const [k, v] of watchBuf) { if (v.done) { victim = k; break; } } // 优先淘汰已收工的(收口点复核 F2)
|
|
149
|
+
watchBuf.delete(victim ?? watchBuf.keys().next().value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (buf.done) return;
|
|
153
|
+
buf.text += cls.text;
|
|
154
|
+
buf.seen += cls.text.length; // 单调累计:缓冲截尾不影响节流(复核 F1——饱和后尾段仍继续检查)
|
|
155
|
+
if (buf.text.length > 20000) buf.text = buf.text.slice(buf.text.length - 20000);
|
|
156
|
+
if (!shouldCheckStream(buf.seen, buf.lastChecked, cfg.watchCheckEvery, cfg.watchMinChars)) return;
|
|
157
|
+
buf.lastChecked = buf.seen;
|
|
158
|
+
const res = watchLoopCore().detectLoops(buf.text.slice(-8000), { window: 100, blockCount: 3, maxWindows: 9000 });
|
|
159
|
+
const sum = summarizeWatch(res, cls.stream, buf.seen, buf.hit);
|
|
160
|
+
if (sum) {
|
|
161
|
+
buf.done = true;
|
|
162
|
+
log({ kind: 'loop-watch', session: sid, turn, step, ...sum });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// V0 遗物兼容:`assistant/chunk` 事件(现役 v4 格式不再发此事件;字段面保留,防旧宿主)。
|
|
167
|
+
const onChunk = (session, event) => {
|
|
168
|
+
const sid = String(session?.id ?? '').replace(/^session-/, '');
|
|
169
|
+
const chunk = event?.data?.chunk;
|
|
170
|
+
if (!sid || !chunk || typeof chunk.type !== 'string') return;
|
|
171
|
+
probeType(sid, chunk.type);
|
|
172
|
+
const cls = classifyChunk(chunk);
|
|
173
|
+
if (!cls || cls.stream === 'tool') return;
|
|
174
|
+
feedStreamChunk(sid, event?.data?.turn ?? null, event?.data?.step ?? null, `${sid}:${event?.data?.turn ?? '?'}:${chunk.index ?? 0}`, cls);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// 现役格式:`agent/assistant-stream` 进程内瞬态帧(真值对 dsh-agent-loop AssistantStreamAttempt 与 dsh-headless 消费口径)。
|
|
178
|
+
// frame:{type:'start'|'chunk'|'end', attemptId, turn, step, chunk?};chunk:text-delta/reasoning-delta/tool-call-delta/block-*/usage/finish。
|
|
179
|
+
const onStreamFrame = (agent, frame) => {
|
|
180
|
+
const sid = String(agent?.session?.id ?? agent?.id ?? '').replace(/^session-/, '');
|
|
181
|
+
const frameType = String(frame?.type ?? '');
|
|
182
|
+
if (!sid || !frameType) return;
|
|
183
|
+
probeType(sid, frameType + (frame?.chunk?.type ? '/' + frame.chunk.type : ''));
|
|
184
|
+
const attemptId = String(frame?.attemptId ?? '');
|
|
185
|
+
if (frameType === 'start') { // start 帧带 turn/step;chunk 帧不带(2026-10-07 复查)→ 缓存供补全
|
|
186
|
+
attemptTurns.set(attemptId, { turn: frame?.turn ?? null, step: frame?.step ?? null });
|
|
187
|
+
if (attemptTurns.size > 64) attemptTurns.delete(attemptTurns.keys().next().value);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (frameType === 'end') { attemptTurns.delete(attemptId); return; }
|
|
191
|
+
if (frameType !== 'chunk') return;
|
|
192
|
+
const cls = classifyChunk(frame.chunk);
|
|
193
|
+
if (!cls || cls.stream === 'tool') return;
|
|
194
|
+
const meta = attemptTurns.get(attemptId) ?? {};
|
|
195
|
+
feedStreamChunk(sid, meta.turn ?? frame?.turn ?? null, meta.step ?? frame?.step ?? null, `${sid}:${attemptId || '?'}:${frame.chunk?.index ?? 0}`, cls);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const rememberAssistant = (session, event) => {
|
|
199
|
+
const sid = String(session?.id ?? '').replace(/^session-/, '');
|
|
200
|
+
if (!sid) return;
|
|
201
|
+
lastAssistant.set(sid, { turn: event?.data?.turn ?? null, message: event?.data?.message });
|
|
202
|
+
if (lastAssistant.size > 64) lastAssistant.delete(lastAssistant.keys().next().value);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// turn 环:全文检测。取数=事件流缓存(2026-10-07 复查:官方 Session 无 .events 属性,
|
|
206
|
+
// 同步读者 snapshotEvents/ownEvents 已被官方弃用「新调用禁止」——宿主容器改事件驱动,插件不得伸手进日志)。
|
|
207
|
+
const onTurnWatch = (sidIn, turn) => {
|
|
208
|
+
const sid = String(sidIn ?? '').replace(/^session-/, '');
|
|
209
|
+
const key = `${sid}:${turn ?? '?'}`;
|
|
210
|
+
if (!sid || watchTurnDone.has(key)) return;
|
|
211
|
+
const cached = lastAssistant.get(sid);
|
|
212
|
+
if (!cached?.message) return;
|
|
213
|
+
if (turn != null && cached.turn !== turn) return; // 缓存尚未到该回合(等 turn/end 兜底)
|
|
214
|
+
watchTurnDone.add(key);
|
|
215
|
+
if (watchTurnDone.size > 200) watchTurnDone.delete(watchTurnDone.keys().next().value);
|
|
216
|
+
const message = cached.message;
|
|
217
|
+
for (const stream of ['reasoning', 'text']) {
|
|
218
|
+
const text = (message.content ?? []).filter((b) => b?.type === stream).map((b) => String(b.text ?? '')).join('');
|
|
219
|
+
if (text.length < 120) continue;
|
|
220
|
+
const res = watchLoopCore().detectLoops(text, {});
|
|
221
|
+
const sum = summarizeWatch(res, stream, text.length, new Set(), 'turn');
|
|
222
|
+
if (sum) log({ kind: 'loop-watch', session: sid, turn: turn ?? null, phase: 'turn', stream, chars: text.length, hit: sum.hit });
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const indexNow = (sidIn, trigger, extra = {}) => {
|
|
227
|
+
const sid = String(sidIn ?? '').replace(/^session-/, ''); // 前缀归一:宿主事件常见 'session-<id>',目录 / 记账恒用裸 id
|
|
228
|
+
try {
|
|
229
|
+
if (!sid) return; // 空 id:静默跳过(正常路径不出现;见 README 降级节)
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
if (now - (lastFailAt.get(sid) ?? 0) < FAIL_BACKOFF_MS) return; // 失败背压:窗口内静默
|
|
232
|
+
const file = core().resolveSessionFile(cfg.sessionsRoot, sid);
|
|
233
|
+
if (!file) { lastFailAt.set(sid, now); log({ kind: 'session-index-error', session: sid, trigger, message: '未找到会话档案' }); return; }
|
|
234
|
+
lastFailAt.delete(sid);
|
|
235
|
+
let st = null;
|
|
236
|
+
try { st = statSync(file); } catch { /* stat 失败 → 走全量 */ }
|
|
237
|
+
if (!shouldIndex(lastIndexedAt.get(sid) ?? 0, now, effectiveThrottle(st?.size ?? 0, cfg.minIndexIntervalMs))) {
|
|
238
|
+
log({ kind: 'session-index-skip', session: sid, trigger, sinceMs: now - (lastIndexedAt.get(sid) ?? 0) });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// 设计:去抖标记先置位——索引失败后 30s 内不重试(错误背压);统一失败窗口见 lastFailAt
|
|
242
|
+
lastIndexedAt.set(sid, now);
|
|
243
|
+
// 廉价快路:字节与 mtime 都没变 → 没有新内容,跳过重读(省一次全量解压)
|
|
244
|
+
if (st) {
|
|
245
|
+
const prev = lastStat.get(sid);
|
|
246
|
+
if (prev && prev.size === st.size && prev.mtimeMs === st.mtimeMs) {
|
|
247
|
+
log({ kind: 'session-index-skip', session: sid, trigger, reason: 'unchanged' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const t0 = Date.now();
|
|
252
|
+
const r = core().indexSession({ file, session: sid, storeFile: cfg.storeFile, metaFile: cfg.metaFile });
|
|
253
|
+
if (st) lastStat.set(sid, { size: st.size, mtimeMs: st.mtimeMs }); // 只记成功:失败后同档可重试
|
|
254
|
+
const dur = Date.now() - t0;
|
|
255
|
+
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
|
+
if (dur > 1500) log({ kind: 'session-index-slow', session: sid, trigger, ms: dur, lagMs: loopLagMs, added: r.added }); // >1.5s 告警线(2026-10-07 补4)
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const msg = String(error?.message ?? error);
|
|
259
|
+
const hint = /not yet fully loaded|does not provide an export named|Cannot find module/.test(msg) ? '(疑似宿主模块代缓存——重启桌面端后恢复)' : '';
|
|
260
|
+
lastFailAt.set(sid, Date.now());
|
|
261
|
+
log({ kind: 'session-index-error', session: sid, trigger, message: (msg + hint).slice(0, 300) });
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// 触发面:回合结束(durable)+ 空闲追平 + 定时兜底 + 压缩信号(compaction/end 真值;2026-10-07 复查)。
|
|
266
|
+
ctx.on('session/event', (session, event) => {
|
|
267
|
+
try {
|
|
268
|
+
if (event?.type === 'turn/end') indexNow(String(session?.id ?? ''), 'turn-end', { turn: event?.data?.turn ?? null });
|
|
269
|
+
else if (event?.type === 'compaction/end' && event?.data?.error === void 0) {
|
|
270
|
+
const sid = String(session?.id ?? '').replace(/^session-/, '');
|
|
271
|
+
compacts.push({ session: sid, ts: Date.now() });
|
|
272
|
+
if (compacts.length > 20) compacts.splice(0, compacts.length - 20);
|
|
273
|
+
log({ kind: 'session-compact', session: sid });
|
|
274
|
+
}
|
|
275
|
+
} catch { /* 静默 */ }
|
|
276
|
+
});
|
|
277
|
+
ctx.on('agent/status', (payload) => {
|
|
278
|
+
try { if (payload?.status === 'idle') indexNow(String(payload?.agent?.id ?? ''), 'idle'); } catch { /* 静默 */ }
|
|
279
|
+
});
|
|
280
|
+
if (watchEnabled) {
|
|
281
|
+
ctx.on('agent/assistant-stream', (payload) => {
|
|
282
|
+
try { onStreamFrame(payload?.agent, payload?.frame); } catch { /* 观测不拖垮宿主 */ }
|
|
283
|
+
});
|
|
284
|
+
ctx.on('session/event', (session, event) => {
|
|
285
|
+
try {
|
|
286
|
+
if (event?.type === 'assistant/chunk') onChunk(session, event);
|
|
287
|
+
else if (event?.type === 'assistant/message') rememberAssistant(session, event);
|
|
288
|
+
else if (event?.type === 'turn/end') onTurnWatch(String(session?.id ?? ''), event?.data?.turn ?? null);
|
|
289
|
+
} catch { /* 观测不拖垮宿主 */ }
|
|
290
|
+
});
|
|
291
|
+
ctx.on('agent/turn-stopping', (payload) => {
|
|
292
|
+
try { onTurnWatch(String(payload?.agent?.session?.id ?? payload?.agent?.id ?? ''), payload?.turn ?? null); } catch { /* 静默 */ }
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (typeof ctx.interval === 'function') {
|
|
297
|
+
try {
|
|
298
|
+
const stop = ctx.interval(() => { try { for (const sid of [...lastIndexedAt.keys()]) indexNow(sid, 'timer'); } catch { /* 静默 */ } }, cfg.timerCheckMs);
|
|
299
|
+
ctx.effect(() => () => { try { stop(); } catch { /* 静默 */ } });
|
|
300
|
+
} catch { /* 静默 */ }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 条子:每回合一行(无内容静默)。
|
|
304
|
+
ctx.systemPrompt.context({
|
|
305
|
+
name: 'akasha:session',
|
|
306
|
+
order: cfg.contextOrder,
|
|
307
|
+
text: () => {
|
|
308
|
+
try { return renderSessionLine({ meta: readMeta(), compacts }, Date.now()); }
|
|
309
|
+
catch { return null; }
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
log({ kind: 'session-armed', pid: process.pid, minIndexIntervalMs: cfg.minIndexIntervalMs, timerCheckMs: cfg.timerCheckMs, contextOrder: cfg.contextOrder, loopWatch: watchEnabled, watchMinChars: cfg.watchMinChars, watchCheckEvery: cfg.watchCheckEvery });
|
|
314
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akasha-book/session",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "小阿卡夏:会话层插件 —— 自动增量索引(turn/end 去抖)+ 压缩事件行 + 每回合节奏条 + 循环观测 P0(dry 干跑,只观测不干预)",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/index.js",
|
|
12
|
+
"cordis.patch.yml",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"dsh": {
|
|
16
|
+
"bundle": {
|
|
17
|
+
"patch": "./cordis.patch.yml"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|