@vellumai/assistant 0.4.11 → 0.4.13

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 (111) hide show
  1. package/ARCHITECTURE.md +401 -385
  2. package/package.json +1 -1
  3. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
  4. package/src/__tests__/registry.test.ts +235 -187
  5. package/src/__tests__/secure-keys.test.ts +27 -0
  6. package/src/__tests__/session-agent-loop.test.ts +521 -256
  7. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
  8. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  9. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  10. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  11. package/src/__tests__/skills.test.ts +334 -276
  12. package/src/__tests__/slack-skill.test.ts +124 -0
  13. package/src/__tests__/starter-task-flow.test.ts +7 -17
  14. package/src/agent/loop.ts +10 -3
  15. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
  16. package/src/config/bundled-skills/doordash/SKILL.md +171 -0
  17. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
  18. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
  19. package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
  20. package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
  21. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
  22. package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
  23. package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
  24. package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
  25. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
  26. package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
  27. package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
  28. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
  29. package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
  30. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
  31. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
  32. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
  33. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
  34. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
  35. package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
  36. package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
  37. package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
  38. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
  39. package/src/config/bundled-skills/messaging/SKILL.md +59 -42
  40. package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
  41. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
  42. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
  43. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
  44. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
  45. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
  46. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
  47. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
  48. package/src/config/bundled-skills/notion/SKILL.md +240 -0
  49. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
  50. package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
  51. package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
  52. package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
  53. package/src/config/bundled-skills/slack/SKILL.md +49 -0
  54. package/src/config/bundled-skills/slack/TOOLS.json +167 -0
  55. package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
  56. package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
  57. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
  58. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
  59. package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
  60. package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
  61. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
  62. package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
  63. package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
  64. package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
  65. package/src/config/bundled-tool-registry.ts +292 -267
  66. package/src/config/schema.ts +1 -1
  67. package/src/daemon/handlers/skills.ts +334 -234
  68. package/src/daemon/ipc-contract/messages.ts +2 -0
  69. package/src/daemon/ipc-contract/surfaces.ts +2 -0
  70. package/src/daemon/lifecycle.ts +358 -221
  71. package/src/daemon/response-tier.ts +2 -0
  72. package/src/daemon/server.ts +453 -193
  73. package/src/daemon/session-agent-loop-handlers.ts +43 -2
  74. package/src/daemon/session-agent-loop.ts +3 -0
  75. package/src/daemon/session-lifecycle.ts +3 -0
  76. package/src/daemon/session-process.ts +1 -0
  77. package/src/daemon/session-surfaces.ts +22 -20
  78. package/src/daemon/session-tool-setup.ts +1 -0
  79. package/src/daemon/session.ts +5 -2
  80. package/src/messaging/outreach-classifier.ts +12 -5
  81. package/src/messaging/provider-types.ts +5 -0
  82. package/src/messaging/provider.ts +1 -1
  83. package/src/messaging/providers/gmail/adapter.ts +11 -5
  84. package/src/messaging/providers/gmail/client.ts +2 -0
  85. package/src/messaging/providers/slack/adapter.ts +1 -0
  86. package/src/messaging/providers/slack/client.ts +8 -0
  87. package/src/messaging/providers/slack/types.ts +5 -0
  88. package/src/runtime/http-errors.ts +33 -20
  89. package/src/runtime/http-server.ts +706 -291
  90. package/src/runtime/http-types.ts +26 -16
  91. package/src/runtime/routes/secret-routes.ts +57 -2
  92. package/src/runtime/routes/surface-action-routes.ts +66 -0
  93. package/src/runtime/routes/trust-rules-routes.ts +140 -0
  94. package/src/security/keychain-to-encrypted-migration.ts +59 -0
  95. package/src/security/secure-keys.ts +17 -0
  96. package/src/skills/frontmatter.ts +9 -7
  97. package/src/tools/apps/executors.ts +2 -1
  98. package/src/tools/tool-manifest.ts +44 -42
  99. package/src/tools/types.ts +9 -0
  100. package/src/__tests__/skill-mirror-parity.test.ts +0 -176
  101. package/src/config/vellum-skills/catalog.json +0 -63
  102. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
  103. package/src/skills/vellum-catalog-remote.ts +0 -166
  104. package/src/tools/skills/vellum-catalog.ts +0 -168
  105. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
  106. /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
  107. /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
  108. /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
  109. /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
  110. /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
  111. /package/src/config/{vellum-skills → bundled-skills}/trusted-contacts/SKILL.md +0 -0
