@vellumai/assistant 0.5.13 → 0.5.14

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 (425) hide show
  1. package/.env.example +1 -6
  2. package/AGENTS.md +4 -0
  3. package/ARCHITECTURE.md +0 -1
  4. package/bunfig.toml +1 -0
  5. package/docs/architecture/memory.md +3 -3
  6. package/openapi.yaml +127 -22
  7. package/package.json +1 -1
  8. package/src/__tests__/access-request-decision.test.ts +2 -32
  9. package/src/__tests__/actor-token-service.test.ts +1 -31
  10. package/src/__tests__/anthropic-provider.test.ts +53 -40
  11. package/src/__tests__/app-git-history.test.ts +9 -17
  12. package/src/__tests__/app-git-service.test.ts +14 -20
  13. package/src/__tests__/app-store-dir-names.test.ts +10 -20
  14. package/src/__tests__/approval-cascade.test.ts +2 -19
  15. package/src/__tests__/approval-primitive.test.ts +2 -27
  16. package/src/__tests__/approval-routes-http.test.ts +2 -30
  17. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -28
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -45
  19. package/src/__tests__/attachments-store.test.ts +5 -32
  20. package/src/__tests__/audit-log-rotation.test.ts +5 -36
  21. package/src/__tests__/avatar-e2e.test.ts +1 -9
  22. package/src/__tests__/avatar-generator.test.ts +1 -7
  23. package/src/__tests__/browser-fill-credential.test.ts +0 -4
  24. package/src/__tests__/browser-manager.test.ts +0 -6
  25. package/src/__tests__/call-controller.test.ts +1 -22
  26. package/src/__tests__/call-conversation-messages.test.ts +0 -21
  27. package/src/__tests__/call-domain.test.ts +0 -25
  28. package/src/__tests__/call-pointer-messages.test.ts +0 -21
  29. package/src/__tests__/call-recovery.test.ts +0 -22
  30. package/src/__tests__/call-routes-http.test.ts +0 -24
  31. package/src/__tests__/call-store.test.ts +0 -21
  32. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -24
  33. package/src/__tests__/canonical-guardian-store.test.ts +48 -21
  34. package/src/__tests__/channel-approval-routes.test.ts +6 -26
  35. package/src/__tests__/channel-approvals.test.ts +1 -38
  36. package/src/__tests__/channel-delivery-store.test.ts +0 -21
  37. package/src/__tests__/channel-guardian.test.ts +0 -26
  38. package/src/__tests__/channel-reply-delivery.test.ts +5 -0
  39. package/src/__tests__/channel-retry-sweep.test.ts +0 -21
  40. package/src/__tests__/checker.test.ts +26 -61
  41. package/src/__tests__/clawhub.test.ts +9 -25
  42. package/src/__tests__/cli-command-risk-guard.test.ts +0 -18
  43. package/src/__tests__/config-loader-backfill.test.ts +9 -28
  44. package/src/__tests__/config-schema-cmd.test.ts +5 -25
  45. package/src/__tests__/config-schema.test.ts +21 -40
  46. package/src/__tests__/config-watcher.test.ts +4 -91
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -21
  48. package/src/__tests__/contacts-tools.test.ts +0 -21
  49. package/src/__tests__/context-memory-e2e.test.ts +0 -21
  50. package/src/__tests__/context-window-manager.test.ts +130 -3
  51. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -4
  52. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -4
  53. package/src/__tests__/conversation-agent-loop.test.ts +0 -4
  54. package/src/__tests__/conversation-attachments.test.ts +1 -24
  55. package/src/__tests__/conversation-attention-store.test.ts +0 -21
  56. package/src/__tests__/conversation-attention-telegram.test.ts +0 -22
  57. package/src/__tests__/conversation-clear-safety.test.ts +0 -22
  58. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -21
  59. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -24
  60. package/src/__tests__/conversation-disk-view-integration.test.ts +1 -23
  61. package/src/__tests__/conversation-disk-view.test.ts +5 -27
  62. package/src/__tests__/conversation-error.test.ts +1 -1
  63. package/src/__tests__/conversation-fork-crud.test.ts +1 -33
  64. package/src/__tests__/conversation-fork-route.test.ts +0 -27
  65. package/src/__tests__/conversation-history-web-search.test.ts +23 -16
  66. package/src/__tests__/conversation-init.benchmark.test.ts +22 -43
  67. package/src/__tests__/conversation-key-store-disk-view.test.ts +8 -34
  68. package/src/__tests__/conversation-load-history-repair.test.ts +0 -4
  69. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -4
  70. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -4
  71. package/src/__tests__/conversation-queue.test.ts +8 -8
  72. package/src/__tests__/conversation-routes-disk-view.test.ts +13 -51
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +64 -38
  74. package/src/__tests__/conversation-slash-commands.test.ts +5 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +0 -4
  76. package/src/__tests__/conversation-slash-unknown.test.ts +0 -4
  77. package/src/__tests__/conversation-speed-override.test.ts +326 -0
  78. package/src/__tests__/conversation-starter-routes.test.ts +0 -23
  79. package/src/__tests__/conversation-store.test.ts +0 -21
  80. package/src/__tests__/conversation-unread-route.test.ts +0 -24
  81. package/src/__tests__/conversation-usage.test.ts +56 -21
  82. package/src/__tests__/conversation-wipe.test.ts +0 -21
  83. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -4
  84. package/src/__tests__/conversation-workspace-injection.test.ts +0 -4
  85. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -4
  86. package/src/__tests__/credential-execution-shell-lockdown.test.ts +8 -5
  87. package/src/__tests__/credential-vault-unit.test.ts +9 -428
  88. package/src/__tests__/credentials-cli.test.ts +10 -10
  89. package/src/__tests__/daemon-assistant-events.test.ts +0 -19
  90. package/src/__tests__/date-context.test.ts +77 -97
  91. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +7 -24
  92. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +29 -42
  93. package/src/__tests__/delete-managed-skill-tool.test.ts +2 -10
  94. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -26
  95. package/src/__tests__/docker-signing-key-bootstrap.test.ts +61 -15
  96. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -36
  97. package/src/__tests__/email-cli.test.ts +6 -6
  98. package/src/__tests__/ephemeral-permissions.test.ts +5 -17
  99. package/src/__tests__/first-greeting.test.ts +4 -32
  100. package/src/__tests__/followup-tools.test.ts +0 -21
  101. package/src/__tests__/gateway-only-enforcement.test.ts +0 -20
  102. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -23
  103. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -23
  104. package/src/__tests__/guardian-action-followup-store.test.ts +0 -21
  105. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -21
  106. package/src/__tests__/guardian-action-late-reply.test.ts +0 -21
  107. package/src/__tests__/guardian-action-store.test.ts +0 -21
  108. package/src/__tests__/guardian-action-sweep.test.ts +0 -21
  109. package/src/__tests__/guardian-binding-drift-heal.test.ts +0 -23
  110. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +172 -22
  111. package/src/__tests__/guardian-dispatch.test.ts +0 -21
  112. package/src/__tests__/guardian-grant-minting.test.ts +0 -22
  113. package/src/__tests__/guardian-outbound-http.test.ts +0 -22
  114. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -23
  115. package/src/__tests__/guardian-routing-invariants.test.ts +0 -22
  116. package/src/__tests__/guardian-routing-state.test.ts +0 -22
  117. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -24
  118. package/src/__tests__/headless-browser-interactions.test.ts +0 -4
  119. package/src/__tests__/headless-browser-navigate.test.ts +0 -4
  120. package/src/__tests__/headless-browser-read-tools.test.ts +0 -4
  121. package/src/__tests__/headless-browser-snapshot.test.ts +0 -4
  122. package/src/__tests__/heartbeat-service.test.ts +99 -26
  123. package/src/__tests__/hooks-blocking.test.ts +3 -3
  124. package/src/__tests__/hooks-config.test.ts +7 -7
  125. package/src/__tests__/hooks-discovery.test.ts +3 -3
  126. package/src/__tests__/hooks-integration.test.ts +5 -5
  127. package/src/__tests__/hooks-manager.test.ts +3 -3
  128. package/src/__tests__/hooks-runner.test.ts +5 -23
  129. package/src/__tests__/hooks-settings.test.ts +3 -3
  130. package/src/__tests__/hooks-templates.test.ts +3 -3
  131. package/src/__tests__/http-conversation-lineage.test.ts +0 -27
  132. package/src/__tests__/identity-intro-cache.test.ts +0 -4
  133. package/src/__tests__/inbound-invite-redemption.test.ts +0 -22
  134. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -16
  135. package/src/__tests__/intent-routing.test.ts +2 -55
  136. package/src/__tests__/invite-redemption-service.test.ts +0 -21
  137. package/src/__tests__/invite-routes-http.test.ts +0 -21
  138. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +0 -17
  139. package/src/__tests__/journal-context.test.ts +8 -75
  140. package/src/__tests__/list-messages-attachments.test.ts +0 -22
  141. package/src/__tests__/llm-context-route-provider.test.ts +0 -21
  142. package/src/__tests__/llm-request-log-turn-query.test.ts +46 -28
  143. package/src/__tests__/llm-usage-store.test.ts +0 -21
  144. package/src/__tests__/log-export-workspace.test.ts +1 -1
  145. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -1
  146. package/src/__tests__/managed-store.test.ts +1 -1
  147. package/src/__tests__/mcp-cli.test.ts +7 -10
  148. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -21
  149. package/src/__tests__/memory-jobs-worker-backoff.test.ts +0 -11
  150. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -21
  151. package/src/__tests__/memory-recall-log-store.test.ts +0 -27
  152. package/src/__tests__/memory-recall-quality.test.ts +0 -21
  153. package/src/__tests__/memory-regressions.experimental.test.ts +31 -30
  154. package/src/__tests__/memory-regressions.test.ts +282 -70
  155. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -21
  156. package/src/__tests__/memory-upsert-concurrency.test.ts +0 -21
  157. package/src/__tests__/messaging-send-tool.test.ts +201 -0
  158. package/src/__tests__/migration-cross-version-compatibility.test.ts +18 -13
  159. package/src/__tests__/migration-export-http.test.ts +7 -1
  160. package/src/__tests__/migration-import-commit-http.test.ts +16 -14
  161. package/src/__tests__/migration-import-preflight-http.test.ts +27 -44
  162. package/src/__tests__/migration-validate-http.test.ts +1 -28
  163. package/src/__tests__/native-web-search.test.ts +25 -22
  164. package/src/__tests__/non-member-access-request.test.ts +0 -22
  165. package/src/__tests__/notification-guardian-path.test.ts +0 -21
  166. package/src/__tests__/notification-schedule-dedup.test.ts +1 -25
  167. package/src/__tests__/oauth-apps-routes.test.ts +103 -2
  168. package/src/__tests__/oauth-cli.test.ts +52 -0
  169. package/src/__tests__/oauth-provider-profiles.test.ts +0 -16
  170. package/src/__tests__/oauth-provider-serializer.test.ts +232 -0
  171. package/src/__tests__/oauth-providers-routes.test.ts +257 -0
  172. package/src/__tests__/oauth-store.test.ts +0 -21
  173. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  174. package/src/__tests__/openai-provider.test.ts +261 -0
  175. package/src/__tests__/pairing-concurrent.test.ts +6 -6
  176. package/src/__tests__/pairing-routes.test.ts +7 -1
  177. package/src/__tests__/path-policy.test.ts +1 -1
  178. package/src/__tests__/platform.test.ts +64 -88
  179. package/src/__tests__/playbook-execution.test.ts +0 -21
  180. package/src/__tests__/playbook-tools.test.ts +0 -21
  181. package/src/__tests__/pricing.test.ts +100 -0
  182. package/src/__tests__/relay-server.test.ts +1 -25
  183. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -24
  184. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -24
  185. package/src/__tests__/runtime-events-sse.test.ts +0 -24
  186. package/src/__tests__/sandbox-diagnostics.test.ts +2 -1
  187. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -1
  188. package/src/__tests__/schedule-store.test.ts +0 -21
  189. package/src/__tests__/schedule-tools.test.ts +0 -21
  190. package/src/__tests__/scheduler-recurrence.test.ts +0 -21
  191. package/src/__tests__/scoped-approval-grants.test.ts +0 -21
  192. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -21
  193. package/src/__tests__/secret-allowlist.test.ts +1 -1
  194. package/src/__tests__/secret-ingress-channel.test.ts +0 -5
  195. package/src/__tests__/secret-ingress-cli.test.ts +0 -6
  196. package/src/__tests__/secret-ingress-http.test.ts +0 -5
  197. package/src/__tests__/secret-ingress.test.ts +0 -5
  198. package/src/__tests__/send-endpoint-busy.test.ts +0 -24
  199. package/src/__tests__/sequence-store.test.ts +0 -21
  200. package/src/__tests__/server-history-render.test.ts +0 -24
  201. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -4
  202. package/src/__tests__/skill-load-inline-command.test.ts +9 -0
  203. package/src/__tests__/skill-load-inline-includes.test.ts +9 -0
  204. package/src/__tests__/skill-load-tool.test.ts +11 -0
  205. package/src/__tests__/skills-uninstall.test.ts +10 -8
  206. package/src/__tests__/skills.test.ts +1 -1
  207. package/src/__tests__/slack-channel-config.test.ts +1 -1
  208. package/src/__tests__/slack-inbound-verification.test.ts +0 -22
  209. package/src/__tests__/starter-bundle.test.ts +4 -1
  210. package/src/__tests__/suggestion-routes.test.ts +2 -0
  211. package/src/__tests__/system-prompt.test.ts +1 -1
  212. package/src/__tests__/terminal-tools.test.ts +1 -1
  213. package/src/__tests__/test-preload.ts +31 -0
  214. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -1
  215. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  216. package/src/__tests__/tool-executor.test.ts +0 -20
  217. package/src/__tests__/tool-input-summary.test.ts +124 -0
  218. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -1
  219. package/src/__tests__/trust-store.test.ts +7 -1
  220. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -1
  221. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -1
  222. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -1
  223. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  224. package/src/__tests__/turn-boundary-resolution.test.ts +1 -1
  225. package/src/__tests__/twilio-routes.test.ts +1 -1
  226. package/src/__tests__/update-bulletin.test.ts +1 -1
  227. package/src/__tests__/vbundle-pax-and-symlink.test.ts +1 -1
  228. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -0
  229. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -1
  230. package/src/__tests__/voice-session-bridge.test.ts +1 -1
  231. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +4 -4
  232. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -1
  233. package/src/__tests__/workspace-migration-down-functions.test.ts +15 -3
  234. package/src/__tests__/workspace-migration-seed-device-id.test.ts +40 -4
  235. package/src/agent/loop.ts +6 -9
  236. package/src/approvals/guardian-decision-primitive.ts +46 -18
  237. package/src/approvals/guardian-request-resolvers.ts +19 -2
  238. package/src/calls/active-call-lease.ts +2 -2
  239. package/src/cli/AGENTS.md +1 -1
  240. package/src/cli/commands/doctor.ts +9 -9
  241. package/src/cli/commands/memory.ts +142 -0
  242. package/src/cli/commands/oauth/__tests__/connect.test.ts +13 -11
  243. package/src/cli/commands/oauth/__tests__/ping.test.ts +1 -1
  244. package/src/cli/commands/oauth/connect.ts +13 -12
  245. package/src/cli/commands/oauth/index.ts +1 -1
  246. package/src/cli/commands/oauth/providers.ts +47 -62
  247. package/src/cli/commands/platform/__tests__/connect.test.ts +72 -46
  248. package/src/cli/commands/platform/__tests__/disconnect.test.ts +54 -1
  249. package/src/cli/commands/platform/__tests__/status.test.ts +36 -0
  250. package/src/cli/commands/platform/connect.ts +17 -7
  251. package/src/cli/commands/platform/disconnect.ts +28 -3
  252. package/src/cli/commands/platform/index.ts +3 -3
  253. package/src/cli.ts +1 -299
  254. package/src/config/assistant-feature-flags.ts +23 -15
  255. package/src/config/bundled-skills/app-builder/TOOLS.json +16 -0
  256. package/src/config/bundled-skills/app-builder/tools/app-create.ts +4 -0
  257. package/src/config/bundled-skills/app-builder/tools/app-delete.ts +5 -1
  258. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +9 -1
  259. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +5 -1
  260. package/src/config/bundled-skills/contacts/TOOLS.json +8 -0
  261. package/src/config/bundled-skills/contacts/tools/contact-search.ts +10 -1
  262. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +16 -2
  263. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +1 -0
  264. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  265. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +37 -0
  266. package/src/config/bundled-skills/slack/SKILL.md +18 -0
  267. package/src/config/env-registry.ts +15 -11
  268. package/src/config/env.ts +1 -11
  269. package/src/config/feature-flag-registry.json +16 -0
  270. package/src/config/schema.ts +4 -0
  271. package/src/config/schemas/heartbeat.ts +6 -1
  272. package/src/config/schemas/inference.ts +14 -3
  273. package/src/config/schemas/memory-processing.ts +16 -8
  274. package/src/config/schemas/memory-retrieval.ts +3 -3
  275. package/src/config/skills.ts +1 -1
  276. package/src/context/window-manager.ts +174 -51
  277. package/src/credential-execution/executable-discovery.ts +2 -2
  278. package/src/daemon/approved-devices-store.ts +2 -2
  279. package/src/daemon/assistant-attachments.ts +2 -0
  280. package/src/daemon/config-watcher.ts +4 -50
  281. package/src/daemon/conversation-agent-loop-handlers.ts +9 -1
  282. package/src/daemon/conversation-agent-loop.ts +12 -0
  283. package/src/daemon/conversation-error.ts +3 -5
  284. package/src/daemon/conversation-history.ts +7 -3
  285. package/src/daemon/conversation-lifecycle.ts +16 -0
  286. package/src/daemon/conversation-messaging.ts +1 -0
  287. package/src/daemon/conversation-notifiers.ts +67 -30
  288. package/src/daemon/conversation-process.ts +161 -2
  289. package/src/daemon/conversation-queue-manager.ts +2 -0
  290. package/src/daemon/conversation-runtime-assembly.ts +33 -11
  291. package/src/daemon/conversation-slash.ts +14 -3
  292. package/src/daemon/conversation-tool-setup.ts +2 -0
  293. package/src/daemon/conversation-usage.ts +32 -4
  294. package/src/daemon/conversation.ts +33 -1
  295. package/src/daemon/daemon-control.ts +32 -16
  296. package/src/daemon/date-context.ts +47 -45
  297. package/src/daemon/dictation-profile-store.ts +2 -2
  298. package/src/daemon/handlers/conversations.ts +19 -0
  299. package/src/daemon/handlers/shared.ts +14 -21
  300. package/src/daemon/lifecycle.ts +5 -7
  301. package/src/daemon/message-types/conversations.ts +2 -0
  302. package/src/daemon/message-types/guardian-actions.ts +3 -17
  303. package/src/daemon/message-types/integrations.ts +11 -1
  304. package/src/daemon/message-types/messages.ts +1 -0
  305. package/src/daemon/pairing-store.ts +2 -79
  306. package/src/daemon/server.ts +154 -8
  307. package/src/daemon/watch-handler.ts +65 -21
  308. package/src/email/guardrails.ts +3 -3
  309. package/src/heartbeat/heartbeat-service.ts +14 -7
  310. package/src/hooks/cli.ts +2 -2
  311. package/src/hooks/config.ts +2 -2
  312. package/src/hooks/discovery.ts +2 -2
  313. package/src/hooks/manager.ts +2 -2
  314. package/src/hooks/runner.ts +5 -2
  315. package/src/hooks/templates.ts +2 -2
  316. package/src/memory/admin.ts +181 -2
  317. package/src/memory/app-git-service.ts +61 -4
  318. package/src/memory/attachments-store.ts +2 -0
  319. package/src/memory/canonical-guardian-store.ts +16 -0
  320. package/src/memory/db-init.ts +8 -0
  321. package/src/memory/embedding-local.ts +5 -2
  322. package/src/memory/indexer.ts +44 -26
  323. package/src/memory/items-extractor.ts +34 -82
  324. package/src/memory/job-handlers/batch-extraction.ts +741 -0
  325. package/src/memory/job-handlers/journal-carry-forward.test.ts +383 -0
  326. package/src/memory/job-handlers/journal-carry-forward.ts +255 -0
  327. package/src/memory/jobs-store.ts +28 -0
  328. package/src/memory/jobs-worker.ts +56 -9
  329. package/src/memory/lifecycle-events-store.ts +4 -2
  330. package/src/memory/llm-request-log-store.ts +40 -2
  331. package/src/memory/llm-usage-store.ts +4 -3
  332. package/src/memory/migrations/199-guardian-request-enrichment-columns.ts +71 -0
  333. package/src/memory/migrations/200-usage-llm-call-count.ts +20 -0
  334. package/src/memory/migrations/index.ts +2 -0
  335. package/src/memory/query-expansion.ts +83 -0
  336. package/src/memory/retriever.test.ts +119 -0
  337. package/src/memory/retriever.ts +513 -105
  338. package/src/memory/schema/guardian.ts +4 -0
  339. package/src/memory/schema/infrastructure.ts +1 -0
  340. package/src/memory/search/formatting.test.ts +140 -0
  341. package/src/memory/search/formatting.ts +143 -198
  342. package/src/memory/search/mmr.ts +136 -0
  343. package/src/memory/search/staleness.ts +0 -15
  344. package/src/memory/search/tier-classifier.ts +10 -21
  345. package/src/memory/search/types.ts +17 -0
  346. package/src/messaging/providers/slack/adapter.ts +51 -5
  347. package/src/notifications/broadcaster.ts +13 -0
  348. package/src/notifications/copy-composer.ts +8 -0
  349. package/src/oauth/connect-orchestrator.ts +1 -1
  350. package/src/oauth/connection-resolver.ts +2 -2
  351. package/src/oauth/provider-serializer.ts +116 -0
  352. package/src/permissions/trust-store.ts +24 -7
  353. package/src/prompts/__tests__/build-cli-reference-section.test.ts +5 -0
  354. package/src/prompts/journal-context.ts +50 -35
  355. package/src/prompts/persona-resolver.ts +1 -1
  356. package/src/prompts/system-prompt.ts +27 -28
  357. package/src/prompts/templates/BOOTSTRAP.md +14 -1
  358. package/src/prompts/templates/HEARTBEAT.md +10 -0
  359. package/src/prompts/templates/NOW.md +19 -25
  360. package/src/prompts/templates/SOUL.md +13 -1
  361. package/src/prompts/templates/UPDATES.md +12 -0
  362. package/src/prompts/update-bulletin.ts +1 -1
  363. package/src/providers/anthropic/client.ts +89 -18
  364. package/src/providers/model-catalog.ts +22 -2
  365. package/src/providers/model-intents.ts +2 -2
  366. package/src/providers/openai/client.ts +40 -1
  367. package/src/providers/retry.ts +23 -4
  368. package/src/providers/types.ts +2 -0
  369. package/src/runtime/assistant-scope.ts +1 -1
  370. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -0
  371. package/src/runtime/auth/route-policy.ts +1 -0
  372. package/src/runtime/auth/token-service.ts +51 -29
  373. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -1
  374. package/src/runtime/guardian-decision-types.ts +16 -10
  375. package/src/runtime/http-server.ts +3 -14
  376. package/src/runtime/http-types.ts +1 -0
  377. package/src/runtime/migrations/vbundle-builder.ts +7 -4
  378. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -4
  379. package/src/runtime/migrations/vbundle-importer.ts +1 -1
  380. package/src/runtime/routes/conversation-query-routes.ts +40 -8
  381. package/src/runtime/routes/conversation-routes.ts +125 -3
  382. package/src/runtime/routes/guardian-action-routes.ts +9 -3
  383. package/src/runtime/routes/identity-routes.ts +25 -4
  384. package/src/runtime/routes/llm-context-normalization.ts +1 -0
  385. package/src/runtime/routes/log-export-routes.ts +34 -12
  386. package/src/runtime/routes/migration-routes.ts +6 -10
  387. package/src/runtime/routes/oauth-apps.ts +2 -9
  388. package/src/runtime/routes/oauth-providers.ts +60 -0
  389. package/src/runtime/routes/pairing-routes.ts +0 -8
  390. package/src/runtime/routes/settings-routes.ts +0 -1
  391. package/src/runtime/routes/telemetry-routes.ts +16 -4
  392. package/src/security/encrypted-store.ts +2 -2
  393. package/src/security/secret-allowlist.ts +3 -3
  394. package/src/signals/emit-event.ts +42 -0
  395. package/src/signals/user-message.ts +37 -0
  396. package/src/telemetry/usage-telemetry-reporter.test.ts +83 -19
  397. package/src/telemetry/usage-telemetry-reporter.ts +23 -17
  398. package/src/tools/browser/runtime-check.ts +2 -2
  399. package/src/tools/credentials/vault.ts +2 -249
  400. package/src/tools/memory/definitions.ts +1 -1
  401. package/src/tools/memory/handlers.test.ts +50 -8
  402. package/src/tools/memory/handlers.ts +3 -1
  403. package/src/tools/side-effects.ts +1 -6
  404. package/src/tools/terminal/safe-env.ts +3 -2
  405. package/src/tools/terminal/shell.ts +11 -14
  406. package/src/tools/tool-approval-handler.ts +20 -1
  407. package/src/tools/tool-input-summary.ts +66 -0
  408. package/src/tools/types.ts +4 -0
  409. package/src/usage/types.ts +4 -0
  410. package/src/util/device-id.ts +10 -10
  411. package/src/util/platform.ts +71 -33
  412. package/src/util/pricing.ts +19 -6
  413. package/src/util/strip-comment-lines.ts +28 -0
  414. package/src/workspace/git-service.ts +8 -18
  415. package/src/workspace/migrations/003-seed-device-id.ts +6 -4
  416. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +7 -1
  417. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -4
  418. package/src/workspace/migrations/021-move-signals-to-workspace.ts +84 -0
  419. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +94 -0
  420. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +86 -0
  421. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +126 -0
  422. package/src/workspace/migrations/migrate-to-workspace-volume.ts +3 -6
  423. package/src/workspace/migrations/registry.ts +8 -0
  424. package/src/signals/confirm.ts +0 -82
  425. package/src/signals/trust-rule.ts +0 -174
