@vellumai/assistant 0.5.5 → 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.
Files changed (102) hide show
  1. package/Dockerfile +3 -4
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-token-service.test.ts +113 -0
  4. package/src/__tests__/config-schema.test.ts +2 -2
  5. package/src/__tests__/context-window-manager.test.ts +78 -0
  6. package/src/__tests__/conversation-title-service.test.ts +30 -1
  7. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  8. package/src/__tests__/memory-regressions.test.ts +8 -30
  9. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  10. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  11. package/src/__tests__/tool-executor.test.ts +4 -0
  12. package/src/cli/commands/conversations.ts +0 -18
  13. package/src/config/env.ts +8 -2
  14. package/src/config/feature-flag-registry.json +0 -8
  15. package/src/config/schema.ts +0 -12
  16. package/src/config/schemas/memory.ts +0 -4
  17. package/src/config/schemas/platform.ts +1 -1
  18. package/src/config/schemas/security.ts +4 -0
  19. package/src/context/window-manager.ts +53 -2
  20. package/src/daemon/config-watcher.ts +1 -4
  21. package/src/daemon/conversation-agent-loop.ts +0 -60
  22. package/src/daemon/conversation-memory.ts +0 -117
  23. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  24. package/src/daemon/handlers/conversations.ts +0 -11
  25. package/src/daemon/lifecycle.ts +3 -46
  26. package/src/followups/followup-store.ts +5 -2
  27. package/src/memory/conversation-crud.ts +0 -236
  28. package/src/memory/conversation-title-service.ts +26 -10
  29. package/src/memory/db-init.ts +5 -13
  30. package/src/memory/indexer.ts +15 -106
  31. package/src/memory/job-handlers/embedding.ts +0 -79
  32. package/src/memory/job-utils.ts +1 -1
  33. package/src/memory/jobs-store.ts +0 -8
  34. package/src/memory/jobs-worker.ts +0 -20
  35. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  36. package/src/memory/migrations/index.ts +1 -3
  37. package/src/memory/qdrant-client.ts +4 -6
  38. package/src/memory/schema/conversations.ts +0 -3
  39. package/src/memory/schema/index.ts +0 -2
  40. package/src/messaging/draft-store.ts +2 -2
  41. package/src/permissions/defaults.ts +3 -3
  42. package/src/permissions/trust-client.ts +2 -13
  43. package/src/permissions/trust-store.ts +8 -3
  44. package/src/runtime/auth/route-policy.ts +14 -0
  45. package/src/runtime/auth/token-service.ts +133 -0
  46. package/src/runtime/http-server.ts +2 -0
  47. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  48. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  49. package/src/runtime/routes/conversation-routes.ts +2 -1
  50. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  51. package/src/runtime/routes/memory-item-routes.ts +124 -2
  52. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  53. package/src/schedule/schedule-store.ts +0 -21
  54. package/src/skills/inline-command-render.ts +5 -1
  55. package/src/skills/inline-command-runner.ts +30 -2
  56. package/src/tools/memory/handlers.ts +1 -129
  57. package/src/tools/permission-checker.ts +18 -0
  58. package/src/tools/skills/load.ts +9 -2
  59. package/src/util/platform.ts +5 -5
  60. package/src/util/xml.ts +8 -0
  61. package/src/workspace/heartbeat-service.ts +5 -24
  62. package/src/__tests__/archive-recall.test.ts +0 -560
  63. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  64. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  65. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  66. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  67. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  68. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  69. package/src/__tests__/memory-brief-time.test.ts +0 -285
  70. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  71. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  72. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  73. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  74. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  75. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  76. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  77. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  78. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  79. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  80. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  81. package/src/__tests__/memory-reducer.test.ts +0 -704
  82. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  83. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  84. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  85. package/src/config/schemas/memory-simplified.ts +0 -101
  86. package/src/memory/archive-recall.ts +0 -516
  87. package/src/memory/archive-store.ts +0 -400
  88. package/src/memory/brief-formatting.ts +0 -33
  89. package/src/memory/brief-open-loops.ts +0 -266
  90. package/src/memory/brief-time.ts +0 -162
  91. package/src/memory/brief.ts +0 -75
  92. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  93. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  94. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  95. package/src/memory/migrations/186-memory-archive.ts +0 -109
  96. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  97. package/src/memory/reducer-scheduler.ts +0 -242
  98. package/src/memory/reducer-store.ts +0 -271
  99. package/src/memory/reducer-types.ts +0 -106
  100. package/src/memory/reducer.ts +0 -467
  101. package/src/memory/schema/memory-archive.ts +0 -121
  102. package/src/memory/schema/memory-brief.ts +0 -55
