@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -11,17 +11,20 @@ import {
11
11
  createAssistantMessage,
12
12
  createUserMessage,
13
13
  } from "../agent/message-types.js";
14
+ import { compileApp } from "../bundler/app-compiler.js";
14
15
  import {
15
16
  type ChannelId,
16
17
  type InterfaceId,
17
18
  parseChannelId,
18
19
  parseInterfaceId,
20
+ supportsHostProxy,
19
21
  } from "../channels/types.js";
20
22
  import { getConfig } from "../config/loader.js";
21
23
  import { onContactChange } from "../contacts/contact-events.js";
22
24
  import type { CesClient } from "../credential-execution/client.js";
23
25
  import type { CesProcessManager } from "../credential-execution/process-manager.js";
24
26
  import type { HeartbeatService } from "../heartbeat/heartbeat-service.js";
27
+ import { getApp, getAppDirPath, isMultifileApp } from "../memory/app-store.js";
25
28
  import * as attachmentsStore from "../memory/attachments-store.js";
26
29
  import {
27
30
  createCanonicalGuardianRequest,
@@ -49,6 +52,7 @@ import { bridgeConfirmationRequestToGuardian } from "../runtime/confirmation-req
49
52
  import * as pendingInteractions from "../runtime/pending-interactions.js";
50
53
  import { checkIngressForSecrets } from "../security/secret-ingress.js";
51
54
  import { redactSecrets } from "../security/secret-scanner.js";
55
+ import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
52
56
  import { registerCancelCallback } from "../signals/cancel.js";
53
57
  import { registerConversationUndoCallback } from "../signals/conversation-undo.js";
54
58
  import { appendEventToStream } from "../signals/event-stream.js";
@@ -57,10 +61,12 @@ import { getSubagentManager } from "../subagent/index.js";
57
61
  import { summarizeToolInput } from "../tools/tool-input-summary.js";
58
62
  import { getLogger } from "../util/logger.js";
59
63
  import {
64
+ getAvatarImagePath,
60
65
  getSandboxWorkingDir,
61
66
  getWorkspacePromptPath,
62
67
  } from "../util/platform.js";
63
68
  import { registerDaemonCallbacks } from "../work-items/work-item-runner.js";
69
+ import { AppSourceWatcher } from "./app-source-watcher.js";
64
70
  import { ConfigWatcher } from "./config-watcher.js";
65
71
  import {
66
72
  Conversation,
@@ -71,6 +77,7 @@ import { ConversationEvictor } from "./conversation-evictor.js";
71
77
  import { formatCompactResult } from "./conversation-process.js";
72
78
  import { resolveChannelCapabilities } from "./conversation-runtime-assembly.js";
73
79
  import { resolveSlash, type SlashContext } from "./conversation-slash.js";
80
+ import { refreshSurfacesForApp } from "./conversation-surfaces.js";
74
81
  import { undoLastMessage } from "./handlers/conversations.js";
75
82
  import { parseIdentityFields } from "./handlers/identity.js";
76
83
  import type {
@@ -85,6 +92,7 @@ import type {
85
92
  ServerMessage,
86
93
  UserMessageAttachment,
87
94
  } from "./message-protocol.js";
95
+ import { buildTransportHints } from "./transport-hints.js";
88
96
 
89
97
  const log = getLogger("server");
90
98
 
@@ -201,13 +209,10 @@ function makePendingInteractionRegistrar(
201
209
  guardianPrincipalId: trustContext?.guardianPrincipalId ?? undefined,
202
210
  toolName: msg.toolName,
203
211
  commandPreview:
204
- redactSecrets(
205
- summarizeToolInput(msg.toolName, inputRecord),
206
- ) || undefined,
212
+ redactSecrets(summarizeToolInput(msg.toolName, inputRecord)) ||
213
+ undefined,
207
214
  riskLevel: msg.riskLevel,
208
- activityText: activityRaw
209
- ? redactSecrets(activityRaw)
210
- : undefined,
215
+ activityText: activityRaw ? redactSecrets(activityRaw) : undefined,
211
216
  executionTarget: msg.executionTarget,
212
217
  status: "pending",
213
218
  requestCode: generateCanonicalRequestCode(),
@@ -271,6 +276,7 @@ export class DaemonServer {
271
276
 
272
277
  // Composed subsystems
273
278
  private configWatcher = new ConfigWatcher();
279
+ private appSourceWatcher = new AppSourceWatcher();
274
280
 
275
281
  // CES (Credential Execution Service) — process-level singleton.
276
282
  // Lifecycle is managed by startCesProcess() in lifecycle.ts; the server
@@ -369,7 +375,7 @@ export class DaemonServer {
369
375
  { channelId: transport.channelId },
370
376
  "Transport metadata received",
371
377
  );
372
- conversation.setTransportHints(transport.hints);
378
+ conversation.setTransportHints(buildTransportHints(transport));
373
379
  }
374
380
 
375
381
  constructor() {
@@ -523,6 +529,63 @@ export class DaemonServer {
523
529
  }
524
530
  }
525
531
 
532
+ private broadcastConfigChanged(): void {
533
+ this.broadcast({ type: "config_changed" });
534
+ }
535
+
536
+ private broadcastSoundsConfigUpdated(): void {
537
+ this.broadcast({ type: "sounds_config_updated" });
538
+ }
539
+
540
+ private broadcastFeatureFlagsChanged(): void {
541
+ this.broadcast({ type: "feature_flags_changed" });
542
+ }
543
+
544
+ private broadcastAvatarUpdated(): void {
545
+ this.broadcast({
546
+ type: "avatar_updated",
547
+ avatarPath: getAvatarImagePath(),
548
+ });
549
+ }
550
+
551
+ /**
552
+ * Handle a detected app source file change from the filesystem watcher.
553
+ * Recompiles multifile apps and refreshes surfaces across ALL conversations.
554
+ */
555
+ private handleAppSourceChange(appId: string): void {
556
+ const app = getApp(appId);
557
+ if (!app) return;
558
+
559
+ const doRefresh = () => {
560
+ for (const conversation of this.conversations.values()) {
561
+ refreshSurfacesForApp(conversation, appId, { fileChange: true });
562
+ }
563
+ this.broadcast({ type: "app_files_changed", appId });
564
+ void updatePublishedAppDeployment(appId);
565
+ };
566
+
567
+ if (isMultifileApp(app)) {
568
+ const appDir = getAppDirPath(appId);
569
+ void compileApp(appDir)
570
+ .then((result) => {
571
+ if (!result.ok) {
572
+ log.warn(
573
+ { appId, errors: result.errors },
574
+ "Recompile failed on app source change",
575
+ );
576
+ }
577
+ doRefresh();
578
+ })
579
+ .catch((err) => {
580
+ log.warn({ appId, err }, "Recompile threw on app source change");
581
+ doRefresh();
582
+ });
583
+ return;
584
+ }
585
+
586
+ doRefresh();
587
+ }
588
+
526
589
  // ── Server lifecycle ────────────────────────────────────────────────
527
590
 
528
591
  async start(): Promise<void> {
@@ -672,8 +735,14 @@ export class DaemonServer {
672
735
  this.configWatcher.start(
673
736
  () => this.evictConversationsForReload(),
674
737
  () => this.broadcastIdentityChanged(),
738
+ () => this.broadcastSoundsConfigUpdated(),
739
+ () => this.broadcastAvatarUpdated(),
740
+ () => this.broadcastConfigChanged(),
741
+ () => this.broadcastFeatureFlagsChanged(),
675
742
  );
676
743
 
744
+ this.appSourceWatcher.start((appId) => this.handleAppSourceChange(appId));
745
+
677
746
  // Broadcast contacts_changed to all clients when any contact mutation occurs.
678
747
  this.unsubscribeContactChange = onContactChange(() => {
679
748
  this.broadcast({ type: "contacts_changed" });
@@ -687,6 +756,7 @@ export class DaemonServer {
687
756
  disposeAcpSessionManager();
688
757
  this.evictor.stop();
689
758
  this.configWatcher.stop();
759
+ this.appSourceWatcher.stop();
690
760
  if (this.unsubscribeContactChange) {
691
761
  this.unsubscribeContactChange();
692
762
  this.unsubscribeContactChange = null;
@@ -899,7 +969,13 @@ export class DaemonServer {
899
969
  }
900
970
  this.evictor.touch(conversationId);
901
971
  } else {
902
- this.applyTransportMetadata(conversation, options);
972
+ // Only apply transport metadata when the conversation is idle.
973
+ // When processing, the hints are stored on the queued message and
974
+ // will be applied at dequeue time — applying them here would
975
+ // overwrite the in-flight conversation's transportHints.
976
+ if (!conversation.isProcessing()) {
977
+ this.applyTransportMetadata(conversation, options);
978
+ }
903
979
  this.evictor.touch(conversationId);
904
980
  }
905
981
  return conversation;
@@ -1012,7 +1088,7 @@ export class DaemonServer {
1012
1088
  // Guard: don't replace an active proxy during concurrent turn races —
1013
1089
  // another request may have started processing between the isProcessing()
1014
1090
  // check above and the await on ensureActorScopedHistory().
1015
- if (resolvedInterface === "macos" || resolvedInterface === "ios") {
1091
+ if (supportsHostProxy(resolvedInterface)) {
1016
1092
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
1017
1093
  conversation.setHostBashProxy(
1018
1094
  new HostBashProxy(conversation.getCurrentSender(), (requestId) => {
@@ -1369,8 +1445,9 @@ export class DaemonServer {
1369
1445
  */
1370
1446
  async getConversationForMessages(
1371
1447
  conversationId: string,
1448
+ options?: ConversationCreateOptions,
1372
1449
  ): Promise<Conversation> {
1373
- return this.getOrCreateConversation(conversationId);
1450
+ return this.getOrCreateConversation(conversationId, options);
1374
1451
  }
1375
1452
 
1376
1453
  /**
@@ -1,5 +1,6 @@
1
1
  import * as Sentry from "@sentry/node";
2
2
 
3
+ import type { FilingService } from "../filing/filing-service.js";
3
4
  import type { HeartbeatService } from "../heartbeat/heartbeat-service.js";
4
5
  import type { HookManager } from "../hooks/manager.js";
5
6
  import type { McpServerManager } from "../mcp/manager.js";
@@ -7,6 +8,7 @@ import { getSqlite, resetDb } from "../memory/db.js";
7
8
  import type { QdrantManager } from "../memory/qdrant-manager.js";
8
9
  import type { RuntimeHttpServer } from "../runtime/http-server.js";
9
10
  import { browserManager } from "../tools/browser/browser-manager.js";
11
+ import { cleanupShellOutputTempFiles } from "../tools/shared/shell-output.js";
10
12
  import { getLogger } from "../util/logger.js";
11
13
  import { getEnrichmentService } from "../workspace/commit-message-enrichment-service.js";
12
14
  import type { WorkspaceHeartbeatService } from "../workspace/heartbeat-service.js";
@@ -18,6 +20,7 @@ export interface ShutdownDeps {
18
20
  server: DaemonServer;
19
21
  workspaceHeartbeat: WorkspaceHeartbeatService;
20
22
  heartbeat: HeartbeatService;
23
+ filing: FilingService;
21
24
  hookManager: HookManager;
22
25
  runtimeHttp: RuntimeHttpServer | null;
23
26
  scheduler: { stop(): void };
@@ -51,6 +54,7 @@ export function installShutdownHandlers(deps: ShutdownDeps): void {
51
54
 
52
55
  await deps.workspaceHeartbeat.stop();
53
56
  await deps.heartbeat.stop();
57
+ await deps.filing.stop();
54
58
 
55
59
  try {
56
60
  await deps.hookManager.trigger("daemon-stop", { pid: process.pid });
@@ -105,6 +109,7 @@ export function installShutdownHandlers(deps: ShutdownDeps): void {
105
109
 
106
110
  if (deps.runtimeHttp) await deps.runtimeHttp.stop();
107
111
  await browserManager.closeAllPages();
112
+ cleanupShellOutputTempFiles();
108
113
  deps.scheduler.stop();
109
114
  deps.getMemoryWorker()?.stop();
110
115
 
@@ -158,12 +158,32 @@ registerHook(
158
158
  },
159
159
  );
160
160
 
161
- // Trigger compilation + surface refresh + broadcast when an app is refreshed.
161
+ // Trigger surface refresh + broadcast when an app is refreshed.
162
+ // If the executor already compiled (multifile path), skip the redundant
163
+ // recompile and just refresh surfaces / broadcast / deploy directly.
162
164
  registerHook(
163
165
  "app_refresh",
164
- (_name, input, _result, { ctx, broadcastToAllClients }) => {
166
+ (_name, input, result, { ctx, broadcastToAllClients }) => {
165
167
  const appId = input.app_id as string | undefined;
166
- if (appId) {
168
+ if (!appId) return;
169
+
170
+ // executeAppRefresh already compiled multifile apps and included a
171
+ // "compiled" field in the result. Skip the expensive recompile and
172
+ // go straight to surface refresh + broadcast + deploy.
173
+ let alreadyCompiled = false;
174
+ try {
175
+ const parsed = JSON.parse(result.content) as { compiled?: boolean };
176
+ alreadyCompiled = parsed.compiled !== undefined;
177
+ } catch {
178
+ // Result wasn't valid JSON — fall through to handleAppChange.
179
+ }
180
+
181
+ if (alreadyCompiled) {
182
+ const opts = { fileChange: true };
183
+ refreshSurfacesForApp(ctx, appId, opts);
184
+ broadcastToAllClients?.({ type: "app_files_changed", appId });
185
+ void updatePublishedAppDeployment(appId);
186
+ } else {
167
187
  handleAppChange(ctx, appId, broadcastToAllClients, { fileChange: true });
168
188
  }
169
189
  },
@@ -0,0 +1,33 @@
1
+ import { parseInterfaceId } from "../channels/types.js";
2
+ import type { ConversationTransportMetadata } from "./message-types/conversations.js";
3
+
4
+ /**
5
+ * Build enriched transport hints from conversation transport metadata.
6
+ *
7
+ * Interface ID first, then host environment (macOS only), then any
8
+ * client-provided hints. Shared between the conversation creation path
9
+ * (server.ts) and the queue drain path (conversation-process.ts).
10
+ */
11
+ export function buildTransportHints(
12
+ transport: ConversationTransportMetadata,
13
+ ): string[] {
14
+ const hints: string[] = [];
15
+
16
+ const interfaceLabel = parseInterfaceId(transport.interfaceId) ?? "vellum";
17
+ hints.push(`User is messaging from interface: ${interfaceLabel}`);
18
+
19
+ if (transport.interfaceId === "macos") {
20
+ if (transport.hostHomeDir) {
21
+ hints.push(`Host home directory: ${transport.hostHomeDir}`);
22
+ }
23
+ if (transport.hostUsername) {
24
+ hints.push(`Host username: ${transport.hostUsername}`);
25
+ }
26
+ }
27
+
28
+ if (transport.hints) {
29
+ hints.push(...transport.hints);
30
+ }
31
+
32
+ return hints;
33
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Transcript formatter for conversation analysis.
3
+ *
4
+ * Builds a markdown transcript of a conversation, including inline
5
+ * subagent conversation sections when present in message metadata.
6
+ */
7
+
8
+ import {
9
+ getConversation,
10
+ getMessages,
11
+ messageMetadataSchema,
12
+ } from "../memory/conversation-crud.js";
13
+ import { truncate } from "../util/truncate.js";
14
+
15
+ interface ContentBlock {
16
+ type: string;
17
+ text?: string;
18
+ name?: string;
19
+ input?: Record<string, unknown>;
20
+ content?: string;
21
+ tool_use_id?: string;
22
+ is_error?: boolean;
23
+ source?: { media_type?: string; filename?: string };
24
+ }
25
+
26
+ function formatTimestamp(ms: number): string {
27
+ return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
28
+ }
29
+
30
+ function extractAnalysisText(blocks: ContentBlock[]): string {
31
+ const parts: string[] = [];
32
+ for (const block of blocks) {
33
+ switch (block.type) {
34
+ case "text":
35
+ if (block.text) parts.push(block.text);
36
+ break;
37
+ case "tool_use":
38
+ parts.push(
39
+ `[Tool: ${block.name}] ${JSON.stringify(block.input ?? {})}`,
40
+ );
41
+ break;
42
+ case "tool_result":
43
+ if (block.is_error) {
44
+ parts.push(`[Error: ${block.content ?? ""}]`);
45
+ } else {
46
+ parts.push(`[Result: ${truncate(block.content ?? "", 500)}]`);
47
+ }
48
+ break;
49
+ case "server_tool_use":
50
+ parts.push(`[Web search: ${block.name ?? "web_search"}]`);
51
+ break;
52
+ case "web_search_tool_result":
53
+ parts.push("[Web search results]");
54
+ break;
55
+ case "image":
56
+ parts.push("[Image attachment]");
57
+ break;
58
+ case "file":
59
+ parts.push(`[File: ${block.source?.filename ?? "unknown"}]`);
60
+ break;
61
+ case "thinking":
62
+ case "redacted_thinking":
63
+ // Skip internal model reasoning blocks
64
+ break;
65
+ }
66
+ }
67
+ return parts.join("\n");
68
+ }
69
+
70
+ function formatRole(role: string): string {
71
+ return role === "user" ? "User" : "Assistant";
72
+ }
73
+
74
+ function formatSubagentMessages(msgs: ReturnType<typeof getMessages>): string {
75
+ const lines: string[] = [];
76
+ for (const msg of msgs) {
77
+ const role = formatRole(msg.role);
78
+ const time = formatTimestamp(msg.createdAt);
79
+ const content = parseContent(msg.content);
80
+ const text = extractAnalysisText(content);
81
+ if (text) {
82
+ lines.push(`> **${role}** (${time})`);
83
+ for (const line of text.split("\n")) {
84
+ lines.push(`> ${line}`);
85
+ }
86
+ lines.push(">");
87
+ }
88
+ }
89
+ return lines.join("\n");
90
+ }
91
+
92
+ function parseContent(raw: string): ContentBlock[] {
93
+ try {
94
+ return JSON.parse(raw) as ContentBlock[];
95
+ } catch {
96
+ return [{ type: "text", text: raw }];
97
+ }
98
+ }
99
+
100
+ export function buildAnalysisTranscript(conversationId: string): string {
101
+ const conversation = getConversation(conversationId);
102
+ if (!conversation) {
103
+ return `# Conversation not found: ${conversationId}\n`;
104
+ }
105
+
106
+ const allMessages = getMessages(conversationId);
107
+ const title = conversation.title ?? "Untitled";
108
+ const lines: string[] = [];
109
+
110
+ lines.push(`# Conversation: ${title}`);
111
+ lines.push(`Created: ${formatTimestamp(conversation.createdAt)}`);
112
+ lines.push("");
113
+
114
+ for (const msg of allMessages) {
115
+ const role = formatRole(msg.role);
116
+ const time = formatTimestamp(msg.createdAt);
117
+ const content = parseContent(msg.content);
118
+ const text = extractAnalysisText(content);
119
+
120
+ lines.push(`## ${role} (${time})`);
121
+ lines.push(text);
122
+ lines.push("");
123
+
124
+ // Check for subagent notifications in metadata
125
+ if (msg.metadata) {
126
+ try {
127
+ const parsed = messageMetadataSchema.safeParse(JSON.parse(msg.metadata));
128
+ if (parsed.success && parsed.data.subagentNotification) {
129
+ const notif = parsed.data.subagentNotification;
130
+ if (
131
+ (notif.status === "completed" || notif.status === "failed" || notif.status === "aborted") &&
132
+ notif.conversationId
133
+ ) {
134
+ const subMessages = getMessages(notif.conversationId);
135
+ lines.push(`### Subagent: ${notif.label} (${notif.status})`);
136
+ lines.push("");
137
+ lines.push(formatSubagentMessages(subMessages));
138
+ lines.push("");
139
+ }
140
+ }
141
+ } catch {
142
+ // Skip unparseable metadata
143
+ }
144
+ }
145
+ }
146
+
147
+ return lines.join("\n");
148
+ }
@@ -0,0 +1,228 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getConfig } from "../config/loader.js";
5
+ import type { Speed } from "../config/schemas/inference.js";
6
+ import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
7
+ import { getLogger } from "../util/logger.js";
8
+ import { getWorkspaceDir } from "../util/platform.js";
9
+ import { stripCommentLines } from "../util/strip-comment-lines.js";
10
+
11
+ const log = getLogger("filing-service");
12
+
13
+ const FILING_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
14
+
15
+ const FILING_PROMPT_TEMPLATE = `You are running a periodic knowledge base filing job. This is a background maintenance task.
16
+
17
+ ## Part 1: File the buffer
18
+
19
+ Read \`pkb/buffer.md\`. For each item in the buffer:
20
+ 1. Determine which topic file it belongs in. Check \`pkb/INDEX.md\` to see what topic files exist.
21
+ 2. Read the target topic file, then append or integrate the new fact.
22
+ 3. If the fact is important enough to always be in context, add it to \`pkb/essentials.md\` instead.
23
+ 4. If the fact is a commitment, follow-up, or active project, add it to \`pkb/threads.md\`.
24
+ 5. If no existing topic file fits, create a new one and update \`pkb/INDEX.md\`.
25
+
26
+ After all items are filed, clear the processed items from \`pkb/buffer.md\` (leave the file empty, don't delete it).
27
+
28
+ ## Part 2: Nest
29
+
30
+ Pick 1-2 topic files from your knowledge base and review them:
31
+ - Is the information still accurate and up to date?
32
+ - Are there duplicates that should be consolidated?
33
+ - Is anything important enough to promote to \`pkb/essentials.md\`?
34
+ - Is anything in \`pkb/essentials.md\` that's no longer essential? Demote it to a topic file.
35
+ - Are any threads in \`pkb/threads.md\` completed or stale? Remove them.
36
+ - Is any file getting too long? Consider splitting it.
37
+ - Should any topic file be restructured for clarity?
38
+
39
+ Make improvements as you see fit. This is your knowledge base — keep it sharp.`;
40
+
41
+ export interface FilingDeps {
42
+ processMessage: (
43
+ conversationId: string,
44
+ content: string,
45
+ options?: { speed?: Speed },
46
+ ) => Promise<{ messageId: string }>;
47
+ onConversationCreated?: (info: {
48
+ conversationId: string;
49
+ title: string;
50
+ }) => void;
51
+ getCurrentHour?: () => number;
52
+ }
53
+
54
+ export class FilingService {
55
+ private readonly deps: FilingDeps;
56
+ private timer: ReturnType<typeof setInterval> | null = null;
57
+ private activeRun: Promise<void> | null = null;
58
+ private _lastRunAt: number | null = null;
59
+ private _nextRunAt: number | null = null;
60
+
61
+ constructor(deps: FilingDeps) {
62
+ this.deps = deps;
63
+ }
64
+
65
+ get lastRunAt(): number | null {
66
+ return this._lastRunAt;
67
+ }
68
+
69
+ get nextRunAt(): number | null {
70
+ return this._nextRunAt;
71
+ }
72
+
73
+ start(): void {
74
+ const config = getConfig().filing;
75
+ if (!config.enabled) {
76
+ log.info("Filing service disabled by config");
77
+ this._nextRunAt = null;
78
+ return;
79
+ }
80
+ if (this.timer) return;
81
+
82
+ log.info({ intervalMs: config.intervalMs }, "Filing service started");
83
+ this.scheduleNextRun(config.intervalMs);
84
+ this.timer = setInterval(() => {
85
+ this.runOnce().catch((err) => {
86
+ log.error({ err }, "Filing runOnce failed");
87
+ });
88
+ }, config.intervalMs);
89
+ }
90
+
91
+ reconfigure(): void {
92
+ if (this.timer) {
93
+ clearInterval(this.timer);
94
+ this.timer = null;
95
+ }
96
+ this._nextRunAt = null;
97
+ this.start();
98
+ }
99
+
100
+ async stop(): Promise<void> {
101
+ if (this.timer) {
102
+ clearInterval(this.timer);
103
+ this.timer = null;
104
+ }
105
+ this._nextRunAt = null;
106
+ if (this.activeRun) {
107
+ let timerId: ReturnType<typeof setTimeout>;
108
+ const timeout = new Promise<void>((resolve) => {
109
+ timerId = setTimeout(resolve, 5_000);
110
+ });
111
+ await Promise.race([this.activeRun, timeout]);
112
+ clearTimeout(timerId!);
113
+ }
114
+ log.info("Filing service stopped");
115
+ }
116
+
117
+ async runOnce({ force = false }: { force?: boolean } = {}): Promise<boolean> {
118
+ const config = getConfig().filing;
119
+ if (!force && !config.enabled) return false;
120
+
121
+ if (
122
+ !force &&
123
+ config.activeHoursStart != null &&
124
+ config.activeHoursEnd != null
125
+ ) {
126
+ const hour = this.deps.getCurrentHour?.() ?? new Date().getHours();
127
+ if (
128
+ !isWithinActiveHours(
129
+ hour,
130
+ config.activeHoursStart,
131
+ config.activeHoursEnd,
132
+ )
133
+ ) {
134
+ log.debug("Outside active hours, skipping filing");
135
+ this.scheduleNextRun(config.intervalMs);
136
+ return false;
137
+ }
138
+ }
139
+
140
+ if (this.activeRun) {
141
+ log.debug("Previous filing run still active, skipping");
142
+ return false;
143
+ }
144
+
145
+ // Skip if buffer is empty — no work to do
146
+ if (!force && !this.hasBufferContent()) {
147
+ log.debug("Buffer is empty, skipping filing");
148
+ this.scheduleNextRun(config.intervalMs);
149
+ return false;
150
+ }
151
+
152
+ const run = this.executeRun();
153
+ this.activeRun = run;
154
+ try {
155
+ await Promise.race([
156
+ run,
157
+ new Promise<never>((_, reject) =>
158
+ setTimeout(
159
+ () => reject(new Error("Filing execution timed out")),
160
+ FILING_TIMEOUT_MS,
161
+ ),
162
+ ),
163
+ ]);
164
+ } finally {
165
+ this.activeRun = null;
166
+ this._lastRunAt = Date.now();
167
+ this.scheduleNextRun(getConfig().filing.intervalMs);
168
+ }
169
+ return true;
170
+ }
171
+
172
+ private scheduleNextRun(intervalMs: number): void {
173
+ this._nextRunAt = Date.now() + intervalMs;
174
+ }
175
+
176
+ private hasBufferContent(): boolean {
177
+ const bufferPath = join(getWorkspaceDir(), "pkb", "buffer.md");
178
+ if (!existsSync(bufferPath)) return false;
179
+ try {
180
+ const content = stripCommentLines(
181
+ readFileSync(bufferPath, "utf-8"),
182
+ ).trim();
183
+ return content.length > 0;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ private async executeRun(): Promise<void> {
190
+ log.info("Running filing job");
191
+
192
+ try {
193
+ const config = getConfig().filing;
194
+
195
+ const conversation = bootstrapConversation({
196
+ conversationType: "background",
197
+ source: "filing",
198
+ groupId: "system:background",
199
+ origin: "filing",
200
+ systemHint: "Filing",
201
+ });
202
+
203
+ this.deps.onConversationCreated?.({
204
+ conversationId: conversation.id,
205
+ title: "Filing",
206
+ });
207
+
208
+ await this.deps.processMessage(conversation.id, FILING_PROMPT_TEMPLATE, {
209
+ speed: config.speed,
210
+ });
211
+
212
+ log.info({ conversationId: conversation.id }, "Filing job completed");
213
+ } catch (err) {
214
+ log.error({ err }, "Filing job failed");
215
+ }
216
+ }
217
+ }
218
+
219
+ function isWithinActiveHours(
220
+ hour: number,
221
+ start: number,
222
+ end: number,
223
+ ): boolean {
224
+ if (start <= end) {
225
+ return hour >= start && hour < end;
226
+ }
227
+ return hour >= start || hour < end;
228
+ }