hippo-memory 1.57.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 +5 -5
- package/dist/api.js +40 -47
- 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/cli/shared.js +10 -6
- package/dist/cli.js +100 -39
- 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/slack/dlq.js +6 -2
- package/dist/connectors/slack/web-client.js +7 -5
- package/dist/consolidate.d.ts +10 -0
- package/dist/consolidate.js +36 -34
- package/dist/customer-notes.js +14 -13
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +1 -1
- package/dist/db.d.ts +12 -0
- package/dist/db.js +62 -1
- package/dist/decisions.js +9 -8
- 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/extract.js +3 -2
- package/dist/http-retry.d.ts +21 -0
- package/dist/http-retry.js +50 -0
- package/dist/http-util.d.ts +8 -0
- package/dist/http-util.js +10 -0
- package/dist/importers.d.ts +2 -0
- package/dist/importers.js +16 -5
- package/dist/incidents.js +11 -10
- package/dist/judgment.js +10 -17
- package/dist/log.d.ts +25 -0
- package/dist/log.js +48 -0
- package/dist/mcp/server.js +52 -24
- package/dist/mcp/tool-args.d.ts +21 -0
- package/dist/mcp/tool-args.js +80 -0
- package/dist/memory.js +3 -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 +12 -11
- package/dist/predictions.js +9 -8
- package/dist/processes.js +14 -13
- package/dist/project-briefs.js +16 -15
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +25 -1
- package/dist/raw-archive.js +7 -6
- package/dist/recall-scope.d.ts +2 -1
- package/dist/recall-scope.js +2 -1
- 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.js +14 -2
- 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 +180 -409
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +15 -14
- package/dist/store.js +4 -4
- package/dist/token-ledger.d.ts +4 -2
- package/dist/token-ledger.js +2 -2
- 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/project-briefs.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* Lifecycle: active -> superseded (a newer version replaces it) or active ->
|
|
22
22
|
* closed (retired).
|
|
23
23
|
*/
|
|
24
|
+
import { BadRequestError, ConflictError, NotFoundError } from './api-errors.js';
|
|
24
25
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
25
26
|
import { writeEntry } from './store.js';
|
|
26
27
|
import { assertTenantId } from './tenant.js';
|
|
@@ -56,21 +57,21 @@ export const MAX_RECEIPT_HEADLINE_LEN = 200;
|
|
|
56
57
|
function validateBriefFields(repo, summary, changeSummary) {
|
|
57
58
|
const normalizedRepo = (repo ?? '').trim();
|
|
58
59
|
if (normalizedRepo.length === 0)
|
|
59
|
-
throw new
|
|
60
|
+
throw new BadRequestError('saveProjectBrief: repo is required');
|
|
60
61
|
if (/[\r\n]/.test(normalizedRepo)) {
|
|
61
|
-
throw new
|
|
62
|
+
throw new BadRequestError('saveProjectBrief: repo must be a single line (no newlines)');
|
|
62
63
|
}
|
|
63
64
|
if (normalizedRepo.length > MAX_REPO_LEN) {
|
|
64
|
-
throw new
|
|
65
|
+
throw new BadRequestError(`saveProjectBrief: repo exceeds the ${MAX_REPO_LEN}-char cap`);
|
|
65
66
|
}
|
|
66
67
|
if (!summary || summary.trim().length === 0) {
|
|
67
|
-
throw new
|
|
68
|
+
throw new BadRequestError('saveProjectBrief: summary is required');
|
|
68
69
|
}
|
|
69
70
|
if (summary.length > MAX_BRIEF_SUMMARY_LEN) {
|
|
70
|
-
throw new
|
|
71
|
+
throw new BadRequestError(`saveProjectBrief: summary exceeds the ${MAX_BRIEF_SUMMARY_LEN}-char cap`);
|
|
71
72
|
}
|
|
72
73
|
if (changeSummary !== undefined && changeSummary.length > MAX_CHANGE_SUMMARY_LEN) {
|
|
73
|
-
throw new
|
|
74
|
+
throw new BadRequestError(`saveProjectBrief: changeSummary exceeds the ${MAX_CHANGE_SUMMARY_LEN}-char cap`);
|
|
74
75
|
}
|
|
75
76
|
return { repo: normalizedRepo };
|
|
76
77
|
}
|
|
@@ -145,10 +146,10 @@ export function saveProjectBrief(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
|
145
146
|
// shape for the matching row, or undefined when no brief/tenant pair matches.
|
|
146
147
|
const pred = db.prepare(`SELECT status, version FROM project_briefs WHERE id = ? AND tenant_id = ?`).get(opts.supersedesBriefId, tenantId);
|
|
147
148
|
if (!pred) {
|
|
148
|
-
throw new
|
|
149
|
+
throw new NotFoundError(`saveProjectBrief: brief ${opts.supersedesBriefId} to supersede not found for tenant ${tenantId}`);
|
|
149
150
|
}
|
|
150
151
|
if (pred.status !== 'active') {
|
|
151
|
-
throw new
|
|
152
|
+
throw new ConflictError(`saveProjectBrief: brief ${opts.supersedesBriefId} is not active (status='${pred.status}'); only active briefs can be superseded.`);
|
|
152
153
|
}
|
|
153
154
|
version = pred.version + 1;
|
|
154
155
|
}
|
|
@@ -166,7 +167,7 @@ export function saveProjectBrief(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
|
166
167
|
WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
|
|
167
168
|
`).run(briefId, now, opts.supersedesBriefId, tenantId, briefId);
|
|
168
169
|
if (sup.changes === 0) {
|
|
169
|
-
throw new
|
|
170
|
+
throw new ConflictError(`saveProjectBrief: brief ${opts.supersedesBriefId} could not be superseded (no longer active or self-reference).`);
|
|
170
171
|
}
|
|
171
172
|
appendAuditEvent(db, {
|
|
172
173
|
tenantId,
|
|
@@ -234,9 +235,9 @@ export function closeProjectBrief(hippoRoot, tenantId, id, actor = 'cli') {
|
|
|
234
235
|
// shape, or undefined when the id/tenant pair doesn't exist.
|
|
235
236
|
const existing = db.prepare(`SELECT status FROM project_briefs WHERE id = ? AND tenant_id = ?`).get(id, tenantId);
|
|
236
237
|
if (!existing) {
|
|
237
|
-
throw new
|
|
238
|
+
throw new NotFoundError(`closeProjectBrief: brief ${id} not found for tenant ${tenantId}`);
|
|
238
239
|
}
|
|
239
|
-
throw new
|
|
240
|
+
throw new ConflictError(`closeProjectBrief: brief ${id} is not active (status='${existing.status}'); only active briefs can be closed.`);
|
|
240
241
|
}
|
|
241
242
|
// SAFETY: SELECT ${BRIEF_COLS} projects exactly the ProjectBriefRow
|
|
242
243
|
// columns; .get() returns that row for the just-updated id, or undefined
|
|
@@ -244,7 +245,7 @@ export function closeProjectBrief(hippoRoot, tenantId, id, actor = 'cli') {
|
|
|
244
245
|
const row = db.prepare(`SELECT ${BRIEF_COLS} FROM project_briefs WHERE id = ? AND tenant_id = ?`)
|
|
245
246
|
.get(id, tenantId);
|
|
246
247
|
if (!row)
|
|
247
|
-
throw new
|
|
248
|
+
throw new NotFoundError(`closeProjectBrief: brief ${id} not found after UPDATE`);
|
|
248
249
|
appendAuditEvent(db, {
|
|
249
250
|
tenantId,
|
|
250
251
|
actor,
|
|
@@ -298,7 +299,7 @@ export function loadProjectBriefs(hippoRoot, tenantId, opts = {}) {
|
|
|
298
299
|
assertTenantId('loadProjectBriefs', tenantId);
|
|
299
300
|
const limit = opts.limit ?? 100;
|
|
300
301
|
if (opts.status && !VALID_BRIEF_STATES.has(opts.status)) {
|
|
301
|
-
throw new
|
|
302
|
+
throw new BadRequestError(`loadProjectBriefs: status must be one of ${Array.from(VALID_BRIEF_STATES).join('|')}; got ${opts.status}`);
|
|
302
303
|
}
|
|
303
304
|
const db = openHippoDb(hippoRoot);
|
|
304
305
|
try {
|
|
@@ -385,7 +386,7 @@ export function assembleBriefFromReceipts(hippoRoot, tenantId, repo) {
|
|
|
385
386
|
assertTenantId('assembleBriefFromReceipts', tenantId);
|
|
386
387
|
const normalizedRepo = (repo ?? '').trim();
|
|
387
388
|
if (normalizedRepo.length === 0) {
|
|
388
|
-
throw new
|
|
389
|
+
throw new BadRequestError('assembleBriefFromReceipts: repo is required');
|
|
389
390
|
}
|
|
390
391
|
const tag = `path:${normalizedRepo.toLowerCase()}`;
|
|
391
392
|
const likeParam = `%"${escapeLike(tag)}"%`;
|
|
@@ -478,7 +479,7 @@ export function refreshBrief(hippoRoot, tenantId, repo, actor = 'cli') {
|
|
|
478
479
|
assertTenantId('refreshBrief', tenantId);
|
|
479
480
|
const normalizedRepo = (repo ?? '').trim();
|
|
480
481
|
if (normalizedRepo.length === 0)
|
|
481
|
-
throw new
|
|
482
|
+
throw new BadRequestError('refreshBrief: repo is required');
|
|
482
483
|
const { markdown, receiptCount } = assembleBriefFromReceipts(hippoRoot, tenantId, normalizedRepo);
|
|
483
484
|
const active = loadActiveBriefForRepo(hippoRoot, tenantId, normalizedRepo);
|
|
484
485
|
return saveProjectBrief(hippoRoot, tenantId, {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
export interface ProjectIdentity {
|
|
20
20
|
/** Realpath-resolved root directory of the project (the start dir when not in a project). */
|
|
21
21
|
root: string;
|
|
22
|
-
/** Lowercased basename of the project root
|
|
22
|
+
/** Lowercased basename of the project root, or of its repo for a linked worktree with no `.hippo` (a store keeps the name its rows carry); '' outside a project. */
|
|
23
23
|
name: string;
|
|
24
24
|
/** True when the directory resolves to the user home working set. */
|
|
25
25
|
isHome: boolean;
|
package/dist/project-identity.js
CHANGED
|
@@ -66,7 +66,8 @@ export function resolveProjectIdentity(cwd, opts) {
|
|
|
66
66
|
let identity;
|
|
67
67
|
const root = hippoRoot ?? gitRoot;
|
|
68
68
|
if (root !== null) {
|
|
69
|
-
|
|
69
|
+
const name = (hippoRoot === null ? linkedWorktreeRepoName(root, home) : null) ?? path.basename(root).toLowerCase();
|
|
70
|
+
identity = { root, name, isHome: false };
|
|
70
71
|
}
|
|
71
72
|
else if (reachedHome || isUnder(start, home)) {
|
|
72
73
|
identity = { root: home, name: '', isHome: true };
|
|
@@ -80,6 +81,29 @@ export function resolveProjectIdentity(cwd, opts) {
|
|
|
80
81
|
identityCache.set(startInput, identity);
|
|
81
82
|
return identity;
|
|
82
83
|
}
|
|
84
|
+
/** The repo name a linked worktree shares with its main checkout (`repo.git` or `repo/.bare` for a bare repo), so one repo is one project; null for any other checkout. */
|
|
85
|
+
function linkedWorktreeRepoName(gitRoot, home) {
|
|
86
|
+
const marker = path.join(gitRoot, '.git');
|
|
87
|
+
if (!fs.existsSync(marker) || isDirectoryAt(marker))
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
const gitDir = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(marker, 'utf8'))?.[1]?.trim();
|
|
91
|
+
if (!gitDir)
|
|
92
|
+
return null;
|
|
93
|
+
const linkDir = path.resolve(gitRoot, gitDir);
|
|
94
|
+
const commondir = path.join(linkDir, 'commondir');
|
|
95
|
+
if (!fs.existsSync(commondir))
|
|
96
|
+
return null; // a submodule's or a separate git dir's own checkout
|
|
97
|
+
const common = realpathOrResolve(path.resolve(linkDir, fs.readFileSync(commondir, 'utf8').trim()));
|
|
98
|
+
const repo = ['.git', '.bare'].includes(path.basename(common)) ? path.dirname(common) : common;
|
|
99
|
+
if (samePath(repo, home))
|
|
100
|
+
return null; // a dotfiles repo at home must not make its worktrees user-global
|
|
101
|
+
return path.basename(repo).replace(/\.git$/i, '').toLowerCase() || null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null; // an unreadable link is still a checkout, named after its own folder
|
|
105
|
+
}
|
|
106
|
+
}
|
|
83
107
|
/** Climb from start toward the root; home (never a project) and every stop dir end the walk unchecked. */
|
|
84
108
|
function walkProjectMarkers(start, home, stopDirs) {
|
|
85
109
|
let hippoRoot = null;
|
package/dist/raw-archive.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { BadRequestError, NotFoundError } from './api-errors.js';
|
|
1
2
|
import { isFtsAvailable } from './db.js';
|
|
2
|
-
import { appendAuditEvent } from './audit.js';
|
|
3
|
+
import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
|
|
3
4
|
import { markSummaryDirtyInTx } from './store.js';
|
|
4
5
|
export function archiveRawMemory(db, id, opts) {
|
|
5
6
|
// SAFETY: SELECT * FROM memories returns every column of the memories table; only
|
|
@@ -7,9 +8,9 @@ export function archiveRawMemory(db, id, opts) {
|
|
|
7
8
|
// null) by the memories schema.
|
|
8
9
|
const row = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id);
|
|
9
10
|
if (!row)
|
|
10
|
-
throw new
|
|
11
|
+
throw new NotFoundError(`memory not found: ${id}`);
|
|
11
12
|
if (row.kind !== 'raw') {
|
|
12
|
-
throw new
|
|
13
|
+
throw new BadRequestError(`memory ${id} is not raw (kind=${String(row.kind)})`);
|
|
13
14
|
}
|
|
14
15
|
// SAVEPOINT (not BEGIN) so this works whether or not we're already inside a
|
|
15
16
|
// transaction. SQLite refuses BEGIN within a transaction; SAVEPOINT nests safely.
|
|
@@ -58,9 +59,9 @@ export function archiveRawMemory(db, id, opts) {
|
|
|
58
59
|
metadata: { reason: opts.reason },
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
|
-
catch {
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
catch (error) {
|
|
63
|
+
// The archive itself has already succeeded; an unwritable audit table must not undo it.
|
|
64
|
+
reportAuditWriteFailure('archive_raw', String(error), id);
|
|
64
65
|
}
|
|
65
66
|
// v0.30 / E2 — DAG live-coupling: archive of a child under a level-2
|
|
66
67
|
// summary marks parent dirty. Inside the SAVEPOINT so the dirty-mark
|
package/dist/recall-scope.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
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';
|
|
9
10
|
/**
|
|
10
11
|
* Literal scopes excluded from recall by default-deny when the
|
|
11
12
|
* caller passes no `scope`. The SQL clause in `loadSearchRows` and the JS
|
|
@@ -83,7 +84,7 @@ export declare function passesCliRecallScopeFilter(scope: string | null, request
|
|
|
83
84
|
* Thrown when a caller requests a scope its role may not read. The HTTP layer
|
|
84
85
|
* maps it to 403.
|
|
85
86
|
*/
|
|
86
|
-
export declare class ScopeForbiddenError extends
|
|
87
|
+
export declare class ScopeForbiddenError extends ForbiddenError {
|
|
87
88
|
readonly scope: string;
|
|
88
89
|
constructor(scope: string);
|
|
89
90
|
}
|
package/dist/recall-scope.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
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';
|
|
9
10
|
/**
|
|
10
11
|
* Literal scopes excluded from recall by default-deny when the
|
|
11
12
|
* caller passes no `scope`. The SQL clause in `loadSearchRows` and the JS
|
|
@@ -105,7 +106,7 @@ export function passesCliRecallScopeFilter(scope, requested) {
|
|
|
105
106
|
* Thrown when a caller requests a scope its role may not read. The HTTP layer
|
|
106
107
|
* maps it to 403.
|
|
107
108
|
*/
|
|
108
|
-
export class ScopeForbiddenError extends
|
|
109
|
+
export class ScopeForbiddenError extends ForbiddenError {
|
|
109
110
|
scope;
|
|
110
111
|
constructor(scope) {
|
|
111
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.js
CHANGED
|
@@ -17,7 +17,13 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
17
17
|
import { rrfFuse } from './rrf.js';
|
|
18
18
|
import { graphRankStream, selectGraphSeeds, DEFAULT_GRAPH_SEED_COUNT } from './graph-stream.js';
|
|
19
19
|
import { compareEntryIdentity, compareScoredResults } from './compare.js';
|
|
20
|
+
import { log } from './log.js';
|
|
21
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
20
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
|
+
}
|
|
21
27
|
export function churnStaleFactor(entry) {
|
|
22
28
|
return entry.tags.includes(CHURN_STALE_TAG) ? CHURN_STALE_RANK_MULTIPLIER : 1.0;
|
|
23
29
|
}
|
|
@@ -267,11 +273,17 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
267
273
|
embeddingIndex = idx;
|
|
268
274
|
useEmbeddings = true;
|
|
269
275
|
}
|
|
276
|
+
else {
|
|
277
|
+
warnBm25Fallback('empty-query-vector', 'the embedding provider returned no vector for the query');
|
|
278
|
+
}
|
|
270
279
|
}
|
|
271
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
|
+
}
|
|
272
284
|
}
|
|
273
|
-
catch {
|
|
274
|
-
|
|
285
|
+
catch (err) {
|
|
286
|
+
warnBm25Fallback('error', err instanceof Error ? err.message : String(err));
|
|
275
287
|
}
|
|
276
288
|
}
|
|
277
289
|
// Compute cosine similarities for RRF ranking (need all before scoring)
|
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
|