hippo-memory 1.52.6 → 1.52.8

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 (55) hide show
  1. package/README.md +57 -34
  2. package/dist/api.d.ts +1 -0
  3. package/dist/api.js +36 -11
  4. package/dist/autolearn.d.ts +1 -4
  5. package/dist/autolearn.js +6 -7
  6. package/dist/capture.d.ts +3 -0
  7. package/dist/capture.js +51 -56
  8. package/dist/churn-git.js +2 -2
  9. package/dist/cli.d.ts +1 -4
  10. package/dist/cli.js +240 -150
  11. package/dist/config.js +8 -1
  12. package/dist/consolidate.d.ts +2 -0
  13. package/dist/consolidate.js +13 -6
  14. package/dist/customer-notes.js +3 -2
  15. package/dist/dag.js +5 -0
  16. package/dist/dashboard.js +4 -0
  17. package/dist/decisions.d.ts +1 -1
  18. package/dist/decisions.js +4 -3
  19. package/dist/extensions/openclaw-plugin/index.js +1 -0
  20. package/dist/extract.js +3 -0
  21. package/dist/graph-recall.js +8 -7
  22. package/dist/half-life-migration.d.ts +14 -5
  23. package/dist/half-life-migration.js +89 -21
  24. package/dist/handoff.d.ts +2 -0
  25. package/dist/hooks.d.ts +4 -4
  26. package/dist/hooks.js +10 -4
  27. package/dist/importers.d.ts +2 -1
  28. package/dist/importers.js +19 -7
  29. package/dist/incidents.d.ts +1 -1
  30. package/dist/incidents.js +4 -3
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +6 -1
  33. package/dist/mcp/server.js +3 -3
  34. package/dist/memory.d.ts +6 -13
  35. package/dist/memory.js +0 -10
  36. package/dist/policies.js +3 -2
  37. package/dist/predictions.js +2 -0
  38. package/dist/processes.js +3 -2
  39. package/dist/project-briefs.js +3 -2
  40. package/dist/secret-detect.js +3 -3
  41. package/dist/shared.d.ts +1 -1
  42. package/dist/shared.js +6 -1
  43. package/dist/skills.js +3 -2
  44. package/dist/store.d.ts +5 -5
  45. package/dist/store.js +25 -13
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/dist-ui/assets/index-BhT8RvO6.js +61 -0
  49. package/dist-ui/index.html +1 -1
  50. package/extensions/openclaw-plugin/index.ts +1 -0
  51. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  52. package/extensions/openclaw-plugin/package.json +1 -1
  53. package/openclaw.plugin.json +1 -1
  54. package/package.json +1 -1
  55. package/dist-ui/assets/index-BgmA7Hwe.js +0 -61
package/dist/importers.js CHANGED
@@ -12,6 +12,7 @@ import { getGlobalRoot, initGlobal } from './shared.js';
12
12
  import { remember, archiveRaw, isPrivateScope } from './api.js';
13
13
  import { openHippoDb, closeHippoDb } from './db.js';
14
14
  import { RejectedValueError, checkRejectionGuard } from './rejection.js';
15
+ import { loadConfig } from './config.js';
15
16
  // ---------------------------------------------------------------------------
16
17
  // Shared core: dedup + write
17
18
  // ---------------------------------------------------------------------------
@@ -27,6 +28,7 @@ export function importEntries(chunks, source, tags, options) {
27
28
  }
28
29
  const existing = loadAllEntries(targetRoot, options.global ? undefined : options.tenantId);
29
30
  const allTags = [...new Set([...tags, ...(options.extraTags ?? [])])];
31
+ const baseHalfLifeDays = loadConfig(targetRoot).defaultHalfLifeDays;
30
32
  let total = 0;
31
33
  let imported = 0;
32
34
  let skipped = 0;
@@ -80,6 +82,7 @@ export function importEntries(chunks, source, tags, options) {
80
82
  source,
81
83
  confidence: 'observed',
82
84
  tenantId: options.global ? undefined : options.tenantId,
85
+ baseHalfLifeDays,
83
86
  });
84
87
  if (options.dryRun) {
85
88
  if (dryRunDb) {
@@ -314,9 +317,16 @@ function parseCursorFile(content) {
314
317
  return parts.map((p) => p.trim()).filter(Boolean);
315
318
  });
316
319
  }
