@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.
- package/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -34,6 +34,8 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
34
34
|
deleteSecureKeyAsync: mockDeleteSecureKeyAsync,
|
|
35
35
|
setSecureKeyAsync: mockSetSecureKeyAsync,
|
|
36
36
|
getSecureKey: (account: string) => secureKeyValues.get(account),
|
|
37
|
+
getSecureKeyAsync: (account: string) =>
|
|
38
|
+
Promise.resolve(secureKeyValues.get(account)),
|
|
37
39
|
}));
|
|
38
40
|
|
|
39
41
|
import { initializeDb, resetDb, resetTestTables } from "../memory/db.js";
|
|
@@ -179,6 +181,35 @@ describe("provider operations", () => {
|
|
|
179
181
|
// createdAt should be preserved from the original insert
|
|
180
182
|
expect(row!.createdAt).toBe(originalCreatedAt);
|
|
181
183
|
});
|
|
184
|
+
|
|
185
|
+
test("persists pingUrl when provided", () => {
|
|
186
|
+
seedProviders([
|
|
187
|
+
{
|
|
188
|
+
providerKey: "github",
|
|
189
|
+
authUrl: "https://github.com/authorize",
|
|
190
|
+
tokenUrl: "https://github.com/token",
|
|
191
|
+
defaultScopes: ["repo"],
|
|
192
|
+
scopePolicy: {},
|
|
193
|
+
pingUrl: "https://api.github.com/user",
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
const row = getProvider("github");
|
|
197
|
+
expect(row!.pingUrl).toBe("https://api.github.com/user");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("pingUrl defaults to null when omitted", () => {
|
|
201
|
+
seedProviders([
|
|
202
|
+
{
|
|
203
|
+
providerKey: "github",
|
|
204
|
+
authUrl: "https://github.com/authorize",
|
|
205
|
+
tokenUrl: "https://github.com/token",
|
|
206
|
+
defaultScopes: ["repo"],
|
|
207
|
+
scopePolicy: {},
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
const row = getProvider("github");
|
|
211
|
+
expect(row!.pingUrl).toBeNull();
|
|
212
|
+
});
|
|
182
213
|
});
|
|
183
214
|
|
|
184
215
|
describe("getProvider", () => {
|
|
@@ -279,13 +310,18 @@ describe("app operations", () => {
|
|
|
279
310
|
|
|
280
311
|
test("stores clientSecret in secure storage on new app creation", async () => {
|
|
281
312
|
seedTestProvider("github");
|
|
282
|
-
const app = await upsertApp("github", "client-abc",
|
|
313
|
+
const app = await upsertApp("github", "client-abc", {
|
|
314
|
+
clientSecretValue: "my-secret",
|
|
315
|
+
});
|
|
283
316
|
|
|
284
317
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
|
|
285
318
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
|
|
286
319
|
`oauth_app/${app.id}/client_secret`,
|
|
287
320
|
"my-secret",
|
|
288
321
|
);
|
|
322
|
+
expect(app.clientSecretCredentialPath).toBe(
|
|
323
|
+
`oauth_app/${app.id}/client_secret`,
|
|
324
|
+
);
|
|
289
325
|
});
|
|
290
326
|
|
|
291
327
|
test("stores clientSecret in secure storage when upserting an existing app", async () => {
|
|
@@ -293,11 +329,13 @@ describe("app operations", () => {
|
|
|
293
329
|
const first = await upsertApp("github", "client-abc");
|
|
294
330
|
mockSetSecureKeyAsync.mockClear();
|
|
295
331
|
|
|
296
|
-
await upsertApp("github", "client-abc",
|
|
332
|
+
await upsertApp("github", "client-abc", {
|
|
333
|
+
clientSecretValue: "updated-secret",
|
|
334
|
+
});
|
|
297
335
|
|
|
298
336
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
|
|
299
337
|
expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
|
|
300
|
-
|
|
338
|
+
first.clientSecretCredentialPath,
|
|
301
339
|
"updated-secret",
|
|
302
340
|
);
|
|
303
341
|
});
|
|
@@ -307,9 +345,70 @@ describe("app operations", () => {
|
|
|
307
345
|
mockSetSecureKeyAsync.mockResolvedValueOnce(false);
|
|
308
346
|
|
|
309
347
|
await expect(
|
|
310
|
-
upsertApp("github", "client-abc", "bad-secret"),
|
|
348
|
+
upsertApp("github", "client-abc", { clientSecretValue: "bad-secret" }),
|
|
311
349
|
).rejects.toThrow("Failed to store client_secret in secure storage");
|
|
312
350
|
});
|
|
351
|
+
|
|
352
|
+
test("accepts clientSecretCredentialPath and verifies existence", async () => {
|
|
353
|
+
seedTestProvider("github");
|
|
354
|
+
secureKeyValues.set("custom/path", "stored-secret");
|
|
355
|
+
|
|
356
|
+
const app = await upsertApp("github", "client-abc", {
|
|
357
|
+
clientSecretCredentialPath: "custom/path",
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(app.clientSecretCredentialPath).toBe("custom/path");
|
|
361
|
+
// Should not have called setSecureKeyAsync since we only provided a path
|
|
362
|
+
expect(mockSetSecureKeyAsync).not.toHaveBeenCalled();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("throws when clientSecretCredentialPath points to nonexistent secret", async () => {
|
|
366
|
+
seedTestProvider("github");
|
|
367
|
+
|
|
368
|
+
await expect(
|
|
369
|
+
upsertApp("github", "client-abc", {
|
|
370
|
+
clientSecretCredentialPath: "nonexistent/path",
|
|
371
|
+
}),
|
|
372
|
+
).rejects.toThrow("No secret found at credential path: nonexistent/path");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("throws when both clientSecretValue and clientSecretCredentialPath are provided", async () => {
|
|
376
|
+
seedTestProvider("github");
|
|
377
|
+
|
|
378
|
+
await expect(
|
|
379
|
+
upsertApp("github", "client-abc", {
|
|
380
|
+
clientSecretValue: "my-secret",
|
|
381
|
+
clientSecretCredentialPath: "custom/path",
|
|
382
|
+
}),
|
|
383
|
+
).rejects.toThrow(
|
|
384
|
+
"Cannot provide both clientSecretValue and clientSecretCredentialPath",
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("records default clientSecretCredentialPath when neither value nor path is provided", async () => {
|
|
389
|
+
seedTestProvider("github");
|
|
390
|
+
const app = await upsertApp("github", "client-abc");
|
|
391
|
+
|
|
392
|
+
expect(app.clientSecretCredentialPath).toBe(
|
|
393
|
+
`oauth_app/${app.id}/client_secret`,
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("updates clientSecretCredentialPath on existing row when path is provided", async () => {
|
|
398
|
+
seedTestProvider("github");
|
|
399
|
+
const first = await upsertApp("github", "client-abc");
|
|
400
|
+
expect(first.clientSecretCredentialPath).toBe(
|
|
401
|
+
`oauth_app/${first.id}/client_secret`,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
secureKeyValues.set("new/custom/path", "stored-secret");
|
|
405
|
+
const updated = await upsertApp("github", "client-abc", {
|
|
406
|
+
clientSecretCredentialPath: "new/custom/path",
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
expect(updated.id).toBe(first.id);
|
|
410
|
+
expect(updated.clientSecretCredentialPath).toBe("new/custom/path");
|
|
411
|
+
});
|
|
313
412
|
});
|
|
314
413
|
|
|
315
414
|
describe("getApp", () => {
|
|
@@ -353,14 +452,29 @@ describe("app operations", () => {
|
|
|
353
452
|
expect(getApp(app.id)).toBeUndefined();
|
|
354
453
|
});
|
|
355
454
|
|
|
356
|
-
test("cleans up client_secret from secure storage", async () => {
|
|
455
|
+
test("cleans up client_secret from secure storage using stored path", async () => {
|
|
357
456
|
const app = await createTestApp("github", "client-1");
|
|
358
457
|
mockDeleteSecureKeyAsync.mockClear();
|
|
359
458
|
|
|
360
459
|
await deleteApp(app.id);
|
|
361
460
|
|
|
362
461
|
expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
|
|
363
|
-
|
|
462
|
+
app.clientSecretCredentialPath,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("uses custom clientSecretCredentialPath when deleting", async () => {
|
|
467
|
+
seedTestProvider("github");
|
|
468
|
+
secureKeyValues.set("custom/secret/path", "the-secret");
|
|
469
|
+
const app = await upsertApp("github", "client-1", {
|
|
470
|
+
clientSecretCredentialPath: "custom/secret/path",
|
|
471
|
+
});
|
|
472
|
+
mockDeleteSecureKeyAsync.mockClear();
|
|
473
|
+
|
|
474
|
+
await deleteApp(app.id);
|
|
475
|
+
|
|
476
|
+
expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
|
|
477
|
+
"custom/secret/path",
|
|
364
478
|
);
|
|
365
479
|
});
|
|
366
480
|
|
|
@@ -21,12 +21,14 @@ interface MockCallLog {
|
|
|
21
21
|
|
|
22
22
|
let mockCollectionExists: boolean;
|
|
23
23
|
let mockCollectionSize: number;
|
|
24
|
+
let mockUseNamedVectors: boolean;
|
|
24
25
|
let mockSentinelPayload: Record<string, unknown> | null;
|
|
25
26
|
let callLog: MockCallLog;
|
|
26
27
|
|
|
27
28
|
function resetMockState() {
|
|
28
29
|
mockCollectionExists = false;
|
|
29
30
|
mockCollectionSize = 384;
|
|
31
|
+
mockUseNamedVectors = false;
|
|
30
32
|
mockSentinelPayload = null;
|
|
31
33
|
callLog = {
|
|
32
34
|
collectionExists: 0,
|
|
@@ -51,7 +53,9 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
51
53
|
return {
|
|
52
54
|
config: {
|
|
53
55
|
params: {
|
|
54
|
-
vectors:
|
|
56
|
+
vectors: mockUseNamedVectors
|
|
57
|
+
? { dense: { size: mockCollectionSize } }
|
|
58
|
+
: { size: mockCollectionSize },
|
|
55
59
|
},
|
|
56
60
|
},
|
|
57
61
|
};
|
|
@@ -77,7 +81,12 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
77
81
|
mockSentinelPayload &&
|
|
78
82
|
opts.ids.includes("00000000-0000-0000-0000-000000000000")
|
|
79
83
|
) {
|
|
80
|
-
return [
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
id: "00000000-0000-0000-0000-000000000000",
|
|
87
|
+
payload: mockSentinelPayload,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
81
90
|
}
|
|
82
91
|
return [];
|
|
83
92
|
}
|
|
@@ -97,6 +106,7 @@ beforeEach(() => {
|
|
|
97
106
|
describe("Qdrant collection migration", () => {
|
|
98
107
|
test("deletes and recreates collection on dimension mismatch", async () => {
|
|
99
108
|
mockCollectionExists = true;
|
|
109
|
+
mockUseNamedVectors = true;
|
|
100
110
|
mockCollectionSize = 384; // Current collection has 384-dim vectors
|
|
101
111
|
|
|
102
112
|
const client = new VellumQdrantClient({
|
|
@@ -108,14 +118,16 @@ describe("Qdrant collection migration", () => {
|
|
|
108
118
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
109
119
|
});
|
|
110
120
|
|
|
111
|
-
await client.ensureCollection();
|
|
121
|
+
const result = await client.ensureCollection();
|
|
112
122
|
|
|
113
123
|
expect(callLog.deleteCollection).toBe(1);
|
|
114
124
|
expect(callLog.createCollection).toBe(1);
|
|
125
|
+
expect(result.migrated).toBe(true);
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
test("deletes and recreates collection on model-only mismatch", async () => {
|
|
118
129
|
mockCollectionExists = true;
|
|
130
|
+
mockUseNamedVectors = true;
|
|
119
131
|
mockCollectionSize = 768; // Same dimension
|
|
120
132
|
mockSentinelPayload = {
|
|
121
133
|
_meta: true,
|
|
@@ -131,16 +143,18 @@ describe("Qdrant collection migration", () => {
|
|
|
131
143
|
embeddingModel: "gemini:gemini-embedding-2-preview", // New model
|
|
132
144
|
});
|
|
133
145
|
|
|
134
|
-
await client.ensureCollection();
|
|
146
|
+
const result = await client.ensureCollection();
|
|
135
147
|
|
|
136
148
|
expect(callLog.deleteCollection).toBe(1);
|
|
137
149
|
expect(callLog.createCollection).toBe(1);
|
|
138
150
|
// Sentinel should be written for the new model
|
|
139
151
|
expect(callLog.upsert).toBe(1);
|
|
152
|
+
expect(result.migrated).toBe(true);
|
|
140
153
|
});
|
|
141
154
|
|
|
142
155
|
test("leaves collection untouched when dimensions and model match", async () => {
|
|
143
156
|
mockCollectionExists = true;
|
|
157
|
+
mockUseNamedVectors = true;
|
|
144
158
|
mockCollectionSize = 768;
|
|
145
159
|
mockSentinelPayload = {
|
|
146
160
|
_meta: true,
|
|
@@ -156,14 +170,16 @@ describe("Qdrant collection migration", () => {
|
|
|
156
170
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
157
171
|
});
|
|
158
172
|
|
|
159
|
-
await client.ensureCollection();
|
|
173
|
+
const result = await client.ensureCollection();
|
|
160
174
|
|
|
161
175
|
expect(callLog.deleteCollection).toBe(0);
|
|
162
176
|
expect(callLog.createCollection).toBe(0);
|
|
177
|
+
expect(result.migrated).toBe(false);
|
|
163
178
|
});
|
|
164
179
|
|
|
165
180
|
test("does not rebuild pre-existing collection without sentinel (graceful upgrade)", async () => {
|
|
166
181
|
mockCollectionExists = true;
|
|
182
|
+
mockUseNamedVectors = true;
|
|
167
183
|
mockCollectionSize = 768;
|
|
168
184
|
mockSentinelPayload = null; // No sentinel — pre-existing collection
|
|
169
185
|
|
|
@@ -176,11 +192,12 @@ describe("Qdrant collection migration", () => {
|
|
|
176
192
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
177
193
|
});
|
|
178
194
|
|
|
179
|
-
await client.ensureCollection();
|
|
195
|
+
const result = await client.ensureCollection();
|
|
180
196
|
|
|
181
197
|
// No sentinel found → no model mismatch → collection kept
|
|
182
198
|
expect(callLog.deleteCollection).toBe(0);
|
|
183
199
|
expect(callLog.createCollection).toBe(0);
|
|
200
|
+
expect(result.migrated).toBe(false);
|
|
184
201
|
});
|
|
185
202
|
|
|
186
203
|
test("writes sentinel point when creating a new collection", async () => {
|
|
@@ -195,11 +212,37 @@ describe("Qdrant collection migration", () => {
|
|
|
195
212
|
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
196
213
|
});
|
|
197
214
|
|
|
198
|
-
await client.ensureCollection();
|
|
215
|
+
const result = await client.ensureCollection();
|
|
199
216
|
|
|
200
217
|
expect(callLog.createCollection).toBe(1);
|
|
201
218
|
// Sentinel upsert should be called
|
|
202
219
|
expect(callLog.upsert).toBe(1);
|
|
220
|
+
// Fresh collection, not a migration
|
|
221
|
+
expect(result.migrated).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("deletes and recreates collection when migrating from unnamed to named vectors", async () => {
|
|
225
|
+
mockCollectionExists = true;
|
|
226
|
+
mockUseNamedVectors = false; // Legacy unnamed vectors
|
|
227
|
+
mockCollectionSize = 768;
|
|
228
|
+
|
|
229
|
+
const client = new VellumQdrantClient({
|
|
230
|
+
url: "http://localhost:6333",
|
|
231
|
+
collection: "memory",
|
|
232
|
+
vectorSize: 768, // Same dimension
|
|
233
|
+
onDisk: false,
|
|
234
|
+
quantization: "none",
|
|
235
|
+
embeddingModel: "gemini:gemini-embedding-2-preview",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = await client.ensureCollection();
|
|
239
|
+
|
|
240
|
+
// Unnamed vectors should trigger delete + recreate with named vectors
|
|
241
|
+
expect(callLog.deleteCollection).toBe(1);
|
|
242
|
+
expect(callLog.createCollection).toBe(1);
|
|
243
|
+
// Sentinel should be written for the new collection
|
|
244
|
+
expect(callLog.upsert).toBe(1);
|
|
245
|
+
expect(result.migrated).toBe(true);
|
|
203
246
|
});
|
|
204
247
|
|
|
205
248
|
test("does not write sentinel when embeddingModel is not provided", async () => {
|
|
@@ -214,10 +257,12 @@ describe("Qdrant collection migration", () => {
|
|
|
214
257
|
// No embeddingModel
|
|
215
258
|
});
|
|
216
259
|
|
|
217
|
-
await client.ensureCollection();
|
|
260
|
+
const result = await client.ensureCollection();
|
|
218
261
|
|
|
219
262
|
expect(callLog.createCollection).toBe(1);
|
|
220
263
|
// No sentinel should be written
|
|
221
264
|
expect(callLog.upsert).toBe(0);
|
|
265
|
+
// Fresh collection, not a migration
|
|
266
|
+
expect(result.migrated).toBe(false);
|
|
222
267
|
});
|
|
223
268
|
});
|
|
@@ -4055,7 +4055,7 @@ describe("relay-server", () => {
|
|
|
4055
4055
|
.filter((m) => m.type === "text");
|
|
4056
4056
|
expect(
|
|
4057
4057
|
textMessages.some((m) =>
|
|
4058
|
-
(m.token ?? "").includes("
|
|
4058
|
+
(m.token ?? "").includes("verified that you are Eve"),
|
|
4059
4059
|
),
|
|
4060
4060
|
).toBe(true);
|
|
4061
4061
|
|
|
@@ -4079,6 +4079,51 @@ describe("relay-server", () => {
|
|
|
4079
4079
|
relay.destroy();
|
|
4080
4080
|
});
|
|
4081
4081
|
|
|
4082
|
+
test("outbound invite prompt uses assistant introduction", async () => {
|
|
4083
|
+
ensureConversation("conv-outbound-invite-origin");
|
|
4084
|
+
ensureConversation("conv-outbound-invite");
|
|
4085
|
+
const session = createCallSession({
|
|
4086
|
+
conversationId: "conv-outbound-invite",
|
|
4087
|
+
provider: "twilio",
|
|
4088
|
+
fromNumber: "+15551111111",
|
|
4089
|
+
toNumber: "+15558887777",
|
|
4090
|
+
callMode: "invite",
|
|
4091
|
+
inviteFriendName: "Grace",
|
|
4092
|
+
inviteGuardianName: "Hank",
|
|
4093
|
+
initiatedFromConversationId: "conv-outbound-invite-origin",
|
|
4094
|
+
});
|
|
4095
|
+
|
|
4096
|
+
mockAssistantName = "Vellum";
|
|
4097
|
+
|
|
4098
|
+
const { ws, relay } = createMockWs(session.id);
|
|
4099
|
+
|
|
4100
|
+
await relay.handleMessage(
|
|
4101
|
+
JSON.stringify({
|
|
4102
|
+
type: "setup",
|
|
4103
|
+
callSid: "CA_outbound_invite",
|
|
4104
|
+
from: "+15551111111",
|
|
4105
|
+
to: "+15558887777",
|
|
4106
|
+
}),
|
|
4107
|
+
);
|
|
4108
|
+
|
|
4109
|
+
// Should be in verification-pending for invite redemption
|
|
4110
|
+
expect(relay.getConnectionState()).toBe("verification_pending");
|
|
4111
|
+
|
|
4112
|
+
// The prompt should use the outbound assistant introduction
|
|
4113
|
+
const textMessages = ws.sentMessages
|
|
4114
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
4115
|
+
.filter((m) => m.type === "text");
|
|
4116
|
+
expect(
|
|
4117
|
+
textMessages.some(
|
|
4118
|
+
(m) =>
|
|
4119
|
+
(m.token ?? "").includes("this is Vellum") &&
|
|
4120
|
+
(m.token ?? "").includes("Hank's assistant"),
|
|
4121
|
+
),
|
|
4122
|
+
).toBe(true);
|
|
4123
|
+
|
|
4124
|
+
relay.destroy();
|
|
4125
|
+
});
|
|
4126
|
+
|
|
4082
4127
|
// ── resolveGuardianLabel resolution priority ─────────────────────────
|
|
4083
4128
|
|
|
4084
4129
|
test("guardian label: USER.md name takes precedence over Contact.displayName", async () => {
|
|
@@ -61,6 +61,11 @@ const ctx: ToolContext = {
|
|
|
61
61
|
trustClass: "guardian",
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
const trustedCtx: ToolContext = {
|
|
65
|
+
...ctx,
|
|
66
|
+
trustClass: "trusted_contact",
|
|
67
|
+
};
|
|
68
|
+
|
|
64
69
|
// ── schedule_create ─────────────────────────────────────────────────
|
|
65
70
|
|
|
66
71
|
describe("schedule_create tool", () => {
|
|
@@ -169,6 +174,20 @@ describe("schedule_create tool", () => {
|
|
|
169
174
|
expect(result.isError).toBe(true);
|
|
170
175
|
expect(result.content).toContain("Invalid cron expression");
|
|
171
176
|
});
|
|
177
|
+
|
|
178
|
+
test("rejects non-guardian actors", async () => {
|
|
179
|
+
const result = await executeScheduleCreate(
|
|
180
|
+
{
|
|
181
|
+
name: "Blocked schedule",
|
|
182
|
+
expression: "0 9 * * *",
|
|
183
|
+
message: "test",
|
|
184
|
+
},
|
|
185
|
+
trustedCtx,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(result.isError).toBe(true);
|
|
189
|
+
expect(result.content).toContain("restricted to guardian actors");
|
|
190
|
+
});
|
|
172
191
|
});
|
|
173
192
|
|
|
174
193
|
// ── schedule_create with fire_at (one-shot) ──────────────────────────
|
|
@@ -680,6 +699,19 @@ describe("schedule_update tool", () => {
|
|
|
680
699
|
expect(result.isError).toBe(true);
|
|
681
700
|
expect(result.content).toContain("Invalid cron expression");
|
|
682
701
|
});
|
|
702
|
+
|
|
703
|
+
test("rejects non-guardian actors", async () => {
|
|
704
|
+
const result = await executeScheduleUpdate(
|
|
705
|
+
{
|
|
706
|
+
job_id: "nonexistent-id",
|
|
707
|
+
message: "injected",
|
|
708
|
+
},
|
|
709
|
+
trustedCtx,
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
expect(result.isError).toBe(true);
|
|
713
|
+
expect(result.content).toContain("restricted to guardian actors");
|
|
714
|
+
});
|
|
683
715
|
});
|
|
684
716
|
|
|
685
717
|
// ── schedule_update with mode and routing ────────────────────────────
|
|
@@ -43,6 +43,7 @@ mock.module("../security/secure-keys.js", () => {
|
|
|
43
43
|
};
|
|
44
44
|
return {
|
|
45
45
|
getSecureKey: (key: string) => storedKeys.get(key) ?? null,
|
|
46
|
+
getSecureKeyAsync: async (key: string) => storedKeys.get(key) ?? undefined,
|
|
46
47
|
setSecureKey: syncSet,
|
|
47
48
|
setSecureKeyAsync: async (key: string, value: string) =>
|
|
48
49
|
syncSet(key, value),
|
|
@@ -46,9 +46,14 @@ mock.module("../security/keychain-broker-client.js", () => ({
|
|
|
46
46
|
return { found: false };
|
|
47
47
|
},
|
|
48
48
|
set: async (account: string, value: string) => {
|
|
49
|
-
if (mockBrokerSetError)
|
|
49
|
+
if (mockBrokerSetError)
|
|
50
|
+
return {
|
|
51
|
+
status: "rejected" as const,
|
|
52
|
+
code: "KEYCHAIN_ERROR",
|
|
53
|
+
message: "mock error",
|
|
54
|
+
};
|
|
50
55
|
mockBrokerStore.set(account, value);
|
|
51
|
-
return
|
|
56
|
+
return { status: "ok" as const };
|
|
52
57
|
},
|
|
53
58
|
del: async (account: string) => {
|
|
54
59
|
if (mockBrokerDelError) return false;
|
|
@@ -118,6 +118,7 @@ function makeCompletingSession(): Session {
|
|
|
118
118
|
setHostBashProxy: () => {},
|
|
119
119
|
setHostFileProxy: () => {},
|
|
120
120
|
setHostCuProxy: () => {},
|
|
121
|
+
addPreactivatedSkillId: () => {},
|
|
121
122
|
hasAnyPendingConfirmation: () => false,
|
|
122
123
|
hasPendingConfirmation: () => false,
|
|
123
124
|
denyAllPendingConfirmations: () => {},
|
|
@@ -175,6 +176,7 @@ function makeHangingSession(): Session {
|
|
|
175
176
|
setHostBashProxy: () => {},
|
|
176
177
|
setHostFileProxy: () => {},
|
|
177
178
|
setHostCuProxy: () => {},
|
|
179
|
+
addPreactivatedSkillId: () => {},
|
|
178
180
|
hasAnyPendingConfirmation: () => false,
|
|
179
181
|
hasPendingConfirmation: () => false,
|
|
180
182
|
denyAllPendingConfirmations: () => {},
|
|
@@ -260,6 +262,7 @@ function makePendingApprovalSession(
|
|
|
260
262
|
setHostBashProxy: () => {},
|
|
261
263
|
setHostFileProxy: () => {},
|
|
262
264
|
setHostCuProxy: () => {},
|
|
265
|
+
addPreactivatedSkillId: () => {},
|
|
263
266
|
hasAnyPendingConfirmation: () => pending.size > 0,
|
|
264
267
|
hasPendingConfirmation: (candidateRequestId: string) =>
|
|
265
268
|
pending.has(candidateRequestId),
|
|
@@ -75,18 +75,6 @@ mock.module("../security/secret-allowlist.js", () => ({
|
|
|
75
75
|
resetAllowlist: () => {},
|
|
76
76
|
}));
|
|
77
77
|
|
|
78
|
-
mock.module("../memory/admin.js", () => ({
|
|
79
|
-
getMemoryConflictAndCleanupStats: () => ({
|
|
80
|
-
conflicts: { pending: 0, resolved: 0, oldestPendingAgeMs: null },
|
|
81
|
-
cleanup: {
|
|
82
|
-
resolvedBacklog: 0,
|
|
83
|
-
supersededBacklog: 0,
|
|
84
|
-
resolvedCompleted24h: 0,
|
|
85
|
-
supersededCompleted24h: 0,
|
|
86
|
-
},
|
|
87
|
-
}),
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
78
|
// Track all messages persisted to DB
|
|
91
79
|
let persistedMessages: Array<{ role: string; content: string }> = [];
|
|
92
80
|
|
|
@@ -128,13 +116,12 @@ mock.module("../memory/retriever.js", () => ({
|
|
|
128
116
|
enabled: false,
|
|
129
117
|
degraded: false,
|
|
130
118
|
injectedText: "",
|
|
131
|
-
|
|
119
|
+
|
|
132
120
|
semanticHits: 0,
|
|
133
121
|
recencyHits: 0,
|
|
134
122
|
injectedTokens: 0,
|
|
135
123
|
latencyMs: 0,
|
|
136
124
|
}),
|
|
137
|
-
injectMemoryRecallIntoUserMessage: (msg: Message) => msg,
|
|
138
125
|
stripMemoryRecallMessages: (msgs: Message[]) => msgs,
|
|
139
126
|
}));
|
|
140
127
|
|