@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -690,7 +690,7 @@ function buildMemoryRecallSection(): string {
690
690
  "- The auto-injected memory context doesn't contain what you need",
691
691
  "- The user references something from a previous session",
692
692
  "",
693
- "The tool searches across semantic, lexical, entity graph, and recency sources. Be specific in your query for best results.",
693
+ "The tool uses hybrid search (dense and sparse vectors) supplemented by recency. Be specific in your query for best results.",
694
694
  ].join("\n");
695
695
  }
696
696
 
@@ -845,7 +845,7 @@ export function buildCliReferenceSection(): string {
845
845
  return [
846
846
  "## Assistant CLI",
847
847
  "",
848
- "The `assistant` CLI is installed on the user's machine and available via `bash`.",
848
+ "The `assistant` CLI is available in the sandbox. Always use the `bash` tool (never `host_bash`) when running `assistant` commands.",
849
849
  "For account and authentication work, prefer real `assistant` CLI workflows over any legacy account-record abstraction.",
850
850
  "- Use `assistant credentials ...` for stored secrets and credential metadata.",
851
851
  "- Use `assistant oauth connections token <provider-key>` for connected integration tokens.",
@@ -960,6 +960,22 @@ function buildDynamicSkillWorkflowSection(
960
960
  );
961
961
  }
962
962
 
963
+ lines.push(
964
+ "",
965
+ "### Community Skills Discovery",
966
+ "",
967
+ "When no built-in skill satisfies a request, search the community skills.sh registry:",
968
+ "1. Run `assistant skills search <query>` to find community skills. Results include install counts and security audit badges (ATH, Socket, Snyk).",
969
+ "2. Present the search results to the user, highlighting the security audit status. ATH is Gen Agent Trust Hub. Audits show PASS (safe/low risk), WARN (medium risk), or FAIL (high/critical risk) for each provider.",
970
+ "3. Check the skill's **source owner** to determine the trust level:",
971
+ " - **Vellum-owned** (source starts with `vellum-ai/`): These are first-party skills published by the Vellum team. Install them directly without prompting — they are vetted and trusted.",
972
+ " - **Third-party** (any other owner): Ask the user for permission before installing. Say something like: \"I found a community skill that could help with this, but it's published by a third party — we haven't vetted it. Want to install it anyway?\" Share the skill name, source, audit results, and install count.",
973
+ "4. Install with `assistant skills add <owner>/<repo>@<skill-name>` (e.g., `assistant skills add vercel-labs/skills@find-skills`).",
974
+ "5. After installation, load the skill with `skill_load` as usual.",
975
+ "",
976
+ "**Never install third-party community skills without explicit user confirmation.** Vellum-owned skills (`vellum-ai/*`) can be installed automatically.",
977
+ );
978
+
963
979
  return lines.join("\n");
964
980
  }
965
981
 
@@ -63,20 +63,6 @@ function isToolUseBlock(block: unknown): block is Anthropic.ToolUseBlockParam {
63
63
  );
64
64
  }
65
65
 
66
- /** Type-guard for server_tool_use blocks (e.g. native web search). */
67
- function isServerToolUseBlock(block: unknown): block is {
68
- type: "server_tool_use";
69
- id: string;
70
- name: string;
71
- input: unknown;
72
- } {
73
- return (
74
- typeof block === "object" &&
75
- block != null &&
76
- (block as { type: string }).type === "server_tool_use"
77
- );
78
- }
79
-
80
66
  /** Type-guard for tool_result blocks in Anthropic-formatted content. */
81
67
  function isToolResultBlock(
82
68
  block: unknown,
@@ -88,19 +74,6 @@ function isToolResultBlock(
88
74
  );
89
75
  }
90
76
 
