pi-think-anchor 1.0.2 → 1.0.3

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.ts +90 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,7 +28,7 @@
28
28
  ## 安装
29
29
 
30
30
  ```bash
31
- pi install npm:pi-think-anchor@1.0.0
31
+ pi install npm:pi-think-anchor
32
32
  pi install ./pi-think-anchor
33
33
  ```
34
34
 
package/index.ts CHANGED
@@ -181,6 +181,14 @@ interface TAState {
181
181
  lastL2: number;
182
182
  lastRatio: number;
183
183
  lastBand: TABand;
184
+ /** v3:效果闭环与自适应(借鉴 ptc-meter 的数据自校正 + patch-keeper 的条件触发) */
185
+ recent: number[];
186
+ pending: { turn: number; pre: number } | null;
187
+ effSamples: number;
188
+ effImproved: number;
189
+ autoMuted: boolean;
190
+ lastLoggedBand: TABand;
191
+ skipped: number;
184
192
  }
185
193
 
186
194
  function newState(cfg: TAConfig): TAState {
@@ -197,9 +205,22 @@ function newState(cfg: TAConfig): TAState {
197
205
  lastL2: 0,
198
206
  lastRatio: 0,
199
207
  lastBand: "mixed",
208
+ recent: [],
209
+ pending: null,
210
+ effSamples: 0,
211
+ effImproved: 0,
212
+ autoMuted: false,
213
+ lastLoggedBand: "mixed",
214
+ skipped: 0,
200
215
  };
201
216
  }
202
217
 
218
+ /** 采样写盘(v3):非事件轮每 5 轮留一行——实测 6151 行 / 1.0 MB 逼近 1 MiB 上限,降为 1/5 仍够统计 */
219
+ const SAMPLE_EVERY = 5;
220
+ /** 自适应(v3):有效样本 ≥ AUTO_MIN 且下降率 < AUTO_RATE(不及抛硬币)即转静默,只记录不注入 */
221
+ const AUTO_MIN = 8;
222
+ const AUTO_RATE = 0.5;
223
+
203
224
  /** 日志上限 1 MiB(与 ptc-meter 同口径):超限保留尾部,避免无界增长。 */
204
225
  const MAX_LOG = 1 << 20;
205
226
 
@@ -234,7 +255,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
234
255
  const fp = fingerprint(thinking, st.cfg.lexicon);
235
256
  if (fp.pos + fp.neg + fp.neu === 0) {
236
257
  st.turns++;
237
- appendLog({ ts: new Date().toISOString(), turn: st.turns, v: 2, len: thinking.length, pos: 0, neg: 0, neu: 0, ratio: null, hits: 0, negRate: null, band: "none", phase: st.phase, want: "none", acted: "none" });
258
+ if (st.turns % SAMPLE_EVERY === 0) appendLog({ ts: new Date().toISOString(), turn: st.turns, v: 3, len: thinking.length, pos: 0, neg: 0, neu: 0, ratio: null, hits: 0, negRate: null, band: "none", phase: st.phase, want: "none", acted: "none", eff: null });
238
259
  return;
239
260
  }
240
261
  st.window.push(fp);
@@ -256,7 +277,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
256
277
  const now = Date.now();
257
278
  if (d.level === "none") {
258
279
  st.phase = d.phase;
259
- } else if (d.level === "L1" && now - st.lastL1 > st.cfg.intervention.l1CooldownMin * 60_000) {
280
+ } else if (d.level === "L1" && !st.autoMuted && now - st.lastL1 > st.cfg.intervention.l1CooldownMin * 60_000) {
260
281
  st.lastL1 = now;
261
282
  st.l1Count++;
262
283
  (pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
@@ -264,8 +285,9 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
264
285
  { deliverAs: "nextTurn" },
265
286
  );
266
287
  acted = "L1";
288
+ st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)) };
267
289
  st.phase = d.phase;
268
- } else if (d.level === "L2" && now - st.lastL2 > st.cfg.intervention.l2CooldownMin * 60_000) {
290
+ } else if (d.level === "L2" && !st.autoMuted && now - st.lastL2 > st.cfg.intervention.l2CooldownMin * 60_000) {
269
291
  st.lastL2 = now;
270
292
  st.l2Tried++;
271
293
  st.l2Count++;
@@ -274,36 +296,58 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
274
296
  { deliverAs: "nextTurn" },
275
297
  );
276
298
  acted = "L2";
299
+ st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)) };
277
300
  st.phase = d.phase;
278
301
  } else {
279
302
  // 被冷却/上限挡下:记 blocked,避免与「无需动作」混淆(实测 583 次 want=L2 只有 55 次真发)
280
303
  st.phase = prevPhase;
281
- if (d.level !== "none") acted = "blocked-" + d.level;
304
+ if (d.level !== "none") acted = st.autoMuted ? "muted-" + d.level : "blocked-" + d.level;
282
305
  }
283
306
  if (band === "spec" && st.phase === "healthy") st.l2Tried = 0; // 恢复后重置 L2 计数