317
- export function importCursor(filePath, options) {
318
- const raw = fs.readFileSync(filePath, 'utf8');
319
- const chunks = parseCursorFile(raw);
320
+ const CURSOR_RULE_FILE = /\.mdc?$/i;
321
+ /** Import `.cursorrules`, one rule file, or a `.cursor/rules` tree of `.mdc` and `.md` rules. */
322
+ export function importCursor(sourcePath, options) {
323
+ const files = fs.statSync(sourcePath).isDirectory()
324
+ ? collectMarkdownFiles(sourcePath, options.hippoRoot, CURSOR_RULE_FILE).map((rel) => path.join(sourcePath, rel))
325
+ : [sourcePath];
326
+ const chunks = files.flatMap((file) => {
327
+ const raw = fs.readFileSync(file, 'utf8');
328
+ return parseCursorFile(CURSOR_RULE_FILE.test(file) ? parseFrontmatter(raw).body : raw);
329
+ });
320
330
  return importEntries(chunks, 'import:cursor', ['imported', 'cursor'], options);
321
331
  }
322
332
  // ---------------------------------------------------------------------------
@@ -521,8 +531,8 @@ function parseWikilinks(body) {
521
531
  }
522
532
  return out;
523
533
  }
524
- /** Recursively collect `*.md` files under `root`, returning paths relative to
525
- * `root` with forward-slash separators (stable artifactRef keys across OSes).
534
+ /** Recursively collect files whose name matches `match` (`*.md` by default) under `root`,
535
+ * as paths relative to `root` with forward-slash separators (stable artifactRef keys across OSes).
526
536
  * Symlinks are not followed. Skips dot-directories (the default `.hippo` store,
527
537
  * `.git`, `.obsidian`, `.trash`) AND the canonicalized Hippo store path during
528
538
  * the walk, so re-importing a vault that CONTAINS the store never ingests its own
@@ -531,7 +541,7 @@ function parseWikilinks(body) {
531
541
  * every run). The root-IS-the-store case is handled one level up in
532
542
  * importVault (a no-op early return), NOT here: returning [] for it would feed
533
543
  * the deletion-sync an empty scan that mass-archives every live row (codex R8). */
