@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
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Route handlers for memory item CRUD endpoints.
3
+ *
4
+ * GET /v1/memory-items — list memory items (with filtering, search, sort, pagination)
5
+ * GET /v1/memory-items/:id — get a single memory item
6
+ * POST /v1/memory-items — create a new memory item
7
+ * PATCH /v1/memory-items/:id — update an existing memory item
8
+ * DELETE /v1/memory-items/:id — delete a memory item and its embeddings
9
+ */
10
+
11
+ import { and, asc, count, desc, eq, like, ne, or } from "drizzle-orm";
12
+ import { v4 as uuid } from "uuid";
13
+
14
+ import { getDb } from "../../memory/db.js";
15
+ import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
16
+ import { enqueueMemoryJob } from "../../memory/jobs-store.js";
17
+ import { memoryEmbeddings, memoryItems } from "../../memory/schema.js";
18
+ import { truncate } from "../../util/truncate.js";
19
+ import { httpError } from "../http-errors.js";
20
+ import type { RouteContext, RouteDefinition } from "../http-router.js";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Constants
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const VALID_KINDS = [
27
+ "identity",
28
+ "preference",
29
+ "project",
30
+ "decision",
31
+ "constraint",
32
+ "event",
33
+ ] as const;
34
+
35
+ type MemoryItemKind = (typeof VALID_KINDS)[number];
36
+
37
+ const VALID_SORT_FIELDS = [
38
+ "lastSeenAt",
39
+ "importance",
40
+ "kind",
41
+ "firstSeenAt",
42
+ ] as const;
43
+
44
+ type SortField = (typeof VALID_SORT_FIELDS)[number];
45
+
46
+ const SORT_COLUMN_MAP = {
47
+ lastSeenAt: memoryItems.lastSeenAt,
48
+ importance: memoryItems.importance,
49
+ kind: memoryItems.kind,
50
+ firstSeenAt: memoryItems.firstSeenAt,
51
+ } as const;
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Helpers
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function isValidKind(value: string): value is MemoryItemKind {
58
+ return (VALID_KINDS as readonly string[]).includes(value);
59
+ }
60
+
61
+ function isValidSortField(value: string): value is SortField {
62
+ return (VALID_SORT_FIELDS as readonly string[]).includes(value);
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // GET /v1/memory-items
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export function handleListMemoryItems(url: URL): Response {
70
+ const kindParam = url.searchParams.get("kind");
71
+ const statusParam = url.searchParams.get("status") ?? "active";
72
+ const searchParam = url.searchParams.get("search");
73
+ const sortParam = url.searchParams.get("sort") ?? "lastSeenAt";
74
+ const orderParam = url.searchParams.get("order") ?? "desc";
75
+ const limitParam = Number(url.searchParams.get("limit") ?? 100);
76
+ const offsetParam = Number(url.searchParams.get("offset") ?? 0);
77
+
78
+ if (kindParam && !isValidKind(kindParam)) {
79
+ return httpError(
80
+ "BAD_REQUEST",
81
+ `Invalid kind "${kindParam}". Must be one of: ${VALID_KINDS.join(", ")}`,
82
+ 400,
83
+ );
84
+ }
85
+
86
+ if (!isValidSortField(sortParam)) {
87
+ return httpError(
88
+ "BAD_REQUEST",
89
+ `Invalid sort "${sortParam}". Must be one of: ${VALID_SORT_FIELDS.join(", ")}`,
90
+ 400,
91
+ );
92
+ }
93
+
94
+ if (orderParam !== "asc" && orderParam !== "desc") {
95
+ return httpError(
96
+ "BAD_REQUEST",
97
+ `Invalid order "${orderParam}". Must be "asc" or "desc"`,
98
+ 400,
99
+ );
100
+ }
101
+
102
+ const db = getDb();
103
+
104
+ // Build WHERE conditions
105
+ const conditions = [];
106
+ if (statusParam && statusParam !== "all") {
107
+ conditions.push(eq(memoryItems.status, statusParam));
108
+ }
109
+ if (kindParam) {
110
+ conditions.push(eq(memoryItems.kind, kindParam));
111
+ }
112
+ if (searchParam) {
113
+ conditions.push(
114
+ or(
115
+ like(memoryItems.subject, `%${searchParam}%`),
116
+ like(memoryItems.statement, `%${searchParam}%`),
117
+ )!,
118
+ );
119
+ }
120
+
121
+ const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
122
+
123
+ // Count query
124
+ const countResult = db
125
+ .select({ count: count() })
126
+ .from(memoryItems)
127
+ .where(whereClause)
128
+ .get();
129
+ const total = countResult?.count ?? 0;
130
+
131
+ // Data query
132
+ const sortColumn = SORT_COLUMN_MAP[sortParam];
133
+ const orderFn = orderParam === "asc" ? asc : desc;
134
+
135
+ const items = db
136
+ .select()
137
+ .from(memoryItems)
138
+ .where(whereClause)
139
+ .orderBy(orderFn(sortColumn))
140
+ .limit(limitParam)
141
+ .offset(offsetParam)
142
+ .all();
143
+
144
+ return Response.json({ items, total });
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // GET /v1/memory-items/:id
149
+ // ---------------------------------------------------------------------------
150
+
151
+ export function handleGetMemoryItem(ctx: RouteContext): Response {
152
+ const { id } = ctx.params;
153
+ const db = getDb();
154
+
155
+ const item = db
156
+ .select()
157
+ .from(memoryItems)
158
+ .where(eq(memoryItems.id, id))
159
+ .get();
160
+
161
+ if (!item) {
162
+ return httpError("NOT_FOUND", "Memory item not found", 404);
163
+ }
164
+
165
+ let supersedesSubject: string | undefined;
166
+ let supersededBySubject: string | undefined;
167
+
168
+ if (item.supersedes) {
169
+ const superseded = db
170
+ .select({ subject: memoryItems.subject })
171
+ .from(memoryItems)
172
+ .where(eq(memoryItems.id, item.supersedes))
173
+ .get();
174
+ supersedesSubject = superseded?.subject;
175
+ }
176
+
177
+ if (item.supersededBy) {
178
+ const superseding = db
179
+ .select({ subject: memoryItems.subject })
180
+ .from(memoryItems)
181
+ .where(eq(memoryItems.id, item.supersededBy))
182
+ .get();
183
+ supersededBySubject = superseding?.subject;
184
+ }
185
+
186
+ return Response.json({
187
+ item: {
188
+ ...item,
189
+ ...(supersedesSubject !== undefined ? { supersedesSubject } : {}),
190
+ ...(supersededBySubject !== undefined ? { supersededBySubject } : {}),
191
+ },
192
+ });
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // POST /v1/memory-items
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export async function handleCreateMemoryItem(
200
+ ctx: RouteContext,
201
+ ): Promise<Response> {
202
+ const body = (await ctx.req.json()) as {
203
+ kind?: string;
204
+ subject?: string;
205
+ statement?: string;
206
+ importance?: number;
207
+ };
208
+
209
+ const { kind, subject, statement, importance } = body;
210
+
211
+ // Validate kind
212
+ if (typeof kind !== "string" || !isValidKind(kind)) {
213
+ return httpError(
214
+ "BAD_REQUEST",
215
+ `kind is required and must be one of: ${VALID_KINDS.join(", ")}`,
216
+ 400,
217
+ );
218
+ }
219
+
220
+ // Validate subject
221
+ if (typeof subject !== "string" || subject.trim().length === 0) {
222
+ return httpError(
223
+ "BAD_REQUEST",
224
+ "subject is required and must be a non-empty string",
225
+ 400,
226
+ );
227
+ }
228
+
229
+ // Validate statement
230
+ if (typeof statement !== "string" || statement.trim().length === 0) {
231
+ return httpError(
232
+ "BAD_REQUEST",
233
+ "statement is required and must be a non-empty string",
234
+ 400,
235
+ );
236
+ }
237
+
238
+ const trimmedSubject = truncate(subject.trim(), 80, "");
239
+ const trimmedStatement = truncate(statement.trim(), 500, "");
240
+
241
+ const scopeId = "default";
242
+ const fingerprint = computeMemoryFingerprint(
243
+ scopeId,
244
+ kind,
245
+ trimmedSubject,
246
+ trimmedStatement,
247
+ );
248
+
249
+ const db = getDb();
250
+
251
+ // Check for existing item with same fingerprint + scopeId
252
+ const existing = db
253
+ .select()
254
+ .from(memoryItems)
255
+ .where(
256
+ and(
257
+ eq(memoryItems.fingerprint, fingerprint),
258
+ eq(memoryItems.scopeId, scopeId),
259
+ ),
260
+ )
261
+ .get();
262
+
263
+ if (existing) {
264
+ return httpError(
265
+ "CONFLICT",
266
+ "A memory with this content already exists",
267
+ 409,
268
+ );
269
+ }
270
+
271
+ const id = uuid();
272
+ const now = Date.now();
273
+
274
+ db.insert(memoryItems)
275
+ .values({
276
+ id,
277
+ kind,
278
+ subject: trimmedSubject,
279
+ statement: trimmedStatement,
280
+ status: "active",
281
+ confidence: 0.95,
282
+ importance: importance ?? 0.8,
283
+ fingerprint,
284
+ verificationState: "user_confirmed",
285
+ scopeId,
286
+ firstSeenAt: now,
287
+ lastSeenAt: now,
288
+ lastUsedAt: null,
289
+ overrideConfidence: "explicit",
290
+ })
291
+ .run();
292
+
293
+ enqueueMemoryJob("embed_item", { itemId: id });
294
+
295
+ // Fetch the inserted row to return it
296
+ const insertedRow = db
297
+ .select()
298
+ .from(memoryItems)
299
+ .where(eq(memoryItems.id, id))
300
+ .get();
301
+
302
+ return Response.json({ item: insertedRow }, { status: 201 });
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // PATCH /v1/memory-items/:id
307
+ // ---------------------------------------------------------------------------
308
+
309
+ export async function handleUpdateMemoryItem(
310
+ ctx: RouteContext,
311
+ ): Promise<Response> {
312
+ const { id } = ctx.params;
313
+ const body = (await ctx.req.json()) as {
314
+ subject?: string;
315
+ statement?: string;
316
+ kind?: string;
317
+ status?: string;
318
+ importance?: number;
319
+ verificationState?: string;
320
+ };
321
+
322
+ const db = getDb();
323
+
324
+ const existing = db
325
+ .select()
326
+ .from(memoryItems)
327
+ .where(eq(memoryItems.id, id))
328
+ .get();
329
+
330
+ if (!existing) {
331
+ return httpError("NOT_FOUND", "Memory item not found", 404);
332
+ }
333
+
334
+ // Build the update set with only provided fields
335
+ const set: Record<string, unknown> = {
336
+ lastSeenAt: Date.now(),
337
+ };
338
+
339
+ if (body.subject !== undefined) {
340
+ if (typeof body.subject !== "string") {
341
+ return httpError("BAD_REQUEST", "subject must be a string", 400);
342
+ }
343
+ set.subject = truncate(body.subject.trim(), 80, "");
344
+ }
345
+ if (body.statement !== undefined) {
346
+ if (typeof body.statement !== "string") {
347
+ return httpError("BAD_REQUEST", "statement must be a string", 400);
348
+ }
349
+ set.statement = truncate(body.statement.trim(), 500, "");
350
+ }
351
+ if (body.kind !== undefined) {
352
+ if (!isValidKind(body.kind)) {
353
+ return httpError(
354
+ "BAD_REQUEST",
355
+ `Invalid kind "${body.kind}". Must be one of: ${VALID_KINDS.join(", ")}`,
356
+ 400,
357
+ );
358
+ }
359
+ set.kind = body.kind;
360
+ }
361
+ if (body.status !== undefined) {
362
+ set.status = body.status;
363
+ }
364
+ if (body.importance !== undefined) {
365
+ set.importance = body.importance;
366
+ }
367
+ if (body.verificationState !== undefined) {
368
+ set.verificationState = body.verificationState;
369
+ }
370
+
371
+ // If subject, statement, or kind changed, recompute fingerprint
372
+ const contentChanged =
373
+ body.subject !== undefined ||
374
+ body.statement !== undefined ||
375
+ body.kind !== undefined;
376
+
377
+ if (contentChanged) {
378
+ const newSubject = (set.subject as string | undefined) ?? existing.subject;
379
+ const newStatement =
380
+ (set.statement as string | undefined) ?? existing.statement;
381
+ const newKind = (set.kind as string | undefined) ?? existing.kind;
382
+ const scopeId = existing.scopeId;
383
+
384
+ const fingerprint = computeMemoryFingerprint(
385
+ scopeId,
386
+ newKind,
387
+ newSubject,
388
+ newStatement,
389
+ );
390
+
391
+ // Check for collision (exclude self)
392
+ const collision = db
393
+ .select({ id: memoryItems.id })
394
+ .from(memoryItems)
395
+ .where(
396
+ and(
397
+ eq(memoryItems.fingerprint, fingerprint),
398
+ eq(memoryItems.scopeId, scopeId),
399
+ ne(memoryItems.id, id),
400
+ ),
401
+ )
402
+ .get();
403
+
404
+ if (collision) {
405
+ return httpError(
406
+ "CONFLICT",
407
+ "Another memory item with this content already exists",
408
+ 409,
409
+ );
410
+ }
411
+
412
+ set.fingerprint = fingerprint;
413
+ }
414
+
415
+ db.update(memoryItems).set(set).where(eq(memoryItems.id, id)).run();
416
+
417
+ // If statement changed, enqueue embed job
418
+ if (body.statement !== undefined) {
419
+ enqueueMemoryJob("embed_item", { itemId: id });
420
+ }
421
+
422
+ // Fetch and return the updated row
423
+ const updatedRow = db
424
+ .select()
425
+ .from(memoryItems)
426
+ .where(eq(memoryItems.id, id))
427
+ .get();
428
+
429
+ return Response.json({ item: updatedRow });
430
+ }
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // DELETE /v1/memory-items/:id
434
+ // ---------------------------------------------------------------------------
435
+
436
+ export async function handleDeleteMemoryItem(
437
+ ctx: RouteContext,
438
+ ): Promise<Response> {
439
+ const { id } = ctx.params;
440
+ const db = getDb();
441
+
442
+ const existing = db
443
+ .select()
444
+ .from(memoryItems)
445
+ .where(eq(memoryItems.id, id))
446
+ .get();
447
+
448
+ if (!existing) {
449
+ return httpError("NOT_FOUND", "Memory item not found", 404);
450
+ }
451
+
452
+ // Delete embeddings for this item
453
+ db.delete(memoryEmbeddings)
454
+ .where(
455
+ and(
456
+ eq(memoryEmbeddings.targetType, "item"),
457
+ eq(memoryEmbeddings.targetId, id),
458
+ ),
459
+ )
460
+ .run();
461
+
462
+ // Delete the item (cascades memoryItemSources)
463
+ db.delete(memoryItems).where(eq(memoryItems.id, id)).run();
464
+
465
+ return new Response(null, { status: 204 });
466
+ }
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // Route definitions
470
+ // ---------------------------------------------------------------------------
471
+
472
+ export function memoryItemRouteDefinitions(): RouteDefinition[] {
473
+ return [
474
+ {
475
+ endpoint: "memory-items",
476
+ method: "GET",
477
+ handler: (ctx) => handleListMemoryItems(ctx.url),
478
+ },
479
+ {
480
+ endpoint: "memory-items/:id",
481
+ method: "GET",
482
+ policyKey: "memory-items",
483
+ handler: (ctx) => handleGetMemoryItem(ctx),
484
+ },
485
+ {
486
+ endpoint: "memory-items",
487
+ method: "POST",
488
+ handler: (ctx) => handleCreateMemoryItem(ctx),
489
+ },
490
+ {
491
+ endpoint: "memory-items/:id",
492
+ method: "PATCH",
493
+ policyKey: "memory-items",
494
+ handler: (ctx) => handleUpdateMemoryItem(ctx),
495
+ },
496
+ {
497
+ endpoint: "memory-items/:id",
498
+ method: "DELETE",
499
+ policyKey: "memory-items",
500
+ handler: (ctx) => handleDeleteMemoryItem(ctx),
501
+ },
502
+ ];
503
+ }
@@ -29,7 +29,7 @@ export interface SessionManagementDeps {
29
29
  renameSession: (sessionId: string, name: string) => boolean;
30
30
  clearAllSessions: () => number;
31
31
  cancelGeneration: (sessionId: string) => boolean;
32
- undoLastMessage: (sessionId: string) => { removedCount: number } | null;
32
+ undoLastMessage: (sessionId: string) => Promise<{ removedCount: number } | null>;
33
33
  regenerateResponse: (
34
34
  sessionId: string,
35
35
  ) => Promise<{ requestId: string } | null>;
@@ -115,8 +115,8 @@ export function sessionManagementRouteDefinitions(
115
115
  endpoint: "conversations/:id/undo",
116
116
  method: "POST",
117
117
  policyKey: "conversations/undo",
118
- handler: ({ params }) => {
119
- const result = deps.undoLastMessage(params.id);
118
+ handler: async ({ params }) => {
119
+ const result = await deps.undoLastMessage(params.id);
120
120
  if (!result) {
121
121
  return httpError(
122
122
  "NOT_FOUND",
@@ -160,7 +160,7 @@ async function handleOAuthConnectStart(body: {
160
160
  const app = getApp(conn.oauthAppId);
161
161
  if (app) {
162
162
  clientId = app.clientId;
163
- clientSecret = getSecureKey(`oauth_app/${app.id}/client_secret`);
163
+ clientSecret = getSecureKey(app.clientSecretCredentialPath);
164
164
  }
165
165
  }
166
166
 
@@ -170,7 +170,7 @@ async function handleOAuthConnectStart(body: {
170
170
  if (dbApp) {
171
171
  clientId = dbApp.clientId;
172
172
  if (!clientSecret) {
173
- clientSecret = getSecureKey(`oauth_app/${dbApp.id}/client_secret`);
173
+ clientSecret = getSecureKey(dbApp.clientSecretCredentialPath);
174
174
  }
175
175
  }
176
176
  }
@@ -48,6 +48,13 @@ export async function handleAddTrustRuleManage(
48
48
  if (!toolName || typeof toolName !== "string") {
49
49
  return httpError("BAD_REQUEST", "toolName is required", 400);
50
50
  }
51
+ if (toolName.startsWith("__internal:")) {
52
+ return httpError(
53
+ "BAD_REQUEST",
54
+ "toolName must not start with __internal:",
55
+ 400,
56
+ );
57
+ }
51
58
  if (!pattern || typeof pattern !== "string") {
52
59
  return httpError("BAD_REQUEST", "pattern is required", 400);
53
60
  }
@@ -124,6 +131,13 @@ export async function handleUpdateTrustRuleManage(
124
131
  priority?: number;
125
132
  };
126
133
 
134
+ if (typeof body.tool === "string" && body.tool.startsWith("__internal:")) {
135
+ return httpError(
136
+ "BAD_REQUEST",
137
+ "tool must not start with __internal:",
138
+ 400,
139
+ );
140
+ }
127
141
  if (body.decision !== undefined) {
128
142
  const validDecisions = ["allow", "deny", "ask"] as const;
129
143
  if (
@@ -33,6 +33,7 @@ interface TreeEntry {
33
33
 
34
34
  function handleWorkspaceTree(ctx: RouteContext): Response {
35
35
  const requestedPath = ctx.url.searchParams.get("path") ?? "";
36
+ const showHidden = ctx.url.searchParams.get("showHidden") === "true";
36
37
  const resolved = resolveWorkspacePath(requestedPath);
37
38
  if (resolved === undefined) {
38
39
  return httpError("BAD_REQUEST", "Invalid path", 400);
@@ -45,7 +46,7 @@ function handleWorkspaceTree(ctx: RouteContext): Response {
45
46
  const entries: TreeEntry[] = [];
46
47
  for (const entry of dirents) {
47
48
  // Filter out dotfiles/directories (.env, .git, .private, etc.)
48
- if (entry.name.startsWith(".")) continue;
49
+ if (!showHidden && entry.name.startsWith(".")) continue;
49
50
 
50
51
  const fullPath = join(resolved, entry.name);
51
52
 
@@ -33,11 +33,18 @@ const REQUEST_TIMEOUT_MS = 5_000;
33
33
  * back); `{ found: false }` means the key doesn't exist in the keychain. */
34
34
  export type BrokerGetResult = { found: boolean; value?: string } | null;
35
35
 
36
+ /** Result of a `set()` call — distinguishes broker-unreachable from an active
37
+ * rejection so callers can log meaningful diagnostics. */
38
+ export type BrokerSetResult =
39
+ | { status: "ok" }
40
+ | { status: "unreachable" }
41
+ | { status: "rejected"; code: string; message: string };
42
+
36
43
  export interface KeychainBrokerClient {
37
44
  isAvailable(): boolean;
38
45
  ping(): Promise<{ pong: boolean } | null>;
39
46
  get(account: string): Promise<BrokerGetResult>;
40
- set(account: string, value: string): Promise<boolean>;
47
+ set(account: string, value: string): Promise<BrokerSetResult>;
41
48
  del(account: string): Promise<boolean>;
42
49
  list(): Promise<string[]>;
43
50
  }
@@ -360,12 +367,18 @@ export function createBrokerClient(): KeychainBrokerClient {
360
367
  }
361
368
  },
362
369
 
363
- async set(account: string, value: string): Promise<boolean> {
370
+ async set(account: string, value: string): Promise<BrokerSetResult> {
364
371
  try {
365
372
  const response = await doRequest("key.set", { account, value });
366
- return response?.ok === true;
373
+ if (!response) return { status: "unreachable" };
374
+ if (response.ok) return { status: "ok" };
375
+ return {
376
+ status: "rejected",
377
+ code: response.error?.code ?? "UNKNOWN",
378
+ message: response.error?.message ?? "unknown error",
379
+ };
367
380
  } catch {
368
- return false;
381
+ return { status: "unreachable" };
369
382
  }
370
383
  },
371
384
 
@@ -7,10 +7,13 @@
7
7
  * encrypted store (startup code paths cannot do async I/O).
8
8
  */
9
9
 
10
+ import { getLogger } from "../util/logger.js";
10
11
  import * as encryptedStore from "./encrypted-store.js";
11
12
  import type { KeychainBrokerClient } from "./keychain-broker-client.js";
12
13
  import { createBrokerClient } from "./keychain-broker-client.js";
13
14
 
15
+ const log = getLogger("secure-keys");
16
+
14
17
  let _broker: KeychainBrokerClient | undefined;
15
18
 
16
19
  function getBroker(): KeychainBrokerClient {
@@ -120,13 +123,32 @@ export async function setSecureKeyAsync(
120
123
  ): Promise<boolean> {
121
124
  const broker = getBroker();
122
125
  if (broker.isAvailable()) {
123
- const brokerOk = await broker.set(account, value);
124
- if (!brokerOk) return false;
126
+ const result = await broker.set(account, value);
127
+ if (result.status !== "ok") {
128
+ log.warn(
129
+ {
130
+ account,
131
+ brokerStatus: result.status,
132
+ ...(result.status === "rejected"
133
+ ? { brokerCode: result.code, brokerMessage: result.message }
134
+ : {}),
135
+ },
136
+ "Broker set failed for secure key",
137
+ );
138
+ return false;
139
+ }
125
140
  // Broker succeeded — also persist to encrypted store for sync callers.
126
141
  const encOk = encryptedStore.setKey(account, value);
142
+ if (!encOk) {
143
+ log.warn({ account }, "Encrypted store set failed after broker success");
144
+ }
127
145
  return encOk;
128
146
  }
129
- return encryptedStore.setKey(account, value);
147
+ const encOk = encryptedStore.setKey(account, value);
148
+ if (!encOk) {
149
+ log.warn({ account }, "Encrypted store set failed (broker unavailable)");
150
+ }
151
+ return encOk;
130
152
  }
131
153
 
132
154
  /**