284
- appendLog({
285
- ts: new Date().toISOString(),
286
- turn: st.turns,
287
- v: 2,
288
- len: thinking.length,
289
- pos: fp.pos,
290
- neg: fp.neg,
291
- neu: fp.neu,
292
- hits,
293
- negRate: Math.round(negRate * 100) / 100,
294
- ratio: Math.round(ratio * 100) / 100,
295
- band,
296
- phase: st.phase,
297
- want: d.level,
298
- acted,
299
- });
307
+ // v3 效果闭环:干预后第 3 轮结算(pre = 干预前最近 3 轮 ratio 均值)
308
+ st.recent.push(ratio);
309
+ if (st.recent.length > 6) st.recent.shift();
310
+ let eff: number | null = null;
311
+ if (st.pending && st.turns - st.pending.turn >= 3) {
312
+ const tail = st.recent.slice(-3);
313
+ const post = tail.reduce((a, b) => a + b, 0) / Math.max(1, tail.length);
314
+ eff = Math.round((post - st.pending.pre) * 100) / 100;
315
+ st.effSamples++;
316
+ if (eff < 0) st.effImproved++;
317
+ st.pending = null;
318
+ // 自适应:样本够且实测下降率不及抛硬币 → 转静默(只记录,不注入)
319
+ st.autoMuted = st.effSamples >= AUTO_MIN && st.effImproved / st.effSamples < AUTO_RATE;
320
+ }
321
+ // 采样写盘:事件轮(有干预/有结算/波段变化)必写,其余每 SAMPLE_EVERY 轮一行
322
+ const eventful = acted !== "none" || eff !== null || band !== st.lastLoggedBand;
323
+ if (!eventful && st.turns % SAMPLE_EVERY !== 0) { st.skipped++; }
324
+ else {
325
+ st.lastLoggedBand = band;
326
+ appendLog({
327
+ ts: new Date().toISOString(),
328
+ turn: st.turns,
329
+ v: 3,
330
+ len: thinking.length,
331
+ pos: fp.pos,
332
+ neg: fp.neg,
333
+ neu: fp.neu,
334
+ hits,
335
+ negRate: Math.round(negRate * 100) / 100,
336
+ ratio: Math.round(ratio * 100) / 100,
337
+ band,
338
+ phase: st.phase,
339
+ want: d.level,
340
+ acted,
341
+ eff,
342
+ });
343
+ }
300
344
  } catch {
301
345
  /* 绝不抛错 */
302
346
  }
303
347
  });
304
348
 
305
349
  pi.registerCommand("think-anchor", {
306
- description: "思维锚定监测:状态 / on / off / reset",
350
+ description: "思维锚定监测:状态 / stats(日志统计)/ on / off / reset",
307
351
  handler: async (args, ctx) => {
308
352
  const sub = (args || "").trim().toLowerCase();
309
353
  if (sub === "on") {
@@ -321,6 +365,28 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
321
365
  if (ctx.hasUI) ctx.ui.notify("think-anchor: 会话状态已重置。", "info");
322
366
  return;
323
367
  }
368
+ if (sub === "stats") {
369
+ try {
370
+ const txt = readFileSync(LOG, "utf8");
371
+ const rows = txt.split("\n").filter((l) => l.trim()).map((l) => { try { return JSON.parse(l) as Record<string, unknown>; } catch { return null; } }).filter(Boolean) as Array<Record<string, unknown>>;
372
+ const group = (k: string) => {
373
+ const m = new Map<string, number>();
374
+ for (const r of rows) { const key = String(r[k] ?? "?"); m.set(key, (m.get(key) ?? 0) + 1); }
375
+ return [...m.entries()].map(([a, b]) => a + "=" + b).join(" ") || "-";
376
+ };
377
+ const effRows = rows.filter((r) => typeof r.eff === "number");
378
+ const improved = effRows.filter((r) => (r.eff as number) < 0).length;
379
+ const size = (statSync(LOG).size / 1048576).toFixed(2);
380
+ const lines = [
381
+ "think-anchor 统计:" + size + " MB / 上限 1.00 · " + rows.length + " 行",
382
+ " 版本 " + group("v") + " · 波段 " + group("band"),
383
+ " 动作 " + group("acted") + " · want " + group("want"),
384
+ " 效果结算 " + improved + "/" + effRows.length + (effRows.length ? "(下降 " + Math.round((100 * improved) / effRows.length) + "%)" : ""),
385
+ ];
386
+ if (ctx.hasUI) ctx.ui.notify(lines.join("\n"), "info");
387
+ } catch { if (ctx.hasUI) ctx.ui.notify("think-anchor: 统计失败(日志读取异常)", "warning"); }
388
+ return;
389
+ }
324
390
  const line =
325
391
  "think-anchor:" +
326
392
  (st.cfg.enabled ? "ON" : "OFF") +
@@ -328,7 +394,9 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
328
394
  " · 消息 " + st.turns +
329
395
  " · 波段 spec/mixed/react=" + st.bandHist.spec + "/" + st.bandHist.mixed + "/" + st.bandHist.react +
330
396
  " · 干预 L1×" + st.l1Count + " L2×" + st.l2Count +
331
- " · 上轮 命中" + lastHits + " 发散占比" + (Math.round(lastNegRate * 100) / 100) + "(" + st.lastBand + ")";
397
+ " · 有效率 " + st.effImproved + "/" + st.effSamples + (st.autoMuted ? "(不及抛硬币→已静默)" : "") +
398
+ " · 上轮 命中" + lastHits + " 发散占比" + (Math.round(lastNegRate * 100) / 100) + "(" + st.lastBand + ")" +
399
+ " · 采样跳过 " + st.skipped;
332
400
  if (ctx.hasUI) ctx.ui.notify(line, "info");
333
401
  },
334
402
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-think-anchor",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Monitors thinking focus drift and nudges the model back on track.",
5
5
  "type": "module",
6
6
  "license": "MIT",