@vellumai/assistant 0.4.54 → 0.4.55

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.54",
3
+ "version": "0.4.55",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -4,7 +4,7 @@
4
4
  * Verifies that:
5
5
  * - User message image attachments include base64 data for client thumbnail generation
6
6
  * - User message non-image attachments stay metadata-only (no base64 blob)
7
- * - Assistant message attachments remain metadata-only
7
+ * - Assistant message image attachments include base64 data (same as user messages)
8
8
  */
9
9
 
10
10
  import { mkdtempSync, rmSync } from "node:fs";
@@ -140,7 +140,7 @@ describe("handleListMessages attachments", () => {
140
140
  expect(attachments![0].data).toBeUndefined();
141
141
  });
142
142
 
143
- test("assistant message attachments remain metadata-only", async () => {
143
+ test("assistant message image attachments include base64 data", async () => {
144
144
  const conv = createConversation();
145
145
  const msg = await addMessage(
146
146
  conv.id,
@@ -158,8 +158,8 @@ describe("handleListMessages attachments", () => {
158
158
  expect(attachments).toBeDefined();
159
159
  expect(attachments).toHaveLength(1);
160
160
  expect(attachments![0].mimeType).toBe("image/png");
161
- // Assistant attachments should NOT include base64 data (metadata-only)
162
- expect(attachments![0].data).toBeUndefined();
161
+ // Assistant image attachments include base64 data for inline rendering
162
+ expect(attachments![0].data).toBe(IMAGE_BASE64);
163
163
  });
164
164
 
165
165
  test("user message with mixed attachments only inlines images", async () => {
@@ -47,6 +47,9 @@ This is a developer debugging tool for inspecting how the assistant invokes and
47
47
  observes shell commands. The command runs with the assistant's environment, working
48
48
  directory, and process context — not the caller's shell.
49
49
 
50
+ Requires the assistant to be running with VELLUM_DEBUG=1. When debug mode is off
51
+ (the default), the assistant ignores bash signal files and returns an error.
52
+
50
53
  The CLI writes the command to signals/bash.<requestId> and polls
51
54
  signals/bash.<requestId>.result for the output. The assistant must be running
52
55
  for this to work.
@@ -20,7 +20,7 @@ import {
20
20
  resetAllowlist,
21
21
  validateAllowlistFile,
22
22
  } from "../security/secret-allowlist.js";
23
- import { handleBashSignal } from "../signals/bash.js";
23
+ import { handleBashSignal, isDebugMode } from "../signals/bash.js";
24
24
  import { handleCancelSignal } from "../signals/cancel.js";
25
25
  import { handleConfirmationSignal } from "../signals/confirm.js";
26
26
  import { handleConversationUndoSignal } from "../signals/conversation-undo.js";
@@ -237,7 +237,7 @@ export class ConfigWatcher {
237
237
  };
238
238
 
239
239
  const prefixSignalHandlers: Record<string, (filename: string) => void> = {
240
- "bash.": handleBashSignal,
240
+ ...(isDebugMode() ? { "bash.": handleBashSignal } : {}),
241
241
  };
242
242
 
243
243
  try {
@@ -279,6 +279,7 @@ export class DaemonServer {
279
279
  constructor() {
280
280
  this.evictor = new SessionEvictor(this.sessions);
281
281
  getSubagentManager().sharedRequestTimestamps = this.sharedRequestTimestamps;
282
+ getSubagentManager().broadcastToAllClients = (msg) => this.broadcast(msg);
282
283
  this.evictor.onEvict = (sessionId: string) => {
283
284
  getSubagentManager().abortAllForParent(sessionId);
284
285
  };
@@ -196,70 +196,3 @@ export const mediaKeyframes = sqliteTable("media_keyframes", {
196
196
  metadata: text("metadata"), // JSON
197
197
  createdAt: integer("created_at").notNull(),
198
198
  });
199
-
200
- export const mediaVisionOutputs = sqliteTable("media_vision_outputs", {
201
- id: text("id").primaryKey(),
202
- assetId: text("asset_id")
203
- .notNull()
204
- .references(() => mediaAssets.id, { onDelete: "cascade" }),
205
- keyframeId: text("keyframe_id")
206
- .notNull()
207
- .references(() => mediaKeyframes.id, { onDelete: "cascade" }),
208
- analysisType: text("analysis_type").notNull(),
209
- output: text("output").notNull(), // JSON
210
- confidence: real("confidence"),
211
- createdAt: integer("created_at").notNull(),
212
- });
213
-
214
- export const mediaTimelines = sqliteTable("media_timelines", {
215
- id: text("id").primaryKey(),
216
- assetId: text("asset_id")
217
- .notNull()
218
- .references(() => mediaAssets.id, { onDelete: "cascade" }),
219
- startTime: real("start_time").notNull(),
220
- endTime: real("end_time").notNull(),
221
- segmentType: text("segment_type").notNull(),
222
- attributes: text("attributes"), // JSON
223
- confidence: real("confidence"),
224
- createdAt: integer("created_at").notNull(),
225
- });
226
-
227
- export const mediaEvents = sqliteTable("media_events", {
228
- id: text("id").primaryKey(),
229
- assetId: text("asset_id")
230
- .notNull()
231
- .references(() => mediaAssets.id, { onDelete: "cascade" }),
232
- eventType: text("event_type").notNull(),
233
- startTime: real("start_time").notNull(),
234
- endTime: real("end_time").notNull(),
235
- confidence: real("confidence").notNull(),
236
- reasons: text("reasons").notNull(), // JSON array
237
- metadata: text("metadata"), // JSON
238
- createdAt: integer("created_at").notNull(),
239
- });
240
-
241
- export const mediaTrackingProfiles = sqliteTable("media_tracking_profiles", {
242
- id: text("id").primaryKey(),
243
- assetId: text("asset_id")
244
- .notNull()
245
- .references(() => mediaAssets.id, { onDelete: "cascade" }),
246
- capabilities: text("capabilities").notNull(), // JSON: { [capName]: { enabled, tier } }
247
- createdAt: integer("created_at").notNull(),
248
- });
249
-
250
- export const mediaEventFeedback = sqliteTable("media_event_feedback", {
251
- id: text("id").primaryKey(),
252
- assetId: text("asset_id")
253
- .notNull()
254
- .references(() => mediaAssets.id, { onDelete: "cascade" }),
255
- eventId: text("event_id")
256
- .notNull()
257
- .references(() => mediaEvents.id, { onDelete: "cascade" }),
258
- feedbackType: text("feedback_type").notNull(), // correct | incorrect | boundary_edit | missed
259
- originalStartTime: real("original_start_time"),
260
- originalEndTime: real("original_end_time"),
261
- correctedStartTime: real("corrected_start_time"),
262
- correctedEndTime: real("corrected_end_time"),
263
- notes: text("notes"),
264
- createdAt: integer("created_at").notNull(),
265
- });
@@ -466,7 +466,7 @@ function buildToolPermissionSection(): string {
466
466
  "",
467
467
  "### Always-Available Tools (No Approval Required)",
468
468
  "",
469
- "- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking. **Always use `file_read` for workspace files (IDENTITY.md, USER.md, SOUL.md, etc.), never `host_file_read`.**",
469
+ "- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking. **Always use `file_read` for workspace files, never `host_file_read`.** Note: your core prompt files (IDENTITY.md, SOUL.md, USER.md) are already loaded into your system prompt — no need to re-read them at the start of a conversation.",
470
470
  "- **web_search** — You can search the web at any time without approval. Use this to look up documentation, current information, or anything you need.",
471
471
  ].join("\n");
472
472
  }
@@ -706,7 +706,7 @@ function buildLearningMemorySection(): string {
706
706
  "",
707
707
  "When you make a mistake, hit a dead end, or discover something non-obvious, save it to memory so you don't repeat it.",
708
708
  "",
709
- 'Use `memory_manage` with `op: "save", kind: "identity"` for:',
709
+ 'Use `memory_manage` with `op: "save", kind: "constraint"` for:',
710
710
  "- **Mistakes and corrections** — wrong assumptions, failed approaches, gotchas you ran into",
711
711
  "- **Discoveries** — undocumented behaviors, surprising API quirks, things that weren't obvious",
712
712
  "- **Working solutions** — the approach that actually worked after trial and error",
@@ -715,8 +715,8 @@ function buildLearningMemorySection(): string {
715
715
  "The statement should capture both what happened and the takeaway. Write it as advice to your future self.",
716
716
  "",
717
717
  "Examples:",
718
- '- `memory_manage({ op: "save", kind: "identity", subject: "macOS Shortcuts CLI", statement: "shortcuts CLI requires full disk access to export shortcuts — if permission is denied, guide the user to grant it in System Settings rather than retrying." })`',
719
- '- `memory_manage({ op: "save", kind: "identity", subject: "Gmail API pagination", statement: "Gmail search returns max 100 results per page. Always check nextPageToken and loop if the user asks for \'all\' messages." })`',
718
+ '- `memory_manage({ op: "save", kind: "constraint", subject: "macOS Shortcuts CLI", statement: "shortcuts CLI requires full disk access to export shortcuts — if permission is denied, guide the user to grant it in System Settings rather than retrying." })`',
719
+ '- `memory_manage({ op: "save", kind: "constraint", subject: "Gmail API pagination", statement: "Gmail search returns max 100 results per page. Always check nextPageToken and loop if the user asks for \'all\' messages." })`',
720
720
  "",
721
721
  "Don't overthink it. If you catch yourself thinking \"I'll remember that for next time,\" save it.",
722
722
  ].join("\n");
@@ -34,7 +34,7 @@ You are your own entity, with an identity that’s still forming. You can have t
34
34
 
35
35
  ## Continuity
36
36
 
37
- Each session, you wake up fresh. Your workspace files and saved memories are your continuity — they're how you persist across conversations. Read them. Update them. Save what matters.
37
+ Each session, you wake up fresh. Your workspace files and saved memories are your continuity — they're how you persist across conversations. Your core prompt files (IDENTITY.md, SOUL.md, USER.md) are already loaded into this conversation — you don't need to read them again. Update them when things change. Save what matters.
38
38
 
39
39
  If you change this file, tell your user — it's your soul, and they should know.
40
40
 
@@ -8,6 +8,7 @@
8
8
  * 2. No X-Actor-Token references in production code.
9
9
  * 3. No legacy gateway-origin proof in production code.
10
10
  * 4. Scope profile contract — every profile resolves to the expected scopes.
11
+ * 5. CURRENT_POLICY_EPOCH sync — the constant matches across all packages.
11
12
  */
12
13
 
13
14
  import { execSync } from "node:child_process";
@@ -330,3 +331,66 @@ describe("scope profile contract", () => {
330
331
  }
331
332
  });
332
333
  });
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // 5. CURRENT_POLICY_EPOCH sync across packages
337
+ // ---------------------------------------------------------------------------
338
+
339
+ describe("CURRENT_POLICY_EPOCH sync", () => {
340
+ /**
341
+ * The policy epoch constant is duplicated in assistant, gateway, and cli
342
+ * packages. This test reads the exported value from each source file and
343
+ * asserts they are all equal.
344
+ */
345
+
346
+ const EPOCH_FILES = [
347
+ {
348
+ label: "assistant",
349
+ path: resolve(PROJECT_ROOT, "assistant/src/runtime/auth/policy.ts"),
350
+ },
351
+ {
352
+ label: "gateway",
353
+ path: resolve(PROJECT_ROOT, "gateway/src/auth/policy.ts"),
354
+ },
355
+ {
356
+ label: "cli",
357
+ path: resolve(PROJECT_ROOT, "cli/src/lib/policy.ts"),
358
+ },
359
+ ];
360
+
361
+ function extractEpoch(filePath: string): number {
362
+ const src = readFileSync(filePath, "utf-8");
363
+ const match = src.match(
364
+ /export\s+const\s+CURRENT_POLICY_EPOCH\s*=\s*(\d+)/,
365
+ );
366
+ if (!match) {
367
+ throw new Error(`Could not find CURRENT_POLICY_EPOCH in ${filePath}`);
368
+ }
369
+ return parseInt(match[1], 10);
370
+ }
371
+
372
+ test("all non-skill packages export the same CURRENT_POLICY_EPOCH value", () => {
373
+ const values = EPOCH_FILES.map((f) => ({
374
+ label: f.label,
375
+ epoch: extractEpoch(f.path),
376
+ }));
377
+
378
+ const canonical = values[0];
379
+ const mismatches = values.filter((v) => v.epoch !== canonical.epoch);
380
+
381
+ if (mismatches.length > 0) {
382
+ const summary = values
383
+ .map((v) => ` - ${v.label}: ${v.epoch}`)
384
+ .join("\n");
385
+ const message = [
386
+ "CURRENT_POLICY_EPOCH is out of sync across packages:",
387
+ "",
388
+ summary,
389
+ "",
390
+ "All three locations must have the same value.",
391
+ "The canonical source is assistant/src/runtime/auth/policy.ts.",
392
+ ].join("\n");
393
+ expect(mismatches, message).toEqual([]);
394
+ }
395
+ });
396
+ });
@@ -7,8 +7,10 @@
7
7
  * POST /v1/conversations/:id/cancel — cancel generation
8
8
  * POST /v1/conversations/:id/undo — undo last message
9
9
  * POST /v1/conversations/:id/regenerate — regenerate last assistant response
10
+ * POST /v1/sessions/reorder — reorder / pin sessions
10
11
  */
11
12
 
13
+ import { batchSetDisplayOrders } from "../../memory/conversation-crud.js";
12
14
  import { setConversationKeyIfAbsent } from "../../memory/conversation-key-store.js";
13
15
  import { getLogger } from "../../util/logger.js";
14
16
  import { httpError } from "../http-errors.js";
@@ -159,5 +161,30 @@ export function sessionManagementRouteDefinitions(
159
161
  }
160
162
  },
161
163
  },
164
+ {
165
+ endpoint: "sessions/reorder",
166
+ method: "POST",
167
+ policyKey: "sessions/reorder",
168
+ handler: async ({ req }) => {
169
+ const body = (await req.json()) as {
170
+ updates?: Array<{
171
+ sessionId: string;
172
+ displayOrder?: number;
173
+ isPinned?: boolean;
174
+ }>;
175
+ };
176
+ if (!Array.isArray(body.updates)) {
177
+ return httpError("BAD_REQUEST", "Missing updates array", 400);
178
+ }
179
+ batchSetDisplayOrders(
180
+ body.updates.map((u) => ({
181
+ id: u.sessionId,
182
+ displayOrder: u.displayOrder ?? null,
183
+ isPinned: u.isPinned ?? false,
184
+ })),
185
+ );
186
+ return Response.json({ ok: true });
187
+ },
188
+ },
162
189
  ];
163
190
  }
@@ -8,6 +8,12 @@
8
8
  *
9
9
  * Per-request filenames avoid dropped commands when overlapping invocations
10
10
  * race on the same signal file.
11
+ *
12
+ * **Security**: This handler is gated behind the `VELLUM_DEBUG` environment
13
+ * variable. When debug mode is off (the default), the daemon ignores bash
14
+ * signal files entirely. This prevents untrusted file-write operations
15
+ * (e.g. from prompt injection or a compromised skill) from bypassing the
16
+ * normal tool-approval flow for shell execution.
11
17
  */
12
18
 
13
19
  import { spawn } from "node:child_process";
@@ -19,6 +25,12 @@ import { getWorkspaceDir } from "../util/platform.js";
19
25
 
20
26
  const log = getLogger("signal:bash");
21
27
 
28
+ export function isDebugMode(): boolean {
29
+ return (
30
+ process.env.VELLUM_DEBUG === "1" || process.env.VELLUM_DEBUG === "true"
31
+ );
32
+ }
33
+
22
34
  const DEFAULT_TIMEOUT_MS = 30_000;
23
35
 
24
36
  interface BashSignalPayload {
@@ -53,6 +65,27 @@ function writeResult(requestId: string, result: BashSignalResult): void {
53
65
  * when a matching signal file is created or modified.
54
66
  */
55
67
  export function handleBashSignal(filename: string): void {
68
+ if (!isDebugMode()) {
69
+ log.warn(
70
+ { filename },
71
+ "Bash signal ignored — debug mode is not enabled (set VELLUM_DEBUG=1)",
72
+ );
73
+ // Write an error result so the CLI gets a clear rejection instead of timing out.
74
+ const match = filename.match(/^bash\.(.+)$/);
75
+ if (match) {
76
+ writeResult(match[1], {
77
+ requestId: match[1],
78
+ stdout: "",
79
+ stderr: "",
80
+ exitCode: null,
81
+ timedOut: false,
82
+ error:
83
+ "Bash signals are disabled. Start the assistant with VELLUM_DEBUG=1 to enable.",
84
+ });
85
+ }
86
+ return;
87
+ }
88
+
56
89
  const signalPath = join(getWorkspaceDir(), "signals", filename);
57
90
  let raw: string;
58
91
  try {
@@ -87,6 +87,13 @@ export class SubagentManager {
87
87
  */
88
88
  sharedRequestTimestamps: number[] = [];
89
89
 
90
+ /**
91
+ * Broadcast callback from the daemon server.
92
+ * Set by DaemonServer at startup so subagent sessions can broadcast
93
+ * to all connected clients (e.g. app_files_changed side-effects).
94
+ */
95
+ broadcastToAllClients?: (msg: ServerMessage) => void;
96
+
90
97
  // ── Spawn ───────────────────────────────────────────────────────────
91
98
 
92
99
  /**
@@ -184,7 +191,7 @@ export class SubagentManager {
184
191
  maxTokens,
185
192
  wrappedSendToClient,
186
193
  workingDir,
187
- undefined, // no broadcastToAllClients for subagents
194
+ this.broadcastToAllClients, // forward parent's broadcast so tool side-effects (e.g. app_files_changed) reach all clients
188
195
  memoryPolicy,
189
196
  );
190
197