@@ -1,61 +1,91 @@
1
- import { chmodSync, existsSync, readFileSync,statSync } from 'node:fs';
2
- import * as net from 'node:net';
3
- import { join } from 'node:path';
4
- import * as tls from 'node:tls';
5
-
6
- import { createAssistantMessage,createUserMessage } from '../agent/message-types.js';
7
- import { type ChannelId, type InterfaceId,parseChannelId, parseInterfaceId } from '../channels/types.js';
8
- import { getConfig } from '../config/loader.js';
9
- import { buildSystemPrompt } from '../config/system-prompt.js';
10
- import type { HeartbeatService } from '../heartbeat/heartbeat-service.js';
11
- import { bootstrapHomeBaseAppLink } from '../home-base/bootstrap.js';
12
- import * as attachmentsStore from '../memory/attachments-store.js';
1
+ import { chmodSync, existsSync, readFileSync, statSync } from "node:fs";
2
+ import * as net from "node:net";
3
+ import { join } from "node:path";
4
+ import * as tls from "node:tls";
5
+
6
+ import {
7
+ createAssistantMessage,
8
+ createUserMessage,
9
+ } from "../agent/message-types.js";
10
+ import {
11
+ type ChannelId,
12
+ type InterfaceId,
13
+ parseChannelId,
14
+ parseInterfaceId,
15
+ } from "../channels/types.js";
16
+ import { getConfig } from "../config/loader.js";
17
+ import { buildSystemPrompt } from "../config/system-prompt.js";
18
+ import type { HeartbeatService } from "../heartbeat/heartbeat-service.js";
19
+ import { bootstrapHomeBaseAppLink } from "../home-base/bootstrap.js";
20
+ import * as attachmentsStore from "../memory/attachments-store.js";
13
21
  import {
14
22
  createCanonicalGuardianRequest,
15
23
  generateCanonicalRequestCode,
16
- } from '../memory/canonical-guardian-store.js';
17
- import * as conversationStore from '../memory/conversation-store.js';
18
- import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
19
- import { RateLimitProvider } from '../providers/ratelimit.js';
20
- import { getFailoverProvider, initializeProviders } from '../providers/registry.js';
21
- import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
22
- import { bridgeConfirmationRequestToGuardian } from '../runtime/confirmation-request-guardian-bridge.js';
23
- import * as pendingInteractions from '../runtime/pending-interactions.js';
24
- import { checkIngressForSecrets } from '../security/secret-ingress.js';
25
- import { getSubagentManager } from '../subagent/index.js';
26
- import { IngressBlockedError } from '../util/errors.js';
27
- import { getLogger } from '../util/logger.js';
28
- import { getLocalIPv4 } from '../util/network-info.js';
29
- import { getSandboxWorkingDir, getSocketPath, getTCPHost, getTCPPort, getWorkspacePromptPath, isIOSPairingEnabled,isTCPEnabled, removeSocketFile } from '../util/platform.js';
30
- import { registerDaemonCallbacks } from '../work-items/work-item-runner.js';
31
- import { AuthManager } from './auth-manager.js';
32
- import { ComputerUseSession } from './computer-use-session.js';
33
- import { ConfigWatcher } from './config-watcher.js';
34
- import { handleMessage, type HandlerContext, type SessionCreateOptions } from './handlers.js';
35
- import { parseIdentityFields } from './handlers/identity.js';
36
- import { cleanupRecordingsOnDisconnect } from './handlers/recording.js';
37
- import { ensureBlobDir, sweepStaleBlobs } from './ipc-blob-store.js';
38
- import { IpcSender } from './ipc-handler.js';
24
+ } from "../memory/canonical-guardian-store.js";
25
+ import * as conversationStore from "../memory/conversation-store.js";
26
+ import { provenanceFromGuardianContext } from "../memory/conversation-store.js";
27
+ import { RateLimitProvider } from "../providers/ratelimit.js";
28
+ import {
29
+ getFailoverProvider,
30
+ initializeProviders,
31
+ } from "../providers/registry.js";
32
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
33
+ import { bridgeConfirmationRequestToGuardian } from "../runtime/confirmation-request-guardian-bridge.js";
34
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
35
+ import { checkIngressForSecrets } from "../security/secret-ingress.js";
36
+ import { getSubagentManager } from "../subagent/index.js";
37
+ import { IngressBlockedError } from "../util/errors.js";
38
+ import { getLogger } from "../util/logger.js";
39
+ import { getLocalIPv4 } from "../util/network-info.js";
40
+ import {
41
+ getSandboxWorkingDir,
42
+ getSocketPath,
43
+ getTCPHost,
44
+ getTCPPort,
45
+ getWorkspacePromptPath,
46
+ isIOSPairingEnabled,
47
+ isTCPEnabled,
48
+ removeSocketFile,
49
+ } from "../util/platform.js";
50
+ import { registerDaemonCallbacks } from "../work-items/work-item-runner.js";
51
+ import { AuthManager } from "./auth-manager.js";
52
+ import { ComputerUseSession } from "./computer-use-session.js";
53
+ import { ConfigWatcher } from "./config-watcher.js";
54
+ import {
55
+ handleMessage,
56
+ type HandlerContext,
57
+ type SessionCreateOptions,
58
+ } from "./handlers.js";
59
+ import { parseIdentityFields } from "./handlers/identity.js";
60
+ import { cleanupRecordingsOnDisconnect } from "./handlers/recording.js";
61
+ import { ensureBlobDir, sweepStaleBlobs } from "./ipc-blob-store.js";
62
+ import { IpcSender } from "./ipc-handler.js";
39
63
  import {
40
64
  createMessageParser,
41
65
  MAX_LINE_SIZE,
42
66
  normalizeThreadType,
43
67
  serialize,
44
68
  type ServerMessage,
45
- } from './ipc-protocol.js';
46
- import { validateClientMessage } from './ipc-validate.js';
47
- import { DEFAULT_MEMORY_POLICY, Session, type SessionMemoryPolicy } from './session.js';
48
- import { SessionEvictor } from './session-evictor.js';
49
- import { resolveChannelCapabilities } from './session-runtime-assembly.js';
50
- import { resolveSlash } from './session-slash.js';
51
- import { ensureTlsCert } from './tls-certs.js';
69
+ } from "./ipc-protocol.js";
70
+ import { validateClientMessage } from "./ipc-validate.js";
71
+ import {
72
+ DEFAULT_MEMORY_POLICY,
73
+ Session,
74
+ type SessionMemoryPolicy,
75
+ } from "./session.js";
76
+ import { SessionEvictor } from "./session-evictor.js";
77
+ import { resolveChannelCapabilities } from "./session-runtime-assembly.js";
78
+ import { resolveSlash } from "./session-slash.js";
79
+ import { ensureTlsCert } from "./tls-certs.js";
52
80
 
53
- const log = getLogger('server');
81
+ const log = getLogger("server");
54
82
 
55
83
  function readPackageVersion(): string | undefined {
56
84
  try {
57
- const pkgPath = join(import.meta.dir, '../../package.json');
58
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };
85
+ const pkgPath = join(import.meta.dir, "../../package.json");
86
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
87
+ version?: string;
88
+ };
59
89
  return pkg.version;
