hippo-memory 1.57.0 → 1.59.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 +24 -1
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/agent-memories/legacy.js +4 -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 +5 -1
- package/dist/audit.js +13 -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/output.d.ts +3 -0
- package/dist/cli/output.js +7 -0
- package/dist/cli/projects.d.ts +4 -0
- package/dist/cli/projects.js +90 -0
- package/dist/cli/shared.js +23 -13
- package/dist/cli/sleep.js +5 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +486 -397
- 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 +48 -35
- package/dist/customer-notes.js +14 -13
- package/dist/dag.js +7 -4
- 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/dedupe.js +1 -1
- package/dist/doctor.js +28 -0
- package/dist/dormant.d.ts +2 -2
- package/dist/embedding-provider.js +3 -3
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +72 -16
- package/dist/extract.js +19 -18
- 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/project-merge.d.ts +52 -0
- package/dist/project-merge.js +168 -0
- package/dist/raw-archive.js +7 -6
- package/dist/recall-scope.d.ts +5 -4
- package/dist/recall-scope.js +7 -5
- 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/rerankers/clef.d.ts +29 -0
- package/dist/rerankers/clef.js +182 -0
- package/dist/rerankers/index.js +3 -0
- package/dist/rerankers/jev.d.ts +11 -0
- package/dist/rerankers/jev.js +10 -5
- package/dist/rerankers/types.d.ts +16 -0
- 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 +9 -2
- package/dist/server.js +188 -411
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +15 -14
- package/dist/store.js +10 -10
- 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 +5 -2
- package/dist/connectors/slack/ratelimit.d.ts +0 -9
- package/dist/connectors/slack/ratelimit.js +0 -18
package/README.md
CHANGED
|
@@ -535,6 +535,18 @@ removes pinned memories, raw receipts (Slack, GitHub, vault imports) or the memo
|
|
|
535
535
|
Claude Code compaction saved either way, and duplicate removal and junk cleanup still
|
|
536
536
|
delete other memories. `hippo forget` still deletes a compaction memory.
|
|
537
537
|
|
|
538
|
+
**Clean up project names left by older versions.** Older versions tagged memories saved in
|
|
539
|
+
a git worktree with the worktree's folder name, and older sleep saved merged memories as
|
|
540
|
+
user-global, so every project could see them. Upgrading stops new damage; these commands
|
|
541
|
+
repair old rows. Each is a dry run until you add `--apply`. With `--apply`, it backs up the
|
|
542
|
+
database to `.hippo/backups/` first and logs every id it touched in the audit log:
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
hippo projects --global # names, counts, live worktrees of this repo
|
|
546
|
+
hippo projects merge hippo-wt-fix hippo --global # fold an old worktree name into its repo
|
|
547
|
+
hippo projects repair --global # re-tag user-global merges by their parents
|
|
548
|
+
```
|
|
549
|
+
|
|
538
550
|
**See what memory costs in tokens.** Every block of memory text hippo hands an agent (the
|
|
539
551
|
per-prompt hook, the block `hippo compact-resume` restores after compaction, `hippo context`,
|
|
540
552
|
`hippo recall`, the MCP tools, the HTTP API) is recorded in a token ledger: counts, surface
|
|
@@ -559,6 +571,17 @@ are not recorded yet. It holds ids, hashes, counts and reasons only, never promp
|
|
|
559
571
|
text; the prompt hash is unsalted, so a very short prompt can be guessed. A ledger failure prints one stderr line and never changes what the hook prints. Rows
|
|
560
572
|
older than 90 days are pruned; at a heavy 300 prompts a day that is about 190 MB per store.
|
|
561
573
|
|
|
574
|
+
**Run a pilot with a holdout group.** Set `{"pilot":{"holdoutRateBp":2000}}` in `.hippo/config.json`
|
|
575
|
+
to hold back memories from about 20% of sessions. The rate is in basis points, 0 to 10000, and 0
|
|
576
|
+
is off (the default). The setting is read from the store the token ledger writes to. A session
|
|
577
|
+
lands in its arm by a hash of its id, and the first hook call writes one row to the token ledger.
|
|
578
|
+
A holdout session gets no memories from the per-prompt hook, the SessionStart hook or compact-resume.
|
|
579
|
+
The agent's own `hippo context` pull is gated in Claude Code only, so a Codex holdout session still
|
|
580
|
+
gets memories from it. Capture still runs. `hippo recall`, the HTTP API and the MCP tools are not gated
|
|
581
|
+
and write no arm row. Agents are told to call the MCP context tool at session start, and those calls
|
|
582
|
+
are not recorded. Set the rate to 0 only after the pilot window closes, because 0 ends every holdout at once.
|
|
583
|
+
`hippo doctor` shows the pilot when it is on.
|
|
584
|
+
|
|
562
585
|
---
|
|
563
586
|
|
|
564
587
|
### Outcome feedback
|
|
@@ -1115,7 +1138,7 @@ Mark it, and it drops out of the top results. `hippo outcome --bad` weakens the
|
|
|
1115
1138
|
|
|
1116
1139
|
### Where does hippo keep my data?
|
|
1117
1140
|
|
|
1118
|
-
On your machine, in SQLite: `.hippo/hippo.db` in each project, plus a global store in `~/.hippo/` for lessons shared across projects, with markdown mirrors you can read and commit. Recall makes no network call by default. Text goes to an outside provider only through features that use one: an API embedder, the Jev or LLM reranker, `hippo refine`, and the fact extraction `hippo sleep` runs through Anthropic's API whenever `ANTHROPIC_API_KEY` is set in its environment. To turn that last one off, set `{"extraction":{"enabled":false}}` in `.hippo/config.json`.
|
|
1141
|
+
On your machine, in SQLite: `.hippo/hippo.db` in each project, plus a global store in `~/.hippo/` for lessons shared across projects, with markdown mirrors you can read and commit. Recall makes no network call by default. Text goes to an outside provider only through features that use one: an API embedder, the Jev, CLEF or LLM reranker, `hippo refine`, and the fact extraction `hippo sleep` runs through Anthropic's API whenever `ANTHROPIC_API_KEY` is set in its environment. To turn that last one off, set `{"extraction":{"enabled":false}}` in `.hippo/config.json`.
|
|
1119
1142
|
|
|
1120
1143
|
### What does hippo cost?
|
|
1121
1144
|
|
|
@@ -34,7 +34,7 @@ export interface ContainerOutcome {
|
|
|
34
34
|
}
|
|
35
35
|
/** Throws SQLITE_BUSY when another writer holds the store past its busy timeout; nothing is written then. */
|
|
36
36
|
export declare function syncContainer(s: StoreSession, work: ContainerWork): ContainerOutcome;
|
|
37
|
-
export type SetAsideWhy = 'note-gone' | 'note-changed' | 'handover';
|
|
37
|
+
export type SetAsideWhy = 'note-gone' | 'note-changed' | 'handover' | 'project-merge';
|
|
38
38
|
export type SetAsideResult = {
|
|
39
39
|
readonly kind: 'untagged';
|
|
40
40
|
readonly entry: MemoryEntry;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { realpathOrResolve } from '../project-identity.js';
|
|
5
|
-
import { isStringValue } from '../capture.js';
|
|
5
|
+
import { isStringValue } from '../capture-contract.js';
|
|
6
6
|
import { isJsonObject } from '../hooks.js';
|
|
7
7
|
import { expandHome, frontmatterField, itemTime, readTextFile, splitFrontmatter } from './files.js';
|
|
8
8
|
import { markdownNotes, readFolderStore, uniqueFolders } from './folder-store.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Gemini CLI: the "Gemini Added Memories" section of GEMINI.md, and the auto-memory folder that projects.json names.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { isStringValue } from '../capture.js';
|
|
4
|
+
import { isStringValue } from '../capture-contract.js';
|
|
5
5
|
import { isJsonObject } from '../hooks.js';
|
|
6
6
|
import { readTextFile, splitFrontmatter } from './files.js';
|
|
7
7
|
import { markdownNotes, readFolderStore } from './folder-store.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Rows the old Claude import wrote (`claude-memory:<file>`), taken over by the notes they came from (plan design 10).
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { duplicateKey } from '../same-text.js';
|
|
4
|
+
import { maskEmails } from '../secret-detect.js';
|
|
4
5
|
import { selectLiveEntriesBySourcePrefix } from '../store.js';
|
|
5
6
|
import { matchLegacy } from './plan.js';
|
|
6
7
|
import { MIN_ITEM_CHARS, storedText } from './source.js';
|
|
@@ -25,7 +26,9 @@ export function legacyWork(db, tenantId, listings) {
|
|
|
25
26
|
refs.push({ dir: container.path, key: item.key });
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
const match = matchLegacy(
|
|
29
|
+
const match = matchLegacy(
|
|
30
|
+
// The old import kept emails in clear, and targets are masked; without the mask those rows never matched and imported twice.
|
|
31
|
+
rows.map((r) => ({ id: r.id, file: r.source.slice(LEGACY_SOURCE_PREFIX.length), textKey: duplicateKey(maskEmails(r.content)) })), targets);
|
|
29
32
|
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
30
33
|
const add = (pick, ref, id) => {
|
|
31
34
|
const { dir, key } = refs[Number(ref)];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Errors whose HTTP status is part of the API contract; the server maps by class, so message text can change freely. */
|
|
2
|
+
export type ApiErrorStatus = 400 | 403 | 404 | 409;
|
|
3
|
+
/** Base class: an error the caller caused, carrying the status the HTTP layer answers with. */
|
|
4
|
+
export declare abstract class ApiError extends Error {
|
|
5
|
+
abstract readonly status: ApiErrorStatus;
|
|
6
|
+
}
|
|
7
|
+
/** The request cannot be applied as given: bad input, or a state this route reports as 400. */
|
|
8
|
+
export declare class BadRequestError extends ApiError {
|
|
9
|
+
readonly status = 400;
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/** The actor's role or identity does not allow the operation. */
|
|
13
|
+
export declare class ForbiddenError extends ApiError {
|
|
14
|
+
readonly status = 403;
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/** The named row does not exist for this tenant. */
|
|
18
|
+
export declare class NotFoundError extends ApiError {
|
|
19
|
+
readonly status = 404;
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
/** The row exists but its current state forbids the change (already superseded, closed, or decided). */
|
|
23
|
+
export declare class ConflictError extends ApiError {
|
|
24
|
+
readonly status = 409;
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=api-errors.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Errors whose HTTP status is part of the API contract; the server maps by class, so message text can change freely. */
|
|
2
|
+
/** Base class: an error the caller caused, carrying the status the HTTP layer answers with. */
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/** The request cannot be applied as given: bad input, or a state this route reports as 400. */
|
|
6
|
+
export class BadRequestError extends ApiError {
|
|
7
|
+
status = 400;
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'BadRequestError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The actor's role or identity does not allow the operation. */
|
|
14
|
+
export class ForbiddenError extends ApiError {
|
|
15
|
+
status = 403;
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'ForbiddenError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** The named row does not exist for this tenant. */
|
|
22
|
+
export class NotFoundError extends ApiError {
|
|
23
|
+
status = 404;
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'NotFoundError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The row exists but its current state forbids the change (already superseded, closed, or decided). */
|
|
30
|
+
export class ConflictError extends ApiError {
|
|
31
|
+
status = 409;
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'ConflictError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=api-errors.js.map
|
package/dist/api.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* in exactly one place.
|
|
8
8
|
*/
|
|
9
9
|
import { type DatabaseSyncLike } from './db.js';
|
|
10
|
+
import { BadRequestError } from './api-errors.js';
|
|
11
|
+
export { ApiError, BadRequestError, ConflictError, ForbiddenError, NotFoundError } from './api-errors.js';
|
|
10
12
|
import { deleteEntry, loadAllEntries, type TaskSnapshot, type SessionEvent } from './store.js';
|
|
11
13
|
import { type RejectedValueRow } from './rejection.js';
|
|
12
14
|
import { type DormantMemory, type ListDormantOpts } from './dormant.js';
|
|
@@ -77,14 +79,10 @@ export declare function adminActor(subject: string): Actor;
|
|
|
77
79
|
* full-store fallback (codex v1.7.0 diff-pass P1). Validated upfront
|
|
78
80
|
* so the contract holds.
|
|
79
81
|
*/
|
|
80
|
-
export declare class RecallContractError extends
|
|
82
|
+
export declare class RecallContractError extends BadRequestError {
|
|
81
83
|
readonly code: 'fresh_tail_requires_session_id' | 'invalid_scorer_window';
|
|
82
84
|
constructor(code: 'fresh_tail_requires_session_id' | 'invalid_scorer_window', message: string);
|
|
83
85
|
}
|
|
84
|
-
/** The actor's role or identity does not allow the operation. HTTP maps it to 403. */
|
|
85
|
-
export declare class ForbiddenError extends Error {
|
|
86
|
-
constructor(message: string);
|
|
87
|
-
}
|
|
88
86
|
import { isPrivateScope, passesScopeFilterForRecall } from './recall-scope.js';
|
|
89
87
|
export { isPrivateScope, passesScopeFilterForRecall };
|
|
90
88
|
export { passesCliRecallScopeFilter, ScopeForbiddenError } from './recall-scope.js';
|
|
@@ -125,6 +123,8 @@ export interface RememberResult {
|
|
|
125
123
|
quarantined?: {
|
|
126
124
|
reason: string;
|
|
127
125
|
};
|
|
126
|
+
/** Set only when the content held secret material: untrusted text had it redacted, typed text was stored as sent. */
|
|
127
|
+
warnings?: string[];
|
|
128
128
|
}
|
|
129
129
|
export declare function remember(ctx: Context, opts: RememberOpts): RememberResult;
|
|
130
130
|
export interface RecallOpts {
|
package/dist/api.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* in exactly one place.
|
|
8
8
|
*/
|
|
9
9
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
10
|
+
import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from './api-errors.js';
|
|
11
|
+
export { ApiError, BadRequestError, ConflictError, ForbiddenError, NotFoundError } from './api-errors.js';
|
|
10
12
|
import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, memoriesBackingObjects, } from './store.js';
|
|
11
13
|
import { RejectedValueError } from './rejection.js';
|
|
12
14
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
@@ -17,7 +19,7 @@ import { quarantineScopeFor, recordQuarantine, getQuarantineRow, listQuarantineR
|
|
|
17
19
|
import { summarizeFailures } from './failure-log.js';
|
|
18
20
|
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
19
21
|
import { createMemory, createSuccessor, applyOutcome, calculateStrength, markRetrieved, CHURN_STALE_TAG, COMPACTION_MEMORY_TAG, } from './memory.js';
|
|
20
|
-
import { appendAuditEvent, auditQueryFields, queryAuditEvents, auditMemories, isContentWorthStoring, } from './audit.js';
|
|
22
|
+
import { appendAuditEvent, reportAuditWriteFailure, auditQueryFields, queryAuditEvents, auditMemories, isContentWorthStoring, } from './audit.js';
|
|
21
23
|
import { promoteToGlobal, getGlobalRoot, autoShare, searchBothHybrid } from './shared.js';
|
|
22
24
|
import { writeRecallTrace, writeRecallTraceAtRoot, recordTraceOutcome } from './recall-trace.js';
|
|
23
25
|
import { evalNow } from './ablation.js';
|
|
@@ -32,7 +34,7 @@ import { consolidate } from './consolidate.js';
|
|
|
32
34
|
import { loadConfig } from './config.js';
|
|
33
35
|
import { resolveProjectIdentity, classifyOriginProject, isGlobalStoreRoot } from './project-identity.js';
|
|
34
36
|
import { promptTokens, contentTokens, gatePromptRecall, } from './prompt-recall.js';
|
|
35
|
-
import { detectSecret } from './secret-detect.js';
|
|
37
|
+
import { detectSecret, vetSecrets } from './secret-detect.js';
|
|
36
38
|
import { isSessionDigestRow } from './session-digest.js';
|
|
37
39
|
import { deduplicateStore } from './dedupe.js';
|
|
38
40
|
import { computeAmbientState } from './ambient.js';
|
|
@@ -68,7 +70,7 @@ export function adminActor(subject) {
|
|
|
68
70
|
* full-store fallback (codex v1.7.0 diff-pass P1). Validated upfront
|
|
69
71
|
* so the contract holds.
|
|
70
72
|
*/
|
|
71
|
-
export class RecallContractError extends
|
|
73
|
+
export class RecallContractError extends BadRequestError {
|
|
72
74
|
code;
|
|
73
75
|
constructor(code, message) {
|
|
74
76
|
super(message);
|
|
@@ -76,13 +78,6 @@ export class RecallContractError extends Error {
|
|
|
76
78
|
this.code = code;
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
/** The actor's role or identity does not allow the operation. HTTP maps it to 403. */
|
|
80
|
-
export class ForbiddenError extends Error {
|
|
81
|
-
constructor(message) {
|
|
82
|
-
super(message);
|
|
83
|
-
this.name = 'ForbiddenError';
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
81
|
// v1.25.0: the recall-side scope predicates (PRIVATE_SCOPE_RE, isPrivateScope,
|
|
87
82
|
// passesScopeFilterForRecall) live in recall-scope.ts (leaf) so shared.ts can
|
|
88
83
|
// apply the same default-deny rule to searchBothHybrid's internal loads
|
|
@@ -170,9 +165,10 @@ export function oneCopyPerMemory(local, global, now) {
|
|
|
170
165
|
return [local.filter((e) => kept.has(e)), global.filter((e) => kept.has(e))];
|
|
171
166
|
}
|
|
172
167
|
export function remember(ctx, opts) {
|
|
173
|
-
const
|
|
168
|
+
const vetted = vetSecrets(opts.content, opts.tags ?? [], opts.untrusted === true);
|
|
169
|
+
const detection = opts.untrusted ? detectInstruction(vetted.content) : { flagged: false, reason: null };
|
|
174
170
|
const requestedScope = opts.scope ?? null;
|
|
175
|
-
const entry = createMemory(
|
|
171
|
+
const entry = createMemory(vetted.content, {
|
|
176
172
|
kind: opts.kind ?? 'distilled',
|
|
177
173
|
scope: detection.flagged ? quarantineScopeFor(requestedScope) : requestedScope,
|
|
178
174
|
owner: opts.owner ?? null,
|
|
@@ -199,6 +195,8 @@ export function remember(ctx, opts) {
|
|
|
199
195
|
const result = { id: entry.id, kind: entry.kind, tenantId: ctx.tenantId };
|
|
200
196
|
if (detection.flagged)
|
|
201
197
|
result.quarantined = { reason: detection.reason ?? 'unknown' };
|
|
198
|
+
if (vetted.warnings.length > 0)
|
|
199
|
+
result.warnings = vetted.warnings;
|
|
202
200
|
return result;
|
|
203
201
|
}
|
|
204
202
|
/**
|
|
@@ -1107,7 +1105,7 @@ export function forget(ctx, id) {
|
|
|
1107
1105
|
.prepare(`SELECT tenant_id FROM memories WHERE id = ?`)
|
|
1108
1106
|
.get(id);
|
|
1109
1107
|
if (!row || row.tenant_id !== ctx.tenantId) {
|
|
1110
|
-
throw new
|
|
1108
|
+
throw new NotFoundError(`memory not found: ${id}`);
|
|
1111
1109
|
}
|
|
1112
1110
|
}
|
|
1113
1111
|
finally {
|
|
@@ -1115,7 +1113,7 @@ export function forget(ctx, id) {
|
|
|
1115
1113
|
}
|
|
1116
1114
|
const removed = deleteEntry(ctx.hippoRoot, id, { actor: ctx.actor.subject });
|
|
1117
1115
|
if (!removed) {
|
|
1118
|
-
throw new
|
|
1116
|
+
throw new NotFoundError(`memory not found: ${id}`);
|
|
1119
1117
|
}
|
|
1120
1118
|
// Counted here, not in the CLI: both callers of this function (cmdForget and
|
|
1121
1119
|
// the HTTP route) are the two paths of one user command, so neither can miss
|
|
@@ -1150,7 +1148,7 @@ export function reject(ctx, opts) {
|
|
|
1150
1148
|
.prepare(`SELECT tenant_id FROM memories WHERE id = ?`)
|
|
1151
1149
|
.get(opts.memoryId);
|
|
1152
1150
|
if (!row || row.tenant_id !== ctx.tenantId) {
|
|
1153
|
-
throw new
|
|
1151
|
+
throw new NotFoundError(`memory not found: ${opts.memoryId}`);
|
|
1154
1152
|
}
|
|
1155
1153
|
}
|
|
1156
1154
|
finally {
|
|
@@ -1176,10 +1174,10 @@ export function reject(ctx, opts) {
|
|
|
1176
1174
|
export function unreject(ctx, digestOrPrefix) {
|
|
1177
1175
|
const outcome = unrejectValue(ctx.hippoRoot, ctx.tenantId, digestOrPrefix, ctx.actor.subject);
|
|
1178
1176
|
if (outcome.status === 'not_found') {
|
|
1179
|
-
throw new
|
|
1177
|
+
throw new NotFoundError(`no rejected value matches: ${digestOrPrefix}`);
|
|
1180
1178
|
}
|
|
1181
1179
|
if (outcome.status === 'ambiguous') {
|
|
1182
|
-
throw new
|
|
1180
|
+
throw new BadRequestError(`"${digestOrPrefix}" matches ${outcome.candidates.length} tombstones; use a longer prefix`);
|
|
1183
1181
|
}
|
|
1184
1182
|
return { ok: true, digest: outcome.digest };
|
|
1185
1183
|
}
|
|
@@ -1202,7 +1200,7 @@ export function promote(ctx, id) {
|
|
|
1202
1200
|
.prepare(`SELECT tenant_id FROM memories WHERE id = ?`)
|
|
1203
1201
|
.get(id);
|
|
1204
1202
|
if (!row || row.tenant_id !== ctx.tenantId) {
|
|
1205
|
-
throw new
|
|
1203
|
+
throw new NotFoundError(`memory not found: ${id}`);
|
|
1206
1204
|
}
|
|
1207
1205
|
}
|
|
1208
1206
|
finally {
|
|
@@ -1234,13 +1232,13 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
1234
1232
|
// info leak.
|
|
1235
1233
|
const old = readEntry(ctx.hippoRoot, oldId, ctx.tenantId);
|
|
1236
1234
|
if (!old) {
|
|
1237
|
-
throw new
|
|
1235
|
+
throw new NotFoundError(`Memory not found: ${oldId}`);
|
|
1238
1236
|
}
|
|
1239
1237
|
// Guard: not already superseded. The CAS UPDATE below race-safely closes
|
|
1240
1238
|
// the window between this read and the write; this check just produces a
|
|
1241
1239
|
// clearer error in the common single-writer case.
|
|
1242
1240
|
if (old.superseded_by) {
|
|
1243
|
-
throw new
|
|
1241
|
+
throw new ConflictError(`Memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
1244
1242
|
}
|
|
1245
1243
|
const newEntry = createSuccessor(old, newContent, {
|
|
1246
1244
|
tenantId: ctx.tenantId,
|
|
@@ -1268,7 +1266,7 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
1268
1266
|
`).run(newEntry.id, oldId, ctx.tenantId);
|
|
1269
1267
|
if ((result.changes ?? 0) === 0) {
|
|
1270
1268
|
db.exec('ROLLBACK');
|
|
1271
|
-
throw new
|
|
1269
|
+
throw new ConflictError(`Memory ${oldId} already superseded by another writer`);
|
|
1272
1270
|
}
|
|
1273
1271
|
// v0.30 / E2 — DAG live-coupling: OLD entry just transitioned to
|
|
1274
1272
|
// superseded. Its parent (if any) needs rebuild. Lands strictly
|
|
@@ -1339,7 +1337,7 @@ export function archiveRaw(ctx, id, reason, opts = {}) {
|
|
|
1339
1337
|
.prepare(`SELECT tenant_id FROM memories WHERE id = ?`)
|
|
1340
1338
|
.get(id);
|
|
1341
1339
|
if (!row || row.tenant_id !== ctx.tenantId) {
|
|
1342
|
-
throw new
|
|
1340
|
+
throw new NotFoundError(`memory not found: ${id}`);
|
|
1343
1341
|
}
|
|
1344
1342
|
archiveRawMemory(db, id, {
|
|
1345
1343
|
reason,
|
|
@@ -1418,8 +1416,9 @@ export function authCreate(ctx, opts) {
|
|
|
1418
1416
|
},
|
|
1419
1417
|
});
|
|
1420
1418
|
}
|
|
1421
|
-
catch {
|
|
1419
|
+
catch (error) {
|
|
1422
1420
|
// Audit must not crash a successful mint.
|
|
1421
|
+
reportAuditWriteFailure('auth_create', String(error), result.keyId);
|
|
1423
1422
|
}
|
|
1424
1423
|
return { keyId: result.keyId, plaintext: result.plaintext, tenantId: ctx.tenantId, role };
|
|
1425
1424
|
}
|
|
@@ -1457,11 +1456,11 @@ export function authRevoke(ctx, keyId) {
|
|
|
1457
1456
|
.prepare(`SELECT key_id, tenant_id, revoked_at, role FROM api_keys WHERE key_id = ?`)
|
|
1458
1457
|
.get(keyId);
|
|
1459
1458
|
if (!row) {
|
|
1460
|
-
throw new
|
|
1459
|
+
throw new NotFoundError(`Unknown key_id: ${keyId}`);
|
|
1461
1460
|
}
|
|
1462
1461
|
// Cross-tenant access denied: same message as missing key, no info leak.
|
|
1463
1462
|
if (row.tenant_id !== ctx.tenantId) {
|
|
1464
|
-
throw new
|
|
1463
|
+
throw new NotFoundError(`Unknown key_id: ${keyId}`);
|
|
1465
1464
|
}
|
|
1466
1465
|
if (ctx.actor.viaAuthResolver && row.role === 'admin') {
|
|
1467
1466
|
throw new ForbiddenError('An auth resolver admin cannot revoke an admin key, which outranks it');
|
|
@@ -1490,8 +1489,9 @@ export function authRevoke(ctx, keyId) {
|
|
|
1490
1489
|
targetId: keyId,
|
|
1491
1490
|
});
|
|
1492
1491
|
}
|
|
1493
|
-
catch {
|
|
1492
|
+
catch (error) {
|
|
1494
1493
|
// Audit must not crash a successful revoke.
|
|
1494
|
+
reportAuditWriteFailure('auth_revoke', String(error), keyId);
|
|
1495
1495
|
}
|
|
1496
1496
|
}
|
|
1497
1497
|
return { ok: true, revokedAt };
|
|
@@ -1519,13 +1519,13 @@ function changeScopeGrant(ctx, keyId, scope, op) {
|
|
|
1519
1519
|
.prepare(`SELECT tenant_id, revoked_at FROM api_keys WHERE key_id = ?`)
|
|
1520
1520
|
.get(keyId);
|
|
1521
1521
|
if (!row || row.tenant_id !== ctx.tenantId) {
|
|
1522
|
-
throw new
|
|
1522
|
+
throw new NotFoundError(`Unknown key_id: ${keyId}`);
|
|
1523
1523
|
}
|
|
1524
1524
|
if (op === 'auth_grant' && row.revoked_at) {
|
|
1525
|
-
throw new
|
|
1525
|
+
throw new ConflictError(`${keyId} is revoked; a grant on it would never apply`);
|
|
1526
1526
|
}
|
|
1527
1527
|
if (!isRestrictedScope(scope)) {
|
|
1528
|
-
throw new
|
|
1528
|
+
throw new BadRequestError(`${scope} is not a restricted scope; it is already readable by default`);
|
|
1529
1529
|
}
|
|
1530
1530
|
if (op === 'auth_grant')
|
|
1531
1531
|
grantScope(db, keyId, scope);
|
|
@@ -1536,7 +1536,7 @@ function changeScopeGrant(ctx, keyId, scope, op) {
|
|
|
1536
1536
|
}
|
|
1537
1537
|
catch (err) {
|
|
1538
1538
|
// Audit must not undo a grant change that already committed; surface it instead.
|
|
1539
|
-
|
|
1539
|
+
reportAuditWriteFailure(op, String(err), keyId);
|
|
1540
1540
|
}
|
|
1541
1541
|
return { ok: true };
|
|
1542
1542
|
}
|
|
@@ -2231,10 +2231,10 @@ export function restoreDormant(ctx, id) {
|
|
|
2231
2231
|
try {
|
|
2232
2232
|
const dormant = readDormantSnapshot(db, ctx.tenantId, id);
|
|
2233
2233
|
if (!dormant) {
|
|
2234
|
-
throw new
|
|
2234
|
+
throw new NotFoundError(`dormant memory not found: ${id}`);
|
|
2235
2235
|
}
|
|
2236
2236
|
if (db.prepare(`SELECT 1 FROM memories WHERE id = ?`).get(id) !== undefined) {
|
|
2237
|
-
throw new
|
|
2237
|
+
throw new ConflictError(`memory ${id} is already active; forget it before restoring its dormant copy`);
|
|
2238
2238
|
}
|
|
2239
2239
|
const now = new Date();
|
|
2240
2240
|
// Dormant rows are long-lived, so a snapshot can predate a field added
|
|
@@ -2293,7 +2293,7 @@ export function forgetDormant(ctx, id) {
|
|
|
2293
2293
|
const db = openHippoDb(ctx.hippoRoot);
|
|
2294
2294
|
try {
|
|
2295
2295
|
if (!deleteDormantRow(db, ctx.tenantId, id)) {
|
|
2296
|
-
throw new
|
|
2296
|
+
throw new NotFoundError(`dormant memory not found: ${id}`);
|
|
2297
2297
|
}
|
|
2298
2298
|
try {
|
|
2299
2299
|
appendAuditEvent(db, {
|
|
@@ -2304,8 +2304,9 @@ export function forgetDormant(ctx, id) {
|
|
|
2304
2304
|
metadata: { dormant: true },
|
|
2305
2305
|
});
|
|
2306
2306
|
}
|
|
2307
|
-
catch {
|
|
2307
|
+
catch (error) {
|
|
2308
2308
|
// Best-effort, like every other forget audit row: the delete stands.
|
|
2309
|
+
reportAuditWriteFailure('forget', String(error), id);
|
|
2309
2310
|
}
|
|
2310
2311
|
}
|
|
2311
2312
|
finally {
|
|
@@ -2351,9 +2352,9 @@ export function quarantineList(ctx, opts = {}) {
|
|
|
2351
2352
|
function loadPendingQuarantineRow(db, tenantId, id) {
|
|
2352
2353
|
const row = getQuarantineRow(db, tenantId, id);
|
|
2353
2354
|
if (!row)
|
|
2354
|
-
throw new
|
|
2355
|
+
throw new NotFoundError(`not quarantined: ${id}`);
|
|
2355
2356
|
if (row.status !== 'pending')
|
|
2356
|
-
throw new
|
|
2357
|
+
throw new ConflictError(`${id} is already ${row.status}`);
|
|
2357
2358
|
return row;
|
|
2358
2359
|
}
|
|
2359
2360
|
/** Release a quarantined memory to its original scope. Admin only; the scope guard refuses a row moved since (mirrors restoreDormant). */
|
|
@@ -2371,7 +2372,7 @@ export function quarantineApprove(ctx, id) {
|
|
|
2371
2372
|
.prepare(`UPDATE memories SET scope = ? WHERE id = ? AND tenant_id = ? AND scope = ?`)
|
|
2372
2373
|
.run(row.originalScope, id, ctx.tenantId, quarantineScope);
|
|
2373
2374
|
if (Number(updated.changes ?? 0) !== 1) {
|
|
2374
|
-
throw new
|
|
2375
|
+
throw new ConflictError(`memory ${id} scope changed since quarantine; refusing to approve`);
|
|
2375
2376
|
}
|
|
2376
2377
|
approveQuarantineRow(db, ctx.tenantId, id, ctx.actor.subject);
|
|
2377
2378
|
appendAuditEvent(db, {
|
|
@@ -2688,16 +2689,8 @@ export async function sleep(ctx, opts = {}) {
|
|
|
2688
2689
|
}
|
|
2689
2690
|
}
|
|
2690
2691
|
catch (auditErr) {
|
|
2691
|
-
//
|
|
2692
|
-
|
|
2693
|
-
// This guards the case where consolidation AND audit-emit fail in the
|
|
2694
|
-
// same invocation against the same DB (correlated: same disk, same
|
|
2695
|
-
// schema state) — losing the original error makes diagnosis much harder.
|
|
2696
|
-
// SAFETY: this is a best-effort log message only; property access on
|
|
2697
|
-
// any JS value is safe (undefined if absent), preserving the existing
|
|
2698
|
-
// lenient formatting even when something non-Error was thrown.
|
|
2699
|
-
// eslint-disable-next-line no-console
|
|
2700
|
-
console.error(`[hippo] api.sleep audit emit failed: ${auditErr.message}`);
|
|
2692
|
+
// Logged, never thrown: a second failure must not mask the original phaseError.
|
|
2693
|
+
reportAuditWriteFailure('consolidate', String(auditErr));
|
|
2701
2694
|
}
|
|
2702
2695
|
}
|
|
2703
2696
|
}
|
package/dist/audit.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function auditMemory(entry: MemoryEntry, backsObject?: boolean):
|
|
|
18
18
|
/** `backing`: ids of memories that back an object (store.memoriesBackingObjects). */
|
|
19
19
|
export declare function auditMemories(entries: MemoryEntry[], backing: ReadonlySet<string>): AuditResult;
|
|
20
20
|
export declare function isContentWorthStoring(content: string): boolean;
|
|
21
|
-
export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject", "agent_memory_restore", "agent_memory_set_aside"];
|
|
21
|
+
export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject", "agent_memory_restore", "agent_memory_set_aside", "project_merge", "project_repair"];
|
|
22
22
|
export type AuditOp = (typeof AUDIT_OPS)[number];
|
|
23
23
|
export interface AppendAuditOpts {
|
|
24
24
|
tenantId: string;
|
|
@@ -33,6 +33,10 @@ export type AuditQueryFields = {
|
|
|
33
33
|
};
|
|
34
34
|
export declare function auditQueryFields(query: string): AuditQueryFields;
|
|
35
35
|
export declare function appendAuditEvent(db: DatabaseSyncLike, opts: AppendAuditOpts): void;
|
|
36
|
+
/** For callers that keep a mutation when its audit row fails: the failure is logged and counted, never silent. */
|
|
37
|
+
export declare function reportAuditWriteFailure(op: AuditOp, reason: string, targetId?: string | null): void;
|
|
38
|
+
/** Audit rows this process failed to write; the loopback `/health` body reports it. */
|
|
39
|
+
export declare function auditWriteFailureCount(): number;
|
|
36
40
|
export interface QueryAuditOpts {
|
|
37
41
|
tenantId: string;
|
|
38
42
|
op?: AuditOp;
|
package/dist/audit.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canAutoDelete } from './memory.js';
|
|
3
|
+
import { log } from './log.js';
|
|
3
4
|
export const STOP_WORDS = new Set([
|
|
4
5
|
'the', 'a', 'an', 'is', 'was', 'are', 'were', 'be', 'been', 'being',
|
|
5
6
|
'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'it',
|
|
@@ -234,6 +235,8 @@ export const AUDIT_OPS = [
|
|
|
234
235
|
'quarantine_reject', // emitted by api.quarantineReject
|
|
235
236
|
'agent_memory_restore', // emitted by the agent memory sync when a deleted note comes back
|
|
236
237
|
'agent_memory_set_aside', // emitted by the agent memory sync when a note is deleted or refused
|
|
238
|
+
'project_merge', // emitted by `hippo projects merge --apply` with every id it touched
|
|
239
|
+
'project_repair', // emitted by `hippo projects repair --apply` with every id it touched
|
|
237
240
|
];
|
|
238
241
|
function isBigIntValue(value) {
|
|
239
242
|
return typeof value === 'bigint';
|
|
@@ -250,6 +253,16 @@ export function auditQueryFields(query) {
|
|
|
250
253
|
export function appendAuditEvent(db, opts) {
|
|
251
254
|
db.prepare(`INSERT INTO audit_log (ts, tenant_id, actor, op, target_id, metadata_json) VALUES (?, ?, ?, ?, ?, ?)`).run(new Date().toISOString(), opts.tenantId, opts.actor, opts.op, opts.targetId ?? null, JSON.stringify(opts.metadata ?? {}, bigintSafeReplacer));
|
|
252
255
|
}
|
|
256
|
+
let auditWriteFailures = 0;
|
|
257
|
+
/** For callers that keep a mutation when its audit row fails: the failure is logged and counted, never silent. */
|
|
258
|
+
export function reportAuditWriteFailure(op, reason, targetId) {
|
|
259
|
+
auditWriteFailures++;
|
|
260
|
+
log.error(`audit write failed: ${reason}`, { op, target: targetId ?? undefined });
|
|
261
|
+
}
|
|
262
|
+
/** Audit rows this process failed to write; the loopback `/health` body reports it. */
|
|
263
|
+
export function auditWriteFailureCount() {
|
|
264
|
+
return auditWriteFailures;
|
|
265
|
+
}
|
|
253
266
|
export function queryAuditEvents(db, opts) {
|
|
254
267
|
const where = ['tenant_id = ?'];
|
|
255
268
|
const params = [opts.tenantId];
|
package/dist/autolearn.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function extractLessons(gitLog: string, customPatterns?: string[]
|
|
|
20
20
|
* steps: this function is the write-path gate, called by each caller that
|
|
21
21
|
* actually stores a lesson, not by the parser itself.
|
|
22
22
|
*
|
|
23
|
-
* Order is preserved in both output arrays.
|
|
23
|
+
* Order is preserved in both output arrays; both hold lessons with secret shapes redacted.
|
|
24
24
|
*/
|
|
25
25
|
export declare function partitionLessons(lessons: string[]): {
|
|
26
26
|
kept: string[];
|
package/dist/autolearn.js
CHANGED
|
@@ -7,14 +7,16 @@ import { createMemory, Layer, DEFAULT_HALF_LIFE_DAYS } from './memory.js';
|
|
|
7
7
|
import { loadAllEntries } from './store.js';
|
|
8
8
|
import { textOverlap } from './search.js';
|
|
9
9
|
import { isContentWorthStoring } from './audit.js';
|
|
10
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
10
11
|
/** A memory of a failed command, "Command '<cmd>' failed: <truncated stderr>"; no store is in reach, so `hippo watch` re-derives its half-life from the store's config. */
|
|
11
12
|
export function captureError(exitCode, stderr, command, tenantId) {
|
|
12
13
|
// Truncate to first 500 chars to avoid storing megabytes of build logs
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
const clean = redactSecretsStrict(stderr);
|
|
15
|
+
const wasTruncated = clean.length > 500;
|
|
16
|
+
const truncated = clean.slice(0, 500).trim();
|
|
15
17
|
const suffix = wasTruncated ? ' [truncated]' : '';
|
|
16
18
|
// Strip leading env var assignments (KEY=val or key=val) before the actual command name
|
|
17
|
-
const safeCmd = command.replace(/^([A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/, '').trim() || '(redacted)';
|
|
19
|
+
const safeCmd = redactSecretsStrict(command.replace(/^([A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/, '').trim()) || '(redacted)';
|
|
18
20
|
const content = `Command '${safeCmd}' failed (exit ${exitCode}): ${truncated}${suffix}`;
|
|
19
21
|
// Derive a sanitized tag from the command name (first word, strip path)
|
|
20
22
|
const cmdBase = safeCmd.split(/\s+/)[0].replace(/[^a-zA-Z0-9-]/g, '');
|
|
@@ -78,12 +80,12 @@ export function extractLessons(gitLog, customPatterns) {
|
|
|
78
80
|
* steps: this function is the write-path gate, called by each caller that
|
|
79
81
|
* actually stores a lesson, not by the parser itself.
|
|
80
82
|
*
|
|
81
|
-
* Order is preserved in both output arrays.
|
|
83
|
+
* Order is preserved in both output arrays; both hold lessons with secret shapes redacted.
|
|
82
84
|
*/
|
|
83
85
|
export function partitionLessons(lessons) {
|
|
84
86
|
const kept = [];
|
|
85
87
|
const dropped = [];
|
|
86
|
-
for (const lesson of lessons) {
|
|
88
|
+
for (const lesson of lessons.map((l) => redactSecretsStrict(l))) {
|
|
87
89
|
if (isContentWorthStoring(lesson)) {
|
|
88
90
|
kept.push(lesson);
|
|
89
91
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** A host lifecycle moment hippo can capture from. */
|
|
2
|
+
export type CaptureEvent = 'prompt' | 'tool-failure' | 'pre-compact' | 'post-compact' | 'session-start' | 'session-end' | 'reset';
|
|
3
|
+
/** What hippo read from one host payload, before anything is written. */
|
|
4
|
+
export interface CaptureInput {
|
|
5
|
+
readonly runtime: string;
|
|
6
|
+
readonly event: CaptureEvent;
|
|
7
|
+
/** True when no host payload arrived: a person ran the verb by hand. */
|
|
8
|
+
readonly manual: boolean;
|
|
9
|
+
readonly sessionId: string | null;
|
|
10
|
+
readonly cwd: string | null;
|
|
11
|
+
readonly transcriptPath: string | null;
|
|
12
|
+
/** The host's own reason for the event, such as `auto` or `manual` compaction. */
|
|
13
|
+
readonly trigger: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** The outcome of reading a payload: usable input, a payload hippo refuses, or no payload at all. */
|
|
16
|
+
export type CaptureReceipt = {
|
|
17
|
+
readonly status: 'received';
|
|
18
|
+
readonly input: CaptureInput;
|
|
19
|
+
} | {
|
|
20
|
+
readonly status: 'skipped' | 'unavailable';
|
|
21
|
+
readonly reason: string;
|
|
22
|
+
};
|
|
23
|
+
/** Working state saved before the host drops context, so the session can resume; it is not a lesson. */
|
|
24
|
+
export interface Checkpoint {
|
|
25
|
+
readonly runtime: string;
|
|
26
|
+
readonly sessionId: string | null;
|
|
27
|
+
readonly task: string;
|
|
28
|
+
readonly summary: string;
|
|
29
|
+
readonly nextStep: string;
|
|
30
|
+
readonly savedAt: string;
|
|
31
|
+
}
|
|
32
|
+
/** How far capture has read one session's source, so a retry resumes instead of re-reading or skipping. */
|
|
33
|
+
export interface ProgressCursor {
|
|
34
|
+
readonly runtime: string;
|
|
35
|
+
readonly sessionId: string;
|
|
36
|
+
readonly source: string;
|
|
37
|
+
/** Opaque position in the source, such as a byte offset or the last turn id read. */
|
|
38
|
+
readonly position: string;
|
|
39
|
+
readonly updatedAt: string;
|
|
40
|
+
}
|
|
41
|
+
/** A string guard that avoids `typeof`; it matches it for every value `JSON.parse` can produce. */
|
|
42
|
+
export declare function isStringValue<T>(value: T): value is T & string;
|
|
43
|
+
/** An object guard that avoids `typeof`; arrays count as objects, as they do for `typeof`. */
|
|
44
|
+
export declare function isObjectLike<T>(value: T): value is T & object;
|
|
45
|
+
/** Reads a Claude Code PreCompact payload; only an empty stdin counts as a manual run. */
|
|
46
|
+
export declare function readClaudeCodePreCompact(stdinText: string | undefined, timedOut: boolean): CaptureReceipt;
|
|
47
|
+
//# sourceMappingURL=capture-contract.d.ts.map
|