@vellumai/assistant 0.3.28 → 0.4.1
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 +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +25 -21
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/send-endpoint-busy.test.ts +288 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +50 -12
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/response-tier.ts +6 -5
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +166 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/reminder/reminder-store.ts +10 -14
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -145,7 +145,7 @@ const MemoryFreshnessConfigSchema = z.object({
|
|
|
145
145
|
.number({ error: 'memory.retrieval.freshness.maxAgeDays.opinion must be a number' })
|
|
146
146
|
.nonnegative('memory.retrieval.freshness.maxAgeDays.opinion must be non-negative')
|
|
147
147
|
.default(60),
|
|
148
|
-
}).default({
|
|
148
|
+
}).default({ fact: 0, preference: 0, behavior: 90, event: 30, opinion: 60 }),
|
|
149
149
|
staleDecay: z
|
|
150
150
|
.number({ error: 'memory.retrieval.freshness.staleDecay must be a number' })
|
|
151
151
|
.min(0, 'memory.retrieval.freshness.staleDecay must be >= 0')
|
|
@@ -181,15 +181,15 @@ export const MemoryRetrievalConfigSchema = z.object({
|
|
|
181
181
|
error: 'memory.retrieval.injectionStrategy must be "prepend_user_block" or "separate_context_message"',
|
|
182
182
|
})
|
|
183
183
|
.default('prepend_user_block'),
|
|
184
|
-
reranking: MemoryRerankingConfigSchema.default({}
|
|
185
|
-
freshness: MemoryFreshnessConfigSchema.default({}
|
|
184
|
+
reranking: MemoryRerankingConfigSchema.default(MemoryRerankingConfigSchema.parse({})),
|
|
185
|
+
freshness: MemoryFreshnessConfigSchema.default(MemoryFreshnessConfigSchema.parse({})),
|
|
186
186
|
scopePolicy: z
|
|
187
187
|
.enum(['allow_global_fallback', 'strict'], {
|
|
188
188
|
error: 'memory.retrieval.scopePolicy must be "allow_global_fallback" or "strict"',
|
|
189
189
|
})
|
|
190
190
|
.default('allow_global_fallback'),
|
|
191
|
-
dynamicBudget: MemoryDynamicBudgetConfigSchema.default({}
|
|
192
|
-
earlyTermination: MemoryEarlyTerminationConfigSchema.default({}
|
|
191
|
+
dynamicBudget: MemoryDynamicBudgetConfigSchema.default(MemoryDynamicBudgetConfigSchema.parse({})),
|
|
192
|
+
earlyTermination: MemoryEarlyTerminationConfigSchema.default(MemoryEarlyTerminationConfigSchema.parse({})),
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
export const MemorySegmentationConfigSchema = z.object({
|
|
@@ -287,7 +287,7 @@ export const MemoryEntityConfigSchema = z.object({
|
|
|
287
287
|
.int('memory.entity.extractRelations.backfillBatchSize must be an integer')
|
|
288
288
|
.positive('memory.entity.extractRelations.backfillBatchSize must be a positive integer')
|
|
289
289
|
.default(200),
|
|
290
|
-
}).default({
|
|
290
|
+
}).default({ enabled: true, backfillBatchSize: 200 }),
|
|
291
291
|
relationRetrieval: z.object({
|
|
292
292
|
enabled: z
|
|
293
293
|
.boolean({ error: 'memory.entity.relationRetrieval.enabled must be a boolean' })
|
|
@@ -320,7 +320,15 @@ export const MemoryEntityConfigSchema = z.object({
|
|
|
320
320
|
depthDecay: z
|
|
321
321
|
.boolean({ error: 'memory.entity.relationRetrieval.depthDecay must be a boolean' })
|
|
322
322
|
.default(true),
|
|
323
|
-
}).default({
|
|
323
|
+
}).default({
|
|
324
|
+
enabled: true,
|
|
325
|
+
maxSeedEntities: 8,
|
|
326
|
+
maxNeighborEntities: 20,
|
|
327
|
+
maxEdges: 40,
|
|
328
|
+
neighborScoreMultiplier: 0.7,
|
|
329
|
+
maxDepth: 3,
|
|
330
|
+
depthDecay: true,
|
|
331
|
+
}),
|
|
324
332
|
});
|
|
325
333
|
|
|
326
334
|
export const MemoryConflictsConfigSchema = z.object({
|
|
@@ -384,18 +392,18 @@ export const MemoryConfigSchema = z.object({
|
|
|
384
392
|
enabled: z
|
|
385
393
|
.boolean({ error: 'memory.enabled must be a boolean' })
|
|
386
394
|
.default(true),
|
|
387
|
-
embeddings: MemoryEmbeddingsConfigSchema.default({}
|
|
388
|
-
qdrant: QdrantConfigSchema.default({}
|
|
389
|
-
retrieval: MemoryRetrievalConfigSchema.default({}
|
|
390
|
-
segmentation: MemorySegmentationConfigSchema.default({}
|
|
391
|
-
jobs: MemoryJobsConfigSchema.default({}
|
|
392
|
-
retention: MemoryRetentionConfigSchema.default({}
|
|
393
|
-
cleanup: MemoryCleanupConfigSchema.default({}
|
|
394
|
-
extraction: MemoryExtractionConfigSchema.default({}
|
|
395
|
-
summarization: MemorySummarizationConfigSchema.default({}
|
|
396
|
-
entity: MemoryEntityConfigSchema.default({}
|
|
397
|
-
conflicts: MemoryConflictsConfigSchema.default({}
|
|
398
|
-
profile: MemoryProfileConfigSchema.default({}
|
|
395
|
+
embeddings: MemoryEmbeddingsConfigSchema.default(MemoryEmbeddingsConfigSchema.parse({})),
|
|
396
|
+
qdrant: QdrantConfigSchema.default(QdrantConfigSchema.parse({})),
|
|
397
|
+
retrieval: MemoryRetrievalConfigSchema.default(MemoryRetrievalConfigSchema.parse({})),
|
|
398
|
+
segmentation: MemorySegmentationConfigSchema.default(MemorySegmentationConfigSchema.parse({})),
|
|
399
|
+
jobs: MemoryJobsConfigSchema.default(MemoryJobsConfigSchema.parse({})),
|
|
400
|
+
retention: MemoryRetentionConfigSchema.default(MemoryRetentionConfigSchema.parse({})),
|
|
401
|
+
cleanup: MemoryCleanupConfigSchema.default(MemoryCleanupConfigSchema.parse({})),
|
|
402
|
+
extraction: MemoryExtractionConfigSchema.default(MemoryExtractionConfigSchema.parse({})),
|
|
403
|
+
summarization: MemorySummarizationConfigSchema.default(MemorySummarizationConfigSchema.parse({})),
|
|
404
|
+
entity: MemoryEntityConfigSchema.default(MemoryEntityConfigSchema.parse({})),
|
|
405
|
+
conflicts: MemoryConflictsConfigSchema.default(MemoryConflictsConfigSchema.parse({})),
|
|
406
|
+
profile: MemoryProfileConfigSchema.default(MemoryProfileConfigSchema.parse({})),
|
|
399
407
|
});
|
|
400
408
|
|
|
401
409
|
export type MemoryEmbeddingsConfig = z.infer<typeof MemoryEmbeddingsConfigSchema>;
|
package/src/config/schema.ts
CHANGED
|
@@ -206,34 +206,34 @@ export const AssistantConfigSchema = z.object({
|
|
|
206
206
|
.int('maxToolUseTurns must be an integer')
|
|
207
207
|
.positive('maxToolUseTurns must be a positive integer')
|
|
208
208
|
.default(60),
|
|
209
|
-
thinking: ThinkingConfigSchema.default({}
|
|
210
|
-
contextWindow: ContextWindowConfigSchema.default({}
|
|
211
|
-
memory: MemoryConfigSchema.default({}
|
|
209
|
+
thinking: ThinkingConfigSchema.default(ThinkingConfigSchema.parse({})),
|
|
210
|
+
contextWindow: ContextWindowConfigSchema.default(ContextWindowConfigSchema.parse({})),
|
|
211
|
+
memory: MemoryConfigSchema.default(MemoryConfigSchema.parse({})),
|
|
212
212
|
dataDir: z
|
|
213
213
|
.string({ error: 'dataDir must be a string' })
|
|
214
214
|
.default(getDataDir()),
|
|
215
|
-
timeouts: TimeoutConfigSchema.default({}
|
|
216
|
-
sandbox: SandboxConfigSchema.default({}
|
|
217
|
-
rateLimit: RateLimitConfigSchema.default({}
|
|
218
|
-
secretDetection: SecretDetectionConfigSchema.default({}
|
|
219
|
-
permissions: PermissionsConfigSchema.default({}
|
|
220
|
-
auditLog: AuditLogConfigSchema.default({}
|
|
221
|
-
logFile: LogFileConfigSchema.default({}
|
|
215
|
+
timeouts: TimeoutConfigSchema.default(TimeoutConfigSchema.parse({})),
|
|
216
|
+
sandbox: SandboxConfigSchema.default(SandboxConfigSchema.parse({})),
|
|
217
|
+
rateLimit: RateLimitConfigSchema.default(RateLimitConfigSchema.parse({})),
|
|
218
|
+
secretDetection: SecretDetectionConfigSchema.default(SecretDetectionConfigSchema.parse({})),
|
|
219
|
+
permissions: PermissionsConfigSchema.default(PermissionsConfigSchema.parse({})),
|
|
220
|
+
auditLog: AuditLogConfigSchema.default(AuditLogConfigSchema.parse({})),
|
|
221
|
+
logFile: LogFileConfigSchema.default(LogFileConfigSchema.parse({})),
|
|
222
222
|
pricingOverrides: z
|
|
223
223
|
.array(ModelPricingOverrideSchema)
|
|
224
224
|
.default([]),
|
|
225
|
-
heartbeat: HeartbeatConfigSchema.default({}
|
|
226
|
-
swarm: SwarmConfigSchema.default({}
|
|
227
|
-
mcp: McpConfigSchema.default({}
|
|
228
|
-
skills: SkillsConfigSchema.default({}
|
|
229
|
-
workspaceGit: WorkspaceGitConfigSchema.default({}
|
|
230
|
-
calls: CallsConfigSchema.default({}
|
|
231
|
-
sms: SmsConfigSchema.default({}
|
|
225
|
+
heartbeat: HeartbeatConfigSchema.default(HeartbeatConfigSchema.parse({})),
|
|
226
|
+
swarm: SwarmConfigSchema.default(SwarmConfigSchema.parse({})),
|
|
227
|
+
mcp: McpConfigSchema.default(McpConfigSchema.parse({})),
|
|
228
|
+
skills: SkillsConfigSchema.default(SkillsConfigSchema.parse({})),
|
|
229
|
+
workspaceGit: WorkspaceGitConfigSchema.default(WorkspaceGitConfigSchema.parse({})),
|
|
230
|
+
calls: CallsConfigSchema.default(CallsConfigSchema.parse({})),
|
|
231
|
+
sms: SmsConfigSchema.default(SmsConfigSchema.parse({})),
|
|
232
232
|
ingress: IngressConfigSchema,
|
|
233
|
-
platform: PlatformConfigSchema.default({}
|
|
234
|
-
daemon: DaemonConfigSchema.default({}
|
|
235
|
-
notifications: NotificationsConfigSchema.default({}
|
|
236
|
-
ui: UiConfigSchema.default({}
|
|
233
|
+
platform: PlatformConfigSchema.default(PlatformConfigSchema.parse({})),
|
|
234
|
+
daemon: DaemonConfigSchema.default(DaemonConfigSchema.parse({})),
|
|
235
|
+
notifications: NotificationsConfigSchema.default(NotificationsConfigSchema.parse({})),
|
|
236
|
+
ui: UiConfigSchema.default(UiConfigSchema.parse({})),
|
|
237
237
|
featureFlags: z
|
|
238
238
|
.record(z.string(), z.boolean({ error: 'featureFlags values must be booleans' }))
|
|
239
239
|
.default({} as any),
|
|
@@ -24,8 +24,8 @@ export const RemoteProviderConfigSchema = z.object({
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
export const RemoteProvidersConfigSchema = z.object({
|
|
27
|
-
skillssh: RemoteProviderConfigSchema.default({}
|
|
28
|
-
clawhub: RemoteProviderConfigSchema.default({}
|
|
27
|
+
skillssh: RemoteProviderConfigSchema.default(RemoteProviderConfigSchema.parse({})),
|
|
28
|
+
clawhub: RemoteProviderConfigSchema.default(RemoteProviderConfigSchema.parse({})),
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
// 'unknown' is valid as a risk label on a skill but not as a threshold — setting the threshold
|
|
@@ -41,12 +41,12 @@ export const RemotePolicyConfigSchema = z.object({
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
export const SkillsConfigSchema = z.object({
|
|
44
|
-
entries: z.record(z.string(), SkillEntryConfigSchema).default({} as
|
|
45
|
-
load: SkillsLoadConfigSchema.default({}
|
|
46
|
-
install: SkillsInstallConfigSchema.default({}
|
|
44
|
+
entries: z.record(z.string(), SkillEntryConfigSchema).default({} as Record<string, never>),
|
|
45
|
+
load: SkillsLoadConfigSchema.default(SkillsLoadConfigSchema.parse({})),
|
|
46
|
+
install: SkillsInstallConfigSchema.default(SkillsInstallConfigSchema.parse({})),
|
|
47
47
|
allowBundled: z.array(z.string()).nullable().default(null),
|
|
48
|
-
remoteProviders: RemoteProvidersConfigSchema.default({}
|
|
49
|
-
remotePolicy: RemotePolicyConfigSchema.default({}
|
|
48
|
+
remoteProviders: RemoteProvidersConfigSchema.default(RemoteProvidersConfigSchema.parse({})),
|
|
49
|
+
remotePolicy: RemotePolicyConfigSchema.default(RemotePolicyConfigSchema.parse({})),
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
export type SkillEntryConfig = z.infer<typeof SkillEntryConfigSchema>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Trusted Contacts"
|
|
3
|
-
description: "Manage trusted contacts and
|
|
3
|
+
description: "Manage trusted contacts and invite links — list, allow, revoke, block users, and create/list/revoke invite links for Telegram and voice (phone call) channels"
|
|
4
4
|
user-invocable: true
|
|
5
5
|
metadata: {"vellum": {"emoji": "\ud83d\udc65"}}
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are helping your user manage trusted contacts and invite links for the Vellum Assistant. Trusted contacts control who is allowed to send messages to the assistant through external channels like Telegram and
|
|
8
|
+
You are helping your user manage trusted contacts and invite links for the Vellum Assistant. Trusted contacts control who is allowed to send messages to the assistant through external channels like Telegram, SMS, and voice (phone calls). Invite links let the guardian share a Telegram deep link that automatically grants access when opened. Voice invites let the guardian authorize a specific phone number to call in — the invitee must call from that phone number AND enter a one-time numeric code. All operations go through the gateway HTTP API using `curl` with bearer auth.
|
|
9
9
|
|
|
10
10
|
## Prerequisites
|
|
11
11
|
|
|
@@ -18,8 +18,9 @@ You are helping your user manage trusted contacts and invite links for the Vellu
|
|
|
18
18
|
- **Member**: A user identity (external user ID or chat ID) from a specific channel that has been registered with a policy.
|
|
19
19
|
- **Policy**: Controls what the member can do — `allow` (can message freely) or `deny` (blocked from messaging).
|
|
20
20
|
- **Status**: The member's lifecycle state — `active` (currently effective), `revoked` (access removed), or `blocked` (explicitly denied).
|
|
21
|
-
- **Source channel**: The messaging platform the contact uses (e.g., `telegram`, `sms`).
|
|
21
|
+
- **Source channel**: The messaging platform the contact uses (e.g., `telegram`, `sms`, `voice`).
|
|
22
22
|
- **Invite link**: A shareable Telegram deep link that, when opened by someone, automatically grants them trusted-contact access. Each invite has a token, usage limits, and optional expiration.
|
|
23
|
+
- **Voice invite**: An invite bound to a specific phone number for phone-call access. The guardian provides the invitee's phone number (E.164 format, e.g., `+15551234567`), and the system generates a numeric code. The invitee must call from that exact phone number AND enter the code when prompted. Both conditions must be met — the call must originate from the bound number, and the correct code must be entered. Voice invites do not have a Telegram-style deep link and do not use `/start` payload tokens. SMS-based invites are not supported.
|
|
23
24
|
|
|
24
25
|
## Available Actions
|
|
25
26
|
|
|
@@ -128,14 +129,6 @@ Use this when the guardian wants to invite someone to message the assistant on T
|
|
|
128
129
|
```bash
|
|
129
130
|
TOKEN=$(cat ~/.vellum/http-token)
|
|
130
131
|
|
|
131
|
-
BOT_CONFIG_JSON=$(curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/telegram/config" \
|
|
132
|
-
-H "Authorization: Bearer $TOKEN")
|
|
133
|
-
BOT_USERNAME=$(printf '%s' "$BOT_CONFIG_JSON" | tr -d '\n' | sed -n 's/.*"botUsername"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
134
|
-
if [ -z "$BOT_USERNAME" ]; then
|
|
135
|
-
echo "error:no_bot_username"
|
|
136
|
-
exit 1
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
132
|
INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
|
|
140
133
|
-H "Content-Type: application/json" \
|
|
141
134
|
-H "Authorization: Bearer $TOKEN" \
|
|
@@ -144,14 +137,40 @@ INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
|
|
|
144
137
|
"maxUses": 1,
|
|
145
138
|
"note": "<optional note, e.g. the person it is for>"
|
|
146
139
|
}')
|
|
147
|
-
|
|
140
|
+
|
|
141
|
+
INVITE_TOKEN=$(printf '%s' "$INVITE_JSON" | python3 -c "
|
|
142
|
+
import json, sys
|
|
143
|
+
data = json.load(sys.stdin)
|
|
144
|
+
invite = data.get('invite', {})
|
|
145
|
+
print(invite.get('token', ''), end='')
|
|
146
|
+
")
|
|
147
|
+
INVITE_URL=$(printf '%s' "$INVITE_JSON" | python3 -c "
|
|
148
|
+
import json, sys
|
|
149
|
+
data = json.load(sys.stdin)
|
|
150
|
+
invite = data.get('invite', {})
|
|
151
|
+
share = invite.get('share') or {}
|
|
152
|
+
print(share.get('url', ''), end='')
|
|
153
|
+
")
|
|
154
|
+
|
|
148
155
|
if [ -z "$INVITE_TOKEN" ]; then
|
|
149
156
|
printf '%s\n' "$INVITE_JSON"
|
|
150
157
|
exit 1
|
|
151
158
|
fi
|
|
152
159
|
|
|
160
|
+
# Prefer backend-provided canonical link when available.
|
|
161
|
+
if [ -z "$INVITE_URL" ]; then
|
|
162
|
+
BOT_CONFIG_JSON=$(curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/telegram/config" \
|
|
163
|
+
-H "Authorization: Bearer $TOKEN")
|
|
164
|
+
BOT_USERNAME=$(printf '%s' "$BOT_CONFIG_JSON" | tr -d '\n' | sed -n 's/.*"botUsername"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
165
|
+
if [ -z "$BOT_USERNAME" ]; then
|
|
166
|
+
echo "error:no_share_url_or_bot_username"
|
|
167
|
+
exit 1
|
|
168
|
+
fi
|
|
169
|
+
INVITE_URL="https://t.me/$BOT_USERNAME?start=iv_$INVITE_TOKEN"
|
|
170
|
+
fi
|
|
171
|
+
|
|
153
172
|
echo "<vellum-sensitive-output kind=\"invite_code\" value=\"$INVITE_TOKEN\" />"
|
|
154
|
-
echo "
|
|
173
|
+
echo "$INVITE_URL"
|
|
155
174
|
```
|
|
156
175
|
|
|
157
176
|
Optional fields:
|
|
@@ -159,7 +178,11 @@ Optional fields:
|
|
|
159
178
|
- `expiresInMs` — expiration time in milliseconds from now (e.g., `86400000` for 24 hours). Defaults to 7 days (`604800000`) if omitted.
|
|
160
179
|
- `note` — a human-readable label for the invite (e.g., "For Mom", "Family group").
|
|
161
180
|
|
|
162
|
-
The create response contains `{ ok: true, invite: { id, token, ... } }`.
|
|
181
|
+
The create response contains `{ ok: true, invite: { id, token, share?, ... } }`.
|
|
182
|
+
- `token` is the raw invite token and is only returned at creation time.
|
|
183
|
+
- `share.url` is the canonical shareable deep link (when channel transport config is available).
|
|
184
|
+
|
|
185
|
+
Always use `invite.share.url` when present. Do not manually construct `?start=` links if the API already provided one.
|
|
163
186
|
|
|
164
187
|
**Presenting to the guardian**: Give the guardian the link with clear copy-paste instructions:
|
|
165
188
|
|
|
@@ -216,9 +239,99 @@ curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites/<invite_id>" \
|
|
|
216
239
|
|
|
217
240
|
Replace `<invite_id>` with the invite's `id` from the list response.
|
|
218
241
|
|
|
242
|
+
### 8. Create a voice invite
|
|
243
|
+
|
|
244
|
+
Use this when the guardian wants to authorize a specific phone number to call the assistant. Voice invites are identity-bound: the invitee must call from the specified phone number AND enter a one-time numeric code.
|
|
245
|
+
|
|
246
|
+
**Important**: The response includes a `voiceCode` field that is only returned at creation time and cannot be retrieved later. Extract and present it clearly.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
250
|
+
|
|
251
|
+
INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
|
|
252
|
+
-H "Content-Type: application/json" \
|
|
253
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
254
|
+
-d '{
|
|
255
|
+
"sourceChannel": "voice",
|
|
256
|
+
"expectedExternalUserId": "<phone_number_E164>",
|
|
257
|
+
"maxUses": 1,
|
|
258
|
+
"note": "<optional note, e.g. the person it is for>"
|
|
259
|
+
}')
|
|
260
|
+
printf '%s\n' "$INVITE_JSON"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Required fields:
|
|
264
|
+
- `sourceChannel` — must be `"voice"`
|
|
265
|
+
- `expectedExternalUserId` — the invitee's phone number in E.164 format (e.g., `+15551234567`)
|
|
266
|
+
|
|
267
|
+
Optional fields:
|
|
268
|
+
- `maxUses` — how many times the code can be used (default: 1)
|
|
269
|
+
- `expiresInMs` — expiration time in milliseconds from now (e.g., `86400000` for 24 hours). Defaults to 7 days if omitted.
|
|
270
|
+
- ~~`voiceCodeDigits`~~ — always 6 digits; this parameter is accepted but ignored
|
|
271
|
+
- `note` — a human-readable label for the invite (e.g., "For Mom", "Dr. Smith")
|
|
272
|
+
|
|
273
|
+
The create response contains `{ ok: true, invite: { id, voiceCode, expectedExternalUserId, ... } }`.
|
|
274
|
+
- `voiceCode` is the numeric code the invitee must enter and is only returned at creation time.
|
|
275
|
+
- Voice invite responses do **not** include `token` or `share.url`. Do not try to build or send a deep link for voice invites.
|
|
276
|
+
|
|
277
|
+
**Presenting to the guardian**: Give the guardian clear instructions to relay to the invitee:
|
|
278
|
+
|
|
279
|
+
> Voice invite created for **<phone_number>**:
|
|
280
|
+
>
|
|
281
|
+
> **Invite code: `<voiceCode>`**
|
|
282
|
+
>
|
|
283
|
+
> Share these instructions with the person you are inviting:
|
|
284
|
+
> 1. Call the assistant's phone number from **<phone_number>** (the call must come from this exact number)
|
|
285
|
+
> 2. When prompted, enter the code **<voiceCode>**
|
|
286
|
+
> 3. Once verified, they will be added as a trusted contact and can call the assistant directly in the future
|
|
287
|
+
>
|
|
288
|
+
> This code can be used <maxUses> time(s)<and expires in X hours/days if applicable>.
|
|
289
|
+
|
|
290
|
+
There is no "open link" step for voice invites. The invite is redeemed only during a live phone call from the bound number.
|
|
291
|
+
|
|
292
|
+
If the user provides a phone number without the `+` country code prefix, ask them to confirm the full E.164 number (e.g., US numbers should be `+1XXXXXXXXXX`).
|
|
293
|
+
|
|
294
|
+
**Note**: SMS-based invites are not currently supported. Only voice (phone call) invites are available for phone-based access.
|
|
295
|
+
|
|
296
|
+
### 9. List voice invites
|
|
297
|
+
|
|
298
|
+
Use this to show the guardian their active voice invites.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
302
|
+
curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites?sourceChannel=voice" \
|
|
303
|
+
-H "Authorization: Bearer $TOKEN"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Optional query parameters:
|
|
307
|
+
- `status` — filter by status (`active`, `revoked`, `redeemed`, `expired`)
|
|
308
|
+
|
|
309
|
+
The response format is the same as regular invites but voice invites also include:
|
|
310
|
+
- `expectedExternalUserId` — the bound phone number
|
|
311
|
+
- `voiceCodeDigits` — always 6 (the code itself is not retrievable after creation)
|
|
312
|
+
- `token` and `share` are not present for voice invites
|
|
313
|
+
|
|
314
|
+
**Presenting results**: Format as a readable list. Show the note (or "unnamed" as fallback), bound phone number, status, uses remaining, and expiration. Highlight which invites are still active.
|
|
315
|
+
|
|
316
|
+
### 10. Revoke a voice invite
|
|
317
|
+
|
|
318
|
+
Use this when the guardian wants to cancel an active voice invite. **Always confirm before revoking.**
|
|
319
|
+
|
|
320
|
+
Ask the user: *"I'll revoke the voice invite for [phone number or note]. The code will no longer work. Should I proceed?"*
|
|
321
|
+
|
|
322
|
+
First, list voice invites to find the invite's `id`, then revoke:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
326
|
+
curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites/<invite_id>" \
|
|
327
|
+
-H "Authorization: Bearer $TOKEN"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Replace `<invite_id>` with the invite's `id` from the list response. The same revoke endpoint is used for both Telegram and voice invites.
|
|
331
|
+
|
|
219
332
|
## Confirmation Requirements
|
|
220
333
|
|
|
221
|
-
**All mutating actions (allow, revoke, block, revoke invite) require explicit user confirmation before execution.** This is a safety measure — modifying who can access the assistant should always be a deliberate choice. Creating an invite link does not require confirmation since it does not grant access until
|
|
334
|
+
**All mutating actions (allow, revoke, block, revoke invite) require explicit user confirmation before execution.** This is a safety measure — modifying who can access the assistant should always be a deliberate choice. Creating an invite (Telegram link or voice invite) does not require confirmation since it does not grant access until the invitee redeems it.
|
|
222
335
|
|
|
223
336
|
- Clearly state what action you are about to take and who it affects.
|
|
224
337
|
- Wait for the user to confirm before running the curl command.
|
|
@@ -232,7 +345,9 @@ Replace `<invite_id>` with the invite's `id` from the list response.
|
|
|
232
345
|
- `At least one of externalUserId or externalChatId is required` — ask the user for the contact's channel-specific identifier.
|
|
233
346
|
- `Member not found or cannot be revoked` — the member ID may be invalid or the member is already revoked.
|
|
234
347
|
- `Member not found or already blocked` — the member ID may be invalid or the member is already blocked.
|
|
235
|
-
- `sourceChannel is required for create` — when creating an invite, always pass `"sourceChannel": "telegram"
|
|
348
|
+
- `sourceChannel is required for create` — when creating an invite, always pass `"sourceChannel": "telegram"` for Telegram or `"sourceChannel": "voice"` for voice invites.
|
|
349
|
+
- `expectedExternalUserId is required for voice invites` — voice invites must include the invitee's phone number.
|
|
350
|
+
- `expectedExternalUserId must be in E.164 format` — the phone number must start with `+` followed by country code and number (e.g., `+15551234567`).
|
|
236
351
|
- `Invite not found or already revoked` — the invite ID may be invalid or the invite is already revoked.
|
|
237
352
|
|
|
238
353
|
## Typical Workflows
|
|
@@ -252,3 +367,11 @@ Replace `<invite_id>` with the invite's `id` from the list response.
|
|
|
252
367
|
**"Show my invites"** / **"List active invite links"** — List invites filtered by `sourceChannel=telegram`, present active invites with uses remaining and expiration info.
|
|
253
368
|
|
|
254
369
|
**"Revoke invite"** / **"Cancel invite link"** — List invites to identify the target, confirm, then revoke by ID.
|
|
370
|
+
|
|
371
|
+
**"Create a voice invite for +15551234567"** — Create a voice invite with `sourceChannel: "voice"` and the given phone number as `expectedExternalUserId`. Present the invite code and instructions: the person must call from that number and enter the code.
|
|
372
|
+
|
|
373
|
+
**"Let my mom call in"** / **"Invite someone by phone"** — Ask for the phone number in E.164 format, create a voice invite, and present the code + calling instructions.
|
|
374
|
+
|
|
375
|
+
**"Show my voice invites"** / **"List phone invites"** — List invites filtered by `sourceChannel=voice`, present active invites with bound phone number and expiration info.
|
|
376
|
+
|
|
377
|
+
**"Revoke voice invite"** / **"Cancel the phone invite for +15551234567"** — List voice invites, identify the target by phone number or note, confirm, then revoke by ID.
|
|
@@ -303,9 +303,9 @@ async function executeApprove(
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
session.setAssistantId(assistantId);
|
|
306
|
-
// The guardian approved this escalation, so tag as guardian to avoid
|
|
307
|
-
//
|
|
308
|
-
session.setGuardianContext({
|
|
306
|
+
// The guardian approved this escalation, so tag as guardian trust to avoid
|
|
307
|
+
// unknown-provenance memory gating.
|
|
308
|
+
session.setGuardianContext({ trustClass: 'guardian', sourceChannel: sourceChannel ?? 'vellum' });
|
|
309
309
|
session.setCommandIntent(null);
|
|
310
310
|
|
|
311
311
|
// Process the message through the agent loop (no IPC event callback
|
|
@@ -379,7 +379,7 @@ async function executeDeny(
|
|
|
379
379
|
// Store a system note about the denial in the conversation
|
|
380
380
|
const denialInterface = isInterfaceId(sourceChannel) ? sourceChannel : undefined;
|
|
381
381
|
await addMessage(conversationId, 'assistant', denialText, {
|
|
382
|
-
|
|
382
|
+
provenanceTrustClass: 'guardian' as const,
|
|
383
383
|
userMessageChannel: sourceChannel,
|
|
384
384
|
assistantMessageChannel: sourceChannel,
|
|
385
385
|
...(denialInterface ? { userMessageInterface: denialInterface, assistantMessageInterface: denialInterface } : {}),
|
|
@@ -2,18 +2,26 @@ import * as net from 'node:net';
|
|
|
2
2
|
|
|
3
3
|
import { v4 as uuid } from 'uuid';
|
|
4
4
|
|
|
5
|
+
import { createAssistantMessage, createUserMessage } from '../../agent/message-types.js';
|
|
5
6
|
import { type InterfaceId,isChannelId, parseChannelId, parseInterfaceId } from '../../channels/types.js';
|
|
6
7
|
import { getConfig } from '../../config/loader.js';
|
|
7
8
|
import { getAttachmentsForMessage, getFilePathForAttachment, setAttachmentThumbnail } from '../../memory/attachments-store.js';
|
|
9
|
+
import {
|
|
10
|
+
listCanonicalGuardianRequests,
|
|
11
|
+
listPendingCanonicalGuardianRequestsByDestinationConversation,
|
|
12
|
+
} from '../../memory/canonical-guardian-store.js';
|
|
8
13
|
import { getAttentionStateByConversationIds } from '../../memory/conversation-attention-store.js';
|
|
9
14
|
import * as conversationStore from '../../memory/conversation-store.js';
|
|
10
15
|
import { GENERATING_TITLE, queueGenerateConversationTitle, UNTITLED_FALLBACK } from '../../memory/conversation-title-service.js';
|
|
11
16
|
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
17
|
+
import { routeGuardianReply } from '../../runtime/guardian-reply-router.js';
|
|
18
|
+
import * as pendingInteractions from '../../runtime/pending-interactions.js';
|
|
12
19
|
import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
13
20
|
import { compileCustomPatterns, redactSecrets } from '../../security/secret-scanner.js';
|
|
14
21
|
import { getSubagentManager } from '../../subagent/index.js';
|
|
15
22
|
import { silentlyWithLog } from '../../util/silently.js';
|
|
16
23
|
import { truncate } from '../../util/truncate.js';
|
|
24
|
+
import { createApprovalConversationGenerator } from '../approval-generators.js';
|
|
17
25
|
import { getAssistantName } from '../identity-helpers.js';
|
|
18
26
|
import type { UserMessageAttachment } from '../ipc-contract.js';
|
|
19
27
|
import type {
|
|
@@ -56,6 +64,8 @@ import {
|
|
|
56
64
|
wireEscalationHandler,
|
|
57
65
|
} from './shared.js';
|
|
58
66
|
|
|
67
|
+
const desktopApprovalConversationGenerator = createApprovalConversationGenerator();
|
|
68
|
+
|
|
59
69
|
export async function handleUserMessage(
|
|
60
70
|
msg: UserMessage,
|
|
61
71
|
socket: net.Socket,
|
|
@@ -172,9 +182,9 @@ export async function handleUserMessage(
|
|
|
172
182
|
assistantMessageInterface: ipcInterface,
|
|
173
183
|
});
|
|
174
184
|
session.setAssistantId('self');
|
|
175
|
-
// IPC/desktop user IS the guardian — default to guardian
|
|
176
|
-
// are not tagged
|
|
177
|
-
session.setGuardianContext({
|
|
185
|
+
// IPC/desktop user IS the guardian — default to guardian trust so
|
|
186
|
+
// messages are not tagged as unknown provenance.
|
|
187
|
+
session.setGuardianContext({ trustClass: 'guardian', sourceChannel: ipcChannel });
|
|
178
188
|
session.setCommandIntent(null);
|
|
179
189
|
// Fire-and-forget: don't block the IPC handler so the connection can
|
|
180
190
|
// continue receiving messages (e.g. cancel, confirmations, or
|
|
@@ -451,12 +461,146 @@ export async function handleUserMessage(
|
|
|
451
461
|
}
|
|
452
462
|
}
|
|
453
463
|
|
|
464
|
+
// If exactly one live turn is waiting on confirmation (no queued turns),
|
|
465
|
+
// try to consume this text as an inline approval decision first.
|
|
466
|
+
if (
|
|
467
|
+
session.hasAnyPendingConfirmation()
|
|
468
|
+
&& session.getQueueDepth() === 0
|
|
469
|
+
&& messageText.trim().length > 0
|
|
470
|
+
) {
|
|
471
|
+
try {
|
|
472
|
+
const pendingInteractionRequestIdsForConversation = pendingInteractions
|
|
473
|
+
.getByConversation(msg.sessionId)
|
|
474
|
+
.filter(
|
|
475
|
+
(interaction) =>
|
|
476
|
+
interaction.kind === 'confirmation'
|
|
477
|
+
&& interaction.session === session
|
|
478
|
+
&& session.hasPendingConfirmation(interaction.requestId),
|
|
479
|
+
)
|
|
480
|
+
.map((interaction) => interaction.requestId);
|
|
481
|
+
|
|
482
|
+
const pendingCanonicalRequestIdsForConversation = [
|
|
483
|
+
...listPendingCanonicalGuardianRequestsByDestinationConversation(msg.sessionId, ipcChannel)
|
|
484
|
+
.filter((request) => request.kind === 'tool_approval')
|
|
485
|
+
.map((request) => request.id),
|
|
486
|
+
...listCanonicalGuardianRequests({
|
|
487
|
+
status: 'pending',
|
|
488
|
+
conversationId: msg.sessionId,
|
|
489
|
+
kind: 'tool_approval',
|
|
490
|
+
}).map((request) => request.id),
|
|
491
|
+
].filter((pendingRequestId) => session.hasPendingConfirmation(pendingRequestId));
|
|
492
|
+
|
|
493
|
+
const pendingRequestIdsForConversation = Array.from(new Set([
|
|
494
|
+
...pendingInteractionRequestIdsForConversation,
|
|
495
|
+
...pendingCanonicalRequestIdsForConversation,
|
|
496
|
+
]));
|
|
497
|
+
|
|
498
|
+
if (pendingRequestIdsForConversation.length > 0) {
|
|
499
|
+
const routerResult = await routeGuardianReply({
|
|
500
|
+
messageText: messageText.trim(),
|
|
501
|
+
channel: ipcChannel,
|
|
502
|
+
actor: {
|
|
503
|
+
externalUserId: undefined,
|
|
504
|
+
channel: ipcChannel,
|
|
505
|
+
isTrusted: true,
|
|
506
|
+
},
|
|
507
|
+
conversationId: msg.sessionId,
|
|
508
|
+
pendingRequestIds: pendingRequestIdsForConversation,
|
|
509
|
+
approvalConversationGenerator: desktopApprovalConversationGenerator,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (routerResult.consumed && routerResult.type !== 'nl_keep_pending') {
|
|
513
|
+
const consumedChannelMeta = {
|
|
514
|
+
userMessageChannel: ipcChannel,
|
|
515
|
+
assistantMessageChannel: ipcChannel,
|
|
516
|
+
userMessageInterface: ipcInterface,
|
|
517
|
+
assistantMessageInterface: ipcInterface,
|
|
518
|
+
provenanceActorRole: 'guardian' as const,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const consumedUserMessage = createUserMessage(messageText, msg.attachments ?? []);
|
|
522
|
+
await conversationStore.addMessage(
|
|
523
|
+
msg.sessionId,
|
|
524
|
+
'user',
|
|
525
|
+
JSON.stringify(consumedUserMessage.content),
|
|
526
|
+
consumedChannelMeta,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const replyText = (routerResult.replyText?.trim())
|
|
530
|
+
|| (routerResult.decisionApplied ? 'Decision applied.' : 'Request already resolved.');
|
|
531
|
+
const consumedAssistantMessage = createAssistantMessage(replyText);
|
|
532
|
+
await conversationStore.addMessage(
|
|
533
|
+
msg.sessionId,
|
|
534
|
+
'assistant',
|
|
535
|
+
JSON.stringify(consumedAssistantMessage.content),
|
|
536
|
+
consumedChannelMeta,
|
|
537
|
+
);
|
|
538
|
+
// Avoid mutating in-memory history while an agent loop is active;
|
|
539
|
+
// the loop owns history reconstruction for the in-flight turn.
|
|
540
|
+
if (!session.isProcessing()) {
|
|
541
|
+
// Keep in-memory history aligned with persisted transcript so
|
|
542
|
+
// session-history operations (undo/regenerate) target the same turn.
|
|
543
|
+
session.messages.push(consumedUserMessage, consumedAssistantMessage);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Mirror the normal queued/dequeued lifecycle so desktop clients can
|
|
547
|
+
// reconcile queued bubble state for this just-sent user message.
|
|
548
|
+
ctx.send(socket, {
|
|
549
|
+
type: 'message_queued',
|
|
550
|
+
sessionId: msg.sessionId,
|
|
551
|
+
requestId,
|
|
552
|
+
position: 0,
|
|
553
|
+
});
|
|
554
|
+
ctx.send(socket, {
|
|
555
|
+
type: 'message_dequeued',
|
|
556
|
+
sessionId: msg.sessionId,
|
|
557
|
+
requestId,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Only emit the reply delta when no agent turn is in-flight.
|
|
561
|
+
// When the agent is active, currentAssistantMessageId on the client
|
|
562
|
+
// points to the agent's streaming message and this delta would
|
|
563
|
+
// contaminate it. The reply is already persisted to the DB, so the
|
|
564
|
+
// client will see it on the next transcript reload / session switch.
|
|
565
|
+
if (!session.isProcessing()) {
|
|
566
|
+
ctx.send(socket, {
|
|
567
|
+
type: 'assistant_text_delta',
|
|
568
|
+
text: replyText,
|
|
569
|
+
sessionId: msg.sessionId,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
ctx.send(socket, {
|
|
573
|
+
type: 'message_request_complete',
|
|
574
|
+
sessionId: msg.sessionId,
|
|
575
|
+
requestId,
|
|
576
|
+
runStillActive: session.isProcessing(),
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
rlog.info(
|
|
580
|
+
{ routerType: routerResult.type, decisionApplied: routerResult.decisionApplied, routerRequestId: routerResult.requestId },
|
|
581
|
+
'Consumed pending-confirmation reply before auto-deny',
|
|
582
|
+
);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch (err) {
|
|
587
|
+
rlog.warn({ err }, 'Failed to process pending-confirmation reply; falling back to auto-deny behavior');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
454
591
|
// If the session has a pending tool confirmation, auto-deny it so the
|
|
455
|
-
// agent can process the user's follow-up message instead.
|
|
592
|
+
// agent can process the user's follow-up message instead. The agent
|
|
456
593
|
// will see the denial and can re-request the tool if still needed.
|
|
457
594
|
if (session.hasAnyPendingConfirmation()) {
|
|
458
595
|
rlog.info('Auto-denying pending confirmation(s) due to new user message');
|
|
459
596
|
session.denyAllPendingConfirmations();
|
|
597
|
+
// Keep the pending-interaction tracker aligned with the prompter so
|
|
598
|
+
// stale request IDs are not reused as routing candidates.
|
|
599
|
+
for (const interaction of pendingInteractions.getByConversation(msg.sessionId)) {
|
|
600
|
+
if (interaction.session === session && interaction.kind === 'confirmation') {
|
|
601
|
+
pendingInteractions.resolve(interaction.requestId);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
460
604
|
}
|
|
461
605
|
|
|
462
606
|
dispatchUserMessage(
|
|
@@ -173,6 +173,21 @@ export interface MessageDequeued {
|
|
|
173
173
|
requestId: string;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Request-level terminal signal for a user message lifecycle.
|
|
178
|
+
*
|
|
179
|
+
* Unlike `message_complete`, this does not imply the active assistant turn
|
|
180
|
+
* has completed. It is used for paths that consume a request inline while a
|
|
181
|
+
* separate in-flight turn may still be running.
|
|
182
|
+
*/
|
|
183
|
+
export interface MessageRequestComplete {
|
|
184
|
+
type: 'message_request_complete';
|
|
185
|
+
sessionId: string;
|
|
186
|
+
requestId: string;
|
|
187
|
+
/** True when an existing turn is still running after this request is finalized. */
|
|
188
|
+
runStillActive?: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
176
191
|
export interface MessageQueuedDeleted {
|
|
177
192
|
type: 'message_queued_deleted';
|
|
178
193
|
sessionId: string;
|
|
@@ -241,6 +256,7 @@ export type _MessagesServerMessages =
|
|
|
241
256
|
| SecretDetected
|
|
242
257
|
| MessageQueued
|
|
243
258
|
| MessageDequeued
|
|
259
|
+
| MessageRequestComplete
|
|
244
260
|
| MessageQueuedDeleted
|
|
245
261
|
| SuggestionResponse
|
|
246
262
|
| TraceEvent;
|