hippo-memory 1.67.0 → 1.69.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 +1 -1
- package/dist/agent-memories/copilot.d.ts +3 -1
- package/dist/agent-memories/copilot.js +2 -2
- package/dist/api/auth.d.ts +14 -18
- package/dist/api/auth.js +93 -60
- package/dist/api/context-select.d.ts +10 -0
- package/dist/api/context-select.js +16 -18
- package/dist/api/context.js +101 -60
- package/dist/api/forget.d.ts +2 -2
- package/dist/api/forget.js +11 -0
- package/dist/api/outcome.d.ts +4 -4
- package/dist/api/outcome.js +29 -6
- package/dist/api/promote.d.ts +3 -3
- package/dist/api/promote.js +45 -90
- package/dist/api/remember.d.ts +3 -2
- package/dist/api/remember.js +38 -11
- package/dist/api/types.d.ts +9 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +14 -6
- package/dist/capture/compact.js +21 -2
- package/dist/capture/copilot-transcript.d.ts +3 -0
- package/dist/capture/copilot-transcript.js +8 -1
- package/dist/capture/session-worker.d.ts +13 -0
- package/dist/capture/session-worker.js +194 -0
- package/dist/capture-contract.d.ts +3 -1
- package/dist/capture-contract.js +36 -0
- package/dist/cli/session-hooks.js +90 -22
- package/dist/cli/setup.js +70 -24
- package/dist/cli/usage.js +4 -1
- package/dist/cli.js +1 -1
- package/dist/connectors/github/ingest.js +2 -1
- package/dist/connectors/slack/deletion.js +2 -1
- package/dist/connectors/slack/ingest.js +2 -1
- package/dist/embeddings.d.ts +10 -10
- package/dist/embeddings.js +103 -18
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -0
- package/dist/hooks/copilot.d.ts +50 -8
- package/dist/hooks/copilot.js +116 -30
- package/dist/hooks/json-hooks.d.ts +2 -0
- package/dist/hooks/json-hooks.js +11 -2
- package/dist/hooks/shared.d.ts +7 -1
- package/dist/hooks/shared.js +18 -3
- package/dist/mcp/memory-tools.d.ts +2 -2
- package/dist/mcp/memory-tools.js +9 -8
- package/dist/mcp/request.js +3 -3
- package/dist/memory.d.ts +2 -0
- package/dist/memory.js +7 -0
- package/dist/prompt-recall.d.ts +5 -0
- package/dist/prompt-recall.js +13 -0
- package/dist/raw-archive.d.ts +2 -2
- package/dist/raw-archive.js +5 -3
- package/dist/recall-scope.d.ts +2 -5
- package/dist/recall-scope.js +5 -6
- package/dist/server/routes/admin.js +2 -2
- package/dist/server/routes/memories.js +9 -6
- package/dist/server-detect.js +3 -2
- package/dist/server.d.ts +20 -4
- package/dist/server.js +23 -11
- package/dist/store/audit-event.d.ts +1 -1
- package/dist/store/audit-event.js +1 -1
- package/dist/store/entry-writes-group.d.ts +9 -0
- package/dist/store/entry-writes-group.js +157 -0
- package/dist/store/handoffs.d.ts +5 -2
- package/dist/store/handoffs.js +35 -5
- package/dist/store/key-writes.d.ts +9 -0
- package/dist/store/key-writes.js +30 -0
- package/dist/store/open.d.ts +2 -0
- package/dist/store/open.js +10 -0
- package/dist/store/search-rows.js +3 -9
- package/dist/store/sessions.d.ts +5 -0
- package/dist/store/sessions.js +4 -1
- package/dist/store/vector-writes.d.ts +6 -0
- package/dist/store/vector-writes.js +59 -0
- package/dist/store-port.d.ts +143 -1
- package/dist/store-port.js +48 -12
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/hooks/shared.js
CHANGED
|
@@ -2,6 +2,7 @@ import { envHomeDir, processEnv } from '../env.js';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import { vscodeDataFolders } from '../agent-memories/copilot.js';
|
|
5
6
|
/** JSON-value plain-object check (excludes arrays and null); `constructor` avoids the `typeof` tag banned by anti-slop/no-runtime-typeof. */
|
|
6
7
|
export function isJsonObject(value) {
|
|
7
8
|
return value !== undefined && value !== null && !Array.isArray(value) && value.constructor === Object;
|
|
@@ -36,9 +37,23 @@ export function claudeConfigDir(home = os.homedir(), env = processEnv()) {
|
|
|
36
37
|
export function isCodexPresent(home = homeDir()) {
|
|
37
38
|
return fs.statSync(codexHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
+
/** hippo's own hooks file, in the hooks folder the Copilot CLI reads. */
|
|
41
|
+
export function copilotHooksFile() {
|
|
42
|
+
return path.join(copilotHomeDir(), 'hooks', 'hippo.json');
|
|
43
|
+
}
|
|
44
|
+
/** Where VS Code's agent finds hippo.json: its docs name ~/.copilot/hooks as the user hooks folder, whatever COPILOT_HOME says. */
|
|
45
|
+
export function vscodeUserHooksFile(home = os.homedir()) {
|
|
46
|
+
return path.join(home, '.copilot', 'hooks', 'hippo.json');
|
|
47
|
+
}
|
|
48
|
+
/** The VS Code User folders that exist; Stable and Insiders keep their own, and only an edition the user has run has one. */
|
|
49
|
+
export function vscodeUserDirs(env = processEnv(), home = os.homedir()) {
|
|
50
|
+
return vscodeDataFolders({ env, home, platform: process.platform })
|
|
51
|
+
.map((dir) => path.join(dir, 'User'))
|
|
52
|
+
.filter((dir) => fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory() === true);
|
|
53
|
+
}
|
|
54
|
+
/** Copilot counts as installed only when its config folder or a VS Code User folder exists, so setup never creates ~/.copilot on a machine with neither. */
|
|
40
55
|
export function isCopilotPresent(home = os.homedir()) {
|
|
41
|
-
return fs.statSync(copilotHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
56
|
+
return fs.statSync(copilotHomeDir(home), { throwIfNoEntry: false })?.isDirectory() === true || vscodeUserDirs(processEnv(), home).length > 0;
|
|
42
57
|
}
|
|
43
58
|
/** Codex hashes each hook and skips new or changed ones until the user reviews them in `/hooks`, so the reminder says what they would trust. */
|
|
44
59
|
export const CODEX_TRUST_LINE = "The per-prompt hook sends your pinned memories plus up to 5 that match the prompt. Codex runs hippo's hooks only after you trust them once in `/hooks`.";
|
|
@@ -76,7 +91,7 @@ export function detectInstalledTools() {
|
|
|
76
91
|
{ name: 'opencode', configDir: '~/.config/opencode', detected: exists('.config', 'opencode'), kind: 'plugin', notes: 'installs a TS plugin at ~/.config/opencode/plugins/hippo.ts' },
|
|
77
92
|
{ name: 'openclaw', configDir: '~/.openclaw', detected: exists('.openclaw'), kind: 'plugin', notes: 'install via `openclaw plugins install hippo-memory`' },
|
|
78
93
|
{ name: 'codex', configDir: '~/.codex', detected: isCodexPresent(home), kind: 'wrapper', notes: 'memory hooks in hooks.json, and wraps the detected codex launcher for session-end consolidation' },
|
|
79
|
-
{ name: 'copilot', configDir: copilotHomeDir(), detected: isCopilotPresent(), kind: 'json-hook', notes: 'hooks in hooks/hippo.json, the MCP server in mcp-config.json and a block in copilot-instructions.md' },
|
|
94
|
+
{ name: 'copilot', configDir: copilotHomeDir(), detected: isCopilotPresent(), kind: 'json-hook', notes: 'hooks in hooks/hippo.json, the MCP server in mcp-config.json and a block in copilot-instructions.md; for VS Code, the server in each User mcp.json and prompts/hippo.instructions.md' },
|
|
80
95
|
{ name: 'cursor', configDir: '~/.cursor', detected: exists('.cursor'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
|
|
81
96
|
{ name: 'pi', configDir: '~/.pi', detected: exists('.pi'), kind: 'markdown-instruction', notes: 'no hook API - patches AGENTS.md in the project' },
|
|
82
97
|
];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ToolCall } from './protocol.js';
|
|
2
|
-
export declare function runRememberTool({ args, ctx, hippoRoot, config, tenantId }: ToolCall): string
|
|
3
|
-
export declare function runOutcomeTool({ args, ctx, hippoRoot, tenantId }: ToolCall): string
|
|
2
|
+
export declare function runRememberTool({ args, ctx, hippoRoot, config, tenantId }: ToolCall): Promise<string>;
|
|
3
|
+
export declare function runOutcomeTool({ args, ctx, hippoRoot, tenantId }: ToolCall): Promise<string>;
|
|
4
4
|
export declare function runLearnTool({ args, ctx, hippoRoot, tenantId }: ToolCall): string;
|
|
5
5
|
//# sourceMappingURL=memory-tools.d.ts.map
|
package/dist/mcp/memory-tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Write-side tool handlers: hippo_remember, hippo_outcome and hippo_learn.
|
|
2
2
|
import { log } from '../log.js';
|
|
3
|
-
import {
|
|
3
|
+
import { storeFor } from '../store-port.js';
|
|
4
4
|
import { countCreatedSinceLastSleep } from '../store/index-and-stats.js';
|
|
5
5
|
import { consolidate } from '../consolidate/sleep.js';
|
|
6
6
|
import { outsideRequestStores, runWithRequestStores, scopedBusyWait } from '../db.js';
|
|
@@ -8,7 +8,7 @@ import { resolveTenantId } from '../tenant.js';
|
|
|
8
8
|
import { remember as apiRemember, outcome as apiOutcome, learn as apiLearn, MCP_LEARN } from '../api.js';
|
|
9
9
|
import { mcpActor } from './protocol.js';
|
|
10
10
|
import { lastRecalledIds, autoSleepInFlight, resolveClientKey } from './session-state.js';
|
|
11
|
-
export function runRememberTool({ args, ctx, hippoRoot, config, tenantId }) {
|
|
11
|
+
export async function runRememberTool({ args, ctx, hippoRoot, config, tenantId }) {
|
|
12
12
|
const text = String(args.text || '');
|
|
13
13
|
if (!text)
|
|
14
14
|
return 'No text provided.';
|
|
@@ -28,16 +28,17 @@ export function runRememberTool({ args, ctx, hippoRoot, config, tenantId }) {
|
|
|
28
28
|
actor: mcpActor(ctx),
|
|
29
29
|
store: ctx?.store,
|
|
30
30
|
};
|
|
31
|
-
const result = apiRemember(apiCtx, {
|
|
31
|
+
const result = await apiRemember(apiCtx, {
|
|
32
32
|
content: text,
|
|
33
33
|
tags,
|
|
34
34
|
personal: args.personal === true,
|
|
35
35
|
project: ctx?.project,
|
|
36
36
|
});
|
|
37
|
-
const entry =
|
|
37
|
+
const [entry] = await storeFor(apiCtx).entriesByIds([result.id], tenantId);
|
|
38
38
|
// Auto-sleep: one run per store at a time, triggered by what arrived since the last one.
|
|
39
|
-
// Consolidation is host-wide, so only the host tenant's writes may start it.
|
|
40
|
-
if (ctx?.
|
|
39
|
+
// Consolidation is host-wide, so only the host tenant's writes may start it. It runs on hippo.db, so another store skips it.
|
|
40
|
+
if ((ctx?.store === undefined || ctx.store.kind === 'sqlite') &&
|
|
41
|
+
ctx?.autoSleep !== false &&
|
|
41
42
|
config.autoSleep.enabled &&
|
|
42
43
|
tenantId === resolveTenantId({}) &&
|
|
43
44
|
!autoSleepInFlight.has(hippoRoot) &&
|
|
@@ -57,7 +58,7 @@ export function runRememberTool({ args, ctx, hippoRoot, config, tenantId }) {
|
|
|
57
58
|
const warnings = (result.warnings ?? []).map((w) => `\nWarning: ${w}`).join('');
|
|
58
59
|
return `Remembered [${result.id}] (half-life: ${halfLife}d, tags: ${tagStr})${warnings}`;
|
|
59
60
|
}
|
|
60
|
-
export function runOutcomeTool({ args, ctx, hippoRoot, tenantId }) {
|
|
61
|
+
export async function runOutcomeTool({ args, ctx, hippoRoot, tenantId }) {
|
|
61
62
|
const good = Boolean(args.good);
|
|
62
63
|
const clientKey = resolveClientKey(ctx);
|
|
63
64
|
const ids = lastRecalledIds.get(clientKey) ?? [];
|
|
@@ -73,7 +74,7 @@ export function runOutcomeTool({ args, ctx, hippoRoot, tenantId }) {
|
|
|
73
74
|
actor: mcpActor(ctx),
|
|
74
75
|
store: ctx?.store,
|
|
75
76
|
};
|
|
76
|
-
const { applied } = apiOutcome(apiCtx, ids, good);
|
|
77
|
+
const { applied } = await apiOutcome(apiCtx, ids, good);
|
|
77
78
|
return `Applied ${good ? 'positive' : 'negative'} outcome to ${applied} memories`;
|
|
78
79
|
}
|
|
79
80
|
export function runLearnTool({ args, ctx, hippoRoot, tenantId }) {
|
package/dist/mcp/request.js
CHANGED
|
@@ -64,9 +64,9 @@ const TOOL_HANDLERS = new Map([
|
|
|
64
64
|
['hippo_assemble', { handler: runAssembleTool }],
|
|
65
65
|
['hippo_drill', { handler: runDrillTool }],
|
|
66
66
|
['hippo_predict_baserate', { handler: runPredictBaserateTool }],
|
|
67
|
-
['hippo_remember', { handler: runRememberTool }],
|
|
68
|
-
['hippo_outcome', { handler: runOutcomeTool }],
|
|
69
|
-
['hippo_context', { handler: runContextTool }],
|
|
67
|
+
['hippo_remember', { handler: runRememberTool, storeReady: 'entryWrites' }],
|
|
68
|
+
['hippo_outcome', { handler: runOutcomeTool, storeReady: 'entryWrites' }],
|
|
69
|
+
['hippo_context', { handler: runContextTool, storeReady: 'contextReads' }],
|
|
70
70
|
['hippo_status', { handler: runStatusTool }],
|
|
71
71
|
['hippo_learn', { handler: runLearnTool }],
|
|
72
72
|
['hippo_conflicts', { handler: runConflictsTool }],
|
package/dist/memory.d.ts
CHANGED
|
@@ -190,6 +190,8 @@ export declare function deriveHalfLife(base: number, entry: Partial<MemoryEntry>
|
|
|
190
190
|
* cumulative reward signal, inspired by R-STDP in spiking networks.
|
|
191
191
|
*/
|
|
192
192
|
export declare function applyOutcome(entry: MemoryEntry, good: boolean): MemoryEntry;
|
|
193
|
+
/** applyOutcome as a served outcome applies it: a good outcome reconfirms the entry, so its churn-stale tag goes. */
|
|
194
|
+
export declare function entryAfterOutcome(entry: MemoryEntry, good: boolean): MemoryEntry;
|
|
193
195
|
/**
|
|
194
196
|
* Generate a random memory ID using crypto.randomUUID().
|
|
195
197
|
*/
|
package/dist/memory.js
CHANGED
|
@@ -277,6 +277,13 @@ export function applyOutcome(entry, good) {
|
|
|
277
277
|
updated.strength = calculateStrength(updated);
|
|
278
278
|
return updated;
|
|
279
279
|
}
|
|
280
|
+
/** applyOutcome as a served outcome applies it: a good outcome reconfirms the entry, so its churn-stale tag goes. */
|
|
281
|
+
export function entryAfterOutcome(entry, good) {
|
|
282
|
+
const updated = applyOutcome(entry, good);
|
|
283
|
+
if (!good || !updated.tags.includes(CHURN_STALE_TAG))
|
|
284
|
+
return updated;
|
|
285
|
+
return { ...updated, tags: updated.tags.filter((t) => t !== CHURN_STALE_TAG) };
|
|
286
|
+
}
|
|
280
287
|
/**
|
|
281
288
|
* Generate a random memory ID using crypto.randomUUID().
|
|
282
289
|
*/
|
package/dist/prompt-recall.d.ts
CHANGED
|
@@ -30,4 +30,9 @@ export declare const RAREST_TERM_COUNT = 8;
|
|
|
30
30
|
/** Terms sorted by ascending FTS doc count (rarest first), zero-count terms dropped, ties by term.
|
|
31
31
|
* Pure: `docCount` (an FTS lookup in the caller) does the only I/O. */
|
|
32
32
|
export declare function rarestPromptTerms(terms: Iterable<string>, docCount: (term: string) => number, maxTerms?: number): string[];
|
|
33
|
+
/** The pieces FTS5's unicode61 tokenizer indexes a term as, so `journal_mode` is `journal` and `mode`. */
|
|
34
|
+
export declare function ftsTermParts(term: string): string[];
|
|
35
|
+
/** The rarest terms as a space-joined FTS query, given each part's document count; a term counts as its rarest part,
|
|
36
|
+
* an upper bound on rows holding the whole term, and a term with no parts counts 0, so it is dropped. */
|
|
37
|
+
export declare function rarestFtsQuery(terms: readonly string[], docCount: (part: string) => number, maxTerms?: number): string;
|
|
33
38
|
//# sourceMappingURL=prompt-recall.d.ts.map
|
package/dist/prompt-recall.js
CHANGED
|
@@ -64,4 +64,17 @@ export function rarestPromptTerms(terms, docCount, maxTerms = RAREST_TERM_COUNT)
|
|
|
64
64
|
.slice(0, maxTerms)
|
|
65
65
|
.map((x) => x.t);
|
|
66
66
|
}
|
|
67
|
+
/** The pieces FTS5's unicode61 tokenizer indexes a term as, so `journal_mode` is `journal` and `mode`. */
|
|
68
|
+
export function ftsTermParts(term) {
|
|
69
|
+
return term.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
/** The rarest terms as a space-joined FTS query, given each part's document count; a term counts as its rarest part,
|
|
72
|
+
* an upper bound on rows holding the whole term, and a term with no parts counts 0, so it is dropped. */
|
|
73
|
+
export function rarestFtsQuery(terms, docCount, maxTerms = RAREST_TERM_COUNT) {
|
|
74
|
+
const termCount = (t) => {
|
|
75
|
+
const parts = ftsTermParts(t);
|
|
76
|
+
return parts.length === 0 ? 0 : Math.min(...parts.map(docCount));
|
|
77
|
+
};
|
|
78
|
+
return rarestPromptTerms(terms, termCount, maxTerms).join(' ');
|
|
79
|
+
}
|
|
67
80
|
//# sourceMappingURL=prompt-recall.js.map
|
package/dist/raw-archive.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface ArchiveOpts {
|
|
|
18
18
|
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
19
19
|
* write scope, which nests inside an outer transaction (e.g. batchWriteAndDelete).
|
|
20
20
|
*
|
|
21
|
-
* Throws if the row does not exist or is not `kind='raw'`.
|
|
21
|
+
* Throws if the row does not exist or is not `kind='raw'`. Returns the archived_at it wrote.
|
|
22
22
|
*/
|
|
23
|
-
export declare function archiveRawMemory(db: DatabaseSyncLike, id: string, opts: ArchiveOpts):
|
|
23
|
+
export declare function archiveRawMemory(db: DatabaseSyncLike, id: string, opts: ArchiveOpts): string;
|
|
24
24
|
//# sourceMappingURL=raw-archive.d.ts.map
|
package/dist/raw-archive.js
CHANGED
|
@@ -34,6 +34,7 @@ function moveRowToArchive(db, id, row, opts) {
|
|
|
34
34
|
// Archived text must be unsearchable at commit, so a failed purge fails (and rolls back) the whole archive.
|
|
35
35
|
if (isFtsAvailable(db))
|
|
36
36
|
db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(id);
|
|
37
|
+
return archivedAt;
|
|
37
38
|
}
|
|
38
39
|
// Emit the archive_raw audit event inside the SAVEPOINT so the audit row is
|
|
39
40
|
// committed atomically with the row deletion. Use the row's own tenant_id
|
|
@@ -62,14 +63,14 @@ function auditArchive(db, id, row, opts) {
|
|
|
62
63
|
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
63
64
|
* write scope, which nests inside an outer transaction (e.g. batchWriteAndDelete).
|
|
64
65
|
*
|
|
65
|
-
* Throws if the row does not exist or is not `kind='raw'`.
|
|
66
|
+
* Throws if the row does not exist or is not `kind='raw'`. Returns the archived_at it wrote.
|
|
66
67
|
*/
|
|
67
68
|
export function archiveRawMemory(db, id, opts) {
|
|
68
69
|
const row = loadRawRow(db, id);
|
|
69
70
|
// A SAVEPOINT when already inside a transaction (e.g. batchWriteAndDelete), so a throw here
|
|
70
71
|
// rolls back only this archive.
|
|
71
|
-
withWriteScope(db, 'archive_raw', () => {
|
|
72
|
-
moveRowToArchive(db, id, row, opts);
|
|
72
|
+
return withWriteScope(db, 'archive_raw', () => {
|
|
73
|
+
const archivedAt = moveRowToArchive(db, id, row, opts);
|
|
73
74
|
auditArchive(db, id, row, opts);
|
|
74
75
|
// Archiving a child under a level-2 summary marks the parent dirty, inside the
|
|
75
76
|
// SAVEPOINT so the dirty-mark commits atomically with the archive.
|
|
@@ -84,6 +85,7 @@ export function archiveRawMemory(db, id, opts) {
|
|
|
84
85
|
if (opts.afterArchive) {
|
|
85
86
|
opts.afterArchive(db, id);
|
|
86
87
|
}
|
|
88
|
+
return archivedAt;
|
|
87
89
|
});
|
|
88
90
|
}
|
|
89
91
|
//# sourceMappingURL=raw-archive.js.map
|
package/dist/recall-scope.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export declare function assertClientScope(scope: string | null | undefined): voi
|
|
|
60
60
|
export declare function canTouchScope(actor: {
|
|
61
61
|
owner?: string;
|
|
62
62
|
}, scope: string | null): boolean;
|
|
63
|
+
/** canTouchScope for a caller known by its own personal scope, as a store method receives it. */
|
|
64
|
+
export declare function ownScopeTouches(ownScope: string | null, scope: string | null): boolean;
|
|
63
65
|
/**
|
|
64
66
|
* Recall-side scope filter — the canonical JS half of the recall default-deny
|
|
65
67
|
* rule (the SQL half lives in `loadSearchRows` via `loadRecallSearchEntries`).
|
|
@@ -68,11 +70,6 @@ export declare function canTouchScope(actor: {
|
|
|
68
70
|
* - When `requested` is undefined/empty: default-deny on any
|
|
69
71
|
* `<source>:private:*` scope and on the `RECALL_DEFAULT_DENY_SCOPES`
|
|
70
72
|
* quarantine buckets. `null` and public scopes pass, and so does `ownScope`, the caller's own personal scope.
|
|
71
|
-
*
|
|
72
|
-
* @internal Exported for test parity with
|
|
73
|
-
* `RECALL_DEFAULT_DENY_SCOPES` (single-source-of-truth verification). NOT part
|
|
74
|
-
* of the public API surface; not re-exported from `src/index.ts`. Subject to
|
|
75
|
-
* change without semver bump.
|
|
76
73
|
*/
|
|
77
74
|
export declare function passesScopeFilterForRecall(scope: string | null, requested: string | undefined, ownScope?: string | null): boolean;
|
|
78
75
|
export interface SqlFragment {
|
package/dist/recall-scope.js
CHANGED
|
@@ -83,7 +83,11 @@ export function assertClientScope(scope) {
|
|
|
83
83
|
}
|
|
84
84
|
/** True when `actor` may change or delete a row in `scope`: any non-personal scope, or its own personal one. */
|
|
85
85
|
export function canTouchScope(actor, scope) {
|
|
86
|
-
return
|
|
86
|
+
return ownScopeTouches(personalScopeOf(actor), scope);
|
|
87
|
+
}
|
|
88
|
+
/** canTouchScope for a caller known by its own personal scope, as a store method receives it. */
|
|
89
|
+
export function ownScopeTouches(ownScope, scope) {
|
|
90
|
+
return !isPersonalScope(scope) || scope === ownScope;
|
|
87
91
|
}
|
|
88
92
|
/**
|
|
89
93
|
* Recall-side scope filter — the canonical JS half of the recall default-deny
|
|
@@ -93,11 +97,6 @@ export function canTouchScope(actor, scope) {
|
|
|
93
97
|
* - When `requested` is undefined/empty: default-deny on any
|
|
94
98
|
* `<source>:private:*` scope and on the `RECALL_DEFAULT_DENY_SCOPES`
|
|
95
99
|
* quarantine buckets. `null` and public scopes pass, and so does `ownScope`, the caller's own personal scope.
|
|
96
|
-
*
|
|
97
|
-
* @internal Exported for test parity with
|
|
98
|
-
* `RECALL_DEFAULT_DENY_SCOPES` (single-source-of-truth verification). NOT part
|
|
99
|
-
* of the public API surface; not re-exported from `src/index.ts`. Subject to
|
|
100
|
-
* change without semver bump.
|
|
101
100
|
*/
|
|
102
101
|
export function passesScopeFilterForRecall(scope, requested, ownScope) {
|
|
103
102
|
if (requested !== undefined && requested !== '') {
|
|
@@ -44,7 +44,7 @@ export async function handleCreateAuthKey({ req, res, opts }) {
|
|
|
44
44
|
// bound to the caller's authenticated tenant (ctx.tenantId, resolved
|
|
45
45
|
// from the Bearer token). Forwarding body.tenantId here would let
|
|
46
46
|
// tenant A mint a key for tenant B — see authCreate doc comment.
|
|
47
|
-
const result = authCreate(ctx, {
|
|
47
|
+
const result = await authCreate(ctx, {
|
|
48
48
|
label: labelRaw,
|
|
49
49
|
role,
|
|
50
50
|
});
|
|
@@ -68,7 +68,7 @@ export async function handleListAuthKeys({ req, res, opts, query }) {
|
|
|
68
68
|
const limit = parseListLimit(query.get('limit'), MAX_AUTH_KEYS_PAGE, MAX_AUTH_KEYS_PAGE);
|
|
69
69
|
const after = parseCursor(query.get('cursor'), 'integer', 'integer');
|
|
70
70
|
const ctx = await buildContextWithAuth(req, opts);
|
|
71
|
-
const page = pageOf(authListRows(ctx, { active, limit: limit + 1, after }), limit, (r) => ({ key: r.rowId, id: r.rowId }));
|
|
71
|
+
const page = pageOf(await authListRows(ctx, { active, limit: limit + 1, after }), limit, (r) => ({ key: r.rowId, id: r.rowId }));
|
|
72
72
|
setNextCursorHeader(res, page.nextCursor);
|
|
73
73
|
sendJson(res, 200, page.items.map((r) => r.key));
|
|
74
74
|
return;
|
|
@@ -29,7 +29,7 @@ export async function handleCreateMemory({ req, res, opts }) {
|
|
|
29
29
|
if (personalRaw !== undefined && !isJsonBoolean(personalRaw)) {
|
|
30
30
|
throw new HttpError(400, 'personal must be a boolean');
|
|
31
31
|
}
|
|
32
|
-
const result = remember(ctx, {
|
|
32
|
+
const result = await remember(ctx, {
|
|
33
33
|
content,
|
|
34
34
|
kind: kindRaw,
|
|
35
35
|
scope: getString(body, 'scope'),
|
|
@@ -69,7 +69,7 @@ export async function handleArchiveMemory({ req, res, opts }, archiveMatch) {
|
|
|
69
69
|
if (!reason) {
|
|
70
70
|
throw new HttpError(400, 'reason is required');
|
|
71
71
|
}
|
|
72
|
-
const result = archiveRaw(ctx, archiveMatch.id, reason);
|
|
72
|
+
const result = await archiveRaw(ctx, archiveMatch.id, reason);
|
|
73
73
|
sendJson(res, 200, result);
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
@@ -81,7 +81,7 @@ export async function handleSupersedeMemory({ req, res, opts }, supersedeMatch)
|
|
|
81
81
|
if (!content) {
|
|
82
82
|
throw new HttpError(400, 'content is required');
|
|
83
83
|
}
|
|
84
|
-
const result = supersede(ctx, supersedeMatch.id, content);
|
|
84
|
+
const result = await supersede(ctx, supersedeMatch.id, content);
|
|
85
85
|
sendJson(res, 200, result);
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
@@ -95,7 +95,7 @@ export async function handlePromoteMemory({ req, res, opts }, promoteMatch) {
|
|
|
95
95
|
export async function handleForgetMemory({ req, res, opts }, idMatch) {
|
|
96
96
|
validateIdSegment(idMatch.id, 'memory id');
|
|
97
97
|
const ctx = await buildContextWithAuth(req, opts);
|
|
98
|
-
const result = forget(ctx, idMatch.id);
|
|
98
|
+
const result = await forget(ctx, idMatch.id);
|
|
99
99
|
sendJson(res, 200, result);
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
@@ -132,11 +132,14 @@ export async function handleApplyOutcome({ req, res, opts }) {
|
|
|
132
132
|
ids = idsRaw;
|
|
133
133
|
}
|
|
134
134
|
if (ids !== undefined) {
|
|
135
|
-
const { applied } = outcome(ctx, ids, good);
|
|
135
|
+
const { applied } = await outcome(ctx, ids, good);
|
|
136
136
|
sendJson(res, 200, { applied });
|
|
137
137
|
}
|
|
138
138
|
else {
|
|
139
|
-
|
|
139
|
+
// The last recall's ids live in hippo.db alone, so a client of another store must send the ids its recall returned.
|
|
140
|
+
if (ctx.store !== undefined && ctx.store.kind !== 'sqlite')
|
|
141
|
+
throw new HttpError(501, 'this store keeps no last recall: send the ids your recall returned');
|
|
142
|
+
const result = await outcomeForLastRecall(ctx, good);
|
|
140
143
|
sendJson(res, 200, result);
|
|
141
144
|
}
|
|
142
145
|
return;
|
package/dist/server-detect.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { envHealthProbeMs } from './env.js';
|
|
3
4
|
// Pidfile sits directly inside hippoRoot. `hippoRoot` is the `.hippo`
|
|
4
5
|
// directory itself (the same convention used by api.ts / store.ts /
|
|
5
6
|
// openHippoDb), so this resolves to `${hippoRoot}/server.pid`.
|
|
@@ -8,7 +9,7 @@ const PIDFILE = 'server.pid';
|
|
|
8
9
|
* How long detectServer waits for the `/health` liveness probe before
|
|
9
10
|
* treating the pidfile as stale. Short by design: the probe only fires on
|
|
10
11
|
* the rare path where a pidfile exists and its pid is live, and the target
|
|
11
|
-
* is always loopback, so a healthy server answers well within this bound.
|
|
12
|
+
* is always loopback, so a healthy server answers well within this bound. HIPPO_HEALTH_PROBE_MS overrides it.
|
|
12
13
|
*/
|
|
13
14
|
const HEALTH_PROBE_TIMEOUT_MS = 300;
|
|
14
15
|
/**
|
|
@@ -84,7 +85,7 @@ async function healthMatchesPidfile(hippoRoot, info) {
|
|
|
84
85
|
// returns null WITHOUT unlinking. The pidfile survives for the next probe.
|
|
85
86
|
try {
|
|
86
87
|
const res = await fetch(`${info.url}/health`, {
|
|
87
|
-
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),
|
|
88
|
+
signal: AbortSignal.timeout(envHealthProbeMs() ?? HEALTH_PROBE_TIMEOUT_MS),
|
|
88
89
|
});
|
|
89
90
|
if (!res.ok || !res.body) {
|
|
90
91
|
removePidfile(hippoRoot);
|
package/dist/server.d.ts
CHANGED
|
@@ -11,8 +11,14 @@ export { HttpError } from './http-util.js';
|
|
|
11
11
|
export { promptHookContext, type CallerProject } from './prompt-hook.js';
|
|
12
12
|
export type { JsonValue } from './json.js';
|
|
13
13
|
export { captureSessionTexts, type SessionCaptureRequest, type SessionCaptureResult } from './capture/session-texts.js';
|
|
14
|
-
export { hasGroup, sqliteStore, type HippoStore, type KeyAudit, type KeyRevoke, type RecallSearchArgs, type RecallWrites, type StoreGroup, type StoreGroups, type VectorReads, } from './store-port.js';
|
|
15
|
-
export
|
|
14
|
+
export { hasGroup, sqliteStore, type AmbientCandidateRequest, type ContextReads, type EntryRemoval, type EntryTarget, type EntryWrite, type EntryWrites, type HippoStore, type KeyAudit, type KeyListQuery, type KeyMint, type KeyRevoke, type KeyWrites, type OutcomeWrite, type RawArchive, type RecallSearchArgs, type RecallWrites, type SelfKeyMint, type StoreGroup, type StoreGroups, type SupersedeWrite, type VectorReads, type VectorBackfillQuery, type VectorRowWrite, type VectorWrite, type VectorWriteResult, type VectorWrites, } from './store-port.js';
|
|
15
|
+
export { entryAfterOutcome } from './memory.js';
|
|
16
|
+
export { rejectionDigest, RejectedValueError } from './rejection.js';
|
|
17
|
+
export { ownScopeTouches } from './recall-scope.js';
|
|
18
|
+
export { BadRequestError, ConflictError } from './api-errors.js';
|
|
19
|
+
export type { HippoDbContext, StoreReply } from './api/types.js';
|
|
20
|
+
export type { ApiKeyListItem, ApiKeyListRow, ApiKeyRecord, ListApiKeysOpts, NewApiKey } from './auth.js';
|
|
21
|
+
export type { KeysetPosition } from './keyset.js';
|
|
16
22
|
export type { AppendAuditOpts, AuditEvent, ListAuditAfterOpts } from './audit.js';
|
|
17
23
|
export type { ContinuityBlock } from './api/recall-types.js';
|
|
18
24
|
export type { ActiveGoals, GetActiveGoalsOpts, Goal, GoalRecallLogRow, RetrievalPolicy } from './goals.js';
|
|
@@ -23,12 +29,22 @@ export type { RecallTraceInput } from './recall-trace.js';
|
|
|
23
29
|
export type { StrengthenOptions } from './store/entry-writes.js';
|
|
24
30
|
export type { OriginFilter, RecallScopeFilter, VectorCandidateSpec } from './store/search-rows.js';
|
|
25
31
|
export type { ContinuityKey } from './store/sessions.js';
|
|
32
|
+
export type { SessionEvent, TaskSnapshot } from './store/rows.js';
|
|
33
|
+
export type { SessionHandoff } from './handoff.js';
|
|
34
|
+
export type { AmbientLoadResult, AmbientRecallRequest, ContextCandidateFilter, RecentOrigins } from './store/candidates.js';
|
|
35
|
+
export type { AmbientStoreFilter } from './ambient-store.js';
|
|
36
|
+
export type { AmbientTallies } from './ambient.js';
|
|
26
37
|
export type { TokenUse } from './token-ledger.js';
|
|
27
38
|
export type { EmbeddingIndexState } from './embeddings.js';
|
|
28
39
|
export type { PhysicsParticle } from './physics.js';
|
|
29
40
|
export { StoreBusyError } from './db.js';
|
|
30
|
-
export { decodeVector, EMBEDDING_MODEL_META_KEY, rankVectorRows, type VectorMatch, type VectorRow } from './vector-store.js';
|
|
31
|
-
export { bufferToFloat32 } from './physics-state.js';
|
|
41
|
+
export { decodeVector, EMBEDDING_MODEL_META_KEY, encodeVector, rankVectorRows, type VectorMatch, type VectorRow } from './vector-store.js';
|
|
42
|
+
export { bufferToFloat32, float32ToBuffer } from './physics-state.js';
|
|
43
|
+
export { replacesIndex } from './embeddings.js';
|
|
44
|
+
export { passesScopeFilterForRecall, RECALL_DEFAULT_DENY_SCOPES } from './recall-scope.js';
|
|
45
|
+
export { SECRET_TAGS } from './secret-detect.js';
|
|
46
|
+
export { tallyAmbientEntries } from './ambient.js';
|
|
47
|
+
export { ftsTermParts, rarestFtsQuery } from './prompt-recall.js';
|
|
32
48
|
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
33
49
|
export { authCreate, type AuthCreateOpts, type AuthCreateResult } from './api.js';
|
|
34
50
|
export { isSharedStore } from './config.js';
|
package/dist/server.js
CHANGED
|
@@ -47,10 +47,22 @@ export { promptHookContext } from './prompt-hook.js';
|
|
|
47
47
|
export { captureSessionTexts } from './capture/session-texts.js';
|
|
48
48
|
// An add-on serves from another database by passing serve() its own HippoStore.
|
|
49
49
|
export { hasGroup, sqliteStore, } from './store-port.js';
|
|
50
|
+
// An add-on store's entry writes apply an outcome, guard tombstones and check reach exactly as hippo.db does.
|
|
51
|
+
export { entryAfterOutcome } from './memory.js';
|
|
52
|
+
export { rejectionDigest, RejectedValueError } from './rejection.js';
|
|
53
|
+
export { ownScopeTouches } from './recall-scope.js';
|
|
54
|
+
export { BadRequestError, ConflictError } from './api-errors.js';
|
|
50
55
|
export { StoreBusyError } from './db.js';
|
|
51
|
-
// An add-on store decodes and ranks with hippo.db's own code,
|
|
52
|
-
|
|
53
|
-
export {
|
|
56
|
+
// An add-on store encodes, decodes and ranks vectors and particles with hippo.db's own code, and drops the index by its rule,
|
|
57
|
+
// so both stores keep the same bytes and return the same ids in the same order.
|
|
58
|
+
export { decodeVector, EMBEDDING_MODEL_META_KEY, encodeVector, rankVectorRows } from './vector-store.js';
|
|
59
|
+
export { bufferToFloat32, float32ToBuffer } from './physics-state.js';
|
|
60
|
+
export { replacesIndex } from './embeddings.js';
|
|
61
|
+
// An add-on's ContextReads applies hippo.db's scope, secret, tally and rarest-term rules with the same code.
|
|
62
|
+
export { passesScopeFilterForRecall, RECALL_DEFAULT_DENY_SCOPES } from './recall-scope.js';
|
|
63
|
+
export { SECRET_TAGS } from './secret-detect.js';
|
|
64
|
+
export { tallyAmbientEntries } from './ambient.js';
|
|
65
|
+
export { ftsTermParts, rarestFtsQuery } from './prompt-recall.js';
|
|
54
66
|
// store copy --db writes the marker and reads the old hippo.db under the waiver.
|
|
55
67
|
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
56
68
|
// An add-on's install step mints the first admin key into a store folder it names, which `hippo auth create` cannot reach.
|
|
@@ -96,20 +108,20 @@ const VERSION = PACKAGE_VERSION;
|
|
|
96
108
|
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
97
109
|
/** The /v1 routes in dispatch order; the first entry whose method and path match handles the request. */
|
|
98
110
|
const V1_ROUTES = [
|
|
99
|
-
{ method: 'POST', path: '/v1/memories', handler: handleCreateMemory },
|
|
111
|
+
{ method: 'POST', path: '/v1/memories', storeReady: 'entryWrites', handler: handleCreateMemory },
|
|
100
112
|
{ method: 'GET', path: '/v1/graph', handler: handleGetGraph },
|
|
101
113
|
{ method: 'GET', path: '/v1/memories', storeReady: 'base', handler: handleRecallMemories },
|
|
102
114
|
{ method: 'GET', pattern: '/v1/sessions/:id/assemble', handler: handleAssembleSession },
|
|
103
115
|
{ method: 'GET', pattern: '/v1/recall/drill/:id', handler: handleDrillRecall },
|
|
104
|
-
{ method: 'POST', pattern: '/v1/memories/:id/archive', handler: handleArchiveMemory },
|
|
105
|
-
{ method: 'POST', pattern: '/v1/memories/:id/supersede', handler: handleSupersedeMemory },
|
|
116
|
+
{ method: 'POST', pattern: '/v1/memories/:id/archive', storeReady: 'entryWrites', handler: handleArchiveMemory },
|
|
117
|
+
{ method: 'POST', pattern: '/v1/memories/:id/supersede', storeReady: 'entryWrites', handler: handleSupersedeMemory },
|
|
106
118
|
{ method: 'POST', pattern: '/v1/memories/:id/promote', handler: handlePromoteMemory },
|
|
107
|
-
{ method: 'DELETE', pattern: '/v1/memories/:id', handler: handleForgetMemory },
|
|
108
|
-
{ method: 'POST', path: '/v1/outcome', handler: handleApplyOutcome },
|
|
109
|
-
{ method: 'GET', path: '/v1/context', handler: handleGetContext },
|
|
119
|
+
{ method: 'DELETE', pattern: '/v1/memories/:id', storeReady: 'entryWrites', handler: handleForgetMemory },
|
|
120
|
+
{ method: 'POST', path: '/v1/outcome', storeReady: 'entryWrites', handler: handleApplyOutcome },
|
|
121
|
+
{ method: 'GET', path: '/v1/context', storeReady: 'contextReads', handler: handleGetContext },
|
|
110
122
|
{ method: 'POST', path: '/v1/sleep', handler: handleSleep },
|
|
111
|
-
{ method: 'POST', path: '/v1/auth/keys', handler: handleCreateAuthKey },
|
|
112
|
-
{ method: 'GET', path: '/v1/auth/keys', handler: handleListAuthKeys },
|
|
123
|
+
{ method: 'POST', path: '/v1/auth/keys', storeReady: 'keyWrites', handler: handleCreateAuthKey },
|
|
124
|
+
{ method: 'GET', path: '/v1/auth/keys', storeReady: 'keyWrites', handler: handleListAuthKeys },
|
|
113
125
|
{ method: 'DELETE', pattern: '/v1/auth/keys/:keyId', storeReady: 'keyAudit', handler: handleRevokeAuthKey },
|
|
114
126
|
{ method: 'GET', path: '/v1/quarantine', handler: handleListQuarantine },
|
|
115
127
|
{ method: 'POST', pattern: '/v1/quarantine/:id/approve', handler: handleApproveQuarantine },
|
|
@@ -11,7 +11,7 @@ export declare function audit(db: ReturnType<typeof openHippoDb>, op: AuditOp, t
|
|
|
11
11
|
/**
|
|
12
12
|
* Refusal audit for the rejected-value guard. Written by the
|
|
13
13
|
* transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
|
|
14
|
-
* there, so this lands in a fresh implicit transaction) and
|
|
14
|
+
* there, so this lands in a fresh implicit transaction) and commitSupersede's
|
|
15
15
|
* catch (after its own ROLLBACK) — never inside a scope the caller's own
|
|
16
16
|
* rollback could claw back. Best-effort `audit()` semantics: never throws.
|
|
17
17
|
*/
|
|
@@ -23,7 +23,7 @@ export function audit(db, op, targetId, metadata, actor = 'cli', tenantId) {
|
|
|
23
23
|
/**
|
|
24
24
|
* Refusal audit for the rejected-value guard. Written by the
|
|
25
25
|
* transaction OWNER post-rollback — writeEntry's catch (no outer tx exists
|
|
26
|
-
* there, so this lands in a fresh implicit transaction) and
|
|
26
|
+
* there, so this lands in a fresh implicit transaction) and commitSupersede's
|
|
27
27
|
* catch (after its own ROLLBACK) — never inside a scope the caller's own
|
|
28
28
|
* rollback could claw back. Best-effort `audit()` semantics: never throws.
|
|
29
29
|
*/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
2
|
+
import type { EntryWrites, SupersedeWrite } from '../store-port.js';
|
|
3
|
+
/** Each call on its own handle; mirrors and the forgotten counter follow the commit, so a rolled-back write leaves neither. */
|
|
4
|
+
export declare function sqliteEntryWrites(hippoRoot: string): EntryWrites;
|
|
5
|
+
/** Reach, the CAS on the old row, the successor's insert and the supersede row in one BEGIN IMMEDIATE transaction. Two racing supersedes: one CAS wins. */
|
|
6
|
+
export declare function commitSupersede(db: DatabaseSyncLike, write: SupersedeWrite): void;
|
|
7
|
+
/** A mirror left on disk would bring the archived row back on the next import; on failure the reaper retries, as mirror_cleaned_at stays NULL. */
|
|
8
|
+
export declare function cleanArchivedMirrors(db: DatabaseSyncLike, hippoRoot: string, id: string): void;
|
|
9
|
+
//# sourceMappingURL=entry-writes-group.d.ts.map
|