sentinelayer-cli 0.39.2 → 0.41.0
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 +4 -4
- package/package.json +2 -3
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +703 -263
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +64 -0
- package/src/engram/namespace.js +89 -0
- package/src/engram/observations.js +34 -0
- package/src/engram/sla.js +104 -0
- package/src/engram/store.js +115 -0
- package/src/engram/tools.js +146 -0
- package/src/engram/trust.js +105 -0
- package/src/legacy-cli.js +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +194 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon.js +19 -124
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/listener.js +243 -20
- package/src/session/listeners.js +71 -80
- package/src/session/paths.js +2 -0
- package/src/session/recall/embedder.js +181 -0
- package/src/session/recall/entities.js +0 -0
- package/src/session/recall/index-build.js +38 -0
- package/src/session/recall/index-core.js +108 -0
- package/src/session/recall/index.js +131 -0
- package/src/session/recall/needle-eval.js +279 -0
- package/src/session/recall/observation-core.js +183 -0
- package/src/session/recall/observations.js +21 -0
- package/src/session/recall/provenance.js +153 -0
- package/src/session/recall/retrieve.js +374 -0
- package/src/session/recall/text.js +0 -0
- package/src/session/recall/token-stats.js +81 -0
- package/src/session/recap.js +29 -23
- package/src/session/remote-hydrate.js +5 -1
- package/src/session/sync.js +661 -11
- package/src/session/wake/pump.js +8 -3
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §1 — pluggable dense text embedder (256-d, int8, deterministic).
|
|
3
|
+
*
|
|
4
|
+
* Design fidelity (ENGRAM §4 model roster + §7 storage math):
|
|
5
|
+
* - 256-d Matryoshka-truncated text vectors, int8-quantized for the
|
|
6
|
+
* "exact int8 scan" recall path (§7 stage 2a). No ANN — a session is
|
|
7
|
+
* small-data (Thesis 1), so exhaustive scan is 100% recall by definition.
|
|
8
|
+
* - The interface is PLUGGABLE. The DEFAULT is a deterministic
|
|
9
|
+
* hash-projection stub (the feature-hashing trick) so the 8-Needle
|
|
10
|
+
* harness (§11) is reproducible in CI with no model download. A real
|
|
11
|
+
* EmbeddingGemma-class embedder plugs in behind the same interface via
|
|
12
|
+
* `provider: "api"`, degrading to the stub on any failure — the same
|
|
13
|
+
* provider-fallback shape the repo already uses in
|
|
14
|
+
* `src/memory/retrieval.js`.
|
|
15
|
+
*
|
|
16
|
+
* The stub is intentionally lexical-leaning: chains in Needle-Chain are
|
|
17
|
+
* linked ONLY through shared entities (their head/tail share no tokens), so
|
|
18
|
+
* chain completion is carried by the graph-expansion stage (§7 spreading
|
|
19
|
+
* activation), not by dense similarity — exactly as the design intends.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { tokenize, bigrams, charNGrams, fnv1a32 } from "./text.js";
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_DIM = 256;
|
|
25
|
+
const QUANT_SCALE = 127;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Deterministic hash-projection into a dense float vector, then L2-normalize.
|
|
29
|
+
* Signed feature hashing: each feature lands in one bucket with a stable
|
|
30
|
+
* sign, so collisions cancel in expectation rather than compound.
|
|
31
|
+
* @param {string} text
|
|
32
|
+
* @param {number} dim
|
|
33
|
+
* @returns {Float32Array}
|
|
34
|
+
*/
|
|
35
|
+
function hashProjectionFloat(text, dim) {
|
|
36
|
+
const vec = new Float32Array(dim);
|
|
37
|
+
const tokens = tokenize(text);
|
|
38
|
+
if (tokens.length === 0) return vec;
|
|
39
|
+
// Features: token unigrams + token bigrams (phrase order) + character
|
|
40
|
+
// trigrams per token (robust overlap for short texts). Char trigrams make
|
|
41
|
+
// genuine token overlap dominate collision noise in the 256-d space.
|
|
42
|
+
const features = tokens.concat(bigrams(tokens));
|
|
43
|
+
for (const token of tokens) {
|
|
44
|
+
for (const gram of charNGrams(token, 3)) features.push(`c:${gram}`);
|
|
45
|
+
}
|
|
46
|
+
for (const feature of features) {
|
|
47
|
+
const bucket = fnv1a32(feature) % dim;
|
|
48
|
+
const sign = (fnv1a32(`sign:${feature}`) & 1) === 1 ? 1 : -1;
|
|
49
|
+
vec[bucket] += sign;
|
|
50
|
+
}
|
|
51
|
+
let sumSquares = 0;
|
|
52
|
+
for (let i = 0; i < dim; i += 1) sumSquares += vec[i] * vec[i];
|
|
53
|
+
const norm = Math.sqrt(sumSquares);
|
|
54
|
+
if (norm > 0) {
|
|
55
|
+
for (let i = 0; i < dim; i += 1) vec[i] /= norm;
|
|
56
|
+
}
|
|
57
|
+
return vec;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Quantize a normalized float vector to int8 (×127, rounded, clamped).
|
|
62
|
+
* @param {Float32Array} floatVec
|
|
63
|
+
* @returns {Int8Array}
|
|
64
|
+
*/
|
|
65
|
+
export function quantizeInt8(floatVec) {
|
|
66
|
+
const out = new Int8Array(floatVec.length);
|
|
67
|
+
for (let i = 0; i < floatVec.length; i += 1) {
|
|
68
|
+
const scaled = Math.round(floatVec[i] * QUANT_SCALE);
|
|
69
|
+
out[i] = scaled > QUANT_SCALE ? QUANT_SCALE : scaled < -QUANT_SCALE ? -QUANT_SCALE : scaled;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* L2 norm of an int8 vector (computed once at index time, cached for the
|
|
76
|
+
* exact-scan cosine).
|
|
77
|
+
* @param {Int8Array} vec
|
|
78
|
+
* @returns {number}
|
|
79
|
+
*/
|
|
80
|
+
export function normInt8(vec) {
|
|
81
|
+
let sumSquares = 0;
|
|
82
|
+
for (let i = 0; i < vec.length; i += 1) sumSquares += vec[i] * vec[i];
|
|
83
|
+
return Math.sqrt(sumSquares);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Dot product of two equal-length int8 vectors.
|
|
88
|
+
* @param {Int8Array} a
|
|
89
|
+
* @param {Int8Array} b
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
export function dotInt8(a, b) {
|
|
93
|
+
let dot = 0;
|
|
94
|
+
const len = Math.min(a.length, b.length);
|
|
95
|
+
for (let i = 0; i < len; i += 1) dot += a[i] * b[i];
|
|
96
|
+
return dot;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Cosine similarity of two int8 vectors given precomputed norms.
|
|
101
|
+
* @returns {number} in [-1, 1]
|
|
102
|
+
*/
|
|
103
|
+
export function cosineInt8(a, b, normA, normB) {
|
|
104
|
+
if (!normA || !normB) return 0;
|
|
105
|
+
return dotInt8(a, b) / (normA * normB);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create an embedder. Default is the deterministic hash-projection stub.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} [options]
|
|
112
|
+
* @param {number} [options.dim=256]
|
|
113
|
+
* @param {"stub"|"api"} [options.provider="stub"]
|
|
114
|
+
* @param {string} [options.endpoint="")] Real-embedder HTTP endpoint (provider="api").
|
|
115
|
+
* @param {string} [options.apiKey=""]
|
|
116
|
+
* @param {Function} [options.fetchImpl] Injected fetch implementation for tests/provider.
|
|
117
|
+
* @returns {{
|
|
118
|
+
* name: string, dim: number,
|
|
119
|
+
* embedFloat: (text: string) => Float32Array,
|
|
120
|
+
* embed: (text: string) => Int8Array,
|
|
121
|
+
* embedRemote: (text: string) => Promise<{vector: Int8Array, provider: string, fallback: boolean}>
|
|
122
|
+
* }}
|
|
123
|
+
*/
|
|
124
|
+
export function createEmbedder({
|
|
125
|
+
dim = DEFAULT_DIM,
|
|
126
|
+
provider = "stub",
|
|
127
|
+
endpoint = "",
|
|
128
|
+
apiKey = "",
|
|
129
|
+
fetchImpl = globalThis.fetch,
|
|
130
|
+
} = {}) {
|
|
131
|
+
const normalizedDim = Number.isInteger(dim) && dim > 0 ? dim : DEFAULT_DIM;
|
|
132
|
+
const embedFloat = (text) => hashProjectionFloat(text, normalizedDim);
|
|
133
|
+
const embed = (text) => quantizeInt8(embedFloat(text));
|
|
134
|
+
|
|
135
|
+
// Bound the remote embedder call so a hung provider can't stall recall; on
|
|
136
|
+
// timeout AbortSignal rejects the fetch and we fall back to the deterministic
|
|
137
|
+
// stub via the catch below.
|
|
138
|
+
const EMBED_REMOTE_TIMEOUT_MS = 10_000;
|
|
139
|
+
async function embedRemote(text) {
|
|
140
|
+
const canUseApi =
|
|
141
|
+
provider === "api" && endpoint && typeof fetchImpl === "function";
|
|
142
|
+
if (!canUseApi) {
|
|
143
|
+
return { vector: embed(text), provider: "stub", fallback: provider === "api" };
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetchImpl(String(endpoint), {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"content-type": "application/json",
|
|
150
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({ text: String(text ?? ""), dim: normalizedDim }),
|
|
153
|
+
signal: AbortSignal.timeout(EMBED_REMOTE_TIMEOUT_MS),
|
|
154
|
+
});
|
|
155
|
+
if (!response || !response.ok) {
|
|
156
|
+
throw new Error(`embedder api ${response ? response.status : "no_response"}`);
|
|
157
|
+
}
|
|
158
|
+
const payload = await response.json();
|
|
159
|
+
const values = Array.isArray(payload?.vector) ? payload.vector : null;
|
|
160
|
+
if (!values || values.length === 0) {
|
|
161
|
+
throw new Error("embedder api returned empty vector");
|
|
162
|
+
}
|
|
163
|
+
// Matryoshka-truncate/pad to dim, then quantize for the shared scan path.
|
|
164
|
+
const floatVec = new Float32Array(normalizedDim);
|
|
165
|
+
for (let i = 0; i < normalizedDim && i < values.length; i += 1) {
|
|
166
|
+
floatVec[i] = Number(values[i]) || 0;
|
|
167
|
+
}
|
|
168
|
+
return { vector: quantizeInt8(floatVec), provider: "api", fallback: false };
|
|
169
|
+
} catch {
|
|
170
|
+
return { vector: embed(text), provider: "stub", fallback: true };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
name: provider === "api" ? `api:${normalizedDim}` : `hash-projection:${normalizedDim}`,
|
|
176
|
+
dim: normalizedDim,
|
|
177
|
+
embedFloat,
|
|
178
|
+
embed,
|
|
179
|
+
embedRemote,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SentinelLayer session adapter for the pure ENGRAM recall-index builder.
|
|
3
|
+
*
|
|
4
|
+
* Raw session events are filtered and normalized here. Detachable ENGRAM
|
|
5
|
+
* consumers import index-core.js directly and therefore never pull the
|
|
6
|
+
* SentinelLayer session runtime into their dependency closure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { buildObservations } from "./observations.js";
|
|
10
|
+
import { buildRecallIndex as buildCoreRecallIndex } from "./index-core.js";
|
|
11
|
+
|
|
12
|
+
export { BM25_K1, BM25_B } from "./index-core.js";
|
|
13
|
+
|
|
14
|
+
export function buildRecallIndex({
|
|
15
|
+
events = [],
|
|
16
|
+
observations: prebuiltObservations = null,
|
|
17
|
+
messageActions = [],
|
|
18
|
+
embedder,
|
|
19
|
+
sessionId = "",
|
|
20
|
+
includeControlEvents = false,
|
|
21
|
+
} = {}) {
|
|
22
|
+
const built = Array.isArray(prebuiltObservations)
|
|
23
|
+
? {
|
|
24
|
+
observations: prebuiltObservations,
|
|
25
|
+
droppedControlEvents: 0,
|
|
26
|
+
materialCount: prebuiltObservations.length,
|
|
27
|
+
}
|
|
28
|
+
: buildObservations(events, { sessionId, includeControlEvents });
|
|
29
|
+
|
|
30
|
+
return buildCoreRecallIndex({
|
|
31
|
+
observations: built.observations,
|
|
32
|
+
messageActions,
|
|
33
|
+
embedder,
|
|
34
|
+
sessionId,
|
|
35
|
+
droppedControlEvents: built.droppedControlEvents,
|
|
36
|
+
materialCount: built.materialCount,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure ENGRAM recall-index builder.
|
|
3
|
+
*
|
|
4
|
+
* This module accepts canonical observations only. Session event filtering is
|
|
5
|
+
* an adapter concern in index-build.js, which keeps the retrieval closure
|
|
6
|
+
* reusable without importing SentinelLayer session runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { buildEntityGraph } from "./entities.js";
|
|
10
|
+
import { createEmbedder, normInt8 } from "./embedder.js";
|
|
11
|
+
|
|
12
|
+
// BM25 free parameters (Robertson/Sparck-Jones defaults — standard, not tuned).
|
|
13
|
+
export const BM25_K1 = 1.2;
|
|
14
|
+
export const BM25_B = 0.75;
|
|
15
|
+
|
|
16
|
+
function buildBm25(observations) {
|
|
17
|
+
const postings = new Map();
|
|
18
|
+
const docLen = new Map();
|
|
19
|
+
let totalLen = 0;
|
|
20
|
+
|
|
21
|
+
for (const obs of observations) {
|
|
22
|
+
const tokens = obs.tokens;
|
|
23
|
+
docLen.set(obs.id, tokens.length);
|
|
24
|
+
totalLen += tokens.length;
|
|
25
|
+
const tf = new Map();
|
|
26
|
+
for (const token of tokens) tf.set(token, (tf.get(token) || 0) + 1);
|
|
27
|
+
for (const [term, count] of tf.entries()) {
|
|
28
|
+
let bucket = postings.get(term);
|
|
29
|
+
if (!bucket) {
|
|
30
|
+
bucket = new Map();
|
|
31
|
+
postings.set(term, bucket);
|
|
32
|
+
}
|
|
33
|
+
bucket.set(obs.id, count);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const df = new Map();
|
|
38
|
+
for (const [term, bucket] of postings.entries()) df.set(term, bucket.size);
|
|
39
|
+
|
|
40
|
+
const N = observations.length;
|
|
41
|
+
const avgdl = N > 0 ? totalLen / N : 0;
|
|
42
|
+
return { postings, df, docLen, avgdl, N };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildDense(observations, embedder) {
|
|
46
|
+
const vectors = new Map();
|
|
47
|
+
const norms = new Map();
|
|
48
|
+
for (const obs of observations) {
|
|
49
|
+
const vector = embedder.embed(obs.text);
|
|
50
|
+
vectors.set(obs.id, vector);
|
|
51
|
+
norms.set(obs.id, normInt8(vector));
|
|
52
|
+
}
|
|
53
|
+
return { vectors, norms, dim: embedder.dim };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a recall index from already-normalized observations. */
|
|
57
|
+
export function buildRecallIndex({
|
|
58
|
+
observations = [],
|
|
59
|
+
messageActions = [],
|
|
60
|
+
embedder = createEmbedder(),
|
|
61
|
+
sessionId = "",
|
|
62
|
+
droppedControlEvents = 0,
|
|
63
|
+
materialCount = observations.length,
|
|
64
|
+
} = {}) {
|
|
65
|
+
if (!Array.isArray(observations)) {
|
|
66
|
+
throw new TypeError("observations must be an array.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const byId = new Map();
|
|
70
|
+
for (const obs of observations) byId.set(obs.id, obs);
|
|
71
|
+
|
|
72
|
+
const order = observations
|
|
73
|
+
.slice()
|
|
74
|
+
.sort((a, b) => {
|
|
75
|
+
const sa = a.sequenceId || 0;
|
|
76
|
+
const sb = b.sequenceId || 0;
|
|
77
|
+
if (sa !== sb) return sa - sb;
|
|
78
|
+
return a.index - b.index;
|
|
79
|
+
})
|
|
80
|
+
.map((obs) => obs.id);
|
|
81
|
+
|
|
82
|
+
const graph = buildEntityGraph(observations, messageActions);
|
|
83
|
+
const bm25 = buildBm25(observations);
|
|
84
|
+
const dense = buildDense(observations, embedder);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: "1.0.0",
|
|
88
|
+
sessionId,
|
|
89
|
+
observations,
|
|
90
|
+
byId,
|
|
91
|
+
order,
|
|
92
|
+
graph,
|
|
93
|
+
bm25,
|
|
94
|
+
dense,
|
|
95
|
+
occurrences: graph.occurrences,
|
|
96
|
+
embedder,
|
|
97
|
+
meta: {
|
|
98
|
+
observationCount: observations.length,
|
|
99
|
+
materialCount,
|
|
100
|
+
droppedControlEvents,
|
|
101
|
+
entityCount: graph.counts.entities,
|
|
102
|
+
edgeCount: graph.counts.edges,
|
|
103
|
+
occurrenceCount: graph.counts.occurrences,
|
|
104
|
+
embedder: embedder.name,
|
|
105
|
+
dim: embedder.dim,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §1 — Session Recall: public entry point.
|
|
3
|
+
*
|
|
4
|
+
* `runSessionRecall` is the orchestrator behind `sl session recall`:
|
|
5
|
+
* 1. (optional) full remote hydrate — tail:0 backfill so we never index a
|
|
6
|
+
* truncated local mirror (Redis hot-caches only the latest ~2k events;
|
|
7
|
+
* older history lives in Postgres and must be paged down first). We
|
|
8
|
+
* honor `eventsBackfillComplete` and warn if the backfill is partial.
|
|
9
|
+
* 2. read the full local NDJSON stream (tail:0)
|
|
10
|
+
* 3. pull the message-actions layer (reply edges + ACT-R rehearsal fuel)
|
|
11
|
+
* 4. build the tri-layer recall index
|
|
12
|
+
* 5. run the Recall Engine
|
|
13
|
+
* 6. compute the token cut vs full-tail replay
|
|
14
|
+
*
|
|
15
|
+
* Runtime dependencies are injected (defaults wire the real session
|
|
16
|
+
* modules) so the orchestrator is unit-testable without network or fs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readStream } from "../stream.js";
|
|
20
|
+
import { hydrateSessionFromRemote } from "../remote-hydrate.js";
|
|
21
|
+
import { listSessionMessageActions } from "../sync.js";
|
|
22
|
+
|
|
23
|
+
import { buildRecallIndex } from "./index-build.js";
|
|
24
|
+
import { recall } from "./retrieve.js";
|
|
25
|
+
import { createEmbedder } from "./embedder.js";
|
|
26
|
+
import { computeTokenCut } from "./token-stats.js";
|
|
27
|
+
|
|
28
|
+
export { buildRecallIndex } from "./index-build.js";
|
|
29
|
+
export { recall, RECALL_DEFAULTS, RECALL_WEIGHTS } from "./retrieve.js";
|
|
30
|
+
export { createEmbedder, DEFAULT_DIM } from "./embedder.js";
|
|
31
|
+
export { buildObservations } from "./observations.js";
|
|
32
|
+
export { buildEntityGraph, matchQueryEntities, EDGE_TYPES } from "./entities.js";
|
|
33
|
+
export { buildProvenance } from "./provenance.js";
|
|
34
|
+
export { computeTokenCut } from "./token-stats.js";
|
|
35
|
+
export { runEightNeedle } from "./needle-eval.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hydrate + build + recall for a session.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} params
|
|
41
|
+
* @param {string} params.sessionId
|
|
42
|
+
* @param {string} params.need The recall query / need.
|
|
43
|
+
* @param {string} [params.targetPath]
|
|
44
|
+
* @param {number} [params.k=12]
|
|
45
|
+
* @param {string} [params.role]
|
|
46
|
+
* @param {boolean} [params.remote=true] Hydrate from the API before indexing.
|
|
47
|
+
* @param {boolean} [params.includeControlEvents=false]
|
|
48
|
+
* @param {object} [params.embedder]
|
|
49
|
+
* @param {number} [params.now] ACT-R clock (default Date.now()).
|
|
50
|
+
* @param {Function} [params._hydrate] seam
|
|
51
|
+
* @param {Function} [params._readStream] seam
|
|
52
|
+
* @param {Function} [params._listActions] seam
|
|
53
|
+
* @returns {Promise<object>} recall payload (JSON-serializable)
|
|
54
|
+
*/
|
|
55
|
+
export async function runSessionRecall({
|
|
56
|
+
sessionId,
|
|
57
|
+
need,
|
|
58
|
+
targetPath = process.cwd(),
|
|
59
|
+
k = 12,
|
|
60
|
+
role = "",
|
|
61
|
+
remote = true,
|
|
62
|
+
includeControlEvents = false,
|
|
63
|
+
embedder = createEmbedder(),
|
|
64
|
+
now = Date.now(),
|
|
65
|
+
_hydrate = hydrateSessionFromRemote,
|
|
66
|
+
_readStream = readStream,
|
|
67
|
+
_listActions = listSessionMessageActions,
|
|
68
|
+
} = {}) {
|
|
69
|
+
const normalizedSessionId = String(sessionId || "").trim();
|
|
70
|
+
const normalizedNeed = String(need || "").trim();
|
|
71
|
+
if (!normalizedSessionId) {
|
|
72
|
+
return { ok: false, reason: "invalid_session_id", results: [] };
|
|
73
|
+
}
|
|
74
|
+
if (normalizedNeed.length < 2) {
|
|
75
|
+
return { ok: false, reason: "query_too_short", results: [] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const backfill = { attempted: false, ok: false, complete: null, warning: "" };
|
|
79
|
+
if (remote) {
|
|
80
|
+
backfill.attempted = true;
|
|
81
|
+
const hydration = await _hydrate({
|
|
82
|
+
sessionId: normalizedSessionId,
|
|
83
|
+
targetPath,
|
|
84
|
+
includeControlEvents,
|
|
85
|
+
}).catch((error) => ({ ok: false, reason: String(error?.message || error) }));
|
|
86
|
+
backfill.ok = Boolean(hydration?.ok);
|
|
87
|
+
backfill.complete = hydration?.eventsBackfillComplete !== false;
|
|
88
|
+
if (!hydration?.ok) {
|
|
89
|
+
backfill.warning = `remote hydrate skipped (${hydration?.reason || "unknown"}); indexing local mirror only`;
|
|
90
|
+
} else if (hydration.eventsBackfillComplete === false) {
|
|
91
|
+
backfill.warning = `remote backfill incomplete (${hydration.eventsBackfillReason || "more pages"}); recall may miss older memories`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const events = await _readStream(normalizedSessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
96
|
+
|
|
97
|
+
let messageActions = [];
|
|
98
|
+
const actionsResult = await _listActions(normalizedSessionId, { targetPath, limit: 500 }).catch(() => null);
|
|
99
|
+
if (actionsResult?.ok && Array.isArray(actionsResult.actions)) {
|
|
100
|
+
messageActions = actionsResult.actions;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const index = buildRecallIndex({
|
|
104
|
+
events,
|
|
105
|
+
messageActions,
|
|
106
|
+
embedder,
|
|
107
|
+
sessionId: normalizedSessionId,
|
|
108
|
+
includeControlEvents,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const recalled = recall(index, { query: normalizedNeed, k, role, now });
|
|
112
|
+
const tokenCut = await computeTokenCut({
|
|
113
|
+
observations: index.observations,
|
|
114
|
+
results: recalled.results,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
reason: "",
|
|
120
|
+
sessionId: normalizedSessionId,
|
|
121
|
+
need: normalizedNeed,
|
|
122
|
+
role: role || null,
|
|
123
|
+
k: recalled.k,
|
|
124
|
+
backfill,
|
|
125
|
+
meta: index.meta,
|
|
126
|
+
matchedEntities: recalled.matchedEntityIds,
|
|
127
|
+
poolSize: recalled.poolSize,
|
|
128
|
+
results: recalled.results,
|
|
129
|
+
tokenCut,
|
|
130
|
+
};
|
|
131
|
+
}
|