open-memex
Version:
Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.
60 lines (59 loc) • 3.2 kB
JavaScript
/**
* Turn hand-off between keyword capture and the agent (D73).
*
* The keyword hook and the agent are two writers for one user turn. The hook
* stores the user's sentence verbatim, with no agent in the loop; the agent
* then sees only the chat transcript and cannot tell the sentence is already
* stored, so it saves its own paraphrase — the same statement lands two or
* three times (observed 2026-10-04: one `remember …` sentence → one
* `source: keyword` file plus two `source: tool` files).
*
* Two mechanisms, in this order:
* (a) the hand-off note — this module's `formatHandoffBlock`, pushed into the
* turn's system context so the agent knows what is already stored;
* (b) the turn-echo guard — `TURN_ECHO_THRESHOLD` below, enforced in
* `addMemory` (src/tools/ops.ts) over what the hook just wrote.
*
* Pure module: no db, no fs, no config, so the threshold behavior is testable
* in scripts/smoke-pure.ts.
*/
/**
* Overlap bar for refusing a re-save of a just-captured user statement.
*
* Deliberately high, and only ever applied to memories the *hook* wrote
* (`source: keyword` / `user`) inside `TURN_ECHO_WINDOW_MS`. Measured on the
* reported case and its near neighbours (token Jaccard, see
* `similarity` in src/store/lifecycle.ts): a wholesale re-save of the captured
* sentence scores 0.29–0.36, while a genuinely distinct fact the same
* sentence also contained scores 0.21 and below. Lexical overlap cannot
* separate an agent's gloss from a new fact — those two measured 0.211 and
* 0.214 — so the guard sits at the high-confidence end and everything under
* the bar is left to (a) and the agent's judgment. A refusal is always
* reported with the stored id, so the caller can supersede or rephrase.
*/
export const TURN_ECHO_THRESHOLD = 0.28;
/** How long a hook capture counts as "this turn" (10 minutes). */
export const TURN_ECHO_WINDOW_MS = 10 * 60 * 1000;
export function isTurnEcho(score) {
return score >= TURN_ECHO_THRESHOLD;
}
/** Records still inside the window. */
export function freshCaptures(records, now) {
return records.filter((r) => now - r.at <= TURN_ECHO_WINDOW_MS);
}
function preview(content) {
const one = content.replace(/\s+/g, " ").trim();
return one.length > 160 ? `${one.slice(0, 160)}…` : one;
}
/** The hand-off note (a). Empty string when there is nothing to hand off. */
export function formatHandoffBlock(records) {
if (records.length === 0)
return "";
const lines = records.map((r) => `- id=${r.id} (${r.scopeKey}) — "${preview(r.content)}"`);
return [
"[open-memex] The user's own wording in this turn was ALREADY stored verbatim, before you saw it. It is not unsaved.",
...lines,
"Do not call memory_add to save that text again in any rewording — one statement is already one memory, and a rewording only adds a second copy that retrieval will return alongside the first.",
"Save a memory only for information those lines do NOT contain, as its own self-contained statement. If your version is meant to replace one of them, call memory_supersede with that id instead of memory_add.",
].join("\n");
}