@vellumai/assistant 0.5.7 → 0.5.9

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 (205) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/eslint.config.mjs +0 -31
  5. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  9. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  10. package/package.json +1 -1
  11. package/src/__tests__/approval-cascade.test.ts +0 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  13. package/src/__tests__/call-controller.test.ts +0 -1
  14. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  15. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  16. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  17. package/src/__tests__/config-schema.test.ts +2 -0
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  19. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  20. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  21. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  22. package/src/__tests__/conversation-error.test.ts +15 -1
  23. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  24. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  26. package/src/__tests__/conversation-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
  28. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  29. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  30. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  32. package/src/__tests__/credential-execution-client.test.ts +5 -2
  33. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  34. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  35. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  37. package/src/__tests__/credentials-cli.test.ts +4 -3
  38. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  39. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  40. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  41. package/src/__tests__/journal-context.test.ts +335 -0
  42. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  43. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  44. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  45. package/src/__tests__/memory-regressions.test.ts +408 -363
  46. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  47. package/src/__tests__/non-member-access-request.test.ts +2 -2
  48. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  49. package/src/__tests__/oauth-cli.test.ts +5 -1
  50. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  51. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  52. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  53. package/src/__tests__/relay-server.test.ts +1 -2
  54. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  55. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  56. package/src/__tests__/secure-keys.test.ts +18 -15
  57. package/src/__tests__/skill-memory.test.ts +17 -3
  58. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  59. package/src/__tests__/stt-hints.test.ts +437 -0
  60. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  61. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  62. package/src/__tests__/voice-quality.test.ts +58 -0
  63. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  64. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  65. package/src/acp/agent-process.ts +9 -1
  66. package/src/agent/loop.ts +1 -1
  67. package/src/approvals/guardian-request-resolvers.ts +164 -38
  68. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  69. package/src/calls/call-controller.ts +9 -5
  70. package/src/calls/fish-audio-client.ts +26 -14
  71. package/src/calls/stt-hints.ts +189 -0
  72. package/src/calls/tts-text-sanitizer.ts +61 -0
  73. package/src/calls/twilio-routes.ts +32 -4
  74. package/src/calls/voice-quality.ts +15 -3
  75. package/src/calls/voice-session-bridge.ts +1 -0
  76. package/src/cli/commands/avatar.ts +2 -2
  77. package/src/cli/commands/credentials.ts +110 -94
  78. package/src/cli/commands/doctor.ts +2 -2
  79. package/src/cli/commands/keys.ts +7 -7
  80. package/src/cli/commands/memory.ts +1 -1
  81. package/src/cli/commands/oauth/connections.ts +11 -29
  82. package/src/cli/commands/oauth/platform.ts +389 -43
  83. package/src/cli/lib/daemon-credential-client.ts +284 -0
  84. package/src/cli.ts +1 -1
  85. package/src/config/bundled-skills/AGENTS.md +34 -0
  86. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  87. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  88. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  89. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  90. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  91. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  92. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  93. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  94. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  95. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  96. package/src/config/bundled-tool-registry.ts +4 -0
  97. package/src/config/defaults.ts +0 -2
  98. package/src/config/env-registry.ts +4 -4
  99. package/src/config/env.ts +14 -1
  100. package/src/config/feature-flag-registry.json +1 -1
  101. package/src/config/loader.ts +8 -11
  102. package/src/config/schema.ts +5 -16
  103. package/src/config/schemas/calls.ts +17 -0
  104. package/src/config/schemas/inference.ts +2 -2
  105. package/src/config/schemas/journal.ts +16 -0
  106. package/src/config/schemas/memory-processing.ts +2 -2
  107. package/src/config/types.ts +1 -0
  108. package/src/contacts/contact-store.ts +2 -2
  109. package/src/credential-execution/executable-discovery.ts +1 -1
  110. package/src/credential-execution/startup-timeout.ts +36 -0
  111. package/src/daemon/approval-generators.ts +3 -9
  112. package/src/daemon/conversation-agent-loop.ts +6 -0
  113. package/src/daemon/conversation-error.ts +13 -1
  114. package/src/daemon/conversation-memory.ts +1 -2
  115. package/src/daemon/conversation-process.ts +18 -1
  116. package/src/daemon/conversation-runtime-assembly.ts +61 -1
  117. package/src/daemon/conversation-surfaces.ts +30 -1
  118. package/src/daemon/conversation.ts +20 -9
  119. package/src/daemon/guardian-action-generators.ts +3 -9
  120. package/src/daemon/lifecycle.ts +18 -11
  121. package/src/daemon/message-types/conversations.ts +1 -0
  122. package/src/daemon/server.ts +2 -3
  123. package/src/memory/app-store.ts +31 -0
  124. package/src/memory/db-init.ts +4 -0
  125. package/src/memory/indexer.ts +19 -10
  126. package/src/memory/items-extractor.ts +315 -322
  127. package/src/memory/job-handlers/summarization.ts +26 -16
  128. package/src/memory/jobs-store.ts +33 -1
  129. package/src/memory/journal-memory.ts +214 -0
  130. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  131. package/src/memory/migrations/index.ts +1 -0
  132. package/src/memory/migrations/registry.ts +8 -0
  133. package/src/memory/retriever.test.ts +37 -25
  134. package/src/memory/retriever.ts +24 -49
  135. package/src/memory/schema/memory-core.ts +2 -0
  136. package/src/memory/search/formatting.ts +7 -44
  137. package/src/memory/search/staleness.ts +4 -0
  138. package/src/memory/search/tier-classifier.ts +10 -2
  139. package/src/memory/search/types.ts +2 -5
  140. package/src/memory/task-memory-cleanup.ts +4 -3
  141. package/src/notifications/adapters/slack.ts +168 -6
  142. package/src/notifications/broadcaster.ts +1 -0
  143. package/src/notifications/copy-composer.ts +59 -2
  144. package/src/notifications/signal.ts +2 -0
  145. package/src/notifications/types.ts +2 -0
  146. package/src/prompts/journal-context.ts +133 -0
  147. package/src/prompts/persona-resolver.ts +80 -24
  148. package/src/prompts/system-prompt.ts +30 -0
  149. package/src/prompts/templates/NOW.md +26 -0
  150. package/src/prompts/templates/SOUL.md +20 -0
  151. package/src/prompts/update-bulletin-format.ts +0 -2
  152. package/src/providers/provider-send-message.ts +3 -32
  153. package/src/providers/registry.ts +2 -139
  154. package/src/providers/types.ts +1 -1
  155. package/src/runtime/access-request-helper.ts +4 -0
  156. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  157. package/src/runtime/auth/route-policy.ts +2 -0
  158. package/src/runtime/gateway-client.ts +47 -4
  159. package/src/runtime/guardian-decision-types.ts +45 -4
  160. package/src/runtime/http-server.ts +5 -2
  161. package/src/runtime/routes/access-request-decision.ts +2 -2
  162. package/src/runtime/routes/app-management-routes.ts +2 -1
  163. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  164. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  165. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  166. package/src/runtime/routes/debug-routes.ts +12 -9
  167. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  168. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  169. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  170. package/src/runtime/routes/identity-routes.ts +1 -1
  171. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  172. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  173. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  174. package/src/runtime/routes/integrations/twilio.ts +52 -10
  175. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  176. package/src/runtime/routes/memory-item-routes.ts +25 -11
  177. package/src/runtime/routes/secret-routes.ts +141 -10
  178. package/src/runtime/routes/tts-routes.ts +11 -1
  179. package/src/security/ces-credential-client.ts +18 -9
  180. package/src/security/ces-rpc-credential-backend.ts +4 -3
  181. package/src/security/credential-backend.ts +10 -4
  182. package/src/security/secure-keys.ts +21 -4
  183. package/src/skills/catalog-install.ts +4 -36
  184. package/src/skills/inline-command-expansions.ts +7 -7
  185. package/src/skills/skill-memory.ts +1 -0
  186. package/src/subagent/manager.ts +2 -5
  187. package/src/tools/acp/spawn.ts +78 -1
  188. package/src/tools/credentials/vault.ts +5 -3
  189. package/src/tools/memory/definitions.ts +3 -2
  190. package/src/tools/memory/handlers.ts +10 -7
  191. package/src/tools/sensitive-output-placeholders.ts +2 -2
  192. package/src/tools/terminal/safe-env.ts +1 -0
  193. package/src/util/browser.ts +15 -0
  194. package/src/util/platform.ts +1 -1
  195. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  196. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  197. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  198. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  199. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  200. package/src/workspace/migrations/registry.ts +4 -0
  201. package/src/workspace/provider-commit-message-generator.ts +12 -21
  202. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  203. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  204. package/src/memory/search/lexical.ts +0 -48
  205. package/src/providers/failover.ts +0 -186
