@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.3
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/AGENTS.md +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/knip.json +3 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +81 -44
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +8 -6
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
20
|
|
|
22
21
|
import { getRawShareAnalytics } from "../platform/consent-cache.js";
|
|
23
22
|
import { recordWatchdogEvent } from "../telemetry/watchdog-events-store.js";
|
|
24
23
|
import { getLogger } from "../util/logger.js";
|
|
25
24
|
import { getDbPath, getMonitoringDataDir } from "../util/platform.js";
|
|
25
|
+
import { resolveWorkerCommand } from "../util/worker-process.js";
|
|
26
26
|
import type { IntegritySampleResult } from "./db-integrity-check.js";
|
|
27
27
|
|
|
28
28
|
const log = getLogger("db-integrity-sample");
|
|
@@ -60,13 +60,12 @@ let activeChild: ReturnType<typeof Bun.spawn> | null = null;
|
|
|
60
60
|
async function runCheckSubprocess(
|
|
61
61
|
dbPath: string,
|
|
62
62
|
): Promise<IntegritySampleResult | null> {
|
|
63
|
-
|
|
64
|
-
// install path with a space in it would not resolve. See worker-process.ts.
|
|
65
|
-
const entry = fileURLToPath(
|
|
63
|
+
const command = resolveWorkerCommand(
|
|
66
64
|
new URL("./db-integrity-check.ts", import.meta.url),
|
|
65
|
+
"db-integrity",
|
|
67
66
|
);
|
|
68
67
|
const child = Bun.spawn({
|
|
69
|
-
cmd: [
|
|
68
|
+
cmd: [...command, dbPath],
|
|
70
69
|
stdio: ["ignore", "pipe", "ignore"],
|
|
71
70
|
});
|
|
72
71
|
activeChild = child;
|
|
@@ -36,7 +36,7 @@ export class PermissionPrompter {
|
|
|
36
36
|
* pendingInteractions, matching the host proxy pattern.
|
|
37
37
|
*/
|
|
38
38
|
private ownedIds = new Set<string>();
|
|
39
|
-
private sendToClient: (msg: AssistantEvent) => void;
|
|
39
|
+
private readonly sendToClient: (msg: AssistantEvent) => void;
|
|
40
40
|
private onStateChanged?: ConfirmationStateCallback;
|
|
41
41
|
|
|
42
42
|
constructor(sendToClient: (msg: AssistantEvent) => void) {
|
|
@@ -47,10 +47,6 @@ export class PermissionPrompter {
|
|
|
47
47
|
this.onStateChanged = cb;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
updateSender(sendToClient: (msg: AssistantEvent) => void): void {
|
|
51
|
-
this.sendToClient = sendToClient;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
50
|
async prompt(
|
|
55
51
|
toolName: string,
|
|
56
52
|
input: Record<string, unknown>,
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
and,
|
|
3
|
+
count,
|
|
4
|
+
desc,
|
|
5
|
+
eq,
|
|
6
|
+
getTableColumns,
|
|
7
|
+
isNull,
|
|
8
|
+
lt,
|
|
9
|
+
sql,
|
|
10
|
+
} from "drizzle-orm";
|
|
11
|
+
import type { SQLiteSelect } from "drizzle-orm/sqlite-core";
|
|
2
12
|
|
|
3
13
|
import {
|
|
4
14
|
parseExternalContentEnvelope,
|
|
@@ -263,6 +273,15 @@ export interface ConversationListFilter {
|
|
|
263
273
|
* section, and a UUID one custom group. Omit to span every group.
|
|
264
274
|
*/
|
|
265
275
|
groupId?: string;
|
|
276
|
+
/**
|
|
277
|
+
* Restrict to conversations whose latest assistant message the user has
|
|
278
|
+
* not seen: the same "unseen" predicate the unread count and the section
|
|
279
|
+
* index apply ({@link unseenAttentionStateConditions}), so every reader
|
|
280
|
+
* of "needs attention" agrees. Only `true` narrows; omit to span every
|
|
281
|
+
* conversation. A client that keeps no complete list can ask for exactly
|
|
282
|
+
* the rows its attention surfaces need instead of scanning for them.
|
|
283
|
+
*/
|
|
284
|
+
needsAttention?: true;
|
|
266
285
|
}
|
|
267
286
|
|
|
268
287
|
export interface ConversationListQuery extends ConversationListFilter {
|
|
@@ -327,21 +346,55 @@ function originChannelClause(originChannel: string) {
|
|
|
327
346
|
return eq(conversations.originChannel, originChannel);
|
|
328
347
|
}
|
|
329
348
|
|
|
349
|
+
/**
|
|
350
|
+
* The join between a conversation and its attention projection, one row per
|
|
351
|
+
* conversation in `conversation_assistant_attention_state`. Every reader of
|
|
352
|
+
* "unseen" joins on this: the list and count filters, the unread count, and
|
|
353
|
+
* the section index.
|
|
354
|
+
*/
|
|
355
|
+
const attentionJoin = eq(
|
|
356
|
+
conversationAssistantAttentionState.conversationId,
|
|
357
|
+
conversations.id,
|
|
358
|
+
);
|
|
359
|
+
|
|
330
360
|
function conversationListWhere(filter: ConversationListFilter) {
|
|
331
361
|
const {
|
|
332
362
|
conversationType = "standard",
|
|
333
363
|
archiveStatus = "active",
|
|
334
364
|
originChannel,
|
|
335
365
|
groupId,
|
|
366
|
+
needsAttention,
|
|
336
367
|
} = filter;
|
|
337
368
|
return and(
|
|
338
369
|
conversationTypeClause(conversationType),
|
|
339
370
|
archiveStatusClause(archiveStatus) ?? undefined,
|
|
340
371
|
originChannel ? originChannelClause(originChannel) : undefined,
|
|
341
372
|
groupId ? groupIdClause(groupId) : undefined,
|
|
373
|
+
...(needsAttention ? unseenAttentionStateConditions() : []),
|
|
342
374
|
);
|
|
343
375
|
}
|
|
344
376
|
|
|
377
|
+
/**
|
|
378
|
+
* The joins {@link conversationListWhere}'s predicates need, applied to a
|
|
379
|
+
* list or count query over `conversations`. The attention predicate lives on
|
|
380
|
+
* `conversation_assistant_attention_state`, so `needsAttention` joins it.
|
|
381
|
+
* Inner, not left: a conversation with no
|
|
382
|
+
* attention row has no unseen message by definition. Joined only when the
|
|
383
|
+
* filter asks, so a query without the filter runs without the join and
|
|
384
|
+
* lists every conversation the predicates admit. One place for both
|
|
385
|
+
* {@link listConversations} and {@link countConversations}, so a page and
|
|
386
|
+
* its total always describe the same set.
|
|
387
|
+
*/
|
|
388
|
+
function withListFilterJoins<T extends SQLiteSelect>(
|
|
389
|
+
query: T,
|
|
390
|
+
filter: ConversationListFilter,
|
|
391
|
+
) {
|
|
392
|
+
if (!filter.needsAttention) {
|
|
393
|
+
return query;
|
|
394
|
+
}
|
|
395
|
+
return query.innerJoin(conversationAssistantAttentionState, attentionJoin);
|
|
396
|
+
}
|
|
397
|
+
|
|
345
398
|
export function listConversations(
|
|
346
399
|
query: ConversationListQuery = {},
|
|
347
400
|
): ConversationRow[] {
|
|
@@ -362,9 +415,13 @@ export function listConversations(
|
|
|
362
415
|
// distinguish a total order from a lucky one. The guarantee becomes
|
|
363
416
|
// observable, and gets its test, with the keyset pagination work.
|
|
364
417
|
const tiebreak = desc(conversations.id);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
418
|
+
// Selecting the conversation columns explicitly keeps the row shape
|
|
419
|
+
// `parseConversation` maps identical whether or not the attention join is
|
|
420
|
+
// present.
|
|
421
|
+
return withListFilterJoins(
|
|
422
|
+
db.select(getTableColumns(conversations)).from(conversations).$dynamic(),
|
|
423
|
+
filter,
|
|
424
|
+
)
|
|
368
425
|
.where(conversationListWhere(filter))
|
|
369
426
|
.orderBy(recency, tiebreak)
|
|
370
427
|
.limit(limit ?? 100)
|
|
@@ -542,9 +599,10 @@ export function countConversations(
|
|
|
542
599
|
): number {
|
|
543
600
|
ensureGroupMigration();
|
|
544
601
|
const db = getDb();
|
|
545
|
-
const [{ total }] =
|
|
546
|
-
.select({ total: count() })
|
|
547
|
-
|
|
602
|
+
const [{ total }] = withListFilterJoins(
|
|
603
|
+
db.select({ total: count() }).from(conversations).$dynamic(),
|
|
604
|
+
filter,
|
|
605
|
+
)
|
|
548
606
|
.where(conversationListWhere(filter))
|
|
549
607
|
.all();
|
|
550
608
|
return total;
|
|
@@ -582,10 +640,7 @@ export function countUnreadConversations(): number {
|
|
|
582
640
|
const [{ total }] = db
|
|
583
641
|
.select({ total: count() })
|
|
584
642
|
.from(conversations)
|
|
585
|
-
.innerJoin(
|
|
586
|
-
conversationAssistantAttentionState,
|
|
587
|
-
eq(conversationAssistantAttentionState.conversationId, conversations.id),
|
|
588
|
-
)
|
|
643
|
+
.innerJoin(conversationAssistantAttentionState, attentionJoin)
|
|
589
644
|
.where(
|
|
590
645
|
and(
|
|
591
646
|
conversationTypeClause("standard"),
|
|
@@ -647,11 +702,6 @@ export function countConversationSections(): ConversationSectionCounts {
|
|
|
647
702
|
...unseenAttentionStateConditions(),
|
|
648
703
|
)} THEN 1 ELSE 0 END)`;
|
|
649
704
|
|
|
650
|
-
const attentionJoin = eq(
|
|
651
|
-
conversationAssistantAttentionState.conversationId,
|
|
652
|
-
conversations.id,
|
|
653
|
-
);
|
|
654
|
-
|
|
655
705
|
const groups = db
|
|
656
706
|
.select({
|
|
657
707
|
groupId: sql<string>`group_id`,
|
|
@@ -33,6 +33,45 @@ const SHUTDOWN_GRACE_MS = 5_000;
|
|
|
33
33
|
const STDOUT_CAPTURE_LIMIT = 16_384;
|
|
34
34
|
const STDERR_CAPTURE_LIMIT = 4_096;
|
|
35
35
|
|
|
36
|
+
export function resolveQdrantReleaseAsset(
|
|
37
|
+
os: NodeJS.Platform,
|
|
38
|
+
cpu: string,
|
|
39
|
+
): {
|
|
40
|
+
binaryName: string;
|
|
41
|
+
filename: string;
|
|
42
|
+
format: "tar.gz" | "zip";
|
|
43
|
+
sha256?: string;
|
|
44
|
+
} {
|
|
45
|
+
let target: string;
|
|
46
|
+
if (os === "darwin" && cpu === "arm64") {
|
|
47
|
+
target = "aarch64-apple-darwin";
|
|
48
|
+
} else if (os === "darwin" && cpu === "x64") {
|
|
49
|
+
target = "x86_64-apple-darwin";
|
|
50
|
+
} else if (os === "linux" && cpu === "x64") {
|
|
51
|
+
target = "x86_64-unknown-linux-musl";
|
|
52
|
+
} else if (os === "linux" && cpu === "arm64") {
|
|
53
|
+
target = "aarch64-unknown-linux-musl";
|
|
54
|
+
} else if (os === "win32" && cpu === "x64") {
|
|
55
|
+
return {
|
|
56
|
+
binaryName: "qdrant.exe",
|
|
57
|
+
filename: "qdrant-x86_64-pc-windows-msvc.zip",
|
|
58
|
+
format: "zip",
|
|
59
|
+
sha256:
|
|
60
|
+
"9d3b1d1fa58566bc71709347c4c9b83a0111d23a550daa93c3ee48e3150c4470",
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Unsupported platform: ${os}/${cpu}. ` +
|
|
65
|
+
"Set QDRANT_URL to use an external Qdrant instance.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
binaryName: "qdrant",
|
|
70
|
+
filename: `qdrant-${target}.tar.gz`,
|
|
71
|
+
format: "tar.gz",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
36
75
|
export interface QdrantManagerConfig {
|
|
37
76
|
url: string;
|
|
38
77
|
storagePath?: string;
|
|
@@ -181,34 +220,18 @@ export class QdrantManager {
|
|
|
181
220
|
private async installBinary(binaryPath: string): Promise<void> {
|
|
182
221
|
const os = platform();
|
|
183
222
|
const cpu = arch();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (os === "darwin" && cpu === "arm64") {
|
|
187
|
-
target = "aarch64-apple-darwin";
|
|
188
|
-
} else if (os === "darwin" && cpu === "x64") {
|
|
189
|
-
target = "x86_64-apple-darwin";
|
|
190
|
-
} else if (os === "linux" && cpu === "x64") {
|
|
191
|
-
target = "x86_64-unknown-linux-musl";
|
|
192
|
-
} else if (os === "linux" && cpu === "arm64") {
|
|
193
|
-
target = "aarch64-unknown-linux-musl";
|
|
194
|
-
} else {
|
|
195
|
-
throw new Error(
|
|
196
|
-
`Unsupported platform: ${os}/${cpu}. ` +
|
|
197
|
-
"Set QDRANT_URL to use an external Qdrant instance.",
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const filename = `qdrant-${target}.tar.gz`;
|
|
223
|
+
const release = resolveQdrantReleaseAsset(os, cpu);
|
|
224
|
+
const { filename } = release;
|
|
202
225
|
const baseUrl = `https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}`;
|
|
203
226
|
const url = `${baseUrl}/${filename}`;
|
|
204
227
|
const checksumUrl = `${baseUrl}/${filename}.sha256`;
|
|
205
228
|
|
|
206
229
|
log.info({ url, binaryPath }, "Downloading Qdrant binary");
|
|
207
230
|
|
|
208
|
-
// Fetch the
|
|
231
|
+
// Fetch the archive and its SHA-256 checksum in parallel
|
|
209
232
|
const [response, checksumResponse] = await Promise.all([
|
|
210
233
|
fetch(url),
|
|
211
|
-
fetch(checksumUrl),
|
|
234
|
+
release.sha256 ? Promise.resolve(null) : fetch(checksumUrl),
|
|
212
235
|
]);
|
|
213
236
|
|
|
214
237
|
if (!response.ok) {
|
|
@@ -217,15 +240,17 @@ export class QdrantManager {
|
|
|
217
240
|
);
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
const
|
|
243
|
+
const archive = await response.arrayBuffer();
|
|
221
244
|
|
|
222
|
-
// Verify SHA-256 integrity if
|
|
223
|
-
if (checksumResponse
|
|
224
|
-
const checksumText =
|
|
245
|
+
// Verify SHA-256 integrity if a pinned or published checksum is available
|
|
246
|
+
if (release.sha256 || checksumResponse?.ok) {
|
|
247
|
+
const checksumText = release.sha256
|
|
248
|
+
? release.sha256
|
|
249
|
+
: (await checksumResponse!.text()).trim();
|
|
225
250
|
// Checksum files contain "<hex> <filename>" or just "<hex>"
|
|
226
251
|
const expectedHash = checksumText.split(/\s+/)[0].toLowerCase();
|
|
227
252
|
const actualHash = createHash("sha256")
|
|
228
|
-
.update(Buffer.from(
|
|
253
|
+
.update(Buffer.from(archive))
|
|
229
254
|
.digest("hex");
|
|
230
255
|
|
|
231
256
|
if (actualHash !== expectedHash) {
|
|
@@ -237,39 +262,45 @@ export class QdrantManager {
|
|
|
237
262
|
log.info({ hash: actualHash }, "Qdrant binary checksum verified");
|
|
238
263
|
} else {
|
|
239
264
|
log.warn(
|
|
240
|
-
{ checksumUrl, status: checksumResponse
|
|
241
|
-
"Could not fetch Qdrant checksum
|
|
265
|
+
{ checksumUrl, status: checksumResponse?.status },
|
|
266
|
+
"Could not fetch Qdrant checksum; skipping integrity check",
|
|
242
267
|
);
|
|
243
268
|
}
|
|
244
269
|
|
|
245
|
-
// Extract the qdrant binary from the tarball
|
|
246
270
|
const binDir = dirname(binaryPath);
|
|
247
271
|
mkdirSync(binDir, { recursive: true });
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const proc = Bun.spawn({
|
|
255
|
-
cmd: ["tar", "xzf", tmpTar, "-C", binDir, "qdrant"],
|
|
256
|
-
stdout: "ignore",
|
|
257
|
-
stderr: "pipe",
|
|
258
|
-
});
|
|
259
|
-
await proc.exited;
|
|
260
|
-
if (proc.exitCode !== 0) {
|
|
261
|
-
const stderr = await new Response(proc.stderr).text();
|
|
262
|
-
throw new Error(`Failed to extract Qdrant binary: ${stderr}`);
|
|
272
|
+
if (release.format === "zip") {
|
|
273
|
+
const { default: JSZip } = await import("jszip");
|
|
274
|
+
const zip = await JSZip.loadAsync(archive);
|
|
275
|
+
const entry = zip.file(release.binaryName);
|
|
276
|
+
if (!entry) {
|
|
277
|
+
throw new Error(`${filename} does not contain ${release.binaryName}.`);
|
|
263
278
|
}
|
|
264
|
-
|
|
279
|
+
writeFileSync(binaryPath, await entry.async("nodebuffer"));
|
|
280
|
+
} else {
|
|
281
|
+
const tmpTar = join(binDir, `qdrant-download-${Date.now()}.tar.gz`);
|
|
282
|
+
writeFileSync(tmpTar, Buffer.from(archive));
|
|
265
283
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
284
|
+
const proc = Bun.spawn({
|
|
285
|
+
cmd: ["tar", "xzf", tmpTar, "-C", binDir, release.binaryName],
|
|
286
|
+
stdout: "ignore",
|
|
287
|
+
stderr: "pipe",
|
|
288
|
+
});
|
|
289
|
+
await proc.exited;
|
|
290
|
+
if (proc.exitCode !== 0) {
|
|
291
|
+
const stderr = await new Response(proc.stderr).text();
|
|
292
|
+
throw new Error(`Failed to extract Qdrant binary: ${stderr}`);
|
|
293
|
+
}
|
|
294
|
+
} finally {
|
|
295
|
+
try {
|
|
296
|
+
unlinkSync(tmpTar);
|
|
297
|
+
} catch {}
|
|
269
298
|
}
|
|
270
299
|
}
|
|
271
300
|
|
|
272
|
-
|
|
301
|
+
if (os !== "win32") {
|
|
302
|
+
chmodSync(binaryPath, 0o755);
|
|
303
|
+
}
|
|
273
304
|
log.info(
|
|
274
305
|
{ binaryPath, version: QDRANT_VERSION },
|
|
275
306
|
"Qdrant binary installed",
|
|
@@ -395,7 +426,8 @@ export class QdrantManager {
|
|
|
395
426
|
}
|
|
396
427
|
|
|
397
428
|
private getBinaryPath(): string {
|
|
398
|
-
|
|
429
|
+
const binaryName = platform() === "win32" ? "qdrant.exe" : "qdrant";
|
|
430
|
+
return join(getDataDir(), "qdrant", "bin", binaryName);
|
|
399
431
|
}
|
|
400
432
|
|
|
401
433
|
private cleanupStaleProcess(): void {
|
|
@@ -447,6 +479,9 @@ export class QdrantManager {
|
|
|
447
479
|
* discoverable by tools that scan for "vellum" in process names.
|
|
448
480
|
*/
|
|
449
481
|
private ensureVellumSymlink(binaryPath: string): string {
|
|
482
|
+
if (platform() === "win32") {
|
|
483
|
+
return binaryPath;
|
|
484
|
+
}
|
|
450
485
|
const symlinkPath = join(dirname(binaryPath), "vellum-qdrant");
|
|
451
486
|
const expectedTarget = realpathSync(binaryPath);
|
|
452
487
|
|
|
@@ -7,20 +7,11 @@ const COLUMN_DEFINITION = "workspace_path TEXT";
|
|
|
7
7
|
/**
|
|
8
8
|
* Add `workspace_path` to `documents` and enforce one document per file.
|
|
9
9
|
*
|
|
10
|
-
* The column
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* drafts, client-created surfaces) stay NULL.
|
|
16
|
-
*
|
|
17
|
-
* The unique index is partial (`WHERE workspace_path IS NOT NULL`) so the many
|
|
18
|
-
* NULL rows stay unconstrained while a given file resolves to exactly one
|
|
19
|
-
* document. The find-or-create route depends on that uniqueness for its
|
|
20
|
-
* idempotency.
|
|
21
|
-
*
|
|
22
|
-
* Nullable with no backfill: nothing recorded a file binding before this
|
|
23
|
-
* column existed, so pre-existing rows correctly stay NULL.
|
|
10
|
+
* The column and its partial unique index are unused by the daemon: no code
|
|
11
|
+
* reads or writes them. Some rows carry a non-NULL path; new rows stay NULL.
|
|
12
|
+
* The migration stays in the chain because migrations are append-only, and the
|
|
13
|
+
* drizzle model in `schema/documents.ts` carries the column so it keeps
|
|
14
|
+
* describing the physical table.
|
|
24
15
|
*
|
|
25
16
|
* Idempotent: the column add is guarded with `tableHasColumn` and the index
|
|
26
17
|
* with `IF NOT EXISTS`, so a crash between the two does not break the next
|
|
@@ -22,13 +22,13 @@ export const documents = sqliteTable(
|
|
|
22
22
|
wordCount: integer("word_count").notNull().default(0),
|
|
23
23
|
createdAt: integer("created_at").notNull(),
|
|
24
24
|
updatedAt: integer("updated_at").notNull(),
|
|
25
|
-
//
|
|
26
|
-
// NULL
|
|
25
|
+
// Unused by the daemon: no code reads or writes the column, and some rows
|
|
26
|
+
// carry a non-NULL path. It stays in the model because migration 360 puts
|
|
27
|
+
// it in the physical table and the model must keep describing that table.
|
|
27
28
|
workspacePath: text("workspace_path"),
|
|
28
29
|
},
|
|
29
30
|
(table) => [
|
|
30
|
-
// Partial so the
|
|
31
|
-
// file resolves to exactly one document.
|
|
31
|
+
// Partial, so the NULL rows stay unconstrained.
|
|
32
32
|
uniqueIndex("idx_documents_workspace_path")
|
|
33
33
|
.on(table.workspacePath)
|
|
34
34
|
.where(sql`workspace_path IS NOT NULL`),
|
|
@@ -39,6 +39,14 @@ export const HOOKS = {
|
|
|
39
39
|
/** Union of every hook name declared in {@link HOOKS}. */
|
|
40
40
|
export type HookName = (typeof HOOKS)[keyof typeof HOOKS];
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Message kind for the hidden user row that starts a voice escalation leg.
|
|
44
|
+
* Hooks use this marker to distinguish the continuation instruction from
|
|
45
|
+
* other transcript-suppressed prompts without depending on host voice code.
|
|
46
|
+
*/
|
|
47
|
+
export const VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND =
|
|
48
|
+
"voice_escalation_continuation";
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* Appended (inside the `<system_notice>` wrapper) to the internal continuation /
|
|
44
52
|
* completion nudge strings that re-query the model. Those notices are injected
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -67,7 +67,11 @@
|
|
|
67
67
|
*/
|
|
68
68
|
|
|
69
69
|
export type { HookName } from "./constants.js";
|
|
70
|
-
export {
|
|
70
|
+
export {
|
|
71
|
+
HOOKS,
|
|
72
|
+
INTERNAL_NUDGE_OUTPUT_SUPPRESSION,
|
|
73
|
+
VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND,
|
|
74
|
+
} from "./constants.js";
|
|
71
75
|
// Conversation message/content shapes. A hook receives the live message
|
|
72
76
|
// history (e.g. `PostToolUseContext.latestMessages: Message[]`), so plugins
|
|
73
77
|
// that inspect or narrow content blocks — reading a `tool_use` block's input,
|
|
@@ -90,22 +90,24 @@ function registrationType(plugin: string, route: string): string {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* Keep a trailing slash on a resolved callback URL.
|
|
94
94
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* than at each plugin that registers a URL.
|
|
95
|
+
* Django in front of managed callbacks canonicalizes onto `/`. A vendor given
|
|
96
|
+
* the slashless spelling is 301'd, and clients that follow a 301 on POST
|
|
97
|
+
* typically retry as GET and drop the body. The gateway serves both spellings
|
|
98
|
+
* of a plugin webhook, so the slashed URL is the one to hand out.
|
|
100
99
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
100
|
+
* Query-bearing URLs are left alone: this resolver passes no query
|
|
101
|
+
* parameters, and appending there would cut into the query rather than the
|
|
102
|
+
* path. The callback path registered with the platform stays slashless. The
|
|
103
|
+
* platform strips slashes on store, and Django's path converter typically
|
|
104
|
+
* does not include the final `/` in the lookup key.
|
|
103
105
|
*/
|
|
104
|
-
function
|
|
106
|
+
function withTrailingSlash(url: string): string {
|
|
105
107
|
if (url.includes("?") || url.includes("#")) {
|
|
106
108
|
return url;
|
|
107
109
|
}
|
|
108
|
-
return url.
|
|
110
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
/**
|
|
@@ -147,5 +149,5 @@ export async function resolveWebhookUrl(
|
|
|
147
149
|
undefined,
|
|
148
150
|
sourceIdentifier,
|
|
149
151
|
);
|
|
150
|
-
return
|
|
152
|
+
return withTrailingSlash(resolved);
|
|
151
153
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { resolveBundledDir } from "../../util/bundled-asset.js";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Filesystem-backed access to the first-party default plugins. This module
|
|
6
8
|
* will eventually replace `defaults/index.ts`.
|
|
@@ -13,12 +15,10 @@ import { dirname, join, resolve } from "node:path";
|
|
|
13
15
|
* that carries a `package.json` is one plugin, named by that manifest — the
|
|
14
16
|
* same `package.json` the barrel imports for its `manifest.name`.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
* source hierarchy as-is (`bun --compile` is not supported), so the tree this
|
|
18
|
-
* module reads in production is the tree the plugins are loaded from.
|
|
18
|
+
* Compiled builds ship this metadata tree beside the executable.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
const DEFAULTS_DIR = import.meta.dir;
|
|
21
|
+
const DEFAULTS_DIR = resolveBundledDir(import.meta.dir, ".", "default-plugins");
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Route-namespace prefix for default plugins. A default plugin's route
|
|
@@ -105,9 +105,8 @@ export function getDefaultPluginManifestName(dirName: string): string | null {
|
|
|
105
105
|
* `default-` prefix is stripped to recover the source directory, so a name
|
|
106
106
|
* without the prefix (e.g. the bare directory name) resolves to `null`. The
|
|
107
107
|
* containment guard rejects any `..`/nested segment taken from a URL path so it
|
|
108
|
-
* can never escape the defaults tree.
|
|
109
|
-
*
|
|
110
|
-
* which the assistant always ships and runs un-bundled.
|
|
108
|
+
* can never escape the defaults tree. `DEFAULTS_DIR` resolves either the source
|
|
109
|
+
* tree or the externally packaged compiled metadata tree.
|
|
111
110
|
*/
|
|
112
111
|
export function getDefaultPluginRoutesDir(name: string): string | null {
|
|
113
112
|
if (!name.startsWith(DEFAULT_PLUGIN_NAMESPACE_PREFIX)) {
|
package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts
CHANGED
|
@@ -371,6 +371,38 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (per-turn path)",
|
|
|
371
371
|
expect(result.runMessages).toEqual(messages);
|
|
372
372
|
});
|
|
373
373
|
|
|
374
|
+
test("separate routing history directs v1 retrieval without replacing model history", async () => {
|
|
375
|
+
const memory = makeMemory();
|
|
376
|
+
const config = makeConfig(false);
|
|
377
|
+
const callerText = "What is my preferred editor?";
|
|
378
|
+
const continuationText = "Continue the answer.";
|
|
379
|
+
const routingMessages = makeMessages(callerText);
|
|
380
|
+
const messages: Message[] = [
|
|
381
|
+
...routingMessages,
|
|
382
|
+
{
|
|
383
|
+
role: "assistant",
|
|
384
|
+
content: [{ type: "text", text: "Let me check that." }],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
role: "user",
|
|
388
|
+
content: [{ type: "text", text: continuationText }],
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const result = await memory.prepareMemory(
|
|
393
|
+
messages,
|
|
394
|
+
config,
|
|
395
|
+
new AbortController().signal,
|
|
396
|
+
noopEvent,
|
|
397
|
+
routingMessages,
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
expect(retrieveForTurnMock).toHaveBeenCalledWith(
|
|
401
|
+
expect.objectContaining({ userLastMessage: callerText }),
|
|
402
|
+
);
|
|
403
|
+
expect(result.runMessages).toEqual(messages);
|
|
404
|
+
});
|
|
405
|
+
|
|
374
406
|
test("config on → v2 block prepended, mode is per-turn", async () => {
|
|
375
407
|
stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
|
|
376
408
|
|
|
@@ -498,6 +530,51 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (per-turn path)",
|
|
|
498
530
|
).toBe(false);
|
|
499
531
|
});
|
|
500
532
|
|
|
533
|
+
test("separate routing history directs v2 retrieval while injection stays on model history", async () => {
|
|
534
|
+
stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
|
|
535
|
+
|
|
536
|
+
const memory = makeMemory();
|
|
537
|
+
const config = makeConfig(true);
|
|
538
|
+
const callerText = "What is my preferred editor?";
|
|
539
|
+
const continuationText = "Continue the answer.";
|
|
540
|
+
const routingMessages = makeMessages(callerText);
|
|
541
|
+
const messages: Message[] = [
|
|
542
|
+
...routingMessages,
|
|
543
|
+
{
|
|
544
|
+
role: "assistant",
|
|
545
|
+
content: [{ type: "text", text: "Let me check that." }],
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
role: "user",
|
|
549
|
+
content: [{ type: "text", text: continuationText }],
|
|
550
|
+
},
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
const result = await memory.prepareMemory(
|
|
554
|
+
messages,
|
|
555
|
+
config,
|
|
556
|
+
new AbortController().signal,
|
|
557
|
+
noopEvent,
|
|
558
|
+
routingMessages,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
expect(generateSparseEmbeddingMock.mock.calls).toContainEqual([callerText]);
|
|
562
|
+
expect(generateSparseEmbeddingMock.mock.calls).not.toContainEqual([
|
|
563
|
+
continuationText,
|
|
564
|
+
]);
|
|
565
|
+
const tail = result.runMessages[result.runMessages.length - 1];
|
|
566
|
+
expect(tail?.role).toBe("user");
|
|
567
|
+
expect(tail?.content).toContainEqual({
|
|
568
|
+
type: "text",
|
|
569
|
+
text: continuationText,
|
|
570
|
+
});
|
|
571
|
+
expect(tail?.content[0]).toEqual(expect.objectContaining({ type: "text" }));
|
|
572
|
+
if (tail?.content[0]?.type !== "text") {
|
|
573
|
+
throw new Error("unexpected block type");
|
|
574
|
+
}
|
|
575
|
+
expect(tail.content[0].text).toContain("# memory/concepts/alice-vscode.md");
|
|
576
|
+
});
|
|
577
|
+
|
|
501
578
|
test("config on with empty Qdrant hits → no retrieved concepts in the v2 block, v1 fallback skipped", async () => {
|
|
502
579
|
// No `stageTurn` call — every channel returns `{ points: [] }` so the
|
|
503
580
|
// candidate set is empty and retrieval contributes nothing. Always-candidate
|