@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.
- package/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +8 -0
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- 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 {
|
|
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:
|
|
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
|
-
?
|
|
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:
|
|
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
|
-
?
|
|
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
|
-
//
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
310
|
+
const SLACK_THINKING_MAX_DURATION_MS = 120_000;
|
|
314
311
|
|
|
315
312
|
/**
|
|
316
|
-
*
|
|
317
|
-
* function that
|
|
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-
|
|
320
|
-
* to prevent stuck
|
|
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
|
|
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
|
|
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
|
|
333
|
-
// preventing a race where
|
|
334
|
-
const
|
|
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
|
-
|
|
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,
|
|
351
|
+
log.debug({ err, chatId, threadTs }, "Failed to set Slack thinking status");
|
|
344
352
|
});
|
|
345
353
|
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
348
|
-
|
|
354
|
+
const clearStatus = () => {
|
|
355
|
+
if (cleared) return;
|
|
356
|
+
cleared = true;
|
|
349
357
|
clearTimeout(safetyTimer);
|
|
350
|
-
void
|
|
358
|
+
void setPromise.then(() =>
|
|
351
359
|
deliverChannelReply(
|
|
352
360
|
callbackUrl,
|
|
353
361
|
{
|
|
354
362
|
chatId,
|
|
355
363
|
assistantId,
|
|
356
|
-
|
|
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,
|
|
362
|
-
"Failed to
|
|
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(
|
|
380
|
+
const safetyTimer = setTimeout(clearStatus, SLACK_THINKING_MAX_DURATION_MS);
|
|
369
381
|
(safetyTimer as { unref?: () => void }).unref?.();
|
|
370
382
|
|
|
371
|
-
return
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
771
|
-
expect(body.item.statement
|
|
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 =
|
|
450
|
-
const trimmedStatement =
|
|
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 =
|
|
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 =
|
|
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
|