hippo-memory 1.57.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/agent-memories/claude-code.js +1 -1
- package/dist/agent-memories/gemini.js +1 -1
- package/dist/api-errors.d.ts +27 -0
- package/dist/api-errors.js +37 -0
- package/dist/api.d.ts +5 -5
- package/dist/api.js +40 -47
- package/dist/audit.d.ts +4 -0
- package/dist/audit.js +11 -0
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +7 -5
- package/dist/capture-contract.d.ts +47 -0
- package/dist/capture-contract.js +49 -0
- package/dist/capture-error.js +2 -1
- package/dist/capture.d.ts +0 -13
- package/dist/capture.js +5 -66
- package/dist/cli/shared.js +10 -6
- package/dist/cli.js +100 -39
- package/dist/client.js +9 -0
- package/dist/codex-patch.js +1 -1
- package/dist/compaction-record.d.ts +1 -1
- package/dist/compaction-record.js +3 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/connectors/github/dlq.js +5 -2
- package/dist/connectors/github/octokit-client.js +4 -2
- package/dist/connectors/slack/dlq.js +6 -2
- package/dist/connectors/slack/web-client.js +7 -5
- package/dist/consolidate.d.ts +10 -0
- package/dist/consolidate.js +36 -34
- package/dist/customer-notes.js +14 -13
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +1 -1
- package/dist/db.d.ts +12 -0
- package/dist/db.js +62 -1
- package/dist/decisions.js +9 -8
- package/dist/doctor.js +5 -0
- package/dist/embedding-provider.js +3 -3
- package/dist/embeddings.d.ts +4 -4
- package/dist/embeddings.js +72 -16
- package/dist/extract.js +3 -2
- package/dist/http-retry.d.ts +21 -0
- package/dist/http-retry.js +50 -0
- package/dist/http-util.d.ts +8 -0
- package/dist/http-util.js +10 -0
- package/dist/importers.d.ts +2 -0
- package/dist/importers.js +16 -5
- package/dist/incidents.js +11 -10
- package/dist/judgment.js +10 -17
- package/dist/log.d.ts +25 -0
- package/dist/log.js +48 -0
- package/dist/mcp/server.js +52 -24
- package/dist/mcp/tool-args.d.ts +21 -0
- package/dist/mcp/tool-args.js +80 -0
- package/dist/memory.js +3 -2
- package/dist/overlap-index.d.ts +7 -0
- package/dist/overlap-index.js +38 -0
- package/dist/pilot-arm.d.ts +9 -0
- package/dist/pilot-arm.js +47 -0
- package/dist/policies.js +12 -11
- package/dist/predictions.js +9 -8
- package/dist/processes.js +14 -13
- package/dist/project-briefs.js +16 -15
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +25 -1
- package/dist/raw-archive.js +7 -6
- package/dist/recall-scope.d.ts +2 -1
- package/dist/recall-scope.js +2 -1
- package/dist/refine-llm.js +3 -2
- package/dist/reject-flow.js +6 -9
- package/dist/rejection.d.ts +2 -1
- package/dist/rejection.js +2 -1
- package/dist/search.js +14 -2
- package/dist/secret-detect.d.ts +13 -1
- package/dist/secret-detect.js +33 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +180 -409
- package/dist/session-digest.js +2 -1
- package/dist/shared.js +7 -6
- package/dist/skills.js +15 -14
- package/dist/store.js +4 -4
- package/dist/token-ledger.d.ts +4 -2
- package/dist/token-ledger.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/connectors/slack/ratelimit.d.ts +0 -9
- package/dist/connectors/slack/ratelimit.js +0 -18
package/README.md
CHANGED
|
@@ -559,6 +559,17 @@ are not recorded yet. It holds ids, hashes, counts and reasons only, never promp
|
|
|
559
559
|
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
560
|
older than 90 days are pruned; at a heavy 300 prompts a day that is about 190 MB per store.
|
|
561
561
|
|
|
562
|
+
**Run a pilot with a holdout group.** Set `{"pilot":{"holdoutRateBp":2000}}` in `.hippo/config.json`
|
|
563
|
+
to hold back memories from about 20% of sessions. The rate is in basis points, 0 to 10000, and 0
|
|
564
|
+
is off (the default). The setting is read from the store the token ledger writes to. A session
|
|
565
|
+
lands in its arm by a hash of its id, and the first hook call writes one row to the token ledger.
|
|
566
|
+
A holdout session gets no memories from the per-prompt hook, the SessionStart hook or compact-resume.
|
|
567
|
+
The agent's own `hippo context` pull is gated in Claude Code only, so a Codex holdout session still
|
|
568
|
+
gets memories from it. Capture still runs. `hippo recall`, the HTTP API and the MCP tools are not gated
|
|
569
|
+
and write no arm row. Agents are told to call the MCP context tool at session start, and those calls
|
|
570
|
+
are not recorded. Set the rate to 0 only after the pilot window closes, because 0 ends every holdout at once.
|
|
571
|
+
`hippo doctor` shows the pilot when it is on.
|
|
572
|
+
|
|
562
573
|
---
|
|
563
574
|
|
|
564
575
|
### Outcome feedback
|
|
@@ -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';
|
|
@@ -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
|
@@ -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',
|
|
@@ -250,6 +251,16 @@ export function auditQueryFields(query) {
|
|
|
250
251
|
export function appendAuditEvent(db, opts) {
|
|
251
252
|
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
253
|
}
|
|
254
|
+
let auditWriteFailures = 0;
|
|
255
|
+
/** For callers that keep a mutation when its audit row fails: the failure is logged and counted, never silent. */
|
|
256
|
+
export function reportAuditWriteFailure(op, reason, targetId) {
|
|
257
|
+
auditWriteFailures++;
|
|
258
|
+
log.error(`audit write failed: ${reason}`, { op, target: targetId ?? undefined });
|
|
259
|
+
}
|
|
260
|
+
/** Audit rows this process failed to write; the loopback `/health` body reports it. */
|
|
261
|
+
export function auditWriteFailureCount() {
|
|
262
|
+
return auditWriteFailures;
|
|
263
|
+
}
|
|
253
264
|
export function queryAuditEvents(db, opts) {
|
|
254
265
|
const where = ['tenant_id = ?'];
|
|
255
266
|
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
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The shared shape every agent adapter turns a host payload into, so capture code is written once for all runtimes.
|
|
2
|
+
// Field meanings, statuses and how to add a runtime: docs/integrations/agent-inventory.md.
|
|
3
|
+
/** A string guard that avoids `typeof`; it matches it for every value `JSON.parse` can produce. */
|
|
4
|
+
export function isStringValue(value) {
|
|
5
|
+
return String(value) === value;
|
|
6
|
+
}
|
|
7
|
+
/** An object guard that avoids `typeof`; arrays count as objects, as they do for `typeof`. */
|
|
8
|
+
export function isObjectLike(value) {
|
|
9
|
+
return value !== null && value instanceof Object;
|
|
10
|
+
}
|
|
11
|
+
/** Reads a Claude Code PreCompact payload; only an empty stdin counts as a manual run. */
|
|
12
|
+
export function readClaudeCodePreCompact(stdinText, timedOut) {
|
|
13
|
+
const empty = !stdinText || stdinText.trim() === '';
|
|
14
|
+
if (timedOut && empty) {
|
|
15
|
+
return { status: 'unavailable', reason: 'no PreCompact payload arrived before the stdin wait window closed' };
|
|
16
|
+
}
|
|
17
|
+
const base = { runtime: 'claude-code', event: 'pre-compact' };
|
|
18
|
+
if (empty) {
|
|
19
|
+
return { status: 'received', input: { ...base, manual: true, sessionId: null, cwd: null, transcriptPath: null, trigger: null } };
|
|
20
|
+
}
|
|
21
|
+
let payload;
|
|
22
|
+
try {
|
|
23
|
+
payload = JSON.parse(stdinText.trim());
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Non-JSON stdin leaves payload undefined, which the shape check rejects like an explicit null.
|
|
27
|
+
}
|
|
28
|
+
// A bad payload is skipped, never treated as manual: discovery could pick up another session's transcript.
|
|
29
|
+
if (!isObjectLike(payload) || !('transcript_path' in payload) || !isStringValue(payload.transcript_path)) {
|
|
30
|
+
return { status: 'skipped', reason: 'malformed or incomplete PreCompact payload (missing string transcript_path)' };
|
|
31
|
+
}
|
|
32
|
+
const transcriptPath = payload.transcript_path;
|
|
33
|
+
// Path shape only: CLAUDE_CONFIG_DIR can move the transcript root, and a same-user process can already read every transcript.
|
|
34
|
+
if (!/\.jsonl$/i.test(transcriptPath)) {
|
|
35
|
+
return { status: 'skipped', reason: `payload transcript_path is not a .jsonl file: ${transcriptPath}` };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
status: 'received',
|
|
39
|
+
input: {
|
|
40
|
+
...base,
|
|
41
|
+
manual: false,
|
|
42
|
+
sessionId: 'session_id' in payload && isStringValue(payload.session_id) ? payload.session_id : null,
|
|
43
|
+
cwd: 'cwd' in payload && isStringValue(payload.cwd) ? payload.cwd : null,
|
|
44
|
+
transcriptPath,
|
|
45
|
+
trigger: 'trigger' in payload && isStringValue(payload.trigger) ? payload.trigger : null,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=capture-contract.js.map
|
package/dist/capture-error.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadConfig } from './config.js';
|
|
|
7
7
|
import { closeHippoDb, openHippoDb } from './db.js';
|
|
8
8
|
import { recordFailure } from './failure-log.js';
|
|
9
9
|
import { blockHash } from './token-ledger.js';
|
|
10
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
10
11
|
const MAX_LEN = 200;
|
|
11
12
|
/** The user, a permission prompt or a hook said no: routine, not a lesson. */
|
|
12
13
|
const DECLINED = /user (?:doesn't|does not) want|denied by (?:the )?user|user (?:rejected|declined|denied)|permission to use|was blocked by (?:a )?hook/i;
|
|
@@ -44,7 +45,7 @@ export function lessonFromFailure(payload) {
|
|
|
44
45
|
if (!isString(p.error) || p.error.trim().length < 12)
|
|
45
46
|
return { skip: 'skipped-invalid', text: null, detail: null };
|
|
46
47
|
const tool = isString(p.tool_name) ? p.tool_name : 'tool';
|
|
47
|
-
const error = p.error.replace(/\s+/g, ' ').trim();
|
|
48
|
+
const error = redactSecretsStrict(p.error.replace(/\s+/g, ' ').trim());
|
|
48
49
|
const text = `${tool}: ${error}`.slice(0, MAX_LEN);
|
|
49
50
|
const command = isObject(p.tool_input) && isString(p.tool_input['command']) ? p.tool_input['command'].replace(LEADING_CD, '') : '';
|
|
50
51
|
const head = command.trim().split(/\s+/).slice(0, 2).join(' ');
|
package/dist/capture.d.ts
CHANGED
|
@@ -49,19 +49,6 @@ export interface CaptureOptions {
|
|
|
49
49
|
tenantId?: string;
|
|
50
50
|
originProject?: string;
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Runtime shape guards used throughout this file wherever a value arrives
|
|
54
|
-
* unparsed (JSONL transcript records, stdout/stderr write() chunks). Generic
|
|
55
|
-
* over the input so the parameter is never annotated `unknown` directly — TS
|
|
56
|
-
* infers it from the call site — while the check itself avoids `typeof` by
|
|
57
|
-
* testing identity against the coercion (`String(x) === x`) /
|
|
58
|
-
* prototype-chain (`instanceof Object`) instead. Behaviourally equivalent to
|
|
59
|
-
* `typeof x === 'string'` / a truthy `typeof x === 'object'` check for
|
|
60
|
-
* anything `JSON.parse` can produce (the only divergence is boxed
|
|
61
|
-
* primitives, which JSON.parse never yields).
|
|
62
|
-
*/
|
|
63
|
-
export declare function isStringValue<T>(value: T): value is T & string;
|
|
64
|
-
export declare function isObjectLike<T>(value: T): value is T & object;
|
|
65
52
|
/** Message for a caught value of unknown shape. `cause` names the sanctioned unknown-input case (error-cause enrichment). */
|
|
66
53
|
export declare function errorMessage(cause: unknown): string;
|
|
67
54
|
/** Transcript-line flags isNonHumanUserLine reads (any may be absent); `promptSource: 'system'` marks Claude Code's own notices. */
|