vellum 0.2.1 → 0.2.7
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 +71 -100
- package/package.json +5 -3
- 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 +305 -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-twilio-config.test.ts +221 -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 +71 -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-regressions.test.ts +100 -2
- 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-commit-message-generator.test.ts +303 -0
- 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-conflict-gate.test.ts +28 -25
- 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/__tests__/twilio-webhook-urls.test.ts +162 -0
- 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-config.ts +8 -8
- package/src/calls/twilio-provider.ts +13 -9
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/twilio-webhook-urls.ts +50 -0
- 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 +270 -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 +34 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +165 -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/config/vellum-skills/telegram-setup/SKILL.md +1 -5
- 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 +205 -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 +32 -4
- package/src/daemon/ipc-contract.ts +156 -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 +75 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-conflict-gate.ts +18 -109
- 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/conflict-intent.ts +114 -0
- 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/job-handlers/conflict.ts +23 -1
- 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/gateway-client.ts +36 -0
- package/src/runtime/http-server.ts +166 -22
- 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 +125 -88
- 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 +293 -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 +207 -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 +269 -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,10 +10,10 @@ import { renderHistoryContent } from '../../daemon/handlers.js';
|
|
|
10
10
|
import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
11
11
|
import { IngressBlockedError } from '../../util/errors.js';
|
|
12
12
|
import { getLogger } from '../../util/logger.js';
|
|
13
|
+
import { deliverChannelReply } from '../gateway-client.js';
|
|
13
14
|
import type {
|
|
14
15
|
MessageProcessor,
|
|
15
16
|
RuntimeAttachmentMetadata,
|
|
16
|
-
RuntimeMessagePayload,
|
|
17
17
|
} from '../http-types.js';
|
|
18
18
|
|
|
19
19
|
const log = getLogger('runtime-http');
|
|
@@ -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
|
}
|
|
@@ -54,6 +54,7 @@ export async function handleChannelInbound(
|
|
|
54
54
|
senderExternalUserId?: string;
|
|
55
55
|
senderUsername?: string;
|
|
56
56
|
sourceMetadata?: Record<string, unknown>;
|
|
57
|
+
replyCallbackUrl?: string;
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
const {
|
|
@@ -89,7 +90,7 @@ export async function handleChannelInbound(
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
if (hasAttachments) {
|
|
92
|
-
const resolved = attachmentsStore.getAttachmentsByIds(
|
|
93
|
+
const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
|
|
93
94
|
if (resolved.length !== attachmentIds.length) {
|
|
94
95
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
95
96
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
@@ -112,7 +113,6 @@ export async function handleChannelInbound(
|
|
|
112
113
|
if (isEdit && sourceMessageId) {
|
|
113
114
|
// Dedup the edit event itself (retried edited_message webhooks)
|
|
114
115
|
const editResult = channelDeliveryStore.recordInbound(
|
|
115
|
-
"self",
|
|
116
116
|
sourceChannel,
|
|
117
117
|
externalChatId,
|
|
118
118
|
externalMessageId,
|
|
@@ -136,7 +136,6 @@ export async function handleChannelInbound(
|
|
|
136
136
|
let original: { messageId: string; conversationId: string } | null = null;
|
|
137
137
|
for (let attempt = 0; attempt <= EDIT_LOOKUP_RETRIES; attempt++) {
|
|
138
138
|
original = channelDeliveryStore.findMessageBySourceId(
|
|
139
|
-
"self",
|
|
140
139
|
sourceChannel,
|
|
141
140
|
externalChatId,
|
|
142
141
|
sourceMessageId,
|
|
@@ -173,7 +172,6 @@ export async function handleChannelInbound(
|
|
|
173
172
|
|
|
174
173
|
// ── New message path ──
|
|
175
174
|
const result = channelDeliveryStore.recordInbound(
|
|
176
|
-
"self",
|
|
177
175
|
sourceChannel,
|
|
178
176
|
externalChatId,
|
|
179
177
|
externalMessageId,
|
|
@@ -188,41 +186,92 @@ export async function handleChannelInbound(
|
|
|
188
186
|
? sourceMetadata.uxBrief.trim()
|
|
189
187
|
: undefined;
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
const replyCallbackUrl = body.replyCallbackUrl;
|
|
190
|
+
|
|
191
|
+
// For new (non-duplicate) messages, run the secret ingress check
|
|
192
|
+
// synchronously, then fire off the agent loop in the background.
|
|
193
193
|
if (!result.duplicate && processMessage) {
|
|
194
|
+
// Persist the raw payload first so dead-lettered events can always be
|
|
195
|
+
// replayed. If the ingress check later detects secrets we clear it
|
|
196
|
+
// before throwing, so secret-bearing content is never left on disk.
|
|
197
|
+
channelDeliveryStore.storePayload(result.eventId, {
|
|
198
|
+
sourceChannel, externalChatId, externalMessageId, content,
|
|
199
|
+
attachmentIds, sourceMetadata: body.sourceMetadata,
|
|
200
|
+
senderName: body.senderName,
|
|
201
|
+
senderExternalUserId: body.senderExternalUserId,
|
|
202
|
+
senderUsername: body.senderUsername,
|
|
203
|
+
replyCallbackUrl,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const contentToCheck = content ?? '';
|
|
207
|
+
let ingressCheck: ReturnType<typeof checkIngressForSecrets>;
|
|
194
208
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
});
|
|
209
|
+
ingressCheck = checkIngressForSecrets(contentToCheck);
|
|
210
|
+
} catch (checkErr) {
|
|
211
|
+
channelDeliveryStore.clearPayload(result.eventId);
|
|
212
|
+
throw checkErr;
|
|
213
|
+
}
|
|
214
|
+
if (ingressCheck.blocked) {
|
|
215
|
+
channelDeliveryStore.clearPayload(result.eventId);
|
|
216
|
+
throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
|
|
217
|
+
}
|
|
205
218
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
// Fire-and-forget: process the message and deliver the reply in the background.
|
|
220
|
+
// The HTTP response returns immediately so the gateway webhook is not blocked.
|
|
221
|
+
processChannelMessageInBackground({
|
|
222
|
+
processMessage,
|
|
223
|
+
conversationId: result.conversationId,
|
|
224
|
+
eventId: result.eventId,
|
|
225
|
+
content: content ?? '',
|
|
226
|
+
attachmentIds: hasAttachments ? attachmentIds : undefined,
|
|
227
|
+
sourceChannel,
|
|
228
|
+
externalChatId,
|
|
229
|
+
metadataHints,
|
|
230
|
+
metadataUxBrief,
|
|
231
|
+
replyCallbackUrl,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return Response.json({
|
|
236
|
+
accepted: result.accepted,
|
|
237
|
+
duplicate: result.duplicate,
|
|
238
|
+
eventId: result.eventId,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
221
241
|
|
|
242
|
+
interface BackgroundProcessingParams {
|
|
243
|
+
processMessage: MessageProcessor;
|
|
244
|
+
conversationId: string;
|
|
245
|
+
eventId: string;
|
|
246
|
+
content: string;
|
|
247
|
+
attachmentIds?: string[];
|
|
248
|
+
sourceChannel: string;
|
|
249
|
+
externalChatId: string;
|
|
250
|
+
metadataHints: string[];
|
|
251
|
+
metadataUxBrief?: string;
|
|
252
|
+
replyCallbackUrl?: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function processChannelMessageInBackground(params: BackgroundProcessingParams): void {
|
|
256
|
+
const {
|
|
257
|
+
processMessage,
|
|
258
|
+
conversationId,
|
|
259
|
+
eventId,
|
|
260
|
+
content,
|
|
261
|
+
attachmentIds,
|
|
262
|
+
sourceChannel,
|
|
263
|
+
externalChatId,
|
|
264
|
+
metadataHints,
|
|
265
|
+
metadataUxBrief,
|
|
266
|
+
replyCallbackUrl,
|
|
267
|
+
} = params;
|
|
268
|
+
|
|
269
|
+
(async () => {
|
|
270
|
+
try {
|
|
222
271
|
const { messageId: userMessageId } = await processMessage(
|
|
223
|
-
|
|
224
|
-
content
|
|
225
|
-
|
|
272
|
+
conversationId,
|
|
273
|
+
content,
|
|
274
|
+
attachmentIds,
|
|
226
275
|
{
|
|
227
276
|
transport: {
|
|
228
277
|
channelId: sourceChannel,
|
|
@@ -232,65 +281,54 @@ export async function handleChannelInbound(
|
|
|
232
281
|
},
|
|
233
282
|
sourceChannel,
|
|
234
283
|
);
|
|
235
|
-
|
|
236
|
-
channelDeliveryStore.
|
|
237
|
-
|
|
238
|
-
|
|
284
|
+
channelDeliveryStore.linkMessage(eventId, userMessageId);
|
|
285
|
+
channelDeliveryStore.markProcessed(eventId);
|
|
286
|
+
|
|
287
|
+
if (replyCallbackUrl) {
|
|
288
|
+
await deliverReplyViaCallback(conversationId, externalChatId, replyCallbackUrl);
|
|
289
|
+
}
|
|
239
290
|
} catch (err) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
console.error(`[runtime-http] Processing failed`, err);
|
|
243
|
-
log.error({ err, conversationId: result.conversationId }, 'Failed to process channel inbound message');
|
|
244
|
-
channelDeliveryStore.recordProcessingFailure(result.eventId, err);
|
|
291
|
+
log.error({ err, conversationId }, 'Background channel message processing failed');
|
|
292
|
+
channelDeliveryStore.recordProcessingFailure(eventId, err);
|
|
245
293
|
}
|
|
246
|
-
}
|
|
294
|
+
})();
|
|
295
|
+
}
|
|
247
296
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
timestamp: new Date(msgs[i].createdAt).toISOString(),
|
|
276
|
-
attachments: replyAttachments,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
break;
|
|
297
|
+
async function deliverReplyViaCallback(
|
|
298
|
+
conversationId: string,
|
|
299
|
+
externalChatId: string,
|
|
300
|
+
callbackUrl: string,
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const msgs = conversationStore.getMessages(conversationId);
|
|
303
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
304
|
+
if (msgs[i].role === 'assistant') {
|
|
305
|
+
let parsed: unknown;
|
|
306
|
+
try { parsed = JSON.parse(msgs[i].content); } catch { parsed = msgs[i].content; }
|
|
307
|
+
const rendered = renderHistoryContent(parsed);
|
|
308
|
+
|
|
309
|
+
const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id);
|
|
310
|
+
const replyAttachments: RuntimeAttachmentMetadata[] = linked.map((a) => ({
|
|
311
|
+
id: a.id,
|
|
312
|
+
filename: a.originalFilename,
|
|
313
|
+
mimeType: a.mimeType,
|
|
314
|
+
sizeBytes: a.sizeBytes,
|
|
315
|
+
kind: a.kind,
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
if (rendered.text || replyAttachments.length > 0) {
|
|
319
|
+
await deliverChannelReply(callbackUrl, {
|
|
320
|
+
chatId: externalChatId,
|
|
321
|
+
text: rendered.text || undefined,
|
|
322
|
+
attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
|
|
323
|
+
});
|
|
280
324
|
}
|
|
325
|
+
break;
|
|
281
326
|
}
|
|
282
327
|
}
|
|
283
|
-
|
|
284
|
-
return Response.json({
|
|
285
|
-
accepted: result.accepted,
|
|
286
|
-
duplicate: result.duplicate,
|
|
287
|
-
eventId: result.eventId,
|
|
288
|
-
...(assistantMessage ? { assistantMessage } : {}),
|
|
289
|
-
});
|
|
290
328
|
}
|
|
291
329
|
|
|
292
330
|
export function handleListDeadLetters(): Response {
|
|
293
|
-
const events = channelDeliveryStore.getDeadLetterEvents(
|
|
331
|
+
const events = channelDeliveryStore.getDeadLetterEvents();
|
|
294
332
|
return Response.json({ events });
|
|
295
333
|
}
|
|
296
334
|
|
|
@@ -302,7 +340,7 @@ export async function handleReplayDeadLetters(req: Request): Promise<Response> {
|
|
|
302
340
|
return Response.json({ error: 'eventIds array is required' }, { status: 400 });
|
|
303
341
|
}
|
|
304
342
|
|
|
305
|
-
const replayed = channelDeliveryStore.replayDeadLetters(
|
|
343
|
+
const replayed = channelDeliveryStore.replayDeadLetters(eventIds);
|
|
306
344
|
return Response.json({ replayed });
|
|
307
345
|
}
|
|
308
346
|
|
|
@@ -326,7 +364,6 @@ export async function handleChannelDeliveryAck(req: Request): Promise<Response>
|
|
|
326
364
|
}
|
|
327
365
|
|
|
328
366
|
const acked = channelDeliveryStore.acknowledgeDelivery(
|
|
329
|
-
"self",
|
|
330
367
|
sourceChannel,
|
|
331
368
|
externalChatId,
|
|
332
369
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type ScheduleSyntax = 'cron' | 'rrule';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect whether an expression string is cron or RRULE syntax.
|
|
5
|
+
* Returns null for ambiguous or invalid expressions.
|
|
6
|
+
*/
|
|
7
|
+
export function detectScheduleSyntax(expression: string): ScheduleSyntax | null {
|
|
8
|
+
if (!expression || typeof expression !== 'string') return null;
|
|
9
|
+
const trimmed = expression.trim();
|
|
10
|
+
if (!trimmed) return null;
|
|
11
|
+
|
|
12
|
+
// RRULE detection: starts with RRULE:, DTSTART, or contains FREQ=
|
|
13
|
+
if (/^(RRULE:|DTSTART)/m.test(trimmed) || /FREQ=/i.test(trimmed)) {
|
|
14
|
+
return 'rrule';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Cron detection: 5 space-separated fields
|
|
18
|
+
const fields = trimmed.split(/\s+/);
|
|
19
|
+
if (fields.length === 5) {
|
|
20
|
+
// Basic sanity check: each field should match cron-like characters
|
|
21
|
+
const cronFieldPattern = /^[\d\*\/\-\,\?LW#]+$/;
|
|
22
|
+
if (fields.every(f => cronFieldPattern.test(f))) {
|
|
23
|
+
return 'cron';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize schedule syntax from tool/API inputs.
|
|
32
|
+
* Resolution order:
|
|
33
|
+
* 1. If explicit `syntax` is provided, use it
|
|
34
|
+
* 2. If `expression` is provided, auto-detect from expression
|
|
35
|
+
* 3. If `legacyCronExpression` is provided, treat as cron
|
|
36
|
+
* 4. Return null if nothing resolved
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeScheduleSyntax(input: {
|
|
39
|
+
syntax?: ScheduleSyntax;
|
|
40
|
+
expression?: string;
|
|
41
|
+
legacyCronExpression?: string;
|
|
42
|
+
}): { syntax: ScheduleSyntax; expression: string } | null {
|
|
43
|
+
// Explicit syntax + expression
|
|
44
|
+
if (input.syntax && input.expression) {
|
|
45
|
+
return { syntax: input.syntax, expression: input.expression };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Auto-detect from expression
|
|
49
|
+
if (input.expression) {
|
|
50
|
+
const detected = detectScheduleSyntax(input.expression);
|
|
51
|
+
if (detected) {
|
|
52
|
+
return { syntax: detected, expression: input.expression };
|
|
53
|
+
}
|
|
54
|
+
// If we have an explicit syntax but couldn't detect, trust the explicit syntax
|
|
55
|
+
if (input.syntax) {
|
|
56
|
+
return { syntax: input.syntax, expression: input.expression };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Legacy cron_expression fallback
|
|
62
|
+
if (input.legacyCronExpression) {
|
|
63
|
+
return { syntax: 'cron', expression: input.legacyCronExpression };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|