@desplega.ai/agent-swarm 1.157.0 → 1.158.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 (256) hide show
  1. package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
  29. package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +29 -15
  47. package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
  59. package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +2 -1
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/modelsdev-reasoning.json +551 -0
  149. package/src/rbac/legacy-policy.ts +16 -0
  150. package/src/rbac/permissions.ts +4 -0
  151. package/src/scheduler/schedule-task.ts +3 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  154. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  155. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  156. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  157. package/src/server.ts +12 -7
  158. package/src/slack/app.ts +35 -4
  159. package/src/slack/connection-state.ts +17 -0
  160. package/src/tasks/context-key.ts +1 -0
  161. package/src/tasks/worker-follow-up.ts +6 -2
  162. package/src/tests/agent-task-activity.test.ts +74 -0
  163. package/src/tests/agents-list-model-display.test.ts +6 -1
  164. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  165. package/src/tests/approval-requests.test.ts +395 -0
  166. package/src/tests/approval-sweeps-config.test.ts +47 -0
  167. package/src/tests/approval-sweeps.test.ts +508 -0
  168. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  169. package/src/tests/apps-spike.test.ts +56 -1
  170. package/src/tests/apps-sync-engine.test.ts +20 -9
  171. package/src/tests/boot-seeds.test.ts +170 -0
  172. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  173. package/src/tests/claude-managed-adapter.test.ts +6 -0
  174. package/src/tests/config-key-filter.test.ts +80 -0
  175. package/src/tests/context-window.test.ts +1 -0
  176. package/src/tests/db-client-busy-retry.test.ts +96 -0
  177. package/src/tests/db-client.test.ts +60 -1
  178. package/src/tests/db-task-read-contract.test.ts +10 -0
  179. package/src/tests/db-task-write-contract.test.ts +9 -0
  180. package/src/tests/docs-index-footprint.test.js +3 -3
  181. package/src/tests/events-http.test.ts +103 -0
  182. package/src/tests/extensions-catalog.test.ts +22 -17
  183. package/src/tests/extensions-pre-task.test.ts +107 -0
  184. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  185. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  186. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  187. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  188. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  189. package/src/tests/heartbeat.test.ts +99 -3
  190. package/src/tests/hook-mcp-config.test.ts +29 -0
  191. package/src/tests/http-api-integration.test.ts +16 -2
  192. package/src/tests/kv-storage.test.ts +62 -0
  193. package/src/tests/linear-outbound-sync.test.ts +120 -5
  194. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  195. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  196. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  197. package/src/tests/memory-hybrid.test.ts +161 -1
  198. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  199. package/src/tests/memory.test.ts +22 -0
  200. package/src/tests/onboard-container-engine.test.ts +240 -0
  201. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  202. package/src/tests/pricing-routes.test.ts +14 -0
  203. package/src/tests/profile-sync-warning.test.ts +8 -4
  204. package/src/tests/promote-draft-task-route.test.ts +19 -0
  205. package/src/tests/provider-registration.test.ts +296 -42
  206. package/src/tests/rbac-engine.test.ts +30 -0
  207. package/src/tests/reasoning-effort.test.ts +8 -0
  208. package/src/tests/script-connections.test.ts +7 -0
  209. package/src/tests/sdk-allowlist.test.ts +39 -0
  210. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  211. package/src/tests/sessions-detail-batch.test.ts +250 -0
  212. package/src/tests/sessions.test.ts +118 -0
  213. package/src/tests/slack-config.test.ts +22 -0
  214. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  215. package/src/tests/status.test.ts +36 -1
  216. package/src/tests/swarm-diff.test.ts +316 -10
  217. package/src/tests/task-completion-idempotency.test.ts +2 -1
  218. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  219. package/src/tests/tool-annotations.test.ts +1 -1
  220. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  221. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  222. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  223. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  224. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  225. package/src/tests/workflow-http-v2.test.ts +99 -8
  226. package/src/tests/workflow-retry-validation.test.ts +56 -0
  227. package/src/tests/workflow-tla-races.test.ts +389 -0
  228. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  229. package/src/tools/cancel-approval-request.ts +57 -0
  230. package/src/tools/memory-search.ts +1 -0
  231. package/src/tools/request-human-input.ts +4 -2
  232. package/src/tools/send-task.ts +4 -0
  233. package/src/tools/templates.ts +19 -0
  234. package/src/tools/tool-config.ts +2 -1
  235. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  236. package/src/types.ts +38 -0
  237. package/src/utils/context-window.ts +1 -0
  238. package/src/workflows/approval-cancel.ts +152 -0
  239. package/src/workflows/approval-notifications.ts +84 -0
  240. package/src/workflows/checkpoint.ts +10 -5
  241. package/src/workflows/definition.ts +21 -0
  242. package/src/workflows/engine.ts +152 -21
  243. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  244. package/src/workflows/recovery.ts +100 -45
  245. package/src/workflows/resume.ts +49 -18
  246. package/src/workflows/retry-poller.ts +71 -21
  247. package/templates/extensions/README.md +1 -0
  248. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  249. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  250. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  251. package/templates/skills/apps/content.md +20 -1
  252. package/templates/skills/code-quality/content.md +26 -1
  253. package/templates/skills/pages/content.md +16 -4
  254. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  255. package/dist/cli-2daksqps.js +0 -378
  256. package/dist/rbac-roles-3gxmnb20.js +0 -37
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.157.0",
3
+ "version": "1.158.0",
4
4
  "description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
