gnosys 5.13.0 → 5.14.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/dist/cli.js +22 -3
- package/dist/index.js +2 -2
- package/dist/lib/db.d.ts +6 -0
- package/dist/lib/db.js +9 -0
- package/dist/lib/dreamCommand.js +12 -3
- package/dist/lib/dreamRunLog.d.ts +10 -0
- package/dist/lib/dreamRunLog.js +14 -0
- package/dist/lib/projectIdentity.js +48 -14
- package/dist/lib/recall.d.ts +4 -3
- package/dist/lib/recall.js +55 -1
- package/dist/lib/recallCommand.js +24 -9
- package/dist/lib/recallHookCommand.d.ts +30 -0
- package/dist/lib/recallHookCommand.js +85 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1173,9 +1173,16 @@ dreamCmd
|
|
|
1173
1173
|
.option("--force", "Run even if this machine is not the designated dream node")
|
|
1174
1174
|
.option("--scheduled", "Run as the machine-level scheduler (applies night/idle/cooldown gates)")
|
|
1175
1175
|
.option("--json", "Output raw JSON report")
|
|
1176
|
-
.action(async (opts)
|
|
1176
|
+
.action(async function (opts) {
|
|
1177
|
+
// v5.13.1: the bare `gnosys dream` parent declares the same flags, and
|
|
1178
|
+
// commander resolves parent-declared options onto the PARENT during
|
|
1179
|
+
// `dream run --scheduled` — so run's opts arrived empty and every
|
|
1180
|
+
// scheduled invocation (the launchd agent's exact command line) ran as
|
|
1181
|
+
// a MANUAL dream, bypassing the night/idle/dreamworthiness gates.
|
|
1182
|
+
// Merge parent opts (same pattern as `dream log` below).
|
|
1183
|
+
const merged = { ...(this.parent?.opts() ?? {}), ...opts };
|
|
1177
1184
|
const { runDreamCommand } = await import("./lib/dreamCommand.js");
|
|
1178
|
-
await runDreamCommand(
|
|
1185
|
+
await runDreamCommand(merged);
|
|
1179
1186
|
});
|
|
1180
1187
|
// `gnosys dream log` — view recent dream runs from audit_log
|
|
1181
1188
|
dreamCmd
|
|
@@ -1300,6 +1307,15 @@ program
|
|
|
1300
1307
|
const { runRecallCommand } = await import("./lib/recallCommand.js");
|
|
1301
1308
|
await runRecallCommand(query, opts);
|
|
1302
1309
|
});
|
|
1310
|
+
// ─── gnosys recall-hook ──────────────────────────────────────────────────
|
|
1311
|
+
program
|
|
1312
|
+
.command("recall-hook")
|
|
1313
|
+
.description("Claude Code hook entry — reads the hook event JSON from stdin and prints a <gnosys-recall> context block. Wired automatically by gnosys init into UserPromptSubmit + SessionStart.")
|
|
1314
|
+
.option("--limit <n>", "Max memories to inject (default from config)")
|
|
1315
|
+
.action(async (opts) => {
|
|
1316
|
+
const { runRecallHookCommand } = await import("./lib/recallHookCommand.js");
|
|
1317
|
+
await runRecallHookCommand(opts);
|
|
1318
|
+
});
|
|
1303
1319
|
// ─── gnosys audit ────────────────────────────────────────────────────────
|
|
1304
1320
|
program
|
|
1305
1321
|
.command("audit")
|
|
@@ -1848,10 +1864,13 @@ if (!isTestEnv()) {
|
|
|
1848
1864
|
// console.log during boot corrupts the protocol and the host (Grok, Codex,
|
|
1849
1865
|
// etc.) sees the server as [unavailable]. Suppress the nag in serve mode.
|
|
1850
1866
|
const isServeCmd = process.argv.slice(2).some(a => a === "serve");
|
|
1867
|
+
// v5.14.0: recall-hook runs on every Claude Code prompt — keep its
|
|
1868
|
+
// stderr quiet too so hook error logs stay clean.
|
|
1869
|
+
const isHookCmd = process.argv.slice(2).some(a => a === "recall-hook");
|
|
1851
1870
|
// v5.9.3 Phase H: fire on any mismatch (upgrade OR downgrade).
|
|
1852
1871
|
const mismatch = lastVersion !== null && lastVersion !== undefined &&
|
|
1853
1872
|
compareSemver(currentVersion, lastVersion) !== 0;
|
|
1854
|
-
if (mismatch && !isUpgradeCmd && !isSetupSyncCmd && !isServeCmd) {
|
|
1873
|
+
if (mismatch && !isUpgradeCmd && !isSetupSyncCmd && !isServeCmd && !isHookCmd) {
|
|
1855
1874
|
// v5.9.3 Phase H: emit on STDERR (was stdout). Safer invariant per
|
|
1856
1875
|
// deci-045 — stdout is reserved for command output.
|
|
1857
1876
|
const isMajorOrMinor = (() => {
|
package/dist/index.js
CHANGED
|
@@ -2246,7 +2246,7 @@ async function reindexAllStores() {
|
|
|
2246
2246
|
//
|
|
2247
2247
|
// Priority 1 + audience: assistant = hosts inject this before every message.
|
|
2248
2248
|
regResource("gnosys_recall", "gnosys://recall", {
|
|
2249
|
-
description: "
|
|
2249
|
+
description: "Top-memory injection block for hosts that read MCP resources. Returns a <gnosys-recall> block with [[wikilinks]] and relevance scores. NOTE: most hosts (Claude Code, Cursor) do NOT auto-read MCP resources per turn — for true automatic injection use the hooks `gnosys init` installs (UserPromptSubmit/SessionStart → `gnosys recall-hook`). Configure aggressiveness in gnosys.json: recall.aggressive (default: true).",
|
|
2250
2250
|
mimeType: "text/markdown",
|
|
2251
2251
|
annotations: {
|
|
2252
2252
|
audience: ["assistant"],
|
|
@@ -2294,7 +2294,7 @@ regResource("gnosys_recall", "gnosys://recall", {
|
|
|
2294
2294
|
// ─── Tool: gnosys_recall (query-specific fallback) ──────────────────────
|
|
2295
2295
|
// For hosts that don't support MCP Resources, or when the agent wants to
|
|
2296
2296
|
// recall memories for a specific query. The resource above is preferred.
|
|
2297
|
-
regTool("gnosys_recall", "Fast memory recall — inject relevant memories as context. Returns <gnosys-recall> block. In aggressive mode (default), always returns top memories even at medium relevance.
|
|
2297
|
+
regTool("gnosys_recall", "Fast memory recall — inject relevant memories as context. Returns <gnosys-recall> block. In aggressive mode (default), always returns top memories even at medium relevance. A wildcard query ('*') returns the top memories by reinforcement/confidence/recency. Hosts with gnosys hooks installed (via gnosys init) already get this automatically per prompt.", {
|
|
2298
2298
|
query: z
|
|
2299
2299
|
.string()
|
|
2300
2300
|
.describe("What the agent is currently working on. Use keywords. Example: 'auth JWT middleware' or 'database migration schema'"),
|
package/dist/lib/db.d.ts
CHANGED
|
@@ -330,6 +330,12 @@ export declare class GnosysDB {
|
|
|
330
330
|
tags: string;
|
|
331
331
|
content: string;
|
|
332
332
|
}>;
|
|
333
|
+
/**
|
|
334
|
+
* v5.13.1: top active memories for wildcard recall. The gnosys://recall
|
|
335
|
+
* resource asks for "the best memories, no query" — rank by how often a
|
|
336
|
+
* memory proved useful (reinforcement), then confidence, then recency.
|
|
337
|
+
*/
|
|
338
|
+
getTopActiveMemories(limit?: number): DbMemory[];
|
|
333
339
|
/** v5.13.0: memories whose embedding column is NULL (embedding-health check). */
|
|
334
340
|
countMemoriesMissingEmbedding(): number;
|
|
335
341
|
updateEmbedding(id: string, embedding: Buffer): void;
|
package/dist/lib/db.js
CHANGED
|
@@ -1264,6 +1264,15 @@ export class GnosysDB {
|
|
|
1264
1264
|
const lim = limit && limit > 0 ? ` LIMIT ${Math.floor(limit)}` : "";
|
|
1265
1265
|
return this.withRecovery(() => this.prep(`SELECT id, title, relevance, tags, content FROM memories ${where} ORDER BY modified DESC${lim}`).all());
|
|
1266
1266
|
}
|
|
1267
|
+
/**
|
|
1268
|
+
* v5.13.1: top active memories for wildcard recall. The gnosys://recall
|
|
1269
|
+
* resource asks for "the best memories, no query" — rank by how often a
|
|
1270
|
+
* memory proved useful (reinforcement), then confidence, then recency.
|
|
1271
|
+
*/
|
|
1272
|
+
getTopActiveMemories(limit = 15) {
|
|
1273
|
+
return this.withRecovery(() => this.prep(`SELECT ${LEAN_MEMORY_PROJECTION} FROM memories WHERE tier = 'active' AND status = 'active'
|
|
1274
|
+
ORDER BY reinforcement_count DESC, confidence DESC, modified DESC LIMIT ?`).all(limit));
|
|
1275
|
+
}
|
|
1267
1276
|
/** v5.13.0: memories whose embedding column is NULL (embedding-health check). */
|
|
1268
1277
|
countMemoriesMissingEmbedding() {
|
|
1269
1278
|
return this.withRecovery(() => {
|
package/dist/lib/dreamCommand.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GnosysResolver } from "./resolver.js";
|
|
2
2
|
import { GnosysDB } from "./db.js";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
|
-
import { acquireDreamLock, appendDreamRun,
|
|
4
|
+
import { acquireDreamLock, appendDreamRun, countDreamworthyChanges, createSkipRunRecord, getSystemIdleMinutes, isInsideNightWindow, readDreamState, } from "./dreamRunLog.js";
|
|
5
5
|
export async function runDreamCommand(opts) {
|
|
6
6
|
const resolver = new GnosysResolver();
|
|
7
7
|
await resolver.resolve();
|
|
@@ -15,7 +15,14 @@ export async function runDreamCommand(opts) {
|
|
|
15
15
|
const { getMachineId } = await import("./remote.js");
|
|
16
16
|
const storePath = stores[0].path;
|
|
17
17
|
const cfg = await loadConfig(storePath);
|
|
18
|
-
|
|
18
|
+
// v5.13.1: Dream operates on the brain — the central DB — not the
|
|
19
|
+
// project store the command happened to be run from. Opening
|
|
20
|
+
// stores[0].path meant `gnosys dream run` failed with "run gnosys
|
|
21
|
+
// migrate" from any project directory whose local .gnosys has no
|
|
22
|
+
// migrated DB, even though the central DB was fine (and the designation
|
|
23
|
+
// check below already used openCentral). DbClass alias keeps the lazy
|
|
24
|
+
// import pattern.
|
|
25
|
+
const db = DbClass.openCentral();
|
|
19
26
|
if (!db.isAvailable() || !db.isMigrated()) {
|
|
20
27
|
console.error("Dream Mode requires gnosys.db (v2.0). Run 'gnosys migrate' first.");
|
|
21
28
|
process.exit(1);
|
|
@@ -114,7 +121,9 @@ export async function runDreamCommand(opts) {
|
|
|
114
121
|
gates.push({ name: "system-idle", passed: true, details: { idleMinutes } });
|
|
115
122
|
const state = readDreamState();
|
|
116
123
|
const memories = db.getActiveMemories();
|
|
117
|
-
|
|
124
|
+
// v5.13.1: max(date-watermark changes, count delta) — self-heals
|
|
125
|
+
// stale/polluted state files and catches bulk imports/deletions.
|
|
126
|
+
const changed = countDreamworthyChanges(memories, state);
|
|
118
127
|
const lastRunMs = state.lastSuccessfulRunAt ? Date.parse(state.lastSuccessfulRunAt) : 0;
|
|
119
128
|
const hoursSince = lastRunMs ? (Date.now() - lastRunMs) / 3_600_000 : Infinity;
|
|
120
129
|
const enoughMemories = changed >= cfg.dream.minNewMemoriesToDream;
|
|
@@ -107,6 +107,16 @@ export declare function memoryWatermark(memories: DbMemory[]): {
|
|
|
107
107
|
maxModified: string | undefined;
|
|
108
108
|
};
|
|
109
109
|
export declare function countChangedMemoriesSince(memories: DbMemory[], sinceIso?: string): number;
|
|
110
|
+
/**
|
|
111
|
+
* v5.13.1: total change signal for the dreamworthiness gate. The date-only
|
|
112
|
+
* watermark misses bulk imports, deletions, and stale/polluted state files
|
|
113
|
+
* (a test-suite bug wrote lastMemoryCount=5 against 1,200+ memory brains,
|
|
114
|
+
* suppressing scheduled dreaming indefinitely — fixed in v5.13.0, but
|
|
115
|
+
* already-polluted machines need the gate itself to self-heal). A mismatch
|
|
116
|
+
* between the stored memory count and reality IS change — take the max of
|
|
117
|
+
* both signals.
|
|
118
|
+
*/
|
|
119
|
+
export declare function countDreamworthyChanges(memories: DbMemory[], state: DreamState): number;
|
|
110
120
|
export declare function isInsideNightWindow(now: Date, schedule: DreamConfig["schedule"]): boolean;
|
|
111
121
|
export declare function getSystemIdleMinutes(): number | null;
|
|
112
122
|
export declare function createSkipRunRecord(input: {
|
package/dist/lib/dreamRunLog.js
CHANGED
|
@@ -175,6 +175,20 @@ export function countChangedMemoriesSince(memories, sinceIso) {
|
|
|
175
175
|
return memories.length;
|
|
176
176
|
return memories.filter((m) => Date.parse(m.modified || m.created) > sinceMs).length;
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* v5.13.1: total change signal for the dreamworthiness gate. The date-only
|
|
180
|
+
* watermark misses bulk imports, deletions, and stale/polluted state files
|
|
181
|
+
* (a test-suite bug wrote lastMemoryCount=5 against 1,200+ memory brains,
|
|
182
|
+
* suppressing scheduled dreaming indefinitely — fixed in v5.13.0, but
|
|
183
|
+
* already-polluted machines need the gate itself to self-heal). A mismatch
|
|
184
|
+
* between the stored memory count and reality IS change — take the max of
|
|
185
|
+
* both signals.
|
|
186
|
+
*/
|
|
187
|
+
export function countDreamworthyChanges(memories, state) {
|
|
188
|
+
const byDate = countChangedMemoriesSince(memories, state.lastMemoryMaxModified);
|
|
189
|
+
const countDelta = state.lastMemoryCount == null ? 0 : Math.abs(memories.length - state.lastMemoryCount);
|
|
190
|
+
return Math.max(byDate, countDelta);
|
|
191
|
+
}
|
|
178
192
|
export function isInsideNightWindow(now, schedule) {
|
|
179
193
|
const start = schedule.startHour;
|
|
180
194
|
const end = schedule.endHour;
|
|
@@ -202,29 +202,61 @@ export async function configureClaudeCode(projectDir) {
|
|
|
202
202
|
catch {
|
|
203
203
|
// No settings yet
|
|
204
204
|
}
|
|
205
|
-
//
|
|
205
|
+
// v5.14.0: the hook command is `gnosys recall-hook` — it reads the hook
|
|
206
|
+
// event JSON from stdin (per the Claude Code hooks contract) and prints
|
|
207
|
+
// a <gnosys-recall> block, which Claude Code adds to the model context.
|
|
208
|
+
// Pre-5.14 installs wrote `gnosys recall --query ... --projectRoot ...`,
|
|
209
|
+
// options that never existed — the hook failed silently on every
|
|
210
|
+
// session start. Detect and heal that shape below.
|
|
211
|
+
const hookCommand = "gnosys recall-hook 2>/dev/null || true";
|
|
206
212
|
const gnosysHook = {
|
|
207
213
|
type: "command",
|
|
208
|
-
command:
|
|
214
|
+
command: hookCommand,
|
|
209
215
|
timeout: 10,
|
|
210
216
|
};
|
|
211
|
-
|
|
217
|
+
const isGnosysHookCmd = (h) => typeof h.command === "string" &&
|
|
218
|
+
(h.command.includes("gnosys recall-hook") ||
|
|
219
|
+
// the exact broken pre-5.14 shape only — a user's own `gnosys recall
|
|
220
|
+
// <topic>` hook must not count as "already configured"
|
|
221
|
+
h.command.includes("gnosys recall --query"));
|
|
222
|
+
const isBrokenLegacyCmd = (h) => typeof h.command === "string" && h.command.includes("gnosys recall --query");
|
|
223
|
+
// Merge into existing hooks without clobbering other entries
|
|
212
224
|
const hooks = (settings.hooks || {});
|
|
225
|
+
let changed = false;
|
|
226
|
+
// Heal the broken legacy command wherever it appears
|
|
227
|
+
for (const eventName of Object.keys(hooks)) {
|
|
228
|
+
for (const entry of (hooks[eventName] || [])) {
|
|
229
|
+
const entryHooks = (entry.hooks || []);
|
|
230
|
+
for (const h of entryHooks) {
|
|
231
|
+
if (isBrokenLegacyCmd(h)) {
|
|
232
|
+
h.command = hookCommand;
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// SessionStart: top memories at startup/resume/compact
|
|
213
239
|
const sessionStartEntries = (hooks.SessionStart || []);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return entryHooks.some((h) => typeof h.command === "string" && h.command.includes("gnosys recall"));
|
|
218
|
-
});
|
|
219
|
-
if (!hasGnosysHook) {
|
|
220
|
-
sessionStartEntries.push({
|
|
221
|
-
matcher: "startup|resume|compact",
|
|
222
|
-
hooks: [gnosysHook],
|
|
223
|
-
});
|
|
240
|
+
const hasSessionHook = sessionStartEntries.some((entry) => (entry.hooks || []).some(isGnosysHookCmd));
|
|
241
|
+
if (!hasSessionHook) {
|
|
242
|
+
sessionStartEntries.push({ matcher: "startup|resume|compact", hooks: [gnosysHook] });
|
|
224
243
|
hooks.SessionStart = sessionStartEntries;
|
|
244
|
+
changed = true;
|
|
245
|
+
}
|
|
246
|
+
// UserPromptSubmit: per-prompt recall with the prompt text as the query —
|
|
247
|
+
// this is the "automatic memory injection" path (no matcher: always fires)
|
|
248
|
+
const promptEntries = (hooks.UserPromptSubmit || []);
|
|
249
|
+
const hasPromptHook = promptEntries.some((entry) => (entry.hooks || []).some(isGnosysHookCmd));
|
|
250
|
+
if (!hasPromptHook) {
|
|
251
|
+
promptEntries.push({ hooks: [gnosysHook] });
|
|
252
|
+
hooks.UserPromptSubmit = promptEntries;
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
if (changed) {
|
|
225
256
|
settings.hooks = hooks;
|
|
226
257
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
227
258
|
}
|
|
259
|
+
const hasGnosysHook = !changed;
|
|
228
260
|
// v5.8.4: also register the MCP server itself (not just the recall hook),
|
|
229
261
|
// so `gnosys init` is a one-stop shop. Previously the user had to ALSO
|
|
230
262
|
// run `gnosys setup` for agents to actually call gnosys MCP tools.
|
|
@@ -234,7 +266,9 @@ export async function configureClaudeCode(projectDir) {
|
|
|
234
266
|
configured: true,
|
|
235
267
|
filePath: settingsPath,
|
|
236
268
|
details: [
|
|
237
|
-
hasGnosysHook
|
|
269
|
+
hasGnosysHook
|
|
270
|
+
? "Recall hooks already configured"
|
|
271
|
+
: "Recall hooks configured (SessionStart + UserPromptSubmit → gnosys recall-hook)",
|
|
238
272
|
mcpResult.success ? mcpResult.message : `MCP register skipped: ${mcpResult.message}`,
|
|
239
273
|
].join("; "),
|
|
240
274
|
};
|
package/dist/lib/recall.d.ts
CHANGED
|
@@ -62,9 +62,10 @@ interface RecallMemory {
|
|
|
62
62
|
*/
|
|
63
63
|
export declare function recall(query: string, options: {
|
|
64
64
|
limit?: number;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/** Optional in v5.14: callers on the DB fast path (hooks) skip the file-store deps. */
|
|
66
|
+
search?: GnosysSearch;
|
|
67
|
+
resolver?: GnosysResolver;
|
|
68
|
+
storePath?: string;
|
|
68
69
|
traceId?: string;
|
|
69
70
|
recallConfig?: RecallConfig;
|
|
70
71
|
/** v2.0: When provided, recall uses SQLite directly — no filesystem reads */
|
package/dist/lib/recall.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { GnosysArchive } from "./archive.js";
|
|
24
24
|
import { mergeOverlayDiscoverResults, pendingAddToDbMemory, } from "./clientReadOverlay.js";
|
|
25
25
|
import { auditLog } from "./audit.js";
|
|
26
|
+
import { ftsTerms } from "./ftsQuery.js";
|
|
26
27
|
/** Default recall config */
|
|
27
28
|
const DEFAULT_RECALL_CONFIG = {
|
|
28
29
|
aggressive: true,
|
|
@@ -71,6 +72,17 @@ export async function recall(query, options) {
|
|
|
71
72
|
return recallFromDb(query, options.gnosysDb, limit, cfg, options.traceId, options.pendingOverlay);
|
|
72
73
|
}
|
|
73
74
|
// ─── v1.x legacy path (filesystem + search.db) ────────────────────
|
|
75
|
+
// v5.14: DB-only callers (recall-hook) pass no file-store deps; without
|
|
76
|
+
// them and without a central DB there is nothing to search.
|
|
77
|
+
if (!options.search || !options.resolver) {
|
|
78
|
+
return {
|
|
79
|
+
memories: [],
|
|
80
|
+
totalActive: 0,
|
|
81
|
+
totalArchived: 0,
|
|
82
|
+
recallTimeMs: Math.round((performance.now() - start) * 100) / 100,
|
|
83
|
+
aggressive: cfg.aggressive,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
74
86
|
const memories = [];
|
|
75
87
|
// Step 1: Fast keyword search on active memories (FTS5 — sub-10ms)
|
|
76
88
|
const fetchLimit = Math.max(limit * 2, 15);
|
|
@@ -95,7 +107,7 @@ export async function recall(query, options) {
|
|
|
95
107
|
}
|
|
96
108
|
// Step 2: Archive fallback if active results are thin
|
|
97
109
|
let totalArchived = 0;
|
|
98
|
-
if (memories.length < limit) {
|
|
110
|
+
if (memories.length < limit && options.storePath) {
|
|
99
111
|
try {
|
|
100
112
|
const archive = new GnosysArchive(options.storePath);
|
|
101
113
|
if (archive.isAvailable()) {
|
|
@@ -150,6 +162,48 @@ function recallFromDb(query, db, limit, cfg, traceId, pendingOverlay) {
|
|
|
150
162
|
const memories = [];
|
|
151
163
|
// Step 1: FTS5 discover on gnosys.db
|
|
152
164
|
const fetchLimit = Math.max(limit * 2, 15);
|
|
165
|
+
// v5.13.1: wildcard recall. The gnosys://recall resource passes "*"
|
|
166
|
+
// meaning "inject the top memories, no specific query" — but "*" was
|
|
167
|
+
// never a valid FTS5 match-all (it's a syntax error), so the resource
|
|
168
|
+
// returned zero memories since it shipped in v4.0.0. Any query with no
|
|
169
|
+
// searchable terms now serves top active memories ranked by
|
|
170
|
+
// reinforcement, confidence, and recency.
|
|
171
|
+
if (ftsTerms(query).length === 0) {
|
|
172
|
+
const top = db.getTopActiveMemories(fetchLimit);
|
|
173
|
+
for (let i = 0; i < top.length; i++) {
|
|
174
|
+
const mem = top[i];
|
|
175
|
+
memories.push({
|
|
176
|
+
id: mem.id,
|
|
177
|
+
title: mem.title,
|
|
178
|
+
category: mem.category,
|
|
179
|
+
relevance: mem.relevance || "",
|
|
180
|
+
confidence: mem.confidence,
|
|
181
|
+
path: mem.id,
|
|
182
|
+
fromArchive: false,
|
|
183
|
+
snippet: mem.content.substring(0, 300),
|
|
184
|
+
// Rank-derived score, floored above minRelevance (0.4 default) so
|
|
185
|
+
// non-aggressive mode keeps top memories too.
|
|
186
|
+
relevanceScore: Math.max(0.9 - i * 0.02, 0.5),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const wildcardCounts = db.getMemoryCount();
|
|
190
|
+
const result = applyRecallFiltering(memories, top.length, wildcardCounts.archived, limit, cfg, start);
|
|
191
|
+
db.logAudit({
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
operation: "recall",
|
|
194
|
+
memory_id: null,
|
|
195
|
+
details: JSON.stringify({
|
|
196
|
+
query,
|
|
197
|
+
wildcard: true,
|
|
198
|
+
aggressive: cfg.aggressive,
|
|
199
|
+
totalCandidates: memories.length,
|
|
200
|
+
filtered: result.memories.length,
|
|
201
|
+
}),
|
|
202
|
+
duration_ms: Math.round(result.recallTimeMs),
|
|
203
|
+
trace_id: traceId || null,
|
|
204
|
+
});
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
153
207
|
let dbResults = db.discoverFts(query, fetchLimit);
|
|
154
208
|
if (pendingOverlay?.length) {
|
|
155
209
|
dbResults = mergeOverlayDiscoverResults(dbResults, pendingOverlay, query, fetchLimit, (p) => ({
|
|
@@ -70,7 +70,7 @@ export async function runRecallCommand(query, opts) {
|
|
|
70
70
|
}
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
//
|
|
73
|
+
// Default recall path
|
|
74
74
|
const resolver = new GnosysResolver();
|
|
75
75
|
await resolver.resolve();
|
|
76
76
|
const stores = resolver.getStores();
|
|
@@ -91,14 +91,29 @@ export async function runRecallCommand(query, opts) {
|
|
|
91
91
|
// Build search index
|
|
92
92
|
const search = new GnosysSearch(storePath);
|
|
93
93
|
await search.addStoreMemories(stores[0].store);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
// v5.14.0: recall from the central DB (the brain) like the MCP tool
|
|
95
|
+
// does — the CLI previously only searched the project file store, so
|
|
96
|
+
// `gnosys recall` (and the SessionStart hook built on it) returned
|
|
97
|
+
// nothing on DB-only installs. Snapshot-aware via clientReadResolve;
|
|
98
|
+
// falls back to the file-store path when no central DB exists.
|
|
99
|
+
const { resolveClientRead } = await import("./clientReadResolve.js");
|
|
100
|
+
const clientRead = resolveClientRead();
|
|
101
|
+
let result;
|
|
102
|
+
try {
|
|
103
|
+
result = await recall(query, {
|
|
104
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
105
|
+
search,
|
|
106
|
+
resolver,
|
|
107
|
+
storePath,
|
|
108
|
+
traceId: opts.traceId,
|
|
109
|
+
recallConfig,
|
|
110
|
+
gnosysDb: clientRead?.db,
|
|
111
|
+
pendingOverlay: clientRead?.pendingOverlay,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
clientRead?.release();
|
|
116
|
+
}
|
|
102
117
|
if (opts.json) {
|
|
103
118
|
console.log(JSON.stringify(result, null, 2));
|
|
104
119
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gnosys recall-hook` — Claude Code hook entry point (v5.14.0).
|
|
3
|
+
*
|
|
4
|
+
* This is what makes "automatic memory injection" real: Claude Code never
|
|
5
|
+
* auto-reads MCP resources, but its hooks contract
|
|
6
|
+
* (code.claude.com/docs/en/hooks.md) adds a hook's plain stdout to the
|
|
7
|
+
* model context on exit 0. `gnosys init` wires this command into
|
|
8
|
+
* UserPromptSubmit (every prompt, query = the prompt text) and
|
|
9
|
+
* SessionStart (startup/resume/compact, wildcard = top memories).
|
|
10
|
+
*
|
|
11
|
+
* Contract obligations:
|
|
12
|
+
* - FAST: runs on every prompt. DB fast path only — no file stores, no
|
|
13
|
+
* embeddings, no model loads. (~150ms cold including node startup.)
|
|
14
|
+
* - NEVER breaks the prompt: always exit 0; every failure is silent.
|
|
15
|
+
* - Empty stdout when there is nothing to inject (no noise strings).
|
|
16
|
+
* - Output stays well under the 10,000-char hook cap.
|
|
17
|
+
*/
|
|
18
|
+
export type HookEvent = {
|
|
19
|
+
hook_event_name?: string;
|
|
20
|
+
prompt_text?: string;
|
|
21
|
+
/** Older docs name the field `prompt`; accept both. */
|
|
22
|
+
prompt?: string;
|
|
23
|
+
source?: string;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Parse the hook event JSON into a recall query ("*" = top memories). */
|
|
27
|
+
export declare function hookQueryFromStdin(raw: string): string;
|
|
28
|
+
export declare function runRecallHookCommand(opts?: {
|
|
29
|
+
limit?: string;
|
|
30
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gnosys recall-hook` — Claude Code hook entry point (v5.14.0).
|
|
3
|
+
*
|
|
4
|
+
* This is what makes "automatic memory injection" real: Claude Code never
|
|
5
|
+
* auto-reads MCP resources, but its hooks contract
|
|
6
|
+
* (code.claude.com/docs/en/hooks.md) adds a hook's plain stdout to the
|
|
7
|
+
* model context on exit 0. `gnosys init` wires this command into
|
|
8
|
+
* UserPromptSubmit (every prompt, query = the prompt text) and
|
|
9
|
+
* SessionStart (startup/resume/compact, wildcard = top memories).
|
|
10
|
+
*
|
|
11
|
+
* Contract obligations:
|
|
12
|
+
* - FAST: runs on every prompt. DB fast path only — no file stores, no
|
|
13
|
+
* embeddings, no model loads. (~150ms cold including node startup.)
|
|
14
|
+
* - NEVER breaks the prompt: always exit 0; every failure is silent.
|
|
15
|
+
* - Empty stdout when there is nothing to inject (no noise strings).
|
|
16
|
+
* - Output stays well under the 10,000-char hook cap.
|
|
17
|
+
*/
|
|
18
|
+
/** Max chars of the user prompt used as the recall query. */
|
|
19
|
+
const MAX_QUERY_CHARS = 400;
|
|
20
|
+
/** Keep comfortably under Claude Code's 10k hook-output cap. */
|
|
21
|
+
const MAX_OUTPUT_CHARS = 8_000;
|
|
22
|
+
/** Parse the hook event JSON into a recall query ("*" = top memories). */
|
|
23
|
+
export function hookQueryFromStdin(raw) {
|
|
24
|
+
try {
|
|
25
|
+
const evt = JSON.parse(raw);
|
|
26
|
+
const prompt = evt.prompt_text ?? evt.prompt;
|
|
27
|
+
if (typeof prompt === "string" && prompt.trim().length > 0) {
|
|
28
|
+
return prompt.trim().slice(0, MAX_QUERY_CHARS);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Not JSON (manual invocation / future contract change) — wildcard.
|
|
33
|
+
}
|
|
34
|
+
return "*";
|
|
35
|
+
}
|
|
36
|
+
async function readStdin(timeoutMs) {
|
|
37
|
+
if (process.stdin.isTTY)
|
|
38
|
+
return "";
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let data = "";
|
|
41
|
+
const timer = setTimeout(() => resolve(data), timeoutMs);
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
process.stdin.setEncoding("utf8");
|
|
44
|
+
process.stdin.on("data", (chunk) => {
|
|
45
|
+
data += chunk;
|
|
46
|
+
});
|
|
47
|
+
process.stdin.on("end", () => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
resolve(data);
|
|
50
|
+
});
|
|
51
|
+
process.stdin.on("error", () => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
resolve(data);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function runRecallHookCommand(opts = {}) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readStdin(1_000);
|
|
60
|
+
const query = hookQueryFromStdin(raw);
|
|
61
|
+
const { resolveClientRead } = await import("./clientReadResolve.js");
|
|
62
|
+
const clientRead = resolveClientRead();
|
|
63
|
+
if (!clientRead)
|
|
64
|
+
return; // no central DB — inject nothing, exit 0
|
|
65
|
+
try {
|
|
66
|
+
const { recall, formatRecall } = await import("./recall.js");
|
|
67
|
+
const result = await recall(query, {
|
|
68
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
69
|
+
gnosysDb: clientRead.db,
|
|
70
|
+
pendingOverlay: clientRead.pendingOverlay,
|
|
71
|
+
traceId: "claude-code-hook",
|
|
72
|
+
});
|
|
73
|
+
if (result.memories.length === 0)
|
|
74
|
+
return; // empty stdout = no injection
|
|
75
|
+
const block = formatRecall(result);
|
|
76
|
+
process.stdout.write(block.length > MAX_OUTPUT_CHARS ? `${block.slice(0, MAX_OUTPUT_CHARS)}\n</gnosys-recall>\n` : `${block}\n`);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
clientRead.release();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Never fail the user's prompt over memory recall.
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gnosys",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
4
4
|
"description": "Gnosys — Persistent Memory for AI Agents. Sandbox-first runtime, central SQLite brain, federated search, Dream Mode, Web Knowledge Base, Obsidian export.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|