The previous commit shipped renderFaded + tick + seed on Block but left them dead code because openclaw's `before_prompt_build` hook ctx doesn't carry a currentTurn signal — the hook called plain render() and turnFor() returned 0, so add() always stamped last_refresh_at_turn=0 and fade distance never moved. This adds a small per-session turn tracker (tools/turn-tracker.ts) that the hook bumps each fire. On the first bump after plugin start the counter is seeded from max(entry.last_refresh_at_turn) on the loaded block, so a restart doesn't regress accumulated fade for surviving entries. dynamic-kb-cache now reads currentTurnForSession via the same counter so a fact cached on turn N reads back at d=0 on turn N+1. The hook now: bumps turn, ticks (drop entries past the m% threshold, log + save when anything drops), and renderFaded() into appendSystemContext. Same algorithm as the Plexum-side RenderDynamicSubblock path. KBDeps.turnFor signature changed from (agentId) → (sessionId) since turn is session-scoped — fixed the one cache-tool call site. Verified end-to-end with a standalone smoke replaying the full hook+tool flow against a temp profile: stale entry (d=201) drops on turn 1, fresh entry (d=1) survives untouched, restart re-seeds the counter from the surviving entry's last_refresh.
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
// turn-tracker.ts — per-session monotonic turn counter for KB fade.
|
|
//
|
|
// The openclaw `before_prompt_build` hook does not expose currentTurn
|
|
// to plugins (verified against the SDK hook-types: PluginHookAgentContext
|
|
// has agentId/sessionId/workspaceDir/... but no turn). To make fade
|
|
// effective, we maintain a small Map<sessionId, turn> that increments
|
|
// once per before_prompt_build fire. The `dynamic-kb-cache` tool reads
|
|
// the same counter so each new Entry's `last_refresh_at_turn` is
|
|
// consistent with the hook's view of the clock.
|
|
//
|
|
// Restart safety: in-memory counters reset to 0 on plugin reload, but
|
|
// `seedFromBlock` is called the first time we touch a given session
|
|
// during a hook; it scans the loaded block's entries and seeds the
|
|
// counter to max(last_refresh_at_turn). The next bump then yields a
|
|
// strictly-greater turn, so existing entries keep their accumulated
|
|
// fade distance instead of regressing to d=0.
|
|
//
|
|
// Process-local only. The Plexum side keeps the same semantics in Go
|
|
// (it has currentTurn from the host).
|
|
|
|
import type { Block } from './kbblock.js';
|
|
|
|
const counters = new Map<string, number>();
|
|
|
|
/**
|
|
* Return the latest turn assigned to this session, or 0 if untouched.
|
|
* `dynamic-kb-cache` uses this for `at_turn` on Block.add so a fact
|
|
* cached during turn N reads back at d=0 on turn N+1.
|
|
*/
|
|
export function currentTurnForSession(sessionId: string): number {
|
|
if (!sessionId) return 0;
|
|
return counters.get(sessionId) ?? 0;
|
|
}
|
|
|
|
/**
|
|
* Increment the counter for this session and return the new value.
|
|
* If the counter is unset (first hook after plugin start), seed it to
|
|
* max(entry.last_refresh_at_turn) so restart doesn't regress fade.
|
|
*
|
|
* Pass the loaded block so seeding doesn't need a second disk read.
|
|
*/
|
|
export function bumpTurnForSession(sessionId: string, block: Block): number {
|
|
if (!sessionId) return 0;
|
|
let cur = counters.get(sessionId);
|
|
if (cur === undefined) {
|
|
cur = 0;
|
|
for (const e of block.entries) {
|
|
if (e.last_refresh_at_turn > cur) cur = e.last_refresh_at_turn;
|
|
}
|
|
}
|
|
const next = cur + 1;
|
|
counters.set(sessionId, next);
|
|
return next;
|
|
}
|
|
|
|
/**
|
|
* Test hook. Forget all in-memory counters. Not exported through any
|
|
* stable contract — for unit tests only.
|
|
*/
|
|
export function _resetForTesting(): void {
|
|
counters.clear();
|
|
}
|