@swarmclawai/swarmclaw 1.2.4 → 1.2.5
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 +14 -0
- package/bin/daemon-cmd.js +169 -0
- package/bin/server-cmd.js +3 -0
- package/bin/swarmclaw.js +11 -0
- package/package.json +17 -16
- package/src/app/api/agents/[id]/clone/route.ts +3 -32
- package/src/app/api/agents/[id]/route.ts +6 -158
- package/src/app/api/agents/[id]/status/route.ts +2 -3
- package/src/app/api/agents/[id]/thread/route.ts +4 -17
- package/src/app/api/agents/bulk/route.ts +5 -47
- package/src/app/api/agents/route.ts +5 -119
- package/src/app/api/agents/trash/route.ts +13 -24
- package/src/app/api/auth/route.ts +3 -9
- package/src/app/api/autonomy/estop/route.ts +5 -5
- package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
- package/src/app/api/chatrooms/[id]/route.ts +23 -2
- package/src/app/api/chatrooms/route.ts +13 -2
- package/src/app/api/chats/[id]/clear/route.ts +2 -13
- package/src/app/api/chats/[id]/deploy/route.ts +2 -3
- package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
- package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
- package/src/app/api/chats/[id]/queue/route.ts +17 -64
- package/src/app/api/chats/[id]/retry/route.ts +4 -22
- package/src/app/api/chats/[id]/route.ts +10 -138
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/migrate-messages/route.ts +7 -0
- package/src/app/api/chats/route.ts +13 -134
- package/src/app/api/connectors/[id]/access/route.ts +12 -229
- package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
- package/src/app/api/connectors/[id]/health/route.ts +12 -39
- package/src/app/api/connectors/[id]/route.ts +14 -122
- package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
- package/src/app/api/connectors/doctor/route.ts +1 -1
- package/src/app/api/connectors/route.ts +12 -70
- package/src/app/api/credentials/[id]/route.ts +2 -4
- package/src/app/api/credentials/route.ts +10 -19
- package/src/app/api/daemon/health-check/route.ts +3 -4
- package/src/app/api/daemon/route.ts +10 -8
- package/src/app/api/documents/route.ts +11 -10
- package/src/app/api/external-agents/route.ts +3 -3
- package/src/app/api/gateways/[id]/health/route.ts +2 -3
- package/src/app/api/gateways/[id]/route.ts +7 -122
- package/src/app/api/gateways/route.ts +3 -103
- package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
- package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
- package/src/app/api/openclaw/directory/route.ts +2 -2
- package/src/app/api/openclaw/history/route.ts +3 -5
- package/src/app/api/providers/[id]/route.test.ts +49 -0
- package/src/app/api/providers/ollama/route.ts +6 -5
- package/src/app/api/schedules/[id]/route.ts +14 -108
- package/src/app/api/schedules/[id]/run/route.ts +6 -67
- package/src/app/api/schedules/route.ts +9 -51
- package/src/app/api/settings/route.ts +4 -3
- package/src/app/api/setup/check-provider/route.ts +15 -1
- package/src/app/api/setup/openclaw-device/route.ts +2 -2
- package/src/app/api/system/status/route.ts +2 -2
- package/src/app/api/tasks/[id]/route.ts +16 -202
- package/src/app/api/tasks/bulk/route.ts +5 -86
- package/src/app/api/tasks/metrics/route.ts +2 -1
- package/src/app/api/tasks/route.ts +11 -171
- package/src/app/api/upload/route.ts +1 -1
- package/src/app/api/uploads/[filename]/route.ts +1 -1
- package/src/app/api/uploads/route.ts +1 -1
- package/src/app/api/webhooks/[id]/history/route.ts +2 -2
- package/src/app/layout.tsx +9 -6
- package/src/app/protocols/page.tsx +71 -89
- package/src/app/tasks/page.tsx +32 -32
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/auth/setup-wizard/index.tsx +4 -4
- package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
- package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
- package/src/components/auth/setup-wizard/utils.ts +1 -1
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
- package/src/components/connectors/connector-list.tsx +26 -40
- package/src/components/connectors/connector-sheet.tsx +95 -149
- package/src/components/gateways/gateway-sheet.tsx +61 -110
- package/src/components/layout/live-query-sync.tsx +121 -0
- package/src/components/protocols/structured-session-launcher.tsx +24 -45
- package/src/components/providers/app-query-provider.tsx +17 -0
- package/src/components/providers/provider-list.tsx +60 -61
- package/src/components/providers/provider-sheet.tsx +74 -56
- package/src/components/skills/skill-list.tsx +5 -18
- package/src/components/skills/skill-sheet.tsx +21 -20
- package/src/components/skills/skills-workspace.tsx +48 -87
- package/src/components/tasks/task-card.tsx +20 -13
- package/src/components/tasks/task-column.tsx +22 -7
- package/src/components/tasks/task-list.tsx +8 -11
- package/src/components/tasks/task-sheet.tsx +111 -103
- package/src/features/agents/queries.ts +20 -0
- package/src/features/chatrooms/queries.ts +20 -0
- package/src/features/chats/queries.ts +27 -0
- package/src/features/connectors/queries.ts +145 -0
- package/src/features/credentials/queries.ts +37 -0
- package/src/features/extensions/queries.ts +26 -0
- package/src/features/external-agents/queries.ts +36 -0
- package/src/features/gateways/queries.ts +274 -0
- package/src/features/missions/queries.ts +23 -0
- package/src/features/projects/queries.ts +20 -0
- package/src/features/protocols/queries.ts +149 -0
- package/src/features/providers/queries.ts +142 -0
- package/src/features/settings/queries.ts +20 -0
- package/src/features/skills/queries.ts +182 -0
- package/src/features/tasks/queries.ts +189 -0
- package/src/hooks/use-ws.ts +3 -2
- package/src/lib/app/api-client.ts +2 -2
- package/src/lib/query/client.ts +17 -0
- package/src/lib/server/agents/agent-runtime-config.ts +1 -1
- package/src/lib/server/agents/agent-service.ts +429 -0
- package/src/lib/server/agents/agent-thread-session.ts +6 -5
- package/src/lib/server/agents/autonomy-contract.ts +1 -4
- package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
- package/src/lib/server/agents/delegation-advisory.ts +251 -0
- package/src/lib/server/agents/main-agent-loop.ts +98 -40
- package/src/lib/server/agents/subagent-runtime.ts +12 -0
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
- package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
- package/src/lib/server/build-llm.ts +7 -15
- package/src/lib/server/capability-router.test.ts +70 -1
- package/src/lib/server/capability-router.ts +24 -99
- package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
- package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
- package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
- package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
- package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
- package/src/lib/server/chat-execution/message-classifier.ts +74 -32
- package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
- package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
- package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
- package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
- package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
- package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
- package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
- package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
- package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
- package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
- package/src/lib/server/chats/chat-session-service.ts +410 -0
- package/src/lib/server/connectors/access.ts +1 -1
- package/src/lib/server/connectors/commands.ts +7 -6
- package/src/lib/server/connectors/connector-inbound.ts +14 -7
- package/src/lib/server/connectors/connector-outbound.ts +16 -11
- package/src/lib/server/connectors/connector-service.ts +453 -0
- package/src/lib/server/connectors/delivery.ts +17 -12
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
- package/src/lib/server/connectors/media.ts +1 -1
- package/src/lib/server/connectors/response-media.ts +1 -1
- package/src/lib/server/connectors/session-consolidation.ts +11 -7
- package/src/lib/server/connectors/session.ts +9 -7
- package/src/lib/server/connectors/voice-note.ts +2 -1
- package/src/lib/server/context-manager.ts +20 -1
- package/src/lib/server/cost.ts +2 -3
- package/src/lib/server/credentials/credential-repository.ts +43 -4
- package/src/lib/server/credentials/credential-service.ts +112 -0
- package/src/lib/server/daemon/admin-metadata.ts +64 -0
- package/src/lib/server/daemon/controller.ts +577 -0
- package/src/lib/server/daemon/daemon-runtime.ts +352 -0
- package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
- package/src/lib/server/daemon/types.ts +101 -0
- package/src/lib/server/embeddings.ts +3 -9
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -2
- package/src/lib/server/execution-brief.test.ts +167 -0
- package/src/lib/server/execution-brief.ts +295 -0
- package/src/lib/server/execution-engine/chat-turn.ts +9 -0
- package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
- package/src/lib/server/execution-engine/index.ts +35 -0
- package/src/lib/server/execution-engine/task-attempt.ts +303 -0
- package/src/lib/server/execution-engine/types.ts +33 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
- package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
- package/src/lib/server/memory/session-archive-memory.ts +12 -10
- package/src/lib/server/messages/message-repository.ts +330 -0
- package/src/lib/server/missions/mission-service/core.ts +8 -6
- package/src/lib/server/openclaw/agent-resolver.ts +2 -3
- package/src/lib/server/openclaw/doctor.ts +1 -1
- package/src/lib/server/openclaw/gateway.test.ts +10 -1
- package/src/lib/server/openclaw/gateway.ts +5 -14
- package/src/lib/server/openclaw/health.ts +3 -11
- package/src/lib/server/openclaw/sync.ts +8 -6
- package/src/lib/server/persistence/storage-context.ts +3 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
- package/src/lib/server/protocols/protocol-normalization.ts +1 -1
- package/src/lib/server/protocols/protocol-queries.ts +13 -7
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
- package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
- package/src/lib/server/protocols/protocol-swarm.ts +8 -8
- package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
- package/src/lib/server/protocols/protocol-templates.ts +4 -2
- package/src/lib/server/protocols/protocol-types.ts +10 -7
- package/src/lib/server/provider-endpoint.ts +7 -12
- package/src/lib/server/provider-model-discovery.ts +2 -11
- package/src/lib/server/query-expansion.ts +5 -6
- package/src/lib/server/run-context.test.ts +365 -0
- package/src/lib/server/run-context.ts +367 -0
- package/src/lib/server/runtime/heartbeat-service.ts +7 -5
- package/src/lib/server/runtime/queue/core.ts +61 -190
- package/src/lib/server/runtime/run-ledger.ts +8 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
- package/src/lib/server/schedules/schedule-route-service.ts +230 -0
- package/src/lib/server/service-result.ts +16 -0
- package/src/lib/server/session-note.ts +2 -3
- package/src/lib/server/session-reset-policy.ts +4 -3
- package/src/lib/server/session-tools/connector.ts +9 -6
- package/src/lib/server/session-tools/context-mgmt.ts +58 -9
- package/src/lib/server/session-tools/crud.ts +162 -10
- package/src/lib/server/session-tools/delegate.ts +1 -1
- package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
- package/src/lib/server/session-tools/memory.ts +6 -4
- package/src/lib/server/session-tools/session-info.test.ts +56 -0
- package/src/lib/server/session-tools/session-info.ts +119 -12
- package/src/lib/server/session-tools/skill-runtime.ts +3 -1
- package/src/lib/server/session-tools/skills.ts +15 -15
- package/src/lib/server/session-tools/subagent.test.ts +115 -1
- package/src/lib/server/session-tools/subagent.ts +125 -7
- package/src/lib/server/session-tools/team-context.ts +4 -3
- package/src/lib/server/session-tools/wallet.ts +0 -58
- package/src/lib/server/sessions/session-lineage.ts +55 -0
- package/src/lib/server/sessions/session-repository.ts +2 -2
- package/src/lib/server/skills/learned-skills.ts +24 -23
- package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
- package/src/lib/server/skills/skill-repository.ts +136 -13
- package/src/lib/server/skills/skill-suggestions.ts +25 -28
- package/src/lib/server/storage-normalization.test.ts +44 -267
- package/src/lib/server/storage-normalization.ts +75 -0
- package/src/lib/server/storage.ts +19 -0
- package/src/lib/server/structured-extract.ts +3 -14
- package/src/lib/server/tasks/task-followups.ts +16 -11
- package/src/lib/server/tasks/task-result.test.ts +25 -29
- package/src/lib/server/tasks/task-result.ts +5 -9
- package/src/lib/server/tasks/task-route-service.ts +449 -0
- package/src/lib/server/text-normalization.ts +41 -0
- package/src/lib/server/tool-planning.ts +6 -42
- package/src/lib/server/upload-path.ts +5 -0
- package/src/lib/server/working-state/extraction.ts +614 -0
- package/src/lib/server/working-state/normalization.ts +866 -0
- package/src/lib/server/working-state/prompt.ts +60 -0
- package/src/lib/server/working-state/repository.ts +38 -0
- package/src/lib/server/working-state/service.test.ts +253 -0
- package/src/lib/server/working-state/service.ts +293 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/ws-client.ts +3 -3
- package/src/stores/slices/task-slice.ts +1 -4
- package/src/stores/use-chatroom-store.ts +2 -2
- package/src/types/index.ts +277 -12
|
@@ -13,16 +13,17 @@ import { loadSessions, saveSessions } from '@/lib/server/sessions/session-reposi
|
|
|
13
13
|
import { loadSettings } from '@/lib/server/settings/settings-repository'
|
|
14
14
|
import { loadTasks, saveTasks } from '@/lib/server/tasks/task-repository'
|
|
15
15
|
import { notify } from '@/lib/server/ws-hub'
|
|
16
|
+
import { getMessages, getLastMessage, appendMessage } from '@/lib/server/messages/message-repository'
|
|
16
17
|
import { perf } from '@/lib/server/runtime/perf'
|
|
17
18
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
18
19
|
import { createAgentTaskSession } from '@/lib/server/agents/task-session'
|
|
19
20
|
import { formatValidationFailure } from '@/lib/server/tasks/task-validation'
|
|
20
21
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/agents/main-agent-loop'
|
|
21
|
-
import {
|
|
22
|
+
import type { ExecuteChatTurnResult } from '@/lib/server/chat-execution/chat-execution-types'
|
|
22
23
|
import { checkAgentBudgetLimits } from '@/lib/server/cost'
|
|
24
|
+
import { enqueueExecution } from '@/lib/server/execution-engine'
|
|
23
25
|
import { extractTaskResult, formatResultBody } from '@/lib/server/tasks/task-result'
|
|
24
26
|
import {
|
|
25
|
-
assessAutonomyRun,
|
|
26
27
|
classifyRuntimeFailure,
|
|
27
28
|
observeAutonomyRunOutcome,
|
|
28
29
|
recordSupervisorIncident,
|
|
@@ -40,7 +41,7 @@ import {
|
|
|
40
41
|
} from '@/lib/server/tasks/task-followups'
|
|
41
42
|
import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
|
|
42
43
|
import { cascadeUnblock } from '@/lib/server/dag-validation'
|
|
43
|
-
import {
|
|
44
|
+
import { prepareGuardianRecovery } from '@/lib/server/agents/guardian'
|
|
44
45
|
import { notifyOrchestrators } from '@/lib/server/runtime/orchestrator-events'
|
|
45
46
|
import type { Agent, BoardTask, Message, Session } from '@/types'
|
|
46
47
|
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
|
|
@@ -549,9 +550,9 @@ function collectTaskResultDeliveryData(
|
|
|
549
550
|
): TaskResultDeliveryData {
|
|
550
551
|
const runSessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
|
|
551
552
|
const runSession = runSessionId ? sessions[runSessionId] : null
|
|
552
|
-
const fallbackText =
|
|
553
|
+
const fallbackText = runSessionId ? latestAssistantText(runSessionId) : ''
|
|
553
554
|
const taskResult = extractTaskResult(
|
|
554
|
-
|
|
555
|
+
runSessionId ? getMessages(runSessionId) : [],
|
|
555
556
|
task.result || fallbackText || null,
|
|
556
557
|
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
557
558
|
)
|
|
@@ -606,10 +607,11 @@ function buildTaskTerminalMessage(
|
|
|
606
607
|
return parts.join('\n\n')
|
|
607
608
|
}
|
|
608
609
|
|
|
609
|
-
function latestAssistantText(
|
|
610
|
-
if (!
|
|
611
|
-
|
|
612
|
-
|
|
610
|
+
function latestAssistantText(sessionId: string | null | undefined): string {
|
|
611
|
+
if (!sessionId) return ''
|
|
612
|
+
const messages = getMessages(sessionId)
|
|
613
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
614
|
+
const msg = messages[i]
|
|
613
615
|
if (msg?.role !== 'assistant') continue
|
|
614
616
|
const text = typeof msg?.text === 'string' ? msg.text.trim() : ''
|
|
615
617
|
if (!text) continue
|
|
@@ -619,23 +621,6 @@ function latestAssistantText(session: SessionLike | null | undefined): string {
|
|
|
619
621
|
return ''
|
|
620
622
|
}
|
|
621
623
|
|
|
622
|
-
// Task result extraction now uses Zod-validated structured data
|
|
623
|
-
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
624
|
-
|
|
625
|
-
/** Check if a task result looks incomplete (agent stopped mid-objective). */
|
|
626
|
-
function looksIncomplete(text: string): boolean {
|
|
627
|
-
if (!text) return false
|
|
628
|
-
const trimmed = text.trim()
|
|
629
|
-
// Ends with ellipsis or continuation signal
|
|
630
|
-
if (trimmed.endsWith('...') || trimmed.endsWith('…')) return true
|
|
631
|
-
// Ends with a step/phase header (agent was listing next steps)
|
|
632
|
-
if (/(?:^|\n)#{1,3}\s+(?:Step|Phase|Next)\s+\d/i.test(trimmed.slice(-200))) return true
|
|
633
|
-
// Contains forward-looking language at the end
|
|
634
|
-
const lastChunk = trimmed.slice(-300).toLowerCase()
|
|
635
|
-
if (/\b(?:next i(?:'ll| will)|now i(?:'ll| will)|let me (?:now|next)|moving on to|proceeding to)\b/.test(lastChunk)) return true
|
|
636
|
-
return false
|
|
637
|
-
}
|
|
638
|
-
|
|
639
624
|
function queueTaskAutonomyObservation(input: {
|
|
640
625
|
runId: string
|
|
641
626
|
sessionId: string
|
|
@@ -663,128 +648,6 @@ function queueTaskAutonomyObservation(input: {
|
|
|
663
648
|
})
|
|
664
649
|
}
|
|
665
650
|
|
|
666
|
-
async function executeTaskRun(
|
|
667
|
-
task: BoardTask,
|
|
668
|
-
agent: Agent,
|
|
669
|
-
sessionId: string,
|
|
670
|
-
): Promise<ExecuteChatTurnResult> {
|
|
671
|
-
if (agent.autoRecovery) {
|
|
672
|
-
const cwd = task.projectId
|
|
673
|
-
? path.join(WORKSPACE_DIR, 'projects', task.projectId)
|
|
674
|
-
: WORKSPACE_DIR
|
|
675
|
-
captureGuardianCheckpoint(cwd, `task:${task.id}`)
|
|
676
|
-
}
|
|
677
|
-
const settings = loadSettings()
|
|
678
|
-
const basePrompt = task.description || task.title
|
|
679
|
-
const prompt = [
|
|
680
|
-
basePrompt,
|
|
681
|
-
'',
|
|
682
|
-
'Completion requirements:',
|
|
683
|
-
'- Execute the task before replying; do not reply with only a plan.',
|
|
684
|
-
'- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
|
|
685
|
-
'- If blocked, state the blocker explicitly and what input or permission is missing.',
|
|
686
|
-
].join('\n')
|
|
687
|
-
// All agents go through the unified chat execution path.
|
|
688
|
-
// Agents with delegation enabled get delegation tools automatically via session-tools.
|
|
689
|
-
let latestRun: ExecuteChatTurnResult = await executeSessionChatTurn({
|
|
690
|
-
sessionId,
|
|
691
|
-
message: prompt,
|
|
692
|
-
internal: false,
|
|
693
|
-
source: 'task',
|
|
694
|
-
runId: task.id,
|
|
695
|
-
})
|
|
696
|
-
let text = typeof latestRun.text === 'string' ? latestRun.text.trim() : ''
|
|
697
|
-
let previousSummary: string | null = null
|
|
698
|
-
let totalInputTokens = latestRun.inputTokens || 0
|
|
699
|
-
let totalOutputTokens = latestRun.outputTokens || 0
|
|
700
|
-
let totalEstimatedCost = Number(latestRun.estimatedCost || 0)
|
|
701
|
-
if (latestRun.error) {
|
|
702
|
-
return {
|
|
703
|
-
...latestRun,
|
|
704
|
-
text,
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const maxSupervisorFollowups = 2
|
|
709
|
-
for (let followupIndex = 0; followupIndex < maxSupervisorFollowups; followupIndex += 1) {
|
|
710
|
-
const sessions = loadSessions()
|
|
711
|
-
const session = sessions[sessionId] as unknown as Session | undefined
|
|
712
|
-
const assessment = assessAutonomyRun({
|
|
713
|
-
runId: `${task.id}:attempt-${(task.attempts || 0) + 1}:step-${followupIndex + 1}`,
|
|
714
|
-
sessionId,
|
|
715
|
-
taskId: task.id,
|
|
716
|
-
agentId: agent.id,
|
|
717
|
-
source: 'task',
|
|
718
|
-
status: latestRun.error ? 'failed' : 'completed',
|
|
719
|
-
resultText: text,
|
|
720
|
-
error: latestRun.error,
|
|
721
|
-
toolEvents: latestRun.toolEvents,
|
|
722
|
-
mainLoopState: {
|
|
723
|
-
followupChainCount: followupIndex + 1,
|
|
724
|
-
summary: previousSummary,
|
|
725
|
-
missionCostUsd: totalEstimatedCost,
|
|
726
|
-
},
|
|
727
|
-
session: session || null,
|
|
728
|
-
settings,
|
|
729
|
-
})
|
|
730
|
-
if (assessment.shouldBlock) break
|
|
731
|
-
if (assessment.autoActions?.length) {
|
|
732
|
-
const { executeSupervisorAutoActions } = await import('@/lib/server/autonomy/supervisor-reflection')
|
|
733
|
-
const result = await executeSupervisorAutoActions({
|
|
734
|
-
actions: assessment.autoActions,
|
|
735
|
-
sessionId,
|
|
736
|
-
agentId: agent?.id,
|
|
737
|
-
})
|
|
738
|
-
if (result.blocked) break
|
|
739
|
-
}
|
|
740
|
-
const followupMessage = assessment.interventionPrompt
|
|
741
|
-
|| (text && looksIncomplete(text)
|
|
742
|
-
? 'Continue and complete the remaining steps. Provide a final summary when done.'
|
|
743
|
-
: null)
|
|
744
|
-
if (!followupMessage) break
|
|
745
|
-
|
|
746
|
-
// Budget check before follow-up
|
|
747
|
-
const typedAgentForBudget = agent as Agent
|
|
748
|
-
if (typedAgentForBudget.monthlyBudget || typedAgentForBudget.dailyBudget || typedAgentForBudget.hourlyBudget) {
|
|
749
|
-
try {
|
|
750
|
-
const followupBudget = checkAgentBudgetLimits(typedAgentForBudget)
|
|
751
|
-
if (!followupBudget.ok) {
|
|
752
|
-
log.warn(TAG, `[queue] Budget exceeded for "${typedAgentForBudget.name}" during follow-up, stopping.`)
|
|
753
|
-
break
|
|
754
|
-
}
|
|
755
|
-
} catch {}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
previousSummary = text || previousSummary
|
|
759
|
-
const followUp = await executeSessionChatTurn({
|
|
760
|
-
sessionId,
|
|
761
|
-
message: followupMessage,
|
|
762
|
-
internal: false,
|
|
763
|
-
source: 'task',
|
|
764
|
-
})
|
|
765
|
-
totalInputTokens += followUp.inputTokens || 0
|
|
766
|
-
totalOutputTokens += followUp.outputTokens || 0
|
|
767
|
-
totalEstimatedCost += Number(followUp.estimatedCost || 0)
|
|
768
|
-
text = typeof followUp.text === 'string' ? followUp.text.trim() : ''
|
|
769
|
-
latestRun = {
|
|
770
|
-
...followUp,
|
|
771
|
-
text,
|
|
772
|
-
inputTokens: totalInputTokens,
|
|
773
|
-
outputTokens: totalOutputTokens,
|
|
774
|
-
estimatedCost: totalEstimatedCost,
|
|
775
|
-
}
|
|
776
|
-
if (latestRun.error) break
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return {
|
|
780
|
-
...latestRun,
|
|
781
|
-
text,
|
|
782
|
-
inputTokens: totalInputTokens,
|
|
783
|
-
outputTokens: totalOutputTokens,
|
|
784
|
-
estimatedCost: totalEstimatedCost,
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
651
|
function hasFinishedExecutionSession(session: SessionLike | Session | null | undefined): boolean {
|
|
789
652
|
if (!session) return false
|
|
790
653
|
return session.active === false && !session.currentRunId
|
|
@@ -810,7 +673,7 @@ export function reconcileFinishedRunningTasks(): { reconciled: number; deadLette
|
|
|
810
673
|
const session = sessions[sessionId]
|
|
811
674
|
if (!hasFinishedExecutionSession(session)) continue
|
|
812
675
|
|
|
813
|
-
const fallbackText = latestAssistantText(
|
|
676
|
+
const fallbackText = latestAssistantText(sessionId)
|
|
814
677
|
if (!fallbackText && !task.result) {
|
|
815
678
|
task.status = 'failed'
|
|
816
679
|
task.result = 'Agent session finished without producing output.'
|
|
@@ -821,7 +684,7 @@ export function reconcileFinishedRunningTasks(): { reconciled: number; deadLette
|
|
|
821
684
|
|
|
822
685
|
applyTaskPolicyDefaults(task)
|
|
823
686
|
const taskResult = extractTaskResult(
|
|
824
|
-
|
|
687
|
+
getMessages(sessionId),
|
|
825
688
|
task.result || fallbackText || null,
|
|
826
689
|
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
827
690
|
)
|
|
@@ -930,8 +793,7 @@ function pushUserFacingTaskResult(task: BoardTask, sessions: Record<string, Sess
|
|
|
930
793
|
const taskLink = `[${task.title}](#task:${task.id})`
|
|
931
794
|
const body = buildTaskTerminalMessage(`Task ${delivery.statusLabel}: **${taskLink}**`, task, delivery)
|
|
932
795
|
const now = Date.now()
|
|
933
|
-
|
|
934
|
-
const lastMsg = targetSession.messages.at(-1)
|
|
796
|
+
const lastMsg = getLastMessage(targetSessionId)
|
|
935
797
|
if (lastMsg?.role === 'assistant' && lastMsg?.text === body && typeof lastMsg?.time === 'number' && now - lastMsg.time < 30_000) {
|
|
936
798
|
return
|
|
937
799
|
}
|
|
@@ -943,9 +805,7 @@ function pushUserFacingTaskResult(task: BoardTask, sessions: Record<string, Sess
|
|
|
943
805
|
kind: 'system',
|
|
944
806
|
}
|
|
945
807
|
if (delivery.firstImage) message.imageUrl = delivery.firstImage.url
|
|
946
|
-
|
|
947
|
-
targetSession.lastActiveAt = now
|
|
948
|
-
saveSessions(sessions as Record<string, Session>)
|
|
808
|
+
appendMessage(targetSessionId, message)
|
|
949
809
|
notify(`messages:${targetSessionId}`)
|
|
950
810
|
}
|
|
951
811
|
|
|
@@ -1455,32 +1315,33 @@ export async function processNext() {
|
|
|
1455
1315
|
})
|
|
1456
1316
|
|
|
1457
1317
|
// Save initial assistant message so user sees context when opening the session
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1318
|
+
{
|
|
1319
|
+
const sessionExists = Boolean(loadSessions()[sessionId])
|
|
1320
|
+
if (sessionExists) {
|
|
1321
|
+
const isDelegation = (task as unknown as Record<string, unknown>).sourceType === 'delegation'
|
|
1322
|
+
let initialText: string
|
|
1323
|
+
if (isDelegation) {
|
|
1324
|
+
const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
|
|
1325
|
+
const delegator = delegatorId ? agents[delegatorId] : null
|
|
1326
|
+
const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
|
|
1327
|
+
initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
|
|
1328
|
+
} else {
|
|
1329
|
+
initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
|
|
1330
|
+
}
|
|
1331
|
+
// Inject upstream task results context
|
|
1332
|
+
if (Array.isArray(task.upstreamResults) && task.upstreamResults.length > 0) {
|
|
1333
|
+
const upstreamBlock = task.upstreamResults
|
|
1334
|
+
.map((ur) => `### ${ur.taskTitle}\n${ur.resultPreview || '(no result)'}`)
|
|
1335
|
+
.join('\n\n')
|
|
1336
|
+
initialText += `\n\n## Context from upstream tasks\n\n${upstreamBlock}`
|
|
1337
|
+
}
|
|
1338
|
+
appendMessage(sessionId, {
|
|
1339
|
+
role: 'assistant',
|
|
1340
|
+
text: initialText,
|
|
1341
|
+
time: Date.now(),
|
|
1342
|
+
...(isDelegation ? { kind: 'system' as const } : {}),
|
|
1343
|
+
})
|
|
1476
1344
|
}
|
|
1477
|
-
sessions[sessionId].messages.push({
|
|
1478
|
-
role: 'assistant',
|
|
1479
|
-
text: initialText,
|
|
1480
|
-
time: Date.now(),
|
|
1481
|
-
...(isDelegation ? { kind: 'system' as const } : {}),
|
|
1482
|
-
})
|
|
1483
|
-
saveSessions(sessions)
|
|
1484
1345
|
}
|
|
1485
1346
|
|
|
1486
1347
|
log.info(TAG, `[queue] Running task "${task.title}" (${taskId}) with ${agent.name}`)
|
|
@@ -1488,7 +1349,14 @@ export async function processNext() {
|
|
|
1488
1349
|
try {
|
|
1489
1350
|
const taskRunId = `${taskId}:attempt-${(task.attempts || 0) + 1}`
|
|
1490
1351
|
const endTaskRunPerf = perf.start('queue', 'executeTaskRun', { taskId, agentName: agent.name })
|
|
1491
|
-
const
|
|
1352
|
+
const taskRunHandle = enqueueExecution({
|
|
1353
|
+
kind: 'task_attempt',
|
|
1354
|
+
task,
|
|
1355
|
+
agent,
|
|
1356
|
+
sessionId,
|
|
1357
|
+
executionId: taskRunId,
|
|
1358
|
+
})
|
|
1359
|
+
const taskRun = await taskRunHandle.promise
|
|
1492
1360
|
endTaskRunPerf()
|
|
1493
1361
|
// Update lastActivityAt after execution completes (idle timeout tracking)
|
|
1494
1362
|
{
|
|
@@ -1524,9 +1392,8 @@ export async function processNext() {
|
|
|
1524
1392
|
if (t2[taskId]) {
|
|
1525
1393
|
applyTaskPolicyDefaults(t2[taskId])
|
|
1526
1394
|
// Structured extraction: Zod-validated result with typed artifacts
|
|
1527
|
-
const runSessions = loadSessions()
|
|
1528
1395
|
const taskResult = extractTaskResult(
|
|
1529
|
-
|
|
1396
|
+
getMessages(sessionId),
|
|
1530
1397
|
result || null,
|
|
1531
1398
|
{ sinceTime: typeof t2[taskId].startedAt === 'number' ? t2[taskId].startedAt : null },
|
|
1532
1399
|
)
|
|
@@ -1723,13 +1590,17 @@ export async function processNext() {
|
|
|
1723
1590
|
t2[taskId].repairRunId = repairRunId
|
|
1724
1591
|
t2[taskId].lastRepairAttemptAt = Date.now()
|
|
1725
1592
|
saveTasks(t2)
|
|
1726
|
-
await
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1593
|
+
await enqueueExecution({
|
|
1594
|
+
kind: 'session_turn',
|
|
1595
|
+
input: {
|
|
1596
|
+
sessionId: t2[taskId].sessionId!,
|
|
1597
|
+
message: `[AUTO-REPAIR] ${failureClassification.repairPrompt}\n\nOriginal error: ${errMsg.slice(0, 300)}`,
|
|
1598
|
+
internal: true,
|
|
1599
|
+
source: 'task-repair',
|
|
1600
|
+
mode: 'followup',
|
|
1601
|
+
dedupeKey: repairRunId,
|
|
1602
|
+
},
|
|
1603
|
+
}).promise
|
|
1733
1604
|
log.info(TAG, `[queue] Repair turn completed for task "${task.title}" (${taskId})`)
|
|
1734
1605
|
} catch (repairErr: unknown) {
|
|
1735
1606
|
log.warn(TAG, `[queue] Repair turn failed for task "${task.title}":`, repairErr instanceof Error ? repairErr.message : String(repairErr))
|
|
@@ -73,6 +73,10 @@ export function listPersistedRuns(params?: {
|
|
|
73
73
|
export function appendPersistedRunEvent(input: {
|
|
74
74
|
runId: string
|
|
75
75
|
sessionId: string
|
|
76
|
+
kind?: RunEventRecord['kind']
|
|
77
|
+
ownerType?: RunEventRecord['ownerType']
|
|
78
|
+
ownerId?: RunEventRecord['ownerId']
|
|
79
|
+
parentExecutionId?: RunEventRecord['parentExecutionId']
|
|
76
80
|
phase: 'status' | 'event'
|
|
77
81
|
status?: SessionRunStatus
|
|
78
82
|
event: SSEEvent
|
|
@@ -86,6 +90,10 @@ export function appendPersistedRunEvent(input: {
|
|
|
86
90
|
id: genId(12),
|
|
87
91
|
runId: input.runId,
|
|
88
92
|
sessionId: input.sessionId,
|
|
93
|
+
kind: input.kind,
|
|
94
|
+
ownerType: input.ownerType,
|
|
95
|
+
ownerId: input.ownerId,
|
|
96
|
+
parentExecutionId: input.parentExecutionId,
|
|
89
97
|
timestamp,
|
|
90
98
|
phase: input.phase,
|
|
91
99
|
status: input.status,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { executeExecutionChatTurn } from '@/lib/server/execution-engine/chat-turn'
|
|
2
2
|
import { log } from '@/lib/server/logger'
|
|
3
3
|
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
4
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
@@ -98,7 +98,7 @@ export async function drainExecution(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
|
-
const result = await
|
|
101
|
+
const result = await executeExecutionChatTurn({
|
|
102
102
|
sessionId: next.run.sessionId,
|
|
103
103
|
message: next.message,
|
|
104
104
|
imagePath: next.imagePath,
|
|
@@ -6,6 +6,7 @@ import { log } from '@/lib/server/logger'
|
|
|
6
6
|
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
7
7
|
import { getEnabledToolIds } from '@/lib/capability-selection'
|
|
8
8
|
import { isAllEstopEngaged, isAutonomyEstopEngaged } from '@/lib/server/runtime/estop'
|
|
9
|
+
import { isRestartRecoverableSource } from '@/lib/server/runtime/run-ledger'
|
|
9
10
|
import { getActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
|
|
10
11
|
|
|
11
12
|
import { cancelPendingForSession } from './cancellation'
|
|
@@ -215,6 +216,11 @@ export function enqueueSessionRun(
|
|
|
215
216
|
id: runId,
|
|
216
217
|
sessionId: input.sessionId,
|
|
217
218
|
missionId: input.missionId ?? getSession(input.sessionId)?.missionId ?? null,
|
|
219
|
+
kind: 'session_turn',
|
|
220
|
+
ownerType: 'session',
|
|
221
|
+
ownerId: input.sessionId,
|
|
222
|
+
parentExecutionId: null,
|
|
223
|
+
recoveryPolicy: isRestartRecoverableSource(source) ? 'restart_recoverable' : 'ephemeral',
|
|
218
224
|
source,
|
|
219
225
|
internal,
|
|
220
226
|
mode,
|
|
@@ -118,6 +118,10 @@ export function persistEventForRun(entry: SessionRunQueueEntry, event: SSEEvent,
|
|
|
118
118
|
appendPersistedRunEvent({
|
|
119
119
|
runId: entry.run.id,
|
|
120
120
|
sessionId: entry.run.sessionId,
|
|
121
|
+
kind: entry.run.kind,
|
|
122
|
+
ownerType: entry.run.ownerType,
|
|
123
|
+
ownerId: entry.run.ownerId,
|
|
124
|
+
parentExecutionId: entry.run.parentExecutionId,
|
|
121
125
|
phase: opts?.phase || 'event',
|
|
122
126
|
status: opts?.status,
|
|
123
127
|
summary: opts?.summary,
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
|
|
2
|
+
import { loadAgents } from '@/lib/server/agents/agent-repository'
|
|
3
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
|
+
import { enqueueTask } from '@/lib/server/runtime/queue'
|
|
5
|
+
import { loadSessions } from '@/lib/server/sessions/session-repository'
|
|
6
|
+
import { prepareScheduleUpdate, prepareScheduleCreate } from '@/lib/server/schedules/schedule-service'
|
|
7
|
+
import {
|
|
8
|
+
archiveScheduleCluster,
|
|
9
|
+
purgeArchivedScheduleCluster,
|
|
10
|
+
restoreArchivedScheduleCluster,
|
|
11
|
+
} from '@/lib/server/schedules/schedule-lifecycle'
|
|
12
|
+
import { loadSchedule, loadSchedules, upsertSchedule, upsertSchedules } from '@/lib/server/schedules/schedule-repository'
|
|
13
|
+
import { serviceFail, serviceOk } from '@/lib/server/service-result'
|
|
14
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
15
|
+
import { getScheduleSignatureKey } from '@/lib/schedules/schedule-dedupe'
|
|
16
|
+
import { prepareScheduledTaskRun } from '@/lib/server/tasks/task-lifecycle'
|
|
17
|
+
import { loadTasks, saveTask } from '@/lib/server/tasks/task-repository'
|
|
18
|
+
import { pushMainLoopEventToMainSessions } from '@/lib/server/agents/main-agent-loop'
|
|
19
|
+
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
20
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
21
|
+
import type { Schedule } from '@/types'
|
|
22
|
+
import type { ScheduleLike } from '@/lib/schedules/schedule-dedupe'
|
|
23
|
+
import type { ServiceResult } from '@/lib/server/service-result'
|
|
24
|
+
|
|
25
|
+
type InFlightTask = {
|
|
26
|
+
status?: string
|
|
27
|
+
sourceScheduleKey?: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function listSchedulesForApi(includeArchived: boolean) {
|
|
31
|
+
const schedules = loadSchedules()
|
|
32
|
+
if (includeArchived) return schedules
|
|
33
|
+
const filtered: typeof schedules = {}
|
|
34
|
+
for (const [id, schedule] of Object.entries(schedules)) {
|
|
35
|
+
if (schedule.status === 'archived') continue
|
|
36
|
+
filtered[id] = schedule
|
|
37
|
+
}
|
|
38
|
+
return filtered
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createScheduleFromRoute(body: Record<string, unknown>): ServiceResult<ScheduleLike> {
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
const schedules = loadSchedules()
|
|
44
|
+
const agents = loadAgents()
|
|
45
|
+
const candidateAgentId = typeof body.agentId === 'string' ? body.agentId.trim() : ''
|
|
46
|
+
const agent = agents[candidateAgentId]
|
|
47
|
+
if (!agent) {
|
|
48
|
+
return serviceFail(400, `Agent not found: ${String(body.agentId)}`)
|
|
49
|
+
}
|
|
50
|
+
if (isAgentDisabled(agent)) {
|
|
51
|
+
return serviceFail(409, buildAgentDisabledMessage(agent, 'take scheduled work'))
|
|
52
|
+
}
|
|
53
|
+
const prepared = prepareScheduleCreate({
|
|
54
|
+
input: body,
|
|
55
|
+
schedules,
|
|
56
|
+
now,
|
|
57
|
+
cwd: WORKSPACE_DIR,
|
|
58
|
+
})
|
|
59
|
+
if (!prepared.ok) {
|
|
60
|
+
return serviceFail(400, prepared.error)
|
|
61
|
+
}
|
|
62
|
+
if (prepared.kind === 'duplicate') {
|
|
63
|
+
if (prepared.entries.length === 1) upsertSchedule(prepared.scheduleId, prepared.schedule)
|
|
64
|
+
else if (prepared.entries.length > 1) upsertSchedules(prepared.entries)
|
|
65
|
+
if (prepared.entries.length > 0) notify('schedules')
|
|
66
|
+
return serviceOk(prepared.schedule)
|
|
67
|
+
}
|
|
68
|
+
upsertSchedule(prepared.scheduleId, prepared.schedule)
|
|
69
|
+
logActivity({
|
|
70
|
+
entityType: 'schedule',
|
|
71
|
+
entityId: prepared.scheduleId,
|
|
72
|
+
action: 'created',
|
|
73
|
+
actor: 'user',
|
|
74
|
+
summary: `Schedule created: "${prepared.schedule.name}"`,
|
|
75
|
+
})
|
|
76
|
+
notify('schedules')
|
|
77
|
+
return serviceOk(prepared.schedule)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function updateScheduleFromRoute(id: string, body: Record<string, unknown>): ServiceResult<ScheduleLike & { [key: string]: unknown }> {
|
|
81
|
+
const schedules = loadSchedules()
|
|
82
|
+
const current = schedules[id]
|
|
83
|
+
if (!current) return serviceFail(404, 'Schedule not found')
|
|
84
|
+
|
|
85
|
+
if (body.restore === true) {
|
|
86
|
+
const restored = restoreArchivedScheduleCluster(id, {
|
|
87
|
+
actor: { actor: 'user' },
|
|
88
|
+
})
|
|
89
|
+
if (!restored.ok || !restored.schedule) {
|
|
90
|
+
return serviceFail(409, 'Schedule is not archived.')
|
|
91
|
+
}
|
|
92
|
+
return serviceOk({
|
|
93
|
+
...restored.schedule,
|
|
94
|
+
restoredIds: restored.restoredIds,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (body.status === 'archived') {
|
|
99
|
+
const archived = archiveScheduleCluster(id, {
|
|
100
|
+
actor: { actor: 'user' },
|
|
101
|
+
})
|
|
102
|
+
if (!archived.ok || !archived.schedule) {
|
|
103
|
+
return serviceFail(500, 'Failed to archive schedule.')
|
|
104
|
+
}
|
|
105
|
+
return serviceOk({
|
|
106
|
+
...archived.schedule,
|
|
107
|
+
archivedIds: archived.archivedIds,
|
|
108
|
+
cancelledTaskIds: archived.cancelledTaskIds,
|
|
109
|
+
abortedRunSessionIds: archived.abortedRunSessionIds,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sessions = loadSessions()
|
|
114
|
+
const agents = loadAgents()
|
|
115
|
+
const sessionCwd = typeof current.createdInSessionId === 'string'
|
|
116
|
+
? sessions[current.createdInSessionId]?.cwd
|
|
117
|
+
: null
|
|
118
|
+
const prepared = prepareScheduleUpdate({
|
|
119
|
+
id,
|
|
120
|
+
current,
|
|
121
|
+
patch: body,
|
|
122
|
+
schedules,
|
|
123
|
+
now: Date.now(),
|
|
124
|
+
cwd: sessionCwd || WORKSPACE_DIR,
|
|
125
|
+
agentExists: (agentId) => Boolean(agents[agentId]),
|
|
126
|
+
propagateEquivalentStatuses: true,
|
|
127
|
+
propagationSource: current as unknown as Record<string, unknown>,
|
|
128
|
+
})
|
|
129
|
+
if (!prepared.ok) {
|
|
130
|
+
return serviceFail(400, errorMessage(prepared.error))
|
|
131
|
+
}
|
|
132
|
+
upsertSchedules(prepared.entries)
|
|
133
|
+
logActivity({
|
|
134
|
+
entityType: 'schedule',
|
|
135
|
+
entityId: id,
|
|
136
|
+
action: 'updated',
|
|
137
|
+
actor: 'user',
|
|
138
|
+
summary: `Schedule updated: "${prepared.schedule.name}"`,
|
|
139
|
+
detail: prepared.affectedScheduleIds.length > 1 ? { affectedScheduleIds: prepared.affectedScheduleIds } : undefined,
|
|
140
|
+
})
|
|
141
|
+
notify('schedules')
|
|
142
|
+
return serviceOk(
|
|
143
|
+
prepared.affectedScheduleIds.length > 1
|
|
144
|
+
? { ...prepared.schedule, affectedScheduleIds: prepared.affectedScheduleIds }
|
|
145
|
+
: prepared.schedule,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function deleteScheduleFromRoute(id: string, purge: boolean): ServiceResult<Record<string, unknown>> {
|
|
150
|
+
const current = loadSchedule(id)
|
|
151
|
+
if (!current) return serviceFail(404, 'Schedule not found')
|
|
152
|
+
if (purge) {
|
|
153
|
+
const purged = purgeArchivedScheduleCluster(id, {
|
|
154
|
+
actor: { actor: 'user' },
|
|
155
|
+
})
|
|
156
|
+
if (!purged.ok) {
|
|
157
|
+
return serviceFail(409, 'Only archived schedules can be purged.')
|
|
158
|
+
}
|
|
159
|
+
return serviceOk({ ok: true, purgedIds: purged.purgedIds })
|
|
160
|
+
}
|
|
161
|
+
const archived = archiveScheduleCluster(id, {
|
|
162
|
+
actor: { actor: 'user' },
|
|
163
|
+
})
|
|
164
|
+
if (!archived.ok || !archived.schedule) {
|
|
165
|
+
return serviceFail(500, 'Failed to archive schedule.')
|
|
166
|
+
}
|
|
167
|
+
return serviceOk({
|
|
168
|
+
ok: true,
|
|
169
|
+
archivedIds: archived.archivedIds,
|
|
170
|
+
cancelledTaskIds: archived.cancelledTaskIds,
|
|
171
|
+
removedQueuedTaskIds: archived.removedQueuedTaskIds,
|
|
172
|
+
abortedRunSessionIds: archived.abortedRunSessionIds,
|
|
173
|
+
schedule: archived.schedule,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function runScheduleNow(id: string): ServiceResult<Record<string, unknown>> {
|
|
178
|
+
const schedule = loadSchedule(id) as Schedule | null
|
|
179
|
+
if (!schedule) return serviceFail(404, 'Schedule not found')
|
|
180
|
+
if (schedule.status === 'archived') {
|
|
181
|
+
return serviceFail(409, 'Archived schedules must be restored before they can run.')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const agents = loadAgents()
|
|
185
|
+
const agent = agents[schedule.agentId]
|
|
186
|
+
if (!agent) return serviceFail(400, 'Agent not found')
|
|
187
|
+
if (isAgentDisabled(agent)) {
|
|
188
|
+
return serviceFail(409, buildAgentDisabledMessage(agent, 'run schedules'))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const tasks = loadTasks()
|
|
192
|
+
const scheduleSignature = getScheduleSignatureKey(schedule)
|
|
193
|
+
if (scheduleSignature) {
|
|
194
|
+
const inFlight = Object.values(tasks as Record<string, InFlightTask>).some((task) =>
|
|
195
|
+
task
|
|
196
|
+
&& (task.status === 'queued' || task.status === 'running')
|
|
197
|
+
&& task.sourceScheduleKey === scheduleSignature
|
|
198
|
+
)
|
|
199
|
+
if (inFlight) {
|
|
200
|
+
return serviceOk({ ok: true, queued: false, reason: 'in_flight' })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const now = Date.now()
|
|
205
|
+
schedule.runNumber = (schedule.runNumber || 0) + 1
|
|
206
|
+
const { taskId } = prepareScheduledTaskRun({
|
|
207
|
+
schedule,
|
|
208
|
+
tasks,
|
|
209
|
+
now,
|
|
210
|
+
scheduleSignature,
|
|
211
|
+
})
|
|
212
|
+
saveTask(taskId, tasks[taskId])
|
|
213
|
+
enqueueTask(taskId)
|
|
214
|
+
pushMainLoopEventToMainSessions({
|
|
215
|
+
type: 'schedule_fired',
|
|
216
|
+
text: `Schedule fired manually: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
|
|
217
|
+
})
|
|
218
|
+
schedule.lastRunAt = now
|
|
219
|
+
upsertSchedule(schedule.id, schedule)
|
|
220
|
+
logActivity({
|
|
221
|
+
entityType: 'schedule',
|
|
222
|
+
entityId: schedule.id,
|
|
223
|
+
action: 'started',
|
|
224
|
+
actor: 'user',
|
|
225
|
+
summary: `Schedule run started: "${schedule.name}"`,
|
|
226
|
+
detail: { taskId, runNumber: schedule.runNumber },
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
return serviceOk({ ok: true, queued: true, taskId, runNumber: schedule.runNumber })
|
|
230
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized result type for route-facing services.
|
|
3
|
+
*
|
|
4
|
+
* Routes inspect `ok` to decide the HTTP status code and response body.
|
|
5
|
+
*/
|
|
6
|
+
export type ServiceResult<T> =
|
|
7
|
+
| { ok: true; payload: T }
|
|
8
|
+
| { ok: false; status: number; payload: { error: string } }
|
|
9
|
+
|
|
10
|
+
export function serviceOk<T>(payload: T): ServiceResult<T> {
|
|
11
|
+
return { ok: true, payload }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function serviceFail(status: number, error: string): ServiceResult<never> {
|
|
15
|
+
return { ok: false, status, payload: { error } }
|
|
16
|
+
}
|