@pleri/olam-cli 0.1.204 → 0.1.205
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/README.md +1 -1
- package/dist/image-digests.json +8 -8
- package/dist/index.js +1601 -1404
- package/hermes-bundle/version.json +1 -1
- package/host-cp/k8s/manifests/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +1 -1
- package/memory-hooks/agentmemory-classify-queue.mjs +363 -0
- package/memory-hooks/agentmemory-recall-trigger.mjs +233 -0
- package/memory-hooks/agentmemory-reflect-cite.mjs +332 -0
- package/memory-hooks/agentmemory-session-recall.js +332 -0
- package/memory-hooks/recall-log.mjs +185 -0
- package/package.json +2 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall-log.mjs — the session-local recall-linkage log (Phase D).
|
|
3
|
+
*
|
|
4
|
+
* When the SessionStart recall hook surfaces memories, it appends the surfaced
|
|
5
|
+
* set here, keyed by a stable session key. At session end the citation hook
|
|
6
|
+
* reads this log + the transcript and reinforces the subset that was literally
|
|
7
|
+
* cited (the deterministic backstop to the explicit `memory_reinforce` tool).
|
|
8
|
+
*
|
|
9
|
+
* Design constraints:
|
|
10
|
+
* - Pure-ish + fail-open: any IO error is swallowed (the log is a
|
|
11
|
+
* best-effort signal, never load-bearing for the session).
|
|
12
|
+
* - Bounded: each session file keeps at most {@link MAX_ENTRIES} recall
|
|
13
|
+
* events and at most {@link MAX_SURFACED_PER_ENTRY} memories per event, so a
|
|
14
|
+
* long pathological session can't grow the file without bound.
|
|
15
|
+
* - Append/merge: re-recall within the same session appends a new entry
|
|
16
|
+
* rather than clobbering — the citation detector unions across entries.
|
|
17
|
+
*
|
|
18
|
+
* The module is ESM so the .mjs citation hook + the node:test suite can import
|
|
19
|
+
* it directly. The CJS recall hook (agentmemory-session-recall.js) inlines a
|
|
20
|
+
* small copy of {@link sessionKeyFor} + {@link appendRecall} for the same
|
|
21
|
+
* no-build reason it inlines deriveRecallQuery.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, statSync, rmSync } from 'node:fs';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { randomBytes } from 'node:crypto';
|
|
28
|
+
|
|
29
|
+
export const MAX_ENTRIES = 50;
|
|
30
|
+
export const MAX_SURFACED_PER_ENTRY = 20;
|
|
31
|
+
/** Excerpt cap per surfaced memory — keeps the log small + matches recall's 240-char excerpt budget. */
|
|
32
|
+
export const MAX_EXCERPT_CHARS = 240;
|
|
33
|
+
/** Delete session logs older than this on the occasional sweep (best-effort GC). */
|
|
34
|
+
export const LOG_TTL_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
|
|
35
|
+
|
|
36
|
+
/** Resolved recall-log directory: `~/.olam/recall-log/`. */
|
|
37
|
+
export function recallLogDir() {
|
|
38
|
+
return join(homedir(), '.olam', 'recall-log');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stable, filesystem-safe session key. Prefers the harness session_id; falls
|
|
43
|
+
* back to a cwd-slug + UTC date so two sessions in the same dir on the same day
|
|
44
|
+
* coalesce (acceptable — the citation detector is conservative regardless).
|
|
45
|
+
*
|
|
46
|
+
* @param {{ session_id?: string, cwd?: string }} event
|
|
47
|
+
* @param {Date} [now]
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function sessionKeyFor(event, now = new Date()) {
|
|
51
|
+
const sid = event && typeof event.session_id === 'string' ? event.session_id.trim() : '';
|
|
52
|
+
if (sid) return slug(sid);
|
|
53
|
+
const cwd = event && typeof event.cwd === 'string' ? event.cwd : '';
|
|
54
|
+
const cwdSlug = slug(cwd.replace(/\/+$/, '').split('/').slice(-2).join('-')) || 'session';
|
|
55
|
+
const date = now.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
56
|
+
return `${cwdSlug}-${date}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function slug(s) {
|
|
60
|
+
return String(s).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sessionLogPath(sessionKey) {
|
|
64
|
+
return join(recallLogDir(), `${slug(sessionKey)}.json`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureDir(dir = recallLogDir()) {
|
|
68
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalise an array of surfaced memories to the stored shape, dropping
|
|
73
|
+
* entries without an id and capping count + excerpt length.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<{id?: string, obsId?: string, title?: string, excerpt?: string, contentExcerpt?: string, content?: string}>} surfaced
|
|
76
|
+
* @returns {Array<{id: string, title: string, excerpt: string}>}
|
|
77
|
+
*/
|
|
78
|
+
export function normalizeSurfaced(surfaced) {
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const m of Array.isArray(surfaced) ? surfaced : []) {
|
|
81
|
+
if (!m || typeof m !== 'object') continue;
|
|
82
|
+
const id = typeof m.id === 'string' ? m.id : typeof m.obsId === 'string' ? m.obsId : '';
|
|
83
|
+
if (!id) continue;
|
|
84
|
+
const title = typeof m.title === 'string' ? m.title : '';
|
|
85
|
+
const rawExcerpt =
|
|
86
|
+
(typeof m.excerpt === 'string' && m.excerpt) ||
|
|
87
|
+
(typeof m.contentExcerpt === 'string' && m.contentExcerpt) ||
|
|
88
|
+
(typeof m.content === 'string' && m.content) ||
|
|
89
|
+
'';
|
|
90
|
+
out.push({ id, title, excerpt: String(rawExcerpt).slice(0, MAX_EXCERPT_CHARS) });
|
|
91
|
+
if (out.length >= MAX_SURFACED_PER_ENTRY) break;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read the recall log for a session, returning `{ entries: [...] }` or an empty
|
|
98
|
+
* shape on any error / absence. Each entry is `{ ts, surfaced: [{id,title,excerpt}] }`.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} sessionKey
|
|
101
|
+
* @returns {{ entries: Array<{ ts: number, surfaced: Array<{id:string,title:string,excerpt:string}> }> }}
|
|
102
|
+
*/
|
|
103
|
+
export function readRecallLog(sessionKey) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = readFileSync(sessionLogPath(sessionKey), 'utf8');
|
|
106
|
+
const parsed = JSON.parse(raw);
|
|
107
|
+
if (parsed && Array.isArray(parsed.entries)) return parsed;
|
|
108
|
+
} catch {
|
|
109
|
+
// fall through to empty
|
|
110
|
+
}
|
|
111
|
+
return { entries: [] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Append a recall event to the session log (append/merge, bounded, atomic,
|
|
116
|
+
* fail-open). Returns the surfaced set that was actually recorded (post-
|
|
117
|
+
* normalisation), or [] if nothing was written.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} sessionKey
|
|
120
|
+
* @param {Array<object>} surfaced
|
|
121
|
+
* @param {number} [now]
|
|
122
|
+
* @returns {Array<{id:string,title:string,excerpt:string}>}
|
|
123
|
+
*/
|
|
124
|
+
export function appendRecall(sessionKey, surfaced, now = Date.now()) {
|
|
125
|
+
const normalized = normalizeSurfaced(surfaced);
|
|
126
|
+
if (normalized.length === 0) return [];
|
|
127
|
+
try {
|
|
128
|
+
ensureDir();
|
|
129
|
+
const log = readRecallLog(sessionKey);
|
|
130
|
+
log.entries.push({ ts: now, surfaced: normalized });
|
|
131
|
+
// Bound: keep only the most recent MAX_ENTRIES events.
|
|
132
|
+
if (log.entries.length > MAX_ENTRIES) {
|
|
133
|
+
log.entries = log.entries.slice(log.entries.length - MAX_ENTRIES);
|
|
134
|
+
}
|
|
135
|
+
const final = sessionLogPath(sessionKey);
|
|
136
|
+
const tmp = `${final}.${randomBytes(6).toString('hex')}.tmp`;
|
|
137
|
+
writeFileSync(tmp, JSON.stringify(log), 'utf8');
|
|
138
|
+
renameSync(tmp, final);
|
|
139
|
+
return normalized;
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Collect the de-duplicated union of all surfaced memories across every entry in
|
|
147
|
+
* a session log. Used by the citation hook as the candidate set to test against
|
|
148
|
+
* the transcript. Returns a map id → {id,title,excerpt} (latest excerpt wins).
|
|
149
|
+
*
|
|
150
|
+
* @param {string} sessionKey
|
|
151
|
+
* @returns {Array<{id:string,title:string,excerpt:string}>}
|
|
152
|
+
*/
|
|
153
|
+
export function collectSurfaced(sessionKey) {
|
|
154
|
+
const byId = new Map();
|
|
155
|
+
for (const entry of readRecallLog(sessionKey).entries) {
|
|
156
|
+
for (const m of entry.surfaced ?? []) {
|
|
157
|
+
if (m && typeof m.id === 'string') byId.set(m.id, m);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [...byId.values()];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Best-effort GC: delete session logs older than {@link LOG_TTL_MS}. Fail-open.
|
|
165
|
+
* Called opportunistically by the citation hook at session end.
|
|
166
|
+
*/
|
|
167
|
+
export function sweepStaleLogs(now = Date.now()) {
|
|
168
|
+
try {
|
|
169
|
+
const dir = recallLogDir();
|
|
170
|
+
if (!existsSync(dir)) return;
|
|
171
|
+
for (const name of readdirSync(dir)) {
|
|
172
|
+
if (!name.endsWith('.json')) continue;
|
|
173
|
+
const p = join(dir, name);
|
|
174
|
+
try {
|
|
175
|
+
if (now - statSync(p).mtimeMs > LOG_TTL_MS) {
|
|
176
|
+
rmSync(p, { force: true });
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
/* ignore single-file errors */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
/* ignore */
|
|
184
|
+
}
|
|
185
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pleri/olam-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.205",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"olam": "./bin/olam.cjs"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"hermes-bundle",
|
|
16
16
|
"hooks",
|
|
17
17
|
"host-cp",
|
|
18
|
+
"memory-hooks",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|