hippo-memory 1.63.1 → 1.64.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.
Files changed (275) hide show
  1. package/README.md +13 -5
  2. package/dist/agent-memories/claude-code.js +3 -3
  3. package/dist/ambient-store.d.ts +2 -0
  4. package/dist/ambient-store.js +4 -3
  5. package/dist/api/assemble.d.ts +2 -0
  6. package/dist/api/assemble.js +11 -7
  7. package/dist/api/auth.d.ts +15 -11
  8. package/dist/api/auth.js +116 -22
  9. package/dist/api/context-select.d.ts +5 -3
  10. package/dist/api/context-select.js +6 -11
  11. package/dist/api/context-types.d.ts +2 -1
  12. package/dist/api/context.d.ts +1 -1
  13. package/dist/api/context.js +79 -55
  14. package/dist/api/dormant.d.ts +5 -5
  15. package/dist/api/dormant.js +13 -9
  16. package/dist/api/drill-down.d.ts +2 -0
  17. package/dist/api/drill-down.js +12 -7
  18. package/dist/api/forget.d.ts +1 -1
  19. package/dist/api/forget.js +8 -4
  20. package/dist/api/learn.js +5 -3
  21. package/dist/api/outcome.d.ts +1 -1
  22. package/dist/api/outcome.js +2 -1
  23. package/dist/api/promote.js +8 -5
  24. package/dist/api/recall-types.d.ts +4 -2
  25. package/dist/api/recall.js +51 -36
  26. package/dist/api/remember.d.ts +7 -0
  27. package/dist/api/remember.js +26 -2
  28. package/dist/api/sleep-run.d.ts +28 -0
  29. package/dist/api/sleep-run.js +267 -0
  30. package/dist/api/sleep.d.ts +5 -45
  31. package/dist/api/sleep.js +5 -252
  32. package/dist/api/types.d.ts +5 -0
  33. package/dist/api/types.js +4 -0
  34. package/dist/audit-prune.js +1 -1
  35. package/dist/audit.d.ts +1 -3
  36. package/dist/audit.js +7 -103
  37. package/dist/auth.d.ts +48 -4
  38. package/dist/auth.js +111 -42
  39. package/dist/autolearn.js +2 -2
  40. package/dist/capture/caller-session.d.ts +16 -0
  41. package/dist/capture/caller-session.js +58 -0
  42. package/dist/capture/command.d.ts +19 -1
  43. package/dist/capture/command.js +28 -21
  44. package/dist/capture/compact-resume-caller.d.ts +11 -0
  45. package/dist/capture/compact-resume-caller.js +35 -0
  46. package/dist/capture/compact.d.ts +0 -14
  47. package/dist/capture/compact.js +7 -160
  48. package/dist/capture/compaction-items-caller.d.ts +16 -0
  49. package/dist/capture/compaction-items-caller.js +32 -0
  50. package/dist/capture/extract.d.ts +2 -14
  51. package/dist/capture/extract.js +135 -346
  52. package/dist/capture/failure-caller.d.ts +21 -0
  53. package/dist/capture/failure-caller.js +74 -0
  54. package/dist/capture/failure-reading.d.ts +44 -0
  55. package/dist/capture/failure-reading.js +78 -0
  56. package/dist/capture/pre-compact-caller.d.ts +16 -0
  57. package/dist/capture/pre-compact-caller.js +39 -0
  58. package/dist/capture/session-end-caller.d.ts +21 -0
  59. package/dist/capture/session-end-caller.js +30 -0
  60. package/dist/capture/session-texts.d.ts +14 -0
  61. package/dist/capture/session-texts.js +42 -0
  62. package/dist/capture/transcript.d.ts +6 -1
  63. package/dist/capture/transcript.js +20 -16
  64. package/dist/capture/working-state.d.ts +21 -0
  65. package/dist/capture/working-state.js +154 -0
  66. package/dist/capture-error.d.ts +8 -22
  67. package/dist/capture-error.js +9 -61
  68. package/dist/cli/audit.js +25 -0
  69. package/dist/cli/auth.js +4 -3
  70. package/dist/cli/context.js +13 -158
  71. package/dist/cli/continuity.js +2 -1
  72. package/dist/cli/init.js +5 -3
  73. package/dist/cli/last-sleep.d.ts +6 -0
  74. package/dist/cli/last-sleep.js +68 -0
  75. package/dist/cli/quality-repair-once.d.ts +3 -0
  76. package/dist/cli/quality-repair-once.js +36 -0
  77. package/dist/cli/recall.js +2 -1
  78. package/dist/cli/remember.d.ts +2 -0
  79. package/dist/cli/remember.js +4 -1
  80. package/dist/cli/session-hooks.d.ts +0 -3
  81. package/dist/cli/session-hooks.js +12 -49
  82. package/dist/cli/setup.js +10 -2
  83. package/dist/cli/shared.d.ts +7 -14
  84. package/dist/cli/shared.js +19 -78
  85. package/dist/cli/sleep.js +6 -3
  86. package/dist/cli/usage.js +18 -9
  87. package/dist/cli.d.ts +1 -1
  88. package/dist/cli.js +3 -2
  89. package/dist/client.js +1 -1
  90. package/dist/compaction-record.d.ts +19 -1
  91. package/dist/compaction-record.js +61 -132
  92. package/dist/compaction-spool.d.ts +45 -0
  93. package/dist/compaction-spool.js +454 -0
  94. package/dist/config.d.ts +8 -0
  95. package/dist/config.js +49 -0
  96. package/dist/connectors/github/deletion.d.ts +1 -1
  97. package/dist/connectors/github/deletion.js +5 -17
  98. package/dist/consolidate/conflicts.js +4 -0
  99. package/dist/consolidate/flush-units.d.ts +9 -0
  100. package/dist/consolidate/flush-units.js +63 -0
  101. package/dist/consolidate/llm-passes.js +5 -4
  102. package/dist/consolidate/merge.js +5 -1
  103. package/dist/consolidate/run.d.ts +2 -1
  104. package/dist/consolidate/sleep.d.ts +2 -0
  105. package/dist/consolidate/sleep.js +82 -15
  106. package/dist/context-render.d.ts +14 -0
  107. package/dist/context-render.js +28 -0
  108. package/dist/dag.js +6 -0
  109. package/dist/dashboard-actions.js +3 -2
  110. package/dist/dashboard.js +4 -3
  111. package/dist/db/backup.d.ts +6 -0
  112. package/dist/db/backup.js +29 -0
  113. package/dist/db/busy.d.ts +9 -0
  114. package/dist/db/busy.js +35 -0
  115. package/dist/db/continuity.d.ts +1 -0
  116. package/dist/db/continuity.js +31 -4
  117. package/dist/db/meta.d.ts +2 -0
  118. package/dist/db/meta.js +7 -0
  119. package/dist/db/migrate.js +2 -3
  120. package/dist/db/migrations/index.d.ts +1 -1
  121. package/dist/db/migrations/index.js +5 -1
  122. package/dist/db/migrations/v39.js +2 -7
  123. package/dist/db/migrations/v53.d.ts +3 -0
  124. package/dist/db/migrations/v53.js +16 -0
  125. package/dist/db/migrations/v54.d.ts +3 -0
  126. package/dist/db/migrations/v54.js +25 -0
  127. package/dist/db/open.d.ts +11 -0
  128. package/dist/db/open.js +27 -0
  129. package/dist/db/tables.d.ts +2 -0
  130. package/dist/db/tables.js +6 -2
  131. package/dist/db.d.ts +2 -2
  132. package/dist/db.js +2 -2
  133. package/dist/doctor.d.ts +1 -1
  134. package/dist/doctor.js +31 -15
  135. package/dist/dormant.d.ts +16 -20
  136. package/dist/dormant.js +24 -30
  137. package/dist/entry/json-hooks.d.ts +3 -0
  138. package/dist/entry/json-hooks.js +4 -0
  139. package/dist/entry/project-identity.d.ts +2 -0
  140. package/dist/entry/project-identity.js +3 -0
  141. package/dist/entry/session-text.d.ts +7 -0
  142. package/dist/entry/session-text.js +8 -0
  143. package/dist/extract.js +6 -0
  144. package/dist/failure-log.d.ts +10 -2
  145. package/dist/failure-log.js +19 -2
  146. package/dist/gated-write.js +1 -1
  147. package/dist/graph/delta.d.ts +44 -0
  148. package/dist/graph/delta.js +109 -0
  149. package/dist/graph/types.d.ts +5 -0
  150. package/dist/graph/write.d.ts +20 -20
  151. package/dist/graph/write.js +103 -36
  152. package/dist/graph-extract.d.ts +43 -9
  153. package/dist/graph-extract.js +140 -157
  154. package/dist/graph-recall.d.ts +1 -0
  155. package/dist/graph-recall.js +2 -2
  156. package/dist/graph-view.d.ts +1 -0
  157. package/dist/graph-view.js +25 -2
  158. package/dist/half-life-migration.js +5 -1
  159. package/dist/handoff-evidence.d.ts +5 -0
  160. package/dist/handoff-evidence.js +30 -0
  161. package/dist/home-path.d.ts +5 -0
  162. package/dist/home-path.js +24 -0
  163. package/dist/hooks/json-hooks.d.ts +13 -2
  164. package/dist/hooks/json-hooks.js +246 -117
  165. package/dist/hooks/opencode.js +1 -1
  166. package/dist/hooks/shared.d.ts +2 -0
  167. package/dist/hooks/shared.js +6 -1
  168. package/dist/http-util.d.ts +16 -7
  169. package/dist/http-util.js +65 -20
  170. package/dist/importers/vault.js +2 -0
  171. package/dist/index.d.ts +2 -1
  172. package/dist/index.js +1 -0
  173. package/dist/json.d.ts +2 -0
  174. package/dist/json.js +7 -0
  175. package/dist/ledger-db.d.ts +10 -0
  176. package/dist/ledger-db.js +42 -0
  177. package/dist/mcp/admin-tools.d.ts +2 -2
  178. package/dist/mcp/admin-tools.js +47 -5
  179. package/dist/mcp/memory-tools.js +7 -2
  180. package/dist/mcp/protocol.d.ts +6 -0
  181. package/dist/mcp/protocol.js +4 -2
  182. package/dist/mcp/recall-tools.js +19 -6
  183. package/dist/mcp/request.js +4 -0
  184. package/dist/mcp/session-state.d.ts +4 -0
  185. package/dist/mcp/session-state.js +15 -6
  186. package/dist/mcp/shared-gate.d.ts +4 -0
  187. package/dist/mcp/shared-gate.js +22 -0
  188. package/dist/mcp/tools.js +1 -0
  189. package/dist/memory-quality.d.ts +35 -0
  190. package/dist/memory-quality.js +191 -0
  191. package/dist/memory.d.ts +4 -4
  192. package/dist/physics-state.js +1 -1
  193. package/dist/pilot-arm.d.ts +14 -3
  194. package/dist/pilot-arm.js +32 -10
  195. package/dist/postinstall.js +2 -2
  196. package/dist/project-briefs.js +4 -4
  197. package/dist/project-identity.d.ts +14 -10
  198. package/dist/project-identity.js +29 -11
  199. package/dist/project-merge.d.ts +0 -2
  200. package/dist/project-merge.js +9 -13
  201. package/dist/prompt-hook.d.ts +65 -0
  202. package/dist/prompt-hook.js +213 -0
  203. package/dist/prompt-recall.js +1 -1
  204. package/dist/quality-repair.d.ts +27 -0
  205. package/dist/quality-repair.js +220 -0
  206. package/dist/rate-limit.d.ts +1 -0
  207. package/dist/rate-limit.js +5 -3
  208. package/dist/raw-archive.d.ts +1 -1
  209. package/dist/raw-archive.js +6 -16
  210. package/dist/recall-scope.d.ts +28 -5
  211. package/dist/recall-scope.js +52 -7
  212. package/dist/recall-trace.js +1 -1
  213. package/dist/reject-flow.d.ts +2 -0
  214. package/dist/reject-flow.js +20 -6
  215. package/dist/secret-detect.js +7 -8
  216. package/dist/server/auth.d.ts +8 -6
  217. package/dist/server/auth.js +85 -38
  218. package/dist/server/client-ip.d.ts +2 -0
  219. package/dist/server/client-ip.js +16 -1
  220. package/dist/server/mcp-http.d.ts +9 -3
  221. package/dist/server/mcp-http.js +56 -13
  222. package/dist/server/request.d.ts +1 -1
  223. package/dist/server/request.js +5 -5
  224. package/dist/server/routes/admin.js +12 -6
  225. package/dist/server/routes/memories.js +9 -1
  226. package/dist/server/routes/recall.js +14 -2
  227. package/dist/server/types.d.ts +42 -5
  228. package/dist/server/validation.d.ts +5 -2
  229. package/dist/server/validation.js +19 -2
  230. package/dist/server.d.ts +21 -4
  231. package/dist/server.js +207 -49
  232. package/dist/session-digest.d.ts +0 -1
  233. package/dist/session-digest.js +3 -12
  234. package/dist/session-owners.d.ts +7 -0
  235. package/dist/session-owners.js +54 -0
  236. package/dist/share-scrub.d.ts +3 -0
  237. package/dist/share-scrub.js +8 -0
  238. package/dist/shared.d.ts +1 -0
  239. package/dist/shared.js +26 -18
  240. package/dist/store/candidates.d.ts +14 -3
  241. package/dist/store/candidates.js +48 -26
  242. package/dist/store/conflicts.d.ts +15 -0
  243. package/dist/store/conflicts.js +68 -44
  244. package/dist/store/delete-and-batch.d.ts +25 -1
  245. package/dist/store/delete-and-batch.js +121 -69
  246. package/dist/store/entry-reads.d.ts +5 -6
  247. package/dist/store/entry-reads.js +24 -12
  248. package/dist/store/entry-row.d.ts +7 -10
  249. package/dist/store/entry-row.js +40 -18
  250. package/dist/store/entry-writes.d.ts +3 -3
  251. package/dist/store/entry-writes.js +9 -23
  252. package/dist/store/handoffs.d.ts +9 -6
  253. package/dist/store/handoffs.js +72 -59
  254. package/dist/store/index-and-stats.js +2 -2
  255. package/dist/store/open.d.ts +3 -1
  256. package/dist/store/open.js +3 -3
  257. package/dist/store/search-rows.d.ts +8 -5
  258. package/dist/store/search-rows.js +15 -18
  259. package/dist/store/sessions.d.ts +25 -6
  260. package/dist/store/sessions.js +61 -19
  261. package/dist/store/summaries.d.ts +1 -1
  262. package/dist/store/summaries.js +3 -18
  263. package/dist/store/tenant-lookup.d.ts +7 -3
  264. package/dist/store/tenant-lookup.js +9 -9
  265. package/dist/store-port.d.ts +13 -0
  266. package/dist/store-port.js +20 -0
  267. package/dist/version.d.ts +5 -1
  268. package/dist/version.js +5 -1
  269. package/dist/working-memory.js +1 -1
  270. package/dist/write-budget.d.ts +15 -0
  271. package/dist/write-budget.js +15 -0
  272. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  273. package/extensions/openclaw-plugin/package.json +1 -1
  274. package/openclaw.plugin.json +1 -1
  275. package/package.json +8 -4
