@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
@@ -1,18 +1,69 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
1
4
  import { getConfig } from "../config/loader.js";
2
5
  import type { Speed } from "../config/schemas/inference.js";
3
6
  import type { HeartbeatAlert } from "../daemon/message-protocol.js";
4
7
  import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
8
+ import { isTemplateContent } from "../prompts/system-prompt.js";
5
9
  import { readTextFileSync } from "../util/fs.js";
6
10
  import { getLogger } from "../util/logger.js";
7
- import { getWorkspacePromptPath } from "../util/platform.js";
11
+ import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
8
12
  import { stripCommentLines } from "../util/strip-comment-lines.js";
9
13
 
10
14
  const log = getLogger("heartbeat-check");
11
15
 
12
16
  const DEFAULT_CHECKLIST = `- Check in with yourself. Read NOW.md. Is it still accurate? Update it if anything has changed.
13
17
  - Think about your user. Is there anything from recent conversations you should follow up on? Anything you noticed that you should bring up?
18
+ - Have a thought. Think about something your user would find interesting or worth talking about. A follow-up, a connection you made, something you came across. Give them a reason to open a conversation.
14
19
  - Check if there's anything on the horizon — events, deadlines, things they mentioned wanting to do.
15
- - If you have a thought worth sharing, send it. A follow-up, a useful find, a check-in. Not every beat, but when it feels right.`;
20
+ - If you have a thought worth sharing, send it. A follow-up, a useful find, a check-in. Not every beat, but when it feels right.
21
+ - If something has happened since your last journal entry, write one. Even a few sentences. The journal is how future-you stays connected.`;
22
+
23
+ const REENGAGEMENT_COOLDOWN_MS = 18 * 60 * 60 * 1000; // 18 hours
24
+ const HEARTBEAT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
25
+
26
+ /** @internal Exported for testing. */
27
+ export function isShallowProfile(): boolean {
28
+ try {
29
+ const identityPath = getWorkspacePromptPath("IDENTITY.md");
30
+ const userPath = getWorkspacePromptPath("USER.md");
31
+ const rawIdentity = readTextFileSync(identityPath);
32
+ const rawUser = readTextFileSync(userPath);
33
+ const identity = rawIdentity != null ? stripCommentLines(rawIdentity) : null;
34
+ const user = rawUser != null ? stripCommentLines(rawUser) : null;
35
+ return (
36
+ isTemplateContent(identity, "IDENTITY.md") &&
37
+ isTemplateContent(user, "USER.md")
38
+ );
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function getReengagementTimestampPath(): string {
45
+ return join(getWorkspaceDir(), ".reengagement-ts");
46
+ }
47
+
48
+ function isReengagementCooldownElapsed(): boolean {
49
+ const tsPath = getReengagementTimestampPath();
50
+ if (!existsSync(tsPath)) return true;
51
+ try {
52
+ const lastTs = parseInt(readFileSync(tsPath, "utf-8").trim(), 10);
53
+ if (isNaN(lastTs)) return true;
54
+ return Date.now() - lastTs >= REENGAGEMENT_COOLDOWN_MS;
55
+ } catch {
56
+ return true;
57
+ }
58
+ }
59
+
60
+ function recordReengagementTimestamp(): void {
61
+ try {
62
+ writeFileSync(getReengagementTimestampPath(), Date.now().toString());
63
+ } catch {
64
+ // Best-effort; don't block the heartbeat.
65
+ }
66
+ }
16
67
 
17
68
  export interface HeartbeatDeps {
18
69
  processMessage: (
@@ -141,6 +192,7 @@ export class HeartbeatService {
141
192
  },
142
193
  "Outside active hours, skipping",
143
194
  );
195
+ this.scheduleNextRun(config.intervalMs);
144
196
  return false;
145
197
  }
146
198
  }
@@ -153,10 +205,34 @@ export class HeartbeatService {
153
205
 
154
206
  const run = this.executeRun();
155
207
  this.activeRun = run;
208
+ // Clear activeRun once executeRun finishes. On timeout, runOnce releases
209
+ // activeRun separately (see catch block below) so future runs aren't
210
+ // permanently blocked. The .finally() handler still serves as the
211
+ // normal-completion cleanup path and uses an identity guard to avoid
212
+ // clearing a different run's activeRun.
213
+ run.finally(() => {
214
+ if (this.activeRun === run) {
215
+ this.activeRun = null;
216
+ }
217
+ }).catch(() => {}); // Suppress unhandled rejection if executeRun rejects
218
+
219
+ let timerId: ReturnType<typeof setTimeout> | undefined;
156
220
  try {
157
- await run;
158
- } finally {
221
+ const timeout = new Promise<never>((_, reject) => {
222
+ timerId = setTimeout(
223
+ () => reject(new Error("Heartbeat execution timed out")),
224
+ HEARTBEAT_TIMEOUT_MS,
225
+ );
226
+ });
227
+ timeout.catch(() => {}); // Prevent unhandled rejection if run resolves first
228
+ await Promise.race([run, timeout]);
229
+ } catch (err) {
230
+ log.warn({ err }, "Heartbeat run timed out");
231
+ // Release activeRun so the overlap guard doesn't permanently block
232
+ // future heartbeat runs when executeRun hangs past the timeout.
159
233
  this.activeRun = null;
234
+ } finally {
235
+ clearTimeout(timerId);
160
236
  this._lastRunAt = Date.now();
161
237
  this.scheduleNextRun(getConfig().heartbeat.intervalMs);
162
238
  }
@@ -173,7 +249,7 @@ export class HeartbeatService {
173
249
  try {
174
250
  const config = getConfig().heartbeat;
175
251
  const checklist = this.readChecklist();
176
- const prompt = this.buildPrompt(checklist);
252
+ const { prompt, includedReengagement } = this.buildPrompt(checklist);
177
253
 
178
254
  const conversation = bootstrapConversation({
179
255
  conversationType: "background",
@@ -191,6 +267,11 @@ export class HeartbeatService {
191
267
  await this.deps.processMessage(conversation.id, prompt, {
192
268
  speed: config.speed,
193
269
  });
270
+
271
+ if (includedReengagement) {
272
+ recordReengagementTimestamp();
273
+ }
274
+
194
275
  log.info({ conversationId: conversation.id }, "Heartbeat completed");
195
276
  } catch (err) {
196
277
  log.error({ err }, "Heartbeat failed");
@@ -214,8 +295,8 @@ export class HeartbeatService {
214
295
  }
215
296
 
216
297
  /** @internal Exposed for testing. */
217
- buildPrompt(checklist: string): string {
218
- return `You are running a periodic heartbeat check. Review the following checklist and take any necessary actions.
298
+ buildPrompt(checklist: string): { prompt: string; includedReengagement: boolean } {
299
+ let prompt = `You are running a periodic heartbeat check. Review the following checklist and take any necessary actions.
219
300
 
220
301
  <heartbeat-checklist>
221
302
  ${checklist}
@@ -226,6 +307,14 @@ After completing your review, end your response with one of:
226
307
  - HEARTBEAT_OK — if everything looks good, no action needed
227
308
  - HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
228
309
  </heartbeat-disposition>`;
310
+
311
+ let includedReengagement = false;
312
+ if (isShallowProfile() && isReengagementCooldownElapsed()) {
313
+ includedReengagement = true;
314
+ prompt += `\n\n<relationship-depth>\nYou don't know much about this person yet — their profile is still sparse. If the moment feels right during this beat, gently invite them to share something about themselves. Not an interrogation — something natural like "I realized I don't actually know much about what you do. Fill me in sometime?" Only do this occasionally, not every beat. If they engage, save what you learn.\n</relationship-depth>`;
315
+ }
316
+
317
+ return { prompt, includedReengagement };
229
318
  }