534
- function collectMarkdownFiles(root, hippoRoot) {
544
+ function collectMarkdownFiles(root, hippoRoot, match = /\.md$/i) {
535
545
  const out = [];
536
546
  // Canonicalize (realpath) so a non-dot HIPPO_HOME store nested in the vault is
537
547
  // skipped even when hippoRoot is an aliased path (junction / Windows case
@@ -553,7 +563,7 @@ function collectMarkdownFiles(root, hippoRoot) {
553
563
  continue;
554
564
  walk(abs);
555
565
  }
556
- else if (ent.isFile() && ent.name.toLowerCase().endsWith('.md')) {
566
+ else if (ent.isFile() && match.test(ent.name)) {
557
567
  out.push(path.relative(root, abs).split(path.sep).join('/'));
558
568
  }
559
569
  }
@@ -693,6 +703,7 @@ export function importVault(folderPath, options) {
693
703
  let rejected = 0;
694
704
  let archived = 0;
695
705
  const entries = [];
706
+ const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
696
707
  // AT1 P2 fix: dry-run never called remember(), so it never probed
697
708
  // tombstones — every note that reached the write step counted as
698
709
  // `imported` even when a real run would refuse it. Probe (read-only) via
@@ -799,6 +810,7 @@ export function importVault(folderPath, options) {
799
810
  owner: 'agent:vault-import',
800
811
  artifact_ref: artifactRef,
801
812
  tenantId,
813
+ baseHalfLifeDays,
802
814
  });
803
815
  // dryRun preview: count what WOULD import, but make no writes (codex P2).
804
816
  if (!dryRun) {
@@ -63,7 +63,7 @@ export interface ListIncidentsOpts {
63
63
  * inside writeEntry's SAVEPOINT 'write_entry'.
64
64
  *
65
65
  * The memory mirror: tags ['incident', ...extraTags], source 'incident',
66
- * confidence 'verified', half_life INCIDENT_HALF_LIFE_DAYS, content =
66
+ * confidence 'verified', the half-life objectHalfLifeDays picks, content =
67
67
  * "<text>\n\nContext: <context>" when context is given.
68
68
  *
69
69
  * linked_memory_ids are validated BEFORE insert: each must exist in the SAME
package/dist/incidents.js CHANGED
@@ -27,8 +27,9 @@
27
27
  */
28
28
  import { openHippoDb, closeHippoDb } from './db.js';
29
29
  import { writeEntry, assertTenantId } from './store.js';
30
- import { createMemory, Layer, INCIDENT_HALF_LIFE_DAYS } from './memory.js';
30
+ import { createMemory, Layer } from './memory.js';
31
31
  import { appendAuditEvent } from './audit.js';
32
+ import { objectHalfLifeDays } from './half-life-migration.js';
32
33
  export const VALID_INCIDENT_STATES = new Set([
33
34
  'open',
34
35
  'resolved',
@@ -77,7 +78,7 @@ const INCIDENT_COLS = `
77
78
  * inside writeEntry's SAVEPOINT 'write_entry'.
78
79
  *
79
80
  * The memory mirror: tags ['incident', ...extraTags], source 'incident',
80
- * confidence 'verified', half_life INCIDENT_HALF_LIFE_DAYS, content =
81
+ * confidence 'verified', the half-life objectHalfLifeDays picks, content =
81
82
  * "<text>\n\nContext: <context>" when context is given.
82
83
  *
83
84
  * linked_memory_ids are validated BEFORE insert: each must exist in the SAME
@@ -98,9 +99,9 @@ export function saveIncident(hippoRoot, tenantId, opts, actor = 'cli') {
98
99
  layer: Layer.Semantic,
99
100
  confidence: 'verified',
100
101
  source: 'incident',
102
+ baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
101
103
  tenantId,
102
104
  });
103
- mem.half_life_days = INCIDENT_HALF_LIFE_DAYS;
104
105
  const linkInput = opts.linkedMemoryIds ?? [];
105
106
  // Populated inside afterWrite so the linked-id validation, the INSERT, and the
106
107
  // memory write all share one SAVEPOINT.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Hippo public API - re-exports for programmatic use.
3
3
  */
4
- export { MemoryEntry, Layer, EmotionalValence, ConfidenceLevel, DecayOptions, calculateStrength, resolveConfidence, confidenceFacets, type ConfidenceFacets, createMemory, applyOutcome, generateId, computeSchemaFit } from './memory.js';
4
+ import { type CreateMemoryOptions, type MemoryEntry } from './memory.js';
5
+ export { MemoryEntry, Layer, EmotionalValence, ConfidenceLevel, DecayOptions, calculateStrength, resolveConfidence, confidenceFacets, type ConfidenceFacets, applyOutcome, generateId, computeSchemaFit } from './memory.js';
6
+ /** Published signature, so `baseHalfLifeDays` stays optional here; hippo's own writers use the strict one in memory.ts. */
7
+ export declare function createMemory(content: string, options?: Partial<CreateMemoryOptions>): MemoryEntry;
5
8
  export { search, hybridSearch, physicsSearch, markRetrieved, estimateTokens, textOverlap, tokenize, explainMatch, detectTemporalDirection, temporalBoost, computeTemporalRange, SearchResult, MatchExplanation } from './search.js';
6
9
  export { multihopSearch } from './multihop.js';
7
10
  export { graphExpandRecall, MAX_HOPS, DEFAULT_MAX_NEIGHBORS, type GraphExpandOpts } from './graph-recall.js';
package/dist/index.js CHANGED
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Hippo public API - re-exports for programmatic use.
3
3
  */
4
- export { Layer, calculateStrength, resolveConfidence, confidenceFacets, createMemory, applyOutcome, generateId, computeSchemaFit } from './memory.js';
4
+ import { createMemory as createStoreMemory, DEFAULT_HALF_LIFE_DAYS } from './memory.js';
5
+ export { Layer, calculateStrength, resolveConfidence, confidenceFacets, applyOutcome, generateId, computeSchemaFit } from './memory.js';
6
+ /** Published signature, so `baseHalfLifeDays` stays optional here; hippo's own writers use the strict one in memory.ts. */
7
+ export function createMemory(content, options = {}) {
8
+ return createStoreMemory(content, { ...options, baseHalfLifeDays: options.baseHalfLifeDays ?? DEFAULT_HALF_LIFE_DAYS });
9
+ }
5
10
  export { search, hybridSearch, physicsSearch, markRetrieved, estimateTokens, textOverlap, tokenize, explainMatch, detectTemporalDirection, temporalBoost, computeTemporalRange } from './search.js';
6
11
  export { multihopSearch } from './multihop.js';
7
12
  export { graphExpandRecall, MAX_HOPS, DEFAULT_MAX_NEIGHBORS } from './graph-recall.js';
@@ -989,9 +989,9 @@ async function executeTool(name, args, ctx) {
989
989
  // Auto-detect query from git
990
990
  let query = '';
991
991
  try {
992
- const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8' }).trim();
993
- const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8' }).trim();
994
- const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8' }).trim();
992
+ const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
993
+ const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
994
+ const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8', windowsHide: true }).trim();
995
995
  query = [branch, log, diff].filter(Boolean).join(' ');
996
996
  }
997
997
  catch { /* not a git repo */ }
package/dist/memory.d.ts CHANGED
@@ -113,13 +113,6 @@ export interface MemoryEntry {
113
113
  */
114
114
  bm25_score?: number;
115
115
  }
116
- export declare const DECISION_HALF_LIFE_DAYS = 90;
117
- export declare const INCIDENT_HALF_LIFE_DAYS = 90;
118
- export declare const PROCESS_HALF_LIFE_DAYS = 90;
119
- export declare const POLICY_HALF_LIFE_DAYS = 90;
120
- export declare const SKILL_HALF_LIFE_DAYS = 90;
121
- export declare const PROJECT_BRIEF_HALF_LIFE_DAYS = 90;
122
- export declare const CUSTOMER_NOTE_HALF_LIFE_DAYS = 90;
123
116
  /** FE2: tag on a memory whose named file/symbol/script changed after it was stored. */
124
117
  export declare const CHURN_STALE_TAG = "churn-stale";
125
118
  /**
@@ -222,10 +215,7 @@ export declare function resolveConfidence(entry: MemoryEntry, now?: Date): Confi
222
215
  export declare const DEFAULT_HALF_LIFE_DAYS = 365;
223
216
  export declare const AUTO_DELETABLE_SQL = "pinned = 0 AND kind != 'raw'";
224
217
  export declare function canAutoDelete(entry: Pick<MemoryEntry, 'pinned' | 'kind'>): boolean;
225
- /**
226
- * Create a new memory entry with defaults.
227
- */
228
- export declare function createMemory(content: string, options?: {
218
+ export interface CreateMemoryOptions {
229
219
  layer?: Layer;
230
220
  tags?: string[];
231
221
  emotional_valence?: EmotionalValence;
@@ -233,7 +223,8 @@ export declare function createMemory(content: string, options?: {
233
223
  schema_fit?: number;
234
224
  source?: string;
235
225
  confidence?: ConfidenceLevel;
236
- baseHalfLifeDays?: number;
226
+ /** The store's `loadConfig(hippoRoot).defaultHalfLifeDays`; required so no writer falls back to the compiled default by leaving it out. */
227
+ baseHalfLifeDays: number;
237
228
  trace_outcome?: TraceOutcome;
238
229
  source_session_id?: string | null;
239
230
  valid_from?: string;
@@ -245,7 +236,9 @@ export declare function createMemory(content: string, options?: {
245
236
  owner?: string | null;
246
237
  artifact_ref?: string | null;
247
238
  tenantId?: string;
248
- }): MemoryEntry;
239
+ }
240
+ /** Create a new memory entry with defaults. Untyped JavaScript callers that omit the options get the compiled default half-life. */
241
+ export declare function createMemory(content: string, options: CreateMemoryOptions): MemoryEntry;
249
242
  /**
250
243
  * Compute how well new content fits existing knowledge patterns.
251
244
  * Returns 0..1 where:
package/dist/memory.js CHANGED
@@ -11,13 +11,6 @@ export var Layer;
11
11
  Layer["Semantic"] = "semantic";
12
12
  Layer["Trace"] = "trace";
13
13
  })(Layer || (Layer = {}));
14
- export const DECISION_HALF_LIFE_DAYS = 90;
15
- export const INCIDENT_HALF_LIFE_DAYS = 90;
16
- export const PROCESS_HALF_LIFE_DAYS = 90;
17
- export const POLICY_HALF_LIFE_DAYS = 90;
18
- export const SKILL_HALF_LIFE_DAYS = 90;
19
- export const PROJECT_BRIEF_HALF_LIFE_DAYS = 90;
20
- export const CUSTOMER_NOTE_HALF_LIFE_DAYS = 90;
21
14
  /** FE2: tag on a memory whose named file/symbol/script changed after it was stored. */
22
15
  export const CHURN_STALE_TAG = 'churn-stale';
23
16
  // Emotional multipliers from PLAN.md.
@@ -326,9 +319,6 @@ export const AUTO_DELETABLE_SQL = "pinned = 0 AND kind != 'raw'";
326
319
  export function canAutoDelete(entry) {
327
320
  return !entry.pinned && entry.kind !== 'raw';
328
321
  }
329
- /**
330
- * Create a new memory entry with defaults.
331
- */
332
322
  export function createMemory(content, options = {}) {
333
323
  const trimmed = content.trim();
334
324
  if (trimmed.length < 3) {
package/dist/policies.js CHANGED
@@ -39,8 +39,9 @@
39
39
  import { openHippoDb, closeHippoDb } from './db.js';
40
40
  import { writeEntry, assertTenantId } from './store.js';
41
41
  import { markGraphDirty, removeGraphEntitiesForObject } from './graph.js';
42
- import { createMemory, Layer, POLICY_HALF_LIFE_DAYS } from './memory.js';
42
+ import { createMemory, Layer } from './memory.js';
43
43
  import { appendAuditEvent } from './audit.js';
44
+ import { objectHalfLifeDays } from './half-life-migration.js';
44
45
  export const VALID_POLICY_STATES = new Set([
45
46
  'active',
46
47
  'superseded',
@@ -144,9 +145,9 @@ export function savePolicy(hippoRoot, tenantId, opts, actor = 'cli') {
144
145
  layer: Layer.Semantic,
145
146
  confidence: 'verified',
146
147
  source: 'policy',
148
+ baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
147
149
  tenantId,
148
150
  });
149
- mem.half_life_days = POLICY_HALF_LIFE_DAYS;
150
151
  let savedRow;
151
152
  writeEntry(hippoRoot, mem, {
152
153
  actor,
@@ -28,6 +28,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
28
28
  import { writeEntry, assertTenantId } from './store.js';
29
29
  import { createMemory, Layer } from './memory.js';
30
30
  import { appendAuditEvent } from './audit.js';
31
+ import { loadConfig } from './config.js';
31
32
  import { detectForwardClaim } from './forward-claim-detector.js';
32
33
  export const VALID_CLOSURE_STATES = new Set([
33
34
  'open',
@@ -81,6 +82,7 @@ export function savePrediction(hippoRoot, tenantId, opts, actor = 'cli') {
81
82
  source: 'prediction',
82
83
  // SAFETY: 'distilled' is a valid MemoryKind literal (see memory.ts).
83
84
  kind: 'distilled',
85
+ baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
84
86
  tenantId,
85
87
  });
86
88
  // Captured for the return value; populated inside afterWrite hook so the
package/dist/processes.js CHANGED
@@ -32,8 +32,9 @@
32
32
  */
33
33
  import { openHippoDb, closeHippoDb } from './db.js';
34
34
  import { writeEntry, assertTenantId } from './store.js';
35
- import { createMemory, Layer, PROCESS_HALF_LIFE_DAYS } from './memory.js';
35
+ import { createMemory, Layer } from './memory.js';
36
36
  import { appendAuditEvent } from './audit.js';
37
+ import { objectHalfLifeDays } from './half-life-migration.js';
37
38
  export const VALID_PROCESS_STATES = new Set([
38
39
  'active',
39
40
  'superseded',
@@ -155,9 +156,9 @@ export function saveProcess(hippoRoot, tenantId, opts, actor = 'cli') {
155
156
  layer: Layer.Semantic,
156
157
  confidence: 'verified',
157
158
  source: 'process',
159
+ baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
158
160
  tenantId,
159
161
  });
160
- mem.half_life_days = PROCESS_HALF_LIFE_DAYS;
161
162
  let savedRow;
162
163
  writeEntry(hippoRoot, mem, {
163
164
  actor,
@@ -24,8 +24,9 @@
24
24
  import { openHippoDb, closeHippoDb } from './db.js';
25
25
  import { writeEntry, assertTenantId, RECALL_DEFAULT_DENY_SCOPES } from './store.js';
26
26
  import { markGraphDirty, removeGraphEntitiesForObject } from './graph.js';
27
- import { createMemory, Layer, PROJECT_BRIEF_HALF_LIFE_DAYS } from './memory.js';
27
+ import { createMemory, Layer } from './memory.js';
28
28
  import { appendAuditEvent } from './audit.js';
29
+ import { objectHalfLifeDays } from './half-life-migration.js';
29
30
  export const VALID_BRIEF_STATES = new Set([
30
31
  'active',
31
32
  'superseded',
@@ -125,9 +126,9 @@ export function saveProjectBrief(hippoRoot, tenantId, opts, actor = 'cli') {
125
126
  layer: Layer.Semantic,
126
127
  confidence: 'verified',
127
128
  source: 'project_brief',
129
+ baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
128
130
  tenantId,
129
131
  });
130
- mem.half_life_days = PROJECT_BRIEF_HALF_LIFE_DAYS;
131
132
  let savedRow;
132
133
  writeEntry(hippoRoot, mem, {
133
134
  actor,
@@ -39,7 +39,7 @@ const SECRET_PATTERNS = [
39
39
  // sk-... (OpenAI/Anthropic style) and sk_<vendor>_... shapes. Both require
40
40
  // a key-ish noun somewhere in the content (co-occurrence guard) so prose
41
41
  // like "the sk- prefix identifies API keys" without an actual long token
42
- // does not flag, but a stored key ("2chain prod API key sk_keith_...")
42
+ // does not flag, but a stored key ("prod API key sk_live_...")
43
43
  // does.
44
44
  { name: 'sk-style-key', re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
45
45
  { name: 'sk-underscore-key', re: /\bsk_[A-Za-z0-9]+_[A-Za-z0-9_]{6,}\b/ },
@@ -51,7 +51,7 @@ const SECRET_PATTERNS = [
51
51
  // rollout worked well" were verified false positives that would silently
52
52
  // hide real code-lesson memories from ambient context (post-merge
53
53
  // adversarial review, 2026-07-02).
54
- { name: 'secret-assignment', re: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?(?=[A-Za-z0-9_\-+/=]*\d)[A-Za-z0-9_\-+/=]{12,}/i },
54
+ { name: 'secret-assignment', re: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?(?=[A-Za-z0-9_\-+/]*\d)[A-Za-z0-9_\-+/=]{12,}/i },
55
55
  ];
56
56
  const KEYISH_CONTEXT_RE = /key|token|secret|credential|bearer|auth|password/i;
57
57
  const CO_OCCURRENCE_GUARDED = new Set(['sk-style-key', 'sk-underscore-key']);
@@ -59,7 +59,7 @@ const CO_OCCURRENCE_GUARDED = new Set(['sk-style-key', 'sk-underscore-key']);
59
59
  const STRICT_ONLY_PATTERNS = [
60
60
  /\bbearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi,
61
61
  /\bauthorization["']?\s*[:=]\s*["']?basic\s+[A-Za-z0-9+/]{8,}={0,2}/gi,
62
- /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g,
62
+ /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g,
63
63
  ];
64
64
  /**
65
65
  * Scan a memory's tags + content for secret material.
package/dist/shared.d.ts CHANGED
@@ -157,7 +157,7 @@ export declare function autoShare(localRoot: string, options?: {
157
157
  }): MemoryEntry[];
158
158
  /**
159
159
  * Copy all global memories into the local store.
160
- * Skips entries that already exist locally (by ID or by near-identical content).
160
+ * Skips entries that already exist locally, by ID or by text (promote and share copy under a new ID).
161
161
  * Returns the count of newly copied entries.
162
162
  */
163
163
  export declare function syncGlobalToLocal(localRoot: string, globalRoot: string, opts?: {
package/dist/shared.js CHANGED
@@ -457,7 +457,7 @@ export function autoShare(localRoot, options = {}) {
457
457
  }
458
458
  /**
459
459
  * Copy all global memories into the local store.
460
- * Skips entries that already exist locally (by ID or by near-identical content).
460
+ * Skips entries that already exist locally, by ID or by text (promote and share copy under a new ID).
461
461
  * Returns the count of newly copied entries.
462
462
  */
463
463
  export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
@@ -468,6 +468,8 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
468
468
  // the local-root context provides one.
469
469
  const globalEntries = loadAllEntries(globalRoot);
470
470
  const localIndex = loadIndex(localRoot);
471
+ const textKey = (e) => `${e.tenantId}\n${e.content}`;
472
+ const localText = new Set(loadAllEntries(localRoot).map(textKey));
471
473
  // v39 (codex P1-4): syncing down must not re-import what ambient context
472
474
  // excludes - other-project rows are skipped by default and secret rows
473
475
  // are never copied. origin_project is preserved on the copy (writeEntry
@@ -484,6 +486,8 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
484
486
  // Skip if already present by ID
485
487
  if (localIndex.entries[entry.id])
486
488
  continue;
489
+ if (localText.has(textKey(entry)))
490
+ continue;
487
491
  if (detectSecret(entry).flagged)
488
492
  continue;
489
493
  if (!opts.includeCrossProject &&
@@ -499,6 +503,7 @@ export function syncGlobalToLocal(localRoot, globalRoot, opts = {}) {
499
503
  }
500
504
  throw err;
501
505
  }
506
+ localText.add(textKey(entry));
502
507
  count++;
503
508
  }
504
509
  if (rejected > 0) {
package/dist/skills.js CHANGED
@@ -23,8 +23,9 @@
23
23
  */
24
24
  import { openHippoDb, closeHippoDb } from './db.js';
25
25
  import { writeEntry, assertTenantId } from './store.js';
26
- import { createMemory, Layer, SKILL_HALF_LIFE_DAYS } from './memory.js';
26
+ import { createMemory, Layer } from './memory.js';
27
27
  import { appendAuditEvent } from './audit.js';
28
+ import { objectHalfLifeDays } from './half-life-migration.js';
28
29
  export const VALID_SKILL_STATES = new Set([
29
30
  'active',
30
31
  'superseded',
@@ -130,9 +131,9 @@ export function saveSkill(hippoRoot, tenantId, opts, actor = 'cli') {
130
131
  layer: Layer.Semantic,
131
132
  confidence: 'verified',
132
133
  source: 'skill',
134
+ baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
133
135
  tenantId,
134
136
  });
135
- mem.half_life_days = SKILL_HALF_LIFE_DAYS;
136
137
  let savedRow;
137
138
  writeEntry(hippoRoot, mem, {
138
139
  actor,
package/dist/store.d.ts CHANGED
@@ -113,6 +113,8 @@ export declare function initStore(hippoRoot: string): void;
113
113
  export declare function openStore(hippoRoot: string): DatabaseSyncLike;
114
114
  /** `meta` key holding the default half-life base a store's memories are on (src/half-life-migration.ts). */
115
115
  export declare const HALF_LIFE_BASE_META_KEY = "default_half_life_base";
116
+ /** `meta` key set once no memory of a decision, incident or other object sits on the old flat 90 days. */
117
+ export declare const TYPED_HALF_LIFE_META_KEY = "typed_half_life_on_default";
116
118
  /**
117
119
  * Serialize a MemoryEntry to markdown with YAML frontmatter.
118
120
  */
@@ -650,12 +652,10 @@ export declare function loadLatestHandoff(hippoRoot: string, tenantId: string, s
650
652
  export declare function loadHandoffById(hippoRoot: string, tenantId: string, id: number): SessionHandoff | null;
651
653
  /** Stamp the outcome on a session's newest handoff, only if it has none yet. Returns rows changed. */
652
654
  export declare function stampHandoffOutcome(hippoRoot: string, tenantId: string, sessionId: string, outcome: HandoffOutcome): number;
653
- /**
654
- * Auto-write a handoff at session-end from the session's active snapshot (DF1 T3).
655
+ /** Auto-write a handoff at session-end (DF1 T3) from the session's active snapshot, else from `derived`, its transcript state.
655
656
  * @param evidence best-effort git state; outcome comes from the newest session_complete event.
656
- * @returns null unless the snapshot belongs to sessionId and no newer handoff already covers it.
657
- */
658
- export declare function writeSessionEndHandoff(hippoRoot: string, tenantId: string, sessionId: string, evidence: HandoffEvidence | null): SessionHandoff | null;
657
+ * @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
658
+ export declare function writeSessionEndHandoff(hippoRoot: string, tenantId: string, sessionId: string, evidence: HandoffEvidence | null, derived?: Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null): SessionHandoff | null;
659
659
  export declare function transitionCard(db: DatabaseSyncLike, tenantId: string, cardId: string, from: CardStatus[], to: CardStatus, extra?: {
660
660
  setSql?: string;
661
661
  whereSql?: string;
package/dist/store.js CHANGED
@@ -16,6 +16,7 @@ import { isRecallBoostAblated } from './ablation.js';
16
16
  import { rarestPromptTerms, RAREST_TERM_COUNT } from './prompt-recall.js';
17
17
  import { appendAuditEvent } from './audit.js';
18
18
  import { resolveTenantId } from './tenant.js';
19
+ import { redactSecretsStrict } from './secret-detect.js';
19
20
  import { deriveOriginProject, originFromSource, findHippoStoreDir, realpathOrResolve } from './project-identity.js';
20
21
  import { checkRejectionGuard, RejectedValueError, rejectionDigest, normalizeValueForRejection, insertRejectedValue, findRejectedValue, } from './rejection.js';
21
22
  // AT1 (plan §5): resolveConflict's kind-aware loser removal needs
@@ -140,6 +141,8 @@ export function openStore(hippoRoot) {
140
141
  }
141
142
  /** `meta` key holding the default half-life base a store's memories are on (src/half-life-migration.ts). */
142
143
  export const HALF_LIFE_BASE_META_KEY = 'default_half_life_base';
144
+ /** `meta` key set once no memory of a decision, incident or other object sits on the old flat 90 days. */
145
+ export const TYPED_HALF_LIFE_META_KEY = 'typed_half_life_on_default';
143
146
  /**
144
147
  * A store with no memories starts on the current default half-life base, so
145
148
  * `hippo sleep` never migrates it. A store that already holds memories and
@@ -152,6 +155,7 @@ function recordHalfLifeBaseForNewStore(db) {
152
155
  if (db.prepare(`SELECT 1 AS x FROM memories LIMIT 1`).get() !== undefined)
153
156
  return;
154
157
  setMeta(db, HALF_LIFE_BASE_META_KEY, String(DEFAULT_HALF_LIFE_DAYS));
158
+ setMeta(db, TYPED_HALF_LIFE_META_KEY, '1');
155
159
  }
156
160
  function ensureMirrorDirectories(hippoRoot) {
157
161
  const dirs = [
@@ -2143,7 +2147,7 @@ export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
2143
2147
  const result = db.prepare(`
2144
2148
  INSERT INTO task_snapshots(task, summary, next_step, status, source, session_id, scope, tenant_id, created_at, updated_at)
2145
2149
  VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
2146
- `).run(snapshot.task, snapshot.summary, snapshot.next_step, snapshot.source ?? 'cli', snapshot.session_id ?? null, snapshot.scope ?? null, tenantId, now, now);
2150
+ `).run(redactSecretsStrict(snapshot.task), redactSecretsStrict(snapshot.summary), redactSecretsStrict(snapshot.next_step), snapshot.source ?? 'cli', snapshot.session_id ?? null, snapshot.scope ?? null, tenantId, now, now);
2147
2151
  db.exec('COMMIT');
2148
2152
  const id = Number(result.lastInsertRowid ?? 0);
2149
2153
  // SAFETY: row's shape matches the ten columns named in the SELECT above.
@@ -2901,20 +2905,28 @@ export function stampHandoffOutcome(hippoRoot, tenantId, sessionId, outcome) {
2901
2905
  closeHippoDb(db);
2902
2906
  }
2903
2907
  }
2904
- /**
2905
- * Auto-write a handoff at session-end from the session's active snapshot (DF1 T3).
2908
+ /** Auto-write a handoff at session-end (DF1 T3) from the session's active snapshot, else from `derived`, its transcript state.
2906
2909
  * @param evidence best-effort git state; outcome comes from the newest session_complete event.
2907
- * @returns null unless the snapshot belongs to sessionId and no newer handoff already covers it.
2908
- */
2909
- export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence) {
2910
+ * @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
2911
+ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence, derived = null) {
2910
2912
  assertTenantId('writeSessionEndHandoff', tenantId);
2911
- const snapshot = loadActiveTaskSnapshot(hippoRoot, tenantId);
2912
- if (!snapshot || snapshot.session_id !== sessionId)
2913
- return null;
2913
+ const active = loadActiveTaskSnapshot(hippoRoot, tenantId);
2914
2914
  const existing = loadLatestHandoff(hippoRoot, tenantId, sessionId);
2915
- // Strict '>': a same-millisecond tie must not swallow the session's only write (test 6e).
2916
- if (existing && existing.updatedAt > snapshot.updated_at)
2917
- return null;
2915
+ let snapshot;
2916
+ let handoffEvidence = evidence;
2917
+ if (active && active.session_id === sessionId) {
2918
+ // Strict '>': a same-millisecond tie must not swallow the session's only write (test 6e).
2919
+ if (existing && existing.updatedAt > active.updated_at)
2920
+ return null;
2921
+ snapshot = active;
2922
+ }
2923
+ else {
2924
+ // Only an earlier transcript read gives way; `hippo handoff create` and unmarked older handoffs keep winning.
2925
+ if (!derived || (existing && existing.evidence?.derivedFrom !== 'transcript'))
2926
+ return null;
2927
+ snapshot = { ...derived, scope: null };
2928
+ handoffEvidence = { ...evidence, derivedFrom: 'transcript' };
2929
+ }
2918
2930
  const db = openHippoDb(hippoRoot);
2919
2931
  let outcome = null;
2920
2932
  try {
@@ -2944,7 +2956,7 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence)
2944
2956
  nextAction: snapshot.next_step,
2945
2957
  artifacts: carryForward ? existing.artifacts : [],
2946
2958
  scope: snapshot.scope,
2947
- evidence,
2959
+ evidence: handoffEvidence,
2948
2960
  outcome,
2949
2961
  constraints: carryForward ? existing.constraints : undefined,
2950
2962
  targetRuntime: carryForward ? existing.targetRuntime : undefined,
package/dist/version.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export declare const PACKAGE_VERSION = "1.52.6";
19
+ export declare const PACKAGE_VERSION = "1.52.8";
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export declare function compareSemver(a: string, b: string): number;
22
22
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export const PACKAGE_VERSION = '1.52.6';
19
+ export const PACKAGE_VERSION = '1.52.8';
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export function compareSemver(a, b) {
22
22
  const parse = (v) => {