@vellumai/assistant 0.10.4-dev.202607011113.fffc936 → 0.10.4-dev.202607011307.b79a0c5

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/openapi.yaml CHANGED
@@ -18137,6 +18137,12 @@ paths:
18137
18137
  - low
18138
18138
  - medium
18139
18139
  - high
18140
+ hidden:
18141
+ description:
18142
+ When true, persist the user message but suppress it from the UI transcript (it stays in LLM-side history
18143
+ and still drives the turn). Used to prime a proactive assistant greeting without showing the
18144
+ triggering user message. Honored on the standard send path only.
18145
+ type: boolean
18140
18146
  onboarding:
18141
18147
  type: object
18142
18148
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.4-dev.202607011113.fffc936",
3
+ "version": "0.10.4-dev.202607011307.b79a0c5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -323,6 +323,90 @@ describe("handleSendMessage canonical guardian reply interception", () => {
323
323
  expect(runAgentLoop).toHaveBeenCalledTimes(1);
324
324
  });
325
325
 
326
+ test("hidden:true persists hidden metadata and still runs the turn", async () => {
327
+ listPendingByDestinationMock.mockReturnValue([]);
328
+ listCanonicalMock.mockReturnValue([]);
329
+ routeGuardianReplyMock.mockResolvedValue({
330
+ consumed: false,
331
+ decisionApplied: false,
332
+ type: "not_consumed",
333
+ });
334
+
335
+ const persistUserMessage = mock(async () => ({
336
+ id: "persisted-user-id",
337
+ deduplicated: false,
338
+ }));
339
+ const runAgentLoop = mock(async () => undefined);
340
+ const session = {
341
+ setTrustContext: () => {},
342
+ updateClient: () => {},
343
+ emitConfirmationStateChanged: () => {},
344
+ emitActivityState: () => {},
345
+ setTurnChannelContext: () => {},
346
+ setTurnInterfaceContext: () => {},
347
+ ensureActorScopedHistory: async () => {},
348
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
349
+ isProcessing: () => false,
350
+ hasAnyPendingConfirmation: () => false,
351
+ denyAllPendingConfirmations: () => {},
352
+ enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
353
+ persistUserMessage,
354
+ runAgentLoop,
355
+ getMessages: () => [] as unknown[],
356
+ assistantId: "self",
357
+ trustContext: undefined,
358
+ hasPendingConfirmation: () => false,
359
+ setHostBrowserProxy: () => {},
360
+ setHostCuProxy: () => {},
361
+ setHostAppControlProxy: () => {},
362
+ restoreBrowserProxyAvailability: () => {},
363
+ addPreactivatedSkillId: () => {},
364
+ } as unknown as import("../daemon/conversation.js").Conversation;
365
+
366
+ const req = new Request("http://localhost/v1/messages", {
367
+ method: "POST",
368
+ headers: {
369
+ "Content-Type": "application/json",
370
+ "x-vellum-actor-principal-id": "test-user",
371
+ "x-vellum-principal-type": "actor",
372
+ },
373
+ body: JSON.stringify({
374
+ conversationKey: "guardian-conversation-key",
375
+ content: "Hey, how are you? Show me what you can do.",
376
+ sourceChannel: "vellum",
377
+ interface: "web",
378
+ hidden: true,
379
+ }),
380
+ });
381
+
382
+ const res = await callHandler(
383
+ (args) =>
384
+ handleSendMessage(args, {
385
+ sendMessageDeps: {
386
+ getOrCreateConversation: async () => session,
387
+ assistantEventHub: { publish: async () => {} } as any,
388
+ resolveAttachments: () => [],
389
+ },
390
+ }),
391
+ req,
392
+ undefined,
393
+ 202,
394
+ );
395
+
396
+ expect(res.status).toBe(202);
397
+ // The hidden message is persisted with metadata.hidden so the list-messages
398
+ // filter suppresses it from the UI transcript while keeping it in LLM-side
399
+ // history (see list-messages-hidden-metadata.test.ts for the filter).
400
+ expect(persistUserMessage).toHaveBeenCalledTimes(1);
401
+ const persistArgs = (persistUserMessage as any).mock.calls[0][0] as {
402
+ metadata?: { hidden?: boolean };
403
+ };
404
+ expect(persistArgs.metadata?.hidden).toBe(true);
405
+ // The turn still runs — the assistant's reply streams normally, so the chat
406
+ // reads as a proactive greeting.
407
+ expect(runAgentLoop).toHaveBeenCalledTimes(1);
408
+ });
409
+
326
410
  test("excludes stale tool_approval hints without a live pending confirmation", async () => {
327
411
  listPendingByDestinationMock.mockReturnValue([
328
412
  { id: "tool-approval-live", kind: "tool_approval" },
@@ -873,12 +873,9 @@ export async function runDaemon(): Promise<void> {
873
873
  writePid(process.pid);
874
874
 
875
875
  // Install the `assistant` CLI symlink idempotently on every daemon start.
876
- // Non-blocking failures are logged but don't affect startup.
877
- try {
878
- installAssistantSymlink();
879
- } catch (err) {
880
- log.warn({ err }, "Assistant symlink installation failed — continuing");
881
- }
876
+ // Best-effort and self-contained: every step swallows its own errors, so a
877
+ // failure never affects startup.
878
+ installAssistantSymlink();
882
879
 
883
880
  startEmbeddingRuntimeManager();
884
881
 
@@ -0,0 +1,12 @@
1
+ import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+
3
+ export const conversationGroups = sqliteTable("conversation_groups", {
4
+ id: text("id").primaryKey(),
5
+ name: text("name").notNull(),
6
+ sortPosition: real("sort_position").notNull().default(0),
7
+ isSystemGroup: integer("is_system_group", { mode: "boolean" })
8
+ .notNull()
9
+ .default(false),
10
+ createdAt: integer("created_at").notNull(),
11
+ updatedAt: integer("updated_at").notNull(),
12
+ });
@@ -0,0 +1,65 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ primaryKey,
5
+ sqliteTable,
6
+ text,
7
+ } from "drizzle-orm/sqlite-core";
8
+
9
+ import { conversations } from "./conversations.js";
10
+
11
+ export const documents = sqliteTable("documents", {
12
+ surfaceId: text("surface_id").primaryKey(),
13
+ conversationId: text("conversation_id")
14
+ .notNull()
15
+ .references(() => conversations.id, { onDelete: "cascade" }),
16
+ title: text("title").notNull(),
17
+ content: text("content").notNull(),
18
+ wordCount: integer("word_count").notNull().default(0),
19
+ createdAt: integer("created_at").notNull(),
20
+ updatedAt: integer("updated_at").notNull(),
21
+ });
22
+
23
+ // Junction table mapping a document surface to every conversation it appears in.
24
+ export const documentConversations = sqliteTable(
25
+ "document_conversations",
26
+ {
27
+ surfaceId: text("surface_id")
28
+ .notNull()
29
+ .references(() => documents.surfaceId, { onDelete: "cascade" }),
30
+ conversationId: text("conversation_id").notNull(),
31
+ createdAt: integer("created_at").notNull(),
32
+ },
33
+ (table) => [
34
+ primaryKey({ columns: [table.surfaceId, table.conversationId] }),
35
+ index("idx_doc_conv_conversation_id").on(table.conversationId),
36
+ ],
37
+ );
38
+
39
+ export const documentComments = sqliteTable(
40
+ "document_comments",
41
+ {
42
+ id: text("id").primaryKey(),
43
+ surfaceId: text("surface_id")
44
+ .notNull()
45
+ .references(() => documents.surfaceId, { onDelete: "cascade" }),
46
+ conversationId: text("conversation_id").notNull(),
47
+ author: text("author").notNull(),
48
+ content: text("content").notNull(),
49
+ anchorStart: integer("anchor_start"),
50
+ anchorEnd: integer("anchor_end"),
51
+ anchorText: text("anchor_text"),
52
+ // Self-referential parent link (threaded replies). The FK to
53
+ // document_comments(id) ON DELETE CASCADE is enforced at the DB level.
54
+ parentCommentId: text("parent_comment_id"),
55
+ status: text("status").notNull().default("open"),
56
+ resolvedBy: text("resolved_by"),
57
+ resolvedAt: integer("resolved_at"),
58
+ createdAt: integer("created_at").notNull(),
59
+ updatedAt: integer("updated_at").notNull(),
60
+ },
61
+ (table) => [
62
+ index("idx_document_comments_surface").on(table.surfaceId),
63
+ index("idx_document_comments_parent").on(table.parentCommentId),
64
+ ],
65
+ );
@@ -3,12 +3,16 @@ export * from "./acp.js";
3
3
  export * from "./bookmarks.js";
4
4
  export * from "./calls.js";
5
5
  export * from "./contacts.js";
6
+ export * from "./conversation-groups.js";
6
7
  export * from "./conversations.js";
8
+ export * from "./documents.js";
7
9
  export * from "./guardian.js";
8
10
  export * from "./inference.js";
9
11
  export * from "./infrastructure.js";
10
12
  export * from "./memory-core.js";
11
13
  export * from "./memory-graph.js";
14
+ export * from "./memory-injection.js";
12
15
  export * from "./notifications.js";
13
16
  export * from "./oauth.js";
14
17
  export * from "./tasks.js";
18
+ export * from "./workflows.js";
@@ -0,0 +1,62 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ primaryKey,
5
+ sqliteTable,
6
+ text,
7
+ } from "drizzle-orm/sqlite-core";
8
+
9
+ // Time-series of memory-v2 card injections, used by the router to decay a
10
+ // concept's recent injection pressure.
11
+ export const memoryV2InjectionEvents = sqliteTable(
12
+ "memory_v2_injection_events",
13
+ {
14
+ id: integer("id").primaryKey(),
15
+ slug: text("slug").notNull(),
16
+ injectedAt: integer("injected_at").notNull(),
17
+ },
18
+ (table) => [
19
+ index("idx_memory_v2_injection_events_slug_time").on(
20
+ table.slug,
21
+ table.injectedAt,
22
+ ),
23
+ index("idx_memory_v2_injection_events_time").on(table.injectedAt),
24
+ ],
25
+ );
26
+
27
+ // Per-conversation record of every memory-v3 card ever injected, with a
28
+ // pruned_at tombstone so re-injection can be suppressed after pruning.
29
+ export const memoryV3EverInjected = sqliteTable(
30
+ "memory_v3_ever_injected",
31
+ {
32
+ conversationId: text("conversation_id").notNull(),
33
+ slug: text("slug").notNull(),
34
+ injectedAt: integer("injected_at").notNull(),
35
+ bytes: integer("bytes").notNull().default(0),
36
+ prunedAt: integer("pruned_at"),
37
+ },
38
+ (table) => [
39
+ primaryKey({ columns: [table.conversationId, table.slug] }),
40
+ index("idx_memory_v3_ever_injected_conv").on(table.conversationId),
41
+ ],
42
+ );
43
+
44
+ // Per-turn log of which memory-v3 cards were selected, with lane attribution.
45
+ export const memoryV3Selections = sqliteTable(
46
+ "memory_v3_selections",
47
+ {
48
+ conversationId: text("conversation_id").notNull(),
49
+ turn: integer("turn").notNull(),
50
+ slug: text("slug").notNull(),
51
+ source: text("source").notNull(),
52
+ pinned: integer("pinned").notNull().default(0),
53
+ createdAt: integer("created_at").notNull(),
54
+ messageId: text("message_id"),
55
+ sectionOrdinal: integer("section_ordinal"),
56
+ sectionTitle: text("section_title"),
57
+ },
58
+ (table) => [
59
+ primaryKey({ columns: [table.conversationId, table.turn, table.slug] }),
60
+ index("idx_memory_v3_selections_conv").on(table.conversationId, table.turn),
61
+ ],
62
+ );
@@ -0,0 +1,55 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ primaryKey,
5
+ sqliteTable,
6
+ text,
7
+ } from "drizzle-orm/sqlite-core";
8
+
9
+ export const workflowRuns = sqliteTable(
10
+ "workflow_runs",
11
+ {
12
+ id: text("id").primaryKey(),
13
+ name: text("name"),
14
+ scriptSource: text("script_source").notNull(),
15
+ scriptHash: text("script_hash").notNull(),
16
+ argsJson: text("args_json"),
17
+ capabilitiesJson: text("capabilities_json"),
18
+ status: text("status").notNull(),
19
+ conversationId: text("conversation_id"),
20
+ agentsSpawned: integer("agents_spawned").notNull().default(0),
21
+ inputTokens: integer("input_tokens").notNull().default(0),
22
+ outputTokens: integer("output_tokens").notNull().default(0),
23
+ resultJson: text("result_json"),
24
+ error: text("error"),
25
+ createdAt: integer("created_at"),
26
+ updatedAt: integer("updated_at"),
27
+ finishedAt: integer("finished_at"),
28
+ trustJson: text("trust_json"),
29
+ },
30
+ (table) => [
31
+ index("idx_workflow_runs_status_created_at").on(
32
+ table.status,
33
+ table.createdAt,
34
+ ),
35
+ ],
36
+ );
37
+
38
+ // Append-only journal of each workflow's agent() calls, keyed by run + sequence
39
+ // for crash-recovery replay.
40
+ export const workflowJournal = sqliteTable(
41
+ "workflow_journal",
42
+ {
43
+ runId: text("run_id").notNull(),
44
+ seq: integer("seq").notNull(),
45
+ callHash: text("call_hash").notNull(),
46
+ kind: text("kind").notNull(),
47
+ requestJson: text("request_json"),
48
+ resultJson: text("result_json"),
49
+ status: text("status").notNull(),
50
+ createdAt: integer("created_at"),
51
+ inputTokens: integer("input_tokens"),
52
+ outputTokens: integer("output_tokens"),
53
+ },
54
+ (table) => [primaryKey({ columns: [table.runId, table.seq] })],
55
+ );
@@ -1275,6 +1275,11 @@ export async function handleSendMessage(
1275
1275
  interface?: string;
1276
1276
  conversationType?: string;
1277
1277
  automated?: boolean;
1278
+ // Persist the user message but suppress it from the UI transcript (kept in
1279
+ // LLM history). Used by flows like research-onboarding's "Let's chat"
1280
+ // handoff to prime a proactive assistant greeting without showing the
1281
+ // triggering user message. Honored on the standard send path only.
1282
+ hidden?: boolean;
1278
1283
  bypassSecretCheck?: boolean;
1279
1284
  hostHomeDir?: string;
1280
1285
  hostUsername?: string;
@@ -2356,7 +2361,13 @@ export async function handleSendMessage(
2356
2361
  content: resolvedContent,
2357
2362
  attachments,
2358
2363
  requestId,
2359
- metadata: body.automated === true ? { automated: true } : undefined,
2364
+ metadata:
2365
+ body.automated === true || body.hidden === true
2366
+ ? {
2367
+ ...(body.automated === true ? { automated: true } : {}),
2368
+ ...(body.hidden === true ? { hidden: true } : {}),
2369
+ }
2370
+ : undefined,
2360
2371
  clientMessageId,
2361
2372
  });
2362
2373
 
@@ -2370,14 +2381,20 @@ export async function handleSendMessage(
2370
2381
  };
2371
2382
  }
2372
2383
 
2373
- broadcastMessage({
2374
- type: "user_message_echo",
2375
- text: resolvedContent,
2376
- conversationId: mapping.conversationId,
2377
- messageId,
2378
- requestId,
2379
- clientMessageId,
2380
- });
2384
+ // A hidden message is suppressed from the UI transcript: don't echo it back
2385
+ // to clients (the echo would render a user bubble the list-messages filter
2386
+ // otherwise hides). The turn still runs below, and the assistant's reply
2387
+ // streams normally — so the chat reads as a proactive greeting.
2388
+ if (body.hidden !== true) {
2389
+ broadcastMessage({
2390
+ type: "user_message_echo",
2391
+ text: resolvedContent,
2392
+ conversationId: mapping.conversationId,
2393
+ messageId,
2394
+ requestId,
2395
+ clientMessageId,
2396
+ });
2397
+ }
2381
2398
  publishConversationMessagesChanged(mapping.conversationId, originClientId);
2382
2399
 
2383
2400
  // Fire-and-forget the agent loop; events flow to the hub via broadcastMessage.
@@ -2890,6 +2907,12 @@ export const ROUTES: RouteDefinition[] = [
2890
2907
  .optional(),
2891
2908
  inferenceProfile: z.string().nullable().optional(),
2892
2909
  riskThreshold: z.enum(VALID_RISK_THRESHOLDS).optional(),
2910
+ hidden: z
2911
+ .boolean()
2912
+ .optional()
2913
+ .describe(
2914
+ "When true, persist the user message but suppress it from the UI transcript (it stays in LLM-side history and still drives the turn). Used to prime a proactive assistant greeting without showing the triggering user message. Honored on the standard send path only.",
2915
+ ),
2893
2916
  onboarding: z
2894
2917
  .object({
2895
2918
  tools: z.array(z.string()),