@@ -7,6 +7,7 @@ import { applyGuardianDecision } from "../../../approvals/guardian-decision-prim
7
7
  import type { ChannelId } from "../../../channels/types.js";
8
8
  import {
9
9
  getAllPendingApprovalsByGuardianChat,
10
+ getApprovalRequestById,
10
11
  getPendingApprovalByRequestAndGuardianChat,
11
12
  type GuardianApprovalRequest,
12
13
  } from "../../../memory/guardian-approvals.js";
@@ -42,6 +43,17 @@ import {
42
43
 
43
44
  const log = getLogger("runtime-http");
44
45
 
46
+ /**
47
+ * Resolve the Slack ephemeral user ID when the source channel is Slack.
48
+ * Returns `undefined` for non-Slack channels.
49
+ */
50
+ function slackEphemeralUserId(
51
+ sourceChannel: ChannelId,
52
+ userId: string | undefined,
53
+ ): string | undefined {
54
+ return sourceChannel === "slack" && userId ? userId : undefined;
55
+ }
56
+
45
57
  export interface GuardianCallbackDecisionParams {
46
58
  content: string;
47
59
  callbackData?: string;
@@ -53,6 +65,8 @@ export interface GuardianCallbackDecisionParams {
53
65
  assistantId: string;
54
66
  approvalCopyGenerator?: ApprovalCopyGenerator;
55
67
  approvalConversationGenerator?: ApprovalConversationGenerator;
68
+ /** Original approval message timestamp (Slack ts) for editing after resolution. */
69
+ approvalMessageTs?: string;
56
70
  }
57
71
 
58
72
  export interface ApprovalInterceptionResult {
@@ -84,6 +98,7 @@ export async function handleGuardianCallbackDecision(
84
98
  assistantId,
85
99
  approvalCopyGenerator,
86
100
  approvalConversationGenerator,
101
+ approvalMessageTs,
87
102
  } = params;
88
103
 
89
104
  // Callback/button path: deterministic and takes priority.
@@ -129,6 +144,7 @@ export async function handleGuardianCallbackDecision(
129
144
  "Failed to deliver stale callback disambiguation notice",
130
145
  extraContext: { pendingCount: allPending.length },
131
146
  errorLogContext: { conversationExternalId },
147
+ ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
132
148
  });
133
149
  return { handled: true, type: "stale_ignored" };
134
150
  }
@@ -181,6 +197,7 @@ export async function handleGuardianCallbackDecision(
181
197
  logger: log,
182
198
  errorLogMessage: "Failed to deliver guardian identity rejection notice",
183
199
  errorLogContext: { conversationExternalId },
200
+ ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
184
201
  });
