hippo-memory 1.56.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/api-errors.d.ts +27 -0
- package/dist/api-errors.js +37 -0
- package/dist/api.d.ts +21 -14
- package/dist/api.js +97 -71
- package/dist/audit.d.ts +4 -0
- package/dist/audit.js +11 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +7 -5
- package/dist/capture-contract.d.ts +47 -0
- package/dist/capture-contract.js +49 -0
- package/dist/capture-error.js +2 -1
- package/dist/capture.d.ts +0 -13
- package/dist/capture.js +5 -66
- package/dist/card-detail.d.ts +1 -1
- package/dist/card-detail.js +1 -1
- package/dist/cli/shared.d.ts +137 -0
- package/dist/cli/shared.js +834 -0
- package/dist/cli/sleep.d.ts +10 -0
- package/dist/cli/sleep.js +171 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +322 -1827
- package/dist/client.js +9 -0
- package/dist/codex-patch.js +1 -1
- package/dist/compaction-record.d.ts +1 -1
- package/dist/compaction-record.js +3 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/github/dlq.js +5 -2
- package/dist/connectors/github/octokit-client.js +4 -2
- package/dist/connectors/github/webhook.d.ts +19 -0
- package/dist/connectors/github/webhook.js +313 -0
- package/dist/connectors/slack/dlq.js +6 -2
- package/dist/connectors/slack/web-client.js +7 -5
- package/dist/connectors/slack/webhook.d.ts +22 -0
- package/dist/connectors/slack/webhook.js +203 -0
- package/dist/consolidate.d.ts +10 -0
- package/dist/consolidate.js +38 -35
- package/dist/context-auto.d.ts +3 -0
- package/dist/context-auto.js +34 -0
- package/dist/customer-notes.js +16 -14
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +3 -2
- package/dist/db.d.ts +12 -0
- package/dist/db.js +62 -1
- package/dist/decisions.js +11 -9
- package/dist/doctor.js +5 -0
- package/dist/embedding-provider.js +3 -3
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +72 -16
- package/dist/eval-stats.d.ts +58 -0
- package/dist/eval-stats.js +111 -0
- package/dist/extract.js +3 -2
- package/dist/goals.d.ts +49 -25
- package/dist/goals.js +39 -22
- package/dist/graph-extract.js +1 -1
- package/dist/graph-recall.d.ts +1 -1
- package/dist/graph-recall.js +1 -1
- package/dist/graph.js +1 -1
- package/dist/hooks.d.ts +1 -3
- package/dist/hooks.js +2 -4
- package/dist/http-retry.d.ts +21 -0
- package/dist/http-retry.js +50 -0
- package/dist/http-util.d.ts +39 -0
- package/dist/http-util.js +56 -0
- package/dist/importers.d.ts +2 -0
- package/dist/importers.js +16 -5
- package/dist/incidents.js +13 -11
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/judgment.js +10 -17
- package/dist/log.d.ts +25 -0
- package/dist/log.js +48 -0
- package/dist/mcp/server.js +224 -308
- package/dist/mcp/tool-args.d.ts +21 -0
- package/dist/mcp/tool-args.js +80 -0
- package/dist/memory.d.ts +19 -0
- package/dist/memory.js +41 -2
- package/dist/overlap-index.d.ts +7 -0
- package/dist/overlap-index.js +38 -0
- package/dist/pilot-arm.d.ts +9 -0
- package/dist/pilot-arm.js +47 -0
- package/dist/policies.js +14 -12
- package/dist/predictions.js +11 -9
- package/dist/processes.js +16 -14
- package/dist/project-briefs.js +19 -16
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +25 -1
- package/dist/prompt-recall.js +1 -1
- package/dist/raw-archive.js +7 -6
- package/dist/recall-history.d.ts +5 -0
- package/dist/recall-history.js +9 -0
- package/dist/recall-pipeline.d.ts +101 -0
- package/dist/recall-pipeline.js +313 -0
- package/dist/recall-scope.d.ts +24 -1
- package/dist/recall-scope.js +29 -2
- package/dist/refine-llm.js +3 -2
- package/dist/reject-flow.js +6 -9
- package/dist/rejection.d.ts +2 -1
- package/dist/rejection.js +2 -1
- package/dist/search.d.ts +0 -20
- package/dist/search.js +16 -51
- package/dist/secret-detect.d.ts +13 -1
- package/dist/secret-detect.js +33 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +1854 -2566
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +17 -15
- package/dist/store-cards.d.ts +53 -0
- package/dist/store-cards.js +512 -0
- package/dist/store.d.ts +2 -89
- package/dist/store.js +10 -566
- package/dist/tenant.d.ts +22 -0
- package/dist/tenant.js +26 -0
- package/dist/token-ledger.d.ts +4 -2
- package/dist/token-ledger.js +2 -2
- package/dist/tokenize.d.ts +2 -0
- package/dist/tokenize.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/connectors/slack/ratelimit.d.ts +0 -9
- package/dist/connectors/slack/ratelimit.js +0 -18
package/dist/recall-scope.d.ts
CHANGED
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
* these for its own call sites AND re-exports them for back-compat
|
|
7
7
|
* (`api.isPrivateScope`, test imports of `passesScopeFilterForRecall`).
|
|
8
8
|
*/
|
|
9
|
+
import { ForbiddenError } from './api-errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* Literal scopes excluded from recall by default-deny when the
|
|
12
|
+
* caller passes no `scope`. The SQL clause in `loadSearchRows` and the JS
|
|
13
|
+
* helper `passesScopeFilterForRecall` (src/api.ts) both read from this
|
|
14
|
+
* constant. Adding a deny scope is a one-place change.
|
|
15
|
+
*
|
|
16
|
+
* Regex-based denies (e.g. `<source>:private:*`) stay in
|
|
17
|
+
* `passesScopeFilterForRecall` as a separate JS step — they don't translate
|
|
18
|
+
* cleanly to SQL.
|
|
19
|
+
*
|
|
20
|
+
* Invariant: never empty. An empty array would silently allow quarantine
|
|
21
|
+
* scopes through both paths (SQL clause omitted, JS check vacuous). The
|
|
22
|
+
* module-load assertion below pins this loudly.
|
|
23
|
+
*/
|
|
24
|
+
export declare const RECALL_DEFAULT_DENY_SCOPES: readonly ["unknown:legacy"];
|
|
25
|
+
/**
|
|
26
|
+
* @internal Runtime guard against a future maintainer blanking a
|
|
27
|
+
* load-bearing literal array. Extracted from the inline guard so the throw
|
|
28
|
+
* path is directly testable. `as const` arrays widen via `readonly T[]` at
|
|
29
|
+
* the call site so the empty case is reachable at runtime.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assertNonEmpty<T>(arr: readonly T[], name: string): void;
|
|
9
32
|
/**
|
|
10
33
|
* v1.2.1: source-agnostic private-scope detector. A scope string is treated
|
|
11
34
|
* as private when it has the shape `<lowercase-source>:private:<rest>`.
|
|
@@ -61,7 +84,7 @@ export declare function passesCliRecallScopeFilter(scope: string | null, request
|
|
|
61
84
|
* Thrown when a caller requests a scope its role may not read. The HTTP layer
|
|
62
85
|
* maps it to 403.
|
|
63
86
|
*/
|
|
64
|
-
export declare class ScopeForbiddenError extends
|
|
87
|
+
export declare class ScopeForbiddenError extends ForbiddenError {
|
|
65
88
|
readonly scope: string;
|
|
66
89
|
constructor(scope: string);
|
|
67
90
|
}
|
package/dist/recall-scope.js
CHANGED
|
@@ -6,7 +6,34 @@
|
|
|
6
6
|
* these for its own call sites AND re-exports them for back-compat
|
|
7
7
|
* (`api.isPrivateScope`, test imports of `passesScopeFilterForRecall`).
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { ForbiddenError } from './api-errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* Literal scopes excluded from recall by default-deny when the
|
|
12
|
+
* caller passes no `scope`. The SQL clause in `loadSearchRows` and the JS
|
|
13
|
+
* helper `passesScopeFilterForRecall` (src/api.ts) both read from this
|
|
14
|
+
* constant. Adding a deny scope is a one-place change.
|
|
15
|
+
*
|
|
16
|
+
* Regex-based denies (e.g. `<source>:private:*`) stay in
|
|
17
|
+
* `passesScopeFilterForRecall` as a separate JS step — they don't translate
|
|
18
|
+
* cleanly to SQL.
|
|
19
|
+
*
|
|
20
|
+
* Invariant: never empty. An empty array would silently allow quarantine
|
|
21
|
+
* scopes through both paths (SQL clause omitted, JS check vacuous). The
|
|
22
|
+
* module-load assertion below pins this loudly.
|
|
23
|
+
*/
|
|
24
|
+
export const RECALL_DEFAULT_DENY_SCOPES = ['unknown:legacy'];
|
|
25
|
+
/**
|
|
26
|
+
* @internal Runtime guard against a future maintainer blanking a
|
|
27
|
+
* load-bearing literal array. Extracted from the inline guard so the throw
|
|
28
|
+
* path is directly testable. `as const` arrays widen via `readonly T[]` at
|
|
29
|
+
* the call site so the empty case is reachable at runtime.
|
|
30
|
+
*/
|
|
31
|
+
export function assertNonEmpty(arr, name) {
|
|
32
|
+
if (arr.length === 0) {
|
|
33
|
+
throw new Error(`${name} cannot be empty — would silently allow quarantine scopes`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
assertNonEmpty(RECALL_DEFAULT_DENY_SCOPES, 'RECALL_DEFAULT_DENY_SCOPES');
|
|
10
37
|
/**
|
|
11
38
|
* v1.2.1: source-agnostic private-scope detector. A scope string is treated
|
|
12
39
|
* as private when it has the shape `<lowercase-source>:private:<rest>`.
|
|
@@ -79,7 +106,7 @@ export function passesCliRecallScopeFilter(scope, requested) {
|
|
|
79
106
|
* Thrown when a caller requests a scope its role may not read. The HTTP layer
|
|
80
107
|
* maps it to 403.
|
|
81
108
|
*/
|
|
82
|
-
export class ScopeForbiddenError extends
|
|
109
|
+
export class ScopeForbiddenError extends ForbiddenError {
|
|
83
110
|
scope;
|
|
84
111
|
constructor(scope) {
|
|
85
112
|
super(`scope ${scope} requires admin role`);
|
package/dist/refine-llm.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { Layer } from './memory.js';
|
|
16
16
|
import { loadAllEntries, readEntry, writeEntry } from './store.js';
|
|
17
17
|
import { redactSecrets } from './secret-detect.js';
|
|
18
|
+
import { fetchWithRetry, llmTimeoutMs } from './http-retry.js';
|
|
18
19
|
const REFINED_TAG = 'llm-refined';
|
|
19
20
|
const CONSOLIDATED_MARKERS = [
|
|
20
21
|
'[Consolidated from',
|
|
@@ -48,7 +49,7 @@ Source memories (up to 8 shown):
|
|
|
48
49
|
${sourceBlock}`;
|
|
49
50
|
let res;
|
|
50
51
|
try {
|
|
51
|
-
res = await
|
|
52
|
+
res = await fetchWithRetry('https://api.anthropic.com/v1/messages', {
|
|
52
53
|
method: 'POST',
|
|
53
54
|
headers: {
|
|
54
55
|
'content-type': 'application/json',
|
|
@@ -60,7 +61,7 @@ ${sourceBlock}`;
|
|
|
60
61
|
max_tokens: 800,
|
|
61
62
|
messages: [{ role: 'user', content: prompt }],
|
|
62
63
|
}),
|
|
63
|
-
});
|
|
64
|
+
}, { timeoutMs: llmTimeoutMs(), fetchFn });
|
|
64
65
|
}
|
|
65
66
|
catch {
|
|
66
67
|
return null;
|
package/dist/reject-flow.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* cli.ts and api.ts, so it introduces no cycle.
|
|
13
13
|
*/
|
|
14
14
|
import { closeHippoDb } from './db.js';
|
|
15
|
-
import { appendAuditEvent } from './audit.js';
|
|
15
|
+
import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
|
|
16
16
|
import { archiveRawMemory } from './raw-archive.js';
|
|
17
17
|
import { deleteDormantRow, listDormantSnapshots, purgeDormantByDigest, replaceDormantEntry } from './dormant.js';
|
|
18
18
|
import { openStore, deleteEntryCore, purgeMirrorBestEffort, selectAllEntries, stampOriginProject, writeEntryDbOnly, writeEntryMirrors, } from './store.js';
|
|
@@ -144,12 +144,9 @@ export function rejectValue(opts) {
|
|
|
144
144
|
metadata: { digest, removedIds, count: removedIds.length },
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
catch {
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
// statement): a swallowed audit failure lets the tombstone +
|
|
151
|
-
// removals commit without the trail row, rather than rolling the
|
|
152
|
-
// whole reject back over bookkeeping.
|
|
147
|
+
catch (error) {
|
|
148
|
+
// Inside the open transaction: the reject commits without its trail row rather than rolling back over bookkeeping.
|
|
149
|
+
reportAuditWriteFailure('reject_value', String(error), opts.memoryId);
|
|
153
150
|
}
|
|
154
151
|
db.exec('COMMIT');
|
|
155
152
|
}
|
|
@@ -217,8 +214,8 @@ export function unrejectValue(hippoRoot, tenantId, digestOrPrefix, actor) {
|
|
|
217
214
|
metadata: { digest: target.digest, reason: target.reason },
|
|
218
215
|
});
|
|
219
216
|
}
|
|
220
|
-
catch {
|
|
221
|
-
|
|
217
|
+
catch (error) {
|
|
218
|
+
reportAuditWriteFailure('unreject_value', String(error), target.sourceMemoryId);
|
|
222
219
|
}
|
|
223
220
|
return { status: 'ok', digest: target.digest, reason: target.reason };
|
|
224
221
|
}
|
package/dist/rejection.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* be a cycle.
|
|
13
13
|
*/
|
|
14
14
|
import type { DatabaseSyncLike } from './db.js';
|
|
15
|
+
import { BadRequestError } from './api-errors.js';
|
|
15
16
|
/**
|
|
16
17
|
* Normalize content for rejection-digest comparisons: Unicode NFC →
|
|
17
18
|
* lowercase → collapse whitespace runs to a single space → trim. No
|
|
@@ -33,7 +34,7 @@ export declare function rejectionDigest(content: string): string;
|
|
|
33
34
|
* blocks (writeEntry, api.supersede) to write a post-rollback
|
|
34
35
|
* `reject_refusal` audit row via `auditRejectionRefusal` (plan §3).
|
|
35
36
|
*/
|
|
36
|
-
export declare class RejectedValueError extends
|
|
37
|
+
export declare class RejectedValueError extends BadRequestError {
|
|
37
38
|
readonly digest: string;
|
|
38
39
|
readonly tenantId: string;
|
|
39
40
|
readonly entryId: string;
|
package/dist/rejection.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* be a cycle.
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
+
import { BadRequestError } from './api-errors.js';
|
|
15
16
|
/**
|
|
16
17
|
* Normalize content for rejection-digest comparisons: Unicode NFC →
|
|
17
18
|
* lowercase → collapse whitespace runs to a single space → trim. No
|
|
@@ -37,7 +38,7 @@ export function rejectionDigest(content) {
|
|
|
37
38
|
* blocks (writeEntry, api.supersede) to write a post-rollback
|
|
38
39
|
* `reject_refusal` audit row via `auditRejectionRefusal` (plan §3).
|
|
39
40
|
*/
|
|
40
|
-
export class RejectedValueError extends
|
|
41
|
+
export class RejectedValueError extends BadRequestError {
|
|
41
42
|
digest;
|
|
42
43
|
tenantId;
|
|
43
44
|
entryId;
|
package/dist/search.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { MemoryEntry } from './memory.js';
|
|
|
7
7
|
import type { PhysicsConfig } from './physics-config.js';
|
|
8
8
|
export declare const CHURN_STALE_RANK_MULTIPLIER = 0.5;
|
|
9
9
|
export declare function churnStaleFactor(entry: MemoryEntry): number;
|
|
10
|
-
export declare function tokenize(text: string): string[];
|
|
11
10
|
/**
|
|
12
11
|
* Tokenized BM25 corpus. Callers can pre-build this with `buildCorpus` once
|
|
13
12
|
* and reuse across many `hybridSearch` calls on the same entry set — the
|
|
@@ -287,25 +286,6 @@ export declare function search(query: string, entries: MemoryEntry[], options?:
|
|
|
287
286
|
includeSuperseded?: boolean;
|
|
288
287
|
asOf?: string;
|
|
289
288
|
}): SearchResult[];
|
|
290
|
-
/**
|
|
291
|
-
* Update retrieval metadata on entries that were returned by a search.
|
|
292
|
-
* Returns the mutated copies (caller must persist to disk).
|
|
293
|
-
*
|
|
294
|
-
* EVAL-ONLY ablation (see ablation.ts): with HIPPO_ABLATE_RECALL_BOOST set,
|
|
295
|
-
* this returns the entries UNMUTATED - neutralizing all three strengthening
|
|
296
|
-
* sub-effects (clock reset, retrieval_count, half-life increment) at the
|
|
297
|
-
* single shared write site. The entries (not an empty array) must be
|
|
298
|
-
* returned because callers derive `last_retrieval_ids` from the return
|
|
299
|
-
* value, and a later `hippo outcome --good/--bad` targets those ids - an
|
|
300
|
-
* empty return would silently co-ablate the outcome channel in the
|
|
301
|
-
* strengthen-off arm (codex round-7 P2). PERSISTENCE is gated separately at
|
|
302
|
-
* each persisting caller (CLI recall, api context, MCP recall/context,
|
|
303
|
-
* consolidation replay): writeEntry on identical rows still refreshes
|
|
304
|
-
* updated_at, rewrites mirrors, and marks DAG parents dirty (codex round-6
|
|
305
|
-
* P2), so those write loops skip under the flag.
|
|
306
|
-
* The default `now` honors HIPPO_FAKE_NOW (simulated-time protocols).
|
|
307
|
-
*/
|
|
308
|
-
export declare function markRetrieved(entries: MemoryEntry[], now?: Date): MemoryEntry[];
|
|
309
289
|
export interface MatchExplanation {
|
|
310
290
|
/** Human-readable reason string */
|
|
311
291
|
reason: string;
|
package/dist/search.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Zero external dependencies when embeddings are not available.
|
|
4
4
|
*/
|
|
5
5
|
import { estimateTokens } from './token-ledger.js';
|
|
6
|
-
import { calculateStrength,
|
|
6
|
+
import { calculateStrength, CHURN_STALE_TAG } from './memory.js';
|
|
7
|
+
import { tokenize } from './tokenize.js';
|
|
7
8
|
import { isOutcomeFastAblated, isRecallBoostAblated, isRecencyAblated, evalRecencyScaleDays, evalNow } from './ablation.js';
|
|
8
9
|
import { extractPathTags, pathBoostMultiplier } from './path-context.js';
|
|
9
10
|
import { detectScope, scopeMatch } from './scope.js';
|
|
@@ -16,20 +17,16 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
16
17
|
import { rrfFuse } from './rrf.js';
|
|
17
18
|
import { graphRankStream, selectGraphSeeds, DEFAULT_GRAPH_SEED_COUNT } from './graph-stream.js';
|
|
18
19
|
import { compareEntryIdentity, compareScoredResults } from './compare.js';
|
|
20
|
+
import { log } from './log.js';
|
|
21
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
19
22
|
export const CHURN_STALE_RANK_MULTIPLIER = 0.5; // SHORTCUT: untuned; FE3 measures before any default.
|
|
23
|
+
// Search runs on every hook prompt, so one line per reason per process says why vectors went unused without flooding stderr.
|
|
24
|
+
function warnBm25Fallback(reason, detail) {
|
|
25
|
+
log.once(`search.bm25-fallback.${reason}`, 'warn', `hybrid search fell back to BM25 only: ${redactSecretsStrict(detail)}`);
|
|
26
|
+
}
|
|
20
27
|
export function churnStaleFactor(entry) {
|
|
21
28
|
return entry.tags.includes(CHURN_STALE_TAG) ? CHURN_STALE_RANK_MULTIPLIER : 1.0;
|
|
22
29
|
}
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Tokenizer
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
export function tokenize(text) {
|
|
27
|
-
return text
|
|
28
|
-
.toLowerCase()
|
|
29
|
-
.replace(/[^\w\s]/g, ' ')
|
|
30
|
-
.split(/\s+/)
|
|
31
|
-
.filter((t) => t.length > 1);
|
|
32
|
-
}
|
|
33
30
|
const BM25_K1 = 1.5;
|
|
34
31
|
const BM25_B = 0.75;
|
|
35
32
|
export function buildCorpus(texts) {
|
|
@@ -276,11 +273,17 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
276
273
|
embeddingIndex = idx;
|
|
277
274
|
useEmbeddings = true;
|
|
278
275
|
}
|
|
276
|
+
else {
|
|
277
|
+
warnBm25Fallback('empty-query-vector', 'the embedding provider returned no vector for the query');
|
|
278
|
+
}
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
|
+
else if (provider.isAvailable()) {
|
|
282
|
+
warnBm25Fallback('reindex', "the embedding index was built by another model or is being rebuilt; run 'hippo embed'");
|
|
283
|
+
}
|
|
281
284
|
}
|
|
282
|
-
catch {
|
|
283
|
-
|
|
285
|
+
catch (err) {
|
|
286
|
+
warnBm25Fallback('error', err instanceof Error ? err.message : String(err));
|
|
284
287
|
}
|
|
285
288
|
}
|
|
286
289
|
// Compute cosine similarities for RRF ranking (need all before scoring)
|
|
@@ -902,44 +905,6 @@ export function search(query, entries, options = {}) {
|
|
|
902
905
|
}
|
|
903
906
|
return fitBudget(dedupedSync, budget, minResults, options.cost);
|
|
904
907
|
}
|
|
905
|
-
/**
|
|
906
|
-
* Update retrieval metadata on entries that were returned by a search.
|
|
907
|
-
* Returns the mutated copies (caller must persist to disk).
|
|
908
|
-
*
|
|
909
|
-
* EVAL-ONLY ablation (see ablation.ts): with HIPPO_ABLATE_RECALL_BOOST set,
|
|
910
|
-
* this returns the entries UNMUTATED - neutralizing all three strengthening
|
|
911
|
-
* sub-effects (clock reset, retrieval_count, half-life increment) at the
|
|
912
|
-
* single shared write site. The entries (not an empty array) must be
|
|
913
|
-
* returned because callers derive `last_retrieval_ids` from the return
|
|
914
|
-
* value, and a later `hippo outcome --good/--bad` targets those ids - an
|
|
915
|
-
* empty return would silently co-ablate the outcome channel in the
|
|
916
|
-
* strengthen-off arm (codex round-7 P2). PERSISTENCE is gated separately at
|
|
917
|
-
* each persisting caller (CLI recall, api context, MCP recall/context,
|
|
918
|
-
* consolidation replay): writeEntry on identical rows still refreshes
|
|
919
|
-
* updated_at, rewrites mirrors, and marks DAG parents dirty (codex round-6
|
|
920
|
-
* P2), so those write loops skip under the flag.
|
|
921
|
-
* The default `now` honors HIPPO_FAKE_NOW (simulated-time protocols).
|
|
922
|
-
*/
|
|
923
|
-
// Confidence is deliberately absent below: it is an epistemic tier, not a
|
|
924
|
-
// recency signal, and a stored 'stale' is always a deliberate mark (2026-09-07).
|
|
925
|
-
export function markRetrieved(entries, now = evalNow()) {
|
|
926
|
-
if (isRecallBoostAblated())
|
|
927
|
-
return entries;
|
|
928
|
-
return entries.map((e) => {
|
|
929
|
-
if (e.superseded_by)
|
|
930
|
-
return e;
|
|
931
|
-
const wrong = netWrong(e) > 0;
|
|
932
|
-
const updated = {
|
|
933
|
-
...e,
|
|
934
|
-
retrieval_count: e.retrieval_count + 1,
|
|
935
|
-
last_retrieved: wrong ? e.last_retrieved : now.toISOString(),
|
|
936
|
-
// +2 days half-life per retrieval (PLAN.md); a wrong memory keeps both, since last_retrieved is the decay anchor
|
|
937
|
-
half_life_days: wrong ? e.half_life_days : e.half_life_days + 2,
|
|
938
|
-
};
|
|
939
|
-
updated.strength = calculateStrength(updated, now);
|
|
940
|
-
return updated;
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
908
|
/**
|
|
944
909
|
* Explain why a search result matched a query.
|
|
945
910
|
* Computes which query terms overlapped with the document and whether
|
package/dist/secret-detect.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* a deliberate act.
|
|
13
13
|
*
|
|
14
14
|
* This is deliberately a thin slice of the A4 lifecycle-compliance item
|
|
15
|
-
* (no
|
|
15
|
+
* (no PII detection). Write-time scrubbing covers only text no person
|
|
16
|
+
* typed into hippo; see `vetSecrets`.
|
|
16
17
|
*
|
|
17
18
|
* Leaf module: keep free of imports from store/api/shared so all of them
|
|
18
19
|
* can import it without cycles.
|
|
@@ -42,4 +43,15 @@ export declare function redactSecrets(text: string): string;
|
|
|
42
43
|
export declare function maskEmails(text: string): string;
|
|
43
44
|
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer and Basic auth headers and JWTs. */
|
|
44
45
|
export declare function redactSecretsStrict(text: string): string;
|
|
46
|
+
/** The text a write keeps, plus what to tell its caller about secret material in it. */
|
|
47
|
+
export interface SecretVet {
|
|
48
|
+
content: string;
|
|
49
|
+
warnings: string[];
|
|
50
|
+
}
|
|
51
|
+
/** Write-time veto: `scrub` text no person typed gets capture's strict redaction; typed text is a person's own call, so it is kept and flagged. */
|
|
52
|
+
export declare function vetSecrets(content: string, tags: readonly string[], scrub: boolean): SecretVet;
|
|
53
|
+
/** Appended to a dead-letter row's error when `redactPayload` changed its body. */
|
|
54
|
+
export declare const DLQ_REDACTED_NOTE = "secret-shaped text redacted, signature dropped; replay needs --force";
|
|
55
|
+
/** `vetSecrets` redaction for a raw webhook body: JSON stays valid for replay, and an untouched body stays byte-identical so its signature still verifies. */
|
|
56
|
+
export declare function redactPayload(raw: string): string;
|
|
45
57
|
//# sourceMappingURL=secret-detect.d.ts.map
|
package/dist/secret-detect.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* a deliberate act.
|
|
13
13
|
*
|
|
14
14
|
* This is deliberately a thin slice of the A4 lifecycle-compliance item
|
|
15
|
-
* (no
|
|
15
|
+
* (no PII detection). Write-time scrubbing covers only text no person
|
|
16
|
+
* typed into hippo; see `vetSecrets`.
|
|
16
17
|
*
|
|
17
18
|
* Leaf module: keep free of imports from store/api/shared so all of them
|
|
18
19
|
* can import it without cycles.
|
|
@@ -100,6 +101,37 @@ export function maskEmails(text) {
|
|
|
100
101
|
export function redactSecretsStrict(text) {
|
|
101
102
|
return redactText(text, true);
|
|
102
103
|
}
|
|
104
|
+
/** Write-time veto: `scrub` text no person typed gets capture's strict redaction; typed text is a person's own call, so it is kept and flagged. */
|
|
105
|
+
export function vetSecrets(content, tags, scrub) {
|
|
106
|
+
const kept = scrub ? redactSecretsStrict(content) : content;
|
|
107
|
+
const warnings = [];
|
|
108
|
+
if (kept !== content) {
|
|
109
|
+
const found = detectSecret({ content, tags: [] }).reason;
|
|
110
|
+
warnings.push(`secret-shaped text was redacted before storing${found ? ` (${found})` : ''}`);
|
|
111
|
+
}
|
|
112
|
+
const left = detectSecret({ content: kept, tags: [...tags] });
|
|
113
|
+
if (left.flagged) {
|
|
114
|
+
warnings.push(`content looks like a secret (${left.reason}) and was stored as sent; forget it if it should not be kept`);
|
|
115
|
+
}
|
|
116
|
+
return { content: kept, warnings };
|
|
117
|
+
}
|
|
118
|
+
const JSON_STRING = /"(?:[^"\\]|\\.)*"/g;
|
|
119
|
+
/** Appended to a dead-letter row's error when `redactPayload` changed its body. */
|
|
120
|
+
export const DLQ_REDACTED_NOTE = 'secret-shaped text redacted, signature dropped; replay needs --force';
|
|
121
|
+
/** `vetSecrets` redaction for a raw webhook body: JSON stays valid for replay, and an untouched body stays byte-identical so its signature still verifies. */
|
|
122
|
+
export function redactPayload(raw) {
|
|
123
|
+
try {
|
|
124
|
+
JSON.parse(raw);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return redactSecretsStrict(raw);
|
|
128
|
+
}
|
|
129
|
+
return raw.replace(JSON_STRING, (literal) => {
|
|
130
|
+
const text = JSON.parse(literal);
|
|
131
|
+
const kept = redactSecretsStrict(text);
|
|
132
|
+
return kept === text ? literal : JSON.stringify(kept);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
103
135
|
function redactText(text, strict) {
|
|
104
136
|
if (!text)
|
|
105
137
|
return text;
|
package/dist/server.d.ts
CHANGED
|
@@ -66,7 +66,9 @@ export declare function clientIpForRateLimit(req: IncomingMessage): string;
|
|
|
66
66
|
* webhooks in PUBLIC_ROUTES, which are HMAC-gated by their own signing
|
|
67
67
|
* secrets and 404 when those secrets are unset. But the loopback
|
|
68
68
|
* no-auth fallback inside buildContextWithAuth still admits unauthenticated
|
|
69
|
-
* requests from a loopback remote address
|
|
69
|
+
* requests from a loopback remote address (unless they carry Forwarded,
|
|
70
|
+
* X-Forwarded-For/-Host/-Proto, X-Real-IP, Cf-Connecting-Ip or True-Client-Ip, which mark a same-host proxy and get
|
|
71
|
+
* a 401 like any keyless remote request), so binding to a non-loopback host
|
|
70
72
|
* is only safe once that fallback is disabled with HIPPO_REQUIRE_AUTH=1,
|
|
71
73
|
* which forces every request (loopback or not) through Bearer-token
|
|
72
74
|
* validation. Without that env var set, a non-loopback bind would expose the
|