@amalgm/chat 0.1.2 → 0.2.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 (286) hide show
  1. package/AGENTS.md +98 -0
  2. package/PURPOSE.md +143 -112
  3. package/README.md +82 -54
  4. package/dist/acp/index.d.ts +7 -0
  5. package/dist/acp/index.d.ts.map +1 -0
  6. package/dist/acp/index.js +7 -0
  7. package/dist/acp/index.js.map +1 -0
  8. package/dist/acp/projection.d.ts +30 -0
  9. package/dist/acp/projection.d.ts.map +1 -0
  10. package/dist/acp/projection.js +147 -0
  11. package/dist/acp/projection.js.map +1 -0
  12. package/dist/api/conversations.d.ts +66 -0
  13. package/dist/api/conversations.d.ts.map +1 -0
  14. package/dist/api/conversations.js +202 -0
  15. package/dist/api/conversations.js.map +1 -0
  16. package/dist/api/index.d.ts +3 -3
  17. package/dist/api/index.d.ts.map +1 -1
  18. package/dist/api/index.js +1 -2
  19. package/dist/api/index.js.map +1 -1
  20. package/dist/conversations/index.d.ts +3 -0
  21. package/dist/conversations/index.d.ts.map +1 -0
  22. package/dist/conversations/index.js +3 -0
  23. package/dist/conversations/index.js.map +1 -0
  24. package/dist/conversations/transcript.d.ts +13 -0
  25. package/dist/conversations/transcript.d.ts.map +1 -0
  26. package/dist/conversations/transcript.js +171 -0
  27. package/dist/conversations/transcript.js.map +1 -0
  28. package/dist/conversations/types.d.ts +147 -0
  29. package/dist/conversations/types.d.ts.map +1 -0
  30. package/dist/conversations/types.js +9 -0
  31. package/dist/conversations/types.js.map +1 -0
  32. package/dist/events/constructors.d.ts +9 -15
  33. package/dist/events/constructors.d.ts.map +1 -1
  34. package/dist/events/constructors.js +6 -12
  35. package/dist/events/constructors.js.map +1 -1
  36. package/dist/events/index.d.ts +2 -3
  37. package/dist/events/index.d.ts.map +1 -1
  38. package/dist/events/index.js +2 -3
  39. package/dist/events/index.js.map +1 -1
  40. package/dist/events/schema.d.ts +2 -5
  41. package/dist/events/schema.d.ts.map +1 -1
  42. package/dist/events/schema.js +2 -5
  43. package/dist/events/schema.js.map +1 -1
  44. package/dist/execution/contract.d.ts +51 -0
  45. package/dist/execution/contract.d.ts.map +1 -0
  46. package/dist/execution/contract.js +127 -0
  47. package/dist/execution/contract.js.map +1 -0
  48. package/dist/execution/index.d.ts +6 -0
  49. package/dist/execution/index.d.ts.map +1 -0
  50. package/dist/execution/index.js +4 -0
  51. package/dist/execution/index.js.map +1 -0
  52. package/dist/execution/revision.d.ts +5 -0
  53. package/dist/execution/revision.d.ts.map +1 -0
  54. package/dist/execution/revision.js +17 -0
  55. package/dist/execution/revision.js.map +1 -0
  56. package/dist/execution/store.d.ts +26 -0
  57. package/dist/execution/store.d.ts.map +1 -0
  58. package/dist/execution/store.js +38 -0
  59. package/dist/execution/store.js.map +1 -0
  60. package/dist/index.d.ts +8 -25
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +7 -25
  63. package/dist/index.js.map +1 -1
  64. package/dist/input/index.d.ts +3 -0
  65. package/dist/input/index.d.ts.map +1 -0
  66. package/dist/input/index.js +2 -0
  67. package/dist/input/index.js.map +1 -0
  68. package/dist/input/prompt.d.ts +17 -0
  69. package/dist/input/prompt.d.ts.map +1 -0
  70. package/dist/input/prompt.js +104 -0
  71. package/dist/input/prompt.js.map +1 -0
  72. package/dist/normalizers/claude.d.ts +6 -9
  73. package/dist/normalizers/claude.d.ts.map +1 -1
  74. package/dist/normalizers/claude.js +5 -8
  75. package/dist/normalizers/claude.js.map +1 -1
  76. package/dist/normalizers/codex.d.ts +5 -8
  77. package/dist/normalizers/codex.d.ts.map +1 -1
  78. package/dist/normalizers/codex.js +4 -7
  79. package/dist/normalizers/codex.js.map +1 -1
  80. package/dist/normalizers/cursor.d.ts +1 -2
  81. package/dist/normalizers/cursor.d.ts.map +1 -1
  82. package/dist/normalizers/cursor.js +2 -3
  83. package/dist/normalizers/cursor.js.map +1 -1
  84. package/dist/normalizers/index.d.ts +3 -5
  85. package/dist/normalizers/index.d.ts.map +1 -1
  86. package/dist/normalizers/index.js +3 -5
  87. package/dist/normalizers/index.js.map +1 -1
  88. package/dist/normalizers/opencode.d.ts +2 -4
  89. package/dist/normalizers/opencode.d.ts.map +1 -1
  90. package/dist/normalizers/opencode.js +2 -4
  91. package/dist/normalizers/opencode.js.map +1 -1
  92. package/dist/normalizers/pi.d.ts +3 -4
  93. package/dist/normalizers/pi.d.ts.map +1 -1
  94. package/dist/normalizers/pi.js +3 -4
  95. package/dist/normalizers/pi.js.map +1 -1
  96. package/dist/sessions/chat.d.ts +176 -0
  97. package/dist/sessions/chat.d.ts.map +1 -0
  98. package/dist/sessions/chat.js +701 -0
  99. package/dist/sessions/chat.js.map +1 -0
  100. package/dist/sessions/index.d.ts +3 -0
  101. package/dist/sessions/index.d.ts.map +1 -0
  102. package/dist/sessions/index.js +2 -0
  103. package/dist/sessions/index.js.map +1 -0
  104. package/dist/transcript/index.d.ts +1 -1
  105. package/dist/transcript/index.js +1 -1
  106. package/dist/transcript/message-reducer.d.ts +1 -1
  107. package/dist/transcript/message-reducer.js +1 -1
  108. package/dist/transcript/part-accumulator.d.ts +1 -1
  109. package/dist/transcript/part-accumulator.js +1 -1
  110. package/dist/transcript/tool-display.d.ts +1 -1
  111. package/dist/transcript/tool-display.js +1 -1
  112. package/dist/transcript/tool-shape.d.ts +1 -1
  113. package/dist/transcript/tool-shape.js +1 -1
  114. package/dist/usage/finalize.d.ts +7 -9
  115. package/dist/usage/finalize.d.ts.map +1 -1
  116. package/dist/usage/finalize.js +11 -13
  117. package/dist/usage/finalize.js.map +1 -1
  118. package/dist/usage/index.d.ts +2 -3
  119. package/dist/usage/index.d.ts.map +1 -1
  120. package/dist/usage/index.js +2 -3
  121. package/dist/usage/index.js.map +1 -1
  122. package/dist/usage/model-catalog.d.ts +2 -3
  123. package/dist/usage/model-catalog.d.ts.map +1 -1
  124. package/dist/usage/model-catalog.js +10 -3
  125. package/dist/usage/model-catalog.js.map +1 -1
  126. package/dist/usage/normalize.d.ts +5 -7
  127. package/dist/usage/normalize.d.ts.map +1 -1
  128. package/dist/usage/normalize.js +3 -5
  129. package/dist/usage/normalize.js.map +1 -1
  130. package/dist/usage/ports.d.ts +14 -24
  131. package/dist/usage/ports.d.ts.map +1 -1
  132. package/dist/usage/ports.js +9 -17
  133. package/dist/usage/ports.js.map +1 -1
  134. package/dist/usage/steps.d.ts +70 -0
  135. package/dist/usage/steps.d.ts.map +1 -0
  136. package/dist/usage/steps.js +152 -0
  137. package/dist/usage/steps.js.map +1 -0
  138. package/dist/usage/tokens.d.ts +4 -7
  139. package/dist/usage/tokens.d.ts.map +1 -1
  140. package/dist/usage/tokens.js +4 -7
  141. package/dist/usage/tokens.js.map +1 -1
  142. package/docs/contracts/acp-and-step-usage.md +51 -0
  143. package/docs/contracts/conversation-persistence.md +47 -0
  144. package/docs/contracts/input-and-execution.md +177 -0
  145. package/docs/contracts/platform-authorization.md +17 -0
  146. package/host/adapters/acp-capabilities.js +30 -0
  147. package/host/adapters/acp-client.js +156 -0
  148. package/host/adapters/acp.js +284 -0
  149. package/host/adapters/claude.js +235 -0
  150. package/host/adapters/codex.js +733 -0
  151. package/host/adapters/cursor.js +347 -0
  152. package/host/adapters/input-capabilities.js +89 -0
  153. package/host/adapters/opencode.js +486 -0
  154. package/host/adapters/pi.js +519 -0
  155. package/host/adapters/prompt.js +212 -0
  156. package/host/auth.js +257 -0
  157. package/host/credentials/store.js +209 -0
  158. package/host/http.d.ts +32 -0
  159. package/host/http.js +238 -0
  160. package/host/index.d.ts +84 -0
  161. package/host/index.js +59 -0
  162. package/host/lib/runtime-paths.js +44 -0
  163. package/host/model-resolution.js +313 -0
  164. package/host/native-contract.js +185 -0
  165. package/host/native-runtime.js +194 -0
  166. package/host/permissions.js +27 -0
  167. package/host/platform-egress.js +114 -0
  168. package/host/recorder.js +61 -0
  169. package/host/sqlite/index.d.ts +69 -0
  170. package/host/sqlite/index.js +704 -0
  171. package/host/tooling/mcp-bundle.js +214 -0
  172. package/host/tooling/mcp-relay.js +83 -0
  173. package/host/tooling/native-binaries.js +667 -0
  174. package/host/tooling/native-config.js +460 -0
  175. package/host/tooling/package-import.js +111 -0
  176. package/host/tooling/runtime-home.js +26 -0
  177. package/host/tooling/system-instructions.js +69 -0
  178. package/host/tooling/system-prompt.js +35 -0
  179. package/package.json +44 -24
  180. package/skills/chat/SKILL.md +97 -0
  181. package/skills/chat/agents/openai.yaml +4 -0
  182. package/skills/chat/references/contracts.md +131 -0
  183. package/dist/api/chat-payloads.d.ts +0 -10
  184. package/dist/api/chat-payloads.d.ts.map +0 -1
  185. package/dist/api/chat-payloads.js +0 -57
  186. package/dist/api/chat-payloads.js.map +0 -1
  187. package/dist/contract/chat-input.d.ts +0 -144
  188. package/dist/contract/chat-input.d.ts.map +0 -1
  189. package/dist/contract/chat-input.js +0 -261
  190. package/dist/contract/chat-input.js.map +0 -1
  191. package/dist/contract/create.d.ts +0 -191
  192. package/dist/contract/create.d.ts.map +0 -1
  193. package/dist/contract/create.js +0 -249
  194. package/dist/contract/create.js.map +0 -1
  195. package/dist/contract/freeze.d.ts +0 -85
  196. package/dist/contract/freeze.d.ts.map +0 -1
  197. package/dist/contract/freeze.js +0 -151
  198. package/dist/contract/freeze.js.map +0 -1
  199. package/dist/contract/harnesses.d.ts +0 -21
  200. package/dist/contract/harnesses.d.ts.map +0 -1
  201. package/dist/contract/harnesses.js +0 -39
  202. package/dist/contract/harnesses.js.map +0 -1
  203. package/dist/contract/index.d.ts +0 -17
  204. package/dist/contract/index.d.ts.map +0 -1
  205. package/dist/contract/index.js +0 -17
  206. package/dist/contract/index.js.map +0 -1
  207. package/dist/contract/input.d.ts +0 -45
  208. package/dist/contract/input.d.ts.map +0 -1
  209. package/dist/contract/input.js +0 -78
  210. package/dist/contract/input.js.map +0 -1
  211. package/dist/contract/models.d.ts +0 -57
  212. package/dist/contract/models.d.ts.map +0 -1
  213. package/dist/contract/models.js +0 -380
  214. package/dist/contract/models.js.map +0 -1
  215. package/dist/contract/payload-merge.d.ts +0 -76
  216. package/dist/contract/payload-merge.d.ts.map +0 -1
  217. package/dist/contract/payload-merge.js +0 -215
  218. package/dist/contract/payload-merge.js.map +0 -1
  219. package/dist/events/tool-display.d.ts +0 -22
  220. package/dist/events/tool-display.d.ts.map +0 -1
  221. package/dist/events/tool-display.js +0 -60
  222. package/dist/events/tool-display.js.map +0 -1
  223. package/dist/events/tool-shape.d.ts +0 -48
  224. package/dist/events/tool-shape.d.ts.map +0 -1
  225. package/dist/events/tool-shape.js +0 -225
  226. package/dist/events/tool-shape.js.map +0 -1
  227. package/dist/relay/index.d.ts +0 -65
  228. package/dist/relay/index.d.ts.map +0 -1
  229. package/dist/relay/index.js +0 -197
  230. package/dist/relay/index.js.map +0 -1
  231. package/dist/turns/index.d.ts +0 -11
  232. package/dist/turns/index.d.ts.map +0 -1
  233. package/dist/turns/index.js +0 -11
  234. package/dist/turns/index.js.map +0 -1
  235. package/dist/turns/queries.d.ts +0 -36
  236. package/dist/turns/queries.d.ts.map +0 -1
  237. package/dist/turns/queries.js +0 -37
  238. package/dist/turns/queries.js.map +0 -1
  239. package/dist/turns/replay.d.ts +0 -105
  240. package/dist/turns/replay.d.ts.map +0 -1
  241. package/dist/turns/replay.js +0 -163
  242. package/dist/turns/replay.js.map +0 -1
  243. package/dist/turns/status.d.ts +0 -49
  244. package/dist/turns/status.d.ts.map +0 -1
  245. package/dist/turns/status.js +0 -56
  246. package/dist/turns/status.js.map +0 -1
  247. package/dist/turns/turn-store.d.ts +0 -109
  248. package/dist/turns/turn-store.d.ts.map +0 -1
  249. package/dist/turns/turn-store.js +0 -169
  250. package/dist/turns/turn-store.js.map +0 -1
  251. package/dist/warm/client-cache.d.ts +0 -47
  252. package/dist/warm/client-cache.d.ts.map +0 -1
  253. package/dist/warm/client-cache.js +0 -48
  254. package/dist/warm/client-cache.js.map +0 -1
  255. package/dist/warm/envelope-cache.d.ts +0 -79
  256. package/dist/warm/envelope-cache.d.ts.map +0 -1
  257. package/dist/warm/envelope-cache.js +0 -85
  258. package/dist/warm/envelope-cache.js.map +0 -1
  259. package/dist/warm/index.d.ts +0 -18
  260. package/dist/warm/index.d.ts.map +0 -1
  261. package/dist/warm/index.js +0 -12
  262. package/dist/warm/index.js.map +0 -1
  263. package/dist/warm/payload.d.ts +0 -137
  264. package/dist/warm/payload.d.ts.map +0 -1
  265. package/dist/warm/payload.js +0 -112
  266. package/dist/warm/payload.js.map +0 -1
  267. package/dist/warm/revision.d.ts +0 -70
  268. package/dist/warm/revision.d.ts.map +0 -1
  269. package/dist/warm/revision.js +0 -63
  270. package/dist/warm/revision.js.map +0 -1
  271. package/dist/warm/store-policy.d.ts +0 -57
  272. package/dist/warm/store-policy.d.ts.map +0 -1
  273. package/dist/warm/store-policy.js +0 -89
  274. package/dist/warm/store-policy.js.map +0 -1
  275. package/dist/warm/store.d.ts +0 -42
  276. package/dist/warm/store.d.ts.map +0 -1
  277. package/dist/warm/store.js +0 -69
  278. package/dist/warm/store.js.map +0 -1
  279. package/dist/wire/frames.d.ts +0 -208
  280. package/dist/wire/frames.d.ts.map +0 -1
  281. package/dist/wire/frames.js +0 -245
  282. package/dist/wire/frames.js.map +0 -1
  283. package/dist/wire/index.d.ts +0 -7
  284. package/dist/wire/index.d.ts.map +0 -1
  285. package/dist/wire/index.js +0 -7
  286. package/dist/wire/index.js.map +0 -1
