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.
Files changed (62) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -3
  3. package/src/agents/ai-governance/tools/base.js +19 -166
  4. package/src/agents/backend/tools/base.js +33 -186
  5. package/src/agents/code-quality/tools/base.js +27 -154
  6. package/src/agents/data-layer/tools/base.js +35 -180
  7. package/src/agents/documentation/tools/base.js +18 -146
  8. package/src/agents/infrastructure/tools/base.js +17 -164
  9. package/src/agents/observability/tools/base.js +35 -180
  10. package/src/agents/performance/tools/base.js +35 -205
  11. package/src/agents/release/tools/base.js +35 -180
  12. package/src/agents/reliability/tools/base.js +35 -180
  13. package/src/agents/security/tools/base.js +33 -187
  14. package/src/agents/shared-tools/domain-base.js +246 -0
  15. package/src/agents/supply-chain/tools/base.js +15 -144
  16. package/src/agents/testing/tools/base.js +33 -184
  17. package/src/cli.js +5 -0
  18. package/src/commands/mcp.js +1 -1
  19. package/src/commands/ring-owner.js +58 -0
  20. package/src/commands/session.js +703 -263
  21. package/src/engram/governance.js +67 -0
  22. package/src/engram/index.js +64 -0
  23. package/src/engram/namespace.js +89 -0
  24. package/src/engram/observations.js +34 -0
  25. package/src/engram/sla.js +104 -0
  26. package/src/engram/store.js +115 -0
  27. package/src/engram/tools.js +146 -0
  28. package/src/engram/trust.js +105 -0
  29. package/src/legacy-cli.js +6 -3
  30. package/src/mcp/registry.js +27 -10
  31. package/src/mcp/session-stdio-server.js +209 -18
  32. package/src/mcp/smoke.js +9 -8
  33. package/src/pocket/ring-owner.js +97 -0
  34. package/src/review/local-review.js +9 -5
  35. package/src/review/omargate-orchestrator.js +111 -14
  36. package/src/review/spec-binding.js +16 -1
  37. package/src/scan/generator.js +1 -1
  38. package/src/session/agent-registry.js +22 -63
  39. package/src/session/checkpoints.js +194 -0
  40. package/src/session/coordination-guidance.js +6 -6
  41. package/src/session/daemon.js +19 -124
  42. package/src/session/file-lease-integrations.js +1300 -0
  43. package/src/session/file-locks.js +967 -464
  44. package/src/session/listener.js +243 -20
  45. package/src/session/listeners.js +71 -80
  46. package/src/session/paths.js +2 -0
  47. package/src/session/recall/embedder.js +181 -0
  48. package/src/session/recall/entities.js +0 -0
  49. package/src/session/recall/index-build.js +38 -0
  50. package/src/session/recall/index-core.js +108 -0
  51. package/src/session/recall/index.js +131 -0
  52. package/src/session/recall/needle-eval.js +279 -0
  53. package/src/session/recall/observation-core.js +183 -0
  54. package/src/session/recall/observations.js +21 -0
  55. package/src/session/recall/provenance.js +153 -0
  56. package/src/session/recall/retrieve.js +374 -0
  57. package/src/session/recall/text.js +0 -0
  58. package/src/session/recall/token-stats.js +81 -0
  59. package/src/session/recap.js +29 -23
  60. package/src/session/remote-hydrate.js +5 -1
  61. package/src/session/sync.js +661 -11
  62. 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
+ }