230
319
  }
231
320
 
package/src/mcp/client.ts CHANGED
@@ -5,6 +5,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
5
5
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
6
 
7
7
  import type { McpTransport } from "../config/schemas/mcp.js";
8
+ import { shouldUsePlatformCallbacks } from "../inbound/platform-callback-registration.js";
8
9
  import { getSecureKeyAsync } from "../security/secure-keys.js";
9
10
  import { getLogger } from "../util/logger.js";
10
11
  import { McpOAuthProvider } from "./mcp-oauth-provider.js";
@@ -63,9 +64,14 @@ export class McpClient {
63
64
  `mcp:${this.serverId}:tokens`,
64
65
  );
65
66
  if (cachedTokens) {
67
+ const callbackTransport = shouldUsePlatformCallbacks()
68
+ ? "gateway"
69
+ : "loopback";
66
70
  this.oauthProvider = new McpOAuthProvider(
67
71
  this.serverId,
68
72
  transportConfig.url,
73
+ /* interactive */ false,
74
+ callbackTransport,
69
75
  );
70
76
  }
71
77
  }
@@ -2,9 +2,21 @@
2
2
  * OAuthClientProvider implementation for MCP servers.
3
3
  *
4
4
  * Uses secure-keys (credential store) for persistent credential storage
5
- * and a loopback HTTP server for the browser callback.
5
+ * and either a loopback HTTP server or the gateway callback registry
6
+ * for the browser callback.
7
+ *
8
+ * Two callback transports:
9
+ *
10
+ * 1. **Loopback** (default) — starts a temporary HTTP server on localhost.
11
+ * Works when the daemon runs on the user's machine (desktop app).
12
+ *
13
+ * 2. **Gateway** — routes callbacks through the platform's public ingress
14
+ * URL and the in-memory oauth-callback-registry. Used when the daemon
15
+ * runs inside Docker/platform where localhost is unreachable from the
16
+ * user's browser.
6
17
  */
