@vellumai/assistant 0.5.7 → 0.5.8

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 (197) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  14. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  15. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  16. package/src/__tests__/config-schema.test.ts +2 -0
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  21. package/src/__tests__/conversation-error.test.ts +15 -1
  22. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  23. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-queue.test.ts +0 -1
  26. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  28. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  29. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  30. package/src/__tests__/credential-execution-client.test.ts +5 -2
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  33. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  35. package/src/__tests__/credentials-cli.test.ts +4 -3
  36. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  37. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  38. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  39. package/src/__tests__/journal-context.test.ts +335 -0
  40. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  42. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  43. package/src/__tests__/memory-regressions.test.ts +408 -363
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  45. package/src/__tests__/non-member-access-request.test.ts +2 -2
  46. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  47. package/src/__tests__/oauth-cli.test.ts +5 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  49. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  50. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  51. package/src/__tests__/relay-server.test.ts +1 -2
  52. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  53. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  54. package/src/__tests__/secure-keys.test.ts +18 -15
  55. package/src/__tests__/skill-memory.test.ts +17 -3
  56. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  57. package/src/__tests__/stt-hints.test.ts +437 -0
  58. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  59. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  60. package/src/__tests__/voice-quality.test.ts +58 -0
  61. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  62. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  63. package/src/acp/agent-process.ts +9 -1
  64. package/src/agent/loop.ts +1 -1
  65. package/src/approvals/guardian-request-resolvers.ts +164 -38
  66. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  67. package/src/calls/call-controller.ts +9 -5
  68. package/src/calls/fish-audio-client.ts +26 -14
  69. package/src/calls/stt-hints.ts +189 -0
  70. package/src/calls/tts-text-sanitizer.ts +61 -0
  71. package/src/calls/twilio-routes.ts +32 -4
  72. package/src/calls/voice-quality.ts +15 -3
  73. package/src/calls/voice-session-bridge.ts +1 -0
  74. package/src/cli/commands/avatar.ts +2 -2
  75. package/src/cli/commands/credentials.ts +110 -94
  76. package/src/cli/commands/doctor.ts +2 -2
  77. package/src/cli/commands/keys.ts +7 -7
  78. package/src/cli/commands/memory.ts +1 -1
  79. package/src/cli/commands/oauth/connections.ts +11 -29
  80. package/src/cli/commands/oauth/platform.ts +389 -43
  81. package/src/cli/lib/daemon-credential-client.ts +284 -0
  82. package/src/cli.ts +1 -1
  83. package/src/config/bundled-skills/AGENTS.md +34 -0
  84. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  85. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  86. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  87. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  88. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  89. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  90. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  91. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  92. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  93. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  94. package/src/config/bundled-tool-registry.ts +4 -0
  95. package/src/config/defaults.ts +0 -2
  96. package/src/config/env-registry.ts +4 -4
  97. package/src/config/env.ts +14 -1
  98. package/src/config/feature-flag-registry.json +1 -1
  99. package/src/config/loader.ts +8 -11
  100. package/src/config/schema.ts +5 -16
  101. package/src/config/schemas/calls.ts +17 -0
  102. package/src/config/schemas/inference.ts +2 -2
  103. package/src/config/schemas/journal.ts +16 -0
  104. package/src/config/schemas/memory-processing.ts +2 -2
  105. package/src/config/types.ts +1 -0
  106. package/src/contacts/contact-store.ts +2 -2
  107. package/src/credential-execution/executable-discovery.ts +1 -1
  108. package/src/credential-execution/startup-timeout.ts +36 -0
  109. package/src/daemon/approval-generators.ts +3 -9
  110. package/src/daemon/conversation-error.ts +13 -1
  111. package/src/daemon/conversation-memory.ts +1 -2
  112. package/src/daemon/conversation-process.ts +18 -1
  113. package/src/daemon/conversation-surfaces.ts +30 -1
  114. package/src/daemon/conversation.ts +20 -9
  115. package/src/daemon/guardian-action-generators.ts +3 -9
  116. package/src/daemon/lifecycle.ts +18 -11
  117. package/src/daemon/message-types/conversations.ts +1 -0
  118. package/src/daemon/server.ts +2 -3
  119. package/src/memory/app-store.ts +31 -0
  120. package/src/memory/db-init.ts +4 -0
  121. package/src/memory/indexer.ts +19 -10
  122. package/src/memory/items-extractor.ts +315 -322
  123. package/src/memory/job-handlers/summarization.ts +26 -16
  124. package/src/memory/jobs-store.ts +33 -1
  125. package/src/memory/journal-memory.ts +214 -0
  126. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  127. package/src/memory/migrations/index.ts +1 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/retriever.test.ts +37 -25
  130. package/src/memory/retriever.ts +24 -49
  131. package/src/memory/schema/memory-core.ts +2 -0
  132. package/src/memory/search/formatting.ts +7 -44
  133. package/src/memory/search/staleness.ts +4 -0
  134. package/src/memory/search/tier-classifier.ts +10 -2
  135. package/src/memory/search/types.ts +2 -5
  136. package/src/memory/task-memory-cleanup.ts +4 -3
  137. package/src/notifications/adapters/slack.ts +168 -6
  138. package/src/notifications/broadcaster.ts +1 -0
  139. package/src/notifications/copy-composer.ts +59 -2
  140. package/src/notifications/signal.ts +2 -0
  141. package/src/notifications/types.ts +2 -0
  142. package/src/prompts/journal-context.ts +133 -0
  143. package/src/prompts/persona-resolver.ts +80 -24
  144. package/src/prompts/system-prompt.ts +8 -0
  145. package/src/prompts/templates/SOUL.md +10 -0
  146. package/src/providers/provider-send-message.ts +3 -32
  147. package/src/providers/registry.ts +2 -139
  148. package/src/providers/types.ts +1 -1
  149. package/src/runtime/access-request-helper.ts +4 -0
  150. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  151. package/src/runtime/auth/route-policy.ts +2 -0
  152. package/src/runtime/gateway-client.ts +47 -4
  153. package/src/runtime/guardian-decision-types.ts +45 -4
  154. package/src/runtime/http-server.ts +5 -2
  155. package/src/runtime/routes/access-request-decision.ts +2 -2
  156. package/src/runtime/routes/app-management-routes.ts +2 -1
  157. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  158. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  159. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  160. package/src/runtime/routes/debug-routes.ts +12 -9
  161. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  162. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  163. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  164. package/src/runtime/routes/identity-routes.ts +1 -1
  165. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  168. package/src/runtime/routes/integrations/twilio.ts +52 -10
  169. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  170. package/src/runtime/routes/memory-item-routes.ts +25 -11
  171. package/src/runtime/routes/secret-routes.ts +141 -10
  172. package/src/runtime/routes/tts-routes.ts +11 -1
  173. package/src/security/ces-credential-client.ts +18 -9
  174. package/src/security/ces-rpc-credential-backend.ts +4 -3
  175. package/src/security/credential-backend.ts +10 -4
  176. package/src/security/secure-keys.ts +21 -4
  177. package/src/skills/catalog-install.ts +4 -36
  178. package/src/skills/skill-memory.ts +1 -0
  179. package/src/subagent/manager.ts +2 -5
  180. package/src/tools/acp/spawn.ts +78 -1
  181. package/src/tools/credentials/vault.ts +5 -3
  182. package/src/tools/memory/definitions.ts +3 -2
  183. package/src/tools/memory/handlers.ts +10 -7
  184. package/src/tools/terminal/safe-env.ts +1 -0
  185. package/src/util/browser.ts +15 -0
  186. package/src/util/platform.ts +1 -1
  187. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  188. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  189. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  190. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  191. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  192. package/src/workspace/migrations/registry.ts +4 -0
  193. package/src/workspace/provider-commit-message-generator.ts +12 -21
  194. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  195. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  196. package/src/memory/search/lexical.ts +0 -48
  197. package/src/providers/failover.ts +0 -186
