@vellumai/assistant 0.5.4 → 0.5.6
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/Dockerfile +17 -27
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +113 -0
- package/src/__tests__/config-schema.test.ts +2 -2
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-title-service.test.ts +30 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +0 -18
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/env.ts +8 -2
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +0 -12
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -0
- package/src/context/window-manager.ts +53 -2
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +10 -47
- package/src/daemon/providers-setup.ts +2 -1
- package/src/followups/followup-store.ts +5 -2
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +26 -10
- package/src/memory/db-init.ts +5 -13
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/indexer.ts +15 -106
- package/src/memory/job-handlers/conversation-starters.ts +24 -36
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +0 -8
- package/src/memory/jobs-worker.ts +0 -20
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/index.ts +1 -3
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +332 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +531 -39
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +4 -2
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +44 -2
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/secret-routes.ts +4 -1
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +21 -19
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/load.ts +9 -2
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- package/src/memory/schema/memory-brief.ts +0 -55
|
@@ -37,13 +37,55 @@ mock.module("../../util/logger.js", () => ({
|
|
|
37
37
|
}),
|
|
38
38
|
}));
|
|
39
39
|
|
|
40
|
-
// Stub config loader
|
|
40
|
+
// Stub config loader — returns a config with memory enabled by default
|
|
41
41
|
mock.module("../../config/loader.js", () => ({
|
|
42
|
-
loadConfig: () =>
|
|
43
|
-
getConfig: () =>
|
|
42
|
+
loadConfig: () => mockConfig,
|
|
43
|
+
getConfig: () => mockConfig,
|
|
44
44
|
invalidateConfigCache: () => {},
|
|
45
45
|
}));
|
|
46
46
|
|
|
47
|
+
// ── Controllable mocks for semantic search ─────────────────────────────
|
|
48
|
+
const mockConfig: unknown = {};
|
|
49
|
+
|
|
50
|
+
let mockBackendStatus: {
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
provider: string | null;
|
|
53
|
+
model: string | null;
|
|
54
|
+
} = { enabled: false, provider: null, model: null };
|
|
55
|
+
|
|
56
|
+
const mockEmbedResult: {
|
|
57
|
+
provider: string;
|
|
58
|
+
model: string;
|
|
59
|
+
vectors: number[][];
|
|
60
|
+
} = { provider: "local", model: "test", vectors: [[0.1, 0.2, 0.3]] };
|
|
61
|
+
|
|
62
|
+
let mockHybridSearchResults: Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
score: number;
|
|
65
|
+
payload: Record<string, unknown>;
|
|
66
|
+
}> = [];
|
|
67
|
+
|
|
68
|
+
mock.module("../../memory/embedding-backend.js", () => ({
|
|
69
|
+
getMemoryBackendStatus: async () => mockBackendStatus,
|
|
70
|
+
embedWithBackend: async () => mockEmbedResult,
|
|
71
|
+
generateSparseEmbedding: () => ({
|
|
72
|
+
indices: [0, 1, 2],
|
|
73
|
+
values: [0.5, 0.3, 0.2],
|
|
74
|
+
}),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
mock.module("../../memory/qdrant-client.js", () => ({
|
|
78
|
+
getQdrantClient: () => ({
|
|
79
|
+
hybridSearch: async () => [...mockHybridSearchResults],
|
|
80
|
+
searchWithFilter: async () => [...mockHybridSearchResults],
|
|
81
|
+
}),
|
|
82
|
+
initQdrantClient: () => {},
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
mock.module("../../memory/qdrant-circuit-breaker.js", () => ({
|
|
86
|
+
withQdrantBreaker: async (fn: () => Promise<unknown>) => fn(),
|
|
87
|
+
}));
|
|
88
|
+
|
|
47
89
|
import { and, eq } from "drizzle-orm";
|
|
48
90
|
|
|
49
91
|
import { getDb, initializeDb, resetDb } from "../../memory/db.js";
|
|
@@ -392,6 +434,182 @@ describe("Memory Item Routes", () => {
|
|
|
392
434
|
const res = await handler(ctx);
|
|
393
435
|
expect(res.status).toBe(400);
|
|
394
436
|
});
|
|
437
|
+
|
|
438
|
+
// ── Semantic / hybrid search ──────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
test("uses semantic search when embedding backend is available", async () => {
|
|
441
|
+
insertItem({
|
|
442
|
+
id: "i1",
|
|
443
|
+
kind: "preference",
|
|
444
|
+
subject: "dark mode",
|
|
445
|
+
statement: "User prefers dark mode",
|
|
446
|
+
});
|
|
447
|
+
insertItem({
|
|
448
|
+
id: "i2",
|
|
449
|
+
kind: "identity",
|
|
450
|
+
subject: "name",
|
|
451
|
+
statement: "User name is Alice",
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Enable semantic search
|
|
455
|
+
mockBackendStatus = {
|
|
456
|
+
enabled: true,
|
|
457
|
+
provider: "local",
|
|
458
|
+
model: "test",
|
|
459
|
+
};
|
|
460
|
+
// Qdrant returns i2 first (higher relevance), then i1
|
|
461
|
+
mockHybridSearchResults = [
|
|
462
|
+
{
|
|
463
|
+
id: "pt-2",
|
|
464
|
+
score: 0.95,
|
|
465
|
+
payload: { target_type: "item", target_id: "i2" },
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: "pt-1",
|
|
469
|
+
score: 0.7,
|
|
470
|
+
payload: { target_type: "item", target_id: "i1" },
|
|
471
|
+
},
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const ctx = makeCtx({ search: "alice" });
|
|
475
|
+
const res = await handler(ctx);
|
|
476
|
+
const body = (await res.json()) as {
|
|
477
|
+
items: Array<{ id: string }>;
|
|
478
|
+
total: number;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Results in Qdrant relevance order (i2 first)
|
|
482
|
+
expect(body.items.length).toBe(2);
|
|
483
|
+
expect(body.items[0].id).toBe("i2");
|
|
484
|
+
expect(body.items[1].id).toBe("i1");
|
|
485
|
+
expect(body.total).toBe(2);
|
|
486
|
+
|
|
487
|
+
// Reset
|
|
488
|
+
mockBackendStatus = { enabled: false, provider: null, model: null };
|
|
489
|
+
mockHybridSearchResults = [];
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("falls back to SQL LIKE when backend is unavailable", async () => {
|
|
493
|
+
insertItem({
|
|
494
|
+
id: "i1",
|
|
495
|
+
kind: "preference",
|
|
496
|
+
subject: "dark mode",
|
|
497
|
+
statement: "User prefers dark mode",
|
|
498
|
+
});
|
|
499
|
+
insertItem({
|
|
500
|
+
id: "i2",
|
|
501
|
+
kind: "identity",
|
|
502
|
+
subject: "name",
|
|
503
|
+
statement: "User name is Alice",
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Backend unavailable
|
|
507
|
+
mockBackendStatus = { enabled: false, provider: null, model: null };
|
|
508
|
+
mockHybridSearchResults = [];
|
|
509
|
+
|
|
510
|
+
const ctx = makeCtx({ search: "dark" });
|
|
511
|
+
const res = await handler(ctx);
|
|
512
|
+
const body = (await res.json()) as {
|
|
513
|
+
items: Array<{ id: string }>;
|
|
514
|
+
total: number;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// SQL LIKE fallback finds "dark" in subject/statement
|
|
518
|
+
expect(body.total).toBe(1);
|
|
519
|
+
expect(body.items[0].id).toBe("i1");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("semantic search respects pagination", async () => {
|
|
523
|
+
insertItem({
|
|
524
|
+
id: "i1",
|
|
525
|
+
kind: "preference",
|
|
526
|
+
subject: "s1",
|
|
527
|
+
statement: "first item",
|
|
528
|
+
});
|
|
529
|
+
insertItem({
|
|
530
|
+
id: "i2",
|
|
531
|
+
kind: "preference",
|
|
532
|
+
subject: "s2",
|
|
533
|
+
statement: "second item",
|
|
534
|
+
});
|
|
535
|
+
insertItem({
|
|
536
|
+
id: "i3",
|
|
537
|
+
kind: "preference",
|
|
538
|
+
subject: "s3",
|
|
539
|
+
statement: "third item",
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
mockBackendStatus = {
|
|
543
|
+
enabled: true,
|
|
544
|
+
provider: "local",
|
|
545
|
+
model: "test",
|
|
546
|
+
};
|
|
547
|
+
mockHybridSearchResults = [
|
|
548
|
+
{
|
|
549
|
+
id: "pt-1",
|
|
550
|
+
score: 0.9,
|
|
551
|
+
payload: { target_type: "item", target_id: "i1" },
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
id: "pt-2",
|
|
555
|
+
score: 0.8,
|
|
556
|
+
payload: { target_type: "item", target_id: "i2" },
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
id: "pt-3",
|
|
560
|
+
score: 0.7,
|
|
561
|
+
payload: { target_type: "item", target_id: "i3" },
|
|
562
|
+
},
|
|
563
|
+
];
|
|
564
|
+
|
|
565
|
+
// Request page 2 (offset=1, limit=1)
|
|
566
|
+
const ctx = makeCtx({ search: "item", limit: "1", offset: "1" });
|
|
567
|
+
const res = await handler(ctx);
|
|
568
|
+
const body = (await res.json()) as {
|
|
569
|
+
items: Array<{ id: string }>;
|
|
570
|
+
total: number;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
expect(body.items.length).toBe(1);
|
|
574
|
+
expect(body.items[0].id).toBe("i2"); // second by relevance
|
|
575
|
+
expect(body.total).toBe(3);
|
|
576
|
+
|
|
577
|
+
// Reset
|
|
578
|
+
mockBackendStatus = { enabled: false, provider: null, model: null };
|
|
579
|
+
mockHybridSearchResults = [];
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("falls back to SQL when semantic returns empty results", async () => {
|
|
583
|
+
insertItem({
|
|
584
|
+
id: "i1",
|
|
585
|
+
kind: "preference",
|
|
586
|
+
subject: "dark mode",
|
|
587
|
+
statement: "User prefers dark mode",
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
mockBackendStatus = {
|
|
591
|
+
enabled: true,
|
|
592
|
+
provider: "local",
|
|
593
|
+
model: "test",
|
|
594
|
+
};
|
|
595
|
+
// Qdrant returns nothing
|
|
596
|
+
mockHybridSearchResults = [];
|
|
597
|
+
|
|
598
|
+
const ctx = makeCtx({ search: "dark" });
|
|
599
|
+
const res = await handler(ctx);
|
|
600
|
+
const body = (await res.json()) as {
|
|
601
|
+
items: Array<{ id: string }>;
|
|
602
|
+
total: number;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// Falls through to SQL LIKE
|
|
606
|
+
expect(body.total).toBe(1);
|
|
607
|
+
expect(body.items[0].id).toBe("i1");
|
|
608
|
+
|
|
609
|
+
// Reset
|
|
610
|
+
mockBackendStatus = { enabled: false, provider: null, model: null };
|
|
611
|
+
mockHybridSearchResults = [];
|
|
612
|
+
});
|
|
395
613
|
});
|
|
396
614
|
|
|
397
615
|
// =========================================================================
|
|
@@ -11,18 +11,29 @@
|
|
|
11
11
|
import { and, asc, count, desc, eq, inArray, like, ne, or } from "drizzle-orm";
|
|
12
12
|
import { v4 as uuid } from "uuid";
|
|
13
13
|
|
|
14
|
+
import { getConfig } from "../../config/loader.js";
|
|
14
15
|
import { getDb } from "../../memory/db.js";
|
|
16
|
+
import {
|
|
17
|
+
embedWithBackend,
|
|
18
|
+
generateSparseEmbedding,
|
|
19
|
+
getMemoryBackendStatus,
|
|
20
|
+
} from "../../memory/embedding-backend.js";
|
|
15
21
|
import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
|
|
16
22
|
import { enqueueMemoryJob } from "../../memory/jobs-store.js";
|
|
23
|
+
import { withQdrantBreaker } from "../../memory/qdrant-circuit-breaker.js";
|
|
24
|
+
import { getQdrantClient } from "../../memory/qdrant-client.js";
|
|
17
25
|
import {
|
|
18
26
|
conversations,
|
|
19
27
|
memoryEmbeddings,
|
|
20
28
|
memoryItems,
|
|
21
29
|
} from "../../memory/schema.js";
|
|
30
|
+
import { getLogger } from "../../util/logger.js";
|
|
22
31
|
import { truncate } from "../../util/truncate.js";
|
|
23
32
|
import { httpError } from "../http-errors.js";
|
|
24
33
|
import type { RouteContext, RouteDefinition } from "../http-router.js";
|
|
25
34
|
|
|
35
|
+
const log = getLogger("memory-item-routes");
|
|
36
|
+
|
|
26
37
|
// ---------------------------------------------------------------------------
|
|
27
38
|
// Constants
|
|
28
39
|
// ---------------------------------------------------------------------------
|
|
@@ -116,11 +127,76 @@ function buildConversationTitleMap(
|
|
|
116
127
|
return map;
|
|
117
128
|
}
|
|
118
129
|
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Semantic search helper
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Attempt hybrid semantic search for memory items via Qdrant.
|
|
136
|
+
* Returns ordered item IDs + total count on success, or `null` when
|
|
137
|
+
* the embedding backend / Qdrant is unavailable (caller falls back to SQL).
|
|
138
|
+
*/
|
|
139
|
+
async function searchItemsSemantic(
|
|
140
|
+
query: string,
|
|
141
|
+
fetchLimit: number,
|
|
142
|
+
kindFilter: string | null,
|
|
143
|
+
statusFilter: string,
|
|
144
|
+
): Promise<{ ids: string[]; total: number } | null> {
|
|
145
|
+
try {
|
|
146
|
+
const config = getConfig();
|
|
147
|
+
const backendStatus = await getMemoryBackendStatus(config);
|
|
148
|
+
if (!backendStatus.provider) return null;
|
|
149
|
+
|
|
150
|
+
const embedded = await embedWithBackend(config, [query]);
|
|
151
|
+
const queryVector = embedded.vectors[0];
|
|
152
|
+
if (!queryVector) return null;
|
|
153
|
+
|
|
154
|
+
const sparse = generateSparseEmbedding(query);
|
|
155
|
+
const sparseVector = { indices: sparse.indices, values: sparse.values };
|
|
156
|
+
|
|
157
|
+
// Build Qdrant filter — items only, exclude capability kind and sentinel
|
|
158
|
+
const mustConditions: Array<Record<string, unknown>> = [
|
|
159
|
+
{ key: "target_type", match: { value: "item" } },
|
|
160
|
+
];
|
|
161
|
+
if (statusFilter && statusFilter !== "all") {
|
|
162
|
+
mustConditions.push({ key: "status", match: { value: statusFilter } });
|
|
163
|
+
}
|
|
164
|
+
if (kindFilter) {
|
|
165
|
+
mustConditions.push({ key: "kind", match: { value: kindFilter } });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const filter = {
|
|
169
|
+
must: mustConditions,
|
|
170
|
+
must_not: [
|
|
171
|
+
{ key: "kind", match: { value: "capability" } },
|
|
172
|
+
{ key: "_meta", match: { value: true } },
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const qdrant = getQdrantClient();
|
|
177
|
+
const results = await withQdrantBreaker(() =>
|
|
178
|
+
qdrant.hybridSearch({
|
|
179
|
+
denseVector: queryVector,
|
|
180
|
+
sparseVector,
|
|
181
|
+
filter,
|
|
182
|
+
limit: fetchLimit,
|
|
183
|
+
prefetchLimit: fetchLimit,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const ids = results.map((r) => r.payload.target_id);
|
|
188
|
+
return { ids, total: ids.length };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
log.warn({ err }, "Semantic memory search failed, falling back to SQL");
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
119
195
|
// ---------------------------------------------------------------------------
|
|
120
196
|
// GET /v1/memory-items
|
|
121
197
|
// ---------------------------------------------------------------------------
|
|
122
198
|
|
|
123
|
-
export function handleListMemoryItems(url: URL): Response {
|
|
199
|
+
export async function handleListMemoryItems(url: URL): Promise<Response> {
|
|
124
200
|
const kindParam = url.searchParams.get("kind");
|
|
125
201
|
const statusParam = url.searchParams.get("status") ?? "active";
|
|
126
202
|
const searchParam = url.searchParams.get("search");
|
|
@@ -155,7 +231,53 @@ export function handleListMemoryItems(url: URL): Response {
|
|
|
155
231
|
|
|
156
232
|
const db = getDb();
|
|
157
233
|
|
|
158
|
-
//
|
|
234
|
+
// ── Semantic search path ────────────────────────────────────────────
|
|
235
|
+
// When a search query is present, try Qdrant hybrid search first.
|
|
236
|
+
// Falls back to SQL LIKE when embeddings / Qdrant are unavailable.
|
|
237
|
+
if (searchParam) {
|
|
238
|
+
const semanticResult = await searchItemsSemantic(
|
|
239
|
+
searchParam,
|
|
240
|
+
limitParam + offsetParam,
|
|
241
|
+
kindParam,
|
|
242
|
+
statusParam,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (semanticResult && semanticResult.ids.length > 0) {
|
|
246
|
+
// Slice for pagination
|
|
247
|
+
const pageIds = semanticResult.ids.slice(
|
|
248
|
+
offsetParam,
|
|
249
|
+
offsetParam + limitParam,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Batch-fetch full rows from SQLite
|
|
253
|
+
const rows = db
|
|
254
|
+
.select()
|
|
255
|
+
.from(memoryItems)
|
|
256
|
+
.where(inArray(memoryItems.id, pageIds))
|
|
257
|
+
.all();
|
|
258
|
+
|
|
259
|
+
// Preserve Qdrant relevance ordering
|
|
260
|
+
const idOrder = new Map(pageIds.map((id, i) => [id, i]));
|
|
261
|
+
rows.sort((a, b) => (idOrder.get(a.id) ?? 0) - (idOrder.get(b.id) ?? 0));
|
|
262
|
+
|
|
263
|
+
const titleMap = buildConversationTitleMap(
|
|
264
|
+
db,
|
|
265
|
+
rows.map((i) => i.scopeId),
|
|
266
|
+
);
|
|
267
|
+
const enrichedItems = rows.map((item) => ({
|
|
268
|
+
...item,
|
|
269
|
+
scopeLabel: resolveScopeLabel(item.scopeId, titleMap),
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
return Response.json({
|
|
273
|
+
items: enrichedItems,
|
|
274
|
+
total: semanticResult.total,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// semanticResult was null (Qdrant unavailable) or empty — fall through to SQL
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── SQL path (default or fallback) ──────────────────────────────────
|
|
159
281
|
const conditions = [];
|
|
160
282
|
// Hide system-managed capability memories (skill announcements) from the UI
|
|
161
283
|
conditions.push(ne(memoryItems.kind, "capability"));
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
invalidateConfigCache,
|
|
11
11
|
} from "../../config/loader.js";
|
|
12
12
|
import type { CesClient } from "../../credential-execution/client.js";
|
|
13
|
-
import { setSentryOrganizationId } from "../../instrument.js";
|
|
13
|
+
import { setSentryOrganizationId, setSentryUserId } from "../../instrument.js";
|
|
14
14
|
import { clearEmbeddingBackendCache } from "../../memory/embedding-backend.js";
|
|
15
15
|
import { syncManualTokenConnection } from "../../oauth/manual-token-connection.js";
|
|
16
16
|
import { validateAnthropicApiKey } from "../../providers/anthropic/client.js";
|
|
@@ -235,6 +235,7 @@ export async function handleAddSecret(
|
|
|
235
235
|
setSentryOrganizationId(undefined);
|
|
236
236
|
} else if (field === "platform_user_id") {
|
|
237
237
|
setPlatformUserId(undefined);
|
|
238
|
+
setSentryUserId(undefined);
|
|
238
239
|
}
|
|
239
240
|
deleteCredentialMetadata(service, field);
|
|
240
241
|
} else {
|
|
@@ -260,6 +261,7 @@ export async function handleAddSecret(
|
|
|
260
261
|
}
|
|
261
262
|
if (service === "vellum" && field === "platform_user_id") {
|
|
262
263
|
setPlatformUserId(effectiveValue || undefined);
|
|
264
|
+
setSentryUserId(effectiveValue || undefined);
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
if (isManagedProxyCredential(service, field)) {
|
|
@@ -395,6 +397,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
395
397
|
}
|
|
396
398
|
if (service === "vellum" && field === "platform_user_id") {
|
|
397
399
|
setPlatformUserId(undefined);
|
|
400
|
+
setSentryUserId(undefined);
|
|
398
401
|
}
|
|
399
402
|
if (isManagedProxyCredential(service, field)) {
|
|
400
403
|
await initializeProviders(getConfig());
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade broadcast endpoint — publishes service group update lifecycle
|
|
3
|
+
* events (starting / complete) to all connected SSE clients.
|
|
4
|
+
*
|
|
5
|
+
* Protected by a route policy restricting access to gateway service
|
|
6
|
+
* principals only (`svc_gateway` with `internal.write` scope), following
|
|
7
|
+
* the same pattern as other gateway-forwarded control-plane endpoints.
|
|
8
|
+
* The gateway requires a valid edge JWT and forwards the request with a
|
|
9
|
+
* minted service token.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ServiceGroupUpdateComplete,
|
|
14
|
+
ServiceGroupUpdateStarting,
|
|
15
|
+
} from "../../daemon/message-types/upgrades.js";
|
|
16
|
+
import { buildAssistantEvent } from "../assistant-event.js";
|
|
17
|
+
import { assistantEventHub } from "../assistant-event-hub.js";
|
|
18
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
19
|
+
import { httpError } from "../http-errors.js";
|
|
20
|
+
import type { RouteDefinition } from "../http-router.js";
|
|
21
|
+
|
|
22
|
+
export function upgradeBroadcastRouteDefinitions(): RouteDefinition[] {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
endpoint: "admin/upgrade-broadcast",
|
|
26
|
+
method: "POST",
|
|
27
|
+
handler: async ({ req }) => {
|
|
28
|
+
let body: unknown;
|
|
29
|
+
try {
|
|
30
|
+
body = await req.json();
|
|
31
|
+
} catch {
|
|
32
|
+
return httpError("BAD_REQUEST", "Invalid JSON body", 400);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!body || typeof body !== "object") {
|
|
36
|
+
return httpError(
|
|
37
|
+
"BAD_REQUEST",
|
|
38
|
+
"Request body must be a JSON object",
|
|
39
|
+
400,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { type } = body as { type?: unknown };
|
|
44
|
+
|
|
45
|
+
if (type === "starting") {
|
|
46
|
+
const { targetVersion, expectedDowntimeSeconds } = body as {
|
|
47
|
+
targetVersion?: unknown;
|
|
48
|
+
expectedDowntimeSeconds?: unknown;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (typeof targetVersion !== "string" || targetVersion.length === 0) {
|
|
52
|
+
return httpError(
|
|
53
|
+
"BAD_REQUEST",
|
|
54
|
+
"targetVersion is required and must be a non-empty string",
|
|
55
|
+
400,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const downtime =
|
|
60
|
+
expectedDowntimeSeconds === undefined
|
|
61
|
+
? 60
|
|
62
|
+
: expectedDowntimeSeconds;
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
typeof downtime !== "number" ||
|
|
66
|
+
!isFinite(downtime) ||
|
|
67
|
+
downtime < 0
|
|
68
|
+
) {
|
|
69
|
+
return httpError(
|
|
70
|
+
"BAD_REQUEST",
|
|
71
|
+
"expectedDowntimeSeconds must be a non-negative number",
|
|
72
|
+
400,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const message: ServiceGroupUpdateStarting = {
|
|
77
|
+
type: "service_group_update_starting",
|
|
78
|
+
targetVersion,
|
|
79
|
+
expectedDowntimeSeconds: downtime,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
await assistantEventHub.publish(
|
|
83
|
+
buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, message),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return Response.json({ ok: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (type === "complete") {
|
|
90
|
+
const { installedVersion, success, rolledBackToVersion } = body as {
|
|
91
|
+
installedVersion?: unknown;
|
|
92
|
+
success?: unknown;
|
|
93
|
+
rolledBackToVersion?: unknown;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
typeof installedVersion !== "string" ||
|
|
98
|
+
installedVersion.length === 0
|
|
99
|
+
) {
|
|
100
|
+
return httpError(
|
|
101
|
+
"BAD_REQUEST",
|
|
102
|
+
"installedVersion is required and must be a non-empty string",
|
|
103
|
+
400,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof success !== "boolean") {
|
|
108
|
+
return httpError(
|
|
109
|
+
"BAD_REQUEST",
|
|
110
|
+
"success is required and must be a boolean",
|
|
111
|
+
400,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
rolledBackToVersion !== undefined &&
|
|
117
|
+
(typeof rolledBackToVersion !== "string" ||
|
|
118
|
+
rolledBackToVersion.length === 0)
|
|
119
|
+
) {
|
|
120
|
+
return httpError(
|
|
121
|
+
"BAD_REQUEST",
|
|
122
|
+
"rolledBackToVersion must be a non-empty string when provided",
|
|
123
|
+
400,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const message: ServiceGroupUpdateComplete = {
|
|
128
|
+
type: "service_group_update_complete",
|
|
129
|
+
installedVersion,
|
|
130
|
+
success,
|
|
131
|
+
...(typeof rolledBackToVersion === "string"
|
|
132
|
+
? { rolledBackToVersion }
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await assistantEventHub.publish(
|
|
137
|
+
buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, message),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return Response.json({ ok: true });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return httpError(
|
|
144
|
+
"BAD_REQUEST",
|
|
145
|
+
'type must be "starting" or "complete"',
|
|
146
|
+
400,
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
}
|
|
@@ -206,27 +206,6 @@ export function listSchedules(options?: {
|
|
|
206
206
|
return rows.map(parseJobRow);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/**
|
|
210
|
-
* Return enabled schedules whose next run falls within a time window.
|
|
211
|
-
* Used by the memory brief compiler to surface due-soon schedule entries.
|
|
212
|
-
*/
|
|
213
|
-
export function getDueSoonSchedules(
|
|
214
|
-
now: number,
|
|
215
|
-
horizonMs: number,
|
|
216
|
-
): ScheduleJob[] {
|
|
217
|
-
const db = getDb();
|
|
218
|
-
const cutoff = now + horizonMs;
|
|
219
|
-
const rows = db
|
|
220
|
-
.select()
|
|
221
|
-
.from(scheduleJobs)
|
|
222
|
-
.where(
|
|
223
|
-
and(eq(scheduleJobs.enabled, true), lte(scheduleJobs.nextRunAt, cutoff)),
|
|
224
|
-
)
|
|
225
|
-
.orderBy(asc(scheduleJobs.nextRunAt))
|
|
226
|
-
.all();
|
|
227
|
-
return rows.map(parseJobRow);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
209
|
export function updateSchedule(
|
|
231
210
|
id: string,
|
|
232
211
|
updates: {
|