@vellumai/assistant 0.6.0 → 0.6.1

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 (285) 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 +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -11,6 +11,7 @@ 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,
@@ -22,6 +23,7 @@ import { onContactChange } from "../contacts/contact-events.js";
22
23
  import type { CesClient } from "../credential-execution/client.js";
23
24
  import type { CesProcessManager } from "../credential-execution/process-manager.js";
24
25
  import type { HeartbeatService } from "../heartbeat/heartbeat-service.js";
26
+ import { getApp, getAppDirPath, isMultifileApp } from "../memory/app-store.js";
25
27
  import * as attachmentsStore from "../memory/attachments-store.js";
26
28
  import {
27
29
  createCanonicalGuardianRequest,
@@ -49,6 +51,7 @@ import { bridgeConfirmationRequestToGuardian } from "../runtime/confirmation-req
49
51
  import * as pendingInteractions from "../runtime/pending-interactions.js";
50
52
  import { checkIngressForSecrets } from "../security/secret-ingress.js";
51
53
  import { redactSecrets } from "../security/secret-scanner.js";
54
+ import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
52
55
  import { registerCancelCallback } from "../signals/cancel.js";
53
56
  import { registerConversationUndoCallback } from "../signals/conversation-undo.js";
54
57
  import { appendEventToStream } from "../signals/event-stream.js";
@@ -57,10 +60,12 @@ import { getSubagentManager } from "../subagent/index.js";
57
60
  import { summarizeToolInput } from "../tools/tool-input-summary.js";
58
61
  import { getLogger } from "../util/logger.js";
59
62
  import {
63
+ getAvatarImagePath,
60
64
  getSandboxWorkingDir,
61
65
  getWorkspacePromptPath,
62
66
  } from "../util/platform.js";
63
67
  import { registerDaemonCallbacks } from "../work-items/work-item-runner.js";
68
+ import { AppSourceWatcher } from "./app-source-watcher.js";
64
69
  import { ConfigWatcher } from "./config-watcher.js";
65
70
  import {
66
71
  Conversation,
@@ -71,6 +76,7 @@ import { ConversationEvictor } from "./conversation-evictor.js";
71
76
  import { formatCompactResult } from "./conversation-process.js";
72
77
  import { resolveChannelCapabilities } from "./conversation-runtime-assembly.js";
73
78
  import { resolveSlash, type SlashContext } from "./conversation-slash.js";
79
+ import { refreshSurfacesForApp } from "./conversation-surfaces.js";
74
80
  import { undoLastMessage } from "./handlers/conversations.js";
75
81
  import { parseIdentityFields } from "./handlers/identity.js";
76
82
  import type {
@@ -201,13 +207,10 @@ function makePendingInteractionRegistrar(
201
207
  guardianPrincipalId: trustContext?.guardianPrincipalId ?? undefined,
202
208
  toolName: msg.toolName,
203
209
  commandPreview:
204
- redactSecrets(
205
- summarizeToolInput(msg.toolName, inputRecord),
206
- ) || undefined,
210
+ redactSecrets(summarizeToolInput(msg.toolName, inputRecord)) ||
211
+ undefined,
207
212
  riskLevel: msg.riskLevel,
208
- activityText: activityRaw
209
- ? redactSecrets(activityRaw)
210
- : undefined,
213
+ activityText: activityRaw ? redactSecrets(activityRaw) : undefined,
211
214
  executionTarget: msg.executionTarget,
212
215
  status: "pending",
213
216
  requestCode: generateCanonicalRequestCode(),
@@ -271,6 +274,7 @@ export class DaemonServer {
271
274
 
272
275
  // Composed subsystems
273
276
  private configWatcher = new ConfigWatcher();
277
+ private appSourceWatcher = new AppSourceWatcher();
274
278
 
275
279
  // CES (Credential Execution Service) — process-level singleton.
276
280
  // Lifecycle is managed by startCesProcess() in lifecycle.ts; the server
@@ -369,7 +373,28 @@ export class DaemonServer {
369
373
  { channelId: transport.channelId },
370
374
  "Transport metadata received",
371
375
  );
372
- conversation.setTransportHints(transport.hints);
376
+
377
+ // Build enriched hints: interface ID first, then host environment (macOS
378
+ // only), then any client-provided hints.
379
+ const enrichedHints: string[] = [];
380
+
381
+ const interfaceLabel = parseInterfaceId(transport.interfaceId) ?? "vellum";
382
+ enrichedHints.push(`User is messaging from interface: ${interfaceLabel}`);
383
+
384
+ if (transport.interfaceId === "macos") {
385
+ if (transport.hostHomeDir) {
386
+ enrichedHints.push(`Host home directory: ${transport.hostHomeDir}`);
387
+ }
388
+ if (transport.hostUsername) {
389
+ enrichedHints.push(`Host username: ${transport.hostUsername}`);
390
+ }
391
+ }
392
+
393
+ if (transport.hints) {
394
+ enrichedHints.push(...transport.hints);
395
+ }
396
+
397
+ conversation.setTransportHints(enrichedHints);
373
398
  }
374
399
 
375
400
  constructor() {
@@ -523,6 +548,55 @@ export class DaemonServer {
523
548
  }
524
549
  }
525
550
 
551
+ private broadcastSoundsConfigUpdated(): void {
552
+ this.broadcast({ type: "sounds_config_updated" });
553
+ }
554
+
555
+ private broadcastAvatarUpdated(): void {
556
+ this.broadcast({
557
+ type: "avatar_updated",
558
+ avatarPath: getAvatarImagePath(),
559
+ });
560
+ }
561
+
562
+ /**
563
+ * Handle a detected app source file change from the filesystem watcher.
564
+ * Recompiles multifile apps and refreshes surfaces across ALL conversations.
565
+ */
566
+ private handleAppSourceChange(appId: string): void {
567
+ const app = getApp(appId);
568
+ if (!app) return;
569
+
570
+ const doRefresh = () => {
571
+ for (const conversation of this.conversations.values()) {
572
+ refreshSurfacesForApp(conversation, appId, { fileChange: true });
573
+ }
574
+ this.broadcast({ type: "app_files_changed", appId });
575
+ void updatePublishedAppDeployment(appId);
576
+ };
577
+
578
+ if (isMultifileApp(app)) {
579
+ const appDir = getAppDirPath(appId);
580
+ void compileApp(appDir)
581
+ .then((result) => {
582
+ if (!result.ok) {
583
+ log.warn(
584
+ { appId, errors: result.errors },
585
+ "Recompile failed on app source change",
586
+ );
587
+ }
588
+ doRefresh();
589
+ })
590
+ .catch((err) => {
591
+ log.warn({ appId, err }, "Recompile threw on app source change");
592
+ doRefresh();
593
+ });
594
+ return;
595
+ }
596
+
597
+ doRefresh();
598
+ }
599
+
526
600
  // ── Server lifecycle ────────────────────────────────────────────────
527
601
 
528
602
  async start(): Promise<void> {
@@ -672,8 +746,12 @@ export class DaemonServer {
672
746
  this.configWatcher.start(
673
747
  () => this.evictConversationsForReload(),
674
748
  () => this.broadcastIdentityChanged(),
749
+ () => this.broadcastSoundsConfigUpdated(),
750
+ () => this.broadcastAvatarUpdated(),
675
751
  );
676
752
 
753
+ this.appSourceWatcher.start((appId) => this.handleAppSourceChange(appId));
754
+
677
755
  // Broadcast contacts_changed to all clients when any contact mutation occurs.
678
756
  this.unsubscribeContactChange = onContactChange(() => {
679
757
  this.broadcast({ type: "contacts_changed" });
@@ -687,6 +765,7 @@ export class DaemonServer {
687
765
  disposeAcpSessionManager();
688
766
  this.evictor.stop();
689
767
  this.configWatcher.stop();
768
+ this.appSourceWatcher.stop();
690
769
  if (this.unsubscribeContactChange) {
691
770
  this.unsubscribeContactChange();
692
771
  this.unsubscribeContactChange = null;
@@ -1012,7 +1091,7 @@ export class DaemonServer {
1012
1091
  // Guard: don't replace an active proxy during concurrent turn races —
1013
1092
  // another request may have started processing between the isProcessing()
1014
1093
  // check above and the await on ensureActorScopedHistory().
1015
- if (resolvedInterface === "macos" || resolvedInterface === "ios") {
1094
+ if (resolvedInterface === "macos") {
1016
1095
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
1017
1096
  conversation.setHostBashProxy(
1018
1097
  new HostBashProxy(conversation.getCurrentSender(), (requestId) => {
@@ -1369,8 +1448,9 @@ export class DaemonServer {
1369
1448
  */
1370
1449
  async getConversationForMessages(
1371
1450
  conversationId: string,
1451
+ options?: ConversationCreateOptions,
1372
1452
  ): Promise<Conversation> {
1373
- return this.getOrCreateConversation(conversationId);
1453
+ return this.getOrCreateConversation(conversationId, options);
1374
1454
  }
1375
1455
 
1376
1456
  /**
@@ -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,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
+ }