dsh-whale-loli 0.5.7 → 0.5.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-loli",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "鲸鱼娘 Agent 预设(三个)— 极简模式工具面 + 鲸鱼娘·小鱼人设 + 跨会话长期记忆(提示变量通道,per-preset 独立文件)+ 最近 20 轮历史窗口:调宿主 ctx.compaction.compactRegion() 把更早的内容压成一条摘要,让它们不再计入输入;自己不写任何会话事件(宿主摘要+写入+校验)。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,14 +24,23 @@
24
24
  * · 裁完轮数会掉到 N 上下,必须等到再次超过 N 才会触发 —— 天然限流,不会每轮都调。
25
25
  * · 任何失败都被吞掉并跳过本次,**绝不影响正常对话**。
26
26
  */
27
+ import { appendFileSync } from 'node:fs';
27
28
  import { homedir } from 'node:os';
28
29
  import { join } from 'node:path';
29
30
 
30
31
  /** Cordis 插件名。 */
31
32
  export const name = 'whale-history-window';
32
33
 
33
- /** 需要会话表(用来拿 agent.session)。 */
34
- export const inject = ['sessions'];
34
+ /**
35
+ * **不要求任何注入**。
36
+ *
37
+ * 曾经写 `inject = ['sessions']`,结果整个插件在预设作用域里可能一直不激活
38
+ * (`apply` 压根不跑,表现就是「记忆注入正常、但裁剪完全没反应」)。
39
+ * 而本插件其实什么都不需要:
40
+ * · agent / session 从 `agent/status` 的 payload 里拿
41
+ * · compaction 服务用 `ctx.get('compaction')` 惰性取,取不到就跳过
42
+ */
43
+ export const inject = [];
35
44
 
36
45
  /** 默认保留轮数。 */
37
46
  export const DEFAULT_KEEP_TURNS = 20;
@@ -139,6 +148,33 @@ export async function compactHistory(ctx, agent, keepTurns = DEFAULT_KEEP_TURNS,
139
148
  /** 每个 agent 同时只允许一次压缩(压缩服务本身也拒绝并发,这里提前避开)。 */
140
149
  const inFlight = new WeakSet();
141
150
 
151
+ /**
152
+ * 诊断日志:把每次 idle 的判断过程写进 `<DSH_HOME>/whale-history-window.log`。
153
+ *
154
+ * 为什么要写文件:宿主的 `ctx.logger` 只到控制台,用户看不到、我也读不到。
155
+ * 而这个插件是**静默跳过**式设计(任何异常都吞掉),没有日志就无法区分
156
+ * 「没收到 idle」「没拿到 compaction 服务」「范围不合格」「compaction 抛错」。
157
+ */
158
+ function diagPath() {
159
+ return join(dshHome(), 'whale-history-window.log');
160
+ }
161
+
162
+ function diag(line) {
163
+ try {
164
+ appendFileSync(diagPath(), `${new Date().toISOString()} ${line}\n`, 'utf8');
165
+ } catch {
166
+ // 日志失败绝不能影响任何东西
167
+ }
168
+ }
169
+
170
+ /** 该会话当前有多少「真实用户轮」(诊断用)。 */
171
+ function countTurns(session) {
172
+ const nodes = session?.surface?.nodes ?? [];
173
+ let n = 0;
174
+ for (const seq of nodes) if (isUserTurn(session.eventAt(seq))) n++;
175
+ return n;
176
+ }
177
+
142
178
  /**
143
179
  * 挂载:agent 转为空闲时检查轮数,超过 keepTurns 就压缩掉更早的部分。
144
180
  *
@@ -147,23 +183,66 @@ const inFlight = new WeakSet();
147
183
  * - `keepTurns` 保留最近多少轮(默认 20)
148
184
  */
149
185
  export function apply(ctx, config = {}) {
150
- if (config.enable === false) return;
186
+ if (config.enable === false) {
187
+ diag('apply: enable=false,不挂载');
188
+ return;
189
+ }
151
190
 
152
191
  const keepTurns = typeof config.keepTurns === 'number' ? config.keepTurns : DEFAULT_KEEP_TURNS;
153
- if (typeof ctx.on !== 'function') return;
192
+ if (typeof ctx.on !== 'function') {
193
+ diag('apply: ctx.on 不可用,不挂载');
194
+ return;
195
+ }
196
+
197
+ // 两个通道都试:Cordis 的服务既可以 ctx.get(name),也通常是 ctx[name]
198
+ const serviceVia = () => {
199
+ if (typeof ctx.get === 'function') {
200
+ const s = ctx.get('compaction');
201
+ if (s !== undefined) return { service: s, via: 'ctx.get' };
202
+ }
203
+ const s2 = ctx.compaction;
204
+ if (s2 !== undefined) return { service: s2, via: 'ctx.compaction' };
205
+ return { service: undefined, via: 'none' };
206
+ };
207
+
208
+ const start = serviceVia();
209
+ diag(`apply: 已挂载 keepTurns=${keepTurns} 服务=${start.via}`);
154
210
 
155
211
  ctx.effect(
156
212
  () =>
157
213
  ctx.on('agent/status', (payload) => {
158
- if (payload?.status !== 'idle') return;
214
+ const status = payload?.status;
159
215
  const agent = payload?.agent;
160
- if (agent?.session === undefined) return;
161
- if (inFlight.has(agent)) return;
216
+ if (status !== 'idle') {
217
+ diag(`agent/status=${status}(非 idle,跳过)`);
218
+ return;
219
+ }
220
+ if (agent?.session === undefined) {
221
+ diag('idle 但 payload.agent.session 缺失');
222
+ return;
223
+ }
224
+ if (inFlight.has(agent)) {
225
+ diag('idle 但上一次压缩还在跑,跳过');
226
+ return;
227
+ }
228
+ const turns = countTurns(agent.session);
229
+ const { service, via } = serviceVia();
230
+ if (service?.compactRegion === undefined) {
231
+ diag(`idle turns=${turns} 但拿不到 compaction 服务(via=${via})`);
232
+ return;
233
+ }
234
+ const plan = planCompact(agent.session, keepTurns);
235
+ if (plan === null) {
236
+ diag(`idle turns=${turns} 未超过 ${keepTurns},无需压缩`);
237
+ return;
238
+ }
162
239
  inFlight.add(agent);
240
+ diag(`idle turns=${turns} 触发压缩 [${plan.startSeq}, ${plan.endSeq}] 压掉 ${plan.removedTurns} 轮`);
163
241
  // 不 await:这是空闲期的后台整理,绝不能拖住/影响对话。
164
242
  void Promise.resolve()
165
- .then(() => compactHistory(ctx, agent, keepTurns))
166
- .catch(() => {})
243
+ .then(() => service.compactRegion(plan.startSeq, plan.endSeq, agent))
244
+ .then(() => diag('压缩成功'))
245
+ .catch((err) => diag(`压缩失败: ${String(err?.message ?? err)}`))
167
246
  .finally(() => inFlight.delete(agent));
168
247
  }),
169
248
  'whale-history-window.idle-compact',