@swarmclawai/swarmclaw 1.9.21 → 1.9.23
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 +23 -5
- package/package.json +2 -2
- package/src/components/chat/activity-moment.tsx +4 -0
- package/src/components/chat/tool-call-bubble.tsx +6 -0
- package/src/components/schedules/schedule-console.tsx +3 -0
- package/src/lib/server/capability-router.test.ts +4 -4
- package/src/lib/server/capability-router.ts +1 -0
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +27 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +21 -0
- package/src/lib/server/chat-execution/iteration-event-handler.ts +1 -1
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -2
- package/src/lib/server/plugins-advanced.test.ts +7 -3
- package/src/lib/server/runtime/scheduler.test.ts +129 -0
- package/src/lib/server/runtime/scheduler.ts +62 -35
- package/src/lib/server/schedules/schedule-history.test.ts +14 -0
- package/src/lib/server/schedules/schedule-history.ts +1 -0
- package/src/lib/server/schedules/schedule-lifecycle.ts +5 -28
- package/src/lib/server/schedules/schedule-normalization.ts +6 -28
- package/src/lib/server/schedules/schedule-timing.test.ts +80 -0
- package/src/lib/server/schedules/schedule-timing.ts +179 -0
- package/src/lib/server/session-tools/web-crawl.test.ts +106 -0
- package/src/lib/server/session-tools/web-inputs.test.ts +5 -0
- package/src/lib/server/session-tools/web-utils.ts +8 -2
- package/src/lib/server/session-tools/web.ts +256 -29
- package/src/lib/server/storage.ts +2 -0
- package/src/lib/server/tasks/task-lifecycle.ts +35 -5
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +3 -3
- package/src/lib/server/tool-capability-policy.ts +4 -1
- package/src/lib/server/tool-planning.test.ts +2 -1
- package/src/lib/server/tool-planning.ts +31 -0
- package/src/lib/server/untrusted-content.ts +2 -2
- package/src/types/schedule.ts +2 -2
- package/src/types/session.ts +2 -0
- package/src/types/task.ts +1 -0
package/README.md
CHANGED
|
@@ -151,13 +151,13 @@ clawhub install swarmclaw
|
|
|
151
151
|
|
|
152
152
|
[Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
|
|
153
153
|
|
|
154
|
-
## v1.9.
|
|
154
|
+
## v1.9.23 Highlights
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
Schedule reliability is now more deterministic for recurring autonomous work, especially after restarts or stale stored timing state.
|
|
157
157
|
|
|
158
|
-
- **
|
|
159
|
-
- **
|
|
160
|
-
- **
|
|
158
|
+
- **Cron drift repair.** Active schedules repair stale future cron slots before they skip the nearest run.
|
|
159
|
+
- **Stable stagger.** Staggered schedules keep a deterministic per-schedule offset.
|
|
160
|
+
- **Mission continuity.** Schedule-created board tasks keep a persistent mission link across recurring runs.
|
|
161
161
|
|
|
162
162
|
## Hosted Deploys
|
|
163
163
|
|
|
@@ -409,6 +409,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
409
409
|
|
|
410
410
|
## Releases
|
|
411
411
|
|
|
412
|
+
### v1.9.23 Highlights
|
|
413
|
+
|
|
414
|
+
Schedule reliability release: recurring work now repairs stale timing state before it can skip the nearest run, and scheduled board tasks keep mission context across repeat launches.
|
|
415
|
+
|
|
416
|
+
- **Cron drift repair.** Active cron schedules repair missing or invalid `nextRunAt` values and stale future cron slots before the scheduler decides whether work is due.
|
|
417
|
+
- **Tick-time advancement.** Cron and interval schedules now advance from the scheduler tick time instead of the process wall clock, making restart and catch-up behavior deterministic.
|
|
418
|
+
- **Stable stagger.** Schedule stagger offsets are deterministic per schedule, avoiding thundering-herd launches without moving a saved next-run target on every recompute.
|
|
419
|
+
- **Mission continuity.** Schedule-created board tasks attach to a persistent mission link, so recurring runs share the same operational context.
|
|
420
|
+
|
|
421
|
+
### v1.9.22 Highlights
|
|
422
|
+
|
|
423
|
+
Research tools release: agents now get direct `web_extract` and `web_crawl` tools alongside `web_search`, `web_fetch`, and the unified `web` tool.
|
|
424
|
+
|
|
425
|
+
- **Source-grounded extraction.** `web_extract` returns a page title, canonical URL, and readable content for known source URLs.
|
|
426
|
+
- **Bounded crawls.** `web_crawl` walks same-origin links by default with conservative page and depth caps, plus an explicit external-link opt-in.
|
|
427
|
+
- **Better routing.** Tool aliases, capability policy, planning hints, continuation recovery, and the chat UI all recognize the granular research tools.
|
|
428
|
+
- **Regression coverage.** New tests cover action inference, tool-call translation, direct tool registration, extraction cleanup, and same-origin crawl bounds.
|
|
429
|
+
|
|
412
430
|
### v1.9.21 Highlights
|
|
413
431
|
|
|
414
432
|
Provider diagnostics release: connection checks now return a structured step timeline across setup, provider settings, and agent editing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.23",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
90
90
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
91
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -19,6 +19,9 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
|
|
|
19
19
|
delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
|
|
20
20
|
check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
|
|
21
21
|
web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
|
|
22
|
+
web_fetch: { label: 'Read a web page', color: '#22C55E', icon: 'search' },
|
|
23
|
+
web_extract: { label: 'Extracted a web page', color: '#22C55E', icon: 'search' },
|
|
24
|
+
web_crawl: { label: 'Crawled a site', color: '#22C55E', icon: 'search' },
|
|
22
25
|
connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -35,6 +38,7 @@ function extractSnippet(toolName: string, toolInput: string): string | null {
|
|
|
35
38
|
if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
|
|
36
39
|
if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
|
|
37
40
|
if (toolName === 'web_search' && parsed.query) return parsed.query
|
|
41
|
+
if ((toolName === 'web_fetch' || toolName === 'web_extract' || toolName === 'web_crawl') && parsed.url) return parsed.url
|
|
38
42
|
if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
|
|
39
43
|
} catch { /* ignore parse errors */ }
|
|
40
44
|
return null
|
|
@@ -20,6 +20,8 @@ const TOOL_COLORS: Record<string, string> = {
|
|
|
20
20
|
create_spreadsheet: '#10B981',
|
|
21
21
|
web_search: '#3B82F6',
|
|
22
22
|
web_fetch: '#3B82F6',
|
|
23
|
+
web_extract: '#3B82F6',
|
|
24
|
+
web_crawl: '#3B82F6',
|
|
23
25
|
spawn_subagent: '#8B5CF6',
|
|
24
26
|
delegate_to_agent: '#6366F1',
|
|
25
27
|
check_delegation_status: '#6366F1',
|
|
@@ -77,6 +79,8 @@ export const TOOL_LABELS: Record<string, string> = {
|
|
|
77
79
|
create_spreadsheet: 'Create Spreadsheet',
|
|
78
80
|
web_search: 'Web Search',
|
|
79
81
|
web_fetch: 'Web Fetch',
|
|
82
|
+
web_extract: 'Web Extract',
|
|
83
|
+
web_crawl: 'Web Crawl',
|
|
80
84
|
claude_code: 'Claude Code',
|
|
81
85
|
codex_cli: 'Codex CLI',
|
|
82
86
|
opencode_cli: 'OpenCode CLI',
|
|
@@ -127,6 +131,8 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
127
131
|
create_spreadsheet: 'Create Excel or CSV files from structured data',
|
|
128
132
|
web_search: 'Search the web for information',
|
|
129
133
|
web_fetch: 'Fetch and read web page content',
|
|
134
|
+
web_extract: 'Extract readable content from a source URL',
|
|
135
|
+
web_crawl: 'Crawl a bounded set of pages from one site',
|
|
130
136
|
claude_code: 'Enable delegation to Claude Code CLI',
|
|
131
137
|
codex_cli: 'Enable delegation to OpenAI Codex CLI',
|
|
132
138
|
opencode_cli: 'Enable delegation to OpenCode CLI',
|
|
@@ -162,6 +162,8 @@ function historyActionLabel(action: ScheduleHistoryEntry['action']): string {
|
|
|
162
162
|
return 'Skipped'
|
|
163
163
|
case 'failed':
|
|
164
164
|
return 'Failed'
|
|
165
|
+
case 'repaired':
|
|
166
|
+
return 'Repaired'
|
|
165
167
|
default:
|
|
166
168
|
return action
|
|
167
169
|
}
|
|
@@ -171,6 +173,7 @@ function historyActionBadge(action: ScheduleHistoryEntry['action']): string {
|
|
|
171
173
|
if (action === 'created' || action === 'restored' || action === 'run_started') return badgeClass('completed')
|
|
172
174
|
if (action === 'failed') return badgeClass('failed')
|
|
173
175
|
if (action === 'skipped' || action === 'archived') return badgeClass('paused')
|
|
176
|
+
if (action === 'repaired') return badgeClass('running')
|
|
174
177
|
return badgeClass('running')
|
|
175
178
|
}
|
|
176
179
|
|
|
@@ -26,7 +26,7 @@ test('routeTaskIntent keeps coding prompts prioritized over memory keywords', ()
|
|
|
26
26
|
test('routeTaskIntent keeps hybrid research-plus-media prompts in research intent', () => {
|
|
27
27
|
const decision = routeTaskIntent(
|
|
28
28
|
'Can you tell me more if there is any news related to the US-Iran war, and can you send me some screenshots and give me a summary and maybe send me a voice note about it?',
|
|
29
|
-
['web_search', 'web_fetch', 'browser', 'manage_connectors'],
|
|
29
|
+
['web_search', 'web_fetch', 'web_crawl', 'browser', 'manage_connectors'],
|
|
30
30
|
null,
|
|
31
31
|
makeClassification({
|
|
32
32
|
taskIntent: 'research',
|
|
@@ -39,7 +39,7 @@ test('routeTaskIntent keeps hybrid research-plus-media prompts in research inten
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
assert.equal(decision.intent, 'research')
|
|
42
|
-
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'browser', 'connector_message_tool'])
|
|
42
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser', 'connector_message_tool'])
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
|
|
@@ -72,7 +72,7 @@ test('routeTaskIntent treats keep-watching update requests as research even with
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
assert.equal(decision.intent, 'research')
|
|
75
|
-
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch'])
|
|
75
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'web_extract', 'web_crawl'])
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
test('routeTaskIntent uses structured classification when available', () => {
|
|
@@ -99,7 +99,7 @@ test('routeTaskIntent uses structured classification when available', () => {
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
assert.equal(decision.intent, 'browsing')
|
|
102
|
-
assert.deepEqual(decision.preferredTools, ['browser', 'web_fetch'])
|
|
102
|
+
assert.deepEqual(decision.preferredTools, ['browser', 'web_fetch', 'web_extract'])
|
|
103
103
|
})
|
|
104
104
|
|
|
105
105
|
function makeClassification(overrides: Partial<MessageClassification>): MessageClassification {
|
|
@@ -144,6 +144,7 @@ export function routeTaskIntent(
|
|
|
144
144
|
[
|
|
145
145
|
TOOL_CAPABILITY.researchSearch,
|
|
146
146
|
TOOL_CAPABILITY.researchFetch,
|
|
147
|
+
TOOL_CAPABILITY.researchCrawl,
|
|
147
148
|
...(wantsScreenshots ? [TOOL_CAPABILITY.browserCapture] : []),
|
|
148
149
|
...(wantsVoiceDelivery ? [TOOL_CAPABILITY.deliveryVoiceNote] : []),
|
|
149
150
|
...(wantsOutboundDelivery ? [TOOL_CAPABILITY.deliveryMedia, TOOL_CAPABILITY.deliveryMessage] : []),
|
|
@@ -407,6 +407,33 @@ describe('translateRequestedToolInvocation advanced', () => {
|
|
|
407
407
|
assert.equal(args.action, 'search')
|
|
408
408
|
assert.equal(args.query, 'test query')
|
|
409
409
|
})
|
|
410
|
+
|
|
411
|
+
it('maps web_extract to web with action=extract', () => {
|
|
412
|
+
const { toolName, args } = translateRequestedToolInvocation(
|
|
413
|
+
'web_extract',
|
|
414
|
+
{ url: 'https://example.com/source' },
|
|
415
|
+
'',
|
|
416
|
+
['web'],
|
|
417
|
+
)
|
|
418
|
+
assert.equal(toolName, 'web')
|
|
419
|
+
assert.equal(args.action, 'extract')
|
|
420
|
+
assert.equal(args.url, 'https://example.com/source')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('maps web_crawl to web with bounded crawl arguments', () => {
|
|
424
|
+
const { toolName, args } = translateRequestedToolInvocation(
|
|
425
|
+
'web_crawl',
|
|
426
|
+
{ url: 'https://example.com/', maxPages: 4, maxDepth: 1, includeExternal: false },
|
|
427
|
+
'',
|
|
428
|
+
['web'],
|
|
429
|
+
)
|
|
430
|
+
assert.equal(toolName, 'web')
|
|
431
|
+
assert.equal(args.action, 'crawl')
|
|
432
|
+
assert.equal(args.url, 'https://example.com/')
|
|
433
|
+
assert.equal(args.maxPages, 4)
|
|
434
|
+
assert.equal(args.maxDepth, 1)
|
|
435
|
+
assert.equal(args.includeExternal, false)
|
|
436
|
+
})
|
|
410
437
|
})
|
|
411
438
|
|
|
412
439
|
// ---------------------------------------------------------------------------
|
|
@@ -127,6 +127,27 @@ export function translateRequestedToolInvocation(
|
|
|
127
127
|
},
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
if (requestedName === 'web_extract') {
|
|
131
|
+
return {
|
|
132
|
+
toolName: 'web',
|
|
133
|
+
args: {
|
|
134
|
+
action: 'extract',
|
|
135
|
+
url: rawArgs.url,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (requestedName === 'web_crawl') {
|
|
140
|
+
return {
|
|
141
|
+
toolName: 'web',
|
|
142
|
+
args: {
|
|
143
|
+
action: 'crawl',
|
|
144
|
+
url: rawArgs.url || rawArgs.query,
|
|
145
|
+
maxPages: rawArgs.maxPages ?? rawArgs.maxResults,
|
|
146
|
+
maxDepth: rawArgs.maxDepth,
|
|
147
|
+
includeExternal: rawArgs.includeExternal,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
130
151
|
if (requestedName === 'delegate_to_claude_code') {
|
|
131
152
|
return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
|
|
132
153
|
}
|
|
@@ -349,7 +349,7 @@ export async function processIterationEvents(opts: ProcessIterationEventsOpts):
|
|
|
349
349
|
}
|
|
350
350
|
if (
|
|
351
351
|
boundedExternalExecutionTask
|
|
352
|
-
&& ['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(toolName)
|
|
352
|
+
&& ['http_request', 'web', 'web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser'].includes(toolName)
|
|
353
353
|
&& countExternalExecutionResearchSteps(state.streamedToolEvents) >= 5
|
|
354
354
|
&& countDistinctExternalResearchHosts(state.streamedToolEvents) >= 3
|
|
355
355
|
) {
|
|
@@ -196,7 +196,7 @@ function getRequestedArtifactStatus(params: {
|
|
|
196
196
|
|
|
197
197
|
export function countExternalExecutionResearchSteps(toolEvents: MessageToolEvent[]): number {
|
|
198
198
|
return toolEvents.filter((event) => {
|
|
199
|
-
return ['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(event.name)
|
|
199
|
+
return ['http_request', 'web', 'web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser'].includes(event.name)
|
|
200
200
|
}).length
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -300,6 +300,8 @@ const RECOVERABLE_TOOL_ERROR_NAMES = new Set([
|
|
|
300
300
|
'web',
|
|
301
301
|
'web_search',
|
|
302
302
|
'web_fetch',
|
|
303
|
+
'web_extract',
|
|
304
|
+
'web_crawl',
|
|
303
305
|
'http_request',
|
|
304
306
|
])
|
|
305
307
|
|
|
@@ -390,6 +392,8 @@ export function getToolFrequencyHint(toolName: string, sessionExtensions: string
|
|
|
390
392
|
case 'http_request':
|
|
391
393
|
case 'web_search':
|
|
392
394
|
case 'web_fetch':
|
|
395
|
+
case 'web_extract':
|
|
396
|
+
case 'web_crawl':
|
|
393
397
|
return 'Hint: You have done extensive research. Stop gathering more sources and use the information you already have to complete the task.'
|
|
394
398
|
|
|
395
399
|
case 'spawn_subagent':
|
|
@@ -490,7 +494,7 @@ function buildDeliverableFollowthroughPrompt(params: {
|
|
|
490
494
|
}
|
|
491
495
|
|
|
492
496
|
if (
|
|
493
|
-
params.toolEvents.some((event) => ['web', 'web_search', 'web_fetch', 'browser', 'http_request'].includes(event.name))
|
|
497
|
+
params.toolEvents.some((event) => ['web', 'web_search', 'web_fetch', 'web_extract', 'web_crawl', 'browser', 'http_request'].includes(event.name))
|
|
494
498
|
&& !params.toolEvents.some((event) => ['files', 'write_file', 'edit_file', 'shell', 'execute_command'].includes(event.name))
|
|
495
499
|
) {
|
|
496
500
|
lines.push(
|
|
@@ -135,11 +135,13 @@ describe('expandExtensionIds', () => {
|
|
|
135
135
|
}
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
-
it('web expands to include
|
|
138
|
+
it('web expands to include granular web tools', () => {
|
|
139
139
|
const result = expandExtensionIds(['web'])
|
|
140
140
|
assert.ok(result.includes('web'))
|
|
141
141
|
assert.ok(result.includes('web_search'))
|
|
142
142
|
assert.ok(result.includes('web_fetch'))
|
|
143
|
+
assert.ok(result.includes('web_extract'))
|
|
144
|
+
assert.ok(result.includes('web_crawl'))
|
|
143
145
|
})
|
|
144
146
|
|
|
145
147
|
it('removes duplicates after expansion', () => {
|
|
@@ -199,12 +201,14 @@ describe('expandExtensionIds', () => {
|
|
|
199
201
|
// getExtensionAliases
|
|
200
202
|
// ---------------------------------------------------------------------------
|
|
201
203
|
describe('getExtensionAliases', () => {
|
|
202
|
-
it('web returns
|
|
204
|
+
it('web returns the full web alias group', () => {
|
|
203
205
|
const result = getExtensionAliases('web')
|
|
204
206
|
assert.ok(result.includes('web'))
|
|
205
207
|
assert.ok(result.includes('web_search'))
|
|
206
208
|
assert.ok(result.includes('web_fetch'))
|
|
207
|
-
assert.
|
|
209
|
+
assert.ok(result.includes('web_extract'))
|
|
210
|
+
assert.ok(result.includes('web_crawl'))
|
|
211
|
+
assert.equal(result.length, 7) // web, web_search, web_fetch, web_extract, web_crawl, http_request, http
|
|
208
212
|
})
|
|
209
213
|
|
|
210
214
|
it('web_search returns the same group as web', () => {
|
|
@@ -202,6 +202,135 @@ describe('scheduler wake targeting', () => {
|
|
|
202
202
|
assert.deepEqual(output.deliveryModes, ['silent'])
|
|
203
203
|
})
|
|
204
204
|
|
|
205
|
+
it('repairs stale future cron next-run slots without launching a run', () => {
|
|
206
|
+
const output = runSchedulerWithTempDataDir(`
|
|
207
|
+
const storageMod = await import('@/lib/server/storage')
|
|
208
|
+
const schedulerMod = await import('@/lib/server/runtime/scheduler')
|
|
209
|
+
const storage = storageMod.default || storageMod
|
|
210
|
+
const scheduler = schedulerMod.default || schedulerMod
|
|
211
|
+
|
|
212
|
+
const now = Date.parse('2026-05-06T07:30:00.000Z')
|
|
213
|
+
const staleFuture = Date.parse('2026-05-12T08:00:00.000Z')
|
|
214
|
+
|
|
215
|
+
storage.saveSchedules({
|
|
216
|
+
'sched-cron': {
|
|
217
|
+
id: 'sched-cron',
|
|
218
|
+
name: 'Daily status',
|
|
219
|
+
agentId: 'agent-1',
|
|
220
|
+
taskPrompt: 'Send the daily status.',
|
|
221
|
+
scheduleType: 'cron',
|
|
222
|
+
cron: '0 8 * * *',
|
|
223
|
+
timezone: 'UTC',
|
|
224
|
+
status: 'active',
|
|
225
|
+
nextRunAt: staleFuture,
|
|
226
|
+
createdAt: now - 10_000,
|
|
227
|
+
updatedAt: now - 10_000,
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
await scheduler.runSchedulerTickForTests(now)
|
|
232
|
+
const schedule = storage.loadSchedules()['sched-cron']
|
|
233
|
+
|
|
234
|
+
console.log(JSON.stringify({
|
|
235
|
+
status: schedule.status,
|
|
236
|
+
nextRunAt: schedule.nextRunAt,
|
|
237
|
+
taskCount: Object.keys(storage.loadTasks()).length,
|
|
238
|
+
historyAction: schedule.history?.[0]?.action || null,
|
|
239
|
+
historyReason: schedule.history?.[0]?.metadata?.reason || null,
|
|
240
|
+
}))
|
|
241
|
+
`)
|
|
242
|
+
|
|
243
|
+
assert.equal(output.status, 'active')
|
|
244
|
+
assert.equal(output.nextRunAt, Date.parse('2026-05-06T08:00:00.000Z'))
|
|
245
|
+
assert.equal(output.taskCount, 0)
|
|
246
|
+
assert.equal(output.historyAction, 'repaired')
|
|
247
|
+
assert.equal(output.historyReason, 'stale_future')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('advances cron schedules from the scheduler tick time after firing', () => {
|
|
251
|
+
const output = runSchedulerWithTempDataDir(`
|
|
252
|
+
const storageMod = await import('@/lib/server/storage')
|
|
253
|
+
const schedulerMod = await import('@/lib/server/runtime/scheduler')
|
|
254
|
+
const heartbeatWakeMod = await import('@/lib/server/runtime/heartbeat-wake')
|
|
255
|
+
const storage = storageMod.default || storageMod
|
|
256
|
+
const scheduler = schedulerMod.default || schedulerMod
|
|
257
|
+
const heartbeatWake = heartbeatWakeMod.default || heartbeatWakeMod
|
|
258
|
+
|
|
259
|
+
const now = Date.parse('2030-01-01T08:00:30.000Z')
|
|
260
|
+
const dueAt = Date.parse('2030-01-01T08:00:00.000Z')
|
|
261
|
+
|
|
262
|
+
storage.saveAgents({
|
|
263
|
+
'agent-1': {
|
|
264
|
+
id: 'agent-1',
|
|
265
|
+
name: 'Daily Agent',
|
|
266
|
+
description: '',
|
|
267
|
+
systemPrompt: '',
|
|
268
|
+
provider: 'openai',
|
|
269
|
+
model: 'gpt-test',
|
|
270
|
+
threadSessionId: 'thread-main',
|
|
271
|
+
createdAt: now - 10_000,
|
|
272
|
+
updatedAt: now - 10_000,
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
storage.saveSessions({
|
|
277
|
+
'thread-main': {
|
|
278
|
+
id: 'thread-main',
|
|
279
|
+
name: 'Daily Agent',
|
|
280
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
281
|
+
user: 'tester',
|
|
282
|
+
provider: 'openai',
|
|
283
|
+
model: 'gpt-test',
|
|
284
|
+
claudeSessionId: null,
|
|
285
|
+
messages: [],
|
|
286
|
+
createdAt: now - 10_000,
|
|
287
|
+
lastActiveAt: now - 5_000,
|
|
288
|
+
active: true,
|
|
289
|
+
currentRunId: null,
|
|
290
|
+
agentId: 'agent-1',
|
|
291
|
+
shortcutForAgentId: 'agent-1',
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
storage.saveSchedules({
|
|
296
|
+
'sched-cron': {
|
|
297
|
+
id: 'sched-cron',
|
|
298
|
+
name: 'Daily wake',
|
|
299
|
+
agentId: 'agent-1',
|
|
300
|
+
taskPrompt: 'Wake for the daily status.',
|
|
301
|
+
taskMode: 'wake_only',
|
|
302
|
+
message: 'Run the daily status.',
|
|
303
|
+
scheduleType: 'cron',
|
|
304
|
+
cron: '0 8 * * *',
|
|
305
|
+
timezone: 'UTC',
|
|
306
|
+
status: 'active',
|
|
307
|
+
nextRunAt: dueAt,
|
|
308
|
+
createdInSessionId: 'thread-main',
|
|
309
|
+
createdAt: now - 10_000,
|
|
310
|
+
updatedAt: now - 10_000,
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
await scheduler.runSchedulerTickForTests(now)
|
|
315
|
+
const schedule = storage.loadSchedules()['sched-cron']
|
|
316
|
+
const wakes = heartbeatWake.snapshotPendingHeartbeatWakesForTests()
|
|
317
|
+
|
|
318
|
+
console.log(JSON.stringify({
|
|
319
|
+
status: schedule.status,
|
|
320
|
+
nextRunAt: schedule.nextRunAt,
|
|
321
|
+
runNumber: schedule.runNumber,
|
|
322
|
+
historyAction: schedule.history?.[0]?.action || null,
|
|
323
|
+
wakeCount: wakes.length,
|
|
324
|
+
}))
|
|
325
|
+
`)
|
|
326
|
+
|
|
327
|
+
assert.equal(output.status, 'active')
|
|
328
|
+
assert.equal(output.nextRunAt, Date.parse('2030-01-02T08:00:00.000Z'))
|
|
329
|
+
assert.equal(output.runNumber, 1)
|
|
330
|
+
assert.equal(output.historyAction, 'run_started')
|
|
331
|
+
assert.equal(output.wakeCount, 1)
|
|
332
|
+
})
|
|
333
|
+
|
|
205
334
|
it('reuses a persistent mission for scheduled task runs', () => {
|
|
206
335
|
const output = runSchedulerWithTempDataDir(`
|
|
207
336
|
const storageMod = await import('@/lib/server/storage')
|
|
@@ -2,7 +2,6 @@ import { listAgents } from '@/lib/server/agents/agent-repository'
|
|
|
2
2
|
import { loadSchedules, upsertSchedule, upsertSchedules } from '@/lib/server/schedules/schedule-repository'
|
|
3
3
|
import { loadTasks, upsertTask } from '@/lib/server/tasks/task-repository'
|
|
4
4
|
import { enqueueTask } from '@/lib/server/runtime/queue'
|
|
5
|
-
import { CronExpressionParser } from 'cron-parser'
|
|
6
5
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/agents/main-agent-loop'
|
|
7
6
|
import { getScheduleSignatureKey } from '@/lib/schedules/schedule-dedupe'
|
|
8
7
|
import { dispatchWake } from '@/lib/server/runtime/wake-dispatcher'
|
|
@@ -14,6 +13,7 @@ import { hasActiveProtocolRunForSchedule, launchProtocolRunForSchedule } from '@
|
|
|
14
13
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
15
14
|
import { log } from '@/lib/server/logger'
|
|
16
15
|
import { appendScheduleHistoryEntry } from '@/lib/server/schedules/schedule-history'
|
|
16
|
+
import { assessScheduleNextRunRepair, computeScheduleNextRunAt } from '@/lib/server/schedules/schedule-timing'
|
|
17
17
|
import type { Schedule } from '@/types'
|
|
18
18
|
|
|
19
19
|
const TAG = 'scheduler'
|
|
@@ -52,7 +52,7 @@ export function startScheduler() {
|
|
|
52
52
|
if (schedulerState.intervalId) return
|
|
53
53
|
log.info(TAG, 'Starting scheduler engine (60s tick)')
|
|
54
54
|
|
|
55
|
-
// Compute initial nextRunAt
|
|
55
|
+
// Compute initial timing and repair stale nextRunAt values before the first tick.
|
|
56
56
|
computeNextRuns()
|
|
57
57
|
|
|
58
58
|
schedulerState.intervalId = setInterval(tick, TICK_INTERVAL)
|
|
@@ -66,32 +66,64 @@ export function stopScheduler() {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function computeNextRuns() {
|
|
69
|
+
function computeNextRuns(now = Date.now()): Record<string, Schedule> {
|
|
70
70
|
const schedules = loadSchedules()
|
|
71
71
|
const changedEntries: Array<[string, Schedule]> = []
|
|
72
72
|
for (const schedule of Object.values(schedules)) {
|
|
73
73
|
if (schedule.status !== 'active') continue
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
const assessment = assessScheduleNextRunRepair(schedule, now)
|
|
75
|
+
if (!assessment.ok) {
|
|
76
|
+
log.error(TAG, `Invalid cron for ${schedule.id}`)
|
|
77
|
+
const failedSchedule = appendScheduleHistoryEntry({
|
|
78
|
+
...schedule,
|
|
79
|
+
status: 'failed',
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
}, {
|
|
82
|
+
now,
|
|
83
|
+
actor: 'system',
|
|
84
|
+
action: 'failed',
|
|
85
|
+
summary: `Schedule failed because cron could not be parsed: "${schedule.name}"`,
|
|
86
|
+
changes: [{
|
|
87
|
+
field: 'status',
|
|
88
|
+
label: 'Status',
|
|
89
|
+
before: 'active',
|
|
90
|
+
after: 'failed',
|
|
91
|
+
}],
|
|
92
|
+
metadata: { reason: 'invalid_cron' },
|
|
93
|
+
})
|
|
94
|
+
schedules[schedule.id] = failedSchedule
|
|
95
|
+
changedEntries.push([schedule.id, failedSchedule])
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
if (assessment.repair) {
|
|
99
|
+
const repairedSchedule = appendScheduleHistoryEntry({
|
|
100
|
+
...schedule,
|
|
101
|
+
nextRunAt: assessment.nextRunAt,
|
|
102
|
+
updatedAt: now,
|
|
103
|
+
}, {
|
|
104
|
+
now,
|
|
105
|
+
actor: 'system',
|
|
106
|
+
action: 'repaired',
|
|
107
|
+
summary: `Schedule timing repaired: "${schedule.name}"`,
|
|
108
|
+
changes: [{
|
|
109
|
+
field: 'nextRunAt',
|
|
110
|
+
label: 'Next run',
|
|
111
|
+
before: assessment.previousNextRunAt == null ? null : String(assessment.previousNextRunAt),
|
|
112
|
+
after: String(assessment.nextRunAt),
|
|
113
|
+
}],
|
|
114
|
+
metadata: { reason: assessment.reason },
|
|
115
|
+
})
|
|
116
|
+
schedules[schedule.id] = repairedSchedule
|
|
117
|
+
changedEntries.push([schedule.id, repairedSchedule])
|
|
87
118
|
}
|
|
88
119
|
}
|
|
89
120
|
if (changedEntries.length > 0) upsertSchedules(changedEntries)
|
|
121
|
+
return schedules
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
async function tick(now = Date.now()) {
|
|
93
125
|
await processDueWatchJobs(now)
|
|
94
|
-
const schedules =
|
|
126
|
+
const schedules = computeNextRuns(now)
|
|
95
127
|
const agents = listAgents()
|
|
96
128
|
const tasks = loadTasks()
|
|
97
129
|
const inFlightScheduleKeys = new Set<string>(
|
|
@@ -101,27 +133,22 @@ async function tick(now = Date.now()) {
|
|
|
101
133
|
.filter((value: string) => value.length > 0),
|
|
102
134
|
)
|
|
103
135
|
|
|
104
|
-
const applyStagger = (ts: number, staggerSec: number | null | undefined): number => {
|
|
105
|
-
if (!staggerSec || staggerSec <= 0) return ts
|
|
106
|
-
return ts + Math.floor(Math.random() * staggerSec * 1000)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
136
|
const advanceSchedule = (schedule: Schedule): void => {
|
|
110
|
-
if (schedule.scheduleType === '
|
|
111
|
-
try {
|
|
112
|
-
const interval = CronExpressionParser.parse(
|
|
113
|
-
schedule.cron,
|
|
114
|
-
schedule.timezone ? { tz: schedule.timezone } : undefined,
|
|
115
|
-
)
|
|
116
|
-
schedule.nextRunAt = applyStagger(interval.next().getTime(), schedule.staggerSec)
|
|
117
|
-
} catch {
|
|
118
|
-
schedule.status = 'failed'
|
|
119
|
-
}
|
|
120
|
-
} else if (schedule.scheduleType === 'interval' && schedule.intervalMs) {
|
|
121
|
-
schedule.nextRunAt = applyStagger(now + schedule.intervalMs, schedule.staggerSec)
|
|
122
|
-
} else if (schedule.scheduleType === 'once') {
|
|
137
|
+
if (schedule.scheduleType === 'once') {
|
|
123
138
|
schedule.status = 'completed'
|
|
124
139
|
schedule.nextRunAt = undefined
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const nextRunAt = computeScheduleNextRunAt(schedule, now)
|
|
145
|
+
if (nextRunAt == null) {
|
|
146
|
+
schedule.status = 'failed'
|
|
147
|
+
} else {
|
|
148
|
+
schedule.nextRunAt = nextRunAt
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
schedule.status = 'failed'
|
|
125
152
|
}
|
|
126
153
|
}
|
|
127
154
|
|
|
@@ -118,4 +118,18 @@ describe('schedule history', () => {
|
|
|
118
118
|
assert.equal(history[24].id, 'hist-5')
|
|
119
119
|
assert.equal(schedule.revision, 30)
|
|
120
120
|
})
|
|
121
|
+
|
|
122
|
+
it('retains scheduler repair history entries', () => {
|
|
123
|
+
const history = normalizeScheduleHistory([{
|
|
124
|
+
id: 'hist-repair',
|
|
125
|
+
at: 1_000,
|
|
126
|
+
actor: 'system',
|
|
127
|
+
action: 'repaired',
|
|
128
|
+
revision: 1,
|
|
129
|
+
summary: 'Schedule timing repaired',
|
|
130
|
+
}])
|
|
131
|
+
|
|
132
|
+
assert.equal(history.length, 1)
|
|
133
|
+
assert.equal(history[0].action, 'repaired')
|
|
134
|
+
})
|
|
121
135
|
})
|