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
|
@@ -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>;
|
|
@@ -104,6 +104,52 @@ export function isAllowlisted(value: string): boolean {
|
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
export interface AllowlistValidationError {
|
|
108
|
+
index: number;
|
|
109
|
+
pattern: string;
|
|
110
|
+
message: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate all regex patterns in an allowlist config without loading them.
|
|
115
|
+
* Returns an array of validation errors (empty = all valid).
|
|
116
|
+
*/
|
|
117
|
+
export function validateAllowlist(config: AllowlistConfig): AllowlistValidationError[] {
|
|
118
|
+
const errors: AllowlistValidationError[] = [];
|
|
119
|
+
if (!config.patterns) return errors;
|
|
120
|
+
if (!Array.isArray(config.patterns)) {
|
|
121
|
+
errors.push({ index: -1, pattern: String(config.patterns), message: '"patterns" must be an array' });
|
|
122
|
+
return errors;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < config.patterns.length; i++) {
|
|
126
|
+
const p = config.patterns[i];
|
|
127
|
+
if (typeof p !== 'string') {
|
|
128
|
+
errors.push({ index: i, pattern: String(p), message: 'Pattern is not a string' });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
new RegExp(p);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
errors.push({ index: i, pattern: p, message: (err as Error).message });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return errors;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Read secret-allowlist.json from disk and validate it.
|
|
142
|
+
* Returns validation errors, or null if the file doesn't exist.
|
|
143
|
+
*/
|
|
144
|
+
export function validateAllowlistFile(): AllowlistValidationError[] | null {
|
|
145
|
+
const filePath = join(getRootDir(), 'protected', 'secret-allowlist.json');
|
|
146
|
+
if (!existsSync(filePath)) return null;
|
|
147
|
+
|
|
148
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
149
|
+
const config: AllowlistConfig = JSON.parse(raw);
|
|
150
|
+
return validateAllowlist(config);
|
|
151
|
+
}
|
|
152
|
+
|
|
107
153
|
/**
|
|
108
154
|
* Reset cached state so the allowlist is reloaded on next check.
|
|
109
155
|
* Called by the daemon file watcher when secret-allowlist.json changes,
|
package/src/skills/clawhub.ts
CHANGED
|
@@ -395,7 +395,7 @@ export async function clawhubInspect(slug: string): Promise<{ data?: ClawhubInsp
|
|
|
395
395
|
size: (f.size as number) ?? 0,
|
|
396
396
|
contentType: (f.contentType as string) ?? undefined,
|
|
397
397
|
})) : null,
|
|
398
|
-
skillMdContent: parsed.skillMdContent ?? parsed.fileContents?.['SKILL.md'] ?? null,
|
|
398
|
+
skillMdContent: parsed.skillMdContent ?? parsed.fileContents?.['SKILL.md'] ?? parsed.file?.content ?? null,
|
|
399
399
|
};
|
|
400
400
|
return { data };
|
|
401
401
|
} catch {
|
package/src/subagent/manager.ts
CHANGED
|
@@ -52,10 +52,18 @@ interface ManagedSubagent {
|
|
|
52
52
|
parentSendToClient: (msg: ServerMessage) => void;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface SubagentNotificationInfo {
|
|
56
|
+
subagentId: string;
|
|
57
|
+
label: string;
|
|
58
|
+
status: 'completed' | 'failed' | 'aborted';
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export type ParentNotifyCallback = (
|
|
56
63
|
parentSessionId: string,
|
|
57
64
|
message: string,
|
|
58
65
|
sendToClient: (msg: ServerMessage) => void,
|
|
66
|
+
notification: SubagentNotificationInfo,
|
|
59
67
|
) => void;
|
|
60
68
|
|
|
61
69
|
export class SubagentManager {
|
|
@@ -221,6 +229,8 @@ export class SubagentManager {
|
|
|
221
229
|
await managed.session.runAgentLoop(objective, messageId, onEvent);
|
|
222
230
|
|
|
223
231
|
// Agent loop completed successfully.
|
|
232
|
+
// Copy usage stats from the session before sending status (which includes usage).
|
|
233
|
+
managed.state.usage = { ...managed.session.usageStats };
|
|
224
234
|
// Only update state + notify if still non-terminal (guards against abort race).
|
|
225
235
|
if (!TERMINAL_STATUSES.has(managed.state.status)) {
|
|
226
236
|
managed.state.completedAt = Date.now();
|
|
@@ -235,6 +245,7 @@ export class SubagentManager {
|
|
|
235
245
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
236
246
|
managed.state.error = errorMsg;
|
|
237
247
|
managed.state.completedAt = Date.now();
|
|
248
|
+
managed.state.usage = { ...managed.session.usageStats };
|
|
238
249
|
|
|
239
250
|
// Only update status if not already terminal (e.g. aborted).
|
|
240
251
|
if (!TERMINAL_STATUSES.has(managed.state.status)) {
|
|
@@ -267,16 +278,28 @@ export class SubagentManager {
|
|
|
267
278
|
managed.session.abort();
|
|
268
279
|
managed.state.completedAt = Date.now();
|
|
269
280
|
if (parentSendToClient) {
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
//
|
|
281
|
+
// Route the status update through the stored parent sender so the
|
|
282
|
+
// owning session's UI chip updates, even when the abort comes from a
|
|
283
|
+
// different socket (e.g. after thread switching). Fall back to the
|
|
284
|
+
// caller-provided sender if no stored sender exists.
|
|
285
|
+
const statusSender = managed.parentSendToClient ?? parentSendToClient;
|
|
286
|
+
this.setStatus(subagentId, 'aborted', statusSender);
|
|
287
|
+
// Notify parent that the subagent was explicitly aborted — tell it NOT to re-spawn.
|
|
288
|
+
// Skip when the parent LLM itself called subagent_abort (it already has the tool result).
|
|
273
289
|
if (this.onSubagentFinished && !options?.suppressNotification) {
|
|
274
290
|
const label = managed.state.config.label;
|
|
291
|
+
const message =
|
|
292
|
+
`[Subagent "${label}" was explicitly aborted]\n\n` +
|
|
293
|
+
`This subagent was cancelled on purpose. Do NOT re-spawn or retry it.`;
|
|
275
294
|
try {
|
|
295
|
+
// Use the managed subagent's stored parentSendToClient so the
|
|
296
|
+
// notification routes to the parent session's socket, not the
|
|
297
|
+
// aborting socket (which may be a different thread after switching).
|
|
276
298
|
this.onSubagentFinished(
|
|
277
299
|
managed.state.config.parentSessionId,
|
|
278
|
-
|
|
279
|
-
parentSendToClient,
|
|
300
|
+
message,
|
|
301
|
+
managed.parentSendToClient,
|
|
302
|
+
{ subagentId, label, status: 'aborted' },
|
|
280
303
|
);
|
|
281
304
|
} catch (err) {
|
|
282
305
|
log.error({ subagentId, err }, 'Failed to notify parent about abort');
|
|
@@ -460,16 +483,25 @@ export class SubagentManager {
|
|
|
460
483
|
if (outcome === 'completed') {
|
|
461
484
|
message =
|
|
462
485
|
`[Subagent "${config.label}" completed]\n\n` +
|
|
463
|
-
`Use subagent_read with subagent_id "${config.id}" to retrieve the full output
|
|
486
|
+
`Use subagent_read with subagent_id "${config.id}" to retrieve the full output.\n` +
|
|
487
|
+
`Do NOT re-spawn this subagent — just read and share the results.`;
|
|
464
488
|
} else {
|
|
465
489
|
const error = managed.state.error ?? 'Unknown error';
|
|
466
490
|
message =
|
|
467
491
|
`[Subagent "${config.label}" failed]\n\n` +
|
|
468
|
-
`Error: ${error}
|
|
492
|
+
`Error: ${error}\n` +
|
|
493
|
+
`Do NOT re-spawn or retry this subagent unless the user explicitly asks.`;
|
|
469
494
|
}
|
|
470
495
|
|
|
496
|
+
const notification: SubagentNotificationInfo = {
|
|
497
|
+
subagentId: config.id,
|
|
498
|
+
label: config.label,
|
|
499
|
+
status: outcome,
|
|
500
|
+
...(outcome === 'failed' ? { error: managed.state.error ?? 'Unknown error' } : {}),
|
|
501
|
+
};
|
|
502
|
+
|
|
471
503
|
try {
|
|
472
|
-
this.onSubagentFinished(config.parentSessionId, message, parentSendToClient);
|
|
504
|
+
this.onSubagentFinished(config.parentSessionId, message, parentSendToClient, notification);
|
|
473
505
|
} catch (err) {
|
|
474
506
|
log.error({ subagentId: config.id, err }, 'Failed to notify parent session');
|
|
475
507
|
}
|