@vellumai/assistant 0.9.0 → 0.9.1-staging.1

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 (222) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +4 -4
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
  12. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  13. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  14. package/openapi.yaml +135 -59
  15. package/package.json +2 -1
  16. package/scripts/sync-llm-catalog.ts +6 -15
  17. package/scripts/sync-web-search-catalog.ts +3 -11
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
  19. package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
  20. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  21. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  22. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  23. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  24. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  25. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  26. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  27. package/src/__tests__/btw-routes.test.ts +6 -8
  28. package/src/__tests__/checker.test.ts +0 -3
  29. package/src/__tests__/config-loader-backfill.test.ts +103 -6
  30. package/src/__tests__/config-watcher.test.ts +0 -18
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  32. package/src/__tests__/credential-broker.test.ts +449 -1
  33. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  34. package/src/__tests__/credential-prompt-route.test.ts +3 -4
  35. package/src/__tests__/credential-routes.test.ts +360 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -13
  37. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  38. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  39. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  40. package/src/__tests__/identity-routes.test.ts +0 -189
  41. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  42. package/src/__tests__/invite-redemption-service.test.ts +4 -4
  43. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -22
  45. package/src/__tests__/llm-resolver.test.ts +49 -24
  46. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  47. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  48. package/src/__tests__/persona-resolver.test.ts +11 -14
  49. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  50. package/src/__tests__/registry.test.ts +2 -7
  51. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  52. package/src/__tests__/schedule-routes.test.ts +0 -30
  53. package/src/__tests__/schedule-tools.test.ts +2 -18
  54. package/src/__tests__/skill-execute-input.test.ts +46 -1
  55. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  56. package/src/__tests__/subagent-tools.test.ts +116 -0
  57. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  58. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  59. package/src/__tests__/token-manager.test.ts +519 -0
  60. package/src/__tests__/tool-executor.test.ts +0 -79
  61. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  62. package/src/__tests__/trusted-contact-verification.test.ts +6 -6
  63. package/src/__tests__/voice-invite-redemption.test.ts +2 -2
  64. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  65. package/src/__tests__/workspace-greetings.test.ts +152 -0
  66. package/src/agent/loop.ts +25 -5
  67. package/src/api/README.md +6 -6
  68. package/src/api/responses/conversation-message.ts +2 -4
  69. package/src/api/responses/home.ts +0 -4
  70. package/src/approvals/guardian-request-resolvers.ts +2 -2
  71. package/src/calls/relay-access-wait.ts +1 -1
  72. package/src/calls/voice-session-bridge.ts +2 -2
  73. package/src/cli/commands/plugins.ts +143 -2
  74. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  75. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
  76. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
  77. package/src/cli/lib/diff-plugin.ts +346 -0
  78. package/src/cli/lib/install-from-github.ts +105 -17
  79. package/src/cli/lib/merge-plugin-tree.ts +228 -0
  80. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  81. package/src/cli/lib/upgrade-plugin.ts +270 -10
  82. package/src/cli/program.ts +0 -2
  83. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  84. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  85. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  86. package/src/config/bundled-tool-registry.ts +2 -7
  87. package/src/config/call-site-defaults.ts +12 -2
  88. package/src/config/feature-flag-registry.json +1 -17
  89. package/src/config/inference-profile-validation.ts +26 -0
  90. package/src/config/loader.ts +4 -0
  91. package/src/config/profile-order.ts +28 -0
  92. package/src/config/schemas/elevenlabs.ts +0 -1
  93. package/src/config/schemas/platform.ts +0 -8
  94. package/src/config/seed-inference-profiles.ts +25 -20
  95. package/src/contacts/contact-store.ts +87 -96
  96. package/src/contacts/contacts-write.ts +5 -21
  97. package/src/context/compactor.ts +2 -2
  98. package/src/credential-execution/process-manager.ts +55 -14
  99. package/src/credential-execution/prompted-credential.ts +2 -3
  100. package/src/daemon/config-watcher.ts +0 -4
  101. package/src/daemon/conversation-agent-loop.ts +15 -4
  102. package/src/daemon/conversation-slash.ts +2 -23
  103. package/src/daemon/conversation-tool-setup.ts +11 -3
  104. package/src/daemon/conversation.ts +2 -0
  105. package/src/daemon/handlers/config-channels.ts +20 -16
  106. package/src/daemon/handlers/config-slack-channel.ts +2 -3
  107. package/src/daemon/lifecycle.ts +0 -7
  108. package/src/daemon/message-types/conversations.ts +3 -3
  109. package/src/daemon/message-types/sync.ts +0 -1
  110. package/src/daemon/orphan-reaper.test.ts +0 -19
  111. package/src/daemon/orphan-reaper.ts +2 -24
  112. package/src/daemon/server.ts +0 -10
  113. package/src/home/relationship-state.ts +2 -4
  114. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  115. package/src/memory/bookmark-crud.ts +1 -2
  116. package/src/memory/db-init.ts +7 -17
  117. package/src/memory/embedding-backend.ts +23 -0
  118. package/src/memory/embedding-billing-breaker.ts +96 -0
  119. package/src/memory/jobs-store.ts +25 -13
  120. package/src/memory/jobs-worker.ts +52 -0
  121. package/src/memory/memory-retrospective-constants.ts +4 -4
  122. package/src/memory/memory-retrospective-job.ts +19 -227
  123. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
  124. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
  125. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  126. package/src/memory/migrations/index.ts +1 -0
  127. package/src/memory/migrations/run-migrations.ts +41 -0
  128. package/src/memory/migrations/validate-migration-state.ts +1 -1
  129. package/src/memory/schema/contacts.ts +0 -4
  130. package/src/messaging/providers/slack/adapter.ts +1 -1
  131. package/src/notifications/adapters/shared.ts +29 -0
  132. package/src/notifications/adapters/slack.ts +5 -32
  133. package/src/notifications/adapters/telegram.ts +2 -20
  134. package/src/notifications/broadcaster.ts +10 -1
  135. package/src/notifications/home-feed-side-effect.ts +4 -3
  136. package/src/notifications/notification-utils.ts +17 -19
  137. package/src/notifications/types.ts +7 -0
  138. package/src/oauth/AGENTS.md +5 -24
  139. package/src/plugin-api/constants.ts +1 -1
  140. package/src/plugin-api/index.ts +6 -1
  141. package/src/plugin-api/model-profiles.ts +33 -0
  142. package/src/plugin-api/types.ts +50 -2
  143. package/src/plugins/defaults/index.ts +25 -0
  144. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  145. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  146. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  147. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  148. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  149. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  150. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
  151. package/src/prompts/persona-resolver.ts +2 -2
  152. package/src/runtime/AGENTS.md +0 -1
  153. package/src/runtime/actor-trust-resolver.ts +12 -44
  154. package/src/runtime/btw-sidechain.ts +3 -6
  155. package/src/runtime/channel-approval-types.ts +18 -45
  156. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  157. package/src/runtime/channel-verification-service.ts +4 -3
  158. package/src/runtime/invite-redemption-service.ts +3 -3
  159. package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
  160. package/src/runtime/routes/app-routes.ts +1 -1
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  162. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  163. package/src/runtime/routes/btw-routes.ts +1 -27
  164. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  165. package/src/runtime/routes/conversation-routes.ts +2 -2
  166. package/src/runtime/routes/credential-routes.ts +40 -16
  167. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  168. package/src/runtime/routes/identity-routes.ts +1 -296
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
  170. package/src/runtime/routes/plugins-routes.ts +171 -5
  171. package/src/runtime/routes/schedule-routes.ts +0 -22
  172. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  173. package/src/runtime/routes/workflow-routes.ts +0 -28
  174. package/src/runtime/routes/workspace-greetings.ts +55 -0
  175. package/src/runtime/sync/resource-sync-events.ts +1 -11
  176. package/src/schedule/inference-profile.ts +2 -14
  177. package/src/subagent/manager.ts +6 -0
  178. package/src/subagent/types.ts +6 -0
  179. package/src/tools/AGENTS.md +3 -3
  180. package/src/tools/browser/browser-execution.ts +1 -1
  181. package/src/tools/network/web-search-error.ts +1 -1
  182. package/src/tools/permission-checker.ts +1 -1
  183. package/src/tools/schedule/create.ts +3 -9
  184. package/src/tools/schedule/update.ts +2 -10
  185. package/src/tools/side-effects.ts +2 -17
  186. package/src/tools/skills/execute.ts +34 -0
  187. package/src/tools/subagent/spawn.ts +34 -9
  188. package/src/tools/tool-approval-handler.ts +1 -3
  189. package/src/tools/tool-manifest.ts +0 -2
  190. package/src/tools/ui-surface/definitions.ts +39 -1
  191. package/src/tools/workflows/run-workflow.test.ts +8 -18
  192. package/src/util/platform.ts +2 -2
  193. package/src/workflows/capabilities.ts +2 -3
  194. package/src/workflows/run-manager.test.ts +0 -25
  195. package/src/workflows/run-manager.ts +2 -24
  196. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  197. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  198. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  199. package/src/__tests__/credential-vault.test.ts +0 -1706
  200. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  201. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  202. package/src/cli/commands/__tests__/task.test.ts +0 -914
  203. package/src/cli/commands/task.ts +0 -771
  204. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  205. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  206. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  207. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  208. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  209. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  210. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  211. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  212. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  213. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  214. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  215. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  216. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  217. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  218. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  219. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  220. package/src/memory/preloaded-apps.ts +0 -116
  221. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  222. package/src/tools/credentials/vault.ts +0 -712