60
90
  } catch {
61
91
  return undefined;
@@ -64,7 +94,10 @@ function readPackageVersion(): string | undefined {
64
94
 
65
95
  const daemonVersion = readPackageVersion();
66
96
 
67
- function resolveTurnChannel(sourceChannel?: string, transportChannelId?: string): ChannelId {
97
+ function resolveTurnChannel(
98
+ sourceChannel?: string,
99
+ transportChannelId?: string,
100
+ ): ChannelId {
68
101
  if (sourceChannel != null) {
69
102
  const parsed = parseChannelId(sourceChannel);
70
103
  if (!parsed) {
@@ -79,7 +112,7 @@ function resolveTurnChannel(sourceChannel?: string, transportChannelId?: string)
79
112
  }
80
113
  return parsed;
81
114
  }
82
- return 'vellum';
115
+ return "vellum";
83
116
  }
84
117
 
85
118
  function resolveTurnInterface(sourceInterface?: string): InterfaceId {
@@ -92,17 +125,19 @@ function resolveTurnInterface(sourceInterface?: string): InterfaceId {
92
125
  }
93
126
  // Interface and channel are orthogonal dimensions; default explicitly
94
127
  // instead of deriving interface from channel.
95
- return 'vellum';
128
+ return "vellum";
96
129
  }
97
130
 
98
- function resolveCanonicalRequestSourceType(sourceChannel: string | undefined): 'desktop' | 'channel' | 'voice' {
99
- if (sourceChannel === 'voice') {
100
- return 'voice';
131
+ function resolveCanonicalRequestSourceType(
132
+ sourceChannel: string | undefined,
133
+ ): "desktop" | "channel" | "voice" {
134
+ if (sourceChannel === "voice") {
135
+ return "voice";
101
136
  }
102
- if (sourceChannel === 'vellum') {
103
- return 'desktop';
137
+ if (sourceChannel === "vellum") {
138
+ return "desktop";
104
139
  }
105
- return 'channel';
140
+ return "channel";
106
141
  }
107
142
 
108
143
  /**
@@ -115,11 +150,11 @@ function makePendingInteractionRegistrar(
115
150
  conversationId: string,
116
151
  ): (msg: ServerMessage) => void {
117
152
  return (msg: ServerMessage) => {
118
- if (msg.type === 'confirmation_request') {
153
+ if (msg.type === "confirmation_request") {
119
154
  pendingInteractions.register(msg.requestId, {
120
155
  session,
121
156
  conversationId,
122
- kind: 'confirmation',
157
+ kind: "confirmation",
123
158
  confirmationDetails: {
124
159
  toolName: msg.toolName,
125
160
  input: msg.input,
@@ -135,19 +170,20 @@ function makePendingInteractionRegistrar(
135
170
  // via applyCanonicalGuardianDecision.
136
171
  try {
137
172
  const guardianContext = session.guardianContext;
138
- const sourceChannel = guardianContext?.sourceChannel ?? 'vellum';
173
+ const sourceChannel = guardianContext?.sourceChannel ?? "vellum";
139
174
  const canonicalRequest = createCanonicalGuardianRequest({
140
175
  id: msg.requestId,
141
- kind: 'tool_approval',
176
+ kind: "tool_approval",
142
177
  sourceType: resolveCanonicalRequestSourceType(sourceChannel),
143
178
  sourceChannel,
144
179
  conversationId,
145
180
  requesterExternalUserId: guardianContext?.requesterExternalUserId,
146
181
  requesterChatId: guardianContext?.requesterChatId,
147
182
  guardianExternalUserId: guardianContext?.guardianExternalUserId,
148
- guardianPrincipalId: guardianContext?.guardianPrincipalId ?? undefined,
183
+ guardianPrincipalId:
184
+ guardianContext?.guardianPrincipalId ?? undefined,
149
185
  toolName: msg.toolName,
150
- status: 'pending',
186
+ status: "pending",
151
187
  requestCode: generateCanonicalRequestCode(),
152
188
  expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
153
189
  });
@@ -166,14 +202,14 @@ function makePendingInteractionRegistrar(
166
202
  } catch (err) {
167
203
  log.debug(
168
204
  { err, requestId: msg.requestId, conversationId },
169
- 'Failed to create canonical request from pending interaction registrar',
205
+ "Failed to create canonical request from pending interaction registrar",
170
206
  );
171
207
  }
172
- } else if (msg.type === 'secret_request') {
208
+ } else if (msg.type === "secret_request") {
173
209
  pendingInteractions.register(msg.requestId, {
174
210
  session,
175
211
  conversationId,
176
- kind: 'secret',
212
+ kind: "secret",
177
213
  });
178
214
  }
179
215
  };
@@ -206,7 +242,7 @@ export class DaemonServer {
206
242
  /**
207
243
  * Logical assistant identifier used when publishing to the assistant-events hub.
208
244
  */
209
- assistantId: string = 'default';
245
+ assistantId: string = "default";
210
246
 
211
247
  /** Optional heartbeat service reference for "Run Now" from the UI. */
212
248
  private _heartbeatService?: HeartbeatService;
@@ -216,8 +252,9 @@ export class DaemonServer {
216
252
  }
217
253
 
218
254
  private deriveMemoryPolicy(conversationId: string): SessionMemoryPolicy {
219
- const threadType = conversationStore.getConversationThreadType(conversationId);
220
- if (threadType === 'private') {
255
+ const threadType =
256
+ conversationStore.getConversationThreadType(conversationId);
257
+ if (threadType === "private") {
221
258
  return {
222
259
  scopeId: conversationStore.getConversationMemoryScopeId(conversationId),
223
260
  includeDefaultFallback: true,
@@ -227,10 +264,16 @@ export class DaemonServer {
227
264
  return DEFAULT_MEMORY_POLICY;
228
265
  }
229
266
 
230
- private applyTransportMetadata(_session: Session, options: SessionCreateOptions | undefined): void {
267
+ private applyTransportMetadata(
268
+ _session: Session,
269
+ options: SessionCreateOptions | undefined,
270
+ ): void {
231
271
  const transport = options?.transport;
232
272
  if (!transport) return;
233
- log.debug({ channelId: transport.channelId }, 'Transport metadata received');
273
+ log.debug(
274
+ { channelId: transport.channelId },
275
+ "Transport metadata received",
276
+ );
234
277
  }
235
278
 
236
279
  constructor() {
@@ -242,26 +285,57 @@ export class DaemonServer {
242
285
  };
243
286
  this.evictor.shouldProtect = (sessionId: string) => {
244
287
  const children = getSubagentManager().getChildrenOf(sessionId);
245
- return children.some((c) => c.status === 'running' || c.status === 'pending');
288
+ return children.some(
289
+ (c) => c.status === "running" || c.status === "pending",
290
+ );
246
291
  };
247
- getSubagentManager().onSubagentFinished = async (parentSessionId, message, sendToClient, notification) => {
292
+ getSubagentManager().onSubagentFinished = async (
293
+ parentSessionId,
294
+ message,
295
+ sendToClient,
296
+ notification,
297
+ ) => {
248
298
  const parentSession = this.sessions.get(parentSessionId);
249
299
  if (!parentSession) {
250
- log.warn({ parentSessionId }, 'Subagent finished but parent session not found');
300
+ log.warn(
301
+ { parentSessionId },
302
+ "Subagent finished but parent session not found",
303
+ );
251
304
  return;
252
305
  }
253
306
  const requestId = `subagent-notify-${Date.now()}`;
254
307
  const metadata = { subagentNotification: notification };
255
- const enqueueResult = parentSession.enqueueMessage(message, [], sendToClient, requestId, undefined, undefined, metadata);
308
+ const enqueueResult = parentSession.enqueueMessage(
309
+ message,
310
+ [],
311
+ sendToClient,
312
+ requestId,
313
+ undefined,
314
+ undefined,
315
+ metadata,
316
+ );
256
317
  if (enqueueResult.rejected) {
257
- log.warn({ parentSessionId }, 'Parent session queue full, dropping subagent notification');
318
+ log.warn(
319
+ { parentSessionId },
320
+ "Parent session queue full, dropping subagent notification",
321
+ );
258
322
  return;
259
323
  }
260
324
  if (!enqueueResult.queued) {
261
- const messageId = await parentSession.persistUserMessage(message, [], undefined, metadata);
262
- parentSession.runAgentLoop(message, messageId, sendToClient).catch((err) => {
263
- log.error({ parentSessionId, err }, 'Failed to process subagent notification in parent');
264
- });
325
+ const messageId = await parentSession.persistUserMessage(
326
+ message,
327
+ [],
328
+ undefined,
329
+ metadata,
330
+ );
331
+ parentSession
332
+ .runAgentLoop(message, messageId, sendToClient)
333
+ .catch((err) => {
334
+ log.error(
335
+ { parentSessionId, err },
336
+ "Failed to process subagent notification in parent",
337
+ );
338
+ });
265
339
  }
266
340
  };
267
341
  }
@@ -284,11 +358,13 @@ export class DaemonServer {
284
358
 
285
359
  private broadcastIdentityChanged(): void {
286
360
  try {
287
- const identityPath = getWorkspacePromptPath('IDENTITY.md');
288
- const content = existsSync(identityPath) ? readFileSync(identityPath, 'utf-8') : '';
361
+ const identityPath = getWorkspacePromptPath("IDENTITY.md");
362
+ const content = existsSync(identityPath)
363
+ ? readFileSync(identityPath, "utf-8")
364
+ : "";
289
365
  const fields = parseIdentityFields(content);
290
366
  this.broadcast({
291
- type: 'identity_changed',
367
+ type: "identity_changed",
292
368
  name: fields.name,
293
369
  role: fields.role,
294
370
  personality: fields.personality,
@@ -296,7 +372,7 @@ export class DaemonServer {
296
372
  home: fields.home,
297
373
  });
298
374
  } catch (err) {
299
- log.error({ err }, 'Failed to broadcast identity change');
375
+ log.error({ err }, "Failed to broadcast identity change");
300
376
  }
301
377
  }
302
378
 
@@ -312,22 +388,29 @@ export class DaemonServer {
312
388
  try {
313
389
  bootstrapHomeBaseAppLink();
314
390
  } catch (err) {
315
- log.warn({ err }, 'Failed to bootstrap Home Base app link at daemon startup');
391
+ log.warn(
392
+ { err },
393
+ "Failed to bootstrap Home Base app link at daemon startup",
394
+ );
316
395
  }
317
396
 
318
397
  this.evictor.start();
319
398
 
320
399
  registerDaemonCallbacks({
321
- getOrCreateSession: (conversationId) => this.getOrCreateSession(conversationId),
400
+ getOrCreateSession: (conversationId) =>
401
+ this.getOrCreateSession(conversationId),
322
402
  broadcast: (msg) => this.broadcast(msg),
323
403
  });
324
404
 
325
405
  ensureBlobDir();
326
- this.blobSweepTimer = setInterval(() => {
327
- sweepStaleBlobs(30 * 60 * 1000).catch((err) => {
328
- log.warn({ err }, 'Blob sweep failed');
329
- });
330
- }, 5 * 60 * 1000);
406
+ this.blobSweepTimer = setInterval(
407
+ () => {
408
+ sweepStaleBlobs(30 * 60 * 1000).catch((err) => {
409
+ log.warn({ err }, "Blob sweep failed");
410
+ });
411
+ },
412
+ 5 * 60 * 1000,
413
+ );
331
414
 
332
415
  this.configWatcher.start(
333
416
  () => this.evictSessionsForReload(),
@@ -335,12 +418,16 @@ export class DaemonServer {
335
418
  );
336
419
  this.auth.initToken();
337
420
 
338
- let tlsCreds: { cert: string; key: string; fingerprint: string } | null = null;
421
+ let tlsCreds: { cert: string; key: string; fingerprint: string } | null =
422
+ null;
339
423
  if (isTCPEnabled()) {
340
424
  try {
341
425
  tlsCreds = await ensureTlsCert();
342
426
  } catch (err) {
343
- log.error({ err }, 'Failed to generate TLS certificate — TCP listener will not start');
427
+ log.error(
428
+ { err },
429
+ "Failed to generate TLS certificate — TCP listener will not start",
430
+ );
344
431
  }
345
432
  }
346
433
 
@@ -351,53 +438,68 @@ export class DaemonServer {
351
438
 
352
439
  const oldUmask = process.umask(0o177);
353
440
 
354
- this.server.once('error', (err) => {
441
+ this.server.once("error", (err) => {
355
442
  process.umask(oldUmask);
356
- log.error({ err, socketPath: this.socketPath }, 'Server failed to start (is another daemon already running?)');
443
+ log.error(
444
+ { err, socketPath: this.socketPath },
445
+ "Server failed to start (is another daemon already running?)",
446
+ );
357
447
  reject(err);
358
448
  });
359
449
 
360
450
  this.server.listen(this.socketPath, () => {
361
451
  process.umask(oldUmask);
362
- this.server!.removeAllListeners('error');
363
- this.server!.on('error', (err) => {
364
- log.error({ err, socketPath: this.socketPath }, 'Server socket error while running');
452
+ this.server!.removeAllListeners("error");
453
+ this.server!.on("error", (err) => {
454
+ log.error(
455
+ { err, socketPath: this.socketPath },
456
+ "Server socket error while running",
457
+ );
365
458
  });
366
459
  chmodSync(this.socketPath, 0o600);
367
460
  // Validate the chmod actually took effect — some filesystems
368
461
  // (e.g. FAT32 mounts, container overlays) silently ignore chmod.
369
462
  const socketStat = statSync(this.socketPath);
370
463
  if ((socketStat.mode & 0o077) !== 0) {
371
- const actual = '0o' + (socketStat.mode & 0o777).toString(8);
464
+ const actual = "0o" + (socketStat.mode & 0o777).toString(8);
372
465
  log.error(
373
466
  { socketPath: this.socketPath, mode: actual },
374
- 'IPC socket is accessible by other users (expected 0600) — filesystem may not support Unix permissions',
467
+ "IPC socket is accessible by other users (expected 0600) — filesystem may not support Unix permissions",
375
468
  );
376
469
  }
377
- log.info({ socketPath: this.socketPath }, 'Daemon server listening');
470
+ log.info({ socketPath: this.socketPath }, "Daemon server listening");
378
471
 
379
472
  if (tlsCreds) {
380
473
  const tcpPort = getTCPPort();
381
474
  const tcpHost = getTCPHost();
382
475
  this.tcpServer = tls.createServer(
383
476
  { cert: tlsCreds.cert, key: tlsCreds.key },
384
- (socket) => { this.handleConnection(socket); },
477
+ (socket) => {
478
+ this.handleConnection(socket);
479
+ },
385
480
  );
386
- this.tcpServer.on('error', (err) => {
387
- log.error({ err, tcpPort }, 'TLS TCP server error');
481
+ this.tcpServer.on("error", (err) => {
482
+ log.error({ err, tcpPort }, "TLS TCP server error");
388
483
  });
389
484
  const fingerprint = tlsCreds.fingerprint;
390
485
  this.tcpServer.listen(tcpPort, tcpHost, () => {
391
486
  const localIP = getLocalIPv4();
392
487
  log.info(
393
- { tcpPort, tcpHost, fingerprint, localIP, iosPairing: isIOSPairingEnabled() },
394
- 'TLS TCP listener started',
488
+ {
489
+ tcpPort,
490
+ tcpHost,
491
+ fingerprint,
492
+ localIP,
493
+ iosPairing: isIOSPairingEnabled(),
494
+ },
495
+ "TLS TCP listener started",
395
496
  );
396
497
  if (isIOSPairingEnabled() && localIP) {
397
498
  log.warn(
398
499
  { localIP, tcpPort },
399
- 'iOS pairing enabled — daemon is reachable on the local network at %s:%d',
400
- localIP, tcpPort,
500
+ "iOS pairing enabled — daemon is reachable on the local network at %s:%d",
501
+ localIP,
502
+ tcpPort,
401
503
  );
402
504
  }
403
505
  });
@@ -424,7 +526,10 @@ export class DaemonServer {
424
526
  try {
425
527
  removeSocketFile(this.socketPath);
426
528
  } catch (err) {
427
- log.warn({ err, socketPath: this.socketPath }, 'Failed to remove socket file during shutdown');
529
+ log.warn(
530
+ { err, socketPath: this.socketPath },
531
+ "Failed to remove socket file during shutdown",
532
+ );
428
533
  }
429
534
  resolve();
430
535
  });
@@ -462,75 +567,117 @@ export class DaemonServer {
462
567
  this.cuObservationParseSequence.clear();
463
568
 
464
569
  await Promise.all([serverClosed, tcpServerClosed]);
465
- log.info('Daemon server stopped');
570
+ log.info("Daemon server stopped");
466
571
  }
467
572
 
468
573
  // ── Connection handling ─────────────────────────────────────────────
469
574
 
470
575
  private handleConnection(socket: net.Socket): void {
471
576
  if (this.connectedSockets.size >= DaemonServer.MAX_CONNECTIONS) {
472
- log.warn({ current: this.connectedSockets.size, max: DaemonServer.MAX_CONNECTIONS }, 'Connection limit reached, rejecting client');
473
- socket.once('error', (err) => {
474
- log.error({ err }, 'Socket error while rejecting connection');
577
+ log.warn(
578
+ {
579
+ current: this.connectedSockets.size,
580
+ max: DaemonServer.MAX_CONNECTIONS,
581
+ },
582
+ "Connection limit reached, rejecting client",
583
+ );
584
+ socket.once("error", (err) => {
585
+ log.error({ err }, "Socket error while rejecting connection");
475
586
  });
476
- socket.write(serialize({ type: 'error', message: `Connection limit reached (max ${DaemonServer.MAX_CONNECTIONS})` }));
587
+ socket.write(
588
+ serialize({
589
+ type: "error",
590
+ message: `Connection limit reached (max ${DaemonServer.MAX_CONNECTIONS})`,
591
+ }),
592
+ );
477
593
  socket.destroy();
478
594
  return;
479
595
  }
480
596
 
481
- log.info('Client connected');
597
+ log.info("Client connected");
482
598
  this.connectedSockets.add(socket);
483
599
  const parser = createMessageParser({ maxLineSize: MAX_LINE_SIZE });
484
600
 
485
601
  if (this.auth.shouldAutoAuth()) {
486
602
  this.auth.markAuthenticated(socket);
487
- log.warn('Auto-authenticated client (VELLUM_DAEMON_NOAUTH is set — token auth bypassed)');
488
- this.send(socket, { type: 'auth_result', success: true });
603
+ log.warn(
604
+ "Auto-authenticated client (VELLUM_DAEMON_NOAUTH is set token auth bypassed)",
605
+ );
606
+ this.send(socket, { type: "auth_result", success: true });
489
607
  this.sendInitialSession(socket).catch((err) => {
490
- log.error({ err }, 'Failed to send initial session info after auto-auth');
608
+ log.error(
609
+ { err },
610
+ "Failed to send initial session info after auto-auth",
611
+ );
491
612
  });
492
613
  }
493
614
 
494
615
  this.auth.startTimeout(socket, () => {
495
- this.send(socket, { type: 'error', message: 'Authentication timeout' });
616
+ this.send(socket, { type: "error", message: "Authentication timeout" });
496
617
  socket.destroy();
497
618
  });
498
619
 
499
- socket.on('data', (data) => {
620
+ socket.on("data", (data) => {
500
621
  const chunkReceivedAtMs = Date.now();
501
622
  const parseStartNs = process.hrtime.bigint();
502
623
  let parsed;
503
624
  try {
504
625
  parsed = parser.feedRaw(data.toString());
505
626
  } catch (err) {
506
- log.error({ err }, 'IPC parse error (malformed JSON or message exceeded size limit), dropping client');
507
- socket.write(serialize({ type: 'error', message: `IPC parse error: ${(err as Error).message}` }));
627
+ log.error(
628
+ { err },
629
+ "IPC parse error (malformed JSON or message exceeded size limit), dropping client",
630
+ );
631
+ socket.write(
632
+ serialize({
633
+ type: "error",
634
+ message: `IPC parse error: ${(err as Error).message}`,
635
+ }),
636
+ );
508
637
  socket.destroy();
509
638
  return;
510
639
  }
511
640
  const parsedAtMs = Date.now();
512
- const parseDurationMs = Number(process.hrtime.bigint() - parseStartNs) / 1_000_000;
641
+ const parseDurationMs =
642
+ Number(process.hrtime.bigint() - parseStartNs) / 1_000_000;
513
643
  for (const entry of parsed) {
514
644
  const msg = entry.msg;
515
- if (typeof msg === 'object' && msg != null && (msg as { type?: unknown }).type === 'cu_observation') {
645
+ if (
646
+ typeof msg === "object" &&
647
+ msg != null &&
648
+ (msg as { type?: unknown }).type === "cu_observation"
649
+ ) {
516
650
  const maybeSessionId = (msg as { sessionId?: unknown }).sessionId;
517
- const sessionId = typeof maybeSessionId === 'string' ? maybeSessionId : 'unknown';
518
- const previousSequence = this.cuObservationParseSequence.get(sessionId) ?? 0;
651
+ const sessionId =
652
+ typeof maybeSessionId === "string" ? maybeSessionId : "unknown";
653
+ const previousSequence =
654
+ this.cuObservationParseSequence.get(sessionId) ?? 0;
519
655
  const sequence = previousSequence + 1;
520
656
  this.cuObservationParseSequence.set(sessionId, sequence);
521
- log.info({
522
- sessionId,
523
- sequence,
524
- chunkReceivedAtMs,
525
- parsedAtMs,
526
- parseDurationMs,
527
- messageBytes: entry.rawByteLength,
528
- }, 'IPC_METRIC cu_observation_parse');
657
+ log.info(
658
+ {
659
+ sessionId,
660
+ sequence,
661
+ chunkReceivedAtMs,
662
+ parsedAtMs,
663
+ parseDurationMs,
664
+ messageBytes: entry.rawByteLength,
665
+ },
666
+ "IPC_METRIC cu_observation_parse",
667
+ );
529
668
  }
530
669
  const result = validateClientMessage(msg);
531
670
  if (!result.valid) {
532
- log.warn({ reason: result.reason }, 'Invalid IPC message, dropping client');
533
- socket.write(serialize({ type: 'error', message: `Invalid message: ${result.reason}` }));
671
+ log.warn(
672
+ { reason: result.reason },
673
+ "Invalid IPC message, dropping client",
674
+ );
675
+ socket.write(
676
+ serialize({
677
+ type: "error",
678
+ message: `Invalid message: ${result.reason}`,
679
+ }),
680
+ );
534
681
  socket.destroy();
535
682
  return;
536
683
  }
@@ -539,29 +686,42 @@ export class DaemonServer {
539
686
  if (!this.auth.isAuthenticated(socket)) {
540
687
  this.auth.clearTimeout(socket);
541
688
 
542
- if (result.message.type === 'auth') {
543
- const authMsg = result.message as { type: 'auth'; token: string };
689
+ if (result.message.type === "auth") {
690
+ const authMsg = result.message as { type: "auth"; token: string };
544
691
  if (this.auth.authenticate(socket, authMsg.token)) {
545
- this.send(socket, { type: 'auth_result', success: true });
692
+ this.send(socket, { type: "auth_result", success: true });
546
693
  this.sendInitialSession(socket).catch((err) => {
547
- log.error({ err }, 'Failed to send initial session info after auth');
694
+ log.error(
695
+ { err },
696
+ "Failed to send initial session info after auth",
697
+ );
548
698
  });
549
699
  } else {
550
- this.send(socket, { type: 'auth_result', success: false, message: 'Invalid token' });
700
+ this.send(socket, {
701
+ type: "auth_result",
702
+ success: false,
703
+ message: "Invalid token",
704
+ });
551
705
  socket.destroy();
552
706
  }
553
707
  continue;
554
708
  }
555
709
 
556
- log.warn({ type: result.message.type }, 'Unauthenticated client sent non-auth message, disconnecting');
557
- this.send(socket, { type: 'error', message: 'Authentication required' });
710
+ log.warn(
711
+ { type: result.message.type },
712
+ "Unauthenticated client sent non-auth message, disconnecting",
713
+ );
714
+ this.send(socket, {
715
+ type: "error",
716
+ message: "Authentication required",
717
+ });
558
718
  socket.destroy();
559
719
  return;
560
720
  }
561
721
 
562
722
  // Already-authenticated socket sending auth (e.g. auto-auth'd + local token)
563
- if (result.message.type === 'auth') {
564
- this.send(socket, { type: 'auth_result', success: true });
723
+ if (result.message.type === "auth") {
724
+ this.send(socket, { type: "auth_result", success: true });
565
725
  continue;
566
726
  }
567
727
 
@@ -569,7 +729,7 @@ export class DaemonServer {
569
729
  }
570
730
  });
571
731
 
572
- socket.on('close', () => {
732
+ socket.on("close", () => {
573
733
  this.auth.cleanupSocket(socket);
574
734
  this.connectedSockets.delete(socket);
575
735
  this.socketSandboxOverride.delete(socket);
@@ -604,11 +764,14 @@ export class DaemonServer {
604
764
  }
605
765
  }
606
766
  this.socketToCuSession.delete(socket);
607
- log.info('Client disconnected');
767
+ log.info("Client disconnected");
608
768
  });
609
769
 
610
- socket.on('error', (err) => {
611
- log.error({ err, remoteAddress: socket.remoteAddress }, 'Client socket error');
770
+ socket.on("error", (err) => {
771
+ log.error(
772
+ { err, remoteAddress: socket.remoteAddress },
773
+ "Client socket error",
774
+ );
612
775
  });
613
776
  }
614
777
 
@@ -617,7 +780,7 @@ export class DaemonServer {
617
780
  setHttpPort(port: number): void {
618
781
  this.httpPort = port;
619
782
  this.broadcast({
620
- type: 'daemon_status',
783
+ type: "daemon_status",
621
784
  httpPort: port,
622
785
  version: daemonVersion,
623
786
  });
@@ -670,7 +833,7 @@ export class DaemonServer {
670
833
  const conversation = conversationStore.getLatestConversation();
671
834
  if (!conversation) {
672
835
  this.send(socket, {
673
- type: 'daemon_status',
836
+ type: "daemon_status",
674
837
  httpPort: this.httpPort,
675
838
  version: daemonVersion,
676
839
  });
@@ -680,14 +843,14 @@ export class DaemonServer {
680
843
  await this.getOrCreateSession(conversation.id, undefined, false);
681
844
 
682
845
  this.send(socket, {
683
- type: 'session_info',
846
+ type: "session_info",
684
847
  sessionId: conversation.id,
685
- title: conversation.title ?? 'New Conversation',
848
+ title: conversation.title ?? "New Conversation",
686
849
  threadType: normalizeThreadType(conversation.threadType),
687
850
  });
688
851
 
689
852
  this.send(socket, {
690
- type: 'daemon_status',
853
+ type: "daemon_status",
691
854
  httpPort: this.httpPort,
692
855
  version: daemonVersion,
693
856
  });
@@ -710,7 +873,7 @@ export class DaemonServer {
710
873
  getSubagentManager().updateParentSender(conversationId, sendToClient);
711
874
  };
712
875
 
713
- if (options && Object.values(options).some(v => v !== undefined)) {
876
+ if (options && Object.values(options).some((v) => v !== undefined)) {
714
877
  this.sessionOptions.set(conversationId, {
715
878
  ...this.sessionOptions.get(conversationId),
716
879
  ...options,
@@ -734,14 +897,25 @@ export class DaemonServer {
734
897
 
735
898
  const createPromise = (async () => {
736
899
  const config = getConfig();
737
- let provider = getFailoverProvider(config.provider, config.providerOrder);
900
+ let provider = getFailoverProvider(
901
+ config.provider,
902
+ config.providerOrder,
903
+ );
738
904
  const { rateLimit } = config;
739
- if (rateLimit.maxRequestsPerMinute > 0 || rateLimit.maxTokensPerSession > 0) {
740
- provider = new RateLimitProvider(provider, rateLimit, this.sharedRequestTimestamps);
905
+ if (
906
+ rateLimit.maxRequestsPerMinute > 0 ||
907
+ rateLimit.maxTokensPerSession > 0
908
+ ) {
909
+ provider = new RateLimitProvider(
910
+ provider,
911
+ rateLimit,
912
+ this.sharedRequestTimestamps,
913
+ );
741
914
  }
742
915
  const workingDir = getSandboxWorkingDir();
743
916
 
744
- const systemPrompt = storedOptions?.systemPromptOverride ?? buildSystemPrompt();
917
+ const systemPrompt =
918
+ storedOptions?.systemPromptOverride ?? buildSystemPrompt();
745
919
  const maxTokens = storedOptions?.maxResponseTokens ?? config.maxTokens;
746
920
 
747
921
  const memoryPolicy = this.deriveMemoryPolicy(conversationId);
@@ -795,7 +969,9 @@ export class DaemonServer {
795
969
  sharedRequestTimestamps: this.sharedRequestTimestamps,
796
970
  debounceTimers: this.configWatcher.timers,
797
971
  suppressConfigReload: this.configWatcher.suppressConfigReload,
798
- setSuppressConfigReload: (value: boolean) => { this.configWatcher.suppressConfigReload = value; },
972
+ setSuppressConfigReload: (value: boolean) => {
973
+ this.configWatcher.suppressConfigReload = value;
974
+ },
799
975
  updateConfigFingerprint: () => {
800
976
  this.configWatcher.updateFingerprint();
801
977
  },
@@ -809,16 +985,25 @@ export class DaemonServer {
809
985
  };
810
986
  }
811
987
 
812
- private dispatchMessage(msg: Parameters<typeof handleMessage>[0], socket: net.Socket): void {
813
- if (msg.type !== 'ping') {
988
+ private dispatchMessage(
989
+ msg: Parameters<typeof handleMessage>[0],
990
+ socket: net.Socket,
991
+ ): void {
992
+ if (msg.type !== "ping") {
814
993
  const now = Date.now();
815
- if (now - this.configWatcher.lastConfigRefreshTime >= ConfigWatcher.REFRESH_INTERVAL_MS) {
994
+ if (
995
+ now - this.configWatcher.lastConfigRefreshTime >=
996
+ ConfigWatcher.REFRESH_INTERVAL_MS
997
+ ) {
816
998
  try {
817
999
  const changed = this.configWatcher.refreshConfigFromSources();
818
1000
  if (changed) this.evictSessionsForReload();
819
1001
  this.configWatcher.lastConfigRefreshTime = now;
820
1002
  } catch (err) {
821
- log.warn({ err }, 'Failed to refresh config from secure sources before handling IPC message');
1003
+ log.warn(
1004
+ { err },
1005
+ "Failed to refresh config from secure sources before handling IPC message",
1006
+ );
822
1007
  }
823
1008
  }
824
1009
  }
@@ -834,24 +1019,47 @@ export class DaemonServer {
834
1019
  options: SessionCreateOptions | undefined,
835
1020
  sourceChannel: string | undefined,
836
1021
  sourceInterface: string | undefined,
837
- ): Promise<{ session: Session; attachments: { id: string; filename: string; mimeType: string; data: string }[] }> {
1022
+ ): Promise<{
1023
+ session: Session;
1024
+ attachments: {
1025
+ id: string;
1026
+ filename: string;
1027
+ mimeType: string;
1028
+ data: string;
1029
+ }[];
1030
+ }> {
838
1031
  const ingressCheck = checkIngressForSecrets(content);
839
1032
  if (ingressCheck.blocked) {
840
- throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
1033
+ throw new IngressBlockedError(
1034
+ ingressCheck.userNotice!,
1035
+ ingressCheck.detectedTypes,
1036
+ );
841
1037
  }
842
1038
 
843
- const session = await this.getOrCreateSession(conversationId, undefined, true, options);
1039
+ const session = await this.getOrCreateSession(
1040
+ conversationId,
1041
+ undefined,
1042
+ true,
1043
+ options,
1044
+ );
844
1045
 
845
1046
  if (session.isProcessing()) {
846
- throw new Error('Session is already processing a message');
1047
+ throw new Error("Session is already processing a message");
847
1048
  }
848
1049
 
849
- const resolvedChannel = resolveTurnChannel(sourceChannel, options?.transport?.channelId);
1050
+ const resolvedChannel = resolveTurnChannel(
1051
+ sourceChannel,
1052
+ options?.transport?.channelId,
1053
+ );
850
1054
  const resolvedInterface = resolveTurnInterface(sourceInterface);
851
- session.setAssistantId(options?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID);
1055
+ session.setAssistantId(
1056
+ options?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
1057
+ );
852
1058
  session.setGuardianContext(options?.guardianContext ?? null);
853
1059
  await session.ensureActorScopedHistory();
854
- session.setChannelCapabilities(resolveChannelCapabilities(sourceChannel, sourceInterface));
1060
+ session.setChannelCapabilities(
1061
+ resolveChannelCapabilities(sourceChannel, sourceInterface),
1062
+ );
855
1063
  session.setCommandIntent(options?.commandIntent ?? null);
856
1064
  session.setTurnChannelContext({
857
1065
  userMessageChannel: resolvedChannel,
@@ -883,11 +1091,20 @@ export class DaemonServer {
883
1091
  sourceInterface?: string,
884
1092
  ): Promise<{ messageId: string }> {
885
1093
  const { session, attachments } = await this.prepareSessionForMessage(
886
- conversationId, content, attachmentIds, options, sourceChannel, sourceInterface,
1094
+ conversationId,
1095
+ content,
1096
+ attachmentIds,
1097
+ options,
1098
+ sourceChannel,
1099
+ sourceInterface,
887
1100
  );
888
1101
 
889
1102
  const requestId = crypto.randomUUID();
890
- const messageId = await session.persistUserMessage(content, attachments, requestId);
1103
+ const messageId = await session.persistUserMessage(
1104
+ content,
1105
+ attachments,
1106
+ requestId,
1107
+ );
891
1108
 
892
1109
  // Register pending interactions so channel approval interception can
893
1110
  // find the session by requestId when confirmation/secret events fire.
@@ -900,16 +1117,22 @@ export class DaemonServer {
900
1117
  session.updateClient(onEvent, false);
901
1118
  }
902
1119
 
903
- session.runAgentLoop(content, messageId, onEvent, { isInteractive: options?.isInteractive ?? false })
1120
+ session
1121
+ .runAgentLoop(content, messageId, onEvent, {
1122
+ isInteractive: options?.isInteractive ?? false,
1123
+ })
904
1124
  .finally(() => {
905
1125
  // Only reset if no other caller (e.g. a real IPC client) has rebound
906
1126
  // the session's sender while the agent loop was running.
907
- if (options?.isInteractive === true && session.getCurrentSender() === onEvent) {
1127
+ if (
1128
+ options?.isInteractive === true &&
1129
+ session.getCurrentSender() === onEvent
1130
+ ) {
908
1131
  session.updateClient(() => {}, true);
909
1132
  }
910
1133
  })
911
1134
  .catch((err) => {
912
- log.error({ err, conversationId }, 'Background agent loop failed');
1135
+ log.error({ err, conversationId }, "Background agent loop failed");
913
1136
  });
914
1137
 
915
1138
  return { messageId };
@@ -924,28 +1147,42 @@ export class DaemonServer {
924
1147
  sourceInterface?: string,
925
1148
  ): Promise<{ messageId: string }> {
926
1149
  const { session, attachments } = await this.prepareSessionForMessage(
927
- conversationId, content, attachmentIds, options, sourceChannel, sourceInterface,
1150
+ conversationId,
1151
+ content,
1152
+ attachmentIds,
1153
+ options,
1154
+ sourceChannel,
1155
+ sourceInterface,
928
1156
  );
929
1157
 
930
1158
  const slashResult = resolveSlash(content);
931
1159
 
932
- if (slashResult.kind === 'unknown') {
1160
+ if (slashResult.kind === "unknown") {
933
1161
  const serverTurnCtx = session.getTurnChannelContext();
934
1162
  const serverInterfaceCtx = session.getTurnInterfaceContext();
935
- const serverProvenance = provenanceFromGuardianContext(session.guardianContext);
1163
+ const serverProvenance = provenanceFromGuardianContext(
1164
+ session.guardianContext,
1165
+ );
936
1166
  const serverChannelMeta = {
937
1167
  ...serverProvenance,
938
1168
  ...(serverTurnCtx
939
- ? { userMessageChannel: serverTurnCtx.userMessageChannel, assistantMessageChannel: serverTurnCtx.assistantMessageChannel }
1169
+ ? {
1170
+ userMessageChannel: serverTurnCtx.userMessageChannel,
1171
+ assistantMessageChannel: serverTurnCtx.assistantMessageChannel,
1172
+ }
940
1173
  : {}),
941
1174
  ...(serverInterfaceCtx
942
- ? { userMessageInterface: serverInterfaceCtx.userMessageInterface, assistantMessageInterface: serverInterfaceCtx.assistantMessageInterface }
1175
+ ? {
1176
+ userMessageInterface: serverInterfaceCtx.userMessageInterface,
1177
+ assistantMessageInterface:
1178
+ serverInterfaceCtx.assistantMessageInterface,
1179
+ }
943
1180
  : {}),
944
1181
  };
945
1182
  const userMsg = createUserMessage(content, attachments);
946
1183
  const persisted = await conversationStore.addMessage(
947
1184
  conversationId,
948
- 'user',
1185
+ "user",
949
1186
  JSON.stringify(userMsg.content),
950
1187
  serverChannelMeta,
951
1188
  );
@@ -953,23 +1190,35 @@ export class DaemonServer {
953
1190
 
954
1191
  if (serverTurnCtx) {
955
1192
  try {
956
- conversationStore.setConversationOriginChannelIfUnset(conversationId, serverTurnCtx.userMessageChannel);
1193
+ conversationStore.setConversationOriginChannelIfUnset(
1194
+ conversationId,
1195
+ serverTurnCtx.userMessageChannel,
1196
+ );
957
1197
  } catch (err) {
958
- log.warn({ err, conversationId }, 'Failed to set origin channel (best-effort)');
1198
+ log.warn(
1199
+ { err, conversationId },
1200
+ "Failed to set origin channel (best-effort)",
1201
+ );
959
1202
  }
960
1203
  }
961
1204
  if (serverInterfaceCtx) {
962
1205
  try {
963
- conversationStore.setConversationOriginInterfaceIfUnset(conversationId, serverInterfaceCtx.userMessageInterface);
1206
+ conversationStore.setConversationOriginInterfaceIfUnset(
1207
+ conversationId,
1208
+ serverInterfaceCtx.userMessageInterface,
1209
+ );
964
1210
  } catch (err) {
965
- log.warn({ err, conversationId }, 'Failed to set origin interface (best-effort)');
1211
+ log.warn(
1212
+ { err, conversationId },
1213
+ "Failed to set origin interface (best-effort)",
1214
+ );
966
1215
  }
967
1216
  }
968
1217
 
969
1218
  const assistantMsg = createAssistantMessage(slashResult.message);
970
1219
  await conversationStore.addMessage(
971
1220
  conversationId,
972
- 'assistant',
1221
+ "assistant",
973
1222
  JSON.stringify(assistantMsg.content),
974
1223
  serverChannelMeta,
975
1224
  );
@@ -979,14 +1228,18 @@ export class DaemonServer {
979
1228
 
980
1229
  const resolvedContent = slashResult.content;
981
1230
 
982
- if (slashResult.kind === 'rewritten') {
1231
+ if (slashResult.kind === "rewritten") {
983
1232
  session.setPreactivatedSkillIds([slashResult.skillId]);
984
1233
  }
985
1234
 
986
1235
  const requestId = crypto.randomUUID();
987
1236
  let messageId: string;
988
1237
  try {
989
- messageId = await session.persistUserMessage(resolvedContent, attachments, requestId);
1238
+ messageId = await session.persistUserMessage(
1239
+ resolvedContent,
1240
+ attachments,
1241
+ requestId,
1242
+ );
990
1243
  } catch (err) {
991
1244
  session.setPreactivatedSkillIds(undefined);
992
1245
  throw err;
@@ -1004,16 +1257,16 @@ export class DaemonServer {
1004
1257
  }
1005
1258
 
1006
1259
  try {
1007
- await session.runAgentLoop(
1008
- resolvedContent,
1009
- messageId,
1010
- onEvent,
1011
- { isInteractive: options?.isInteractive ?? false },
1012
- );
1260
+ await session.runAgentLoop(resolvedContent, messageId, onEvent, {
1261
+ isInteractive: options?.isInteractive ?? false,
1262
+ });
1013
1263
  } finally {
1014
1264
  // Only reset if no other caller (e.g. a real IPC client) has rebound
1015
1265
  // the session's sender while the agent loop was running.
1016
- if (options?.isInteractive === true && session.getCurrentSender() === onEvent) {
1266
+ if (
1267
+ options?.isInteractive === true &&
1268
+ session.getCurrentSender() === onEvent
1269
+ ) {
1017
1270
  session.updateClient(() => {}, true);
1018
1271
  }
1019
1272
  }
@@ -1029,4 +1282,11 @@ export class DaemonServer {
1029
1282
  return this.getOrCreateSession(conversationId, undefined, true);
1030
1283
  }
1031
1284
 
1285
+ /**
1286
+ * Look up an active session by ID without creating one.
1287
+ * Returns undefined if no session with that ID exists.
1288
+ */
1289
+ findSession(sessionId: string): Session | undefined {
1290
+ return this.sessions.get(sessionId);
1291
+ }
1032
1292
  }