hippo-memory 1.58.0 → 1.60.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 +13 -1
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/legacy.js +4 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.js +25 -9
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +3 -0
- package/dist/autolearn.js +5 -2
- package/dist/capture.js +8 -6
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +7 -0
- package/dist/cli/projects.d.ts +4 -0
- package/dist/cli/projects.js +90 -0
- package/dist/cli/shared.js +14 -8
- package/dist/cli/sleep.js +5 -3
- package/dist/cli.d.ts +356 -0
- package/dist/cli.js +1587 -1404
- package/dist/compaction-record.js +7 -6
- package/dist/config.js +12 -11
- package/dist/connectors/github/cli-impl.js +1 -0
- package/dist/consolidate.js +16 -4
- package/dist/dag.js +11 -9
- package/dist/dashboard.js +4 -2
- package/dist/db.js +16 -3
- package/dist/dedupe.js +1 -1
- package/dist/delivery-recorder.js +1 -0
- package/dist/doctor.js +24 -0
- package/dist/dormant.d.ts +2 -2
- package/dist/dormant.js +1 -0
- package/dist/embedding-provider.js +3 -2
- package/dist/embeddings.js +10 -7
- package/dist/extract.js +20 -19
- package/dist/graph.js +3 -8
- package/dist/handoff.js +3 -0
- package/dist/hooks.js +3 -0
- package/dist/importers.js +6 -3
- package/dist/incidents.js +1 -0
- package/dist/judgment.js +5 -2
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +32 -13
- package/dist/memory.d.ts +5 -3
- package/dist/processes.js +1 -0
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +9 -4
- package/dist/project-merge.d.ts +52 -0
- package/dist/project-merge.js +168 -0
- package/dist/raw-archive-mirror-cleanup.js +2 -1
- package/dist/recall-scope.d.ts +3 -3
- package/dist/recall-scope.js +5 -4
- package/dist/recall-trace.d.ts +2 -2
- package/dist/recall-trace.js +10 -14
- package/dist/refine-llm.js +18 -10
- package/dist/rerankers/clef.d.ts +29 -0
- package/dist/rerankers/clef.js +223 -0
- package/dist/rerankers/cross-encoder.js +6 -4
- package/dist/rerankers/index.js +3 -0
- package/dist/rerankers/jev.d.ts +14 -1
- package/dist/rerankers/jev.js +30 -20
- package/dist/rerankers/llm.js +3 -3
- package/dist/rerankers/types.d.ts +16 -0
- package/dist/same-text.d.ts +2 -0
- package/dist/same-text.js +4 -0
- package/dist/scheduler.js +1 -0
- package/dist/search.js +4 -2
- package/dist/secret-detect.js +1 -0
- package/dist/server.d.ts +6 -1
- package/dist/server.js +38 -15
- package/dist/shared.js +21 -19
- package/dist/stdin.js +1 -0
- package/dist/store.d.ts +33 -1
- package/dist/store.js +123 -22
- package/dist/token-ledger.js +1 -0
- 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 +6 -2
package/dist/extract.js
CHANGED
|
@@ -2,9 +2,10 @@ import { Layer, createMemory } from './memory.js';
|
|
|
2
2
|
import { writeEntry } from './store.js';
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
4
|
import { RejectedValueError } from './rejection.js';
|
|
5
|
-
import {
|
|
5
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
6
6
|
import { fetchWithRetry, llmTimeoutMs } from './http-retry.js';
|
|
7
7
|
import { neverAutoShareTags } from './shared.js';
|
|
8
|
+
import { log } from './log.js';
|
|
8
9
|
function isJsonString(value) {
|
|
9
10
|
return typeof value === 'string';
|
|
10
11
|
}
|
|
@@ -35,7 +36,7 @@ export async function extractFacts(text, opts) {
|
|
|
35
36
|
body: JSON.stringify({
|
|
36
37
|
model,
|
|
37
38
|
max_tokens: 1200,
|
|
38
|
-
messages: [{ role: 'user', content: EXTRACTION_PROMPT +
|
|
39
|
+
messages: [{ role: 'user', content: EXTRACTION_PROMPT + redactSecretsStrict(text) }],
|
|
39
40
|
}),
|
|
40
41
|
}, { timeoutMs: llmTimeoutMs(), fetchFn });
|
|
41
42
|
}
|
|
@@ -92,22 +93,22 @@ export function storeExtractedFacts(hippoRoot, source, facts) {
|
|
|
92
93
|
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
93
94
|
for (const fact of facts) {
|
|
94
95
|
const tags = ['extracted', ...inheritedTags, ...fact.tags];
|
|
95
|
-
const entry = createMemory(fact.content, {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
96
|
+
const entry = { ...createMemory(fact.content, {
|
|
97
|
+
layer: Layer.Semantic,
|
|
98
|
+
tags,
|
|
99
|
+
emotional_valence: fact.valence,
|
|
100
|
+
confidence: 'inferred',
|
|
101
|
+
source: source.source,
|
|
102
|
+
extracted_from: source.id,
|
|
103
|
+
scope: source.scope,
|
|
104
|
+
// T1 executor check (2026-08-15 hardening pass): same defect as the
|
|
105
|
+
// consolidate.ts merge/trace passes — createMemory with no tenantId
|
|
106
|
+
// option stamps 'default' (memory.ts:535) regardless of the source
|
|
107
|
+
// entry's own tenant. Thread it through so extracted facts land in
|
|
108
|
+
// the same tenant as the episodic memory they were extracted from.
|
|
109
|
+
tenantId: source.tenantId,
|
|
110
|
+
baseHalfLifeDays,
|
|
111
|
+
}), origin_project: source.origin_project };
|
|
111
112
|
// AT1 containment: a refusal is per-VALUE — one rejected fact must not
|
|
112
113
|
// drop the rest of this batch. writeEntry has already audited the
|
|
113
114
|
// refusal (reject_refusal) before rethrowing, so skip-and-count here.
|
|
@@ -124,7 +125,7 @@ export function storeExtractedFacts(hippoRoot, source, facts) {
|
|
|
124
125
|
entries.push(entry);
|
|
125
126
|
}
|
|
126
127
|
if (rejected > 0) {
|
|
127
|
-
|
|
128
|
+
log.warn(`storeExtractedFacts: skipped ${rejected} rejected value(s)`);
|
|
128
129
|
}
|
|
129
130
|
return entries;
|
|
130
131
|
}
|
package/dist/graph.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
21
21
|
import { assertTenantId } from './tenant.js';
|
|
22
|
+
import { log } from './log.js';
|
|
22
23
|
const SOURCE_OBJECT_TABLE = {
|
|
23
24
|
decision: 'decisions',
|
|
24
25
|
policy: 'policies',
|
|
@@ -665,10 +666,7 @@ export function markGraphDirty(hippoRoot, tenantId, memoryId) {
|
|
|
665
666
|
catch (err) {
|
|
666
667
|
// Logged (warn) so a SYSTEMATIC enqueue failure surfaces to operators, but
|
|
667
668
|
// swallowed so the already-committed E2 write is never rolled back.
|
|
668
|
-
|
|
669
|
-
// JS value is safe (undefined if absent), preserving the existing lenient
|
|
670
|
-
// formatting even when something non-Error was thrown.
|
|
671
|
-
console.warn(`markGraphDirty: enqueue failed for tenant=${tenantId} memory=${memoryId}: ${err.message}`);
|
|
669
|
+
log.warn(`markGraphDirty: enqueue failed for tenant=${tenantId} memory=${memoryId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
672
670
|
}
|
|
673
671
|
}
|
|
674
672
|
/**
|
|
@@ -707,10 +705,7 @@ export function removeGraphEntitiesForObject(hippoRoot, tenantId, sourceObjectTy
|
|
|
707
705
|
}
|
|
708
706
|
}
|
|
709
707
|
catch (err) {
|
|
710
|
-
|
|
711
|
-
// JS value is safe (undefined if absent), preserving the existing lenient
|
|
712
|
-
// formatting even when something non-Error was thrown.
|
|
713
|
-
console.warn(`removeGraphEntitiesForObject: failed for tenant=${tenantId} ${sourceObjectType}#${sourceObjectId}: ${err.message}`);
|
|
708
|
+
log.warn(`removeGraphEntitiesForObject: failed for tenant=${tenantId} ${sourceObjectType}#${sourceObjectId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
714
709
|
}
|
|
715
710
|
}
|
|
716
711
|
/**
|
package/dist/handoff.js
CHANGED
|
@@ -25,6 +25,7 @@ export function rowToSessionHandoff(row) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
catch {
|
|
28
|
+
// A corrupt column degrades to empty instead of hiding the whole handoff.
|
|
28
29
|
artifacts = [];
|
|
29
30
|
}
|
|
30
31
|
let constraints = [];
|
|
@@ -35,6 +36,7 @@ export function rowToSessionHandoff(row) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
catch {
|
|
39
|
+
// Same degrade-to-empty rule as artifacts.
|
|
38
40
|
constraints = [];
|
|
39
41
|
}
|
|
40
42
|
let evidence = null;
|
|
@@ -43,6 +45,7 @@ export function rowToSessionHandoff(row) {
|
|
|
43
45
|
evidence = row.evidence_json ? JSON.parse(row.evidence_json) : null;
|
|
44
46
|
}
|
|
45
47
|
catch {
|
|
48
|
+
// Same degrade rule as artifacts: bad evidence reads as none.
|
|
46
49
|
evidence = null;
|
|
47
50
|
}
|
|
48
51
|
return {
|
package/dist/hooks.js
CHANGED
|
@@ -183,6 +183,7 @@ function readCodexWrapperMetadata() {
|
|
|
183
183
|
return parsed;
|
|
184
184
|
}
|
|
185
185
|
catch {
|
|
186
|
+
// Unreadable metadata is treated like the missing-file case above.
|
|
186
187
|
return null;
|
|
187
188
|
}
|
|
188
189
|
}
|
|
@@ -191,6 +192,7 @@ function readTextFile(filePath) {
|
|
|
191
192
|
return fs.readFileSync(filePath, 'utf8');
|
|
192
193
|
}
|
|
193
194
|
catch {
|
|
195
|
+
// Callers treat an unreadable file as absent.
|
|
194
196
|
return null;
|
|
195
197
|
}
|
|
196
198
|
}
|
|
@@ -879,6 +881,7 @@ export function uninstallJsonHooks(target) {
|
|
|
879
881
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
880
882
|
}
|
|
881
883
|
catch {
|
|
884
|
+
// Never rewrite a settings file we cannot parse; report nothing uninstalled.
|
|
882
885
|
return false;
|
|
883
886
|
}
|
|
884
887
|
if (!isJsonObject(settings) || !isJsonObject(settings.hooks))
|
package/dist/importers.js
CHANGED
|
@@ -14,6 +14,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
14
14
|
import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
15
15
|
import { loadConfig } from './config.js';
|
|
16
16
|
import { vetSecrets } from './secret-detect.js';
|
|
17
|
+
import { log } from './log.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Shared core: dedup + write
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -48,7 +49,7 @@ export function importEntries(chunks, source, tags, options) {
|
|
|
48
49
|
const trimmed = vetSecrets(original, allTags, true).content;
|
|
49
50
|
const wasRedacted = trimmed !== original;
|
|
50
51
|
if (trimmed.length > 1000) {
|
|
51
|
-
|
|
52
|
+
log.warn(`imported memory truncated from ${trimmed.length} to 1000 chars`);
|
|
52
53
|
}
|
|
53
54
|
const chunk = trimmed.slice(0, 1000);
|
|
54
55
|
// Skip empty or too-short chunks
|
|
@@ -584,7 +585,8 @@ function realpathOrResolve(p) {
|
|
|
584
585
|
try {
|
|
585
586
|
return fs.realpathSync.native(p);
|
|
586
587
|
}
|
|
587
|
-
catch {
|
|
588
|
+
catch (err) {
|
|
589
|
+
log.debug(`import: realpath fell back to resolve for ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
588
590
|
return path.resolve(p);
|
|
589
591
|
}
|
|
590
592
|
}
|
|
@@ -890,7 +892,8 @@ function parseJsonArrayLoose(value) {
|
|
|
890
892
|
const parsed = JSON.parse(value);
|
|
891
893
|
return Array.isArray(parsed) ? parsed.filter(isJsonString) : [];
|
|
892
894
|
}
|
|
893
|
-
catch {
|
|
895
|
+
catch (err) {
|
|
896
|
+
log.debug(`import: unreadable tags_json read as no tags: ${err instanceof Error ? err.message : String(err)}`);
|
|
894
897
|
return [];
|
|
895
898
|
}
|
|
896
899
|
}
|
package/dist/incidents.js
CHANGED
package/dist/judgment.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Regex picks WHAT is a candidate; it cannot say what is worth keeping, so
|
|
3
3
|
* every captured memory currently lands on a flat schema_fit of 0.5. */
|
|
4
4
|
import { fetchWithRetry } from './http-retry.js';
|
|
5
|
+
import { log } from './log.js';
|
|
5
6
|
const ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
|
|
6
7
|
const DEFAULT_MODEL = 'jev-1.13.0';
|
|
7
8
|
const MAX_CONCURRENCY = 8;
|
|
@@ -66,7 +67,8 @@ async function post(content, opts) {
|
|
|
66
67
|
}),
|
|
67
68
|
}, { timeoutMs: JUDGE_TIMEOUT_MS, fetchFn: opts.fetcher });
|
|
68
69
|
}
|
|
69
|
-
catch {
|
|
70
|
+
catch (err) {
|
|
71
|
+
log.debug(`judge: request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
72
|
return null;
|
|
71
73
|
}
|
|
72
74
|
return res.ok ? res : null;
|
|
@@ -87,7 +89,8 @@ export async function judge(content, opts) {
|
|
|
87
89
|
// optional-chained and range-checked before use, so a lie here returns null.
|
|
88
90
|
data = await res.json();
|
|
89
91
|
}
|
|
90
|
-
catch {
|
|
92
|
+
catch (err) {
|
|
93
|
+
log.debug(`judge: unreadable response: ${err instanceof Error ? err.message : String(err)}`);
|
|
91
94
|
return null;
|
|
92
95
|
}
|
|
93
96
|
const durable = data.answers?.durable?.noul;
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -25,8 +25,13 @@ interface McpResponse {
|
|
|
25
25
|
error?: {
|
|
26
26
|
code: number;
|
|
27
27
|
message: string;
|
|
28
|
+
data?: {
|
|
29
|
+
requestId: string;
|
|
30
|
+
};
|
|
28
31
|
};
|
|
29
32
|
}
|
|
33
|
+
/** JSON-RPC reply for a request that threw: typed API errors keep their text; anything else is logged and answered generically. */
|
|
34
|
+
export declare function mcpErrorResponse<E>(id: McpResponse['id'], err: E, requestId?: string): McpResponse;
|
|
30
35
|
export type { McpRequest, McpResponse };
|
|
31
36
|
/**
|
|
32
37
|
* Optional execution context threaded from a non-stdio transport. When the
|
package/dist/mcp/server.js
CHANGED
|
@@ -9,14 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { INTERNAL_ERROR_MESSAGE, mapApiError } from '../http-util.js';
|
|
14
|
+
import { log } from '../log.js';
|
|
12
15
|
import { createMemory, Layer, calculateStrength, } from '../memory.js';
|
|
13
16
|
import { fitBudget, estimateTokens } from '../search.js';
|
|
14
17
|
import { evalNow } from '../ablation.js';
|
|
15
|
-
import {
|
|
18
|
+
import { loadStrengthRows, loadTextsHoldingWords, writeEntry, readEntry, listMemoryConflicts, resolveConflict, countCreatedSinceLastSleep } from '../store.js';
|
|
16
19
|
import { shareMemory, listPeers, getGlobalRoot, initGlobal } from '../shared.js';
|
|
17
20
|
import { consolidate } from '../consolidate.js';
|
|
18
21
|
import { fetchGitLog, extractLessons, partitionLessons, isGitRepo } from '../autolearn.js';
|
|
19
|
-
import { dropHeldCopies, duplicateKey, storedTextKeys } from '../same-text.js';
|
|
22
|
+
import { dropHeldCopies, duplicateKey, longestWord, storedTextKeys } from '../same-text.js';
|
|
20
23
|
import { loadConfig } from '../config.js';
|
|
21
24
|
import { confidenceLabel } from '../memory.js';
|
|
22
25
|
import { resolveTenantId } from '../tenant.js';
|
|
@@ -50,6 +53,18 @@ export function findHippoRoot(cwd = process.cwd(), opts) {
|
|
|
50
53
|
const global = getGlobalRoot();
|
|
51
54
|
return fs.existsSync(global) ? global : null;
|
|
52
55
|
}
|
|
56
|
+
/** JSON-RPC reply for a request that threw: typed API errors keep their text; anything else is logged and answered generically. */
|
|
57
|
+
export function mcpErrorResponse(id, err, requestId = randomUUID()) {
|
|
58
|
+
const { status, message } = mapApiError(err);
|
|
59
|
+
if (status !== 500)
|
|
60
|
+
return { jsonrpc: '2.0', id, error: { code: -32603, message } };
|
|
61
|
+
log.error(`mcp request failed: ${err instanceof Error ? err.message : String(err)}`, { requestId });
|
|
62
|
+
return {
|
|
63
|
+
jsonrpc: '2.0',
|
|
64
|
+
id,
|
|
65
|
+
error: { code: -32603, message: `${INTERNAL_ERROR_MESSAGE} (request id ${requestId})`, data: { requestId } },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
53
68
|
/**
|
|
54
69
|
* The api-layer actor for a tool call. Stdio (no ctx) is the local operator
|
|
55
70
|
* and runs as admin; over HTTP the transport's authenticated role is used, so
|
|
@@ -477,7 +492,7 @@ function resolveClientKey(ctx) {
|
|
|
477
492
|
function createGlobalStoreOnFirstRun() {
|
|
478
493
|
initGlobal();
|
|
479
494
|
const root = getGlobalRoot();
|
|
480
|
-
|
|
495
|
+
log.warn(`no memory store found; created the global store at ${root}. Run \`hippo init\` in a project for a project store.`);
|
|
481
496
|
return root;
|
|
482
497
|
}
|
|
483
498
|
// ── Token ledger (ROADMAP TE0) ──
|
|
@@ -524,7 +539,7 @@ async function executeTool(name, args, ctx) {
|
|
|
524
539
|
// resolve tenant from HIPPO_TENANT.
|
|
525
540
|
const hippoRoot = ctx?.hippoRoot ?? findHippoRoot() ?? createGlobalStoreOnFirstRun();
|
|
526
541
|
const config = loadConfig(hippoRoot);
|
|
527
|
-
// A5: every
|
|
542
|
+
// A5: every store read in this server returns to the caller and is
|
|
528
543
|
// tenant-isolated. Resolved once per tool call: prefer the transport's
|
|
529
544
|
// ctx.tenantId so an HTTP Bearer for tenant B doesn't drop to HIPPO_TENANT.
|
|
530
545
|
const tenantId = ctx?.tenantId ?? resolveTenantId({});
|
|
@@ -866,7 +881,7 @@ async function executeTool(name, args, ctx) {
|
|
|
866
881
|
// Fire-and-forget (never block the response); an unhandled rejection would kill the server, so log it.
|
|
867
882
|
consolidate(hippoRoot)
|
|
868
883
|
.catch((err) => {
|
|
869
|
-
|
|
884
|
+
log.error(`auto-sleep consolidate failed (tenant ${tenantId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
870
885
|
})
|
|
871
886
|
.finally(() => autoSleepInFlight.delete(hippoRoot));
|
|
872
887
|
}
|
|
@@ -922,7 +937,8 @@ async function executeTool(name, args, ctx) {
|
|
|
922
937
|
+ formatMemories(result.entries);
|
|
923
938
|
}
|
|
924
939
|
case 'hippo_status': {
|
|
925
|
-
|
|
940
|
+
// Every row counts toward the averages, so this scans the store, but without its text.
|
|
941
|
+
const entries = loadStrengthRows(hippoRoot, tenantId);
|
|
926
942
|
const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
927
943
|
let atRisk = 0;
|
|
928
944
|
let totalStrength = 0;
|
|
@@ -965,7 +981,7 @@ async function executeTool(name, args, ctx) {
|
|
|
965
981
|
let added = 0;
|
|
966
982
|
let skipped = 0;
|
|
967
983
|
let rejected = 0;
|
|
968
|
-
const keys = storedTextKeys(
|
|
984
|
+
const keys = storedTextKeys(loadTextsHoldingWords(hippoRoot, tenantId, lessons.map(longestWord)));
|
|
969
985
|
for (const lesson of lessons) {
|
|
970
986
|
if (keys.has(duplicateKey(lesson))) {
|
|
971
987
|
skipped++;
|
|
@@ -1138,17 +1154,20 @@ function dispatch(body) {
|
|
|
1138
1154
|
req = JSON.parse(body);
|
|
1139
1155
|
}
|
|
1140
1156
|
catch {
|
|
1141
|
-
|
|
1157
|
+
log.debug('mcp: skipped a frame that is not valid JSON');
|
|
1158
|
+
return;
|
|
1142
1159
|
}
|
|
1143
1160
|
if (!req.method)
|
|
1144
1161
|
return;
|
|
1145
1162
|
if (req.method.startsWith('notifications/')) {
|
|
1146
|
-
handleMcpRequest(req).catch(() => {
|
|
1163
|
+
handleMcpRequest(req).catch((err) => {
|
|
1164
|
+
log.error(`mcp notification ${req.method} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1165
|
+
});
|
|
1147
1166
|
return;
|
|
1148
1167
|
}
|
|
1149
1168
|
handleMcpRequest(req).then((resp) => { if (resp)
|
|
1150
1169
|
send(resp); }).catch((err) => {
|
|
1151
|
-
send(
|
|
1170
|
+
send(mcpErrorResponse(req.id, err));
|
|
1152
1171
|
});
|
|
1153
1172
|
}
|
|
1154
1173
|
/**
|
|
@@ -1171,10 +1190,10 @@ export function startStdioLoop() {
|
|
|
1171
1190
|
});
|
|
1172
1191
|
process.stdin.on('end', () => process.exit(0));
|
|
1173
1192
|
process.on('uncaughtException', (err) => {
|
|
1174
|
-
|
|
1193
|
+
log.error(`mcp uncaught: ${err instanceof Error ? err.message : String(err)}`);
|
|
1175
1194
|
});
|
|
1176
1195
|
process.on('unhandledRejection', (err) => {
|
|
1177
|
-
|
|
1196
|
+
log.error(`mcp unhandled: ${err instanceof Error ? err.message : String(err)}`);
|
|
1178
1197
|
});
|
|
1179
1198
|
}
|
|
1180
1199
|
// Auto-start when invoked as the main module (node dist/mcp/server.js or via
|
|
@@ -1196,7 +1215,7 @@ const isMainModule = (() => {
|
|
|
1196
1215
|
return import.meta.url === mainUrl || import.meta.url === `file:///${argv1.replace(/\\/g, '/')}`;
|
|
1197
1216
|
}
|
|
1198
1217
|
catch {
|
|
1199
|
-
return false;
|
|
1218
|
+
return false; // an unreadable argv means this file was imported, not run; never start the stdio loop then
|
|
1200
1219
|
}
|
|
1201
1220
|
})();
|
|
1202
1221
|
if (isMainModule) {
|
package/dist/memory.d.ts
CHANGED
|
@@ -135,19 +135,21 @@ export declare function _resetLossAversionRatioCacheForTests(): void;
|
|
|
135
135
|
* Modulates effective half-life: memories with consistent positive outcomes
|
|
136
136
|
* decay slower; consistent negative outcomes decay faster.
|
|
137
137
|
*/
|
|
138
|
-
export declare function calculateRewardFactor(entry: MemoryEntry): number;
|
|
138
|
+
export declare function calculateRewardFactor(entry: Pick<MemoryEntry, 'outcome_positive' | 'outcome_negative'>): number;
|
|
139
139
|
/**
|
|
140
140
|
* Net wrongness: bad outcome marks past good ones, never below zero.
|
|
141
141
|
* Strength halves per unit (capped at 3) and recall stops strengthening
|
|
142
142
|
* the memory, so a correction outranks pinning, error tags and heavy recall.
|
|
143
143
|
*/
|
|
144
|
-
export declare function netWrong(entry: MemoryEntry): number;
|
|
144
|
+
export declare function netWrong(entry: Pick<MemoryEntry, 'outcome_positive' | 'outcome_negative'>): number;
|
|
145
145
|
/**
|
|
146
146
|
* Options for decay basis.
|
|
147
147
|
* - clock: wall-clock time (default pre-v0.15)
|
|
148
148
|
* - session: decay by sleep cycle count (for intermittent agents)
|
|
149
149
|
* - adaptive: auto-scale half-life by session frequency (default v0.15+)
|
|
150
150
|
*/
|
|
151
|
+
/** What calculateStrength reads, so a caller can score a row without loading its text. */
|
|
152
|
+
export type StrengthInputs = Pick<MemoryEntry, 'pinned' | 'created' | 'last_retrieved' | 'half_life_days' | 'retrieval_count' | 'emotional_valence' | 'outcome_positive' | 'outcome_negative'>;
|
|
151
153
|
export interface DecayOptions {
|
|
152
154
|
decayBasis?: 'clock' | 'session' | 'adaptive';
|
|
153
155
|
/** Average interval between sleep cycles, in days. Used by 'adaptive' and 'session' modes. */
|
|
@@ -166,7 +168,7 @@ export interface DecayOptions {
|
|
|
166
168
|
*
|
|
167
169
|
* Pinned memories skip time decay; being marked wrong still fades them (netWrong).
|
|
168
170
|
*/
|
|
169
|
-
export declare function calculateStrength(entry:
|
|
171
|
+
export declare function calculateStrength(entry: StrengthInputs, now?: Date, options?: DecayOptions): number;
|
|
170
172
|
/**
|
|
171
173
|
* Derive half-life based on signals, as per PLAN.md table.
|
|
172
174
|
*/
|
package/dist/processes.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* directory is the project root; if none exists, the nearest ancestor
|
|
8
8
|
* containing `.git` (directory or worktree file).
|
|
9
9
|
* - The user home directory is NEVER a project, even though it contains the
|
|
10
|
-
* global store at `~/.hippo`. Reaching home ends the walk.
|
|
10
|
+
* global store at `~/.hippo`. Reaching home or the temp root (inside home on Windows) ends the walk.
|
|
11
11
|
* - A directory with no marker anywhere up the walk is NOT a project: it
|
|
12
12
|
* resolves to the user-global identity (empty name), so memories written
|
|
13
13
|
* there stay injectable everywhere (matches pre-isolation behavior).
|
package/dist/project-identity.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { log } from './log.js';
|
|
4
5
|
const MAX_WALK_DEPTH = 64;
|
|
5
6
|
const identityCache = new Map();
|
|
6
7
|
/** Clear the per-process identity cache (test seam). */
|
|
@@ -15,7 +16,8 @@ export function realpathOrResolve(p) {
|
|
|
15
16
|
try {
|
|
16
17
|
return fs.realpathSync.native(p);
|
|
17
18
|
}
|
|
18
|
-
catch {
|
|
19
|
+
catch (err) {
|
|
20
|
+
log.debug(`project identity: realpath fell back to resolve for ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19
21
|
return path.resolve(p);
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -43,7 +45,8 @@ function isDirectoryAt(p) {
|
|
|
43
45
|
try {
|
|
44
46
|
return fs.statSync(p).isDirectory();
|
|
45
47
|
}
|
|
46
|
-
catch {
|
|
48
|
+
catch (err) {
|
|
49
|
+
log.debug(`project identity: no marker at ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
50
|
return false;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
@@ -60,9 +63,11 @@ export function resolveProjectIdentity(cwd, opts) {
|
|
|
60
63
|
return cached;
|
|
61
64
|
}
|
|
62
65
|
const home = realpathOrResolve(opts?.homeDir ?? os.homedir());
|
|
63
|
-
const
|
|
66
|
+
const stops = [realpathOrResolve(os.tmpdir())];
|
|
67
|
+
if (opts?.stopDir)
|
|
68
|
+
stops.push(realpathOrResolve(opts.stopDir));
|
|
64
69
|
const start = realpathOrResolve(startInput);
|
|
65
|
-
const { hippoRoot, gitRoot, reachedHome } = walkProjectMarkers(start, home,
|
|
70
|
+
const { hippoRoot, gitRoot, reachedHome } = walkProjectMarkers(start, home, stops);
|
|
66
71
|
let identity;
|
|
67
72
|
const root = hippoRoot ?? gitRoot;
|
|
68
73
|
if (root !== null) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DatabaseSyncLike } from './db.js';
|
|
2
|
+
export interface ProjectSummary {
|
|
3
|
+
/** '' is user-global, null is unknown; neither can be merged. */
|
|
4
|
+
readonly origin: string | null;
|
|
5
|
+
readonly live: number;
|
|
6
|
+
readonly imported: number;
|
|
7
|
+
readonly newest: string;
|
|
8
|
+
/** Imported notes whose text another project name also holds: evidence of a duplicate import, never a merge target. */
|
|
9
|
+
readonly copiesElsewhere: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MergeResult {
|
|
12
|
+
readonly from: string;
|
|
13
|
+
readonly into: string;
|
|
14
|
+
/** Imported note copies moved to dormant storage; the next sync under `into` imports the notes still on disk. */
|
|
15
|
+
readonly setAside: readonly string[];
|
|
16
|
+
readonly restamped: readonly string[];
|
|
17
|
+
readonly dormantRestamped: readonly string[];
|
|
18
|
+
readonly compactions: number;
|
|
19
|
+
readonly backup: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface RepairResult {
|
|
22
|
+
readonly toProject: ReadonlyArray<{
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly origin: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Parents in two projects: the merged text blends them, so it goes dormant and the parents re-merge per project. */
|
|
27
|
+
readonly setAside: readonly string[];
|
|
28
|
+
/** No parent left to say which project, or pinned: left as they are, since hiding them would rest on no evidence. */
|
|
29
|
+
readonly untraced: readonly string[];
|
|
30
|
+
readonly backup: string | null;
|
|
31
|
+
}
|
|
32
|
+
/** Live rows per project name, newest write first, with how many of its imported notes are copies held elsewhere. */
|
|
33
|
+
export declare function listProjects(db: DatabaseSyncLike, tenantId: string): ProjectSummary[];
|
|
34
|
+
/** Copies the database before a repair writes, so the audit ids plus this file are the way back. */
|
|
35
|
+
export declare function backupStore(db: DatabaseSyncLike, hippoRoot: string, label: string, now?: Date): string;
|
|
36
|
+
/** Refuses user-global and unknown: they are not projects, and folding them would leak or hide every row. */
|
|
37
|
+
export declare function validateMergeNames(from: string, into: string): string | null;
|
|
38
|
+
/** Folds project `from` into `into` for one tenant in one transaction; a dry run rolls back and writes nothing, mirrors included. */
|
|
39
|
+
export declare function mergeProjects(db: DatabaseSyncLike, hippoRoot: string, opts: {
|
|
40
|
+
tenantId: string;
|
|
41
|
+
from: string;
|
|
42
|
+
into: string;
|
|
43
|
+
dryRun: boolean;
|
|
44
|
+
}): MergeResult;
|
|
45
|
+
/** Reads only, so doctor can call it on a read-only handle: what the repair would do to each user-global merged row. */
|
|
46
|
+
export declare function planUserGlobalRepair(db: DatabaseSyncLike, tenantId: string): Omit<RepairResult, 'backup'>;
|
|
47
|
+
/** Re-tags sleep's merged rows saved as user-global before the fix, by the projects of their parents. */
|
|
48
|
+
export declare function repairUserGlobalMerges(db: DatabaseSyncLike, hippoRoot: string, opts: {
|
|
49
|
+
tenantId: string;
|
|
50
|
+
dryRun: boolean;
|
|
51
|
+
}): RepairResult;
|
|
52
|
+
//# sourceMappingURL=project-merge.d.ts.map
|