hippo-memory 1.32.1 → 1.34.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 +33 -17
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +88 -144
- package/dist/api.js.map +1 -1
- package/dist/audit-prune.d.ts.map +1 -1
- package/dist/audit-prune.js +7 -1
- package/dist/audit-prune.js.map +1 -1
- package/dist/audit.d.ts +3 -2
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +13 -6
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +6 -0
- package/dist/auth.js.map +1 -1
- package/dist/autolearn.js +4 -4
- package/dist/autolearn.js.map +1 -1
- package/dist/benchmarks/e1.3/incident-recall-eval.js +4 -0
- package/dist/benchmarks/e1.3/incident-recall-eval.js.map +1 -1
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +2 -2
- package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +1 -1
- package/dist/capture.d.ts +9 -0
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +97 -67
- package/dist/capture.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +99 -7
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +0 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -14
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -4
- package/dist/config.js.map +1 -1
- package/dist/connectors/github/backfill.d.ts.map +1 -1
- package/dist/connectors/github/backfill.js +25 -16
- package/dist/connectors/github/backfill.js.map +1 -1
- package/dist/connectors/github/cli-impl.d.ts +2 -1
- package/dist/connectors/github/cli-impl.d.ts.map +1 -1
- package/dist/connectors/github/cli-impl.js +20 -4
- package/dist/connectors/github/cli-impl.js.map +1 -1
- package/dist/connectors/github/deletion.d.ts.map +1 -1
- package/dist/connectors/github/deletion.js +2 -0
- package/dist/connectors/github/deletion.js.map +1 -1
- package/dist/connectors/github/dlq.d.ts.map +1 -1
- package/dist/connectors/github/dlq.js +9 -1
- package/dist/connectors/github/dlq.js.map +1 -1
- package/dist/connectors/github/idempotency.d.ts.map +1 -1
- package/dist/connectors/github/idempotency.js +2 -0
- package/dist/connectors/github/idempotency.js.map +1 -1
- package/dist/connectors/github/octokit-client.d.ts +2 -1
- package/dist/connectors/github/octokit-client.d.ts.map +1 -1
- package/dist/connectors/github/octokit-client.js +8 -3
- package/dist/connectors/github/octokit-client.js.map +1 -1
- package/dist/connectors/github/tenant-routing.d.ts.map +1 -1
- package/dist/connectors/github/tenant-routing.js +8 -0
- package/dist/connectors/github/tenant-routing.js.map +1 -1
- package/dist/connectors/github/types.d.ts +14 -5
- package/dist/connectors/github/types.d.ts.map +1 -1
- package/dist/connectors/github/types.js +35 -29
- package/dist/connectors/github/types.js.map +1 -1
- package/dist/connectors/slack/backfill.d.ts.map +1 -1
- package/dist/connectors/slack/backfill.js +2 -0
- package/dist/connectors/slack/backfill.js.map +1 -1
- package/dist/connectors/slack/deletion.d.ts.map +1 -1
- package/dist/connectors/slack/deletion.js +2 -0
- package/dist/connectors/slack/deletion.js.map +1 -1
- package/dist/connectors/slack/dlq.d.ts.map +1 -1
- package/dist/connectors/slack/dlq.js +3 -1
- package/dist/connectors/slack/dlq.js.map +1 -1
- package/dist/connectors/slack/idempotency.d.ts.map +1 -1
- package/dist/connectors/slack/idempotency.js +2 -0
- package/dist/connectors/slack/idempotency.js.map +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts.map +1 -1
- package/dist/connectors/slack/tenant-routing.js +4 -0
- package/dist/connectors/slack/tenant-routing.js.map +1 -1
- package/dist/connectors/slack/types.d.ts +8 -3
- package/dist/connectors/slack/types.d.ts.map +1 -1
- package/dist/connectors/slack/types.js +22 -10
- package/dist/connectors/slack/types.js.map +1 -1
- package/dist/connectors/slack/web-client.d.ts +0 -9
- package/dist/connectors/slack/web-client.d.ts.map +1 -1
- package/dist/connectors/slack/web-client.js +11 -11
- package/dist/connectors/slack/web-client.js.map +1 -1
- package/dist/connectors/slack/workspaces.d.ts.map +1 -1
- package/dist/connectors/slack/workspaces.js +2 -0
- package/dist/connectors/slack/workspaces.js.map +1 -1
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +8 -3
- package/dist/consolidate.js.map +1 -1
- package/dist/customer-notes.d.ts.map +1 -1
- package/dist/customer-notes.js +17 -0
- package/dist/customer-notes.js.map +1 -1
- package/dist/dag.js +1 -1
- package/dist/dag.js.map +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +5 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +24 -0
- package/dist/db.js.map +1 -1
- package/dist/decisions.d.ts.map +1 -1
- package/dist/decisions.js +10 -0
- package/dist/decisions.js.map +1 -1
- package/dist/dedupe.d.ts +6 -4
- package/dist/dedupe.d.ts.map +1 -1
- package/dist/dedupe.js.map +1 -1
- package/dist/embedding-provider.d.ts.map +1 -1
- package/dist/embedding-provider.js +24 -15
- package/dist/embedding-provider.js.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +17 -1
- package/dist/embeddings.js.map +1 -1
- package/dist/eval-suite.d.ts.map +1 -1
- package/dist/eval-suite.js +5 -0
- package/dist/eval-suite.js.map +1 -1
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +10 -3
- package/dist/extract.js.map +1 -1
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +26 -12
- package/dist/goals.js.map +1 -1
- package/dist/graph-extract.d.ts.map +1 -1
- package/dist/graph-extract.js +9 -1
- package/dist/graph-extract.js.map +1 -1
- package/dist/graph-view.d.ts.map +1 -1
- package/dist/graph-view.js.map +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +38 -13
- package/dist/graph.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +42 -21
- package/dist/hooks.js.map +1 -1
- package/dist/importers.d.ts.map +1 -1
- package/dist/importers.js +23 -42
- package/dist/importers.js.map +1 -1
- package/dist/incidents.d.ts.map +1 -1
- package/dist/incidents.js +14 -0
- package/dist/incidents.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +4 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +64 -30
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory-value.d.ts +0 -10
- package/dist/memory-value.d.ts.map +1 -1
- package/dist/memory-value.js +8 -2
- package/dist/memory-value.js.map +1 -1
- package/dist/memory.js.map +1 -1
- package/dist/physics-state.d.ts.map +1 -1
- package/dist/physics-state.js +5 -0
- package/dist/physics-state.js.map +1 -1
- package/dist/physics.js.map +1 -1
- package/dist/policies.d.ts.map +1 -1
- package/dist/policies.js +22 -0
- package/dist/policies.js.map +1 -1
- package/dist/predictions.d.ts.map +1 -1
- package/dist/predictions.js +14 -0
- package/dist/predictions.js.map +1 -1
- package/dist/processes.d.ts +7 -1
- package/dist/processes.d.ts.map +1 -1
- package/dist/processes.js +21 -2
- package/dist/processes.js.map +1 -1
- package/dist/project-briefs.d.ts.map +1 -1
- package/dist/project-briefs.js +28 -2
- package/dist/project-briefs.js.map +1 -1
- package/dist/raw-archive-mirror-cleanup.d.ts.map +1 -1
- package/dist/raw-archive-mirror-cleanup.js +2 -0
- package/dist/raw-archive-mirror-cleanup.js.map +1 -1
- package/dist/raw-archive.d.ts +0 -9
- package/dist/raw-archive.d.ts.map +1 -1
- package/dist/raw-archive.js +3 -9
- package/dist/raw-archive.js.map +1 -1
- package/dist/recall-scope.d.ts.map +1 -1
- package/dist/recall-scope.js +11 -6
- package/dist/recall-scope.js.map +1 -1
- package/dist/recall-trace.d.ts.map +1 -1
- package/dist/recall-trace.js +7 -3
- package/dist/recall-trace.js.map +1 -1
- package/dist/refine-llm.d.ts.map +1 -1
- package/dist/refine-llm.js +3 -0
- package/dist/refine-llm.js.map +1 -1
- package/dist/reject-flow.d.ts.map +1 -1
- package/dist/reject-flow.js +2 -0
- package/dist/reject-flow.js.map +1 -1
- package/dist/rejection.d.ts.map +1 -1
- package/dist/rejection.js +3 -0
- package/dist/rejection.js.map +1 -1
- package/dist/replay.js.map +1 -1
- package/dist/rerankers/cross-encoder.d.ts.map +1 -1
- package/dist/rerankers/cross-encoder.js +11 -4
- package/dist/rerankers/cross-encoder.js.map +1 -1
- package/dist/rerankers/index.d.ts.map +1 -1
- package/dist/rerankers/index.js +5 -3
- package/dist/rerankers/index.js.map +1 -1
- package/dist/rerankers/llm.d.ts.map +1 -1
- package/dist/rerankers/llm.js +8 -5
- package/dist/rerankers/llm.js.map +1 -1
- package/dist/rerankers/types.d.ts +6 -1
- package/dist/rerankers/types.d.ts.map +1 -1
- package/dist/scheduler.d.ts +16 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +15 -5
- package/dist/scheduler.js.map +1 -1
- package/dist/search.d.ts +4 -6
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/server-detect.d.ts.map +1 -1
- package/dist/server-detect.js +2 -0
- package/dist/server-detect.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +194 -135
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +10 -3
- package/dist/skills.js.map +1 -1
- package/dist/src/api.js +88 -144
- package/dist/src/api.js.map +1 -1
- package/dist/src/audit-prune.js +7 -1
- package/dist/src/audit-prune.js.map +1 -1
- package/dist/src/audit.js +13 -6
- package/dist/src/audit.js.map +1 -1
- package/dist/src/auth.js +6 -0
- package/dist/src/auth.js.map +1 -1
- package/dist/src/autolearn.js +4 -4
- package/dist/src/autolearn.js.map +1 -1
- package/dist/src/capture.js +97 -67
- package/dist/src/capture.js.map +1 -1
- package/dist/src/cli.js +99 -7
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.js +32 -14
- package/dist/src/client.js.map +1 -1
- package/dist/src/config.js +7 -4
- package/dist/src/config.js.map +1 -1
- package/dist/src/connectors/github/backfill.js +25 -16
- package/dist/src/connectors/github/backfill.js.map +1 -1
- package/dist/src/connectors/github/cli-impl.js +20 -4
- package/dist/src/connectors/github/cli-impl.js.map +1 -1
- package/dist/src/connectors/github/deletion.js +2 -0
- package/dist/src/connectors/github/deletion.js.map +1 -1
- package/dist/src/connectors/github/dlq.js +9 -1
- package/dist/src/connectors/github/dlq.js.map +1 -1
- package/dist/src/connectors/github/idempotency.js +2 -0
- package/dist/src/connectors/github/idempotency.js.map +1 -1
- package/dist/src/connectors/github/octokit-client.js +8 -3
- package/dist/src/connectors/github/octokit-client.js.map +1 -1
- package/dist/src/connectors/github/tenant-routing.js +8 -0
- package/dist/src/connectors/github/tenant-routing.js.map +1 -1
- package/dist/src/connectors/github/types.js +35 -29
- package/dist/src/connectors/github/types.js.map +1 -1
- package/dist/src/connectors/slack/backfill.js +2 -0
- package/dist/src/connectors/slack/backfill.js.map +1 -1
- package/dist/src/connectors/slack/deletion.js +2 -0
- package/dist/src/connectors/slack/deletion.js.map +1 -1
- package/dist/src/connectors/slack/dlq.js +3 -1
- package/dist/src/connectors/slack/dlq.js.map +1 -1
- package/dist/src/connectors/slack/idempotency.js +2 -0
- package/dist/src/connectors/slack/idempotency.js.map +1 -1
- package/dist/src/connectors/slack/tenant-routing.js +4 -0
- package/dist/src/connectors/slack/tenant-routing.js.map +1 -1
- package/dist/src/connectors/slack/types.js +22 -10
- package/dist/src/connectors/slack/types.js.map +1 -1
- package/dist/src/connectors/slack/web-client.js +11 -11
- package/dist/src/connectors/slack/web-client.js.map +1 -1
- package/dist/src/connectors/slack/workspaces.js +2 -0
- package/dist/src/connectors/slack/workspaces.js.map +1 -1
- package/dist/src/consolidate.js +8 -3
- package/dist/src/consolidate.js.map +1 -1
- package/dist/src/customer-notes.js +17 -0
- package/dist/src/customer-notes.js.map +1 -1
- package/dist/src/dag.js +1 -1
- package/dist/src/dag.js.map +1 -1
- package/dist/src/dashboard.js +5 -2
- package/dist/src/dashboard.js.map +1 -1
- package/dist/src/db.js +24 -0
- package/dist/src/db.js.map +1 -1
- package/dist/src/decisions.js +10 -0
- package/dist/src/decisions.js.map +1 -1
- package/dist/src/dedupe.js.map +1 -1
- package/dist/src/embedding-provider.js +24 -15
- package/dist/src/embedding-provider.js.map +1 -1
- package/dist/src/embeddings.js +17 -1
- package/dist/src/embeddings.js.map +1 -1
- package/dist/src/eval-suite.js +5 -0
- package/dist/src/eval-suite.js.map +1 -1
- package/dist/src/extract.js +10 -3
- package/dist/src/extract.js.map +1 -1
- package/dist/src/goals.js +26 -12
- package/dist/src/goals.js.map +1 -1
- package/dist/src/graph-extract.js +9 -1
- package/dist/src/graph-extract.js.map +1 -1
- package/dist/src/graph-view.js.map +1 -1
- package/dist/src/graph.js +38 -13
- package/dist/src/graph.js.map +1 -1
- package/dist/src/hooks.js +42 -21
- package/dist/src/hooks.js.map +1 -1
- package/dist/src/importers.js +23 -42
- package/dist/src/importers.js.map +1 -1
- package/dist/src/incidents.js +14 -0
- package/dist/src/incidents.js.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/server.js +64 -30
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/memory-value.js +8 -2
- package/dist/src/memory-value.js.map +1 -1
- package/dist/src/memory.js.map +1 -1
- package/dist/src/physics-state.js +5 -0
- package/dist/src/physics-state.js.map +1 -1
- package/dist/src/physics.js.map +1 -1
- package/dist/src/policies.js +22 -0
- package/dist/src/policies.js.map +1 -1
- package/dist/src/predictions.js +14 -0
- package/dist/src/predictions.js.map +1 -1
- package/dist/src/processes.js +21 -2
- package/dist/src/processes.js.map +1 -1
- package/dist/src/project-briefs.js +28 -2
- package/dist/src/project-briefs.js.map +1 -1
- package/dist/src/raw-archive-mirror-cleanup.js +2 -0
- package/dist/src/raw-archive-mirror-cleanup.js.map +1 -1
- package/dist/src/raw-archive.js +3 -9
- package/dist/src/raw-archive.js.map +1 -1
- package/dist/src/recall-scope.js +11 -6
- package/dist/src/recall-scope.js.map +1 -1
- package/dist/src/recall-trace.js +7 -3
- package/dist/src/recall-trace.js.map +1 -1
- package/dist/src/refine-llm.js +3 -0
- package/dist/src/refine-llm.js.map +1 -1
- package/dist/src/reject-flow.js +2 -0
- package/dist/src/reject-flow.js.map +1 -1
- package/dist/src/rejection.js +3 -0
- package/dist/src/rejection.js.map +1 -1
- package/dist/src/replay.js.map +1 -1
- package/dist/src/rerankers/cross-encoder.js +11 -4
- package/dist/src/rerankers/cross-encoder.js.map +1 -1
- package/dist/src/rerankers/index.js +5 -3
- package/dist/src/rerankers/index.js.map +1 -1
- package/dist/src/rerankers/llm.js +8 -5
- package/dist/src/rerankers/llm.js.map +1 -1
- package/dist/src/scheduler.js +15 -5
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/search.js.map +1 -1
- package/dist/src/server-detect.js +2 -0
- package/dist/src/server-detect.js.map +1 -1
- package/dist/src/server.js +194 -135
- package/dist/src/server.js.map +1 -1
- package/dist/src/skills.js +10 -3
- package/dist/src/skills.js.map +1 -1
- package/dist/src/store.js +213 -14
- package/dist/src/store.js.map +1 -1
- package/dist/src/trace.js +15 -7
- package/dist/src/trace.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/working-memory.js +8 -0
- package/dist/src/working-memory.js.map +1 -1
- package/dist/src/yaml.js +12 -3
- package/dist/src/yaml.js.map +1 -1
- package/dist/store.d.ts +61 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +213 -14
- package/dist/store.js.map +1 -1
- package/dist/trace.d.ts.map +1 -1
- package/dist/trace.js +15 -7
- package/dist/trace.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +6 -2
- package/dist/working-memory.d.ts.map +1 -1
- package/dist/working-memory.js +8 -0
- package/dist/working-memory.js.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/dist/yaml.js +12 -3
- package/dist/yaml.js.map +1 -1
- package/dist-ui/assets/{index-Cxa4RcAh.js → index-DOqenbV2.js} +11 -11
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/index.ts +50 -10
- 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 +4 -1
package/dist/store.js
CHANGED
|
@@ -175,7 +175,11 @@ export function serializeEntry(entry) {
|
|
|
175
175
|
if (entry.origin_project !== undefined && entry.origin_project !== null) {
|
|
176
176
|
frontmatter['origin_project'] = entry.origin_project;
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
// Spread into a fresh object literal: dumpFrontmatter's Record<string,
|
|
179
|
+
// YamlValue> parameter needs an index signature, which a named interface
|
|
180
|
+
// reference (EntryFrontmatterFields) doesn't structurally provide even
|
|
181
|
+
// though every property's value type already matches.
|
|
182
|
+
const fm = dumpFrontmatter({ ...frontmatter });
|
|
179
183
|
return `${fm}\n\n${entry.content}\n`;
|
|
180
184
|
}
|
|
181
185
|
/**
|
|
@@ -185,6 +189,11 @@ export function deserializeEntry(raw) {
|
|
|
185
189
|
const { data, content } = parseFrontmatter(raw);
|
|
186
190
|
if (!data['id'] || !data['layer'])
|
|
187
191
|
return null;
|
|
192
|
+
// SAFETY: every `as X` below narrows a raw YAML frontmatter field to an
|
|
193
|
+
// enum/union member of MemoryEntry; frontmatter is only ever written by
|
|
194
|
+
// serializeEntry (whose own fields are typed), so out-of-range values here
|
|
195
|
+
// would indicate hand-edited files, which this parser is not required to
|
|
196
|
+
// reject — matches the pre-existing permissive-parse behavior.
|
|
188
197
|
return {
|
|
189
198
|
id: String(data['id']),
|
|
190
199
|
created: String(data['created'] ?? new Date().toISOString()),
|
|
@@ -231,7 +240,11 @@ function normalizeStringArray(value) {
|
|
|
231
240
|
return value.map((item) => String(item));
|
|
232
241
|
}
|
|
233
242
|
function rowToEntry(row) {
|
|
234
|
-
|
|
243
|
+
// SAFETY: every `as X` below narrows a SQLite column value to an
|
|
244
|
+
// enum/union member of MemoryEntry; `row` comes from MEMORY_SELECT_COLUMNS
|
|
245
|
+
// / MEMORY_SEARCH_COLUMNS, which are the only queries producing MemoryRow,
|
|
246
|
+
// and the DB layer only ever writes these columns from the same enums.
|
|
247
|
+
const entry = {
|
|
235
248
|
id: row.id,
|
|
236
249
|
created: row.created,
|
|
237
250
|
last_retrieved: row.last_retrieved,
|
|
@@ -273,13 +286,14 @@ function rowToEntry(row) {
|
|
|
273
286
|
last_rebuilt_at: row.last_rebuilt_at ?? null,
|
|
274
287
|
rebuild_count: Number(row.rebuild_count ?? 0),
|
|
275
288
|
dag_level_3_built_at: row.dag_level_3_built_at ?? null,
|
|
276
|
-
// F1 (v1.7.0): preserve bm25_score from the FTS path. `'bm25_score' in row`
|
|
277
|
-
// distinguishes "absent column" (non-FTS path) from "column present but
|
|
278
|
-
// value 0" — though FTS5 bm25() never returns 0, this is defensive.
|
|
279
|
-
...('bm25_score' in row && row.bm25_score !== undefined && row.bm25_score !== null
|
|
280
|
-
? { bm25_score: Number(row.bm25_score) }
|
|
281
|
-
: {}),
|
|
282
289
|
};
|
|
290
|
+
// F1 (v1.7.0): preserve bm25_score from the FTS path. `'bm25_score' in row`
|
|
291
|
+
// distinguishes "absent column" (non-FTS path) from "column present but
|
|
292
|
+
// value 0" — though FTS5 bm25() never returns 0, this is defensive.
|
|
293
|
+
if ('bm25_score' in row && row.bm25_score !== undefined && row.bm25_score !== null) {
|
|
294
|
+
entry.bm25_score = Number(row.bm25_score);
|
|
295
|
+
}
|
|
296
|
+
return entry;
|
|
283
297
|
}
|
|
284
298
|
function parseJsonArray(raw) {
|
|
285
299
|
if (!raw)
|
|
@@ -309,12 +323,15 @@ function parseLastTraceId(raw) {
|
|
|
309
323
|
return null;
|
|
310
324
|
return trimmed;
|
|
311
325
|
}
|
|
326
|
+
function isPlainJsonObject(x) {
|
|
327
|
+
return x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
328
|
+
}
|
|
312
329
|
function parseJsonObject(raw) {
|
|
313
330
|
if (!raw)
|
|
314
331
|
return {};
|
|
315
332
|
try {
|
|
316
333
|
const parsed = JSON.parse(raw);
|
|
317
|
-
if (
|
|
334
|
+
if (isPlainJsonObject(parsed)) {
|
|
318
335
|
return parsed;
|
|
319
336
|
}
|
|
320
337
|
return {};
|
|
@@ -585,6 +602,8 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter) {
|
|
|
585
602
|
// this no-terms path had the same shape). Apply LIMIT so all four
|
|
586
603
|
// candidate paths honour the caller's cap when set.
|
|
587
604
|
const sql = `SELECT ${MEMORY_SELECT_COLUMNS} FROM memories${tenantOnlyPredicate}${archivedClauseTenantOnly}${scopeClauseTenantOnly} ORDER BY created ASC, id ASC LIMIT ?`;
|
|
605
|
+
// SAFETY: sql selects exactly MEMORY_SELECT_COLUMNS, whose column list
|
|
606
|
+
// matches MemoryRow's field set.
|
|
588
607
|
return db.prepare(sql).all(...tenantParams, ...scopeParams, limit);
|
|
589
608
|
}
|
|
590
609
|
// v1.7.1 — test/diagnostic hook: `HIPPO_FORCE_LIKE_PATH=1` forces the
|
|
@@ -603,6 +622,8 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter) {
|
|
|
603
622
|
// F1 (v1.7.0): MEMORY_SEARCH_COLUMNS adds bm25_score as the trailing
|
|
604
623
|
// result column. Every other column is m.<col> AS <col> so rowToEntry
|
|
605
624
|
// sees the same shape it always has.
|
|
625
|
+
// SAFETY: MEMORY_SEARCH_COLUMNS aliases every column to the same name
|
|
626
|
+
// MEMORY_SELECT_COLUMNS uses (plus bm25_score), matching MemoryRow.
|
|
606
627
|
const rows = db.prepare(`
|
|
607
628
|
SELECT ${MEMORY_SEARCH_COLUMNS}
|
|
608
629
|
FROM memories m
|
|
@@ -624,6 +645,8 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter) {
|
|
|
624
645
|
const like = `%${escapeLike(term)}%`;
|
|
625
646
|
return [like, like];
|
|
626
647
|
});
|
|
648
|
+
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
649
|
+
// MemoryRow's field set.
|
|
627
650
|
const rows = db.prepare(`
|
|
628
651
|
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
629
652
|
FROM memories
|
|
@@ -639,6 +662,8 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter) {
|
|
|
639
662
|
// candidate-pool size. Apply LIMIT here so all four paths honour the
|
|
640
663
|
// caller's cap.
|
|
641
664
|
const fallback = `SELECT ${MEMORY_SELECT_COLUMNS} FROM memories${tenantOnlyPredicate}${archivedClauseTenantOnly}${scopeClauseTenantOnly} ORDER BY created ASC, id ASC LIMIT ?`;
|
|
665
|
+
// SAFETY: fallback selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
666
|
+
// MemoryRow's field set.
|
|
642
667
|
return db.prepare(fallback).all(...tenantParams, ...scopeParams, limit);
|
|
643
668
|
}
|
|
644
669
|
function writeMarkdownMirror(hippoRoot, entry) {
|
|
@@ -724,6 +749,8 @@ export function purgeMirrorBestEffort(hippoRoot, id, isRaw, logPrefix) {
|
|
|
724
749
|
}
|
|
725
750
|
}
|
|
726
751
|
function bootstrapLegacyStore(db, hippoRoot) {
|
|
752
|
+
// SAFETY: countRow's shape matches the single `COUNT(*) AS count` column
|
|
753
|
+
// selected above; `.get()` returns undefined only when no row exists.
|
|
727
754
|
const countRow = db.prepare(`SELECT COUNT(*) AS count FROM memories`).get();
|
|
728
755
|
const memoryCount = Number(countRow?.count ?? 0);
|
|
729
756
|
if (memoryCount > 0)
|
|
@@ -788,7 +815,7 @@ function bootstrapLegacyStore(db, hippoRoot) {
|
|
|
788
815
|
const runs = Array.isArray(legacyStats.consolidation_runs) ? legacyStats.consolidation_runs : [];
|
|
789
816
|
const insertRun = db.prepare(`INSERT INTO consolidation_runs(timestamp, decayed, merged, removed) VALUES (?, ?, ?, ?)`);
|
|
790
817
|
for (const run of runs) {
|
|
791
|
-
if (!run
|
|
818
|
+
if (!isPlainJsonObject(run))
|
|
792
819
|
continue;
|
|
793
820
|
const row = run;
|
|
794
821
|
insertRun.run(String(row.timestamp ?? new Date().toISOString()), Number(row.decayed ?? 0), Number(row.merged ?? 0), Number(row.removed ?? 0));
|
|
@@ -827,6 +854,9 @@ function loadLegacyIndexFile(hippoRoot) {
|
|
|
827
854
|
return { version: 1, entries: {}, last_retrieval_ids: [], last_trace_id: null };
|
|
828
855
|
}
|
|
829
856
|
try {
|
|
857
|
+
// SAFETY: index.json is only ever written by writeIndexMirror below,
|
|
858
|
+
// which always serializes a HippoIndex; a hand-edited or corrupted file
|
|
859
|
+
// that violates the shape falls through to the catch block's fallback.
|
|
830
860
|
return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
831
861
|
}
|
|
832
862
|
catch {
|
|
@@ -844,6 +874,10 @@ function loadLegacyStatsFile(hippoRoot) {
|
|
|
844
874
|
};
|
|
845
875
|
}
|
|
846
876
|
try {
|
|
877
|
+
// SAFETY: stats.json is only ever written by writeStatsMirror below,
|
|
878
|
+
// which always emits exactly these four fields; callers additionally
|
|
879
|
+
// guard every read with `?? 0` / `Array.isArray`, tolerating a
|
|
880
|
+
// hand-edited or corrupted file even if this optimistic cast is wrong.
|
|
847
881
|
return JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
|
848
882
|
}
|
|
849
883
|
catch {
|
|
@@ -968,9 +1002,12 @@ function deleteFtsRow(db, id) {
|
|
|
968
1002
|
* without duplicating this query.
|
|
969
1003
|
*/
|
|
970
1004
|
export function buildIndexFromDb(db) {
|
|
1005
|
+
// SAFETY: rows' shape matches the seven columns named in the SELECT below.
|
|
971
1006
|
const rows = db.prepare(`SELECT id, created, last_retrieved, strength, layer, tags_json, pinned FROM memories ORDER BY created ASC, id ASC`).all();
|
|
972
1007
|
const entries = {};
|
|
973
1008
|
for (const row of rows) {
|
|
1009
|
+
// SAFETY: layer is only ever written from the Layer enum by this
|
|
1010
|
+
// module's own INSERT/UPDATE paths.
|
|
974
1011
|
const layer = row.layer;
|
|
975
1012
|
entries[row.id] = {
|
|
976
1013
|
id: row.id,
|
|
@@ -989,6 +1026,7 @@ export function buildIndexFromDb(db) {
|
|
|
989
1026
|
// them, handing the reader mismatched last_retrieval_ids / last_trace_id
|
|
990
1027
|
// and re-opening the mislinkage hole saveIndex's BEGIN/COMMIT closed on
|
|
991
1028
|
// the write side. One SELECT = one SQLite read snapshot.
|
|
1029
|
+
// SAFETY: lockstepRows' shape matches the key/value columns named above.
|
|
992
1030
|
const lockstepRows = db.prepare(`SELECT key, value FROM meta WHERE key IN ('last_retrieval_ids', 'last_trace_id')`).all();
|
|
993
1031
|
const lockstep = new Map(lockstepRows.map((r) => [r.key, r.value]));
|
|
994
1032
|
return {
|
|
@@ -999,12 +1037,18 @@ export function buildIndexFromDb(db) {
|
|
|
999
1037
|
};
|
|
1000
1038
|
}
|
|
1001
1039
|
function buildStatsFromDb(db) {
|
|
1040
|
+
// SAFETY: runs' shape matches the four columns named in the SELECT above.
|
|
1002
1041
|
const runs = db.prepare(`SELECT timestamp, decayed, merged, removed FROM consolidation_runs ORDER BY timestamp ASC, id ASC`).all();
|
|
1003
1042
|
return {
|
|
1004
1043
|
total_remembered: Number(getMeta(db, 'total_remembered', '0')),
|
|
1005
1044
|
total_recalled: Number(getMeta(db, 'total_recalled', '0')),
|
|
1006
1045
|
total_forgotten: Number(getMeta(db, 'total_forgotten', '0')),
|
|
1007
|
-
consolidation_runs: runs
|
|
1046
|
+
consolidation_runs: runs.map((run) => ({
|
|
1047
|
+
timestamp: run.timestamp,
|
|
1048
|
+
decayed: run.decayed,
|
|
1049
|
+
merged: run.merged,
|
|
1050
|
+
removed: run.removed,
|
|
1051
|
+
})),
|
|
1008
1052
|
};
|
|
1009
1053
|
}
|
|
1010
1054
|
/**
|
|
@@ -1021,10 +1065,14 @@ function writeStatsMirror(hippoRoot, stats) {
|
|
|
1021
1065
|
fs.writeFileSync(path.join(hippoRoot, 'stats.json'), JSON.stringify(stats, null, 2), 'utf8');
|
|
1022
1066
|
}
|
|
1023
1067
|
function syncMirrorFiles(hippoRoot, db) {
|
|
1068
|
+
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
1069
|
+
// MemoryRow's field set.
|
|
1024
1070
|
const entries = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
|
|
1025
1071
|
for (const entry of entries.map(rowToEntry)) {
|
|
1026
1072
|
writeMarkdownMirror(hippoRoot, entry);
|
|
1027
1073
|
}
|
|
1074
|
+
// SAFETY: conflicts' shape matches the eight columns named in the SELECT
|
|
1075
|
+
// above.
|
|
1028
1076
|
const conflicts = db.prepare(`
|
|
1029
1077
|
SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
|
|
1030
1078
|
FROM memory_conflicts
|
|
@@ -1225,6 +1273,8 @@ export function readEntry(hippoRoot, id, tenantId) {
|
|
|
1225
1273
|
initStore(hippoRoot);
|
|
1226
1274
|
const db = openHippoDb(hippoRoot);
|
|
1227
1275
|
try {
|
|
1276
|
+
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
|
|
1277
|
+
// MemoryRow's field set.
|
|
1228
1278
|
const row = tenantId !== undefined
|
|
1229
1279
|
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id = ? AND tenant_id = ?`).get(id, tenantId)
|
|
1230
1280
|
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id = ?`).get(id);
|
|
@@ -1251,6 +1301,8 @@ export function loadEntriesByIds(hippoRoot, ids, tenantId) {
|
|
|
1251
1301
|
// T2: no ORDER BY meant row order followed SQLite's IN(...) scan order
|
|
1252
1302
|
// (undefined w.r.t. the caller's `ids` order). created ASC, id ASC
|
|
1253
1303
|
// makes it deterministic.
|
|
1304
|
+
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
|
|
1305
|
+
// MemoryRow's field set.
|
|
1254
1306
|
const rows = tenantId !== undefined
|
|
1255
1307
|
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders}) AND tenant_id = ? ORDER BY created ASC, content ASC, id ASC`).all(...capped, tenantId)
|
|
1256
1308
|
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders}) ORDER BY created ASC, content ASC, id ASC`).all(...capped);
|
|
@@ -1288,10 +1340,12 @@ export function loadSessionRawMemories(hippoRoot, sessionId, tenantId, cap) {
|
|
|
1288
1340
|
if (cap !== undefined && cap > 0) {
|
|
1289
1341
|
sql += ' ORDER BY created DESC, id DESC LIMIT ?';
|
|
1290
1342
|
params.push(cap);
|
|
1343
|
+
// SAFETY: sql starts from MEMORY_SELECT_COLUMNS, matching MemoryRow.
|
|
1291
1344
|
const rows = db.prepare(sql).all(...params);
|
|
1292
1345
|
return rows.reverse().map(rowToEntry);
|
|
1293
1346
|
}
|
|
1294
1347
|
sql += ' ORDER BY created ASC, id ASC';
|
|
1348
|
+
// SAFETY: sql starts from MEMORY_SELECT_COLUMNS, matching MemoryRow.
|
|
1295
1349
|
const rows = db.prepare(sql).all(...params);
|
|
1296
1350
|
return rows.map(rowToEntry);
|
|
1297
1351
|
}
|
|
@@ -1338,6 +1392,7 @@ export function countSessionRawMemories(hippoRoot, sessionId, tenantId, scope) {
|
|
|
1338
1392
|
// AND != 'unknown:legacy'). Mirrors api.passesScopeFilterForRecall.
|
|
1339
1393
|
sql += ` AND (scope IS NULL OR (scope NOT LIKE '%:private:%' AND scope != 'unknown:legacy'))`;
|
|
1340
1394
|
}
|
|
1395
|
+
// SAFETY: row's shape matches the single `COUNT(*) AS c` column above.
|
|
1341
1396
|
const row = db.prepare(sql).get(...params);
|
|
1342
1397
|
return Number(row?.c ?? 0);
|
|
1343
1398
|
}
|
|
@@ -1392,6 +1447,7 @@ export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
|
|
|
1392
1447
|
// cross-ingest-stable.
|
|
1393
1448
|
sql += ' ORDER BY created DESC, content ASC, id ASC LIMIT ?';
|
|
1394
1449
|
params.push(capped);
|
|
1450
|
+
// SAFETY: sql starts from MEMORY_SELECT_COLUMNS, matching MemoryRow.
|
|
1395
1451
|
const rows = db.prepare(sql).all(...params);
|
|
1396
1452
|
return rows.map(rowToEntry);
|
|
1397
1453
|
}
|
|
@@ -1408,6 +1464,8 @@ export function loadChildrenOf(hippoRoot, parentId, tenantId) {
|
|
|
1408
1464
|
initStore(hippoRoot);
|
|
1409
1465
|
const db = openHippoDb(hippoRoot);
|
|
1410
1466
|
try {
|
|
1467
|
+
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
|
|
1468
|
+
// MemoryRow's field set.
|
|
1411
1469
|
const rows = tenantId !== undefined
|
|
1412
1470
|
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id = ? AND tenant_id = ? ORDER BY created ASC, id ASC`).all(parentId, tenantId)
|
|
1413
1471
|
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id = ? ORDER BY created ASC, id ASC`).all(parentId);
|
|
@@ -1437,6 +1495,7 @@ export function loadChildrenOf(hippoRoot, parentId, tenantId) {
|
|
|
1437
1495
|
* with `id` existed.
|
|
1438
1496
|
*/
|
|
1439
1497
|
export function deleteEntryCore(db, id, opts) {
|
|
1498
|
+
// SAFETY: row's shape matches the three columns named in the SELECT above.
|
|
1440
1499
|
const row = db
|
|
1441
1500
|
.prepare(`SELECT id, tenant_id, dag_parent_id FROM memories WHERE id = ?`)
|
|
1442
1501
|
.get(id);
|
|
@@ -1511,6 +1570,8 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds) {
|
|
|
1511
1570
|
const tenantById = new Map();
|
|
1512
1571
|
if (toDeleteIds.length > 0) {
|
|
1513
1572
|
const placeholders = toDeleteIds.map(() => '?').join(',');
|
|
1573
|
+
// SAFETY: rows' shape matches the two columns named in the SELECT
|
|
1574
|
+
// above.
|
|
1514
1575
|
const rows = db.prepare(`SELECT dag_parent_id, tenant_id FROM memories WHERE id IN (${placeholders})`).all(...toDeleteIds);
|
|
1515
1576
|
for (const row of rows) {
|
|
1516
1577
|
if (row.dag_parent_id) {
|
|
@@ -1633,6 +1694,8 @@ export function loadAllEntries(hippoRoot, tenantId) {
|
|
|
1633
1694
|
initStore(hippoRoot);
|
|
1634
1695
|
const db = openHippoDb(hippoRoot);
|
|
1635
1696
|
try {
|
|
1697
|
+
// SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
|
|
1698
|
+
// MemoryRow's field set.
|
|
1636
1699
|
const rows = tenantId !== undefined
|
|
1637
1700
|
? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE tenant_id = ? ORDER BY created ASC, id ASC`).all(tenantId)
|
|
1638
1701
|
: db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
|
|
@@ -1709,6 +1772,7 @@ export function rebuildIndex(hippoRoot) {
|
|
|
1709
1772
|
initStore(hippoRoot);
|
|
1710
1773
|
const db = openHippoDb(hippoRoot);
|
|
1711
1774
|
try {
|
|
1775
|
+
// SAFETY: rows' shape matches the single `id` column selected above.
|
|
1712
1776
|
const existingIds = new Set(db.prepare(`SELECT id FROM memories`).all().map((row) => row.id));
|
|
1713
1777
|
const legacyEntries = loadLegacyEntriesFromMarkdown(hippoRoot).filter((entry) => !existingIds.has(entry.id));
|
|
1714
1778
|
if (legacyEntries.length > 0) {
|
|
@@ -1803,6 +1867,7 @@ export function loadSessionDecayContext(hippoRoot) {
|
|
|
1803
1867
|
const db = openHippoDb(hippoRoot);
|
|
1804
1868
|
try {
|
|
1805
1869
|
// Get recent consolidation timestamps (last 20)
|
|
1870
|
+
// SAFETY: rows' shape matches the single `timestamp` column above.
|
|
1806
1871
|
const rows = db.prepare(`SELECT timestamp FROM consolidation_runs ORDER BY timestamp DESC, id DESC LIMIT 20`).all();
|
|
1807
1872
|
const sleepCount = Number(getMeta(db, 'sleep_count', '0')) || rows.length;
|
|
1808
1873
|
if (rows.length < 2) {
|
|
@@ -1876,6 +1941,7 @@ export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
|
|
|
1876
1941
|
`).run(snapshot.task, snapshot.summary, snapshot.next_step, snapshot.source ?? 'cli', snapshot.session_id ?? null, snapshot.scope ?? null, tenantId, now, now);
|
|
1877
1942
|
db.exec('COMMIT');
|
|
1878
1943
|
const id = Number(result.lastInsertRowid ?? 0);
|
|
1944
|
+
// SAFETY: row's shape matches the ten columns named in the SELECT above.
|
|
1879
1945
|
const row = db.prepare(`
|
|
1880
1946
|
SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
|
|
1881
1947
|
FROM task_snapshots
|
|
@@ -1906,6 +1972,7 @@ export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
|
|
|
1906
1972
|
initStore(hippoRoot);
|
|
1907
1973
|
const db = openHippoDb(hippoRoot);
|
|
1908
1974
|
try {
|
|
1975
|
+
// SAFETY: row's shape matches the ten columns named in the SELECT above.
|
|
1909
1976
|
const row = db.prepare(`
|
|
1910
1977
|
SELECT id, task, summary, next_step, status, source, session_id, scope, created_at, updated_at
|
|
1911
1978
|
FROM task_snapshots
|
|
@@ -1925,12 +1992,64 @@ export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
|
|
|
1925
1992
|
closeHippoDb(db);
|
|
1926
1993
|
}
|
|
1927
1994
|
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Default freshness bound for AMBIENT active-task-snapshot reads (DF1,
|
|
1997
|
+
* docs/plans/2026-08-23-df1-snapshot-lifecycle.md): 72h, chosen over 48h so
|
|
1998
|
+
* a Friday-evening orphan still offers continuity on Monday morning.
|
|
1999
|
+
* Exported so callers can override via `loadFreshActiveTaskSnapshot`'s
|
|
2000
|
+
* `opts.maxAgeMs`; deliberately no env knob (Simplicity First).
|
|
2001
|
+
*/
|
|
2002
|
+
export const SNAPSHOT_AMBIENT_MAX_AGE_MS = 72 * 60 * 60 * 1000;
|
|
2003
|
+
/** A usable session id: non-null, non-empty string. Named predicate (not an
|
|
2004
|
+
* inline `typeof` check) so the owner-match rule in
|
|
2005
|
+
* `loadFreshActiveTaskSnapshot` states its contract once. */
|
|
2006
|
+
function isNonEmptySessionId(value) {
|
|
2007
|
+
return typeof value === 'string' && value.length > 0;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Bounded read for AMBIENT active-task-snapshot surfaces (UserPromptSubmit
|
|
2011
|
+
* hook context, MCP recall block) — the never-expires fix for DF1. A
|
|
2012
|
+
* snapshot written by `hippo pre-compact` has no death path tied to the
|
|
2013
|
+
* session that owns it, so an orphaned row would otherwise inject into
|
|
2014
|
+
* every prompt of every later session forever. Wraps `loadActiveTaskSnapshot`
|
|
2015
|
+
* (unchanged, still the source of truth for explicit continuity surfaces),
|
|
2016
|
+
* then applies, in order:
|
|
2017
|
+
*
|
|
2018
|
+
* 1. Owner match — ONLY when both `opts.sessionId` and the snapshot's
|
|
2019
|
+
* `session_id` are non-null, non-empty strings and strictly equal
|
|
2020
|
+
* (`===`). Owner reads are unbounded: the session that owns the snapshot
|
|
2021
|
+
* can always see its own working state, regardless of age.
|
|
2022
|
+
* 2. Age check — everything else, including absent-vs-absent ids. A
|
|
2023
|
+
* null/undefined/empty id on EITHER side never counts as an owner match;
|
|
2024
|
+
* it falls through here instead. (`runPreCompact` can legitimately save a
|
|
2025
|
+
* snapshot with `session_id = null`; a null-equals-null "match" would
|
|
2026
|
+
* reopen indefinite ambient injection for exactly those rows.) Returns
|
|
2027
|
+
* the snapshot only when `age(updated_at) <= maxAgeMs` (default
|
|
2028
|
+
* `SNAPSHOT_AMBIENT_MAX_AGE_MS`); otherwise null.
|
|
2029
|
+
*
|
|
2030
|
+
* No SQL change — age derives from the existing `updated_at` column.
|
|
2031
|
+
*/
|
|
2032
|
+
export function loadFreshActiveTaskSnapshot(hippoRoot, tenantId, opts = {}) {
|
|
2033
|
+
const snapshot = loadActiveTaskSnapshot(hippoRoot, tenantId);
|
|
2034
|
+
if (!snapshot)
|
|
2035
|
+
return null;
|
|
2036
|
+
const callerSessionId = opts.sessionId;
|
|
2037
|
+
const isOwnerMatch = isNonEmptySessionId(callerSessionId) &&
|
|
2038
|
+
isNonEmptySessionId(snapshot.session_id) &&
|
|
2039
|
+
callerSessionId === snapshot.session_id;
|
|
2040
|
+
if (isOwnerMatch)
|
|
2041
|
+
return snapshot;
|
|
2042
|
+
const maxAgeMs = opts.maxAgeMs ?? SNAPSHOT_AMBIENT_MAX_AGE_MS;
|
|
2043
|
+
const ageMs = Date.now() - Date.parse(snapshot.updated_at);
|
|
2044
|
+
return ageMs <= maxAgeMs ? snapshot : null;
|
|
2045
|
+
}
|
|
1928
2046
|
export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cleared') {
|
|
1929
2047
|
assertTenantId('clearActiveTaskSnapshot', tenantId);
|
|
1930
2048
|
initStore(hippoRoot);
|
|
1931
2049
|
const db = openHippoDb(hippoRoot);
|
|
1932
2050
|
const now = new Date().toISOString();
|
|
1933
2051
|
try {
|
|
2052
|
+
// SAFETY: active's shape matches the single `id` column selected above.
|
|
1934
2053
|
const active = db.prepare(`SELECT id FROM task_snapshots WHERE status = 'active' AND tenant_id = ? ORDER BY updated_at DESC, id DESC LIMIT 1`).get(tenantId);
|
|
1935
2054
|
if (!active?.id) {
|
|
1936
2055
|
removeActiveTaskMirror(hippoRoot, tenantId);
|
|
@@ -1944,6 +2063,28 @@ export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cl
|
|
|
1944
2063
|
closeHippoDb(db);
|
|
1945
2064
|
}
|
|
1946
2065
|
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Close the `active` task snapshot(s) owned by `sessionId`, for the T3
|
|
2068
|
+
* session-end death path (DF1, docs/plans/2026-08-23-df1-snapshot-lifecycle.md).
|
|
2069
|
+
* Only one `active` row exists per tenant in practice (supersession happens
|
|
2070
|
+
* at save), but the WHERE clause scopes on `session_id` too — not just
|
|
2071
|
+
* `status='active' AND tenant_id=?` — so an ending session can never close a
|
|
2072
|
+
* different, newer session's active snapshot. Returns the number of rows
|
|
2073
|
+
* closed (0 when no active row is owned by `sessionId`).
|
|
2074
|
+
*/
|
|
2075
|
+
export function closeTaskSnapshotsForSession(hippoRoot, tenantId, sessionId, status = 'session-ended') {
|
|
2076
|
+
assertTenantId('closeTaskSnapshotsForSession', tenantId);
|
|
2077
|
+
initStore(hippoRoot);
|
|
2078
|
+
const db = openHippoDb(hippoRoot);
|
|
2079
|
+
const now = new Date().toISOString();
|
|
2080
|
+
try {
|
|
2081
|
+
const result = db.prepare(`UPDATE task_snapshots SET status = ?, updated_at = ? WHERE status = 'active' AND tenant_id = ? AND session_id = ?`).run(status, now, tenantId, sessionId);
|
|
2082
|
+
return Number(result.changes ?? 0);
|
|
2083
|
+
}
|
|
2084
|
+
finally {
|
|
2085
|
+
closeHippoDb(db);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
1947
2088
|
export function appendSessionEvent(hippoRoot, tenantId, event) {
|
|
1948
2089
|
assertTenantId('appendSessionEvent', tenantId);
|
|
1949
2090
|
initStore(hippoRoot);
|
|
@@ -1957,6 +2098,8 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
|
|
|
1957
2098
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1958
2099
|
`).run(event.session_id, event.task ?? null, event.event_type, event.content, event.source ?? 'cli', event.scope ?? null, JSON.stringify(event.metadata ?? {}), tenantId, now);
|
|
1959
2100
|
const id = Number(result.lastInsertRowid ?? 0);
|
|
2101
|
+
// SAFETY: row's shape matches the nine columns named in the SELECT
|
|
2102
|
+
// above.
|
|
1960
2103
|
const row = db.prepare(`
|
|
1961
2104
|
SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
|
|
1962
2105
|
FROM session_events
|
|
@@ -1966,6 +2109,8 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
|
|
|
1966
2109
|
throw new Error('Failed to reload saved session event');
|
|
1967
2110
|
}
|
|
1968
2111
|
const loaded = rowToSessionEvent(row);
|
|
2112
|
+
// SAFETY: recentRows' shape matches the nine columns named in the
|
|
2113
|
+
// SELECT above.
|
|
1969
2114
|
const recentRows = db.prepare(`
|
|
1970
2115
|
SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
|
|
1971
2116
|
FROM session_events
|
|
@@ -1999,6 +2144,8 @@ export function listSessionEvents(hippoRoot, tenantId, options = {}) {
|
|
|
1999
2144
|
const limit = Math.max(1, Math.trunc(options.limit ?? 8));
|
|
2000
2145
|
params.push(limit);
|
|
2001
2146
|
const where = `WHERE ${clauses.join(' AND ')}`;
|
|
2147
|
+
// SAFETY: rows' shape matches the nine columns named in the SELECT
|
|
2148
|
+
// above.
|
|
2002
2149
|
const rows = db.prepare(`
|
|
2003
2150
|
SELECT id, session_id, task, event_type, content, source, scope, metadata_json, created_at
|
|
2004
2151
|
FROM session_events
|
|
@@ -2021,6 +2168,8 @@ export function findPromotableSessions(hippoRoot, tenantId, sinceMs) {
|
|
|
2021
2168
|
initStore(hippoRoot);
|
|
2022
2169
|
const db = openHippoDb(hippoRoot);
|
|
2023
2170
|
try {
|
|
2171
|
+
// SAFETY: rows' shape matches the single `session_id` column selected
|
|
2172
|
+
// above.
|
|
2024
2173
|
const rows = db.prepare(`
|
|
2025
2174
|
SELECT DISTINCT session_id FROM session_events
|
|
2026
2175
|
WHERE event_type = 'session_complete' AND created_at >= ? AND tenant_id = ?
|
|
@@ -2065,6 +2214,8 @@ export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
|
|
|
2065
2214
|
// Tenanted query — JOIN to memories on both conflict members and require
|
|
2066
2215
|
// each in-tenant, so neither a normal cross-tenant pair nor a stale
|
|
2067
2216
|
// pre-fix row surfaces (consistent with resolveConflict).
|
|
2217
|
+
// SAFETY: both branches select the same eight mc.* columns (aliased
|
|
2218
|
+
// to MemoryConflictRow's field names) from memory_conflicts.
|
|
2068
2219
|
rows = allStatuses
|
|
2069
2220
|
? db.prepare(`
|
|
2070
2221
|
SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
|
|
@@ -2087,6 +2238,8 @@ export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
|
|
|
2087
2238
|
}
|
|
2088
2239
|
else {
|
|
2089
2240
|
// Unscoped query — legacy direct-mode (CLI, tests, consolidate).
|
|
2241
|
+
// SAFETY: both branches select the same eight columns matching
|
|
2242
|
+
// MemoryConflictRow's field set.
|
|
2090
2243
|
rows = allStatuses
|
|
2091
2244
|
? db.prepare(`
|
|
2092
2245
|
SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
|
|
@@ -2116,6 +2269,7 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
|
|
|
2116
2269
|
// inserting rows and when rebuilding conflicts_with_json, so a stale
|
|
2117
2270
|
// cross-tenant row can neither persist nor leak a foreign id.
|
|
2118
2271
|
const tenantById = new Map();
|
|
2272
|
+
// SAFETY: rows' shape matches the two columns named in the SELECT below.
|
|
2119
2273
|
for (const r of db.prepare(`SELECT id, tenant_id FROM memories`).all()) {
|
|
2120
2274
|
tenantById.set(r.id, r.tenant_id);
|
|
2121
2275
|
}
|
|
@@ -2130,6 +2284,8 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
|
|
|
2130
2284
|
score: conflict.score,
|
|
2131
2285
|
}));
|
|
2132
2286
|
const detectedKeys = new Set(canonicalDetected.map((conflict) => `${conflict.memory_a_id}::${conflict.memory_b_id}`));
|
|
2287
|
+
// SAFETY: openRows' shape matches the eight columns named in the SELECT
|
|
2288
|
+
// above.
|
|
2133
2289
|
const openRows = db.prepare(`
|
|
2134
2290
|
SELECT id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at
|
|
2135
2291
|
FROM memory_conflicts
|
|
@@ -2162,6 +2318,7 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
|
|
|
2162
2318
|
updated_at = excluded.updated_at
|
|
2163
2319
|
`).run(conflict.memory_a_id, conflict.memory_b_id, conflict.reason, conflict.score, detectedAt, detectedAt);
|
|
2164
2320
|
}
|
|
2321
|
+
// SAFETY: openConflicts' shape matches the two columns named above.
|
|
2165
2322
|
const openConflicts = db.prepare(`
|
|
2166
2323
|
SELECT memory_a_id, memory_b_id
|
|
2167
2324
|
FROM memory_conflicts
|
|
@@ -2180,6 +2337,8 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
|
|
|
2180
2337
|
refMap.get(row.memory_a_id).add(row.memory_b_id);
|
|
2181
2338
|
refMap.get(row.memory_b_id).add(row.memory_a_id);
|
|
2182
2339
|
}
|
|
2340
|
+
// SAFETY: memoryRows' shape matches the single `id` column selected
|
|
2341
|
+
// above.
|
|
2183
2342
|
const memoryRows = db.prepare(`SELECT id FROM memories`).all();
|
|
2184
2343
|
for (const memory of memoryRows) {
|
|
2185
2344
|
const refs = Array.from(refMap.get(memory.id) ?? []).sort();
|
|
@@ -2226,6 +2385,8 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
2226
2385
|
const memScope = tenantId !== undefined ? ' AND tenant_id = ?' : '';
|
|
2227
2386
|
const memArgs = tenantId !== undefined ? [tenantId] : [];
|
|
2228
2387
|
try {
|
|
2388
|
+
// SAFETY: both branches select the same eight columns (aliased in the
|
|
2389
|
+
// tenanted branch) matching MemoryConflictRow's field set.
|
|
2229
2390
|
const row = (tenantId !== undefined
|
|
2230
2391
|
? db.prepare(`
|
|
2231
2392
|
SELECT mc.id, mc.memory_a_id, mc.memory_b_id, mc.reason, mc.score,
|
|
@@ -2273,6 +2434,8 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
2273
2434
|
const extraRemovedRawIds = [];
|
|
2274
2435
|
const removeLoser = forgetLoser || opts?.rejectLoserValue === true;
|
|
2275
2436
|
if (removeLoser) {
|
|
2437
|
+
// SAFETY: loserRow's shape matches the three columns named in the
|
|
2438
|
+
// SELECT above.
|
|
2276
2439
|
const loserRow = db
|
|
2277
2440
|
.prepare(`SELECT kind, content, tenant_id FROM memories WHERE id = ?${memScope}`)
|
|
2278
2441
|
.get(loserId, ...memArgs);
|
|
@@ -2303,6 +2466,8 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
2303
2466
|
// to keep it in this same resolution, and this branch must not
|
|
2304
2467
|
// undo that choice in the same transaction.
|
|
2305
2468
|
const loserTenantId = loserRow.tenant_id ?? 'default';
|
|
2469
|
+
// SAFETY: dupRows' shape matches the three columns named in the
|
|
2470
|
+
// SELECT above.
|
|
2306
2471
|
const dupRows = db
|
|
2307
2472
|
.prepare(`SELECT id, kind, content FROM memories WHERE tenant_id = ? AND id != ? AND id != ?`)
|
|
2308
2473
|
.all(loserTenantId, loserId, keepId);
|
|
@@ -2337,6 +2502,8 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
2337
2502
|
.run(loserId, ...memArgs);
|
|
2338
2503
|
}
|
|
2339
2504
|
// Clean up conflicts_with references
|
|
2505
|
+
// SAFETY: keepRow's shape matches the single `conflicts_with_json`
|
|
2506
|
+
// column selected above.
|
|
2340
2507
|
const keepRow = db.prepare(`SELECT conflicts_with_json FROM memories WHERE id = ?${memScope}`).get(keepId, ...memArgs);
|
|
2341
2508
|
if (keepRow) {
|
|
2342
2509
|
const refs = JSON.parse(keepRow.conflicts_with_json || '[]');
|
|
@@ -2345,6 +2512,8 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
2345
2512
|
.run(JSON.stringify(cleaned), keepId, ...memArgs);
|
|
2346
2513
|
}
|
|
2347
2514
|
if (!loserRemoved) {
|
|
2515
|
+
// SAFETY: loserRow's shape matches the single `conflicts_with_json`
|
|
2516
|
+
// column named in the SELECT below.
|
|
2348
2517
|
const loserRow = db.prepare(`SELECT conflicts_with_json FROM memories WHERE id = ?${memScope}`).get(loserId, ...memArgs);
|
|
2349
2518
|
if (loserRow) {
|
|
2350
2519
|
const refs = JSON.parse(loserRow.conflicts_with_json || '[]');
|
|
@@ -2356,18 +2525,26 @@ export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = fal
|
|
|
2356
2525
|
// AT1: the missing audit (plan §5 — resolveConflict wrote ZERO audit_log
|
|
2357
2526
|
// rows on any path before this). Every path — weaken, forget, reject —
|
|
2358
2527
|
// lands exactly one conflict_resolve row.
|
|
2359
|
-
|
|
2528
|
+
const conflictResolveMeta = {
|
|
2360
2529
|
conflictId,
|
|
2361
2530
|
keepId,
|
|
2362
2531
|
loserId,
|
|
2363
2532
|
disposition: loserRemoved ? (loserWasRaw ? 'archived_raw' : 'deleted') : 'weakened',
|
|
2364
2533
|
rejected: Boolean(opts?.rejectLoserValue),
|
|
2365
|
-
rejectedDigest,
|
|
2366
2534
|
// AT1 P1 fix: every row this call removed, not just loserId — the
|
|
2367
2535
|
// same-tenant duplicate sweep above (extraRemovedIds) needs an
|
|
2368
2536
|
// audit trail too.
|
|
2369
2537
|
removedIds: loserRemoved ? [loserId, ...extraRemovedIds] : [],
|
|
2370
|
-
}
|
|
2538
|
+
};
|
|
2539
|
+
// Assigned only when present so the serialized audit payload keeps
|
|
2540
|
+
// omitting the key, exactly as the pre-migration object literal did.
|
|
2541
|
+
if (rejectedDigest !== undefined)
|
|
2542
|
+
conflictResolveMeta.rejectedDigest = rejectedDigest;
|
|
2543
|
+
// Fresh spread literal: ConflictResolveMeta is a closed interface (no
|
|
2544
|
+
// index signature) and isn't directly assignable to audit()'s
|
|
2545
|
+
// Record<string, JsonValue> metadata param; a spread into a fresh
|
|
2546
|
+
// object literal satisfies it without widening the declared type above.
|
|
2547
|
+
audit(db, 'conflict_resolve', keepId, { ...conflictResolveMeta }, opts?.rejectedBy ?? 'cli', tenantId);
|
|
2371
2548
|
db.exec('COMMIT');
|
|
2372
2549
|
syncMirrorFiles(hippoRoot, db);
|
|
2373
2550
|
// AT1 P1b fix: mirror purge + reaper stamp for EVERY removed loser, not
|
|
@@ -2431,6 +2608,8 @@ export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
|
|
|
2431
2608
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2432
2609
|
`).run(handoff.sessionId, handoff.repoRoot ?? null, handoff.taskId ?? null, handoff.summary, handoff.nextAction ?? null, JSON.stringify(handoff.artifacts ?? []), handoff.scope ?? null, tenantId, now);
|
|
2433
2610
|
const id = Number(result.lastInsertRowid ?? 0);
|
|
2611
|
+
// SAFETY: row's shape matches the nine columns named in the SELECT
|
|
2612
|
+
// above.
|
|
2434
2613
|
const row = db.prepare(`
|
|
2435
2614
|
SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
|
|
2436
2615
|
FROM session_handoffs
|
|
@@ -2455,6 +2634,8 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId) {
|
|
|
2455
2634
|
try {
|
|
2456
2635
|
let row;
|
|
2457
2636
|
if (sessionId) {
|
|
2637
|
+
// SAFETY: row's shape matches the nine columns named in the SELECT
|
|
2638
|
+
// below.
|
|
2458
2639
|
row = db.prepare(`
|
|
2459
2640
|
SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
|
|
2460
2641
|
FROM session_handoffs
|
|
@@ -2464,6 +2645,8 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId) {
|
|
|
2464
2645
|
`).get(sessionId, tenantId);
|
|
2465
2646
|
}
|
|
2466
2647
|
else {
|
|
2648
|
+
// SAFETY: row's shape matches the nine columns named in the SELECT
|
|
2649
|
+
// below.
|
|
2467
2650
|
row = db.prepare(`
|
|
2468
2651
|
SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
|
|
2469
2652
|
FROM session_handoffs
|
|
@@ -2486,6 +2669,8 @@ export function loadHandoffById(hippoRoot, tenantId, id) {
|
|
|
2486
2669
|
initStore(hippoRoot);
|
|
2487
2670
|
const db = openHippoDb(hippoRoot);
|
|
2488
2671
|
try {
|
|
2672
|
+
// SAFETY: row's shape matches the nine columns named in the SELECT
|
|
2673
|
+
// above.
|
|
2489
2674
|
const row = db.prepare(`
|
|
2490
2675
|
SELECT id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at
|
|
2491
2676
|
FROM session_handoffs
|
|
@@ -2519,6 +2704,8 @@ export function loadDirtySummaries(hippoRoot, tenantId) {
|
|
|
2519
2704
|
initStore(hippoRoot);
|
|
2520
2705
|
const db = openHippoDb(hippoRoot);
|
|
2521
2706
|
try {
|
|
2707
|
+
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
2708
|
+
// MemoryRow's field set.
|
|
2522
2709
|
const rows = db.prepare(`
|
|
2523
2710
|
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
2524
2711
|
FROM memories
|
|
@@ -2553,6 +2740,8 @@ export function markSummaryDirtyInTx(db, summaryId, tenantId, actor) {
|
|
|
2553
2740
|
// v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
|
|
2554
2741
|
// actual level in same round trip so audit metadata stays accurate without
|
|
2555
2742
|
// a SELECT-before-UPDATE extra DB op on this hot path (5 caller sites).
|
|
2743
|
+
// SAFETY: result's shape matches the single `dag_level` column returned
|
|
2744
|
+
// above.
|
|
2556
2745
|
const result = db.prepare(`
|
|
2557
2746
|
UPDATE memories
|
|
2558
2747
|
SET summary_dirty = 1
|
|
@@ -2587,6 +2776,8 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
|
|
|
2587
2776
|
try {
|
|
2588
2777
|
// v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
|
|
2589
2778
|
// actual level in same round trip.
|
|
2779
|
+
// SAFETY: result's shape matches the single `dag_level` column returned
|
|
2780
|
+
// above.
|
|
2590
2781
|
const result = db.prepare(`
|
|
2591
2782
|
UPDATE memories
|
|
2592
2783
|
SET summary_dirty = 1
|
|
@@ -2631,6 +2822,8 @@ export function loadAllL2Summaries(hippoRoot) {
|
|
|
2631
2822
|
initStore(hippoRoot);
|
|
2632
2823
|
const db = openHippoDb(hippoRoot);
|
|
2633
2824
|
try {
|
|
2825
|
+
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
2826
|
+
// MemoryRow's field set.
|
|
2634
2827
|
const rows = db.prepare(`
|
|
2635
2828
|
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
2636
2829
|
FROM memories
|
|
@@ -2658,6 +2851,8 @@ export function loadAllDirtySummaries(hippoRoot) {
|
|
|
2658
2851
|
initStore(hippoRoot);
|
|
2659
2852
|
const db = openHippoDb(hippoRoot);
|
|
2660
2853
|
try {
|
|
2854
|
+
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
2855
|
+
// MemoryRow's field set.
|
|
2661
2856
|
const rows = db.prepare(`
|
|
2662
2857
|
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
2663
2858
|
FROM memories
|
|
@@ -2683,6 +2878,8 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
|
|
|
2683
2878
|
initStore(hippoRoot);
|
|
2684
2879
|
const db = openHippoDb(hippoRoot);
|
|
2685
2880
|
try {
|
|
2881
|
+
// SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
|
|
2882
|
+
// MemoryRow's field set.
|
|
2686
2883
|
const rows = db.prepare(`
|
|
2687
2884
|
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
2688
2885
|
FROM memories
|
|
@@ -2864,6 +3061,8 @@ export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, acto
|
|
|
2864
3061
|
try {
|
|
2865
3062
|
// v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
|
|
2866
3063
|
// actual level so audit metadata stays accurate without an extra SELECT.
|
|
3064
|
+
// SAFETY: result's shape matches the single `dag_level` column returned
|
|
3065
|
+
// below.
|
|
2867
3066
|
const result = db.prepare(`
|
|
2868
3067
|
UPDATE memories
|
|
2869
3068
|
SET summary_dirty = 0
|