vellum 0.2.1 → 0.2.2
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/README.md +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +299 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +62 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +163 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +28 -4
- package/src/daemon/ipc-contract.ts +133 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +108 -20
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +5 -10
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -10,7 +10,6 @@ import { resolve } from 'node:path';
|
|
|
10
10
|
import { timingSafeEqual } from 'node:crypto';
|
|
11
11
|
import { ConfigError, IngressBlockedError } from '../util/errors.js';
|
|
12
12
|
import { getLogger } from '../util/logger.js';
|
|
13
|
-
import { getSecureKey } from '../security/secure-keys.js';
|
|
14
13
|
import { TwilioConversationRelayProvider } from '../calls/twilio-provider.js';
|
|
15
14
|
import type { RunOrchestrator } from './run-orchestrator.js';
|
|
16
15
|
|
|
@@ -47,11 +46,16 @@ import {
|
|
|
47
46
|
handleDeleteSharedApp,
|
|
48
47
|
} from './routes/app-routes.js';
|
|
49
48
|
import { handleAddSecret } from './routes/secret-routes.js';
|
|
49
|
+
import {
|
|
50
|
+
handleStartCall,
|
|
51
|
+
handleGetCallStatus,
|
|
52
|
+
handleCancelCall,
|
|
53
|
+
handleAnswerCall,
|
|
54
|
+
} from './routes/call-routes.js';
|
|
50
55
|
import {
|
|
51
56
|
handleVoiceWebhook,
|
|
52
57
|
handleStatusCallback,
|
|
53
58
|
handleConnectAction,
|
|
54
|
-
handleCallAnswer,
|
|
55
59
|
} from '../calls/twilio-routes.js';
|
|
56
60
|
import { RelayConnection, activeRelayConnections } from '../calls/relay-server.js';
|
|
57
61
|
import type { RelayWebSocketData } from '../calls/relay-server.js';
|
|
@@ -113,25 +117,50 @@ function getDiskSpaceInfo(): DiskSpaceInfo | null {
|
|
|
113
117
|
*/
|
|
114
118
|
const TWILIO_WEBHOOK_RE = /^\/v1\/(?:assistants\/[^/]+\/)?calls\/twilio\/(.+)$/;
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Gateway-compatible Twilio webhook paths:
|
|
122
|
+
* /webhooks/twilio/<subpath>
|
|
123
|
+
*
|
|
124
|
+
* Maps gateway path segments to the internal subpath names used by the
|
|
125
|
+
* dispatcher below (e.g. "voice" -> "voice-webhook").
|
|
126
|
+
*/
|
|
127
|
+
const TWILIO_GATEWAY_WEBHOOK_RE = /^\/webhooks\/twilio\/(.+)$/;
|
|
128
|
+
const GATEWAY_SUBPATH_MAP: Record<string, string> = {
|
|
129
|
+
voice: 'voice-webhook',
|
|
130
|
+
status: 'status',
|
|
131
|
+
'connect-action': 'connect-action',
|
|
132
|
+
};
|
|
133
|
+
|
|
116
134
|
/**
|
|
117
135
|
* Validate a Twilio webhook request's X-Twilio-Signature header.
|
|
118
136
|
*
|
|
119
137
|
* Returns the raw body text on success so callers can reconstruct the Request
|
|
120
138
|
* for downstream handlers (which also need to read the body).
|
|
121
139
|
* Returns a 403 Response if signature validation fails.
|
|
122
|
-
*
|
|
140
|
+
*
|
|
141
|
+
* Fail-closed: if the auth token is not configured, the request is rejected
|
|
142
|
+
* with 403 rather than silently skipping validation. An explicit local-dev
|
|
143
|
+
* bypass is available via TWILIO_WEBHOOK_VALIDATION_DISABLED=true.
|
|
123
144
|
*/
|
|
124
145
|
async function validateTwilioWebhook(
|
|
125
146
|
req: Request,
|
|
126
147
|
): Promise<{ body: string } | Response> {
|
|
127
148
|
const rawBody = await req.text();
|
|
128
|
-
const authToken = getSecureKey('twilio_auth_token');
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
// Allow explicit local-dev bypass — must be exactly "true"
|
|
151
|
+
if (process.env.TWILIO_WEBHOOK_VALIDATION_DISABLED === 'true') {
|
|
152
|
+
log.warn('Twilio webhook signature validation explicitly disabled via TWILIO_WEBHOOK_VALIDATION_DISABLED');
|
|
132
153
|
return { body: rawBody };
|
|
133
154
|
}
|
|
134
155
|
|
|
156
|
+
const authToken = TwilioConversationRelayProvider.getAuthToken();
|
|
157
|
+
|
|
158
|
+
// Fail-closed: reject if no auth token is configured
|
|
159
|
+
if (!authToken) {
|
|
160
|
+
log.error('Twilio auth token not configured — rejecting webhook request (fail-closed)');
|
|
161
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
162
|
+
}
|
|
163
|
+
|
|
135
164
|
const signature = req.headers.get('x-twilio-signature');
|
|
136
165
|
if (!signature) {
|
|
137
166
|
log.warn('Twilio webhook request missing X-Twilio-Signature header');
|
|
@@ -159,6 +188,7 @@ async function validateTwilioWebhook(
|
|
|
159
188
|
publicUrl,
|
|
160
189
|
params,
|
|
161
190
|
signature,
|
|
191
|
+
authToken,
|
|
162
192
|
);
|
|
163
193
|
|
|
164
194
|
if (!isValid) {
|
|
@@ -302,11 +332,18 @@ export class RuntimeHttpServer {
|
|
|
302
332
|
|
|
303
333
|
// ── Twilio webhook endpoints — before auth check because Twilio
|
|
304
334
|
// webhook POSTs don't include bearer tokens.
|
|
305
|
-
// Supports
|
|
335
|
+
// Supports /v1/calls/twilio/*, /v1/assistants/:id/calls/twilio/*,
|
|
336
|
+
// and gateway-compatible /webhooks/twilio/* paths.
|
|
306
337
|
// Validates X-Twilio-Signature to prevent unauthorized access. ──
|
|
307
338
|
const twilioMatch = path.match(TWILIO_WEBHOOK_RE);
|
|
308
|
-
|
|
309
|
-
|
|
339
|
+
const gatewayTwilioMatch = !twilioMatch ? path.match(TWILIO_GATEWAY_WEBHOOK_RE) : null;
|
|
340
|
+
const resolvedTwilioSubpath = twilioMatch
|
|
341
|
+
? twilioMatch[1]
|
|
342
|
+
: gatewayTwilioMatch
|
|
343
|
+
? GATEWAY_SUBPATH_MAP[gatewayTwilioMatch[1]]
|
|
344
|
+
: null;
|
|
345
|
+
if (resolvedTwilioSubpath && req.method === 'POST') {
|
|
346
|
+
const twilioSubpath = resolvedTwilioSubpath;
|
|
310
347
|
|
|
311
348
|
// Validate Twilio request signature before dispatching
|
|
312
349
|
const validation = await validateTwilioWebhook(req);
|
|
@@ -335,17 +372,6 @@ export class RuntimeHttpServer {
|
|
|
335
372
|
}
|
|
336
373
|
}
|
|
337
374
|
|
|
338
|
-
// ── Call answer endpoint — behind auth gate ──────────────────────
|
|
339
|
-
const callAnswerMatch = path.match(/^\/v1\/calls\/([^/]+)\/answer$/);
|
|
340
|
-
if (callAnswerMatch && req.method === 'POST') {
|
|
341
|
-
try {
|
|
342
|
-
return await handleCallAnswer(req, callAnswerMatch[1]);
|
|
343
|
-
} catch (err) {
|
|
344
|
-
log.error({ err, callSessionId: callAnswerMatch[1] }, 'Runtime HTTP handler error answering call');
|
|
345
|
-
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
375
|
// Serve shareable app pages
|
|
350
376
|
const pagesMatch = path.match(/^\/pages\/([^/]+)$/);
|
|
351
377
|
if (pagesMatch && req.method === 'GET') {
|
|
@@ -529,6 +555,68 @@ export class RuntimeHttpServer {
|
|
|
529
555
|
return await handleReplayDeadLetters(req);
|
|
530
556
|
}
|
|
531
557
|
|
|
558
|
+
// ── Call API routes ───────────────────────────────────────────
|
|
559
|
+
if (endpoint === 'calls/start' && req.method === 'POST') {
|
|
560
|
+
return await handleStartCall(req);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Match calls/:callSessionId and calls/:callSessionId/cancel, calls/:callSessionId/answer
|
|
564
|
+
const callsMatch = endpoint.match(/^calls\/([^/]+?)(\/cancel|\/answer)?$/);
|
|
565
|
+
if (callsMatch) {
|
|
566
|
+
const callSessionId = callsMatch[1];
|
|
567
|
+
// Skip known sub-paths that are handled elsewhere (twilio, relay)
|
|
568
|
+
if (callSessionId !== 'twilio' && callSessionId !== 'relay' && callSessionId !== 'start') {
|
|
569
|
+
if (callsMatch[2] === '/cancel' && req.method === 'POST') {
|
|
570
|
+
return await handleCancelCall(req, callSessionId);
|
|
571
|
+
}
|
|
572
|
+
if (callsMatch[2] === '/answer' && req.method === 'POST') {
|
|
573
|
+
return await handleAnswerCall(req, callSessionId);
|
|
574
|
+
}
|
|
575
|
+
if (!callsMatch[2] && req.method === 'GET') {
|
|
576
|
+
return handleGetCallStatus(callSessionId);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ── Internal Twilio forwarding endpoints (gateway → runtime) ──
|
|
582
|
+
// These accept JSON payloads from the gateway (which already validated
|
|
583
|
+
// the Twilio signature) and reconstruct requests for the existing
|
|
584
|
+
// Twilio route handlers.
|
|
585
|
+
if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
|
|
586
|
+
const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
|
|
587
|
+
const formBody = new URLSearchParams(json.params).toString();
|
|
588
|
+
// Reconstruct request URL: keep the original URL query string (callSessionId)
|
|
589
|
+
const reconstructedUrl = json.originalUrl ?? req.url;
|
|
590
|
+
const fakeReq = new Request(reconstructedUrl, {
|
|
591
|
+
method: 'POST',
|
|
592
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
593
|
+
body: formBody,
|
|
594
|
+
});
|
|
595
|
+
return await handleVoiceWebhook(fakeReq);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
|
|
599
|
+
const json = await req.json() as { params: Record<string, string> };
|
|
600
|
+
const formBody = new URLSearchParams(json.params).toString();
|
|
601
|
+
const fakeReq = new Request(req.url, {
|
|
602
|
+
method: 'POST',
|
|
603
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
604
|
+
body: formBody,
|
|
605
|
+
});
|
|
606
|
+
return await handleStatusCallback(fakeReq);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (endpoint === 'internal/twilio/connect-action' && req.method === 'POST') {
|
|
610
|
+
const json = await req.json() as { params: Record<string, string> };
|
|
611
|
+
const formBody = new URLSearchParams(json.params).toString();
|
|
612
|
+
const fakeReq = new Request(req.url, {
|
|
613
|
+
method: 'POST',
|
|
614
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
615
|
+
body: formBody,
|
|
616
|
+
});
|
|
617
|
+
return await handleConnectAction(fakeReq);
|
|
618
|
+
}
|
|
619
|
+
|
|
532
620
|
return Response.json({ error: 'Not found', source: 'runtime' }, { status: 404 });
|
|
533
621
|
} catch (err) {
|
|
534
622
|
if (err instanceof IngressBlockedError) {
|
|
@@ -56,7 +56,6 @@ export async function handleUploadAttachment(req: Request): Promise<Response> {
|
|
|
56
56
|
let attachment: attachmentsStore.StoredAttachment;
|
|
57
57
|
try {
|
|
58
58
|
attachment = attachmentsStore.uploadAttachment(
|
|
59
|
-
"self",
|
|
60
59
|
filename,
|
|
61
60
|
mimeType,
|
|
62
61
|
data,
|
|
@@ -98,7 +97,7 @@ export async function handleDeleteAttachment(req: Request): Promise<Response> {
|
|
|
98
97
|
);
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
const result = attachmentsStore.deleteAttachment(
|
|
100
|
+
const result = attachmentsStore.deleteAttachment(attachmentId);
|
|
102
101
|
|
|
103
102
|
if (result === 'not_found') {
|
|
104
103
|
return Response.json(
|
|
@@ -118,7 +117,7 @@ export async function handleDeleteAttachment(req: Request): Promise<Response> {
|
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
export function handleGetAttachment(attachmentId: string): Response {
|
|
121
|
-
const attachment = attachmentsStore.getAttachmentById(
|
|
120
|
+
const attachment = attachmentsStore.getAttachmentById(attachmentId);
|
|
122
121
|
if (!attachment) {
|
|
123
122
|
return Response.json({ error: 'Attachment not found' }, { status: 404 });
|
|
124
123
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime HTTP route handlers for the call API.
|
|
3
|
+
*
|
|
4
|
+
* POST /v1/calls/start — initiate a new call
|
|
5
|
+
* GET /v1/calls/:callSessionId — get call status
|
|
6
|
+
* POST /v1/calls/:callSessionId/cancel — cancel a call
|
|
7
|
+
* POST /v1/calls/:callSessionId/answer — answer a pending question
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { startCall, getCallStatus, cancelCall, answerCall } from '../../calls/call-domain.js';
|
|
11
|
+
import { getConfig } from '../../config/loader.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* POST /v1/calls/start
|
|
15
|
+
*
|
|
16
|
+
* Body: { phoneNumber: string; task: string; context?: string; conversationId: string }
|
|
17
|
+
*/
|
|
18
|
+
export async function handleStartCall(req: Request): Promise<Response> {
|
|
19
|
+
if (!getConfig().calls.enabled) {
|
|
20
|
+
return Response.json(
|
|
21
|
+
{ error: 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.' },
|
|
22
|
+
{ status: 403 },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let body: {
|
|
27
|
+
phoneNumber?: string;
|
|
28
|
+
task?: string;
|
|
29
|
+
context?: string;
|
|
30
|
+
conversationId?: string;
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
body = await req.json() as typeof body;
|
|
34
|
+
} catch {
|
|
35
|
+
return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!body.conversationId) {
|
|
39
|
+
return Response.json({ error: 'conversationId is required' }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await startCall({
|
|
43
|
+
phoneNumber: body.phoneNumber ?? '',
|
|
44
|
+
task: body.task ?? '',
|
|
45
|
+
context: body.context,
|
|
46
|
+
conversationId: body.conversationId,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
return Response.json({ error: result.error }, { status: result.status ?? 500 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Response.json({
|
|
54
|
+
callSessionId: result.session.id,
|
|
55
|
+
callSid: result.callSid,
|
|
56
|
+
status: result.session.status,
|
|
57
|
+
toNumber: result.session.toNumber,
|
|
58
|
+
fromNumber: result.session.fromNumber,
|
|
59
|
+
}, { status: 201 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* GET /v1/calls/:callSessionId
|
|
64
|
+
*/
|
|
65
|
+
export function handleGetCallStatus(callSessionId: string): Response {
|
|
66
|
+
const result = getCallStatus(callSessionId);
|
|
67
|
+
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
return Response.json({ error: result.error }, { status: result.status ?? 500 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { session } = result;
|
|
73
|
+
return Response.json({
|
|
74
|
+
callSessionId: session.id,
|
|
75
|
+
conversationId: session.conversationId,
|
|
76
|
+
status: session.status,
|
|
77
|
+
toNumber: session.toNumber,
|
|
78
|
+
fromNumber: session.fromNumber,
|
|
79
|
+
provider: session.provider,
|
|
80
|
+
providerCallSid: session.providerCallSid,
|
|
81
|
+
task: session.task,
|
|
82
|
+
startedAt: session.startedAt ? new Date(session.startedAt).toISOString() : null,
|
|
83
|
+
endedAt: session.endedAt ? new Date(session.endedAt).toISOString() : null,
|
|
84
|
+
lastError: session.lastError,
|
|
85
|
+
pendingQuestion: result.pendingQuestion ?? null,
|
|
86
|
+
createdAt: new Date(session.createdAt).toISOString(),
|
|
87
|
+
updatedAt: new Date(session.updatedAt).toISOString(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* POST /v1/calls/:callSessionId/cancel
|
|
93
|
+
*
|
|
94
|
+
* Body: { reason?: string }
|
|
95
|
+
*/
|
|
96
|
+
export async function handleCancelCall(req: Request, callSessionId: string): Promise<Response> {
|
|
97
|
+
let reason: string | undefined;
|
|
98
|
+
try {
|
|
99
|
+
const body = await req.json() as { reason?: string };
|
|
100
|
+
reason = body.reason;
|
|
101
|
+
} catch {
|
|
102
|
+
// Empty body is fine
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = await cancelCall({ callSessionId, reason });
|
|
106
|
+
|
|
107
|
+
if (!result.ok) {
|
|
108
|
+
return Response.json({ error: result.error }, { status: result.status ?? 500 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Response.json({
|
|
112
|
+
callSessionId: result.session.id,
|
|
113
|
+
status: result.session.status,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* POST /v1/calls/:callSessionId/answer
|
|
119
|
+
*
|
|
120
|
+
* Body: { answer: string }
|
|
121
|
+
*/
|
|
122
|
+
export async function handleAnswerCall(req: Request, callSessionId: string): Promise<Response> {
|
|
123
|
+
let body: { answer?: string };
|
|
124
|
+
try {
|
|
125
|
+
body = await req.json() as typeof body;
|
|
126
|
+
} catch {
|
|
127
|
+
return Response.json({ error: 'Invalid JSON in request body' }, { status: 400 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await answerCall({
|
|
131
|
+
callSessionId,
|
|
132
|
+
answer: body.answer ?? '',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!result.ok) {
|
|
136
|
+
return Response.json({ error: result.error }, { status: result.status ?? 500 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Response.json({ ok: true, questionId: result.questionId });
|
|
140
|
+
}
|
|
@@ -34,7 +34,7 @@ export async function handleDeleteConversation(req: Request): Promise<Response>
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const conversationKey = `${sourceChannel}:${externalChatId}`;
|
|
37
|
-
deleteConversationKey(
|
|
37
|
+
deleteConversationKey(conversationKey);
|
|
38
38
|
|
|
39
39
|
return Response.json({ ok: true });
|
|
40
40
|
}
|
|
@@ -89,7 +89,7 @@ export async function handleChannelInbound(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
if (hasAttachments) {
|
|
92
|
-
const resolved = attachmentsStore.getAttachmentsByIds(
|
|
92
|
+
const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
|
|
93
93
|
if (resolved.length !== attachmentIds.length) {
|
|
94
94
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
95
95
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
@@ -112,7 +112,6 @@ export async function handleChannelInbound(
|
|
|
112
112
|
if (isEdit && sourceMessageId) {
|
|
113
113
|
// Dedup the edit event itself (retried edited_message webhooks)
|
|
114
114
|
const editResult = channelDeliveryStore.recordInbound(
|
|
115
|
-
"self",
|
|
116
115
|
sourceChannel,
|
|
117
116
|
externalChatId,
|
|
118
117
|
externalMessageId,
|
|
@@ -136,7 +135,6 @@ export async function handleChannelInbound(
|
|
|
136
135
|
let original: { messageId: string; conversationId: string } | null = null;
|
|
137
136
|
for (let attempt = 0; attempt <= EDIT_LOOKUP_RETRIES; attempt++) {
|
|
138
137
|
original = channelDeliveryStore.findMessageBySourceId(
|
|
139
|
-
"self",
|
|
140
138
|
sourceChannel,
|
|
141
139
|
externalChatId,
|
|
142
140
|
sourceMessageId,
|
|
@@ -173,7 +171,6 @@ export async function handleChannelInbound(
|
|
|
173
171
|
|
|
174
172
|
// ── New message path ──
|
|
175
173
|
const result = channelDeliveryStore.recordInbound(
|
|
176
|
-
"self",
|
|
177
174
|
sourceChannel,
|
|
178
175
|
externalChatId,
|
|
179
176
|
externalMessageId,
|
|
@@ -239,7 +236,6 @@ export async function handleChannelInbound(
|
|
|
239
236
|
} catch (err) {
|
|
240
237
|
// Secret ingress blocks are not retryable — let the top-level handler return 422
|
|
241
238
|
if (err instanceof IngressBlockedError) throw err;
|
|
242
|
-
console.error(`[runtime-http] Processing failed`, err);
|
|
243
239
|
log.error({ err, conversationId: result.conversationId }, 'Failed to process channel inbound message');
|
|
244
240
|
channelDeliveryStore.recordProcessingFailure(result.eventId, err);
|
|
245
241
|
}
|
|
@@ -257,7 +253,7 @@ export async function handleChannelInbound(
|
|
|
257
253
|
try { parsed = JSON.parse(msgs[i].content); } catch { parsed = msgs[i].content; }
|
|
258
254
|
const rendered = renderHistoryContent(parsed);
|
|
259
255
|
|
|
260
|
-
const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id
|
|
256
|
+
const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id);
|
|
261
257
|
const replyAttachments: RuntimeAttachmentMetadata[] = linked.map((a) => ({
|
|
262
258
|
id: a.id,
|
|
263
259
|
filename: a.originalFilename,
|
|
@@ -290,7 +286,7 @@ export async function handleChannelInbound(
|
|
|
290
286
|
}
|
|
291
287
|
|
|
292
288
|
export function handleListDeadLetters(): Response {
|
|
293
|
-
const events = channelDeliveryStore.getDeadLetterEvents(
|
|
289
|
+
const events = channelDeliveryStore.getDeadLetterEvents();
|
|
294
290
|
return Response.json({ events });
|
|
295
291
|
}
|
|
296
292
|
|
|
@@ -302,7 +298,7 @@ export async function handleReplayDeadLetters(req: Request): Promise<Response> {
|
|
|
302
298
|
return Response.json({ error: 'eventIds array is required' }, { status: 400 });
|
|
303
299
|
}
|
|
304
300
|
|
|
305
|
-
const replayed = channelDeliveryStore.replayDeadLetters(
|
|
301
|
+
const replayed = channelDeliveryStore.replayDeadLetters(eventIds);
|
|
306
302
|
return Response.json({ replayed });
|
|
307
303
|
}
|
|
308
304
|
|
|
@@ -326,7 +322,6 @@ export async function handleChannelDeliveryAck(req: Request): Promise<Response>
|
|
|
326
322
|
}
|
|
327
323
|
|
|
328
324
|
const acked = channelDeliveryStore.acknowledgeDelivery(
|
|
329
|
-
"self",
|
|
330
325
|
sourceChannel,
|
|
331
326
|
externalChatId,
|
|
332
327
|
externalMessageId,
|
|
@@ -54,7 +54,7 @@ export function handleListMessages(
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const mapping = getConversationByKey(
|
|
57
|
+
const mapping = getConversationByKey(conversationKey);
|
|
58
58
|
if (!mapping) {
|
|
59
59
|
return Response.json({ messages: [] });
|
|
60
60
|
}
|
|
@@ -89,7 +89,7 @@ export function handleListMessages(
|
|
|
89
89
|
const messages: RuntimeMessagePayload[] = merged.map((m) => {
|
|
90
90
|
let msgAttachments: RuntimeAttachmentMetadata[] = [];
|
|
91
91
|
if (m.role === 'assistant' && m.id) {
|
|
92
|
-
const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id
|
|
92
|
+
const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
|
|
93
93
|
if (linked.length > 0) {
|
|
94
94
|
msgAttachments = linked.map((a) => ({
|
|
95
95
|
id: a.id,
|
|
@@ -170,7 +170,7 @@ export async function handleSendMessage(
|
|
|
170
170
|
|
|
171
171
|
// Validate that all attachment IDs resolve
|
|
172
172
|
if (hasAttachments) {
|
|
173
|
-
const resolved = attachmentsStore.getAttachmentsByIds(
|
|
173
|
+
const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
|
|
174
174
|
if (resolved.length !== attachmentIds.length) {
|
|
175
175
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
176
176
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
@@ -181,7 +181,7 @@ export async function handleSendMessage(
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
const mapping = getOrCreateConversation(
|
|
184
|
+
const mapping = getOrCreateConversation(conversationKey);
|
|
185
185
|
|
|
186
186
|
const processor = deps.persistAndProcessMessage ?? deps.processMessage;
|
|
187
187
|
if (!processor) {
|
|
@@ -247,7 +247,7 @@ export async function handleGetSuggestion(
|
|
|
247
247
|
);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
const mapping = getConversationByKey(
|
|
250
|
+
const mapping = getConversationByKey(conversationKey);
|
|
251
251
|
if (!mapping) {
|
|
252
252
|
return Response.json({ suggestion: null, messageId: null, source: 'none' as const });
|
|
253
253
|
}
|
|
@@ -38,7 +38,7 @@ export async function handleCreateRun(
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (hasAttachments) {
|
|
41
|
-
const resolved = attachmentsStore.getAttachmentsByIds(
|
|
41
|
+
const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
|
|
42
42
|
if (resolved.length !== attachmentIds.length) {
|
|
43
43
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
44
44
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
@@ -49,7 +49,7 @@ export async function handleCreateRun(
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const mapping = getOrCreateConversation(
|
|
52
|
+
const mapping = getOrCreateConversation(conversationKey);
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
55
|
const run = await runOrchestrator.startRun(
|
|
@@ -14,6 +14,7 @@ import * as runsStore from '../memory/runs-store.js';
|
|
|
14
14
|
import type { Run } from '../memory/runs-store.js';
|
|
15
15
|
import type { Session } from '../daemon/session.js';
|
|
16
16
|
import type { ServerMessage } from '../daemon/ipc-protocol.js';
|
|
17
|
+
import { resolveChannelCapabilities } from '../daemon/session-runtime-assembly.js';
|
|
17
18
|
import type { UserDecision } from '../permissions/types.js';
|
|
18
19
|
import { checkIngressForSecrets } from '../security/secret-ingress.js';
|
|
19
20
|
import { IngressBlockedError } from '../util/errors.js';
|
|
@@ -88,10 +89,11 @@ export class RunOrchestrator {
|
|
|
88
89
|
|
|
89
90
|
const requestId = crypto.randomUUID();
|
|
90
91
|
const messageId = session.persistUserMessage(content, attachments, requestId);
|
|
91
|
-
const run = runsStore.createRun(
|
|
92
|
+
const run = runsStore.createRun(conversationId, messageId);
|
|
92
93
|
|
|
93
|
-
//
|
|
94
|
-
|
|
94
|
+
// Runs are always HTTP-originated; set channel capabilities so the attachment
|
|
95
|
+
// scope heuristic resolves to 'self' rather than 'local-assistant'.
|
|
96
|
+
session.setChannelCapabilities(resolveChannelCapabilities('http-api'));
|
|
95
97
|
|
|
96
98
|
// Serialized publish chain so hub subscribers observe events in order.
|
|
97
99
|
let hubChain: Promise<void> = Promise.resolve();
|
|
@@ -110,6 +112,7 @@ export class RunOrchestrator {
|
|
|
110
112
|
});
|
|
111
113
|
};
|
|
112
114
|
|
|
115
|
+
|
|
113
116
|
// Hook into session to intercept confirmation_request events.
|
|
114
117
|
// When the prompter sends a confirmation_request, we record it in the
|
|
115
118
|
// run store so the web UI can poll and submit a decision.
|
|
@@ -144,6 +147,9 @@ export class RunOrchestrator {
|
|
|
144
147
|
// Fire-and-forget the agent loop
|
|
145
148
|
const cleanup = () => {
|
|
146
149
|
this.pending.delete(run.id);
|
|
150
|
+
// Reset channel capabilities so a subsequent IPC/desktop session on the
|
|
151
|
+
// same conversation is not incorrectly treated as an HTTP-API client.
|
|
152
|
+
session.setChannelCapabilities(null);
|
|
147
153
|
// Reset the session's client callback to a no-op so the stale
|
|
148
154
|
// closure doesn't intercept events from future runs on the same session.
|
|
149
155
|
// Set hasNoClient=true here since the run is done and no HTTP caller
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Cron } from 'croner';
|
|
2
|
+
import { rrulestr, RRuleSet } from 'rrule';
|
|
3
|
+
import type { ScheduleSyntax } from './recurrence-types.js';
|
|
4
|
+
|
|
5
|
+
export interface ScheduleSpec {
|
|
6
|
+
syntax: ScheduleSyntax;
|
|
7
|
+
expression: string;
|
|
8
|
+
timezone?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SUPPORTED_RRULE_PREFIXES = ['DTSTART', 'RRULE:', 'RDATE', 'EXDATE', 'EXRULE'];
|
|
12
|
+
|
|
13
|
+
function normalizeRruleExpression(expression: string): string {
|
|
14
|
+
// Handle escaped newlines from JSON transport
|
|
15
|
+
return expression.replace(/\\n/g, '\n').trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseRruleLines(expression: string): string[] {
|
|
19
|
+
return normalizeRruleExpression(expression)
|
|
20
|
+
.split(/\r?\n/)
|
|
21
|
+
.map(l => l.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateRruleLines(lines: string[]): string | null {
|
|
26
|
+
let hasInclusion = false;
|
|
27
|
+
let hasDtstart = false;
|
|
28
|
+
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const upper = line.toUpperCase();
|
|
31
|
+
if (!SUPPORTED_RRULE_PREFIXES.some(p => upper.startsWith(p))) {
|
|
32
|
+
return `Unsupported recurrence line: ${line}`;
|
|
33
|
+
}
|
|
34
|
+
if (upper.startsWith('DTSTART')) hasDtstart = true;
|
|
35
|
+
if (upper.startsWith('RRULE:') || upper.startsWith('RDATE')) hasInclusion = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!hasDtstart) return 'RRULE expression must include DTSTART for deterministic scheduling';
|
|
39
|
+
if (!hasInclusion) return 'RRULE expression must include at least one RRULE or RDATE';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Detect whether an RRULE expression contains set constructs (RDATE, EXDATE,
|
|
45
|
+
* EXRULE, or multiple RRULE lines) that require RRuleSet parsing.
|
|
46
|
+
*/
|
|
47
|
+
export function hasSetConstructs(expression: string): boolean {
|
|
48
|
+
const lines = parseRruleLines(expression);
|
|
49
|
+
let rruleCount = 0;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const upper = line.toUpperCase();
|
|
52
|
+
if (upper.startsWith('RDATE') || upper.startsWith('EXDATE') || upper.startsWith('EXRULE')) return true;
|
|
53
|
+
if (upper.startsWith('RRULE:')) rruleCount++;
|
|
54
|
+
}
|
|
55
|
+
return rruleCount > 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate RRULE set lines in an expression. Returns null if valid, or an
|
|
60
|
+
* actionable error string describing the problem. This is intended for tool
|
|
61
|
+
* layers that want to surface a specific error message before calling the
|
|
62
|
+
* store.
|
|
63
|
+
*/
|
|
64
|
+
export function validateRruleSetLines(expression: string): string | null {
|
|
65
|
+
const lines = parseRruleLines(expression);
|
|
66
|
+
return validateRruleLines(lines);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate a schedule expression. Returns true if the expression is valid
|
|
71
|
+
* for the given syntax, false otherwise.
|
|
72
|
+
*/
|
|
73
|
+
export function isValidScheduleExpression(spec: ScheduleSpec): boolean {
|
|
74
|
+
try {
|
|
75
|
+
if (spec.syntax === 'cron') {
|
|
76
|
+
new Cron(spec.expression, { maxRuns: 0 });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (spec.syntax === 'rrule') {
|
|
81
|
+
const lines = parseRruleLines(spec.expression);
|
|
82
|
+
const error = validateRruleLines(lines);
|
|
83
|
+
if (error) return false;
|
|
84
|
+
|
|
85
|
+
const normalized = normalizeRruleExpression(spec.expression);
|
|
86
|
+
const tzid = spec.timezone ?? undefined;
|
|
87
|
+
if (hasSetConstructs(normalized)) {
|
|
88
|
+
rrulestr(normalized, { forceset: true, tzid });
|
|
89
|
+
} else {
|
|
90
|
+
rrulestr(normalized, { tzid });
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Compute the next run timestamp (epoch ms) for a schedule expression.
|
|
103
|
+
* Throws if no future runs exist.
|
|
104
|
+
*/
|
|
105
|
+
export function computeNextRunAt(spec: ScheduleSpec, nowMs?: number): number {
|
|
106
|
+
const now = nowMs ?? Date.now();
|
|
107
|
+
|
|
108
|
+
if (spec.syntax === 'cron') {
|
|
109
|
+
const cron = new Cron(spec.expression, {
|
|
110
|
+
timezone: spec.timezone ?? undefined,
|
|
111
|
+
});
|
|
112
|
+
const next = cron.nextRun(new Date(now));
|
|
113
|
+
if (!next) {
|
|
114
|
+
throw new Error(`Cron expression "${spec.expression}" has no upcoming runs`);
|
|
115
|
+
}
|
|
116
|
+
return next.getTime();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (spec.syntax === 'rrule') {
|
|
120
|
+
const normalized = normalizeRruleExpression(spec.expression);
|
|
121
|
+
const lines = parseRruleLines(normalized);
|
|
122
|
+
const error = validateRruleLines(lines);
|
|
123
|
+
if (error) throw new Error(error);
|
|
124
|
+
|
|
125
|
+
const useSet = hasSetConstructs(normalized);
|
|
126
|
+
const tzid = spec.timezone ?? undefined;
|
|
127
|
+
const parsed = useSet
|
|
128
|
+
? (rrulestr(normalized, { forceset: true, tzid }) as RRuleSet)
|
|
129
|
+
: rrulestr(normalized, { tzid });
|
|
130
|
+
const next = parsed.after(new Date(now));
|
|
131
|
+
if (!next) {
|
|
132
|
+
throw new Error(`RRULE expression has no upcoming runs after ${new Date(now).toISOString()}`);
|
|
133
|
+
}
|
|
134
|
+
return next.getTime();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(`Unsupported schedule syntax: ${spec.syntax}`);
|
|
138
|
+
}
|