@vellumai/assistant 0.6.1 → 0.6.2

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 (115) hide show
  1. package/docker-entrypoint.sh +12 -2
  2. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  3. package/openapi.yaml +1 -1
  4. package/package.json +1 -1
  5. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  6. package/src/__tests__/checker.test.ts +104 -170
  7. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  8. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  9. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  10. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  11. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  12. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  13. package/src/__tests__/inline-command-runner.test.ts +7 -5
  14. package/src/__tests__/log-export-workspace.test.ts +190 -0
  15. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  16. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  17. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  18. package/src/__tests__/onboarding-template-contract.test.ts +5 -4
  19. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  20. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  21. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  22. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  23. package/src/__tests__/terminal-tools.test.ts +2 -5
  24. package/src/__tests__/test-preload.ts +14 -0
  25. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  26. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  27. package/src/__tests__/tool-executor.test.ts +0 -1
  28. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  29. package/src/__tests__/trust-store.test.ts +4 -4
  30. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  31. package/src/__tests__/workspace-policy.test.ts +2 -7
  32. package/src/agent/loop.ts +0 -29
  33. package/src/channels/types.ts +5 -0
  34. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  35. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  36. package/src/cli/commands/default-action.ts +68 -1
  37. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  38. package/src/cli/commands/oauth/connect.ts +11 -0
  39. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  40. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  41. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  42. package/src/cli/program.ts +9 -2
  43. package/src/config/assistant-feature-flags.ts +59 -55
  44. package/src/config/bundled-skills/app-builder/SKILL.md +87 -4
  45. package/src/config/bundled-skills/gmail/SKILL.md +11 -6
  46. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  47. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  48. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  49. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  50. package/src/config/feature-flag-registry.json +2 -2
  51. package/src/config/schemas/services.ts +8 -0
  52. package/src/credential-execution/approval-bridge.ts +0 -1
  53. package/src/credential-execution/managed-catalog.ts +3 -7
  54. package/src/daemon/config-watcher.ts +6 -2
  55. package/src/daemon/context-overflow-approval.ts +0 -1
  56. package/src/daemon/conversation-agent-loop.ts +33 -12
  57. package/src/daemon/conversation-attachments.ts +0 -1
  58. package/src/daemon/conversation-messaging.ts +3 -0
  59. package/src/daemon/conversation-process.ts +18 -2
  60. package/src/daemon/conversation-queue-manager.ts +8 -0
  61. package/src/daemon/conversation-runtime-assembly.ts +64 -7
  62. package/src/daemon/conversation-surfaces.ts +65 -0
  63. package/src/daemon/conversation-tool-setup.ts +0 -3
  64. package/src/daemon/conversation.ts +3 -5
  65. package/src/daemon/handlers/conversations.ts +2 -1
  66. package/src/daemon/handlers/shared.ts +7 -0
  67. package/src/daemon/lifecycle.ts +21 -1
  68. package/src/daemon/message-types/conversations.ts +4 -0
  69. package/src/daemon/message-types/messages.ts +0 -1
  70. package/src/daemon/message-types/notifications.ts +12 -0
  71. package/src/daemon/message-types/settings.ts +12 -0
  72. package/src/daemon/server.ts +21 -24
  73. package/src/daemon/transport-hints.ts +33 -0
  74. package/src/index.ts +1 -1
  75. package/src/memory/conversation-crud.ts +15 -10
  76. package/src/memory/conversation-directories.ts +39 -0
  77. package/src/memory/conversation-group-migration.ts +65 -5
  78. package/src/memory/embedding-local.ts +1 -1
  79. package/src/memory/graph/capability-seed.ts +3 -5
  80. package/src/memory/group-crud.ts +25 -9
  81. package/src/messaging/provider.ts +1 -1
  82. package/src/notifications/broadcaster.ts +6 -0
  83. package/src/notifications/conversation-pairing.ts +12 -4
  84. package/src/notifications/emit-signal.ts +14 -0
  85. package/src/notifications/signal.ts +11 -0
  86. package/src/oauth/platform-connection.test.ts +2 -2
  87. package/src/oauth/seed-providers.ts +1 -0
  88. package/src/permissions/checker.ts +3 -3
  89. package/src/permissions/defaults.ts +7 -8
  90. package/src/permissions/prompter.ts +0 -2
  91. package/src/platform/client.ts +1 -1
  92. package/src/prompts/templates/BOOTSTRAP.md +14 -5
  93. package/src/prompts/templates/SOUL.md +11 -11
  94. package/src/runtime/assistant-event-hub.ts +22 -0
  95. package/src/runtime/auth/token-service.ts +8 -0
  96. package/src/runtime/routes/conversation-analysis-routes.ts +18 -6
  97. package/src/runtime/routes/conversation-routes.ts +9 -3
  98. package/src/runtime/routes/group-routes.ts +22 -8
  99. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  100. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  101. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  102. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  103. package/src/runtime/routes/log-export-routes.ts +18 -3
  104. package/src/skills/inline-command-runner.ts +12 -14
  105. package/src/tools/permission-checker.ts +0 -18
  106. package/src/tools/secret-detection-handler.ts +0 -1
  107. package/src/tools/skills/sandbox-runner.ts +3 -6
  108. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  109. package/src/tools/terminal/sandbox.ts +4 -1
  110. package/src/tools/terminal/shell.ts +3 -5
  111. package/src/tools/types.ts +0 -3
  112. package/src/watcher/provider-types.ts +1 -1
  113. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  114. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  115. package/src/workspace/migrations/registry.ts +2 -0
