feat(kb): wire kb-block fade into before_prompt_build

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.
This commit is contained in:
h z
2026-06-06 01:06:30 +01:00
parent db323a2118
commit 70b0ccb7d2
4 changed files with 96 additions and 14 deletions

View File

@@ -21,6 +21,8 @@ import type { OpenClawAgentInfo } from './core/openclaw-agents.js';
import { registerGatewayStartHook } from './hooks/gateway-start.js';
import { registerGatewayStopHook } from './hooks/gateway-stop.js';
import { Block as KBBlock } from './tools/kbblock.js';
import { defaultFadeParams } from './tools/fade.js';
import { bumpTurnForSession, currentTurnForSession } from './tools/turn-tracker.js';
import { KBClient } from './tools/kbclient.js';
import {
type KBDeps,
@@ -978,7 +980,7 @@ function register(api: PluginAPI): void {
}
},
makeClient: (token: string) => new KBClient(kbBackendUrl, token),
turnFor: () => 0,
turnFor: (sessionId: string) => currentTurnForSession(sessionId),
};
// Wrap each KB tool factory: pass ctx into execute so cache/evict can
@@ -1003,7 +1005,12 @@ function register(api: PluginAPI): void {
// <kb-block> subblock injection via before_prompt_build hook
// (DESIGN-DYNAMIC-BLOCK.md §2 + §7: openclaw side uses
// appendSystemContext since the hook can't replace baked-in
// <available_skills> precisely). Empty block → no append.
// <available_skills> precisely).
//
// Per-turn fade: bump the session turn counter, then tick
// (dropping entries past the m% threshold) + renderFaded so the
// rendered text shows accumulated underscore masking. The Plexum
// mirror runs the same algorithm inside RenderDynamicSubblock.
const apiOn = (api as any).on;
if (typeof apiOn === 'function') {
apiOn.call(
@@ -1016,7 +1023,16 @@ function register(api: PluginAPI): void {
let body = '';
try {
const block = KBBlock.open(agentId, sessionId);
body = block.render();
const turn = bumpTurnForSession(sessionId, block);
const fadeParams = defaultFadeParams();
const dropped = block.tick(turn, fadeParams);
if (dropped.length > 0) {
logger.info(
`kb-block fade tick agent=${agentId} session=${sessionId} dropped=[${dropped.join(',')}]`,
);
block.save();
}
body = block.renderFaded(turn, fadeParams);
} catch (err) {
logger.warn(`kb-block render failed: ${err}`);
return undefined;