7
18
 
19
+ import { randomBytes } from "node:crypto";
8
20
  import { createServer, type Server } from "node:http";
9
21
 
10
22
  import type {
@@ -22,8 +34,8 @@ import {
22
34
  getSecureKeyAsync,
23
35
  setSecureKeyAsync,
24
36
  } from "../security/secure-keys.js";
37
+ import { openInHostBrowser } from "../util/browser.js";
25
38
  import { getLogger } from "../util/logger.js";
26
- import { isLinux, isMacOS } from "../util/platform.js";
27
39
 
28
40
  const log = getLogger("mcp-oauth");
29
41
 
@@ -46,24 +58,41 @@ export interface McpOAuthCallbackResult {
46
58
  codePromise: Promise<string>;
47
59
  }
48
60
 
61
+ /** Which callback transport to use for receiving the OAuth redirect. */
62
+ export type McpOAuthCallbackTransport = "loopback" | "gateway";
63
+
49
64
  export class McpOAuthProvider implements OAuthClientProvider {
50
65
  private readonly serverId: string;
51
66
  private readonly serverUrl: string;
52
67
  private readonly interactive: boolean;
68
+ private readonly callbackTransport: McpOAuthCallbackTransport;
53
69
  private _codeVerifier: string | undefined;
70
+ private _state: string | undefined;
54
71
  private _redirectUrl: string | undefined;
55
72
  private _codePromise: Promise<string> | null = null;
56
73
  private callbackServer: Server | null = null;
57
74
  private callbackTimeout: ReturnType<typeof setTimeout> | null = null;
75
+ /** Deferred resolver/rejector for the gateway code promise. */
76
+ private _gatewayCodeResolve: ((code: string) => void) | undefined;
77
+ private _gatewayCodeReject: ((err: Error) => void) | undefined;
58
78
 
59
79
  /**
60
80
  * @param interactive When true (e.g. `mcp auth` CLI), opens browser for OAuth.
61
81
  * When false (daemon), logs a message instead.
82
+ * @param callbackTransport Which transport to use for the OAuth redirect.
83
+ * - `"loopback"` (default): localhost HTTP server — for desktop clients.
84
+ * - `"gateway"`: platform ingress + callback registry — for Docker/platform.
62
85
  */
63
- constructor(serverId: string, serverUrl: string, interactive = false) {
86
+ constructor(
87
+ serverId: string,
88
+ serverUrl: string,
89
+ interactive = false,
90
+ callbackTransport: McpOAuthCallbackTransport = "loopback",
91
+ ) {
64
92
  this.serverId = serverId;
65
93
  this.serverUrl = serverUrl;
66
94
  this.interactive = interactive;
95
+ this.callbackTransport = callbackTransport;
67
96
  }
68
97
 
69
98
  // --- redirectUrl ---
@@ -161,6 +190,26 @@ export class McpOAuthProvider implements OAuthClientProvider {
161
190
  return this._codeVerifier;
162
191
  }
163
192
 
193
+ // --- State (CSRF token for OAuth) ---
194
+
195
+ /**
196
+ * Return a `state` value for the authorization URL.
197
+ *
198
+ * The MCP SDK calls `provider.state?.()` and, when the return value is
199
+ * truthy, appends it as the `state` query parameter. For the **gateway**
200
+ * transport the state is mandatory because it is the key used by the
201
+ * `oauth-callback-registry` to route the redirect back to this flow.
202
+ * For the **loopback** transport the state is optional (the loopback
203
+ * server matches on the callback URL itself), but we generate one anyway
204
+ * for defense-in-depth.
205
+ */
206
+ async state(): Promise<string> {
207
+ if (!this._state) {
208
+ this._state = randomBytes(16).toString("hex");
209
+ }
210
+ return this._state;
211
+ }
212
+
164
213
  // --- Discovery State ---
165
214
 
166
215
  async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
@@ -191,6 +240,35 @@ export class McpOAuthProvider implements OAuthClientProvider {
191
240
  async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
192
241
  const url = authorizationUrl.toString();
193
242
 
243
+ // For gateway transport, extract the SDK-generated `state` from the
244
+ // authorization URL and register it with the callback registry now.
245
+ if (
246
+ this.callbackTransport === "gateway" &&
247
+ this._gatewayCodeResolve &&
248
+ this._gatewayCodeReject
249
+ ) {
250
+ const sdkState = authorizationUrl.searchParams.get("state");
251
+ if (sdkState) {
252
+ // Dynamic import to avoid circular deps
253
+ const { registerPendingCallback } =
254
+ await import("../security/oauth-callback-registry.js");
255
+ registerPendingCallback(
256
+ sdkState,
257
+ this._gatewayCodeResolve,
258
+ this._gatewayCodeReject,
259
+ );
260
+ log.info(
261
+ { serverId: this.serverId, state: sdkState },
262
+ "MCP OAuth gateway callback registered with SDK state",
263
+ );
264
+ } else {
265
+ log.warn(
266
+ { serverId: this.serverId },
267
+ "Authorization URL missing state parameter — gateway callback may not resolve",
268
+ );
269
+ }
270
+ }
271
+
194
272
  if (!this.interactive) {
195
273
  // Daemon mode — don't open browser, just log guidance
196
274
  log.info(
@@ -208,28 +286,8 @@ export class McpOAuthProvider implements OAuthClientProvider {
208
286
  `[MCP] Opening browser for OAuth authorization of "${this.serverId}"...`,
209
287
  );
210
288
 
211
- try {
212
- const { execFile } = await import("node:child_process");
213
- const onError = (err: Error | null) => {
214
- if (err) {
215
- log.warn({ err }, "Failed to open browser");
216
- console.log(`[MCP] Please open this URL in your browser:\n${url}`);
217
- }
218
- };
219
- if (isMacOS()) {
220
- execFile("open", [url], onError);
221
- } else if (isLinux()) {
222
- execFile("xdg-open", [url], onError);
223
- } else {
224
- log.warn(
225
- "Unsupported platform for browser open — please visit the URL manually",
226
- );
227
- console.log(`[MCP] Please open this URL in your browser:\n${url}`);
228
- }
229
- } catch (err) {
230
- log.warn({ err }, "Failed to open browser");
231
- console.log(`[MCP] Please open this URL in your browser:\n${url}`);
232
- }
289
+ await openInHostBrowser(url);
290
+ console.log(`[MCP] If the browser did not open, visit this URL:\n${url}`);
233
291
  }
234
292
 
235
293
  // --- Invalidate Credentials ---
@@ -272,6 +330,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
272
330
  }
273
331
  if (scope === "all" || scope === "verifier") {
274
332
  this._codeVerifier = undefined;
333
+ this._state = undefined;
275
334
  }
276
335
  if (scope === "all" || scope === "discovery") {
277
336
  const result = await deleteSecureKeyAsync(discoveryKey(this.serverId));
@@ -292,10 +351,64 @@ export class McpOAuthProvider implements OAuthClientProvider {
292
351
  // --- Callback Server ---
293
352
 
294
353
  /**
295
- * Start a loopback HTTP server to receive the OAuth callback.
296
- * Returns a promise that resolves with the authorization code.
354
+ * Start listening for the OAuth callback.
355
+ *
356
+ * - **Loopback transport**: starts a temporary HTTP server on localhost.
357
+ * - **Gateway transport**: registers a pending callback with the
358
+ * oauth-callback-registry and resolves a public redirect URL via
359
+ * the platform callback registration system.
360
+ *
361
+ * Returns a promise that resolves with the authorization code promise.
297
362
  */
298
363
  startCallbackServer(): Promise<McpOAuthCallbackResult> {
364
+ if (this.callbackTransport === "gateway") {
365
+ return this.startGatewayCallback();
366
+ }
367
+ return this.startLoopbackServer();
368
+ }
369
+
370
+ /**
371
+ * Gateway transport: resolve the public redirect URL and create a
372
+ * deferred code promise. The actual `registerPendingCallback` call
373
+ * is deferred until `redirectToAuthorization` where we can extract
374
+ * the SDK-generated `state` parameter from the authorization URL.
375
+ */
376
+ private async startGatewayCallback(): Promise<McpOAuthCallbackResult> {
377
+ const { resolveCallbackUrl } =
378
+ await import("../inbound/platform-callback-registration.js");
379
+ const { getOAuthCallbackUrl } =
380
+ await import("../inbound/public-ingress-urls.js");
381
+ const { loadConfig } = await import("../config/loader.js");
382
+
383
+ const appConfig = loadConfig();
384
+ const redirectUrl = await resolveCallbackUrl(
385
+ () => getOAuthCallbackUrl(appConfig),
386
+ "webhooks/oauth/callback",
387
+ "mcp_oauth",
388
+ );
389
+
390
+ this._redirectUrl = redirectUrl;
391
+
392
+ // Create a deferred promise — it will be wired to the callback
393
+ // registry in redirectToAuthorization() once we know the SDK's state.
394
+ const codePromise = new Promise<string>((resolve, reject) => {
395
+ this._gatewayCodeResolve = resolve;
396
+ this._gatewayCodeReject = reject;
397
+ });
398
+ this._codePromise = codePromise;
399
+
400
+ log.info(
401
+ { serverId: this.serverId, redirectUrl },
402
+ "MCP OAuth gateway callback prepared (awaiting state from auth URL)",
403
+ );
404
+
405
+ return { codePromise };
406
+ }
407
+
408
+ /**
409
+ * Loopback transport: start a temporary HTTP server on localhost.
410
+ */
411
+ private startLoopbackServer(): Promise<McpOAuthCallbackResult> {
299
412
  return new Promise((resolveSetup, rejectSetup) => {
300
413
  let settled = false;
301
414
  let listening = false;
@@ -423,6 +536,15 @@ export class McpOAuthProvider implements OAuthClientProvider {
423
536
  this.callbackServer.close();
424
537
  this.callbackServer = null;
425
538
  }
539
+ // Gateway transport cleanup — reject the deferred promise so callers
540
+ // awaiting codePromise don't hang indefinitely.
541
+ if (this._gatewayCodeReject) {
542
+ this._gatewayCodeReject(
543
+ new Error("MCP OAuth gateway callback cancelled"),
544
+ );
545
+ this._gatewayCodeResolve = undefined;
546
+ this._gatewayCodeReject = undefined;
547
+ }
426
548
  }
427
549
  }
428
550
 
@@ -7,14 +7,20 @@ import { getConversationMemoryScopeId } from "./conversation-crud.js";
7
7
  import { getDb, rawGet } from "./db.js";
8
8
  import { getMemoryBackendStatus } from "./embedding-backend.js";
9
9
  import { handleRecall, type RecallResult } from "./graph/tool-handlers.js";
10
- import { enqueueBackfillJob, enqueueRebuildIndexJob, MIN_SEGMENT_CHARS } from "./indexer.js";
11
10
  import {
12
- enqueueMemoryJob,
13
- getMemoryJobCounts,
14
- } from "./jobs-store.js";
11
+ enqueueBackfillJob,
12
+ enqueueRebuildIndexJob,
13
+ MIN_SEGMENT_CHARS,
14
+ } from "./indexer.js";
15
+ import { enqueueMemoryJob, getMemoryJobCounts } from "./jobs-store.js";
15
16
  import { withQdrantBreaker } from "./qdrant-circuit-breaker.js";
16
17
  import { getQdrantClient } from "./qdrant-client.js";
17
- import { conversations, memorySegments, memorySummaries, messages } from "./schema.js";
18
+ import {
19
+ conversations,
20
+ memorySegments,
21
+ memorySummaries,
22
+ messages,
23
+ } from "./schema.js";
18
24
 
19
25
  const log = getLogger("memory-admin");
20
26
 
@@ -69,10 +75,6 @@ export function requestMemoryRebuildIndex(): string {
69
75
  return id;
70
76
  }
71
77
 
72
- export function requestMemoryCleanup(_retentionMs?: number): void {
73
- log.info("Memory cleanup requested (legacy items table dropped — no-op)");
74
- }
75
-
76
78
  export async function queryMemory(
77
79
  query: string,
78
80
  _conversationId: string,
@@ -94,9 +96,9 @@ export interface CleanupShortSegmentsResult {
94
96
  * These short fragments waste embedding budget, retrieval slots, and
95
97
  * injection tokens.
96
98
  */
97
- export async function cleanupShortSegments(
98
- opts?: { dryRun?: boolean },
99
- ): Promise<CleanupShortSegmentsResult> {
99
+ export async function cleanupShortSegments(opts?: {
100
+ dryRun?: boolean;
101
+ }): Promise<CleanupShortSegmentsResult> {
100
102
  const db = getDb();
101
103
 
102
104
  const shortSegments = db
@@ -117,18 +119,22 @@ export async function cleanupShortSegments(
117
119
  await withQdrantBreaker(() => qdrant.deleteByTarget("segment", row.id));
118
120
  } catch (err) {
119
121
  // Keep the SQLite row so the target ID is preserved for retry
120
- log.warn({ segmentId: row.id, err }, "Qdrant deletion failed — skipping SQLite deletion to preserve target ID");
122
+ log.warn(
123
+ { segmentId: row.id, err },
124
+ "Qdrant deletion failed — skipping SQLite deletion to preserve target ID",
125
+ );
121
126
  failed++;
122
127
  continue;
123
128
  }
124
129
 
125
- db.delete(memorySegments)
126
- .where(eq(memorySegments.id, row.id))
127
- .run();
130
+ db.delete(memorySegments).where(eq(memorySegments.id, row.id)).run();
128
131
  removed++;
129
132
  }
130
133
 
131
- log.info({ removed, failed, threshold: MIN_SEGMENT_CHARS }, "Cleaned up short segments");
134
+ log.info(
135
+ { removed, failed, threshold: MIN_SEGMENT_CHARS },
136
+ "Cleaned up short segments",
137
+ );
132
138
  return { removed, failed };
133
139
  }
134
140
 
@@ -160,7 +166,7 @@ export function findReextractTargets(limit: number): ReextractTarget[] {
160
166
  .from(conversations)
161
167
  .leftJoin(messages, eq(messages.conversationId, conversations.id))
162
168
  .where(
163
- sql`${conversations.conversationType} NOT IN ('background', 'private')`,
169
+ sql`${conversations.conversationType} NOT IN ('background', 'private', 'scheduled')`,
164
170
  )
165
171
  .groupBy(conversations.id)
166
172
  .orderBy(desc(sql`count(${messages.id})`))
@@ -204,25 +210,21 @@ export function findReextractTarget(
204
210
  /**
205
211
  * Queue re-extraction for a set of conversations.
206
212
  * Resets extraction checkpoints and clears extraction summaries so the
207
- * batch extraction handler processes all messages from scratch with
213
+ * graph extraction handler processes all messages from scratch with
208
214
  * expanded supersession context.
209
215
  */
210
- export function requestReextract(
211
- targets: ReextractTarget[],
212
- ): { jobIds: string[] } {
216
+ export function requestReextract(targets: ReextractTarget[]): {
217
+ jobIds: string[];
218
+ } {
213
219
  const db = getDb();
214
220
  const jobIds: string[] = [];
215
221
 
216
222
  for (const target of targets) {
217
223
  const { conversationId } = target;
218
224
 
219
- // Reset batch extraction checkpoints
220
- deleteMemoryCheckpoint(
221
- `batch_extract:${conversationId}:last_message_id`,
222
- );
223
- deleteMemoryCheckpoint(
224
- `batch_extract:${conversationId}:pending_count`,
225
- );
225
+ // Reset graph extraction checkpoints
226
+ deleteMemoryCheckpoint(`graph_extract:${conversationId}:last_ts`);
227
+ deleteMemoryCheckpoint(`graph_extract:${conversationId}:pending_count`);
226
228
 
227
229
  // Clear the extraction summary so it starts fresh
228
230
  db.delete(memorySummaries)
@@ -234,12 +236,11 @@ export function requestReextract(
234
236
  )
235
237
  .run();
236
238
 
237
- // Resolve scope and enqueue with fullReextract flag
239
+ // Resolve scope and enqueue re-extraction
238
240
  const scopeId = getConversationMemoryScopeId(conversationId);
239
- const jobId = enqueueMemoryJob("batch_extract", {
241
+ const jobId = enqueueMemoryJob("graph_extract", {
240
242
  conversationId,
241
243
  scopeId,
242
- fullReextract: true,
243
244
  });
244
245
  jobIds.push(jobId);
245
246
 
@@ -216,6 +216,8 @@ export function generateAppDirName(
216
216
 
217
217
  /** Cache of id -> dirName mappings to avoid repeated filesystem scans. */
218
218
  const idToDirNameCache = new Map<string, string>();
219
+ /** Reverse cache: dirName -> id. */
220
+ const dirNameToIdCache = new Map<string, string>();
219
221
 
220
222
  /**
221
223
  * Resolve an app's directory name and path from its ID.
@@ -265,12 +267,79 @@ export function getAppDirPath(appId: string): string {
265
267
  return resolveAppDir(appId).appDir;
266
268
  }
267
269
 
270
+ /**
271
+ * Resolve an app ID from its directory name (slug).
272
+ * Checks caches first, then reads the JSON definition file directly.
273
+ */
274
+ export function resolveAppIdByDirName(dirName: string): string | null {
275
+ const cached = dirNameToIdCache.get(dirName);
276
+ if (cached) return cached;
277
+
278
+ // Check forward cache (reverse iteration)
279
+ for (const [id, dn] of idToDirNameCache) {
280
+ if (dn === dirName) {
281
+ dirNameToIdCache.set(dirName, id);
282
+ return id;
283
+ }
284
+ }
285
+
286
+ // Read the JSON definition file directly
287
+ const dir = getAppsDir();
288
+ const jsonPath = join(dir, `${dirName}.json`);
289
+ if (existsSync(jsonPath)) {
290
+ try {
291
+ const raw = readFileSync(jsonPath, "utf-8");
292
+ const parsed = JSON.parse(raw) as { id?: string; dirName?: string };
293
+ if (parsed.id) {
294
+ dirNameToIdCache.set(dirName, parsed.id);
295
+ idToDirNameCache.set(parsed.id, dirName);
296
+ return parsed.id;
297
+ }
298
+ } catch {
299
+ // skip malformed files
300
+ }
301
+ }
302
+
303
+ return null;
304
+ }
305
+
306
+ /**
307
+ * Extract app ID from an absolute file path if it falls within the apps
308
+ * directory and targets a source file (not records/ or dist/).
309
+ */
310
+ export function resolveAppIdFromPath(filePath: string): string | null {
311
+ let appsDir: string;
312
+ try {
313
+ appsDir = getAppsDir();
314
+ } catch {
315
+ return null;
316
+ }
317
+ if (!filePath.startsWith(appsDir + "/")) return null;
318
+
319
+ const relPath = filePath.slice(appsDir.length + 1);
320
+ const slashIdx = relPath.indexOf("/");
321
+ if (slashIdx === -1) return null; // file directly in apps/ (e.g. the .json definition)
322
+
323
+ const dirName = relPath.slice(0, slashIdx);
324
+ const innerPath = relPath.slice(slashIdx + 1);
325
+
326
+ // Skip non-source directories
327
+ if (innerPath.startsWith("records/") || innerPath.startsWith("dist/")) {
328
+ return null;
329
+ }
330
+
331
+ return resolveAppIdByDirName(dirName);
332
+ }
333
+
268
334
  /** Invalidate the id->dirName cache for a specific app or all apps. */
269
335
  function invalidateDirNameCache(appId?: string): void {
270
336
  if (appId) {
337
+ const dirName = idToDirNameCache.get(appId);
271
338
  idToDirNameCache.delete(appId);
339
+ if (dirName) dirNameToIdCache.delete(dirName);
272
340
  } else {
273
341
  idToDirNameCache.clear();
342
+ dirNameToIdCache.clear();
274
343
  }
275
344
  }
276
345
 
@@ -6,7 +6,7 @@ import {
6
6
  } from "./conversation-title-service.js";
7
7
 
8
8
  export interface BootstrapConversationOptions {
9
- conversationType?: "standard" | "private" | "background";
9
+ conversationType?: "standard" | "private" | "background" | "scheduled";
10
10
  source?: string;
11
11
  origin: TitleOrigin;
12
12
  systemHint: string;