@vellumai/assistant 0.3.14 → 0.3.16
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 +142 -0
- package/Dockerfile +2 -2
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -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 +331 -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 +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- 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 +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- 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 +2 -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 +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- 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 +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -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 +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- 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/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 +4 -4
- package/src/cli/core-commands.ts +3 -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 +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -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 +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- 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 +8 -8
- 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/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- 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-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-executor.ts +1 -1
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- 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 +256 -23
- 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 +6 -7
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -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 +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +4 -7
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -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 +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- 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 +520 -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/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 +8 -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 +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- 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 +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- 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 +5 -4
- 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 +82 -20
- 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 +2 -2
- 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 +272 -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/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
|
@@ -14,14 +14,24 @@ import { getConfig } from '../config/loader.js';
|
|
|
14
14
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
15
15
|
import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
|
|
16
16
|
import {
|
|
17
|
+
finalizeFollowup,
|
|
18
|
+
getExpiredDeliveryByConversation,
|
|
19
|
+
getFollowupDeliveryByConversation,
|
|
17
20
|
getGuardianActionRequest,
|
|
18
21
|
getPendingDeliveryByConversation,
|
|
22
|
+
progressFollowupState,
|
|
19
23
|
resolveGuardianActionRequest,
|
|
24
|
+
startFollowupFromExpiredRequest,
|
|
20
25
|
} from '../memory/guardian-action-store.js';
|
|
26
|
+
import { processGuardianFollowUpTurn } from '../runtime/guardian-action-conversation-turn.js';
|
|
27
|
+
import { executeFollowupAction } from '../runtime/guardian-action-followup-executor.js';
|
|
28
|
+
import { composeGuardianActionMessageGenerative } from '../runtime/guardian-action-message-composer.js';
|
|
29
|
+
import type { GuardianActionCopyGenerator, GuardianFollowUpConversationGenerator } from '../runtime/http-types.js';
|
|
21
30
|
import { extractPreferences } from '../notifications/preference-extractor.js';
|
|
22
31
|
import { createPreference } from '../notifications/preferences-store.js';
|
|
23
32
|
import type { Message } from '../providers/types.js';
|
|
24
33
|
import { getLogger } from '../util/logger.js';
|
|
34
|
+
import { resolveGuardianVerificationIntent } from './guardian-verification-intent.js';
|
|
25
35
|
import type { UsageStats } from './ipc-contract.js';
|
|
26
36
|
import type { ServerMessage, UserMessageAttachment } from './ipc-protocol.js';
|
|
27
37
|
import type { MessageQueue } from './session-queue-manager.js';
|
|
@@ -32,6 +42,25 @@ import type { TraceEmitter } from './trace-emitter.js';
|
|
|
32
42
|
|
|
33
43
|
const log = getLogger('session-process');
|
|
34
44
|
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Module-level generator injection
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// The daemon lifecycle creates the generator once and injects it here so the
|
|
49
|
+
// mac/IPC channel path can classify follow-up replies without threading the
|
|
50
|
+
// generator through Session / DaemonServer constructors.
|
|
51
|
+
let _guardianFollowUpGenerator: GuardianFollowUpConversationGenerator | undefined;
|
|
52
|
+
let _guardianActionCopyGenerator: GuardianActionCopyGenerator | undefined;
|
|
53
|
+
|
|
54
|
+
/** Inject the guardian follow-up conversation generator (called from lifecycle.ts). */
|
|
55
|
+
export function setGuardianFollowUpConversationGenerator(gen: GuardianFollowUpConversationGenerator): void {
|
|
56
|
+
_guardianFollowUpGenerator = gen;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Inject the guardian action copy generator (called from lifecycle.ts). */
|
|
60
|
+
export function setGuardianActionCopyGenerator(gen: GuardianActionCopyGenerator): void {
|
|
61
|
+
_guardianActionCopyGenerator = gen;
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
/** Build a model_info event with fresh config data. */
|
|
36
65
|
function buildModelInfoEvent(): ServerMessage {
|
|
37
66
|
const config = getConfig();
|
|
@@ -75,12 +104,12 @@ export interface ProcessSessionContext {
|
|
|
75
104
|
/** Assistant identity — used for scoping notification preferences. */
|
|
76
105
|
readonly assistantId?: string;
|
|
77
106
|
guardianContext?: GuardianRuntimeContext;
|
|
78
|
-
persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string, metadata?: Record<string, unknown
|
|
107
|
+
persistUserMessage(content: string, attachments: UserMessageAttachment[], requestId?: string, metadata?: Record<string, unknown>, displayContent?: string): Promise<string>;
|
|
79
108
|
runAgentLoop(
|
|
80
109
|
content: string,
|
|
81
110
|
userMessageId: string,
|
|
82
111
|
onEvent: (msg: ServerMessage) => void,
|
|
83
|
-
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean },
|
|
112
|
+
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; titleText?: string },
|
|
84
113
|
): Promise<void>;
|
|
85
114
|
getTurnChannelContext(): TurnChannelContext | null;
|
|
86
115
|
setTurnChannelContext(ctx: TurnChannelContext): void;
|
|
@@ -146,7 +175,7 @@ function buildSlashContext(session: ProcessSessionContext): SlashContext {
|
|
|
146
175
|
* block, we must explicitly continue draining on failure — otherwise
|
|
147
176
|
* remaining queued messages would be stranded.
|
|
148
177
|
*/
|
|
149
|
-
export function drainQueue(session: ProcessSessionContext, reason: QueueDrainReason = 'loop_complete'): void {
|
|
178
|
+
export async function drainQueue(session: ProcessSessionContext, reason: QueueDrainReason = 'loop_complete'): Promise<void> {
|
|
150
179
|
const next = session.queue.shift();
|
|
151
180
|
if (!next) return;
|
|
152
181
|
|
|
@@ -191,16 +220,22 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
|
|
|
191
220
|
: {}),
|
|
192
221
|
};
|
|
193
222
|
const userMsg = createUserMessage(next.content, next.attachments);
|
|
194
|
-
|
|
223
|
+
// When displayContent is provided (e.g. original text before recording
|
|
224
|
+
// intent stripping), persist that to DB so users see the full message.
|
|
225
|
+
// The in-memory userMessage (sent to the LLM) still uses the stripped content.
|
|
226
|
+
const contentToPersist = next.displayContent
|
|
227
|
+
? JSON.stringify(createUserMessage(next.displayContent, next.attachments).content)
|
|
228
|
+
: JSON.stringify(userMsg.content);
|
|
229
|
+
await conversationStore.addMessage(
|
|
195
230
|
session.conversationId,
|
|
196
231
|
'user',
|
|
197
|
-
|
|
232
|
+
contentToPersist,
|
|
198
233
|
drainChannelMeta,
|
|
199
234
|
);
|
|
200
235
|
session.messages.push(userMsg);
|
|
201
236
|
|
|
202
237
|
const assistantMsg = createAssistantMessage(slashResult.message);
|
|
203
|
-
conversationStore.addMessage(
|
|
238
|
+
await conversationStore.addMessage(
|
|
204
239
|
session.conversationId,
|
|
205
240
|
'assistant',
|
|
206
241
|
JSON.stringify(assistantMsg.content),
|
|
@@ -237,7 +272,7 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
|
|
|
237
272
|
next.onEvent({ type: 'error', message });
|
|
238
273
|
}
|
|
239
274
|
// Continue draining regardless of success/failure
|
|
240
|
-
drainQueue(session);
|
|
275
|
+
await drainQueue(session);
|
|
241
276
|
return;
|
|
242
277
|
}
|
|
243
278
|
|
|
@@ -248,13 +283,26 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
|
|
|
248
283
|
session.preactivatedSkillIds = [slashResult.skillId];
|
|
249
284
|
}
|
|
250
285
|
|
|
286
|
+
// Guardian verification intent interception for queued messages.
|
|
287
|
+
// Preserve the original user content for persistence; only the agent
|
|
288
|
+
// loop receives the rewritten instruction.
|
|
289
|
+
let agentLoopContent = resolvedContent;
|
|
290
|
+
if (slashResult.kind === 'passthrough') {
|
|
291
|
+
const guardianIntent = resolveGuardianVerificationIntent(resolvedContent);
|
|
292
|
+
if (guardianIntent.kind === 'direct_setup') {
|
|
293
|
+
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted in queue — forcing skill flow');
|
|
294
|
+
agentLoopContent = guardianIntent.rewrittenContent;
|
|
295
|
+
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
251
299
|
// Try to persist and run the dequeued message. If persistUserMessage
|
|
252
300
|
// succeeds, runAgentLoop is called and its finally block will drain
|
|
253
301
|
// the next message. If persistUserMessage fails, processMessage
|
|
254
302
|
// resolves early (no runAgentLoop call), so we must continue draining.
|
|
255
303
|
let userMessageId: string;
|
|
256
304
|
try {
|
|
257
|
-
userMessageId = session.persistUserMessage(resolvedContent, next.attachments, next.requestId, next.metadata);
|
|
305
|
+
userMessageId = await session.persistUserMessage(resolvedContent, next.attachments, next.requestId, next.metadata, next.displayContent);
|
|
258
306
|
} catch (err) {
|
|
259
307
|
const message = err instanceof Error ? err.message : String(err);
|
|
260
308
|
log.error({ err, conversationId: session.conversationId, requestId: next.requestId }, 'Failed to persist queued message');
|
|
@@ -267,7 +315,7 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
|
|
|
267
315
|
// runAgentLoop never ran, so its finally block won't clear this
|
|
268
316
|
session.preactivatedSkillIds = undefined;
|
|
269
317
|
// Continue draining — don't strand remaining messages
|
|
270
|
-
drainQueue(session);
|
|
318
|
+
await drainQueue(session);
|
|
271
319
|
return;
|
|
272
320
|
}
|
|
273
321
|
|
|
@@ -301,8 +349,12 @@ export function drainQueue(session: ProcessSessionContext, reason: QueueDrainRea
|
|
|
301
349
|
// Fire-and-forget: persistUserMessage set session.processing = true
|
|
302
350
|
// so subsequent messages will still be enqueued.
|
|
303
351
|
// runAgentLoop's finally block will call drainQueue when this run completes.
|
|
304
|
-
|
|
305
|
-
|
|
352
|
+
const drainLoopOptions: { isInteractive?: boolean; titleText?: string } = {};
|
|
353
|
+
if (next.isInteractive !== undefined) drainLoopOptions.isInteractive = next.isInteractive;
|
|
354
|
+
if (agentLoopContent !== resolvedContent) drainLoopOptions.titleText = resolvedContent;
|
|
355
|
+
|
|
356
|
+
session.runAgentLoop(agentLoopContent, userMessageId, next.onEvent,
|
|
357
|
+
Object.keys(drainLoopOptions).length > 0 ? drainLoopOptions : undefined,
|
|
306
358
|
).catch((err) => {
|
|
307
359
|
const message = err instanceof Error ? err.message : String(err);
|
|
308
360
|
log.error({ err, conversationId: session.conversationId, requestId: next.requestId }, 'Error processing queued message');
|
|
@@ -325,6 +377,7 @@ export async function processMessage(
|
|
|
325
377
|
activeSurfaceId?: string,
|
|
326
378
|
currentPage?: string,
|
|
327
379
|
options?: { isInteractive?: boolean },
|
|
380
|
+
displayContent?: string,
|
|
328
381
|
): Promise<string> {
|
|
329
382
|
session.currentActiveSurfaceId = activeSurfaceId;
|
|
330
383
|
session.currentPage = currentPage;
|
|
@@ -339,7 +392,7 @@ export async function processMessage(
|
|
|
339
392
|
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
340
393
|
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
341
394
|
const userMsg = createUserMessage(content, attachments);
|
|
342
|
-
const persisted = conversationStore.addMessage(
|
|
395
|
+
const persisted = await conversationStore.addMessage(
|
|
343
396
|
session.conversationId,
|
|
344
397
|
'user',
|
|
345
398
|
JSON.stringify(userMsg.content),
|
|
@@ -357,9 +410,9 @@ export async function processMessage(
|
|
|
357
410
|
const resolved = resolveGuardianActionRequest(guardianRequest.id, content, 'vellum');
|
|
358
411
|
const replyText = resolved
|
|
359
412
|
? 'Your answer has been relayed to the call.'
|
|
360
|
-
:
|
|
413
|
+
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_answered' }, {}, _guardianActionCopyGenerator);
|
|
361
414
|
const replyMsg = createAssistantMessage(replyText);
|
|
362
|
-
conversationStore.addMessage(
|
|
415
|
+
await conversationStore.addMessage(
|
|
363
416
|
session.conversationId,
|
|
364
417
|
'assistant',
|
|
365
418
|
JSON.stringify(replyMsg.content),
|
|
@@ -370,21 +423,176 @@ export async function processMessage(
|
|
|
370
423
|
} else {
|
|
371
424
|
const errorDetail = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
372
425
|
log.warn({ callSessionId: guardianRequest.callSessionId, error: errorDetail }, 'answerCall failed for mac guardian answer');
|
|
373
|
-
const
|
|
374
|
-
|
|
426
|
+
const failureText = await composeGuardianActionMessageGenerative(
|
|
427
|
+
{ scenario: 'guardian_answer_delivery_failed' },
|
|
428
|
+
{},
|
|
429
|
+
_guardianActionCopyGenerator,
|
|
430
|
+
);
|
|
431
|
+
const failMsg = createAssistantMessage(failureText);
|
|
432
|
+
await conversationStore.addMessage(
|
|
375
433
|
session.conversationId,
|
|
376
434
|
'assistant',
|
|
377
435
|
JSON.stringify(failMsg.content),
|
|
378
436
|
guardianChannelMeta,
|
|
379
437
|
);
|
|
380
438
|
session.messages.push(failMsg);
|
|
381
|
-
onEvent({ type: 'assistant_text_delta', text:
|
|
439
|
+
onEvent({ type: 'assistant_text_delta', text: failureText });
|
|
382
440
|
}
|
|
383
441
|
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
384
442
|
return persisted.id;
|
|
385
443
|
}
|
|
386
444
|
}
|
|
387
445
|
|
|
446
|
+
// ── Expired guardian action late answer interception (mac channel) ──
|
|
447
|
+
// If no pending delivery was found, check for expired requests eligible
|
|
448
|
+
// for follow-up (status='expired', followup_state='none').
|
|
449
|
+
const expiredDelivery = getExpiredDeliveryByConversation(session.conversationId);
|
|
450
|
+
if (expiredDelivery) {
|
|
451
|
+
const expiredRequest = getGuardianActionRequest(expiredDelivery.requestId);
|
|
452
|
+
if (expiredRequest && expiredRequest.status === 'expired' && expiredRequest.followupState === 'none') {
|
|
453
|
+
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
454
|
+
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
455
|
+
const userMsg = createUserMessage(content, attachments);
|
|
456
|
+
const persisted = await conversationStore.addMessage(
|
|
457
|
+
session.conversationId,
|
|
458
|
+
'user',
|
|
459
|
+
JSON.stringify(userMsg.content),
|
|
460
|
+
guardianChannelMeta,
|
|
461
|
+
);
|
|
462
|
+
session.messages.push(userMsg);
|
|
463
|
+
|
|
464
|
+
const followupResult = startFollowupFromExpiredRequest(expiredRequest.id, content);
|
|
465
|
+
if (followupResult) {
|
|
466
|
+
const followupText = await composeGuardianActionMessageGenerative(
|
|
467
|
+
{
|
|
468
|
+
scenario: 'guardian_late_answer_followup',
|
|
469
|
+
questionText: expiredRequest.questionText,
|
|
470
|
+
lateAnswerText: content,
|
|
471
|
+
},
|
|
472
|
+
{},
|
|
473
|
+
_guardianActionCopyGenerator,
|
|
474
|
+
);
|
|
475
|
+
const replyMsg = createAssistantMessage(followupText);
|
|
476
|
+
await conversationStore.addMessage(
|
|
477
|
+
session.conversationId,
|
|
478
|
+
'assistant',
|
|
479
|
+
JSON.stringify(replyMsg.content),
|
|
480
|
+
guardianChannelMeta,
|
|
481
|
+
);
|
|
482
|
+
session.messages.push(replyMsg);
|
|
483
|
+
onEvent({ type: 'assistant_text_delta', text: followupText });
|
|
484
|
+
} else {
|
|
485
|
+
// Follow-up already started or conflict — send stale message
|
|
486
|
+
const staleText = await composeGuardianActionMessageGenerative(
|
|
487
|
+
{ scenario: 'guardian_stale_expired' },
|
|
488
|
+
{},
|
|
489
|
+
_guardianActionCopyGenerator,
|
|
490
|
+
);
|
|
491
|
+
const staleMsg = createAssistantMessage(staleText);
|
|
492
|
+
await conversationStore.addMessage(
|
|
493
|
+
session.conversationId,
|
|
494
|
+
'assistant',
|
|
495
|
+
JSON.stringify(staleMsg.content),
|
|
496
|
+
guardianChannelMeta,
|
|
497
|
+
);
|
|
498
|
+
session.messages.push(staleMsg);
|
|
499
|
+
onEvent({ type: 'assistant_text_delta', text: staleText });
|
|
500
|
+
}
|
|
501
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
502
|
+
return persisted.id;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── Guardian follow-up conversation interception (mac channel) ──
|
|
507
|
+
// When a request is in `awaiting_guardian_choice` state, the guardian has
|
|
508
|
+
// already been asked "call back or send a message?". Their next message
|
|
509
|
+
// is the reply — route it through the conversation engine.
|
|
510
|
+
const followupDelivery = getFollowupDeliveryByConversation(session.conversationId);
|
|
511
|
+
if (followupDelivery) {
|
|
512
|
+
const followupRequest = getGuardianActionRequest(followupDelivery.requestId);
|
|
513
|
+
if (followupRequest && followupRequest.followupState === 'awaiting_guardian_choice') {
|
|
514
|
+
const guardianIfCtx = session.getTurnInterfaceContext();
|
|
515
|
+
const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
|
|
516
|
+
const userMsg = createUserMessage(content, attachments);
|
|
517
|
+
const persisted = await conversationStore.addMessage(
|
|
518
|
+
session.conversationId,
|
|
519
|
+
'user',
|
|
520
|
+
JSON.stringify(userMsg.content),
|
|
521
|
+
guardianChannelMeta,
|
|
522
|
+
);
|
|
523
|
+
session.messages.push(userMsg);
|
|
524
|
+
|
|
525
|
+
const turnResult = await processGuardianFollowUpTurn(
|
|
526
|
+
{
|
|
527
|
+
questionText: followupRequest.questionText,
|
|
528
|
+
lateAnswerText: followupRequest.lateAnswerText ?? '',
|
|
529
|
+
guardianReply: content,
|
|
530
|
+
},
|
|
531
|
+
_guardianFollowUpGenerator,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Apply the disposition to the follow-up state machine.
|
|
535
|
+
// Both progressFollowupState and finalizeFollowup are compare-and-set:
|
|
536
|
+
// they return null when the transition was not applied (e.g. a concurrent
|
|
537
|
+
// reply already advanced the state). In that case we notify the guardian
|
|
538
|
+
// that the request was already resolved and skip action execution.
|
|
539
|
+
let stateApplied = true;
|
|
540
|
+
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
541
|
+
stateApplied = progressFollowupState(followupRequest.id, 'dispatching', turnResult.disposition) !== null;
|
|
542
|
+
} else if (turnResult.disposition === 'decline') {
|
|
543
|
+
stateApplied = finalizeFollowup(followupRequest.id, 'declined') !== null;
|
|
544
|
+
}
|
|
545
|
+
// keep_pending: no state change — guardian can reply again
|
|
546
|
+
|
|
547
|
+
if (!stateApplied) {
|
|
548
|
+
log.warn({ requestId: followupRequest.id, disposition: turnResult.disposition }, 'Follow-up state transition failed (already resolved)');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const replyText = stateApplied
|
|
552
|
+
? turnResult.replyText
|
|
553
|
+
: await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_followup' }, {}, _guardianActionCopyGenerator);
|
|
554
|
+
const replyMsg = createAssistantMessage(replyText);
|
|
555
|
+
await conversationStore.addMessage(
|
|
556
|
+
session.conversationId,
|
|
557
|
+
'assistant',
|
|
558
|
+
JSON.stringify(replyMsg.content),
|
|
559
|
+
guardianChannelMeta,
|
|
560
|
+
);
|
|
561
|
+
session.messages.push(replyMsg);
|
|
562
|
+
onEvent({ type: 'assistant_text_delta', text: replyText });
|
|
563
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
564
|
+
|
|
565
|
+
// Execute the action and send a completion/failure message (fire-and-forget).
|
|
566
|
+
// The initial reply above acknowledges the guardian's choice; the executor
|
|
567
|
+
// carries out the actual call_back or message_back and posts a second message.
|
|
568
|
+
if (stateApplied && (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back')) {
|
|
569
|
+
void (async () => {
|
|
570
|
+
try {
|
|
571
|
+
const execResult = await executeFollowupAction(
|
|
572
|
+
followupRequest.id,
|
|
573
|
+
turnResult.disposition as 'call_back' | 'message_back',
|
|
574
|
+
_guardianActionCopyGenerator,
|
|
575
|
+
);
|
|
576
|
+
const completionMsg = createAssistantMessage(execResult.guardianReplyText);
|
|
577
|
+
await conversationStore.addMessage(
|
|
578
|
+
session.conversationId,
|
|
579
|
+
'assistant',
|
|
580
|
+
JSON.stringify(completionMsg.content),
|
|
581
|
+
guardianChannelMeta,
|
|
582
|
+
);
|
|
583
|
+
session.messages.push(completionMsg);
|
|
584
|
+
onEvent({ type: 'assistant_text_delta', text: execResult.guardianReplyText });
|
|
585
|
+
onEvent({ type: 'message_complete', sessionId: session.conversationId });
|
|
586
|
+
} catch (execErr) {
|
|
587
|
+
log.error({ err: execErr, requestId: followupRequest.id }, 'Follow-up action execution or completion message failed');
|
|
588
|
+
}
|
|
589
|
+
})();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return persisted.id;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
388
596
|
// Resolve slash commands before persistence
|
|
389
597
|
const slashResult = resolveSlash(content, buildSlashContext(session));
|
|
390
598
|
|
|
@@ -405,16 +613,22 @@ export async function processMessage(
|
|
|
405
613
|
: {}),
|
|
406
614
|
};
|
|
407
615
|
const userMsg = createUserMessage(content, attachments);
|
|
408
|
-
|
|
616
|
+
// When displayContent is provided (e.g. original text before recording
|
|
617
|
+
// intent stripping), persist that to DB so users see the full message.
|
|
618
|
+
// The in-memory userMessage (sent to the LLM) still uses the stripped content.
|
|
619
|
+
const contentToPersist = displayContent
|
|
620
|
+
? JSON.stringify(createUserMessage(displayContent, attachments).content)
|
|
621
|
+
: JSON.stringify(userMsg.content);
|
|
622
|
+
const persisted = await conversationStore.addMessage(
|
|
409
623
|
session.conversationId,
|
|
410
624
|
'user',
|
|
411
|
-
|
|
625
|
+
contentToPersist,
|
|
412
626
|
pmChannelMeta,
|
|
413
627
|
);
|
|
414
628
|
session.messages.push(userMsg);
|
|
415
629
|
|
|
416
630
|
const assistantMsg = createAssistantMessage(slashResult.message);
|
|
417
|
-
conversationStore.addMessage(
|
|
631
|
+
await conversationStore.addMessage(
|
|
418
632
|
session.conversationId,
|
|
419
633
|
'assistant',
|
|
420
634
|
JSON.stringify(assistantMsg.content),
|
|
@@ -450,9 +664,24 @@ export async function processMessage(
|
|
|
450
664
|
session.preactivatedSkillIds = [slashResult.skillId];
|
|
451
665
|
}
|
|
452
666
|
|
|
667
|
+
// Guardian verification intent interception — force direct guardian
|
|
668
|
+
// verification requests into the guardian-verify-setup skill flow on
|
|
669
|
+
// the first turn, avoiding conceptual preambles from the agent.
|
|
670
|
+
// We keep the original user content for persistence and use the
|
|
671
|
+
// rewritten content only for the agent loop instruction.
|
|
672
|
+
let agentLoopContent = resolvedContent;
|
|
673
|
+
if (slashResult.kind === 'passthrough') {
|
|
674
|
+
const guardianIntent = resolveGuardianVerificationIntent(resolvedContent);
|
|
675
|
+
if (guardianIntent.kind === 'direct_setup') {
|
|
676
|
+
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted — forcing skill flow');
|
|
677
|
+
agentLoopContent = guardianIntent.rewrittenContent;
|
|
678
|
+
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
453
682
|
let userMessageId: string;
|
|
454
683
|
try {
|
|
455
|
-
userMessageId = session.persistUserMessage(resolvedContent, attachments, requestId);
|
|
684
|
+
userMessageId = await session.persistUserMessage(resolvedContent, attachments, requestId, undefined, displayContent);
|
|
456
685
|
} catch (err) {
|
|
457
686
|
const message = err instanceof Error ? err.message : String(err);
|
|
458
687
|
onEvent({ type: 'error', message });
|
|
@@ -485,8 +714,12 @@ export async function processMessage(
|
|
|
485
714
|
});
|
|
486
715
|
}
|
|
487
716
|
|
|
488
|
-
|
|
489
|
-
|
|
717
|
+
const loopOptions: { isInteractive?: boolean; titleText?: string } = {};
|
|
718
|
+
if (options?.isInteractive !== undefined) loopOptions.isInteractive = options.isInteractive;
|
|
719
|
+
if (agentLoopContent !== resolvedContent) loopOptions.titleText = resolvedContent;
|
|
720
|
+
|
|
721
|
+
await session.runAgentLoop(agentLoopContent, userMessageId, onEvent,
|
|
722
|
+
Object.keys(loopOptions).length > 0 ? loopOptions : undefined,
|
|
490
723
|
);
|
|
491
724
|
return userMessageId;
|
|
492
725
|
}
|
|
@@ -25,6 +25,8 @@ export interface QueuedMessage {
|
|
|
25
25
|
isInteractive?: boolean;
|
|
26
26
|
/** Timestamp (ms) when the message was enqueued. */
|
|
27
27
|
queuedAt: number;
|
|
28
|
+
/** Original user message text to persist to DB when recording intent stripping produced a different `content`. */
|
|
29
|
+
displayContent?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export const MAX_QUEUE_DEPTH = 10;
|
|
@@ -25,6 +25,10 @@ export interface ChannelCapabilities {
|
|
|
25
25
|
supportsDynamicUi: boolean;
|
|
26
26
|
/** Whether the channel supports voice/microphone input. */
|
|
27
27
|
supportsVoiceInput: boolean;
|
|
28
|
+
/** Push-to-talk activation key (e.g. 'fn', 'ctrl', 'fn_shift', 'none'). Only present on desktop clients. */
|
|
29
|
+
pttActivationKey?: string;
|
|
30
|
+
/** Whether the client has been granted microphone permission by the OS. */
|
|
31
|
+
microphonePermissionGranted?: boolean;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/** Guardian identity/trust context for external chat channels. */
|
|
@@ -39,10 +43,26 @@ export interface GuardianRuntimeContext {
|
|
|
39
43
|
denialReason?: 'no_binding' | 'no_identity';
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/** Allowed push-to-talk activation key values. Used to validate client-provided keys before system-prompt injection. */
|
|
47
|
+
const PTT_KEY_ALLOWLIST = new Set(['fn', 'ctrl', 'fn_shift', 'none']);
|
|
48
|
+
|
|
49
|
+
/** Validate a PTT activation key against the allowlist. Returns the key if valid, 'unknown' otherwise. */
|
|
50
|
+
export function sanitizePttActivationKey(key: string | undefined | null): string | undefined {
|
|
51
|
+
if (key == null) return undefined;
|
|
52
|
+
return PTT_KEY_ALLOWLIST.has(key) ? key : 'unknown';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Optional PTT metadata provided by the client alongside each message. */
|
|
56
|
+
export interface PttMetadata {
|
|
57
|
+
pttActivationKey?: string;
|
|
58
|
+
microphonePermissionGranted?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
/** Derive channel capabilities from source channel + interface identifiers. */
|
|
43
62
|
export function resolveChannelCapabilities(
|
|
44
63
|
sourceChannel?: string | null,
|
|
45
64
|
sourceInterface?: string | null,
|
|
65
|
+
pttMetadata?: PttMetadata | null,
|
|
46
66
|
): ChannelCapabilities {
|
|
47
67
|
// Normalise legacy pseudo-channel IDs to canonical ChannelId values.
|
|
48
68
|
let channel: string;
|
|
@@ -85,6 +105,8 @@ export function resolveChannelCapabilities(
|
|
|
85
105
|
dashboardCapable: supportsDesktopUi,
|
|
86
106
|
supportsDynamicUi: supportsDesktopUi,
|
|
87
107
|
supportsVoiceInput: supportsDesktopUi,
|
|
108
|
+
pttActivationKey: sanitizePttActivationKey(pttMetadata?.pttActivationKey),
|
|
109
|
+
microphonePermissionGranted: pttMetadata?.microphonePermissionGranted,
|
|
88
110
|
};
|
|
89
111
|
}
|
|
90
112
|
case 'telegram':
|
|
@@ -334,6 +356,23 @@ export function injectChannelCapabilityContext(message: Message, caps: ChannelCa
|
|
|
334
356
|
lines.push('- Do NOT ask the user to use voice or microphone input.');
|
|
335
357
|
}
|
|
336
358
|
|
|
359
|
+
// PTT state — only relevant on channels that support voice input
|
|
360
|
+
if (caps.supportsVoiceInput) {
|
|
361
|
+
if (caps.pttActivationKey && caps.pttActivationKey !== 'none') {
|
|
362
|
+
const keyLabel = caps.pttActivationKey === 'fn_shift' ? 'Fn+Shift' : caps.pttActivationKey === 'fn' ? 'Fn (Globe)' : caps.pttActivationKey;
|
|
363
|
+
lines.push(`ptt_activation_key: ${caps.pttActivationKey}`);
|
|
364
|
+
lines.push(`ptt_enabled: true`);
|
|
365
|
+
lines.push(`Push-to-talk is configured with the ${keyLabel} key. The user can hold ${keyLabel} to dictate text or start a voice conversation.`);
|
|
366
|
+
} else if (caps.pttActivationKey === 'none') {
|
|
367
|
+
lines.push(`ptt_activation_key: none`);
|
|
368
|
+
lines.push(`ptt_enabled: false`);
|
|
369
|
+
lines.push('Push-to-talk is disabled. You can offer to enable it for the user.');
|
|
370
|
+
}
|
|
371
|
+
if (caps.microphonePermissionGranted !== undefined) {
|
|
372
|
+
lines.push(`microphone_permission_granted: ${caps.microphonePermissionGranted}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
337
376
|
lines.push('</channel_capabilities>');
|
|
338
377
|
|
|
339
378
|
const block = lines.join('\n');
|
|
@@ -282,17 +282,18 @@ export function projectSkillTools(
|
|
|
282
282
|
);
|
|
283
283
|
|
|
284
284
|
if (tools.length > 0) {
|
|
285
|
+
let accepted = tools;
|
|
285
286
|
const prevHash = prevActive.get(skillId);
|
|
286
287
|
if (prevHash === undefined) {
|
|
287
288
|
// Newly active skill — register for the first time
|
|
288
|
-
registerSkillTools(tools);
|
|
289
|
+
accepted = registerSkillTools(tools);
|
|
289
290
|
} else if (prevHash !== currentHash) {
|
|
290
291
|
// Hash changed — unregister stale tools, then re-register with new definitions
|
|
291
292
|
log.info({ skillId, prevHash, currentHash }, 'Skill version changed, re-registering tools');
|
|
292
293
|
unregisterSkillTools(skillId);
|
|
293
294
|
alreadyUnregistered.add(skillId);
|
|
294
295
|
try {
|
|
295
|
-
registerSkillTools(tools);
|
|
296
|
+
accepted = registerSkillTools(tools);
|
|
296
297
|
} catch (err) {
|
|
297
298
|
log.error({ err, skillId }, 'Failed to re-register skill tools after version change');
|
|
298
299
|
// Don't add to successfulEntries — will be cleaned up as transiently-failed
|
|
@@ -306,12 +307,20 @@ export function projectSkillTools(
|
|
|
306
307
|
if (existing && existing.ownerSkillBundled !== (skill.bundled ?? undefined)) {
|
|
307
308
|
log.info({ skillId, bundled: skill.bundled }, 'Skill bundled status changed, re-registering tools');
|
|
308
309
|
unregisterSkillTools(skillId);
|
|
309
|
-
registerSkillTools(tools);
|
|
310
|
+
accepted = registerSkillTools(tools);
|
|
311
|
+
} else {
|
|
312
|
+
// Filter to only tools that are actually registered for this skill.
|
|
313
|
+
// Some tools may have been skipped during initial registration due
|
|
314
|
+
// to core-name collisions — don't let them leak back in.
|
|
315
|
+
accepted = tools.filter((t) => {
|
|
316
|
+
const reg = getTool(t.name);
|
|
317
|
+
return reg !== undefined && reg.origin === 'skill' && reg.ownerSkillId === skillId;
|
|
318
|
+
});
|
|
310
319
|
}
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
successfulEntries.set(skillId, currentHash);
|
|
314
|
-
for (const tool of
|
|
323
|
+
for (const tool of accepted) {
|
|
315
324
|
allToolDefinitions.push(tool.getDefinition());
|
|
316
325
|
allToolNames.add(tool.name);
|
|
317
326
|
}
|
|
@@ -25,8 +25,8 @@ import { requestComputerControlTool } from '../tools/computer-use/request-comput
|
|
|
25
25
|
import type { ProxyApprovalCallback, ProxyApprovalRequest } from '../tools/network/script-proxy/index.js';
|
|
26
26
|
import { getAllToolDefinitions } from '../tools/registry.js';
|
|
27
27
|
import { allUiSurfaceTools } from '../tools/ui-surface/definitions.js';
|
|
28
|
-
import { projectSkillTools, type SkillProjectionCache } from './session-skill-tools.js';
|
|
29
28
|
import type { GuardianRuntimeContext } from './session-runtime-assembly.js';
|
|
29
|
+
import { projectSkillTools, type SkillProjectionCache } from './session-skill-tools.js';
|
|
30
30
|
import type { SurfaceSessionContext } from './session-surfaces.js';
|
|
31
31
|
import {
|
|
32
32
|
surfaceProxyResolver,
|
|
@@ -117,12 +117,11 @@ export function createToolExecutor(
|
|
|
117
117
|
forcePromptSideEffects: ctx.memoryPolicy.strictSideEffects,
|
|
118
118
|
onToolLifecycleEvent: handleToolLifecycleEvent,
|
|
119
119
|
sendToClient: (msg) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const s = serverMsg as unknown as UiSurfaceShow;
|
|
120
|
+
// Tool context's sendToClient uses a loose { type: string; [key: string]: unknown }
|
|
121
|
+
// signature, but at runtime these are always ServerMessage instances.
|
|
122
|
+
ctx.sendToClient(msg as ServerMessage);
|
|
123
|
+
if (msg.type === 'ui_surface_show') {
|
|
124
|
+
const s = msg as unknown as UiSurfaceShow;
|
|
126
125
|
ctx.currentTurnSurfaces.push({
|
|
127
126
|
surfaceId: s.surfaceId,
|
|
128
127
|
surfaceType: s.surfaceType,
|
package/src/daemon/session.ts
CHANGED
|
@@ -397,8 +397,9 @@ export class Session {
|
|
|
397
397
|
currentPage?: string,
|
|
398
398
|
metadata?: Record<string, unknown>,
|
|
399
399
|
options?: { isInteractive?: boolean },
|
|
400
|
+
displayContent?: string,
|
|
400
401
|
): { queued: boolean; rejected?: boolean; requestId: string } {
|
|
401
|
-
return enqueueMessageImpl(this, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, metadata, options);
|
|
402
|
+
return enqueueMessageImpl(this, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, metadata, options, displayContent);
|
|
402
403
|
}
|
|
403
404
|
|
|
404
405
|
getQueueDepth(): number {
|
|
@@ -425,6 +426,14 @@ export class Session {
|
|
|
425
426
|
return this.prompter.hasPendingRequest(requestId);
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
hasAnyPendingConfirmation(): boolean {
|
|
430
|
+
return this.prompter.hasPending;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
denyAllPendingConfirmations(): void {
|
|
434
|
+
this.prompter.denyAllPending();
|
|
435
|
+
}
|
|
436
|
+
|
|
428
437
|
hasPendingSecret(requestId: string): boolean {
|
|
429
438
|
return this.secretPrompter.hasPendingRequest(requestId);
|
|
430
439
|
}
|
|
@@ -489,13 +498,14 @@ export class Session {
|
|
|
489
498
|
return this.currentTurnInterfaceContext;
|
|
490
499
|
}
|
|
491
500
|
|
|
492
|
-
persistUserMessage(
|
|
501
|
+
async persistUserMessage(
|
|
493
502
|
content: string,
|
|
494
503
|
attachments: UserMessageAttachment[],
|
|
495
504
|
requestId?: string,
|
|
496
505
|
metadata?: Record<string, unknown>,
|
|
497
|
-
|
|
498
|
-
|
|
506
|
+
displayContent?: string,
|
|
507
|
+
): Promise<string> {
|
|
508
|
+
return persistUserMessageImpl(this, content, attachments, requestId, metadata, displayContent);
|
|
499
509
|
}
|
|
500
510
|
|
|
501
511
|
// ── Agent Loop ───────────────────────────────────────────────────
|
|
@@ -504,14 +514,14 @@ export class Session {
|
|
|
504
514
|
content: string,
|
|
505
515
|
userMessageId: string,
|
|
506
516
|
onEvent: (msg: ServerMessage) => void,
|
|
507
|
-
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean },
|
|
517
|
+
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; titleText?: string },
|
|
508
518
|
): Promise<void> {
|
|
509
519
|
return runAgentLoopImpl(this, content, userMessageId, onEvent, options);
|
|
510
520
|
}
|
|
511
521
|
|
|
512
522
|
|
|
513
|
-
drainQueue(reason: QueueDrainReason = 'loop_complete'): void {
|
|
514
|
-
drainQueueImpl(this as ProcessSessionContext, reason);
|
|
523
|
+
drainQueue(reason: QueueDrainReason = 'loop_complete'): Promise<void> {
|
|
524
|
+
return drainQueueImpl(this as ProcessSessionContext, reason);
|
|
515
525
|
}
|
|
516
526
|
|
|
517
527
|
async processMessage(
|
|
@@ -522,8 +532,9 @@ export class Session {
|
|
|
522
532
|
activeSurfaceId?: string,
|
|
523
533
|
currentPage?: string,
|
|
524
534
|
options?: { isInteractive?: boolean },
|
|
535
|
+
displayContent?: string,
|
|
525
536
|
): Promise<string> {
|
|
526
|
-
return processMessageImpl(this as ProcessSessionContext, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, options);
|
|
537
|
+
return processMessageImpl(this as ProcessSessionContext, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, options, displayContent);
|
|
527
538
|
}
|
|
528
539
|
|
|
529
540
|
// ── History ──────────────────────────────────────────────────────
|