@vellumai/assistant 0.8.2 → 0.8.3

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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -1,4 +1,6 @@
1
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
1
2
  import { resolveCallSiteConfig } from "../config/llm-resolver.js";
3
+ import type { AssistantConfig } from "../config/schema.js";
2
4
  import { type LLMConfig } from "../config/schemas/llm.js";
3
5
  import { getProviderKeyAsync } from "../security/secure-keys.js";
4
6
  import { ProviderNotConfiguredError } from "../util/errors.js";
@@ -26,6 +28,7 @@ const log = getLogger("provider-registry");
26
28
 
27
29
  const providers = new Map<string, Provider>();
28
30
  const routingSources = new Map<string, "user-key" | "managed-proxy">();
31
+ const OPENAI_COMPATIBLE_ENDPOINTS_FLAG = "openai-compatible-endpoints";
29
32
 
30
33
  /** Per-connection provider cache, keyed by connection name. */
31
34
  const connectionProviders = new Map<string, Provider>();
@@ -69,6 +72,16 @@ export interface ProvidersConfig {
69
72
  timeouts?: { providerStreamTimeoutSec?: number };
70
73
  }
71
74
 
75
+ function isProviderFeatureFlagEnabled(
76
+ key: string,
77
+ config: ProvidersConfig,
78
+ ): boolean {
79
+ return isAssistantFeatureFlagEnabled(
80
+ key,
81
+ config as unknown as AssistantConfig,
82
+ );
83
+ }
84
+
72
85
  function resolveModel(config: ProvidersConfig, providerName: string): string {
73
86
  const resolved = resolveCallSiteConfig("mainAgent", config.llm);
74
87
  const inferenceProvider = resolved.provider;
@@ -130,6 +143,13 @@ export async function initializeProviders(
130
143
  ).provider;
131
144
 
132
145
  for (const entry of PROVIDER_CATALOG) {
146
+ if (
147
+ entry.featureFlag &&
148
+ !isProviderFeatureFlagEnabled(entry.featureFlag, config)
149
+ ) {
150
+ continue;
151
+ }
152
+
133
153
  const isKeyless = entry.setupMode === "keyless";
134
154
 
135
155
  // Credential resolution: user key first, managed proxy second. Keyless
@@ -202,10 +222,19 @@ export async function resolveProviderFromConnection(
202
222
  connection: ProviderConnection,
203
223
  config: ProvidersConfig,
204
224
  ): Promise<Provider | null> {
225
+ if (
226
+ connection.provider === "openai-compatible" &&
227
+ !isProviderFeatureFlagEnabled(OPENAI_COMPATIBLE_ENDPOINTS_FLAG, config)
228
+ ) {
229
+ return null;
230
+ }
231
+
205
232
  const cached = connectionProviders.get(connection.name);
206
233
  if (cached) return cached;
207
234
 
208
- const authResult = await resolveAuth(connection.auth, connection.provider);
235
+ const authResult = await resolveAuth(connection.auth, connection.provider, {
236
+ baseUrl: connection.baseUrl,
237
+ });
209
238
  if (!authResult.ok) {
210
239
  const err = authResult.error;
211
240
  if (err.code === "not_implemented") {
@@ -57,7 +57,11 @@ import {
57
57
  } from "../daemon/disk-pressure-policy.js";
58
58
  import type { TrustContext } from "../daemon/trust-context.js";
59
59
  import { getConversationOverrideProfile } from "../memory/conversation-crud.js";
60
- import { recordRequestLog } from "../memory/llm-request-log-store.js";
60
+ import {
61
+ buildProviderErrorResponsePayload,
62
+ recordRequestLog,
63
+ setAgentLoopExitReasonOnLatestLog,
64
+ } from "../memory/llm-request-log-store.js";
61
65
  import type { TurnContext } from "../plugins/types.js";
62
66
  import type { Message } from "../providers/types.js";
63
67
  import { getLogger } from "../util/logger.js";
@@ -548,6 +552,12 @@ export async function wakeAgentForOpportunity(
548
552
  provider?: string;
549
553
  };
550
554
  const pendingLogs: PendingLog[] = [];
555
+ // Exit reason deferred alongside pendingLogs. Same drop-on-silent-
556
+ // wake guarantee: if the wake never goes live, this stays null and
557
+ // no DB row is touched. Applied after pendingLogs flush in goLive
558
+ // so the latest-row lookup in `setAgentLoopExitReasonOnLatestLog`
559
+ // can see the freshly-persisted final usage row.
560
+ let pendingExitReason: string | null = null;
551
561
  const persistLog = (record: PendingLog): void => {
552
562
  try {
553
563
  recordRequestLog(
@@ -564,6 +574,16 @@ export async function wakeAgentForOpportunity(
564
574
  );
565
575
  }
566
576
  };
577
+ const persistExitReason = (reason: string): void => {
578
+ try {
579
+ setAgentLoopExitReasonOnLatestLog(conversationId, reason);
580
+ } catch (err) {
581
+ log.warn(
582
+ { err, conversationId, source, reason },
583
+ "agent-wake: failed to persist agent_loop_exit_reason (non-fatal)",
584
+ );
585
+ }
586
+ };
567
587
  const safeEmit = (event: AgentEvent): void => {
568
588
  try {
569
589
  target.emitAgentEvent(event);
@@ -590,6 +610,38 @@ export async function wakeAgentForOpportunity(
590
610
  persistLog(record);
591
611
  }
592
612
  }
613
+ // Mirror the same recording side-effect for provider-rejected calls.
614
+ // `handleProviderError` in the daemon dispatcher persists these on the
615
+ // normal turn path; the wake path owns its own onEvent and bypasses
616
+ // that dispatcher entirely, so we replicate here. Buffering rules
617
+ // match the success path: if the wake never goes live (silent no-op),
618
+ // the rows are dropped so a stale `messageId IS NULL` row doesn't get
619
+ // mis-backfilled onto an unrelated future assistant message.
620
+ if (event.type === "provider_error") {
621
+ const record: PendingLog = {
622
+ rawRequest: event.rawRequest,
623
+ rawResponse: buildProviderErrorResponsePayload(event.error),
624
+ provider: event.actualProvider,
625
+ };
626
+ if (mode === "buffering") {
627
+ pendingLogs.push(record);
628
+ } else {
629
+ persistLog(record);
630
+ }
631
+ }
632
+ // Replicates the setAgentLoopExitReasonOnLatestLog side-effect that
633
+ // `dispatchAgentEvent` does for the normal path. In live mode the
634
+ // final usage event of the run has already landed its row, so the
635
+ // latest-row lookup hits the right target. In buffering mode the
636
+ // reason is stashed and applied in `goLive` after pendingLogs are
637
+ // persisted, preserving the same ordering guarantee.
638
+ if (event.type === "agent_loop_exit") {
639
+ if (mode === "buffering") {
640
+ pendingExitReason = event.reason;
641
+ } else {
642
+ persistExitReason(event.reason);
643
+ }
644
+ }
593
645
  if (mode === "buffering") {
594
646
  buffered.push(event);
595
647
  return;
@@ -648,6 +700,14 @@ export async function wakeAgentForOpportunity(
648
700
  persistLog(record);
649
701
  }
650
702
  pendingLogs.length = 0;
703
+ // Apply the deferred exit reason after pendingLogs are persisted —
704
+ // the latest-row lookup in `setAgentLoopExitReasonOnLatestLog`
705
+ // needs the final usage row to already exist. Cleared after use so
706
+ // an extremely unlikely double-goLive can't double-stamp.
707
+ if (pendingExitReason !== null) {
708
+ persistExitReason(pendingExitReason);
709
+ pendingExitReason = null;
710
+ }
651
711
  mode = "live";
652
712
  };
653
713
 
@@ -243,6 +243,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
243
243
  endpoint: "integrations/slack/channel/config:DELETE",
244
244
  scopes: ["settings.write"],
245
245
  },
246
+ { endpoint: "integrations/a2a/invite", scopes: ["settings.write"] },
246
247
  { endpoint: "channel-verification-sessions", scopes: ["settings.write"] },
247
248
  {
248
249
  endpoint: "channel-verification-sessions:DELETE",
@@ -686,6 +687,18 @@ for (const endpoint of INTERNAL_ENDPOINTS) {
686
687
  });
687
688
  }
688
689
 
690
+ // A2A invite completion: gateway-only (platform-orchestrated)
691
+ registerPolicy("integrations/a2a/invite/complete", {
692
+ requiredScopes: ["internal.write"],
693
+ allowedPrincipalTypes: ["svc_gateway"],
694
+ });
695
+
696
+ // A2A invite redemption: gateway-only (platform-orchestrated)
697
+ registerPolicy("integrations/a2a/invite/redeem", {
698
+ requiredScopes: ["internal.write"],
699
+ allowedPrincipalTypes: ["svc_gateway"],
700
+ });
701
+
689
702
  // Admin control-plane endpoints: gateway-only
690
703
  registerPolicy("admin/upgrade-broadcast", {
691
704
  requiredScopes: ["internal.write"],
@@ -28,6 +28,8 @@ import {
28
28
  import { isHttpAuthDisabled } from "../config/env.js";
29
29
  import { getIsPlatform } from "../config/env-registry.js";
30
30
  import { getConfig } from "../config/loader.js";
31
+ import { createApprovalCopyGenerator } from "../daemon/approval-generators.js";
32
+ import { createGuardianActionCopyGenerator } from "../daemon/guardian-action-generators.js";
31
33
  import { processMessage } from "../daemon/process-message.js";
32
34
  import { createLiveVoiceSession } from "../live-voice/live-voice-session.js";
33
35
  import { LiveVoiceSessionManager } from "../live-voice/live-voice-session-manager.js";
@@ -96,10 +98,8 @@ import { matchSkillRoute } from "./skill-route-registry.js";
96
98
  export { isPrivateAddress } from "./middleware/auth.js";
97
99
 
98
100
  import type {
99
- ApprovalConversationGenerator,
100
101
  ApprovalCopyGenerator,
101
102
  GuardianActionCopyGenerator,
102
- GuardianFollowUpConversationGenerator,
103
103
  RuntimeHttpServerOptions,
104
104
  } from "./http-types.js";
105
105
 
@@ -161,10 +161,8 @@ export class RuntimeHttpServer {
161
161
  private port: number;
162
162
  private hostname: string;
163
163
 
164
- private approvalCopyGenerator?: ApprovalCopyGenerator;
165
- private approvalConversationGenerator?: ApprovalConversationGenerator;
166
- private guardianActionCopyGenerator?: GuardianActionCopyGenerator;
167
- private guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator;
164
+ private readonly approvalCopyGenerator: ApprovalCopyGenerator;
165
+ private readonly guardianActionCopyGenerator: GuardianActionCopyGenerator;
168
166
  private retrySweepTimer: ReturnType<typeof setInterval> | null = null;
169
167
  private sweepInProgress = false;
170
168
 
@@ -175,11 +173,8 @@ export class RuntimeHttpServer {
175
173
  this.port = options.port ?? DEFAULT_PORT;
176
174
  this.hostname = options.hostname ?? DEFAULT_HOSTNAME;
177
175
 
178
- this.approvalCopyGenerator = options.approvalCopyGenerator;
179
- this.approvalConversationGenerator = options.approvalConversationGenerator;
180
- this.guardianActionCopyGenerator = options.guardianActionCopyGenerator;
181
- this.guardianFollowUpConversationGenerator =
182
- options.guardianFollowUpConversationGenerator;
176
+ this.approvalCopyGenerator = createApprovalCopyGenerator();
177
+ this.guardianActionCopyGenerator = createGuardianActionCopyGenerator();
183
178
  this.liveVoiceSessionManager = new LiveVoiceSessionManager({
184
179
  createSession: (context) => createLiveVoiceSession(context),
185
180
  });
@@ -560,11 +555,7 @@ export class RuntimeHttpServer {
560
555
  const endpoint = url.pathname.slice("/v1/".length).replace(/\/$/, "");
561
556
  meta = this.router.findLoggingMetadata(req.method, endpoint) ?? undefined;
562
557
  }
563
- return withRequestLogging(
564
- req,
565
- () => this.routeRequest(req, server),
566
- meta,
567
- );
558
+ return withRequestLogging(req, () => this.routeRequest(req, server), meta);
568
559
  }
569
560
 
570
561
  private async routeRequest(
@@ -14,10 +14,6 @@ import type {
14
14
  export type { SlackInboundMessageMetadata };
15
15
  import type { ServerMessage } from "../daemon/message-protocol.js";
16
16
  import type { AssistantEventHub } from "./assistant-event-hub.js";
17
- import type {
18
- ApprovalCopyGenerator,
19
- GuardianActionCopyGenerator,
20
- } from "./message-composer-types.js";
21
17
 
22
18
  export type {
23
19
  ApprovalCopyGenerator,
@@ -61,41 +57,6 @@ export type ApprovalConversationGenerator = (
61
57
  context: ApprovalConversationContext,
62
58
  ) => Promise<ApprovalConversationResult>;
63
59
 
64
- // ---------------------------------------------------------------------------
65
- // Guardian follow-up conversation flow types
66
- // ---------------------------------------------------------------------------
67
-
68
- /** The disposition returned by the guardian follow-up conversation engine. */
69
- export type GuardianFollowUpDisposition =
70
- | "call_back"
71
- | "decline"
72
- | "keep_pending";
73
-
74
- /** Structured result from a single turn of the guardian follow-up conversation. */
75
- export interface GuardianFollowUpTurnResult {
76
- disposition: GuardianFollowUpDisposition;
77
- replyText: string;
78
- }
79
-
80
- /** Input context for the guardian follow-up conversation engine. */
81
- export interface GuardianFollowUpConversationContext {
82
- /** The original question that was asked during the voice call. */
83
- questionText: string;
84
- /** The guardian's late answer text that initiated the follow-up. */
85
- lateAnswerText: string;
86
- /** The guardian's latest reply in the follow-up conversation. */
87
- guardianReply: string;
88
- }
89
-
90
- /**
91
- * Daemon-injected function that processes one turn of a guardian follow-up
92
- * conversation. Classifies the guardian's intent into a structured disposition
93
- * and produces a natural reply.
94
- */
95
- export type GuardianFollowUpConversationGenerator = (
96
- context: GuardianFollowUpConversationContext,
97
- ) => Promise<GuardianFollowUpTurnResult>;
98
-
99
60
  export interface RuntimeMessageConversationOptions {
100
61
  transport?: {
101
62
  channelId: ChannelId;
@@ -173,14 +134,6 @@ export interface RuntimeHttpServerOptions {
173
134
  port?: number;
174
135
  /** Hostname / IP to bind to. Defaults to '127.0.0.1' (loopback-only). */
175
136
  hostname?: string;
176
- /** Daemon-injected generator for approval copy (provider-backed). */
177
- approvalCopyGenerator?: ApprovalCopyGenerator;
178
- /** Daemon-injected generator for conversational approval flow (provider-backed). */
179
- approvalConversationGenerator?: ApprovalConversationGenerator;
180
- /** Daemon-injected generator for guardian action copy (provider-backed). */
181
- guardianActionCopyGenerator?: GuardianActionCopyGenerator;
182
- /** Daemon-injected generator for guardian follow-up conversation (provider-backed). */
183
- guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator;
184
137
  }
185
138
 
186
139
  export interface RuntimeAttachmentMetadata {
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Asserts `listConsolidationRuns` maps background-conversation rows tagged
3
+ * with `source = MEMORY_V2_CONSOLIDATION_SOURCE` into the heartbeat-runs
4
+ * response shape, derives `status` / `finishedAt` / `durationMs` from
5
+ * **assistant-message presence** (not `lastMessageAt`), and clamps the
6
+ * `limit` query param.
7
+ *
8
+ * Synthetic-field semantics covered here:
9
+ * - `id` and `conversationId` both equal the conversation row's id.
10
+ * - `scheduledFor` and `startedAt` both equal `conversation.createdAt`
11
+ * (no separate schedule timestamp on the row).
12
+ * - `finishedAt` is the `createdAt` of the LATEST assistant message,
13
+ * NOT `conversation.lastMessageAt` — the kickoff user prompt bumps
14
+ * `lastMessageAt` before the agent runs, so it cannot be used as a
15
+ * completion signal.
16
+ * - `durationMs` is `finishedAt − startedAt` when both are present, else
17
+ * null.
18
+ * - `status` is `"ok"` when the conversation has at least one assistant
19
+ * message (positive evidence the agent emitted output) and `"running"`
20
+ * otherwise — including the case where only the kickoff user prompt
21
+ * has been persisted.
22
+ * - `skipReason` and `error` are always null — the conversation row
23
+ * alone cannot distinguish a clean run from a mid-flight crash even
24
+ * once assistant output exists.
25
+ */
26
+
27
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
28
+
29
+ mock.module("../../../util/logger.js", () => ({
30
+ getLogger: () =>
31
+ new Proxy({} as Record<string, unknown>, {
32
+ get: () => () => {},
33
+ }),
34
+ }));
35
+
36
+ import { createConversation } from "../../../memory/conversation-crud.js";
37
+ import { getDb } from "../../../memory/db-connection.js";
38
+ import { initializeDb } from "../../../memory/db-init.js";
39
+ import { rawRun } from "../../../memory/raw-query.js";
40
+ import { ROUTES } from "../consolidation-routes.js";
41
+ import type { RouteDefinition } from "../types.js";
42
+
43
+ initializeDb();
44
+
45
+ function resetTables(): void {
46
+ const db = getDb();
47
+ db.run(`DELETE FROM messages`);
48
+ db.run(`DELETE FROM conversations`);
49
+ }
50
+
51
+ function findHandler(operationId: string): RouteDefinition["handler"] {
52
+ const route = ROUTES.find((r) => r.operationId === operationId);
53
+ if (!route) throw new Error(`Route ${operationId} not found`);
54
+ return route.handler;
55
+ }
56
+
57
+ function insertMessage(
58
+ conversationId: string,
59
+ role: string,
60
+ createdAt: number,
61
+ ): void {
62
+ rawRun(
63
+ "INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)",
64
+ `msg-${conversationId}-${role}-${createdAt}`,
65
+ conversationId,
66
+ role,
67
+ "x",
68
+ createdAt,
69
+ );
70
+ }
71
+
72
+ interface RunRecord {
73
+ id: string;
74
+ scheduledFor: number;
75
+ startedAt: number | null;
76
+ finishedAt: number | null;
77
+ durationMs: number | null;
78
+ status: "ok" | "running";
79
+ skipReason: string | null;
80
+ error: string | null;
81
+ conversationId: string | null;
82
+ createdAt: number;
83
+ }
84
+
85
+ interface ListRunsResponse {
86
+ runs: RunRecord[];
87
+ }
88
+
89
+ describe("listConsolidationRuns handler", () => {
90
+ beforeEach(() => {
91
+ resetTables();
92
+ });
93
+
94
+ test("returns only conversations sourced from memory_v2_consolidation", async () => {
95
+ createConversation({ title: "c1", source: "memory_v2_consolidation" });
96
+ createConversation({ title: "h1", source: "heartbeat" });
97
+ createConversation({ title: "u1", source: "user" });
98
+
99
+ const handler = findHandler("listConsolidationRuns");
100
+ const result = (await handler({})) as ListRunsResponse;
101
+
102
+ expect(result.runs).toHaveLength(1);
103
+ });
104
+
105
+ test("synthesizes status='ok' with finishedAt from latest assistant message", async () => {
106
+ const conv = createConversation({
107
+ title: "c1",
108
+ source: "memory_v2_consolidation",
109
+ });
110
+ rawRun(
111
+ "UPDATE conversations SET created_at = ? WHERE id = ?",
112
+ 1000,
113
+ conv.id,
114
+ );
115
+ // Kickoff user prompt at t=1100 (bumps lastMessageAt — must NOT be
116
+ // mistaken for completion).
117
+ insertMessage(conv.id, "user", 1100);
118
+ // Agent's first assistant turn at t=2000.
119
+ insertMessage(conv.id, "assistant", 2000);
120
+ // Agent's final assistant turn at t=2500.
121
+ insertMessage(conv.id, "assistant", 2500);
122
+
123
+ const handler = findHandler("listConsolidationRuns");
124
+ const result = (await handler({})) as ListRunsResponse;
125
+
126
+ expect(result.runs).toHaveLength(1);
127
+ const run = result.runs[0]!;
128
+ expect(run.id).toBe(conv.id);
129
+ expect(run.conversationId).toBe(conv.id);
130
+ expect(run.status).toBe("ok");
131
+ expect(run.scheduledFor).toBe(1000);
132
+ expect(run.startedAt).toBe(1000);
133
+ // finishedAt = createdAt of LATEST assistant message (2500), NOT
134
+ // the conversation's lastMessageAt (which sqlite triggers may or may
135
+ // not have updated here — irrelevant to this endpoint).
136
+ expect(run.finishedAt).toBe(2500);
137
+ expect(run.durationMs).toBe(1500);
138
+ expect(run.createdAt).toBe(1000);
139
+ });
140
+
141
+ test("synthesizes status='running' when conversation has no assistant message", async () => {
142
+ createConversation({ title: "c1", source: "memory_v2_consolidation" });
143
+
144
+ const handler = findHandler("listConsolidationRuns");
145
+ const result = (await handler({})) as ListRunsResponse;
146
+
147
+ expect(result.runs).toHaveLength(1);
148
+ const run = result.runs[0]!;
149
+ expect(run.status).toBe("running");
150
+ expect(run.finishedAt).toBeNull();
151
+ expect(run.durationMs).toBeNull();
152
+ });
153
+
154
+ test("status stays 'running' when only the kickoff user prompt exists (Codex bug regression guard)", async () => {
155
+ // Regression guard for the original `status from lastMessageAt`
156
+ // heuristic. `processMessage` persists the background kickoff prompt as
157
+ // a user message BEFORE the agent runs, which bumps
158
+ // `conversation.lastMessageAt`. A run that timed out / threw before
159
+ // emitting any assistant turn must still report status='running' (or
160
+ // an explicit failure status once one exists) — never 'ok'.
161
+ const conv = createConversation({
162
+ title: "c1",
163
+ source: "memory_v2_consolidation",
164
+ });
165
+ rawRun(
166
+ "UPDATE conversations SET created_at = ?, last_message_at = ? WHERE id = ?",
167
+ 1000,
168
+ 1100,
169
+ conv.id,
170
+ );
171
+ insertMessage(conv.id, "user", 1100);
172
+
173
+ const handler = findHandler("listConsolidationRuns");
174
+ const result = (await handler({})) as ListRunsResponse;
175
+
176
+ expect(result.runs).toHaveLength(1);
177
+ const run = result.runs[0]!;
178
+ expect(run.status).toBe("running");
179
+ expect(run.finishedAt).toBeNull();
180
+ expect(run.durationMs).toBeNull();
181
+ });
182
+
183
+ test("skipReason and error are always null (not derivable from conversation row)", async () => {
184
+ const conv = createConversation({
185
+ title: "c1",
186
+ source: "memory_v2_consolidation",
187
+ });
188
+ insertMessage(conv.id, "assistant", 2000);
189
+
190
+ const handler = findHandler("listConsolidationRuns");
191
+ const result = (await handler({})) as ListRunsResponse;
192
+
193
+ expect(result.runs[0]!.skipReason).toBeNull();
194
+ expect(result.runs[0]!.error).toBeNull();
195
+ });
196
+
197
+ test("orders runs by createdAt descending", async () => {
198
+ const a = createConversation({
199
+ title: "a",
200
+ source: "memory_v2_consolidation",
201
+ });
202
+ const b = createConversation({
203
+ title: "b",
204
+ source: "memory_v2_consolidation",
205
+ });
206
+ const c = createConversation({
207
+ title: "c",
208
+ source: "memory_v2_consolidation",
209
+ });
210
+ rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 1000, a.id);
211
+ rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 3000, b.id);
212
+ rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 2000, c.id);
213
+
214
+ const handler = findHandler("listConsolidationRuns");
215
+ const result = (await handler({})) as ListRunsResponse;
216
+
217
+ expect(result.runs.map((r) => r.id)).toEqual([b.id, c.id, a.id]);
218
+ });
219
+
220
+ test("limit defaults to 20, clamps to [1, 100], and falls back on non-numeric input", async () => {
221
+ for (let i = 0; i < 5; i++) {
222
+ createConversation({
223
+ title: `c${i}`,
224
+ source: "memory_v2_consolidation",
225
+ });
226
+ }
227
+
228
+ const handler = findHandler("listConsolidationRuns");
229
+
230
+ // Default — all 5 returned (under the 20 default).
231
+ const def = (await handler({})) as ListRunsResponse;
232
+ expect(def.runs).toHaveLength(5);
233
+
234
+ // Explicit limit honored.
235
+ const lim2 = (await handler({
236
+ queryParams: { limit: "2" },
237
+ })) as ListRunsResponse;
238
+ expect(lim2.runs).toHaveLength(2);
239
+
240
+ // Negative clamps to 1.
241
+ const neg = (await handler({
242
+ queryParams: { limit: "-5" },
243
+ })) as ListRunsResponse;
244
+ expect(neg.runs).toHaveLength(1);
245
+
246
+ // Zero clamps to 1.
247
+ const zero = (await handler({
248
+ queryParams: { limit: "0" },
249
+ })) as ListRunsResponse;
250
+ expect(zero.runs).toHaveLength(1);
251
+
252
+ // Non-numeric falls back to the default (20 → all 5 here).
253
+ const bad = (await handler({
254
+ queryParams: { limit: "garbage" },
255
+ })) as ListRunsResponse;
256
+ expect(bad.runs).toHaveLength(5);
257
+ });
258
+ });
@@ -71,6 +71,10 @@ import {
71
71
  memoryV2ActivationLogs,
72
72
  messages,
73
73
  } from "../../../memory/schema.js";
74
+ import {
75
+ createConnection,
76
+ getConnection,
77
+ } from "../../../providers/inference/connections.js";
74
78
  import { ROUTES } from "../conversation-query-routes.js";
75
79
 
76
80
  // Local subset: this test only exercises a single concept row.
@@ -427,7 +431,7 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
427
431
  expect(savedProfile.provider_connection).toBe("personal-openai");
428
432
  });
429
433
 
430
- test("clears provider_connection when omitted from body (UI-owned key)", async () => {
434
+ test("auto-derives provider_connection when omitted from body (Any active)", async () => {
431
435
  // Seed an existing binding so the test starts from a non-empty state.
432
436
  (
433
437
  rawConfigFixture.llm as {
@@ -441,8 +445,37 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
441
445
  provider: "openai",
442
446
  model: "gpt-5.5",
443
447
  // provider_connection deliberately omitted — the UI cleared the
444
- // picker back to "Any active" and the route must wipe the saved
445
- // binding, not silently round-trip it.
448
+ // picker back to "Any active". The route auto-derives an active
449
+ // connection for the provider to prevent stale inheritance during
450
+ // config deep-merge.
451
+ },
452
+ });
453
+
454
+ expect(result).toEqual({ ok: true });
455
+ const savedProfile = (
456
+ savedRawConfig?.llm as {
457
+ profiles: Record<string, Record<string, unknown>>;
458
+ }
459
+ ).profiles.custom;
460
+
461
+ // The canonical "openai-managed" connection exists in the test DB;
462
+ // the route auto-derives it when the UI omits provider_connection.
463
+ expect(savedProfile.provider_connection).toBe("openai-managed");
464
+ });
465
+
466
+ test("auto-derives provider_connection for BYOK provider (Any active)", async () => {
467
+ // Seed a fireworks connection in the DB.
468
+ createConnection(getDb(), {
469
+ name: "fireworks",
470
+ provider: "fireworks",
471
+ auth: { type: "api_key", credential: "fireworks:api_key" },
472
+ });
473
+
474
+ const result = await replaceProfileRoute.handler({
475
+ pathParams: { name: "custom" },
476
+ body: {
477
+ provider: "fireworks",
478
+ model: "accounts/fireworks/models/llama-v3p1-8b-instruct",
446
479
  },
447
480
  });
448
481
 
@@ -453,7 +486,36 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
453
486
  }
454
487
  ).profiles.custom;
455
488
 
456
- expect(savedProfile.provider_connection).toBeUndefined();
489
+ expect(savedProfile.provider).toBe("fireworks");
490
+ expect(savedProfile.provider_connection).toBe("fireworks");
491
+ });
492
+
493
+ test("auto-creates provider_connection when no connection exists for provider", async () => {
494
+ const result = await replaceProfileRoute.handler({
495
+ pathParams: { name: "custom" },
496
+ body: {
497
+ provider: "openrouter",
498
+ model: "anthropic/claude-sonnet-4-6",
499
+ },
500
+ });
501
+
502
+ expect(result).toEqual({ ok: true });
503
+ const savedProfile = (
504
+ savedRawConfig?.llm as {
505
+ profiles: Record<string, Record<string, unknown>>;
506
+ }
507
+ ).profiles.custom;
508
+
509
+ expect(savedProfile.provider).toBe("openrouter");
510
+ expect(savedProfile.provider_connection).toBe("openrouter-personal");
511
+
512
+ const conn = getConnection(getDb(), "openrouter-personal");
513
+ expect(conn).not.toBeNull();
514
+ expect(conn!.provider).toBe("openrouter");
515
+ expect(conn!.auth).toEqual({
516
+ type: "api_key",
517
+ credential: "credential/openrouter/api_key",
518
+ });
457
519
  });
458
520
 
459
521
  describe("managed profile guard", () => {