@@ -507,6 +507,15 @@ export async function handleChannelInbound(
507
507
  !result.duplicate &&
508
508
  !guardianReplyResult.skipApprovalInterception
509
509
  ) {
510
+ // Extract the original approval message timestamp for Slack button
511
+ // cleanup. When a Slack block_actions payload is forwarded, the gateway
512
+ // sets sourceMetadata.messageId to the ts of the message containing
513
+ // the button. This lets us edit the message after resolution.
514
+ const approvalMessageTs =
515
+ sourceChannel === "slack" && typeof sourceMetadata?.messageId === "string"
516
+ ? sourceMetadata.messageId
517
+ : undefined;
518
+
510
519
  const approvalResult = await handleApprovalInterception({
511
520
  conversationId: result.conversationId,
512
521
  callbackData: body.callbackData,
@@ -520,6 +529,7 @@ export async function handleChannelInbound(
520
529
  assistantId: canonicalAssistantId,
521
530
  approvalCopyGenerator,
522
531
  approvalConversationGenerator,
532
+ approvalMessageTs,
523
533
  });
524
534
 
525
535
  if (approvalResult.handled) {
@@ -598,6 +608,27 @@ export async function handleChannelInbound(
598
608
  }
599
609
  }
600
610
 
611
+ // On Slack, edit the original approval message to remove stale buttons
612
+ // and deliver an ephemeral error so the user gets visible feedback
613
+ // instead of a silent no-op (JARVIS-299).
614
+ if (sourceChannel === "slack" && replyCallbackUrl && approvalMessageTs) {
615
+ deliverChannelReply(
616
+ replyCallbackUrl,
617
+ {
618
+ chatId: conversationExternalId,
619
+ text: "This approval request has been resolved.",
620
+ messageTs: approvalMessageTs,
621
+ assistantId: canonicalAssistantId,
622
+ },
623
+ mintBearerToken(),
624
+ ).catch((err) => {
625
+ log.error(
626
+ { err, conversationId: result.conversationId },
627
+ "Failed to edit stale Slack approval message",
628
+ );
629
+ });
630
+ }
631
+
601
632
  return Response.json({
602
633
  accepted: true,
603
634
  duplicate: false,
@@ -650,7 +681,6 @@ export async function handleChannelInbound(
650
681
  mintBearerToken,
651
682
  assistantId: canonicalAssistantId,
652
683
  approvalCopyGenerator,
653
- externalMessageId: sourceMessageId ?? externalMessageId,
654
684
  chatType: sourceChatType,
655
685
  });
656
686
  }
@@ -8,7 +8,10 @@
8
8
  */
9
9
  import { isInviteCodeRedemptionEnabled } from "../../../channels/config.js";
10
10
  import type { ChannelId } from "../../../channels/types.js";
11
- import { findContactChannel } from "../../../contacts/contact-store.js";
11
+ import {
12
+ findContactChannel,
13
+ findGuardianForChannel,
14
+ } from "../../../contacts/contact-store.js";
12
15
  import { touchChannelLastSeen } from "../../../contacts/contacts-write.js";
13
16
  import type {
14
17
  ChannelStatus,
@@ -21,7 +24,10 @@ import {
21
24
  findByInviteCodeHash,
22
25
  findByInviteCodeHashAnyChannel,
23
26
  } from "../../../memory/invite-store.js";
27
+ import { MESSAGE_PREVIEW_MAX_LENGTH } from "../../../notifications/copy-composer.js";
28
+ import { resolveGuardianName } from "../../../prompts/user-reference.js";
24
29
  import { getLogger } from "../../../util/logger.js";
30
+ import { truncate } from "../../../util/truncate.js";
25
31
  import { hashVoiceCode } from "../../../util/voice-code.js";
26
32
  import { notifyGuardianOfAccessRequest } from "../../access-request-helper.js";
27
33
  import { getInviteAdapterRegistry } from "../../channel-invite-transport.js";
@@ -32,6 +38,7 @@ import {
32
38
  resolveBootstrapToken,
33
39
  } from "../../channel-verification-service.js";
34
40
  import { deliverChannelReply } from "../../gateway-client.js";
41
+ import { ensureVellumGuardianBinding } from "../../guardian-vellum-migration.js";
35
42
  import {
36
43
  redeemInvite,
37
44
  redeemInviteByCode,
@@ -40,6 +47,42 @@ import { getInviteRedemptionReply } from "../../invite-redemption-templates.js";
40
47
 
41
48
  const log = getLogger("runtime-http");
42
49
 
50
+ /**
51
+ * Resolve the guardian's display name for use in requester-facing messages.
52
+ *
53
+ * Uses the assistant's anchored vellum principal to validate the guardian
54
+ * contact, matching the same strategy used by `notifyGuardianOfAccessRequest`.
55
+ * This prevents stale or cross-assistant contacts from leaking a wrong name.
56
+ */
57
+ function resolveGuardianLabel(
58
+ sourceChannel: ChannelId,
59
+ canonicalAssistantId: string,
60
+ ): string {
61
+ const anchoredPrincipalId = ensureVellumGuardianBinding(canonicalAssistantId);
62
+
63
+ // Try source-channel guardian, but only accept it when the principal
64
+ // matches the assistant's anchor.
65
+ const sourceGuardian = findGuardianForChannel(sourceChannel);
66
+ if (
67
+ sourceGuardian &&
68
+ sourceGuardian.contact.principalId === anchoredPrincipalId
69
+ ) {
70
+ return resolveGuardianName(sourceGuardian.contact.displayName);
71
+ }
72
+
73
+ // Fall back to the vellum-channel guardian with the same anchor check.
74
+ const vellumGuardian = findGuardianForChannel("vellum");
75
+ if (
76
+ vellumGuardian &&
77
+ vellumGuardian.contact.principalId === anchoredPrincipalId
78
+ ) {
79
+ return resolveGuardianName(vellumGuardian.contact.displayName);
80
+ }
81
+
82
+ // No anchored guardian found — use generic fallback.
83
+ return resolveGuardianName(undefined);
84
+ }
85
+
43
86
  // ---------------------------------------------------------------------------
44
87
  // Public API
45
88
  // ---------------------------------------------------------------------------
@@ -321,6 +364,10 @@ export async function enforceIngressAcl(
321
364
  actorExternalId: canonicalSenderId ?? rawSenderId,
322
365
  actorDisplayName,
323
366
  actorUsername,
367
+ messagePreview: truncate(
368
+ trimmedContent,
369
+ MESSAGE_PREVIEW_MAX_LENGTH,
370
+ ),
324
371
  });
325
372
  } catch (err) {
326
373
  log.error(
@@ -349,7 +396,7 @@ export async function enforceIngressAcl(
349
396
  dmCallbackUrl,
350
397
  {
351
398
  chatId: senderUserId,
352
- text: "I don't recognize you yet! I've let my owner know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.",
399
+ text: `I don't recognize you yet! I've let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.`,
353
400
  assistantId,
354
401
  },
355
402
  mintBearerToken(),
@@ -387,6 +434,10 @@ export async function enforceIngressAcl(
387
434
  actorExternalId: canonicalSenderId ?? rawSenderId,
388
435
  actorDisplayName,
389
436
  actorUsername,
437
+ messagePreview: truncate(
438
+ trimmedContent,
439
+ MESSAGE_PREVIEW_MAX_LENGTH,
440
+ ),
390
441
  });
391
442
  guardianNotified = accessResult.notified;
392
443
  } catch (err) {
@@ -398,7 +449,7 @@ export async function enforceIngressAcl(
398
449
 
399
450
  if (replyCallbackUrl) {
400
451
  const replyText = guardianNotified
401
- ? "Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."
452
+ ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
402
453
  : "Sorry, you haven't been approved to message this assistant.";
403
454
  const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
404
455
  chatId: conversationExternalId,
@@ -579,6 +630,10 @@ export async function enforceIngressAcl(
579
630
  previousMemberStatus: channelStatusToMemberStatus(
580
631
  resolvedMember.channel.status,
581
632
  ),
633
+ messagePreview: truncate(
634
+ trimmedContent,
635
+ MESSAGE_PREVIEW_MAX_LENGTH,
636
+ ),
582
637
  });
583
638
  } catch (err) {
584
639
  log.error(
@@ -603,7 +658,7 @@ export async function enforceIngressAcl(
603
658
  dmCallbackUrl,
604
659
  {
605
660
  chatId: senderUserId,
606
- text: "I don't recognize you yet! I've let my owner know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.",
661
+ text: `I don't recognize you yet! I've let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you're trying to reach me. They'll need to share a 6-digit verification code with you — ask them directly if you know them. Once you have the code, reply here with it.`,
607
662
  assistantId,
608
663
  },
609
664
  mintBearerToken(),
@@ -645,6 +700,10 @@ export async function enforceIngressAcl(
645
700
  previousMemberStatus: channelStatusToMemberStatus(
646
701
  resolvedMember.channel.status,
647
702
  ),
703
+ messagePreview: truncate(
704
+ trimmedContent,
705
+ MESSAGE_PREVIEW_MAX_LENGTH,
706
+ ),
648
707
  });
649
708
  guardianNotified = accessResult.notified;
650
709
  } catch (err) {
@@ -657,7 +716,7 @@ export async function enforceIngressAcl(
657
716
 
658
717
  if (replyCallbackUrl) {
659
718
  const replyText = guardianNotified
660
- ? "Hmm looks like you don't have access to talk to me. I'll let them know you tried talking to me and get back to you."
719
+ ? `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(sourceChannel, canonicalAssistantId)} know you tried talking to me and get back to you.`
661
720
  : "Sorry, you haven't been approved to message this assistant.";
662
721
  const inactiveReplyPayload: Parameters<
663
722
  typeof deliverChannelReply
@@ -75,8 +75,6 @@ export interface BackgroundProcessingParams {
75
75
  approvalCopyGenerator?: ApprovalCopyGenerator;
76
76
  commandIntent?: Record<string, unknown>;
77
77
  sourceLanguageCode?: string;
78
- /** External message ID (e.g. Slack message ts) used for reaction indicators. */
79
- externalMessageId?: string;
80
78
  /** Chat type from the gateway (e.g. "private", "group", "supergroup"). */
81
79
  chatType?: string;
82
80
  }
@@ -106,7 +104,6 @@ export function processChannelMessageInBackground(
106
104
  approvalCopyGenerator,
107
105
  commandIntent,
108
106
  sourceLanguageCode,
109
- externalMessageId,
110
107
  chatType,
111
108
  } = params;
112
109
 
@@ -126,18 +123,18 @@ export function processChannelMessageInBackground(
126
123
  )
127
124
  : undefined;
128
125
 
129
- // Add 👀 reaction to the inbound Slack message as a processing indicator
130
- const removeSlackReaction =
131
- shouldEmitSlackReaction(sourceChannel, replyCallbackUrl) &&
132
- externalMessageId
133
- ? addSlackEyesReaction(
134
- replyCallbackUrl!,
135
- externalChatId,
136
- externalMessageId,
137
- mintBearerToken,
138
- assistantId,
139
- )
140
- : undefined;
126
+ // Set Slack Assistants API "is thinking..." status indicator
127
+ const clearSlackThinkingStatus = shouldEmitSlackReaction(
128
+ sourceChannel,
129
+ replyCallbackUrl,
130
+ )
131
+ ? setSlackThinkingStatus(
132
+ replyCallbackUrl!,
133
+ externalChatId,
134
+ mintBearerToken,
135
+ assistantId,
136
+ )
137
+ : undefined;
141
138
  const stopApprovalWatcher = replyCallbackUrl
142
139
  ? startPendingApprovalPromptWatcher({
143
140
  conversationId,
@@ -230,7 +227,7 @@ export function processChannelMessageInBackground(
230
227
  deliveryStatus.recordProcessingFailure(eventId, err);
231
228
  } finally {
232
229
  stopTypingHeartbeat?.();
233
- removeSlackReaction?.();
230
+ clearSlackThinkingStatus?.();
234
231
  stopApprovalWatcher?.();
235
232
  stopTcApprovalNotifier?.();
236
233
  }
@@ -295,7 +292,7 @@ export function startTelegramTypingHeartbeat(
295
292
  }
296
293
 
297
294
  // ---------------------------------------------------------------------------
298
- // Slack eyes reaction indicator
295
+ // Slack Assistants API thinking status indicator
299
296
  // ---------------------------------------------------------------------------
300
297
 
301
298
  export function shouldEmitSlackReaction(
@@ -310,65 +307,80 @@ export function shouldEmitSlackReaction(
310
307
  }
311
308
  }
312
309
 
313
- const SLACK_EYES_MAX_DURATION_MS = 120_000;
310
+ const SLACK_THINKING_MAX_DURATION_MS = 120_000;
314
311
 
315
312
  /**
316
- * Add a 👀 reaction to the inbound Slack message and return a cleanup
317
- * function that removes it. Both operations are fire-and-forget.
313
+ * Set the Slack Assistants API "is thinking..." status on the thread and
314
+ * return a cleanup function that clears it. Both operations are fire-and-forget.
318
315
  *
319
- * A safety timer auto-removes the reaction after {@link SLACK_EYES_MAX_DURATION_MS}
320
- * to prevent stuck eyes when `processMessage` hangs (e.g. queued behind
321
- * an active session turn that never completes for this message).
316
+ * A safety timer auto-clears the status after {@link SLACK_THINKING_MAX_DURATION_MS}
317
+ * to prevent a stuck indicator when `processMessage` hangs.
322
318
  */
323
- export function addSlackEyesReaction(
319
+ export function setSlackThinkingStatus(
324
320
  callbackUrl: string,
325
321
  chatId: string,
326
- messageTs: string,
327
322
  mintBearerToken: () => string,
328
323
  assistantId?: string,
329
324
  ): () => void {
330
- let removed = false;
325
+ let cleared = false;
326
+
327
+ // Extract the thread timestamp from the callback URL so we can target
328
+ // the correct thread for the Assistants API status.
329
+ const threadTs = extractThreadTsFromCallbackUrl(callbackUrl);
330
+
331
+ // If there's no thread context, we can't set a thread status — bail.
332
+ if (!threadTs) {
333
+ return () => {};
334
+ }
331
335
 
332
- // Track the add promise so remove waits for it to settle first,
333
- // preventing a race where remove arrives at Slack before add.
334
- const addPromise = deliverChannelReply(
336
+ // Track the set promise so clear waits for it to settle first,
337
+ // preventing a race where clear arrives at Slack before set.
338
+ const setPromise = deliverChannelReply(
335
339
  callbackUrl,
336
340
  {
337
341
  chatId,
338
342
  assistantId,
339
- reaction: { action: "add", name: "eyes", messageTs },
343
+ assistantThreadStatus: {
344
+ channel: chatId,
345
+ threadTs,
346
+ status: "is thinking...",
347
+ },
340
348
  },
341
349
  mintBearerToken(),
342
350
  ).catch((err) => {
343
- log.debug({ err, chatId, messageTs }, "Failed to add Slack eyes reaction");
351
+ log.debug({ err, chatId, threadTs }, "Failed to set Slack thinking status");
344
352
  });
345
353
 
346
- const removeReaction = () => {
347
- if (removed) return;
348
- removed = true;
354
+ const clearStatus = () => {
355
+ if (cleared) return;
356
+ cleared = true;
349
357
  clearTimeout(safetyTimer);
350
- void addPromise.then(() =>
358
+ void setPromise.then(() =>
351
359
  deliverChannelReply(
352
360
  callbackUrl,
353
361
  {
354
362
  chatId,
355
363
  assistantId,
356
- reaction: { action: "remove", name: "eyes", messageTs },
364
+ assistantThreadStatus: {
365
+ channel: chatId,
366
+ threadTs,
367
+ status: "",
368
+ },
357
369
  },
358
370
  mintBearerToken(),
359
371
  ).catch((err) => {
360
372
  log.debug(
361
- { err, chatId, messageTs },
362
- "Failed to remove Slack eyes reaction",
373
+ { err, chatId, threadTs },
374
+ "Failed to clear Slack thinking status",
363
375
  );
364
376
  }),
365
377
  );
366
378
  };
367
379
 
368
- const safetyTimer = setTimeout(removeReaction, SLACK_EYES_MAX_DURATION_MS);
380
+ const safetyTimer = setTimeout(clearStatus, SLACK_THINKING_MAX_DURATION_MS);
369
381
  (safetyTimer as { unref?: () => void }).unref?.();
370
382
 
371
- return removeReaction;
383
+ return clearStatus;
372
384
  }
373
385
 
374
386
  // ---------------------------------------------------------------------------
@@ -91,10 +91,19 @@ export async function handleGetTwilioConfig(): Promise<Response> {
91
91
  export async function handleSetTwilioCredentials(
92
92
  req: Request,
93
93
  ): Promise<Response> {
94
- const body = (await req.json().catch(() => ({}))) as {
95
- accountSid?: string;
96
- authToken?: string;
97
- };
94
+ let body: { accountSid?: string; authToken?: string };
95
+ try {
96
+ body = (await req.json()) as typeof body;
97
+ } catch {
98
+ return Response.json(
99
+ {
100
+ success: false,
101
+ hasCredentials: await hasTwilioCredentials(),
102
+ error: "Invalid JSON in request body",
103
+ },
104
+ { status: 400 },
105
+ );
106
+ }
98
107
 
99
108
  if (!body.accountSid || !body.authToken) {
100
109
  return Response.json(
@@ -261,10 +270,19 @@ export async function handleProvisionTwilioNumber(
261
270
  });
262
271
  }
263
272
 
264
- const body = (await req.json().catch(() => ({}))) as {
265
- country?: string;
266
- areaCode?: string;
267
- };
273
+ let body: { country?: string; areaCode?: string };
274
+ try {
275
+ body = (await req.json()) as typeof body;
276
+ } catch {
277
+ return Response.json(
278
+ {
279
+ success: false,
280
+ hasCredentials: await hasTwilioCredentials(),
281
+ error: "Invalid JSON in request body",
282
+ },
283
+ { status: 400 },
284
+ );
285
+ }
268
286
  const { accountSid, authToken } = await getTwilioCredentials();
269
287
  const country = body.country ?? "US";
270
288
 
@@ -318,7 +336,19 @@ export async function handleProvisionTwilioNumber(
318
336
  export async function handleAssignTwilioNumber(
319
337
  req: Request,
320
338
  ): Promise<Response> {
321
- const body = (await req.json().catch(() => ({}))) as { phoneNumber?: string };
339
+ let body: { phoneNumber?: string };
340
+ try {
341
+ body = (await req.json()) as typeof body;
342
+ } catch {
343
+ return Response.json(
344
+ {
345
+ success: false,
346
+ hasCredentials: await hasTwilioCredentials(),
347
+ error: "Invalid JSON in request body",
348
+ },
349
+ { status: 400 },
350
+ );
351
+ }
322
352
 
323
353
  if (!body.phoneNumber) {
324
354
  return Response.json(
@@ -375,7 +405,19 @@ export async function handleReleaseTwilioNumber(
375
405
  });
376
406
  }
377
407
 
378
- const body = (await req.json().catch(() => ({}))) as { phoneNumber?: string };
408
+ let body: { phoneNumber?: string };
409
+ try {
410
+ body = (await req.json()) as typeof body;
411
+ } catch {
412
+ return Response.json(
413
+ {
414
+ success: false,
415
+ hasCredentials: await hasTwilioCredentials(),
416
+ error: "Invalid JSON in request body",
417
+ },
418
+ { status: 400 },
419
+ );
420
+ }
379
421
  const raw = loadRawConfig();
380
422
  const twilio = (raw?.twilio ?? {}) as Record<string, unknown>;
381
423
  const phoneNumber = body.phoneNumber || (twilio.phoneNumber as string) || "";
@@ -754,7 +754,7 @@ describe("Memory Item Routes", () => {
754
754
  expect(res.status).toBe(400);
755
755
  });
756
756
 
757
- test("truncates long subject and statement", async () => {
757
+ test("preserves long subject and statement without truncation", async () => {
758
758
  const longSubject = "a".repeat(200);
759
759
  const longStatement = "b".repeat(1000);
760
760
  const ctx = makeJsonCtx("memory-items", "POST", {
@@ -767,8 +767,8 @@ describe("Memory Item Routes", () => {
767
767
  const body = (await res.json()) as {
768
768
  item: { subject: string; statement: string };
769
769
  };
770
- expect(body.item.subject.length).toBeLessThanOrEqual(80);
771
- expect(body.item.statement.length).toBeLessThanOrEqual(500);
770
+ expect(body.item.subject).toBe(longSubject);
771
+ expect(body.item.statement).toBe(longStatement);
772
772
  });
773
773
 
774
774
  test("enqueues embed job on create", async () => {
@@ -28,7 +28,6 @@ import {
28
28
  memoryItems,
29
29
  } from "../../memory/schema.js";
30
30
  import { getLogger } from "../../util/logger.js";
31
- import { truncate } from "../../util/truncate.js";
32
31
  import { httpError } from "../http-errors.js";
33
32
  import type { RouteContext, RouteDefinition } from "../http-router.js";
34
33
 
@@ -46,6 +45,7 @@ const VALID_KINDS = [
46
45
  "constraint",
47
46
  "event",
48
47
  "capability",
48
+ "journal",
49
49
  ] as const;
50
50
 
51
51
  type MemoryItemKind = (typeof VALID_KINDS)[number];
@@ -168,9 +168,7 @@ async function searchItemsSemantic(
168
168
 
169
169
  const filter = {
170
170
  must: mustConditions,
171
- must_not: [
172
- { key: "_meta", match: { value: true } },
173
- ],
171
+ must_not: [{ key: "_meta", match: { value: true } }],
174
172
  };
175
173
 
176
174
  const qdrant = getQdrantClient();
@@ -260,9 +258,7 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
260
258
 
261
259
  // Re-apply the same DB-side filters used in the SQL path as defense-
262
260
  // in-depth against stale Qdrant payloads leaking deleted/mismatched rows.
263
- const hydrationConditions = [
264
- inArray(memoryItems.id, pageIds),
265
- ];
261
+ const hydrationConditions = [inArray(memoryItems.id, pageIds)];
266
262
  if (statusParam && statusParam !== "all") {
267
263
  hydrationConditions.push(eq(memoryItems.status, statusParam));
268
264
  }
@@ -446,8 +442,8 @@ export async function handleCreateMemoryItem(
446
442
  );
447
443
  }
448
444
 
449
- const trimmedSubject = truncate(subject.trim(), 80, "");
450
- const trimmedStatement = truncate(statement.trim(), 500, "");
445
+ const trimmedSubject = subject.trim();
446
+ const trimmedStatement = statement.trim();
451
447
 
452
448
  const scopeId = "default";
453
449
  const fingerprint = computeMemoryFingerprint(
@@ -492,6 +488,7 @@ export async function handleCreateMemoryItem(
492
488
  confidence: 0.95,
493
489
  importance: importance ?? 0.8,
494
490
  fingerprint,
491
+ sourceType: "tool",
495
492
  verificationState: "user_confirmed",
496
493
  scopeId,
497
494
  firstSeenAt: now,
@@ -534,6 +531,7 @@ export async function handleUpdateMemoryItem(
534
531
  kind?: string;
535
532
  status?: string;
536
533
  importance?: number;
534
+ sourceType?: string;
537
535
  verificationState?: string;
538
536
  };
539
537
 
@@ -558,13 +556,13 @@ export async function handleUpdateMemoryItem(
558
556
  if (typeof body.subject !== "string") {
559
557
  return httpError("BAD_REQUEST", "subject must be a string", 400);
560
558
  }
561
- set.subject = truncate(body.subject.trim(), 80, "");
559
+ set.subject = body.subject.trim();
562
560
  }
563
561
  if (body.statement !== undefined) {
564
562
  if (typeof body.statement !== "string") {
565
563
  return httpError("BAD_REQUEST", "statement must be a string", 400);
566
564
  }
567
- set.statement = truncate(body.statement.trim(), 500, "");
565
+ set.statement = body.statement.trim();
568
566
  }
569
567
  if (body.kind !== undefined) {
570
568
  if (!isValidKind(body.kind)) {
@@ -582,8 +580,24 @@ export async function handleUpdateMemoryItem(
582
580
  if (body.importance !== undefined) {
583
581
  set.importance = body.importance;
584
582
  }
583
+ if (body.sourceType !== undefined) {
584
+ set.sourceType = body.sourceType;
585
+ }
586
+
587
+ // Accept verificationState from clients that haven't migrated to sourceType yet.
588
+ // Map verificationState → sourceType for forward compat, and write both fields.
585
589
  if (body.verificationState !== undefined) {
586
590
  set.verificationState = body.verificationState;
591
+ // Map verificationState to sourceType if sourceType wasn't explicitly provided
592
+ if (body.sourceType === undefined) {
593
+ set.sourceType =
594
+ body.verificationState === "user_confirmed" ? "tool" : "extraction";
595
+ }
596
+ }
597
+ // If sourceType was set (either directly or via mapping), also write verificationState
598
+ if (body.sourceType !== undefined && body.verificationState === undefined) {
599
+ set.verificationState =
600
+ body.sourceType === "tool" ? "user_confirmed" : "assistant_inferred";
587
601
  }
588
602
 
589
603
  // If subject, statement, or kind changed, recompute fingerprint