91
- /** Type-guard for web_search_tool_result blocks. */
92
- function isWebSearchToolResultBlock(block: unknown): block is {
93
- type: "web_search_tool_result";
94
- tool_use_id: string;
95
- content: unknown;
96
- } {
97
- return (
98
- typeof block === "object" &&
99
- block != null &&
100
- (block as { type: string }).type === "web_search_tool_result"
101
- );
102
- }
103
-
104
77
  /**
105
78
  * Build a short diagnostic summary of a message array for error logging.
106
79
  * Shows role + block types (with tool_use/tool_result IDs) for each message.
@@ -134,79 +107,55 @@ function buildSyntheticToolResult(
134
107
  };
135
108
  }
136
109
 
137
- function buildSyntheticWebSearchToolResult(
138
- toolUseId: string,
139
- ): Anthropic.ContentBlockParam {
140
- return {
141
- type: "web_search_tool_result",
142
- tool_use_id: toolUseId,
143
- content: {
144
- type: "web_search_tool_result_error",
145
- error_code: "unavailable",
146
- },
147
- } as unknown as Anthropic.ContentBlockParam;
148
- }
149
-
150
- /** Build the appropriate synthetic result block based on whether the ID is for a server tool or regular tool. */
151
- function buildSyntheticResult(
152
- toolUseId: string,
153
- serverToolIds: ReadonlySet<string>,
154
- ): Anthropic.ContentBlockParam {
155
- if (serverToolIds.has(toolUseId)) {
156
- return buildSyntheticWebSearchToolResult(toolUseId);
157
- }
158
- return buildSyntheticToolResult(toolUseId);
159
- }
160
110
 