@@ -0,0 +1,7 @@
1
+ export { collectSessionTurns, sessionTail, type SessionTurn } from '../capture/transcript.js';
2
+ export { scrubForSharing } from '../share-scrub.js';
3
+ export { transcriptWorkingState, WORKING_STATE_CAPS } from '../capture/working-state.js';
4
+ export { lessonFromFailure, failureReport, type FailureReport } from '../capture/failure-reading.js';
5
+ export { collectHandoffEvidence } from '../handoff-evidence.js';
6
+ export { compactSummaryBody, parseCompactionItems, COMPACTION_ITEM_MAX_CHARS, COMPACTION_ITEM_ROW_CAP } from '../compaction-items.js';
7
+ //# sourceMappingURL=session-text.d.ts.map
@@ -0,0 +1,8 @@
1
+ // The hippo-memory/session-text subpath: what a hook reads from a session (turns, working state, failures, git state, compaction items) and the sharing scrub, with no store, database or server in its imports.
2
+ export { collectSessionTurns, sessionTail } from '../capture/transcript.js';
3
+ export { scrubForSharing } from '../share-scrub.js';
4
+ export { transcriptWorkingState, WORKING_STATE_CAPS } from '../capture/working-state.js';
5
+ export { lessonFromFailure, failureReport } from '../capture/failure-reading.js';
6
+ export { collectHandoffEvidence } from '../handoff-evidence.js';
7
+ export { compactSummaryBody, parseCompactionItems, COMPACTION_ITEM_MAX_CHARS, COMPACTION_ITEM_ROW_CAP } from '../compaction-items.js';
8
+ //# sourceMappingURL=session-text.js.map
package/dist/extract.js CHANGED
@@ -7,6 +7,7 @@ import { fetchWithRetry, llmTimeoutMs } from './http-retry.js';
7
7
  import { neverAutoShareTags } from './shared.js';
