@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
@@ -30,9 +30,11 @@ export const conversations = sqliteTable(
30
30
  forkParentMessageId: text("fork_parent_message_id"),
31
31
  isAutoTitle: integer("is_auto_title").notNull().default(1),
32
32
  scheduleJobId: text("schedule_job_id"),
33
+ lastMessageAt: integer("last_message_at"),
33
34
  },
34
35
  (table) => [
35
36
  index("idx_conversations_updated_at").on(table.updatedAt),
37
+ index("idx_conversations_last_message_at").on(table.lastMessageAt),
36
38
  index("idx_conversations_conversation_type").on(table.conversationType),
37
39
  index("idx_conversations_fork_parent_conversation_id").on(
38
40
  table.forkParentConversationId,
@@ -109,6 +111,18 @@ export const messageAttachments = sqliteTable("message_attachments", {
109
111
  createdAt: integer("created_at").notNull(),
110
112
  });
111
113
 
114
+ export const conversationGraphMemoryState = sqliteTable(
115
+ "conversation_graph_memory_state",
116
+ {
117
+ conversationId: text("conversation_id")
118
+ .primaryKey()
119
+ .references(() => conversations.id, { onDelete: "cascade" }),
120
+ stateJson: text("state_json").notNull(),
121
+ createdAt: integer("created_at").notNull(),
122
+ updatedAt: integer("updated_at").notNull(),
123
+ },
124
+ );
125
+
112
126
  export const channelInboundEvents = sqliteTable("channel_inbound_events", {
113
127
  id: text("id").primaryKey(),
114
128
  sourceChannel: text("source_channel").notNull(),
@@ -25,6 +25,9 @@ export const cronJobs = sqliteTable("cron_jobs", {
25
25
  routingHintsJson: text("routing_hints_json").notNull().default("{}"),
26
26
  status: text("status").notNull().default("active"), // 'active' | 'firing' | 'fired' | 'cancelled'
27
27
  quiet: integer("quiet", { mode: "boolean" }).notNull().default(false), // suppress completion notifications
28
+ reuseConversation: integer("reuse_conversation", { mode: "boolean" })
29
+ .notNull()
30
+ .default(false), // reuse the same conversation across runs
28
31
  createdAt: integer("created_at").notNull(),
29
32
  updatedAt: integer("updated_at").notNull(),
30
33
  });
@@ -118,7 +121,10 @@ export const llmRequestLogs = sqliteTable(
118
121
  responsePayload: text("response_payload").notNull(),
119
122
  createdAt: integer("created_at").notNull(),
120
123
  },
121
- (table) => [index("idx_llm_request_logs_message_id").on(table.messageId)],
124
+ (table) => [
125
+ index("idx_llm_request_logs_message_id").on(table.messageId),
126
+ index("idx_llm_request_logs_created_at").on(table.createdAt),
127
+ ],
122
128
  );
123
129
 
124
130
  export const memoryRecallLogs = sqliteTable(
@@ -144,6 +150,7 @@ export const memoryRecallLogs = sqliteTable(
144
150
  topCandidatesJson: text("top_candidates_json").notNull(),
145
151
  injectedText: text("injected_text"),
146
152
  reason: text("reason"),
153
+ queryContext: text("query_context"),
147
154
  createdAt: integer("created_at").notNull(),
148
155
  },
149
156
  (table) => [
@@ -2,7 +2,6 @@ import {
2
2
  blob,
3
3
  index,
4
4
  integer,
5
- real,
6
5
  sqliteTable,
7
6
  text,
8
7
  uniqueIndex,
@@ -32,56 +31,6 @@ export const memorySegments = sqliteTable(
32
31
  (table) => [index("idx_memory_segments_scope_id").on(table.scopeId)],
33
32
  );
34
33
 
35
- export const memoryItems = sqliteTable(
36
- "memory_items",
37
- {
38
- id: text("id").primaryKey(),
39
- kind: text("kind").notNull(),
40
- subject: text("subject").notNull(),
41
- statement: text("statement").notNull(),
42
- status: text("status").notNull(),
43
- confidence: real("confidence").notNull(),
44
- importance: real("importance"),
45
- accessCount: integer("access_count").notNull().default(0),
46
- fingerprint: text("fingerprint").notNull(),
47
- verificationState: text("verification_state")
48
- .notNull()
49
- .default("assistant_inferred"),
50
- scopeId: text("scope_id").notNull().default("default"),
51
- firstSeenAt: integer("first_seen_at").notNull(),
52
- lastSeenAt: integer("last_seen_at").notNull(),
53
- lastUsedAt: integer("last_used_at"),
54
- validFrom: integer("valid_from"),
55
- invalidAt: integer("invalid_at"),
56
- supersedes: text("supersedes"),
57
- supersededBy: text("superseded_by"),
58
- overrideConfidence: text("override_confidence").default("inferred"),
59
- sourceType: text("source_type").notNull().default("extraction"),
60
- sourceMessageRole: text("source_message_role"),
61
- },
62
- (table) => [
63
- index("idx_memory_items_scope_id").on(table.scopeId),
64
- index("idx_memory_items_fingerprint").on(table.fingerprint),
65
- ],
66
- );
67
-
68
- export const memoryItemSources = sqliteTable(
69
- "memory_item_sources",
70
- {
71
- memoryItemId: text("memory_item_id")
72
- .notNull()
73
- .references(() => memoryItems.id, { onDelete: "cascade" }),
74
- messageId: text("message_id")
75
- .notNull()
76
- .references(() => messages.id, { onDelete: "cascade" }),
77
- evidence: text("evidence"),
78
- createdAt: integer("created_at").notNull(),
79
- },
80
- (table) => [
81
- index("idx_memory_item_sources_memory_item_id").on(table.memoryItemId),
82
- ],
83
- );
84
-
85
34
  export const memorySummaries = sqliteTable(
86
35
  "memory_summaries",
87
36
  {
@@ -137,3 +137,18 @@ export const memoryGraphTriggers = sqliteTable(
137
137
  index("idx_graph_triggers_type").on(table.type),
138
138
  ],
139
139
  );
140
+
141
+ export const memoryGraphNodeEdits = sqliteTable(
142
+ "memory_graph_node_edits",
143
+ {
144
+ id: text("id").primaryKey(),
145
+ nodeId: text("node_id")
146
+ .notNull()
147
+ .references(() => memoryGraphNodes.id, { onDelete: "cascade" }),
148
+ previousContent: text("previous_content").notNull(),
149
+ newContent: text("new_content").notNull(),
150
+ source: text("source").notNull(),
151
+ conversationId: text("conversation_id"),
152
+ created: integer("created").notNull(),
153
+ },
154
+ );
@@ -9,16 +9,29 @@ const log = getLogger("task-memory-cleanup");
9
9
  * so the check survives daemon restarts.
10
10
  */
11
11
  export function isConversationFailed(conversationId: string): boolean {
12
+ // For reused schedule conversations the same conversation_id appears in
13
+ // multiple cron_runs. A single failed run should NOT mark the conversation
14
+ // as permanently failed — only the *most recent* run for that conversation
15
+ // matters. We therefore check whether the latest cron_run (by created_at,
16
+ // which is a monotonically increasing epoch timestamp) has an error status.
17
+ // Note: cron_runs.id is a UUID v4 (random), so we cannot use MAX(id).
12
18
  const row = rawGet<{ found: number }>(
13
19
  `SELECT 1 AS found
14
20
  FROM (
15
21
  SELECT 1 FROM task_runs WHERE conversation_id = ? AND status = 'failed'
16
22
  UNION ALL
17
- SELECT 1 FROM cron_runs WHERE conversation_id = ? AND status = 'error'
23
+ SELECT 1 FROM cron_runs
24
+ WHERE conversation_id = ?
25
+ AND status = 'error'
26
+ AND id = (
27
+ SELECT id FROM cron_runs WHERE conversation_id = ?
28
+ ORDER BY created_at DESC LIMIT 1
29
+ )
18
30
  )
19
31
  LIMIT 1`,
20
32
  conversationId,
21
33
  conversationId,
34
+ conversationId,
22
35
  );
23
36
  return row != null;
24
37
  }
@@ -57,9 +70,17 @@ export function invalidateAssistantInferredItemsForConversation(
57
70
  AND tr.status = 'failed'
58
71
  )
59
72
  AND NOT EXISTS (
73
+ -- Check only the most recent cron_run for each conversation
74
+ -- so reused conversations with historical errors but recent
75
+ -- successes are still treated as valid corroborators.
60
76
  SELECT 1 FROM cron_runs cr
61
77
  WHERE cr.conversation_id = jc2.value
62
78
  AND cr.status = 'error'
79
+ AND cr.id = (
80
+ SELECT cr2.id FROM cron_runs cr2
81
+ WHERE cr2.conversation_id = jc2.value
82
+ ORDER BY cr2.created_at DESC LIMIT 1
83
+ )
63
84
  )
64
85
  )`,
65
86
  Date.now(),
@@ -79,9 +100,9 @@ export function invalidateAssistantInferredItemsForConversation(
79
100
 
80
101
  /**
81
102
  * Cancel all pending/running memory jobs referencing the given conversation.
82
- * Covers every job type: `extract_items`, `embed_attachment` (keyed by messageId),
103
+ * Covers every job type: `embed_attachment` (keyed by messageId),
83
104
  * `embed_segment` (keyed by segmentId via memory_segments),
84
- * `build_conversation_summary` (keyed by conversationId),
105
+ * `graph_extract`, `build_conversation_summary` (keyed by conversationId),
85
106
  * and `embed_graph_node` (keyed by nodeId sourced from the conversation).
86
107
  */
87
108
  export function cancelPendingJobsForConversation(
@@ -91,7 +112,7 @@ export function cancelPendingJobsForConversation(
91
112
  const now = Date.now();
92
113
  let total = 0;
93
114
 
94
- // Jobs keyed by messageId: extract_items, embed_attachment
115
+ // Jobs keyed by messageId: embed_attachment
95
116
  total += rawRun(
96
117
  `UPDATE memory_jobs
97
118
  SET status = 'failed',
@@ -106,7 +127,7 @@ export function cancelPendingJobsForConversation(
106
127
  conversationId,
107
128
  );
108
129
 
109
- // Jobs keyed by conversationId: build_conversation_summary
130
+ // Jobs keyed by conversationId: graph_extract, build_conversation_summary
110
131
  total += rawRun(
111
132
  `UPDATE memory_jobs
112
133
  SET status = 'failed',
@@ -162,8 +183,8 @@ export function cancelPendingJobsForConversation(
162
183
  }
163
184
 
164
185
  /**
165
- * Cancel only pending/running `extract_items` jobs for messages in the
166
- * given conversation. Used by the task-failure path where we want to
186
+ * Cancel only pending/running `graph_extract` jobs for the given
187
+ * conversation. Used by the task-failure path where we want to
167
188
  * stop new extractions but must NOT cancel `embed_graph_node` jobs —
168
189
  * those nodes may be multi-sourced and still valid.
169
190
  */
@@ -177,10 +198,8 @@ function cancelPendingExtractionJobsForConversation(
177
198
  last_error = 'conversation_failed',
178
199
  updated_at = ?
179
200
  WHERE status IN ('pending', 'running')
180
- AND type = 'extract_items'
181
- AND json_extract(payload, '$.messageId') IN (
182
- SELECT id FROM messages WHERE conversation_id = ?
183
- )`,
201
+ AND type = 'graph_extract'
202
+ AND json_extract(payload, '$.conversationId') = ?`,
184
203
  now,
185
204
  conversationId,
186
205
  );
@@ -408,6 +408,92 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
408
408
  };
409
409
  },
410
410
 
411
+ "ingress.trusted_contact.guardian_decision": (payload) => {
412
+ const decision = str(payload.decision, "decided on");
413
+ const sourceChannel =
414
+ typeof payload.sourceChannel === "string"
415
+ ? payload.sourceChannel
416
+ : undefined;
417
+
418
+ const requesterDisplayName =
419
+ typeof payload.requesterDisplayName === "string" &&
420
+ payload.requesterDisplayName.length > 0
421
+ ? payload.requesterDisplayName
422
+ : undefined;
423
+ const requesterExternalUserId =
424
+ typeof payload.requesterExternalUserId === "string" &&
425
+ payload.requesterExternalUserId.length > 0
426
+ ? payload.requesterExternalUserId
427
+ : undefined;
428
+ const requesterLabel = sanitizeIdentityField(
429
+ requesterDisplayName ??
430
+ (sourceChannel === "slack" &&
431
+ requesterExternalUserId &&
432
+ /^U[A-Z0-9]+$/i.test(requesterExternalUserId)
433
+ ? `<@${requesterExternalUserId}>`
434
+ : requesterExternalUserId) ??
435
+ "Someone",
436
+ );
437
+
438
+ const decidedByDisplayName =
439
+ typeof payload.decidedByDisplayName === "string" &&
440
+ payload.decidedByDisplayName.length > 0
441
+ ? payload.decidedByDisplayName
442
+ : undefined;
443
+ const decidedByExternalUserId =
444
+ typeof payload.decidedByExternalUserId === "string" &&
445
+ payload.decidedByExternalUserId.length > 0
446
+ ? payload.decidedByExternalUserId
447
+ : undefined;
448
+ const decidedByLabel = sanitizeIdentityField(
449
+ decidedByDisplayName ??
450
+ (sourceChannel === "slack" &&
451
+ decidedByExternalUserId &&
452
+ /^U[A-Z0-9]+$/i.test(decidedByExternalUserId)
453
+ ? `<@${decidedByExternalUserId}>`
454
+ : decidedByExternalUserId) ??
455
+ "a guardian",
456
+ );
457
+
458
+ const verb = decision === "approved" ? "approved" : "denied";
459
+ return {
460
+ title: "Trusted Contact Decision",
461
+ body: `${requesterLabel}'s access request has been ${verb} by ${decidedByLabel}.`,
462
+ };
463
+ },
464
+
465
+ "ingress.trusted_contact.denied": (payload) => {
466
+ const sourceChannel =
467
+ typeof payload.sourceChannel === "string"
468
+ ? payload.sourceChannel
469
+ : undefined;
470
+
471
+ const requesterDisplayName =
472
+ typeof payload.requesterDisplayName === "string" &&
473
+ payload.requesterDisplayName.length > 0
474
+ ? payload.requesterDisplayName
475
+ : undefined;
476
+ const requesterExternalUserId =
477
+ typeof payload.requesterExternalUserId === "string" &&
478
+ payload.requesterExternalUserId.length > 0
479
+ ? payload.requesterExternalUserId
480
+ : undefined;
481
+ const requesterLabel = sanitizeIdentityField(
482
+ requesterDisplayName ??
483
+ (sourceChannel === "slack" &&
484
+ requesterExternalUserId &&
485
+ /^U[A-Z0-9]+$/i.test(requesterExternalUserId)
486
+ ? `<@${requesterExternalUserId}>`
487
+ : requesterExternalUserId) ??
488
+ "Someone",
489
+ );
490
+
491
+ return {
492
+ title: "Trusted Contact Denied",
493
+ body: `A trusted contact request from ${requesterLabel} has been denied.`,
494
+ };
495
+ },
496
+
411
497
  "ingress.escalation": (payload) => ({
412
498
  title: "Escalation",
413
499
  body:
@@ -13,6 +13,7 @@ import { v4 as uuid } from "uuid";
13
13
 
14
14
  import { getDeliverableChannels } from "../channels/config.js";
15
15
  import { getConfig } from "../config/loader.js";
16
+ import { listGuardianChannels } from "../contacts/contact-store.js";
16
17
  import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
17
18
  import { buildCoreIdentityContext } from "../prompts/system-prompt.js";
18
19
  import {
@@ -73,6 +74,7 @@ function buildSystemPrompt(
73
74
  preferenceContext?: string,
74
75
  candidateContext?: string,
75
76
  identityContext?: string,
77
+ recipientNotes?: string,
76
78
  ): string {
77
79
  const sections: string[] = [
78
80
  `You are a notification routing engine. Given a signal describing an event, decide whether the user should be notified, on which channel(s), and compose the notification copy.`,
@@ -89,6 +91,16 @@ function buildSystemPrompt(
89
91
  );
90
92
  }
91
93
 
94
+ if (recipientNotes) {
95
+ sections.push(
96
+ ``,
97
+ `<recipient-context>`,
98
+ `The following are notes about the notification recipient. Use this context to tailor notification tone, formality, and content to the recipient's preferences.`,
99
+ recipientNotes,
100
+ `</recipient-context>`,
101
+ );
102
+ }
103
+
92
104
  if (identityContext) {
93
105
  sections.push(
94
106
  ``,
@@ -807,11 +819,34 @@ async function classifyWithLLM(
807
819
  const identityContext = rawIdentityContext
808
820
  ? truncate(rawIdentityContext, MAX_IDENTITY_CONTEXT_CHARS, "\n…[truncated]")
809
821
  : undefined;
822
+
823
+ // Resolve guardian contact notes for recipient context. Use the channel-
824
+ // agnostic guardian lookup so notes are available even when the only
825
+ // deliverable channel is "vellum" (which has no contact channel type).
826
+ let recipientNotes: string | undefined;
827
+ try {
828
+ const guardianResult = listGuardianChannels();
829
+ if (guardianResult?.contact.notes) {
830
+ recipientNotes = truncate(
831
+ guardianResult.contact.notes,
832
+ MAX_IDENTITY_CONTEXT_CHARS,
833
+ "\n…[truncated]",
834
+ );
835
+ }
836
+ } catch (err) {
837
+ const errMsg = err instanceof Error ? err.message : String(err);
838
+ log.warn(
839
+ { err: errMsg },
840
+ "Failed to resolve guardian contact notes, proceeding without recipient context",
841
+ );
842
+ }
843
+
810
844
  const systemPrompt = buildSystemPrompt(
811
845
  availableChannels,
812
846
  preferenceContext,
813
847
  candidateContext,
814
848
  identityContext,
849
+ recipientNotes,
815
850
  );
816
851
  const prompt = buildUserPrompt(signal);
817
852
  const tool = buildDecisionTool(availableChannels);
@@ -677,7 +677,7 @@ export async function classifyRisk(
677
677
  }
678
678
  }
679
679
 
680
- const result = await classifyRiskUncached(
680
+ let result = await classifyRiskUncached(
681
681
  toolName,
682
682
  input,
683
683
  workingDir,
@@ -685,6 +685,17 @@ export async function classifyRisk(
685
685
  manifestOverride,
686
686
  );
687
687
 
688
+ // Proxied bash commands route through the credential proxy which handles
689
+ // per-request approval separately. Cap the bash tool's own risk at Medium
690
+ // so trust rules can auto-allow the command execution.
691
+ if (
692
+ toolName === "bash" &&
693
+ input.network_mode === "proxied" &&
694
+ result === RiskLevel.High
695
+ ) {
696
+ result = RiskLevel.Medium;
697
+ }
698
+
688
699
  if (cacheKey) {
689
700
  if (riskCache.size >= RISK_CACHE_MAX) {
690
701
  const oldest = riskCache.keys().next().value;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Singleton runtime store for the two-axis permission mode.
3
+ *
4
+ * Reads initial state from the `permissions` section of config.json on
5
+ * initialization and persists mutations back to the config file via the
6
+ * raw-config read/write helpers so env-var–derived keys are never leaked
7
+ * to disk.
8
+ *
9
+ * Downstream consumers (e.g. SSE broadcast) register change listeners
10
+ * via `onModeChanged()`.
11
+ */
12
+
13
+ import {
14
+ invalidateConfigCache,
15
+ loadConfig,
16
+ loadRawConfig,
17
+ saveRawConfig,
18
+ } from "../config/loader.js";
19
+ import { getLogger } from "../util/logger.js";
20
+ import type { PermissionMode } from "./permission-mode.js";
21
+ import { DEFAULT_PERMISSION_MODE } from "./permission-mode.js";
22
+
23
+ const log = getLogger("permission-mode-store");
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export type ModeChangeListener = (mode: PermissionMode) => void;
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Module-level state
33
+ // ---------------------------------------------------------------------------
34
+
35
+ let currentMode: PermissionMode = { ...DEFAULT_PERMISSION_MODE };
36
+ let initialized = false;
37
+ const listeners: ModeChangeListener[] = [];
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Internal helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ function notifyListeners(): void {
44
+ const snapshot = { ...currentMode };
45
+ for (const listener of listeners) {
46
+ try {
47
+ listener(snapshot);
48
+ } catch (err) {
49
+ log.error({ err }, "Error in permission mode change listener");
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Persist the current in-memory permission mode to config.json.
56
+ *
57
+ * Uses the raw-config pattern (loadRawConfig → mutate → saveRawConfig) so
58
+ * that env-var–derived fields (API keys, dataDir) are never written to disk.
59
+ */
60
+ function persistToConfig(): void {
61
+ try {
62
+ const raw = loadRawConfig();
63
+
64
+ // Ensure the permissions object exists
65
+ if (
66
+ raw.permissions == null ||
67
+ typeof raw.permissions !== "object" ||
68
+ Array.isArray(raw.permissions)
69
+ ) {
70
+ raw.permissions = {};
71
+ }
72
+
73
+ const permissions = raw.permissions as Record<string, unknown>;
74
+ permissions.askBeforeActing = currentMode.askBeforeActing;
75
+ permissions.hostAccess = currentMode.hostAccess;
76
+
77
+ saveRawConfig(raw);
78
+
79
+ // Invalidate the cached config so the next loadConfig() picks up the
80
+ // persisted values rather than returning stale in-memory state.
81
+ invalidateConfigCache();
82
+ } catch (err) {
83
+ log.error({ err }, "Failed to persist permission mode to config");
84
+ }
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Public API
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Initialize the store from the current config. Safe to call multiple times;
93
+ * subsequent calls are no-ops unless `resetForTesting()` has been called.
94
+ */
95
+ export function initPermissionModeStore(): void {
96
+ if (initialized) return;
97
+
98
+ try {
99
+ const config = loadConfig();
100
+ currentMode = {
101
+ askBeforeActing: config.permissions.askBeforeActing,
102
+ hostAccess: config.permissions.hostAccess,
103
+ };
104
+ } catch (err) {
105
+ log.warn(
106
+ { err },
107
+ "Failed to load permission mode from config; using defaults",
108
+ );
109
+ currentMode = { ...DEFAULT_PERMISSION_MODE };
110
+ }
111
+
112
+ initialized = true;
113
+ }
114
+
115
+ /**
116
+ * Return the current permission mode. Initializes from config on first call
117
+ * if `initPermissionModeStore()` hasn't been called yet.
118
+ */
119
+ export function getMode(): PermissionMode {
120
+ if (!initialized) {
121
+ initPermissionModeStore();
122
+ }
123
+ return { ...currentMode };
124
+ }
125
+
126
+ /**
127
+ * Update the `askBeforeActing` axis. Persists to config.json and notifies
128
+ * change listeners.
129
+ */
130
+ export function setAskBeforeActing(value: boolean): void {
131
+ if (!initialized) {
132
+ initPermissionModeStore();
133
+ }
134
+
135
+ if (currentMode.askBeforeActing === value) return;
136
+
137
+ currentMode.askBeforeActing = value;
138
+ persistToConfig();
139
+ notifyListeners();
140
+ }
141
+
142
+ /**
143
+ * Update the `hostAccess` axis. Persists to config.json and notifies
144
+ * change listeners.
145
+ */
146
+ export function setHostAccess(value: boolean): void {
147
+ if (!initialized) {
148
+ initPermissionModeStore();
149
+ }
150
+
151
+ if (currentMode.hostAccess === value) return;
152
+
153
+ currentMode.hostAccess = value;
154
+ persistToConfig();
155
+ notifyListeners();
156
+ }
157
+
158
+ /**
159
+ * Register a callback that fires whenever the permission mode changes.
160
+ * Returns an unsubscribe function.
161
+ */
162
+ export function onModeChanged(callback: ModeChangeListener): () => void {
163
+ listeners.push(callback);
164
+ return () => {
165
+ const idx = listeners.indexOf(callback);
166
+ if (idx >= 0) {
167
+ listeners.splice(idx, 1);
168
+ }
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Reset the store to uninitialized state. **Test-only** — production code
174
+ * should never call this.
175
+ */
176
+ export function resetForTesting(): void {
177
+ currentMode = { ...DEFAULT_PERMISSION_MODE };
178
+ initialized = false;
179
+ listeners.length = 0;
180
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Two-axis permission model:
5
+ * - `askBeforeActing` — LLM behavior toggle: when true the assistant checks in
6
+ * with the user before taking actions.
7
+ * - `hostAccess` — System-enforced gate: when true the assistant can execute
8
+ * commands on the host machine without prompting.
9
+ */
10
+ export type PermissionMode = {
11
+ askBeforeActing: boolean;
12
+ hostAccess: boolean;
13
+ };
14
+
15
+ export const DEFAULT_PERMISSION_MODE: PermissionMode = {
16
+ askBeforeActing: true,
17
+ hostAccess: false,
18
+ };
19
+
20
+ export const PermissionModeSchema = z.object({
21
+ askBeforeActing: z
22
+ .boolean({ error: "permissionMode.askBeforeActing must be a boolean" })
23
+ .default(true)
24
+ .describe("Whether the assistant should check in before taking actions"),
25
+ hostAccess: z
26
+ .boolean({ error: "permissionMode.hostAccess must be a boolean" })
27
+ .default(false)
28
+ .describe(
29
+ "Whether the assistant can execute commands on the host machine without prompting",
30
+ ),
31
+ });
@@ -74,8 +74,17 @@ const HOST_TOOLS = new Set([
74
74
  "host_file_write",
75
75
  "host_file_edit",
76
76
  "host_bash",
77
+ "computer_use_run_applescript",
77
78
  ]);
78
79
 
80
+ /**
81
+ * Check whether a tool name is a host-level tool that requires the
82
+ * `hostAccess` permission to execute.
83
+ */
84
+ export function isHostTool(toolName: string): boolean {
85
+ return HOST_TOOLS.has(toolName);
86
+ }
87
+
79
88
  /** Safe local-only tools that are always workspace-scoped. */
80
89
  const ALWAYS_SCOPED_TOOLS = new Set([
81
90
  "skill_load",