@@ -41,6 +41,7 @@ import type {
41
41
  ServerMessage,
42
42
  UserMessageAttachment,
43
43
  } from "./message-protocol.js";
44
+ import type { ConversationTransportMetadata } from "./message-types/conversations.js";
44
45
 
45
46
  const log = getLogger("conversation-messaging");
46
47
 
@@ -222,6 +223,7 @@ export function enqueueMessage(
222
223
  metadata?: Record<string, unknown>,
223
224
  options?: { isInteractive?: boolean },
224
225
  displayContent?: string,
226
+ transport?: ConversationTransportMetadata,
225
227
  ): { queued: boolean; requestId: string; rejected?: boolean } {
226
228
  if (!ctx.processing) {
227
229
  return { queued: false, requestId };
@@ -246,6 +248,7 @@ export function enqueueMessage(
246
248
  turnChannelContext,
247
249
  turnInterfaceContext,
248
250
  isInteractive: options?.isInteractive,
251
+ transport,
249
252
  displayContent,
250
253
  sentAt: Date.now(),
251
254
  });
@@ -15,7 +15,11 @@ import type {
15
15
  TurnChannelContext,
16
16
  TurnInterfaceContext,
17
17
  } from "../channels/types.js";
18
- import { parseChannelId, parseInterfaceId } from "../channels/types.js";
18
+ import {
19
+ parseChannelId,
20
+ parseInterfaceId,
21
+ supportsHostProxy,
22
+ } from "../channels/types.js";
19
23
  import { getConfig } from "../config/loader.js";
20
24
  import type { ContextWindowResult } from "../context/window-manager.js";
21
25
  import { listPendingRequestsByConversationScope } from "../memory/canonical-guardian-store.js";
@@ -44,6 +48,7 @@ import type {
44
48
  UserMessageAttachment,
45
49
  } from "./message-protocol.js";
46
50
  import type { TraceEmitter } from "./trace-emitter.js";
51
+ import { buildTransportHints } from "./transport-hints.js";
47
52
  import { resolveVerificationSessionIntent } from "./verification-session-intent.js";
48
53
 
49
54
  const log = getLogger("conversation-process");
@@ -157,6 +162,8 @@ export interface ProcessConversationContext {
157
162
  ): void;
158
163
  /** Force context compaction regardless of threshold/cooldown. */
159
164
  forceCompact(): Promise<ContextWindowResult>;
165
+ /** Set transport-derived hints for the conversation. */
166
+ setTransportHints(hints: string[] | undefined): void;
160
167
  }
161
168
 
