@amalgm/chat 0.1.2 → 0.2.1

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 +87 -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 +22 -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 +146 -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 +45 -26
  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,27 @@
1
+ /** One host permission decision shape shared by every native adapter. */
2
+
3
+ export const PERMISSION_OUTCOMES = Object.freeze([
4
+ 'allow_once',
5
+ 'allow_always',
6
+ 'deny',
7
+ 'cancel',
8
+ ]);
9
+
10
+ export async function requestPermission(port, request) {
11
+ if (typeof port !== 'function') return Object.freeze({ outcome: 'deny' });
12
+ const decision = await port(Object.freeze({ ...request }));
13
+ const outcome = decision?.outcome;
14
+ if (!PERMISSION_OUTCOMES.includes(outcome)) {
15
+ throw new TypeError(`permission port returned an invalid outcome: ${String(outcome)}`);
16
+ }
17
+ return Object.freeze({
18
+ outcome,
19
+ ...(decision.updatedInput && typeof decision.updatedInput === 'object'
20
+ ? { updatedInput: decision.updatedInput }
21
+ : {}),
22
+ });
23
+ }
24
+
25
+ export function allowed(decision) {
26
+ return decision.outcome === 'allow_once' || decision.outcome === 'allow_always';
27
+ }
@@ -0,0 +1,146 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { once } from 'node:events';
3
+
4
+ const MAX_EGRESS_REQUEST_BYTES = 64 * 1024 * 1024;
5
+
6
+ const HOP_HEADERS = new Set([
7
+ 'accept-encoding',
8
+ 'connection',
9
+ 'content-encoding',
10
+ 'content-length',
11
+ 'cookie',
12
+ 'dpop',
13
+ 'host',
14
+ 'proxy-authorization',
15
+ 'transfer-encoding',
16
+ ]);
17
+
18
+ function metadataHeaders(contract) {
19
+ return {
20
+ 'x-amalgm-session-id': contract.sessionId,
21
+ 'x-amalgm-message-id': contract.assistantMessageId || '',
22
+ 'x-amalgm-turn-id': contract.assistantMessageId || '',
23
+ 'x-amalgm-agent-id': contract.agentId,
24
+ 'x-amalgm-harness-id': contract.harness,
25
+ 'x-amalgm-auth-method': contract.authMethod,
26
+ 'x-amalgm-model-id': contract.usageModelId || contract.modelId,
27
+ 'x-amalgm-cwd': contract.cwd || '',
28
+ 'x-amalgm-usage-owner': contract.usageOwner,
29
+ };
30
+ }
31
+
32
+ function requestHeaders(request) {
33
+ const headers = {};
34
+ for (const [name, value] of Object.entries(request.headers || {})) {
35
+ const lower = name.toLowerCase();
36
+ if (
37
+ HOP_HEADERS.has(lower)
38
+ || lower === 'authorization'
39
+ || lower === 'x-api-key'
40
+ || lower.startsWith('x-amalgm-')
41
+ ) continue;
42
+ if (value !== undefined) headers[lower] = Array.isArray(value) ? value[0] : value;
43
+ }
44
+ return headers;
45
+ }
46
+
47
+ function responseHeaders(response) {
48
+ const headers = {};
49
+ for (const [name, value] of response.headers.entries()) {
50
+ if (!HOP_HEADERS.has(name.toLowerCase())) headers[name] = value;
51
+ }
52
+ return headers;
53
+ }
54
+
55
+ /** Forward one provider request through the platform proxy for its bound session. */
56
+ export async function forwardPlatformEgress({ request, response, contract, proxy, upstreamPath }) {
57
+ const authorization = String(request.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
58
+ const apiKey = String(request.headers['x-api-key'] || '').trim();
59
+ const presented = authorization || apiKey;
60
+ if (!contract.auth?.tokenRef || !secretMatches(presented, contract.auth.tokenRef)) {
61
+ response.writeHead(401, { 'content-type': 'application/json', 'cache-control': 'no-store' });
62
+ response.end(JSON.stringify({ error: 'Invalid Chat egress token' }));
63
+ return;
64
+ }
65
+ const method = request.method || 'GET';
66
+ const url = `${proxy.baseUrl.replace(/\/$/, '')}${upstreamPath}`;
67
+ const fetchImpl = proxy.fetch || fetch;
68
+ let upstream;
69
+ try {
70
+ const body = ['GET', 'HEAD'].includes(method) ? undefined : await requestBytes(request);
71
+ for (let attempt = 0; attempt < 2; attempt += 1) {
72
+ const grant = await proxy.credentialBroker.authorize({
73
+ audience: 'amalgm-api-proxy',
74
+ scopes: [scopeForPath(upstreamPath)],
75
+ method,
76
+ url,
77
+ });
78
+ upstream = await fetchImpl(url, {
79
+ method,
80
+ headers: {
81
+ ...requestHeaders(request),
82
+ ...metadataHeaders(contract),
83
+ authorization: grant.authorization,
84
+ dpop: grant.dpop,
85
+ },
86
+ body,
87
+ redirect: 'error',
88
+ });
89
+ if (upstream.status !== 401 || attempt === 1) break;
90
+ await upstream.body?.cancel().catch(() => undefined);
91
+ proxy.credentialBroker.invalidate(grant);
92
+ }
93
+ } catch (error) {
94
+ response.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
95
+ response.end(JSON.stringify({
96
+ error: 'Platform authorization unavailable',
97
+ code: typeof error?.code === 'string' ? error.code : 'PLATFORM_AUTHORIZATION_FAILED',
98
+ }));
99
+ return;
100
+ }
101
+ if (!upstream) throw new Error('Platform egress produced no upstream response');
102
+ response.writeHead(upstream.status, responseHeaders(upstream));
103
+ if (upstream.body) {
104
+ const reader = upstream.body.getReader();
105
+ while (true) {
106
+ const { done, value } = await reader.read();
107
+ if (done) break;
108
+ if (value && response.write(Buffer.from(value)) === false) await once(response, 'drain');
109
+ }
110
+ }
111
+ response.end();
112
+ }
113
+
114
+ function scopeForPath(path) {
115
+ const provider = path.match(/^\/([^/?]+)(?:[/?]|$)/)?.[1];
116
+ if (provider === 'anthropic') return 'llm:anthropic';
117
+ if (provider === 'ai_gateway') return 'llm:gateway';
118
+ if (provider === 'google' || provider === 'gemini') return 'llm:google';
119
+ if (provider === 'xai') return 'llm:xai';
120
+ if (provider === 'openai') return 'llm:openai';
121
+ throw Object.assign(new Error('Platform egress route has no authorization scope'), {
122
+ code: 'EGRESS_SCOPE_UNKNOWN',
123
+ });
124
+ }
125
+
126
+ async function requestBytes(request) {
127
+ const chunks = [];
128
+ let bytes = 0;
129
+ for await (const chunk of request) {
130
+ const value = Buffer.from(chunk);
131
+ bytes += value.byteLength;
132
+ if (bytes > MAX_EGRESS_REQUEST_BYTES) {
133
+ throw Object.assign(new Error('Platform egress request exceeds its byte limit'), {
134
+ code: 'EGRESS_REQUEST_TOO_LARGE',
135
+ });
136
+ }
137
+ chunks.push(value);
138
+ }
139
+ return chunks.length > 0 ? Buffer.concat(chunks) : undefined;
140
+ }
141
+
142
+ function secretMatches(presented, expected) {
143
+ const left = Buffer.from(presented);
144
+ const right = Buffer.from(expected);
145
+ return left.byteLength === right.byteLength && timingSafeEqual(left, right);
146
+ }
@@ -0,0 +1,61 @@
1
+ /** Opt-in redacted native-event recorder for adapter diagnostics. */
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { amalgmDir } from './lib/runtime-paths.js';
7
+
8
+ const SENSITIVE_KEY = /(api[-_]?key|authorization|auth|bearer|cookie|password|secret|credential|access[-_]?token|refresh[-_]?token|session[-_]?token|id[-_]?token)/i;
9
+
10
+ function recordEnabled() {
11
+ return process.env.AMALGM_CHAT_RECORD === '1';
12
+ }
13
+
14
+ function recordPath() {
15
+ if (process.env.AMALGM_CHAT_RECORD_PATH) return process.env.AMALGM_CHAT_RECORD_PATH;
16
+ const root = amalgmDir();
17
+ const session = process.env.AMALGM_CHAT_RECORD_SESSION || new Date().toISOString().slice(0, 10);
18
+ return path.join(root, 'chat-recordings', `${session}.ndjson`);
19
+ }
20
+
21
+ function redactString(value) {
22
+ const home = os.homedir();
23
+ let next = String(value);
24
+ if (home) next = next.split(home).join('~');
25
+ next = next.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [redacted]');
26
+ next = next.replace(/\b(?:sk|sk-ant|v0)_[A-Za-z0-9._-]{12,}\b/g, '[redacted-key]');
27
+ return next;
28
+ }
29
+
30
+ export function redact(value, depth = 0) {
31
+ if (depth > 8) return '[redacted-depth]';
32
+ if (value == null) return value;
33
+ if (typeof value === 'string') return redactString(value);
34
+ if (typeof value !== 'object') return value;
35
+ if (Array.isArray(value)) return value.map((entry) => redact(entry, depth + 1));
36
+
37
+ const output = {};
38
+ for (const [key, entry] of Object.entries(value)) {
39
+ output[key] = SENSITIVE_KEY.test(key) ? '[redacted]' : redact(entry, depth + 1);
40
+ }
41
+ return output;
42
+ }
43
+
44
+ export function recordNativeEvent(source, payload, metadata = {}) {
45
+ if (!recordEnabled()) return;
46
+ const file = recordPath();
47
+ const row = {
48
+ recordedAt: new Date().toISOString(),
49
+ source,
50
+ metadata: redact(metadata),
51
+ payload: redact(payload),
52
+ };
53
+ try {
54
+ fs.mkdirSync(path.dirname(file), { recursive: true });
55
+ fs.appendFileSync(file, `${JSON.stringify(row)}\n`);
56
+ } catch (err) {
57
+ if (process.env.AMALGM_CHAT_RECORD_DEBUG === '1') {
58
+ console.warn('[amalgm-chat-recorder] failed:', err.message);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,69 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type {
3
+ AppendTurnUpdateInput,
4
+ AppendTurnUpdatesInput,
5
+ BeginConversationTurnInput,
6
+ ConversationMessage,
7
+ ConversationRecord,
8
+ ConversationScope,
9
+ ConversationStorePort,
10
+ ConversationTurn,
11
+ CreateConversationInput,
12
+ ListConversationsOptions,
13
+ SealConversationTurnInput,
14
+ StoredTurnUpdate,
15
+ ProviderCheckpointRecord,
16
+ } from '../../dist/conversations/index.js';
17
+
18
+ export interface SqliteConversationStoreOptions {
19
+ databasePath?: string;
20
+ database?: Database.Database;
21
+ busyTimeoutMs?: number;
22
+ synchronous?: 'full' | 'normal';
23
+ }
24
+
25
+ export class SqliteConversationStoreError extends Error {
26
+ readonly code: string;
27
+ constructor(code: string, message: string);
28
+ }
29
+
30
+ export function createSqliteJournalBatchPolicy(options?: {
31
+ maxUpdates?: number;
32
+ maxDelayMs?: number;
33
+ }): {
34
+ readonly maxUpdates: number;
35
+ readonly maxDelayMs: number;
36
+ readonly wait: (milliseconds: number) => Promise<void>;
37
+ };
38
+
39
+ export class SqliteConversationStore implements ConversationStorePort {
40
+ readonly database: Database.Database;
41
+ readonly databasePath: string;
42
+ constructor(options: SqliteConversationStoreOptions);
43
+ createConversation(input: CreateConversationInput): Promise<ConversationRecord>;
44
+ getConversation(scope: ConversationScope, conversationId: string): Promise<ConversationRecord | null>;
45
+ getConversationForOwner(ownerUserId: string, conversationId: string, includeArchived?: boolean): {
46
+ readonly scope: ConversationScope;
47
+ readonly record: ConversationRecord;
48
+ } | null;
49
+ listConversations(ownerUserId: string, options?: ListConversationsOptions): Promise<readonly ConversationRecord[]>;
50
+ updateTitle(scope: ConversationScope, conversationId: string, title: string, updatedAt: string): Promise<ConversationRecord>;
51
+ markSeen(scope: ConversationScope, conversationId: string, updatedAt: string): Promise<ConversationRecord>;
52
+ updateProjection(scope: ConversationScope, conversationId: string, input: {
53
+ readonly providerSessionId?: string | null;
54
+ readonly lastExecutionComputerId?: string | null;
55
+ readonly metadata?: Readonly<Record<string, unknown>>;
56
+ }, updatedAt: string): ConversationRecord;
57
+ archiveConversation(scope: ConversationScope, conversationId: string, archivedAt: string): Promise<ConversationRecord>;
58
+ beginTurn(input: BeginConversationTurnInput): Promise<ConversationTurn>;
59
+ appendTurnUpdate(input: AppendTurnUpdateInput): Promise<StoredTurnUpdate>;
60
+ appendTurnUpdates(input: AppendTurnUpdatesInput): Promise<readonly StoredTurnUpdate[]>;
61
+ sealTurn(input: SealConversationTurnInput): Promise<ConversationTurn>;
62
+ getTurn(scope: ConversationScope, turnId: string): Promise<ConversationTurn | null>;
63
+ getLatestTurn(scope: ConversationScope, conversationId: string): Promise<ConversationTurn | null>;
64
+ listTurnUpdates(scope: ConversationScope, turnId: string, afterSequence?: number, limit?: number): Promise<readonly StoredTurnUpdate[]>;
65
+ listMessages(scope: ConversationScope, conversationId: string): Promise<readonly ConversationMessage[]>;
66
+ getProviderCheckpoint(scope: ConversationScope, conversationId: string, executionRevision: string): Promise<ProviderCheckpointRecord | null>;
67
+ recoverInterrupted(ownerUserId: string, recoveredAt: string): Promise<readonly ConversationTurn[]>;
68
+ close(): void;
69
+ }