185
202
  return { handled: true, type: "guardian_decision_applied" };
186
203
  }
@@ -195,6 +212,7 @@ export async function handleGuardianCallbackDecision(
195
212
  assistantId,
196
213
  bearerToken,
197
214
  approvalCopyGenerator,
215
+ approvalMessageTs,
198
216
  });
199
217
  }
200
218
 
@@ -247,15 +265,20 @@ export async function handleGuardianCallbackDecision(
247
265
  {},
248
266
  approvalCopyGenerator,
249
267
  );
250
- await deliverChannelReply(
251
- replyCallbackUrl,
252
- {
253
- chatId: conversationExternalId,
254
- text,
255
- assistantId,
256
- },
257
- bearerToken,
268
+ const fallbackPayload: Parameters<typeof deliverChannelReply>[1] = {
269
+ chatId: conversationExternalId,
270
+ text,
271
+ assistantId,
272
+ };
273
+ const guardianFallbackEphemeral = slackEphemeralUserId(
274
+ sourceChannel,
275
+ actorExternalId,
258
276
  );
277
+ if (guardianFallbackEphemeral) {
278
+ fallbackPayload.ephemeral = true;
279
+ fallbackPayload.user = guardianFallbackEphemeral;
280
+ }
281
+ await deliverChannelReply(replyCallbackUrl, fallbackPayload, bearerToken);
259
282
  } catch (err) {
260
283
  log.error(
261
284
  { err, conversationExternalId },
@@ -282,6 +305,7 @@ async function handleCallbackDecision(params: {
282
305
  assistantId: string;
283
306
  bearerToken?: string;
284
307
  approvalCopyGenerator?: ApprovalCopyGenerator;
308
+ approvalMessageTs?: string;
285
309
  }): Promise<ApprovalInterceptionResult> {
286
310
  const {
287
311
  guardianApproval,
@@ -292,6 +316,7 @@ async function handleCallbackDecision(params: {
292
316
  assistantId,
293
317
  bearerToken,
294
318
  approvalCopyGenerator,
319
+ approvalMessageTs,
295
320
  } = params;
296
321
 
297
322
  // Access request approvals don't have a pending interaction in the
@@ -326,10 +351,12 @@ async function handleCallbackDecision(params: {
326
351
  callbackDecision.action === "approve_always"
327
352
  ? "approve_once"
328
353
  : callbackDecision.action;
354
+ const decisionOutcome: "approved" | "denied" =
355
+ effectiveAction === "reject" ? "denied" : "approved";
329
356
  const outcomeText = await composeApprovalMessageGenerative(
330
357
  {
331
358
  scenario: "guardian_decision_outcome",
332
- decision: effectiveAction === "reject" ? "denied" : "approved",
359
+ decision: decisionOutcome,
333
360
  toolName: guardianApproval.toolName,
334
361
  channel: sourceChannel,
335
362
  },
@@ -337,15 +364,20 @@ async function handleCallbackDecision(params: {
337
364
  approvalCopyGenerator,
338
365
  );
339
366
  try {
340
- await deliverChannelReply(
341
- replyCallbackUrl,
342
- {
343
- chatId: guardianApproval.requesterChatId,
344
- text: outcomeText,
345
- assistantId,
346
- },
347
- bearerToken,
367
+ const outcomePayload: Parameters<typeof deliverChannelReply>[1] = {
368
+ chatId: guardianApproval.requesterChatId,
369
+ text: outcomeText,
370
+ assistantId,
371
+ };
372
+ const requesterEphemeral = slackEphemeralUserId(
373
+ sourceChannel,
374
+ guardianApproval.requesterExternalUserId,
348
375
  );
376
+ if (requesterEphemeral) {
377
+ outcomePayload.ephemeral = true;
378
+ outcomePayload.user = requesterEphemeral;
379
+ }
380
+ await deliverChannelReply(replyCallbackUrl, outcomePayload, bearerToken);
349
381
  } catch (err) {
350
382
  log.error(
351
383
  { err, conversationId: guardianApproval.conversationId },
@@ -353,12 +385,71 @@ async function handleCallbackDecision(params: {
353
385
  );
354
386
  }
355
387
 
388
+ // Edit the original Slack approval message to show the decision and
389
+ // remove stale action buttons. This prevents users from clicking
390
+ // buttons that have already been resolved.
391
+ if (sourceChannel === "slack" && approvalMessageTs) {
392
+ editSlackApprovalMessage({
393
+ replyCallbackUrl,
394
+ chatId: guardianApproval.guardianChatId,
395
+ messageTs: approvalMessageTs,
396
+ decision: decisionOutcome,
397
+ assistantId,
398
+ bearerToken,
399
+ conversationId: guardianApproval.conversationId,
400
+ });
401
+ }
402
+
356
403
  // Post-decision delivery is handled by the onEvent callback
357
404
  // in the session that registered the pending interaction.
358
405
  return { handled: true, type: "guardian_decision_applied" };
359
406
  }
360
407
 
361
408
  // Race condition: callback arrived after request was already resolved.
409
+ // On Slack, edit the original message to show it's resolved and remove
410
+ // stale buttons so the guardian isn't left with actionable UI that does
411
+ // nothing. Also send an ephemeral error message for visibility.
412
+ if (sourceChannel === "slack" && approvalMessageTs) {
413
+ // Re-read the approval from DB to get the actual resolved status.
414
+ // The in-memory `guardianApproval` was loaded via a pending-status
415
+ // filter and is still "pending" even though it was resolved by
416
+ // another process.
417
+ const refreshed = getApprovalRequestById(guardianApproval.id);
418
+ const resolvedStatus =
419
+ refreshed?.status === "approved" ? "approved" : "denied";
420
+ editSlackApprovalMessage({
421
+ replyCallbackUrl,
422
+ chatId: guardianApproval.guardianChatId,
423
+ messageTs: approvalMessageTs,
424
+ decision: resolvedStatus,
425
+ assistantId,
426
+ bearerToken,
427
+ conversationId: guardianApproval.conversationId,
428
+ });
429
+ }
430
+
431
+ // Deliver a visible ephemeral error so the user sees feedback (JARVIS-299).
432
+ if (sourceChannel === "slack") {
433
+ try {
434
+ await deliverChannelReply(
435
+ replyCallbackUrl,
436
+ {
437
+ chatId: guardianApproval.guardianChatId,
438
+ text: "This approval request has already been resolved.",
439
+ assistantId,
440
+ ephemeral: true,
441
+ user: actorExternalId,
442
+ },
443
+ bearerToken,
444
+ );
445
+ } catch (err) {
446
+ log.error(
447
+ { err, conversationId: guardianApproval.conversationId },
448
+ "Failed to deliver stale approval ephemeral notice",
449
+ );
450
+ }
451
+ }
452
+
362
453
  return { handled: true, type: "stale_ignored" };
363
454
  }
364
455
 
@@ -415,13 +506,22 @@ async function handleConversationalDecision(params: {
415
506
  if (engineResult.disposition === "keep_pending") {
416
507
  // Non-decision follow-up (clarification, disambiguation, etc.)
417
508
  try {
509
+ const keepPendingPayload: Parameters<typeof deliverChannelReply>[1] = {
510
+ chatId: conversationExternalId,
511
+ text: engineResult.replyText,
512
+ assistantId,
513
+ };
514
+ const guardianEphemeral = slackEphemeralUserId(
515
+ sourceChannel,
516
+ actorExternalId,
517
+ );
518
+ if (guardianEphemeral) {
519
+ keepPendingPayload.ephemeral = true;
520
+ keepPendingPayload.user = guardianEphemeral;
521
+ }
418
522
  await deliverChannelReply(
419
523
  replyCallbackUrl,
420
- {
421
- chatId: conversationExternalId,
422
- text: engineResult.replyText,
423
- assistantId,
424
- },
524
+ keepPendingPayload,
425
525
  bearerToken,
426
526
  );
427
527
  } catch (err) {
@@ -481,6 +581,7 @@ async function handleConversationalDecision(params: {
481
581
  errorLogMessage:
482
582
  "Failed to deliver guardian identity mismatch notice for engine target",
483
583
  errorLogContext: { conversationExternalId },
584
+ ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
484
585
  });
485
586
  return { handled: true, type: "guardian_decision_applied" };
486
587
  }
@@ -528,13 +629,23 @@ async function handleConversationalDecision(params: {
528
629
  approvalCopyGenerator,
529
630
  );
530
631
  try {
531
- await deliverChannelReply(
532
- replyCallbackUrl,
632
+ const requesterOutcomePayload: Parameters<typeof deliverChannelReply>[1] =
533
633
  {
534
634
  chatId: targetApproval.requesterChatId,
535
635
  text: outcomeText,
536
636
  assistantId,
537
- },
637
+ };
638
+ const requesterEphemeral = slackEphemeralUserId(
639
+ sourceChannel,
640
+ targetApproval.requesterExternalUserId,
641
+ );
642
+ if (requesterEphemeral) {
643
+ requesterOutcomePayload.ephemeral = true;
644
+ requesterOutcomePayload.user = requesterEphemeral;
645
+ }
646
+ await deliverChannelReply(
647
+ replyCallbackUrl,
648
+ requesterOutcomePayload,
538
649
  bearerToken,
539
650
  );
540
651
  } catch (err) {
@@ -546,13 +657,22 @@ async function handleConversationalDecision(params: {
546
657
 
547
658
  // Deliver the engine's reply to the guardian
548
659
  try {
660
+ const guardianReplyPayload: Parameters<typeof deliverChannelReply>[1] = {
661
+ chatId: conversationExternalId,
662
+ text: engineResult.replyText,
663
+ assistantId,
664
+ };
665
+ const guardianEphemeral = slackEphemeralUserId(
666
+ sourceChannel,
667
+ actorExternalId,
668
+ );
669
+ if (guardianEphemeral) {
670
+ guardianReplyPayload.ephemeral = true;
671
+ guardianReplyPayload.user = guardianEphemeral;
672
+ }
549
673
  await deliverChannelReply(
550
674
  replyCallbackUrl,
551
- {
552
- chatId: conversationExternalId,
553
- text: engineResult.replyText,
554
- assistantId,
555
- },
675
+ guardianReplyPayload,
556
676
  bearerToken,
557
677
  );
558
678
  } catch (err) {
@@ -578,11 +698,80 @@ async function handleConversationalDecision(params: {
578
698
  logger: log,
579
699
  errorLogMessage: "Failed to deliver stale guardian approval notice",
580
700
  errorLogContext: { conversationId: targetApproval.conversationId },
701
+ ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
581
702
  });
582
703
 
583
704
  return { handled: true, type: "stale_ignored" };
584
705
  }
585
706
 
707
+ // ---------------------------------------------------------------------------
708
+ // Slack approval message edit helper
709
+ // ---------------------------------------------------------------------------
710
+
711
+ /**
712
+ * Fire-and-forget: edit the original Slack approval message to show the
713
+ * decision outcome and remove stale action buttons. Uses `chat.update` via
714
+ * the gateway deliver endpoint with `messageTs`.
715
+ *
716
+ * The status line replaces the inline buttons so users see the result
717
+ * inline without any actionable UI remaining.
718
+ */
719
+ function editSlackApprovalMessage(params: {
720
+ replyCallbackUrl: string;
721
+ chatId: string;
722
+ messageTs: string;
723
+ decision: "approved" | "denied";
724
+ assistantId: string;
725
+ bearerToken?: string;
726
+ conversationId: string;
727
+ }): void {
728
+ const {
729
+ replyCallbackUrl,
730
+ chatId,
731
+ messageTs,
732
+ decision,
733
+ assistantId,
734
+ bearerToken,
735
+ conversationId,
736
+ } = params;
737
+
738
+ const statusEmoji = decision === "approved" ? "\u2713" : "\u2717";
739
+ const statusLabel = decision === "approved" ? "Approved" : "Denied";
740
+ const statusText = `${statusEmoji} ${statusLabel}`;
741
+
742
+ // Build Block Kit blocks matching the resolved approval layout:
743
+ // a section with the status text and a context line with the decision.
744
+ // This replaces the original approval prompt's action buttons with a
745
+ // read-only status display.
746
+ const blocks = [
747
+ {
748
+ type: "section",
749
+ text: { type: "mrkdwn", text: statusText },
750
+ },
751
+ {
752
+ type: "context",
753
+ elements: [{ type: "mrkdwn", text: `${statusEmoji} ${statusLabel}` }],
754
+ },
755
+ ];
756
+
757
+ deliverChannelReply(
758
+ replyCallbackUrl,
759
+ {
760
+ chatId,
761
+ text: statusText,
762
+ blocks,
763
+ messageTs,
764
+ assistantId,
765
+ },
766
+ bearerToken,
767
+ ).catch((err) => {
768
+ log.error(
769
+ { err, conversationId, messageTs },
770
+ "Failed to edit Slack approval message after resolution",
771
+ );
772
+ });
773
+ }
774
+
586
775
  // ---------------------------------------------------------------------------
587
776
  // Access request decision helper
588
777
  // ---------------------------------------------------------------------------
@@ -22,6 +22,17 @@ import { deliverStaleApprovalReply } from "../guardian-approval-reply-helpers.js
22
22
 
23
23
  const log = getLogger("runtime-http");
24
24
 
25
+ /**
26
+ * Resolve the Slack ephemeral user ID when the source channel is Slack.
27
+ * Returns `undefined` for non-Slack channels.
28
+ */
29
+ function slackEphemeralUserId(
30
+ sourceChannel: ChannelId,
31
+ userId: string | undefined,
32
+ ): string | undefined {
33
+ return sourceChannel === "slack" && userId ? userId : undefined;
34
+ }
35
+
25
36
  export interface TextEngineDecisionParams {
26
37
  conversationId: string;
27
38
  conversationExternalId: string;
@@ -36,6 +47,8 @@ export interface TextEngineDecisionParams {
36
47
  pending: Array<{ requestId: string; toolName: string }>;
37
48
  /** Allowed actions from the pending prompt. */
38
49
  allowedActions: string[];
50
+ /** External user ID of the actor (for Slack ephemeral routing). */
51
+ actorExternalId?: string;
39
52
  }
40
53
 
41
54
  /**
@@ -58,6 +71,7 @@ export async function handleGuardianTextEngineDecision(
58
71
  approvalConversationGenerator,
59
72
  pending,
60
73
  allowedActions,
74
+ actorExternalId,
61
75
  } = params;
62
76
 
63
77
  const engineContext: ApprovalConversationContext = {
@@ -79,13 +93,19 @@ export async function handleGuardianTextEngineDecision(
79
93
  if (engineResult.disposition === "keep_pending") {
80
94
  // Non-decision follow-up — deliver the engine's reply and keep the request pending
81
95
  try {
96
+ const keepPendingPayload: Parameters<typeof deliverChannelReply>[1] = {
97
+ chatId: conversationExternalId,
98
+ text: engineResult.replyText,
99
+ assistantId,
100
+ };
101
+ const ephemeral = slackEphemeralUserId(sourceChannel, actorExternalId);
102
+ if (ephemeral) {
103
+ keepPendingPayload.ephemeral = true;
104
+ keepPendingPayload.user = ephemeral;
105
+ }
82
106
  await deliverChannelReply(
83
107
  replyCallbackUrl,
84
- {
85
- chatId: conversationExternalId,
86
- text: engineResult.replyText,
87
- assistantId,
88
- },
108
+ keepPendingPayload,
89
109
  bearerToken,
90
110
  );
91
111
  } catch (err) {
@@ -112,15 +132,17 @@ export async function handleGuardianTextEngineDecision(
112
132
  if (result.applied) {
113
133
  // Deliver the engine's reply text to the user
114
134
  try {
115
- await deliverChannelReply(
116
- replyCallbackUrl,
117
- {
118
- chatId: conversationExternalId,
119
- text: engineResult.replyText,
120
- assistantId,
121
- },
122
- bearerToken,
123
- );
135
+ const decisionPayload: Parameters<typeof deliverChannelReply>[1] = {
136
+ chatId: conversationExternalId,
137
+ text: engineResult.replyText,
138
+ assistantId,
139
+ };
140
+ const ephemeral = slackEphemeralUserId(sourceChannel, actorExternalId);
141
+ if (ephemeral) {
142
+ decisionPayload.ephemeral = true;
143
+ decisionPayload.user = ephemeral;
144
+ }
145
+ await deliverChannelReply(replyCallbackUrl, decisionPayload, bearerToken);
124
146
  } catch (err) {
125
147
  log.error(
126
148
  { err, conversationId },
@@ -145,6 +167,7 @@ export async function handleGuardianTextEngineDecision(
145
167
  logger: log,
146
168
  errorLogMessage: "Failed to deliver stale approval notice",
147
169
  errorLogContext: { conversationId },
170
+ ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
148
171
  });
149
172
 
150
173
  return { handled: true, type: "stale_ignored" };
@@ -63,10 +63,15 @@ export async function handleGetChannelReadiness(url: URL): Promise<Response> {
63
63
  export async function handleRefreshChannelReadiness(
64
64
  req: Request,
65
65
  ): Promise<Response> {
66
- const body = (await req.json().catch(() => ({}))) as {
67
- channel?: ChannelId;
68
- includeRemote?: boolean;
69
- };
66
+ let body: { channel?: ChannelId; includeRemote?: boolean };
67
+ try {
68
+ body = (await req.json()) as typeof body;
69
+ } catch {
70
+ return Response.json(
71
+ { success: false, error: "Invalid JSON in request body" },
72
+ { status: 400 },
73
+ );
74
+ }
70
75
 
71
76
  const service = getReadinessService();
72
77
 
@@ -8,7 +8,10 @@ import { getConfig } from "../../config/loader.js";
8
8
  import { countConversations } from "../../memory/conversation-queries.js";
9
9
  import { rawAll } from "../../memory/db.js";
10
10
  import { getMemoryJobCounts } from "../../memory/jobs-store.js";
11
- import { getProviderDebugStatus } from "../../providers/registry.js";
11
+ import {
12
+ getProviderRoutingSource,
13
+ listProviders,
14
+ } from "../../providers/registry.js";
12
15
  import { countSchedules } from "../../schedule/schedule-store.js";
13
16
  import { getDbPath } from "../../util/platform.js";
14
17
  import type { RouteDefinition } from "../http-router.js";
@@ -48,13 +51,11 @@ function handleDebug(): Response {
48
51
  const scheduleCounts = countSchedules();
49
52
 
50
53
  const config = getConfig();
51
- const providerOrder = Array.isArray(config.providerOrder)
52
- ? config.providerOrder
53
- : [];
54
- const providerStatus = getProviderDebugStatus(
55
- config.services.inference.provider,
56
- providerOrder,
57
- );
54
+ const registeredProviders = listProviders();
55
+ const routingSources: Record<string, string | undefined> = {};
56
+ for (const name of registeredProviders) {
57
+ routingSources[name] = getProviderRoutingSource(name);
58
+ }
58
59
 
59
60
  return Response.json({
60
61
  session: {
@@ -62,7 +63,9 @@ function handleDebug(): Response {
62
63
  startedAt: new Date(startedAt).toISOString(),
63
64
  },
64
65
  provider: {
65
- ...providerStatus,
66
+ configuredProvider: config.services.inference.provider,
67
+ registeredProviders,
68
+ routingSources,
66
69
  inferenceMode: config.services.inference.mode,
67
70
  },
68
71
  memory: {