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,454 @@
1
+ // Summaries post-compact could not record because the store was busy, waiting on disk for the next replay.
2
+ import { randomBytes } from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { isObjectLike, isStringValue } from './capture-contract.js';
6
+ import { isSqliteBusy } from './db/busy.js';
7
+ import { errorMessage } from './log.js';
8
+ export const SPOOL_DIR = 'compactions-spool';
9
+ /** Starts the log line of a step that broke with an unexpected error, so the session-start banner counts it; a set-aside file is counted by its `.bad` name instead. */
10
+ export const SPOOL_PROBLEM = 'spool problem: ';
11
+ const LOCK = 'replay.lock';
12
+ /** Long enough that a live replayer has finished with its claim. */
13
+ const STALE_MS = 10 * 60_000;
14
+ const MAX_ATTEMPTS = 3;
15
+ const BUSY_RETRIES = 3;
16
+ const BUSY_WAIT_MS = 50;
17
+ // Antivirus, the indexer or a delete still pending on Windows: the step is left for a later run.
18
+ const BUSY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
19
+ // A stem is `<ms13>-<rand8hex>`, so name order is age order; legacy files are `<session>-<ms>.json`.
20
+ const WAITING = /^([\w-]+)(?:\.a(\d+))?\.json$/;
21
+ const CLAIM = /^([\w-]+)\.a(\d+)\.claim-(\d{13})$/;
22
+ // An old binary's claim carries no claim time, so its mtime stands in.
23
+ const LEGACY_CLAIM = /^([\w-]+)(?:\.a(\d+))?\.json\.claimed$/;
24
+ const TMP = /^([\w-]+)(?:\.a\d+)?\.json\.tmp$/;
25
+ const BAD = /^([\w-]+)\.(?:\w+\.)?bad$/;
26
+ const BAD_TMP = /^[\w-]+\.\w+\.bad\.[0-9a-f]{8}\.tmp$/;
27
+ const NEW_STEM = /^(\d{13})-[0-9a-f]{8}$/;
28
+ const LEGACY_STEM_TIME = /-(\d+)$/;
29
+ let fsx = fs;
30
+ /** Test-only seam, so a test can make one fs call fail; null restores the real fs. */
31
+ export function __setSpoolFs(next) {
32
+ fsx = next ?? fs;
33
+ }
34
+ const stamp = (ms) => String(Math.trunc(ms)).padStart(13, '0');
35
+ const isTime = (value) => typeof value === 'number' && Number.isFinite(value);
36
+ const idle = new Int32Array(new SharedArrayBuffer(4));
37
+ function errCode(cause) {
38
+ return cause instanceof Error && 'code' in cause && isStringValue(cause.code) ? cause.code : '';
39
+ }
40
+ /** Runs one fs step, retrying a busy code briefly; '' when it ran, else the busy code it kept hitting. Any other error throws. */
41
+ function retryBusy(step) {
42
+ for (let retry = 0;; retry++) {
43
+ try {
44
+ step();
45
+ return '';
46
+ }
47
+ catch (err) {
48
+ const code = errCode(err);
49
+ if (!BUSY_CODES.has(code))
50
+ throw err;
51
+ if (retry === BUSY_RETRIES)
52
+ return code;
53
+ Atomics.wait(idle, 0, 0, BUSY_WAIT_MS);
54
+ }
55
+ }
56
+ }
57
+ /** Runs one fs step: ENOENT means gone, a busy code is retried briefly, and any other error is logged and counted busy, so a replay never throws. */
58
+ function settleCode(step, log, what) {
59
+ try {
60
+ const code = retryBusy(step);
61
+ return { settled: code === '' ? 'done' : 'busy', code };
62
+ }
63
+ catch (err) {
64
+ const code = errCode(err);
65
+ if (code === 'ENOENT')
66
+ return { settled: 'gone', code };
67
+ log(`${SPOOL_PROBLEM}${what}: ${errorMessage(err)}`);
68
+ return { settled: 'busy', code };
69
+ }
70
+ }
71
+ /** {@link settleCode} for a caller that needs only the outcome. */
72
+ function settle(step, log, what) {
73
+ return settleCode(step, log, what).settled;
74
+ }
75
+ /** When a stem was spooled: the time that leads a new stem, else the time that ends a legacy one, else 0. */
76
+ function created(stem) {
77
+ const m = NEW_STEM.exec(stem) ?? LEGACY_STEM_TIME.exec(stem);
78
+ return m === null ? 0 : Number(m[1]);
79
+ }
80
+ /** Files waiting for import, oldest first across new and legacy names. */
81
+ function waitingFiles(names) {
82
+ const found = names.flatMap((name) => {
83
+ const m = WAITING.exec(name);
84
+ return m === null ? [] : [{ name, stem: m[1], attempt: Number(m[2] ?? 0), created: created(m[1]) }];
85
+ });
86
+ return found.sort((a, b) => a.created - b.created || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
87
+ }
88
+ function claimOf(name) {
89
+ const m = CLAIM.exec(name);
90
+ if (m !== null)
91
+ return { name, stem: m[1], attempt: Number(m[2]), claimedAt: Number(m[3]) };
92
+ const legacy = LEGACY_CLAIM.exec(name);
93
+ return legacy === null ? null : { name, stem: legacy[1], attempt: Number(legacy[2] ?? 0), claimedAt: null };
94
+ }
95
+ // The session id stays in the body only, and the random part keeps two spools of one millisecond apart.
96
+ function spoolFile(hippoRoot) {
97
+ return path.join(hippoRoot, SPOOL_DIR, `${stamp(Date.now())}-${randomBytes(4).toString('hex')}.a0.json`);
98
+ }
99
+ export function spool(hippoRoot, tenantId, payload, text, at) {
100
+ const file = spoolFile(hippoRoot);
101
+ fsx.mkdirSync(path.dirname(file), { recursive: true });
102
+ const body = { tenantId, sessionId: payload.sessionId, trigger: payload.trigger, cwd: payload.cwd, transcriptPath: payload.transcriptPath, at: at.toISOString(), summary: text.summary, items: text.items };
103
+ // Renamed into place so a replayer listing `.json` files never reads half a file.
104
+ fsx.writeFileSync(`${file}.tmp`, JSON.stringify(body), { encoding: 'utf8', flag: 'wx' });
105
+ // A rename that stays busy leaves a whole tmp, which a replay promotes once it is STALE_MS old.
106
+ retryBusy(() => fsx.renameSync(`${file}.tmp`, file));
107
+ }
108
+ /** null when the text is not a spooled compaction. */
109
+ export function parseSpooled(text, fallbackTenantId) {
110
+ let raw;
111
+ try {
112
+ raw = JSON.parse(text);
113
+ }
114
+ catch {
115
+ return null; // a half-written spool file holds no compaction; the caller treats null as unreadable
116
+ }
117
+ if (!isObjectLike(raw) || !('sessionId' in raw) || !isStringValue(raw.sessionId) || !('summary' in raw) || !isStringValue(raw.summary))
118
+ return null;
119
+ if (!('at' in raw) || !isStringValue(raw.at) || Number.isNaN(Date.parse(raw.at)))
120
+ return null;
121
+ const items = 'items' in raw && Array.isArray(raw.items) ? raw.items.filter(isStringValue) : [];
122
+ return {
123
+ tenantId: 'tenantId' in raw && isStringValue(raw.tenantId) && raw.tenantId !== '' ? raw.tenantId : fallbackTenantId,
124
+ payload: {
125
+ sessionId: raw.sessionId,
126
+ trigger: 'trigger' in raw && isStringValue(raw.trigger) ? raw.trigger : null,
127
+ cwd: 'cwd' in raw && isStringValue(raw.cwd) ? raw.cwd : null,
128
+ transcriptPath: 'transcriptPath' in raw && isStringValue(raw.transcriptPath) ? raw.transcriptPath : null,
129
+ compactSummary: null,
130
+ },
131
+ text: { summary: raw.summary, items },
132
+ at: new Date(raw.at),
133
+ };
134
+ }
135
+ const heldBy = (pid) => `spool left to another replayer (lock held by ${pid === null ? 'another process' : `pid ${pid}`})`;
136
+ function parseLock(text) {
137
+ let raw;
138
+ try {
139
+ raw = JSON.parse(text);
140
+ }
141
+ catch {
142
+ return null; // torn, or its holder is still writing it; the caller judges it by mtime
143
+ }
144
+ if (!isObjectLike(raw) || !('at' in raw) || !isTime(raw.at))
145
+ return null;
146
+ const pid = 'pid' in raw && isTime(raw.pid) ? raw.pid : null;
147
+ const token = 'token' in raw && isStringValue(raw.token) ? raw.token : null;
148
+ return { pid, at: raw.at, token };
149
+ }
150
+ /** The lock's body; its mtime stands in for the time when the body does not parse. */
151
+ function readLock(lock, log) {
152
+ let text = '';
153
+ const read = settle(() => { text = fsx.readFileSync(lock, 'utf8'); }, log, 'spool lock not read');
154
+ if (read !== 'done')
155
+ return read;
156
+ const body = parseLock(text);
157
+ if (body !== null)
158
+ return body;
159
+ let mtime = 0;
160
+ const stat = settle(() => { mtime = fsx.statSync(lock).mtimeMs; }, log, 'spool lock not read');
161
+ return stat === 'done' ? { pid: null, at: mtime, token: null } : stat;
162
+ }
163
+ function createLock(lock, body, log) {
164
+ try {
165
+ fsx.writeFileSync(lock, body, { encoding: 'utf8', flag: 'wx' });
166
+ return 'taken';
167
+ }
168
+ catch (err) {
169
+ const code = errCode(err);
170
+ if (code === 'EEXIST')
171
+ return 'exists';
172
+ // Windows refuses a create while a delete of the same name is pending, and the folder may have just gone.
173
+ if (code !== 'ENOENT' && !BUSY_CODES.has(code))
174
+ log(`spool lock not taken: ${errorMessage(err)}`);
175
+ return 'skip';
176
+ }
177
+ }
178
+ /** This run's token and take time once it holds `replay.lock`, else null: another replayer holds it, or it could not be made. */
179
+ function takeLock(dir, log) {
180
+ const lock = path.join(dir, LOCK);
181
+ const token = randomBytes(8).toString('hex');
182
+ const at = Date.now();
183
+ const body = JSON.stringify({ pid: process.pid, at, token });
184
+ for (let tries = 0; tries < 2; tries++) {
185
+ const made = createLock(lock, body, log);
186
+ if (made === 'taken')
187
+ return { token, at };
188
+ if (made === 'skip')
189
+ break;
190
+ const held = readLock(lock, log);
191
+ if (held === 'gone')
192
+ continue;
193
+ if (held === 'busy')
194
+ break;
195
+ if (Math.abs(Date.now() - held.at) <= STALE_MS) {
196
+ log(heldBy(held.pid));
197
+ return null;
198
+ }
199
+ // SHORTCUT: two takers of one stale lock overlap by one file each; a pid-alive check is the upgrade.
200
+ if (settle(() => fsx.unlinkSync(lock), log, 'spool lock not taken over') === 'busy')
201
+ break;
202
+ }
203
+ log(heldBy(null));
204
+ return null;
205
+ }
206
+ /** False, after saying so, once `replay.lock` no longer holds this run's token or cannot be read. */
207
+ function ownsLock(dir, token, log) {
208
+ const held = readLock(path.join(dir, LOCK), log);
209
+ if (held !== 'gone' && held !== 'busy' && held.token === token)
210
+ return true;
211
+ log(held === 'busy' ? 'spool lock could not be read, stopping' : 'spool left to another replayer (lock taken over)');
212
+ return false;
213
+ }
214
+ /** Removes `replay.lock` only while it holds this run's token, so a run never deletes a successor's lock. Never throws. */
215
+ function releaseLock(dir, mine, log) {
216
+ const lock = path.join(dir, LOCK);
217
+ const held = readLock(lock, log);
218
+ // A takeover needs a lock over STALE_MS old, so a busy read of one this run took more recently is still this run's.
219
+ const ours = held === 'busy' ? Date.now() - mine.at < STALE_MS : held !== 'gone' && held.token === mine.token;
220
+ if (ours)
221
+ settle(() => fsx.unlinkSync(lock), log, 'spool lock not released');
222
+ }
223
+ /** Claim time sits in the claim's name, so a claim over STALE_MS away from now belongs to a replayer that never finished. True when one was put back. */
224
+ function recoverStaleClaims(dir, names, log) {
225
+ const now = Date.now();
226
+ // A claim whose unlink stayed busy after its `.bad` landed is a spare copy, not a second failure.
227
+ const setAside = new Set(names.flatMap((name) => BAD.exec(name)?.[1] ?? []));
228
+ let putBackOne = false;
229
+ for (const claim of names.flatMap((name) => claimOf(name) ?? [])) {
230
+ const file = path.join(dir, claim.name);
231
+ const what = `spool file ${claim.name} not recovered`;
232
+ let at = claim.claimedAt ?? 0;
233
+ if (claim.claimedAt === null && settle(() => { at = fsx.statSync(file).mtimeMs; }, log, what) !== 'done')
234
+ continue;
235
+ if (Math.abs(now - at) <= STALE_MS)
236
+ continue;
237
+ const next = claim.attempt + 1;
238
+ if (setAside.has(claim.stem)) {
239
+ settle(() => fsx.unlinkSync(file), log, what);
240
+ }
241
+ else if (next < MAX_ATTEMPTS) {
242
+ if (settle(() => fsx.renameSync(file, path.join(dir, `${claim.stem}.a${next}.json`)), log, what) === 'done') {
243
+ log(`spool file ${claim.name} was claimed by a replayer that never finished, put back (try ${next} of ${MAX_ATTEMPTS})`);
244
+ putBackOne = true;
245
+ }
246
+ }
247
+ else if (moveBad(dir, file, claim.stem, claim.attempt, 'interrupted', log)) {
248
+ log(`spool file ${claim.name} was claimed by a replayer that never finished ${MAX_ATTEMPTS} times, set aside as .bad`);
249
+ }
250
+ }
251
+ return putBackOne;
252
+ }
253
+ /** Returns a claim to the waiting files as `<stem>.a<attempt>.json`; one that stays busy is left for stale recovery. */
254
+ function putBack(dir, claim, stem, attempt, log) {
255
+ const name = path.basename(claim);
256
+ if (settle(() => fsx.renameSync(claim, path.join(dir, `${stem}.a${attempt}.json`)), log, `spool file ${name} not put back`) === 'busy') {
257
+ log(`spool file ${name} could not be put back; it returns ${STALE_MS / 60_000} minutes after its claim`);
258
+ }
259
+ }
260
+ /** Writes `<stem>.<cause>.bad` through a random temp file, so the rename replaces any torn `.bad` a crash left. */
261
+ function writeBad(dir, stem, cause, text, log, what) {
262
+ const tmp = path.join(dir, `${stem}.${cause}.bad.${randomBytes(4).toString('hex')}.tmp`);
263
+ const written = settle(() => fsx.writeFileSync(tmp, text, { encoding: 'utf8', flag: 'wx' }), log, what);
264
+ const moved = written === 'done' ? settle(() => fsx.renameSync(tmp, path.join(dir, `${stem}.${cause}.bad`)), log, what) : written;
265
+ if (moved !== 'done' && written === 'done')
266
+ settle(() => fsx.unlinkSync(tmp), log, what);
267
+ return moved === 'done';
268
+ }
269
+ /** Sets a claim aside as `.bad` before the claim goes, so a crash between leaves a whole copy; a busy write puts the claim back. */
270
+ function moveBad(dir, claim, stem, attempt, cause, log, raw) {
271
+ const what = `spool file ${path.basename(claim)} not set aside`;
272
+ let text = raw ?? '';
273
+ if (raw === undefined && settle(() => { text = fsx.readFileSync(claim, 'utf8'); }, log, what) !== 'done')
274
+ return false;
275
+ if (!writeBad(dir, stem, cause, text, log, what)) {
276
+ putBack(dir, claim, stem, attempt, log);
277
+ return false;
278
+ }
279
+ settle(() => fsx.unlinkSync(claim), log, what);
280
+ return true;
281
+ }
282
+ /** A tmp over STALE_MS old belongs to a spool that never renamed it: a whole one joins the waiting files, a torn one is set aside. True when one joined. */
283
+ function promoteTmp(dir, names, tenantId, log) {
284
+ const now = Date.now();
285
+ let promoted = false;
286
+ for (const name of names) {
287
+ const m = TMP.exec(name);
288
+ if (m === null && !BAD_TMP.test(name))
289
+ continue;
290
+ const file = path.join(dir, name);
291
+ const what = `spool file ${name} not promoted`;
292
+ let mtime = 0;
293
+ let text = '';
294
+ if (settle(() => { mtime = fsx.statSync(file).mtimeMs; }, log, what) !== 'done' || Math.abs(now - mtime) <= STALE_MS)
295
+ continue;
296
+ if (m === null) {
297
+ // A `.bad` write cut off before its rename: its source goes only after that rename lands, so this copy is spare.
298
+ settle(() => fsx.unlinkSync(file), log, `spool file ${name} not removed`);
299
+ continue;
300
+ }
301
+ if (settle(() => { text = fsx.readFileSync(file, 'utf8'); }, log, what) !== 'done')
302
+ continue;
303
+ if (parseSpooled(text, tenantId) !== null) {
304
+ if (settle(() => fsx.renameSync(file, file.slice(0, -'.tmp'.length)), log, what) === 'done') {
305
+ log(`spool file ${name} was never renamed into place, promoted`);
306
+ promoted = true;
307
+ }
308
+ }
309
+ else if (writeBad(dir, m[1], 'unreadable', text, log, what)) {
310
+ settle(() => fsx.unlinkSync(file), log, what);
311
+ log(`spool file ${name} was never finished, set aside as .bad`);
312
+ }
313
+ }
314
+ return promoted;
315
+ }
316
+ /** Removes a claim once the store holds its summary; a claim left behind is imported again, so the log says so. */
317
+ function removeClaim(claim, name, log) {
318
+ const removed = settleCode(() => fsx.unlinkSync(claim), log, `spool file ${name} claim not removed`);
319
+ if (removed.settled === 'busy')
320
+ log(`spool file ${name} saved; its claim could not be removed (${removed.code}); it will be imported again`);
321
+ }
322
+ /** After an importer throw: a busy store stops the run with the count unchanged, any other error counts one try toward .bad. */
323
+ function importFailed(dir, claim, entry, text, recorded, cause, log) {
324
+ const { name } = entry;
325
+ const busy = isSqliteBusy(cause);
326
+ if (recorded) {
327
+ // The record stays `summarised`, so the stalled-record step of a later replay writes its items.
328
+ log(busy ? `spool file ${name} saved; its memories wait for the next sleep (store busy)` : `spool file ${name} saved; writing its memories failed: ${errorMessage(cause)}`);
329
+ return busy ? 'stop' : 'skipped';
330
+ }
331
+ if (busy) {
332
+ putBack(dir, claim, entry.stem, entry.attempt, log);
333
+ log(`spool file ${name} waits for the next run: the store is busy`);
334
+ return 'stop';
335
+ }
336
+ const next = entry.attempt + 1;
337
+ if (next < MAX_ATTEMPTS) {
338
+ putBack(dir, claim, entry.stem, next, log);
339
+ log(`spool file ${name} failed to import (try ${next} of ${MAX_ATTEMPTS}): ${errorMessage(cause)}`);
340
+ }
341
+ else if (moveBad(dir, claim, entry.stem, entry.attempt, 'failed', log, text)) {
342
+ log(`spool file ${name} set aside as .bad after ${MAX_ATTEMPTS} tries: ${errorMessage(cause)}`);
343
+ }
344
+ return 'skipped';
345
+ }
346
+ /** Claims, reads and imports one waiting file; `stop` ends the run because the store is busy. */
347
+ function importFile(dir, entry, tenantId, log, importOne) {
348
+ const { name } = entry;
349
+ const claim = path.join(dir, `${entry.stem}.a${entry.attempt}.claim-${stamp(Date.now())}`);
350
+ const taken = settleCode(() => fsx.renameSync(path.join(dir, name), claim), log, `spool file ${name} not claimed`);
351
+ if (BUSY_CODES.has(taken.code))
352
+ log(`spool file ${name} is locked by another program, left for the next run`);
353
+ if (taken.settled !== 'done')
354
+ return 'skipped';
355
+ let text = '';
356
+ const read = settle(() => { text = fsx.readFileSync(claim, 'utf8'); }, log, `spool file ${name} not read`);
357
+ if (read === 'gone')
358
+ log(`spool file ${name} vanished`);
359
+ if (read === 'busy')
360
+ putBack(dir, claim, entry.stem, entry.attempt, log);
361
+ if (read !== 'done')
362
+ return 'skipped';
363
+ const spooled = parseSpooled(text, tenantId);
364
+ if (spooled === null) {
365
+ if (moveBad(dir, claim, entry.stem, entry.attempt, 'unreadable', log, text))
366
+ log(`spool file ${name} is not readable, set aside as .bad`);
367
+ return 'skipped';
368
+ }
369
+ let recorded = false;
370
+ const markRecorded = () => {
371
+ if (recorded)
372
+ return;
373
+ recorded = true;
374
+ removeClaim(claim, name, log);
375
+ };
376
+ try {
377
+ importOne(spooled, markRecorded);
378
+ markRecorded();
379
+ return 'imported';
380
+ }
381
+ catch (err) {
382
+ return importFailed(dir, claim, entry, text, recorded, err, log);
383
+ }
384
+ }
385
+ function mtimeOf(file) {
386
+ try {
387
+ return fsx.statSync(file).mtimeMs;
388
+ }
389
+ catch (err) {
390
+ const code = errCode(err);
391
+ // Moved by a replay after the listing, or held by another program: left out of this count rather than failing it.
392
+ if (code === 'ENOENT' || BUSY_CODES.has(code))
393
+ return null;
394
+ throw err;
395
+ }
396
+ }
397
+ /** What doctor reports, read only: files a replay should have taken by now, claims and tmp files a dead process left, and `.bad` files. */
398
+ export function spoolCounts(hippoRoot, now, waitingAfterMs) {
399
+ const counts = { waiting: 0, stale: 0, bad: 0 };
400
+ const dir = path.join(hippoRoot, SPOOL_DIR);
401
+ if (!fsx.existsSync(dir))
402
+ return counts;
403
+ const at = now.getTime();
404
+ for (const name of fsx.readdirSync(dir)) {
405
+ const file = path.join(dir, name);
406
+ const waiting = WAITING.exec(name);
407
+ const claim = claimOf(name);
408
+ if (name.endsWith('.bad'))
409
+ counts.bad++;
410
+ else if (waiting !== null) {
411
+ const spooled = NEW_STEM.test(waiting[1]) ? created(waiting[1]) : mtimeOf(file);
412
+ if (spooled !== null && at - spooled > waitingAfterMs)
413
+ counts.waiting++;
414
+ }
415
+ else if (claim !== null || TMP.test(name)) {
416
+ const since = claim?.claimedAt ?? mtimeOf(file);
417
+ if (since !== null && Math.abs(at - since) > STALE_MS)
418
+ counts.stale++;
419
+ }
420
+ }
421
+ return counts;
422
+ }
423
+ /** One replayer at a time holds `replay.lock`, so only one process claims a file even where Windows lets two renames of it both land. */
424
+ export function importSpool(hippoRoot, tenantId, log, deadline, importOne) {
425
+ const dir = path.join(hippoRoot, SPOOL_DIR);
426
+ if (!fsx.existsSync(dir))
427
+ return 0;
428
+ const mine = takeLock(dir, log);
429
+ if (mine === null)
430
+ return 0;
431
+ try {
432
+ if (!ownsLock(dir, mine.token, log))
433
+ return 0;
434
+ const names = fsx.readdirSync(dir);
435
+ const promoted = promoteTmp(dir, names, tenantId, log);
436
+ const recovered = recoverStaleClaims(dir, names, log);
437
+ let finished = 0;
438
+ // Only promotion and recovery add waiting names, so the folder is listed again only after one of them did.
439
+ for (const entry of waitingFiles(promoted || recovered ? fsx.readdirSync(dir) : names)) {
440
+ if (Date.now() > deadline || !ownsLock(dir, mine.token, log))
441
+ break;
442
+ const outcome = importFile(dir, entry, tenantId, log, importOne);
443
+ if (outcome === 'imported')
444
+ finished++;
445
+ if (outcome === 'stop')
446
+ break;
447
+ }
448
+ return finished;
449
+ }
450
+ finally {
451
+ releaseLock(dir, mine, log);
452
+ }
453
+ }
454
+ //# sourceMappingURL=compaction-spool.js.map
package/dist/config.d.ts CHANGED
@@ -151,6 +151,14 @@ export interface HippoConfig {
151
151
  projectIdentity: {
152
152
  remote: boolean;
153
153
  };
154
+ /** The store serves many people, so a write that names no project is stamped NULL, never this folder's project,
155
+ * and a context read must name the caller's project. Default false. Read it through isSharedStore. */
156
+ sharedStore: boolean;
154
157
  }
155
158
  export declare function loadConfig(hippoRoot: string): HippoConfig;
159
+ /** True when the store's config.json sets `"sharedStore": true`. Once true, a root stays true for this process,
160
+ * so a broken edit while a server runs cannot turn it off. Reads only this key: loadConfig has no cache. */
161
+ export declare function isSharedStore(hippoRoot: string): boolean;
162
+ /** Test seam: forget every root seen as shared. */
163
+ export declare function _resetSharedStoreCacheForTests(): void;
156
164
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -96,6 +96,7 @@ const DEFAULT_CONFIG = {
96
96
  projectIdentity: {
97
97
  remote: true,
98
98
  },
99
+ sharedStore: false,
99
100
  };
100
101
  function isMemoryValueConfig(value) {
101
102
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -220,6 +221,14 @@ function defaultHalfLifeDays(raw) {
220
221
  }
221
222
  return days;
222
223
  }
224
+ // Only a real true turns it on; anything else present warns and stays off.
225
+ function sharedStoreFlag(raw) {
226
+ const value = raw.sharedStore;
227
+ if (value === undefined || value === true || value === false)
228
+ return value === true;
229
+ log.warn(`config.json's "sharedStore" must be true or false (got ${JSON.stringify(value)}) - using false.`);
230
+ return false;
231
+ }
223
232
  export function loadConfig(hippoRoot) {
224
233
  const configPath = path.join(hippoRoot, 'config.json');
225
234
  if (!fs.existsSync(configPath))
@@ -267,6 +276,7 @@ export function loadConfig(hippoRoot) {
267
276
  deliveryLedger: { enabled: deliveryLedgerEnabled(raw.deliveryLedger) },
268
277
  pilot: { holdoutRateBp: pilotHoldoutRate(raw.pilot) },
269
278
  projectIdentity: { remote: projectIdentityRemote(raw.projectIdentity) },
279
+ sharedStore: sharedStoreFlag(raw),
270
280
  };
271
281
  }
272
282
  catch (err) {
@@ -276,4 +286,43 @@ export function loadConfig(hippoRoot) {
276
286
  return { ...DEFAULT_CONFIG };
277
287
  }
278
288
  }
289
+ const sharedStoreRoots = new Set();
290
+ /** The folder as isGlobalStoreRoot compares it (realpath, case-folded on Windows); its helper sits behind an import cycle. */
291
+ function sharedStoreKey(hippoRoot) {
292
+ let real;
293
+ try {
294
+ real = fs.realpathSync.native(hippoRoot);
295
+ }
296
+ catch (err) {
297
+ log.debug(`sharedStore: realpath fell back to resolve for ${hippoRoot}: ${err instanceof Error ? err.message : String(err)}`);
298
+ real = path.resolve(hippoRoot);
299
+ }
300
+ return process.platform === 'win32' ? real.toLowerCase() : real;
301
+ }
302
+ /** True when the store's config.json sets `"sharedStore": true`. Once true, a root stays true for this process,
303
+ * so a broken edit while a server runs cannot turn it off. Reads only this key: loadConfig has no cache. */
304
+ export function isSharedStore(hippoRoot) {
305
+ const key = sharedStoreKey(hippoRoot);
306
+ if (sharedStoreRoots.has(key))
307
+ return true;
308
+ const configPath = path.join(path.resolve(hippoRoot), 'config.json');
309
+ if (!fs.existsSync(configPath))
310
+ return false;
311
+ let raw;
312
+ try {
313
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
314
+ }
315
+ catch (err) {
316
+ log.warn(`failed to read ${configPath}: ${err instanceof Error ? err.message : err} - sharedStore read as false.`);
317
+ return false;
318
+ }
319
+ if (raw?.sharedStore !== true)
320
+ return false;
321
+ sharedStoreRoots.add(key);
322
+ return true;
323
+ }
324
+ /** Test seam: forget every root seen as shared. */
325
+ export function _resetSharedStoreCacheForTests() {
326
+ sharedStoreRoots.clear();
327
+ }
279
328
  //# sourceMappingURL=config.js.map
@@ -24,7 +24,7 @@ export interface DeletionResult {
24
24
  * match a single deletion event. Archive ALL of them.
25
25
  *
26
26
  * ONE shared DB handle wraps ALL archives + the idempotency mark in a single
27
- * outer SAVEPOINT: a per-row failure rolls back the whole batch, idempotency
27
+ * outer write scope: a per-row failure rolls back the whole batch, idempotency
28
28
  * included, so a retry re-attempts cleanly instead of leaving searchable
29
29
  * survivors. archiveRawMemory's own inner SAVEPOINT nests safely inside it.
30
30
  *
@@ -1,4 +1,4 @@
1
- import { openHippoDb, closeHippoDb } from '../../db.js';
1
+ import { openHippoDb, closeHippoDb, withWriteScope } from '../../db.js';
2
2
  import { archiveRawMemory } from '../../raw-archive.js';
3
3
  import { hasSeenKey, markKeySeen } from './idempotency.js';
4
4
  /**
@@ -9,7 +9,7 @@ import { hasSeenKey, markKeySeen } from './idempotency.js';
9
9
  * match a single deletion event. Archive ALL of them.
10
10
  *
11
11
  * ONE shared DB handle wraps ALL archives + the idempotency mark in a single
12
- * outer SAVEPOINT: a per-row failure rolls back the whole batch, idempotency
12
+ * outer write scope: a per-row failure rolls back the whole batch, idempotency
13
13
  * included, so a retry re-attempts cleanly instead of leaving searchable
14
14
  * survivors. archiveRawMemory's own inner SAVEPOINT nests safely inside it.
15
15
  *
@@ -40,11 +40,10 @@ export function handleCommentDeleted(ctx, input) {
40
40
  });
41
41
  return { status: 'archive_skipped_not_found', archivedCount: 0 };
42
42
  }
43
- // Outer SAVEPOINT wrapping all archives + the idempotency mark. Any throw
43
+ // Outer write scope wrapping all archives + the idempotency mark. Any throw
44
44
  // rolls back the whole batch so retry sees neither archive nor mark — and
45
45
  // re-attempts the full set.
46
- db.exec('SAVEPOINT github_delete_all');
47
- try {
46
+ withWriteScope(db, 'github_delete_all', () => {
48
47
  for (const id of memoryIds) {
49
48
  archiveRawMemory(db, id, {
50
49
  reason: `source_deleted:github:${input.eventName}:${input.deliveryId}`,
@@ -57,18 +56,7 @@ export function handleCommentDeleted(ctx, input) {
57
56
  eventName: input.eventName,
58
57
  memoryId: memoryIds[0],
59
58
  });
60
- db.exec('RELEASE SAVEPOINT github_delete_all');
61
- }
62
- catch (e) {
63
- try {
64
- db.exec('ROLLBACK TO SAVEPOINT github_delete_all');
65
- db.exec('RELEASE SAVEPOINT github_delete_all');
66
- }
67
- catch {
68
- // Best effort. Surface the original error.
69
- }
70
- throw e;
71
- }
59
+ });
72
60
  return { status: 'archived', archivedCount: memoryIds.length };
73
61
  }
74
62
  finally {
@@ -1,6 +1,7 @@
1
1
  import { Layer, calculateStrength } from '../memory.js';
2
2
  import { jaccardMinShared, overlapPartners } from '../overlap-index.js';
3
3
  import { isQuarantineScope } from '../quarantine.js';
4
+ import { isPersonalScope } from '../recall-scope.js';
4
5
  import { DECAY_THRESHOLD } from './decay.js';
5
6
  // Contradictions should be gated by content overlap, not shared tags. Tags like
6
7
  // `feedback` / `policy` are too coarse and can make unrelated rules look like
@@ -66,6 +67,9 @@ rescuedIds = new Set()) {
66
67
  }
67
68
  /** One project's ambient context can show both: same tenant, and the same project or a user-global row beside a project's. */
68
69
  function recalledTogether(a, b) {
70
+ // A personal row pairs only inside its own scope; any other pair shows its id to someone else.
71
+ if ((isPersonalScope(a.scope) || isPersonalScope(b.scope)) && a.scope !== b.scope)
72
+ return false;
69
73
  if (a.tenantId !== b.tenantId)
70
74
  return false;
71
75
  const [x, y] = [a.origin_project ?? null, b.origin_project ?? null];
@@ -0,0 +1,9 @@
1
+ import type { MemoryEntry } from '../memory.js';
2
+ import type { DormantMove } from '../dormant.js';
3
+ import type { FlushComponent } from '../store/delete-and-batch.js';
4
+ /** Components that share no id, ordered by each one's first op across writes, then deletes, then dormant moves. */
5
+ export declare function groupFlush(writes: readonly MemoryEntry[], deletes: readonly string[], dormant: readonly DormantMove[], units: readonly (readonly string[])[]): FlushComponent[];
6
+ /** [child, parent] for each queued op on a child whose DAG parent the run removes: a child's change marks the parent
7
+ * dirty, and one transaction always applied that mark after the parent was gone, so it audited nothing. */
8
+ export declare function familyUnits(writes: readonly MemoryEntry[], deletes: readonly string[], dormant: readonly DormantMove[], snapshot: ReadonlyMap<string, MemoryEntry>): string[][];
9
+ //# sourceMappingURL=flush-units.d.ts.map