@@ -6,8 +6,6 @@
6
6
  * identity and access-control state.
7
7
  */
8
8
 
9
- import type { ChannelId } from "../channels/types.js";
10
- import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
11
9
  import { emitContactChange } from "./contact-events.js";
12
10
  import {
13
11
  findContactChannel,
@@ -25,7 +23,6 @@ import type {
25
23
  ContactWriteResult,
26
24
  } from "./types.js";
27
25
 
28
-
29
26
  // ── Guardian operations ──────────────────────────────────────────────
30
27
 
31
28
  /**
@@ -68,12 +65,7 @@ export function upsertContactChannel(params: {
68
65
  let address: string;
69
66
 
70
67
  if (params.externalUserId) {
71
- const canonical =
72
- canonicalizeInboundIdentity(
73
- params.sourceChannel as ChannelId,
74
- params.externalUserId,
75
- ) ?? params.externalUserId;
76
- address = canonical;
68
+ address = params.externalUserId;
77
69
  } else if (params.externalChatId) {
78
70
  address = params.externalChatId;
79
71
  } else {
@@ -84,8 +76,8 @@ export function upsertContactChannel(params: {
84
76
  let displayName = params.displayName ?? params.externalUserId ?? "Unknown";
85
77
 
86
78
  // When binding a channel to a specific contact (invite redemption), preserve
87
- // the target contact's curated displayName (e.g. "Mom") instead of overwriting
88
- // it with the redeemer's externalUserId.
79
+ // the target contact's curated displayName instead of overwriting it
80
+ // with the raw platform identity.
89
81
  if (params.contactId) {
90
82
  const targetContact = getContact(params.contactId);
91
83
  if (targetContact?.displayName?.trim().length) {
@@ -93,13 +85,6 @@ export function upsertContactChannel(params: {
93
85
  }
94
86
  }
95
87
 
96
- const canonicalId = params.externalUserId
97
- ? (canonicalizeInboundIdentity(
98
- params.sourceChannel as ChannelId,
99
- params.externalUserId,
100
- ) ?? params.externalUserId)
101
- : null;
102
-
103
88
  upsertContact({
104
89
  id: params.contactId,
105
90
  displayName,
@@ -108,7 +93,7 @@ export function upsertContactChannel(params: {
108
93
  {
109
94
  type: params.sourceChannel,
110
95
  address,
111
- externalUserId: canonicalId,
96
+ externalUserId: params.externalUserId ? address : null,
112
97
  externalChatId: params.externalChatId ?? null,
113
98
  status: (params.status as ChannelStatus) ?? undefined,
114
99
  policy: (params.policy as ChannelPolicy) ?? undefined,
@@ -133,7 +118,7 @@ export function upsertContactChannel(params: {
133
118
 
134
119
  const contactResult = findContactChannel({
135
120
  channelType: params.sourceChannel,
136
- externalUserId: canonicalId ?? undefined,
121
+ address,
137
122
  externalChatId: params.externalChatId,
138
123
  });
139
124
 
@@ -174,4 +159,3 @@ export function revokeMember(
174
159
  emitContactChange();
175
160
  return { contact, channel: updatedChannel };
176
161
  }
177
-
@@ -1132,8 +1132,8 @@ export async function runAssistantDrivenCompaction(
1132
1132
  // from the moment of capture — keeping them would (a) waste tokens on
1133
1133
  // outdated content, (b) duplicate against the freshly re-injected blocks
1134
1134
  // the next turn produces, and (c) leak `<system_reminder>` text the model
1135
- // is not supposed to see in history. `<turn_context>` and `<workspace>`
1136
- // are intentionally preserved by `RUNTIME_INJECTION_PREFIXES`.
1135
+ // is not supposed to see in history. `<turn_context>` is intentionally
1136
+ // preserved by `RUNTIME_INJECTION_PREFIXES`.
1137
1137
  const tailMessages = stripInjectionsForCompaction(
1138
1138
  args.messages.slice(tailIndex),
1139
1139
  );
@@ -312,6 +312,43 @@ function buildLocalCesEnv(): Record<string, string | undefined> {
312
312
  };
313
313
  }
314
314
 
315
+ // ---------------------------------------------------------------------------
316
+ // Close notification (shared by both transports)
317
+ // ---------------------------------------------------------------------------
318
+
319
+ /**
320
+ * Tracks the transport `alive` state and notifies registered handlers exactly
321
+ * once when the transport dies, so the RPC client can fail-fast any in-flight
322
+ * calls instead of waiting out their timeouts.
323
+ */
324
+ function createCloseNotifier(): {
325
+ isAlive: () => boolean;
326
+ markDead: () => void;
327
+ onClose: (handler: () => void) => void;
328
+ } {
329
+ let alive = true;
330
+ let notified = false;
331
+ const handlers: Array<() => void> = [];
332
+ return {
333
+ isAlive: () => alive,
334
+ markDead() {
335
+ alive = false;
336
+ if (notified) return;
337
+ notified = true;
338
+ for (const handler of handlers) {
339
+ try {
340
+ handler();
341
+ } catch {
342
+ // a close handler must never throw back into the transport
343
+ }
344
+ }
345
+ },
346
+ onClose(handler) {
347
+ handlers.push(handler);
348
+ },
349
+ };
350
+ }
351
+
315
352
  // ---------------------------------------------------------------------------
316
353
  // Stdio transport (local mode)
317
354
  // ---------------------------------------------------------------------------
@@ -319,7 +356,7 @@ function buildLocalCesEnv(): Record<string, string | undefined> {
319
356
  function createStdioTransport(proc: Subprocess): CesTransport {
320
357
  const messageHandlers: Array<(message: string) => void> = [];
321
358
  let buffer = "";
322
- let alive = true;
359
+ const death = createCloseNotifier();
323
360
 
324
361
  // Read stdout line by line — narrow past `number` union arm from Subprocess type
325
362
  if (proc.stdout && typeof proc.stdout !== "number") {
@@ -350,9 +387,9 @@ function createStdioTransport(proc: Subprocess): CesTransport {
350
387
  endReason = "error";
351
388
  readError = err;
352
389
  } finally {
353
- // Preserve existing semantics: the transport reports dead the moment
354
- // the stdout stream ends, regardless of why.
355
- alive = false;
390
+ // Report dead the moment the stdout stream ends (regardless of why),
391
+ // and notify the client so it can fail-fast any in-flight calls.
392
+ death.markDead();
356
393
 
357
394
  // DIAGNOSTIC (observation only). The reconnection path (secure-keys.ts)
358
395
  // bounces CES whenever isAlive() goes false. This classifies WHY the
@@ -384,7 +421,7 @@ function createStdioTransport(proc: Subprocess): CesTransport {
384
421
 
385
422
  // Track process exit
386
423
  void proc.exited.then((exitCode) => {
387
- alive = false;
424
+ death.markDead();
388
425
  log.info(
389
426
  { pid: proc.pid, exitCode },
390
427
  "CES stdio transport: process exited",
@@ -393,7 +430,7 @@ function createStdioTransport(proc: Subprocess): CesTransport {
393
430
 
394
431
  return {
395
432
  write(line: string): void {
396
- if (!alive || !proc.stdin || typeof proc.stdin === "number") {
433
+ if (!death.isAlive() || !proc.stdin || typeof proc.stdin === "number") {
397
434
  throw new Error("CES stdio transport is not alive");
398
435
  }
399
436
  proc.stdin.write(line + "\n");
@@ -404,11 +441,13 @@ function createStdioTransport(proc: Subprocess): CesTransport {
404
441
  },
405
442
 
406
443
  isAlive(): boolean {
407
- return alive;
444
+ return death.isAlive();
408
445
  },
409
446
 
447
+ onClose: death.onClose,
448
+
410
449
  close(): void {
411
- alive = false;
450
+ death.markDead();
412
451
  if (proc.stdin && typeof proc.stdin !== "number") {
413
452
  proc.stdin.end();
414
453
  }
@@ -423,7 +462,7 @@ function createStdioTransport(proc: Subprocess): CesTransport {
423
462
  function createSocketTransport(socket: Socket): CesTransport {
424
463
  const messageHandlers: Array<(message: string) => void> = [];
425
464
  let buffer = "";
426
- let alive = true;
465
+ const death = createCloseNotifier();
427
466
 
428
467
  const decoder = new StringDecoder("utf8");
429
468
 
@@ -442,17 +481,17 @@ function createSocketTransport(socket: Socket): CesTransport {
442
481
  });
443
482
 
444
483
  socket.on("close", () => {
445
- alive = false;
484
+ death.markDead();
446
485
  });
447
486
 
448
487
  socket.on("error", (err) => {
449
488
  log.warn({ err }, "CES socket transport error");
450
- alive = false;
489
+ death.markDead();
451
490
  });
452
491
 
453
492
  return {
454
493
  write(line: string): void {
455
- if (!alive || socket.destroyed) {
494
+ if (!death.isAlive() || socket.destroyed) {
456
495
  throw new Error("CES socket transport is not alive");
457
496
  }
458
497
  socket.write(line + "\n");
@@ -463,11 +502,13 @@ function createSocketTransport(socket: Socket): CesTransport {
463
502
  },
464
503
 
465
504
  isAlive(): boolean {
466
- return alive && !socket.destroyed;
505
+ return death.isAlive() && !socket.destroyed;
467
506
  },
468
507
 
508
+ onClose: death.onClose,
509
+
469
510
  close(): void {
470
- alive = false;
511
+ death.markDead();
471
512
  socket.destroy();
472
513
  },
473
514
  };
@@ -8,9 +8,8 @@
8
8
  * channel actually connects, not just stored;
9
9
  * - any other credential: written to secure storage.
10
10
  *
11
- * Both the `credential_store` tool's prompt action and the CLI
12
- * `credentials prompt` route share this logic so the two entry points behave
13
- * identically.
11
+ * The CLI `credentials prompt` route uses this logic to persist a credential
12
+ * collected through a secure prompt.
14
13
  */
15
14
 
16
15
  import { getConfig } from "../config/loader.js";
@@ -178,8 +178,6 @@ export class ConfigWatcher {
178
178
  * Start all file watchers. `onConversationEvict` is called when watched
179
179
  * files change and conversations need to be evicted for reload.
180
180
  * `onIdentityChanged` is called when IDENTITY.md changes on disk.
181
- * `onIdentityIntroChanged` is called when SOUL.md changes and identity
182
- * intro subscribers should refetch.
183
181
  * `onSkillsChanged` is called after skill directory changes evict
184
182
  * conversations.
185
183
  */
@@ -190,7 +188,6 @@ export class ConfigWatcher {
190
188
  onAvatarChanged?: () => void,
191
189
  onConfigChanged?: () => void,
192
190
  onSkillsChanged?: () => void,
193
- onIdentityIntroChanged?: () => void,
194
191
  ): void {
195
192
  // Reset the stopped flag so a stop()→start() cycle on the same
196
193
  // instance resumes hot-reload instead of silently bailing in every
@@ -226,7 +223,6 @@ export class ConfigWatcher {
226
223
  },
227
224
  "SOUL.md": () => {
228
225
  onConversationEvict();
229
- onIdentityIntroChanged?.();
230
226
  },
231
227
  "IDENTITY.md": () => {
232
228
  onConversationEvict();
@@ -215,6 +215,11 @@ export async function runAgentLoopImpl(
215
215
  * spawns).
216
216
  */
217
217
  overrideProfile?: string;
218
+ /**
219
+ * Float `overrideProfile` above call-site layers for non-main-agent call
220
+ * sites. Used when a caller explicitly pins a background run to a profile.
221
+ */
222
+ forceOverrideProfile?: boolean;
218
223
  },
219
224
  ): Promise<void> {
220
225
  if (!ctx.abortController) {
@@ -290,6 +295,7 @@ export async function runAgentLoopImpl(
290
295
  ctx.toolRoutedProfile = undefined;
291
296
 
292
297
  const turnOverrideProfile = userExplicitOverride;
298
+ const forceOverrideProfile = options?.forceOverrideProfile === true;
293
299
 
294
300
  const readCurrentOverrideProfile = (): string | undefined =>
295
301
  options?.overrideProfile ??
@@ -300,6 +306,7 @@ export async function runAgentLoopImpl(
300
306
  llm: config.llm,
301
307
  callSite: turnCallSite,
302
308
  overrideProfile: turnOverrideProfile ?? undefined,
309
+ forceOverrideProfile,
303
310
  selectionSeed: ctx.conversationId,
304
311
  });
305
312
  let currentEffectiveContextWindow: EffectiveContextWindow =
@@ -307,6 +314,7 @@ export async function runAgentLoopImpl(
307
314
  let currentContextWindowConfig = contextWindowConfigFromEffective(
308
315
  resolveCallSiteConfig(turnCallSite, config.llm, {
309
316
  overrideProfile: turnOverrideProfile ?? undefined,
317
+ forceOverrideProfile,
310
318
  selectionSeed: ctx.conversationId,
311
319
  }).contextWindow,
312
320
  currentEffectiveContextWindow,
@@ -322,11 +330,13 @@ export async function runAgentLoopImpl(
322
330
  llm: config.llm,
323
331
  callSite: turnCallSite,
324
332
  overrideProfile: currentOverrideProfile,
333
+ forceOverrideProfile,
325
334
  selectionSeed: ctx.conversationId,
326
335
  });
327
336
  currentContextWindowConfig = contextWindowConfigFromEffective(
328
337
  resolveCallSiteConfig(turnCallSite, config.llm, {
329
338
  overrideProfile: currentOverrideProfile,
339
+ forceOverrideProfile,
330
340
  selectionSeed: ctx.conversationId,
331
341
  }).contextWindow,
332
342
  currentEffectiveContextWindow,
@@ -901,6 +911,7 @@ export async function runAgentLoopImpl(
901
911
  callSite: turnCallSite,
902
912
  trust: loopTrust,
903
913
  overrideProfile: turnOverrideProfile,
914
+ ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
904
915
  resolveOverrideProfile: resolveCurrentOverrideProfile,
905
916
  resolveContextWindow,
906
917
  compactInPlace,
@@ -1657,10 +1668,10 @@ function collapseRawResponses(rawResponses?: unknown[]): unknown | undefined {
1657
1668
 
1658
1669
  /**
1659
1670
  * Matches any runtime-injection tag that should never appear inside a
1660
- * generated summary. If the regex hits, either the compaction strip logic
1661
- * failed to drop an injected block from the summarizer input, or the
1662
- * summarizer invented tag-like text on its own — both are quality bugs
1663
- * worth surfacing via telemetry.
1671
+ * generated summary. A hit means the summary echoed an injection tag —
1672
+ * either parroted from history the summarizer read or invented outright.
1673
+ * The durable summary should be clean prose, so the match is surfaced via
1674
+ * telemetry.
1664
1675
  */
1665
1676
  const SUMMARY_MEMORY_ECHO_PATTERN =
1666
1677
  /<(?:memory|memory_context|memory_image|turn_context|workspace|workspace_top_level|knowledge_base|pkb|system_reminder|now_scratchpad|NOW\.md|active_thread|active_subagents|active_workspace|active_dynamic_page|channel_capabilities|transport_hints|system_notice|non_interactive_context|temporal_context|guardian_context|inbound_actor_context|channel_turn_context|interface_turn_context|channel_command_context|voice_call_control)\b/i;
@@ -7,6 +7,7 @@ import {
7
7
  loadRawConfig,
8
8
  saveRawConfig,
9
9
  } from "../config/loader.js";
10
+ import { orderProfileKeys } from "../config/profile-order.js";
10
11
  import { getConversationOverrideProfile } from "../memory/conversation-crud.js";
11
12
  import { getConfiguredProviders } from "../providers/provider-availability.js";
12
13
  import { getVisibleProviderCatalog } from "../providers/provider-catalog-visibility.js";
@@ -130,28 +131,6 @@ function parseModelCommand(trimmed: string): ModelCommandParse | null {
130
131
  return { kind: "switch", profileName: rest };
131
132
  }
132
133
 
133
- function orderedProfileNames(
134
- profiles: Record<
135
- string,
136
- { label?: string; description?: string; status?: "active" | "disabled" }
137
- >,
138
- profileOrder: readonly string[] | undefined,
139
- ): string[] {
140
- const order = profileOrder ?? [];
141
- const seen = new Set<string>();
142
- const ordered: string[] = [];
143
- for (const name of order) {
144
- if (profiles[name] != null && !seen.has(name)) {
145
- ordered.push(name);
146
- seen.add(name);
147
- }
148
- }
149
- const tail = Object.keys(profiles)
150
- .filter((n) => !seen.has(n))
151
- .sort();
152
- return [...ordered, ...tail];
153
- }
154
-
155
134
  async function resolveModelCommand(
156
135
  parse: ModelCommandParse,
157
136
  ): Promise<SlashResolution> {
@@ -160,7 +139,7 @@ async function resolveModelCommand(
160
139
  string,
161
140
  { label?: string; description?: string; status?: "active" | "disabled" }
162
141
  >;
163
- const profileNames = orderedProfileNames(profiles, config.llm.profileOrder);
142
+ const profileNames = orderProfileKeys(profiles, config.llm.profileOrder);
164
143
  const activeProfile = config.llm.activeProfile;
165
144
 
166
145
  if (parse.kind === "list") {
@@ -26,7 +26,10 @@ import {
26
26
  ACTIVITY_SKIP_SET,
27
27
  injectActivityField,
28
28
  } from "../tools/schema-transforms.js";
29
- import { resolveSkillExecuteInput } from "../tools/skills/execute.js";
29
+ import {
30
+ augmentSkillExecuteError,
31
+ resolveSkillExecuteInput,
32
+ } from "../tools/skills/execute.js";
30
33
  import { resolveToolInvocationAlias } from "../tools/tool-name-aliases.js";
31
34
  import {
32
35
  isDiskPressureCleanupToolName,
@@ -320,7 +323,12 @@ export function createToolExecutor(
320
323
  const innerRejection = rejectNonAllowlistedTool(toolName);
321
324
  if (innerRejection) return innerRejection;
322
325
 
323
- const result = await executor.execute(toolName, toolInput, toolContext);
326
+ const rawResult = await executor.execute(
327
+ toolName,
328
+ toolInput,
329
+ toolContext,
330
+ );
331
+ const result = augmentSkillExecuteError(toolName, toolInput, rawResult);
324
332
  if (toolContext.approvedViaPrompt) {
325
333
  ctx.approvedViaPromptThisTurn = true;
326
334
  }
@@ -371,7 +379,7 @@ export function createProxyApprovalCallback(
371
379
  * history or explicit preactivation. Without this, their tools are
372
380
  * unavailable in fresh conversations until `skill_load` is called.
373
381
  */
374
- const DEFAULT_PREACTIVATED_SKILL_IDS = ["tasks", "notifications", "subagent"];
382
+ const DEFAULT_PREACTIVATED_SKILL_IDS = ["notifications", "subagent"];
375
383
 
376
384
  /**
377
385
  * Subset of Conversation state that the resolveTools callback reads at each
@@ -1847,6 +1847,8 @@ export class Conversation {
1847
1847
  * this value via {@link SubagentManager.spawn}.
1848
1848
  */
1849
1849
  overrideProfile?: string;
1850
+ /** Float `overrideProfile` above call-site layers for this run. */
1851
+ forceOverrideProfile?: boolean;
1850
1852
  },
1851
1853
  ): Promise<void> {
1852
1854
  const { onEvent, ...rest } = options ?? {};
@@ -204,7 +204,7 @@ export function revokeVerificationForChannel(
204
204
  // revocation becomes a no-op (wrong reason or skipped entirely).
205
205
  const contactResult = findContactChannel({
206
206
  channelType: resolvedChannel,
207
- externalUserId: bindingBeforeRevoke.guardianExternalUserId,
207
+ address: bindingBeforeRevoke.guardianExternalUserId,
208
208
  externalChatId: bindingBeforeRevoke.guardianDeliveryChatId,
209
209
  });
210
210
 
@@ -331,7 +331,12 @@ export async function verifyTrustedContact(
331
331
  const sessionResult = createOutboundSession({
332
332
  channel: verificationChannel,
333
333
  expectedChatId: channel.externalChatId,
334
- expectedExternalUserId: channel.externalUserId ?? undefined,
334
+ expectedExternalUserId:
335
+ channel.externalUserId ??
336
+ (channel.address !== channel.externalChatId
337
+ ? channel.address
338
+ : undefined) ??
339
+ undefined,
335
340
  identityBindingStatus: "bound",
336
341
  destinationAddress: effectiveDestination,
337
342
  verificationPurpose: "trusted_contact",
@@ -404,22 +409,17 @@ export async function verifyTrustedContact(
404
409
 
405
410
  // --- Slack verification ---
406
411
  if (verificationChannel === "slack") {
407
- const slackUserId = channel.externalUserId ?? destination;
408
-
409
- const hasIdentityBinding = Boolean(
410
- channel.externalUserId || channel.externalChatId,
411
- );
412
- if (!hasIdentityBinding) {
413
- return {
414
- success: false,
415
- error:
416
- "Slack verification requires an externalUserId or externalChatId for identity binding",
417
- };
418
- }
412
+ const slackUserId =
413
+ channel.externalUserId ?? channel.address ?? destination;
419
414
 
420
415
  const sessionResult = createOutboundSession({
421
416
  channel: verificationChannel,
422
- expectedExternalUserId: channel.externalUserId ?? undefined,
417
+ expectedExternalUserId:
418
+ channel.externalUserId ??
419
+ (channel.address !== channel.externalChatId
420
+ ? channel.address
421
+ : undefined) ??
422
+ undefined,
423
423
  expectedChatId: channel.externalChatId ?? undefined,
424
424
  identityBindingStatus: "bound",
425
425
  destinationAddress: slackUserId,
@@ -560,7 +560,11 @@ export async function handleChannelVerificationSession(
560
560
  const { to, text, subject, assistantId: aid } = result._pendingEmail;
561
561
  deliverVerificationEmail(to, text, subject, aid);
562
562
  }
563
- const { _pendingSlackDm: _, _pendingEmail: __, ...publicResult } = result;
563
+ const {
564
+ _pendingSlackDm: _,
565
+ _pendingEmail: __,
566
+ ...publicResult
567
+ } = result;
564
568
  broadcastMessage({
565
569
  type: "channel_verification_session_response",
566
570
  ...publicResult,
@@ -440,9 +440,8 @@ export async function setSlackChannelConfig(
440
440
  * the bot_token, app_token, oauth_connection row, and Slack config metadata
441
441
  * untouched so the Socket Mode connection stays up. Returns a
442
442
  * `SlackChannelConfigResult` reflecting the remaining state. A `not-found`
443
- * delete outcome is reported as a failure to match the credential_store
444
- * delete semantics (callers and automation rely on missing-credential
445
- * detection).
443
+ * delete outcome is reported as a failure to match the credential delete
444
+ * semantics (callers and automation rely on missing-credential detection).
446
445
  */
447
446
  export async function clearSlackUserToken(): Promise<SlackChannelConfigResult> {
448
447
  const result = await deleteSecureKeyAsync(
@@ -1281,13 +1281,6 @@ export async function runDaemon(): Promise<void> {
1281
1281
  log.warn({ err }, "Assistant symlink installation failed — continuing");
1282
1282
  }
1283
1283
 
1284
- // Seed preloaded apps (personal landing page) in background (non-blocking).
1285
- void import("../memory/preloaded-apps.js")
1286
- .then(({ seedPreloadedApps }) => seedPreloadedApps(config))
1287
- .catch((err) =>
1288
- log.warn({ err }, "Preloaded app seeding failed — continuing"),
1289
- );
1290
-
1291
1284
  // Download embedding runtime in background (non-blocking).
1292
1285
  // If download fails, local embeddings gracefully fall back to cloud backends.
1293
1286
  void (async () => {
@@ -462,9 +462,9 @@ export interface ContextCompacted {
462
462
  * - `summaryCharCount`: length of the produced summary text.
463
463
  * - `summaryHeaderCount`: number of `## ` section headers in the summary.
464
464
  * - `summaryHadMemoryEcho`: `true` if the summary contains any runtime
465
- * injection tag (e.g. `<memory`, `<turn_context>`, `<workspace>`).
466
- * Should always be `false` — `true` indicates the compaction strip
467
- * logic failed to remove an injected block from the summarizer input.
465
+ * injection tag (e.g. `<memory`, `<turn_context>`, `<workspace>`). The
466
+ * durable summary should be clean prose, so `true` flags an echoed or
467
+ * invented tag worth investigating.
468
468
  */
469
469
  summaryCharCount?: number;
470
470
  summaryHeaderCount?: number;
@@ -10,7 +10,6 @@ export { type SyncChangedEvent, SyncChangedEventSchema };
10
10
  export const SYNC_TAGS = {
11
11
  assistantAvatar: "assistant:self:avatar",
12
12
  assistantIdentity: "assistant:self:identity",
13
- assistantIdentityIntro: "assistant:self:identity-intro",
14
13
  assistantConfig: "assistant:self:config",
15
14
  assistantSounds: "assistant:self:sounds",
16
15
  assistantSchedules: "assistant:self:schedules",
@@ -21,7 +21,6 @@ mock.module("../util/logger.js", () => ({
21
21
  }));
22
22
 
23
23
  const { parseProcStat, selectReapable } = await import("./orphan-reaper.js");
24
- const { DaemonConfigSchema } = await import("../config/schemas/platform.js");
25
24
 
26
25
  describe("parseProcStat", () => {
27
26
  test("parses a normal stat line", () => {
@@ -89,24 +88,6 @@ describe("selectReapable", () => {
89
88
  });
90
89
  });
91
90
 
92
- describe("daemon.reapOrphanedSubprocesses gate", () => {
93
- test("defaults to off so the reaper is opt-in", () => {
94
- // GIVEN a daemon config with the reaper flag unspecified
95
- // WHEN it is parsed with schema defaults
96
- const parsed = DaemonConfigSchema.parse({});
97
- // THEN the reaper is disabled unless explicitly turned on
98
- expect(parsed.reapOrphanedSubprocesses).toBe(false);
99
- });
100
-
101
- test("honors an explicit opt-in", () => {
102
- // GIVEN a daemon config that explicitly enables the reaper
103
- // WHEN it is parsed
104
- const parsed = DaemonConfigSchema.parse({ reapOrphanedSubprocesses: true });
105
- // THEN the flag is respected
106
- expect(parsed.reapOrphanedSubprocesses).toBe(true);
107
- });
108
- });
109
-
110
91
  // ── Integration: real reparented orphan on Linux ────────────────────────────
111
92
  const itLinux = process.platform === "linux" ? test : test.skip;
112
93
 
@@ -26,9 +26,7 @@
26
26
  * macOS dev, or if an init such as tini is ever placed above the daemon),
27
27
  * orphans reparent to that init and are reaped there, so there is nothing for
28
28
  * this to do. Because the daemon is PID 1, orphans already reparent to it and
29
- * `PR_SET_CHILD_SUBREAPER` is unnecessary. It is additionally gated behind the
30
- * `daemon.reapOrphanedSubprocesses` config flag (default off) so the behavior
31
- * can be enabled per workspace for validation before becoming the default.
29
+ * `PR_SET_CHILD_SUBREAPER` is unnecessary.
32
30
  *
33
31
  * References:
34
32
  * - libuv reaps its own children by pid on SIGCHLD (`uv__wait_children`):
@@ -42,7 +40,6 @@
42
40
  import { readdirSync, readFileSync } from "node:fs";
43
41
  import { dlopen, FFIType, ptr } from "bun:ffi";
44
42
 
45
- import { getConfigReadOnly } from "../config/loader.js";
46
43
  import { getLogger } from "../util/logger.js";
47
44
 
48
45
  const log = getLogger("orphan-reaper");
@@ -192,22 +189,9 @@ function reapScan(): void {
192
189
  }
193
190
  }
194
191
 
195
- /**
196
- * Read the opt-in gate from workspace config (`daemon.reapOrphanedSubprocesses`),
197
- * tolerating a missing or malformed config so startup never fails on it.
198
- */
199
- function isReaperEnabled(): boolean {
200
- try {
201
- return getConfigReadOnly().daemon.reapOrphanedSubprocesses;
202
- } catch {
203
- return false;
204
- }
205
- }
206
-
207
192
  /**
208
193
  * Start the periodic orphan reaper. No-op unless the daemon is PID 1 on Linux
209
- * (otherwise reparented orphans are reaped by the real init) and the
210
- * `daemon.reapOrphanedSubprocesses` config gate is enabled.
194
+ * (otherwise reparented orphans are reaped by the real init).
211
195
  */
212
196
  export function startOrphanReaper(): void {
213
197
  if (scanTimer) return;
@@ -218,12 +202,6 @@ export function startOrphanReaper(): void {
218
202
  );
219
203
  return;
220
204
  }
221
- if (!isReaperEnabled()) {
222
- log.info(
223
- "Orphan reaper not started: daemon.reapOrphanedSubprocesses is disabled",
224
- );
225
- return;
226
- }
227
205
  if (!initWaitpid()) return;
228
206
  seenLastScan = new Set();
229
207
  scanTimer = setInterval(reapScan, SCAN_INTERVAL_MS);
@@ -19,7 +19,6 @@ import {
19
19
  publishAvatarChanged,
20
20
  publishConfigChanged,
21
21
  publishIdentityChanged,
22
- publishIdentityIntroChanged,
23
22
  publishSoundsConfigUpdated,
24
23
  } from "../runtime/sync/resource-sync-events.js";
25
24
  import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
@@ -193,14 +192,6 @@ export class DaemonServer {
193
192
  }
194
193
  }
195
194
 
196
- private broadcastIdentityIntroChanged(): void {
197
- try {
198
- publishIdentityIntroChanged();
199
- } catch (err) {
200
- log.error({ err }, "Failed to broadcast identity intro change");
201
- }
202
- }
203
-
204
195
  /** Best-effort sync of the IDENTITY.md name to the platform record. */
205
196
  private syncIdentityToPlatform(): void {
206
197
  try {
@@ -313,7 +304,6 @@ export class DaemonServer {
313
304
  () => this.broadcastAvatarUpdated(),
314
305
  () => this.broadcastConfigChanged(),
315
306
  () => refreshSkillCapabilityMemories(getConfig()),
316
- () => this.broadcastIdentityIntroChanged(),
317
307
  );
318
308
 
319
309
  this.syncIdentityToPlatform();