@@ -88,6 +88,10 @@ export const canonicalGuardianRequests = sqliteTable(
88
88
  requestCode: text("request_code"),
89
89
  toolName: text("tool_name"),
90
90
  inputDigest: text("input_digest"),
91
+ commandPreview: text("command_preview"),
92
+ riskLevel: text("risk_level"),
93
+ activityText: text("activity_text"),
94
+ executionTarget: text("execution_target"),
91
95
  status: text("status").notNull().default("pending"),
92
96
  answerText: text("answer_text"),
93
97
  decidedByExternalUserId: text("decided_by_external_user_id"),
@@ -169,6 +169,7 @@ export const llmUsageEvents = sqliteTable(
169
169
  cacheReadInputTokens: integer("cache_read_input_tokens"),
170
170
  estimatedCostUsd: real("estimated_cost_usd"),
171
171
  pricingStatus: text("pricing_status").notNull(),
172
+ llmCallCount: integer("llm_call_count"),
172
173
  metadataJson: text("metadata_json"),
173
174
  },
174
175
  (table) => [
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Unit tests for buildMemoryInjection — focused on echoes/serendipity
3
+ * rendering and budget enforcement.
4
+ */
5
+ import { describe, expect, test } from "bun:test";
6
+
7
+ import { buildMemoryInjection } from "./formatting.js";
8
+ import type { Candidate } from "./types.js";
9
+
10
+ type CandidateWithLabel = Candidate & { sourceLabel?: string };
11
+
12
+ function makeCandidate(
13
+ overrides: Partial<CandidateWithLabel> & { id: string },
14
+ ): CandidateWithLabel {
15
+ return {
16
+ key: `item:${overrides.id}`,
17
+ type: "item",
18
+ source: "semantic",
19
+ text: overrides.text ?? `Statement for ${overrides.id}`,
20
+ kind: overrides.kind ?? "fact",
21
+ confidence: 1,
22
+ importance: overrides.importance ?? 0.5,
23
+ createdAt: overrides.createdAt ?? Date.now(),
24
+ semantic: 0.8,
25
+ recency: 0.5,
26
+ finalScore: overrides.finalScore ?? 0.6,
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ describe("buildMemoryInjection — echoes section", () => {
32
+ test("renders <echoes> after <recalled> when serendipity items provided", () => {
33
+ const candidates = [makeCandidate({ id: "c1", finalScore: 0.8 })];
34
+ const serendipityItems = [
35
+ makeCandidate({ id: "s1", finalScore: 0, importance: 0.7 }),
36
+ ];
37
+
38
+ const result = buildMemoryInjection({
39
+ candidates,
40
+ serendipityItems,
41
+ totalBudgetTokens: 2000,
42
+ });
43
+
44
+ expect(result).toContain("<recalled>");
45
+ expect(result).toContain("</recalled>");
46
+ expect(result).toContain("<echoes>");
47
+ expect(result).toContain("</echoes>");
48
+ // <echoes> comes after </recalled>
49
+ const recalledEnd = result.indexOf("</recalled>");
50
+ const echoesStart = result.indexOf("<echoes>");
51
+ expect(echoesStart).toBeGreaterThan(recalledEnd);
52
+ });
53
+
54
+ test("renders only <echoes> when no recalled candidates but serendipity items exist", () => {
55
+ const serendipityItems = [
56
+ makeCandidate({ id: "s1", finalScore: 0, importance: 0.6 }),
57
+ makeCandidate({ id: "s2", finalScore: 0, importance: 0.4 }),
58
+ ];
59
+
60
+ const result = buildMemoryInjection({
61
+ candidates: [],
62
+ serendipityItems,
63
+ totalBudgetTokens: 2000,
64
+ });
65
+
66
+ expect(result).toContain("<memory_context");
67
+ expect(result).not.toContain("<recalled>");
68
+ expect(result).toContain("<echoes>");
69
+ expect(result).toContain("</echoes>");
70
+ expect(result).toContain("s1");
71
+ expect(result).toContain("s2");
72
+ });
73
+
74
+ test("echoes section respects ~400 token cap", () => {
75
+ // Create serendipity items with very long text to test budget
76
+ const longText = "word ".repeat(200); // ~200 tokens
77
+ const serendipityItems = [
78
+ makeCandidate({ id: "s1", text: longText, finalScore: 0 }),
79
+ makeCandidate({ id: "s2", text: longText, finalScore: 0 }),
80
+ makeCandidate({ id: "s3", text: longText, finalScore: 0 }),
81
+ ];
82
+
83
+ const result = buildMemoryInjection({
84
+ candidates: [],
85
+ serendipityItems,
86
+ totalBudgetTokens: 5000, // plenty of total budget
87
+ });
88
+
89
+ // At ~200 tokens each, the 400-token echoes cap should allow at most 2
90
+ const itemMatches = result.match(/<item /g) ?? [];
91
+ expect(itemMatches.length).toBeLessThanOrEqual(2);
92
+ });
93
+
94
+ test("no <echoes> section when serendipity array is empty", () => {
95
+ const candidates = [makeCandidate({ id: "c1", finalScore: 0.8 })];
96
+
97
+ const result = buildMemoryInjection({
98
+ candidates,
99
+ serendipityItems: [],
100
+ totalBudgetTokens: 2000,
101
+ });
102
+
103
+ expect(result).toContain("<recalled>");
104
+ expect(result).not.toContain("<echoes>");
105
+ });
106
+
107
+ test("no <echoes> section when serendipity items omitted", () => {
108
+ const candidates = [makeCandidate({ id: "c1", finalScore: 0.8 })];
109
+
110
+ const result = buildMemoryInjection({
111
+ candidates,
112
+ totalBudgetTokens: 2000,
113
+ });
114
+
115
+ expect(result).toContain("<recalled>");
116
+ expect(result).not.toContain("<echoes>");
117
+ });
118
+
119
+ test("echoes items include importance and kind attributes", () => {
120
+ const serendipityItems = [
121
+ makeCandidate({
122
+ id: "echo1",
123
+ kind: "preference",
124
+ importance: 0.85,
125
+ text: "User likes dark mode",
126
+ finalScore: 0,
127
+ }),
128
+ ];
129
+
130
+ const result = buildMemoryInjection({
131
+ candidates: [],
132
+ serendipityItems,
133
+ totalBudgetTokens: 2000,
134
+ });
135
+
136
+ expect(result).toContain('kind="preference"');
137
+ expect(result).toContain('importance="0.85"');
138
+ expect(result).toContain("User likes dark mode");
139
+ });
140
+ });
@@ -1,5 +1,12 @@
1
+ import { eq } from "drizzle-orm";
2
+
1
3
  import { estimateTextTokens } from "../../context/token-estimator.js";
2
- import type { TieredCandidate } from "./tier-classifier.js";
4
+ import { getLogger } from "../../util/logger.js";
5
+ import { getDb } from "../db.js";
6
+ import { memoryItems } from "../schema.js";
7
+ import type { Candidate } from "./types.js";
8
+
9
+ const log = getLogger("memory-formatting");
3
10
 
4
11
  /**
5
12
  * Escape XML-like tag sequences in recalled text to prevent delimiter injection.
@@ -68,215 +75,182 @@ export function formatRelativeTime(epochMs: number): string {
68
75
  }
69
76
 
70
77
  // ---------------------------------------------------------------------------
71
- // Two-layer injection format
78
+ // Unified injection format
72
79
  // ---------------------------------------------------------------------------
73
80
 
74
- /** Kinds classified as identity for the <user_identity> section. */
75
- export const IDENTITY_KINDS = new Set(["identity"]);
76
-
77
- /** Kinds classified as preferences for the <applicable_preferences> section. */
78
- export const PREFERENCE_KINDS = new Set(["preference", "constraint"]);
79
-
80
- /** Kinds classified as capabilities for the <available_capabilities> section. */
81
- export const CAPABILITY_KINDS = new Set(["capability"]);
82
-
83
81
  /**
84
- * Build a two-layer XML injection block from tiered candidates.
82
+ * Build a unified `<memory_context>` XML injection block from scored candidates.
83
+ *
84
+ * All candidates are rendered in a single `<recalled>` section sorted by
85
+ * `finalScore` descending, with each candidate tagged by type:
86
+ * - items: `<item id="item:ID" kind="KIND" importance="N.NN" timestamp="..." from="...">`
87
+ * - segments: `<segment id="seg:ID" timestamp="..." from="...">`
88
+ * - summaries: `<summary id="sum:ID" timestamp="..." from="...">`
85
89
  *
86
- * Sections:
87
- * - `<user_identity>`: identity-kind items from tier 1 (plain statements)
88
- * - `<relevant_context>`: tier 1 non-identity, non-preference items (episode-wrapped)
89
- * - `<applicable_preferences>`: preference/constraint items from tier 1 (plain statements)
90
- * - `<possibly_relevant>`: tier 2 items (episode-wrapped with optional staleness)
90
+ * An optional `<echoes>` section renders serendipity items — random
91
+ * importance-weighted memories for unexpected connections.
91
92
  *
92
- * Empty sections are omitted. If all sections are empty, returns `""`.
93
+ * Respects token budget: iterates candidates in score order, accumulates
94
+ * token estimates, and stops when the budget is exhausted.
93
95
  */
94
- export function buildTwoLayerInjection(params: {
95
- identityItems: TieredCandidate[];
96
- tier1Candidates: TieredCandidate[];
97
- tier2Candidates: TieredCandidate[];
98
- preferences: TieredCandidate[];
99
- capabilities: TieredCandidate[];
96
+ export function buildMemoryInjection(params: {
97
+ candidates: Array<Candidate & { sourceLabel?: string; staleness?: string; supersedes?: string }>;
98
+ serendipityItems?: Array<Candidate & { sourceLabel?: string }>;
100
99
  totalBudgetTokens?: number;
101
100
  }): string {
102
- const {
103
- identityItems,
104
- tier1Candidates,
105
- tier2Candidates,
106
- preferences,
107
- capabilities,
108
- totalBudgetTokens,
109
- } = params;
101
+ const { candidates, serendipityItems, totalBudgetTokens } = params;
110
102
 
111
- // If everything is empty, return empty string
112
- if (
113
- identityItems.length === 0 &&
114
- tier1Candidates.length === 0 &&
115
- tier2Candidates.length === 0 &&
116
- preferences.length === 0 &&
117
- capabilities.length === 0
118
- ) {
103
+ if (candidates.length === 0 && (!serendipityItems || serendipityItems.length === 0)) {
119
104
  return "";
120
105
  }
121
106
 
122
- // Budget tracking tier 1 gets priority.
123
- // Reserve tokens for XML wrapper overhead (<memory_context>, section tags,
124
- // newlines between sections) so the final assembled text stays within budget.
107
+ // Sort by finalScore descending
108
+ const sorted = [...candidates].sort((a, b) => b.finalScore - a.finalScore);
109
+
110
+ // Reserve tokens for structural overhead
125
111
  const WRAPPER_OVERHEAD_TOKENS = estimateTextTokens(
126
- "<memory_context __injected>\n\n\n\n</memory_context>",
112
+ "<memory_context __injected>\n<recalled>\n</recalled>\n</memory_context>",
127
113
  );
128
- const SECTION_TAG_TOKENS = estimateTextTokens(
129
- "<possibly_relevant>\n\n</possibly_relevant>",
130
- );
131
- const sectionCount = [
132
- identityItems.length,
133
- tier1Candidates.length,
134
- tier2Candidates.length,
135
- preferences.length,
136
- capabilities.length,
137
- ].filter((n) => n > 0).length;
138
- const structuralOverhead =
139
- WRAPPER_OVERHEAD_TOKENS + sectionCount * SECTION_TAG_TOKENS;
140
114
  let remainingTokens = totalBudgetTokens
141
- ? Math.max(1, totalBudgetTokens - structuralOverhead)
115
+ ? Math.max(1, totalBudgetTokens - WRAPPER_OVERHEAD_TOKENS)
142
116
  : Infinity;
143
117
 
144
- // Render tier 1 items first (identity, relevant context, preferences)
145
- const identityLines = renderPlainStatements(identityItems, remainingTokens);
146
- remainingTokens -= estimateTextTokens(identityLines.join("\n"));
147
-
148
- const relevantEpisodes = renderEpisodes(tier1Candidates, remainingTokens);
149
- remainingTokens -= estimateTextTokens(relevantEpisodes.join("\n"));
150
-
151
- const preferenceLines = renderPlainStatements(preferences, remainingTokens);
152
- remainingTokens -= estimateTextTokens(preferenceLines.join("\n"));
153
-
154
- const capabilityLines = renderPlainStatements(capabilities, remainingTokens);
155
- remainingTokens -= estimateTextTokens(capabilityLines.join("\n"));
156
-
157
- // Tier 2 uses remaining budget
158
- const possiblyRelevantEpisodes = renderEpisodesWithStaleness(
159
- tier2Candidates,
160
- remainingTokens,
161
- );
162
-
163
- // Assemble sections — omit empty ones
164
- const sections: string[] = [];
165
-
166
- if (identityLines.length > 0) {
167
- sections.push(
168
- `<user_identity>\n${identityLines.join("\n")}\n</user_identity>`,
169
- );
118
+ // Render candidates within budget
119
+ const lines: string[] = [];
120
+ for (const c of sorted) {
121
+ if (remainingTokens <= 0) break;
122
+ const line = renderCandidate(c);
123
+ const tokens = estimateTextTokens(line);
124
+ if (tokens > remainingTokens) continue;
125
+ lines.push(line);
126
+ remainingTokens -= tokens;
170
127
  }
171
128
 
172
- if (relevantEpisodes.length > 0) {
173
- sections.push(
174
- `<relevant_context>\n${relevantEpisodes.join("\n")}\n</relevant_context>`,
175
- );
129
+ if (lines.length === 0 && (!serendipityItems || serendipityItems.length === 0)) {
130
+ return "";
176
131
  }
177
132
 
178
- if (preferenceLines.length > 0) {
179
- sections.push(
180
- `<applicable_preferences>\n${preferenceLines.join("\n")}\n</applicable_preferences>`,
181
- );
182
- }
133
+ const sections: string[] = [];
183
134
 
184
- if (capabilityLines.length > 0) {
185
- sections.push(
186
- `<available_capabilities>\n${capabilityLines.join("\n")}\n</available_capabilities>`,
187
- );
135
+ if (lines.length > 0) {
136
+ sections.push(`<recalled>\n${lines.join("\n")}\n</recalled>`);
188
137
  }
189
138
 
190
- if (possiblyRelevantEpisodes.length > 0) {
191
- sections.push(
192
- `<possibly_relevant>\n${possiblyRelevantEpisodes.join("\n")}\n</possibly_relevant>`,
193
- );
139
+ // Echoes section for serendipity items — capped at ~400 tokens of
140
+ // the remaining budget after <recalled> items are rendered.
141
+ if (serendipityItems && serendipityItems.length > 0) {
142
+ const ECHOES_MAX_TOKENS = 400;
143
+ let echoesBudget = Math.min(remainingTokens, ECHOES_MAX_TOKENS);
144
+ const echoLines: string[] = [];
145
+ for (const c of serendipityItems) {
146
+ if (echoesBudget <= 0) break;
147
+ const line = renderCandidate(c);
148
+ const tokens = estimateTextTokens(line);
149
+ if (tokens > echoesBudget) continue;
150
+ echoLines.push(line);
151
+ echoesBudget -= tokens;
152
+ remainingTokens -= tokens;
153
+ }
154
+ if (echoLines.length > 0) {
155
+ sections.push(`<echoes>\n${echoLines.join("\n")}\n</echoes>`);
156
+ }
194
157
  }
195
158
 
196
159
  if (sections.length === 0) return "";
197
160
 
198
- return `<memory_context __injected>\n\n${sections.join("\n\n")}\n\n</memory_context>`;
161
+ return `<memory_context __injected>\n${sections.join("\n")}\n</memory_context>`;
199
162
  }
200
163
 
201
164
  /**
202
- * Render candidates as plain statement lines (for identity / preference sections).
165
+ * Look up the supersession chain for a given superseded item ID.
166
+ *
167
+ * Returns the immediate predecessor's statement and timestamp, plus the
168
+ * total chain depth (how many items were superseded in sequence).
169
+ * Chain traversal is capped at 10 iterations to prevent infinite loops.
203
170
  */
204
- function renderPlainStatements(
205
- items: TieredCandidate[],
206
- remainingBudget: number,
207
- ): string[] {
208
- const lines: string[] = [];
209
- let used = 0;
210
- for (const item of items) {
211
- if (used >= remainingBudget) break;
212
- const text = escapeXmlTags(item.text);
213
- const tokens = estimateTextTokens(text);
214
- if (used + tokens > remainingBudget) break;
215
- lines.push(text);
216
- used += tokens;
217
- }
218
- return lines;
219
- }
171
+ export function lookupSupersessionChain(supersededId: string): {
172
+ previousStatement: string;
173
+ previousTimestamp: number;
174
+ chainDepth: number;
175
+ } | null {
176
+ try {
177
+ const db = getDb();
220
178
 
221
- /**
222
- * Render candidates as `<episode>` elements with source attribution.
223
- */
224
- function renderEpisodes(
225
- items: TieredCandidate[],
226
- remainingBudget: number,
227
- ): string[] {
228
- const lines: string[] = [];
229
- let used = 0;
230
- for (const item of items) {
231
- if (used >= remainingBudget) break;
232
- const text = escapeXmlTags(item.text);
233
- const sourceAttr = buildSourceAttr(item);
234
- const line = `<episode${sourceAttr}>\n${text}\n</episode>`;
235
- const tokens = estimateTextTokens(line);
236
- if (used + tokens > remainingBudget) break;
237
- lines.push(line);
238
- used += tokens;
179
+ // Look up the immediate predecessor
180
+ const predecessor = db
181
+ .select({
182
+ statement: memoryItems.statement,
183
+ firstSeenAt: memoryItems.firstSeenAt,
184
+ supersedes: memoryItems.supersedes,
185
+ })
186
+ .from(memoryItems)
187
+ .where(eq(memoryItems.id, supersededId))
188
+ .get();
189
+
190
+ if (!predecessor) return null;
191
+
192
+ // Count chain depth by following supersedes links (cap at 10)
193
+ let chainDepth = 1;
194
+ let currentSupersedes = predecessor.supersedes;
195
+ const MAX_CHAIN_DEPTH = 10;
196
+
197
+ while (currentSupersedes && chainDepth < MAX_CHAIN_DEPTH) {
198
+ const ancestor = db
199
+ .select({ supersedes: memoryItems.supersedes })
200
+ .from(memoryItems)
201
+ .where(eq(memoryItems.id, currentSupersedes))
202
+ .get();
203
+
204
+ if (!ancestor) break;
205
+ chainDepth++;
206
+ currentSupersedes = ancestor.supersedes;
207
+ }
208
+
209
+ return {
210
+ previousStatement: predecessor.statement,
211
+ previousTimestamp: predecessor.firstSeenAt,
212
+ chainDepth,
213
+ };
214
+ } catch (err) {
215
+ log.warn({ err }, "Failed to look up supersession chain");
216
+ return null;
239
217
  }
240
- return lines;
241
218
  }
242
219
 
243
220
  /**
244
- * Render tier 2 candidates as `<episode>` elements with staleness annotation.
221
+ * Render a single candidate as an XML element based on its type.
245
222
  */
246
- function renderEpisodesWithStaleness(
247
- items: TieredCandidate[],
248
- remainingBudget: number,
249
- ): string[] {
250
- const lines: string[] = [];
251
- let used = 0;
252
- for (const item of items) {
253
- if (used >= remainingBudget) break;
254
- const text = escapeXmlTags(item.text);
255
- const sourceAttr = buildSourceAttr(item);
256
- const stalenessAttr =
257
- item.staleness && item.staleness !== "fresh"
258
- ? ` staleness="${escapeXmlAttr(item.staleness)}"`
259
- : "";
260
- const line = `<episode${sourceAttr}${stalenessAttr}>\n${text}\n</episode>`;
261
- const tokens = estimateTextTokens(line);
262
- if (used + tokens > remainingBudget) break;
263
- lines.push(line);
264
- used += tokens;
223
+ function renderCandidate(c: Candidate & { sourceLabel?: string; supersedes?: string }): string {
224
+ const text = escapeXmlTags(c.text);
225
+ const timestamp = formatAbsoluteTime(c.createdAt);
226
+ const fromAttr = c.sourceLabel
227
+ ? ` from="${escapeXmlAttr(c.sourceLabel)}"`
228
+ : "";
229
+ const pathAttr = c.sourcePath
230
+ ? ` path="${escapeXmlAttr(c.sourcePath)}"`
231
+ : "";
232
+
233
+ // Build inline supersession suffix for items
234
+ let supersessionSuffix = "";
235
+ if (c.type === "item" && c.supersedes) {
236
+ const chain = lookupSupersessionChain(c.supersedes);
237
+ if (chain) {
238
+ const prevTimestamp = formatAbsoluteTime(chain.previousTimestamp);
239
+ supersessionSuffix = `<supersedes count="${chain.chainDepth}">${escapeXmlTags(chain.previousStatement)} (${prevTimestamp})</supersedes>`;
240
+ }
265
241
  }
266
- return lines;
267
- }
268
242
 
269
- /**
270
- * Build the `source="..."` attribute for an episode tag.
271
- * Uses the candidate's sourceLabel (conversation title) if available,
272
- * combined with a short date from createdAt.
273
- */
274
- function buildSourceAttr(item: TieredCandidate): string {
275
- const date = formatShortDate(item.createdAt);
276
- if (item.sourceLabel) {
277
- return ` source="${escapeXmlAttr(`${item.sourceLabel} (${date})`)}"`;
243
+ switch (c.type) {
244
+ case "item":
245
+ return `<item id="item:${escapeXmlAttr(c.id)}" kind="${escapeXmlAttr(c.kind)}" importance="${c.importance.toFixed(2)}" timestamp="${escapeXmlAttr(timestamp)}"${fromAttr}${pathAttr}>${text}${supersessionSuffix}</item>`;
246
+ case "segment":
247
+ return `<segment id="seg:${escapeXmlAttr(c.id)}" timestamp="${escapeXmlAttr(timestamp)}"${fromAttr}${pathAttr}>${text}</segment>`;
248
+ case "summary":
249
+ return `<summary id="sum:${escapeXmlAttr(c.id)}" timestamp="${escapeXmlAttr(timestamp)}"${fromAttr}${pathAttr}>${text}</summary>`;
250
+ default:
251
+ // media or unknown types — render as item
252
+ return `<item id="item:${escapeXmlAttr(c.id)}" kind="${escapeXmlAttr(c.kind)}" importance="${c.importance.toFixed(2)}" timestamp="${escapeXmlAttr(timestamp)}"${fromAttr}${pathAttr}>${text}${supersessionSuffix}</item>`;
278
253
  }
279
- return ` source="${escapeXmlAttr(date)}"`;
280
254
  }
281
255
 
282
256
  function escapeXmlAttr(text: string): string {
@@ -286,32 +260,3 @@ function escapeXmlAttr(text: string): string {
286
260
  .replace(/</g, "&lt;")
287
261
  .replace(/>/g, "&gt;");
288
262
  }
289
-
290
- /**
291
- * Format epoch-ms as a short human-readable date like "Mar 7" or "Mar 7 2024".
292
- * Omits the year when the date is in the current year.
293
- */
294
- function formatShortDate(epochMs: number): string {
295
- const date = new Date(epochMs);
296
- const now = new Date();
297
- const months = [
298
- "Jan",
299
- "Feb",
300
- "Mar",
301
- "Apr",
302
- "May",
303
- "Jun",
304
- "Jul",
305
- "Aug",
306
- "Sep",
307
- "Oct",
308
- "Nov",
309
- "Dec",
310
- ];
311
- const month = months[date.getMonth()];
312
- const day = date.getDate();
313
- if (date.getFullYear() === now.getFullYear()) {
314
- return `${month} ${day}`;
315
- }
316
- return `${month} ${day} ${date.getFullYear()}`;
317
- }