agent-recall-core 3.4.25 → 3.4.30
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.
- package/dist/helpers/journal-files.d.ts +1 -1
- package/dist/helpers/journal-files.d.ts.map +1 -1
- package/dist/helpers/journal-files.js +4 -3
- package/dist/helpers/journal-files.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/palace/awareness.d.ts.map +1 -1
- package/dist/palace/awareness.js +6 -2
- package/dist/palace/awareness.js.map +1 -1
- package/dist/palace/compress.d.ts.map +1 -1
- package/dist/palace/compress.js +30 -5
- package/dist/palace/compress.js.map +1 -1
- package/dist/palace/rooms.d.ts.map +1 -1
- package/dist/palace/rooms.js +13 -1
- package/dist/palace/rooms.js.map +1 -1
- package/dist/relevance/injector.d.ts +51 -0
- package/dist/relevance/injector.d.ts.map +1 -0
- package/dist/relevance/injector.js +121 -0
- package/dist/relevance/injector.js.map +1 -0
- package/dist/relevance/precision.d.ts +109 -0
- package/dist/relevance/precision.d.ts.map +1 -0
- package/dist/relevance/precision.js +279 -0
- package/dist/relevance/precision.js.map +1 -0
- package/dist/relevance/promoter.d.ts +60 -0
- package/dist/relevance/promoter.d.ts.map +1 -0
- package/dist/relevance/promoter.js +336 -0
- package/dist/relevance/promoter.js.map +1 -0
- package/dist/relevance/stager.d.ts +61 -0
- package/dist/relevance/stager.d.ts.map +1 -0
- package/dist/relevance/stager.js +251 -0
- package/dist/relevance/stager.js.map +1 -0
- package/dist/relevance/surfacer.d.ts +34 -0
- package/dist/relevance/surfacer.d.ts.map +1 -0
- package/dist/relevance/surfacer.js +333 -0
- package/dist/relevance/surfacer.js.map +1 -0
- package/dist/storage/paths.d.ts +6 -1
- package/dist/storage/paths.d.ts.map +1 -1
- package/dist/storage/paths.js +13 -7
- package/dist/storage/paths.js.map +1 -1
- package/dist/storage/project.d.ts +17 -0
- package/dist/storage/project.d.ts.map +1 -1
- package/dist/storage/project.js +73 -1
- package/dist/storage/project.js.map +1 -1
- package/dist/tools-logic/context-synthesize.d.ts.map +1 -1
- package/dist/tools-logic/context-synthesize.js +2 -1
- package/dist/tools-logic/context-synthesize.js.map +1 -1
- package/dist/tools-logic/dashboard-export.d.ts.map +1 -1
- package/dist/tools-logic/dashboard-export.js +13 -1
- package/dist/tools-logic/dashboard-export.js.map +1 -1
- package/dist/tools-logic/journal-capture.d.ts.map +1 -1
- package/dist/tools-logic/journal-capture.js +6 -1
- package/dist/tools-logic/journal-capture.js.map +1 -1
- package/dist/tools-logic/journal-read.js +1 -1
- package/dist/tools-logic/journal-read.js.map +1 -1
- package/dist/tools-logic/journal-search.d.ts.map +1 -1
- package/dist/tools-logic/journal-search.js +2 -1
- package/dist/tools-logic/journal-search.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injector — formats Surfacer Candidates as an "added context" block the host
|
|
3
|
+
* hook can echo into the agent's context (v3.5.0 P-B, spec §4 "Judge" + §5).
|
|
4
|
+
*
|
|
5
|
+
* Contract:
|
|
6
|
+
* • Pure function. Zero fs, zero network — Surfacer already did the I/O.
|
|
7
|
+
* • Empty candidates → returns "" (silence is precision; the hook MUST emit
|
|
8
|
+
* nothing in this case so the agent's context stays clean — see spec §6).
|
|
9
|
+
* • One bullet per candidate, capped at ~140 chars total per line so the
|
|
10
|
+
* injected block never blows past a few hundred tokens even at K=3.
|
|
11
|
+
* • Title-render uses [source] (palace shows palace/<room>) and includes
|
|
12
|
+
* the Surfacer's `why` clause so the model knows WHY this surfaced and
|
|
13
|
+
* can decide whether to act on it.
|
|
14
|
+
*
|
|
15
|
+
* Output shape (verbatim — host hook copy-pastes this into stdout):
|
|
16
|
+
*
|
|
17
|
+
* <agent-recall-context>
|
|
18
|
+
* Possibly relevant prior memory (use if it helps, ignore otherwise):
|
|
19
|
+
* • [palace/goals] Rate limiting in Stripe — must use idempotency keys (why: matches palace/goals)
|
|
20
|
+
* • [journal] 2026-06-12 — Stripe webhook signature verification gotcha (why: matched recent journal 2026-06-12)
|
|
21
|
+
* </agent-recall-context>
|
|
22
|
+
*
|
|
23
|
+
* Worker done-definition self-check:
|
|
24
|
+
* (1) Error path: candidates is null/undefined → coerce to [] before any
|
|
25
|
+
* work. Each candidate field is `?? ""` so undefined never throws.
|
|
26
|
+
* (2) No global binaries — pure node, no shell-outs, no deps.
|
|
27
|
+
* (3) Threshold/ternary ordering — only branch is `length === 0 ? '' : block`,
|
|
28
|
+
* short-circuit-on-empty first.
|
|
29
|
+
* (4) No time-of-day logic; nothing TZ-sensitive.
|
|
30
|
+
*/
|
|
31
|
+
/** Hard cap per bullet line. ~140 chars keeps the K=3 block under ~500 chars. */
|
|
32
|
+
const LINE_MAX_CHARS = 140;
|
|
33
|
+
/** Width budget left over for the title+why after the "• [tag] " prefix. */
|
|
34
|
+
const TITLE_MAX_CHARS = 80;
|
|
35
|
+
const WHY_MAX_CHARS = 60;
|
|
36
|
+
/**
|
|
37
|
+
* Build the bracket-tag prefix shown at the head of each bullet.
|
|
38
|
+
* palace candidates carry their room (`palace/goals`); others fall back to
|
|
39
|
+
* just the source kind.
|
|
40
|
+
*/
|
|
41
|
+
function tagOf(c) {
|
|
42
|
+
if (c.source === "palace" && c.room)
|
|
43
|
+
return `palace/${c.room}`;
|
|
44
|
+
return c.source;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Truncate without splitting mid-codepoint. We only handle BMP here; the
|
|
48
|
+
* Surfacer's snippets are already char-capped, so this is the second-line
|
|
49
|
+
* defense (Injector's caller could in theory pass a fresh Candidate with a
|
|
50
|
+
* longer title — be tolerant).
|
|
51
|
+
*/
|
|
52
|
+
function truncate(s, n) {
|
|
53
|
+
if (!s)
|
|
54
|
+
return "";
|
|
55
|
+
if (s.length <= n)
|
|
56
|
+
return s;
|
|
57
|
+
return s.slice(0, n);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Render one Candidate as a single bullet line, capped to LINE_MAX_CHARS.
|
|
61
|
+
*
|
|
62
|
+
* Format: `• [<tag>] <title> (why: <why>)`
|
|
63
|
+
*
|
|
64
|
+
* If the assembled line exceeds LINE_MAX_CHARS we re-trim the title (the
|
|
65
|
+
* most variable field) until the whole line fits. The `[tag]` prefix and
|
|
66
|
+
* the `why:` clause are short and information-dense, so they survive
|
|
67
|
+
* intact at the expense of the title's tail.
|
|
68
|
+
*/
|
|
69
|
+
export function formatCandidate(c) {
|
|
70
|
+
const tag = tagOf(c);
|
|
71
|
+
const why = truncate((c.why ?? "").trim(), WHY_MAX_CHARS);
|
|
72
|
+
let title = truncate((c.title ?? "").trim(), TITLE_MAX_CHARS);
|
|
73
|
+
// Static parts of the line. `• [tag] ` + ` (why: <why>)` is fixed once
|
|
74
|
+
// tag/why are known; only the title is elastic.
|
|
75
|
+
const prefix = `• [${tag}] `;
|
|
76
|
+
const suffix = why ? ` (why: ${why})` : "";
|
|
77
|
+
const budget = LINE_MAX_CHARS - prefix.length - suffix.length;
|
|
78
|
+
if (budget <= 0) {
|
|
79
|
+
// Pathological case: tag + why already > LINE_MAX_CHARS. Drop the why
|
|
80
|
+
// clause to recover space; the bracket-tag is the more important signal.
|
|
81
|
+
return truncate(`${prefix}${title}`, LINE_MAX_CHARS);
|
|
82
|
+
}
|
|
83
|
+
if (title.length > budget) {
|
|
84
|
+
title = title.slice(0, Math.max(0, budget - 1)) + "…";
|
|
85
|
+
}
|
|
86
|
+
const line = `${prefix}${title}${suffix}`;
|
|
87
|
+
// Final guard — the budget math above should make this a no-op, but a
|
|
88
|
+
// bug in the truncate path must NEVER produce a line that breaks the
|
|
89
|
+
// host's stdout framing. Cap unconditionally.
|
|
90
|
+
return truncate(line, LINE_MAX_CHARS);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Format a list of Candidates for the host hook to inject.
|
|
94
|
+
*
|
|
95
|
+
* Empty input → empty string. This is load-bearing — the hook in the host
|
|
96
|
+
* is wired to echo our stdout verbatim, so an empty string keeps the
|
|
97
|
+
* agent's context clean (silence is precision, spec §6).
|
|
98
|
+
*/
|
|
99
|
+
export function formatForHook(candidates) {
|
|
100
|
+
// Error path #1: null/undefined input — coerce, do not throw.
|
|
101
|
+
const list = Array.isArray(candidates) ? candidates : [];
|
|
102
|
+
if (list.length === 0)
|
|
103
|
+
return "";
|
|
104
|
+
const lines = [];
|
|
105
|
+
for (const c of list) {
|
|
106
|
+
// Skip malformed candidates rather than throwing — Surfacer's contract is
|
|
107
|
+
// strict but defensive code here costs nothing.
|
|
108
|
+
if (!c || typeof c !== "object")
|
|
109
|
+
continue;
|
|
110
|
+
lines.push(formatCandidate(c));
|
|
111
|
+
}
|
|
112
|
+
if (lines.length === 0)
|
|
113
|
+
return "";
|
|
114
|
+
return [
|
|
115
|
+
"<agent-recall-context>",
|
|
116
|
+
"Possibly relevant prior memory (use if it helps, ignore otherwise):",
|
|
117
|
+
...lines,
|
|
118
|
+
"</agent-recall-context>",
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=injector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injector.js","sourceRoot":"","sources":["../../src/relevance/injector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precision meter — `injection_precision = used / injected`
|
|
3
|
+
* (v3.5.0 P-C, Ambient Relevance Loop, spec §5 + §6 "Precision guardrail").
|
|
4
|
+
*
|
|
5
|
+
* Purpose: this is the north-star metric that proves "intelligence, not
|
|
6
|
+
* storage." Of the memories AgentRecall surfaces (`injected`), how many
|
|
7
|
+
* does the agent actually act on (`used`)? A falling number is the
|
|
8
|
+
* early-warning that the Surfacer threshold is mis-tuned (noise > signal).
|
|
9
|
+
*
|
|
10
|
+
* Storage shape — append-only JSONL, one file per kind, per project:
|
|
11
|
+
*
|
|
12
|
+
* ~/.agent-recall/projects/<slug>/_precision/injections.jsonl
|
|
13
|
+
* ~/.agent-recall/projects/<slug>/_precision/usages.jsonl
|
|
14
|
+
*
|
|
15
|
+
* Design contract:
|
|
16
|
+
* • `recordInjection`/`recordUsage` are fire-and-forget — they NEVER
|
|
17
|
+
* throw. Losing a measurement line is acceptable; killing a user turn
|
|
18
|
+
* is not. Errors are swallowed silently (mirrors stager.ts).
|
|
19
|
+
* • `getPrecisionKPI` is tolerant of malformed lines (skipped, not
|
|
20
|
+
* fatal), missing files (treated as empty), and missing fields.
|
|
21
|
+
* • `precision` is **null** (not NaN) when `injected === 0` — the
|
|
22
|
+
* dashboard can render "—" instead of "NaN%".
|
|
23
|
+
* • `precision` is clamped to `[0, 1]` via `Math.min(1, used / injected)`
|
|
24
|
+
* (matches the existing pattern in storage/corrections.ts:373 — under
|
|
25
|
+
* data corruption / duplicate-usage events, the raw ratio can exceed
|
|
26
|
+
* 1.0, which is nonsense as a "precision" metric).
|
|
27
|
+
* • `per_source` groups by the prefix of `candidate_id` before ':'
|
|
28
|
+
* (e.g. `palace:room:slug` → bucket `palace`). Unprefixed ids land in
|
|
29
|
+
* the `unknown` bucket so they're still visible.
|
|
30
|
+
*
|
|
31
|
+
* Worker done-definition self-check:
|
|
32
|
+
* (1) error paths: `readJsonl` returns [] on read failure, JSON.parse
|
|
33
|
+
* wrapped in try/catch per line; recordInjection/Usage's outer
|
|
34
|
+
* try/catch swallows fs errors.
|
|
35
|
+
* (2) no global binaries — pure node:fs/path/crypto, no shell-outs.
|
|
36
|
+
* (3) ternary ordering — only branch is `injected > 0 ? clamp : null`,
|
|
37
|
+
* null is the explicit zero-denominator case.
|
|
38
|
+
* (4) time logic — `cutoff` is computed in ms-since-epoch from `Date.now()`
|
|
39
|
+
* which is TZ-agnostic; `ts` strings are ISO-8601 (also TZ-agnostic).
|
|
40
|
+
* No local-date filename is used here so no TZ bug surface.
|
|
41
|
+
*/
|
|
42
|
+
export interface InjectionEvent {
|
|
43
|
+
/** ISO-8601 timestamp of when the candidates were surfaced. */
|
|
44
|
+
ts: string;
|
|
45
|
+
/** Candidate ids that the Surfacer pushed into the agent's context this turn. */
|
|
46
|
+
candidate_ids: string[];
|
|
47
|
+
/** sha256(user_message + assistant_response) — links to UsageEvent.turn_hash. */
|
|
48
|
+
turn_hash: string;
|
|
49
|
+
}
|
|
50
|
+
export interface UsageEvent {
|
|
51
|
+
/** ISO-8601 timestamp of when the usage was detected. */
|
|
52
|
+
ts: string;
|
|
53
|
+
/** The id of the candidate that the agent acted on. */
|
|
54
|
+
candidate_id: string;
|
|
55
|
+
/** Turn this candidate was used in — joins back to InjectionEvent.turn_hash. */
|
|
56
|
+
turn_hash: string;
|
|
57
|
+
/** Brief reason (e.g. "title appeared in assistant output"). */
|
|
58
|
+
evidence: string;
|
|
59
|
+
}
|
|
60
|
+
export interface PrecisionKPI {
|
|
61
|
+
/** Window the KPI was computed over (days). */
|
|
62
|
+
window_days: number;
|
|
63
|
+
/** Total candidates surfaced (sum of candidate_ids.length across injections). */
|
|
64
|
+
injected: number;
|
|
65
|
+
/** Total candidates the agent used (one row per used candidate). */
|
|
66
|
+
used: number;
|
|
67
|
+
/**
|
|
68
|
+
* `used / injected`, clamped to `[0, 1]`. **null** when `injected === 0`
|
|
69
|
+
* (NaN MUST NOT appear — the dashboard treats null as "no data yet").
|
|
70
|
+
*/
|
|
71
|
+
precision: number | null;
|
|
72
|
+
/** Breakdown by candidate-id source prefix (palace / journal / …). */
|
|
73
|
+
per_source: Record<string, {
|
|
74
|
+
injected: number;
|
|
75
|
+
used: number;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Append one injection event to `_precision/injections.jsonl`.
|
|
80
|
+
*
|
|
81
|
+
* NEVER THROWS. Mirrors `stage()` — the read/write loop continues
|
|
82
|
+
* regardless of fs errors. Losing a measurement is acceptable; killing a
|
|
83
|
+
* user turn is not.
|
|
84
|
+
*/
|
|
85
|
+
export declare function recordInjection(project: string, ev: InjectionEvent): void;
|
|
86
|
+
/**
|
|
87
|
+
* Append one usage event to `_precision/usages.jsonl`.
|
|
88
|
+
*
|
|
89
|
+
* NEVER THROWS. One row per used candidate; the caller is expected to
|
|
90
|
+
* dedupe at the detection layer (one usage row per (turn_hash, candidate_id)).
|
|
91
|
+
*/
|
|
92
|
+
export declare function recordUsage(project: string, ev: UsageEvent): void;
|
|
93
|
+
/**
|
|
94
|
+
* Compute the precision KPI over the trailing `windowDays` (default 14).
|
|
95
|
+
*
|
|
96
|
+
* Algorithm (spec §6):
|
|
97
|
+
* • cutoff = now - windowDays * 86400_000 ms
|
|
98
|
+
* • injected = Σ ev.candidate_ids.length over injections with ts ≥ cutoff
|
|
99
|
+
* • used = count of usage rows with ts ≥ cutoff
|
|
100
|
+
* • precision = injected > 0 ? Math.min(1, used / injected) : null
|
|
101
|
+
* • per_source = { <prefix>: { injected, used } } grouped by id-prefix
|
|
102
|
+
*
|
|
103
|
+
* Invariants:
|
|
104
|
+
* • `precision` is never NaN; it is either a finite number in [0, 1] or null.
|
|
105
|
+
* • Malformed jsonl lines are tolerated (skipped by `readJsonl`).
|
|
106
|
+
* • Empty project → injected=0, used=0, precision=null, per_source={}.
|
|
107
|
+
*/
|
|
108
|
+
export declare function getPrecisionKPI(project: string, windowDays?: number): PrecisionKPI;
|
|
109
|
+
//# sourceMappingURL=precision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"precision.d.ts","sourceRoot":"","sources":["../../src/relevance/precision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAaH,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,EAAE,EAAE,MAAM,CAAC;IACX,iFAAiF;IACjF,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE;AAoGD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAiBzE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI,CAiBjE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,YAAY,CA+DlF"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precision meter — `injection_precision = used / injected`
|
|
3
|
+
* (v3.5.0 P-C, Ambient Relevance Loop, spec §5 + §6 "Precision guardrail").
|
|
4
|
+
*
|
|
5
|
+
* Purpose: this is the north-star metric that proves "intelligence, not
|
|
6
|
+
* storage." Of the memories AgentRecall surfaces (`injected`), how many
|
|
7
|
+
* does the agent actually act on (`used`)? A falling number is the
|
|
8
|
+
* early-warning that the Surfacer threshold is mis-tuned (noise > signal).
|
|
9
|
+
*
|
|
10
|
+
* Storage shape — append-only JSONL, one file per kind, per project:
|
|
11
|
+
*
|
|
12
|
+
* ~/.agent-recall/projects/<slug>/_precision/injections.jsonl
|
|
13
|
+
* ~/.agent-recall/projects/<slug>/_precision/usages.jsonl
|
|
14
|
+
*
|
|
15
|
+
* Design contract:
|
|
16
|
+
* • `recordInjection`/`recordUsage` are fire-and-forget — they NEVER
|
|
17
|
+
* throw. Losing a measurement line is acceptable; killing a user turn
|
|
18
|
+
* is not. Errors are swallowed silently (mirrors stager.ts).
|
|
19
|
+
* • `getPrecisionKPI` is tolerant of malformed lines (skipped, not
|
|
20
|
+
* fatal), missing files (treated as empty), and missing fields.
|
|
21
|
+
* • `precision` is **null** (not NaN) when `injected === 0` — the
|
|
22
|
+
* dashboard can render "—" instead of "NaN%".
|
|
23
|
+
* • `precision` is clamped to `[0, 1]` via `Math.min(1, used / injected)`
|
|
24
|
+
* (matches the existing pattern in storage/corrections.ts:373 — under
|
|
25
|
+
* data corruption / duplicate-usage events, the raw ratio can exceed
|
|
26
|
+
* 1.0, which is nonsense as a "precision" metric).
|
|
27
|
+
* • `per_source` groups by the prefix of `candidate_id` before ':'
|
|
28
|
+
* (e.g. `palace:room:slug` → bucket `palace`). Unprefixed ids land in
|
|
29
|
+
* the `unknown` bucket so they're still visible.
|
|
30
|
+
*
|
|
31
|
+
* Worker done-definition self-check:
|
|
32
|
+
* (1) error paths: `readJsonl` returns [] on read failure, JSON.parse
|
|
33
|
+
* wrapped in try/catch per line; recordInjection/Usage's outer
|
|
34
|
+
* try/catch swallows fs errors.
|
|
35
|
+
* (2) no global binaries — pure node:fs/path/crypto, no shell-outs.
|
|
36
|
+
* (3) ternary ordering — only branch is `injected > 0 ? clamp : null`,
|
|
37
|
+
* null is the explicit zero-denominator case.
|
|
38
|
+
* (4) time logic — `cutoff` is computed in ms-since-epoch from `Date.now()`
|
|
39
|
+
* which is TZ-agnostic; `ts` strings are ISO-8601 (also TZ-agnostic).
|
|
40
|
+
* No local-date filename is used here so no TZ bug surface.
|
|
41
|
+
*/
|
|
42
|
+
import * as fs from "node:fs";
|
|
43
|
+
import * as path from "node:path";
|
|
44
|
+
import { sanitizeProject } from "../storage/paths.js";
|
|
45
|
+
import { ensureDir } from "../storage/fs-utils.js";
|
|
46
|
+
import { getRoot } from "../types.js";
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Internal helpers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/** Resolve the per-project precision directory. Mirrors stager's `_staging/` convention. */
|
|
51
|
+
function precisionDir(project) {
|
|
52
|
+
const safe = sanitizeProject(project);
|
|
53
|
+
return path.join(getRoot(), "projects", safe, "_precision");
|
|
54
|
+
}
|
|
55
|
+
function injectionsPath(project) {
|
|
56
|
+
return path.join(precisionDir(project), "injections.jsonl");
|
|
57
|
+
}
|
|
58
|
+
function usagesPath(project) {
|
|
59
|
+
return path.join(precisionDir(project), "usages.jsonl");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse a JSONL file. Bad lines (truncated writes, manual edits) are
|
|
63
|
+
* silently skipped — never throws. Missing file → empty array.
|
|
64
|
+
*
|
|
65
|
+
* Generic over the row shape; caller is responsible for narrowing.
|
|
66
|
+
*/
|
|
67
|
+
function readJsonl(filePath) {
|
|
68
|
+
if (!fs.existsSync(filePath))
|
|
69
|
+
return [];
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const out = [];
|
|
78
|
+
const lines = raw.split("\n");
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
if (!trimmed)
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
out.push(JSON.parse(trimmed));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Malformed line — skip, never throw.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Append one row to a JSONL file. Wraps the `fs.appendFileSync` call so
|
|
94
|
+
* the public recorders can swallow fs errors at the boundary.
|
|
95
|
+
*/
|
|
96
|
+
function appendJsonl(filePath, row) {
|
|
97
|
+
ensureDir(path.dirname(filePath));
|
|
98
|
+
fs.appendFileSync(filePath, JSON.stringify(row) + "\n", "utf-8");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extract the source prefix from a candidate id.
|
|
102
|
+
* `palace:room:slug` → `palace`. Unprefixed → `unknown`.
|
|
103
|
+
*/
|
|
104
|
+
function sourceOf(candidateId) {
|
|
105
|
+
if (typeof candidateId !== "string" || candidateId.length === 0)
|
|
106
|
+
return "unknown";
|
|
107
|
+
const idx = candidateId.indexOf(":");
|
|
108
|
+
if (idx <= 0)
|
|
109
|
+
return "unknown";
|
|
110
|
+
return candidateId.slice(0, idx);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Narrow an unknown value to `InjectionEvent` shape (defensive — JSONL may
|
|
114
|
+
* have been edited by hand). Returns null on miss.
|
|
115
|
+
*/
|
|
116
|
+
function asInjectionEvent(v) {
|
|
117
|
+
if (!v || typeof v !== "object")
|
|
118
|
+
return null;
|
|
119
|
+
const o = v;
|
|
120
|
+
if (typeof o.ts !== "string")
|
|
121
|
+
return null;
|
|
122
|
+
if (!Array.isArray(o.candidate_ids))
|
|
123
|
+
return null;
|
|
124
|
+
if (typeof o.turn_hash !== "string")
|
|
125
|
+
return null;
|
|
126
|
+
const cids = o.candidate_ids.filter((c) => typeof c === "string");
|
|
127
|
+
return { ts: o.ts, candidate_ids: cids, turn_hash: o.turn_hash };
|
|
128
|
+
}
|
|
129
|
+
function asUsageEvent(v) {
|
|
130
|
+
if (!v || typeof v !== "object")
|
|
131
|
+
return null;
|
|
132
|
+
const o = v;
|
|
133
|
+
if (typeof o.ts !== "string")
|
|
134
|
+
return null;
|
|
135
|
+
if (typeof o.candidate_id !== "string")
|
|
136
|
+
return null;
|
|
137
|
+
if (typeof o.turn_hash !== "string")
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
ts: o.ts,
|
|
141
|
+
candidate_id: o.candidate_id,
|
|
142
|
+
turn_hash: o.turn_hash,
|
|
143
|
+
evidence: typeof o.evidence === "string" ? o.evidence : "",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Public API
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
/**
|
|
150
|
+
* Append one injection event to `_precision/injections.jsonl`.
|
|
151
|
+
*
|
|
152
|
+
* NEVER THROWS. Mirrors `stage()` — the read/write loop continues
|
|
153
|
+
* regardless of fs errors. Losing a measurement is acceptable; killing a
|
|
154
|
+
* user turn is not.
|
|
155
|
+
*/
|
|
156
|
+
export function recordInjection(project, ev) {
|
|
157
|
+
try {
|
|
158
|
+
if (!ev || typeof ev !== "object")
|
|
159
|
+
return;
|
|
160
|
+
const safe = {
|
|
161
|
+
ts: typeof ev.ts === "string" && ev.ts.length > 0
|
|
162
|
+
? ev.ts
|
|
163
|
+
: new Date().toISOString(),
|
|
164
|
+
candidate_ids: Array.isArray(ev.candidate_ids)
|
|
165
|
+
? ev.candidate_ids.filter((c) => typeof c === "string")
|
|
166
|
+
: [],
|
|
167
|
+
turn_hash: typeof ev.turn_hash === "string" ? ev.turn_hash : "",
|
|
168
|
+
};
|
|
169
|
+
appendJsonl(injectionsPath(project), safe);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Swallow — see contract above.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Append one usage event to `_precision/usages.jsonl`.
|
|
177
|
+
*
|
|
178
|
+
* NEVER THROWS. One row per used candidate; the caller is expected to
|
|
179
|
+
* dedupe at the detection layer (one usage row per (turn_hash, candidate_id)).
|
|
180
|
+
*/
|
|
181
|
+
export function recordUsage(project, ev) {
|
|
182
|
+
try {
|
|
183
|
+
if (!ev || typeof ev !== "object")
|
|
184
|
+
return;
|
|
185
|
+
const safe = {
|
|
186
|
+
ts: typeof ev.ts === "string" && ev.ts.length > 0
|
|
187
|
+
? ev.ts
|
|
188
|
+
: new Date().toISOString(),
|
|
189
|
+
candidate_id: typeof ev.candidate_id === "string" ? ev.candidate_id : "",
|
|
190
|
+
turn_hash: typeof ev.turn_hash === "string" ? ev.turn_hash : "",
|
|
191
|
+
evidence: typeof ev.evidence === "string" ? ev.evidence : "",
|
|
192
|
+
};
|
|
193
|
+
if (!safe.candidate_id)
|
|
194
|
+
return; // can't bucket without an id
|
|
195
|
+
appendJsonl(usagesPath(project), safe);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Swallow.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Compute the precision KPI over the trailing `windowDays` (default 14).
|
|
203
|
+
*
|
|
204
|
+
* Algorithm (spec §6):
|
|
205
|
+
* • cutoff = now - windowDays * 86400_000 ms
|
|
206
|
+
* • injected = Σ ev.candidate_ids.length over injections with ts ≥ cutoff
|
|
207
|
+
* • used = count of usage rows with ts ≥ cutoff
|
|
208
|
+
* • precision = injected > 0 ? Math.min(1, used / injected) : null
|
|
209
|
+
* • per_source = { <prefix>: { injected, used } } grouped by id-prefix
|
|
210
|
+
*
|
|
211
|
+
* Invariants:
|
|
212
|
+
* • `precision` is never NaN; it is either a finite number in [0, 1] or null.
|
|
213
|
+
* • Malformed jsonl lines are tolerated (skipped by `readJsonl`).
|
|
214
|
+
* • Empty project → injected=0, used=0, precision=null, per_source={}.
|
|
215
|
+
*/
|
|
216
|
+
export function getPrecisionKPI(project, windowDays) {
|
|
217
|
+
const days = typeof windowDays === "number" && Number.isFinite(windowDays) && windowDays > 0
|
|
218
|
+
? windowDays
|
|
219
|
+
: 14;
|
|
220
|
+
const cutoffMs = Date.now() - days * 86400_000;
|
|
221
|
+
const rawInjections = readJsonl(injectionsPath(project));
|
|
222
|
+
const rawUsages = readJsonl(usagesPath(project));
|
|
223
|
+
const injections = [];
|
|
224
|
+
for (const r of rawInjections) {
|
|
225
|
+
const ev = asInjectionEvent(r);
|
|
226
|
+
if (!ev)
|
|
227
|
+
continue;
|
|
228
|
+
const t = Date.parse(ev.ts);
|
|
229
|
+
if (!Number.isFinite(t))
|
|
230
|
+
continue; // unparseable ts → skip
|
|
231
|
+
if (t < cutoffMs)
|
|
232
|
+
continue;
|
|
233
|
+
injections.push(ev);
|
|
234
|
+
}
|
|
235
|
+
const usages = [];
|
|
236
|
+
for (const r of rawUsages) {
|
|
237
|
+
const ev = asUsageEvent(r);
|
|
238
|
+
if (!ev)
|
|
239
|
+
continue;
|
|
240
|
+
const t = Date.parse(ev.ts);
|
|
241
|
+
if (!Number.isFinite(t))
|
|
242
|
+
continue;
|
|
243
|
+
if (t < cutoffMs)
|
|
244
|
+
continue;
|
|
245
|
+
usages.push(ev);
|
|
246
|
+
}
|
|
247
|
+
let injected = 0;
|
|
248
|
+
const perSource = {};
|
|
249
|
+
for (const ev of injections) {
|
|
250
|
+
for (const cid of ev.candidate_ids) {
|
|
251
|
+
injected++;
|
|
252
|
+
const src = sourceOf(cid);
|
|
253
|
+
if (!perSource[src])
|
|
254
|
+
perSource[src] = { injected: 0, used: 0 };
|
|
255
|
+
perSource[src].injected++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const used = usages.length;
|
|
259
|
+
for (const ev of usages) {
|
|
260
|
+
const src = sourceOf(ev.candidate_id);
|
|
261
|
+
if (!perSource[src])
|
|
262
|
+
perSource[src] = { injected: 0, used: 0 };
|
|
263
|
+
perSource[src].used++;
|
|
264
|
+
}
|
|
265
|
+
// Clamp [0, 1]: data corruption (duplicate usages, late-arriving rows after
|
|
266
|
+
// injections were rotated out, etc.) can push raw used/injected above 1.0.
|
|
267
|
+
// Matches storage/corrections.ts:373 — "150% precision" is nonsense.
|
|
268
|
+
// null (not NaN) is the explicit zero-denominator case so the dashboard
|
|
269
|
+
// can render "—" rather than "NaN%".
|
|
270
|
+
const precision = injected > 0 ? Math.min(1, used / injected) : null;
|
|
271
|
+
return {
|
|
272
|
+
window_days: days,
|
|
273
|
+
injected,
|
|
274
|
+
used,
|
|
275
|
+
precision,
|
|
276
|
+
per_source: perSource,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=precision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"precision.js","sourceRoot":"","sources":["../../src/relevance/precision.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promoter — curates the staging buffer into the palace/journal at session
|
|
3
|
+
* boundaries (v3.5.0 P-A, spec §4 "Promote", §5).
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. Read up to N most-recent staged exchanges via `readStaging`.
|
|
7
|
+
* 2. Apply a pure-function durability heuristic — `isDurableSignal` — to
|
|
8
|
+
* drop chitchat, single-word turns, tool-only exchanges.
|
|
9
|
+
* 3. Dedupe survivors against the project's recent journal titles
|
|
10
|
+
* (`tooSimilarToExistingJournal`, threshold 0.7).
|
|
11
|
+
* 4. Write the survivors via the existing `journalWrite` tool, then clear
|
|
12
|
+
* the staging file up to the newest promoted timestamp.
|
|
13
|
+
*
|
|
14
|
+
* Design notes:
|
|
15
|
+
* • The heuristic is intentionally cheap and pure. It is the FIRST pass —
|
|
16
|
+
* the in-room model (called separately by `sessionEndReflect`) is the
|
|
17
|
+
* true fine-grain judge. Here we just trim the obvious noise.
|
|
18
|
+
* • `opts.dryRun` runs the whole pipeline without writing the journal or
|
|
19
|
+
* clearing staging — used by tests and the dashboard preview.
|
|
20
|
+
* • Threshold ordering inside `isDurableSignal` is high-confidence first
|
|
21
|
+
* (explicit markers) then weaker signals (length + lexical proxy)
|
|
22
|
+
* (worker done-def rule 3).
|
|
23
|
+
*/
|
|
24
|
+
import { type StagedExchange } from "./stager.js";
|
|
25
|
+
export interface PromoteResult {
|
|
26
|
+
promoted: number;
|
|
27
|
+
skipped: number;
|
|
28
|
+
/** Counters keyed by reason for downstream dashboards and tests. */
|
|
29
|
+
reasons: Record<string, number>;
|
|
30
|
+
}
|
|
31
|
+
export interface PromoteOptions {
|
|
32
|
+
/** When true, runs the heuristic + dedup but writes nothing. */
|
|
33
|
+
dryRun?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decide whether a staged exchange carries a durable signal.
|
|
37
|
+
*
|
|
38
|
+
* Returns TRUE on any of:
|
|
39
|
+
* 1. Explicit marker (`decision`, `bug`, `gotcha`, …).
|
|
40
|
+
* 2. Correction shape (`no … wrong`, `actually`, `correction`, `the right way`).
|
|
41
|
+
* 3. Contains a wikilink or fenced code block (durable artifact).
|
|
42
|
+
* 4. Length ≥ 120 chars AND has both an "important word" and an action verb
|
|
43
|
+
* (noun-verb proxy — catches substantive sentences).
|
|
44
|
+
*
|
|
45
|
+
* Returns FALSE for chitchat ("ok", "thanks"), single-sentence questions,
|
|
46
|
+
* and tool-only turns (no user/assistant text).
|
|
47
|
+
*/
|
|
48
|
+
export declare function isDurableSignal(e: StagedExchange): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Promote durable staged exchanges into the journal.
|
|
51
|
+
*
|
|
52
|
+
* Idempotent within a session: once promoted, the corresponding staging
|
|
53
|
+
* entries are cleared (only the newest-promoted timestamp is used as the
|
|
54
|
+
* `beforeTs` cursor, so any post-promote stage() calls survive).
|
|
55
|
+
*
|
|
56
|
+
* `opts.dryRun = true` runs the full pipeline (incl. dedup) but writes
|
|
57
|
+
* nothing and clears nothing. Useful for tests and dashboard previews.
|
|
58
|
+
*/
|
|
59
|
+
export declare function promote(project: string, opts?: PromoteOptions): Promise<PromoteResult>;
|
|
60
|
+
//# sourceMappingURL=promoter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promoter.d.ts","sourceRoot":"","sources":["../../src/relevance/promoter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAoC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAmB1D;AAqFD;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,aAAa,CAAC,CA+JxB"}
|