feat(kbblock,fade): TS fade port + scaffold renderFaded/tick (not wired in v1)

Phase 4b openclaw side. Provides fade primitives matching Plexum-sdk-
go/fade so renderFaded / tick are API-parity available, but stops
short of activating fade in the live before_prompt_build hook since
openclaw plugin SDK ctx doesn't currently expose currentTurn to
plugins. Plexum side fully wires fade per turn.

  plugin/tools/fade.ts (new): TS port of the fade algorithm. Maskable
    rune set matches Plexum source. PRNG is Mulberry32 instead of Go's
    math/rand — documented in the file: cross-runtime mask patterns
    DIFFER, but each runtime fades its own session-local kb-block
    independently so no comparison is expected.

  plugin/tools/kbblock.ts: Entry gains seed (random u31 at add());
    render() unchanged behaviour, plus new renderFaded(currentTurn,
    params) + tick(currentTurn, params) for future wiring when
    openclaw exposes turn signal to plugin hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-06-06 00:26:16 +01:00
parent c9792d1290
commit 2553d102f3
2 changed files with 190 additions and 4 deletions

View File

@@ -9,12 +9,16 @@
// <OPENCLAW_PATH>/agents/<agentId>/sessions/<sessionId>/plugins/
// harbor-forge/kb-block.json
//
// Fade NOT applied in v1 (§9 #3 placeholder; defer until prod data).
// Cross-runtime aligned with Plexum kbblock.
// Fade algorithm + types are implemented (see fade.ts + renderFaded /
// tick methods), but the openclaw `before_prompt_build` hook ctx
// doesn't currently expose currentTurn to plugins. Plain render() (no
// fade) is used in the hook for v1; renderFaded scaffolding stays for
// when turn signal lands. Plexum side fully wires fade per turn.
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { fade, shouldDrop, type FadeParams } from './fade.js';
export interface Entry {
id: number; // HF backend DB primary key
@@ -24,6 +28,7 @@ export interface Entry {
insert_seq: number;
added_at_turn: number;
last_refresh_at_turn: number;
seed: number; // PRNG seed for fade — generated at add() time
}
interface BlockShape {
@@ -92,6 +97,7 @@ export class Block {
insert_seq: this.next_seq,
added_at_turn: at_turn,
last_refresh_at_turn: at_turn,
seed: Math.floor(Math.random() * 0x7FFFFFFF),
};
this.next_seq += 1;
this.entries.push(e);
@@ -135,9 +141,23 @@ export class Block {
/**
* Render the <kb-block> inner body — flat sequence of
* <kb-fact id=N kb=<code> source=topic:<slug>>content</kb-fact>
* ordered by insert_seq (§9 #4). Empty block → "".
* ordered by insert_seq (§9 #4). Empty block → "". No fade applied.
*/
render(): string {
return this.renderInner(null);
}
/**
* RenderFaded mirrors render() but applies fade() per entry given
* currentTurn + params. Available for callers that have turn signal
* (Plexum side does; openclaw `before_prompt_build` hook doesn't
* currently — uses plain render()).
*/
renderFaded(currentTurn: number, params: FadeParams): string {
return this.renderInner((e) => fade(e.content, e.seed, currentTurn - e.last_refresh_at_turn, params).rendered);
}
private renderInner(transform: ((e: Entry) => string) | null): string {
if (this.entries.length === 0) return '';
const ordered = [...this.entries].sort((a, b) => a.insert_seq - b.insert_seq);
const lines: string[] = [];
@@ -147,9 +167,31 @@ export class Block {
if (e.source_topic) head += ` source=topic:${e.source_topic}`;
head += '>';
lines.push(head);
lines.push(e.content);
lines.push(transform ? transform(e) : e.content);
lines.push('</kb-fact>');
});
return lines.join('\n') + '\n';
}
/**
* Tick applies fade tick — drops entries whose underscore ratio
* crossed the m% threshold. Returns IDs dropped. Caller must
* save() after.
*/
tick(currentTurn: number, params: FadeParams): number[] {
if (this.entries.length === 0) return [];
const dropped: number[] = [];
const kept: Entry[] = [];
for (const e of this.entries) {
const d = currentTurn - e.last_refresh_at_turn;
const r = fade(e.content, e.seed, d, params);
if (shouldDrop(r, params)) {
dropped.push(e.id);
} else {
kept.push(e);
}
}
this.entries = kept;
return dropped;
}
}