@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -1,3 +1,4 @@
1
+ import { validateStorageKey } from "./keys.js";
1
2
  /** In-memory KV store with expiry. The default for dev and Node. */
2
3
  export function memoryStorage() {
3
4
  const map = new Map();
@@ -12,11 +13,15 @@ export function memoryStorage() {
12
13
  }
13
14
  return e;
14
15
  };
15
- const write = (key, value, ttlSeconds) => {
16
+ const write = (key, value, opts) => {
17
+ const { ttlSeconds, expiresAtMs } = opts ?? {};
18
+ if (expiresAtMs !== undefined && (!Number.isSafeInteger(expiresAtMs) || ttlSeconds !== undefined)) {
19
+ throw new RangeError("expiresAtMs must be a safe integer epoch timestamp without ttlSeconds");
20
+ }
16
21
  if (ttlSeconds !== undefined && !Number.isFinite(ttlSeconds)) {
17
22
  throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
18
23
  }
19
- const exp = ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined;
24
+ const exp = expiresAtMs ?? (ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined);
20
25
  if (exp !== undefined && !Number.isFinite(exp)) {
21
26
  throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
22
27
  }
@@ -36,16 +41,22 @@ export function memoryStorage() {
36
41
  });
37
42
  };
38
43
  return {
44
+ capabilities: { absoluteExpiry: true },
45
+ describe: () => ({ kind: "memory" }),
39
46
  async get(key) {
47
+ validateStorageKey(key);
40
48
  return fresh(key)?.value ?? null;
41
49
  },
42
50
  async set(key, value, opts) {
43
- write(key, value, opts?.ttlSeconds);
51
+ validateStorageKey(key);
52
+ write(key, value, opts);
44
53
  },
45
54
  async delete(key) {
55
+ validateStorageKey(key);
46
56
  map.delete(key);
47
57
  },
48
58
  async list(prefix) {
59
+ validateStorageKey(prefix);
49
60
  return [...map.keys()]
50
61
  .filter((key) => {
51
62
  fresh(key);
@@ -56,12 +67,13 @@ export function memoryStorage() {
56
67
  // Atomic because nothing between the read and the write yields: the body
57
68
  // runs to completion before any other call on this map can start.
58
69
  async compareAndSet(key, expected, next, opts) {
70
+ validateStorageKey(key);
59
71
  if ((fresh(key)?.value ?? null) !== expected)
60
72
  return false;
61
73
  if (next === null)
62
74
  map.delete(key);
63
75
  else
64
- write(key, next, opts?.ttlSeconds);
76
+ write(key, next, opts);
65
77
  return true;
66
78
  },
67
79
  };
@@ -0,0 +1,66 @@
1
+ import type { ActivityStore } from "../activity.js";
2
+ import type { KVStorage } from "../types.js";
3
+ type SqlValue = string | number | null;
4
+ export interface SqlStatement {
5
+ readonly sql: string;
6
+ readonly params: readonly SqlValue[];
7
+ }
8
+ /** What a store needs from a SQLite-dialect database. */
9
+ export interface SqlDriver {
10
+ /** Rows a query returns. */
11
+ all<Row>(statement: SqlStatement): Promise<Row[]>;
12
+ /** Run one write; resolve with the number of rows it changed. */
13
+ run(statement: SqlStatement): Promise<number>;
14
+ /**
15
+ * Run writes in order as one transaction; resolve with the rows each
16
+ * statement changed.
17
+ */
18
+ batch(statements: readonly SqlStatement[]): Promise<number[]>;
19
+ }
20
+ /**
21
+ * The key-value table. Its shape is the one the 0.28 Worker example's
22
+ * `d1-storage.ts` documented, so a D1 database that already holds it is read
23
+ * as is.
24
+ */
25
+ export declare const KV_SCHEMA: readonly string[];
26
+ /** The two drivers, as `describe()` names them. */
27
+ export type SqlKind = "d1" | "sqlite";
28
+ /** `KVStorage` over one `connecta_kv` table. */
29
+ export declare function sqlStorage(driver: SqlDriver, kind: SqlKind): KVStorage;
30
+ /** One entry copied from another store, with its absolute expiry. */
31
+ export interface SqlCopyEntry {
32
+ readonly key: string;
33
+ readonly value: string;
34
+ /** Epoch milliseconds; null never expires. */
35
+ readonly expiresAtMs: number | null;
36
+ }
37
+ /**
38
+ * What a copy did with one entry: written where no live row was, left alone
39
+ * because the live row already held the value, refused because it held
40
+ * another (`conflict`), or replaced under `overwrite` (`overwritten`).
41
+ */
42
+ export type SqlCopyOutcome = "copied" | "unchanged" | "conflict" | "overwritten";
43
+ /**
44
+ * Copy entries into the key-value table, verbatim: same key, same value,
45
+ * same absolute expiry. A live row holding a different value is kept unless
46
+ * `overwrite` is set, so a second copy of the same entries writes nothing.
47
+ * Writes go out in bounded transactions; the outcomes follow `entries`.
48
+ */
49
+ export declare function copyIntoSql(driver: SqlDriver, entries: readonly SqlCopyEntry[], options: {
50
+ overwrite: boolean;
51
+ now: number;
52
+ }): Promise<SqlCopyOutcome[]>;
53
+ export interface SqlActivityOptions {
54
+ /**
55
+ * Days of history kept. Each write removes a bounded batch of older rows,
56
+ * so the table stays near this window without a scheduled job. Default 90.
57
+ */
58
+ retentionDays?: number;
59
+ }
60
+ /**
61
+ * One append-only row per completed downstream call. Rows carry no arguments,
62
+ * results, generated code, or raw error messages — the store never has a
63
+ * payload to leak.
64
+ */
65
+ export declare function sqlActivityStore(driver: SqlDriver, kind: SqlKind, options?: SqlActivityOptions): ActivityStore;
66
+ export {};
@@ -0,0 +1,543 @@
1
+ // One SQL key-value store and one SQL activity store, over two drivers.
2
+ //
3
+ // D1 (`src/d1.ts`) and Node's built-in SQLite (`src/sqlite.ts`) speak the same
4
+ // SQLite dialect, so every statement here runs unchanged on both. A driver is
5
+ // three methods; nothing in this module knows which one it has. It imports no
6
+ // platform module, so the Worker subpath can carry it.
7
+ //
8
+ // Atomicity. Every compare-and-set is one statement. SQLite executes a
9
+ // statement atomically against the database file, and D1 runs one database's
10
+ // statements one at a time against its primary, so a comparison and its write
11
+ // cannot interleave with another request's: of fifty concurrent claims on an
12
+ // absent key, exactly one changes a row.
13
+ //
14
+ // The database clock creates and checks KV expiry inside each statement.
15
+ // Isolate clock skew cannot shorten another holder's TTL. An expired row reads
16
+ // as absent at once and is removed
17
+ // physically by later writes, a bounded batch at a time, so no request starts
18
+ // a background job and no cron is required.
19
+ //
20
+ // Schema. Each store creates its own tables and indexes the first time one of
21
+ // its methods runs in an isolate or process, with idempotent statements, so a
22
+ // deployment applies no migration by hand. The flag that records it is a
23
+ // boolean, never a shared promise: a request that sees it unset runs the
24
+ // statements itself (INV-7).
25
+ import { activityBehaviorFacts, activityPackageVersion, activityClientFact, InvalidActivityCursorError, } from "../activity.js";
26
+ import { assertKnownOptions, ConfigError, keys, optionsOf } from "../config-schema.js";
27
+ import { agentFrictionForCode } from "../activity-friction.js";
28
+ import { validateStorageKey } from "./keys.js";
29
+ const sql = (text, ...params) => ({
30
+ sql: text,
31
+ params,
32
+ });
33
+ /** Select `column`, and beside it `<column>_bytes` when it holds a NUL. */
34
+ const textColumn = (column) => `${column}, CASE WHEN instr(CAST(${column} AS BLOB), x'00') > 0
35
+ THEN CAST(${column} AS BLOB) END AS ${column}_bytes`;
36
+ // Everything written here was a JavaScript string, so the bytes are UTF-8;
37
+ // a stored row that is not is refused rather than read wrong. A leading
38
+ // U+FEFF is part of the value, not a byte order mark.
39
+ const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
40
+ /** A text column's value, from its bytes when the read selected them. */
41
+ function textOf(text, bytes, what) {
42
+ if (bytes == null)
43
+ return text;
44
+ try {
45
+ return utf8.decode(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes));
46
+ }
47
+ catch {
48
+ throw new TypeError(`${what} is not valid UTF-8`);
49
+ }
50
+ }
51
+ /** Run `ddl` once per store object; a failure leaves it to the next call. */
52
+ function schemaOnce(driver, ddl) {
53
+ let ready = false;
54
+ return async () => {
55
+ if (ready)
56
+ return;
57
+ await ddl(driver);
58
+ ready = true;
59
+ };
60
+ }
61
+ // --- key-value ----------------------------------------------------------
62
+ /**
63
+ * The key-value table. Its shape is the one the 0.28 Worker example's
64
+ * `d1-storage.ts` documented, so a D1 database that already holds it is read
65
+ * as is.
66
+ */
67
+ export const KV_SCHEMA = [
68
+ `CREATE TABLE IF NOT EXISTS connecta_kv (
69
+ key TEXT PRIMARY KEY,
70
+ value TEXT NOT NULL,
71
+ expires_at_ms INTEGER
72
+ )`,
73
+ `CREATE INDEX IF NOT EXISTS connecta_kv_expiry
74
+ ON connecta_kv (expires_at_ms)`,
75
+ ];
76
+ /** Expired rows one write removes on its way. */
77
+ const SWEEP_ROWS = 16;
78
+ function ttlMillis(ttlSeconds) {
79
+ if (ttlSeconds !== undefined && !Number.isFinite(ttlSeconds)) {
80
+ throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
81
+ }
82
+ if (!ttlSeconds)
83
+ return null;
84
+ const expiry = ttlSeconds * 1000;
85
+ if (!Number.isFinite(expiry)) {
86
+ throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
87
+ }
88
+ return Math.ceil(expiry);
89
+ }
90
+ /** `KVStorage` over one `connecta_kv` table. */
91
+ export function sqlStorage(driver, kind) {
92
+ const ensure = schemaOnce(driver, async (d) => {
93
+ await d.batch(KV_SCHEMA.map((statement) => sql(statement)));
94
+ });
95
+ // SQLite and D1 evaluate 'now' in the database, never in a caller isolate.
96
+ const now = "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
97
+ const live = `(expires_at_ms IS NULL OR expires_at_ms > ${now})`;
98
+ const expiryOptions = (opts) => {
99
+ if (opts?.expiresAtMs !== undefined) {
100
+ if (!Number.isSafeInteger(opts.expiresAtMs) || opts.ttlSeconds !== undefined) {
101
+ throw new RangeError("expiresAtMs must be a safe integer epoch timestamp without ttlSeconds");
102
+ }
103
+ return { value: opts.expiresAtMs, expression: "?" };
104
+ }
105
+ return { value: ttlMillis(opts?.ttlSeconds), expression: `(${now} + ?)` };
106
+ };
107
+ const current = async (key) => {
108
+ const [row] = await driver.all(sql(`SELECT ${textColumn("value")} FROM connecta_kv WHERE key = ? AND ${live}`, key));
109
+ return row ? textOf(row.value, row.value_bytes, `the stored value of ${JSON.stringify(key)}`) : null;
110
+ };
111
+ return {
112
+ capabilities: { absoluteExpiry: true },
113
+ describe: () => ({ kind }),
114
+ async get(key) {
115
+ validateStorageKey(key);
116
+ await ensure();
117
+ return current(key);
118
+ },
119
+ async set(key, value, opts) {
120
+ validateStorageKey(key);
121
+ await ensure();
122
+ const expiry = expiryOptions(opts);
123
+ await driver.batch([
124
+ sql(`DELETE FROM connecta_kv WHERE key IN (
125
+ SELECT key FROM connecta_kv WHERE expires_at_ms <= ${now} LIMIT ${SWEEP_ROWS}
126
+ )`),
127
+ sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
128
+ VALUES (?, ?, ${expiry.expression})
129
+ ON CONFLICT (key) DO UPDATE SET
130
+ value = excluded.value, expires_at_ms = excluded.expires_at_ms`, key, value, expiry.value),
131
+ ]);
132
+ },
133
+ async delete(key) {
134
+ validateStorageKey(key);
135
+ await ensure();
136
+ await driver.run(sql("DELETE FROM connecta_kv WHERE key = ?", key));
137
+ },
138
+ async list(prefix) {
139
+ validateStorageKey(prefix);
140
+ await ensure();
141
+ const rows = await driver.all(sql(`SELECT key FROM connecta_kv
142
+ WHERE key >= ?
143
+ AND substr(CAST(key AS BLOB), 1, length(CAST(? AS BLOB))) = CAST(? AS BLOB)
144
+ AND ${live}`, prefix, prefix, prefix));
145
+ // Sort in JavaScript: SQLite orders UTF-8 bytes, the contract orders
146
+ // the UTF-16 code units every other adapter sorts by.
147
+ return rows.map((row) => row.key).sort();
148
+ },
149
+ async compareAndSet(key, expected, next, opts) {
150
+ validateStorageKey(key);
151
+ await ensure();
152
+ if (next === null && expected === null) {
153
+ // Nothing to write; the claim holds exactly when no live row exists.
154
+ return (await current(key)) === null;
155
+ }
156
+ if (next === null) {
157
+ return ((await driver.run(sql(`DELETE FROM connecta_kv WHERE key = ? AND ${live} AND value = ?`, key, expected))) > 0);
158
+ }
159
+ let expiry;
160
+ try {
161
+ expiry = expiryOptions(opts);
162
+ }
163
+ catch (error) {
164
+ // Invalid write options do not change a failed comparison's result.
165
+ // Ordinary claims still use one atomic statement without this read.
166
+ if ((await current(key)) !== expected)
167
+ return false;
168
+ throw error;
169
+ }
170
+ if (expected === null) {
171
+ // Insert, or take over a row that has expired. A live row makes the
172
+ // upsert's WHERE false, so nothing changes and the claim is refused.
173
+ return ((await driver.run(sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
174
+ VALUES (?, ?, ${expiry.expression})
175
+ ON CONFLICT (key) DO UPDATE SET
176
+ value = excluded.value, expires_at_ms = excluded.expires_at_ms
177
+ WHERE connecta_kv.expires_at_ms IS NOT NULL
178
+ AND connecta_kv.expires_at_ms <= ${now}`, key, next, expiry.value))) > 0);
179
+ }
180
+ return ((await driver.run(sql(`UPDATE connecta_kv SET value = ?, expires_at_ms = ${expiry.expression}
181
+ WHERE key = ? AND ${live} AND value = ?`, next, expiry.value, key, expected))) > 0);
182
+ },
183
+ };
184
+ }
185
+ /** Keys one read compares: D1 binds at most 100 parameters, one is `now`. */
186
+ const COPY_READ_KEYS = 99;
187
+ /** Writes one transaction carries, by count and by UTF-8 bytes. */
188
+ const COPY_BATCH_STATEMENTS = 50;
189
+ const COPY_BATCH_BYTES = 4 * 1024 * 1024;
190
+ /**
191
+ * Copy entries into the key-value table, verbatim: same key, same value,
192
+ * same absolute expiry. A live row holding a different value is kept unless
193
+ * `overwrite` is set, so a second copy of the same entries writes nothing.
194
+ * Writes go out in bounded transactions; the outcomes follow `entries`.
195
+ */
196
+ export async function copyIntoSql(driver, entries, options) {
197
+ const { overwrite, now } = options;
198
+ for (const entry of entries)
199
+ validateStorageKey(entry.key);
200
+ await driver.batch(KV_SCHEMA.map((statement) => sql(statement)));
201
+ const live = "(expires_at_ms IS NULL OR expires_at_ms > ?)";
202
+ const currentValues = async (keys) => {
203
+ const values = new Map();
204
+ for (let start = 0; start < keys.length; start += COPY_READ_KEYS) {
205
+ const chunk = keys.slice(start, start + COPY_READ_KEYS);
206
+ const rows = await driver.all(sql(`SELECT key, ${textColumn("value")}, expires_at_ms FROM connecta_kv
207
+ WHERE key IN (${chunk.map(() => "?").join(", ")}) AND ${live}`, ...chunk, now));
208
+ // The message names no key: a copy's errors reach operator output.
209
+ for (const row of rows) {
210
+ values.set(row.key, {
211
+ value: textOf(row.value, row.value_bytes, "a stored value") ?? "",
212
+ expiresAtMs: row.expires_at_ms,
213
+ });
214
+ }
215
+ }
216
+ return values;
217
+ };
218
+ const outcomes = Array.from({ length: entries.length });
219
+ const writes = [];
220
+ const existing = await currentValues(entries.map((entry) => entry.key));
221
+ for (const [index, entry] of entries.entries()) {
222
+ const current = existing.get(entry.key);
223
+ if (current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs) {
224
+ outcomes[index] = "unchanged";
225
+ }
226
+ else if (current !== undefined && !overwrite) {
227
+ outcomes[index] = "conflict";
228
+ }
229
+ else {
230
+ outcomes[index] = current === undefined ? "copied" : "overwritten";
231
+ writes.push({
232
+ index,
233
+ bytes: new TextEncoder().encode(entry.key).byteLength + new TextEncoder().encode(entry.value).byteLength + 32,
234
+ statement: overwrite
235
+ ? sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
236
+ VALUES (?, ?, ?)
237
+ ON CONFLICT (key) DO UPDATE SET
238
+ value = excluded.value, expires_at_ms = excluded.expires_at_ms`, entry.key, entry.value, entry.expiresAtMs)
239
+ : // Absent when read; a live row written since is kept, not replaced.
240
+ sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
241
+ VALUES (?, ?, ?)
242
+ ON CONFLICT (key) DO UPDATE SET
243
+ value = excluded.value, expires_at_ms = excluded.expires_at_ms
244
+ WHERE connecta_kv.expires_at_ms IS NOT NULL
245
+ AND connecta_kv.expires_at_ms <= ?`, entry.key, entry.value, entry.expiresAtMs, now),
246
+ });
247
+ }
248
+ }
249
+ const raced = [];
250
+ let batch = [];
251
+ let batchBytes = 0;
252
+ const flush = async () => {
253
+ if (batch.length === 0)
254
+ return;
255
+ const changes = await driver.batch(batch.map((write) => write.statement));
256
+ for (const [position, write] of batch.entries()) {
257
+ if (!(changes[position] > 0))
258
+ raced.push(write.index);
259
+ }
260
+ batch = [];
261
+ batchBytes = 0;
262
+ };
263
+ for (const write of writes) {
264
+ if (batch.length > 0 && (batch.length >= COPY_BATCH_STATEMENTS || batchBytes + write.bytes > COPY_BATCH_BYTES)) {
265
+ await flush();
266
+ }
267
+ batch.push(write);
268
+ batchBytes += write.bytes;
269
+ }
270
+ await flush();
271
+ // A row another writer added between the read and the write: report what
272
+ // it holds, and leave it.
273
+ if (raced.length > 0) {
274
+ const after = await currentValues(raced.map((index) => entries[index].key));
275
+ for (const index of raced) {
276
+ const entry = entries[index];
277
+ const current = after.get(entry.key);
278
+ outcomes[index] =
279
+ current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs ? "unchanged" : "conflict";
280
+ }
281
+ }
282
+ return outcomes;
283
+ }
284
+ // --- activity -----------------------------------------------------------
285
+ /**
286
+ * The activity table, with keyset paging on `(occurred_at_ms, id)`. Its shape
287
+ * is the 0.28 Worker example's `tool_call_activity`; a table created before
288
+ * later actor, friction, approval, client, or package columns existed gets
289
+ * those columns added without changing old rows.
290
+ */
291
+ const ACTIVITY_SCHEMA = [
292
+ `CREATE TABLE IF NOT EXISTS tool_call_activity (
293
+ id TEXT PRIMARY KEY,
294
+ occurred_at_ms INTEGER NOT NULL,
295
+ request_id TEXT NOT NULL,
296
+ actor_kind TEXT NOT NULL,
297
+ actor_id TEXT,
298
+ actor_namespace TEXT,
299
+ connector_id TEXT NOT NULL,
300
+ tool_name TEXT NOT NULL,
301
+ source TEXT NOT NULL,
302
+ outcome TEXT NOT NULL,
303
+ duration_ms INTEGER NOT NULL,
304
+ attempts INTEGER NOT NULL,
305
+ error_code TEXT,
306
+ friction TEXT,
307
+ approval TEXT,
308
+ package_version TEXT,
309
+ server_name TEXT NOT NULL,
310
+ server_version TEXT NOT NULL,
311
+ client_name TEXT,
312
+ client_version TEXT,
313
+ deployment_id TEXT,
314
+ classification TEXT,
315
+ result_bytes INTEGER,
316
+ event_kind TEXT,
317
+ drift_kind TEXT,
318
+ added_tools INTEGER,
319
+ removed_tools INTEGER,
320
+ changed_tools INTEGER,
321
+ pool_name TEXT,
322
+ actor_basis TEXT
323
+ )`,
324
+ `CREATE INDEX IF NOT EXISTS tool_call_activity_recent
325
+ ON tool_call_activity (occurred_at_ms DESC, id DESC)`,
326
+ ];
327
+ /** Columns added after the table first shipped, in the order they arrived. */
328
+ const LATER_ACTIVITY_COLUMNS = [
329
+ "actor_namespace",
330
+ "friction",
331
+ "approval",
332
+ "client_name",
333
+ "client_version",
334
+ "package_version",
335
+ "classification",
336
+ "result_bytes",
337
+ "event_kind",
338
+ "drift_kind",
339
+ "added_tools",
340
+ "removed_tools",
341
+ "changed_tools",
342
+ "pool_name",
343
+ "actor_basis",
344
+ ];
345
+ /** Every activity column but the three integers, read through `textOf`. */
346
+ const ACTIVITY_TEXT_COLUMNS = [
347
+ "id",
348
+ "request_id",
349
+ "actor_kind",
350
+ "actor_id",
351
+ "actor_namespace",
352
+ "connector_id",
353
+ "tool_name",
354
+ "source",
355
+ "outcome",
356
+ "error_code",
357
+ "friction",
358
+ "approval",
359
+ "package_version",
360
+ "server_name",
361
+ "server_version",
362
+ "client_name",
363
+ "client_version",
364
+ "deployment_id",
365
+ "classification",
366
+ "event_kind",
367
+ "drift_kind",
368
+ "pool_name",
369
+ "actor_basis",
370
+ ];
371
+ const ACTIVITY_SELECT = `SELECT occurred_at_ms, duration_ms, attempts, result_bytes, added_tools, removed_tools, changed_tools,
372
+ ${ACTIVITY_TEXT_COLUMNS.map(textColumn).join(",\n ")}
373
+ FROM tool_call_activity`;
374
+ function activityRow(read) {
375
+ const row = {
376
+ occurred_at_ms: read.occurred_at_ms,
377
+ duration_ms: read.duration_ms,
378
+ attempts: read.attempts,
379
+ result_bytes: read.result_bytes,
380
+ added_tools: read.added_tools,
381
+ removed_tools: read.removed_tools,
382
+ changed_tools: read.changed_tools,
383
+ };
384
+ for (const column of ACTIVITY_TEXT_COLUMNS) {
385
+ row[column] = textOf(read[column], read[`${column}_bytes`], `activity column ${column}`);
386
+ }
387
+ return row;
388
+ }
389
+ function rowToEvent(row) {
390
+ // Rows written before `friction` had a column derive it from the code.
391
+ // Friction without a code — an oversized but successful result — exists
392
+ // only in the column, which is why the column exists.
393
+ const friction = row.friction ?? agentFrictionForCode(row.error_code ?? undefined);
394
+ const packageVersion = activityPackageVersion(row.package_version);
395
+ const clientName = activityClientFact(row.client_name, "name");
396
+ const clientVersion = activityClientFact(row.client_version, "version");
397
+ return {
398
+ schemaVersion: 1,
399
+ id: row.id,
400
+ occurredAt: new Date(row.occurred_at_ms).toISOString(),
401
+ requestId: row.request_id,
402
+ ...activityBehaviorFacts({
403
+ classification: row.classification,
404
+ resultBytes: row.result_bytes,
405
+ kind: row.event_kind,
406
+ pool: row.pool_name,
407
+ actorBasis: row.actor_basis,
408
+ drift: {
409
+ kind: row.drift_kind,
410
+ addedTools: row.added_tools,
411
+ removedTools: row.removed_tools,
412
+ changedTools: row.changed_tools,
413
+ },
414
+ }),
415
+ actor: {
416
+ kind: row.actor_kind,
417
+ ...(row.actor_id ? { id: row.actor_id } : {}),
418
+ ...(row.actor_namespace ? { namespace: row.actor_namespace } : {}),
419
+ },
420
+ connectorId: row.connector_id,
421
+ toolName: row.tool_name,
422
+ address: `${row.connector_id}.${row.tool_name}`,
423
+ source: row.source,
424
+ outcome: row.outcome,
425
+ durationMs: row.duration_ms,
426
+ attempts: row.attempts,
427
+ ...(row.error_code ? { errorCode: row.error_code } : {}),
428
+ ...(friction ? { friction } : {}),
429
+ ...(row.approval ? { approval: row.approval } : {}),
430
+ ...(packageVersion !== undefined ? { packageVersion } : {}),
431
+ serverName: row.server_name,
432
+ serverVersion: row.server_version,
433
+ ...(clientName !== undefined ? { clientName } : {}),
434
+ ...(clientVersion !== undefined ? { clientVersion } : {}),
435
+ ...(row.deployment_id ? { deploymentId: row.deployment_id } : {}),
436
+ };
437
+ }
438
+ function encodeCursor(row) {
439
+ return btoa(`${row.occurred_at_ms}:${row.id}`);
440
+ }
441
+ function decodeCursor(value) {
442
+ let decoded;
443
+ try {
444
+ decoded = atob(value);
445
+ }
446
+ catch {
447
+ throw new InvalidActivityCursorError();
448
+ }
449
+ const separator = decoded.indexOf(":");
450
+ const occurredAtMs = Number(decoded.slice(0, separator));
451
+ const id = decoded.slice(separator + 1);
452
+ if (separator < 1 || !Number.isSafeInteger(occurredAtMs) || occurredAtMs < 0 || !/^[0-9a-f-]{36}$/i.test(id)) {
453
+ throw new InvalidActivityCursorError();
454
+ }
455
+ return { occurredAtMs, id };
456
+ }
457
+ /** The closed options both activity factories accept; see `assertKnownOptions`. */
458
+ const SQL_ACTIVITY_OPTIONS = optionsOf()(keys("retentionDays"));
459
+ /** Older rows one write removes on its way. Above one: writes catch up. */
460
+ const RETENTION_SWEEP_ROWS = 16;
461
+ /**
462
+ * One append-only row per completed downstream call. Rows carry no arguments,
463
+ * results, generated code, or raw error messages — the store never has a
464
+ * payload to leak.
465
+ */
466
+ export function sqlActivityStore(driver, kind, options = {}) {
467
+ const factory = kind === "d1" ? "d1ActivityStore()" : "sqliteActivityStore()";
468
+ const { retentionDays = 90 } = assertKnownOptions(options, factory, SQL_ACTIVITY_OPTIONS);
469
+ if (typeof retentionDays !== "number" || !(retentionDays > 0) || !Number.isFinite(retentionDays)) {
470
+ throw new ConfigError(`${factory}.retentionDays must be a positive number of days.`);
471
+ }
472
+ const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
473
+ const ensure = schemaOnce(driver, async (d) => {
474
+ await d.batch(ACTIVITY_SCHEMA.map((statement) => sql(statement)));
475
+ const columns = async () => new Set((await d.all(sql("PRAGMA table_info(tool_call_activity)"))).map((column) => column.name));
476
+ // Isolates and processes upgrade an old table concurrently, each from its
477
+ // own read of the columns. One column per statement, and a refused ALTER
478
+ // re-reads the table: if another upgrader added the column, this one
479
+ // moves on to the next instead of failing the write that started it.
480
+ let present = await columns();
481
+ for (const name of LATER_ACTIVITY_COLUMNS) {
482
+ if (present.has(name))
483
+ continue;
484
+ try {
485
+ await d.run(sql(`ALTER TABLE tool_call_activity ADD COLUMN ${name} ${["result_bytes", "added_tools", "removed_tools", "changed_tools"].includes(name) ? "INTEGER" : "TEXT"}`));
486
+ }
487
+ catch (error) {
488
+ present = await columns();
489
+ if (!present.has(name))
490
+ throw error;
491
+ }
492
+ }
493
+ });
494
+ return {
495
+ describe: () => ({ kind, retentionDays }),
496
+ async record(event) {
497
+ await ensure();
498
+ const occurredAtMs = Date.parse(event.occurredAt);
499
+ const facts = activityBehaviorFacts(event);
500
+ await driver.batch([
501
+ sql(`INSERT INTO tool_call_activity (
502
+ id, occurred_at_ms, request_id, actor_kind, actor_id,
503
+ actor_namespace, connector_id, tool_name, source, outcome,
504
+ duration_ms, attempts, error_code, friction, approval,
505
+ package_version, server_name, server_version, client_name, client_version, deployment_id,
506
+ classification, result_bytes, event_kind, drift_kind, added_tools, removed_tools, changed_tools, pool_name, actor_basis
507
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, event.id, occurredAtMs, event.requestId, event.actor.kind, event.actor.id ?? null, event.actor.namespace ?? null, event.connectorId, event.toolName, event.source, event.outcome, event.durationMs, event.attempts, event.errorCode ?? null,
508
+ // Stored beside the code rather than derived from it: a truncated
509
+ // result is friction on a call that succeeded, and
510
+ // `error_code IS NOT NULL` stays an honest count of failures.
511
+ event.friction ?? null, event.approval ?? null, activityPackageVersion(event.packageVersion) ?? null, event.serverName, event.serverVersion, activityClientFact(event.clientName, "name") ?? null, activityClientFact(event.clientVersion, "version") ?? null, event.deploymentId ?? null, facts.classification ?? null, facts.resultBytes ?? null, facts.kind ?? null, facts.drift?.kind ?? null, facts.drift?.addedTools ?? null, facts.drift?.removedTools ?? null, facts.drift?.changedTools ?? null, facts.pool ?? null, facts.actorBasis ?? null),
512
+ sql(`DELETE FROM tool_call_activity WHERE id IN (
513
+ SELECT id FROM tool_call_activity
514
+ WHERE occurred_at_ms < ?
515
+ ORDER BY occurred_at_ms ASC
516
+ LIMIT ${RETENTION_SWEEP_ROWS}
517
+ )`, Date.now() - retentionMs),
518
+ ]);
519
+ },
520
+ async list({ cursor, limit }) {
521
+ const boundedLimit = Math.min(100, Math.max(1, Math.trunc(limit)));
522
+ const pageSize = boundedLimit + 1;
523
+ const position = cursor ? decodeCursor(cursor) : undefined;
524
+ await ensure();
525
+ const rows = (await driver.all(position
526
+ ? sql(`${ACTIVITY_SELECT}
527
+ WHERE occurred_at_ms < ?
528
+ OR (occurred_at_ms = ? AND id < ?)
529
+ ORDER BY occurred_at_ms DESC, id DESC
530
+ LIMIT ?`, position.occurredAtMs, position.occurredAtMs, position.id, pageSize)
531
+ : sql(`${ACTIVITY_SELECT}
532
+ ORDER BY occurred_at_ms DESC, id DESC
533
+ LIMIT ?`, pageSize))).map(activityRow);
534
+ const hasMore = rows.length > boundedLimit;
535
+ const visible = hasMore ? rows.slice(0, boundedLimit) : rows;
536
+ const last = visible.at(-1);
537
+ return {
538
+ events: visible.map(rowToEvent),
539
+ ...(hasMore && last ? { nextCursor: encodeCursor(last) } : {}),
540
+ };
541
+ },
542
+ };
543
+ }