hippo-memory 1.52.8 → 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 +158 -98
- package/dist/api.d.ts +51 -18
- package/dist/api.js +121 -76
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/capture.d.ts +37 -0
- package/dist/capture.js +111 -81
- package/dist/cli.js +627 -654
- 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/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/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 +5 -3
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +122 -27
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +171 -210
- 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 +6 -0
- 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 +10 -2
- package/dist/shared.js +35 -30
- package/dist/store.d.ts +1 -0
- package/dist/store.js +4 -0
- 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/extensions/openclaw-plugin/README.md +4 -4
- 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/api.d.ts
CHANGED
|
@@ -274,6 +274,9 @@ export interface RecallOpts {
|
|
|
274
274
|
* callers leave this unset and get the trace.
|
|
275
275
|
*/
|
|
276
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;
|
|
277
280
|
}
|
|
278
281
|
export interface ContinuityBlock {
|
|
279
282
|
activeSnapshot: TaskSnapshot | null;
|
|
@@ -373,7 +376,7 @@ export interface RecallResult {
|
|
|
373
376
|
* the calling agent sees its track record at the moment of forecasting
|
|
374
377
|
* (Lovallo-Kahneman 2003 inside-vs-outside view).
|
|
375
378
|
*
|
|
376
|
-
* Populated by `api.recall` itself via `
|
|
379
|
+
* Populated by `api.recall` itself via `computePlanningFallacyOutput`.
|
|
377
380
|
* Pipeline-invariant: the value depends only on (queryText, tenantId,
|
|
378
381
|
* predictions table state) — all three are identical regardless of
|
|
379
382
|
* which downstream search pipeline produces the memory list, so MCP
|
|
@@ -553,6 +556,11 @@ export interface AssembleOpts {
|
|
|
553
556
|
* is set on the result so the caller knows to widen.
|
|
554
557
|
*/
|
|
555
558
|
rowCap?: number;
|
|
559
|
+
cost?: AssembleCost;
|
|
560
|
+
}
|
|
561
|
+
export interface AssembleCost {
|
|
562
|
+
item: (it: AssembledContextItem) => number;
|
|
563
|
+
fixed: (widest: number) => number;
|
|
556
564
|
}
|
|
557
565
|
export interface AssembledContextItem {
|
|
558
566
|
id: string;
|
|
@@ -623,7 +631,7 @@ export interface DrillDownOpts {
|
|
|
623
631
|
/**
|
|
624
632
|
* Optional token budget. When set, children are appended in chronological
|
|
625
633
|
* order (created ASC) until adding the next child would exceed the budget.
|
|
626
|
-
* Token cost = ceil(content.length / 4)
|
|
634
|
+
* Token cost = the child's printed line under `cost`, else ceil(content.length / 4).
|
|
627
635
|
*
|
|
628
636
|
* For depth > 1, the budget is GLOBAL cumulative (NOT per-level).
|
|
629
637
|
*/
|
|
@@ -636,22 +644,29 @@ export interface DrillDownOpts {
|
|
|
636
644
|
* construction).
|
|
637
645
|
*/
|
|
638
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;
|
|
639
666
|
}
|
|
640
667
|
export interface DrillDownResult {
|
|
641
|
-
summary:
|
|
642
|
-
|
|
643
|
-
content: string;
|
|
644
|
-
descendantCount: number;
|
|
645
|
-
earliestAt: string | null;
|
|
646
|
-
latestAt: string | null;
|
|
647
|
-
};
|
|
648
|
-
children: Array<{
|
|
649
|
-
id: string;
|
|
650
|
-
content: string;
|
|
651
|
-
layer: string;
|
|
652
|
-
dagLevel: number;
|
|
653
|
-
created: string;
|
|
654
|
-
}>;
|
|
668
|
+
summary: DrillDownSummary;
|
|
669
|
+
children: DrillDownChild[];
|
|
655
670
|
totalChildren: number;
|
|
656
671
|
truncated: boolean;
|
|
657
672
|
}
|
|
@@ -955,10 +970,28 @@ export interface ContextOpts {
|
|
|
955
970
|
currentSessionId?: string | null;
|
|
956
971
|
/** Z1: raw hook-payload prompt; only the pinned-only branch reads it, gated on `pinnedInject.promptRecall`. */
|
|
957
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;
|
|
958
990
|
}
|
|
959
991
|
export interface ContextResultEntry {
|
|
960
992
|
entry: MemoryEntry;
|
|
961
993
|
score: number;
|
|
994
|
+
/** What this entry cost the budget: its printed line under `ContextOpts.cost`, else its memory text. */
|
|
962
995
|
tokens: number;
|
|
963
996
|
isGlobal?: boolean;
|
|
964
997
|
isFreshTail?: boolean;
|
|
@@ -1037,8 +1070,8 @@ export declare function recordTokens(ctx: Context, surface: TokenSurface, use: {
|
|
|
1037
1070
|
}): void;
|
|
1038
1071
|
/**
|
|
1039
1072
|
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
1040
|
-
* tokens sent
|
|
1041
|
-
*
|
|
1073
|
+
* tokens sent, skipped as unchanged and re-read by later model calls, per
|
|
1074
|
+
* surface, with session counts and mean tokens per session.
|
|
1042
1075
|
*/
|
|
1043
1076
|
export declare function tokenSummary(ctx: Context, opts?: {
|
|
1044
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';
|
|
@@ -425,6 +427,13 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
425
427
|
}));
|
|
426
428
|
}
|
|
427
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
|
+
}
|
|
428
437
|
// v1.12.13 / C5 — WYSIATI summary_substitutions_added counter.
|
|
429
438
|
summarySubstitutionsCount = substituted.length;
|
|
430
439
|
// v1.7.4 -- baseScored carries the (possibly boosted) per-row scores. When
|
|
@@ -498,9 +507,11 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
498
507
|
...baseRanked.map((r) => r.id),
|
|
499
508
|
...summaryRanked.map((r) => r.id),
|
|
500
509
|
]);
|
|
510
|
+
const shownKeys = storedTextKeys(opts.keepHeldCopies ? [] : [...baseSlice, ...substituted.map((s) => s.entry)]);
|
|
501
511
|
for (const m of recentScoped) {
|
|
502
|
-
if (seenIds.has(m.id))
|
|
512
|
+
if (seenIds.has(m.id) || shownKeys.has(duplicateKey(m.content)))
|
|
503
513
|
continue;
|
|
514
|
+
shownKeys.add(duplicateKey(m.content));
|
|
504
515
|
const item = {
|
|
505
516
|
id: m.id,
|
|
506
517
|
content: m.content,
|
|
@@ -626,10 +637,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
626
637
|
// per-pipeline). opts.actor threads through to the inner
|
|
627
638
|
// computePredictionBaserate call so MCP/HTTP-originated hints attribute
|
|
628
639
|
// correctly instead of defaulting to 'cli'. Disabled by HIPPO_AUTODEBIAS=off.
|
|
629
|
-
//
|
|
630
|
-
// computePlanningFallacyOutput so the no-class-match / tiebreak
|
|
631
|
-
// watching variant can also reach the caller surface. The two
|
|
632
|
-
// 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.
|
|
633
641
|
const planningFallacyOutput = computePlanningFallacyOutput(ctx.hippoRoot, ctx.tenantId, opts.query, { actor: ctx.actor.subject });
|
|
634
642
|
const planningFallacyHint = planningFallacyOutput.hint ?? null;
|
|
635
643
|
const planningFallacyWatching = planningFallacyOutput.watching ?? null;
|
|
@@ -869,9 +877,11 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
869
877
|
olderItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
870
878
|
tailItems.sort((a, b) => cmpIso(a.createdAt, b.createdAt));
|
|
871
879
|
let items = [...olderItems, ...tailItems];
|
|
872
|
-
|
|
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);
|
|
873
883
|
let evicted = 0;
|
|
874
|
-
while (tokens >
|
|
884
|
+
while (tokens > room && items.length > 0) {
|
|
875
885
|
let worstIdx = -1;
|
|
876
886
|
let worstStrength = Infinity;
|
|
877
887
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -884,7 +894,7 @@ export function assemble(ctx, sessionId, opts = {}) {
|
|
|
884
894
|
}
|
|
885
895
|
if (worstIdx === -1)
|
|
886
896
|
break;
|
|
887
|
-
const cost =
|
|
897
|
+
const cost = itemCost(items[worstIdx]);
|
|
888
898
|
items = items.filter((_, i) => i !== worstIdx);
|
|
889
899
|
tokens -= cost;
|
|
890
900
|
evicted++;
|
|
@@ -961,15 +971,32 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
961
971
|
break;
|
|
962
972
|
frontier = nextFrontier;
|
|
963
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
|
+
}));
|
|
964
990
|
// Apply global cumulative token budget + limit cap on collected.
|
|
965
|
-
let children =
|
|
991
|
+
let children = all;
|
|
966
992
|
let truncated = false;
|
|
967
993
|
if (opts.budget !== undefined) {
|
|
968
994
|
const out = [];
|
|
969
995
|
let used = 0;
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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) {
|
|
973
1000
|
truncated = true;
|
|
974
1001
|
break;
|
|
975
1002
|
}
|
|
@@ -983,25 +1010,8 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
983
1010
|
truncated = true;
|
|
984
1011
|
}
|
|
985
1012
|
return {
|
|
986
|
-
summary:
|
|
987
|
-
|
|
988
|
-
content: summary.content,
|
|
989
|
-
// v0.30 / E5: descendant_count stays the summary's STORED value
|
|
990
|
-
// (direct children at creation time). totalChildren below reflects
|
|
991
|
-
// the full BFS collection at the requested depth.
|
|
992
|
-
// independent-review MED #4 fold: legacy fallback uses level-0 direct
|
|
993
|
-
// count (NOT collected.length which is BFS-depth-N total).
|
|
994
|
-
descendantCount: summary.descendant_count ?? level0DirectCount,
|
|
995
|
-
earliestAt: summary.earliest_at ?? null,
|
|
996
|
-
latestAt: summary.latest_at ?? null,
|
|
997
|
-
},
|
|
998
|
-
children: children.map((c) => ({
|
|
999
|
-
id: c.id,
|
|
1000
|
-
content: c.content,
|
|
1001
|
-
layer: c.layer,
|
|
1002
|
-
dagLevel: c.dag_level ?? 0,
|
|
1003
|
-
created: c.created,
|
|
1004
|
-
})),
|
|
1013
|
+
summary: summaryOut,
|
|
1014
|
+
children,
|
|
1005
1015
|
// v0.30 / E5: totalChildren = BFS-collected count (depth-aware). For
|
|
1006
1016
|
// depth=1 this equals the eligible direct-children count (backward
|
|
1007
1017
|
// compat). For depth>1 it is the cumulative count across levels.
|
|
@@ -1557,11 +1567,6 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1557
1567
|
const isolationEnabled = config.contextProjectIsolation !== false;
|
|
1558
1568
|
const currentProjectName = opts.currentProject ?? resolveProjectIdentity(process.cwd()).name;
|
|
1559
1569
|
const includeCrossProject = opts.crossProject === true || !isolationEnabled;
|
|
1560
|
-
const ambientAdmit = (e) => ambientAdmitEntry(e, currentProjectName, includeCrossProject);
|
|
1561
|
-
// Superseded rows never inject, and ambientAdmitEntry regex-scans content for
|
|
1562
|
-
// secrets, so WHICH rows reach this predicate is what loadAmbientEntries cares
|
|
1563
|
-
// about below.
|
|
1564
|
-
const admit = (e) => !e.superseded_by && ambientAdmit(e);
|
|
1565
1570
|
// Z1: decided before the ambient loads so the FTS candidate query below (pinned-only
|
|
1566
1571
|
// branch) can piggyback on that connection instead of opening its own.
|
|
1567
1572
|
const promptRecallPending = pinnedOnly && Boolean(opts.prompt?.trim()) && config.pinnedInject.promptRecall === true;
|
|
@@ -1571,18 +1576,21 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1571
1576
|
const recallRequest = promptRecallTerms.length > 0
|
|
1572
1577
|
? { terms: promptRecallTerms, limit: Math.floor(finiteOr(config.pinnedInject.promptRecallCandidates, 100, 1)) }
|
|
1573
1578
|
: undefined;
|
|
1574
|
-
|
|
1575
|
-
const
|
|
1576
|
-
?
|
|
1577
|
-
:
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
+
};
|
|
1586
1594
|
// DF1 T2: bounded read — an orphaned snapshot (no later pre-compact
|
|
1587
1595
|
// superseded it, no session-end closed it) must age out of this ambient
|
|
1588
1596
|
// surface instead of injecting into every future prompt forever. Owner
|
|
@@ -1621,12 +1629,38 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1621
1629
|
limit: 5,
|
|
1622
1630
|
}).filter((e) => passesScopeFilterForRecall(rowScope(e), undefined))
|
|
1623
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;
|
|
1624
1658
|
if (!promptRecallPending &&
|
|
1625
1659
|
localEntries.length === 0 &&
|
|
1626
1660
|
globalEntries.length === 0 &&
|
|
1627
|
-
!
|
|
1628
|
-
!
|
|
1629
|
-
|
|
1661
|
+
!shownSnapshot &&
|
|
1662
|
+
!shownHandoff &&
|
|
1663
|
+
shownEvents.length === 0) {
|
|
1630
1664
|
return { entries: [], tokens: 0 };
|
|
1631
1665
|
}
|
|
1632
1666
|
let selectedItems = [];
|
|
@@ -1637,8 +1671,8 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1637
1671
|
if (!pinnedCfg.pinnedInject.enabled) {
|
|
1638
1672
|
return { entries: [], tokens: 0 };
|
|
1639
1673
|
}
|
|
1640
|
-
// Effective budget: explicit opts.budget wins over config.
|
|
1641
|
-
const effBudget =
|
|
1674
|
+
// Effective budget: explicit opts.budget wins over config, less what the sections took.
|
|
1675
|
+
const effBudget = left;
|
|
1642
1676
|
const nowP = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
|
|
1643
1677
|
const [localPool, globalPool] = oneCopyPerMemory(localEntries, globalEntries, nowP);
|
|
1644
1678
|
const selectedIds = new Set();
|
|
@@ -1658,7 +1692,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1658
1692
|
return {
|
|
1659
1693
|
entry,
|
|
1660
1694
|
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1) * sBst,
|
|
1661
|
-
tokens:
|
|
1695
|
+
tokens: price(entry, isGlobal),
|
|
1662
1696
|
isGlobal,
|
|
1663
1697
|
};
|
|
1664
1698
|
})
|
|
@@ -1727,7 +1761,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1727
1761
|
for (const g of gated) {
|
|
1728
1762
|
if (selectedIds.has(g.item.id))
|
|
1729
1763
|
continue;
|
|
1730
|
-
const tokens =
|
|
1764
|
+
const tokens = price(g.item.entry, g.item.isGlobal, true);
|
|
1731
1765
|
if (usedP + tokens > recentBudget)
|
|
1732
1766
|
continue;
|
|
1733
1767
|
selectedItems.push({ entry: g.item.entry, score: g.score, tokens, isGlobal: g.item.isGlobal, promptRecall: true });
|
|
@@ -1775,7 +1809,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1775
1809
|
.map(({ entry, isGlobal }) => ({
|
|
1776
1810
|
entry,
|
|
1777
1811
|
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1),
|
|
1778
|
-
tokens:
|
|
1812
|
+
tokens: price(entry, isGlobal),
|
|
1779
1813
|
isGlobal,
|
|
1780
1814
|
}));
|
|
1781
1815
|
for (const r of recent) {
|
|
@@ -1790,7 +1824,8 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1790
1824
|
}
|
|
1791
1825
|
if (pinnedLocal.length === 0 &&
|
|
1792
1826
|
pinnedGlobal.length === 0 &&
|
|
1793
|
-
selectedItems.length === 0
|
|
1827
|
+
selectedItems.length === 0 &&
|
|
1828
|
+
!digestHiddenForHandoff) {
|
|
1794
1829
|
return { entries: [], tokens: 0 };
|
|
1795
1830
|
}
|
|
1796
1831
|
for (const r of rankedPinned) {
|
|
@@ -1812,7 +1847,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1812
1847
|
.map((e) => ({
|
|
1813
1848
|
entry: e,
|
|
1814
1849
|
score: calculateStrength(e, now),
|
|
1815
|
-
tokens:
|
|
1850
|
+
tokens: price(e, false),
|
|
1816
1851
|
isGlobal: false,
|
|
1817
1852
|
}))
|
|
1818
1853
|
.sort(compareScoredResults);
|
|
@@ -1820,14 +1855,14 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1820
1855
|
.map((e) => ({
|
|
1821
1856
|
entry: e,
|
|
1822
1857
|
score: calculateStrength(e, now) * (1 / 1.2),
|
|
1823
|
-
tokens:
|
|
1858
|
+
tokens: price(e, true),
|
|
1824
1859
|
isGlobal: true,
|
|
1825
1860
|
}))
|
|
1826
1861
|
.sort(compareScoredResults);
|
|
1827
1862
|
const combined = [...localRanked, ...globalRanked].sort(compareScoredResults);
|
|
1828
1863
|
let used = 0;
|
|
1829
1864
|
for (const r of combined) {
|
|
1830
|
-
if (used + r.tokens >
|
|
1865
|
+
if (used + r.tokens > left)
|
|
1831
1866
|
continue;
|
|
1832
1867
|
selectedItems.push(r);
|
|
1833
1868
|
used += r.tokens;
|
|
@@ -1837,6 +1872,7 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1837
1872
|
else {
|
|
1838
1873
|
// Real query: hybrid search (global + local) or physics+hybrid (local only).
|
|
1839
1874
|
let results;
|
|
1875
|
+
const minResults = cost ? 0 : undefined; // a priced block skips an oversize top hit too, so the budget bounds it
|
|
1840
1876
|
if (hasGlobal) {
|
|
1841
1877
|
// searchBothHybrid loads from the store roots itself, so the ambient
|
|
1842
1878
|
// filter above never saw its candidates. Admission runs INSIDE the
|
|
@@ -1845,39 +1881,47 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1845
1881
|
// excluded row saturate the budget (codex rounds 1+3) or shadow its
|
|
1846
1882
|
// admitted duplicate in the dedupe pass (codex round 4). Recall paths
|
|
1847
1883
|
// never set entryFilter, so their behavior is unchanged.
|
|
1884
|
+
const localIndex = loadIndex(ctx.hippoRoot);
|
|
1885
|
+
const isGlobalHit = (e) => !localIndex.entries[e.id];
|
|
1848
1886
|
const merged = await searchBothHybrid(query, ctx.hippoRoot, globalRoot, {
|
|
1849
|
-
budget,
|
|
1887
|
+
budget: left,
|
|
1888
|
+
minResults,
|
|
1889
|
+
cost: cost && ((r) => price(r.entry, isGlobalHit(r.entry))),
|
|
1850
1890
|
scope: activeScope,
|
|
1851
1891
|
tenantId: ctx.tenantId,
|
|
1852
1892
|
entryFilter: ambientAdmit,
|
|
1853
1893
|
});
|
|
1854
|
-
const localIndex = loadIndex(ctx.hippoRoot);
|
|
1855
1894
|
results = merged.map((r) => ({
|
|
1856
1895
|
entry: r.entry,
|
|
1857
1896
|
score: r.score,
|
|
1858
|
-
tokens: r.
|
|
1859
|
-
isGlobal:
|
|
1897
|
+
tokens: price(r.entry, isGlobalHit(r.entry)),
|
|
1898
|
+
isGlobal: isGlobalHit(r.entry),
|
|
1860
1899
|
}));
|
|
1861
1900
|
}
|
|
1862
1901
|
else {
|
|
1863
1902
|
const ctxConfig = loadConfig(ctx.hippoRoot);
|
|
1864
1903
|
const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
|
|
1904
|
+
const localCost = cost && ((r) => price(r.entry, false));
|
|
1865
1905
|
const ctxResults = usePhysicsCtx
|
|
1866
1906
|
? await physicsSearch(query, localEntries, {
|
|
1867
|
-
budget,
|
|
1907
|
+
budget: left,
|
|
1908
|
+
minResults,
|
|
1909
|
+
cost: localCost,
|
|
1868
1910
|
hippoRoot: ctx.hippoRoot,
|
|
1869
1911
|
physicsConfig: ctxConfig.physics,
|
|
1870
1912
|
scope: activeScope,
|
|
1871
1913
|
})
|
|
1872
1914
|
: await hybridSearch(query, localEntries, {
|
|
1873
|
-
budget,
|
|
1915
|
+
budget: left,
|
|
1916
|
+
minResults,
|
|
1917
|
+
cost: localCost,
|
|
1874
1918
|
hippoRoot: ctx.hippoRoot,
|
|
1875
1919
|
scope: activeScope,
|
|
1876
1920
|
});
|
|
1877
1921
|
results = ctxResults.map((r) => ({
|
|
1878
1922
|
entry: r.entry,
|
|
1879
1923
|
score: r.score,
|
|
1880
|
-
tokens: r.
|
|
1924
|
+
tokens: price(r.entry, false),
|
|
1881
1925
|
isGlobal: false,
|
|
1882
1926
|
}));
|
|
1883
1927
|
}
|
|
@@ -1922,8 +1966,9 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1922
1966
|
}
|
|
1923
1967
|
if (limit < selectedItems.length) {
|
|
1924
1968
|
selectedItems = selectedItems.slice(0, limit);
|
|
1925
|
-
totalTokens = selectedItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
1926
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);
|
|
1927
1972
|
// v39: annotate every returned entry with its origin and how it relates to
|
|
1928
1973
|
// the active project, so renderers can demarcate cross-project inclusions.
|
|
1929
1974
|
selectedItems = selectedItems.map((r) => ({
|
|
@@ -1932,9 +1977,9 @@ export async function getContext(ctx, opts = {}) {
|
|
|
1932
1977
|
category: classifyOriginProject(r.entry.origin_project, currentProjectName),
|
|
1933
1978
|
}));
|
|
1934
1979
|
if (selectedItems.length === 0 &&
|
|
1935
|
-
!
|
|
1936
|
-
!
|
|
1937
|
-
|
|
1980
|
+
!shownSnapshot &&
|
|
1981
|
+
!shownHandoff &&
|
|
1982
|
+
shownEvents.length === 0) {
|
|
1938
1983
|
// LC1 F5 fix: this bare early-return used to skip tracing entirely — a
|
|
1939
1984
|
// query that found nothing is exactly the coverage-gap signal Track LC
|
|
1940
1985
|
// needs. Write an empty trace (result_count 0, no result rows) so it
|
|
@@ -2013,9 +2058,9 @@ export async function getContext(ctx, opts = {}) {
|
|
|
2013
2058
|
return {
|
|
2014
2059
|
entries: selectedItems,
|
|
2015
2060
|
tokens: totalTokens,
|
|
2016
|
-
activeSnapshot:
|
|
2017
|
-
sessionHandoff:
|
|
2018
|
-
recentEvents:
|
|
2061
|
+
activeSnapshot: shownSnapshot ?? undefined,
|
|
2062
|
+
sessionHandoff: shownHandoff ?? undefined,
|
|
2063
|
+
recentEvents: shownEvents.length > 0 ? shownEvents : undefined,
|
|
2019
2064
|
ambientState,
|
|
2020
2065
|
};
|
|
2021
2066
|
}
|
|
@@ -2047,8 +2092,8 @@ export function recordTokens(ctx, surface, use) {
|
|
|
2047
2092
|
}
|
|
2048
2093
|
/**
|
|
2049
2094
|
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
2050
|
-
* tokens sent
|
|
2051
|
-
*
|
|
2095
|
+
* tokens sent, skipped as unchanged and re-read by later model calls, per
|
|
2096
|
+
* surface, with session counts and mean tokens per session.
|
|
2052
2097
|
*/
|
|
2053
2098
|
export function tokenSummary(ctx, opts = {}) {
|
|
2054
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;
|
package/dist/audit.js
CHANGED
|
@@ -167,6 +167,69 @@ export function isContentWorthStoring(content) {
|
|
|
167
167
|
return false;
|
|
168
168
|
return true;
|
|
169
169
|
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// A5 audit log primitives (append-only mutation trail)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// The one list of audit ops: the AuditOp type, `hippo audit list --op` and GET /v1/audit?op= all read it.
|
|
174
|
+
export const AUDIT_OPS = [
|
|
175
|
+
'remember',
|
|
176
|
+
'recall',
|
|
177
|
+
'promote',
|
|
178
|
+
'supersede',
|
|
179
|
+
'forget',
|
|
180
|
+
'archive_raw',
|
|
181
|
+
'auth_revoke',
|
|
182
|
+
'auth_create', // emitted by api.authCreate
|
|
183
|
+
'outcome',
|
|
184
|
+
'consolidate', // emitted once per api.sleep invocation
|
|
185
|
+
'audit_prune', // emitted by pruneAuditLog after each retention prune
|
|
186
|
+
'summary_marked_dirty', // emitted by markSummaryDirty on the 0->1 transition
|
|
187
|
+
'summary_marked_clean', // emitted by clearSummaryDirtyAfterBuild after the buildDag child-link loop
|
|
188
|
+
'summary_rebuilt', // emitted by applyRebuildResult on a successful sleep-cycle rebuild
|
|
189
|
+
'predict_create', // emitted by savePrediction
|
|
190
|
+
'predict_close', // emitted by closePrediction
|
|
191
|
+
'predict_baserate', // emitted by computePredictionBaserate
|
|
192
|
+
'recall_autodebias_hint', // emitted by computePlanningFallacyOutput on success
|
|
193
|
+
'recall_autodebias_hint_no_class_match', // telemetry: forward-claim detected, no class scored
|
|
194
|
+
'recall_autodebias_hint_tiebreak', // telemetry: forward-claim detected, two or more classes tied
|
|
195
|
+
'recall_anchor_detected_query_repeat', // emitted by the anchoring detector when the same query returns the same top-1
|
|
196
|
+
'recall_anchor_detected_memory_dominance', // emitted by the anchoring detector when one memory wins top-1 across distinct queries
|
|
197
|
+
'recall_anchor_skipped_no_session', // telemetry: no sessionId, so ring tracking was skipped
|
|
198
|
+
'recall_availability_detected', // emitted when the availability/recency-bias hint fires
|
|
199
|
+
'decision_create', // emitted by saveDecision
|
|
200
|
+
'decision_supersede', // emitted by saveDecision when --supersedes resolves to an active decision
|
|
201
|
+
'decision_close', // emitted by closeDecision
|
|
202
|
+
'incident_open', // emitted by saveIncident
|
|
203
|
+
'incident_resolve', // emitted by resolveIncident
|
|
204
|
+
'incident_close', // emitted by closeIncident
|
|
205
|
+
'process_create', // emitted by saveProcess
|
|
206
|
+
'process_supersede', // emitted by saveProcess on a supersession
|
|
207
|
+
'process_close', // emitted by closeProcess
|
|
208
|
+
'policy_create', // emitted by savePolicy
|
|
209
|
+
'policy_supersede', // emitted by savePolicy on a supersession
|
|
210
|
+
'policy_close', // emitted by closePolicy
|
|
211
|
+
'skill_create', // emitted by saveSkill
|
|
212
|
+
'skill_supersede', // emitted by saveSkill on a supersession
|
|
213
|
+
'skill_close', // emitted by closeSkill
|
|
214
|
+
'project_brief_create', // emitted by saveProjectBrief
|
|
215
|
+
'project_brief_supersede', // emitted by saveProjectBrief on a supersession, including a refresh
|
|
216
|
+
'project_brief_close', // emitted by closeProjectBrief
|
|
217
|
+
'customer_note_create', // emitted by saveCustomerNote
|
|
218
|
+
'customer_note_supersede', // emitted by saveCustomerNote on a supersession
|
|
219
|
+
'customer_note_close', // emitted by closeCustomerNote
|
|
220
|
+
'mv_rescue', // emitted by consolidate() per rescued entry when config.memoryValue.enabled
|
|
221
|
+
'reject_value', // emitted by the `hippo reject` verb
|
|
222
|
+
'reject_refusal', // emitted when the rejection guard refuses a write
|
|
223
|
+
'unreject_value', // emitted by the `hippo unreject` verb
|
|
224
|
+
'conflict_resolve', // emitted by resolveConflict on every resolution path
|
|
225
|
+
'half_life_migrate', // emitted by migrateDefaultHalfLife with the rescaled ids
|
|
226
|
+
'dormant_restore', // emitted by api.restoreDormant
|
|
227
|
+
'auth_grant', // emitted by api.authGrant
|
|
228
|
+
'auth_ungrant', // emitted by api.authUngrant
|
|
229
|
+
'quarantine', // emitted by recordQuarantine inside remember's write transaction
|
|
230
|
+
'quarantine_approve', // emitted by api.quarantineApprove
|
|
231
|
+
'quarantine_reject', // emitted by api.quarantineReject
|
|
232
|
+
];
|
|
170
233
|
function isBigIntValue(value) {
|
|
171
234
|
return typeof value === 'bigint';
|
|
172
235
|
}
|