@vellumai/assistant 0.3.15 → 0.3.18
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/ARCHITECTURE.md +211 -12
- package/Dockerfile +1 -1
- package/README.md +11 -5
- package/docs/architecture/http-token-refresh.md +274 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +328 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +19 -15
- package/src/__tests__/checker.test.ts +103 -48
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +356 -0
- package/src/__tests__/conversation-pairing.test.ts +127 -27
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-store.test.ts +182 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-dispatch.test.ts +120 -0
- package/src/__tests__/guardian-outbound-http.test.ts +194 -2
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +23 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +281 -0
- package/src/__tests__/notification-broadcaster.test.ts +115 -4
- package/src/__tests__/notification-decision-strategy.test.ts +138 -1
- package/src/__tests__/notification-deep-link.test.ts +44 -1
- package/src/__tests__/notification-guardian-path.test.ts +157 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +38 -22
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +323 -0
- package/src/__tests__/update-template-contract.test.ts +24 -0
- package/src/__tests__/voice-session-bridge.test.ts +109 -9
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +241 -39
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/guardian-dispatch.ts +8 -0
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +8 -6
- package/src/cli/core-commands.ts +43 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +15 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +54 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin-template-path.ts +6 -0
- package/src/config/update-bulletin.ts +97 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +4 -2
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +219 -8
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +2 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/identity.ts +45 -25
- package/src/daemon/handlers/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract/workspace.ts +12 -1
- package/src/daemon/ipc-contract-inventory.json +6 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +314 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +31 -9
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +546 -59
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +5 -6
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +60 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +35 -567
- package/src/memory/channel-guardian-store.ts +63 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +0 -3
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +44 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +33 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +136 -0
- package/src/memory/guardian-action-store.ts +418 -5
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +521 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +10 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +4 -0
- package/src/memory/schema.ts +31 -8
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +159 -18
- package/src/notifications/broadcaster.ts +69 -33
- package/src/notifications/conversation-pairing.ts +99 -21
- package/src/notifications/decision-engine.ts +176 -8
- package/src/notifications/deliveries-store.ts +39 -8
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +269 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/checker.ts +1 -16
- package/src/permissions/defaults.ts +25 -5
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +78 -16
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +1 -1
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +271 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/version.ts +29 -2
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread candidate builder for notification thread reuse.
|
|
3
|
+
*
|
|
4
|
+
* Builds a lightweight candidate set of recent notification conversations
|
|
5
|
+
* per channel that the decision engine can choose to reuse instead of
|
|
6
|
+
* starting a new thread. Includes guardian-specific context (pending
|
|
7
|
+
* unresolved request count) when available.
|
|
8
|
+
*
|
|
9
|
+
* The candidate set is intentionally compact — only the fields the LLM
|
|
10
|
+
* needs for a routing decision, not full conversation contents.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { and, count, desc, eq, inArray, isNotNull } from 'drizzle-orm';
|
|
14
|
+
|
|
15
|
+
import { getDb } from '../memory/db.js';
|
|
16
|
+
import { channelGuardianApprovalRequests, conversations, notificationDecisions, notificationDeliveries, notificationEvents } from '../memory/schema.js';
|
|
17
|
+
import { getLogger } from '../util/logger.js';
|
|
18
|
+
import type { NotificationChannel } from './types.js';
|
|
19
|
+
|
|
20
|
+
const log = getLogger('thread-candidates');
|
|
21
|
+
|
|
22
|
+
/** Maximum number of candidate threads to surface per channel. */
|
|
23
|
+
const MAX_CANDIDATES_PER_CHANNEL = 5;
|
|
24
|
+
|
|
25
|
+
/** Only consider conversations updated within this window (ms). */
|
|
26
|
+
const CANDIDATE_RECENCY_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
27
|
+
|
|
28
|
+
// -- Public types -------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Guardian-specific context attached to a thread candidate when available. */
|
|
31
|
+
export interface GuardianCandidateContext {
|
|
32
|
+
/** Number of unresolved (pending) guardian approval requests in this conversation. */
|
|
33
|
+
pendingUnresolvedRequestCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A single candidate conversation that the decision engine can select for reuse. */
|
|
37
|
+
export interface ThreadCandidate {
|
|
38
|
+
conversationId: string;
|
|
39
|
+
title: string | null;
|
|
40
|
+
updatedAt: number;
|
|
41
|
+
/** The source event name from the most recent notification delivered to this conversation. */
|
|
42
|
+
latestSourceEventName: string | null;
|
|
43
|
+
channel: NotificationChannel;
|
|
44
|
+
/** Guardian-specific context, present only when there are relevant guardian records. */
|
|
45
|
+
guardianContext?: GuardianCandidateContext;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Candidate set for the decision engine, keyed by channel. */
|
|
49
|
+
export type ThreadCandidateSet = Partial<Record<NotificationChannel, ThreadCandidate[]>>;
|
|
50
|
+
|
|
51
|
+
// -- Core builder -------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the thread candidate set for all selected channels.
|
|
55
|
+
*
|
|
56
|
+
* Queries recent notification-sourced conversations that were delivered
|
|
57
|
+
* to each channel and enriches them with guardian-specific metadata
|
|
58
|
+
* when available.
|
|
59
|
+
*
|
|
60
|
+
* Errors are caught per-channel so a failure in one channel does not
|
|
61
|
+
* block candidates for others.
|
|
62
|
+
*/
|
|
63
|
+
export function buildThreadCandidates(
|
|
64
|
+
channels: NotificationChannel[],
|
|
65
|
+
assistantId: string,
|
|
66
|
+
): ThreadCandidateSet {
|
|
67
|
+
const result: ThreadCandidateSet = {};
|
|
68
|
+
const cutoff = Date.now() - CANDIDATE_RECENCY_WINDOW_MS;
|
|
69
|
+
|
|
70
|
+
for (const channel of channels) {
|
|
71
|
+
try {
|
|
72
|
+
const candidates = buildCandidatesForChannel(channel, assistantId, cutoff);
|
|
73
|
+
if (candidates.length > 0) {
|
|
74
|
+
result[channel] = candidates;
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
log.warn({ err: errMsg, channel }, 'Failed to build thread candidates for channel');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// -- Per-channel query --------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Query recent notification conversations for a given channel.
|
|
89
|
+
*
|
|
90
|
+
* Joins notification_deliveries -> notification_decisions -> notification_events
|
|
91
|
+
* to find conversations that were created by the notification pipeline for
|
|
92
|
+
* this channel, then enriches with guardian context.
|
|
93
|
+
*/
|
|
94
|
+
function buildCandidatesForChannel(
|
|
95
|
+
channel: NotificationChannel,
|
|
96
|
+
assistantId: string,
|
|
97
|
+
cutoffMs: number,
|
|
98
|
+
): ThreadCandidate[] {
|
|
99
|
+
const db = getDb();
|
|
100
|
+
|
|
101
|
+
// Find recent notification deliveries for this channel that have a
|
|
102
|
+
// conversationId and were successfully sent.
|
|
103
|
+
const rows = db
|
|
104
|
+
.select({
|
|
105
|
+
conversationId: notificationDeliveries.conversationId,
|
|
106
|
+
channel: notificationDeliveries.channel,
|
|
107
|
+
deliverySentAt: notificationDeliveries.sentAt,
|
|
108
|
+
sourceEventName: notificationEvents.sourceEventName,
|
|
109
|
+
convTitle: conversations.title,
|
|
110
|
+
convUpdatedAt: conversations.updatedAt,
|
|
111
|
+
})
|
|
112
|
+
.from(notificationDeliveries)
|
|
113
|
+
.innerJoin(
|
|
114
|
+
notificationDecisions,
|
|
115
|
+
eq(notificationDeliveries.notificationDecisionId, notificationDecisions.id),
|
|
116
|
+
)
|
|
117
|
+
.innerJoin(
|
|
118
|
+
notificationEvents,
|
|
119
|
+
eq(notificationDecisions.notificationEventId, notificationEvents.id),
|
|
120
|
+
)
|
|
121
|
+
.innerJoin(
|
|
122
|
+
conversations,
|
|
123
|
+
eq(notificationDeliveries.conversationId, conversations.id),
|
|
124
|
+
)
|
|
125
|
+
.where(
|
|
126
|
+
and(
|
|
127
|
+
eq(notificationDeliveries.channel, channel),
|
|
128
|
+
eq(notificationDeliveries.assistantId, assistantId),
|
|
129
|
+
eq(notificationDeliveries.status, 'sent'),
|
|
130
|
+
isNotNull(notificationDeliveries.conversationId),
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
.orderBy(desc(notificationDeliveries.sentAt))
|
|
134
|
+
.limit(MAX_CANDIDATES_PER_CHANNEL * 3) // over-fetch to allow deduplication
|
|
135
|
+
.all();
|
|
136
|
+
|
|
137
|
+
// Deduplicate by conversationId (keep the most recent delivery per conversation)
|
|
138
|
+
const seen = new Set<string>();
|
|
139
|
+
const candidates: ThreadCandidate[] = [];
|
|
140
|
+
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
if (!row.conversationId) continue;
|
|
143
|
+
if (seen.has(row.conversationId)) continue;
|
|
144
|
+
|
|
145
|
+
// Apply recency filter on the conversation's updatedAt
|
|
146
|
+
if (row.convUpdatedAt < cutoffMs) continue;
|
|
147
|
+
|
|
148
|
+
seen.add(row.conversationId);
|
|
149
|
+
|
|
150
|
+
candidates.push({
|
|
151
|
+
conversationId: row.conversationId,
|
|
152
|
+
title: row.convTitle,
|
|
153
|
+
updatedAt: row.convUpdatedAt,
|
|
154
|
+
latestSourceEventName: row.sourceEventName ?? null,
|
|
155
|
+
channel: channel,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (candidates.length >= MAX_CANDIDATES_PER_CHANNEL) break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Batch-enrich all candidates with guardian context in a single query
|
|
162
|
+
if (candidates.length > 0) {
|
|
163
|
+
const pendingCounts = batchCountPendingByConversation(
|
|
164
|
+
candidates.map((c) => c.conversationId),
|
|
165
|
+
assistantId,
|
|
166
|
+
);
|
|
167
|
+
for (const candidate of candidates) {
|
|
168
|
+
const pendingCount = pendingCounts.get(candidate.conversationId) ?? 0;
|
|
169
|
+
if (pendingCount > 0) {
|
|
170
|
+
candidate.guardianContext = { pendingUnresolvedRequestCount: pendingCount };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return candidates;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -- Guardian context enrichment ----------------------------------------------
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Batch-count pending guardian approval requests for multiple conversations
|
|
182
|
+
* in a single query. Returns a map from conversationId to pending count
|
|
183
|
+
* (only entries with count > 0 are included).
|
|
184
|
+
*/
|
|
185
|
+
function batchCountPendingByConversation(
|
|
186
|
+
conversationIds: string[],
|
|
187
|
+
assistantId: string,
|
|
188
|
+
): Map<string, number> {
|
|
189
|
+
const result = new Map<string, number>();
|
|
190
|
+
if (conversationIds.length === 0) return result;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const db = getDb();
|
|
194
|
+
|
|
195
|
+
const rows = db
|
|
196
|
+
.select({
|
|
197
|
+
conversationId: channelGuardianApprovalRequests.conversationId,
|
|
198
|
+
count: count(),
|
|
199
|
+
})
|
|
200
|
+
.from(channelGuardianApprovalRequests)
|
|
201
|
+
.where(
|
|
202
|
+
and(
|
|
203
|
+
inArray(channelGuardianApprovalRequests.conversationId, conversationIds),
|
|
204
|
+
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
205
|
+
eq(channelGuardianApprovalRequests.assistantId, assistantId),
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
.groupBy(channelGuardianApprovalRequests.conversationId)
|
|
209
|
+
.all();
|
|
210
|
+
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
if (row.count > 0) {
|
|
213
|
+
result.set(row.conversationId, row.count);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
218
|
+
log.warn({ err: errMsg }, 'Failed to batch-query guardian context for candidates');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// -- Prompt serialization -----------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Serialize a thread candidate set into a compact text block suitable for
|
|
228
|
+
* injection into the decision engine's user prompt.
|
|
229
|
+
*
|
|
230
|
+
* Designed to be token-efficient while giving the LLM enough context
|
|
231
|
+
* to make a reuse decision.
|
|
232
|
+
*/
|
|
233
|
+
export function serializeCandidatesForPrompt(candidateSet: ThreadCandidateSet): string | null {
|
|
234
|
+
const channelEntries = Object.entries(candidateSet) as [NotificationChannel, ThreadCandidate[]][];
|
|
235
|
+
if (channelEntries.length === 0) return null;
|
|
236
|
+
|
|
237
|
+
const sections: string[] = [];
|
|
238
|
+
|
|
239
|
+
for (const [channel, candidates] of channelEntries) {
|
|
240
|
+
if (candidates.length === 0) continue;
|
|
241
|
+
|
|
242
|
+
const lines: string[] = [`Channel: ${channel}`];
|
|
243
|
+
for (const c of candidates) {
|
|
244
|
+
// Escape title to prevent format corruption from quotes or newlines in
|
|
245
|
+
// user/model-provided text. JSON.stringify produces a safe single-line
|
|
246
|
+
// quoted string; we strip the outer quotes since we wrap in our own.
|
|
247
|
+
const safeTitle = c.title
|
|
248
|
+
? JSON.stringify(c.title).slice(1, -1)
|
|
249
|
+
: '(untitled)';
|
|
250
|
+
const parts: string[] = [
|
|
251
|
+
` - id=${c.conversationId}`,
|
|
252
|
+
`title="${safeTitle}"`,
|
|
253
|
+
`updated=${new Date(c.updatedAt).toISOString()}`,
|
|
254
|
+
];
|
|
255
|
+
if (c.latestSourceEventName) {
|
|
256
|
+
const safeEventName = JSON.stringify(c.latestSourceEventName).slice(1, -1);
|
|
257
|
+
parts.push(`lastEvent="${safeEventName}"`);
|
|
258
|
+
}
|
|
259
|
+
if (c.guardianContext) {
|
|
260
|
+
parts.push(`pendingRequests=${c.guardianContext.pendingUnresolvedRequestCount}`);
|
|
261
|
+
}
|
|
262
|
+
lines.push(parts.join(' '));
|
|
263
|
+
}
|
|
264
|
+
sections.push(lines.join('\n'));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (sections.length === 0) return null;
|
|
268
|
+
return sections.join('\n\n');
|
|
269
|
+
}
|
|
@@ -79,12 +79,31 @@ export interface RenderedChannelCopy {
|
|
|
79
79
|
threadSeedMessage?: string;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// -- Thread action types ------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/** Start a new conversation thread for the notification delivery. */
|
|
85
|
+
export interface ThreadActionStartNew {
|
|
86
|
+
action: 'start_new';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Reuse an existing conversation thread identified by conversationId. */
|
|
90
|
+
export interface ThreadActionReuseExisting {
|
|
91
|
+
action: 'reuse_existing';
|
|
92
|
+
conversationId: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Per-channel thread action — either start a new thread or reuse an existing one. */
|
|
96
|
+
export type ThreadAction = ThreadActionStartNew | ThreadActionReuseExisting;
|
|
97
|
+
|
|
98
|
+
|
|
82
99
|
/** Output produced by the notification decision engine for a given signal. */
|
|
83
100
|
export interface NotificationDecision {
|
|
84
101
|
shouldNotify: boolean;
|
|
85
102
|
selectedChannels: NotificationChannel[];
|
|
86
103
|
reasoningSummary: string;
|
|
87
104
|
renderedCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>>;
|
|
105
|
+
/** Per-channel thread actions decided by the model. Absent channels default to start_new. */
|
|
106
|
+
threadActions?: Partial<Record<NotificationChannel, ThreadAction>>;
|
|
88
107
|
deepLinkTarget?: Record<string, unknown>;
|
|
89
108
|
dedupeKey: string;
|
|
90
109
|
confidence: number;
|
|
@@ -123,19 +123,6 @@ function getWrappedProgram(seg: { program: string; args: string[] }): string | u
|
|
|
123
123
|
return undefined;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
function isHighRiskRm(args: string[]): boolean {
|
|
127
|
-
// rm with -r, -rf, -fr, or targeting root/home
|
|
128
|
-
for (const arg of args) {
|
|
129
|
-
if (arg.startsWith('-') && (arg.includes('r') || arg.includes('f'))) {
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
if (arg === '/' || arg === '~' || arg === '$HOME') {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
126
|
function getStringField(input: Record<string, unknown>, ...keys: string[]): string {
|
|
140
127
|
for (const key of keys) {
|
|
141
128
|
const value = input[key];
|
|
@@ -398,9 +385,7 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
|
|
|
398
385
|
if (HIGH_RISK_PROGRAMS.has(prog)) return RiskLevel.High;
|
|
399
386
|
|
|
400
387
|
if (prog === 'rm') {
|
|
401
|
-
|
|
402
|
-
maxRisk = RiskLevel.Medium;
|
|
403
|
-
continue;
|
|
388
|
+
return RiskLevel.High;
|
|
404
389
|
}
|
|
405
390
|
|
|
406
391
|
if (prog === 'chmod' || prog === 'chown' || prog === 'chgrp'
|
|
@@ -37,6 +37,13 @@ const COMPUTER_USE_TOOLS = [
|
|
|
37
37
|
* Computed at runtime so paths reflect the configured root directory.
|
|
38
38
|
*/
|
|
39
39
|
export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
40
|
+
// Some test suites mock getConfig() with partial objects; treat missing
|
|
41
|
+
// branches as defaults so rule generation remains deterministic.
|
|
42
|
+
const config = getConfig() as {
|
|
43
|
+
sandbox?: { enabled?: boolean };
|
|
44
|
+
skills?: { load?: { extraDirs?: unknown } };
|
|
45
|
+
};
|
|
46
|
+
|
|
40
47
|
const hostFileRules = HOST_FILE_TOOLS.map((tool) => ({
|
|
41
48
|
id: `default:ask-${tool}-global`,
|
|
42
49
|
tool,
|
|
@@ -50,11 +57,11 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
50
57
|
// global default ask rule uses "**" (globstar) instead of a "tool:*" prefix
|
|
51
58
|
// because commands often contain "/" (e.g. "cat /etc/hosts").
|
|
52
59
|
const hostShellRule: DefaultRuleTemplate = {
|
|
53
|
-
id: 'default:
|
|
60
|
+
id: 'default:allow-host_bash-global',
|
|
54
61
|
tool: 'host_bash',
|
|
55
62
|
pattern: '**',
|
|
56
63
|
scope: 'everywhere',
|
|
57
|
-
decision: '
|
|
64
|
+
decision: 'allow',
|
|
58
65
|
priority: 50,
|
|
59
66
|
};
|
|
60
67
|
|
|
@@ -62,7 +69,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
62
69
|
// them (including high-risk) so the user is never prompted for sandbox work.
|
|
63
70
|
// Only emit this rule when the sandbox is actually enabled; otherwise bash
|
|
64
71
|
// commands execute on the host and must go through normal permission checks.
|
|
65
|
-
const sandboxEnabled =
|
|
72
|
+
const sandboxEnabled = config.sandbox?.enabled !== false;
|
|
66
73
|
const sandboxShellRule: DefaultRuleTemplate | null = sandboxEnabled
|
|
67
74
|
? {
|
|
68
75
|
id: 'default:allow-bash-global',
|
|
@@ -105,7 +112,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
105
112
|
// and write these without prompting. Also allow `rm BOOTSTRAP.md` so the
|
|
106
113
|
// agent can delete it at the end of the onboarding ritual.
|
|
107
114
|
const workspaceDir = join(getRootDir(), 'workspace').replaceAll('\\', '/');
|
|
108
|
-
const WORKSPACE_PROMPT_FILES = ['IDENTITY.md', 'USER.md', 'SOUL.md', 'BOOTSTRAP.md'] as const;
|
|
115
|
+
const WORKSPACE_PROMPT_FILES = ['IDENTITY.md', 'USER.md', 'SOUL.md', 'BOOTSTRAP.md', 'UPDATES.md'] as const;
|
|
109
116
|
const WORKSPACE_FILE_TOOLS = ['file_read', 'file_write', 'file_edit'] as const;
|
|
110
117
|
const workspacePromptRules = WORKSPACE_FILE_TOOLS.flatMap((tool) =>
|
|
111
118
|
WORKSPACE_PROMPT_FILES.map((file) => ({
|
|
@@ -127,6 +134,15 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
127
134
|
priority: 100,
|
|
128
135
|
};
|
|
129
136
|
|
|
137
|
+
const updatesDeleteRule: DefaultRuleTemplate = {
|
|
138
|
+
id: 'default:allow-bash-rm-updates',
|
|
139
|
+
tool: 'bash',
|
|
140
|
+
pattern: 'rm UPDATES.md',
|
|
141
|
+
scope: workspaceDir,
|
|
142
|
+
decision: 'allow',
|
|
143
|
+
priority: 100,
|
|
144
|
+
};
|
|
145
|
+
|
|
130
146
|
// Skill source directories — writing or editing skill source files should
|
|
131
147
|
// require explicit user approval so a compromised agent loop cannot silently
|
|
132
148
|
// modify skill code to escalate privileges.
|
|
@@ -140,7 +156,10 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
140
156
|
|
|
141
157
|
// Append any user-configured extra skill directories so they get the
|
|
142
158
|
// same default ask rules as managed and bundled dirs.
|
|
143
|
-
const
|
|
159
|
+
const rawExtraDirs = config.skills?.load?.extraDirs;
|
|
160
|
+
const extraDirs = Array.isArray(rawExtraDirs)
|
|
161
|
+
? rawExtraDirs.filter((dir): dir is string => typeof dir === 'string')
|
|
162
|
+
: [];
|
|
144
163
|
for (let i = 0; i < extraDirs.length; i++) {
|
|
145
164
|
skillDirs.push({ dir: extraDirs[i].replaceAll('\\', '/'), label: `extra-${i}` });
|
|
146
165
|
}
|
|
@@ -248,6 +267,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
248
267
|
...managedSkillRules,
|
|
249
268
|
...workspacePromptRules,
|
|
250
269
|
bootstrapDeleteRule,
|
|
270
|
+
updatesDeleteRule,
|
|
251
271
|
...skillSourceMutationRules,
|
|
252
272
|
skillLoadRule,
|
|
253
273
|
browserNavigateRule,
|
|
@@ -114,6 +114,23 @@ export class PermissionPrompter {
|
|
|
114
114
|
pending.resolve({ decision, selectedPattern, selectedScope, decisionContext });
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Deny all pending confirmation prompts at once. Used when a new user
|
|
119
|
+
* message arrives while confirmations are outstanding — the agent will
|
|
120
|
+
* see the denial and can re-request if still needed.
|
|
121
|
+
*/
|
|
122
|
+
denyAllPending(): void {
|
|
123
|
+
for (const [requestId, pending] of this.pending) {
|
|
124
|
+
clearTimeout(pending.timer);
|
|
125
|
+
this.pending.delete(requestId);
|
|
126
|
+
pending.resolve({ decision: 'deny', decisionContext: 'The user sent a new message instead of responding to this permission prompt. Stop what you are doing and respond to the user\'s new message. Do NOT retry this tool or request permission again until the user asks you to.' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get hasPending(): boolean {
|
|
131
|
+
return this.pending.size > 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
dispose(): void {
|
|
118
135
|
for (const [, pending] of this.pending) {
|
|
119
136
|
clearTimeout(pending.timer);
|
|
@@ -276,6 +276,8 @@ function loadFromDisk(): TrustRule[] {
|
|
|
276
276
|
// on loaded rules would silently widen their scope to global
|
|
277
277
|
// wildcards. Stripping them and re-saving prevents scope escalation.
|
|
278
278
|
for (const rule of rules) {
|
|
279
|
+
// Legacy v3 rules may carry principal-scoped fields that no longer
|
|
280
|
+
// exist in the TrustRule interface — cast to strip them at runtime.
|
|
279
281
|
const r = rule as unknown as Record<string, unknown>;
|
|
280
282
|
if ('principalKind' in r || 'principalId' in r || 'principalVersion' in r) {
|
|
281
283
|
delete r.principalKind;
|
|
@@ -47,6 +47,12 @@ function isFailoverError(error: unknown): boolean {
|
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export interface ProviderHealthStatus {
|
|
51
|
+
name: string;
|
|
52
|
+
healthy: boolean;
|
|
53
|
+
unhealthySince: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
export class FailoverProvider implements Provider {
|
|
51
57
|
public readonly name: string;
|
|
52
58
|
private readonly healthMap = new Map<string, ProviderHealth>();
|
|
@@ -126,4 +132,17 @@ export class FailoverProvider implements Provider {
|
|
|
126
132
|
undefined,
|
|
127
133
|
);
|
|
128
134
|
}
|
|
135
|
+
|
|
136
|
+
getHealthStatus(): ProviderHealthStatus[] {
|
|
137
|
+
return this.providers.map((p) => {
|
|
138
|
+
const health = this.healthMap.get(p.name)!;
|
|
139
|
+
return {
|
|
140
|
+
name: p.name,
|
|
141
|
+
healthy: health.unhealthySince == null,
|
|
142
|
+
unhealthySince: health.unhealthySince != null
|
|
143
|
+
? new Date(health.unhealthySince).toISOString()
|
|
144
|
+
: null,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
129
148
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { wrapWithLogfire } from "../logfire.js";
|
|
2
2
|
import { ConfigError } from "../util/errors.js";
|
|
3
3
|
import { AnthropicProvider } from "./anthropic/client.js";
|
|
4
|
-
import { FailoverProvider } from "./failover.js";
|
|
4
|
+
import { FailoverProvider, type ProviderHealthStatus } from "./failover.js";
|
|
5
5
|
import { FireworksProvider } from "./fireworks/client.js";
|
|
6
6
|
import { GeminiProvider } from "./gemini/client.js";
|
|
7
7
|
import { getProviderDefaultModel } from "./model-intents.js";
|
|
@@ -138,6 +138,51 @@ function resolveModel(config: ProvidersConfig, providerName: string): string {
|
|
|
138
138
|
return getProviderDefaultModel(providerName);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
export interface ProviderDebugStatus {
|
|
142
|
+
configuredPrimary: string;
|
|
143
|
+
activePrimary: string | null;
|
|
144
|
+
usedFallback: boolean;
|
|
145
|
+
registeredProviders: string[];
|
|
146
|
+
failoverHealth: ProviderHealthStatus[] | null;
|
|
147
|
+
overallHealth: 'healthy' | 'degraded' | 'down';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getProviderDebugStatus(
|
|
151
|
+
configuredProvider: string,
|
|
152
|
+
providerOrder: string[],
|
|
153
|
+
): ProviderDebugStatus {
|
|
154
|
+
const registered = listProviders();
|
|
155
|
+
const selection = resolveProviderSelection(configuredProvider, providerOrder);
|
|
156
|
+
|
|
157
|
+
let failoverHealth: ProviderHealthStatus[] | null = null;
|
|
158
|
+
if (cachedFailoverProvider) {
|
|
159
|
+
failoverHealth = cachedFailoverProvider.getHealthStatus();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let overallHealth: 'healthy' | 'degraded' | 'down' = 'down';
|
|
163
|
+
if (registered.length > 0 && selection.selectedPrimary) {
|
|
164
|
+
if (!failoverHealth) {
|
|
165
|
+
overallHealth = 'healthy';
|
|
166
|
+
} else {
|
|
167
|
+
const healthyCount = failoverHealth.filter((h) => h.healthy).length;
|
|
168
|
+
if (healthyCount === failoverHealth.length) {
|
|
169
|
+
overallHealth = 'healthy';
|
|
170
|
+
} else if (healthyCount > 0) {
|
|
171
|
+
overallHealth = 'degraded';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
configuredPrimary: configuredProvider,
|
|
178
|
+
activePrimary: selection.selectedPrimary,
|
|
179
|
+
usedFallback: selection.usedFallbackPrimary,
|
|
180
|
+
registeredProviders: registered,
|
|
181
|
+
failoverHealth,
|
|
182
|
+
overallHealth,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
141
186
|
export function initializeProviders(config: ProvidersConfig): void {
|
|
142
187
|
providers.clear();
|
|
143
188
|
cachedFailoverProvider = null;
|
|
@@ -223,7 +223,7 @@ export function getFallbackMessage(context: ApprovalMessageContext): string {
|
|
|
223
223
|
// consistency; wording adapts to channel and code type.
|
|
224
224
|
const code = context.verifyCommand ?? 'the verification code';
|
|
225
225
|
// Detect whether the code is a short numeric (identity-bound outbound)
|
|
226
|
-
// or a high-entropy hex (inbound challenge) and adjust wording.
|
|
226
|
+
// or a high-entropy hex (inbound challenge/bootstrap) and adjust wording.
|
|
227
227
|
const isNumeric = /^\d{4,8}$/.test(code);
|
|
228
228
|
if (context.channel === 'voice') {
|
|
229
229
|
if (isNumeric) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { createHash,randomBytes } from 'crypto';
|
|
10
10
|
import { v4 as uuid } from 'uuid';
|
|
11
11
|
|
|
12
|
-
import type { GuardianBinding, IdentityBindingStatus,SessionStatus, VerificationChallenge } from '../memory/channel-guardian-store.js';
|
|
12
|
+
import type { GuardianBinding, IdentityBindingStatus, SessionStatus, VerificationChallenge, VerificationPurpose } from '../memory/channel-guardian-store.js';
|
|
13
13
|
import {
|
|
14
14
|
bindSessionIdentity as storeBindSessionIdentity,
|
|
15
15
|
consumeChallenge,
|
|
@@ -62,7 +62,8 @@ export interface CreateChallengeResult {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export type ValidateChallengeResult =
|
|
65
|
-
| { success: true; bindingId: string }
|
|
65
|
+
| { success: true; bindingId: string; verificationType: 'guardian' }
|
|
66
|
+
| { success: true; verificationType: 'trusted_contact' }
|
|
66
67
|
| { success: false; reason: string };
|
|
67
68
|
|
|
68
69
|
// ---------------------------------------------------------------------------
|
|
@@ -272,6 +273,15 @@ export function validateAndConsumeChallenge(
|
|
|
272
273
|
// Reset the rate-limit counter on success
|
|
273
274
|
resetRateLimit(assistantId, channel, actorExternalUserId, actorChatId);
|
|
274
275
|
|
|
276
|
+
// Trusted contact verification sessions (created by the access request
|
|
277
|
+
// approval flow) should NOT create a guardian binding — the requester is
|
|
278
|
+
// becoming a trusted contact, not a guardian. The explicit verificationPurpose
|
|
279
|
+
// field distinguishes this from guardian outbound verification which also uses
|
|
280
|
+
// identity-bound sessions.
|
|
281
|
+
if (challenge.verificationPurpose === 'trusted_contact') {
|
|
282
|
+
return { success: true, verificationType: 'trusted_contact' };
|
|
283
|
+
}
|
|
284
|
+
|
|
275
285
|
// Reject if a different user already holds the guardian binding
|
|
276
286
|
const existingBinding = getActiveBinding(assistantId, channel);
|
|
277
287
|
if (existingBinding && existingBinding.guardianExternalUserId !== actorExternalUserId) {
|
|
@@ -302,7 +312,7 @@ export function validateAndConsumeChallenge(
|
|
|
302
312
|
metadataJson: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null,
|
|
303
313
|
});
|
|
304
314
|
|
|
305
|
-
return { success: true, bindingId: binding.id };
|
|
315
|
+
return { success: true, bindingId: binding.id, verificationType: 'guardian' };
|
|
306
316
|
}
|
|
307
317
|
|
|
308
318
|
/**
|
|
@@ -396,6 +406,7 @@ export function createOutboundSession(params: {
|
|
|
396
406
|
maxAttempts?: number;
|
|
397
407
|
sessionId?: string;
|
|
398
408
|
bootstrapTokenHash?: string;
|
|
409
|
+
verificationPurpose?: VerificationPurpose;
|
|
399
410
|
}): CreateOutboundSessionResult {
|
|
400
411
|
// Use high-entropy hex for unbound bootstrap sessions to prevent brute-force;
|
|
401
412
|
// 6-digit numeric codes are only safe when identity is already bound.
|
|
@@ -421,6 +432,7 @@ export function createOutboundSession(params: {
|
|
|
421
432
|
destinationAddress: params.destinationAddress,
|
|
422
433
|
codeDigits: params.codeDigits,
|
|
423
434
|
maxAttempts: params.maxAttempts,
|
|
435
|
+
verificationPurpose: params.verificationPurpose,
|
|
424
436
|
bootstrapTokenHash: params.bootstrapTokenHash,
|
|
425
437
|
});
|
|
426
438
|
|
|
@@ -117,6 +117,7 @@ export async function sweepFailedEvents(
|
|
|
117
117
|
},
|
|
118
118
|
assistantId,
|
|
119
119
|
guardianContext,
|
|
120
|
+
isInteractive: guardianContext?.actorRole === 'guardian',
|
|
120
121
|
},
|
|
121
122
|
sourceChannel,
|
|
122
123
|
sourceInterface,
|
|
@@ -133,7 +134,11 @@ export async function sweepFailedEvents(
|
|
|
133
134
|
? payload.externalChatId
|
|
134
135
|
: undefined;
|
|
135
136
|
if (externalChatId) {
|
|
136
|
-
|
|
137
|
+
// processMessage above generated a fresh assistant response, so any
|
|
138
|
+
// previously tracked segment progress belongs to the old response and
|
|
139
|
+
// must not carry over. Reset to 0 so we deliver all segments of the
|
|
140
|
+
// new response.
|
|
141
|
+
channelDeliveryStore.updateDeliveredSegmentCount(event.id, 0);
|
|
137
142
|
await deliverReplyViaCallback(
|
|
138
143
|
event.conversationId,
|
|
139
144
|
externalChatId,
|
|
@@ -141,7 +146,7 @@ export async function sweepFailedEvents(
|
|
|
141
146
|
bearerToken,
|
|
142
147
|
assistantId,
|
|
143
148
|
{
|
|
144
|
-
startFromSegment,
|
|
149
|
+
startFromSegment: 0,
|
|
145
150
|
onSegmentDelivered: (count) =>
|
|
146
151
|
channelDeliveryStore.updateDeliveredSegmentCount(event.id, count),
|
|
147
152
|
},
|