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
package/dist/api.d.ts
CHANGED
|
@@ -95,6 +95,7 @@ export { classifyOriginProject } from './project-identity.js';
|
|
|
95
95
|
* flagged rows with no project origin never ambient-inject.
|
|
96
96
|
*/
|
|
97
97
|
export declare function ambientSecretAdmit(e: MemoryEntry, currentProjectName: string): boolean;
|
|
98
|
+
export declare function oneCopyPerMemory(local: readonly MemoryEntry[], global: readonly MemoryEntry[], now: Date): [MemoryEntry[], MemoryEntry[]];
|
|
98
99
|
export interface RememberOpts {
|
|
99
100
|
content: string;
|
|
100
101
|
kind?: MemoryKind;
|
|
@@ -273,6 +274,9 @@ export interface RecallOpts {
|
|
|
273
274
|
* callers leave this unset and get the trace.
|
|
274
275
|
*/
|
|
275
276
|
suppressRecallTrace?: boolean;
|
|
277
|
+
/** Set only by the MCP recall tool, which ranks with its own scorer and drops copies from its own final list: this call
|
|
278
|
+
* then keeps a memory that a merged row in the same result holds word for word. Other callers leave it unset. */
|
|
279
|
+
keepHeldCopies?: boolean;
|
|
276
280
|
}
|
|
277
281
|
export interface ContinuityBlock {
|
|
278
282
|
activeSnapshot: TaskSnapshot | null;
|
|
@@ -372,7 +376,7 @@ export interface RecallResult {
|
|
|
372
376
|
* the calling agent sees its track record at the moment of forecasting
|
|
373
377
|
* (Lovallo-Kahneman 2003 inside-vs-outside view).
|
|
374
378
|
*
|
|
375
|
-
* Populated by `api.recall` itself via `
|
|
379
|
+
* Populated by `api.recall` itself via `computePlanningFallacyOutput`.
|
|
376
380
|
* Pipeline-invariant: the value depends only on (queryText, tenantId,
|
|
377
381
|
* predictions table state) — all three are identical regardless of
|
|
378
382
|
* which downstream search pipeline produces the memory list, so MCP
|
|
@@ -552,6 +556,11 @@ export interface AssembleOpts {
|
|
|
552
556
|
* is set on the result so the caller knows to widen.
|
|
553
557
|
*/
|
|
554
558
|
rowCap?: number;
|
|
559
|
+
cost?: AssembleCost;
|
|
560
|
+
}
|
|
561
|
+
export interface AssembleCost {
|
|
562
|
+
item: (it: AssembledContextItem) => number;
|
|
563
|
+
fixed: (widest: number) => number;
|
|
555
564
|
}
|
|
556
565
|
export interface AssembledContextItem {
|
|
557
566
|
id: string;
|
|
@@ -622,7 +631,7 @@ export interface DrillDownOpts {
|
|
|
622
631
|
/**
|
|
623
632
|
* Optional token budget. When set, children are appended in chronological
|
|
624
633
|
* order (created ASC) until adding the next child would exceed the budget.
|
|
625
|
-
* Token cost = ceil(content.length / 4)
|
|
634
|
+
* Token cost = the child's printed line under `cost`, else ceil(content.length / 4).
|
|
626
635
|
*
|
|
627
636
|
* For depth > 1, the budget is GLOBAL cumulative (NOT per-level).
|
|
628
637
|
*/
|
|
@@ -635,22 +644,29 @@ export interface DrillDownOpts {
|
|
|
635
644
|
* construction).
|
|
636
645
|
*/
|
|
637
646
|
depth?: number;
|
|
647
|
+
cost?: DrillDownCost;
|
|
648
|
+
}
|
|
649
|
+
export interface DrillDownSummary {
|
|
650
|
+
id: string;
|
|
651
|
+
content: string;
|
|
652
|
+
descendantCount: number;
|
|
653
|
+
earliestAt: string | null;
|
|
654
|
+
latestAt: string | null;
|
|
655
|
+
}
|
|
656
|
+
export interface DrillDownChild {
|
|
657
|
+
id: string;
|
|
658
|
+
content: string;
|
|
659
|
+
layer: string;
|
|
660
|
+
dagLevel: number;
|
|
661
|
+
created: string;
|
|
662
|
+
}
|
|
663
|
+
export interface DrillDownCost {
|
|
664
|
+
child: (c: DrillDownChild) => number;
|
|
665
|
+
fixed: (summary: DrillDownSummary, widest: number) => number;
|
|
638
666
|
}
|
|
639
667
|
export interface DrillDownResult {
|
|
640
|
-
summary:
|
|
641
|
-
|
|
642
|
-
content: string;
|
|
643
|
-
descendantCount: number;
|
|
644
|
-
earliestAt: string | null;
|
|
645
|
-
latestAt: string | null;
|
|
646
|
-
};
|
|
647
|
-
children: Array<{
|
|
648
|
-
id: string;
|
|
649
|
-
content: string;
|
|
650
|
-
layer: string;
|
|
651
|
-
dagLevel: number;
|
|
652
|
-
created: string;
|
|
653
|
-
}>;
|
|
668
|
+
summary: DrillDownSummary;
|
|
669
|
+
children: DrillDownChild[];
|
|
654
670
|
totalChildren: number;
|
|
655
671
|
truncated: boolean;
|
|
656
672
|
}
|
|
@@ -954,10 +970,28 @@ export interface ContextOpts {
|
|
|
954
970
|
currentSessionId?: string | null;
|
|
955
971
|
/** Z1: raw hook-payload prompt; only the pinned-only branch reads it, gated on `pinnedInject.promptRecall`. */
|
|
956
972
|
prompt?: string;
|
|
973
|
+
/** What the budget pays for, from the caller that renders the block. Absent = the memory text alone. */
|
|
974
|
+
cost?: ContextCost;
|
|
975
|
+
}
|
|
976
|
+
/** Budget prices in the text a caller prints, so the budget bounds what reaches the model. */
|
|
977
|
+
export interface ContextCost {
|
|
978
|
+
/** Tokens of one entry as printed. */
|
|
979
|
+
entry: (item: Pick<ContextResultEntry, 'entry' | 'isGlobal' | 'promptRecall' | 'origin' | 'category'>) => number;
|
|
980
|
+
/** Tokens of the headers and footer the block can print at this budget, reserved before any entry. */
|
|
981
|
+
fixed: (budget: number, can: {
|
|
982
|
+
cross: boolean;
|
|
983
|
+
promptRecall: boolean;
|
|
984
|
+
ambient: boolean;
|
|
985
|
+
}) => number;
|
|
986
|
+
/** Tokens of the sections printed ahead of the memories, each as printed. */
|
|
987
|
+
snapshot: (s: TaskSnapshot) => number;
|
|
988
|
+
handoff: (h: SessionHandoff) => number;
|
|
989
|
+
trail: (events: SessionEvent[]) => number;
|
|
957
990
|
}
|
|
958
991
|
export interface ContextResultEntry {
|
|
959
992
|
entry: MemoryEntry;
|
|
960
993
|
score: number;
|
|
994
|
+
/** What this entry cost the budget: its printed line under `ContextOpts.cost`, else its memory text. */
|
|
961
995
|
tokens: number;
|
|
962
996
|
isGlobal?: boolean;
|
|
963
997
|
isFreshTail?: boolean;
|
|
@@ -1036,8 +1070,8 @@ export declare function recordTokens(ctx: Context, surface: TokenSurface, use: {
|
|
|
1036
1070
|
}): void;
|
|
1037
1071
|
/**
|
|
1038
1072
|
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
1039
|
-
* tokens sent
|
|
1040
|
-
*
|
|
1073
|
+
* tokens sent, skipped as unchanged and re-read by later model calls, per
|
|
1074
|
+
* surface, with session counts and mean tokens per session.
|
|
1041
1075
|
*/
|
|
1042
1076
|
export declare function tokenSummary(ctx: Context, opts?: {
|
|
1043
1077
|
days?: number;
|
package/dist/api.js
CHANGED
|
@@ -27,12 +27,14 @@ import { createApiKey, listApiKeys, revokeApiKey, grantScope, ungrantScope, } fr
|
|
|
27
27
|
import { applyGoalStackBoost } from './goals.js';
|
|
28
28
|
import { markRetrieved, estimateTokens, hybridSearch, physicsSearch, churnStaleFactor } from './search.js';
|
|
29
29
|
import { compareEntryIdentity, compareScoredResults } from './compare.js';
|
|
30
|
+
import { dropHeldCopies, duplicateKey, storedTextKeys } from './same-text.js';
|
|
30
31
|
import { scopeMatch } from './scope.js';
|
|
31
32
|
import { consolidate } from './consolidate.js';
|
|
32
33
|
import { loadConfig } from './config.js';
|
|
33
34
|
import { resolveProjectIdentity, classifyOriginProject, isGlobalStoreRoot } from './project-identity.js';
|
|
34
35
|
import { promptTokens, contentTokens, gatePromptRecall, } from './prompt-recall.js';
|
|
35
36
|
import { detectSecret } from './secret-detect.js';
|
|
37
|
+
import { isSessionDigestRow } from './session-digest.js';
|
|
36
38
|
import { deduplicateStore } from './dedupe.js';
|
|
37
39
|
import { computeAmbientState } from './ambient.js';
|
|
38
40
|
import { loadPendingExtractionTenants, markPendingProcessedUpTo } from './graph.js';
|
|
@@ -142,6 +144,26 @@ function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admi
|
|
|
142
144
|
const admitAmbient = (e) => admit(e) && (e.pinned || isContentWorthStoring(e.content));
|
|
143
145
|
return loadAmbientCandidates(hippoRoot, tenantId, includeRecent, admitAmbient, recall);
|
|
144
146
|
}
|
|
147
|
+
// Share and promote copy a memory to the global store under a new id, so equal content is the only link.
|
|
148
|
+
// A pinned copy wins, then the stronger one after the ranking's own global discount; a tie keeps the local copy.
|
|
149
|
+
export function oneCopyPerMemory(local, global, now) {
|
|
150
|
+
const score = (e, isGlobal) => calculateStrength(e, now) * (isGlobal ? 1 / 1.2 : 1);
|
|
151
|
+
const best = new Map();
|
|
152
|
+
const offer = (entry, isGlobal) => {
|
|
153
|
+
const held = best.get(entry.content);
|
|
154
|
+
const wins = !held || (held.entry.pinned !== entry.pinned
|
|
155
|
+
? entry.pinned
|
|
156
|
+
: score(entry, isGlobal) > score(held.entry, held.isGlobal));
|
|
157
|
+
if (wins)
|
|
158
|
+
best.set(entry.content, { entry, isGlobal });
|
|
159
|
+
};
|
|
160
|
+
for (const e of local)
|
|
161
|
+
offer(e, false);
|
|
162
|
+
for (const e of global)
|
|
163
|
+
offer(e, true);
|
|
164
|
+
const kept = new Set([...best.values()].map((b) => b.entry));
|
|
165
|
+
return [local.filter((e) => kept.has(e)), global.filter((e) => kept.has(e))];
|
|
166
|
+
}
|
|
145
167
|
export function remember(ctx, opts) {
|
|
146
168
|
const detection = opts.untrusted ? detectInstruction(opts.content) : { flagged: false, reason: null };
|
|
147
169
|
const requestedScope = opts.scope ?? null;
|
|
@@ -405,6 +427,13 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
405
427
|
}));
|
|
406
428
|
}
|
|
407
429
|
}
|
|
430
|
+
if (!opts.keepHeldCopies) {
|
|
431
|
+
const shownIds = new Set(dropHeldCopies([...baseScored.map((r) => r.entry), ...substituted.map((s) => s.entry)], (e) => e).map((e) => e.id));
|
|
432
|
+
droppedPreRankCount += baseScored.filter((r) => !shownIds.has(r.entry.id)).length;
|
|
433
|
+
baseScored = baseScored.filter((r) => shownIds.has(r.entry.id));
|
|
434
|
+
baseSlice = baseScored.map((r) => r.entry);
|
|
435
|
+
substituted = substituted.filter((s) => shownIds.has(s.entry.id));
|
|
436
|
+
}
|
|
408
437
|
// v1.12.13 / C5 — WYSIATI summary_substitutions_added counter.
|
|
409
438
|
summarySubstitutionsCount = substituted.length;
|
|
410
439
|
// v1.7.4 -- baseScored carries the (possibly boosted) per-row scores. When
|
|
@@ -478,9 +507,11 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
478
507
|
...baseRanked.map((r) => r.id),
|
|
479
508
|
...summaryRanked.map((r) => r.id),
|
|
480
509
|
]);
|
|
510
|
+
const shownKeys = storedTextKeys(opts.keepHeldCopies ? [] : [...baseSlice, ...substituted.map((s) => s.entry)]);
|
|
481
511
|
for (const m of recentScoped) {
|
|
482
|
-
if (seenIds.has(m.id))
|
|
512
|
+
if (seenIds.has(m.id) || shownKeys.has(duplicateKey(m.content)))
|
|
483
513
|
continue;
|
|
514
|
+
shownKeys.add(duplicateKey(m.content));
|
|
484
515
|
const item = {
|
|
485
516
|
id: m.id,
|
|
486
517
|
content: m.content,
|
|
@@ -606,10 +637,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
606
637
|
// per-pipeline). opts.actor threads through to the inner
|
|
607
638
|
// computePredictionBaserate call so MCP/HTTP-originated hints attribute
|
|
608
639
|
// correctly instead of defaulting to 'cli'. Disabled by HIPPO_AUTODEBIAS=off.
|
|
609
|
-
//
|
|
610
|
-
// computePlanningFallacyOutput so the no-class-match / tiebreak
|
|
611
|
-
// watching variant can also reach the caller surface. The two
|
|
612
|
-
// outputs are mutually exclusive; we splat both as optional fields.
|
|
640
|
+
// The hint and the no-class-match / tiebreak watching variant are mutually exclusive; both go out as optional fields.
|
|
613
641
|
const planningFallacyOutput = computePlanningFallacyOutput(ctx.hippoRoot, ctx.tenantId, opts.query, { actor: ctx.actor.subject });
|
|
614
642
|
const planningFallacyHint = planningFallacyOutput.hint ?? null;
|
|
615
643
|
const planningFallacyWatching = planningFallacyOutput.watching ?? null;
|
|
@@ -849,9 +877,11 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
849
877
|
olderItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
850
878
|
tailItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
851
879
|
let items = [...olderItems, ...tailItems];
|
|
852
|
-
|
|
880
|
+
const itemCost = opts.cost?.item ?? ((it) => estimateTokens(it.content));
|
|
881
|
+
const room = budget - (opts.cost?.fixed(Math.max(budget, totalRaw)) ?? 0);
|
|
882
|
+
let tokens = items.reduce((acc, it) => acc + itemCost(it), 0);
|
|
853
883
|
let evicted = 0;
|
|
854
|
-
while (tokens >
|
|
884
|
+
while (tokens > room && items.length > 0) {
|
|
855
885
|
let worstIdx = -1;
|
|
856
886
|
let worstStrength = Infinity;
|
|
857
887
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -864,7 +894,7 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
864
894
|
}
|
|
865
895
|
if (worstIdx === -1)
|
|
866
896
|
break;
|
|
867
|
-
const cost =
|
|
897
|
+
const cost = itemCost(items[worstIdx]);
|
|
868
898
|
items = items.filter((_, i) => i !== worstIdx);
|
|
869
899
|
tokens -= cost;
|
|
870
900
|
evicted++;
|
|
@@ -941,15 +971,32 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
941
971
|
break;
|
|
942
972
|
frontier = nextFrontier;
|
|
943
973
|
}
|
|
974
|
+
const summaryOut = {
|
|
975
|
+
id: summary.id,
|
|
976
|
+
content: summary.content,
|
|
977
|
+
// v0.30 / E5: the STORED direct-child count; the legacy fallback counts
|
|
978
|
+
// level-0 children, never the BFS-depth-N total (independent-review MED #4).
|
|
979
|
+
descendantCount: summary.descendant_count ?? level0DirectCount,
|
|
980
|
+
earliestAt: summary.earliest_at ?? null,
|
|
981
|
+
latestAt: summary.latest_at ?? null,
|
|
982
|
+
};
|
|
983
|
+
const all = collected.map((c) => ({
|
|
984
|
+
id: c.id,
|
|
985
|
+
content: c.content,
|
|
986
|
+
layer: c.layer,
|
|
987
|
+
dagLevel: c.dag_level ?? 0,
|
|
988
|
+
created: c.created,
|
|
989
|
+
}));
|
|
944
990
|
// Apply global cumulative token budget + limit cap on collected.
|
|
945
|
-
let children =
|
|
991
|
+
let children = all;
|
|
946
992
|
let truncated = false;
|
|
947
993
|
if (opts.budget !== undefined) {
|
|
948
994
|
const out = [];
|
|
949
995
|
let used = 0;
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
996
|
+
const room = opts.budget - (opts.cost?.fixed(summaryOut, all.length) ?? 0);
|
|
997
|
+
for (const c of all) {
|
|
998
|
+
const t = opts.cost ? opts.cost.child(c) : estimateTokens(c.content);
|
|
999
|
+
if (out.length > 0 && used + t > room) {
|
|
953
1000
|
truncated = true;
|
|
954
1001
|
break;
|
|
955
1002
|
}
|
|
@@ -963,25 +1010,8 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
963
1010
|
truncated = true;
|
|
964
1011
|
}
|
|
965
1012
|
return {
|
|
966
|
-
summary:
|
|
967
|
-
|
|
968
|
-
content: summary.content,
|
|
969
|
-
// v0.30 / E5: descendant_count stays the summary's STORED value
|
|
970
|
-
// (direct children at creation time). totalChildren below reflects
|
|
971
|
-
// the full BFS collection at the requested depth.
|
|
972
|
-
// independent-review MED #4 fold: legacy fallback uses level-0 direct
|
|
973
|
-
// count (NOT collected.length which is BFS-depth-N total).
|
|
974
|
-
descendantCount: summary.descendant_count ?? level0DirectCount,
|
|
975
|
-
earliestAt: summary.earliest_at ?? null,
|
|
976
|
-
latestAt: summary.latest_at ?? null,
|
|
977
|
-
},
|
|
978
|
-
children: children.map((c) => ({
|
|
979
|
-
id: c.id,
|
|
980
|
-
content: c.content,
|
|
981
|
-
layer: c.layer,
|
|
982
|
-
dagLevel: c.dag_level ?? 0,
|
|
983
|
-
created: c.created,
|
|
984
|
-
})),
|
|
1013
|
+
summary: summaryOut,
|
|
1014
|
+
children,
|
|
985
1015
|
// v0.30 / E5: totalChildren = BFS-collected count (depth-aware). For
|
|
986
1016
|
// depth=1 this equals the eligible direct-children count (backward
|
|
987
1017
|
// compat). For depth>1 it is the cumulative count across levels.
|
|
@@ -1537,11 +1567,6 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1537
1567
|
const isolationEnabled = config.contextProjectIsolation !== false;
|
|
1538
1568
|
const currentProjectName = opts.currentProject ?? resolveProjectIdentity(process.cwd()).name;
|
|
1539
1569
|
const includeCrossProject = opts.crossProject === true || !isolationEnabled;
|
|
1540
|
-
const ambientAdmit = (e) => ambientAdmitEntry(e, currentProjectName, includeCrossProject);
|
|
1541
|
-
// Superseded rows never inject, and ambientAdmitEntry regex-scans content for
|
|
1542
|
-
// secrets, so WHICH rows reach this predicate is what loadAmbientEntries cares
|
|
1543
|
-
// about below.
|
|
1544
|
-
const admit = (e) => !e.superseded_by && ambientAdmit(e);
|
|
1545
1570
|
// Z1: decided before the ambient loads so the FTS candidate query below (pinned-only
|
|
1546
1571
|
// branch) can piggyback on that connection instead of opening its own.
|
|
1547
1572
|
const promptRecallPending = pinnedOnly && Boolean(opts.prompt?.trim()) && config.pinnedInject.promptRecall === true;
|
|
@@ -1551,18 +1576,21 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1551
1576
|
const recallRequest = promptRecallTerms.length > 0
|
|
1552
1577
|
? { terms: promptRecallTerms, limit: Math.floor(finiteOr(config.pinnedInject.promptRecallCandidates, 100, 1)) }
|
|
1553
1578
|
: undefined;
|
|
1554
|
-
|
|
1555
|
-
const
|
|
1556
|
-
?
|
|
1557
|
-
:
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1579
|
+
const cost = opts.cost;
|
|
1580
|
+
const price = (entry, isGlobal, promptRecall) => cost
|
|
1581
|
+
? cost.entry({ entry, isGlobal, promptRecall, origin: entry.origin_project ?? null, category: classifyOriginProject(entry.origin_project, currentProjectName) })
|
|
1582
|
+
: estimateTokens(entry.content);
|
|
1583
|
+
const blockBudget = pinnedOnly && opts.budget === undefined ? config.pinnedInject.budget : budget;
|
|
1584
|
+
let left = cost
|
|
1585
|
+
? Math.max(0, blockBudget - cost.fixed(blockBudget, { cross: includeCrossProject, promptRecall: promptRecallPending, ambient: !pinnedOnly && config.ambient.enabled }))
|
|
1586
|
+
: blockBudget;
|
|
1587
|
+
// Sections print ahead of the memories, so they are paid first; one that does not fit is dropped, as an oversize entry is.
|
|
1588
|
+
const pays = (tokens) => {
|
|
1589
|
+
if (tokens > left)
|
|
1590
|
+
return false;
|
|
1591
|
+
left -= tokens;
|
|
1592
|
+
return true;
|
|
1593
|
+
};
|
|
1566
1594
|
// DF1 T2: bounded read — an orphaned snapshot (no later pre-compact
|
|
1567
1595
|
// superseded it, no session-end closed it) must age out of this ambient
|
|
1568
1596
|
// surface instead of injecting into every future prompt forever. Owner
|
|
@@ -1601,12 +1629,38 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1601
1629
|
limit: 5,
|
|
1602
1630
|
}).filter((e) => passesScopeFilterForRecall(rowScope(e), undefined))
|
|
1603
1631
|
: [];
|
|
1632
|
+
const shownSnapshot = activeSnapshot && (!cost || pays(cost.snapshot(activeSnapshot))) ? activeSnapshot : null;
|
|
1633
|
+
const shownHandoff = sessionHandoff && (!cost || pays(cost.handoff(sessionHandoff))) ? sessionHandoff : null;
|
|
1634
|
+
const shownEvents = recentSessionEvents.length > 0 && (!cost || pays(cost.trail(recentSessionEvents))) ? recentSessionEvents : [];
|
|
1635
|
+
const transcriptHandoffSession = shownHandoff?.evidence?.derivedFrom === 'transcript' ? shownHandoff.sessionId : null;
|
|
1636
|
+
let digestHiddenForHandoff = false;
|
|
1637
|
+
const ambientAdmit = (e) => {
|
|
1638
|
+
// A printed handoff already carries the session's closing message, which its digest would print a second time.
|
|
1639
|
+
if (transcriptHandoffSession !== null && e.source_session_id === transcriptHandoffSession && isSessionDigestRow(e)) {
|
|
1640
|
+
digestHiddenForHandoff = true;
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
return ambientAdmitEntry(e, currentProjectName, includeCrossProject);
|
|
1644
|
+
};
|
|
1645
|
+
// Superseded rows never inject; which rows reach ambientAdmitEntry matters because it regex-scans content for secrets.
|
|
1646
|
+
const admit = (e) => !e.superseded_by && ambientAdmit(e);
|
|
1647
|
+
// Tenant-scoped loads (v1.11.1 lesson: NEVER resolveTenantId({}) here).
|
|
1648
|
+
const localLoad = hasLocal
|
|
1649
|
+
? loadAmbientEntries(ctx.hippoRoot, ctx.tenantId, pinnedOnly, includeRecent, admit, recallRequest)
|
|
1650
|
+
: { entries: [] };
|
|
1651
|
+
const globalLoad = hasGlobal
|
|
1652
|
+
? loadAmbientEntries(globalRoot, ctx.tenantId, pinnedOnly, includeRecent, admit, !isGlobalStoreRoot(ctx.hippoRoot) ? recallRequest : undefined)
|
|
1653
|
+
: { entries: [] };
|
|
1654
|
+
let localEntries = localLoad.entries;
|
|
1655
|
+
let globalEntries = globalLoad.entries;
|
|
1656
|
+
// Computed after markRetrieved runs, so avgStrength reflects post-retrieval strengths.
|
|
1657
|
+
let ambientState;
|
|
1604
1658
|
if (!promptRecallPending &&
|
|
1605
1659
|
localEntries.length === 0 &&
|
|
1606
1660
|
globalEntries.length === 0 &&
|
|
1607
|
-
!
|
|
1608
|
-
!
|
|
1609
|
-
|
|
1661
|
+
!shownSnapshot &&
|
|
1662
|
+
!shownHandoff &&
|
|
1663
|
+
shownEvents.length === 0) {
|
|
1610
1664
|
return { entries: [], tokens: 0 };
|
|
1611
1665
|
}
|
|
1612
1666
|
let selectedItems = [];
|
|
@@ -1617,16 +1671,17 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1617
1671
|
if (!pinnedCfg.pinnedInject.enabled) {
|
|
1618
1672
|
return { entries: [], tokens: 0 };
|
|
1619
1673
|
}
|
|
1620
|
-
// Effective budget: explicit opts.budget wins over config.
|
|
1621
|
-
const effBudget =
|
|
1674
|
+
// Effective budget: explicit opts.budget wins over config, less what the sections took.
|
|
1675
|
+
const effBudget = left;
|
|
1622
1676
|
const nowP = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
1677
|
+
const [localPool, globalPool] = oneCopyPerMemory(localEntries, globalEntries, nowP);
|
|
1623
1678
|
const selectedIds = new Set();
|
|
1624
1679
|
let usedP = 0;
|
|
1625
1680
|
// Pinned entries are explicit user intent, the recent-N list an automatic
|
|
1626
1681
|
// backfill. Both loops share ONE budget and the recent loop runs first, so
|
|
1627
1682
|
// pins are ranked here and reserve their share before it can spend.
|
|
1628
|
-
const pinnedLocal =
|
|
1629
|
-
const pinnedGlobal =
|
|
1683
|
+
const pinnedLocal = localPool.filter((e) => e.pinned);
|
|
1684
|
+
const pinnedGlobal = globalPool.filter((e) => e.pinned);
|
|
1630
1685
|
const rankedPinned = [
|
|
1631
1686
|
...pinnedLocal.map((e) => ({ entry: e, isGlobal: false })),
|
|
1632
1687
|
...pinnedGlobal.map((e) => ({ entry: e, isGlobal: true })),
|
|
@@ -1637,7 +1692,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1637
1692
|
return {
|
|
1638
1693
|
entry,
|
|
1639
1694
|
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1) * sBst,
|
|
1640
|
-
tokens:
|
|
1695
|
+
tokens: price(entry, isGlobal),
|
|
1641
1696
|
isGlobal,
|
|
1642
1697
|
};
|
|
1643
1698
|
})
|
|
@@ -1683,19 +1738,21 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1683
1738
|
const p = promptTokens(opts.prompt ?? '');
|
|
1684
1739
|
if (p.size > 0) {
|
|
1685
1740
|
// Candidates came off the ambient load's own connection (recallRequest above), not a fresh open.
|
|
1686
|
-
|
|
1687
|
-
const
|
|
1741
|
+
// A candidate carrying a pin's text would inject that memory a second time.
|
|
1742
|
+
const pinnedText = new Set(rankedPinned.map((r) => r.entry.content));
|
|
1743
|
+
const eligible = (e) => admit(e) && !e.pinned && isContentWorthStoring(e.content) && !pinnedText.has(e.content);
|
|
1744
|
+
const [localCandidates, globalCandidates] = oneCopyPerMemory((localLoad.recall ?? []).filter(eligible), (globalLoad.recall ?? []).filter(eligible), nowP);
|
|
1688
1745
|
const seenCandidateIds = new Set();
|
|
1689
1746
|
const candidateItems = [];
|
|
1690
1747
|
// Local wins the id collision (a global row synced into the local store).
|
|
1691
1748
|
for (const e of localCandidates) {
|
|
1692
|
-
if (
|
|
1749
|
+
if (seenCandidateIds.has(e.id))
|
|
1693
1750
|
continue;
|
|
1694
1751
|
seenCandidateIds.add(e.id);
|
|
1695
1752
|
candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: false });
|
|
1696
1753
|
}
|
|
1697
1754
|
for (const e of globalCandidates) {
|
|
1698
|
-
if (
|
|
1755
|
+
if (seenCandidateIds.has(e.id))
|
|
1699
1756
|
continue;
|
|
1700
1757
|
seenCandidateIds.add(e.id);
|
|
1701
1758
|
candidateItems.push({ id: e.id, tokens: contentTokens(e.content), entry: e, isGlobal: true });
|
|
@@ -1704,7 +1761,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1704
1761
|
for (const g of gated) {
|
|
1705
1762
|
if (selectedIds.has(g.item.id))
|
|
1706
1763
|
continue;
|
|
1707
|
-
const tokens =
|
|
1764
|
+
const tokens = price(g.item.entry, g.item.isGlobal, true);
|
|
1708
1765
|
if (usedP + tokens > recentBudget)
|
|
1709
1766
|
continue;
|
|
1710
1767
|
selectedItems.push({ entry: g.item.entry, score: g.score, tokens, isGlobal: g.item.isGlobal, promptRecall: true });
|
|
@@ -1715,8 +1772,8 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1715
1772
|
}
|
|
1716
1773
|
else if (includeRecent > 0) {
|
|
1717
1774
|
const recent = [
|
|
1718
|
-
...
|
|
1719
|
-
...
|
|
1775
|
+
...localPool.map((entry) => ({ entry, isGlobal: false })),
|
|
1776
|
+
...globalPool.map((entry) => ({ entry, isGlobal: true })),
|
|
1720
1777
|
]
|
|
1721
1778
|
// T2 (src/compare.ts) note: this already carries an explicit
|
|
1722
1779
|
// per-instance tiebreak (created desc -> id localeCompare) and is
|
|
@@ -1752,7 +1809,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1752
1809
|
.map(({ entry, isGlobal }) => ({
|
|
1753
1810
|
entry,
|
|
1754
1811
|
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1),
|
|
1755
|
-
tokens:
|
|
1812
|
+
tokens: price(entry, isGlobal),
|
|
1756
1813
|
isGlobal,
|
|
1757
1814
|
}));
|
|
1758
1815
|
for (const r of recent) {
|
|
@@ -1767,7 +1824,8 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1767
1824
|
}
|
|
1768
1825
|
if (pinnedLocal.length === 0 &&
|
|
1769
1826
|
pinnedGlobal.length === 0 &&
|
|
1770
|
-
selectedItems.length === 0
|
|
1827
|
+
selectedItems.length === 0 &&
|
|
1828
|
+
!digestHiddenForHandoff) {
|
|
1771
1829
|
return { entries: [], tokens: 0 };
|
|
1772
1830
|
}
|
|
1773
1831
|
for (const r of rankedPinned) {
|
|
@@ -1784,26 +1842,27 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1784
1842
|
else if (query === '*') {
|
|
1785
1843
|
// No query: return strongest memories by strength, up to budget.
|
|
1786
1844
|
const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
1787
|
-
const
|
|
1845
|
+
const [localPool, globalPool] = oneCopyPerMemory(localEntries, globalEntries, now);
|
|
1846
|
+
const localRanked = localPool
|
|
1788
1847
|
.map((e) => ({
|
|
1789
1848
|
entry: e,
|
|
1790
1849
|
score: calculateStrength(e, now),
|
|
1791
|
-
tokens:
|
|
1850
|
+
tokens: price(e, false),
|
|
1792
1851
|
isGlobal: false,
|
|
1793
1852
|
}))
|
|
1794
1853
|
.sort(compareScoredResults);
|
|
1795
|
-
const globalRanked =
|
|
1854
|
+
const globalRanked = globalPool
|
|
1796
1855
|
.map((e) => ({
|
|
1797
1856
|
entry: e,
|
|
1798
1857
|
score: calculateStrength(e, now) * (1 / 1.2),
|
|
1799
|
-
tokens:
|
|
1858
|
+
tokens: price(e, true),
|
|
1800
1859
|
isGlobal: true,
|
|
1801
1860
|
}))
|
|
1802
1861
|
.sort(compareScoredResults);
|
|
1803
1862
|
const combined = [...localRanked, ...globalRanked].sort(compareScoredResults);
|
|
1804
1863
|
let used = 0;
|
|
1805
1864
|
for (const r of combined) {
|
|
1806
|
-
if (used + r.tokens >
|
|
1865
|
+
if (used + r.tokens > left)
|
|
1807
1866
|
continue;
|
|
1808
1867
|
selectedItems.push(r);
|
|
1809
1868
|
used += r.tokens;
|
|
@@ -1813,6 +1872,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1813
1872
|
else {
|
|
1814
1873
|
// Real query: hybrid search (global + local) or physics+hybrid (local only).
|
|
1815
1874
|
let results;
|
|
1875
|
+
const minResults = cost ? 0 : undefined; // a priced block skips an oversize top hit too, so the budget bounds it
|
|
1816
1876
|
if (hasGlobal) {
|
|
1817
1877
|
// searchBothHybrid loads from the store roots itself, so the ambient
|
|
1818
1878
|
// filter above never saw its candidates. Admission runs INSIDE the
|
|
@@ -1821,39 +1881,47 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1821
1881
|
// excluded row saturate the budget (codex rounds 1+3) or shadow its
|
|
1822
1882
|
// admitted duplicate in the dedupe pass (codex round 4). Recall paths
|
|
1823
1883
|
// never set entryFilter, so their behavior is unchanged.
|
|
1884
|
+
const localIndex = loadIndex(ctx.hippoRoot);
|
|
1885
|
+
const isGlobalHit = (e) => !localIndex.entries[e.id];
|
|
1824
1886
|
const merged = await searchBothHybrid(query, ctx.hippoRoot, globalRoot, {
|
|
1825
|
-
budget,
|
|
1887
|
+
budget: left,
|
|
1888
|
+
minResults,
|
|
1889
|
+
cost: cost && ((r) => price(r.entry, isGlobalHit(r.entry))),
|
|
1826
1890
|
scope: activeScope,
|
|
1827
1891
|
tenantId: ctx.tenantId,
|
|
1828
1892
|
entryFilter: ambientAdmit,
|
|
1829
1893
|
});
|
|
1830
|
-
const localIndex = loadIndex(ctx.hippoRoot);
|
|
1831
1894
|
results = merged.map((r) => ({
|
|
1832
1895
|
entry: r.entry,
|
|
1833
1896
|
score: r.score,
|
|
1834
|
-
tokens: r.
|
|
1835
|
-
isGlobal:
|
|
1897
|
+
tokens: price(r.entry, isGlobalHit(r.entry)),
|
|
1898
|
+
isGlobal: isGlobalHit(r.entry),
|
|
1836
1899
|
}));
|
|
1837
1900
|
}
|
|
1838
1901
|
else {
|
|
1839
1902
|
const ctxConfig = loadConfig(ctx.hippoRoot);
|
|
1840
1903
|
const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
|
|
1904
|
+
const localCost = cost && ((r) => price(r.entry, false));
|
|
1841
1905
|
const ctxResults = usePhysicsCtx
|
|
1842
1906
|
? await physicsSearch(query, localEntries, {
|
|
1843
|
-
budget,
|
|
1907
|
+
budget: left,
|
|
1908
|
+
minResults,
|
|
1909
|
+
cost: localCost,
|
|
1844
1910
|
hippoRoot: ctx.hippoRoot,
|
|
1845
1911
|
physicsConfig: ctxConfig.physics,
|
|
1846
1912
|
scope: activeScope,
|
|
1847
1913
|
})
|
|
1848
1914
|
: await hybridSearch(query, localEntries, {
|
|
1849
|
-
budget,
|
|
1915
|
+
budget: left,
|
|
1916
|
+
minResults,
|
|
1917
|
+
cost: localCost,
|
|
1850
1918
|
hippoRoot: ctx.hippoRoot,
|
|
1851
1919
|
scope: activeScope,
|
|
1852
1920
|
});
|
|
1853
1921
|
results = ctxResults.map((r) => ({
|
|
1854
1922
|
entry: r.entry,
|
|
1855
1923
|
score: r.score,
|
|
1856
|
-
tokens: r.
|
|
1924
|
+
tokens: price(r.entry, false),
|
|
1857
1925
|
isGlobal: false,
|
|
1858
1926
|
}));
|
|
1859
1927
|
}
|
|
@@ -1898,8 +1966,9 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1898
1966
|
}
|
|
1899
1967
|
if (limit < selectedItems.length) {
|
|
1900
1968
|
selectedItems = selectedItems.slice(0, limit);
|
|
1901
|
-
totalTokens = selectedItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
1902
1969
|
}
|
|
1970
|
+
selectedItems = dropHeldCopies(selectedItems, (r) => r.entry); // after the last cut, so a merged row that was cut hides nothing
|
|
1971
|
+
totalTokens = selectedItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
1903
1972
|
// v39: annotate every returned entry with its origin and how it relates to
|
|
1904
1973
|
// the active project, so renderers can demarcate cross-project inclusions.
|
|
1905
1974
|
selectedItems = selectedItems.map((r) => ({
|
|
@@ -1908,9 +1977,9 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1908
1977
|
category: classifyOriginProject(r.entry.origin_project, currentProjectName),
|
|
1909
1978
|
}));
|
|
1910
1979
|
if (selectedItems.length === 0 &&
|
|
1911
|
-
!
|
|
1912
|
-
!
|
|
1913
|
-
|
|
1980
|
+
!shownSnapshot &&
|
|
1981
|
+
!shownHandoff &&
|
|
1982
|
+
shownEvents.length === 0) {
|
|
1914
1983
|
// LC1 F5 fix: this bare early-return used to skip tracing entirely — a
|
|
1915
1984
|
// query that found nothing is exactly the coverage-gap signal Track LC
|
|
1916
1985
|
// needs. Write an empty trace (result_count 0, no result rows) so it
|
|
@@ -1989,9 +2058,9 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1989
2058
|
return {
|
|
1990
2059
|
entries: selectedItems,
|
|
1991
2060
|
tokens: totalTokens,
|
|
1992
|
-
activeSnapshot:
|
|
1993
|
-
sessionHandoff:
|
|
1994
|
-
recentEvents:
|
|
2061
|
+
activeSnapshot: shownSnapshot ?? undefined,
|
|
2062
|
+
sessionHandoff: shownHandoff ?? undefined,
|
|
2063
|
+
recentEvents: shownEvents.length > 0 ? shownEvents : undefined,
|
|
1995
2064
|
ambientState,
|
|
1996
2065
|
};
|
|
1997
2066
|
}
|
|
@@ -2023,8 +2092,8 @@ export function recordTokens(ctx, surface, use) {
|
|
|
2023
2092
|
}
|
|
2024
2093
|
/**
|
|
2025
2094
|
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
2026
|
-
* tokens sent
|
|
2027
|
-
*
|
|
2095
|
+
* tokens sent, skipped as unchanged and re-read by later model calls, per
|
|
2096
|
+
* surface, with session counts and mean tokens per session.
|
|
2028
2097
|
*/
|
|
2029
2098
|
export function tokenSummary(ctx, opts = {}) {
|
|
2030
2099
|
const db = openHippoDb(ctx.hippoRoot);
|
package/dist/audit.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export declare const STOP_WORDS: Set<string>;
|
|
|
17
17
|
export declare function auditMemory(entry: MemoryEntry): AuditIssue | null;
|
|
18
18
|
export declare function auditMemories(entries: MemoryEntry[]): AuditResult;
|
|
19
19
|
export declare function isContentWorthStoring(content: string): boolean;
|
|
20
|
-
export
|
|
20
|
+
export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject"];
|
|
21
|
+
export type AuditOp = (typeof AUDIT_OPS)[number];
|
|
21
22
|
export interface AppendAuditOpts {
|
|
22
23
|
tenantId: string;
|
|
23
24
|
actor: string;
|