5
5
  "keywords": [
6
6
  "agent-swarm.dev",
@@ -203,6 +203,7 @@
203
203
  "@opencode-ai/sdk": "^1.18.32",
204
204
  "@openfort/openfort-node": "^0.9.1",
205
205
  "@opentelemetry/api": "^1.9.1",
206
+ "@opentelemetry/api-logs": "^0.218.0",
206
207
  "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
207
208
  "@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
208
209
  "@opentelemetry/resources": "^2.7.1",
@@ -21,6 +21,9 @@
21
21
  "Form",
22
22
  "Drawer",
23
23
  "DetailList",
24
+ "Kanban",
25
+ "Calendar",
26
+ "ContributionGraph",
24
27
  "ElementRef",
25
28
  "ElementSlot"
26
29
  ],
@@ -842,6 +845,232 @@
842
845
  "additionalProperties": false
843
846
  }
844
847
  },
848
+ "Kanban": {
849
+ "description": "Kanban board: one card per row, grouped into `columns` by the row's `columnField`. Bind `data`/`loading` to a named query. `titleField` is the card title; `cardFields` add label/value lines with Table column kinds (badge tones, relative dates). Dragging a card to another column runs `onMove` with `$row` = the moved row, `columnField` already set to the new column — typically `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"<m>\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"<columnField>\": { \"$row\": \"<columnField>\" } } } }]`. Without `onMove` the board is read-only. `onCardClick` runs with `$row` (e.g. an `app.navigate` that opens a Drawer).",
850
+ "props": {
851
+ "$schema": "http://json-schema.org/draft-07/schema#",
852
+ "type": "object",
853
+ "properties": {
854
+ "data": {
855
+ "type": "array",
856
+ "items": {
857
+ "type": "object",
858
+ "propertyNames": {
859
+ "type": "string"
860
+ },
861
+ "additionalProperties": {}
862
+ }
863
+ },
864
+ "loading": {
865
+ "type": "boolean"
866
+ },
867
+ "columnField": {
868
+ "type": "string"
869
+ },
870
+ "columns": {
871
+ "minItems": 1,
872
+ "type": "array",
873
+ "items": {
874
+ "anyOf": [
875
+ {
876
+ "type": "string"
877
+ },
878
+ {
879
+ "type": "object",
880
+ "properties": {
881
+ "id": {
882
+ "type": "string"
883
+ },
884
+ "label": {
885
+ "type": "string"
886
+ }
887
+ },
888
+ "required": ["id"],
889
+ "additionalProperties": false
890
+ }
891
+ ]
892
+ }
893
+ },
894
+ "titleField": {
895
+ "type": "string"
896
+ },
897
+ "cardFields": {
898
+ "type": "array",
899
+ "items": {
900
+ "type": "object",
901
+ "properties": {
902
+ "key": {
903
+ "type": "string"
904
+ },
905
+ "label": {
906
+ "type": "string"
907
+ },
908
+ "kind": {
909
+ "type": "string",
910
+ "enum": ["text", "string", "number", "boolean", "date", "badge", "enum", "code"]
911
+ },
912
+ "tones": {
913
+ "type": "object",
914
+ "propertyNames": {
915
+ "type": "string"
916
+ },
917
+ "additionalProperties": {
918
+ "type": "string",
919
+ "enum": [
920
+ "neutral",
921
+ "success",
922
+ "active",
923
+ "error",
924
+ "info",
925
+ "pending",
926
+ "warning",
927
+ "paused"
928
+ ]
929
+ }
930
+ }
931
+ },
932
+ "required": ["key"],
933
+ "additionalProperties": false
934
+ }
935
+ },
936
+ "idField": {
937
+ "type": "string"
938
+ },
939
+ "onMove": {
940
+ "type": "array",
941
+ "items": {
942
+ "type": "object",
943
+ "properties": {
944
+ "action": {
945
+ "type": "string"
946
+ },
947
+ "params": {
948
+ "type": "object",
949
+ "propertyNames": {
950
+ "type": "string"
951
+ },
952
+ "additionalProperties": {}
953
+ }
954
+ },
955
+ "required": ["action"],
956
+ "additionalProperties": false
957
+ }
958
+ },
959
+ "onCardClick": {
960
+ "type": "array",
961
+ "items": {
962
+ "type": "object",
963
+ "properties": {
964
+ "action": {
965
+ "type": "string"
966
+ },
967
+ "params": {
968
+ "type": "object",
969
+ "propertyNames": {
970
+ "type": "string"
971
+ },
972
+ "additionalProperties": {}
973
+ }
974
+ },
975
+ "required": ["action"],
976
+ "additionalProperties": false
977
+ }
978
+ }
979
+ },
980
+ "required": ["columnField", "columns", "titleField"],
981
+ "additionalProperties": false
982
+ }
983
+ },
984
+ "Calendar": {
985
+ "description": "Read-only month calendar of rows. `startField` (and optional `endField`) hold dates; an event shows on every day it spans. `titleField` labels it. Has its own prev/next month controls; each Calendar keeps its own month. `onSelect` runs when an event is clicked, with `$row` (e.g. `app.navigate` to open a Drawer).",
986
+ "props": {
987
+ "$schema": "http://json-schema.org/draft-07/schema#",
988
+ "type": "object",
989
+ "properties": {
990
+ "data": {
991
+ "type": "array",
992
+ "items": {
993
+ "type": "object",
994
+ "propertyNames": {
995
+ "type": "string"
996
+ },
997
+ "additionalProperties": {}
998
+ }
999
+ },
1000
+ "loading": {
1001
+ "type": "boolean"
1002
+ },
1003
+ "startField": {
1004
+ "type": "string"
1005
+ },
1006
+ "endField": {
1007
+ "type": "string"
1008
+ },
1009
+ "titleField": {
1010
+ "type": "string"
1011
+ },
1012
+ "idField": {
1013
+ "type": "string"
1014
+ },
1015
+ "onSelect": {
1016
+ "type": "array",
1017
+ "items": {
1018
+ "type": "object",
1019
+ "properties": {
1020
+ "action": {
1021
+ "type": "string"
1022
+ },
1023
+ "params": {
1024
+ "type": "object",
1025
+ "propertyNames": {
1026
+ "type": "string"
1027
+ },
1028
+ "additionalProperties": {}
1029
+ }
1030
+ },
1031
+ "required": ["action"],
1032
+ "additionalProperties": false
1033
+ }
1034
+ }
1035
+ },
1036
+ "required": ["startField", "titleField"],
1037
+ "additionalProperties": false
1038
+ }
1039
+ },
1040
+ "ContributionGraph": {
1041
+ "description": "GitHub-style activity heatmap over the trailing `days` (default 365). Buckets rows by the calendar day of `dateField` (default `date`); each row counts 1, or sums `countField` when set. Read-only.",
1042
+ "props": {
1043
+ "$schema": "http://json-schema.org/draft-07/schema#",
1044
+ "type": "object",
1045
+ "properties": {
1046
+ "data": {
1047
+ "type": "array",
1048
+ "items": {
1049
+ "type": "object",
1050
+ "propertyNames": {
1051
+ "type": "string"
1052
+ },
1053
+ "additionalProperties": {}
1054
+ }
1055
+ },
1056
+ "loading": {
1057
+ "type": "boolean"
1058
+ },
1059
+ "dateField": {
1060
+ "type": "string"
1061
+ },
1062
+ "countField": {
1063
+ "type": "string"
1064
+ },
1065
+ "days": {
1066
+ "type": "integer",
1067
+ "minimum": 7,
1068
+ "maximum": 730
1069
+ }
1070
+ },
1071
+ "additionalProperties": false
1072
+ }
1073
+ },
845
1074
  "ElementRef": {
846
1075
  "description": "Reference a reusable element from this app or an exported element from another app. Consumer children are inserted at the target's ElementSlot.",
847
1076
  "slots": ["default"],
@@ -43,6 +43,24 @@ export const ELEMENT_KEYS = new Set([
43
43
  "watch",
44
44
  ]);
45
45
  const UI_STATE_COMPONENTS = new Set(["SearchInput", "Select", "Tabs"]);
46
+
47
+ /**
48
+ * Props that hold a plain action chain (an array of `{ action, params }`),
49
+ * keyed by component type. Table's `rowActions[].actions` is nested one level
50
+ * deeper and handled separately.
51
+ */
52
+ const PROP_ACTION_CHAINS: Record<string, readonly string[]> = {
53
+ Form: ["onSubmit"],
54
+ Kanban: ["onMove", "onCardClick"],
55
+ Calendar: ["onSelect"],
56
+ };
57
+
58
+ /** Props naming a row field of the bound `data` query's model, per type. */
59
+ const DATA_FIELD_PROPS: Record<string, readonly string[]> = {
60
+ Kanban: ["columnField", "titleField", "idField"],
61
+ Calendar: ["startField", "endField", "titleField", "idField"],
62
+ ContributionGraph: ["dateField", "countField"],
63
+ };
46
64
  const CONDITION_KEYS = new Set(["eq", "neq", "gt", "gte", "lt", "lte", "not"]);
47
65
 
48
66
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -705,8 +723,8 @@ function visitPageActionSteps(
705
723
  );
706
724
  }
