@desplega.ai/agent-swarm 1.156.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 (263) hide show
  1. package/dist/{acp-adapter-c1msptrw.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-wd41qtdc.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-tz2qrnah.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-c3q1zj2t.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-0be1krkm.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-r00j03f3.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-9j4hd0vw.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-ncgxz1r2.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-112w28qc.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-k7pb5kfz.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-mx3bzbv8.js → cli-20cj37vt.js} +1968 -44
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-24w39x6n.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-90x9ewbz.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-08n8jxcw.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-7j2098n5.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-b05d7szr.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-9w4ex8vh.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-28wkjpdp.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-7pgcpcrg.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-jk26c1sz.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-865swy5p.js → cli-c07r9a2m.js} +3 -2
  29. package/dist/{cli-nyqza443.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-9pyah9c4.js → cli-ftfvggzx.js} +633 -166
  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-y8bfxjtw.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-7vj52450.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-tp0d7vzb.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-s8hpxeg4.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-hc2fkhdv.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-t1cd3mhp.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-xa4j64zh.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-szcyv1mm.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +30 -16
  47. package/dist/{codex-adapter-g4tj1f2g.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-ad8ha7zt.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-z3gd8ppc.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-bezm97r9.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-a97hc33y.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-wtdy1x02.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-7edgmzng.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fvs58a5b.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-phnf93hp.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-7x131mmf.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-1fktvwxc.js → index-a3vn96n8.js} +31 -28
  59. package/dist/{index-yp8rasxk.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-50jxgw0d.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-7jwk7nsm.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-nnz07ebd.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-mg7qcgww.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-ywmnqrpx.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-c63sqfs1.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-2zj8sqkq.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-jgdfm26f.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-3whntb69.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-3zptcc3p.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-dbvpyysc.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-qwx0j8z9.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-sjbcmvep.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-xezqdkb7.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +3 -2
  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/codex-agents-md.ts +140 -17
  149. package/src/providers/modelsdev-reasoning.json +551 -0
  150. package/src/rbac/legacy-policy.ts +16 -0
  151. package/src/rbac/permissions.ts +4 -0
  152. package/src/scheduler/deferred-task-waits.ts +28 -0
  153. package/src/scheduler/schedule-task.ts +3 -0
  154. package/src/scheduler/scheduler.ts +14 -0
  155. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  156. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  157. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  158. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  159. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  160. package/src/server.ts +12 -7
  161. package/src/slack/app.ts +35 -4
  162. package/src/slack/connection-state.ts +17 -0
  163. package/src/tasks/context-key.ts +1 -0
  164. package/src/tasks/worker-follow-up.ts +6 -2
  165. package/src/tests/agent-task-activity.test.ts +74 -0
  166. package/src/tests/agents-list-model-display.test.ts +6 -1
  167. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  168. package/src/tests/approval-requests.test.ts +395 -0
  169. package/src/tests/approval-sweeps-config.test.ts +47 -0
  170. package/src/tests/approval-sweeps.test.ts +508 -0
  171. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  172. package/src/tests/apps-spike.test.ts +56 -1
  173. package/src/tests/apps-sync-engine.test.ts +20 -9
  174. package/src/tests/boot-seeds.test.ts +170 -0
  175. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  176. package/src/tests/claude-managed-adapter.test.ts +6 -0
  177. package/src/tests/codex-adapter.test.ts +126 -1
  178. package/src/tests/config-key-filter.test.ts +80 -0
  179. package/src/tests/context-window.test.ts +1 -0
  180. package/src/tests/db-client-busy-retry.test.ts +96 -0
  181. package/src/tests/db-client.test.ts +60 -1
  182. package/src/tests/db-task-read-contract.test.ts +10 -0
  183. package/src/tests/db-task-write-contract.test.ts +9 -0
  184. package/src/tests/deferred-schedule-reaper.test.ts +193 -0
  185. package/src/tests/docs-index-footprint.test.js +3 -3
  186. package/src/tests/events-http.test.ts +103 -0
  187. package/src/tests/extensions-catalog.test.ts +22 -17
  188. package/src/tests/extensions-pre-task.test.ts +107 -0
  189. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  190. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  191. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  192. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  193. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  194. package/src/tests/heartbeat.test.ts +99 -3
  195. package/src/tests/hook-mcp-config.test.ts +29 -0
  196. package/src/tests/http-api-integration.test.ts +16 -2
  197. package/src/tests/kv-storage.test.ts +62 -0
  198. package/src/tests/linear-outbound-sync.test.ts +120 -5
  199. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  200. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  201. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  202. package/src/tests/memory-hybrid.test.ts +161 -1
  203. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  204. package/src/tests/memory.test.ts +22 -0
  205. package/src/tests/onboard-container-engine.test.ts +240 -0
  206. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  207. package/src/tests/pricing-routes.test.ts +14 -0
  208. package/src/tests/profile-sync-warning.test.ts +8 -4
  209. package/src/tests/promote-draft-task-route.test.ts +19 -0
  210. package/src/tests/provider-registration.test.ts +296 -42
  211. package/src/tests/rbac-engine.test.ts +30 -0
  212. package/src/tests/reasoning-effort.test.ts +8 -0
  213. package/src/tests/script-connections.test.ts +7 -0
  214. package/src/tests/sdk-allowlist.test.ts +39 -0
  215. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  216. package/src/tests/sessions-detail-batch.test.ts +250 -0
  217. package/src/tests/sessions.test.ts +118 -0
  218. package/src/tests/slack-config.test.ts +22 -0
  219. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  220. package/src/tests/status.test.ts +36 -1
  221. package/src/tests/swarm-diff.test.ts +316 -10
  222. package/src/tests/task-completion-idempotency.test.ts +2 -1
  223. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  224. package/src/tests/tool-annotations.test.ts +1 -1
  225. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  226. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  227. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  228. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  229. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  230. package/src/tests/workflow-http-v2.test.ts +99 -8
  231. package/src/tests/workflow-retry-validation.test.ts +56 -0
  232. package/src/tests/workflow-tla-races.test.ts +389 -0
  233. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  234. package/src/tools/cancel-approval-request.ts +57 -0
  235. package/src/tools/memory-search.ts +1 -0
  236. package/src/tools/request-human-input.ts +4 -2
  237. package/src/tools/schedules/list-schedules.ts +4 -2
  238. package/src/tools/send-task.ts +4 -0
  239. package/src/tools/templates.ts +19 -0
  240. package/src/tools/tool-config.ts +2 -1
  241. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  242. package/src/types.ts +38 -0
  243. package/src/utils/context-window.ts +1 -0
  244. package/src/workflows/approval-cancel.ts +152 -0
  245. package/src/workflows/approval-notifications.ts +84 -0
  246. package/src/workflows/checkpoint.ts +10 -5
  247. package/src/workflows/definition.ts +21 -0
  248. package/src/workflows/engine.ts +152 -21
  249. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  250. package/src/workflows/recovery.ts +100 -45
  251. package/src/workflows/resume.ts +49 -18
  252. package/src/workflows/retry-poller.ts +71 -21
  253. package/templates/extensions/README.md +1 -0
  254. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  255. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  256. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  257. package/templates/skills/apps/content.md +20 -1
  258. package/templates/skills/code-quality/content.md +26 -1
  259. package/templates/skills/pages/content.md +16 -4
  260. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  261. package/templates/skills/scheduling/content.md +1 -1
  262. package/dist/cli-2daksqps.js +0 -378
  263. package/dist/rbac-roles-3zw68x72.js +0 -37
@@ -7,6 +7,7 @@ import {
7
7
  deleteSwarmConfigByKey,
8
8
  EXTENSION_AGENT_ROLE,
9
9
  getAgentById,
10
+ getAgentDailyTaskCounts,
10
11
  getAgentWithTasks,
11
12
  getAllAgents,
12
13
  getAllAgentsWithTasks,
@@ -416,6 +417,27 @@ const listAgentRuntimeInstances = route({
416
417
  },
417
418
  });
418
419
 
420
+ const getAgentTaskActivity = route({
421
+ method: "get",
422
+ path: "/api/agents/{id}/task-activity",
423
+ pattern: ["api", "agents", null, "task-activity"],
424
+ summary: "Daily task counts for an agent",
425
+ description:
426
+ "Number of tasks created for the agent per UTC day over the last `days` days (default 365, max 730), oldest first. Days with no tasks are omitted. Backs the activity heatmap on the agent page.",
427
+ tags: ["Agents"],
428
+ params: z.object({ id: z.string() }),
429
+ query: z.object({ days: z.coerce.number().int().min(1).max(730).optional() }),
430
+ responses: {
431
+ 200: {
432
+ description: "Daily task counts",
433
+ schema: z.object({
434
+ days: z.array(z.object({ date: z.string(), count: z.number().int() })),
435
+ }),
436
+ },
437
+ 404: { description: "Agent not found" },
438
+ },
439
+ });
440
+
419
441
  const ContentHashSchema = z.string().regex(/^[0-9a-f]{64}$/, "sha256 hex");
420
442
 
421
443
  const ProfileSyncRejectionSchema = z.object({
@@ -820,6 +842,19 @@ export async function handleAgentsRest(
820
842
  return true;
821
843
  }
822
844
 
845
+ if (getAgentTaskActivity.match(req.method, pathSegments)) {
846
+ const parsed = await getAgentTaskActivity.parse(req, res, pathSegments, queryParams);
847
+ if (!parsed) return true;
848
+ if (!(await getAgentById(parsed.params.id))) {
849
+ jsonError(res, "Agent not found", 404);
850
+ return true;
851
+ }
852
+ getAgentTaskActivity.respond(res, 200, {
853
+ days: await getAgentDailyTaskCounts(parsed.params.id, parsed.query.days ?? 365),
854
+ });
855
+ return true;
856
+ }
857
+
823
858
  if (updateAgentProfileRoute.match(req.method, pathSegments)) {
824
859
  const parsed = await updateAgentProfileRoute.parse(req, res, pathSegments, queryParams);
825
860
  if (!parsed) return true;
@@ -4,16 +4,19 @@ import { resolveTaskAuditUserId } from "../be/audit-user";
4
4
  import {
5
5
  type ApprovalRequest,
6
6
  createApprovalRequest,
7
- createTaskExtended,
7
+ getAgentById,
8
8
  getApprovalRequestById,
9
- getTaskById,
9
+ getDbClient,
10
10
  getWorkflowRun,
11
11
  getWorkflowRunStep,
12
+ listApprovalRequestSummaries,
12
13
  listApprovalRequests,
13
14
  resolveApprovalRequest,
14
15
  } from "../be/db";
15
- import { resolveTemplate } from "../prompts/resolver";
16
+ import type { RbacPrincipal } from "../rbac";
16
17
  import { getRequestAuth } from "../utils/request-auth-context";
18
+ import { cancelApprovalRequest } from "../workflows/approval-cancel";
19
+ import { createApprovalFollowUpTask } from "../workflows/approval-notifications";
17
20
  import { workflowEventBus } from "../workflows/event-bus";
18
21
  import { route } from "./route-def";
19
22
  import { jsonError } from "./utils";
@@ -90,6 +93,15 @@ const ApprovalRequestSchema = z.object({
90
93
  updatedAt: z.string(),
91
94
  });
92
95
 
96
+ /** Slim list row (`?fields=slim`) — see `ApprovalRequestSummary`. */
97
+ const ApprovalRequestSummarySchema = ApprovalRequestSchema.omit({
98
+ questions: true,
99
+ approvers: true,
100
+ responses: true,
101
+ resolutionReason: true,
102
+ notificationChannels: true,
103
+ }).extend({ questionCount: z.number().int() });
104
+
93
105
  /**
94
106
  * Reshapes a DB `ApprovalRequest` row for `respond()` — identical values,
95
107
  * narrowed from the DB layer's `unknown` fields to the precise wire shape
@@ -196,7 +208,14 @@ const createRoute = route({
196
208
  workflowRunId: z.string().uuid().optional(),
197
209
  workflowRunStepId: z.string().uuid().optional(),
198
210
  sourceTaskId: z.string().uuid().optional(),
199
- timeoutSeconds: z.number().int().min(1).optional(),
211
+ timeoutSeconds: z
212
+ .number()
213
+ .int()
214
+ .min(1)
215
+ .optional()
216
+ .describe(
217
+ "Seconds until the request expires. After that the request becomes 'timeout' and you get a hitl-follow-up task. A request with no timeout is cancelled after APPROVAL_REQUEST_AUTO_CANCELLATION_DAYS days (default 7).",
218
+ ),
200
219
  notifications: z
201
220
  .array(
202
221
  z.object({
@@ -256,21 +275,58 @@ const respondRoute = route({
256
275
  auth: { apiKey: true },
257
276
  });
258
277
 
278
+ const cancelRoute = route({
279
+ method: "post",
280
+ path: "/api/approval-requests/{id}/cancel",
281
+ pattern: ["api", "approval-requests", null, "cancel"],
282
+ summary: "Cancel a pending approval request",
283
+ tags: ["ApprovalRequests"],
284
+ params: z.object({ id: z.string().uuid() }),
285
+ body: z.object({ reason: z.string().max(500).optional() }),
286
+ responses: {
287
+ 200: {
288
+ description:
289
+ "Request cancelled, or already cancelled. A request that gates a running or waiting workflow run cancels that run too.",
290
+ schema: z.object({
291
+ approvalRequest: ApprovalRequestSchema,
292
+ alreadyCancelled: z.boolean(),
293
+ runCancelled: z.boolean(),
294
+ }),
295
+ },
296
+ 403: { description: "Caller may not cancel this request" },
297
+ 404: { description: "Not found" },
298
+ 409: {
299
+ description: "Already resolved with approved, rejected, or timeout, or its expiresAt passed",
300
+ },
301
+ },
302
+ auth: { apiKey: true },
303
+ rbac: { permission: "approval.cancel.any" },
304
+ });
305
+
259
306
  const listRoute = route({
260
307
  method: "get",
261
308
  path: "/api/approval-requests",
262
309
  pattern: ["api", "approval-requests"],
263
310
  summary: "List approval requests with optional filters",
311
+ description:
312
+ "Returns full approval requests by default. Pass `fields=slim` for the list-view shape: `questions`, `approvers`, `responses`, `resolutionReason` and `notificationChannels` are dropped and `questionCount` is added. Fetch one request in full via `GET /api/approval-requests/{id}`.",
264
313
  tags: ["ApprovalRequests"],
265
314
  query: z.object({
266
315
  status: z.string().optional(),
267
316
  workflowRunId: z.string().optional(),
268
317
  limit: z.coerce.number().optional(),
318
+ /** `slim` is the list-view shape; default is full. */
319
+ fields: z.enum(["full", "slim"]).optional(),
269
320
  }),
270
321
  responses: {
271
322
  200: {
272
323
  description: "List of approval requests",
273
- schema: z.object({ approvalRequests: z.array(ApprovalRequestSchema) }),
324
+ schema: z.object({
325
+ approvalRequests: z.union([
326
+ z.array(ApprovalRequestSchema),
327
+ z.array(ApprovalRequestSummarySchema),
328
+ ]),
329
+ }),
274
330
  },
275
331
  },
276
332
  auth: { apiKey: true },
@@ -305,6 +361,24 @@ export async function handleApprovalRequests(
305
361
  return true;
306
362
  }
307
363
 
364
+ // A late answer never resolves the request: it becomes timeout at once.
365
+ // A workflow run then routes on its timeout port on the next heartbeat
366
+ // tick through getStuckApprovalRuns, so no approval.resolved is emitted.
367
+ if (existing.expiresAt && new Date(existing.expiresAt) < new Date()) {
368
+ // The status change and the follow-up task commit together.
369
+ await getDbClient().transaction(async () => {
370
+ const timedOut = await resolveApprovalRequest(existing.id, {
371
+ status: "timeout",
372
+ resolutionReason: `Timed out: the answer arrived after the deadline ${existing.expiresAt}`,
373
+ });
374
+ if (timedOut && !existing.workflowRunId) {
375
+ await createApprovalFollowUpTask(timedOut, "hitl.timeout");
376
+ }
377
+ });
378
+ jsonError(res, `Approval request expired at ${existing.expiresAt}`, 409);
379
+ return true;
380
+ }
381
+
308
382
  const unavailableReason = await getWorkflowApprovalUnavailableReason(existing);
309
383
  if (unavailableReason) {
310
384
  jsonError(res, unavailableReason, 409);
@@ -367,44 +441,36 @@ export async function handleApprovalRequests(
367
441
 
368
442
  // For standalone (non-workflow) requests, create a follow-up task
369
443
  // so the requesting agent is notified of the human's response
370
- if (!updated.workflowRunId && updated.sourceTaskId) {
371
- const sourceTask = await getTaskById(updated.sourceTaskId);
372
- if (sourceTask) {
373
- // Format responses for the template
374
- const formattedResponses = formatResponses(
375
- updated.questions as Array<{ id: string; type: string; label: string }>,
376
- updated.responses as Record<string, unknown>,
377
- );
378
-
379
- const { text: taskText } = resolveTemplate("hitl.follow_up", {
380
- request_id: updated.id,
381
- title: updated.title,
382
- status: updated.status,
383
- responses: formattedResponses,
384
- });
385
-
386
- await createTaskExtended(taskText, {
387
- agentId: sourceTask.agentId,
388
- routingReason: sourceTask.agentId ? "continuity" : undefined,
389
- routingSource: sourceTask.agentId ? "engine_default" : undefined,
390
- parentTaskId: updated.sourceTaskId,
391
- source: "system",
392
- taskType: "hitl-follow-up",
393
- tags: ["hitl", "follow-up"],
394
- // Explicit Slack metadata — parentTaskId auto-inherits too,
395
- // but being explicit ensures the follow-up task always gets
396
- // the right thread context even if inheritance logic changes.
397
- slackChannelId: sourceTask.slackChannelId ?? undefined,
398
- slackThreadTs: sourceTask.slackThreadTs ?? undefined,
399
- slackUserId: sourceTask.slackUserId ?? undefined,
400
- });
401
- }
402
- }
444
+ await createApprovalFollowUpTask(updated, "hitl.follow_up");
403
445
 
404
446
  respondRoute.respond(res, 200, { approvalRequest: toApprovalRequestResponse(updated) });
405
447
  return true;
406
448
  }
407
449
 
450
+ // 4-segment: POST /api/approval-requests/{id}/cancel
451
+ if (cancelRoute.match(req.method, pathSegments)) {
452
+ const parsed = await cancelRoute.parse(req, res, pathSegments, queryParams);
453
+ if (!parsed) return true;
454
+
455
+ const { principal, resolvedBy } = await approvalCancelPrincipal(req);
456
+ const result = await cancelApprovalRequest({
457
+ id: parsed.params.id,
458
+ reason: parsed.body.reason,
459
+ principal,
460
+ resolvedBy,
461
+ });
462
+ if (!result.ok) {
463
+ jsonError(res, result.message, result.status);
464
+ return true;
465
+ }
466
+ cancelRoute.respond(res, 200, {
467
+ approvalRequest: toApprovalRequestResponse(result.request),
468
+ alreadyCancelled: result.alreadyCancelled,
469
+ runCancelled: result.runCancelled,
470
+ });
471
+ return true;
472
+ }
473
+
408
474
  // 3-segment with param: GET /api/approval-requests/{id}
409
475
  if (getByIdRoute.match(req.method, pathSegments)) {
410
476
  const parsed = await getByIdRoute.parse(req, res, pathSegments, queryParams);
@@ -457,12 +523,20 @@ export async function handleApprovalRequests(
457
523
  const parsed = await listRoute.parse(req, res, pathSegments, queryParams);
458
524
  if (!parsed) return true;
459
525
 
460
- const requests = await listApprovalRequests({
526
+ const filters = {
461
527
  status: parsed.query.status || undefined,
462
528
  workflowRunId: parsed.query.workflowRunId || undefined,
463
529
  limit: parsed.query.limit || undefined,
464
- });
530
+ };
531
+ // Opt-in: API, MCP and script callers that don't ask keep the full rows.
532
+ if (parsed.query.fields === "slim") {
533
+ listRoute.respond(res, 200, {
534
+ approvalRequests: await listApprovalRequestSummaries(filters),
535
+ });
536
+ return true;
537
+ }
465
538
 
539
+ const requests = await listApprovalRequests(filters);
466
540
  listRoute.respond(res, 200, {
467
541
  approvalRequests: requests.map(toApprovalRequestResponse),
468
542
  });
@@ -472,28 +546,26 @@ export async function handleApprovalRequests(
472
546
  return false;
473
547
  }
474
548
 
475
- // ─── Helpers ──────────────────────────────────────────────────────────────────
476
-
477
- function formatResponses(
478
- questions: Array<{ id: string; type: string; label: string }>,
479
- responses: Record<string, unknown>,
480
- ): string {
481
- return questions
482
- .map((q) => {
483
- const answer = responses[q.id];
484
- let answerText: string;
485
- if (answer == null) {
486
- answerText = "(no answer)";
487
- } else if (q.type === "approval") {
488
- const a = answer as { approved?: boolean; comment?: string };
489
- answerText = a.approved ? "Approved" : "Rejected";
490
- if (a.comment) answerText += ` — ${a.comment}`;
491
- } else if (typeof answer === "object") {
492
- answerText = JSON.stringify(answer);
493
- } else {
494
- answerText = String(answer);
495
- }
496
- return `- ${q.label}: ${answerText}`;
497
- })
498
- .join("\n");
549
+ /**
550
+ * The caller of the cancel route. The shared API key with an `X-Agent-ID`
551
+ * header identifies that agent, as on the create route.
552
+ */
553
+ async function approvalCancelPrincipal(
554
+ req: IncomingMessage,
555
+ ): Promise<{ principal: RbacPrincipal; resolvedBy: string | null }> {
556
+ const auth = getRequestAuth(req);
557
+ if (auth?.kind === "user") {
558
+ return { principal: { kind: "user", userId: auth.userId }, resolvedBy: auth.userId };
559
+ }
560
+ const rawAgentId = req.headers["x-agent-id"];
561
+ const agentId =
562
+ auth?.kind === "agent" ? auth.agentId : Array.isArray(rawAgentId) ? rawAgentId[0] : rawAgentId;
563
+ if (agentId) {
564
+ const agent = await getAgentById(agentId);
565
+ return {
566
+ principal: { kind: "agent", agentId, isLead: agent?.isLead ?? false },
567
+ resolvedBy: agentId,
568
+ };
569
+ }
570
+ return { principal: { kind: "operator" }, resolvedBy: "operator" };
499
571
  }
package/src/http/apps.ts CHANGED
@@ -1155,9 +1155,20 @@ export async function handleApps(
1155
1155
  json(res, { error: "invalid row query", issues }, 400);
1156
1156
  return true;
1157
1157
  }
1158
- const rows = (await listAppRows(parsed.params.id, parsed.params.model)).filter((row) =>
1159
- filters.every((filter) => rowValue(row, filter.column) === filter.value),
1160
- );
1158
+ // SQL narrows the scan; the `===` re-check keeps the exact filter semantics.
1159
+ const rows = (
1160
+ await listAppRows(
1161
+ parsed.params.id,
1162
+ parsed.params.model,
1163
+ filters.flatMap((filter) =>
1164
+ typeof filter.value === "string" ||
1165
+ typeof filter.value === "number" ||
1166
+ typeof filter.value === "boolean"
1167
+ ? [{ column: filter.column, value: filter.value }]
1168
+ : [],
1169
+ ),
1170
+ )
1171
+ ).filter((row) => filters.every((filter) => rowValue(row, filter.column) === filter.value));
1161
1172
  const sortRaw = parsed.query.sort;
1162
1173
  if (sortRaw) {
1163
1174
  const [column, dir, extra] = sortRaw.split(":");
@@ -134,6 +134,11 @@ const getResolvedConfigRoute = route({
134
134
  query: z.object({
135
135
  agentId: z.string().optional(),
136
136
  repoId: z.string().optional(),
137
+ key: z
138
+ .string()
139
+ .min(1)
140
+ .optional()
141
+ .describe("Return only the entry with this exact key. Omit to return every resolved entry."),
137
142
  includeSecrets: z.enum(["true", "false"]).optional(),
138
143
  }),
139
144
  responses: {
@@ -216,6 +221,11 @@ const listConfig = route({
216
221
  query: z.object({
217
222
  scope: z.string().optional(),
218
223
  scopeId: z.string().optional(),
224
+ key: z
225
+ .string()
226
+ .min(1)
227
+ .optional()
228
+ .describe("Return only entries with this exact key. Omit to return every entry."),
219
229
  includeSecrets: z.enum(["true", "false"]).optional(),
220
230
  }),
221
231
  responses: {
@@ -283,9 +293,12 @@ export async function handleConfig(
283
293
  if (!parsed) return true;
284
294
  const includeSecrets = parsed.query.includeSecrets === "true";
285
295
  const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
296
+ const key = parsed.query.key;
297
+ // Filter by key before masking and before any secret is registered, so a
298
+ // caller asking for one key never receives the rest (matches get-config).
286
299
  const configs = stripApiOnlyKeys(
287
300
  await getResolvedConfig(parsed.query.agentId || undefined, parsed.query.repoId || undefined),
288
- );
301
+ ).filter((c) => !key || c.key === key);
289
302
  const result = effectiveIncludeSecrets ? configs : maskSecrets(configs);
290
303
  if (effectiveIncludeSecrets) {
291
304
  for (const c of result) {
@@ -368,6 +381,7 @@ export async function handleConfig(
368
381
  await getSwarmConfigs({
369
382
  scope: parsed.query.scope || undefined,
370
383
  scopeId: parsed.query.scopeId || undefined,
384
+ key: parsed.query.key,
371
385
  }),
372
386
  );
373
387
  const listResult = effectiveIncludeSecrets ? configs : maskSecrets(configs);
@@ -86,12 +86,33 @@ const getEventsRoute = route({
86
86
  since: z.string().optional(),
87
87
  until: z.string().optional(),
88
88
  limit: z.coerce.number().int().min(1).max(1000).optional(),
89
+ events: z
90
+ .string()
91
+ .optional()
92
+ .describe("Comma-separated event names; matches any of them (ANDed with `event`)"),
93
+ dataFields: z
94
+ .string()
95
+ .optional()
96
+ .describe("Comma-separated `data.field` values, used with latestPerDataField"),
97
+ latestPerDataField: z
98
+ .enum(["true", "false"])
99
+ .optional()
100
+ .describe(
101
+ "When true, return only the newest event per (event, data.field) pair for every name in `event`/`events` and every value in `dataFields`",
102
+ ),
89
103
  }),
90
104
  responses: {
91
105
  200: {
92
106
  description: "List of events",
93
- schema: z.object({ events: z.array(SwarmEventSchema) }),
107
+ schema: z.object({
108
+ events: z.array(SwarmEventSchema),
109
+ latestPerDataField: z
110
+ .literal(true)
111
+ .optional()
112
+ .describe("Present only when the request asked for latestPerDataField=true"),
113
+ }),
94
114
  },
115
+ 400: { description: "Validation error" },
95
116
  },
96
117
  auth: { apiKey: true },
97
118
  });
@@ -122,6 +143,9 @@ const getEventCountsRoute = route({
122
143
  auth: { apiKey: true },
123
144
  });
124
145
 
146
+ /** Bounds the UNION ALL built for latestPerDataField: one indexed lookup per pair. */
147
+ const MAX_LATEST_PAIRS = 50;
148
+
125
149
  // ─── Handler ─────────────────────────────────────────────────────────────────
126
150
 
127
151
  export async function handleEvents(
@@ -184,6 +208,36 @@ export async function handleEvents(
184
208
  const parsed = await getEventsRoute.parse(req, res, pathSegments, queryParams);
185
209
  if (!parsed) return true;
186
210
 
211
+ const eventNames = parsed.query.events
212
+ ? parsed.query.events.split(",").map((name) => EventNameSchema.safeParse(name.trim()))
213
+ : [];
214
+ const invalid = eventNames.find((result) => !result.success);
215
+ if (invalid) {
216
+ jsonError(res, "Invalid event name in `events`", 400);
217
+ return true;
218
+ }
219
+ const eventFilter = eventNames.flatMap((result) => (result.success ? [result.data] : []));
220
+ const latestPerDataField = parsed.query.latestPerDataField === "true";
221
+ const dataFields = (parsed.query.dataFields ?? "")
222
+ .split(",")
223
+ .map((field) => field.trim())
224
+ .filter(Boolean);
225
+ const latestEvents =
226
+ eventFilter.length > 0 ? eventFilter : parsed.query.event ? [parsed.query.event] : [];
227
+ if (
228
+ latestPerDataField &&
229
+ (latestEvents.length === 0 ||
230
+ dataFields.length === 0 ||
231
+ latestEvents.length * dataFields.length > MAX_LATEST_PAIRS)
232
+ ) {
233
+ jsonError(
234
+ res,
235
+ `latestPerDataField needs \`event\` or \`events\` and \`dataFields\`, at most ${MAX_LATEST_PAIRS} pairs`,
236
+ 400,
237
+ );
238
+ return true;
239
+ }
240
+
187
241
  const events = await getEventsFiltered({
188
242
  category: parsed.query.category || undefined,
189
243
  event: parsed.query.event || undefined,
@@ -196,8 +250,14 @@ export async function handleEvents(
196
250
  since: parsed.query.since || undefined,
197
251
  until: parsed.query.until || undefined,
198
252
  limit: parsed.query.limit ?? 100,
253
+ events: eventFilter,
254
+ latestPerDataField: latestPerDataField ? { events: latestEvents, dataFields } : undefined,
199
255
  });
200
- getEventsRoute.respond(res, 200, { events });
256
+ getEventsRoute.respond(
257
+ res,
258
+ 200,
259
+ latestPerDataField ? { events, latestPerDataField: true } : { events },
260
+ );
201
261
  return true;
202
262
  }
203
263
 
package/src/http/index.ts CHANGED
@@ -595,8 +595,10 @@ try {
595
595
  // here surfaces the count in the boot log and makes the API ready to recompute
596
596
  // USD before the first POST /api/session-costs lands.
597
597
  try {
598
- const { seedPricingFromModelsDev } = await import("../be/seed-pricing");
599
- seedPricingFromModelsDev();
598
+ // Guarded per DB handle: `createServer()` (one per MCP session) calls the
599
+ // same helper, so seeding here is what keeps session init off this path.
600
+ const { ensurePricingSeeded } = await import("../be/boot-seeds");
601
+ ensurePricingSeeded();
600
602
  const { startPricingRefreshLoop } = await import("../be/pricing-refresh");
601
603
  startPricingRefreshLoop();
602
604
  } catch (err) {
@@ -604,8 +606,8 @@ try {
604
606
  }
605
607
 
606
608
  try {
607
- const { ensureRbacSeedsSynced } = await import("../be/rbac-roles");
608
- ensureRbacSeedsSynced();
609
+ const { ensureRbacSeeded } = await import("../be/boot-seeds");
610
+ ensureRbacSeeded();
609
611
  } catch (err) {
610
612
  console.error("[startup] Failed to sync RBAC seed rows:", err);
611
613
  // RBAC flag-on must fail closed; flag-off deployments should not be bricked
@@ -24,7 +24,7 @@ import { rerank } from "../be/memory/reranker";
24
24
  import { getRetrievalsForAgent, hasRetrievalForTask } from "../be/memory/retrieval-store";
25
25
  import { getUsefulnessStats } from "../be/memory/usefulness-stats";
26
26
  import { shouldPersistAutomaticTaskMemory } from "../memory/automatic-task-gate";
27
- import { SIMILARITY_THRESHOLD } from "../prompts/memories";
27
+ import { memoryRelevance, SIMILARITY_THRESHOLD } from "../prompts/memories";
28
28
  import { can } from "../rbac";
29
29
  import { AgentMemorySchema, AgentMemoryScopeSchema, AgentMemorySourceSchema } from "../types";
30
30
  import { getRequestAuth } from "../utils/request-auth-context";
@@ -688,7 +688,24 @@ export async function handleMemory(
688
688
  source,
689
689
  isLead: false,
690
690
  });
691
- const ranked = rerank(expanded, { limit: Math.min(limit, 20) });
691
+ const resultLimit = Math.min(limit, 20);
692
+ const consumptionHeader = req.headers["x-memory-consumption"];
693
+ const consumptionMode = Array.isArray(consumptionHeader)
694
+ ? consumptionHeader[0]
695
+ : consumptionHeader;
696
+ // Prompt recall injects only rows above the relevance gate, so the gate
697
+ // must pick the slots, not trim them afterwards: eligible rows fill the
698
+ // cap first (in composite order), ineligible rows only the remainder.
699
+ // Capping by composite first let boosted low-relevance rows take every
700
+ // slot and render an empty prompt while an eligible hit existed.
701
+ const ranked =
702
+ consumptionMode === "prompt"
703
+ ? rerank(expanded, { limit: expanded.length })
704
+ .map((r) => ({ r, eligible: memoryRelevance(r) > SIMILARITY_THRESHOLD }))
705
+ .sort((a, b) => Number(b.eligible) - Number(a.eligible))
706
+ .slice(0, resultLimit)
707
+ .map(({ r }) => r)
708
+ : rerank(expanded, { limit: resultLimit });
692
709
 
693
710
  // Retrieval bridge — when caller passed `X-Source-Task-ID`, record one
694
711
  // `memory_retrieval` row per returned memory so server-side raters
@@ -701,10 +718,6 @@ export async function handleMemory(
701
718
  : sourceTaskIdHeader;
702
719
  const contextKeyHeader = req.headers["x-context-key"];
703
720
  const contextKey = Array.isArray(contextKeyHeader) ? contextKeyHeader[0] : contextKeyHeader;
704
- const consumptionHeader = req.headers["x-memory-consumption"];
705
- const consumptionMode = Array.isArray(consumptionHeader)
706
- ? consumptionHeader[0]
707
- : consumptionHeader;
708
721
  if (sourceTaskId && intent) {
709
722
  try {
710
723
  await recordRetrievals(
@@ -713,6 +726,7 @@ export async function handleMemory(
713
726
  ranked.map((r) => ({
714
727
  memoryId: r.id,
715
728
  similarity: r.similarity,
729
+ relevance: r.rawSimilarity,
716
730
  retrievalSource: r.retrievalSource,
717
731
  })),
718
732
  undefined,
@@ -727,7 +741,7 @@ export async function handleMemory(
727
741
  if (intent) {
728
742
  const consumed =
729
743
  consumptionMode === "prompt"
730
- ? ranked.filter((r) => r.similarity > SIMILARITY_THRESHOLD)
744
+ ? ranked.filter((r) => memoryRelevance(r) > SIMILARITY_THRESHOLD)
731
745
  : ranked;
732
746
  consumedIds = dedupeMemoryDocumentIds(consumed);
733
747
  try {
@@ -81,7 +81,7 @@ const scheduleWithOptionalFavoriteSchema = ScheduledTaskSchema.safeExtend({
81
81
  /** Slim list row — `taskTemplate` swapped for a bounded preview (see `ScheduledTaskSummary`). */
82
82
  const { taskTemplate: _omittedTaskTemplate, ...scheduleShapeWithoutTemplate } =
83
83
  ScheduledTaskSchema.shape;
84
- const scheduleSummaryWithFavoriteSchema = z.object({
84
+ export const scheduleSummaryWithFavoriteSchema = z.object({
85
85
  ...scheduleShapeWithoutTemplate,
86
86
  taskTemplatePreview: z.string(),
87
87
  favorite: z.boolean(),
@@ -18,7 +18,12 @@ import {
18
18
  import { recordSessionCost } from "../otel";
19
19
  import { incrementServerSessionsProcessed } from "../server-runtime-counters";
20
20
  import type { SessionCost } from "../types";
21
- import { SessionCostModelBreakdownSchema, SessionCostSchema, SessionLogSchema } from "../types";
21
+ import {
22
+ PricingProviderSchema,
23
+ SessionCostModelBreakdownSchema,
24
+ SessionCostSchema,
25
+ SessionLogSchema,
26
+ } from "../types";
22
27
  import { route } from "./route-def";
23
28
  import { recomputeSessionCost } from "./session-cost-recompute";
24
29
  import { cachedUsageReport } from "./usage-cache";
@@ -205,9 +210,9 @@ const createSessionCostRoute = route({
205
210
  // its own billing and the adapter reports `totalCostUsd: 0`, so the row
206
211
  // lands as `costSource: 'unpriced'`. It still has to be accepted here or
207
212
  // every ACP session's cost POST 400s and the row is silently dropped.
208
- provider: z
209
- .enum(["claude", "claude-managed", "codex", "pi", "opencode", "devin", "gemini", "acp"])
210
- .optional(),
213
+ // Reuses PricingProviderSchema so this list can't drift from it again
214
+ // (#1636: it previously forgot `dsh`).
215
+ provider: PricingProviderSchema.optional(),
211
216
  /**
212
217
  * Phase 6: epoch-ms timestamp used as the "active price at time T" lookup
213
218
  * basis. Defaults to `Date.now()` when omitted. Including it lets