hippo-memory 1.61.0 → 1.62.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 +28 -53
- package/dist/agent-memories/apply.d.ts +1 -1
- package/dist/agent-memories/claude-code.d.ts +3 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +14 -6
- package/dist/agent-memories/types.d.ts +0 -2
- package/dist/api/assemble.js +55 -54
- package/dist/api/context-select.d.ts +49 -0
- package/dist/api/context-select.js +344 -0
- package/dist/api/context.d.ts +2 -2
- package/dist/api/context.js +195 -522
- package/dist/api/drill-down.js +36 -33
- package/dist/api/promote.js +55 -66
- package/dist/api/recall.js +303 -438
- package/dist/api/sleep.js +203 -218
- package/dist/capture/compact.d.ts +1 -1
- package/dist/capture/compact.js +2 -2
- package/dist/cli/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.js +35 -34
- package/dist/cli/decisions.js +333 -333
- package/dist/cli/explain.js +66 -60
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +387 -370
- package/dist/cli/projects.js +8 -5
- package/dist/cli/recall.js +28 -43
- package/dist/cli/remember.js +113 -70
- package/dist/cli/session-hooks.js +100 -90
- package/dist/cli/setup.js +267 -246
- package/dist/cli/status.js +73 -64
- package/dist/cli/transfer.js +85 -99
- package/dist/compaction-record.d.ts +0 -2
- package/dist/compaction-record.js +1 -1
- package/dist/customer-notes.js +77 -68
- package/dist/dag.js +222 -186
- package/dist/decisions.js +93 -76
- package/dist/doctor.js +11 -7
- package/dist/goals.js +99 -86
- package/dist/incidents.js +45 -38
- package/dist/policies.js +85 -68
- package/dist/processes.js +87 -71
- package/dist/project-briefs.js +135 -108
- package/dist/project-merge.d.ts +12 -4
- package/dist/project-merge.js +130 -45
- package/dist/shared.d.ts +9 -0
- package/dist/shared.js +10 -8
- package/dist/skills.js +81 -65
- package/dist/store/search-rows.d.ts +2 -2
- package/dist/store/search-rows.js +15 -9
- 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/drill-down.js
CHANGED
|
@@ -42,16 +42,40 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
42
42
|
// already preventing. Match the HTTP behaviour at the API level.
|
|
43
43
|
return { failure: 'not_found' };
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const { collected, level0DirectCount } = collectDescendants(ctx, summaryId, depth);
|
|
46
|
+
const summaryOut = {
|
|
47
|
+
id: summary.id,
|
|
48
|
+
content: summary.content,
|
|
49
|
+
// v0.30 / E5: the STORED direct-child count; the legacy fallback counts
|
|
50
|
+
// level-0 children, never the BFS-depth-N total (independent-review MED #4).
|
|
51
|
+
descendantCount: summary.descendant_count ?? level0DirectCount,
|
|
52
|
+
earliestAt: summary.earliest_at ?? null,
|
|
53
|
+
latestAt: summary.latest_at ?? null,
|
|
54
|
+
};
|
|
55
|
+
const all = collected.map((c) => ({
|
|
56
|
+
id: c.id,
|
|
57
|
+
content: c.content,
|
|
58
|
+
layer: c.layer,
|
|
59
|
+
dagLevel: c.dag_level ?? 0,
|
|
60
|
+
created: c.created,
|
|
61
|
+
}));
|
|
62
|
+
const { children, truncated } = capChildren(all, summaryOut, opts, limit);
|
|
63
|
+
return {
|
|
64
|
+
summary: summaryOut,
|
|
65
|
+
children,
|
|
66
|
+
// v0.30 / E5: totalChildren = BFS-collected count (depth-aware). For
|
|
67
|
+
// depth=1 this equals the eligible direct-children count (backward
|
|
68
|
+
// compat). For depth>1 it is the cumulative count across levels.
|
|
69
|
+
totalChildren: collected.length,
|
|
70
|
+
truncated,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// BFS with a visited set: dag_parent_id is not unique, so a misconfigured tree could emit a child twice past depth 1.
|
|
74
|
+
// The level-0 count is kept apart so a legacy summary's descendantCount fallback counts direct children only.
|
|
75
|
+
function collectDescendants(ctx, summaryId, depth) {
|
|
49
76
|
const collected = [];
|
|
50
77
|
const visited = new Set([summaryId]);
|
|
51
78
|
let frontier = [summaryId];
|
|
52
|
-
// independent-review MED #4 fold: track level-0 direct-children count
|
|
53
|
-
// separately so the descendantCount fallback (for legacy summaries with
|
|
54
|
-
// null descendant_count) reflects DIRECT children, not BFS-collected total.
|
|
55
79
|
let level0DirectCount = 0;
|
|
56
80
|
for (let level = 0; level < depth; level++) {
|
|
57
81
|
const nextFrontier = [];
|
|
@@ -72,23 +96,10 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
72
96
|
break;
|
|
73
97
|
frontier = nextFrontier;
|
|
74
98
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// level-0 children, never the BFS-depth-N total (independent-review MED #4).
|
|
80
|
-
descendantCount: summary.descendant_count ?? level0DirectCount,
|
|
81
|
-
earliestAt: summary.earliest_at ?? null,
|
|
82
|
-
latestAt: summary.latest_at ?? null,
|
|
83
|
-
};
|
|
84
|
-
const all = collected.map((c) => ({
|
|
85
|
-
id: c.id,
|
|
86
|
-
content: c.content,
|
|
87
|
-
layer: c.layer,
|
|
88
|
-
dagLevel: c.dag_level ?? 0,
|
|
89
|
-
created: c.created,
|
|
90
|
-
}));
|
|
91
|
-
// Apply global cumulative token budget + limit cap on collected.
|
|
99
|
+
return { collected, level0DirectCount };
|
|
100
|
+
}
|
|
101
|
+
/** Global cumulative token budget first, then the `limit` cap. */
|
|
102
|
+
function capChildren(all, summaryOut, opts, limit) {
|
|
92
103
|
let children = all;
|
|
93
104
|
let truncated = false;
|
|
94
105
|
if (opts.budget !== undefined) {
|
|
@@ -110,14 +121,6 @@ export function drillDown(ctx, summaryId, opts = {}) {
|
|
|
110
121
|
children = children.slice(0, limit);
|
|
111
122
|
truncated = true;
|
|
112
123
|
}
|
|
113
|
-
return {
|
|
114
|
-
summary: summaryOut,
|
|
115
|
-
children,
|
|
116
|
-
// v0.30 / E5: totalChildren = BFS-collected count (depth-aware). For
|
|
117
|
-
// depth=1 this equals the eligible direct-children count (backward
|
|
118
|
-
// compat). For depth>1 it is the cumulative count across levels.
|
|
119
|
-
totalChildren: collected.length,
|
|
120
|
-
truncated,
|
|
121
|
-
};
|
|
124
|
+
return { children, truncated };
|
|
122
125
|
}
|
|
123
126
|
//# sourceMappingURL=drill-down.js.map
|
package/dist/api/promote.js
CHANGED
|
@@ -47,19 +47,7 @@ export function promote(ctx, id) {
|
|
|
47
47
|
return { ok: true, sourceId: id, globalId: globalEntry.id };
|
|
48
48
|
}
|
|
49
49
|
export function supersede(ctx, oldId, newContent) {
|
|
50
|
-
|
|
51
|
-
// tenant A on tenant B's id throws "Memory not found" here without any
|
|
52
|
-
// info leak.
|
|
53
|
-
const old = readEntry(ctx.hippoRoot, oldId, ctx.tenantId);
|
|
54
|
-
if (!old) {
|
|
55
|
-
throw new NotFoundError(`Memory not found: ${oldId}`);
|
|
56
|
-
}
|
|
57
|
-
// Guard: not already superseded. The CAS UPDATE below race-safely closes
|
|
58
|
-
// the window between this read and the write; this check just produces a
|
|
59
|
-
// clearer error in the common single-writer case.
|
|
60
|
-
if (old.superseded_by) {
|
|
61
|
-
throw new ConflictError(`Memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
62
|
-
}
|
|
50
|
+
const old = readSupersedable(ctx, oldId);
|
|
63
51
|
const newEntry = createSuccessor(old, newContent, {
|
|
64
52
|
tenantId: ctx.tenantId,
|
|
65
53
|
baseHalfLifeDays: loadConfig(ctx.hippoRoot).defaultHalfLifeDays,
|
|
@@ -72,59 +60,7 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
72
60
|
// the old.superseded_by pointer.
|
|
73
61
|
const db = openHippoDb(ctx.hippoRoot);
|
|
74
62
|
try {
|
|
75
|
-
db
|
|
76
|
-
try {
|
|
77
|
-
// 1. CAS update: only succeed if old.superseded_by IS NULL AND the
|
|
78
|
-
// row still belongs to ctx.tenantId. Tenant filter is belt-and-
|
|
79
|
-
// braces with the readEntry above — it costs nothing and closes
|
|
80
|
-
// a hypothetical window where ownership changes between read and
|
|
81
|
-
// update.
|
|
82
|
-
const result = db.prepare(`
|
|
83
|
-
UPDATE memories
|
|
84
|
-
SET superseded_by = ?
|
|
85
|
-
WHERE id = ? AND tenant_id = ? AND superseded_by IS NULL
|
|
86
|
-
`).run(newEntry.id, oldId, ctx.tenantId);
|
|
87
|
-
if ((result.changes ?? 0) === 0) {
|
|
88
|
-
db.exec('ROLLBACK');
|
|
89
|
-
throw new ConflictError(`Memory ${oldId} already superseded by another writer`);
|
|
90
|
-
}
|
|
91
|
-
// v0.30 / E2 — DAG live-coupling: OLD entry just transitioned to
|
|
92
|
-
// superseded. Its parent (if any) needs rebuild. Lands strictly
|
|
93
|
-
// between the rollback guard above and the writeEntryDbOnly(NEW)
|
|
94
|
-
// below so a failed CAS hits throw before this hook. The NEW
|
|
95
|
-
// entry's parent (typically same parent) is auto-marked by the
|
|
96
|
-
// writeEntryDbOnly hook (same parent → idempotent, audits once).
|
|
97
|
-
if (old.dag_parent_id) {
|
|
98
|
-
markSummaryDirtyInTx(db, old.dag_parent_id, ctx.tenantId, ctx.actor.subject);
|
|
99
|
-
}
|
|
100
|
-
// 2. Write new memory inside same tx via writeEntryDbOnly (DB-only
|
|
101
|
-
// path). This emits its OWN 'remember' audit row for the new
|
|
102
|
-
// memory inside the SAVEPOINT — atomic with the row INSERT.
|
|
103
|
-
writeEntryDbOnly(db, stampOriginProject(ctx.hippoRoot, newEntry), { actor: ctx.actor.subject });
|
|
104
|
-
// 3. User-facing 'supersede' audit row inside the same tx so the
|
|
105
|
-
// chain pointer + audit trail commit atomically.
|
|
106
|
-
appendAuditEvent(db, {
|
|
107
|
-
tenantId: ctx.tenantId,
|
|
108
|
-
actor: ctx.actor.subject,
|
|
109
|
-
op: 'supersede',
|
|
110
|
-
targetId: oldId,
|
|
111
|
-
metadata: { newId: newEntry.id },
|
|
112
|
-
});
|
|
113
|
-
db.exec('COMMIT');
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
try {
|
|
117
|
-
db.exec('ROLLBACK');
|
|
118
|
-
}
|
|
119
|
-
catch { /* already rolled back */ }
|
|
120
|
-
// AT1 (plan §3): refusal audit lands post-ROLLBACK, in a fresh
|
|
121
|
-
// implicit transaction the aborted outer one cannot claw back — then
|
|
122
|
-
// rethrow so the caller sees the refusal.
|
|
123
|
-
if (err instanceof RejectedValueError) {
|
|
124
|
-
auditRejectionRefusal(db, err, ctx.actor.subject);
|
|
125
|
-
}
|
|
126
|
-
throw err;
|
|
127
|
-
}
|
|
63
|
+
commitSupersede(db, ctx, oldId, old, newEntry);
|
|
128
64
|
// Mirrors after COMMIT, while the db handle is still open. Same
|
|
129
65
|
// invariant as the original writeEntry: a mirror failure leaves disk
|
|
130
66
|
// MISSING the markdown for the new memory (rebuildIndex rewrites every
|
|
@@ -142,6 +78,59 @@ export function supersede(ctx, oldId, newContent) {
|
|
|
142
78
|
}
|
|
143
79
|
return { ok: true, oldId, newId: newEntry.id };
|
|
144
80
|
}
|
|
81
|
+
/** The tenant-scoped row to supersede; readEntry's tenant filter makes another tenant's id read as not found. */
|
|
82
|
+
function readSupersedable(ctx, oldId) {
|
|
83
|
+
const old = readEntry(ctx.hippoRoot, oldId, ctx.tenantId);
|
|
84
|
+
if (!old) {
|
|
85
|
+
throw new NotFoundError(`Memory not found: ${oldId}`);
|
|
86
|
+
}
|
|
87
|
+
// The CAS UPDATE closes the race; this check only gives a clearer error in the common single-writer case.
|
|
88
|
+
if (old.superseded_by) {
|
|
89
|
+
throw new ConflictError(`Memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
90
|
+
}
|
|
91
|
+
return old;
|
|
92
|
+
}
|
|
93
|
+
/** CAS on the old row, the successor's insert and the 'supersede' audit row, in one BEGIN IMMEDIATE transaction. */
|
|
94
|
+
function commitSupersede(db, ctx, oldId, old, newEntry) {
|
|
95
|
+
db.exec('BEGIN IMMEDIATE');
|
|
96
|
+
try {
|
|
97
|
+
// The tenant filter repeats readEntry's check at no cost, closing an ownership change between read and update.
|
|
98
|
+
const result = db.prepare(`
|
|
99
|
+
UPDATE memories
|
|
100
|
+
SET superseded_by = ?
|
|
101
|
+
WHERE id = ? AND tenant_id = ? AND superseded_by IS NULL
|
|
102
|
+
`).run(newEntry.id, oldId, ctx.tenantId);
|
|
103
|
+
if ((result.changes ?? 0) === 0) {
|
|
104
|
+
db.exec('ROLLBACK');
|
|
105
|
+
throw new ConflictError(`Memory ${oldId} already superseded by another writer`);
|
|
106
|
+
}
|
|
107
|
+
// After the CAS guard, so a lost race throws before the old parent is marked for rebuild.
|
|
108
|
+
if (old.dag_parent_id) {
|
|
109
|
+
markSummaryDirtyInTx(db, old.dag_parent_id, ctx.tenantId, ctx.actor.subject);
|
|
110
|
+
}
|
|
111
|
+
// Emits its own 'remember' audit row inside the same transaction.
|
|
112
|
+
writeEntryDbOnly(db, stampOriginProject(ctx.hippoRoot, newEntry), { actor: ctx.actor.subject });
|
|
113
|
+
appendAuditEvent(db, {
|
|
114
|
+
tenantId: ctx.tenantId,
|
|
115
|
+
actor: ctx.actor.subject,
|
|
116
|
+
op: 'supersede',
|
|
117
|
+
targetId: oldId,
|
|
118
|
+
metadata: { newId: newEntry.id },
|
|
119
|
+
});
|
|
120
|
+
db.exec('COMMIT');
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
try {
|
|
124
|
+
db.exec('ROLLBACK');
|
|
125
|
+
}
|
|
126
|
+
catch { /* already rolled back */ }
|
|
127
|
+
// The refusal audit lands after ROLLBACK, in a fresh implicit transaction the aborted one cannot undo.
|
|
128
|
+
if (err instanceof RejectedValueError) {
|
|
129
|
+
auditRejectionRefusal(db, err, ctx.actor.subject);
|
|
130
|
+
}
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
145
134
|
export function archiveRaw(ctx, id, reason, opts = {}) {
|
|
146
135
|
const db = openHippoDb(ctx.hippoRoot);
|
|
147
136
|
let mirrorOk = false;
|