162
169
  function resolveQueuedTurnContext(
@@ -290,6 +297,15 @@ export async function drainQueue(
290
297
  conversation.setTurnInterfaceContext(queuedInterfaceCtx);
291
298
  }
292
299
 
300
+ // Apply transport hints from the queued message so each turn uses the
301
+ // transport metadata that arrived with its message. Messages without
302
+ // transport (subagent notifications, surface actions, etc.) inherit the
303
+ // conversation's existing hints — clearing them would erase the user's
304
+ // environment context for internal turns.
305
+ if (next.transport) {
306
+ conversation.setTransportHints(buildTransportHints(next.transport));
307
+ }
308
+
293
309
  // Non-interactive queued messages (channel requests) must not execute tools
294
310
  // via the desktop host proxy. Clear proxy availability so isAvailable()
295
311
  // returns false and tool execution falls back to local.
@@ -302,7 +318,7 @@ export async function drainQueue(
302
318
  const interfaceCtx =
303
319
  queuedInterfaceCtx ?? conversation.getTurnInterfaceContext();
304
320
  const sourceInterface = interfaceCtx?.userMessageInterface;
305
- if (sourceInterface === "macos") {
321
+ if (sourceInterface && supportsHostProxy(sourceInterface)) {
306
322
  conversation.restoreProxyAvailability();
307
323
  conversation.addPreactivatedSkillId("computer-use");
308
324
  }
@@ -14,6 +14,7 @@ import type {
14
14
  ServerMessage,
15
15
  UserMessageAttachment,
16
16
  } from "./message-protocol.js";
17
+ import type { ConversationTransportMetadata } from "./message-types/conversations.js";
17
18
 
18
19
  const log = getLogger("conversation-queue");
19
20
 
@@ -29,6 +30,8 @@ export interface QueuedMessage {
29
30
  turnInterfaceContext?: TurnInterfaceContext;
30
31
  /** When false, the turn has no interactive user and should skip clarification prompts. */
31
32
  isInteractive?: boolean;
33
+ /** Transport metadata snapshot captured at enqueue time, applied when this message becomes active. */
34
+ transport?: ConversationTransportMetadata;
32
35
  /** Original user message text to persist to DB when recording intent stripping produced a different `content`. */
33
36
  displayContent?: string;
34
37
  /** Wall-clock time (ms since epoch) when the message was enqueued, used as the display timestamp. */
@@ -155,6 +158,11 @@ function estimateItemBytes(item: QueuedMessage): number {
155
158
  bytes += a.data.length * 2;
156
159
  if (a.extractedText) bytes += a.extractedText.length * 2;
157
160
  }
161
+ // Include transport metadata in the estimate so large transport
162
+ // payloads (e.g. hostHomeDir, hostUsername) count against the budget.
163
+ if (item.transport) {
164
+ bytes += JSON.stringify(item.transport).length * 2;
165
+ }
158
166
  // Small fixed overhead for metadata, pointers, etc. (not worth
159
167
  // measuring precisely — the content/attachment data dominates).
160
168
  bytes += 512;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { existsSync, readFileSync, statSync } from "node:fs";
9
- import { join } from "node:path";
9
+ import { join, resolve } from "node:path";
10
10
 
11
11
  import { type ChannelId, parseInterfaceId } from "../channels/types.js";
12
12
  import { getAppDirPath, listAppFiles } from "../memory/app-store.js";
@@ -533,7 +533,9 @@ export function stripNowScratchpad(messages: Message[]): Message[] {
533
533
  // PKB (Personal Knowledge Base) injection
534
534
  // ---------------------------------------------------------------------------
535
535
 
536
- const PKB_FILES = ["INDEX.md", "essentials.md", "threads.md", "buffer.md"];
536
+ const PKB_DEFAULT_FILES = ["INDEX.md", "essentials.md", "threads.md", "buffer.md"];
537
+
538
+ const AUTOINJECT_FILENAME = "_autoinject.md";
537
539
 
538
540
  /** Max buffer.md lines injected into prompts — keeps context bounded even when filing is off. */
539
541
  const MAX_BUFFER_LINES = 50;
@@ -547,9 +549,35 @@ const PKB_NUDGE =
547
549
  "Use `remember` for every new fact you learn, immediately, no batching.";
548
550
 
549
551
  /**
550
- * Read the always-loaded PKB files (INDEX, essentials, threads, buffer)
551
- * and append a nudge encouraging the assistant to proactively read topic
552
- * files and use `remember` aggressively.
552
+ * Read `_autoinject.md` from the PKB directory and return the list of
553
+ * filenames to inject.
554
+ *
555
+ * - Returns `null` when the file is missing or unreadable — callers
556
+ * should fall back to the hardcoded defaults.
557
+ * - Returns `[]` when the file exists but has no entries (empty or
558
+ * comments only) — an explicit opt-out meaning "inject nothing."
559
+ */
560
+ export function readAutoinjectList(pkbDir: string): string[] | null {
561
+ const filePath = join(pkbDir, AUTOINJECT_FILENAME);
562
+ if (!existsSync(filePath)) return null;
563
+ try {
564
+ const raw = stripCommentLines(readFileSync(filePath, "utf-8"));
565
+ const files = raw
566
+ .split("\n")
567
+ .map((l) => l.trim())
568
+ .filter((l) => l.length > 0);
569
+ return files.length > 0 ? files : [];
570
+ } catch {
571
+ return null;
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Read the always-loaded PKB files and append a nudge encouraging the
577
+ * assistant to proactively read topic files and use `remember` aggressively.
578
+ *
579
+ * Which files are loaded is determined by `pkb/_autoinject.md` (one filename
580
+ * per line). Falls back to the built-in defaults when that file is absent.
553
581
  *
554
582
  * Returns the concatenated content ready for injection, or `null` if all
555
583
  * files are missing or empty.
@@ -558,9 +586,14 @@ export function readPkbContext(): string | null {
558
586
  const pkbDir = join(getWorkspaceDir(), "pkb");
559
587
  if (!existsSync(pkbDir)) return null;
560
588
 
589
+ const filesToInject = readAutoinjectList(pkbDir) ?? PKB_DEFAULT_FILES;
590
+
561
591
  const parts: string[] = [];
562
- for (const file of PKB_FILES) {
563
- const filePath = join(pkbDir, file);
592
+ for (const file of filesToInject) {
593
+ // Path traversal guard: reject entries that escape the pkb directory
594
+ const filePath = resolve(pkbDir, file);
595
+ if (!filePath.startsWith(pkbDir + "/")) continue;
596
+
564
597
  if (!existsSync(filePath)) continue;
565
598
  try {
566
599
  let content = stripCommentLines(readFileSync(filePath, "utf-8")).trim();
@@ -599,6 +632,7 @@ export function injectPkbContext(message: Message, content: string): Message {
599
632
  if (
600
633
  block.type === "text" &&
601
634
  (block.text.startsWith("<memory") ||
635
+ block.text.startsWith("</memory_image>") ||
602
636
  block.text.startsWith("<memory_context"))
603
637
  ) {
604
638
  insertIdx = i + 1;
@@ -1051,6 +1085,7 @@ const RUNTIME_INJECTION_PREFIXES = [
1051
1085
  "<now_scratchpad>", // backward-compat: strip legacy blocks from pre-rename history
1052
1086
  "<pkb>",
1053
1087
  "<transport_hints>",
1088
+ "<system_notice>One or more tool calls returned an error.",
1054
1089
  ];
1055
1090
 
1056
1091
  /**
@@ -1085,6 +1120,28 @@ export function findLastInjectedNowContent(messages: Message[]): string | null {
1085
1120
  return null;
1086
1121
  }
1087
1122
 
1123
+ /**
1124
+ * Extract the most recently injected PKB content from the message history.
1125
+ * Returns null if no PKB injection is found.
1126
+ */
1127
+ export function findLastInjectedPkbContent(
1128
+ messages: Message[],
1129
+ ): string | null {
1130
+ const prefix = "<pkb>\n";
1131
+ const suffix = "\n</pkb>";
1132
+ for (let i = messages.length - 1; i >= 0; i--) {
1133
+ const msg = messages[i];
1134
+ if (msg.role !== "user") continue;
1135
+ for (const block of msg.content) {
1136
+ if (block.type === "text" && block.text.startsWith(prefix)) {
1137
+ const end = block.text.lastIndexOf(suffix);
1138
+ if (end > prefix.length) return block.text.slice(prefix.length, end);
1139
+ }
1140
+ }
1141
+ }
1142
+ return null;
1143
+ }
1144
+
1088
1145
  /**
1089
1146
  * Controls which runtime injections are applied.
1090
1147
  *
@@ -9,6 +9,10 @@ import {
9
9
  resolveAppDir,
10
10
  updateApp,
11
11
  } from "../memory/app-store.js";
12
+ import {
13
+ getMessages,
14
+ updateMessageContent,
15
+ } from "../memory/conversation-crud.js";
12
16
  import type { ToolExecutionResult } from "../tools/types.js";
13
17
  import { getLogger } from "../util/logger.js";
14
18
  import { isPlainObject } from "../util/object.js";
@@ -26,11 +30,69 @@ import type {
26
30
  UiSurfaceShow,
27
31
  } from "./message-protocol.js";
28
32
  import { INTERACTIVE_SURFACE_TYPES } from "./message-protocol.js";
33
+ import type { ConversationTransportMetadata } from "./message-types/conversations.js";
29
34
  import type { UserMessageAttachment } from "./message-types/shared.js";
30
35
 
31
36
  const log = getLogger("conversation-surfaces");
32
37
 
33
38
  const MAX_UNDO_DEPTH = 10;
39
+
40
+ /**
41
+ * Mark a `ui_surface` content block as completed in the database so that
42
+ * history reconstruction preserves the completion state. Also updates
43
+ * in-memory messages when available.
44
+ */
45
+ export function markSurfaceCompleted(
46
+ ctx: { conversationId: string; messages?: Array<{ content: unknown }> },
47
+ surfaceId: string,
48
+ summary: string,
49
+ ): void {
50
+ // Update in-memory messages when available so subsequent reads within
51
+ // this session see the change without waiting for DB.
52
+ if (ctx.messages) {
53
+ for (let i = ctx.messages.length - 1; i >= 0; i--) {
54
+ const msg = ctx.messages[i];
55
+ if (!Array.isArray(msg.content)) continue;
56
+ for (const block of msg.content) {
57
+ const b = block as Record<string, unknown>;
58
+ if (b.type === "ui_surface" && b.surfaceId === surfaceId) {
59
+ b.completed = true;
60
+ b.completionSummary = summary;
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // Persist to DB.
68
+ const rows = getMessages(ctx.conversationId);
69
+ for (let r = rows.length - 1; r >= 0; r--) {
70
+ let parsed: unknown[];
71
+ try {
72
+ const result = JSON.parse(rows[r].content);
73
+ if (!Array.isArray(result)) continue;
74
+ parsed = result;
75
+ } catch {
76
+ // Some rows store plain text content (e.g. notification seeding) —
77
+ // skip them and keep scanning.
78
+ continue;
79
+ }
80
+ let found = false;
81
+ for (const pb of parsed) {
82
+ const rb = pb as Record<string, unknown>;
83
+ if (rb.type === "ui_surface" && rb.surfaceId === surfaceId) {
84
+ rb.completed = true;
85
+ rb.completionSummary = summary;
86
+ found = true;
87
+ break;
88
+ }
89
+ }
90
+ if (found) {
91
+ updateMessageContent(rows[r].id, JSON.stringify(parsed));
92
+ return;
93
+ }
94
+ }
95
+ }
34
96
  const TASK_PROGRESS_TEMPLATE_FIELDS = ["title", "status", "steps"] as const;
35
97
 
36
98
  /**
@@ -226,6 +288,7 @@ export interface SurfaceConversationContext {
226
288
  metadata?: Record<string, unknown>,
227
289
  options?: { isInteractive?: boolean },
228
290
  displayContent?: string,
291
+ transport?: ConversationTransportMetadata,
229
292
  ): { queued: boolean; requestId: string; rejected?: boolean };
230
293
  getQueueDepth(): number;
231
294
  processMessage(
@@ -857,6 +920,7 @@ export function handleSurfaceAction(
857
920
  summary,
858
921
  submittedData: mergedData,
859
922
  });
923
+ markSurfaceCompleted(ctx, surfaceId, summary);
860
924
  }
861
925
 
862
926
  // Extract file attachments from action data so they are sent as proper
@@ -1442,6 +1506,7 @@ export async function surfaceProxyResolver(
1442
1506
  summary,
1443
1507
  submittedData: lastAction.data,
1444
1508
  });
1509
+ markSurfaceCompleted(ctx, surfaceId, summary);
1445
1510
  } else {
1446
1511
  ctx.sendToClient({
1447
1512
  type: "ui_surface_dismiss",
@@ -89,7 +89,6 @@ export interface ToolSetupContext extends SurfaceConversationContext {
89
89
  assistantId?: string;
90
90
  currentRequestId?: string;
91
91
  workingDir: string;
92
- sandboxOverride?: boolean;
93
92
  abortController: AbortController | null;
94
93
  /** When set, only tools in this set may execute during the current turn. */
95
94
  allowedToolNames?: Set<string>;
@@ -186,7 +185,6 @@ export function createToolExecutor(
186
185
  : undefined,
187
186
  onOutput,
188
187
  signal: ctx.abortController?.signal,
189
- sandboxOverride: ctx.sandboxOverride,
190
188
  allowedToolNames: ctx.allowedToolNames,
191
189
  memoryScopeId: ctx.memoryPolicy.scopeId,
192
190
  forcePromptSideEffects: ctx.memoryPolicy.strictSideEffects,
@@ -371,7 +369,6 @@ export function createProxyApprovalCallback(
371
369
  allowlistOptions,
372
370
  scopeOptions,
373
371
  undefined,
374
- undefined,
375
372
  ctx.conversationId,
376
373
  );
377
374
 
@@ -114,6 +114,7 @@ import type {
114
114
  UsageStats,
115
115
  UserMessageAttachment,
116
116
  } from "./message-protocol.js";
117
+ import type { ConversationTransportMetadata } from "./message-types/conversations.js";
117
118
  import type {
118
119
  AssistantActivityState,
119
120
  ConfirmationStateChanged,
@@ -156,7 +157,6 @@ export class Conversation {
156
157
  /** @internal */ sendToClient: (msg: ServerMessage) => void;
157
158
  /** @internal */ eventBus = new EventBus<AssistantDomainEvents>();
158
159
  /** @internal */ workingDir: string;
159
- /** @internal */ sandboxOverride?: boolean;
160
160
  /** @internal */ allowedToolNames?: Set<string>;
161
161
  /** @internal */ toolsDisabledDepth = 0;
162
162
  /** @internal */ preactivatedSkillIds?: string[];
@@ -540,10 +540,6 @@ export class Conversation {
540
540
  }
541
541
  }
542
542
 
543
- setSandboxOverride(enabled: boolean | undefined): void {
544
- this.sandboxOverride = enabled;
545
- }
546
-
547
543
  setSubagentAllowedTools(tools: Set<string> | undefined): void {
548
544
  this.subagentAllowedTools = tools;
549
545
  }
@@ -609,6 +605,7 @@ export class Conversation {
609
605
  metadata?: Record<string, unknown>,
610
606
  options?: { isInteractive?: boolean },
611
607
  displayContent?: string,
608
+ transport?: ConversationTransportMetadata,
612
609
  ): { queued: boolean; requestId: string; rejected?: boolean } {
613
610
  return enqueueMessageImpl(
614
611
  this,
@@ -621,6 +618,7 @@ export class Conversation {
621
618
  metadata,
622
619
  options,
623
620
  displayContent,
621
+ transport,
624
622
  );
625
623
  }
626
624
 
@@ -4,6 +4,7 @@ import {
4
4
  type InterfaceId,
5
5
  parseChannelId,
6
6
  parseInterfaceId,
7
+ supportsHostProxy,
7
8
  } from "../../channels/types.js";
8
9
  import { getConfig } from "../../config/loader.js";
9
10
  import {
@@ -301,7 +302,7 @@ export async function handleConversationCreate(
301
302
  // Only create the host bash proxy for desktop client interfaces that can
302
303
  // execute commands on the user's machine. Set before updateClient so
303
304
  // updateClient's call to hostBashProxy.updateSender targets the new proxy.
304
- if (transportInterface === "macos") {
305
+ if (supportsHostProxy(transportInterface)) {
305
306
  const proxy = new HostBashProxy(sendEvent, (requestId) => {
306
307
  pendingInteractions.resolve(requestId);
307
308
  });
@@ -81,6 +81,8 @@ export interface HistorySurface {
81
81
  data: Record<string, unknown>;
82
82
  actions?: Array<{ id: string; label: string; style?: string }>;
83
83
  display?: string;
84
+ completed?: boolean;
85
+ completionSummary?: string;
84
86
  }
85
87
 
86
88
  export interface RenderedHistoryContent {
@@ -281,6 +283,11 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
281
283
  : {},
282
284
  actions: Array.isArray(block.actions) ? block.actions : undefined,
283
285
  display: typeof block.display === "string" ? block.display : undefined,
286
+ completed: block.completed === true ? true : undefined,
287
+ completionSummary:
288
+ typeof block.completionSummary === "string"
289
+ ? block.completionSummary
290
+ : undefined,
284
291
  };
285
292
  surfaces.push(surface);
286
293
  contentOrder.push(`surface:${surfaces.length - 1}`);
@@ -5,6 +5,7 @@ import { reconcileCallsOnStartup } from "../calls/call-recovery.js";
5
5
  import { setRelayBroadcast } from "../calls/relay-server.js";
6
6
  import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
7
7
  import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
8
+ import { initFeatureFlagOverrides } from "../config/assistant-feature-flags.js";
8
9
  import {
9
10
  getPlatformAssistantId,
10
11
  getQdrantHttpPortEnv,
@@ -269,6 +270,15 @@ export async function runDaemon(): Promise<void> {
269
270
  const signingKey = resolveSigningKey();
270
271
  initAuthSigningKey(signingKey);
271
272
 
273
+ // Pre-populate the feature flag cache from the gateway so all
274
+ // subsequent sync isAssistantFeatureFlagEnabled() calls have data.
275
+ // Fired non-blocking so a slow or unreachable gateway doesn't delay
276
+ // daemon startup (the fetch has a 10s timeout that would otherwise
277
+ // stall the critical path).
278
+ void initFeatureFlagOverrides().catch((err) =>
279
+ log.warn({ err }, "Background feature flag init failed"),
280
+ );
281
+
272
282
  seedInterfaceFiles();
273
283
 
274
284
  log.info("Daemon startup: installing templates and initializing DB");
@@ -690,7 +700,7 @@ export async function runDaemon(): Promise<void> {
690
700
  seedUninstalledCatalogSkillMemories,
691
701
  } = await import("../memory/graph/capability-seed.js");
692
702
  seedSkillGraphNodes();
693
- seedCliGraphNodes();
703
+ await seedCliGraphNodes();
694
704
  void seedUninstalledCatalogSkillMemories().catch((err) =>
695
705
  log.warn(
696
706
  { err },
@@ -763,6 +773,11 @@ export async function runDaemon(): Promise<void> {
763
773
  },
764
774
  routingIntent: schedule.routingIntent,
765
775
  routingHints: schedule.routingHints,
776
+ conversationMetadata: {
777
+ groupId: "system:scheduled",
778
+ scheduleJobId: schedule.id,
779
+ source: "schedule",
780
+ },
766
781
  dedupeKey: `schedule:notify:${schedule.id}:${Date.now()}`,
767
782
  throwOnError: true,
768
783
  });
@@ -782,6 +797,11 @@ export async function runDaemon(): Promise<void> {
782
797
  scheduleId: schedule.id,
783
798
  name: schedule.name,
784
799
  },
800
+ conversationMetadata: {
801
+ groupId: "system:scheduled",
802
+ scheduleJobId: schedule.id,
803
+ source: "schedule",
804
+ },
785
805
  dedupeKey: `schedule:complete:${schedule.id}:${Date.now()}`,
786
806
  });
787
807
  },
@@ -309,6 +309,10 @@ export interface HistoryResponseSurface {
309
309
  data?: Record<string, unknown>;
310
310
  }>;
311
311
  display?: string;
312
+ /** True when the surface was completed (e.g. form submitted, action taken). */
313
+ completed?: boolean;
314
+ /** Human-readable summary shown in the completion chip. */
315
+ completionSummary?: string;
312
316
  }
313
317
 
314
318
  export interface HistoryResponse {
@@ -151,7 +151,6 @@ export interface ConfirmationRequest {
151
151
  newContent: string;
152
152
  isNewFile: boolean;
153
153
  };
154
- sandboxed?: boolean;
155
154
  conversationId?: string;
156
155
  /** When false, the client should hide "always allow" / trust-rule persistence affordances. */
157
156
  persistentDecisionsAllowed?: boolean;
@@ -27,6 +27,18 @@ export interface NotificationConversationCreated {
27
27
  * and should only be surfaced by clients bound to this guardian identity.
28
28
  */
29
29
  targetGuardianPrincipalId?: string;
30
+ /**
31
+ * Conversation group identifier propagated from the signal producer.
32
+ * Clients use this to place the conversation in the correct sidebar folder
33
+ * (e.g. "system:scheduled" for schedule completion threads).
34
+ */
35
+ groupId?: string;
36
+ /**
37
+ * Semantic source of the conversation (e.g. "schedule", "reminder").
38
+ * Allows clients to override the default "notification" source so the
39
+ * conversation is attributed correctly.
40
+ */
41
+ source?: string;
30
42
  }
31
43
 
32
44
  /** Client ack sent after UNUserNotificationCenter.add() completes (or fails). */
@@ -34,11 +34,21 @@ export interface AvatarUpdated {
34
34
  avatarPath: string;
35
35
  }
36
36
 
37
+ /** Sent by the daemon when workspace config.json changes on disk. */
38
+ export interface ConfigChanged {
39
+ type: "config_changed";
40
+ }
41
+
37
42
  /** Sent by the daemon when sounds config or sound files change on disk. */
38
43
  export interface SoundsConfigUpdated {
39
44
  type: "sounds_config_updated";
40
45
  }
41
46
 
47
+ /** Sent by the daemon when feature flag files change on disk. */
48
+ export interface FeatureFlagsChanged {
49
+ type: "feature_flags_changed";
50
+ }
51
+
42
52
  /** Response to a generate_avatar request indicating success or failure. */
43
53
  export interface GenerateAvatarResponse {
44
54
  type: "generate_avatar_response";
@@ -56,5 +66,7 @@ export type _SettingsClientMessages =
56
66
  export type _SettingsServerMessages =
57
67
  | ClientSettingsUpdate
58
68
  | AvatarUpdated
69
+ | ConfigChanged
59
70
  | SoundsConfigUpdated
71
+ | FeatureFlagsChanged
60
72
  | GenerateAvatarResponse;
@@ -17,6 +17,7 @@ import {
17
17
  type InterfaceId,
18
18
  parseChannelId,
19
19
  parseInterfaceId,
20
+ supportsHostProxy,
20
21
  } from "../channels/types.js";
21
22
  import { getConfig } from "../config/loader.js";
22
23
  import { onContactChange } from "../contacts/contact-events.js";
@@ -91,6 +92,7 @@ import type {
91
92
  ServerMessage,
92
93
  UserMessageAttachment,
93
94
  } from "./message-protocol.js";
95
+ import { buildTransportHints } from "./transport-hints.js";
94
96
 
95
97
  const log = getLogger("server");
96
98
 
@@ -373,28 +375,7 @@ export class DaemonServer {
373
375
  { channelId: transport.channelId },
374
376
  "Transport metadata received",
375
377
  );
376
-
377
- // Build enriched hints: interface ID first, then host environment (macOS
378
- // only), then any client-provided hints.
379
- const enrichedHints: string[] = [];
380
-
381
- const interfaceLabel = parseInterfaceId(transport.interfaceId) ?? "vellum";
382
- enrichedHints.push(`User is messaging from interface: ${interfaceLabel}`);
383
-
384
- if (transport.interfaceId === "macos") {
385
- if (transport.hostHomeDir) {
386
- enrichedHints.push(`Host home directory: ${transport.hostHomeDir}`);
387
- }
388
- if (transport.hostUsername) {
389
- enrichedHints.push(`Host username: ${transport.hostUsername}`);
390
- }
391
- }
392
-
393
- if (transport.hints) {
394
- enrichedHints.push(...transport.hints);
395
- }
396
-
397
- conversation.setTransportHints(enrichedHints);
378
+ conversation.setTransportHints(buildTransportHints(transport));
398
379
  }
399
380
 
400
381
  constructor() {
@@ -548,10 +529,18 @@ export class DaemonServer {
548
529
  }
549
530
  }
550
531
 
532
+ private broadcastConfigChanged(): void {
533
+ this.broadcast({ type: "config_changed" });
534
+ }
535
+
551
536
  private broadcastSoundsConfigUpdated(): void {
552
537
  this.broadcast({ type: "sounds_config_updated" });
553
538
  }
554
539
 
540
+ private broadcastFeatureFlagsChanged(): void {
541
+ this.broadcast({ type: "feature_flags_changed" });
542
+ }
543
+
555
544
  private broadcastAvatarUpdated(): void {
556
545
  this.broadcast({
557
546
  type: "avatar_updated",
@@ -748,6 +737,8 @@ export class DaemonServer {
748
737
  () => this.broadcastIdentityChanged(),
749
738
  () => this.broadcastSoundsConfigUpdated(),
750
739
  () => this.broadcastAvatarUpdated(),
740
+ () => this.broadcastConfigChanged(),
741
+ () => this.broadcastFeatureFlagsChanged(),
751
742
  );
752
743
 
753
744
  this.appSourceWatcher.start((appId) => this.handleAppSourceChange(appId));
@@ -978,7 +969,13 @@ export class DaemonServer {
978
969
  }
979
970
  this.evictor.touch(conversationId);
980
971
  } else {
981
- this.applyTransportMetadata(conversation, options);
972
+ // Only apply transport metadata when the conversation is idle.
973
+ // When processing, the hints are stored on the queued message and
974
+ // will be applied at dequeue time — applying them here would
975
+ // overwrite the in-flight conversation's transportHints.
976
+ if (!conversation.isProcessing()) {
977
+ this.applyTransportMetadata(conversation, options);
978
+ }
982
979
  this.evictor.touch(conversationId);
983
980
  }
984
981
  return conversation;
@@ -1091,7 +1088,7 @@ export class DaemonServer {
1091
1088
  // Guard: don't replace an active proxy during concurrent turn races —
1092
1089
  // another request may have started processing between the isProcessing()
1093
1090
  // check above and the await on ensureActorScopedHistory().
1094
- if (resolvedInterface === "macos") {
1091
+ if (supportsHostProxy(resolvedInterface)) {
1095
1092
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
1096
1093
  conversation.setHostBashProxy(
1097
1094
  new HostBashProxy(conversation.getCurrentSender(), (requestId) => {