hippo-memory 1.47.0 → 1.49.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 +2 -1
- package/dist/api.d.ts +10 -0
- package/dist/api.js +55 -10
- package/dist/audit.d.ts +1 -1
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +20 -1
- package/dist/cli.js +96 -4
- package/dist/consolidate.d.ts +2 -0
- package/dist/consolidate.js +17 -3
- package/dist/dag.js +15 -6
- package/dist/db.d.ts +7 -0
- package/dist/db.js +54 -2
- package/dist/dedupe.js +5 -2
- package/dist/doctor.js +52 -41
- package/dist/extract.js +1 -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/recall-scope.d.ts +22 -10
- package/dist/recall-scope.js +41 -26
- package/dist/reject-flow.d.ts +2 -2
- package/dist/reject-flow.js +4 -7
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +18 -6
- package/dist/server.js +10 -9
- package/dist/store.d.ts +9 -26
- package/dist/store.js +18 -36
- package/dist/support-bundle.d.ts +12 -0
- package/dist/support-bundle.js +292 -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 +2 -1
package/README.md
CHANGED
|
@@ -256,7 +256,7 @@ hippo recall "data pipeline" --why --limit 5
|
|
|
256
256
|
|
|
257
257
|
Input enters the buffer. Important things get encoded into episodic memory. During "sleep," repeated episodes compress into semantic patterns. Weak memories decay and disappear.
|
|
258
258
|
|
|
259
|
-
The store is SQLite (`.hippo/hippo.db`). The markdown files
|
|
259
|
+
The store is SQLite (`.hippo/hippo.db`). The markdown files are mirrors written after each change. `index.json` is no longer refreshed by writes, deletes or recalls: it is written only when you call `rebuildIndex()` from the package, so a copy an older version left on disk goes stale. Read the store through the CLI, the MCP server or the HTTP API.
|
|
260
260
|
|
|
261
261
|
```mermaid
|
|
262
262
|
flowchart TD
|
|
@@ -629,6 +629,7 @@ hippo watch "npm run build"
|
|
|
629
629
|
| `hippo dormant restore <id>` | Bring a dormant memory back to active memory |
|
|
630
630
|
| `hippo dormant forget <id>` | Delete a dormant memory permanently |
|
|
631
631
|
| `hippo doctor [--json]` | Check the install: Node, store, schema, sleep, agent hooks; each problem names its fix |
|
|
632
|
+
| `hippo support-bundle [--out <file>] [--include-logs]` | Write a redacted JSON file for a support ticket: versions, doctor checks, config, store counts and log names, never memory text; `--include-logs` adds each log's last 200 lines, which can quote it |
|
|
632
633
|
| `hippo tokens [--days n]` | Estimated tokens of memory text handed to agents, per surface, and what skipping unchanged hook blocks saved |
|
|
633
634
|
| `hippo failures [--days n]` | Failed tool calls the capture-error hook saw, by outcome, and how many errors first happened in another session |
|
|
634
635
|
| `hippo embed` | Embed all memories for semantic search |
|
package/dist/api.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface Actor {
|
|
|
40
40
|
/** 'cli' | 'localhost:cli' | 'api_key:<key_id>' | 'mcp' | 'connector:slack' | 'connector:github' */
|
|
41
41
|
subject: string;
|
|
42
42
|
role: 'admin' | 'member';
|
|
43
|
+
/** EI2: restricted scopes a member key may read (auth.ts grantScope). Unused for admin actors. */
|
|
44
|
+
scopes?: readonly string[];
|
|
43
45
|
}
|
|
44
46
|
export interface Context {
|
|
45
47
|
hippoRoot: string;
|
|
@@ -875,6 +877,14 @@ export interface AuthRevokeResult {
|
|
|
875
877
|
revokedAt: string;
|
|
876
878
|
}
|
|
877
879
|
export declare function authRevoke(ctx: Context, keyId: string): AuthRevokeResult;
|
|
880
|
+
/** Shared result shape for authGrant/authUngrant, named per the file's oxlint anti-slop rule. */
|
|
881
|
+
export interface AuthGrantResult {
|
|
882
|
+
ok: true;
|
|
883
|
+
}
|
|
884
|
+
/** Grant `keyId` read access to one restricted `scope` (ROADMAP Part VIII EI2). Admin only. */
|
|
885
|
+
export declare function authGrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
|
886
|
+
/** Revoke `keyId`'s grant on `scope`. Same authorization and lookup rules as authGrant. */
|
|
887
|
+
export declare function authUngrant(ctx: Context, keyId: string, scope: string): AuthGrantResult;
|
|
878
888
|
export interface AuditListOpts {
|
|
879
889
|
op?: AuditOp;
|
|
880
890
|
/** ISO timestamp lower bound. */
|
package/dist/api.js
CHANGED
|
@@ -21,7 +21,7 @@ import { promoteToGlobal, getGlobalRoot, autoShare, searchBothHybrid } from './s
|
|
|
21
21
|
import { writeRecallTrace, writeRecallTraceAtRoot, recordTraceOutcome } from './recall-trace.js';
|
|
22
22
|
import { evalNow } from './ablation.js';
|
|
23
23
|
import { archiveRawMemory } from './raw-archive.js';
|
|
24
|
-
import { createApiKey, listApiKeys, revokeApiKey, } from './auth.js';
|
|
24
|
+
import { createApiKey, listApiKeys, revokeApiKey, grantScope, ungrantScope, } from './auth.js';
|
|
25
25
|
import { applyGoalStackBoost } from './goals.js';
|
|
26
26
|
import { markRetrieved, estimateTokens, hybridSearch, physicsSearch } from './search.js';
|
|
27
27
|
import { compareEntryIdentity, compareScoredResults } from './compare.js';
|
|
@@ -87,7 +87,7 @@ export class ForbiddenError extends Error {
|
|
|
87
87
|
// back-compat (`api.isPrivateScope`, test imports). NOTE: the import statement
|
|
88
88
|
// is required — a bare `export { x } from` re-export does not bind the local
|
|
89
89
|
// names this module's ~9 call sites use.
|
|
90
|
-
import { isPrivateScope, passesScopeFilterForRecall, assertScopeRequestAllowed } from './recall-scope.js';
|
|
90
|
+
import { isPrivateScope, passesScopeFilterForRecall, assertScopeRequestAllowed, isRestrictedScope } from './recall-scope.js';
|
|
91
91
|
export { isPrivateScope, passesScopeFilterForRecall };
|
|
92
92
|
export { passesCliRecallScopeFilter, ScopeForbiddenError } from './recall-scope.js';
|
|
93
93
|
// v39: classifyOriginProject lives in project-identity.ts (leaf) so
|
|
@@ -188,13 +188,13 @@ export function buildSuppressionSummary(counts) {
|
|
|
188
188
|
*/
|
|
189
189
|
export function recall(ctx, opts) {
|
|
190
190
|
// A member key may not unlock a private or quarantined scope by naming it.
|
|
191
|
-
assertScopeRequestAllowed(ctx.actor
|
|
191
|
+
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
192
192
|
const windowSize = recallWindowSize(opts);
|
|
193
193
|
return recallFrom(ctx, opts, windowSize, loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false));
|
|
194
194
|
}
|
|
195
195
|
/** Mode-aware recall that strengthens each returned row; never writes last_retrieval_ids (v1.11.5 lock). */
|
|
196
196
|
export async function retrieve(ctx, opts) {
|
|
197
|
-
assertScopeRequestAllowed(ctx.actor
|
|
197
|
+
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
198
198
|
const windowSize = recallWindowSize(opts);
|
|
199
199
|
let candidates = loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false);
|
|
200
200
|
if (opts.mode === 'hybrid' || opts.mode === 'physics') {
|
|
@@ -288,7 +288,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
288
288
|
// anchored `<source>:private:*` rule (v1.2.1 generalization) and
|
|
289
289
|
// defense-in-depth: connector authors cannot silently surface private
|
|
290
290
|
// rows to no-scope callers even if the SQL clause regresses.
|
|
291
|
-
entries = current.filter((e) => !
|
|
291
|
+
entries = current.filter((e) => !isRestrictedScope(e.scope ?? null));
|
|
292
292
|
}
|
|
293
293
|
// v1.12.13 / C5 — WYSIATI dropped_pre_rank counter (JS scope filter drops
|
|
294
294
|
// for api.recall; cmdRecall pipeline rolls --outcome/--layer/--as-of/etc.
|
|
@@ -725,7 +725,7 @@ function recallFrom(ctx, opts, windowSize, all) {
|
|
|
725
725
|
* - all rows fail the scope/tenant filter
|
|
726
726
|
*/
|
|
727
727
|
export function assemble(ctx, sessionId, opts = {}) {
|
|
728
|
-
assertScopeRequestAllowed(ctx.actor
|
|
728
|
+
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
729
729
|
const budget = opts.budget ?? 4000;
|
|
730
730
|
const freshTailCount = opts.freshTailCount ?? 10;
|
|
731
731
|
const summarizeOlder = opts.summarizeOlder ?? true;
|
|
@@ -1152,6 +1152,7 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
1152
1152
|
source: old.source,
|
|
1153
1153
|
confidence: 'verified',
|
|
1154
1154
|
tenantId: ctx.tenantId,
|
|
1155
|
+
scope: old.scope,
|
|
1155
1156
|
});
|
|
1156
1157
|
// Race-safe transition: open a fresh db handle, BEGIN IMMEDIATE, run all
|
|
1157
1158
|
// three steps (CAS on old + writeEntryDbOnly(new) + supersede audit row)
|
|
@@ -1216,11 +1217,11 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
1216
1217
|
}
|
|
1217
1218
|
// Mirrors after COMMIT, while the db handle is still open. Same
|
|
1218
1219
|
// invariant as the original writeEntry: a mirror failure leaves disk
|
|
1219
|
-
// MISSING the markdown for the new memory (
|
|
1220
|
-
//
|
|
1220
|
+
// MISSING the markdown for the new memory (rebuildIndex rewrites every
|
|
1221
|
+
// markdown mirror from the DB) but DOES NOT desync the DB or
|
|
1221
1222
|
// roll back the supersede. Logged + swallowed, non-fatal.
|
|
1222
1223
|
try {
|
|
1223
|
-
writeEntryMirrors(ctx.hippoRoot,
|
|
1224
|
+
writeEntryMirrors(ctx.hippoRoot, newEntry);
|
|
1224
1225
|
}
|
|
1225
1226
|
catch (mirrorErr) {
|
|
1226
1227
|
console.error('supersede: mirror write failed (non-fatal, will self-heal):', mirrorErr);
|
|
@@ -1401,6 +1402,50 @@ export function authRevoke(ctx, keyId) {
|
|
|
1401
1402
|
closeHippoDb(db);
|
|
1402
1403
|
}
|
|
1403
1404
|
}
|
|
1405
|
+
/** Grant `keyId` read access to one restricted `scope` (ROADMAP Part VIII EI2). Admin only. */
|
|
1406
|
+
export function authGrant(ctx, keyId, scope) {
|
|
1407
|
+
return changeScopeGrant(ctx, keyId, scope, 'auth_grant');
|
|
1408
|
+
}
|
|
1409
|
+
/** Revoke `keyId`'s grant on `scope`. Same authorization and lookup rules as authGrant. */
|
|
1410
|
+
export function authUngrant(ctx, keyId, scope) {
|
|
1411
|
+
return changeScopeGrant(ctx, keyId, scope, 'auth_ungrant');
|
|
1412
|
+
}
|
|
1413
|
+
function changeScopeGrant(ctx, keyId, scope, op) {
|
|
1414
|
+
if (ctx.actor.role !== 'admin') {
|
|
1415
|
+
throw new ForbiddenError('Only an admin key can change scope grants');
|
|
1416
|
+
}
|
|
1417
|
+
const db = openHippoDb(ctx.hippoRoot);
|
|
1418
|
+
try {
|
|
1419
|
+
// SAFETY: row's shape matches the single tenant_id column in the SELECT.
|
|
1420
|
+
const row = db
|
|
1421
|
+
.prepare(`SELECT tenant_id, revoked_at FROM api_keys WHERE key_id = ?`)
|
|
1422
|
+
.get(keyId);
|
|
1423
|
+
if (!row || row.tenant_id !== ctx.tenantId) {
|
|
1424
|
+
throw new Error(`Unknown key_id: ${keyId}`);
|
|
1425
|
+
}
|
|
1426
|
+
if (op === 'auth_grant' && row.revoked_at) {
|
|
1427
|
+
throw new Error(`${keyId} is revoked; a grant on it would never apply`);
|
|
1428
|
+
}
|
|
1429
|
+
if (!isRestrictedScope(scope)) {
|
|
1430
|
+
throw new Error(`${scope} is not a restricted scope; it is already readable by default`);
|
|
1431
|
+
}
|
|
1432
|
+
if (op === 'auth_grant')
|
|
1433
|
+
grantScope(db, keyId, scope);
|
|
1434
|
+
else
|
|
1435
|
+
ungrantScope(db, keyId, scope);
|
|
1436
|
+
try {
|
|
1437
|
+
appendAuditEvent(db, { tenantId: ctx.tenantId, actor: ctx.actor.subject, op, targetId: keyId, metadata: { scope } });
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
// Audit must not undo a grant change that already committed; surface it instead.
|
|
1441
|
+
console.error(`auth: audit write failed for ${op} ${keyId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1442
|
+
}
|
|
1443
|
+
return { ok: true };
|
|
1444
|
+
}
|
|
1445
|
+
finally {
|
|
1446
|
+
closeHippoDb(db);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1404
1449
|
/**
|
|
1405
1450
|
* Read audit events scoped to `ctx.tenantId`. Read-only — no audit emit (matches
|
|
1406
1451
|
* A5: cmdAuditList does not record a 'recall'-style read event).
|
|
@@ -1998,7 +2043,7 @@ export function restoreDormant(ctx, id) {
|
|
|
1998
2043
|
}
|
|
1999
2044
|
throw err;
|
|
2000
2045
|
}
|
|
2001
|
-
writeEntryMirrors(ctx.hippoRoot,
|
|
2046
|
+
writeEntryMirrors(ctx.hippoRoot, restored);
|
|
2002
2047
|
return restored;
|
|
2003
2048
|
}
|
|
2004
2049
|
finally {
|
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';
|
|
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
|
@@ -60,6 +60,8 @@ import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
|
60
60
|
import { loadConfig } from './config.js';
|
|
61
61
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
62
62
|
import { runDoctor, formatDoctor } from './doctor.js';
|
|
63
|
+
import { buildSupportBundle, TAIL_MAX_LINES } from './support-bundle.js';
|
|
64
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
63
65
|
import { captureToolFailure } from './capture-error.js';
|
|
64
66
|
import { blockHash, hookPayloadSessionId, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
65
67
|
import { FAILURE_LOG_RETENTION_DAYS } from './failure-log.js';
|
|
@@ -264,7 +266,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
264
266
|
'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'continuity',
|
|
265
267
|
'cross-project', 'dry-run', 'equal-sources', 'error', 'evc-adaptive', 'extract',
|
|
266
268
|
'filter-conflicts', 'fix', 'force', 'forget', 'git', 'global', 'good', 'graph-stream',
|
|
267
|
-
'help', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
269
|
+
'help', 'include-logs', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
268
270
|
'no-learn', 'no-mmr', 'no-propagate', 'no-schedule', 'no-share', 'no-summarize-older',
|
|
269
271
|
'observed', 'open', 'physics', 'pin', 'pinned-only', 'reject-loser', 'rerank-utility',
|
|
270
272
|
'reset-physics', 'save-baseline', 'show-cases', 'stats', 'stdin',
|
|
@@ -488,7 +490,7 @@ function cmdInit(hippoRoot, flags) {
|
|
|
488
490
|
initStore(hippoRoot);
|
|
489
491
|
console.log('Initialized Hippo at', hippoRoot);
|
|
490
492
|
console.log(' Directories: buffer/ episodic/ semantic/ conflicts/');
|
|
491
|
-
console.log(' Files: hippo.db
|
|
493
|
+
console.log(' Files: hippo.db stats.json');
|
|
492
494
|
}
|
|
493
495
|
const globalRoot = getGlobalRoot();
|
|
494
496
|
registerWorkspace(globalRoot, path.dirname(hippoRoot));
|
|
@@ -850,6 +852,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
850
852
|
source: old.source,
|
|
851
853
|
confidence: 'verified',
|
|
852
854
|
tenantId: old.tenantId,
|
|
855
|
+
scope: old.scope,
|
|
853
856
|
});
|
|
854
857
|
// AT1: write the SUCCESSOR first. The rejection guard fires on the new
|
|
855
858
|
// content — if it refuses, nothing has been mutated yet (the old ordering
|
|
@@ -7883,6 +7886,41 @@ function cmdAuthRevoke(hippoRoot, keyId, flags) {
|
|
|
7883
7886
|
}
|
|
7884
7887
|
console.log(`Revoked ${keyId} at ${revokedAt}`);
|
|
7885
7888
|
}
|
|
7889
|
+
/** EI2: `hippo auth grant|ungrant <key_id> <scope>`, routed through api so the tenant, restricted-scope and audit checks live in one place. */
|
|
7890
|
+
function cmdAuthScopeGrant(hippoRoot, keyId, scope, grant, flags) {
|
|
7891
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
7892
|
+
// The local CLI owns every tenant (as auth revoke does), so the grant runs in the key's own tenant.
|
|
7893
|
+
const db = openHippoDb(root);
|
|
7894
|
+
let keyTenant;
|
|
7895
|
+
try {
|
|
7896
|
+
// SAFETY: row's shape matches the single tenant_id column in the SELECT.
|
|
7897
|
+
const row = db.prepare(`SELECT tenant_id FROM api_keys WHERE key_id = ?`).get(keyId);
|
|
7898
|
+
keyTenant = row?.tenant_id;
|
|
7899
|
+
}
|
|
7900
|
+
finally {
|
|
7901
|
+
closeHippoDb(db);
|
|
7902
|
+
}
|
|
7903
|
+
if (keyTenant === undefined) {
|
|
7904
|
+
console.error(`Unknown key_id: ${keyId}`);
|
|
7905
|
+
process.exit(1);
|
|
7906
|
+
}
|
|
7907
|
+
const ctx = { hippoRoot: root, tenantId: keyTenant, actor: api.adminActor('cli') };
|
|
7908
|
+
try {
|
|
7909
|
+
if (grant)
|
|
7910
|
+
api.authGrant(ctx, keyId, scope);
|
|
7911
|
+
else
|
|
7912
|
+
api.authUngrant(ctx, keyId, scope);
|
|
7913
|
+
}
|
|
7914
|
+
catch (err) {
|
|
7915
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
7916
|
+
process.exit(1);
|
|
7917
|
+
}
|
|
7918
|
+
if (flags['json']) {
|
|
7919
|
+
console.log(JSON.stringify({ keyId, scope, granted: grant }));
|
|
7920
|
+
return;
|
|
7921
|
+
}
|
|
7922
|
+
console.log(grant ? `Granted ${keyId} read access to ${scope}` : `Removed ${keyId}'s grant on ${scope}`);
|
|
7923
|
+
}
|
|
7886
7924
|
// ---------------------------------------------------------------------------
|
|
7887
7925
|
// Audit log subcommands (A5 stub auth — `hippo audit list`)
|
|
7888
7926
|
// ---------------------------------------------------------------------------
|
|
@@ -7939,6 +7977,8 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
7939
7977
|
'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
|
|
7940
7978
|
'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
|
|
7941
7979
|
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
7980
|
+
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
7981
|
+
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
7942
7982
|
]);
|
|
7943
7983
|
function formatAuditRow(ev) {
|
|
7944
7984
|
const target = ev.targetId ?? '-';
|
|
@@ -8262,7 +8302,7 @@ function cmdGoal(hippoRoot, args, flags) {
|
|
|
8262
8302
|
function cmdAuth(hippoRoot, args, flags) {
|
|
8263
8303
|
const sub = args[0];
|
|
8264
8304
|
if (!sub) {
|
|
8265
|
-
console.error('Usage: hippo auth <create|list|revoke> [options]');
|
|
8305
|
+
console.error('Usage: hippo auth <create|list|revoke|grant|ungrant> [options]');
|
|
8266
8306
|
process.exit(1);
|
|
8267
8307
|
}
|
|
8268
8308
|
const subArgs = args.slice(1);
|
|
@@ -8282,8 +8322,18 @@ function cmdAuth(hippoRoot, args, flags) {
|
|
|
8282
8322
|
cmdAuthRevoke(hippoRoot, keyId, flags);
|
|
8283
8323
|
return;
|
|
8284
8324
|
}
|
|
8325
|
+
case 'grant':
|
|
8326
|
+
case 'ungrant': {
|
|
8327
|
+
const [keyId, scope] = subArgs;
|
|
8328
|
+
if (!keyId || !scope) {
|
|
8329
|
+
console.error(`Usage: hippo auth ${sub} <key_id> <scope>`);
|
|
8330
|
+
process.exit(1);
|
|
8331
|
+
}
|
|
8332
|
+
cmdAuthScopeGrant(hippoRoot, keyId, scope, sub === 'grant', flags);
|
|
8333
|
+
return;
|
|
8334
|
+
}
|
|
8285
8335
|
default:
|
|
8286
|
-
console.error(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke.`);
|
|
8336
|
+
console.error(`Unknown auth subcommand: ${sub}. Expected: create | list | revoke | grant | ungrant.`);
|
|
8287
8337
|
process.exit(1);
|
|
8288
8338
|
}
|
|
8289
8339
|
}
|
|
@@ -8675,6 +8725,10 @@ Commands:
|
|
|
8675
8725
|
PostToolUseFailure hook payload on stdin; skips routine failures)
|
|
8676
8726
|
doctor Check the install: Node, store, schema, sleep, agent hooks
|
|
8677
8727
|
--json Machine-readable report (exit code 1 on any failure)
|
|
8728
|
+
support-bundle Write a redacted JSON file for a support ticket: versions, doctor,
|
|
8729
|
+
config without secrets, store counts, log names; never memory text
|
|
8730
|
+
--out <file> Where to write it (default: hippo-support-<time>.json here)
|
|
8731
|
+
--include-logs Add the last ${TAIL_MAX_LINES} lines of each hippo log, known secret shapes removed
|
|
8678
8732
|
tokens Tokens of memory text hippo handed agents, per surface
|
|
8679
8733
|
(hook, context, recall, MCP, HTTP), and what skipping
|
|
8680
8734
|
unchanged hook blocks saved
|
|
@@ -8960,6 +9014,12 @@ Commands:
|
|
|
8960
9014
|
auth revoke <key_id> Revoke an API key (subsequent validate fails)
|
|
8961
9015
|
--json Output as JSON
|
|
8962
9016
|
--global Operate on the global store
|
|
9017
|
+
auth grant <key_id> <scope> Let a member key read one restricted scope
|
|
9018
|
+
--json Output as JSON
|
|
9019
|
+
--global Operate on the global store
|
|
9020
|
+
auth ungrant <key_id> <scope> Remove a scope grant
|
|
9021
|
+
--json Output as JSON
|
|
9022
|
+
--global Operate on the global store
|
|
8963
9023
|
audit <sub> Query the append-only audit log (A5 stub auth)
|
|
8964
9024
|
audit list List audit events for the active tenant
|
|
8965
9025
|
--op <op> Filter by op (remember | recall | promote |
|
|
@@ -9433,6 +9493,38 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9433
9493
|
process.exit(1);
|
|
9434
9494
|
break;
|
|
9435
9495
|
}
|
|
9496
|
+
case 'support-bundle': {
|
|
9497
|
+
const outFlag = cardStringFlag(flags, 'out');
|
|
9498
|
+
if (outFlag === '') {
|
|
9499
|
+
console.error('--out requires a file path.');
|
|
9500
|
+
process.exit(1);
|
|
9501
|
+
}
|
|
9502
|
+
const includeLogs = flags['include-logs'] === true;
|
|
9503
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
9504
|
+
const now = new Date();
|
|
9505
|
+
const bundle = buildSupportBundle({ cwd: process.cwd(), home, version: PACKAGE_VERSION, includeLogs, now });
|
|
9506
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-');
|
|
9507
|
+
const file = outFlag ?? path.join(process.cwd(), `hippo-support-${stamp}.json`);
|
|
9508
|
+
const json = JSON.stringify(bundle, null, 2);
|
|
9509
|
+
try {
|
|
9510
|
+
fs.writeFileSync(file, `${json}\n`, { flag: 'wx', mode: 0o600 });
|
|
9511
|
+
}
|
|
9512
|
+
catch (err) {
|
|
9513
|
+
if (err instanceof Error && 'code' in err && err.code === 'EEXIST') {
|
|
9514
|
+
console.error(`${file} already exists; pass --out to choose another file. Nothing was written.`);
|
|
9515
|
+
}
|
|
9516
|
+
else {
|
|
9517
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
9518
|
+
}
|
|
9519
|
+
process.exit(1);
|
|
9520
|
+
}
|
|
9521
|
+
const kb = Math.round(Buffer.byteLength(json) / 1024);
|
|
9522
|
+
console.log(`Wrote ${file} (${kb} KB).`);
|
|
9523
|
+
console.log(includeLogs
|
|
9524
|
+
? `It holds versions, doctor checks, config with secrets removed, store counts, and the last ${TAIL_MAX_LINES} lines of each hippo log with known secret shapes removed. Those log lines can quote memory text. Read it before you attach it to a ticket.`
|
|
9525
|
+
: 'It holds versions, doctor checks, config with secrets removed, store counts and log file names. It never holds memory text. Read it before you attach it to a ticket.');
|
|
9526
|
+
break;
|
|
9527
|
+
}
|
|
9436
9528
|
case 'snapshot':
|
|
9437
9529
|
cmdSnapshot(hippoRoot, args, flags);
|
|
9438
9530
|
break;
|
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;
|
package/dist/consolidate.js
CHANGED
|
@@ -25,6 +25,7 @@ import { rescueSet, rankNonPinnedByTenant, validateWeights } from './memory-valu
|
|
|
25
25
|
import { MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256 } from './memory-value-weights.js';
|
|
26
26
|
import { appendAuditEvent } from './audit.js';
|
|
27
27
|
import { migrateDefaultHalfLife } from './half-life-migration.js';
|
|
28
|
+
import { derivationScope, commonDerivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
28
29
|
const DECAY_THRESHOLD = 0.05;
|
|
29
30
|
const MERGE_OVERLAP_THRESHOLD = 0.35; // Jaccard similarity for "related"
|
|
30
31
|
const MERGE_MIN_CLUSTER = 2; // minimum cluster size to merge
|
|
@@ -108,6 +109,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
108
109
|
semanticCreated: 0,
|
|
109
110
|
replayed: 0,
|
|
110
111
|
promotedTraces: 0,
|
|
112
|
+
tracesSkippedMixedScope: 0,
|
|
111
113
|
extractionCandidates: 0,
|
|
112
114
|
extracted: 0,
|
|
113
115
|
dagCandidateClusters: 0,
|
|
@@ -368,6 +370,13 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
368
370
|
session_id: session.session_id,
|
|
369
371
|
limit: 1000,
|
|
370
372
|
});
|
|
373
|
+
// T7: a mixed-scope session would otherwise leak into one trace.
|
|
374
|
+
const sessionScope = commonDerivationScope(events.map((e) => e.scope));
|
|
375
|
+
if (!sessionScope.ok) {
|
|
376
|
+
result.tracesSkippedMixedScope++;
|
|
377
|
+
result.details.push(` ⏭ skipped session ${session.session_id}: events span mixed scopes`);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
371
380
|
const completeEvent = events.find((e) => e.event_type === 'session_complete');
|
|
372
381
|
if (!completeEvent)
|
|
373
382
|
continue; // defence-in-depth; findPromotableSessions filters already.
|
|
@@ -391,6 +400,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
391
400
|
source_session_id: session.session_id,
|
|
392
401
|
tags: ['auto-promoted'],
|
|
393
402
|
source: 'auto-promote',
|
|
403
|
+
scope: sessionScope.scope,
|
|
394
404
|
// T1 fix (2026-08-15 hardening pass): stamp the trace into the SAME
|
|
395
405
|
// tenant the traceExistsForSession idempotency check (above) runs
|
|
396
406
|
// under. Before
|
|
@@ -671,11 +681,12 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
671
681
|
// before this fix — byte-identical behavior there.
|
|
672
682
|
const mergeCandidatesByTenant = new Map();
|
|
673
683
|
for (const entry of mergeCandidates) {
|
|
674
|
-
const
|
|
684
|
+
const key = derivationPartitionKey(entry.tenantId, entry.scope);
|
|
685
|
+
const bucket = mergeCandidatesByTenant.get(key);
|
|
675
686
|
if (bucket)
|
|
676
687
|
bucket.push(entry);
|
|
677
688
|
else
|
|
678
|
-
mergeCandidatesByTenant.set(
|
|
689
|
+
mergeCandidatesByTenant.set(key, [entry]);
|
|
679
690
|
}
|
|
680
691
|
// AT1 consolidation-loop fix (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
|
|
681
692
|
// reuses the single consolidateDb handle opened once above (before the
|
|
@@ -686,7 +697,9 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
686
697
|
// declared up at the try's opening above 1.4, not here — it has to
|
|
687
698
|
// survive the try/finally that now wraps this whole section; see the
|
|
688
699
|
// handle-leak restructure comment there.)
|
|
689
|
-
for (const [
|
|
700
|
+
for (const [, tenantCandidates] of mergeCandidatesByTenant) {
|
|
701
|
+
const mergeTenant = tenantCandidates[0].tenantId;
|
|
702
|
+
const mergeScope = derivationScope(tenantCandidates[0].scope);
|
|
690
703
|
for (let i = 0; i < tenantCandidates.length; i++) {
|
|
691
704
|
if (used.has(tenantCandidates[i].id))
|
|
692
705
|
continue;
|
|
@@ -722,6 +735,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
722
735
|
source: 'consolidation',
|
|
723
736
|
confidence: 'inferred',
|
|
724
737
|
tenantId: mergeTenant,
|
|
738
|
+
scope: mergeScope,
|
|
725
739
|
});
|
|
726
740
|
}
|
|
727
741
|
// mergeContents is DETERMINISTIC CONCATENATION (not an LLM paraphrase)
|
package/dist/dag.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createMemory, Layer } from './memory.js';
|
|
|
2
2
|
import { writeEntry, loadAllDirtySummaries, loadChildrenOfSummary, applyRebuildResult, clearSummaryDirtyAfterBuild, } from './store.js';
|
|
3
3
|
import { RejectedValueError } from './rejection.js';
|
|
4
4
|
import { redactSecrets } from './secret-detect.js';
|
|
5
|
+
import { derivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
5
6
|
export function clusterFacts(facts) {
|
|
6
7
|
if (facts.length === 0)
|
|
7
8
|
return [];
|
|
@@ -93,13 +94,16 @@ export async function buildDag(hippoRoot, facts, opts) {
|
|
|
93
94
|
// behavior there.
|
|
94
95
|
const unparentedByTenant = new Map();
|
|
95
96
|
for (const fact of unparented) {
|
|
96
|
-
const
|
|
97
|
+
const key = derivationPartitionKey(fact.tenantId, fact.scope);
|
|
98
|
+
const bucket = unparentedByTenant.get(key);
|
|
97
99
|
if (bucket)
|
|
98
100
|
bucket.push(fact);
|
|
99
101
|
else
|
|
100
|
-
unparentedByTenant.set(
|
|
102
|
+
unparentedByTenant.set(key, [fact]);
|
|
101
103
|
}
|
|
102
|
-
for (const [
|
|
104
|
+
for (const [, tenantFacts] of unparentedByTenant) {
|
|
105
|
+
const factTenant = tenantFacts[0].tenantId;
|
|
106
|
+
const factScope = derivationScope(tenantFacts[0].scope);
|
|
103
107
|
const clusters = clusterFacts(tenantFacts);
|
|
104
108
|
const eligibleClusters = clusters.filter((c) => c.members.length >= 3);
|
|
105
109
|
result.candidateClusters += eligibleClusters.length;
|
|
@@ -118,6 +122,7 @@ export async function buildDag(hippoRoot, facts, opts) {
|
|
|
118
122
|
confidence: 'inferred',
|
|
119
123
|
dag_level: 2,
|
|
120
124
|
tenantId: factTenant,
|
|
125
|
+
scope: factScope,
|
|
121
126
|
});
|
|
122
127
|
// Schema v25: cache descendant_count + earliest/latest_at on the summary
|
|
123
128
|
// row so DAG-aware recall (docs/plans/2026-05-05-dag-recall.md Task 2)
|
|
@@ -299,11 +304,14 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
299
304
|
const byTenant = new Map();
|
|
300
305
|
for (const l2 of unparented) {
|
|
301
306
|
const tid = l2.tenantId ?? 'default';
|
|
302
|
-
const
|
|
307
|
+
const key = derivationPartitionKey(tid, l2.scope);
|
|
308
|
+
const list = byTenant.get(key) ?? [];
|
|
303
309
|
list.push(l2);
|
|
304
|
-
byTenant.set(
|
|
310
|
+
byTenant.set(key, list);
|
|
305
311
|
}
|
|
306
|
-
for (const [
|
|
312
|
+
for (const [, tenantL2s] of byTenant) {
|
|
313
|
+
const tenantId = tenantL2s[0].tenantId ?? 'default';
|
|
314
|
+
const scope = derivationScope(tenantL2s[0].scope);
|
|
307
315
|
const clusters = clusterFacts(tenantL2s);
|
|
308
316
|
const eligible = clusters.filter((c) => c.members.length >= 2);
|
|
309
317
|
result.candidateClusters += eligible.length;
|
|
@@ -321,6 +329,7 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
321
329
|
confidence: 'inferred',
|
|
322
330
|
dag_level: 3,
|
|
323
331
|
tenantId, // HIGH #1 fold: thread tenant explicitly
|
|
332
|
+
scope,
|
|
324
333
|
});
|
|
325
334
|
profileEntry.descendant_count = cluster.members.length;
|
|
326
335
|
profileEntry.earliest_at = memberCreatedAts[0];
|
package/dist/db.d.ts
CHANGED
|
@@ -13,11 +13,18 @@ export interface DatabaseSyncLike {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function getHippoDbPath(hippoRoot: string): string;
|
|
15
15
|
export declare function getCurrentSchemaVersion(): number;
|
|
16
|
+
/** Thrown by {@link assertBinaryCompatible}; doctor uses it to pick the upgrade fix over a generic permissions fix. */
|
|
17
|
+
export declare class IncompatibleBinaryError extends Error {
|
|
18
|
+
}
|
|
16
19
|
export declare function openHippoDb(hippoRoot: string): DatabaseSyncLike;
|
|
20
|
+
/** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
|
|
21
|
+
export declare function openHippoDbReadOnly(hippoRoot: string): DatabaseSyncLike;
|
|
17
22
|
export declare function getSchemaVersion(db: DatabaseSyncLike): number;
|
|
18
23
|
export declare function closeHippoDb(db: DatabaseSyncLike): void;
|
|
19
24
|
export declare function getMeta(db: DatabaseSyncLike, key: string, fallback?: string): string;
|
|
20
25
|
export declare function setMeta(db: DatabaseSyncLike, key: string, value: string): void;
|
|
26
|
+
/** Row count of one table; null when the table is missing or unreadable, which callers show as unknown. */
|
|
27
|
+
export declare function countTableRows(db: DatabaseSyncLike, table: string): number | null;
|
|
21
28
|
export declare function isFtsAvailable(db: DatabaseSyncLike): boolean;
|
|
22
29
|
export declare function pruneConsolidationRuns(db: DatabaseSyncLike, keep?: number): void;
|
|
23
30
|
export {};
|