707
725
  }
708
- if (rawElement.type === "Form") {
709
- visitActionChain(rawElement.props.onSubmit, `${elementPath}.props.onSubmit`, visit, false);
726
+ for (const prop of PROP_ACTION_CHAINS[String(rawElement.type)] ?? []) {
727
+ visitActionChain(rawElement.props[prop], `${elementPath}.props.${prop}`, visit, false);
710
728
  }
711
729
  }
712
730
  }
@@ -1234,7 +1252,7 @@ export function validatePage(
1234
1252
  if (component) {
1235
1253
  const propsPath = appendPath(elementPath, "props");
1236
1254
  const actionChainPath = (path: string): boolean =>
1237
- (type === "Form" && path === `${propsPath}.onSubmit`) ||
1255
+ (PROP_ACTION_CHAINS[String(type)] ?? []).some((prop) => path === `${propsPath}.${prop}`) ||
1238
1256
  (type === "Table" && /^.+\.rowActions\.\d+\.actions$/.test(path));
1239
1257
  const propsResult = validateSchema(
1240
1258
  Object.hasOwn(rawElement, "props") ? rawElement.props : {},
@@ -1331,6 +1349,44 @@ export function validatePage(
1331
1349
  if (found) issues.push(found);
1332
1350
  }
1333
1351
  }
1352
+ } else if (typeof type === "string" && Object.hasOwn(DATA_FIELD_PROPS, type)) {
1353
+ // Kanban / Calendar / ContributionGraph name row fields of the bound
1354
+ // query's model (`columnField`, `startField`, ...) — a typo'd name
1355
+ // would silently render an empty board/grid, so cross-check it.
1356
+ const queryModel = queryModelFromDataBinding(definition, rawElement.props.data);
1357
+ if (queryModel) {
1358
+ const props = rawElement.props;
1359
+ const fieldNames: [string, unknown][] = (DATA_FIELD_PROPS[type] ?? []).map((prop) => [
1360
+ `${elementPath}.props.${prop}`,
1361
+ props[prop],
1362
+ ]);
1363
+ if (type === "Kanban" && Array.isArray(props.cardFields)) {
1364
+ for (const [index, field] of props.cardFields.entries()) {
1365
+ if (isPlainObject(field)) {
1366
+ fieldNames.push([`${elementPath}.props.cardFields.${index}.key`, field.key]);
1367
+ }
1368
+ }
1369
+ }
1370
+ for (const [fieldPath, fieldName] of fieldNames) {
1371
+ if (typeof fieldName !== "string") continue;
1372
+ const found = fieldBindingIssue(
1373
+ queryModel.modelName,
1374
+ queryModel.model,
1375
+ fieldName,
1376
+ fieldPath,
1377
+ );
1378
+ if (found) issues.push(found);
1379
+ }
1380
+ for (const prop of PROP_ACTION_CHAINS[type] ?? []) {
1381
+ collectRowFieldIssues(
1382
+ props[prop],
1383
+ `${elementPath}.props.${prop}`,
1384
+ queryModel.modelName,
1385
+ queryModel.model,
1386
+ issues,
1387
+ );
1388
+ }
1389
+ }
1334
1390
  } else if (type === "Form") {
1335
1391
  // `$`-prefixed names are reserved runtime slots under `/forms/<id>/`
1336
1392
  // (`$error` carries the inline mutate failure) — a field stored there
@@ -1440,19 +1496,18 @@ export function validatePage(
1440
1496
  stateRefs.push(...chainResult.stateRefs);
1441
1497
  }
1442
1498
  }
1443
- if (
1444
- type === "Form" &&
1445
- isPlainObject(rawElement.props) &&
1446
- Object.hasOwn(rawElement.props, "onSubmit")
1447
- ) {
1448
- const chainResult = validateActionChain(
1449
- rawElement.props.onSubmit,
1450
- `${elementPath}.props.onSubmit`,
1451
- definition,
1452
- catalog,
1453
- );
1454
- issues.push(...chainResult.issues);
1455
- stateRefs.push(...chainResult.stateRefs);
1499
+ if (isPlainObject(rawElement.props)) {
1500
+ for (const prop of PROP_ACTION_CHAINS[String(type)] ?? []) {
1501
+ if (!Object.hasOwn(rawElement.props, prop)) continue;
1502
+ const chainResult = validateActionChain(
1503
+ rawElement.props[prop],
1504
+ `${elementPath}.props.${prop}`,
1505
+ definition,
1506
+ catalog,
1507
+ );
1508
+ issues.push(...chainResult.issues);
1509
+ stateRefs.push(...chainResult.stateRefs);
1510
+ }
1456
1511
  }
1457
1512
  }
1458
1513
 
@@ -1,4 +1,5 @@
1
- import { deleteKv, getDbClient, getKv, listKv, upsertKv } from "../be/db";
1
+ import { deleteKv, getDbClient, getKv, kvPrefixUpperBound, listKv, upsertKv } from "../be/db";
2
+ import { isInTransaction } from "../be/db-client";
2
3
  import {
3
4
  AppDefinitionSchema,
4
5
  type AppValidationIssue,
@@ -58,6 +59,15 @@ export class AppRowAppNotFoundError extends Error {
58
59
  const mutationChains = new Map<string, Promise<unknown>>();
59
60
  let lastCreatedAtMs = 0;
60
61
 
62
+ /**
63
+ * Let queued I/O and other requests run. DB calls are synchronous, so a loop
64
+ * of awaited statements otherwise only drains microtasks and holds the event
65
+ * loop until it finishes.
66
+ */
67
+ export function yieldToEventLoop(): Promise<void> {
68
+ return new Promise((resolve) => setImmediate(resolve));
69
+ }
70
+
61
71
  export function appsNamespace(appId: string): string {
62
72
  return `apps:${appId}`;
63
73
  }
@@ -331,11 +341,23 @@ export async function getAppRow(
331
341
  return entry.value as AppRow;
332
342
  }
333
343
 
334
- export async function listAppRows(appId: string, model: string): Promise<AppRow[]> {
344
+ /**
345
+ * `equals` pre-filters rows in SQL on top-level fields so a filtered list
346
+ * does not decode the whole model. The match is looser than `===` (JSON
347
+ * `true` equals `1`); callers re-check the returned rows.
348
+ */
349
+ export async function listAppRows(
350
+ appId: string,
351
+ model: string,
352
+ equals: Array<{ column: string; value: string | number | boolean }> = [],
353
+ ): Promise<AppRow[]> {
335
354
  const entries = await listKv(appsNamespace(appId), {
336
355
  prefix: `${model}/row/`,
337
356
  limit: 100000,
338
357
  offset: 0,
358
+ jsonFieldEquals: equals
359
+ .filter((filter) => !/["\\]/.test(filter.column))
360
+ .map((filter) => ({ field: filter.column, value: filter.value })),
339
361
  });
340
362
  return entries
341
363
  .map((entry) => entry.value)
@@ -355,17 +377,37 @@ export async function listAllAppRowsForMigrationUnlocked(
355
377
  const rows: AppRow[] = [];
356
378
  const namespace = appsNamespace(appId);
357
379
  const prefix = `${model}/row/`;
358
- let offset = 0;
380
+ // Row keys are `<model>/row/<uuid>`, so this bound always exists.
381
+ const upper = kvPrefixUpperBound(prefix)!;
382
+ // Keyset paging on the (namespace, key) primary key: OFFSET re-walked every
383
+ // skipped row on each page. Same rows, order and TTL filter as `listKv`.
384
+ let afterKey = "";
359
385
  while (true) {
360
- const entries = await listKv(namespace, { prefix, limit: MIGRATION_KV_BATCH_SIZE, offset });
361
- for (const entry of entries) {
362
- const value = entry.value;
386
+ const page = await getDbClient().query<{ key: string; value: string; value_type: string }>(
387
+ `SELECT key, value, value_type FROM kv_entries
388
+ WHERE namespace = ? AND key >= ? AND key < ? AND key > ?
389
+ AND (expires_at IS NULL OR expires_at > ?)
390
+ ORDER BY key
391
+ LIMIT ?`,
392
+ [namespace, prefix, upper, afterKey, Date.now(), MIGRATION_KV_BATCH_SIZE],
393
+ );
394
+ for (const entry of page) {
395
+ if (entry.value_type !== "json") continue;
396
+ let value: unknown;
397
+ try {
398
+ value = JSON.parse(entry.value);
399
+ } catch {
400
+ continue;
401
+ }
363
402
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
364
403
  rows.push(value as AppRow);
365
404
  }
366
405
  }
367
- if (entries.length < MIGRATION_KV_BATCH_SIZE) return rows;
368
- offset += entries.length;
406
+ if (page.length < MIGRATION_KV_BATCH_SIZE) return rows;
407
+ afterKey = page[page.length - 1]!.key;
408
+ // Schema migrations call this inside an open DbClient transaction, where a
409
+ // timer would stall every DB caller; only yield outside one.
410
+ if (!isInTransaction()) await yieldToEventLoop();
369
411
  }
370
412
  }
371
413
 
@@ -502,12 +544,25 @@ export function deleteAppRow(
502
544
  });
503
545
  }
504
546
 
547
+ const PURGE_BATCH_SIZE = 1000;
548
+
549
+ /**
550
+ * Batched DELETEs with an event-loop yield between them: one statement per
551
+ * row blocked every other request for the whole purge, and a single DELETE of
552
+ * the namespace would block for its full duration too. Expired entries go as
553
+ * well; a TTL-filtered listing would leave them orphaned.
554
+ */
505
555
  async function purgeNamespace(appId: string): Promise<void> {
506
556
  const namespace = appsNamespace(appId);
507
557
  while (true) {
508
- const entries = await listKv(namespace, { prefix: "", limit: 100000, offset: 0 });
509
- if (entries.length === 0) return;
510
- for (const entry of entries) await deleteKv(namespace, entry.key);
558
+ const result = await getDbClient().run(
559
+ `DELETE FROM kv_entries
560
+ WHERE namespace = ?
561
+ AND key IN (SELECT key FROM kv_entries WHERE namespace = ? ORDER BY key LIMIT ?)`,
562
+ [namespace, namespace, PURGE_BATCH_SIZE],
563
+ );
564
+ if (result.changes === 0) return;
565
+ await yieldToEventLoop();
511
566
  }
512
567
  }
513
568
 
package/src/apps/sync.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as z from "zod";
2
- import { deleteKv, getAllTasks, getKv, type TaskFilters, upsertKv } from "../be/db";
2
+ import { deleteKv, getAllTasks, getDbClient, getKv, type TaskFilters, upsertKv } from "../be/db";
3
3
  import { listScriptConnections } from "../be/script-connections";
4
4
  import { getScriptById } from "../be/scripts/db";
5
5
  import { runSavedScriptAsAgent } from "../be/scripts/run-saved";
@@ -21,6 +21,7 @@ import {
21
21
  listAllAppRowsForMigrationUnlocked,
22
22
  patchAppRowUnlocked,
23
23
  withMutationLock,
24
+ yieldToEventLoop,
24
25
  } from "./row-store";
25
26
  import { appDefinitionNeedsRepair, getApp } from "./store";
26
27
  import { resolveSyncRunAs } from "./sync-run-as";
@@ -585,6 +586,45 @@ type ReconcileCounts = {
585
586
  staleSweepSkipped?: boolean;
586
587
  };
587
588
 
589
+ type ReconcileTally = Omit<ReconcileCounts, "staleSweepSkipped">;
590
+
591
+ /** Rows written per transaction. Big enough to amortize COMMIT, small enough
592
+ * that one chunk holds the event loop for milliseconds, not seconds. */
593
+ export const RECONCILE_CHUNK_SIZE = 200;
594
+
595
+ /**
596
+ * Apply `write` to `items` in transactions of RECONCILE_CHUNK_SIZE, yielding
597
+ * to the event loop between chunks. A chunk's tally lands in `counts` only
598
+ * after it commits, so a failing chunk rolls back its writes and its counts
599
+ * together while earlier chunks stay committed and counted.
600
+ */
601
+ async function writeInChunks<T>(
602
+ items: readonly T[],
603
+ counts: ReconcileCounts,
604
+ write: (item: T, tally: ReconcileTally) => Promise<void>,
605
+ ): Promise<void> {
606
+ for (let start = 0; start < items.length; start += RECONCILE_CHUNK_SIZE) {
607
+ const chunk = items.slice(start, start + RECONCILE_CHUNK_SIZE);
608
+ const tally = await getDbClient().transaction(async () => {
609
+ const chunkTally: ReconcileTally = {
610
+ created: 0,
611
+ updated: 0,
612
+ refreshed: 0,
613
+ unchanged: 0,
614
+ markedStale: 0,
615
+ };
616
+ for (const item of chunk) await write(item, chunkTally);
617
+ return chunkTally;
618
+ });
619
+ counts.created += tally.created;
620
+ counts.updated += tally.updated;
621
+ counts.refreshed += tally.refreshed;
622
+ counts.unchanged += tally.unchanged;
623
+ counts.markedStale += tally.markedStale;
624
+ await yieldToEventLoop();
625
+ }
626
+ }
627
+
588
628
  function reconcile(args: {
589
629
  appId: string;
590
630
  model: string;
@@ -594,13 +634,15 @@ function reconcile(args: {
594
634
  pull: PullResult;
595
635
  warnings: string[];
596
636
  /**
597
- * Mutated write-by-write so a mid-pass failure still reports the row churn
598
- * that actually committed — the writes are independent KV upserts, not a
599
- * transaction, and a thrown error must not zero them out.
637
+ * Mutated chunk-by-chunk so a mid-pass failure still reports the row churn
638
+ * that actually committed: writes run in chunked transactions, and a thrown
639
+ * error must not zero out the chunks that landed before it.
600
640
  */
601
641
  counts: ReconcileCounts;
602
642
  }): Promise<void> {
603
643
  const { appId, model, sourceName, joinKey, pull, warnings, counts } = args;
644
+ // Lock order: the mutation lock is taken here, and every chunk transaction
645
+ // opens beneath it (see withMutationLock).
604
646
  return withMutationLock(appId, model, async () => {
605
647
  // Re-read under the lock: the pull ran unlocked, so the definition it was
606
648
  // planned against may be gone. Anything that moves the identity or the
@@ -639,7 +681,7 @@ function reconcile(args: {
639
681
  const actor = `sync:${sourceName}`;
640
682
  const seen = new Set<string>();
641
683
 
642
- for (const record of pull.records) {
684
+ await writeInChunks(pull.records, counts, async (record, tally) => {
643
685
  if (seen.has(record.key)) {
644
686
  warn(warnings, `source returned duplicate key "${record.key}"; the last record wins`);
645
687
  }
@@ -655,8 +697,8 @@ function reconcile(args: {
655
697
  actor,
656
698
  });
657
699
  mine.set(record.key, created);
658
- counts.created += 1;
659
- continue;
700
+ tally.created += 1;
701
+ return;
660
702
  }
661
703
  const differs = Object.entries(values).some(
662
704
  ([name, value]) => !sameValue(existing[name], value),
@@ -671,12 +713,12 @@ function reconcile(args: {
671
713
  });
672
714
  if (!updated) {
673
715
  warn(warnings, `row "${existing.id}" vanished before it could be updated`);
674
- continue;
716
+ return;
675
717
  }
676
718
  mine.set(record.key, updated);
677
- if (differs) counts.updated += 1;
678
- else counts.refreshed += 1;
679
- }
719
+ if (differs) tally.updated += 1;
720
+ else tally.refreshed += 1;
721
+ });
680
722
 
681
723
  const unseen = [...mine.entries()]
682
724
  .filter(([key]) => !seen.has(key))
@@ -688,10 +730,10 @@ function reconcile(args: {
688
730
  warn(warnings, "pull reported an incomplete window; stale sweep skipped");
689
731
  return;
690
732
  }
691
- for (const row of unseen) {
733
+ await writeInChunks(unseen, counts, async (row, tally) => {
692
734
  if (row.stale === true) {
693
- counts.unchanged += 1;
694
- continue;
735
+ tally.unchanged += 1;
736
+ return;
695
737
  }
696
738
  await patchAppRowUnlocked(
697
739
  appId,
@@ -710,8 +752,8 @@ function reconcile(args: {
710
752
  },
711
753
  },
712
754
  );
713
- counts.markedStale += 1;
714
- }
755
+ tally.markedStale += 1;
756
+ });
715
757
  });
716
758
  }
717
759
 
@@ -778,7 +820,7 @@ async function executePass(args: {
778
820
  return scrubbed;
779
821
  };
780
822
 
781
- // Reconcile mutates this accumulator write-by-write, so the error path below
823
+ // Reconcile mutates this accumulator chunk-by-chunk, so the error path below
782
824
  // reports the churn that actually committed instead of the zero-count base.
783
825
  const counts: ReconcileCounts = {
784
826
  created: 0,