@@ -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: {
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { getLogger } from "../util/logger.js";
17
+ import { escapeXmlContent } from "../util/xml.js";
17
18
  import type { InlineCommandExpansion } from "./inline-command-expansions.js";
18
19
  import type { InlineCommandResult } from "./inline-command-runner.js";
19
20
  import { runInlineCommand } from "./inline-command-runner.js";
@@ -91,7 +92,10 @@ export async function renderInlineCommands(
91
92
 
92
93
  let replacement: string;
93
94
  if (commandResult.ok) {
94
- replacement = wrapInXml(expansion.placeholderId, commandResult.output);
95
+ replacement = wrapInXml(
96
+ expansion.placeholderId,
97
+ escapeXmlContent(commandResult.output),
98
+ );
95
99
  expandedCount++;
96
100
  } else {
97
101
  const stub = failureReasonToStub(commandResult);
@@ -37,6 +37,15 @@ const DEFAULT_TIMEOUT_MS = 10_000;
37
37
  /** Maximum output characters before truncation. */
38
38
  const MAX_OUTPUT_CHARS = 20_000;
39
39
 
40
+ /**
41
+ * Maximum bytes to buffer from stdout during streaming. Once this limit is
42
+ * reached we stop accepting data so a long-running command (e.g. `yes`) cannot
43
+ * grow memory unbounded before the timeout fires. Set generously above
44
+ * MAX_OUTPUT_CHARS to account for multi-byte UTF-8 and ANSI sequences that will
45
+ * be stripped before the character-level clamp.
46
+ */
47
+ const MAX_STDOUT_BUFFER_BYTES = MAX_OUTPUT_CHARS * 4;
48
+
40
49
  /**
41
50
  * ANSI escape sequence pattern (covers SGR, cursor movement, erase, etc.).
42
51
  * Matches: ESC[ ... final_byte and ESC] ... ST (OSC sequences).
@@ -115,7 +124,9 @@ export async function runInlineCommand(
115
124
 
116
125
  return new Promise<InlineCommandResult>((resolve) => {
117
126
  let timedOut = false;
127
+ let stdoutCapped = false;
118
128
  const stdoutChunks: Buffer[] = [];
129
+ let stdoutBytes = 0;
119
130
 
120
131
  let child: ReturnType<typeof spawn>;
121
132
  try {
@@ -140,7 +151,19 @@ export async function runInlineCommand(
140
151
  child.kill("SIGKILL");
141
152
  }, timeoutMs);
142
153
 
143
- child.stdout!.on("data", (data: Buffer) => stdoutChunks.push(data));
154
+ child.stdout!.on("data", (data: Buffer) => {
155
+ if (stdoutBytes >= MAX_STDOUT_BUFFER_BYTES) return;
156
+ stdoutChunks.push(data);
157
+ stdoutBytes += data.length;
158
+ if (stdoutBytes >= MAX_STDOUT_BUFFER_BYTES) {
159
+ // Stop reading to release backpressure on the child process.
160
+ // This destroys the read end of the pipe, which may cause the
161
+ // child to receive SIGPIPE and exit with code=null. The
162
+ // stdoutCapped flag lets the close handler treat this as success.
163
+ stdoutCapped = true;
164
+ child.stdout!.destroy();
165
+ }
166
+ });
144
167
 
145
168
  child.on("close", (code) => {
146
169
  clearTimeout(timer);
@@ -157,7 +180,12 @@ export async function runInlineCommand(
157
180
  }
158
181
 
159
182
  // ── Non-zero exit ────────────────────────────────────────────────
160
- if (code !== 0) {
183
+ // When stdout was capped we destroyed the read end of the pipe,
184
+ // which typically causes SIGPIPE — the process is killed by the
185
+ // signal so the exit code is null. Only suppress the error in that
186
+ // specific case; a command that outputs a lot but exits with a
187
+ // genuine non-zero code (e.g. exit 1) should still be an error.
188
+ if (code !== 0 && !(stdoutCapped && code == null)) {
161
189
  log.debug(
162
190
  { command, exitCode: code },
163
191
  "Inline command exited with non-zero code",
@@ -2,8 +2,6 @@ import { and, eq, ne } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
4
  import type { AssistantConfig } from "../../config/types.js";
5
- import { buildArchiveRecall } from "../../memory/archive-recall.js";
6
- import { insertObservation } from "../../memory/archive-store.js";
7
5
  import { getDb } from "../../memory/db.js";
8
6
  import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
9
7
  import { enqueueMemoryJob } from "../../memory/jobs-store.js";
@@ -20,7 +18,7 @@ const log = getLogger("memory-tools");
20
18
 
21
19
  export async function handleMemorySave(
22
20
  args: Record<string, unknown>,
23
- config: AssistantConfig,
21
+ _config: AssistantConfig,
24
22
  conversationId: string,
25
23
  messageId: string | undefined,
26
24
  scopeId: string = "default",
@@ -65,19 +63,6 @@ export async function handleMemorySave(
65
63
  ? truncate(args.subject.trim(), 80, "")
66
64
  : inferSubjectFromStatement(statement.trim());
67
65
 
68
- // When simplified memory is enabled, save directly to the simplified
69
- // observation/chunk tables instead of the legacy memory_items table.
70
- if (config.memory.simplified.enabled) {
71
- return handleSimplifiedMemorySave(
72
- kind,
73
- subject,
74
- statement.trim(),
75
- conversationId,
76
- messageId,
77
- scopeId,
78
- );
79
- }
80
-
81
66
  try {
82
67
  const db = getDb();
83
68
  const id = uuid();
@@ -290,12 +275,6 @@ export async function handleMemoryRecall(
290
275
  ? args.scope.trim()
291
276
  : "default";
292
277
 
293
- // When simplified memory is enabled, use the archive recall path
294
- // instead of the legacy hybrid retriever.
295
- if (config.memory.simplified.enabled) {
296
- return handleSimplifiedMemoryRecall(query.trim(), scopeId ?? "default");
297
- }
298
-
299
278
  // Scope policy: "conversation" means strict (only that scope),
300
279
  // anything else allows fallback to the default scope.
301
280
  const scopePolicyOverride: ScopePolicyOverride | undefined = scopeId
@@ -432,113 +411,6 @@ export async function handleMemoryDelete(
432
411
  }
433
412
  }
434
413
 
435
- // ── Simplified memory helpers ────────────────────────────────────────
436
-
437
- /**
438
- * Save a memory item as an observation + chunk in the simplified system.
439
- * This is used when simplified memory is enabled instead of writing to
440
- * the legacy memory_items table.
441
- */
442
- function handleSimplifiedMemorySave(
443
- kind: string,
444
- subject: string,
445
- statement: string,
446
- conversationId: string,
447
- messageId: string | undefined,
448
- scopeId: string,
449
- ): ToolExecutionResult {
450
- try {
451
- const trimmedStatement = truncate(statement, 500, "");
452
- const content = `[${kind}] ${subject}: ${trimmedStatement}`;
453
-
454
- const result = insertObservation({
455
- conversationId,
456
- messageId: messageId ?? null,
457
- role: "user",
458
- content,
459
- scopeId,
460
- modality: "text",
461
- source: "tool:memory_save",
462
- });
463
-
464
- log.debug(
465
- {
466
- observationId: result.observationId,
467
- chunkId: result.chunkId,
468
- kind,
469
- subject,
470
- conversationId,
471
- messageId,
472
- },
473
- "Memory saved via simplified system",
474
- );
475
-
476
- return {
477
- content: `Saved to memory (ID: ${result.observationId}).\nKind: ${kind}\nSubject: ${subject}\nStatement: ${trimmedStatement}`,
478
- isError: false,
479
- };
480
- } catch (err) {
481
- const msg = err instanceof Error ? err.message : String(err);
482
- log.error({ err }, "simplified memory_save failed");
483
- return { content: `Error: Failed to save memory: ${msg}`, isError: true };
484
- }
485
- }
486
-
487
- /**
488
- * Recall memories using the simplified archive recall path instead of
489
- * the legacy hybrid retriever.
490
- */
491
- function handleSimplifiedMemoryRecall(
492
- query: string,
493
- scopeId: string,
494
- ): ToolExecutionResult {
495
- try {
496
- const recallResult = buildArchiveRecall(scopeId, query);
497
-
498
- if (recallResult.bullets.length === 0) {
499
- return {
500
- content: JSON.stringify({
501
- text: "No matching memories found.",
502
- resultCount: 0,
503
- degraded: false,
504
- items: [],
505
- sources: { semantic: 0, recency: 0 },
506
- }),
507
- isError: false,
508
- };
509
- }
510
-
511
- const items = recallResult.bullets.map((b) => ({
512
- id: b.sourceId,
513
- type: b.source,
514
- kind: b.source,
515
- }));
516
-
517
- const result = {
518
- text: recallResult.text,
519
- resultCount: recallResult.bullets.length,
520
- degraded: false,
521
- items,
522
- sources: {
523
- semantic: recallResult.prefetchHitCount,
524
- recency: 0,
525
- },
526
- };
527
-
528
- return {
529
- content: JSON.stringify(result),
530
- isError: false,
531
- };
532
- } catch (err) {
533
- const msg = err instanceof Error ? err.message : String(err);
534
- log.error({ err, query }, "simplified memory_recall failed");
535
- return {
536
- content: `Error: Memory recall failed: ${msg}`,
537
- isError: true,
538
- };
539
- }
540
- }
541
-
542
414
  // ── Helpers ──────────────────────────────────────────────────────────
543
415
 
544
416
  function inferSubjectFromStatement(statement: string): string {
@@ -137,6 +137,24 @@ export class PermissionChecker {
137
137
  }
138
138
 
139
139
  if (result.decision === "prompt") {
140
+ // dangerouslySkipPermissions: when enabled, auto-approve all prompts
141
+ // without user interaction. Deny rules are still respected (they
142
+ // return before reaching this block).
143
+ //
144
+ // Note: unlike guardian auto-approve and temporary overrides below,
145
+ // this intentionally does NOT check `context.requireFreshApproval`.
146
+ // The setting is designed to skip ALL interactive prompts
147
+ // unconditionally — it is an explicit operator opt-out from the
148
+ // permission system, so requireFreshApproval does not apply.
149
+ const cfg = getConfig();
150
+ if (cfg.permissions.dangerouslySkipPermissions) {
151
+ log.info(
152
+ { toolName: name, riskLevel },
153
+ "dangerouslySkipPermissions active — auto-approving without prompt",
154
+ );
155
+ return { allowed: true, decision: "dangerously_skip_permissions", riskLevel };
156
+ }
157
+
140
158
  // Guardian-trust sessions (e.g. scheduled jobs, reminders) should be
141
159
  // able to use bundled tools without interactive approval. The guardian
142
160
  // is the owner - prompting makes no sense when there is no client.
@@ -440,9 +440,16 @@ export class SkillLoadTool implements Tool {
440
440
  "Rendered inline command expansions for included skill",
441
441
  );
442
442
  } catch (err) {
443
- log.warn(
443
+ log.error(
444
444
  { err, skillId: childId, parentSkillId: skill.id },
445
- "Failed to render inline commands for included skill, using raw body",
445
+ "Failed to render inline commands for included skill; falling back to sanitized body",
446
+ );
447
+ // Strip raw !`...` inline command tokens so they don't leak into
448
+ // the prompt. Replace with a safe stub to maintain fail-closed
449
+ // contract for raw tokens while still isolating child failures.
450
+ childBody = childBody.replace(
451
+ /!`[^`]*`/g,
452
+ "[inline command unavailable]",
446
453
  );
447
454
  }
448
455
  }
@@ -436,11 +436,11 @@ export function ensureDataDir(): void {
436
436
  const dirs = [
437
437
  // Root-level dirs (runtime)
438
438
  root,
439
- // protected, signals, hooks are local-only skip in containerized mode
440
- // (credentials via CES HTTP API, trust via gateway API, no IPC signals)
441
- ...(containerized
442
- ? []
443
- : [join(root, "protected"), join(root, "signals"), join(root, "hooks")]),
439
+ // signals dir is needed everywhere (MCP reload, user-message signals)
440
+ join(root, "signals"),
441
+ // protected, hooks are local-only — skip in containerized mode
442
+ // (credentials via CES HTTP API, trust via gateway API)
443
+ ...(containerized ? [] : [join(root, "protected"), join(root, "hooks")]),
444
444
  // Workspace dirs
445
445
  workspace,
446
446
  join(workspace, "skills"),
package/src/util/xml.ts CHANGED
@@ -6,3 +6,11 @@ export function escapeXmlAttr(s: string): string {
6
6
  .replace(/</g, "&lt;")
7
7
  .replace(/>/g, "&gt;");
8
8
  }
9
+
10
+ /** Escape a string for safe inclusion as XML/HTML text content. */
11
+ export function escapeXmlContent(s: string): string {
12
+ return s
13
+ .replace(/&/g, "&amp;")
14
+ .replace(/</g, "&lt;")
15
+ .replace(/>/g, "&gt;");
16
+ }
@@ -210,13 +210,11 @@ export class WorkspaceHeartbeatService {
210
210
 
211
211
  try {
212
212
  const now = this.now();
213
- let shutdownFiles: string[] = [];
214
213
  const { committed } = await service.commitIfDirty(
215
214
  (st) => {
216
215
  const uniqueFiles = [
217
216
  ...new Set([...st.staged, ...st.modified, ...st.untracked]),
218
217
  ];
219
- shutdownFiles = uniqueFiles;
220
218
  log.info(
221
219
  { workspaceDir, totalChanges: uniqueFiles.length },
222
220
  "Committing pending changes on shutdown",
@@ -237,28 +235,11 @@ export class WorkspaceHeartbeatService {
237
235
  if (committed) {
238
236
  firstSeenDirty.delete(workspaceDir);
239
237
  result.committed++;
240
-
241
- // Fire-and-forget enrichment
242
- try {
243
- const commitHash = await service.getHeadHash();
244
- const shutdownCtx: CommitContext = {
245
- workspaceDir,
246
- trigger: "shutdown",
247
- changedFiles: shutdownFiles,
248
- timestampMs: this.now(),
249
- };
250
- getEnrichmentService().enqueue({
251
- workspaceDir,
252
- commitHash,
253
- context: shutdownCtx,
254
- gitService: service,
255
- });
256
- } catch (enrichErr) {
257
- log.debug(
258
- { enrichErr },
259
- "Failed to enqueue shutdown enrichment (non-fatal)",
260
- );
261
- }
238
+ // Skip enrichment for shutdown commits — the enrichment queue is
239
+ // about to be shut down anyway, and the fire-and-forget writeNote()
240
+ // can race with subsequent commitAllPending() calls (the async
241
+ // git-notes operation acquires the mutex and may leave behind an
242
+ // index.lock on some git versions, causing the next commit to fail).
262
243
  } else {
263
244
  result.skipped++;
264
245
  }