@@ -0,0 +1,704 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ import {
5
+ applyAcpTranscriptUpdate,
6
+ createAcpTranscriptState,
7
+ finalizeAcpTranscript,
8
+ promptContentPlain,
9
+ } from '../../dist/conversations/index.js';
10
+
11
+ const SCHEMA_VERSION = 2;
12
+ const TERMINAL = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
13
+
14
+ export class SqliteConversationStoreError extends Error {
15
+ constructor(code, message) {
16
+ super(message);
17
+ this.name = 'SqliteConversationStoreError';
18
+ this.code = code;
19
+ }
20
+ }
21
+
22
+ /** Recommended bounded content batching for the Chat constructor. */
23
+ export function createSqliteJournalBatchPolicy(options = {}) {
24
+ const maxUpdates = Number.isFinite(options.maxUpdates)
25
+ ? Math.max(1, Math.floor(options.maxUpdates))
26
+ : 64;
27
+ const maxDelayMs = Number.isFinite(options.maxDelayMs)
28
+ ? Math.max(0, Math.floor(options.maxDelayMs))
29
+ : 12;
30
+ return Object.freeze({
31
+ maxUpdates,
32
+ maxDelayMs,
33
+ wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
34
+ });
35
+ }
36
+
37
+ function required(value, label) {
38
+ if (typeof value !== 'string' || !value.trim()) {
39
+ throw new SqliteConversationStoreError('INVALID_CONVERSATION_INPUT', `${label} must be a non-empty string`);
40
+ }
41
+ return value.trim();
42
+ }
43
+
44
+ function parseJson(value, fallback) {
45
+ if (typeof value !== 'string' || !value) return fallback;
46
+ try { return JSON.parse(value); } catch { return fallback; }
47
+ }
48
+
49
+ function stringify(value) {
50
+ return JSON.stringify(value ?? null);
51
+ }
52
+
53
+ function boolean(value) {
54
+ return Number(value) === 1;
55
+ }
56
+
57
+ function conversationFromRow(row) {
58
+ return Object.freeze({
59
+ id: row.id,
60
+ ownerUserId: row.owner_user_id,
61
+ agentInstallationId: row.agent_installation_id,
62
+ agentRevisionId: row.agent_revision_id,
63
+ adapterId: row.adapter_id,
64
+ title: row.title,
65
+ activityStatus: row.activity_status,
66
+ newMessages: boolean(row.new_messages),
67
+ origin: row.origin,
68
+ originId: row.origin_id,
69
+ lastExecutionComputerId: row.last_execution_computer_id,
70
+ providerSessionId: row.provider_session_id,
71
+ metadata: Object.freeze(parseJson(row.metadata_json, {})),
72
+ createdAt: row.created_at,
73
+ updatedAt: row.updated_at,
74
+ lastMessageAt: row.last_message_at,
75
+ archivedAt: row.archived_at,
76
+ });
77
+ }
78
+
79
+ function turnFromRow(row) {
80
+ return Object.freeze({
81
+ id: row.id,
82
+ conversationId: row.conversation_id,
83
+ status: row.status,
84
+ prompt: Object.freeze(parseJson(row.prompt_json, { parts: [] })),
85
+ execution: Object.freeze(parseJson(row.execution_json, {})),
86
+ error: row.error_json ? Object.freeze(parseJson(row.error_json, {})) : null,
87
+ createdAt: row.created_at,
88
+ startedAt: row.started_at,
89
+ completedAt: row.completed_at,
90
+ nextSequence: Number(row.next_sequence),
91
+ });
92
+ }
93
+
94
+ function updateFromRow(row) {
95
+ return Object.freeze({
96
+ conversationId: row.conversation_id,
97
+ turnId: row.turn_id,
98
+ sequence: Number(row.sequence),
99
+ update: Object.freeze(parseJson(row.update_json, {})),
100
+ createdAt: row.created_at,
101
+ });
102
+ }
103
+
104
+ function messageFromRow(row) {
105
+ return Object.freeze({
106
+ id: row.id,
107
+ conversationId: row.conversation_id,
108
+ turnId: row.turn_id,
109
+ role: row.role,
110
+ parts: Object.freeze(parseJson(row.parts_json, [])),
111
+ contentPlain: row.content_plain,
112
+ modelId: row.model_id,
113
+ createdAt: row.created_at,
114
+ updatedAt: row.updated_at,
115
+ });
116
+ }
117
+
118
+ function checkpointFromRow(row) {
119
+ return Object.freeze({
120
+ conversationId: row.conversation_id,
121
+ executionRevision: row.execution_revision,
122
+ adapterId: row.adapter_id,
123
+ checkpoint: Object.freeze(parseJson(row.checkpoint_json, {})),
124
+ updatedAt: row.updated_at,
125
+ });
126
+ }
127
+
128
+ function migrate(database) {
129
+ const version = Number(database.pragma('user_version', { simple: true }));
130
+ if (version > SCHEMA_VERSION) {
131
+ throw new SqliteConversationStoreError(
132
+ 'UNSUPPORTED_SCHEMA_VERSION',
133
+ `Conversation database version ${version} is newer than supported version ${SCHEMA_VERSION}`,
134
+ );
135
+ }
136
+ if (version === SCHEMA_VERSION) return;
137
+ if (version === 1) {
138
+ database.exec(`
139
+ BEGIN IMMEDIATE;
140
+ CREATE INDEX conversations_origin_activity_idx
141
+ ON conversations(owner_user_id, origin, origin_id, archived_at, last_message_at DESC);
142
+ INSERT INTO schema_migrations(version, applied_at)
143
+ VALUES (2, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
144
+ PRAGMA user_version = 2;
145
+ COMMIT;
146
+ `);
147
+ return;
148
+ }
149
+ database.exec(`
150
+ BEGIN IMMEDIATE;
151
+
152
+ CREATE TABLE schema_migrations (
153
+ version INTEGER PRIMARY KEY,
154
+ applied_at TEXT NOT NULL
155
+ );
156
+
157
+ CREATE TABLE conversations (
158
+ id TEXT PRIMARY KEY,
159
+ owner_user_id TEXT NOT NULL,
160
+ agent_installation_id TEXT NOT NULL,
161
+ agent_revision_id TEXT NOT NULL,
162
+ adapter_id TEXT NOT NULL,
163
+ title TEXT NOT NULL,
164
+ activity_status TEXT NOT NULL CHECK(activity_status IN ('in-progress', 'complete', 'error')),
165
+ new_messages INTEGER NOT NULL CHECK(new_messages IN (0, 1)),
166
+ origin TEXT,
167
+ origin_id TEXT,
168
+ last_execution_computer_id TEXT,
169
+ provider_session_id TEXT,
170
+ metadata_json TEXT NOT NULL,
171
+ created_at TEXT NOT NULL,
172
+ updated_at TEXT NOT NULL,
173
+ last_message_at TEXT NOT NULL,
174
+ archived_at TEXT
175
+ );
176
+
177
+ CREATE TABLE turns (
178
+ id TEXT PRIMARY KEY,
179
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
180
+ status TEXT NOT NULL CHECK(status IN ('running', 'completed', 'failed', 'cancelled', 'interrupted')),
181
+ prompt_json TEXT NOT NULL,
182
+ execution_json TEXT NOT NULL,
183
+ error_json TEXT,
184
+ created_at TEXT NOT NULL,
185
+ started_at TEXT NOT NULL,
186
+ completed_at TEXT,
187
+ next_sequence INTEGER NOT NULL DEFAULT 1 CHECK(next_sequence >= 1)
188
+ );
189
+
190
+ CREATE TABLE turn_updates (
191
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
192
+ turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
193
+ sequence INTEGER NOT NULL CHECK(sequence >= 1),
194
+ update_json TEXT NOT NULL,
195
+ created_at TEXT NOT NULL,
196
+ PRIMARY KEY(turn_id, sequence)
197
+ );
198
+
199
+ CREATE TABLE messages (
200
+ id TEXT PRIMARY KEY,
201
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
202
+ turn_id TEXT NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
203
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
204
+ parts_json TEXT NOT NULL,
205
+ content_plain TEXT,
206
+ model_id TEXT,
207
+ created_at TEXT NOT NULL,
208
+ updated_at TEXT NOT NULL,
209
+ UNIQUE(turn_id, role)
210
+ );
211
+
212
+ CREATE TABLE provider_checkpoints (
213
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
214
+ execution_revision TEXT NOT NULL,
215
+ adapter_id TEXT NOT NULL,
216
+ checkpoint_json TEXT NOT NULL,
217
+ updated_at TEXT NOT NULL,
218
+ PRIMARY KEY(conversation_id, execution_revision)
219
+ );
220
+
221
+ CREATE INDEX conversations_owner_activity_idx
222
+ ON conversations(owner_user_id, archived_at, last_message_at DESC);
223
+ CREATE INDEX conversations_agent_activity_idx
224
+ ON conversations(owner_user_id, agent_installation_id, last_message_at DESC);
225
+ CREATE INDEX conversations_origin_activity_idx
226
+ ON conversations(owner_user_id, origin, origin_id, archived_at, last_message_at DESC);
227
+ CREATE INDEX turns_conversation_created_idx
228
+ ON turns(conversation_id, created_at ASC);
229
+ CREATE UNIQUE INDEX turns_one_running_per_conversation_idx
230
+ ON turns(conversation_id) WHERE status = 'running';
231
+ CREATE INDEX turn_updates_conversation_idx
232
+ ON turn_updates(conversation_id, turn_id, sequence ASC);
233
+ CREATE INDEX messages_conversation_created_idx
234
+ ON messages(conversation_id, created_at ASC);
235
+
236
+ INSERT INTO schema_migrations(version, applied_at)
237
+ VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
238
+ INSERT INTO schema_migrations(version, applied_at)
239
+ VALUES (2, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'));
240
+ PRAGMA user_version = 2;
241
+ COMMIT;
242
+ `);
243
+ }
244
+
245
+ function openDatabase(options) {
246
+ if (options.database) return { database: options.database, databasePath: options.databasePath || ':injected:' };
247
+ if (options.databasePath === ':memory:') {
248
+ return { database: new Database(':memory:'), databasePath: ':memory:' };
249
+ }
250
+ const databasePath = path.resolve(required(options.databasePath, 'databasePath'));
251
+ fs.mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
252
+ return { database: new Database(databasePath), databasePath };
253
+ }
254
+
255
+ export class SqliteConversationStore {
256
+ constructor(options = {}) {
257
+ const opened = openDatabase(options);
258
+ this.database = opened.database;
259
+ this.databasePath = opened.databasePath;
260
+ this.closed = false;
261
+ this.database.pragma('journal_mode = WAL');
262
+ this.database.pragma('foreign_keys = ON');
263
+ this.database.pragma(`busy_timeout = ${Number.isFinite(options.busyTimeoutMs) ? Math.max(0, Math.floor(options.busyTimeoutMs)) : 5000}`);
264
+ this.database.pragma(`synchronous = ${options.synchronous === 'normal' ? 'NORMAL' : 'FULL'}`);
265
+ this.database.pragma('wal_autocheckpoint = 1000');
266
+ migrate(this.database);
267
+ this.statements = this.#prepare();
268
+ }
269
+
270
+ #prepare() {
271
+ return {
272
+ conversation: this.database.prepare(`
273
+ SELECT * FROM conversations
274
+ WHERE id = ? AND owner_user_id = ? AND agent_installation_id = ?
275
+ `),
276
+ turn: this.database.prepare(`
277
+ SELECT t.* FROM turns t
278
+ JOIN conversations c ON c.id = t.conversation_id
279
+ WHERE t.id = ? AND c.owner_user_id = ? AND c.agent_installation_id = ?
280
+ `),
281
+ activeTurn: this.database.prepare(`
282
+ SELECT t.* FROM turns t
283
+ JOIN conversations c ON c.id = t.conversation_id
284
+ WHERE c.id = ? AND c.owner_user_id = ? AND c.agent_installation_id = ? AND t.status = 'running'
285
+ LIMIT 1
286
+ `),
287
+ latestTurn: this.database.prepare(`
288
+ SELECT t.* FROM turns t
289
+ JOIN conversations c ON c.id = t.conversation_id
290
+ WHERE c.id = ? AND c.owner_user_id = ? AND c.agent_installation_id = ?
291
+ ORDER BY t.created_at DESC, t.id DESC
292
+ LIMIT 1
293
+ `),
294
+ };
295
+ }
296
+
297
+ #requireConversation(scope, conversationId) {
298
+ const id = required(conversationId, 'conversationId');
299
+ const row = this.statements.conversation.get(
300
+ id,
301
+ required(scope.ownerUserId, 'ownerUserId'),
302
+ required(scope.agentInstallationId, 'agentInstallationId'),
303
+ );
304
+ if (!row) throw new SqliteConversationStoreError('CONVERSATION_NOT_FOUND', `Conversation not found: ${id}`);
305
+ return row;
306
+ }
307
+
308
+ #requireTurn(scope, turnId) {
309
+ const id = required(turnId, 'turnId');
310
+ const row = this.statements.turn.get(
311
+ id,
312
+ required(scope.ownerUserId, 'ownerUserId'),
313
+ required(scope.agentInstallationId, 'agentInstallationId'),
314
+ );
315
+ if (!row) throw new SqliteConversationStoreError('TURN_NOT_FOUND', `Turn not found: ${id}`);
316
+ return row;
317
+ }
318
+
319
+ async createConversation(input) {
320
+ const existing = this.database.prepare('SELECT id FROM conversations WHERE id = ?').get(input.id);
321
+ if (existing) throw new SqliteConversationStoreError('CONVERSATION_EXISTS', `Conversation already exists: ${input.id}`);
322
+ this.database.prepare(`
323
+ INSERT INTO conversations (
324
+ id, owner_user_id, agent_installation_id, agent_revision_id, adapter_id,
325
+ title, activity_status, new_messages, origin, origin_id,
326
+ last_execution_computer_id, metadata_json, created_at, updated_at, last_message_at
327
+ ) VALUES (?, ?, ?, ?, ?, ?, 'complete', 0, ?, ?, ?, ?, ?, ?, ?)
328
+ `).run(
329
+ required(input.id, 'conversation id'),
330
+ required(input.ownerUserId, 'ownerUserId'),
331
+ required(input.agentInstallationId, 'agentInstallationId'),
332
+ required(input.agentRevisionId, 'agentRevisionId'),
333
+ required(input.adapterId, 'adapterId'),
334
+ required(input.title, 'title'),
335
+ input.origin || null,
336
+ input.originId || null,
337
+ required(input.lastExecutionComputerId, 'lastExecutionComputerId'),
338
+ stringify(input.metadata || {}),
339
+ required(input.createdAt, 'createdAt'),
340
+ input.createdAt,
341
+ input.createdAt,
342
+ );
343
+ return conversationFromRow(this.#requireConversation(input, input.id));
344
+ }
345
+
346
+ async getConversation(scope, conversationId) {
347
+ const row = this.statements.conversation.get(
348
+ required(conversationId, 'conversationId'),
349
+ required(scope.ownerUserId, 'ownerUserId'),
350
+ required(scope.agentInstallationId, 'agentInstallationId'),
351
+ );
352
+ return row ? conversationFromRow(row) : null;
353
+ }
354
+
355
+ /** Host-only owner lookup used to recover installation scope at an API boundary. */
356
+ getConversationForOwner(ownerUserId, conversationId, includeArchived = true) {
357
+ const filters = ['id = ?', 'owner_user_id = ?'];
358
+ if (!includeArchived) filters.push('archived_at IS NULL');
359
+ const row = this.database.prepare(`
360
+ SELECT * FROM conversations WHERE ${filters.join(' AND ')} LIMIT 1
361
+ `).get(
362
+ required(conversationId, 'conversationId'),
363
+ required(ownerUserId, 'ownerUserId'),
364
+ );
365
+ if (!row) return null;
366
+ const record = conversationFromRow(row);
367
+ return {
368
+ scope: {
369
+ ownerUserId: record.ownerUserId,
370
+ agentInstallationId: record.agentInstallationId,
371
+ },
372
+ record,
373
+ };
374
+ }
375
+
376
+ async listConversations(ownerUserId, options = {}) {
377
+ const filters = ['owner_user_id = ?'];
378
+ const values = [required(ownerUserId, 'ownerUserId')];
379
+ if (options.agentInstallationId) {
380
+ filters.push('agent_installation_id = ?');
381
+ values.push(required(options.agentInstallationId, 'agentInstallationId'));
382
+ }
383
+ if (options.origin) {
384
+ filters.push('origin = ?');
385
+ values.push(required(options.origin, 'origin'));
386
+ }
387
+ if (options.originId) {
388
+ filters.push('origin_id = ?');
389
+ values.push(required(options.originId, 'originId'));
390
+ }
391
+ if (!options.includeArchived) filters.push('archived_at IS NULL');
392
+ const offset = Number.isFinite(options.offset) ? Math.max(0, Math.floor(options.offset)) : 0;
393
+ const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(500, Math.floor(options.limit))) : 100;
394
+ return this.database.prepare(`
395
+ SELECT * FROM conversations WHERE ${filters.join(' AND ')}
396
+ ORDER BY last_message_at DESC, id ASC LIMIT ? OFFSET ?
397
+ `).all(...values, limit, offset).map(conversationFromRow);
398
+ }
399
+
400
+ async updateTitle(scope, conversationId, title, updatedAt) {
401
+ const row = this.#requireConversation(scope, conversationId);
402
+ this.database.prepare('UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?')
403
+ .run(required(title, 'title'), required(updatedAt, 'updatedAt'), row.id);
404
+ return conversationFromRow(this.#requireConversation(scope, row.id));
405
+ }
406
+
407
+ async markSeen(scope, conversationId, updatedAt) {
408
+ const row = this.#requireConversation(scope, conversationId);
409
+ this.database.prepare('UPDATE conversations SET new_messages = 0, updated_at = ? WHERE id = ?')
410
+ .run(required(updatedAt, 'updatedAt'), row.id);
411
+ return conversationFromRow(this.#requireConversation(scope, row.id));
412
+ }
413
+
414
+ /** Mutable presentation projection; identity and agent revision remain immutable. */
415
+ updateProjection(scope, conversationId, input, updatedAt) {
416
+ const row = this.#requireConversation(scope, conversationId);
417
+ const metadata = input.metadata === undefined
418
+ ? parseJson(row.metadata_json, {})
419
+ : { ...parseJson(row.metadata_json, {}), ...input.metadata };
420
+ this.database.prepare(`
421
+ UPDATE conversations SET
422
+ provider_session_id = ?,
423
+ last_execution_computer_id = COALESCE(?, last_execution_computer_id),
424
+ metadata_json = ?,
425
+ updated_at = ?
426
+ WHERE id = ?
427
+ `).run(
428
+ input.providerSessionId === undefined ? row.provider_session_id : input.providerSessionId,
429
+ input.lastExecutionComputerId === undefined ? null : input.lastExecutionComputerId,
430
+ stringify(metadata),
431
+ required(updatedAt, 'updatedAt'),
432
+ row.id,
433
+ );
434
+ return conversationFromRow(this.#requireConversation(scope, row.id));
435
+ }
436
+
437
+ async archiveConversation(scope, conversationId, archivedAt) {
438
+ const row = this.#requireConversation(scope, conversationId);
439
+ if (this.statements.activeTurn.get(row.id, scope.ownerUserId, scope.agentInstallationId)) {
440
+ throw new SqliteConversationStoreError('CONVERSATION_BUSY', `Conversation has an active turn: ${row.id}`);
441
+ }
442
+ this.database.prepare('UPDATE conversations SET archived_at = ?, updated_at = ? WHERE id = ?')
443
+ .run(required(archivedAt, 'archivedAt'), archivedAt, row.id);
444
+ return conversationFromRow(this.#requireConversation(scope, row.id));
445
+ }
446
+
447
+ async beginTurn(input) {
448
+ const transaction = this.database.transaction(() => {
449
+ const conversation = this.#requireConversation(input, input.conversationId);
450
+ if (conversation.archived_at) {
451
+ throw new SqliteConversationStoreError('CONVERSATION_ARCHIVED', `Conversation is archived: ${conversation.id}`);
452
+ }
453
+ if (conversation.agent_installation_id !== input.execution.agent.installationId) {
454
+ throw new SqliteConversationStoreError('SESSION_AGENT_MISMATCH', `Conversation belongs to a different agent installation: ${conversation.id}`);
455
+ }
456
+ if (this.statements.activeTurn.get(conversation.id, input.ownerUserId, input.agentInstallationId)) {
457
+ throw new SqliteConversationStoreError('CONVERSATION_BUSY', `Conversation has an active turn: ${conversation.id}`);
458
+ }
459
+ this.database.prepare(`
460
+ INSERT INTO turns (
461
+ id, conversation_id, status, prompt_json, execution_json, created_at, started_at, next_sequence
462
+ ) VALUES (?, ?, 'running', ?, ?, ?, ?, 1)
463
+ `).run(
464
+ required(input.turnId, 'turnId'),
465
+ conversation.id,
466
+ stringify(input.prompt),
467
+ stringify(input.execution),
468
+ required(input.createdAt, 'createdAt'),
469
+ input.createdAt,
470
+ );
471
+ this.database.prepare(`
472
+ INSERT INTO messages (
473
+ id, conversation_id, turn_id, role, parts_json, content_plain, model_id, created_at, updated_at
474
+ ) VALUES (?, ?, ?, 'user', ?, ?, NULL, ?, ?)
475
+ `).run(
476
+ required(input.userMessageId, 'userMessageId'),
477
+ conversation.id,
478
+ input.turnId,
479
+ stringify(input.prompt.parts),
480
+ promptContentPlain(input.prompt.parts),
481
+ input.createdAt,
482
+ input.createdAt,
483
+ );
484
+ this.database.prepare(`
485
+ UPDATE conversations SET
486
+ agent_revision_id = ?, adapter_id = ?, activity_status = 'in-progress',
487
+ new_messages = 0, last_execution_computer_id = ?, updated_at = ?, last_message_at = ?
488
+ WHERE id = ?
489
+ `).run(
490
+ input.execution.agent.revisionId,
491
+ input.execution.agent.adapterId,
492
+ input.execution.computerId,
493
+ input.createdAt,
494
+ input.createdAt,
495
+ conversation.id,
496
+ );
497
+ return this.#requireTurn(input, input.turnId);
498
+ });
499
+ try {
500
+ return turnFromRow(transaction());
501
+ } catch (error) {
502
+ if (String(error?.code || '').startsWith('SQLITE_CONSTRAINT')) {
503
+ throw new SqliteConversationStoreError('CONVERSATION_BUSY', `Conversation has an active turn: ${input.conversationId}`);
504
+ }
505
+ throw error;
506
+ }
507
+ }
508
+
509
+ async appendTurnUpdate(input) {
510
+ const updates = await this.appendTurnUpdates({
511
+ ...input,
512
+ updates: [{ update: input.update, createdAt: input.createdAt }],
513
+ });
514
+ return updates[0];
515
+ }
516
+
517
+ async appendTurnUpdates(input) {
518
+ if (!Array.isArray(input.updates) || input.updates.length === 0) {
519
+ throw new SqliteConversationStoreError('INVALID_CONVERSATION_INPUT', 'updates must not be empty');
520
+ }
521
+ const transaction = this.database.transaction(() => {
522
+ const turn = this.#requireTurn(input, input.turnId);
523
+ if (turn.conversation_id !== input.conversationId || turn.status !== 'running') {
524
+ throw new SqliteConversationStoreError('TURN_NOT_RUNNING', `Turn is not running: ${input.turnId}`);
525
+ }
526
+ let sequence = Number(turn.next_sequence);
527
+ const insert = this.database.prepare(`
528
+ INSERT INTO turn_updates (conversation_id, turn_id, sequence, update_json, created_at)
529
+ VALUES (?, ?, ?, ?, ?)
530
+ `);
531
+ const stored = [];
532
+ for (const item of input.updates) {
533
+ insert.run(
534
+ turn.conversation_id,
535
+ turn.id,
536
+ sequence,
537
+ stringify(item.update),
538
+ required(item.createdAt, 'createdAt'),
539
+ );
540
+ stored.push(this.database.prepare(
541
+ 'SELECT * FROM turn_updates WHERE turn_id = ? AND sequence = ?',
542
+ ).get(turn.id, sequence));
543
+ sequence += 1;
544
+ }
545
+ this.database.prepare('UPDATE turns SET next_sequence = ? WHERE id = ?').run(sequence, turn.id);
546
+ return stored;
547
+ });
548
+ return transaction().map(updateFromRow);
549
+ }
550
+
551
+ async sealTurn(input) {
552
+ const transaction = this.database.transaction(() => {
553
+ const turn = this.#requireTurn(input, input.turnId);
554
+ if (turn.conversation_id !== input.conversationId) {
555
+ throw new SqliteConversationStoreError('TURN_NOT_FOUND', `Turn not found: ${input.turnId}`);
556
+ }
557
+ if (TERMINAL.has(turn.status)) return turn;
558
+ const status = required(input.status, 'status');
559
+ if (!TERMINAL.has(status)) {
560
+ throw new SqliteConversationStoreError('INVALID_CONVERSATION_INPUT', `Invalid terminal turn status: ${status}`);
561
+ }
562
+ this.database.prepare(`
563
+ UPDATE turns SET status = ?, error_json = ?, completed_at = ? WHERE id = ? AND status = 'running'
564
+ `).run(status, input.error ? stringify(input.error) : null, required(input.completedAt, 'completedAt'), turn.id);
565
+ this.database.prepare(`
566
+ INSERT INTO messages (
567
+ id, conversation_id, turn_id, role, parts_json, content_plain, model_id, created_at, updated_at
568
+ ) VALUES (?, ?, ?, 'assistant', ?, ?, ?, ?, ?)
569
+ `).run(
570
+ required(input.assistantMessageId, 'assistantMessageId'),
571
+ turn.conversation_id,
572
+ turn.id,
573
+ stringify(input.assistantParts),
574
+ promptContentPlain(input.assistantParts),
575
+ required(input.modelId, 'modelId'),
576
+ input.completedAt,
577
+ input.completedAt,
578
+ );
579
+ this.database.prepare(`
580
+ UPDATE conversations SET activity_status = ?, new_messages = 1,
581
+ updated_at = ?, last_message_at = ?,
582
+ provider_session_id = COALESCE(?, provider_session_id) WHERE id = ?
583
+ `).run(
584
+ status === 'failed' || status === 'interrupted' ? 'error' : 'complete',
585
+ input.completedAt,
586
+ input.completedAt,
587
+ typeof input.providerCheckpoint?.checkpoint?.providerSessionId === 'string'
588
+ ? input.providerCheckpoint.checkpoint.providerSessionId
589
+ : null,
590
+ turn.conversation_id,
591
+ );
592
+ if (input.providerCheckpoint) {
593
+ this.database.prepare(`
594
+ INSERT INTO provider_checkpoints (
595
+ conversation_id, execution_revision, adapter_id, checkpoint_json, updated_at
596
+ ) VALUES (?, ?, ?, ?, ?)
597
+ ON CONFLICT(conversation_id, execution_revision) DO UPDATE SET
598
+ adapter_id = excluded.adapter_id,
599
+ checkpoint_json = excluded.checkpoint_json,
600
+ updated_at = excluded.updated_at
601
+ `).run(
602
+ turn.conversation_id,
603
+ required(input.providerCheckpoint.executionRevision, 'providerCheckpoint.executionRevision'),
604
+ required(input.providerCheckpoint.adapterId, 'providerCheckpoint.adapterId'),
605
+ stringify(input.providerCheckpoint.checkpoint),
606
+ required(input.providerCheckpoint.updatedAt, 'providerCheckpoint.updatedAt'),
607
+ );
608
+ }
609
+ return this.#requireTurn(input, turn.id);
610
+ });
611
+ return turnFromRow(transaction());
612
+ }
613
+
614
+ async getTurn(scope, turnId) {
615
+ const row = this.statements.turn.get(
616
+ required(turnId, 'turnId'),
617
+ required(scope.ownerUserId, 'ownerUserId'),
618
+ required(scope.agentInstallationId, 'agentInstallationId'),
619
+ );
620
+ return row ? turnFromRow(row) : null;
621
+ }
622
+
623
+ async getLatestTurn(scope, conversationId) {
624
+ const row = this.statements.latestTurn.get(
625
+ required(conversationId, 'conversationId'),
626
+ required(scope.ownerUserId, 'ownerUserId'),
627
+ required(scope.agentInstallationId, 'agentInstallationId'),
628
+ );
629
+ return row ? turnFromRow(row) : null;
630
+ }
631
+
632
+ async listTurnUpdates(scope, turnId, afterSequence = 0, limit = 512) {
633
+ const turn = this.#requireTurn(scope, turnId);
634
+ const cursor = Number.isFinite(afterSequence) ? Math.max(0, Math.floor(afterSequence)) : 0;
635
+ const bounded = Number.isFinite(limit) ? Math.max(1, Math.min(2000, Math.floor(limit))) : 512;
636
+ return this.database.prepare(`
637
+ SELECT * FROM turn_updates WHERE turn_id = ? AND sequence > ? ORDER BY sequence ASC LIMIT ?
638
+ `).all(turn.id, cursor, bounded).map(updateFromRow);
639
+ }
640
+
641
+ async listMessages(scope, conversationId) {
642
+ const conversation = this.#requireConversation(scope, conversationId);
643
+ return this.database.prepare(`
644
+ SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at ASC, role DESC
645
+ `).all(conversation.id).map(messageFromRow);
646
+ }
647
+
648
+ async getProviderCheckpoint(scope, conversationId, executionRevision) {
649
+ const conversation = this.#requireConversation(scope, conversationId);
650
+ const row = this.database.prepare(`
651
+ SELECT * FROM provider_checkpoints
652
+ WHERE conversation_id = ? AND execution_revision = ?
653
+ `).get(conversation.id, required(executionRevision, 'executionRevision'));
654
+ return row ? checkpointFromRow(row) : null;
655
+ }
656
+
657
+ async recoverInterrupted(ownerUserId, recoveredAt) {
658
+ const owner = required(ownerUserId, 'ownerUserId');
659
+ const rows = this.database.prepare(`
660
+ SELECT t.*, c.agent_installation_id FROM turns t
661
+ JOIN conversations c ON c.id = t.conversation_id
662
+ WHERE c.owner_user_id = ? AND t.status = 'running'
663
+ ORDER BY t.created_at ASC
664
+ `).all(owner);
665
+ const recovered = [];
666
+ for (const row of rows) {
667
+ const scope = { ownerUserId: owner, agentInstallationId: row.agent_installation_id };
668
+ const state = createAcpTranscriptState();
669
+ let cursor = 0;
670
+ while (true) {
671
+ const updates = await this.listTurnUpdates(scope, row.id, cursor, 512);
672
+ for (const event of updates) {
673
+ applyAcpTranscriptUpdate(state, event.update);
674
+ cursor = event.sequence;
675
+ }
676
+ if (updates.length < 512) break;
677
+ }
678
+ const execution = parseJson(row.execution_json, {});
679
+ const turn = await this.sealTurn({
680
+ ...scope,
681
+ conversationId: row.conversation_id,
682
+ turnId: row.id,
683
+ assistantMessageId: `interrupted:${row.id}`,
684
+ status: 'interrupted',
685
+ assistantParts: finalizeAcpTranscript(state, true),
686
+ modelId: execution?.model?.modelId || 'unknown',
687
+ error: {
688
+ code: 'PROCESS_RESTARTED',
689
+ message: 'The execution host restarted before the provider operation settled.',
690
+ },
691
+ providerCheckpoint: null,
692
+ completedAt: required(recoveredAt, 'recoveredAt'),
693
+ });
694
+ recovered.push(turn);
695
+ }
696
+ return recovered;
697
+ }
698
+
699
+ close() {
700
+ if (this.closed) return;
701
+ this.closed = true;
702
+ this.database.close();
703
+ }
704
+ }