8
8
  import { log } from './log.js';
9
9
  import { isJsonString } from './json.js';
10
+ import { certainDefect } from './memory-quality.js';
10
11
  const EXTRACTION_PROMPT = `You are extracting factual statements from a conversation or memory entry. Extract 1-8 standalone factual statements that would be useful to remember later.
11
12
 
12
13
  Rules:
@@ -90,6 +91,11 @@ export function storeExtractedFacts(hippoRoot, source, facts) {
90
91
  let rejected = 0;
91
92
  const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
92
93
  for (const fact of facts) {
94
+ const defect = certainDefect(fact.content);
95
+ if (defect !== null) {
96
+ log.warn(`storeExtractedFacts: skipped automatic quality defect (${defect})`);
97
+ continue;
98
+ }
93
99
  const tags = ['extracted', ...inheritedTags, ...fact.tags];
94
100
  const entry = { ...createMemory(fact.content, {
95
101
  layer: Layer.Semantic,
@@ -1,5 +1,4 @@
1
- /** Failure log: every failed tool call the capture-error hook sees, stored or not. */
2
- import type { CaptureErrorOutcome, RoutineRule } from './capture-error.js';
1
+ import type { CaptureErrorOutcome, RoutineRule } from './capture/failure-reading.js';
3
2
  import type { DatabaseSyncLike } from './db.js';
4
3
  /** Rows older than this are pruned on write, which also bounds how far back a repeat can be found. */
5
4
  export declare const FAILURE_LOG_RETENTION_DAYS = 90;
@@ -18,11 +17,20 @@ export interface FailureEvent {
18
17
  sigHash?: string | null;
19
18
  /** Hash of the untruncated error plus the command's first two words, finer than `sigHash`. */
20
19
  detailHash?: string | null;
20
+ /** A shared-store caller's owner and project; null on a local store. */
21
+ ownerSubject?: string | null;
22
+ originProject?: string | null;
23
+ /** The client's queue record id, so a retried send finds the earlier row. */
24
+ requestId?: string | null;
21
25
  /** Override the timestamp (tests). ISO string. */
22
26
  now?: string;
23
27
  }
24
28
  /** Append one failure row and prune rows past {@link FAILURE_LOG_RETENTION_DAYS}. */
25
29
  export declare function recordFailure(db: DatabaseSyncLike, event: FailureEvent): void;
30
+ /** The outcome logged for a caller's request id, or null, so a retried send finds the first one; another session's id is a ConflictError. */
31
+ export declare function requestOutcome(db: DatabaseSyncLike, tenantId: string, requestId: string, sessionId: string): FailureOutcome | null;
32
+ /** A retry that stored what the first try could not rewrites that row, since the request id allows one row per tenant. */
33
+ export declare function settleFailureOutcome(db: DatabaseSyncLike, tenantId: string, requestId: string, outcome: FailureOutcome): void;
26
34
  /** Rated failures and repeats in one session, for {@link failuresBySession}. */
27
35
  export interface SessionFailures {
28
36
  sessionId: string;
@@ -1,3 +1,5 @@
1
+ /** Failure log: every failed tool call the capture-error hook sees, stored or not. */
2
+ import { ConflictError } from './api-errors.js';
1
3
  /** Rows older than this are pruned on write, which also bounds how far back a repeat can be found. */
2
4
  export const FAILURE_LOG_RETENTION_DAYS = 90;
3
5
  /** Longest session id or tool name kept; the hook payload is not trusted to be short. */
@@ -6,11 +8,26 @@ const MAX_FIELD = 128;
6
8
  export function recordFailure(db, event) {
7
9
  // Normalised, because the window and prune compare timestamps as strings.
8
10
  const now = new Date(event.now ?? Date.now()).toISOString();
9
- db.prepare(`INSERT INTO failure_log (ts, tenant_id, session_id, tool, outcome, skip_rule, sig_hash, detail_hash)
10
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(now, event.tenantId, event.sessionId?.slice(0, MAX_FIELD) ?? null, event.tool?.slice(0, MAX_FIELD) ?? null, event.outcome, event.rule ?? null, event.sigHash ?? null, event.detailHash ?? null);
11
+ db.prepare(`INSERT INTO failure_log (ts, tenant_id, session_id, tool, outcome, skip_rule, sig_hash, detail_hash, owner_subject, origin_project, request_id)
12
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(now, event.tenantId, event.sessionId?.slice(0, MAX_FIELD) ?? null, event.tool?.slice(0, MAX_FIELD) ?? null, event.outcome, event.rule ?? null, event.sigHash ?? null, event.detailHash ?? null, event.ownerSubject ?? null, event.originProject ?? null, event.requestId ?? null);
11
13
  const cutoff = new Date(Date.parse(now) - FAILURE_LOG_RETENTION_DAYS * 86_400_000).toISOString();
12
14
  db.prepare(`DELETE FROM failure_log WHERE ts < ?`).run(cutoff);
13
15
  }
16
+ /** The outcome logged for a caller's request id, or null, so a retried send finds the first one; another session's id is a ConflictError. */
17
+ export function requestOutcome(db, tenantId, requestId, sessionId) {
18
+ const row = db.prepare(`SELECT outcome, session_id FROM failure_log WHERE tenant_id = ? AND request_id = ?`)
19
+ .get(tenantId, requestId);
20
+ if (row === undefined)
21
+ return null;
22
+ // Sessions are owner-bound, so this also keeps one owner from reading or settling another's row; compared as stored.
23
+ if (row.session_id !== sessionId.slice(0, MAX_FIELD))
24
+ throw new ConflictError('request id belongs to another session');
25
+ return row.outcome;
26
+ }
27
+ /** A retry that stored what the first try could not rewrites that row, since the request id allows one row per tenant. */
28
+ export function settleFailureOutcome(db, tenantId, requestId, outcome) {
29
+ db.prepare(`UPDATE failure_log SET outcome = ? WHERE tenant_id = ? AND request_id = ?`).run(outcome, tenantId, requestId);
30
+ }
14
31
  /** Rated failures per session since `sinceIso`, the input for repeat-error rate per arm. */
15
32
  export function failuresBySession(db, tenantId, sinceIso) {
16
33
  // SAFETY: the SELECT names exactly these three TEXT columns.
@@ -1,5 +1,5 @@
1
1
  // The one write path for text no person typed into hippo: capture, compaction items and imported agent memories.
2
- import { isContentWorthStoring } from './audit.js';
2
+ import { isContentWorthStoring } from './memory-quality.js';
3
3
  import { RejectedValueError } from './rejection.js';
4
4
  import { detectSecret } from './secret-detect.js';
5
5
  import { auditRejectionRefusal } from './store/audit-event.js';
@@ -0,0 +1,44 @@
1
+ import type { GraphTxDb, EntityType, RelationType, SourceObjectRef } from './types.js';
2
+ /** What names an object-derived entity across rebuilds; v38 has no UNIQUE on it, so the diff keeps one row per key. */
3
+ export interface NaturalKey {
4
+ readonly entityType: EntityType;
5
+ readonly sourceObject: SourceObjectRef;
6
+ }
7
+ export interface DesiredEntity extends NaturalKey {
8
+ readonly name: string;
9
+ readonly memoryId: string | null;
10
+ }
11
+ export interface DesiredRelation {
12
+ readonly from: NaturalKey;
13
+ readonly to: NaturalKey;
14
+ readonly relType: RelationType;
15
+ readonly memoryId: string | null;
16
+ readonly sourceObject: SourceObjectRef;
17
+ }
18
+ export interface DesiredGraph {
19
+ readonly entities: readonly DesiredEntity[];
20
+ readonly relations: readonly DesiredRelation[];
21
+ }
22
+ export type GraphOp = {
23
+ readonly op: 'deleteEntity';
24
+ readonly id: number;
25
+ } | {
26
+ readonly op: 'updateEntity';
27
+ readonly id: number;
28
+ readonly entity: DesiredEntity;
29
+ } | {
30
+ readonly op: 'insertEntity';
31
+ readonly entity: DesiredEntity;
32
+ } | {
33
+ readonly op: 'deleteRelation';
34
+ readonly id: number;
35
+ } | {
36
+ readonly op: 'insertRelation';
37
+ readonly relation: DesiredRelation;
38
+ };
39
+ export declare function entityKey(k: NaturalKey): string;
40
+ /** Ops that turn the stored graph into `desired`, in apply order; reads only, and keeps the lowest id per natural key. */
41
+ export declare function graphDelta(db: GraphTxDb, tenantId: string, desired: DesiredGraph): GraphOp[];
42
+ /** graphDelta on its own connection and outside any write lock; the apply re-checks each op against what changed since. */
43
+ export declare function readGraphDelta(hippoRoot: string, tenantId: string, desired: DesiredGraph): GraphOp[];
44
+ //# sourceMappingURL=delta.d.ts.map
@@ -0,0 +1,109 @@
1
+ // Diffs the derived graph against the stored rows, so a rebuild writes only what changed and an entity keeps the id its relations hang on.
2
+ import { openHippoDb, closeHippoDb } from '../db.js';
3
+ const keyText = (entityType, objectType, objectId) => `${entityType}|${objectType}:${objectId}`;
4
+ export function entityKey(k) {
5
+ return keyText(k.entityType, k.sourceObject.type, k.sourceObject.id);
6
+ }
7
+ function storedKey(row) {
8
+ if (row.source_object_type === null || row.source_object_id === null)
9
+ return null;
10
+ return keyText(row.entity_type, row.source_object_type, row.source_object_id);
11
+ }
12
+ const relationKey = (from, to, relType) => `${from}>${to}|${relType}`;
13
+ /** The (memory, kind) pair a write would store: a missing mirror leaves the object path, which is 'distilled'. */
14
+ function provenanceOf(memoryId, kinds) {
15
+ const kind = memoryId === null ? undefined : kinds.get(memoryId);
16
+ return kind === undefined ? '|distilled' : `${memoryId}|${kind}`;
17
+ }
18
+ function memoryKindsOf(db, desired) {
19
+ const ids = new Set();
20
+ for (const row of [...desired.entities, ...desired.relations])
21
+ if (row.memoryId !== null)
22
+ ids.add(row.memoryId);
23
+ if (ids.size === 0)
24
+ return new Map();
25
+ // SAFETY: the SELECT names exactly the two columns of the row type.
26
+ const rows = db.prepare(`SELECT id, kind FROM memories WHERE id IN (SELECT value FROM json_each(?))`)
27
+ .all(JSON.stringify([...ids]));
28
+ return new Map(rows.map((r) => [r.id, r.kind]));
29
+ }
30
+ function firstByKey(rows, keyOf) {
31
+ const out = new Map();
32
+ for (const row of rows) {
33
+ const key = keyOf(row);
34
+ if (!out.has(key))
35
+ out.set(key, row);
36
+ }
37
+ return out;
38
+ }
39
+ function diffEntities(stored, desired, kinds) {
40
+ const wanted = firstByKey(desired, entityKey);
41
+ const kept = new Map();
42
+ const deletes = [];
43
+ for (const row of stored) {
44
+ const key = storedKey(row);
45
+ if (key === null || kept.has(key) || !wanted.has(key))
46
+ deletes.push({ op: 'deleteEntity', id: row.id });
47
+ else
48
+ kept.set(key, row);
49
+ }
50
+ const updates = [];
51
+ const inserts = [];
52
+ for (const [key, entity] of wanted) {
53
+ const row = kept.get(key);
54
+ if (!row)
55
+ inserts.push({ op: 'insertEntity', entity });
56
+ else if (row.name !== entity.name || `${row.memory_id ?? ''}|${row.source_kind}` !== provenanceOf(entity.memoryId, kinds)) {
57
+ updates.push({ op: 'updateEntity', id: row.id, entity });
58
+ }
59
+ }
60
+ const keptKeyById = new Map([...kept].map(([key, row]) => [row.id, key]));
61
+ return { ops: [...deletes, ...updates, ...inserts], keptKeyById };
62
+ }
63
+ function diffRelations(stored, desired, keptKeyById, kinds) {
64
+ const wanted = firstByKey(desired, (r) => relationKey(entityKey(r.from), entityKey(r.to), r.relType));
65
+ const kept = new Set();
66
+ const deletes = [];
67
+ for (const row of stored) {
68
+ const from = keptKeyById.get(row.from_entity_id);
69
+ const to = keptKeyById.get(row.to_entity_id);
70
+ // An edge on a deleted entity goes with it through the v38 cascade.
71
+ if (from === undefined || to === undefined)
72
+ continue;
73
+ const key = relationKey(from, to, row.rel_type);
74
+ const want = wanted.get(key);
75
+ const same = want !== undefined && !kept.has(key)
76
+ && `${row.memory_id ?? ''}|${row.source_kind}` === provenanceOf(want.memoryId, kinds)
77
+ && row.source_object_type === want.sourceObject.type && row.source_object_id === want.sourceObject.id;
78
+ if (same)
79
+ kept.add(key);
80
+ else
81
+ deletes.push({ op: 'deleteRelation', id: row.id });
82
+ }
83
+ const inserts = [];
84
+ for (const [key, relation] of wanted)
85
+ if (!kept.has(key))
86
+ inserts.push({ op: 'insertRelation', relation });
87
+ return [...deletes, ...inserts];
88
+ }
89
+ /** Ops that turn the stored graph into `desired`, in apply order; reads only, and keeps the lowest id per natural key. */
90
+ export function graphDelta(db, tenantId, desired) {
91
+ const kinds = memoryKindsOf(db, desired);
92
+ // SAFETY: the SELECT names exactly the columns of StoredEntity.
93
+ const entities = db.prepare(`SELECT id, entity_type, name, memory_id, source_kind, source_object_type, source_object_id FROM entities WHERE tenant_id = ? ORDER BY id`).all(tenantId);
94
+ // SAFETY: the SELECT names exactly the columns of StoredRelation.
95
+ const relations = db.prepare(`SELECT id, from_entity_id, to_entity_id, rel_type, memory_id, source_kind, source_object_type, source_object_id FROM relations WHERE tenant_id = ? ORDER BY id`).all(tenantId);
96
+ const entityDelta = diffEntities(entities, desired.entities, kinds);
97
+ return [...entityDelta.ops, ...diffRelations(relations, desired.relations, entityDelta.keptKeyById, kinds)];
98
+ }
99
+ /** graphDelta on its own connection and outside any write lock; the apply re-checks each op against what changed since. */
100
+ export function readGraphDelta(hippoRoot, tenantId, desired) {
101
+ const db = openHippoDb(hippoRoot);
102
+ try {
103
+ return graphDelta(db, tenantId, desired);
104
+ }
105
+ finally {
106
+ closeHippoDb(db);
107
+ }
108
+ }
109
+ //# sourceMappingURL=delta.js.map
@@ -70,6 +70,11 @@ export interface InsertEntityOpts {
70
70
  * null; optional alongside a live memory (both paths may be set). */
71
71
  sourceObject?: SourceObjectRef;
72
72
  }
73
+ /** What an in-place entity update may change; type and source object are the row's identity and stay. */
74
+ export interface UpdateEntityOpts {
75
+ name: string;
76
+ memoryId?: string | null;
77
+ }
73
78
  export interface InsertRelationOpts {
74
79
  fromEntityId: number;
75
80
  toEntityId: number;
@@ -11,12 +11,16 @@
11
11
  * the forbidden state is unrepresentable regardless of code path. These helpers
12
12
  * surface the same guard as clear throws BEFORE hitting the trigger backstop.
13
13
  */
14
- import { type GraphTxDb, type SourceObjectType, type Entity, type Relation, type GraphQueueItem, type InsertEntityOpts, type InsertRelationOpts } from './types.js';
14
+ import { type GraphTxDb, type SourceObjectType, type Entity, type Relation, type GraphQueueItem, type InsertEntityOpts, type InsertRelationOpts, type UpdateEntityOpts } from './types.js';
15
+ import type { GraphOp } from './delta.js';
15
16
  /**
16
17
  * Insert a graph entity extracted from a consolidated memory. Throws if the source
17
18
  * memory is missing / cross-tenant / raw (the DB trigger is the backstop).
18
19
  */
19
20
  export declare function insertEntity(hippoRoot: string, tenantId: string, opts: InsertEntityOpts, txDb?: GraphTxDb): Entity;
21
+ /** Renames an entity or moves its memory provenance in place, keeping id and created_at; null when the row is gone.
22
+ * A delete plus an insert would cascade away every relation that points at it. */
23
+ export declare function updateEntity(hippoRoot: string, tenantId: string, id: number, opts: UpdateEntityOpts, txDb?: GraphTxDb): Entity | null;
20
24
  /**
21
25
  * Insert a graph relation between two entities, sourced from a consolidated memory.
22
26
  * Both entities must exist in the same tenant; the source memory must be consolidated.
@@ -34,25 +38,21 @@ export declare function enqueueExtraction(hippoRoot: string, tenantId: string, m
34
38
  * non-terminal 'pending'.
35
39
  */
36
40
  export declare function markExtractionProcessed(hippoRoot: string, tenantId: string, id: number, status?: 'processed' | 'skipped'): GraphQueueItem;
37
- /**
38
- * Delete ALL entities for a tenant (relations cascade via the from/to FKs). Returns
39
- * the number of entities deleted. The rebuild primitive for graph extraction: the
40
- * deterministic graph is a pure derived function of the consolidated objects, so an
41
- * extract clears then re-derives. Lives in graph.ts (the sole sanctioned graph
42
- * writer), so the CI lint permits this `DELETE FROM entities`. Does NOT touch
43
- * graph_extraction_queue (the enqueue-hook's domain).
44
- */
45
- export declare function clearGraph(hippoRoot: string, tenantId: string, txDb?: GraphTxDb): number;
46
- /**
47
- * Run a full graph rebuild for one tenant inside a single transaction. `clearGraph`
48
- * + every `insertEntity`/`insertRelation` call made inside `fn` (passing the supplied
49
- * `txDb`) share the one connection and its `BEGIN IMMEDIATE` write lock, so the
50
- * rebuild is ATOMIC: two concurrent rebuilds serialize on the write lock (the second
51
- * waits, then re-derives cleanly) instead of interleaving into duplicate rows, and a
52
- * throw mid-rebuild ROLLS BACK the clear (no bricked/empty graph). The sole sanctioned
53
- * place to wrap graph writes in a transaction.
54
- */
55
- export declare function runGraphRebuildTransaction<T>(hippoRoot: string, tenantId: string, fn: (txDb: GraphTxDb) => T): T;
41
+ export interface ApplyGraphOpsResult {
42
+ readonly next: number;
43
+ readonly skipped: number;
44
+ }
45
+ /** Applies `ops` from index `from` on the caller's open transaction and stops at the first op boundary past `opts.holdMs`.
46
+ * Returns where the next chunk starts and how many ops were skipped as stale. */
47
+ export declare function applyGraphOps(db: GraphTxDb, hippoRoot: string, tenantId: string, ops: readonly GraphOp[], from: number, opts: {
48
+ readonly holdMs: number;
49
+ readonly clock?: () => number;
50
+ }): ApplyGraphOpsResult;
51
+ /** Runs one tenant's graph writes in `fn` on `txDb` under one BEGIN IMMEDIATE, so two rebuilds serialize instead of
52
+ * interleaving and a throw rolls back the whole chunk. The sole sanctioned place to wrap graph writes in a transaction. */
53
+ export declare function runGraphRebuildTransaction<T>(hippoRoot: string, tenantId: string, fn: (txDb: GraphTxDb) => T, opts?: {
54
+ busyWaitMs?: number;
55
+ }): T;
56
56
  /**
57
57
  * Fail-soft producer hook: mark a tenant dirty for graph re-extraction by
58
58
  * enqueuing its consolidated mirror memory. NEVER throws into the caller — a
@@ -14,6 +14,7 @@
14
14
  import { openHippoDb, closeHippoDb } from '../db.js';
15
15
  import { assertTenantId } from '../tenant.js';
16
16
  import { log } from '../log.js';
17
+ import { clock } from '../write-budget.js';
17
18
  import { GRAPH_ENTITY_TYPES, GRAPH_RELATION_TYPES, MAX_ENTITY_NAME_LEN } from './types.js';
18
19
  import { rowToEntity, rowToRelation, rowToQueueItem, ENTITY_COLS, RELATION_COLS, QUEUE_COLS } from './rows.js';
19
20
  const SOURCE_OBJECT_TABLE = {
@@ -92,12 +93,7 @@ export function insertEntity(hippoRoot, tenantId, opts, txDb) {
92
93
  if (!GRAPH_ENTITY_TYPES.has(opts.entityType)) {
93
94
  throw new Error(`insertEntity: entityType must be one of ${Array.from(GRAPH_ENTITY_TYPES).join('|')}; got ${opts.entityType}`);
94
95
  }
95
- const name = (opts.name ?? '').trim();
96
- if (name.length === 0)
97
- throw new Error('insertEntity: name is required');
98
- if (name.length > MAX_ENTITY_NAME_LEN) {
99
- throw new Error(`insertEntity: name exceeds the ${MAX_ENTITY_NAME_LEN}-char cap`);
100
- }
96
+ const name = checkedName('insertEntity', opts.name);
101
97
  const now = new Date().toISOString();
102
98
  const memoryId = opts.memoryId ?? null;
103
99
  const sourceObject = opts.sourceObject ?? null;
@@ -121,6 +117,40 @@ export function insertEntity(hippoRoot, tenantId, opts, txDb) {
121
117
  closeHippoDb(ownDb);
122
118
  }
123
119
  }
120
+ function checkedName(label, raw) {
121
+ const name = (raw ?? '').trim();
122
+ if (name.length === 0)
123
+ throw new Error(`${label}: name is required`);
124
+ if (name.length > MAX_ENTITY_NAME_LEN) {
125
+ throw new Error(`${label}: name exceeds the ${MAX_ENTITY_NAME_LEN}-char cap`);
126
+ }
127
+ return name;
128
+ }
129
+ /** Renames an entity or moves its memory provenance in place, keeping id and created_at; null when the row is gone.
130
+ * A delete plus an insert would cascade away every relation that points at it. */
131
+ export function updateEntity(hippoRoot, tenantId, id, opts, txDb) {
132
+ assertTenantId('updateEntity', tenantId);
133
+ const name = checkedName('updateEntity', opts.name);
134
+ const db = txDb ?? openHippoDb(hippoRoot);
135
+ try {
136
+ // SAFETY: row's shape matches the columns named in ENTITY_COLS above.
137
+ const row = db.prepare(`SELECT ${ENTITY_COLS} FROM entities WHERE id = ? AND tenant_id = ?`).get(id, tenantId);
138
+ if (!row)
139
+ return null;
140
+ const entity = rowToEntity(row);
141
+ const sourceObject = entity.sourceObjectType === undefined || entity.sourceObjectId === undefined
142
+ ? null
143
+ : { type: entity.sourceObjectType, id: entity.sourceObjectId };
144
+ const resolved = resolveConsolidatedSource(db, tenantId, opts.memoryId ?? null, sourceObject, 'updateEntity');
145
+ db.prepare(`UPDATE entities SET name = ?, memory_id = ?, source_kind = ? WHERE id = ? AND tenant_id = ?`)
146
+ .run(name, resolved.memoryId, resolved.sourceKind, id, tenantId);
147
+ return { ...entity, name, memoryId: resolved.memoryId, sourceKind: resolved.sourceKind };
148
+ }
149
+ finally {
150
+ if (db !== txDb)
151
+ closeHippoDb(db);
152
+ }
153
+ }
124
154
  /**
125
155
  * Insert a graph relation between two entities, sourced from a consolidated memory.
126
156
  * Both entities must exist in the same tenant; the source memory must be consolidated.
@@ -229,39 +259,76 @@ export function markExtractionProcessed(hippoRoot, tenantId, id, status = 'proce
229
259
  closeHippoDb(db);
230
260
  }
231
261
  }
232
- /**
233
- * Delete ALL entities for a tenant (relations cascade via the from/to FKs). Returns
234
- * the number of entities deleted. The rebuild primitive for graph extraction: the
235
- * deterministic graph is a pure derived function of the consolidated objects, so an
236
- * extract clears then re-derives. Lives in graph.ts (the sole sanctioned graph
237
- * writer), so the CI lint permits this `DELETE FROM entities`. Does NOT touch
238
- * graph_extraction_queue (the enqueue-hook's domain).
239
- */
240
- export function clearGraph(hippoRoot, tenantId, txDb) {
241
- assertTenantId('clearGraph', tenantId);
242
- const ownDb = txDb ? null : openHippoDb(hippoRoot);
243
- const db = txDb ?? ownDb;
244
- try {
245
- const res = db.prepare(`DELETE FROM entities WHERE tenant_id = ?`).run(tenantId);
246
- return Number(res.changes ?? 0);
262
+ /** The source object still holds the loader's rule (active or superseded); a closed or gone one gets no new graph row. */
263
+ function objectInForce(db, tenantId, ref) {
264
+ // `table` comes from the fixed SOURCE_OBJECT_TABLE map, never from input.
265
+ const table = SOURCE_OBJECT_TABLE[ref.type];
266
+ // SAFETY: row's shape matches the single `status` column named in the SELECT.
267
+ const row = db.prepare(`SELECT status FROM ${table} WHERE id = ? AND tenant_id = ?`).get(ref.id, tenantId);
268
+ return row?.status === 'active' || row?.status === 'superseded';
269
+ }
270
+ function entityIdByKey(db, tenantId, key) {
271
+ // SAFETY: row's shape matches the single `id` column named in the SELECT.
272
+ const row = db.prepare(`SELECT id FROM entities WHERE tenant_id = ? AND entity_type = ? AND source_object_type = ? AND source_object_id = ? ORDER BY id LIMIT 1`).get(tenantId, key.entityType, key.sourceObject.type, key.sourceObject.id);
273
+ return row?.id;
274
+ }
275
+ function insertDesiredRelation(db, hippoRoot, tenantId, rel) {
276
+ const fromEntityId = entityIdByKey(db, tenantId, rel.from);
277
+ const toEntityId = entityIdByKey(db, tenantId, rel.to);
278
+ if (fromEntityId === undefined || toEntityId === undefined || !objectInForce(db, tenantId, rel.sourceObject))
279
+ return false;
280
+ // SAFETY: row's shape matches the single aliased column named in the SELECT.
281
+ const present = db.prepare(`SELECT 1 AS hit FROM relations WHERE tenant_id = ? AND from_entity_id = ? AND to_entity_id = ? AND rel_type = ? LIMIT 1`).get(tenantId, fromEntityId, toEntityId, rel.relType);
282
+ if (present)
283
+ return false;
284
+ insertRelation(hippoRoot, tenantId, { fromEntityId, toEntityId, relType: rel.relType, memoryId: rel.memoryId, sourceObject: rel.sourceObject }, db);
285
+ return true;
286
+ }
287
+ /** Applies one op; false when a writer since the diff made it stale, which the next run's diff repairs. */
288
+ function applyGraphOp(db, hippoRoot, tenantId, op) {
289
+ switch (op.op) {
290
+ case 'deleteEntity':
291
+ db.prepare(`DELETE FROM entities WHERE id = ? AND tenant_id = ?`).run(op.id, tenantId);
292
+ return true;
293
+ case 'deleteRelation':
294
+ db.prepare(`DELETE FROM relations WHERE id = ? AND tenant_id = ?`).run(op.id, tenantId);
295
+ return true;
296
+ case 'updateEntity':
297
+ if (!objectInForce(db, tenantId, op.entity.sourceObject))
298
+ return false;
299
+ return updateEntity(hippoRoot, tenantId, op.id, op.entity, db) !== null;
300
+ case 'insertEntity':
301
+ // A mirrorless object closed since the load is never enqueued again, so a stale insert would stay for good.
302
+ if (!objectInForce(db, tenantId, op.entity.sourceObject) || entityIdByKey(db, tenantId, op.entity) !== undefined)
303
+ return false;
304
+ insertEntity(hippoRoot, tenantId, op.entity, db);
305
+ return true;
306
+ case 'insertRelation':
307
+ return insertDesiredRelation(db, hippoRoot, tenantId, op.relation);
247
308
  }
248
- finally {
249
- if (ownDb)
250
- closeHippoDb(ownDb);
309
+ }
310
+ /** Applies `ops` from index `from` on the caller's open transaction and stops at the first op boundary past `opts.holdMs`.
311
+ * Returns where the next chunk starts and how many ops were skipped as stale. */
312
+ export function applyGraphOps(db, hippoRoot, tenantId, ops, from, opts) {
313
+ assertTenantId('applyGraphOps', tenantId);
314
+ const now = opts.clock ?? clock;
315
+ const begunAt = now();
316
+ let next = from;
317
+ let skipped = 0;
318
+ while (next < ops.length) {
319
+ if (!applyGraphOp(db, hippoRoot, tenantId, ops[next]))
320
+ skipped += 1;
321
+ next += 1;
322
+ if (now() - begunAt >= opts.holdMs)
323
+ break;
251
324
  }
325
+ return { next, skipped };
252
326
  }
253
- /**
254
- * Run a full graph rebuild for one tenant inside a single transaction. `clearGraph`
255
- * + every `insertEntity`/`insertRelation` call made inside `fn` (passing the supplied
256
- * `txDb`) share the one connection and its `BEGIN IMMEDIATE` write lock, so the
257
- * rebuild is ATOMIC: two concurrent rebuilds serialize on the write lock (the second
258
- * waits, then re-derives cleanly) instead of interleaving into duplicate rows, and a
259
- * throw mid-rebuild ROLLS BACK the clear (no bricked/empty graph). The sole sanctioned
260
- * place to wrap graph writes in a transaction.
261
- */
262
- export function runGraphRebuildTransaction(hippoRoot, tenantId, fn) {
327
+ /** Runs one tenant's graph writes in `fn` on `txDb` under one BEGIN IMMEDIATE, so two rebuilds serialize instead of
328
+ * interleaving and a throw rolls back the whole chunk. The sole sanctioned place to wrap graph writes in a transaction. */
329
+ export function runGraphRebuildTransaction(hippoRoot, tenantId, fn, opts) {
263
330
  assertTenantId('runGraphRebuildTransaction', tenantId);
264
- const db = openHippoDb(hippoRoot);
331
+ const db = openHippoDb(hippoRoot, opts);
265
332
  try {
266
333
  db.exec('BEGIN IMMEDIATE');
267
334
  let committed = false;
@@ -325,7 +392,7 @@ export function removeGraphEntitiesForObject(hippoRoot, tenantId, sourceObjectTy
325
392
  assertTenantId('removeGraphEntitiesForObject', tenantId);
326
393
  const db = openHippoDb(hippoRoot);
327
394
  try {
328
- db.exec('BEGIN');
395
+ db.exec('BEGIN IMMEDIATE');
329
396
  db.prepare(`DELETE FROM relations WHERE tenant_id = ? AND source_object_type = ? AND source_object_id = ?`)
330
397
  .run(tenantId, sourceObjectType, sourceObjectId);
331
398
  db.prepare(`DELETE FROM entities WHERE tenant_id = ? AND source_object_type = ? AND source_object_id = ?`)
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * Populates the graph from the already-structured consolidated first-class object tables -
5
5
  * NO NLP, no precision gate for entities + supersedes. The graph is a pure derived
6
- * function of the current object state, so `extractGraph` is an idempotent REBUILD: clear
7
- * the tenant's graph, then re-derive entities + `supersedes` relations from decisions /
6
+ * function of the current object state, so `extractGraph` is an idempotent REBUILD: derive
7
+ * entities + `supersedes` relations from decisions /
8
8
  * policies / customer_notes / project_briefs (the four object types whose kind maps to the
9
- * `entity_type` enum). All writes go through the src/graph.ts consolidated-source guard
10
- * (insertEntity / insertRelation / clearGraph); this module issues no raw SQL.
9
+ * `entity_type` enum), then write only their difference from the stored rows. All writes go
10
+ * through the src/graph/write.ts consolidated-source guard; this module issues no raw SQL.
11
11
  *
12
12
  * Pass 3 adds the
13
13
  * first CROSS-OBJECT relations: a deterministic NAME-MATCH heuristic that emits a
@@ -19,6 +19,9 @@
19
19
  * skill/incident/process entities (not in the entity_type enum - needs a migration); the
20
20
  * `hippo sleep` enqueue-hook.
21
21
  */
22
+ import { type EntityType } from './graph/types.js';
23
+ import { type DesiredGraph } from './graph/delta.js';
24
+ import { type WriteBudget } from './write-budget.js';
22
25
  /** Per-type load cap (the loaders default to 100). A type whose active or superseded
23
26
  * set exceeds this is truncated; `ExtractResult.truncated` records it so the
24
27
  * incompleteness is observable rather than silent. */
@@ -44,11 +47,42 @@ export interface ExtractResult {
44
47
  /** Entity types whose active or superseded load hit MAX_EXTRACT_PER_TYPE (the graph
45
48
  * is under-extracted for those). */
46
49
  truncated: string[];
50
+ /** Writes left out because a writer changed their row or object after the diff; set only when above 0. */
51
+ skipped?: number;
47
52
  }
48
- /**
49
- * Idempotent rebuild of the tenant's deterministic graph from its consolidated first-class
50
- * objects. Returns the entity/relation counts (+ which types were truncated at the
51
- * per-type cap). Safe to re-run: output is a pure function of the current object state.
52
- */
53
+ /** A consolidated object row normalised to the fields extraction needs. */
54
+ export interface ExtractRow {
55
+ entityType: EntityType;
56
+ /** The object table id (unique only WITHIN its table, hence keyed with entityType). */
57
+ e2Id: number;
58
+ name: string;
59
+ /** The object's full text searched for OTHER entities' names in Pass 3. */
60
+ searchText: string;
61
+ memoryId: string | null;
62
+ /** The successor's object id (Y) when this row (X) is superseded; null otherwise. */
63
+ supersededBy: number | null;
64
+ }
65
+ /** One object type's loaded rows. */
66
+ export interface LoadedType {
67
+ entityType: EntityType;
68
+ rows: ExtractRow[];
69
+ /** True when either status load filled MAX_EXTRACT_PER_TYPE. */
70
+ hitCap: boolean;
71
+ }
72
+ /** Every source type's rows, each read on its own connection; callers load before any write lock is taken. */
73
+ export declare function loadGraphSources(hippoRoot: string, tenantId: string): LoadedType[];
74
+ /** The graph the loaded objects imply, plus the counts ExtractResult reports. */
75
+ export interface DerivedGraph extends DesiredGraph {
76
+ byType: Record<string, number>;
77
+ truncated: string[];
78
+ references: number;
79
+ }
80
+ /** The tenant's graph as a pure function of its loaded objects; the write side diffs it against what is stored. */
81
+ export declare function deriveGraph(loaded: readonly LoadedType[]): DerivedGraph;
82
+ /** Idempotent rebuild of the tenant's graph from its consolidated objects, in one transaction; safe to re-run.
83
+ * Returns the derived counts and which types hit the per-type cap. */
53
84
  export declare function extractGraph(hippoRoot: string, tenantId: string): ExtractResult;
85
+ /** extractGraph for sleep: diffs outside the lock, then applies in transactions of about `budget.holdMs` with a pause
86
+ * between them. A run stopped between chunks leaves a mixed graph that the next run's diff finishes. */
87
+ export declare function extractGraphChunked(hippoRoot: string, tenantId: string, budget?: WriteBudget): Promise<ExtractResult>;
54
88
  //# sourceMappingURL=graph-extract.d.ts.map