161
- function getOrderedToolUseIds(content: Anthropic.ContentBlockParam[]): {
162
- ids: string[];
163
- serverToolIds: Set<string>;
164
- } {
111
+ /**
112
+ * Collect ordered IDs of client-side tool_use blocks only.
113
+ * Server-side tools (server_tool_use / web_search_tool_result) are self-paired
114
+ * within the assistant message and do not need cross-message pairing.
115
+ */
116
+ function getOrderedToolUseIds(
117
+ content: Anthropic.ContentBlockParam[],
118
+ ): string[] {
165
119
  const ids: string[] = [];
166
120
  const seen = new Set<string>();
167
- const serverToolIds = new Set<string>();
168
121
  for (const block of content) {
169
122
  if (isToolUseBlock(block)) {
170
123
  if (!seen.has(block.id)) {
171
124
  seen.add(block.id);
172
125
  ids.push(block.id);
173
126
  }
174
- } else if (isServerToolUseBlock(block)) {
175
- if (!seen.has(block.id)) {
176
- seen.add(block.id);
177
- ids.push(block.id);
178
- serverToolIds.add(block.id);
179
- }
180
127
  }
181
128
  }
182
- return { ids, serverToolIds };
129
+ return ids;
183
130
  }
184
131
 
185
132
  function hasOrderedToolResultPrefix(
186
133
  content: Anthropic.ContentBlockParam[],
187
134
  orderedToolUseIds: string[],
188
- serverToolIds: ReadonlySet<string>,
189
135
  ): boolean {
190
136
  if (content.length < orderedToolUseIds.length) return false;
191
137
  for (let idx = 0; idx < orderedToolUseIds.length; idx++) {
192
138
  const block = content[idx];
193
139
  const expectedId = orderedToolUseIds[idx];
194
- if (serverToolIds.has(expectedId)) {
195
- if (!isWebSearchToolResultBlock(block)) return false;
196
- if (block.tool_use_id !== expectedId) return false;
197
- } else {
198
- if (!isToolResultBlock(block)) return false;
199
- if (block.tool_use_id !== expectedId) return false;
200
- }
140
+ if (!isToolResultBlock(block)) return false;
141
+ if (block.tool_use_id !== expectedId) return false;
201
142
  }
202
143
  return true;
203
144
  }
204
145
 
146
+ /**
147
+ * Split an assistant message into:
148
+ * - pairedContent: everything up to and including client-side tool_use blocks
149
+ * - carryoverContent: trailing non-tool blocks after the last tool_use
150
+ *
151
+ * Server-side tools (server_tool_use / web_search_tool_result) are treated as
152
+ * regular content — they are self-paired within the assistant message and must
153
+ * not be separated by the cross-message pairing logic.
154
+ */
205
155
  function splitAssistantForToolPairing(content: Anthropic.ContentBlockParam[]): {
206
156
  pairedContent: Anthropic.ContentBlockParam[];
207
157
  carryoverContent: Anthropic.ContentBlockParam[];
208
158
  toolUseIds: string[];
209
- serverToolIds: Set<string>;
210
159
  } {
211
160
  const leading: Anthropic.ContentBlockParam[] = [];
212
161
  const toolUseBlocks: Anthropic.ContentBlockParam[] = [];
@@ -214,7 +163,7 @@ function splitAssistantForToolPairing(content: Anthropic.ContentBlockParam[]): {
214
163
  let seenToolUse = false;
215
164
 
216
165
  for (const block of content) {
217
- if (isToolUseBlock(block) || isServerToolUseBlock(block)) {
166
+ if (isToolUseBlock(block)) {
218
167
  seenToolUse = true;
219
168
  toolUseBlocks.push(block);
220
169
  continue;
@@ -231,7 +180,6 @@ function splitAssistantForToolPairing(content: Anthropic.ContentBlockParam[]): {
231
180
  pairedContent: content,
232
181
  carryoverContent: [],
233
182
  toolUseIds: [],
234
- serverToolIds: new Set(),
235
183
  };
236
184
  }
237
185
 
@@ -239,19 +187,16 @@ function splitAssistantForToolPairing(content: Anthropic.ContentBlockParam[]): {
239
187
  ...leading,
240
188
  ...toolUseBlocks,
241
189
  ];
242
- const { ids, serverToolIds } = getOrderedToolUseIds(pairedContent);
243
190
  return {
244
191
  pairedContent,
245
192
  carryoverContent: carryover,
246
- toolUseIds: ids,
247
- serverToolIds,
193
+ toolUseIds: getOrderedToolUseIds(pairedContent),
248
194
  };
249
195
  }
250
196
 
251
197
  function normalizeFollowingUserContent(
252
198
  nextContent: Anthropic.ContentBlockParam[],
253
199
  orderedToolUseIds: string[],
254
- serverToolIds: ReadonlySet<string>,
255
200
  ): {
256
201
  toolResultPrefix: Anthropic.ContentBlockParam[];
257
202
  remainingContent: Anthropic.ContentBlockParam[];
@@ -266,41 +211,24 @@ function normalizeFollowingUserContent(
266
211
  if (
267
212
  isToolResultBlock(block) &&
268
213
  pendingIds.has(block.tool_use_id) &&
269
- !matchedById.has(block.tool_use_id) &&
270
- !serverToolIds.has(block.tool_use_id)
214
+ !matchedById.has(block.tool_use_id)
271
215
  ) {
272
216
  matchedById.set(block.tool_use_id, block);
273
217
  continue;
274
218
  }
275
- if (
276
- isWebSearchToolResultBlock(block) &&
277
- pendingIds.has(block.tool_use_id) &&
278
- !matchedById.has(block.tool_use_id) &&
279
- serverToolIds.has(block.tool_use_id)
280
- ) {
281
- matchedById.set(
282
- block.tool_use_id,
283
- block as unknown as Anthropic.ContentBlockParam,
284
- );
285
- continue;
286
- }
287
219
  remaining.push(block);
288
220
  }
289
221
 
290
222
  const missingIds = orderedToolUseIds.filter((id) => !matchedById.has(id));
291
223
  const orderedResults = orderedToolUseIds.map(
292
- (id) => matchedById.get(id) ?? buildSyntheticResult(id, serverToolIds),
224
+ (id) => matchedById.get(id) ?? buildSyntheticToolResult(id),
293
225
  );
294
226
 
295
227
  return {
296
228
  toolResultPrefix: orderedResults,
297
229
  remainingContent: remaining,
298
230
  missingIds,
299
- hadOrderedPrefix: hasOrderedToolResultPrefix(
300
- nextContent,
301
- orderedToolUseIds,
302
- serverToolIds,
303
- ),
231
+ hadOrderedPrefix: hasOrderedToolResultPrefix(nextContent, orderedToolUseIds),
304
232
  };
305
233
  }
306
234
 
@@ -328,7 +256,7 @@ function ensureToolPairing(
328
256
  }
329
257
 
330
258
  const content = Array.isArray(msg.content) ? msg.content : [];
331
- const { pairedContent, carryoverContent, toolUseIds, serverToolIds } =
259
+ const { pairedContent, carryoverContent, toolUseIds } =
332
260
  splitAssistantForToolPairing(content);
333
261
 
334
262
  if (toolUseIds.length === 0) {
@@ -337,7 +265,7 @@ function ensureToolPairing(
337
265
  continue;
338
266
  }
339
267
 
340
- // Assistant message — push the paired portion (pre-tool text + tool_use/server_tool_use blocks)
268
+ // Assistant message — push the paired portion (pre-tool text + tool_use blocks)
341
269
  result.push({
342
270
  role: "assistant" as const,
343
271
  content: pairedContent,
@@ -358,11 +286,7 @@ function ensureToolPairing(
358
286
  const next = messages[i + 1];
359
287
  if (next && next.role === "user") {
360
288
  const nextContent = Array.isArray(next.content) ? next.content : [];
361
- const normalized = normalizeFollowingUserContent(
362
- nextContent,
363
- toolUseIds,
364
- serverToolIds,
365
- );
289
+ const normalized = normalizeFollowingUserContent(nextContent, toolUseIds);
366
290
  if (normalized.missingIds.length > 0) {
367
291
  log.warn(
368
292
  {
@@ -427,9 +351,7 @@ function ensureToolPairing(
427
351
  );
428
352
  result.push({
429
353
  role: "user" as const,
430
- content: toolUseIds.map((id) =>
431
- buildSyntheticResult(id, serverToolIds),
432
- ),
354
+ content: toolUseIds.map((id) => buildSyntheticToolResult(id)),
433
355
  });
434
356
 
435
357
  // If the assistant contained collapsed post-tool text, preserve it as a
@@ -445,13 +367,14 @@ function ensureToolPairing(
445
367
  }
446
368
  }
447
369
 
448
- // Self-validation: verify no tool_use/tool_result mismatches remain
370
+ // Self-validation: verify no client-side tool_use/tool_result mismatches remain.
371
+ // Server-side tools (server_tool_use / web_search_tool_result) are self-paired
372
+ // within assistant messages and are not validated here.
449
373
  for (let j = 0; j < result.length; j++) {
450
374
  const m = result[j];
451
375
  if (m.role !== "assistant") continue;
452
376
  const c = Array.isArray(m.content) ? m.content : [];
453
- const { ids: validationIds, serverToolIds: validationServerToolIds } =
454
- getOrderedToolUseIds(c);
377
+ const validationIds = getOrderedToolUseIds(c);
455
378
  if (validationIds.length === 0) continue;
456
379
 
457
380
  const nxt = result[j + 1];
@@ -459,20 +382,9 @@ function ensureToolPairing(
459
382
  nxt && nxt.role === "user" && Array.isArray(nxt.content)
460
383
  ? nxt.content
461
384
  : [];
462
- if (
463
- !hasOrderedToolResultPrefix(
464
- nxtContent,
465
- validationIds,
466
- validationServerToolIds,
467
- )
468
- ) {
385
+ if (!hasOrderedToolResultPrefix(nxtContent, validationIds)) {
469
386
  const unmatchedIds = validationIds.filter((id, idx) => {
470
387
  const block = nxtContent[idx];
471
- if (validationServerToolIds.has(id)) {
472
- return !(
473
- isWebSearchToolResultBlock(block) && block.tool_use_id === id
474
- );
475
- }
476
388
  return !(isToolResultBlock(block) && block.tool_use_id === id);
477
389
  });
478
390
  log.error(
@@ -768,10 +680,14 @@ export class AnthropicProvider implements Provider {
768
680
  onEvent?.({ type: "text_delta", text: " " });
769
681
  }
770
682
  hasSeenTextBlock = true;
771
- } else if (event.type === "content_block_start") {
772
- // Reset on non-text blocks so that text separated by tool_use
773
- // (text -> tool_use -> text) doesn't get a spurious leading space
774
- // in the second text segment.
683
+ } else if (
684
+ event.type === "content_block_start" &&
685
+ event.content_block.type === "tool_use"
686
+ ) {
687
+ // Reset only for client-side tool_use blocks, which create visual
688
+ // separators in the UI. Server-side tool blocks (server_tool_use,
689
+ // web_search_tool_result) are transparent in the text stream and
690
+ // need the space preserved between surrounding text blocks.
775
691
  hasSeenTextBlock = false;
776
692
  }
777
693
  if (
@@ -796,6 +712,20 @@ export class AnthropicProvider implements Provider {
796
712
  type: "server_tool_start",
797
713
  name: event.content_block.name,
798
714
  toolUseId: event.content_block.id,
715
+ input: (
716
+ event.content_block as { input?: Record<string, unknown> }
717
+ ).input ?? {},
718
+ });
719
+ }
720
+ if (
721
+ event.type === "content_block_start" &&
722
+ event.content_block.type === "web_search_tool_result"
723
+ ) {
724
+ onEvent?.({
725
+ type: "server_tool_complete",
726
+ toolUseId: (
727
+ event.content_block as { tool_use_id: string }
728
+ ).tool_use_id,
799
729
  });
800
730
  }
801
731
  if (event.type === "content_block_stop") {
@@ -117,7 +117,13 @@ export type ProviderEvent =
117
117
  toolUseId: string;
118
118
  accumulatedJson: string;
119
119
  }
120
- | { type: "server_tool_start"; name: string; toolUseId: string };
120
+ | {
121
+ type: "server_tool_start";
122
+ name: string;
123
+ toolUseId: string;
124
+ input: Record<string, unknown>;
125
+ }
126
+ | { type: "server_tool_complete"; toolUseId: string };
121
127
 
122
128
  export interface SendMessageConfig {
123
129
  model?: string;
@@ -43,6 +43,15 @@ Host file allows the assistant to perform file operations (read, write, edit) on
43
43
  - `POST /v1/host-file-result` — `{ requestId, content, isError }`
44
44
  - **Tracking**: Uses the same `pending-interactions` tracker as approvals and host bash, with `kind: "host_file"`. The endpoint validates the interaction kind before resolving.
45
45
 
46
+ ### Host CU (desktop proxy computer-use execution)
47
+
48
+ Host CU allows the assistant to proxy computer-use actions (screenshots, mouse/keyboard input) to the desktop host via the client, following the same pattern as host bash and host file.
49
+
50
+ - **Discovery**: Clients discover pending host CU requests via SSE events (`host_cu_request`) which include a `requestId`.
51
+ - **Resolution**: Clients execute the CU action on the host and respond via:
52
+ - `POST /v1/host-cu-result` — `{ requestId, axTree?, axDiff?, screenshot?, screenshotWidthPx?, screenshotHeightPx?, screenWidthPt?, screenHeightPt?, executionResult?, executionError?, secondaryWindows?, userGuidance? }`
53
+ - **Tracking**: Uses the same `pending-interactions` tracker as the other host proxy types, with `kind: "host_cu"`. Registration happens in `conversation-routes.ts` and the route handler is in `host-cu-routes.ts`.
54
+
46
55
  ### Channel approvals (Telegram, Slack)
47
56
 
48
57
  Channel approval flows use `requestId` (not `runId`) as the primary identifier:
@@ -347,6 +347,12 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
347
347
  { endpoint: "skills:DELETE", scopes: ["settings.write"] },
348
348
  { endpoint: "skills:PATCH", scopes: ["settings.write"] },
349
349
 
350
+ // Memory items
351
+ { endpoint: "memory-items:GET", scopes: ["settings.read"] },
352
+ { endpoint: "memory-items:POST", scopes: ["settings.write"] },
353
+ { endpoint: "memory-items:PATCH", scopes: ["settings.write"] },
354
+ { endpoint: "memory-items:DELETE", scopes: ["settings.write"] },
355
+
350
356
  // Trust rule CRUD management
351
357
  { endpoint: "trust-rules/manage:GET", scopes: ["settings.read"] },
352
358
  { endpoint: "trust-rules/manage:POST", scopes: ["settings.write"] },
@@ -378,9 +384,6 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
378
384
  // Delivery ack
379
385
  { endpoint: "channels/delivery-ack", scopes: ["internal.write"] },
380
386
 
381
- // MCP
382
- { endpoint: "mcp/reload", scopes: ["settings.write"] },
383
-
384
387
  // Migrations
385
388
  { endpoint: "migrations/validate", scopes: ["settings.write"] },
386
389
  { endpoint: "migrations/export", scopes: ["settings.write"] },
@@ -30,6 +30,7 @@ import {
30
30
  type CanonicalGuardianRequest,
31
31
  getCanonicalGuardianRequest,
32
32
  getCanonicalGuardianRequestByCode,
33
+ isRequestExpired,
33
34
  listCanonicalGuardianRequests,
34
35
  } from "../memory/canonical-guardian-store.js";
35
36
  import {
@@ -198,49 +199,50 @@ function findPendingCanonicalRequests(
198
199
  pendingRequestIds?: string[],
199
200
  conversationId?: string,
200
201
  ): CanonicalGuardianRequest[] {
202
+ let results: CanonicalGuardianRequest[];
203
+
201
204
  // When explicit IDs are provided, look them up directly
202
205
  if (pendingRequestIds) {
203
206
  if (pendingRequestIds.length === 0) {
204
207
  return [];
205
208
  }
206
- return pendingRequestIds
209
+ results = pendingRequestIds
207
210
  .map(getCanonicalGuardianRequest)
208
211
  .filter((r): r is CanonicalGuardianRequest => r?.status === "pending");
209
- }
210
-
211
- // Query by guardian identity when available
212
- if (actor.actorExternalUserId) {
213
- return listCanonicalGuardianRequests({
212
+ } else if (actor.actorExternalUserId) {
213
+ // Query by guardian identity when available
214
+ results = listCanonicalGuardianRequests({
214
215
  status: "pending",
215
216
  guardianExternalUserId: actor.actorExternalUserId,
216
217
  });
217
- }
218
-
219
- // Actors without an actorExternalUserId: scope by conversationId so the NL
220
- // path can discover pending requests bound to this conversation.
221
- // Include guardianPrincipalId filter when available so the guardian only
222
- // sees requests they are authorized to act on.
223
- if (conversationId) {
224
- return listCanonicalGuardianRequests({
218
+ } else if (conversationId) {
219
+ // Actors without an actorExternalUserId: scope by conversationId so the NL
220
+ // path can discover pending requests bound to this conversation.
221
+ // Include guardianPrincipalId filter when available so the guardian only
222
+ // sees requests they are authorized to act on.
223
+ results = listCanonicalGuardianRequests({
225
224
  status: "pending",
226
225
  conversationId,
227
226
  ...(actor.guardianPrincipalId
228
227
  ? { guardianPrincipalId: actor.guardianPrincipalId }
229
228
  : {}),
230
229
  });
231
- }
232
-
233
- // Actors with a guardianPrincipalId but no actorExternalUserId or
234
- // conversationId: query by principal so desktop sessions can still
235
- // discover pending guardian work via their bound principal.
236
- if (actor.guardianPrincipalId) {
237
- return listCanonicalGuardianRequests({
230
+ } else if (actor.guardianPrincipalId) {
231
+ // Actors with a guardianPrincipalId but no actorExternalUserId or
232
+ // conversationId: query by principal so desktop sessions can still
233
+ // discover pending guardian work via their bound principal.
234
+ results = listCanonicalGuardianRequests({
238
235
  status: "pending",
239
236
  guardianPrincipalId: actor.guardianPrincipalId,
240
237
  });
238
+ } else {
239
+ return [];
241
240
  }
242
241
 
243
- return [];
242
+ // Exclude requests that have passed their expiresAt deadline — they can
243
+ // no longer be resolved and should not trigger disambiguation or NL
244
+ // classification.
245
+ return results.filter((r) => !isRequestExpired(r));
244
246
  }
245
247
 
246
248
  /** Map an approval action string to the NL engine's allowed actions for guardians. */
@@ -135,7 +135,7 @@ import { telegramRouteDefinitions } from "./routes/integrations/telegram.js";
135
135
  import { twilioRouteDefinitions } from "./routes/integrations/twilio.js";
136
136
  import { inviteRouteDefinitions } from "./routes/invite-routes.js";
137
137
  import { logExportRouteDefinitions } from "./routes/log-export-routes.js";
138
- import { mcpRouteDefinitions } from "./routes/mcp-routes.js";
138
+ import { memoryItemRouteDefinitions } from "./routes/memory-item-routes.js";
139
139
  import { migrationRouteDefinitions } from "./routes/migration-routes.js";
140
140
  import type { PairingHandlerContext } from "./routes/pairing-routes.js";
141
141
  import {
@@ -723,9 +723,9 @@ export class RuntimeHttpServer {
723
723
  ...secretRouteDefinitions(),
724
724
  ...identityRouteDefinitions(),
725
725
  ...debugRouteDefinitions(),
726
- ...mcpRouteDefinitions(),
727
726
  ...usageRouteDefinitions(),
728
727
  ...workspaceRouteDefinitions(),
728
+ ...memoryItemRouteDefinitions(),
729
729
  ...settingsRouteDefinitions(),
730
730
  ...scheduleRouteDefinitions({
731
731
  sendMessageDeps: this.sendMessageDeps,
@@ -146,6 +146,12 @@ export function redeemInvite(params: {
146
146
  return { ok: false, reason: "invalid_token" };
147
147
  }
148
148
 
149
+ // Guardian channels must not be reactivated via regular invite redemption —
150
+ // their lifecycle is managed exclusively through the guardian binding flow.
151
+ if (existingContact && existingContact.role === "guardian") {
152
+ return { ok: false, reason: "invalid_token" };
153
+ }
154
+
149
155
  // Inactive member reactivation: when the user already has a member record
150
156
  // in a non-active state (revoked/pending), reactivate it via upsertContactChannel
151
157
  // and consume an invite use atomically. The fresh-member path below also
@@ -338,6 +344,7 @@ export function redeemVoiceInviteCode(params: {
338
344
  externalUserId: canonicalCallerId,
339
345
  });
340
346
  const existingVoiceChannel = voiceContactResult?.channel ?? null;
347
+ const voiceContact = voiceContactResult?.contact ?? null;
341
348
 
342
349
  if (existingVoiceChannel && existingVoiceChannel.status === "active") {
343
350
  return {
@@ -352,13 +359,18 @@ export function redeemVoiceInviteCode(params: {
352
359
  return { ok: false, reason: "invalid_or_expired" };
353
360
  }
354
361
 
362
+ // Guardian channels must not be reactivated via regular invite redemption —
363
+ // their lifecycle is managed exclusively through the guardian binding flow.
364
+ if (voiceContact && voiceContact.role === "guardian") {
365
+ return { ok: false, reason: "invalid_or_expired" };
366
+ }
367
+
355
368
  // Atomic redemption: upsert member + consume invite use in a transaction
356
369
  const STALE_INVITE = Symbol("stale_invite");
357
370
  let memberId: string | undefined;
358
371
 
359
372
  // Reactivation should not overwrite a guardian-managed nickname (same
360
373
  // protection as the token-based redemption path above).
361
- const voiceContact = voiceContactResult?.contact ?? null;
362
374
  const preservedDisplayName = voiceContact?.displayName?.trim().length
363
375
  ? voiceContact.displayName
364
376
  : (invite.friendName ?? undefined);
@@ -487,6 +499,12 @@ export function redeemInviteByCode(params: {
487
499
  return { ok: false, reason: "invalid_token" };
488
500
  }
489
501
 
502
+ // Guardian channels must not be reactivated via regular invite redemption —
503
+ // their lifecycle is managed exclusively through the guardian binding flow.
504
+ if (existingContact && existingContact.role === "guardian") {
505
+ return { ok: false, reason: "invalid_token" };
506
+ }
507
+
490
508
  // Inactive member reactivation: reactivate via upsertContactChannel and consume
491
509
  // an invite use atomically.
492
510
  if (existingChannel) {
@@ -8,6 +8,7 @@
8
8
  * /v1/contacts/channels endpoints.
9
9
  */
10
10
 
11
+ import { startInviteCall } from "../calls/call-domain.js";
11
12
  import { isChannelId } from "../channels/types.js";
12
13
  import {
13
14
  createInvite,
@@ -23,6 +24,7 @@ import {
23
24
  DEFAULT_USER_REFERENCE,
24
25
  resolveGuardianName,
25
26
  } from "../prompts/user-reference.js";
27
+ import { getLogger } from "../util/logger.js";
26
28
  import { isValidE164 } from "../util/phone.js";
27
29
  import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
28
30
  import {
@@ -37,6 +39,8 @@ import {
37
39
  type VoiceRedemptionOutcome,
38
40
  } from "./invite-redemption-service.js";
39
41
 
42
+ const log = getLogger("invite-service");
43
+
40
44
  // ---------------------------------------------------------------------------
41
45
  // Response shapes — used by both HTTP routes and message handlers
42
46
  // ---------------------------------------------------------------------------
@@ -250,6 +254,27 @@ export async function createIngressInvite(params: {
250
254
  });
251
255
  }
252
256
 
257
+ // For voice invites with a known phone number, initiate an outbound call
258
+ // so the contact is prompted to enter their code immediately.
259
+ if (
260
+ params.sourceChannel === "phone" &&
261
+ params.expectedExternalUserId &&
262
+ params.friendName &&
263
+ effectiveGuardianName
264
+ ) {
265
+ // Fire-and-forget: don't block invite creation on call initiation
266
+ startInviteCall({
267
+ phoneNumber: params.expectedExternalUserId,
268
+ friendName: params.friendName,
269
+ guardianName: effectiveGuardianName,
270
+ }).catch((err) => {
271
+ log.warn(
272
+ { err, inviteId: invite.id },
273
+ "Failed to initiate outbound invite call",
274
+ );
275
+ });
276
+ }
277
+
253
278
  // Voice invites must not expose the token — callers must redeem via the
254
279
  // identity-bound voice code flow, not the generic token redemption path.
255
280
  return {
@@ -6,8 +6,8 @@
6
6
  * host_bash_request, host_file_request, or host_cu_request, the onEvent
7
7
  * callback registers the interaction here. Standalone HTTP endpoints
8
8
  * (/v1/confirm, /v1/secret, /v1/trust-rules, /v1/host-bash-result,
9
- * /v1/host-file-result, /v1/host-cu-result) look up the session from
10
- * this tracker to resolve the interaction.
9
+ * /v1/host-file-result, /v1/host-cu-result) look up the session from this
10
+ * tracker to resolve the interaction.
11
11
  */
12
12
 
13
13
  import type { Session } from "../daemon/session.js";