hippo-memory 1.48.0 → 1.50.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/dist/api.d.ts +36 -0
- package/dist/api.js +185 -9
- package/dist/audit.d.ts +1 -1
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +20 -1
- package/dist/cli.js +118 -2
- package/dist/connectors/github/ingest.js +1 -0
- package/dist/connectors/slack/ingest.js +1 -0
- package/dist/consolidate.d.ts +2 -0
- package/dist/consolidate.js +20 -3
- package/dist/dag.js +15 -6
- package/dist/db.js +42 -1
- package/dist/dedupe.js +5 -2
- package/dist/extract.js +1 -0
- package/dist/instruction-detect.d.ts +7 -0
- package/dist/instruction-detect.js +27 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +6 -17
- package/dist/project-briefs.js +4 -2
- package/dist/quarantine.d.ts +31 -0
- package/dist/quarantine.js +63 -0
- package/dist/recall-scope.d.ts +22 -10
- package/dist/recall-scope.js +41 -26
- package/dist/secret-detect.js +1 -1
- package/dist/server.js +69 -10
- package/dist/shared.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { deleteEntry, loadAllEntries, type TaskSnapshot, type SessionEvent } fro
|
|
|
11
11
|
import { type RejectedValueRow } from './rejection.js';
|
|
12
12
|
import { type DormantMemory, type ListDormantOpts } from './dormant.js';
|
|
13
13
|
import { type TokenSummary, type TokenSurface } from './token-ledger.js';
|
|
14
|
+
import { type QuarantineStatus } from './quarantine.js';
|
|
14
15
|
import { type FailureSummary } from './failure-log.js';
|
|
15
16
|
import { type SessionHandoff } from './handoff.js';
|
|
16
17
|
import { type MemoryKind, type MemoryEntry } from './memory.js';
|
|
@@ -40,6 +41,8 @@ export interface Actor {
|
|
|
40
41
|
/** 'cli' | 'localhost:cli' | 'api_key:<key_id>' | 'mcp' | 'connector:slack' | 'connector:github' */
|
|
41
42
|
subject: string;
|
|
42
43
|
role: 'admin' | 'member';
|
|
44
|
+
/** EI2: restricted scopes a member key may read (auth.ts grantScope). Unused for admin actors. */
|
|
45
|
+
scopes?: readonly string[];
|
|
43
46
|
}
|
|
44
47
|
export interface Context {
|
|
45
48
|
hippoRoot: string;
|
|
@@ -108,11 +111,17 @@ export interface RememberOpts {
|
|
|
108
111
|
* back and the error is rethrown.
|
|
109
112
|
*/
|
|
110
113
|
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
114
|
+
/** CD5: connector-ingested content an agent doesn't control; gates detectInstruction. CLI/HTTP/MCP never set this. */
|
|
115
|
+
untrusted?: boolean;
|
|
111
116
|
}
|
|
112
117
|
export interface RememberResult {
|
|
113
118
|
id: string;
|
|
114
119
|
kind: MemoryKind;
|
|
115
120
|
tenantId: string;
|
|
121
|
+
/** Set only when untrusted content was flagged and quarantined instead of stored under its requested scope. */
|
|
122
|
+
quarantined?: {
|
|
123
|
+
reason: string;
|
|
124
|
+
};
|
|
116
125
|
}
|
|
117
126
|
export declare function remember(ctx: Context, opts: RememberOpts): RememberResult;
|
|
118
127
|
export interface RecallOpts {
|
|
@@ -875,6 +884,14 @@ export interface AuthRevokeResult {
|
|
|
875
884
|
revokedAt: string;
|
|
876
885
|
}
|
|
877
886
|
export declare function authRevoke(ctx: Context, keyId: string): AuthRevokeResult;
|
|
887
|
+
/** Shared result shape for authGrant/authUngrant, named per the file's oxlint anti-slop rule. */
|
|
888
|
+
export interface AuthGrantResult {
|
|
889
|
+
ok: true;
|
|
890
|
+
}
|
|
891
|
+
/** Grant `keyId` read access to one restricted `scope` (ROADMAP Part VIII EI2). Admin only. */
|
|
892
|
+
export declare function authGrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
|
893
|
+
/** Revoke `keyId`'s grant on `scope`. Same authorization and lookup rules as authGrant. */
|
|
894
|
+
export declare function authUngrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
|
878
895
|
export interface AuditListOpts {
|
|
879
896
|
op?: AuditOp;
|
|
880
897
|
/** ISO timestamp lower bound. */
|
|
@@ -1051,6 +1068,25 @@ export declare function restoreDormant(ctx: Context, id: string): MemoryEntry;
|
|
|
1051
1068
|
export declare function forgetDormant(ctx: Context, id: string): void;
|
|
1052
1069
|
/** Whether the tenant holds a dormant memory with this id (for "not found" hints). */
|
|
1053
1070
|
export declare function isDormant(ctx: Context, id: string): boolean;
|
|
1071
|
+
export interface QuarantineListItem {
|
|
1072
|
+
id: string;
|
|
1073
|
+
originalScope: string | null;
|
|
1074
|
+
reason: string;
|
|
1075
|
+
status: QuarantineStatus;
|
|
1076
|
+
quarantinedAt: string;
|
|
1077
|
+
decidedAt: string | null;
|
|
1078
|
+
decidedBy: string | null;
|
|
1079
|
+
contentPreview: string;
|
|
1080
|
+
}
|
|
1081
|
+
/** A tenant's quarantined memories, newest first. Default `status` is 'pending' (the review queue). */
|
|
1082
|
+
export declare function quarantineList(ctx: Context, opts?: {
|
|
1083
|
+
status?: QuarantineStatus | 'all';
|
|
1084
|
+
limit?: number;
|
|
1085
|
+
}): QuarantineListItem[];
|
|
1086
|
+
/** Release a quarantined memory to its original scope. Admin only; the scope guard refuses a row moved since (mirrors restoreDormant). */
|
|
1087
|
+
export declare function quarantineApprove(ctx: Context, id: string): void;
|
|
1088
|
+
/** Keep a quarantined memory hidden for good. Admin only; the raw row is untouched (append-only). */
|
|
1089
|
+
export declare function quarantineReject(ctx: Context, id: string): void;
|
|
1054
1090
|
export interface SleepResult {
|
|
1055
1091
|
active: number;
|
|
1056
1092
|
removed: number;
|
package/dist/api.js
CHANGED
|
@@ -13,6 +13,8 @@ import { RejectedValueError } from './rejection.js';
|
|
|
13
13
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
14
14
|
import { listDormantRows, readDormantSnapshot, deleteDormantRow, hasDormantRow, } from './dormant.js';
|
|
15
15
|
import { recordTokenUse, summarizeTokenUse } from './token-ledger.js';
|
|
16
|
+
import { detectInstruction } from './instruction-detect.js';
|
|
17
|
+
import { quarantineScopeFor, recordQuarantine, getQuarantineRow, listQuarantineRows, approveQuarantineRow, rejectQuarantineRow, } from './quarantine.js';
|
|
16
18
|
import { summarizeFailures } from './failure-log.js';
|
|
17
19
|
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
18
20
|
import { createMemory, applyOutcome, calculateStrength, Layer, } from './memory.js';
|
|
@@ -21,7 +23,7 @@ import { promoteToGlobal, getGlobalRoot, autoShare, searchBothHybrid } from './s
|
|
|
21
23
|
import { writeRecallTrace, writeRecallTraceAtRoot, recordTraceOutcome } from './recall-trace.js';
|
|
22
24
|
import { evalNow } from './ablation.js';
|
|
23
25
|
import { archiveRawMemory } from './raw-archive.js';
|
|
24
|
-
import { createApiKey, listApiKeys, revokeApiKey, } from './auth.js';
|
|
26
|
+
import { createApiKey, listApiKeys, revokeApiKey, grantScope, ungrantScope, } from './auth.js';
|
|
25
27
|
import { applyGoalStackBoost } from './goals.js';
|
|
26
28
|
import { markRetrieved, estimateTokens, hybridSearch, physicsSearch } from './search.js';
|
|
27
29
|
import { compareEntryIdentity, compareScoredResults } from './compare.js';
|
|
@@ -87,7 +89,7 @@ export class ForbiddenError extends Error {
|
|
|
87
89
|
// back-compat (`api.isPrivateScope`, test imports). NOTE: the import statement
|
|
88
90
|
// is required — a bare `export { x } from` re-export does not bind the local
|
|
89
91
|
// names this module's ~9 call sites use.
|
|
90
|
-
import { isPrivateScope, passesScopeFilterForRecall, assertScopeRequestAllowed } from './recall-scope.js';
|
|
92
|
+
import { isPrivateScope, passesScopeFilterForRecall, assertScopeRequestAllowed, isRestrictedScope } from './recall-scope.js';
|
|
91
93
|
export { isPrivateScope, passesScopeFilterForRecall };
|
|
92
94
|
export { passesCliRecallScopeFilter, ScopeForbiddenError } from './recall-scope.js';
|
|
93
95
|
// v39: classifyOriginProject lives in project-identity.ts (leaf) so
|
|
@@ -140,9 +142,11 @@ function loadAmbientEntries(hippoRoot, tenantId, pinnedOnly, includeRecent, admi
|
|
|
140
142
|
return loadAmbientCandidates(hippoRoot, tenantId, includeRecent, admitAmbient);
|
|
141
143
|
}
|
|
142
144
|
export function remember(ctx, opts) {
|
|
145
|
+
const detection = opts.untrusted ? detectInstruction(opts.content) : { flagged: false, reason: null };
|
|
146
|
+
const requestedScope = opts.scope ?? null;
|
|
143
147
|
const entry = createMemory(opts.content, {
|
|
144
148
|
kind: opts.kind ?? 'distilled',
|
|
145
|
-
scope:
|
|
149
|
+
scope: detection.flagged ? quarantineScopeFor(requestedScope) : requestedScope,
|
|
146
150
|
owner: opts.owner ?? null,
|
|
147
151
|
artifact_ref: opts.artifactRef ?? null,
|
|
148
152
|
tags: opts.tags,
|
|
@@ -151,8 +155,23 @@ export function remember(ctx, opts) {
|
|
|
151
155
|
});
|
|
152
156
|
// writeEntry threads ctx.actor.subject into its internal audit hook, so exactly
|
|
153
157
|
// one 'remember' event lands in the log with the supplied actor.
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
const afterWrite = detection.flagged
|
|
159
|
+
? (db, memoryId) => {
|
|
160
|
+
recordQuarantine(db, {
|
|
161
|
+
tenantId: ctx.tenantId,
|
|
162
|
+
memoryId,
|
|
163
|
+
originalScope: requestedScope,
|
|
164
|
+
reason: detection.reason ?? 'unknown',
|
|
165
|
+
actor: ctx.actor.subject,
|
|
166
|
+
});
|
|
167
|
+
opts.afterWrite?.(db, memoryId);
|
|
168
|
+
}
|
|
169
|
+
: opts.afterWrite;
|
|
170
|
+
writeEntry(ctx.hippoRoot, entry, { actor: ctx.actor.subject, afterWrite });
|
|
171
|
+
const result = { id: entry.id, kind: entry.kind, tenantId: ctx.tenantId };
|
|
172
|
+
if (detection.flagged)
|
|
173
|
+
result.quarantined = { reason: detection.reason ?? 'unknown' };
|
|
174
|
+
return result;
|
|
156
175
|
}
|
|
157
176
|
/**
|
|
158
177
|
* Shared construction helper for `RecallSuppressionSummary`. Used by
|
|
@@ -188,13 +207,13 @@ export function buildSuppressionSummary(counts) {
|
|
|
188
207
|
*/
|
|
189
208
|
export function recall(ctx, opts) {
|
|
190
209
|
// A member key may not unlock a private or quarantined scope by naming it.
|
|
191
|
-
assertScopeRequestAllowed(ctx.actor
|
|
210
|
+
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
192
211
|
const windowSize = recallWindowSize(opts);
|
|
193
212
|
return recallFrom(ctx, opts, windowSize, loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false));
|
|
194
213
|
}
|
|
195
214
|
/** Mode-aware recall that strengthens each returned row; never writes last_retrieval_ids (v1.11.5 lock). */
|
|
196
215
|
export async function retrieve(ctx, opts) {
|
|
197
|
-
assertScopeRequestAllowed(ctx.actor
|
|
216
|
+
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
198
217
|
const windowSize = recallWindowSize(opts);
|
|
199
218
|
let candidates = loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false);
|
|
200
219
|
if (opts.mode === 'hybrid' || opts.mode === 'physics') {
|
|
@@ -288,7 +307,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
288
307
|
// anchored `<source>:private:*` rule (v1.2.1 generalization) and
|
|
289
308
|
// defense-in-depth: connector authors cannot silently surface private
|
|
290
309
|
// rows to no-scope callers even if the SQL clause regresses.
|
|
291
|
-
entries = current.filter((e) => !
|
|
310
|
+
entries = current.filter((e) => !isRestrictedScope(e.scope ?? null));
|
|
292
311
|
}
|
|
293
312
|
// v1.12.13 / C5 — WYSIATI dropped_pre_rank counter (JS scope filter drops
|
|
294
313
|
// for api.recall; cmdRecall pipeline rolls --outcome/--layer/--as-of/etc.
|
|
@@ -725,7 +744,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
725
744
|
* - all rows fail the scope/tenant filter
|
|
726
745
|
*/
|
|
727
746
|
export function assemble(ctx, sessionId, opts = {}) {
|
|
728
|
-
assertScopeRequestAllowed(ctx.actor
|
|
747
|
+
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
729
748
|
const budget = opts.budget ?? 4000;
|
|
730
749
|
const freshTailCount = opts.freshTailCount ?? 10;
|
|
731
750
|
const summarizeOlder = opts.summarizeOlder ?? true;
|
|
@@ -1152,6 +1171,7 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
1152
1171
|
source: old.source,
|
|
1153
1172
|
confidence: 'verified',
|
|
1154
1173
|
tenantId: ctx.tenantId,
|
|
1174
|
+
scope: old.scope,
|
|
1155
1175
|
});
|
|
1156
1176
|
// Race-safe transition: open a fresh db handle, BEGIN IMMEDIATE, run all
|
|
1157
1177
|
// three steps (CAS on old + writeEntryDbOnly(new) + supersede audit row)
|
|
@@ -1401,6 +1421,50 @@ export function authRevoke(ctx, keyId) {
|
|
|
1401
1421
|
closeHippoDb(db);
|
|
1402
1422
|
}
|
|
1403
1423
|
}
|
|
1424
|
+
/** Grant `keyId` read access to one restricted `scope` (ROADMAP Part VIII EI2). Admin only. */
|
|
1425
|
+
export function authGrant(ctx, keyId, scope) {
|
|
1426
|
+
return changeScopeGrant(ctx, keyId, scope, 'auth_grant');
|
|
1427
|
+
}
|
|
1428
|
+
/** Revoke `keyId`'s grant on `scope`. Same authorization and lookup rules as authGrant. */
|
|
1429
|
+
export function authUngrant(ctx, keyId, scope) {
|
|
1430
|
+
return changeScopeGrant(ctx, keyId, scope, 'auth_ungrant');
|
|
1431
|
+
}
|
|
1432
|
+
function changeScopeGrant(ctx, keyId, scope, op) {
|
|
1433
|
+
if (ctx.actor.role !== 'admin') {
|
|
1434
|
+
throw new ForbiddenError('Only an admin key can change scope grants');
|
|
1435
|
+
}
|
|
1436
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1437
|
+
try {
|
|
1438
|
+
// SAFETY: row's shape matches the single tenant_id column in the SELECT.
|
|
1439
|
+
const row = db
|
|
1440
|
+
.prepare(`SELECT tenant_id, revoked_at FROM api_keys WHERE key_id = ?`)
|
|
1441
|
+
.get(keyId);
|
|
1442
|
+
if (!row || row.tenant_id !== ctx.tenantId) {
|
|
1443
|
+
throw new Error(`Unknown key_id: ${keyId}`);
|
|
1444
|
+
}
|
|
1445
|
+
if (op === 'auth_grant' && row.revoked_at) {
|
|
1446
|
+
throw new Error(`${keyId} is revoked; a grant on it would never apply`);
|
|
1447
|
+
}
|
|
1448
|
+
if (!isRestrictedScope(scope)) {
|
|
1449
|
+
throw new Error(`${scope} is not a restricted scope; it is already readable by default`);
|
|
1450
|
+
}
|
|
1451
|
+
if (op === 'auth_grant')
|
|
1452
|
+
grantScope(db, keyId, scope);
|
|
1453
|
+
else
|
|
1454
|
+
ungrantScope(db, keyId, scope);
|
|
1455
|
+
try {
|
|
1456
|
+
appendAuditEvent(db, { tenantId: ctx.tenantId, actor: ctx.actor.subject, op, targetId: keyId, metadata: { scope } });
|
|
1457
|
+
}
|
|
1458
|
+
catch (err) {
|
|
1459
|
+
// Audit must not undo a grant change that already committed; surface it instead.
|
|
1460
|
+
console.error(`auth: audit write failed for ${op} ${keyId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1461
|
+
}
|
|
1462
|
+
return { ok: true };
|
|
1463
|
+
}
|
|
1464
|
+
finally {
|
|
1465
|
+
closeHippoDb(db);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1404
1468
|
/**
|
|
1405
1469
|
* Read audit events scoped to `ctx.tenantId`. Read-only — no audit emit (matches
|
|
1406
1470
|
* A5: cmdAuditList does not record a 'recall'-style read event).
|
|
@@ -2045,6 +2109,118 @@ export function isDormant(ctx, id) {
|
|
|
2045
2109
|
closeHippoDb(db);
|
|
2046
2110
|
}
|
|
2047
2111
|
}
|
|
2112
|
+
const QUARANTINE_PREVIEW_CHARS = 200;
|
|
2113
|
+
/** A tenant's quarantined memories, newest first. Default `status` is 'pending' (the review queue). */
|
|
2114
|
+
export function quarantineList(ctx, opts = {}) {
|
|
2115
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2116
|
+
try {
|
|
2117
|
+
const rows = listQuarantineRows(db, ctx.tenantId, opts.status ?? 'pending', opts.limit);
|
|
2118
|
+
return rows.map((row) => {
|
|
2119
|
+
const entry = readEntry(ctx.hippoRoot, row.memoryId, ctx.tenantId);
|
|
2120
|
+
return {
|
|
2121
|
+
id: row.memoryId,
|
|
2122
|
+
originalScope: row.originalScope,
|
|
2123
|
+
reason: row.reason,
|
|
2124
|
+
status: row.status,
|
|
2125
|
+
quarantinedAt: row.quarantinedAt,
|
|
2126
|
+
decidedAt: row.decidedAt,
|
|
2127
|
+
decidedBy: row.decidedBy,
|
|
2128
|
+
contentPreview: entry ? entry.content.slice(0, QUARANTINE_PREVIEW_CHARS) : '',
|
|
2129
|
+
};
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
finally {
|
|
2133
|
+
closeHippoDb(db);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
function loadPendingQuarantineRow(db, tenantId, id) {
|
|
2137
|
+
const row = getQuarantineRow(db, tenantId, id);
|
|
2138
|
+
if (!row)
|
|
2139
|
+
throw new Error(`not quarantined: ${id}`);
|
|
2140
|
+
if (row.status !== 'pending')
|
|
2141
|
+
throw new Error(`${id} is already ${row.status}`);
|
|
2142
|
+
return row;
|
|
2143
|
+
}
|
|
2144
|
+
/** Release a quarantined memory to its original scope. Admin only; the scope guard refuses a row moved since (mirrors restoreDormant). */
|
|
2145
|
+
export function quarantineApprove(ctx, id) {
|
|
2146
|
+
if (ctx.actor.role !== 'admin') {
|
|
2147
|
+
throw new ForbiddenError('Only an admin key can approve a quarantined memory');
|
|
2148
|
+
}
|
|
2149
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2150
|
+
try {
|
|
2151
|
+
db.exec('BEGIN IMMEDIATE');
|
|
2152
|
+
try {
|
|
2153
|
+
const row = loadPendingQuarantineRow(db, ctx.tenantId, id);
|
|
2154
|
+
const quarantineScope = quarantineScopeFor(row.originalScope);
|
|
2155
|
+
const updated = db
|
|
2156
|
+
.prepare(`UPDATE memories SET scope = ? WHERE id = ? AND tenant_id = ? AND scope = ?`)
|
|
2157
|
+
.run(row.originalScope, id, ctx.tenantId, quarantineScope);
|
|
2158
|
+
if (Number(updated.changes ?? 0) !== 1) {
|
|
2159
|
+
throw new Error(`memory ${id} scope changed since quarantine; refusing to approve`);
|
|
2160
|
+
}
|
|
2161
|
+
approveQuarantineRow(db, ctx.tenantId, id, ctx.actor.subject);
|
|
2162
|
+
appendAuditEvent(db, {
|
|
2163
|
+
tenantId: ctx.tenantId,
|
|
2164
|
+
actor: ctx.actor.subject,
|
|
2165
|
+
op: 'quarantine_approve',
|
|
2166
|
+
targetId: id,
|
|
2167
|
+
metadata: { originalScope: row.originalScope },
|
|
2168
|
+
});
|
|
2169
|
+
db.exec('COMMIT');
|
|
2170
|
+
}
|
|
2171
|
+
catch (err) {
|
|
2172
|
+
try {
|
|
2173
|
+
db.exec('ROLLBACK');
|
|
2174
|
+
}
|
|
2175
|
+
catch { /* already rolled back */ }
|
|
2176
|
+
throw err;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
finally {
|
|
2180
|
+
closeHippoDb(db);
|
|
2181
|
+
}
|
|
2182
|
+
// Post-commit, best-effort: a failed rewrite leaves the mirror showing the quarantine scope (fail-closed).
|
|
2183
|
+
try {
|
|
2184
|
+
const restored = readEntry(ctx.hippoRoot, id, ctx.tenantId);
|
|
2185
|
+
if (restored)
|
|
2186
|
+
writeEntryMirrors(ctx.hippoRoot, restored);
|
|
2187
|
+
}
|
|
2188
|
+
catch (err) {
|
|
2189
|
+
console.error(`quarantine: mirror rewrite failed for ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
/** Keep a quarantined memory hidden for good. Admin only; the raw row is untouched (append-only). */
|
|
2193
|
+
export function quarantineReject(ctx, id) {
|
|
2194
|
+
if (ctx.actor.role !== 'admin') {
|
|
2195
|
+
throw new ForbiddenError('Only an admin key can reject a quarantined memory');
|
|
2196
|
+
}
|
|
2197
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
2198
|
+
try {
|
|
2199
|
+
db.exec('BEGIN IMMEDIATE');
|
|
2200
|
+
try {
|
|
2201
|
+
loadPendingQuarantineRow(db, ctx.tenantId, id);
|
|
2202
|
+
rejectQuarantineRow(db, ctx.tenantId, id, ctx.actor.subject);
|
|
2203
|
+
appendAuditEvent(db, {
|
|
2204
|
+
tenantId: ctx.tenantId,
|
|
2205
|
+
actor: ctx.actor.subject,
|
|
2206
|
+
op: 'quarantine_reject',
|
|
2207
|
+
targetId: id,
|
|
2208
|
+
metadata: {},
|
|
2209
|
+
});
|
|
2210
|
+
db.exec('COMMIT');
|
|
2211
|
+
}
|
|
2212
|
+
catch (err) {
|
|
2213
|
+
try {
|
|
2214
|
+
db.exec('ROLLBACK');
|
|
2215
|
+
}
|
|
2216
|
+
catch { /* already rolled back */ }
|
|
2217
|
+
throw err;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
finally {
|
|
2221
|
+
closeHippoDb(db);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2048
2224
|
const DEFAULT_SLEEP_PHASES = {
|
|
2049
2225
|
consolidate,
|
|
2050
2226
|
deduplicateStore,
|
package/dist/audit.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface AuditResult {
|
|
|
16
16
|
export declare function auditMemory(entry: MemoryEntry): AuditIssue | null;
|
|
17
17
|
export declare function auditMemories(entries: MemoryEntry[]): AuditResult;
|
|
18
18
|
export declare function isContentWorthStoring(content: string): boolean;
|
|
19
|
-
export type AuditOp = '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' | 'half_life_migrate' | 'dormant_restore' | 'conflict_resolve';
|
|
19
|
+
export type AuditOp = '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' | 'half_life_migrate' | 'dormant_restore' | 'conflict_resolve' | 'auth_grant' | 'auth_ungrant' | 'quarantine' | 'quarantine_approve' | 'quarantine_reject';
|
|
20
20
|
export interface AppendAuditOpts {
|
|
21
21
|
tenantId: string;
|
|
22
22
|
actor: string;
|
package/dist/auth.d.ts
CHANGED
|
@@ -16,9 +16,17 @@ export interface ValidateResult {
|
|
|
16
16
|
keyId?: string;
|
|
17
17
|
/** v1.12.0 A5 v2 sub-1: 'admin' | 'member'. Present only when valid=true. */
|
|
18
18
|
role?: 'admin' | 'member';
|
|
19
|
+
/** EI2: scope grants for this key. Present only when valid=true. */
|
|
20
|
+
scopes?: string[];
|
|
19
21
|
}
|
|
20
22
|
export declare function validateApiKey(db: DatabaseSyncLike, plaintext: string): ValidateResult;
|
|
21
23
|
export declare function revokeApiKey(db: DatabaseSyncLike, keyId: string): void;
|
|
24
|
+
/** EI2: grant `keyId` read access to one restricted `scope`. Idempotent. */
|
|
25
|
+
export declare function grantScope(db: DatabaseSyncLike, keyId: string, scope: string): void;
|
|
26
|
+
/** EI2: revoke `keyId`'s grant on `scope`. Not an error when no such grant exists. */
|
|
27
|
+
export declare function ungrantScope(db: DatabaseSyncLike, keyId: string, scope: string): void;
|
|
28
|
+
/** EI2: every restricted scope `keyId` may read. */
|
|
29
|
+
export declare function listScopeGrants(db: DatabaseSyncLike, keyId: string): string[];
|
|
22
30
|
export interface ApiKeyListItem {
|
|
23
31
|
keyId: string;
|
|
24
32
|
tenantId: string;
|
|
@@ -31,6 +39,8 @@ export interface ApiKeyListItem {
|
|
|
31
39
|
* Fail-safe-to-member cast: any non-'admin' value reads as 'member'.
|
|
32
40
|
*/
|
|
33
41
|
role: 'admin' | 'member';
|
|
42
|
+
/** EI2: restricted scopes this key may read. */
|
|
43
|
+
scopes: string[];
|
|
34
44
|
}
|
|
35
45
|
export declare function listApiKeys(db: DatabaseSyncLike, opts: {
|
|
36
46
|
active: boolean;
|
package/dist/auth.js
CHANGED
|
@@ -74,12 +74,30 @@ export function validateApiKey(db, plaintext) {
|
|
|
74
74
|
// 'superuser', or a NULL slipped past the NOT NULL constraint) downgrades to
|
|
75
75
|
// 'member'. The migration constrains to 'admin' DEFAULT, but defense-in-depth.
|
|
76
76
|
const role = row.role === 'admin' ? 'admin' : 'member';
|
|
77
|
-
|
|
77
|
+
const scopes = listScopeGrants(db, keyId);
|
|
78
|
+
return { valid: true, tenantId: row.tenant_id, keyId, role, scopes };
|
|
78
79
|
}
|
|
79
80
|
export function revokeApiKey(db, keyId) {
|
|
80
81
|
db.prepare(`UPDATE api_keys SET revoked_at = ? WHERE key_id = ? AND revoked_at IS NULL`)
|
|
81
82
|
.run(new Date().toISOString(), keyId);
|
|
82
83
|
}
|
|
84
|
+
/** EI2: grant `keyId` read access to one restricted `scope`. Idempotent. */
|
|
85
|
+
export function grantScope(db, keyId, scope) {
|
|
86
|
+
db.prepare(`INSERT INTO api_key_scope_grants (key_id, scope, granted_at) VALUES (?, ?, ?)
|
|
87
|
+
ON CONFLICT(key_id, scope) DO NOTHING`).run(keyId, scope, new Date().toISOString());
|
|
88
|
+
}
|
|
89
|
+
/** EI2: revoke `keyId`'s grant on `scope`. Not an error when no such grant exists. */
|
|
90
|
+
export function ungrantScope(db, keyId, scope) {
|
|
91
|
+
db.prepare(`DELETE FROM api_key_scope_grants WHERE key_id = ? AND scope = ?`).run(keyId, scope);
|
|
92
|
+
}
|
|
93
|
+
/** EI2: every restricted scope `keyId` may read. */
|
|
94
|
+
export function listScopeGrants(db, keyId) {
|
|
95
|
+
// SAFETY: rows' shape matches the single `scope` column named in the SELECT above.
|
|
96
|
+
const rows = db
|
|
97
|
+
.prepare(`SELECT scope FROM api_key_scope_grants WHERE key_id = ? ORDER BY scope`)
|
|
98
|
+
.all(keyId);
|
|
99
|
+
return rows.map((r) => r.scope);
|
|
100
|
+
}
|
|
83
101
|
export function listApiKeys(db, opts) {
|
|
84
102
|
const sql = opts.active
|
|
85
103
|
? `SELECT key_id, tenant_id, label, created_at, revoked_at, role FROM api_keys WHERE revoked_at IS NULL ORDER BY id DESC`
|
|
@@ -92,6 +110,7 @@ export function listApiKeys(db, opts) {
|
|
|
92
110
|
keyId: r.key_id, tenantId: r.tenant_id, label: r.label,
|
|
93
111
|
createdAt: r.created_at, revokedAt: r.revoked_at,
|
|
94
112
|
role: r.role === 'admin' ? 'admin' : 'member',
|
|
113
|
+
scopes: listScopeGrants(db, r.key_id),
|
|
95
114
|
}));
|
|
96
115
|
}
|
|
97
116
|
//# sourceMappingURL=auth.js.map
|
package/dist/cli.js
CHANGED
|
@@ -852,6 +852,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
852
852
|
source: old.source,
|
|
853
853
|
confidence: 'verified',
|
|
854
854
|
tenantId: old.tenantId,
|
|
855
|
+
scope: old.scope,
|
|
855
856
|
});
|
|
856
857
|
// AT1: write the SUCCESSOR first. The rejection guard fires on the new
|
|
857
858
|
// content — if it refuses, nothing has been mutated yet (the old ordering
|
|
@@ -3896,6 +3897,56 @@ function cmdDormant(hippoRoot, args, flags) {
|
|
|
3896
3897
|
}
|
|
3897
3898
|
console.log('Bring one back: hippo dormant restore <id> Delete for good: hippo dormant forget <id>');
|
|
3898
3899
|
}
|
|
3900
|
+
/** `hippo quarantine [list] [--all] [--json] [--global]`, `quarantine approve <id>`, `quarantine reject <id>` (CD5 poisoning defence). */
|
|
3901
|
+
function cmdQuarantine(hippoRoot, args, flags) {
|
|
3902
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3903
|
+
const ctx = {
|
|
3904
|
+
hippoRoot: root,
|
|
3905
|
+
tenantId: resolveTenantId({}),
|
|
3906
|
+
actor: api.adminActor('cli'),
|
|
3907
|
+
};
|
|
3908
|
+
const sub = args[0];
|
|
3909
|
+
if (sub === 'approve' || sub === 'reject') {
|
|
3910
|
+
const id = (args[1] ?? '').trim();
|
|
3911
|
+
if (!id) {
|
|
3912
|
+
console.error(`Usage: hippo quarantine ${sub} <id>`);
|
|
3913
|
+
process.exit(1);
|
|
3914
|
+
}
|
|
3915
|
+
try {
|
|
3916
|
+
if (sub === 'approve') {
|
|
3917
|
+
api.quarantineApprove(ctx, id);
|
|
3918
|
+
console.log(`Approved ${id}: restored to its original scope.`);
|
|
3919
|
+
}
|
|
3920
|
+
else {
|
|
3921
|
+
api.quarantineReject(ctx, id);
|
|
3922
|
+
console.log(`Rejected ${id}: stays quarantined.`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
catch (err) {
|
|
3926
|
+
console.error(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3927
|
+
process.exit(1);
|
|
3928
|
+
}
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
const status = flags['all'] ? 'all' : 'pending';
|
|
3932
|
+
const rows = api.quarantineList(ctx, { status });
|
|
3933
|
+
if (flags['json']) {
|
|
3934
|
+
console.log(JSON.stringify({ quarantine: rows }, null, 2));
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
if (rows.length === 0) {
|
|
3938
|
+
console.log(status === 'all' ? 'No quarantined memories.' : 'No pending quarantined memories.');
|
|
3939
|
+
return;
|
|
3940
|
+
}
|
|
3941
|
+
console.log(`${rows.length} quarantined memor${rows.length === 1 ? 'y' : 'ies'} (newest first):\n`);
|
|
3942
|
+
for (const row of rows) {
|
|
3943
|
+
console.log(`--- ${row.id} [${row.status}]`);
|
|
3944
|
+
console.log(` ${row.contentPreview}`);
|
|
3945
|
+
console.log(` ${row.reason}, original scope ${row.originalScope ?? '(none)'}, quarantined ${row.quarantinedAt.slice(0, 10)}`);
|
|
3946
|
+
console.log('');
|
|
3947
|
+
}
|
|
3948
|
+
console.log('Approve: hippo quarantine approve <id> Reject: hippo quarantine reject <id>');
|
|
3949
|
+
}
|
|
3899
3950
|
/**
|
|
3900
3951
|
* `hippo tokens [--days <n>] [--json] [--global]`: the token ledger
|
|
3901
3952
|
* (ROADMAP TE0). Tokens of memory text handed to agents per surface, blocks
|
|
@@ -7885,6 +7936,41 @@ function cmdAuthRevoke(hippoRoot, keyId, flags) {
|
|
|
7885
7936
|
}
|
|
7886
7937
|
console.log(`Revoked ${keyId} at ${revokedAt}`);
|
|
7887
7938
|
}
|
|
7939
|
+
/** EI2: `hippo auth grant|ungrant <key_id> <scope>`, routed through api so the tenant, restricted-scope and audit checks live in one place. */
|
|
7940
|
+
function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
|
|
7941
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
7942
|
+
// The local CLI owns every tenant (as auth revoke does), so the grant runs in the key's own tenant.
|
|
7943
|
+
const db = openHippoDb(root);
|
|
7944
|
+
let keyTenant;
|
|
7945
|
+
try {
|
|
7946
|
+
// SAFETY: row's shape matches the single tenant_id column in the SELECT.
|
|
7947
|
+
const row = db.prepare(`SELECT tenant_id FROM api_keys WHERE key_id = ?`).get(keyId);
|
|
7948
|
+
keyTenant = row?.tenant_id;
|
|
7949
|
+
}
|
|
7950
|
+
finally {
|
|
7951
|
+
closeHippoDb(db);
|
|
7952
|
+
}
|
|
7953
|
+
if (keyTenant === undefined) {
|
|
7954
|
+
console.error(`Unknown key_id: ${keyId}`);
|
|
7955
|
+
process.exit(1);
|
|
7956
|
+
}
|
|
7957
|
+
const ctx = { hippoRoot: root, tenantId: keyTenant, actor: api.adminActor('cli') };
|
|
7958
|
+
try {
|
|
7959
|
+
if (grant)
|
|
7960
|
+
api.authGrant(ctx, keyId, scope);
|
|
7961
|
+
else
|
|
7962
|
+
api.authUngrant(ctx, keyId, scope);
|
|
7963
|
+
}
|
|
7964
|
+
catch (err) {
|
|
7965
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
7966
|
+
process.exit(1);
|
|
7967
|
+
}
|
|
7968
|
+
if (flags['json']) {
|
|
7969
|
+
console.log(JSON.stringify({ keyId, scope, granted: grant }));
|
|
7970
|
+
return;
|
|
7971
|
+
}
|
|
7972
|
+
console.log(grant ? `Granted ${keyId} read access to ${scope}` : `Removed ${keyId}'s grant on ${scope}`);
|
|
7973
|
+
}
|
|
7888
7974
|
// ---------------------------------------------------------------------------
|
|
7889
7975
|
// Audit log subcommands (A5 stub auth — `hippo audit list`)
|
|
7890
7976
|
// ---------------------------------------------------------------------------
|
|
@@ -7941,6 +8027,11 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
7941
8027
|
'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
|
|
7942
8028
|
'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
|
|
7943
8029
|
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8030
|
+
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8031
|
+
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8032
|
+
'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
8033
|
+
'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
|
|
8034
|
+
'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
|
|
7944
8035
|
]);
|
|
7945
8036
|
function formatAuditRow(ev) {
|
|
7946
8037
|
const target = ev.targetId ?? '-';
|
|
@@ -8264,7 +8355,7 @@ function cmdGoal(hippoRoot, args, flags) {
|
|
|
8264
8355
|
function cmdAuth(hippoRoot, args, flags) {
|
|
8265
8356
|
const sub = args[0];
|
|
8266
8357
|
if (!sub) {
|
|
8267
|
-
console.error('Usage: hippo auth <create|list|revoke> [options]');
|
|
8358
|
+
console.error('Usage: hippo auth <create|list|revoke|grant|ungrant> [options]');
|
|
8268
8359
|
process.exit(1);
|
|
8269
8360
|
}
|
|
8270
8361
|
const subArgs = args.slice(1);
|
|
@@ -8284,8 +8375,18 @@ function cmdAuth(hippoRoot, args, flags) {
|
|
|
8284
8375
|
cmdAuthRevoke(hippoRoot, keyId, flags);
|
|
8285
8376
|
return;
|
|
8286
8377
|
}
|
|
8378
|
+
case 'grant':
|
|
8379
|
+
case 'ungrant': {
|
|
8380
|
+
const [keyId, scope] = subArgs;
|
|
8381
|
+
if (!keyId || !scope) {
|
|
8382
|
+
console.error(`Usage: hippo auth ${sub} <key_id> <scope>`);
|
|
8383
|
+
process.exit(1);
|
|
8384
|
+
}
|
|
8385
|
+
cmdAuthScopeGrant(hippoRoot, keyId, scope, sub === 'grant', flags);
|
|
8386
|
+
return;
|
|
8387
|
+
}
|
|
8287
8388
|
default:
|
|
8288
|
-
console.error(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke.`);
|
|
8389
|
+
console.error(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke | grant | ungrant.`);
|
|
8289
8390
|
process.exit(1);
|
|
8290
8391
|
}
|
|
8291
8392
|
}
|
|
@@ -8673,6 +8774,12 @@ Commands:
|
|
|
8673
8774
|
--global Operate on the global store
|
|
8674
8775
|
dormant restore <id> Bring a dormant memory back to active memory
|
|
8675
8776
|
dormant forget <id> Delete a dormant memory permanently
|
|
8777
|
+
quarantine [list] List memories a connector flagged as an instruction attempt, pending review
|
|
8778
|
+
--all Include approved and rejected rows too (default: pending only)
|
|
8779
|
+
--json Output as JSON
|
|
8780
|
+
--global Operate on the global store
|
|
8781
|
+
quarantine approve <id> Restore a quarantined memory to its original scope
|
|
8782
|
+
quarantine reject <id> Keep a quarantined memory hidden for good
|
|
8676
8783
|
capture-error Store a failed tool call as an error memory (reads the Claude Code
|
|
8677
8784
|
PostToolUseFailure hook payload on stdin; skips routine failures)
|
|
8678
8785
|
doctor Check the install: Node, store, schema, sleep, agent hooks
|
|
@@ -8966,6 +9073,12 @@ Commands:
|
|
|
8966
9073
|
auth revoke <key_id> Revoke an API key (subsequent validate fails)
|
|
8967
9074
|
--json Output as JSON
|
|
8968
9075
|
--global Operate on the global store
|
|
9076
|
+
auth grant <key_id> <scope> Let a member key read one restricted scope
|
|
9077
|
+
--json Output as JSON
|
|
9078
|
+
--global Operate on the global store
|
|
9079
|
+
auth ungrant <key_id> <scope> Remove a scope grant
|
|
9080
|
+
--json Output as JSON
|
|
9081
|
+
--global Operate on the global store
|
|
8969
9082
|
audit <sub> Query the append-only audit log (A5 stub auth)
|
|
8970
9083
|
audit list List audit events for the active tenant
|
|
8971
9084
|
--op <op> Filter by op (remember | recall | promote |
|
|
@@ -9424,6 +9537,9 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9424
9537
|
case 'dormant':
|
|
9425
9538
|
cmdDormant(hippoRoot, args, flags);
|
|
9426
9539
|
break;
|
|
9540
|
+
case 'quarantine':
|
|
9541
|
+
cmdQuarantine(hippoRoot, args, flags);
|
|
9542
|
+
break;
|
|
9427
9543
|
case 'tokens':
|
|
9428
9544
|
cmdTokens(hippoRoot, flags);
|
|
9429
9545
|
break;
|
|
@@ -108,6 +108,7 @@ export function ingestEvent(ctx, input) {
|
|
|
108
108
|
// v1.12.0: drop the legacy `|| 'connector:github'` fallback (see slack/ingest.ts:73 for rationale).
|
|
109
109
|
const result = remember(ctx, {
|
|
110
110
|
...opts,
|
|
111
|
+
untrusted: true,
|
|
111
112
|
afterWrite: (innerDb, memoryId) => {
|
|
112
113
|
if (input.__testInjectBeforeLog) {
|
|
113
114
|
input.__testInjectBeforeLog(innerDb, idempotencyKey);
|
|
@@ -61,6 +61,7 @@ export function ingestMessage(ctx, input) {
|
|
|
61
61
|
// ever missing); explicit reliance on the caller is safer.
|
|
62
62
|
const result = remember(ctx, {
|
|
63
63
|
...opts,
|
|
64
|
+
untrusted: true,
|
|
64
65
|
afterWrite: (innerDb, memoryId) => {
|
|
65
66
|
const inserted = innerDb
|
|
66
67
|
.prepare(`INSERT OR IGNORE INTO slack_event_log (event_id, ingested_at, memory_id) VALUES (?, ?, ?)`)
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ConsolidationResult {
|
|
|
19
19
|
semanticCreated: number;
|
|
20
20
|
replayed: number;
|
|
21
21
|
promotedTraces: number;
|
|
22
|
+
/** T7: sessions skipped because their events span two derivation scopes. */
|
|
23
|
+
tracesSkippedMixedScope: number;
|
|
22
24
|
extractionCandidates: number;
|
|
23
25
|
extracted: number;
|
|
24
26
|
dagCandidateClusters: number;
|