hippo-memory 1.52.7 → 1.52.9
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 +159 -99
- package/dist/api.d.ts +52 -18
- package/dist/api.js +155 -86
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/autolearn.js +3 -2
- package/dist/capture.d.ts +40 -0
- package/dist/capture.js +141 -119
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +695 -702
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +85 -32
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +4 -0
- package/dist/db.js +6 -6
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +35 -2
- package/dist/dormant.d.ts +4 -0
- package/dist/dormant.js +17 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +13 -10
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +19 -5
- package/dist/hooks.js +131 -30
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +174 -213
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +9 -3
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +11 -3
- package/dist/shared.js +41 -31
- package/dist/store.d.ts +4 -5
- package/dist/store.js +26 -13
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-ui/assets/index-BhT8RvO6.js +61 -0
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/index.ts +1 -0
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
- package/dist-ui/assets/index-BgmA7Hwe.js +0 -61
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Codex prints this line only when a patch applied (apply-patch lib.rs). */
|
|
2
|
+
export declare const PATCH_SUCCESS_LINE = "Success. Updated the following files:";
|
|
3
|
+
/** Paths a patch body adds, deletes, updates or moves to, in patch order; [] when the text holds no patch. */
|
|
4
|
+
export declare function patchPaths(body: string): string[];
|
|
5
|
+
export interface ShellPatch {
|
|
6
|
+
body: string;
|
|
7
|
+
/** The `cd <dir> &&` target, relative to the call's working directory. */
|
|
8
|
+
cd: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** The patch a shell call hands to apply_patch, or null for any other command. */
|
|
11
|
+
export declare function shellPatch(command: readonly string[] | string): ShellPatch | null;
|
|
12
|
+
//# sourceMappingURL=codex-patch.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Files named by a Codex apply_patch call, read the way openai/codex codex-rs/apply-patch reads them
|
|
2
|
+
// (parser.rs for the body, invocation.rs for the shell forms it runs as apply_patch).
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { isStringValue } from './capture.js';
|
|
5
|
+
/** Codex prints this line only when a patch applied (apply-patch lib.rs). */
|
|
6
|
+
export const PATCH_SUCCESS_LINE = 'Success. Updated the following files:';
|
|
7
|
+
const BEGIN_PATCH = '*** Begin Patch';
|
|
8
|
+
const END_PATCH = '*** End Patch';
|
|
9
|
+
const MOVE_TO = '*** Move to: ';
|
|
10
|
+
const FILE_HEADER = /^\*\*\* (Add|Delete|Update) File: (.+)$/;
|
|
11
|
+
/** Paths a patch body adds, deletes, updates or moves to, in patch order; [] when the text holds no patch. */
|
|
12
|
+
export function patchPaths(body) {
|
|
13
|
+
const lines = body.split(/\r?\n/);
|
|
14
|
+
const start = lines.findIndex((l) => l.trim() === BEGIN_PATCH);
|
|
15
|
+
if (start < 0)
|
|
16
|
+
return [];
|
|
17
|
+
const paths = [];
|
|
18
|
+
let updating = false;
|
|
19
|
+
let canMove = false;
|
|
20
|
+
for (const line of lines.slice(start + 1)) {
|
|
21
|
+
// Inside an update hunk a leading space marks a context line, so headers match on trimEnd only there.
|
|
22
|
+
const head = updating ? line.trimEnd() : line.trim();
|
|
23
|
+
if (head === END_PATCH)
|
|
24
|
+
break;
|
|
25
|
+
const header = FILE_HEADER.exec(head);
|
|
26
|
+
if (header) {
|
|
27
|
+
paths.push(header[2].trim());
|
|
28
|
+
updating = header[1] === 'Update';
|
|
29
|
+
canMove = updating;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (canMove && head.startsWith(MOVE_TO))
|
|
33
|
+
paths.push(head.slice(MOVE_TO.length).trim());
|
|
34
|
+
canMove = false;
|
|
35
|
+
}
|
|
36
|
+
return paths.filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
const HEREDOC = /^\s*(?:cd\s+(?<cd>'[^']*'|"[^"]*"|[^\s'"&|;]+)\s+&&\s+)?(?:apply_patch|applypatch)\s+<<\s*(?<q>['"]?)(?<tag>\w+)\k<q>[ \t]*\r?\n(?<body>[\s\S]*?)\r?\n\s*\k<tag>\s*$/;
|
|
39
|
+
/** The script a shell argv runs, for the shells Codex checks for an apply_patch heredoc. */
|
|
40
|
+
function shellScript(argv) {
|
|
41
|
+
const exe = path.basename(argv[0] ?? '').toLowerCase().replace(/\.exe$/, '');
|
|
42
|
+
if ((exe === 'bash' || exe === 'zsh' || exe === 'sh') && argv.length === 3 && (argv[1] === '-lc' || argv[1] === '-c'))
|
|
43
|
+
return argv[2];
|
|
44
|
+
if (exe === 'pwsh' || exe === 'powershell') {
|
|
45
|
+
const rest = argv.slice(1).filter((a) => a.toLowerCase() !== '-noprofile');
|
|
46
|
+
if (rest.length === 2 && rest[0].toLowerCase() === '-command')
|
|
47
|
+
return rest[1];
|
|
48
|
+
}
|
|
49
|
+
if (exe === 'cmd' && argv.length === 3 && argv[1].toLowerCase() === '/c')
|
|
50
|
+
return argv[2];
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/** The patch a shell call hands to apply_patch, or null for any other command. */
|
|
54
|
+
export function shellPatch(command) {
|
|
55
|
+
let script;
|
|
56
|
+
if (isStringValue(command)) {
|
|
57
|
+
script = command;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const exe = path.basename(command[0] ?? '').toLowerCase();
|
|
61
|
+
if (command.length === 2 && (exe === 'apply_patch' || exe === 'applypatch'))
|
|
62
|
+
return { body: command[1], cd: null };
|
|
63
|
+
script = shellScript(command);
|
|
64
|
+
}
|
|
65
|
+
const match = script === null ? null : HEREDOC.exec(script);
|
|
66
|
+
if (!match?.groups)
|
|
67
|
+
return null;
|
|
68
|
+
const cd = match.groups.cd ?? null;
|
|
69
|
+
return { body: match.groups.body, cd: cd === null ? null : cd.replace(/^(['"])(.*)\1$/, '$2') };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=codex-patch.js.map
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -201,8 +201,4 @@ export function loadConfig(hippoRoot) {
|
|
|
201
201
|
return { ...DEFAULT_CONFIG };
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
|
-
export function saveConfig(hippoRoot, config) {
|
|
205
|
-
const configPath = path.join(hippoRoot, 'config.json');
|
|
206
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
207
|
-
}
|
|
208
204
|
//# sourceMappingURL=config.js.map
|
|
@@ -40,7 +40,6 @@ export interface SlackEventEnvelope {
|
|
|
40
40
|
[k: string]: JsonValue;
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
export type SlackInbound = SlackEventEnvelope | SlackUrlVerification;
|
|
44
43
|
export declare function isSlackEventEnvelope(x: JsonValue): x is JsonValue & SlackEventEnvelope;
|
|
45
44
|
export declare function isSlackMessageEvent(x: JsonValue | SlackEventEnvelope['event']): x is SlackMessageEvent;
|
|
46
45
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/consolidate.js
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
import { evalNow, isRecallBoostAblated } from './ablation.js';
|
|
10
10
|
import { Layer, calculateStrength, canAutoDelete, createMemory } from './memory.js';
|
|
11
11
|
import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, } from './store.js';
|
|
12
|
-
import { textOverlap, markRetrieved } from './search.js';
|
|
12
|
+
import { textOverlap, markRetrieved, tokenize } from './search.js';
|
|
13
13
|
import { compareEntryIdentity } from './compare.js';
|
|
14
|
+
import { duplicateKey, mergedText } from './same-text.js';
|
|
15
|
+
import { successorAfterRetirement } from './merged-row.js';
|
|
14
16
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
15
17
|
import { rejectionDigest, findRejectedValue } from './rejection.js';
|
|
16
18
|
import { countExpiredDormant, purgeExpiredDormant } from './dormant.js';
|
|
@@ -27,9 +29,12 @@ import { appendAuditEvent } from './audit.js';
|
|
|
27
29
|
import { migrateDefaultHalfLife, LEGACY_TYPED_HALF_LIFE } from './half-life-migration.js';
|
|
28
30
|
import { derivationScope, commonDerivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
29
31
|
import { isQuarantineScope } from './quarantine.js';
|
|
32
|
+
import { NO_MERGE_TAGS } from './shared.js';
|
|
30
33
|
const DECAY_THRESHOLD = 0.05;
|
|
31
34
|
const MERGE_OVERLAP_THRESHOLD = 0.35; // Jaccard similarity for "related"
|
|
32
35
|
const MERGE_MIN_CLUSTER = 2; // minimum cluster size to merge
|
|
36
|
+
const MERGE_MAX_SOURCES = 5; // with MERGE_MAX_CHARS, keeps a merged row near 500 tokens, a third of the 1,500-token context budget
|
|
37
|
+
const MERGE_MAX_CHARS = 2000; // total source text; sources past either cap stay unmerged and keep their half-life
|
|
33
38
|
// Half-life scale for merged source episodics. Demotion must go through
|
|
34
39
|
// half_life_days: calculateStrength() recomputes live strength from
|
|
35
40
|
// last_retrieved/half_life and never reads the stored strength field, so a
|
|
@@ -61,6 +66,9 @@ const CONFLICT_STOPWORDS = new Set([
|
|
|
61
66
|
'more', 'most', 'less', 'least', 'other', 'such', 'same', 'new', 'old', 'one', 'two',
|
|
62
67
|
]);
|
|
63
68
|
const REPLAY_COUNT_DEFAULT = 5;
|
|
69
|
+
function keptAsWritten(entry) {
|
|
70
|
+
return entry.tags.some((tag) => NO_MERGE_TAGS.has(tag));
|
|
71
|
+
}
|
|
64
72
|
function isJsonString(value) {
|
|
65
73
|
return typeof value === 'string';
|
|
66
74
|
}
|
|
@@ -503,7 +511,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
503
511
|
// 1.6. Batch extraction — extract facts from episodic memories missing them
|
|
504
512
|
// -------------------------------------------------------------------------
|
|
505
513
|
const extractedFromIds = new Set(survivors.filter((e) => e.extracted_from).map((e) => e.extracted_from));
|
|
506
|
-
const extractionCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !extractedFromIds.has(e.id));
|
|
514
|
+
const extractionCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !extractedFromIds.has(e.id) && !keptAsWritten(e));
|
|
507
515
|
result.extractionCandidates = extractionCandidates.length;
|
|
508
516
|
// extraction.enabled=false is the opt-out for every LLM phase below, key or no key.
|
|
509
517
|
const apiKey = config.extraction.enabled !== false ? (process.env.ANTHROPIC_API_KEY ?? '') : '';
|
|
@@ -674,10 +682,35 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
674
682
|
result.details.push(` ⚠️ physics simulation skipped: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
675
683
|
}
|
|
676
684
|
}
|
|
685
|
+
const byId = new Map(all.map((e) => [e.id, e]));
|
|
686
|
+
const rejectedIn = (tenantId) => (text) => {
|
|
687
|
+
const db = getConsolidateDb();
|
|
688
|
+
return db !== null && findRejectedValue(db, tenantId, rejectionDigest(text)) !== null;
|
|
689
|
+
};
|
|
690
|
+
for (let i = survivors.length - 1; i >= 0; i--) {
|
|
691
|
+
const row = survivors[i];
|
|
692
|
+
const successor = retirable(row) ? successorAfterRetirement(row, byId, rejectedIn(row.tenantId)) : undefined;
|
|
693
|
+
if (successor === undefined)
|
|
694
|
+
continue;
|
|
695
|
+
result.details.push(` ✂️ ${row.id} held a retired text${successor ? `, ${successor.id} holds the rest` : ''}`);
|
|
696
|
+
if (dryRun)
|
|
697
|
+
continue;
|
|
698
|
+
pendingDeletes.push(row.id);
|
|
699
|
+
if (successor) {
|
|
700
|
+
pendingWrites.push(successor);
|
|
701
|
+
survivors[i] = successor;
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
survivors.splice(i, 1);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
677
707
|
// -------------------------------------------------------------------------
|
|
678
708
|
// 3. Merge pass - episodic entries only
|
|
679
709
|
// -------------------------------------------------------------------------
|
|
680
|
-
const
|
|
710
|
+
const alreadyMergedIds = new Set(survivors.flatMap((e) => e.parents));
|
|
711
|
+
const mergeCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !keptAsWritten(e) && !alreadyMergedIds.has(e.id)
|
|
712
|
+
&& !e.pinned // a pin merged with a look-alike would read as one of two values
|
|
713
|
+
&& tokenize(e.content).length > 0);
|
|
681
714
|
const used = new Set();
|
|
682
715
|
// T1 fix (2026-08-15 hardening pass): partition by tenantId BEFORE the
|
|
683
716
|
// overlap loop so a cluster can never span tenants. Previously textOverlap
|
|
@@ -708,17 +741,25 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
708
741
|
const mergeTenant = tenantCandidates[0].tenantId;
|
|
709
742
|
const mergeScope = derivationScope(tenantCandidates[0].scope);
|
|
710
743
|
for (let i = 0; i < tenantCandidates.length; i++) {
|
|
711
|
-
if (used.has(tenantCandidates[i].id))
|
|
744
|
+
if (used.has(tenantCandidates[i].id) || tenantCandidates[i].content.length > MERGE_MAX_CHARS)
|
|
712
745
|
continue;
|
|
713
|
-
const
|
|
746
|
+
const related = [tenantCandidates[i]];
|
|
714
747
|
for (let j = i + 1; j < tenantCandidates.length; j++) {
|
|
715
748
|
if (used.has(tenantCandidates[j].id))
|
|
716
749
|
continue;
|
|
717
750
|
const overlap = textOverlap(tenantCandidates[i].content, tenantCandidates[j].content);
|
|
718
751
|
if (overlap >= MERGE_OVERLAP_THRESHOLD) {
|
|
719
|
-
|
|
752
|
+
related.push(tenantCandidates[j]);
|
|
720
753
|
}
|
|
721
754
|
}
|
|
755
|
+
const cluster = [];
|
|
756
|
+
let clusterChars = 0;
|
|
757
|
+
for (const e of related) {
|
|
758
|
+
if (cluster.length === MERGE_MAX_SOURCES || clusterChars + e.content.length > MERGE_MAX_CHARS)
|
|
759
|
+
continue;
|
|
760
|
+
cluster.push(e);
|
|
761
|
+
clusterChars += e.content.length;
|
|
762
|
+
}
|
|
722
763
|
if (cluster.length < MERGE_MIN_CLUSTER)
|
|
723
764
|
continue;
|
|
724
765
|
// Create a semantic summary
|
|
@@ -734,17 +775,20 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
734
775
|
// always 'default'.
|
|
735
776
|
let semantic = null;
|
|
736
777
|
if (!dryRun) {
|
|
737
|
-
semantic =
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
778
|
+
semantic = {
|
|
779
|
+
...createMemory(mergedContent, {
|
|
780
|
+
layer: Layer.Semantic,
|
|
781
|
+
tags: allTags,
|
|
782
|
+
emotional_valence: maxValence,
|
|
783
|
+
schema_fit: 0.7,
|
|
784
|
+
source: 'consolidation',
|
|
785
|
+
confidence: 'inferred',
|
|
786
|
+
tenantId: mergeTenant,
|
|
787
|
+
scope: mergeScope,
|
|
788
|
+
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
789
|
+
}),
|
|
790
|
+
parents: cluster.map((e) => e.id),
|
|
791
|
+
};
|
|
748
792
|
}
|
|
749
793
|
// mergeContents is DETERMINISTIC CONCATENATION (not an LLM paraphrase)
|
|
750
794
|
// — if a human rejected exactly this byte-identical rollup before, an
|
|
@@ -756,13 +800,17 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
756
800
|
// the tombstone is lifted.
|
|
757
801
|
const consolidateDb = getConsolidateDb();
|
|
758
802
|
if (consolidateDb && semantic) {
|
|
759
|
-
const
|
|
760
|
-
const
|
|
803
|
+
const newDigest = rejectionDigest(semantic.content);
|
|
804
|
+
const oldDigest = rejectionDigest(legacyMergeContents(related)); // tombstones from older releases hold this format's digest
|
|
805
|
+
const newHit = findRejectedValue(consolidateDb, semantic.tenantId, newDigest);
|
|
806
|
+
const tombstone = newHit ?? findRejectedValue(consolidateDb, semantic.tenantId, oldDigest);
|
|
807
|
+
const mergeDigest = newHit ? newDigest : oldDigest;
|
|
761
808
|
if (tombstone) {
|
|
762
809
|
// Still mark used — these members are not re-tried against a
|
|
763
810
|
// DIFFERENT cluster within this same pass; next sleep re-clusters
|
|
764
811
|
// them fresh.
|
|
765
|
-
|
|
812
|
+
const rejected = newHit ? cluster : related; // the old format digested the uncapped list, so rows past the cap were rejected too
|
|
813
|
+
for (const e of rejected)
|
|
766
814
|
used.add(e.id);
|
|
767
815
|
mergesSkippedRejected++;
|
|
768
816
|
try {
|
|
@@ -773,7 +821,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
773
821
|
metadata: {
|
|
774
822
|
digest: mergeDigest,
|
|
775
823
|
reason: tombstone.reason,
|
|
776
|
-
sourceIds:
|
|
824
|
+
sourceIds: rejected.map((e) => e.id),
|
|
777
825
|
},
|
|
778
826
|
});
|
|
779
827
|
}
|
|
@@ -922,17 +970,22 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
922
970
|
// Helpers
|
|
923
971
|
// ---------------------------------------------------------------------------
|
|
924
972
|
function mergeContents(entries) {
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
return `[Consolidated from ${entries.length} related memories]\n\n${base}`;
|
|
973
|
+
// Each distinct text goes in once and in full (the merge demotes every source), one bullet with its lines indented, so heldTextKeys can read it back.
|
|
974
|
+
// Newest first says which version is current; compareEntryIdentity settles ties, so the row and its rejection digest depend only on the sources.
|
|
975
|
+
const sorted = [...entries].sort((a, b) => (Date.parse(b.created) - Date.parse(a.created)) || compareEntryIdentity(a, b));
|
|
976
|
+
const texts = new Map();
|
|
977
|
+
for (const e of sorted) {
|
|
978
|
+
if (!texts.has(duplicateKey(e.content)))
|
|
979
|
+
texts.set(duplicateKey(e.content), e.content);
|
|
933
980
|
}
|
|
934
|
-
|
|
935
|
-
|
|
981
|
+
const header = entries.length === 2 ? '[Consolidated from 2 related memories, newest first]' : `[Consolidated pattern from ${entries.length} related memories, newest first]`;
|
|
982
|
+
return mergedText(header, [...texts.values()]);
|
|
983
|
+
}
|
|
984
|
+
function legacyMergeContents(entries) {
|
|
985
|
+
// The old format dropped text, so it is only ever digested to match rejections recorded against it, never written.
|
|
986
|
+
const sorted = [...entries].sort((a, b) => (b.content.length - a.content.length) || compareEntryIdentity(a, b));
|
|
987
|
+
if (entries.length === 2)
|
|
988
|
+
return `[Consolidated from ${entries.length} related memories]\n\n${sorted[0].content}`;
|
|
936
989
|
const bullets = sorted.map((e) => `- ${e.content.split('\n')[0].slice(0, 120)}`).join('\n');
|
|
937
990
|
return `[Consolidated pattern from ${entries.length} related memories]\n\n${bullets}`;
|
|
938
991
|
}
|
|
@@ -966,7 +1019,7 @@ rescuedIds = new Set()) {
|
|
|
966
1019
|
continue;
|
|
967
1020
|
if (survivors[i].superseded_by || survivors[j].superseded_by)
|
|
968
1021
|
continue;
|
|
969
|
-
if (survivors[i].tags.includes('extracted') ||
|
|
1022
|
+
if ([survivors[i], survivors[j]].some((e) => e.tags.includes('extracted') || e.tags.includes('session-digest')))
|
|
970
1023
|
continue;
|
|
971
1024
|
const reasonAndScore = describeConflict(survivors[i], survivors[j]);
|
|
972
1025
|
if (!reasonAndScore)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type MemoryEntry } from './memory.js';
|
|
2
|
+
import { type SessionHandoff } from './handoff.js';
|
|
3
|
+
import type { SessionEvent, TaskSnapshot } from './store.js';
|
|
4
|
+
import type { AssembleCost, AssembleResult, AssembledContextItem, ContextCost, ContextResultEntry, DrillDownChild, DrillDownCost, DrillDownResult } from './api.js';
|
|
5
|
+
export declare function snapshotText(s: TaskSnapshot): string;
|
|
6
|
+
export declare function handoffText(h: SessionHandoff): string;
|
|
7
|
+
/** Needs at least one event; the header reads the latest. */
|
|
8
|
+
export declare function sessionTrailText(events: SessionEvent[]): string;
|
|
9
|
+
export declare function contextHeading(heading: string, entries: number, tokens: number): string;
|
|
10
|
+
export declare function contextLine(item: {
|
|
11
|
+
entry: MemoryEntry;
|
|
12
|
+
isGlobal: boolean;
|
|
13
|
+
}, framing: string, showStrength: boolean, now: Date, strengthPct?: number): string;
|
|
14
|
+
export declare function crossProjectHeading(entries: number): string;
|
|
15
|
+
export declare function crossProjectLine(item: Pick<ContextResultEntry, 'entry' | 'origin'>): string;
|
|
16
|
+
/** A header's token figure is part of the text it counts, so render until the figure matches the text. */
|
|
17
|
+
export declare function settleTokens(render: (tokens: number) => string): string;
|
|
18
|
+
export declare function printedTokens(text: string): number;
|
|
19
|
+
export interface AssembleHeadingCounts {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
items: number;
|
|
22
|
+
tokens: number;
|
|
23
|
+
totalRaw: number;
|
|
24
|
+
summarized: number;
|
|
25
|
+
evicted: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function assembleHeading(c: AssembleHeadingCounts): string;
|
|
28
|
+
export declare function assembleLine(it: AssembledContextItem): string;
|
|
29
|
+
/** The window in full, as the MCP tool returns it; the header counts the whole block. */
|
|
30
|
+
export declare function assembleText(r: AssembleResult): string;
|
|
31
|
+
export declare function assembleCost(sessionId: string): AssembleCost;
|
|
32
|
+
export declare function drillLine(c: DrillDownChild): string;
|
|
33
|
+
export declare function drillText(r: DrillDownResult): string;
|
|
34
|
+
export declare const drillCost: DrillDownCost;
|
|
35
|
+
export declare function contextCost(format: 'markdown' | 'additional-context', framing: string): ContextCost;
|
|
36
|
+
//# sourceMappingURL=context-render.d.ts.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// The strings a context block prints. The budget prices these same strings, so selection and print cannot drift.
|
|
2
|
+
import { calculateStrength, confidenceFacets, confidenceLabel } from './memory.js';
|
|
3
|
+
import { evalNow } from './ablation.js';
|
|
4
|
+
import { estimateTokens } from './search.js';
|
|
5
|
+
import { renderAmbientSummary } from './ambient.js';
|
|
6
|
+
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
7
|
+
export function snapshotText(s) {
|
|
8
|
+
return [
|
|
9
|
+
'## Active Task Snapshot\n',
|
|
10
|
+
`- Task: ${s.task}`,
|
|
11
|
+
`- Status: ${s.status}`,
|
|
12
|
+
`- Updated: ${s.updated_at}`,
|
|
13
|
+
`- Source: ${s.source}`,
|
|
14
|
+
...(s.session_id ? [`- Session: ${s.session_id}`] : []),
|
|
15
|
+
'', '### Summary', s.summary, '', '### Next step', s.next_step, '',
|
|
16
|
+
].join('\n');
|
|
17
|
+
}
|
|
18
|
+
export function handoffText(h) {
|
|
19
|
+
const lines = ['## Session Handoff\n', `- Session: ${h.sessionId}`, `- Updated: ${h.updatedAt}`];
|
|
20
|
+
if (h.taskId)
|
|
21
|
+
lines.push(`- Task: ${h.taskId}`);
|
|
22
|
+
if (h.repoRoot)
|
|
23
|
+
lines.push(`- Repo: ${h.repoRoot}`);
|
|
24
|
+
if (h.outcome)
|
|
25
|
+
lines.push(`- Outcome: ${h.outcome}`);
|
|
26
|
+
if (h.targetRuntime)
|
|
27
|
+
lines.push(`- Target runtime: ${h.targetRuntime}`);
|
|
28
|
+
if (h.cardId)
|
|
29
|
+
lines.push(`- Card: ${h.cardId}`);
|
|
30
|
+
lines.push('', '### Summary', h.summary);
|
|
31
|
+
if (h.nextAction)
|
|
32
|
+
lines.push('', '### Next action', h.nextAction);
|
|
33
|
+
if (h.artifacts && h.artifacts.length > 0)
|
|
34
|
+
lines.push('', '### Artifacts', ...h.artifacts.map((a) => `- ${a}`));
|
|
35
|
+
if (h.constraints && h.constraints.length > 0)
|
|
36
|
+
lines.push('', '### Constraints', ...h.constraints.map((c) => `- ${c}`));
|
|
37
|
+
if (h.evidence)
|
|
38
|
+
lines.push('', '### Evidence', formatHandoffEvidenceLine(h.evidence));
|
|
39
|
+
lines.push('');
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
/** Needs at least one event; the header reads the latest. */
|
|
43
|
+
export function sessionTrailText(events) {
|
|
44
|
+
const latest = events[events.length - 1];
|
|
45
|
+
return [
|
|
46
|
+
'## Recent Session Trail\n',
|
|
47
|
+
`- Session: ${latest.session_id}`,
|
|
48
|
+
`- Task: ${latest.task ?? 'n/a'}`,
|
|
49
|
+
`- Updated: ${latest.created_at}`,
|
|
50
|
+
'',
|
|
51
|
+
...events.map((e) => `- [${e.created_at}] (${e.event_type}) ${e.content}`),
|
|
52
|
+
'',
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
export function contextHeading(heading, entries, tokens) {
|
|
56
|
+
return `## ${heading} (${entries} entries, ${tokens} tokens)\n`;
|
|
57
|
+
}
|
|
58
|
+
export function contextLine(item, framing, showStrength, now, strengthPct = Math.round(calculateStrength(item.entry) * 100)) {
|
|
59
|
+
const e = item.entry;
|
|
60
|
+
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
61
|
+
const strengthStr = showStrength ? ` (${strengthPct}%)` : '';
|
|
62
|
+
const globalPrefix = item.isGlobal ? '[global] ' : '';
|
|
63
|
+
const label = confidenceLabel(e, now);
|
|
64
|
+
const confTag = `[${label.text}]${label.warn ? ' ⚠️' : ''}`;
|
|
65
|
+
if (framing === 'observe') {
|
|
66
|
+
const dateStr = new Date(e.created).toISOString().slice(0, 10);
|
|
67
|
+
// Verified rules print without the date prefix.
|
|
68
|
+
if (confidenceFacets(e, now).tier === 'verified')
|
|
69
|
+
return `- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
70
|
+
return `- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
71
|
+
}
|
|
72
|
+
if (framing === 'suggest')
|
|
73
|
+
return `- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
74
|
+
return `- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
75
|
+
}
|
|
76
|
+
export function crossProjectHeading(entries) {
|
|
77
|
+
return `\n## Other-project memory (explicitly requested, ${entries} entries)\n`;
|
|
78
|
+
}
|
|
79
|
+
export function crossProjectLine(item) {
|
|
80
|
+
const originLabel = item.origin === null || item.origin === '' ? 'unknown-origin' : item.origin;
|
|
81
|
+
const tagStr = item.entry.tags.length > 0 ? ` [${item.entry.tags.join(', ')}]` : '';
|
|
82
|
+
return `- **[${originLabel}]** ${item.entry.content}${tagStr}`;
|
|
83
|
+
}
|
|
84
|
+
/** A header's token figure is part of the text it counts, so render until the figure matches the text. */
|
|
85
|
+
export function settleTokens(render) {
|
|
86
|
+
let t = 0;
|
|
87
|
+
let text = render(t);
|
|
88
|
+
// The figure only gains digits, so this settles in a few rounds; the cap guards a render that reads the clock.
|
|
89
|
+
for (let i = 0; i < 8 && estimateTokens(text) !== t; i++) {
|
|
90
|
+
t = estimateTokens(text);
|
|
91
|
+
text = render(t);
|
|
92
|
+
}
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
// Every summary slot at its longest wording, so the footer reserve covers whatever the summary says.
|
|
96
|
+
const WIDEST_AMBIENT = {
|
|
97
|
+
tagEntropy: 1, avgStrength: 0, recencyFreshness: 1, emotionalSkew: -1, schemaFitRatio: 0, errorDensity: 0,
|
|
98
|
+
consolidationRatio: 1, conflictIntensity: 1, extractionCoverage: 1,
|
|
99
|
+
dagDepth: Number.MAX_SAFE_INTEGER, totalMemories: Number.MAX_SAFE_INTEGER,
|
|
100
|
+
};
|
|
101
|
+
// Each piece is priced with the newline that follows it, so the pieces' sum bounds the joined block.
|
|
102
|
+
export function printedTokens(text) {
|
|
103
|
+
return estimateTokens(text + '\n');
|
|
104
|
+
}
|
|
105
|
+
export function assembleHeading(c) {
|
|
106
|
+
return `Session ${c.sessionId} \u2014 ${c.items} items, ${c.tokens} tokens (raw=${c.totalRaw}, summarized=${c.summarized}, evicted=${c.evicted})`;
|
|
107
|
+
}
|
|
108
|
+
export function assembleLine(it) {
|
|
109
|
+
const prefix = it.isSummary ? '[summary]' : it.isFreshTail ? '[tail]' : '[older]';
|
|
110
|
+
return ` ${prefix} ${it.createdAt} ${it.id} - ${it.content}`;
|
|
111
|
+
}
|
|
112
|
+
/** The window in full, as the MCP tool returns it; the header counts the whole block. */
|
|
113
|
+
export function assembleText(r) {
|
|
114
|
+
return settleTokens((t) => [assembleHeading({ ...r, items: r.items.length, tokens: t }), ...r.items.map(assembleLine)].join('\n'));
|
|
115
|
+
}
|
|
116
|
+
// Every format prices the full lines: JSON carries full content, and the CLI's previews are never longer.
|
|
117
|
+
export function assembleCost(sessionId) {
|
|
118
|
+
return {
|
|
119
|
+
item: (it) => printedTokens(assembleLine(it)),
|
|
120
|
+
fixed: (w) => printedTokens(assembleHeading({ sessionId, items: w, tokens: w, totalRaw: w, summarized: w, evicted: w })),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function drillHead(s, shown, total, truncated) {
|
|
124
|
+
const span = s.earliestAt ? ` (${s.earliestAt} -> ${s.latestAt})` : '';
|
|
125
|
+
return `Summary ${s.id} \u2014 ${s.descendantCount} descendants${span}\n ${s.content}\n\nChildren (${shown}/${total}${truncated ? ', truncated' : ''}):`;
|
|
126
|
+
}
|
|
127
|
+
export function drillLine(c) {
|
|
128
|
+
return ` [L${c.dagLevel}] ${c.id} - ${c.content}`;
|
|
129
|
+
}
|
|
130
|
+
export function drillText(r) {
|
|
131
|
+
return [drillHead(r.summary, r.children.length, r.totalChildren, r.truncated), ...r.children.map(drillLine)].join('\n');
|
|
132
|
+
}
|
|
133
|
+
export const drillCost = {
|
|
134
|
+
child: (c) => printedTokens(drillLine(c)),
|
|
135
|
+
fixed: (s, w) => printedTokens(drillHead(s, w, w, true)),
|
|
136
|
+
};
|
|
137
|
+
// Headers and footer are reserved at their widest (counts at the budget itself, strength at 100%) before any entry.
|
|
138
|
+
export function contextCost(format, framing) {
|
|
139
|
+
const hook = format === 'additional-context';
|
|
140
|
+
const price = printedTokens;
|
|
141
|
+
return {
|
|
142
|
+
entry: (item) => price(item.category === 'cross-project' && !(hook && item.promptRecall)
|
|
143
|
+
? crossProjectLine(item)
|
|
144
|
+
: contextLine({ entry: item.entry, isGlobal: item.isGlobal ?? false }, framing, !hook, evalNow(), 100)),
|
|
145
|
+
fixed: (budget, can) => price(contextHeading('Project Memory', budget, budget))
|
|
146
|
+
+ (can.cross ? price(crossProjectHeading(budget)) : 0)
|
|
147
|
+
+ (hook && can.promptRecall ? price(contextHeading('Prompt-Relevant Memory', budget, budget)) : 0)
|
|
148
|
+
+ (!hook && can.ambient ? price(`\n${renderAmbientSummary(WIDEST_AMBIENT)}`) : 0),
|
|
149
|
+
snapshot: (s) => price(snapshotText(s)),
|
|
150
|
+
handoff: (h) => price(handoffText(h)),
|
|
151
|
+
trail: (events) => price(sessionTrailText(events)),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=context-render.js.map
|
package/dist/dag.js
CHANGED
|
@@ -4,6 +4,7 @@ import { RejectedValueError } from './rejection.js';
|
|
|
4
4
|
import { redactSecrets } from './secret-detect.js';
|
|
5
5
|
import { derivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
|
+
import { neverAutoShareTags } from './shared.js';
|
|
7
8
|
export function clusterFacts(facts) {
|
|
8
9
|
if (facts.length === 0)
|
|
9
10
|
return [];
|
|
@@ -120,7 +121,7 @@ export async function buildDag(hippoRoot, facts, opts) {
|
|
|
120
121
|
// (memory.ts:535 defaults tenantId when the option is omitted).
|
|
121
122
|
const summaryEntry = createMemory(summary, {
|
|
122
123
|
layer: Layer.Semantic,
|
|
123
|
-
tags: [...cluster.entityTags, 'dag-summary'],
|
|
124
|
+
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-summary'],
|
|
124
125
|
confidence: 'inferred',
|
|
125
126
|
dag_level: 2,
|
|
126
127
|
tenantId: factTenant,
|
|
@@ -329,7 +330,7 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
329
330
|
const nowIso = new Date().toISOString();
|
|
330
331
|
const profileEntry = createMemory(summary, {
|
|
331
332
|
layer: Layer.Semantic,
|
|
332
|
-
tags: [...cluster.entityTags, 'dag-entity-profile'],
|
|
333
|
+
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
|
|
333
334
|
confidence: 'inferred',
|
|
334
335
|
dag_level: 3,
|
|
335
336
|
tenantId, // HIGH #1 fold: thread tenant explicitly
|
package/dist/dashboard.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* Usage: hippo dashboard [--port 3333]
|
|
6
6
|
*/
|
|
7
7
|
import * as http from 'http';
|
|
8
|
+
import * as os from 'os';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import * as fs from 'fs';
|
|
11
|
+
import { extractPathTags } from './path-context.js';
|
|
10
12
|
import { loadAllEntries, listCards, listMemoryConflicts, readEntry, writeEntry } from './store.js';
|
|
11
13
|
import { calculateStrength, confidenceFacets } from './memory.js';
|
|
12
14
|
import { loadConfig } from './config.js';
|
|
@@ -110,6 +112,8 @@ function buildDashboardData(hippoRoot) {
|
|
|
110
112
|
defaultHalfLifeDays: config.defaultHalfLifeDays,
|
|
111
113
|
defaultBudget: config.defaultBudget,
|
|
112
114
|
embeddingsEnabled: config.embeddings.enabled,
|
|
115
|
+
// Every memory captured in the home folder carries these, so they name no project.
|
|
116
|
+
homePathTags: extractPathTags(os.homedir()),
|
|
113
117
|
},
|
|
114
118
|
};
|
|
115
119
|
}
|
package/dist/db.js
CHANGED
|
@@ -2398,12 +2398,12 @@ const MIGRATIONS = [
|
|
|
2398
2398
|
version: 45,
|
|
2399
2399
|
up: (db) => {
|
|
2400
2400
|
// Token ledger (src/token-ledger.ts, ROADMAP TE0): one row per block of
|
|
2401
|
-
// memory text hippo hands an agent (hook, CLI, MCP, HTTP). `event` is
|
|
2402
|
-
//
|
|
2403
|
-
//
|
|
2404
|
-
//
|
|
2405
|
-
// unchanged block. Rows older than the retention
|
|
2406
|
-
// write. Additive only: no min_compatible_binary bump.
|
|
2401
|
+
// memory text hippo hands an agent (hook, CLI, MCP, HTTP). `event` is 'inject'
|
|
2402
|
+
// (sent), 'skip' (unchanged since the session's last inject, not sent), 'reset'
|
|
2403
|
+
// (compaction dropped earlier injections, so the next one must be sent) or
|
|
2404
|
+
// 'reread' (re-read by later calls, booked at session end per call day). block_hash
|
|
2405
|
+
// lets the per-prompt hook skip an unchanged block. Rows older than the retention
|
|
2406
|
+
// window are pruned on write. Additive only: no min_compatible_binary bump.
|
|
2407
2407
|
db.exec(`
|
|
2408
2408
|
CREATE TABLE IF NOT EXISTS token_ledger (
|
|
2409
2409
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
package/dist/dedupe.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Store-level deduplication. Scans for
|
|
3
|
-
*
|
|
2
|
+
* Store-level deduplication. Scans for memories with the same text apart
|
|
3
|
+
* from spacing, keeps the stronger copy (by strength + retrieval count),
|
|
4
4
|
* removes the rest.
|
|
5
5
|
*
|
|
6
6
|
* Extracted from cli.ts in Episode A (v1.11.3) so `api.sleep` can dedupe
|
|
@@ -59,13 +59,13 @@ export interface DedupResult {
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function strengthBucket(strength: number | null | undefined): number;
|
|
61
61
|
/**
|
|
62
|
-
* Scan the store for
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* Scan the store for duplicates and remove the weaker copy: same text apart
|
|
63
|
+
* from spacing, since a near-duplicate can differ in a value (port, version,
|
|
64
|
+
* path, name), AND the same tenant: the scan is partitioned by
|
|
65
65
|
* tenantId, so byte-identical content in two tenants is never a duplicate
|
|
66
66
|
* pair (the tenant boundary is an isolation boundary; cross-tenant removal
|
|
67
67
|
* was the v1.32.0 known-issue data-loss bug).
|
|
68
|
-
* Keeps the one with higher strength (or more retrievals if tied).
|
|
68
|
+
* Keeps the one with higher strength (or more retrievals if tied). `threshold` is accepted for old callers and ignored.
|
|
69
69
|
*/
|
|
70
70
|
export declare function deduplicateStore(hippoRoot: string, options?: {
|
|
71
71
|
threshold?: number;
|