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.
Files changed (78) hide show
  1. package/README.md +13 -1
  2. package/dist/agent-memories/apply.d.ts +1 -1
  3. package/dist/agent-memories/legacy.js +4 -1
  4. package/dist/api.d.ts +2 -0
  5. package/dist/api.js +25 -9
  6. package/dist/audit.d.ts +1 -1
  7. package/dist/audit.js +3 -0
  8. package/dist/autolearn.js +5 -2
  9. package/dist/capture.js +8 -6
  10. package/dist/cli/output.d.ts +3 -0
  11. package/dist/cli/output.js +7 -0
  12. package/dist/cli/projects.d.ts +4 -0
  13. package/dist/cli/projects.js +90 -0
  14. package/dist/cli/shared.js +14 -8
  15. package/dist/cli/sleep.js +5 -3
  16. package/dist/cli.d.ts +356 -0
  17. package/dist/cli.js +1587 -1404
  18. package/dist/compaction-record.js +7 -6
  19. package/dist/config.js +12 -11
  20. package/dist/connectors/github/cli-impl.js +1 -0
  21. package/dist/consolidate.js +16 -4
  22. package/dist/dag.js +11 -9
  23. package/dist/dashboard.js +4 -2
  24. package/dist/db.js +16 -3
  25. package/dist/dedupe.js +1 -1
  26. package/dist/delivery-recorder.js +1 -0
  27. package/dist/doctor.js +24 -0
  28. package/dist/dormant.d.ts +2 -2
  29. package/dist/dormant.js +1 -0
  30. package/dist/embedding-provider.js +3 -2
  31. package/dist/embeddings.js +10 -7
  32. package/dist/extract.js +20 -19
  33. package/dist/graph.js +3 -8
  34. package/dist/handoff.js +3 -0
  35. package/dist/hooks.js +3 -0
  36. package/dist/importers.js +6 -3
  37. package/dist/incidents.js +1 -0
  38. package/dist/judgment.js +5 -2
  39. package/dist/mcp/server.d.ts +5 -0
  40. package/dist/mcp/server.js +32 -13
  41. package/dist/memory.d.ts +5 -3
  42. package/dist/processes.js +1 -0
  43. package/dist/project-identity.d.ts +1 -1
  44. package/dist/project-identity.js +9 -4
  45. package/dist/project-merge.d.ts +52 -0
  46. package/dist/project-merge.js +168 -0
  47. package/dist/raw-archive-mirror-cleanup.js +2 -1
  48. package/dist/recall-scope.d.ts +3 -3
  49. package/dist/recall-scope.js +5 -4
  50. package/dist/recall-trace.d.ts +2 -2
  51. package/dist/recall-trace.js +10 -14
  52. package/dist/refine-llm.js +18 -10
  53. package/dist/rerankers/clef.d.ts +29 -0
  54. package/dist/rerankers/clef.js +223 -0
  55. package/dist/rerankers/cross-encoder.js +6 -4
  56. package/dist/rerankers/index.js +3 -0
  57. package/dist/rerankers/jev.d.ts +14 -1
  58. package/dist/rerankers/jev.js +30 -20
  59. package/dist/rerankers/llm.js +3 -3
  60. package/dist/rerankers/types.d.ts +16 -0
  61. package/dist/same-text.d.ts +2 -0
  62. package/dist/same-text.js +4 -0
  63. package/dist/scheduler.js +1 -0
  64. package/dist/search.js +4 -2
  65. package/dist/secret-detect.js +1 -0
  66. package/dist/server.d.ts +6 -1
  67. package/dist/server.js +38 -15
  68. package/dist/shared.js +21 -19
  69. package/dist/stdin.js +1 -0
  70. package/dist/store.d.ts +33 -1
  71. package/dist/store.js +123 -22
  72. package/dist/token-ledger.js +1 -0
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  76. package/extensions/openclaw-plugin/package.json +1 -1
  77. package/openclaw.plugin.json +1 -1
  78. 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 { redactSecrets } from './secret-detect.js';
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 + redactSecrets(text) }],
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
- layer: Layer.Semantic,
97
- tags,
98
- emotional_valence: fact.valence,
99
- confidence: 'inferred',
100
- source: source.source,
101
- extracted_from: source.id,
102
- scope: source.scope,
103
- // T1 executor check (2026-08-15 hardening pass): same defect as the
104
- // consolidate.ts merge/trace passes — createMemory with no tenantId
105
- // option stamps 'default' (memory.ts:535) regardless of the source
106
- // entry's own tenant. Thread it through so extracted facts land in
107
- // the same tenant as the episodic memory they were extracted from.
108
- tenantId: source.tenantId,
109
- baseHalfLifeDays,
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
- console.error(`storeExtractedFacts: skipped ${rejected} rejected value(s)`);
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
- // SAFETY: this is a best-effort log message only; property access on any
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
- // SAFETY: this is a best-effort log message only; property access on any
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
- console.error(`Warning: imported memory truncated from ${trimmed.length} to 1000 chars`);
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
@@ -48,6 +48,7 @@ function parseLinkedMemoryIds(raw) {
48
48
  return [];
49
49
  }
50
50
  catch {
51
+ // A malformed list column reads as empty instead of failing the incident read.
51
52
  return [];
52
53
  }
53
54
  }
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;
@@ -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
@@ -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 { loadAllEntries, writeEntry, readEntry, listMemoryConflicts, resolveConflict, countCreatedSinceLastSleep } from '../store.js';
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
- console.error(`hippo: no memory store found; created the global store at ${root}. Run \`hippo init\` in a project for a project store.`);
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 loadAllEntries() in this server returns to the caller and is
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
- console.error(`auto-sleep consolidate failed (tenant ${tenantId}): ${err instanceof Error ? err.message : err}`);
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
- const entries = loadAllEntries(hippoRoot, tenantId);
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(loadAllEntries(hippoRoot, tenantId));
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
- return; // skip malformed
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({ jsonrpc: '2.0', id: req.id, error: { code: -32603, message: err?.message ?? 'Internal error' } });
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
- process.stderr.write(`hippo-mcp uncaught: ${err?.message ?? err}\n`);
1193
+ log.error(`mcp uncaught: ${err instanceof Error ? err.message : String(err)}`);
1175
1194
  });
1176
1195
  process.on('unhandledRejection', (err) => {
1177
- process.stderr.write(`hippo-mcp unhandled: ${err instanceof Error ? err.message : String(err)}\n`);
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: MemoryEntry, now?: Date, options?: DecayOptions): number;
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
@@ -91,6 +91,7 @@ function parseSteps(raw) {
91
91
  return [];
92
92
  }
93
93
  catch {
94
+ // Legacy garbage reads back as no steps, per the docblock.
94
95
  return [];
95
96
  }
96
97
  }
@@ -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).
@@ -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 stopDir = opts?.stopDir ? realpathOrResolve(opts.stopDir) : null;
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, stopDir === null ? [] : [stopDir]);
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