hippo-memory 1.63.2 → 1.64.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 +10 -4
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -0
- 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 +8 -4
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type Context, type ContextResult, type ContextResultEntry } from './api.js';
|
|
2
|
+
import { type openHippoDb } from './db.js';
|
|
3
|
+
import type { DeliveryRecorder } from './delivery-recorder.js';
|
|
4
|
+
import { type JsonValue } from './json.js';
|
|
5
|
+
import type { MemoryEntry } from './memory.js';
|
|
6
|
+
import { type PilotArm } from './pilot-arm.js';
|
|
7
|
+
/** With `db`, writes on the token ledger's handle (same store); without it, opens its own. A second flush is a no-op. */
|
|
8
|
+
export declare function flushDeliveryRecorder(rec: DeliveryRecorder | null, db?: ReturnType<typeof openHippoDb>): void;
|
|
9
|
+
/** What one render needs once api.getContext has answered. */
|
|
10
|
+
export interface ContextView {
|
|
11
|
+
readonly hippoRoot: string;
|
|
12
|
+
readonly tenantId: string;
|
|
13
|
+
readonly ledgerSessionId: string | undefined;
|
|
14
|
+
readonly payloadSessionId: string | undefined;
|
|
15
|
+
readonly pinnedOnly: boolean;
|
|
16
|
+
readonly framing: string;
|
|
17
|
+
readonly rec: DeliveryRecorder | null;
|
|
18
|
+
readonly result: ContextResult;
|
|
19
|
+
/** Ledger rows stay in `hippoRoot`, never the global store; see promptHookContext. */
|
|
20
|
+
readonly sharedStore?: boolean;
|
|
21
|
+
/** The static block's hash the caller says it printed last. Undefined keeps the ledger-only skip rule;
|
|
22
|
+
* a string or null also needs that hash to match, because the ledger counts blocks a caller that prints elsewhere may never have shown. */
|
|
23
|
+
readonly printedHash?: string | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function hasContextData(result: ContextResult): boolean;
|
|
26
|
+
export type RenderItem = {
|
|
27
|
+
entry: MemoryEntry;
|
|
28
|
+
score: number;
|
|
29
|
+
tokens: number;
|
|
30
|
+
isGlobal: boolean;
|
|
31
|
+
};
|
|
32
|
+
export declare function toRenderItems(entries: ContextResultEntry[]): RenderItem[];
|
|
33
|
+
/** The hook format's stdout, '' when nothing is sent: a skippable static block (snapshot, handoff, events, pins, recent-N) and a never-skipped recall block. */
|
|
34
|
+
export declare function additionalContextOutput(view: ContextView): string;
|
|
35
|
+
/** The project a hook caller runs in: its id, the folder name its older rows carry, and any other names it resolves to. */
|
|
36
|
+
export interface CallerProject {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly legacyName: string;
|
|
39
|
+
readonly aliases?: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
interface PromptHookRequest {
|
|
42
|
+
readonly sessionId: string;
|
|
43
|
+
readonly project: CallerProject;
|
|
44
|
+
readonly payload?: Readonly<Record<string, JsonValue>>;
|
|
45
|
+
/** The staticHash of the last reply whose stdout the caller printed in this session; absent, the block is always sent. */
|
|
46
|
+
readonly printedHash?: string;
|
|
47
|
+
}
|
|
48
|
+
interface PromptHookOpts {
|
|
49
|
+
/** The store serves many people, so the caller is not its owner: no global store, no user-global recent rows,
|
|
50
|
+
* task state only for the caller's own owner and project, and no ledger rows outside this store. */
|
|
51
|
+
readonly sharedStore?: true;
|
|
52
|
+
}
|
|
53
|
+
/** The caps on a hook caller's session id and project names, so a caller other than an HTTP route is bounded too. */
|
|
54
|
+
export declare function assertCallerIds(sessionId: string, project: CallerProject): void;
|
|
55
|
+
/** The text `hippo context --pinned-only --include-recent 5 --format additional-context` prints for this session, read on `ctx`'s store for the caller's project, leaving out an unchanged static block only when `printedHash` matches it.
|
|
56
|
+
* `arm` is the raw ledger arm (`hippo` or `holdout`), null at rate 0; a holdout session gets an empty stdout. `staticHash` is the hash to echo back: null with no static block, for a sub-agent or a holdout.
|
|
57
|
+
* Scope detection (HIPPO_SCOPE and skill env vars) and delivery-ledger events are CLI-only.
|
|
58
|
+
* Throws BadRequestError past the input caps, and on a shared store for a project assertCallerProject refuses, before any arm is booked. */
|
|
59
|
+
export declare function promptHookContext(ctx: Context, req: PromptHookRequest, opts?: PromptHookOpts): Promise<{
|
|
60
|
+
arm: PilotArm | null;
|
|
61
|
+
stdout: string;
|
|
62
|
+
staticHash: string | null;
|
|
63
|
+
}>;
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=prompt-hook.d.ts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// The per-prompt hook's block and ledger rows, rendered here for `hippo context` and for promptHookContext (a remote hook route's call).
|
|
2
|
+
// promptHookContext keeps the server-side copy of the CLI's getContext flags (cli/context.ts); the CLI parity case in tests/prompt-hook-context.test.ts is all that ties the two.
|
|
3
|
+
import { getContext } from './api.js';
|
|
4
|
+
import { BadRequestError } from './api-errors.js';
|
|
5
|
+
import { isSharedStore, loadConfig } from './config.js';
|
|
6
|
+
import { contextBlockLines, contextCost, crossProjectLines, handoffText, sessionTrailText, settleTokens, snapshotText } from './context-render.js';
|
|
7
|
+
import { isSqliteBusy, noteStoreBusy } from './db.js';
|
|
8
|
+
import { MAX_ID_LEN } from './http-util.js';
|
|
9
|
+
import { isJsonString } from './json.js';
|
|
10
|
+
import { withLedgerDb } from './ledger-db.js';
|
|
11
|
+
import { sessionPilotArm } from './pilot-arm.js';
|
|
12
|
+
import { assertCallerProject, MAX_PROJECT_ALIASES } from './project-identity.js';
|
|
13
|
+
import { writeDeliveryEventAtRoot, writeDeliveryEventOnHandle } from './recall-trace.js';
|
|
14
|
+
import { blockHash, estimateTokens, isSubagentPayload, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
15
|
+
/** With `db`, writes on the token ledger's handle (same store); without it, opens its own. A second flush is a no-op. */
|
|
16
|
+
export function flushDeliveryRecorder(rec, db) {
|
|
17
|
+
if (rec === null)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
rec.flush((input) => (db ? writeDeliveryEventOnHandle(db, input) : writeDeliveryEventAtRoot(rec.root, input)));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// The hook's one-line stderr contract pins this exact text, so it bypasses the leveled logger.
|
|
24
|
+
console.error(`[hippo] delivery ledger write failed:${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const NOTHING_RENDERED = Object.freeze({ stdout: '', staticHash: null });
|
|
28
|
+
export function hasContextData(result) {
|
|
29
|
+
return Boolean(result.entries.length > 0 ||
|
|
30
|
+
result.activeSnapshot ||
|
|
31
|
+
result.sessionHandoff ||
|
|
32
|
+
(result.recentEvents && result.recentEvents.length > 0));
|
|
33
|
+
}
|
|
34
|
+
export function toRenderItems(entries) {
|
|
35
|
+
return entries.map((r) => ({ entry: r.entry, score: r.score, tokens: r.tokens, isGlobal: r.isGlobal ?? false }));
|
|
36
|
+
}
|
|
37
|
+
/** The hook format's stdout, '' when nothing is sent: a skippable static block (snapshot, handoff, events, pins, recent-N) and a never-skipped recall block. */
|
|
38
|
+
export function additionalContextOutput(view) {
|
|
39
|
+
return renderAdditionalContext(view).stdout;
|
|
40
|
+
}
|
|
41
|
+
function renderAdditionalContext(view) {
|
|
42
|
+
const { result, rec, framing } = view;
|
|
43
|
+
const staticEntries = result.entries.filter((r) => r.category !== 'cross-project' && !r.promptRecall);
|
|
44
|
+
const staticCrossEntries = result.entries.filter((r) => r.category === 'cross-project' && !r.promptRecall);
|
|
45
|
+
const staticItems = toRenderItems(staticEntries);
|
|
46
|
+
const recallItems = toRenderItems(result.entries.filter((r) => r.promptRecall));
|
|
47
|
+
const staticBlock = settleTokens((t) => [
|
|
48
|
+
...(result.activeSnapshot ? [snapshotText(result.activeSnapshot)] : []),
|
|
49
|
+
...(result.sessionHandoff ? [handoffText(result.sessionHandoff)] : []),
|
|
50
|
+
...(result.recentEvents && result.recentEvents.length > 0 ? [sessionTrailText(result.recentEvents)] : []),
|
|
51
|
+
// No live strength percentage, so an unchanged set of memories renders byte-identically turn after turn.
|
|
52
|
+
...(staticItems.length > 0 ? contextBlockLines(staticItems, t, framing, { showStrength: false }) : []),
|
|
53
|
+
...crossProjectLines(staticCrossEntries),
|
|
54
|
+
].join('\n'));
|
|
55
|
+
const recallBlock = recallItems.length > 0
|
|
56
|
+
? settleTokens((t) => contextBlockLines(recallItems, t, framing, { showStrength: false, heading: 'Prompt-Relevant Memory' }).join('\n'))
|
|
57
|
+
: '';
|
|
58
|
+
if (!staticBlock.trim() && !recallBlock.trim()) {
|
|
59
|
+
rec?.delivered({ state: 'empty' });
|
|
60
|
+
return NOTHING_RENDERED;
|
|
61
|
+
}
|
|
62
|
+
const surface = view.pinnedOnly ? 'hook' : 'context';
|
|
63
|
+
const sendStatic = staticBlock.trim().length > 0 && !skipUnchangedStatic(view, surface, staticBlock, recallBlock, staticItems.length);
|
|
64
|
+
const finalStatic = sendStatic ? staticBlock : '';
|
|
65
|
+
const additionalContext = finalStatic && recallBlock
|
|
66
|
+
? `${finalStatic}\n\n${recallBlock}`
|
|
67
|
+
: finalStatic || recallBlock;
|
|
68
|
+
const staticReused = !sendStatic && staticBlock.trim().length > 0;
|
|
69
|
+
if (!additionalContext.trim()) {
|
|
70
|
+
rec?.delivered({ state: 'reused', staticHash: blockHash(staticBlock), staticReused });
|
|
71
|
+
return NOTHING_RENDERED;
|
|
72
|
+
}
|
|
73
|
+
const stdout = JSON.stringify({
|
|
74
|
+
hookSpecificOutput: {
|
|
75
|
+
hookEventName: 'UserPromptSubmit',
|
|
76
|
+
additionalContext,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
rec?.delivered({
|
|
80
|
+
state: staticReused ? 'reused-recall-sent' : 'sent',
|
|
81
|
+
staticHash: staticBlock.trim() ? blockHash(staticBlock) : null,
|
|
82
|
+
recallHash: recallBlock ? blockHash(recallBlock) : null,
|
|
83
|
+
emittedText: additionalContext,
|
|
84
|
+
staticReused,
|
|
85
|
+
});
|
|
86
|
+
if (finalStatic || recallBlock) {
|
|
87
|
+
recordAdditionalContextRows(view, surface, { text: finalStatic, items: staticItems.length }, { text: recallBlock, items: recallItems.length });
|
|
88
|
+
}
|
|
89
|
+
return { stdout, staticHash: finalStatic ? blockHash(finalStatic) : null };
|
|
90
|
+
}
|
|
91
|
+
/** True, after booking the skip row, when the hook may omit a static block this session already holds. */
|
|
92
|
+
function skipUnchangedStatic(view, surface, staticBlock, recallBlock, staticCount) {
|
|
93
|
+
const { hippoRoot, payloadSessionId, rec } = view;
|
|
94
|
+
const ledgerOpts = { sharedStore: view.sharedStore };
|
|
95
|
+
if (!view.pinnedOnly || payloadSessionId === undefined)
|
|
96
|
+
return false;
|
|
97
|
+
const injectCfg = loadConfig(hippoRoot).pinnedInject;
|
|
98
|
+
if (injectCfg.skipUnchanged === false)
|
|
99
|
+
return false;
|
|
100
|
+
const refreshTurns = Number.isFinite(injectCfg.refreshTurns) && injectCfg.refreshTurns >= 0
|
|
101
|
+
? injectCfg.refreshTurns
|
|
102
|
+
: 10;
|
|
103
|
+
// Hashed on the static text alone so an unchanged pin set still skips while recall varies.
|
|
104
|
+
const staticHash = blockHash(staticBlock);
|
|
105
|
+
// Before the ledger read, so a caller that did not print this block books no skip row.
|
|
106
|
+
if (view.printedHash !== undefined && view.printedHash !== staticHash)
|
|
107
|
+
return false;
|
|
108
|
+
const last = withLedgerDb(hippoRoot, (db) => lastSentState(db, view.tenantId, payloadSessionId, surface), ledgerOpts);
|
|
109
|
+
if (!shouldSkipUnchanged(last ?? null, staticHash, refreshTurns))
|
|
110
|
+
return false;
|
|
111
|
+
withLedgerDb(hippoRoot, (db) => {
|
|
112
|
+
recordTokenUse(db, {
|
|
113
|
+
tenantId: view.tenantId, sessionId: payloadSessionId, surface, event: 'skip',
|
|
114
|
+
items: staticCount, tokens: estimateTokens(staticBlock), hash: staticHash,
|
|
115
|
+
});
|
|
116
|
+
if (recallBlock.trim())
|
|
117
|
+
return;
|
|
118
|
+
rec?.delivered({ state: 'reused', staticHash, staticReused: true });
|
|
119
|
+
flushDeliveryRecorder(rec, db);
|
|
120
|
+
}, ledgerOpts);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/** One connection for both rows; each insert in its own try so one failing doesn't skip the other. */
|
|
124
|
+
function recordAdditionalContextRows(view, surface, staticPart, recallPart) {
|
|
125
|
+
withLedgerDb(view.hippoRoot, (db) => {
|
|
126
|
+
if (staticPart.text) {
|
|
127
|
+
try {
|
|
128
|
+
recordTokenUse(db, {
|
|
129
|
+
tenantId: view.tenantId, sessionId: view.ledgerSessionId, surface, event: 'inject',
|
|
130
|
+
items: staticPart.items, tokens: estimateTokens(staticPart.text), hash: blockHash(staticPart.text),
|
|
131
|
+
});
|
|
132
|
+
// Best-effort row: only a busy store is actionable, and a ledger failure must not break the hook.
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (isSqliteBusy(error))
|
|
136
|
+
noteStoreBusy('token ledger row skipped');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (recallPart.text) {
|
|
140
|
+
try {
|
|
141
|
+
recordTokenUse(db, {
|
|
142
|
+
tenantId: view.tenantId, sessionId: view.ledgerSessionId, surface: 'hook_recall', event: 'inject',
|
|
143
|
+
items: recallPart.items, tokens: estimateTokens(recallPart.text), hash: blockHash(recallPart.text),
|
|
144
|
+
});
|
|
145
|
+
// Same best-effort rule as the inject row above.
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (isSqliteBusy(error))
|
|
149
|
+
noteStoreBusy('token ledger row skipped');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
flushDeliveryRecorder(view.rec, db);
|
|
153
|
+
}, { sharedStore: view.sharedStore });
|
|
154
|
+
}
|
|
155
|
+
// The flags HIPPO_PINNED_INJECT_COMMAND gives the local hook; the CLI parity test fails if the two drift.
|
|
156
|
+
const HOOK_INCLUDE_RECENT = 5;
|
|
157
|
+
const HOOK_BUDGET = 1500;
|
|
158
|
+
const HOOK_FRAMING = 'observe';
|
|
159
|
+
// blockHash's shape (token-ledger.ts).
|
|
160
|
+
const BLOCK_HASH_RE = /^[0-9a-f]{16}$/;
|
|
161
|
+
/** The caps on a hook caller's session id and project names, so a caller other than an HTTP route is bounded too. */
|
|
162
|
+
export function assertCallerIds(sessionId, project) {
|
|
163
|
+
const { name, legacyName, aliases = [] } = project;
|
|
164
|
+
if (aliases.length > MAX_PROJECT_ALIASES)
|
|
165
|
+
throw new BadRequestError(`project aliases: at most ${MAX_PROJECT_ALIASES}`);
|
|
166
|
+
if ([sessionId, name, legacyName, ...aliases].some((v) => v.length > MAX_ID_LEN)) {
|
|
167
|
+
throw new BadRequestError(`session id and project names: at most ${MAX_ID_LEN} characters each`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function assertPromptHookRequest(req) {
|
|
171
|
+
assertCallerIds(req.sessionId, req.project);
|
|
172
|
+
if (req.printedHash !== undefined && !BLOCK_HASH_RE.test(req.printedHash)) {
|
|
173
|
+
throw new BadRequestError('printed hash: 16 lowercase hex characters');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** The text `hippo context --pinned-only --include-recent 5 --format additional-context` prints for this session, read on `ctx`'s store for the caller's project, leaving out an unchanged static block only when `printedHash` matches it.
|
|
177
|
+
* `arm` is the raw ledger arm (`hippo` or `holdout`), null at rate 0; a holdout session gets an empty stdout. `staticHash` is the hash to echo back: null with no static block, for a sub-agent or a holdout.
|
|
178
|
+
* Scope detection (HIPPO_SCOPE and skill env vars) and delivery-ledger events are CLI-only.
|
|
179
|
+
* Throws BadRequestError past the input caps, and on a shared store for a project assertCallerProject refuses, before any arm is booked. */
|
|
180
|
+
export async function promptHookContext(ctx, req, opts = {}) {
|
|
181
|
+
assertPromptHookRequest(req);
|
|
182
|
+
const { sessionId, payload } = req;
|
|
183
|
+
const { sharedStore } = opts;
|
|
184
|
+
if (sharedStore || isSharedStore(ctx.hippoRoot))
|
|
185
|
+
assertCallerProject(req.project);
|
|
186
|
+
// The test the local hook runs on its stdin, so a sub-agent books no arm and no session rows here either.
|
|
187
|
+
const subagent = payload !== undefined && isSubagentPayload(JSON.stringify(payload));
|
|
188
|
+
const ledgerSessionId = subagent ? undefined : sessionId;
|
|
189
|
+
const arm = sessionPilotArm(ctx.hippoRoot, ctx.tenantId, sessionId, !subagent, { sharedStore, ownTenantOnly: true });
|
|
190
|
+
if (arm === 'holdout')
|
|
191
|
+
return { arm, ...NOTHING_RENDERED };
|
|
192
|
+
const prompt = payload?.prompt;
|
|
193
|
+
const result = await getContext(ctx, {
|
|
194
|
+
budget: HOOK_BUDGET,
|
|
195
|
+
pinnedOnly: true,
|
|
196
|
+
includeRecent: HOOK_INCLUDE_RECENT,
|
|
197
|
+
// The project comes from the caller, never from where the store sits or the daemon's cwd.
|
|
198
|
+
currentProject: req.project,
|
|
199
|
+
currentSessionId: sessionId,
|
|
200
|
+
prompt: isJsonString(prompt) ? prompt : undefined,
|
|
201
|
+
cost: contextCost('additional-context', HOOK_FRAMING),
|
|
202
|
+
sharedStore,
|
|
203
|
+
});
|
|
204
|
+
const rendered = hasContextData(result)
|
|
205
|
+
? renderAdditionalContext({
|
|
206
|
+
hippoRoot: ctx.hippoRoot, tenantId: ctx.tenantId, ledgerSessionId, payloadSessionId: ledgerSessionId,
|
|
207
|
+
pinnedOnly: true, framing: HOOK_FRAMING, rec: null, result, sharedStore, printedHash: req.printedHash ?? null,
|
|
208
|
+
})
|
|
209
|
+
: NOTHING_RENDERED;
|
|
210
|
+
// A sub-agent's output is not the session's, so its caller must not record it as printed.
|
|
211
|
+
return { arm, stdout: rendered.stdout, staticHash: subagent ? null : rendered.staticHash };
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=prompt-hook.js.map
|
package/dist/prompt-recall.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Recall gated on the hook prompt, not the five newest memories (pure, no I/O). */
|
|
2
2
|
import { tokenize } from './tokenize.js';
|
|
3
|
-
import { STOP_WORDS } from './
|
|
3
|
+
import { STOP_WORDS } from './memory-quality.js';
|
|
4
4
|
// Latency bound, not tuned: fixed in the prereg regardless of gate config.
|
|
5
5
|
export const PROMPT_RECALL_MAX_CHARS = 4000;
|
|
6
6
|
/** Distinct content tokens: longer than 2 chars, not a stop word. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** One automatic row with a defect. `set-aside` moves to dormant storage on apply, `review` is listed only, `protected` is kept. */
|
|
2
|
+
export interface QualityRepairIssue {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly reason: string;
|
|
5
|
+
readonly disposition: 'set-aside' | 'review' | 'protected';
|
|
6
|
+
readonly protection?: string;
|
|
7
|
+
}
|
|
8
|
+
/** One tenant's plan; `supported` is false, with `blockers` naming each gap, when the schema cannot be changed safely. */
|
|
9
|
+
export interface QualityRepairResult {
|
|
10
|
+
readonly root: string;
|
|
11
|
+
readonly schema: number;
|
|
12
|
+
readonly supported: boolean;
|
|
13
|
+
readonly blockers: readonly string[];
|
|
14
|
+
readonly total: number;
|
|
15
|
+
readonly issues: readonly QualityRepairIssue[];
|
|
16
|
+
readonly appliedIds: readonly string[];
|
|
17
|
+
readonly backup: string | null;
|
|
18
|
+
readonly warnings: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/** Preview by default; apply backs the database up, then moves certain defects to dormant storage, without running migrations. */
|
|
21
|
+
export declare function repairAutomaticMemories(root: string, opts: {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
apply?: boolean;
|
|
24
|
+
}): QualityRepairResult;
|
|
25
|
+
/** Applies the repair once per store, so stores that predate the quality gate are cleaned on upgrade with no command; null once done. */
|
|
26
|
+
export declare function repairQualityOnce(root: string, tenantId: string): QualityRepairResult | null;
|
|
27
|
+
//# sourceMappingURL=quality-repair.d.ts.map
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { appendAuditEvent } from './audit.js';
|
|
4
|
+
import { withBackup } from './db/backup.js';
|
|
5
|
+
import { DatabaseSync } from './db/sqlite.js';
|
|
6
|
+
import { getMeta, setMeta } from './db/meta.js';
|
|
7
|
+
import { tableColumns } from './db/tables.js';
|
|
8
|
+
import { assertBinaryCompatible } from './db/migrate.js';
|
|
9
|
+
import { insertDormantRow, listDormantSnapshots } from './dormant.js';
|
|
10
|
+
import { calculateStrength, canAutoDelete } from './memory.js';
|
|
11
|
+
import { assessAutomaticMemory, BUNDLE_HEADER, isAutomaticEntry, isCertainReason } from './memory-quality.js';
|
|
12
|
+
import { heldTexts } from './same-text.js';
|
|
13
|
+
import { deleteEntryCore, MEMORY_BACKED_TABLES, memoriesBackingObjectsOn } from './store/delete-and-batch.js';
|
|
14
|
+
import { selectAllEntries } from './store/entry-reads.js';
|
|
15
|
+
import { MEMORY_SELECT_COLUMNS, parseJsonArray } from './store/rows.js';
|
|
16
|
+
import { purgeMirrorBestEffort } from './store/mirrors.js';
|
|
17
|
+
const REQUIRED_COLUMNS = {
|
|
18
|
+
memories: MEMORY_SELECT_COLUMNS.split(',').map((s) => s.trim()),
|
|
19
|
+
meta: ['key', 'value'],
|
|
20
|
+
dormant_memories: ['tenant_id', 'id', 'content', 'entry_json', 'reason', 'strength', 'dormant_at'],
|
|
21
|
+
audit_log: ['ts', 'tenant_id', 'actor', 'op', 'target_id', 'metadata_json'],
|
|
22
|
+
...Object.fromEntries(MEMORY_BACKED_TABLES.map((table) => [table, ['memory_id']])),
|
|
23
|
+
};
|
|
24
|
+
function capabilityBlockers(db) {
|
|
25
|
+
const blockers = [];
|
|
26
|
+
for (const [table, required] of Object.entries(REQUIRED_COLUMNS)) {
|
|
27
|
+
const columns = tableColumns(db, table);
|
|
28
|
+
if (columns.size === 0)
|
|
29
|
+
blockers.push(`missing table: ${table}`);
|
|
30
|
+
else
|
|
31
|
+
for (const column of required)
|
|
32
|
+
if (!columns.has(column))
|
|
33
|
+
blockers.push(`missing column: ${table}.${column}`);
|
|
34
|
+
}
|
|
35
|
+
if (blockers.some((blocker) => blocker === 'missing table: meta' || blocker.startsWith('missing column: meta.')))
|
|
36
|
+
return blockers;
|
|
37
|
+
assertBinaryCompatible(db);
|
|
38
|
+
if (getMeta(db, 'fts5_available', '0') === '1') {
|
|
39
|
+
const columns = tableColumns(db, 'memories_fts');
|
|
40
|
+
for (const column of ['id', 'content', 'tags']) {
|
|
41
|
+
if (!columns.has(column))
|
|
42
|
+
blockers.push(`missing column: memories_fts.${column}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return blockers;
|
|
46
|
+
}
|
|
47
|
+
function protection(entry, backing) {
|
|
48
|
+
if (entry.pinned)
|
|
49
|
+
return 'pinned';
|
|
50
|
+
if (entry.kind === 'raw')
|
|
51
|
+
return 'raw receipt';
|
|
52
|
+
if (!canAutoDelete(entry))
|
|
53
|
+
return 'trusted imported note';
|
|
54
|
+
if (backing.has(entry.id))
|
|
55
|
+
return 'backs an object';
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function structured(entry) {
|
|
59
|
+
return entry.trace_outcome !== null || entry.tags.includes('session-digest') || entry.source === 'auto-promote';
|
|
60
|
+
}
|
|
61
|
+
/** A parent that is gone says nothing about who wrote it, so it counts as a person's. */
|
|
62
|
+
function fromAutomaticParents(entry, rows) {
|
|
63
|
+
return entry.parents.length > 0 && entry.parents.every((id) => {
|
|
64
|
+
const parent = rows.get(id);
|
|
65
|
+
return parent !== undefined && isAutomaticEntry(parent);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function contentIssue(entry, rows) {
|
|
69
|
+
if (structured(entry) || !isAutomaticEntry(entry))
|
|
70
|
+
return null;
|
|
71
|
+
// Only a bundle header marks parts; a refined row keeps its lead line, so it is judged whole.
|
|
72
|
+
if (!BUNDLE_HEADER.test(entry.content)) {
|
|
73
|
+
const { reason } = assessAutomaticMemory(entry.content);
|
|
74
|
+
return reason === null ? null : { reason, disposition: isCertainReason(reason) ? 'set-aside' : 'review' };
|
|
75
|
+
}
|
|
76
|
+
const texts = heldTexts({ ...entry, source: 'consolidation' });
|
|
77
|
+
if (texts.length === 0)
|
|
78
|
+
return { reason: 'derived bundle has no safely parsed constituents', disposition: 'review' };
|
|
79
|
+
const reasons = texts.map((text) => assessAutomaticMemory(text).reason).filter((reason) => reason !== null);
|
|
80
|
+
if (reasons.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
const certain = reasons.length === texts.length && reasons.every(isCertainReason) && fromAutomaticParents(entry, rows);
|
|
83
|
+
return { reason: `derived constituents: ${[...new Set(reasons)].join('; ')}`, disposition: certain ? 'set-aside' : 'review' };
|
|
84
|
+
}
|
|
85
|
+
function planRows(db, tenantId) {
|
|
86
|
+
const all = selectAllEntries(db, tenantId);
|
|
87
|
+
const rows = new Map(listDormantSnapshots(db, tenantId).map((snapshot) => [snapshot.entry.id, snapshot.entry]));
|
|
88
|
+
for (const entry of all)
|
|
89
|
+
rows.set(entry.id, entry);
|
|
90
|
+
const entries = all.filter((entry) => !entry.superseded_by && entry.kind !== 'archived');
|
|
91
|
+
const backing = memoriesBackingObjectsOn(db);
|
|
92
|
+
const issues = [];
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const issue = contentIssue(entry, rows);
|
|
95
|
+
if (issue === null)
|
|
96
|
+
continue;
|
|
97
|
+
const kept = protection(entry, backing);
|
|
98
|
+
issues.push(kept === undefined ? { id: entry.id, ...issue } : { id: entry.id, ...issue, disposition: 'protected', protection: kept });
|
|
99
|
+
}
|
|
100
|
+
return { total: entries.length, issues };
|
|
101
|
+
}
|
|
102
|
+
function unsupportedPreview(db, tenantId) {
|
|
103
|
+
const columns = tableColumns(db, 'memories');
|
|
104
|
+
if (!columns.has('id') || !columns.has('content'))
|
|
105
|
+
return { total: 0, issues: [] };
|
|
106
|
+
const tenant = columns.has('tenant_id') ? ' WHERE tenant_id = ?' : '';
|
|
107
|
+
const column = (name, fallback) => (columns.has(name) ? name : `${fallback} AS ${name}`);
|
|
108
|
+
const statement = db.prepare(`SELECT id, content, ${column('source', "''")}, ${column('confidence', 'NULL')}, ${column('extracted_from', 'NULL')}, ${column('dag_level', '0')}, ${column('tags_json', "'[]'")} FROM memories${tenant}`);
|
|
109
|
+
// SAFETY: the SELECT names every Row field, each a literal fallback when its column is missing.
|
|
110
|
+
const rows = (tenant ? statement.all(tenantId) : statement.all());
|
|
111
|
+
const issues = rows.flatMap((row) => {
|
|
112
|
+
const provenance = { ...row, source: row.source ?? '', confidence: row.confidence ?? 'observed', dag_level: row.dag_level ?? 0, tags: parseJsonArray(row.tags_json) };
|
|
113
|
+
if (!isAutomaticEntry(provenance))
|
|
114
|
+
return [];
|
|
115
|
+
const { reason } = assessAutomaticMemory(row.content);
|
|
116
|
+
return reason === null ? [] : [{ id: row.id, reason, disposition: 'review', protection: 'unsupported schema; no changes permitted' }];
|
|
117
|
+
});
|
|
118
|
+
return { total: rows.length, issues };
|
|
119
|
+
}
|
|
120
|
+
function initialResult(db, root, tenantId) {
|
|
121
|
+
const blockers = capabilityBlockers(db);
|
|
122
|
+
// SAFETY: PRAGMA user_version returns one row with one integer column.
|
|
123
|
+
const schema = db.prepare('PRAGMA user_version').get().user_version;
|
|
124
|
+
const plan = blockers.length > 0 ? unsupportedPreview(db, tenantId) : planRows(db, tenantId);
|
|
125
|
+
return {
|
|
126
|
+
root, schema, supported: blockers.length === 0, blockers,
|
|
127
|
+
total: plan.total,
|
|
128
|
+
issues: plan.issues, appliedIds: [], backup: null, warnings: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// No rejection record, as in project repair: the writers refuse these defects themselves, and a record would also refuse a person's copy.
|
|
132
|
+
function setAsideIssue(db, entry, reason, now, backup) {
|
|
133
|
+
const actor = 'quality-repair';
|
|
134
|
+
if (deleteEntryCore(db, entry.id, { actor, automatic: true, suppressForgetAudit: true }) === null)
|
|
135
|
+
return false;
|
|
136
|
+
insertDormantRow(db, { entry, strength: calculateStrength(entry, now), reason: 'quality-repair', dormantAt: now.toISOString() });
|
|
137
|
+
if (getMeta(db, 'fts5_available', '0') === '1' && db.prepare('SELECT id FROM memories_fts WHERE id = ?').get(entry.id)) {
|
|
138
|
+
throw new Error(`Quality repair could not remove the full-text row for ${entry.id}`);
|
|
139
|
+
}
|
|
140
|
+
appendAuditEvent(db, { tenantId: entry.tenantId, actor, op: 'quality_repair', targetId: entry.id, metadata: { reason, backup } });
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
function applyPlan(db, root, tenantId, backup, doneKey) {
|
|
144
|
+
db.exec('BEGIN IMMEDIATE');
|
|
145
|
+
try {
|
|
146
|
+
const result = initialResult(db, root, tenantId);
|
|
147
|
+
if (!result.supported)
|
|
148
|
+
throw new Error(`Quality repair capability changed: ${result.blockers.join('; ')}`);
|
|
149
|
+
const entries = new Map(selectAllEntries(db, tenantId).map((entry) => [entry.id, entry]));
|
|
150
|
+
const appliedIds = [];
|
|
151
|
+
const warnings = [];
|
|
152
|
+
const now = new Date();
|
|
153
|
+
for (const issue of result.issues) {
|
|
154
|
+
if (issue.disposition !== 'set-aside')
|
|
155
|
+
continue;
|
|
156
|
+
if (setAsideIssue(db, entries.get(issue.id), issue.reason, now, backup))
|
|
157
|
+
appliedIds.push(issue.id);
|
|
158
|
+
else
|
|
159
|
+
warnings.push(`Kept ${issue.id}: the store's delete guard protects it.`);
|
|
160
|
+
}
|
|
161
|
+
if (doneKey)
|
|
162
|
+
setMeta(db, doneKey, '1');
|
|
163
|
+
db.exec('COMMIT');
|
|
164
|
+
return { ...result, appliedIds, backup, warnings };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
try {
|
|
168
|
+
db.exec('ROLLBACK');
|
|
169
|
+
}
|
|
170
|
+
catch { /* preserve the mutation error */ }
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function repairOn(db, root, opts) {
|
|
175
|
+
db.exec('BEGIN');
|
|
176
|
+
const initial = initialResult(db, root, opts.tenantId);
|
|
177
|
+
db.exec('ROLLBACK');
|
|
178
|
+
if (!opts.apply || !initial.supported || !initial.issues.some((issue) => issue.disposition === 'set-aside'))
|
|
179
|
+
return initial;
|
|
180
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
181
|
+
const result = withBackup(db, root, 'before-quality-repair', (backup) => applyPlan(db, root, opts.tenantId, backup, opts.doneKey));
|
|
182
|
+
const warnings = [...result.warnings];
|
|
183
|
+
for (const id of result.appliedIds) {
|
|
184
|
+
if (!purgeMirrorBestEffort(root, id, false, 'quality repair'))
|
|
185
|
+
warnings.push(`Mirror cleanup failed for ${id}; remove its stale mirror before rebuilding the index.`);
|
|
186
|
+
}
|
|
187
|
+
return { ...result, warnings };
|
|
188
|
+
}
|
|
189
|
+
function openForRepair(root, readOnly, fn) {
|
|
190
|
+
const file = path.join(root, 'hippo.db');
|
|
191
|
+
if (!fs.existsSync(file))
|
|
192
|
+
throw new Error(`No existing Hippo database at ${file}`);
|
|
193
|
+
const db = new DatabaseSync(file, { readOnly });
|
|
194
|
+
try {
|
|
195
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
196
|
+
return fn(db);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
db.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Preview by default; apply backs the database up, then moves certain defects to dormant storage, without running migrations. */
|
|
203
|
+
export function repairAutomaticMemories(root, opts) {
|
|
204
|
+
return openForRepair(root, !opts.apply, (db) => repairOn(db, root, opts));
|
|
205
|
+
}
|
|
206
|
+
const AUTO_REPAIR_META_KEY = 'quality_repair_auto';
|
|
207
|
+
/** Applies the repair once per store, so stores that predate the quality gate are cleaned on upgrade with no command; null once done. */
|
|
208
|
+
export function repairQualityOnce(root, tenantId) {
|
|
209
|
+
return openForRepair(root, false, (db) => {
|
|
210
|
+
if (getMeta(db, AUTO_REPAIR_META_KEY) === '1')
|
|
211
|
+
return null;
|
|
212
|
+
// The apply commits the flag with the moves, so a lock taken between them cannot hide what moved.
|
|
213
|
+
const result = repairOn(db, root, { tenantId, apply: true, doneKey: AUTO_REPAIR_META_KEY });
|
|
214
|
+
// An unsupported schema changed nothing, so the next run tries again once a migration has run.
|
|
215
|
+
if (result.supported && result.backup === null)
|
|
216
|
+
setMeta(db, AUTO_REPAIR_META_KEY, '1');
|
|
217
|
+
return result;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=quality-repair.js.map
|
package/dist/rate-limit.d.ts
CHANGED
package/dist/rate-limit.js
CHANGED
|
@@ -34,10 +34,10 @@ export function createRateLimiter(opts) {
|
|
|
34
34
|
bucket = { tokens: burst, last: now };
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
|
-
//
|
|
38
|
-
const refill = ((now - existing.last) / 1000) * ratePerSec;
|
|
37
|
+
// A clock stepped back refills nothing, and keeping the later time stops the step forward again refilling that span twice.
|
|
38
|
+
const refill = (Math.max(0, now - existing.last) / 1000) * ratePerSec;
|
|
39
39
|
existing.tokens = Math.min(burst, existing.tokens + refill);
|
|
40
|
-
existing.last = now;
|
|
40
|
+
existing.last = Math.max(existing.last, now);
|
|
41
41
|
bucket = existing;
|
|
42
42
|
// Drop the old slot so the re-insert below puts the key last in Map
|
|
43
43
|
// iteration order — Map order then tracks least-recently-used.
|
|
@@ -55,6 +55,8 @@ export function createRateLimiter(opts) {
|
|
|
55
55
|
buckets.set(key, bucket);
|
|
56
56
|
return allowed;
|
|
57
57
|
},
|
|
58
|
+
// A refused bucket holds under one token and refills at ratePerSec, so one token is back within this.
|
|
59
|
+
retryAfterSec: Math.max(1, Math.ceil(1 / ratePerSec)),
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
//# sourceMappingURL=rate-limit.js.map
|
package/dist/raw-archive.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface ArchiveOpts {
|
|
|
16
16
|
*
|
|
17
17
|
* Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
|
|
18
18
|
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
19
|
-
*
|
|
19
|
+
* write scope, which nests inside an outer transaction (e.g. batchWriteAndDelete).
|
|
20
20
|
*
|
|
21
21
|
* Throws if the row does not exist or is not `kind='raw'`.
|
|
22
22
|
*/
|
package/dist/raw-archive.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BadRequestError, NotFoundError } from './api-errors.js';
|
|
2
|
-
import { isFtsAvailable } from './db.js';
|
|
2
|
+
import { isFtsAvailable, withWriteScope } from './db.js';
|
|
3
3
|
import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
|
|
4
4
|
import { markSummaryDirtyInTx } from './summary-dirty.js';
|
|
5
5
|
function loadRawRow(db, id) {
|
|
@@ -60,16 +60,15 @@ function auditArchive(db, id, row, opts) {
|
|
|
60
60
|
*
|
|
61
61
|
* Snapshots the full row into `raw_archive`, flips `kind` to `'archived'` so the
|
|
62
62
|
* append-only trigger lets the delete through, then deletes the row. All in one
|
|
63
|
-
*
|
|
63
|
+
* write scope, which nests inside an outer transaction (e.g. batchWriteAndDelete).
|
|
64
64
|
*
|
|
65
65
|
* Throws if the row does not exist or is not `kind='raw'`.
|
|
66
66
|
*/
|
|
67
67
|
export function archiveRawMemory(db, id, opts) {
|
|
68
68
|
const row = loadRawRow(db, id);
|
|
69
|
-
// SAVEPOINT
|
|
70
|
-
//
|
|
71
|
-
db
|
|
72
|
-
try {
|
|
69
|
+
// A SAVEPOINT when already inside a transaction (e.g. batchWriteAndDelete), so a throw here
|
|
70
|
+
// rolls back only this archive.
|
|
71
|
+
withWriteScope(db, 'archive_raw', () => {
|
|
73
72
|
moveRowToArchive(db, id, row, opts);
|
|
74
73
|
auditArchive(db, id, row, opts);
|
|
75
74
|
// Archiving a child under a level-2 summary marks the parent dirty, inside the
|
|
@@ -85,15 +84,6 @@ export function archiveRawMemory(db, id, opts) {
|
|
|
85
84
|
if (opts.afterArchive) {
|
|
86
85
|
opts.afterArchive(db, id);
|
|
87
86
|
}
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
catch (e) {
|
|
91
|
-
try {
|
|
92
|
-
db.exec('ROLLBACK TO SAVEPOINT archive_raw');
|
|
93
|
-
db.exec('RELEASE SAVEPOINT archive_raw');
|
|
94
|
-
}
|
|
95
|
-
catch { /* savepoint already discarded; keep the original error */ }
|
|
96
|
-
throw e;
|
|
97
|
-
}
|
|
87
|
+
});
|
|
98
88
|
}
|
|
99
89
|
//# sourceMappingURL=raw-archive.js.map
|