vellum 0.2.0 → 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 +161 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- 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 +5 -8
- 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 +454 -0
- 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-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- 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 +348 -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__/doordash-session.test.ts +9 -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 +96 -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 +17 -10
- 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 +222 -0
- package/src/__tests__/run-orchestrator.test.ts +7 -7
- package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
- package/src/__tests__/runtime-runs-http.test.ts +5 -23
- package/src/__tests__/runtime-runs.test.ts +11 -11
- 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 +89 -16
- 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 +273 -2
- 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 +403 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +35 -14
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +390 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +416 -0
- package/src/calls/relay-server.ts +335 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +173 -0
- package/src/calls/twilio-routes.ts +250 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +256 -25
- 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 +163 -0
- 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.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
- 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 +44 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +218 -1
- package/src/config/system-prompt.ts +100 -6
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +5 -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 +192 -4
- 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 -271
- 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 +495 -39
- package/src/daemon/ipc-contract-inventory.json +40 -4
- package/src/daemon/ipc-contract.ts +185 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +48 -5
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +144 -29
- 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 +222 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1365
- 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 -1151
- package/src/media/gemini-image-service.ts +1 -1
- 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 +362 -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 +65 -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 +277 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +5 -6
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +12 -19
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +39 -6
- 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 +67 -0
- package/src/tools/calls/call-start.ts +73 -0
- package/src/tools/calls/call-status.ts +81 -0
- 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 +21 -5
- 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/registry.ts +2 -4
- 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 -6
- package/src/tools/tasks/task-delete.ts +69 -56
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +74 -102
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +192 -134
- package/src/tools/tasks/work-item-list.ts +33 -78
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +20 -74
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +6 -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 +236 -2
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +272 -52
- package/src/workspace/heartbeat-service.ts +70 -13
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +100 -51
- 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
|
@@ -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
|
+
}
|
|
@@ -2,12 +2,16 @@ import { and, asc, desc, eq, lte } from 'drizzle-orm';
|
|
|
2
2
|
import { v4 as uuid } from 'uuid';
|
|
3
3
|
import { Cron } from 'croner';
|
|
4
4
|
import { getDb } from '../memory/db.js';
|
|
5
|
-
import {
|
|
5
|
+
import { scheduleJobs, scheduleRuns } from '../memory/schema.js';
|
|
6
|
+
import { computeNextRunAt as computeNextRunAtEngine, isValidScheduleExpression } from './recurrence-engine.js';
|
|
7
|
+
import type { ScheduleSyntax } from './recurrence-types.js';
|
|
6
8
|
|
|
7
9
|
export interface ScheduleJob {
|
|
8
10
|
id: string;
|
|
9
11
|
name: string;
|
|
10
12
|
enabled: boolean;
|
|
13
|
+
syntax: ScheduleSyntax;
|
|
14
|
+
expression: string;
|
|
11
15
|
cronExpression: string;
|
|
12
16
|
timezone: string | null;
|
|
13
17
|
message: string;
|
|
@@ -48,7 +52,7 @@ export function computeNextRunAt(cronExpression: string, timezone?: string | nul
|
|
|
48
52
|
});
|
|
49
53
|
const next = cron.nextRun();
|
|
50
54
|
if (!next) {
|
|
51
|
-
throw new Error(`
|
|
55
|
+
throw new Error(`Schedule expression "${cronExpression}" has no upcoming runs`);
|
|
52
56
|
}
|
|
53
57
|
return next.getTime();
|
|
54
58
|
}
|
|
@@ -60,9 +64,16 @@ export function createSchedule(params: {
|
|
|
60
64
|
message: string;
|
|
61
65
|
enabled?: boolean;
|
|
62
66
|
createdBy?: string;
|
|
67
|
+
syntax?: ScheduleSyntax;
|
|
68
|
+
expression?: string;
|
|
63
69
|
}): ScheduleJob {
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// Resolve syntax and expression: prefer explicit values, fall back to cron default
|
|
71
|
+
const syntax: ScheduleSyntax = params.syntax ?? 'cron';
|
|
72
|
+
const expression = params.expression ?? params.cronExpression;
|
|
73
|
+
|
|
74
|
+
const spec = { syntax, expression, timezone: params.timezone };
|
|
75
|
+
if (!isValidScheduleExpression(spec)) {
|
|
76
|
+
throw new Error(`Invalid ${syntax} expression: "${expression}"`);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
const db = getDb();
|
|
@@ -70,13 +81,14 @@ export function createSchedule(params: {
|
|
|
70
81
|
const now = Date.now();
|
|
71
82
|
const enabled = params.enabled ?? true;
|
|
72
83
|
const timezone = params.timezone ?? null;
|
|
73
|
-
const nextRunAt = enabled ?
|
|
84
|
+
const nextRunAt = enabled ? computeNextRunAtEngine(spec) : 0;
|
|
74
85
|
|
|
75
86
|
const row = {
|
|
76
87
|
id,
|
|
77
88
|
name: params.name,
|
|
78
89
|
enabled,
|
|
79
|
-
cronExpression:
|
|
90
|
+
cronExpression: expression,
|
|
91
|
+
scheduleSyntax: syntax,
|
|
80
92
|
timezone,
|
|
81
93
|
message: params.message,
|
|
82
94
|
nextRunAt,
|
|
@@ -88,16 +100,16 @@ export function createSchedule(params: {
|
|
|
88
100
|
updatedAt: now,
|
|
89
101
|
};
|
|
90
102
|
|
|
91
|
-
db.insert(
|
|
92
|
-
return row;
|
|
103
|
+
db.insert(scheduleJobs).values(row).run();
|
|
104
|
+
return parseJobRow(row);
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
export function getSchedule(id: string): ScheduleJob | null {
|
|
96
108
|
const db = getDb();
|
|
97
109
|
const row = db
|
|
98
110
|
.select()
|
|
99
|
-
.from(
|
|
100
|
-
.where(eq(
|
|
111
|
+
.from(scheduleJobs)
|
|
112
|
+
.where(eq(scheduleJobs.id, id))
|
|
101
113
|
.get();
|
|
102
114
|
if (!row) return null;
|
|
103
115
|
return parseJobRow(row);
|
|
@@ -105,12 +117,12 @@ export function getSchedule(id: string): ScheduleJob | null {
|
|
|
105
117
|
|
|
106
118
|
export function listSchedules(options?: { enabledOnly?: boolean }): ScheduleJob[] {
|
|
107
119
|
const db = getDb();
|
|
108
|
-
const conditions = options?.enabledOnly ? eq(
|
|
120
|
+
const conditions = options?.enabledOnly ? eq(scheduleJobs.enabled, true) : undefined;
|
|
109
121
|
const rows = db
|
|
110
122
|
.select()
|
|
111
|
-
.from(
|
|
123
|
+
.from(scheduleJobs)
|
|
112
124
|
.where(conditions)
|
|
113
|
-
.orderBy(asc(
|
|
125
|
+
.orderBy(asc(scheduleJobs.nextRunAt))
|
|
114
126
|
.all();
|
|
115
127
|
return rows.map(parseJobRow);
|
|
116
128
|
}
|
|
@@ -123,90 +135,120 @@ export function updateSchedule(
|
|
|
123
135
|
timezone?: string | null;
|
|
124
136
|
message?: string;
|
|
125
137
|
enabled?: boolean;
|
|
138
|
+
syntax?: ScheduleSyntax;
|
|
139
|
+
expression?: string;
|
|
126
140
|
},
|
|
127
141
|
): ScheduleJob | null {
|
|
128
142
|
const db = getDb();
|
|
129
|
-
const existing = db.select().from(
|
|
143
|
+
const existing = db.select().from(scheduleJobs).where(eq(scheduleJobs.id, id)).get();
|
|
130
144
|
if (!existing) return null;
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
// Resolve the effective syntax and expression after this update
|
|
147
|
+
const newSyntax = updates.syntax ?? (existing.scheduleSyntax as ScheduleSyntax) ?? 'cron';
|
|
148
|
+
const newExpr = updates.expression ?? updates.cronExpression ?? existing.cronExpression;
|
|
149
|
+
const newTimezone = updates.timezone !== undefined ? updates.timezone : existing.timezone;
|
|
150
|
+
const newEnabled = updates.enabled !== undefined ? updates.enabled : existing.enabled;
|
|
151
|
+
|
|
152
|
+
// Validate if expression or syntax changed
|
|
153
|
+
if (updates.expression !== undefined || updates.cronExpression !== undefined || updates.syntax !== undefined) {
|
|
154
|
+
const spec = { syntax: newSyntax, expression: newExpr, timezone: newTimezone };
|
|
155
|
+
if (!isValidScheduleExpression(spec)) {
|
|
156
|
+
throw new Error(`Invalid ${newSyntax} expression: "${newExpr}"`);
|
|
157
|
+
}
|
|
134
158
|
}
|
|
135
159
|
|
|
136
160
|
const now = Date.now();
|
|
137
161
|
const set: Record<string, unknown> = { updatedAt: now };
|
|
138
162
|
|
|
139
163
|
if (updates.name !== undefined) set.name = updates.name;
|
|
140
|
-
if (updates.cronExpression !== undefined) set.cronExpression =
|
|
164
|
+
if (updates.cronExpression !== undefined || updates.expression !== undefined) set.cronExpression = newExpr;
|
|
165
|
+
if (updates.syntax !== undefined) set.scheduleSyntax = newSyntax;
|
|
141
166
|
if (updates.timezone !== undefined) set.timezone = updates.timezone;
|
|
142
167
|
if (updates.message !== undefined) set.message = updates.message;
|
|
143
168
|
if (updates.enabled !== undefined) set.enabled = updates.enabled;
|
|
144
169
|
|
|
145
|
-
|
|
146
|
-
const newTimezone = updates.timezone !== undefined ? updates.timezone : existing.timezone;
|
|
147
|
-
const newEnabled = updates.enabled !== undefined ? updates.enabled : existing.enabled;
|
|
148
|
-
|
|
170
|
+
// Recompute nextRunAt if schedule timing may have changed
|
|
149
171
|
if (
|
|
150
172
|
updates.cronExpression !== undefined ||
|
|
173
|
+
updates.expression !== undefined ||
|
|
174
|
+
updates.syntax !== undefined ||
|
|
151
175
|
updates.timezone !== undefined ||
|
|
152
176
|
updates.enabled !== undefined
|
|
153
177
|
) {
|
|
154
|
-
|
|
178
|
+
const spec = { syntax: newSyntax, expression: newExpr, timezone: newTimezone };
|
|
179
|
+
set.nextRunAt = newEnabled ? computeNextRunAtEngine(spec) : 0;
|
|
155
180
|
}
|
|
156
181
|
|
|
157
|
-
db.update(
|
|
182
|
+
db.update(scheduleJobs).set(set).where(eq(scheduleJobs.id, id)).run();
|
|
158
183
|
|
|
159
184
|
return getSchedule(id);
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
export function deleteSchedule(id: string): boolean {
|
|
163
188
|
const db = getDb();
|
|
164
|
-
const result = db.delete(
|
|
189
|
+
const result = db.delete(scheduleJobs).where(eq(scheduleJobs.id, id)).run() as unknown as { changes?: number };
|
|
165
190
|
return (result.changes ?? 0) > 0;
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
/**
|
|
169
|
-
* Claim due schedules atomically. For each candidate where
|
|
170
|
-
* next_run_at <= now, we advance next_run_at using
|
|
171
|
-
* old value to prevent double-claiming by
|
|
194
|
+
* Claim due recurrence schedules atomically. For each candidate where
|
|
195
|
+
* enabled=true and next_run_at <= now, we advance next_run_at using
|
|
196
|
+
* optimistic locking on the old value to prevent double-claiming by
|
|
197
|
+
* concurrent ticks. Works for both cron and RRULE syntax.
|
|
172
198
|
*/
|
|
173
199
|
export function claimDueSchedules(now: number): ScheduleJob[] {
|
|
174
200
|
const db = getDb();
|
|
175
201
|
const candidates = db
|
|
176
202
|
.select()
|
|
177
|
-
.from(
|
|
178
|
-
.where(and(eq(
|
|
179
|
-
.orderBy(asc(
|
|
203
|
+
.from(scheduleJobs)
|
|
204
|
+
.where(and(eq(scheduleJobs.enabled, true), lte(scheduleJobs.nextRunAt, now)))
|
|
205
|
+
.orderBy(asc(scheduleJobs.nextRunAt))
|
|
180
206
|
.all();
|
|
181
207
|
|
|
182
208
|
const claimed: ScheduleJob[] = [];
|
|
183
209
|
for (const row of candidates) {
|
|
184
|
-
let newNextRunAt: number;
|
|
210
|
+
let newNextRunAt: number | null;
|
|
211
|
+
let exhausted = false;
|
|
185
212
|
try {
|
|
186
|
-
|
|
213
|
+
const syntax = (row.scheduleSyntax as ScheduleSyntax) ?? 'cron';
|
|
214
|
+
newNextRunAt = computeNextRunAtEngine({
|
|
215
|
+
syntax,
|
|
216
|
+
expression: row.cronExpression,
|
|
217
|
+
timezone: row.timezone,
|
|
218
|
+
});
|
|
187
219
|
} catch {
|
|
188
|
-
//
|
|
189
|
-
|
|
220
|
+
// Finite schedule with no future runs — still claim the current due
|
|
221
|
+
// run but disable the schedule so it doesn't fire again.
|
|
222
|
+
newNextRunAt = null;
|
|
223
|
+
exhausted = true;
|
|
190
224
|
}
|
|
191
225
|
|
|
192
226
|
// Optimistic lock: only update if nextRunAt hasn't changed
|
|
227
|
+
const updates: Record<string, unknown> = {
|
|
228
|
+
lastRunAt: now,
|
|
229
|
+
updatedAt: now,
|
|
230
|
+
};
|
|
231
|
+
if (exhausted) {
|
|
232
|
+
updates.nextRunAt = 0;
|
|
233
|
+
updates.enabled = false;
|
|
234
|
+
} else {
|
|
235
|
+
updates.nextRunAt = newNextRunAt!;
|
|
236
|
+
}
|
|
237
|
+
|
|
193
238
|
const result = db
|
|
194
|
-
.update(
|
|
195
|
-
.set(
|
|
196
|
-
|
|
197
|
-
lastRunAt: now,
|
|
198
|
-
updatedAt: now,
|
|
199
|
-
})
|
|
200
|
-
.where(and(eq(cronJobs.id, row.id), eq(cronJobs.nextRunAt, row.nextRunAt)))
|
|
239
|
+
.update(scheduleJobs)
|
|
240
|
+
.set(updates)
|
|
241
|
+
.where(and(eq(scheduleJobs.id, row.id), eq(scheduleJobs.nextRunAt, row.nextRunAt)))
|
|
201
242
|
.run() as unknown as { changes?: number };
|
|
202
243
|
|
|
203
244
|
if ((result.changes ?? 0) === 0) continue;
|
|
204
245
|
|
|
205
246
|
claimed.push(parseJobRow({
|
|
206
247
|
...row,
|
|
207
|
-
nextRunAt: newNextRunAt
|
|
248
|
+
nextRunAt: exhausted ? 0 : newNextRunAt!,
|
|
208
249
|
lastRunAt: now,
|
|
209
250
|
updatedAt: now,
|
|
251
|
+
enabled: exhausted ? false : row.enabled,
|
|
210
252
|
}));
|
|
211
253
|
}
|
|
212
254
|
return claimed;
|
|
@@ -216,7 +258,7 @@ export function createScheduleRun(jobId: string, conversationId: string): string
|
|
|
216
258
|
const db = getDb();
|
|
217
259
|
const id = uuid();
|
|
218
260
|
const now = Date.now();
|
|
219
|
-
db.insert(
|
|
261
|
+
db.insert(scheduleRuns).values({
|
|
220
262
|
id,
|
|
221
263
|
jobId,
|
|
222
264
|
status: 'running',
|
|
@@ -238,12 +280,12 @@ export function completeScheduleRun(
|
|
|
238
280
|
const db = getDb();
|
|
239
281
|
const now = Date.now();
|
|
240
282
|
|
|
241
|
-
const run = db.select().from(
|
|
283
|
+
const run = db.select().from(scheduleRuns).where(eq(scheduleRuns.id, runId)).get();
|
|
242
284
|
if (!run) return;
|
|
243
285
|
|
|
244
286
|
const durationMs = now - run.startedAt;
|
|
245
287
|
|
|
246
|
-
db.update(
|
|
288
|
+
db.update(scheduleRuns)
|
|
247
289
|
.set({
|
|
248
290
|
status: result.status,
|
|
249
291
|
finishedAt: now,
|
|
@@ -251,23 +293,23 @@ export function completeScheduleRun(
|
|
|
251
293
|
output: result.output?.slice(0, 10_000) ?? null,
|
|
252
294
|
error: result.error?.slice(0, 2000) ?? null,
|
|
253
295
|
})
|
|
254
|
-
.where(eq(
|
|
296
|
+
.where(eq(scheduleRuns.id, runId))
|
|
255
297
|
.run();
|
|
256
298
|
|
|
257
299
|
// Update the parent job's lastStatus and retryCount
|
|
258
300
|
if (result.status === 'error') {
|
|
259
301
|
// Increment retry count
|
|
260
|
-
const job = db.select().from(
|
|
302
|
+
const job = db.select().from(scheduleJobs).where(eq(scheduleJobs.id, run.jobId)).get();
|
|
261
303
|
if (job) {
|
|
262
|
-
db.update(
|
|
304
|
+
db.update(scheduleJobs)
|
|
263
305
|
.set({ lastStatus: 'error', retryCount: job.retryCount + 1, updatedAt: now })
|
|
264
|
-
.where(eq(
|
|
306
|
+
.where(eq(scheduleJobs.id, run.jobId))
|
|
265
307
|
.run();
|
|
266
308
|
}
|
|
267
309
|
} else {
|
|
268
|
-
db.update(
|
|
310
|
+
db.update(scheduleJobs)
|
|
269
311
|
.set({ lastStatus: 'ok', retryCount: 0, updatedAt: now })
|
|
270
|
-
.where(eq(
|
|
312
|
+
.where(eq(scheduleJobs.id, run.jobId))
|
|
271
313
|
.run();
|
|
272
314
|
}
|
|
273
315
|
}
|
|
@@ -276,9 +318,9 @@ export function getScheduleRuns(jobId: string, limit?: number): ScheduleRun[] {
|
|
|
276
318
|
const db = getDb();
|
|
277
319
|
const rows = db
|
|
278
320
|
.select()
|
|
279
|
-
.from(
|
|
280
|
-
.where(eq(
|
|
281
|
-
.orderBy(desc(
|
|
321
|
+
.from(scheduleRuns)
|
|
322
|
+
.where(eq(scheduleRuns.jobId, jobId))
|
|
323
|
+
.orderBy(desc(scheduleRuns.createdAt))
|
|
282
324
|
.limit(limit ?? 10)
|
|
283
325
|
.all();
|
|
284
326
|
return rows.map(parseRunRow);
|
|
@@ -296,7 +338,8 @@ export function formatLocalDate(timestamp: number): string {
|
|
|
296
338
|
}
|
|
297
339
|
|
|
298
340
|
// Convert a cron expression to a human-readable description.
|
|
299
|
-
//
|
|
341
|
+
// Only applicable to cron syntax; RRULE schedules should display the
|
|
342
|
+
// raw expression text instead.
|
|
300
343
|
//
|
|
301
344
|
// Examples:
|
|
302
345
|
// "* * * * *" -> "Every minute"
|
|
@@ -418,11 +461,13 @@ export function describeCronExpression(expr: string): string {
|
|
|
418
461
|
}
|
|
419
462
|
}
|
|
420
463
|
|
|
421
|
-
function parseJobRow(row: typeof
|
|
464
|
+
function parseJobRow(row: typeof scheduleJobs.$inferSelect): ScheduleJob {
|
|
422
465
|
return {
|
|
423
466
|
id: row.id,
|
|
424
467
|
name: row.name,
|
|
425
468
|
enabled: row.enabled,
|
|
469
|
+
syntax: (row.scheduleSyntax as ScheduleSyntax) ?? 'cron',
|
|
470
|
+
expression: row.cronExpression,
|
|
426
471
|
cronExpression: row.cronExpression,
|
|
427
472
|
timezone: row.timezone,
|
|
428
473
|
message: row.message,
|
|
@@ -436,7 +481,7 @@ function parseJobRow(row: typeof cronJobs.$inferSelect): ScheduleJob {
|
|
|
436
481
|
};
|
|
437
482
|
}
|
|
438
483
|
|
|
439
|
-
function parseRunRow(row: typeof
|
|
484
|
+
function parseRunRow(row: typeof scheduleRuns.$inferSelect): ScheduleRun {
|
|
440
485
|
return {
|
|
441
486
|
id: row.id,
|
|
442
487
|
jobId: row.jobId,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createScheduleRun,
|
|
6
6
|
completeScheduleRun,
|
|
7
7
|
} from './schedule-store.js';
|
|
8
|
+
import { hasSetConstructs } from './recurrence-engine.js';
|
|
8
9
|
import { claimDueReminders, completeReminder, failReminder, setReminderConversationId } from '../tools/reminder/reminder-store.js';
|
|
9
10
|
import { runWatchersOnce, type WatcherNotifier, type WatcherEscalator } from '../watcher/engine.js';
|
|
10
11
|
|
|
@@ -73,19 +74,20 @@ async function runScheduleOnce(
|
|
|
73
74
|
const now = Date.now();
|
|
74
75
|
let processed = 0;
|
|
75
76
|
|
|
76
|
-
// ──
|
|
77
|
+
// ── Recurrence schedules (cron + RRULE) ─────────────────────────────
|
|
77
78
|
const jobs = claimDueSchedules(now);
|
|
78
79
|
for (const job of jobs) {
|
|
79
80
|
// Check if message is a task invocation (run_task:<task_id>)
|
|
80
81
|
const taskMatch = job.message.match(/^run_task:(\S+)$/);
|
|
81
82
|
if (taskMatch) {
|
|
82
83
|
const taskId = taskMatch[1];
|
|
84
|
+
const isRruleSet = job.syntax === 'rrule' && hasSetConstructs(job.expression);
|
|
83
85
|
try {
|
|
84
|
-
log.info({ jobId: job.id, name: job.name, taskId }, 'Executing scheduled task');
|
|
86
|
+
log.info({ jobId: job.id, name: job.name, taskId, syntax: job.syntax, expression: job.expression, isRruleSet }, 'Executing scheduled task');
|
|
85
87
|
const { runTask } = await import('../tasks/task-runner.js');
|
|
86
88
|
const result = await runTask(
|
|
87
89
|
{ taskId, workingDir: process.cwd() },
|
|
88
|
-
processMessage as (conversationId: string, message: string) => Promise<void>,
|
|
90
|
+
processMessage as (conversationId: string, message: string, taskRunId: string) => Promise<void>,
|
|
89
91
|
);
|
|
90
92
|
|
|
91
93
|
// Track the schedule run using the task's conversation
|
|
@@ -99,7 +101,7 @@ async function runScheduleOnce(
|
|
|
99
101
|
processed += 1;
|
|
100
102
|
} catch (err) {
|
|
101
103
|
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
-
log.warn({ err, jobId: job.id, name: job.name, taskId }, 'Scheduled task execution failed');
|
|
104
|
+
log.warn({ err, jobId: job.id, name: job.name, taskId, syntax: job.syntax, expression: job.expression, isRruleSet }, 'Scheduled task execution failed');
|
|
103
105
|
// Create a fallback conversation for the schedule run record
|
|
104
106
|
const fallbackConversation = createConversation(`Schedule: ${job.name}`);
|
|
105
107
|
const runId = createScheduleRun(job.id, fallbackConversation.id);
|
|
@@ -110,16 +112,17 @@ async function runScheduleOnce(
|
|
|
110
112
|
|
|
111
113
|
const conversation = createConversation(`Schedule: ${job.name}`);
|
|
112
114
|
const runId = createScheduleRun(job.id, conversation.id);
|
|
115
|
+
const isRruleSetMsg = job.syntax === 'rrule' && hasSetConstructs(job.expression);
|
|
113
116
|
|
|
114
117
|
try {
|
|
115
|
-
log.info({ jobId: job.id, name: job.name, conversationId: conversation.id }, 'Executing schedule');
|
|
118
|
+
log.info({ jobId: job.id, name: job.name, syntax: job.syntax, expression: job.expression, isRruleSet: isRruleSetMsg, conversationId: conversation.id }, 'Executing schedule');
|
|
116
119
|
await processMessage(conversation.id, job.message);
|
|
117
120
|
completeScheduleRun(runId, { status: 'ok' });
|
|
118
121
|
notifySchedule({ id: job.id, name: job.name });
|
|
119
122
|
processed += 1;
|
|
120
123
|
} catch (err) {
|
|
121
124
|
const message = err instanceof Error ? err.message : String(err);
|
|
122
|
-
log.warn({ err, jobId: job.id, name: job.name }, 'Schedule execution failed');
|
|
125
|
+
log.warn({ err, jobId: job.id, name: job.name, syntax: job.syntax, expression: job.expression, isRruleSet: isRruleSetMsg }, 'Schedule execution failed');
|
|
123
126
|
completeScheduleRun(runId, { status: 'error', error: message });
|
|
124
127
|
}
|
|
125
128
|
}
|
package/src/security/oauth2.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { randomBytes, createHash } from 'node:crypto';
|
|
9
|
+
import { getLogger } from '../util/logger.js';
|
|
10
|
+
|
|
11
|
+
const log = getLogger('oauth2');
|
|
9
12
|
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
11
14
|
// Types
|
|
@@ -170,8 +173,19 @@ export async function startOAuth2Flow(
|
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
if (!tokenResp.ok) {
|
|
173
|
-
const
|
|
174
|
-
|
|
176
|
+
const rawBody = await tokenResp.text().catch(() => '');
|
|
177
|
+
let safeDetail: Record<string, unknown> = {};
|
|
178
|
+
let errorCode = '';
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(rawBody) as Record<string, unknown>;
|
|
181
|
+
if (parsed.error) { safeDetail.error = String(parsed.error); errorCode = String(parsed.error); }
|
|
182
|
+
if (parsed.error_description) safeDetail.error_description = String(parsed.error_description);
|
|
183
|
+
} catch {
|
|
184
|
+
safeDetail.error = '[non-JSON response]';
|
|
185
|
+
}
|
|
186
|
+
log.error({ status: tokenResp.status, ...safeDetail }, 'OAuth2 token exchange failed');
|
|
187
|
+
const detail = errorCode ? `HTTP ${tokenResp.status}: ${errorCode}` : `HTTP ${tokenResp.status}`;
|
|
188
|
+
throw new Error(`OAuth2 token exchange failed (${detail})`);
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
const tokenData = await tokenResp.json() as Record<string, unknown>;
|
|
@@ -225,8 +239,19 @@ export async function refreshOAuth2Token(
|
|
|
225
239
|
});
|
|
226
240
|
|
|
227
241
|
if (!resp.ok) {
|
|
228
|
-
const
|
|
229
|
-
|
|
242
|
+
const rawBody = await resp.text().catch(() => '');
|
|
243
|
+
let safeDetail: Record<string, unknown> = {};
|
|
244
|
+
let errorCode = '';
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(rawBody) as Record<string, unknown>;
|
|
247
|
+
if (parsed.error) { safeDetail.error = String(parsed.error); errorCode = String(parsed.error); }
|
|
248
|
+
if (parsed.error_description) safeDetail.error_description = String(parsed.error_description);
|
|
249
|
+
} catch {
|
|
250
|
+
safeDetail.error = '[non-JSON response]';
|
|
251
|
+
}
|
|
252
|
+
log.error({ status: resp.status, ...safeDetail }, 'OAuth2 token refresh failed');
|
|
253
|
+
const detail = errorCode ? `HTTP ${resp.status}: ${errorCode}` : `HTTP ${resp.status}`;
|
|
254
|
+
throw new Error(`OAuth2 token refresh failed (${detail})`);
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
const data = await resp.json() as Record<string, unknown>;
|