@swarmclawai/swarmclaw 1.2.1 → 1.2.3
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 +16 -85
- package/bin/server-cmd.js +64 -1
- package/package.json +2 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/devserver/route.ts +13 -19
- package/src/app/api/chats/[id]/messages/route.ts +13 -15
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/preview-server/route.ts +1 -1
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/cli/server-cmd.test.js +74 -0
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +19 -9
- package/src/components/chat/message-list.tsx +37 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
- package/src/instrumentation.ts +1 -1
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +3 -1
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +10 -3
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2266
- package/src/lib/server/openclaw/deploy.test.ts +42 -3
- package/src/lib/server/openclaw/deploy.ts +26 -12
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/runtime/alert-dispatch.ts +1 -1
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1470
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +55 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +2 -2
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2061
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +4 -2
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1377
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/storage.ts +13 -24
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/strip-internal-metadata.test.ts +42 -41
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +21 -5
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
2
|
import type { RunEventRecord, SessionRunRecord, SessionRunStatus, SSEEvent } from '@/types'
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
deleteRuntimeRun,
|
|
5
|
+
deleteRuntimeRunEvent,
|
|
5
6
|
loadRuntimeRun,
|
|
6
7
|
loadRuntimeRunEvents,
|
|
7
8
|
loadRuntimeRunEventsByRunId,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
patchRuntimeRun,
|
|
10
11
|
upsertRuntimeRun,
|
|
11
12
|
upsertRuntimeRunEvent,
|
|
12
|
-
} from '@/lib/server/
|
|
13
|
+
} from '@/lib/server/runtime/run-repository'
|
|
13
14
|
|
|
14
15
|
const MAX_SUMMARY_CHARS = 240
|
|
15
16
|
const RESTART_RECOVERABLE_SOURCES = new Set([
|
|
@@ -137,7 +138,7 @@ export function pruneOldRuns(): { prunedRuns: number; prunedEvents: number } {
|
|
|
137
138
|
// Non-terminal (running/queued) — only prune if stuck for much longer
|
|
138
139
|
if (deadline - endTs < ORPHANED_RUN_RETENTION_MS) continue
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
+
deleteRuntimeRun(id)
|
|
141
142
|
prunedRunIds.add(id)
|
|
142
143
|
prunedRuns++
|
|
143
144
|
}
|
|
@@ -146,7 +147,7 @@ export function pruneOldRuns(): { prunedRuns: number; prunedEvents: number } {
|
|
|
146
147
|
const events = loadRuntimeRunEvents()
|
|
147
148
|
for (const [id, event] of Object.entries(events)) {
|
|
148
149
|
if (prunedRunIds.has(event.runId)) {
|
|
149
|
-
|
|
150
|
+
deleteRuntimeRunEvent(id)
|
|
150
151
|
prunedEvents++
|
|
151
152
|
continue
|
|
152
153
|
}
|
|
@@ -154,7 +155,7 @@ export function pruneOldRuns(): { prunedRuns: number; prunedEvents: number } {
|
|
|
154
155
|
const parentRun = runs[event.runId]
|
|
155
156
|
if (!parentRun || !TERMINAL_STATUSES.has(parentRun.status)) continue
|
|
156
157
|
if (deadline - event.timestamp < RUN_EVENT_RETENTION_MS) continue
|
|
157
|
-
|
|
158
|
+
deleteRuntimeRunEvent(id)
|
|
158
159
|
prunedEvents++
|
|
159
160
|
}
|
|
160
161
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { RunEventRecord, SessionRunRecord } from '@/types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
deleteStoredItem,
|
|
5
|
+
loadRuntimeRun as loadStoredRuntimeRun,
|
|
6
|
+
loadRuntimeRunEvents as loadStoredRuntimeRunEvents,
|
|
7
|
+
loadRuntimeRunEventsByRunId as loadStoredRuntimeRunEventsByRunId,
|
|
8
|
+
loadRuntimeRuns as loadStoredRuntimeRuns,
|
|
9
|
+
patchRuntimeRun as patchStoredRuntimeRun,
|
|
10
|
+
saveRuntimeRunEvents as saveStoredRuntimeRunEvents,
|
|
11
|
+
saveRuntimeRuns as saveStoredRuntimeRuns,
|
|
12
|
+
upsertRuntimeRun as upsertStoredRuntimeRun,
|
|
13
|
+
upsertRuntimeRunEvent as upsertStoredRuntimeRunEvent,
|
|
14
|
+
} from '@/lib/server/storage'
|
|
15
|
+
import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
|
|
16
|
+
|
|
17
|
+
export const runRepository = createRecordRepository<SessionRunRecord>(
|
|
18
|
+
'runtime-runs',
|
|
19
|
+
{
|
|
20
|
+
get(id) {
|
|
21
|
+
return loadStoredRuntimeRun(id) as SessionRunRecord | null
|
|
22
|
+
},
|
|
23
|
+
list() {
|
|
24
|
+
return loadStoredRuntimeRuns() as Record<string, SessionRunRecord>
|
|
25
|
+
},
|
|
26
|
+
upsert(id, value) {
|
|
27
|
+
upsertStoredRuntimeRun(id, value as SessionRunRecord)
|
|
28
|
+
},
|
|
29
|
+
replace(data) {
|
|
30
|
+
saveStoredRuntimeRuns(data as Record<string, SessionRunRecord>)
|
|
31
|
+
},
|
|
32
|
+
patch(id, updater) {
|
|
33
|
+
return patchStoredRuntimeRun(id, updater as (current: SessionRunRecord | null) => SessionRunRecord | null) as SessionRunRecord | null
|
|
34
|
+
},
|
|
35
|
+
delete(id) {
|
|
36
|
+
deleteStoredItem('runtime_runs', id)
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export const runEventRepository = createRecordRepository<RunEventRecord>(
|
|
42
|
+
'runtime-run-events',
|
|
43
|
+
{
|
|
44
|
+
get(id) {
|
|
45
|
+
return (loadStoredRuntimeRunEvents() as Record<string, RunEventRecord>)[id] || null
|
|
46
|
+
},
|
|
47
|
+
list() {
|
|
48
|
+
return loadStoredRuntimeRunEvents() as Record<string, RunEventRecord>
|
|
49
|
+
},
|
|
50
|
+
upsert(id, value) {
|
|
51
|
+
upsertStoredRuntimeRunEvent(id, value as RunEventRecord)
|
|
52
|
+
},
|
|
53
|
+
replace(data) {
|
|
54
|
+
saveStoredRuntimeRunEvents(data as Record<string, RunEventRecord>)
|
|
55
|
+
},
|
|
56
|
+
delete(id) {
|
|
57
|
+
deleteStoredItem('runtime_run_events', id)
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
export const loadRuntimeRuns = () => runRepository.list()
|
|
63
|
+
export const saveRuntimeRuns = (items: Record<string, SessionRunRecord | Record<string, unknown>>) => runRepository.replace(items as Record<string, SessionRunRecord>)
|
|
64
|
+
export const loadRuntimeRun = (id: string) => runRepository.get(id)
|
|
65
|
+
export const upsertRuntimeRun = (id: string, value: SessionRunRecord | Record<string, unknown>) => runRepository.upsert(id, value as SessionRunRecord)
|
|
66
|
+
export const patchRuntimeRun = (id: string, updater: (current: SessionRunRecord | null) => SessionRunRecord | null) => runRepository.patch(id, updater)
|
|
67
|
+
|
|
68
|
+
export const loadRuntimeRunEvents = () => runEventRepository.list()
|
|
69
|
+
export const saveRuntimeRunEvents = (items: Record<string, RunEventRecord | Record<string, unknown>>) => runEventRepository.replace(items as Record<string, RunEventRecord>)
|
|
70
|
+
export const upsertRuntimeRunEvent = (id: string, value: RunEventRecord | Record<string, unknown>) => runEventRepository.upsert(id, value as RunEventRecord)
|
|
71
|
+
export const loadRuntimeRunEventsByRunId = (runId: string) => loadStoredRuntimeRunEventsByRunId(runId)
|
|
72
|
+
export const deleteRuntimeRun = (id: string) => runRepository.delete(id)
|
|
73
|
+
export const deleteRuntimeRunEvent = (id: string) => runEventRepository.delete(id)
|
|
@@ -2,7 +2,7 @@ import type { LoopMode } from '@/types'
|
|
|
2
2
|
import {
|
|
3
3
|
normalizeRuntimeSettingFields,
|
|
4
4
|
} from '@/lib/runtime/runtime-loop'
|
|
5
|
-
import { loadSettings } from '@/lib/server/
|
|
5
|
+
import { loadSettings } from '@/lib/server/settings/settings-repository'
|
|
6
6
|
|
|
7
7
|
export interface RuntimeSettings {
|
|
8
8
|
loopMode: LoopMode
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { hmrSingleton } from '@/lib/shared-utils'
|
|
4
|
+
|
|
5
|
+
export type ActiveSessionProcess = {
|
|
6
|
+
runId?: string | null
|
|
7
|
+
source?: string
|
|
8
|
+
kill: (signal?: NodeJS.Signals | number) => boolean | void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DevServerRuntime {
|
|
12
|
+
proc: ChildProcess
|
|
13
|
+
url: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RuntimeStateRegistry {
|
|
17
|
+
activeSessionProcesses: Map<string, ActiveSessionProcess>
|
|
18
|
+
devServers: Map<string, DevServerRuntime>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const state = hmrSingleton<RuntimeStateRegistry>('__swarmclaw_runtime_state__', () => ({
|
|
22
|
+
activeSessionProcesses: new Map<string, ActiveSessionProcess>(),
|
|
23
|
+
devServers: new Map<string, DevServerRuntime>(),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
if (!state.activeSessionProcesses) state.activeSessionProcesses = new Map<string, ActiveSessionProcess>()
|
|
27
|
+
if (!state.devServers) state.devServers = new Map<string, DevServerRuntime>()
|
|
28
|
+
|
|
29
|
+
export const activeSessionProcesses = state.activeSessionProcesses
|
|
30
|
+
export const devServers = state.devServers
|
|
31
|
+
|
|
32
|
+
export function getActiveSessionProcess(sessionId: string): ActiveSessionProcess | undefined {
|
|
33
|
+
return state.activeSessionProcesses.get(sessionId)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function hasActiveSessionProcess(sessionId: string): boolean {
|
|
37
|
+
return state.activeSessionProcesses.has(sessionId)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerActiveSessionProcess(sessionId: string, process: ActiveSessionProcess): void {
|
|
41
|
+
state.activeSessionProcesses.set(sessionId, process)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stopActiveSessionProcess(sessionId: string, signal?: NodeJS.Signals | number): boolean {
|
|
45
|
+
const process = state.activeSessionProcesses.get(sessionId)
|
|
46
|
+
if (!process) return false
|
|
47
|
+
try {
|
|
48
|
+
process.kill(signal)
|
|
49
|
+
} catch {
|
|
50
|
+
// Ignore process teardown errors during cleanup.
|
|
51
|
+
}
|
|
52
|
+
state.activeSessionProcesses.delete(sessionId)
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function clearActiveSessionProcess(sessionId: string): void {
|
|
57
|
+
state.activeSessionProcesses.delete(sessionId)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getDevServer(sessionId: string): DevServerRuntime | undefined {
|
|
61
|
+
return state.devServers.get(sessionId)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function hasDevServer(sessionId: string): boolean {
|
|
65
|
+
return state.devServers.has(sessionId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerDevServer(sessionId: string, runtime: DevServerRuntime): void {
|
|
69
|
+
state.devServers.set(sessionId, runtime)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function updateDevServerUrl(sessionId: string, url: string): void {
|
|
73
|
+
const runtime = state.devServers.get(sessionId)
|
|
74
|
+
if (!runtime) return
|
|
75
|
+
runtime.url = url
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function stopDevServer(sessionId: string): boolean {
|
|
79
|
+
const runtime = state.devServers.get(sessionId)
|
|
80
|
+
if (!runtime) return false
|
|
81
|
+
try {
|
|
82
|
+
runtime.proc.kill('SIGTERM')
|
|
83
|
+
} catch {
|
|
84
|
+
// Ignore process teardown errors during cleanup.
|
|
85
|
+
}
|
|
86
|
+
if (typeof runtime.proc.pid === 'number') {
|
|
87
|
+
try {
|
|
88
|
+
process.kill(-runtime.proc.pid, 'SIGTERM')
|
|
89
|
+
} catch {
|
|
90
|
+
// Ignore process-group teardown errors when the child is already gone.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
state.devServers.delete(sessionId)
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function clearDevServer(sessionId: string): void {
|
|
98
|
+
state.devServers.delete(sessionId)
|
|
99
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listAgents } from '@/lib/server/agents/agent-repository'
|
|
2
|
+
import { loadSchedules, upsertSchedule, upsertSchedules } from '@/lib/server/schedules/schedule-repository'
|
|
3
|
+
import { loadTasks, upsertTask } from '@/lib/server/tasks/task-repository'
|
|
2
4
|
import { enqueueTask } from '@/lib/server/runtime/queue'
|
|
3
5
|
import { CronExpressionParser } from 'cron-parser'
|
|
4
6
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/agents/main-agent-loop'
|
|
@@ -90,7 +92,7 @@ function computeNextRuns() {
|
|
|
90
92
|
async function tick(now = Date.now()) {
|
|
91
93
|
await processDueWatchJobs(now)
|
|
92
94
|
const schedules = loadSchedules()
|
|
93
|
-
const agents =
|
|
95
|
+
const agents = listAgents()
|
|
94
96
|
const tasks = loadTasks()
|
|
95
97
|
const inFlightScheduleKeys = new Set<string>(
|
|
96
98
|
Object.values(tasks as Record<string, ScheduleTaskLike>)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
abortSessionRuntime,
|
|
5
|
+
decrementNonHeartbeatWork,
|
|
6
|
+
emitRunMeta,
|
|
7
|
+
now,
|
|
8
|
+
reconcileSessionActivityLease,
|
|
9
|
+
state,
|
|
10
|
+
syncRunRecord,
|
|
11
|
+
} from './state'
|
|
12
|
+
import type { SessionRunQueueEntry } from './types'
|
|
13
|
+
|
|
14
|
+
export function cancelPendingForSession(sessionId: string, reason: string): number {
|
|
15
|
+
let cancelled = 0
|
|
16
|
+
for (const [key, queue] of state.queueByExecution.entries()) {
|
|
17
|
+
if (!queue.length) continue
|
|
18
|
+
const keep: SessionRunQueueEntry[] = []
|
|
19
|
+
for (const entry of queue) {
|
|
20
|
+
if (entry.run.sessionId !== sessionId) {
|
|
21
|
+
keep.push(entry)
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
entry.run.status = 'cancelled'
|
|
25
|
+
entry.run.endedAt = now()
|
|
26
|
+
entry.run.error = reason
|
|
27
|
+
syncRunRecord(entry.run)
|
|
28
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
29
|
+
entry.reject(new Error(reason))
|
|
30
|
+
decrementNonHeartbeatWork(entry)
|
|
31
|
+
cancelled += 1
|
|
32
|
+
}
|
|
33
|
+
if (keep.length > 0) state.queueByExecution.set(key, keep)
|
|
34
|
+
else state.queueByExecution.delete(key)
|
|
35
|
+
}
|
|
36
|
+
reconcileSessionActivityLease(sessionId)
|
|
37
|
+
return cancelled
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cancelQueuedEntries(
|
|
41
|
+
matcher: (entry: SessionRunQueueEntry) => boolean,
|
|
42
|
+
reason: string,
|
|
43
|
+
): { cancelled: number; sessionIds: Set<string> } {
|
|
44
|
+
let cancelled = 0
|
|
45
|
+
const sessionIds = new Set<string>()
|
|
46
|
+
for (const [key, queue] of state.queueByExecution.entries()) {
|
|
47
|
+
if (!queue.length) continue
|
|
48
|
+
const keep: SessionRunQueueEntry[] = []
|
|
49
|
+
for (const entry of queue) {
|
|
50
|
+
if (!matcher(entry)) {
|
|
51
|
+
keep.push(entry)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
entry.run.status = 'cancelled'
|
|
55
|
+
entry.run.endedAt = now()
|
|
56
|
+
entry.run.error = reason
|
|
57
|
+
syncRunRecord(entry.run)
|
|
58
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
59
|
+
entry.reject(new Error(reason))
|
|
60
|
+
decrementNonHeartbeatWork(entry)
|
|
61
|
+
sessionIds.add(entry.run.sessionId)
|
|
62
|
+
cancelled += 1
|
|
63
|
+
}
|
|
64
|
+
if (keep.length > 0) state.queueByExecution.set(key, keep)
|
|
65
|
+
else state.queueByExecution.delete(key)
|
|
66
|
+
}
|
|
67
|
+
for (const sessionId of sessionIds) reconcileSessionActivityLease(sessionId)
|
|
68
|
+
return { cancelled, sessionIds }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'): { cancelledQueued: number; abortedRunning: number } {
|
|
72
|
+
let cancelledQueued = 0
|
|
73
|
+
let abortedRunning = 0
|
|
74
|
+
|
|
75
|
+
for (const [key, queue] of state.queueByExecution.entries()) {
|
|
76
|
+
if (!queue.length) continue
|
|
77
|
+
const keep: SessionRunQueueEntry[] = []
|
|
78
|
+
for (const entry of queue) {
|
|
79
|
+
const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
|
|
80
|
+
if (!isHeartbeat) {
|
|
81
|
+
keep.push(entry)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
entry.run.status = 'cancelled'
|
|
85
|
+
entry.run.endedAt = now()
|
|
86
|
+
entry.run.error = reason
|
|
87
|
+
syncRunRecord(entry.run)
|
|
88
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
89
|
+
entry.reject(new Error(reason))
|
|
90
|
+
cancelledQueued += 1
|
|
91
|
+
}
|
|
92
|
+
if (keep.length > 0) state.queueByExecution.set(key, keep)
|
|
93
|
+
else state.queueByExecution.delete(key)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const entry of state.runningByExecution.values()) {
|
|
97
|
+
const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
|
|
98
|
+
if (!isHeartbeat) continue
|
|
99
|
+
abortedRunning += 1
|
|
100
|
+
abortSessionRuntime(entry, reason)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { cancelledQueued, abortedRunning }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function cancelAllRuns(reason = 'Cancelled'): { cancelledQueued: number; abortedRunning: number } {
|
|
107
|
+
let cancelledQueued = 0
|
|
108
|
+
let abortedRunning = 0
|
|
109
|
+
|
|
110
|
+
for (const [key, queue] of state.queueByExecution.entries()) {
|
|
111
|
+
if (!queue.length) continue
|
|
112
|
+
for (const entry of queue) {
|
|
113
|
+
entry.run.status = 'cancelled'
|
|
114
|
+
entry.run.endedAt = now()
|
|
115
|
+
entry.run.error = reason
|
|
116
|
+
syncRunRecord(entry.run)
|
|
117
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
118
|
+
entry.reject(new Error(reason))
|
|
119
|
+
cancelledQueued += 1
|
|
120
|
+
}
|
|
121
|
+
state.queueByExecution.delete(key)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const entry of state.runningByExecution.values()) {
|
|
125
|
+
abortedRunning += 1
|
|
126
|
+
abortSessionRuntime(entry, reason)
|
|
127
|
+
}
|
|
128
|
+
state.runningByExecution.clear()
|
|
129
|
+
state.nonHeartbeatWorkCount.clear()
|
|
130
|
+
|
|
131
|
+
return { cancelledQueued, abortedRunning }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function cancelQueuedRunById(runId: string, reason = 'Removed from queue'): boolean {
|
|
135
|
+
const result = cancelQueuedEntries((entry) => entry.run.id === runId, reason)
|
|
136
|
+
return result.cancelled > 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function cancelQueuedRunsForSession(sessionId: string, reason = 'Cleared queued messages'): number {
|
|
140
|
+
const result = cancelQueuedEntries((entry) => entry.run.sessionId === sessionId, reason)
|
|
141
|
+
return result.cancelled
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function cancelSessionRuns(sessionId: string, reason = 'Cancelled'): { cancelledQueued: number; cancelledRunning: boolean } {
|
|
145
|
+
const running = Array.from(state.runningByExecution.values())
|
|
146
|
+
.find((entry) => entry.run.sessionId === sessionId)
|
|
147
|
+
let cancelledRunning = false
|
|
148
|
+
if (running) {
|
|
149
|
+
cancelledRunning = true
|
|
150
|
+
abortSessionRuntime(running, reason)
|
|
151
|
+
state.runningByExecution.delete(running.executionKey)
|
|
152
|
+
decrementNonHeartbeatWork(running)
|
|
153
|
+
}
|
|
154
|
+
const cancelledQueued = cancelPendingForSession(sessionId, reason)
|
|
155
|
+
reconcileSessionActivityLease(sessionId)
|
|
156
|
+
return { cancelledQueued, cancelledRunning }
|
|
157
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { executeSessionChatTurn } from '@/lib/server/chat-execution/chat-execution'
|
|
2
|
+
import { log } from '@/lib/server/logger'
|
|
3
|
+
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
6
|
+
import { handleMainLoopRunResult } from '@/lib/server/agents/main-agent-loop'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
clearDeferredDrain,
|
|
10
|
+
decrementNonHeartbeatWork,
|
|
11
|
+
emitRunMeta,
|
|
12
|
+
emitToSubscribers,
|
|
13
|
+
hasActiveNonHeartbeatSessionLease,
|
|
14
|
+
hasExternalSessionExecutionHold,
|
|
15
|
+
HEARTBEAT_BUSY_RETRY_MS,
|
|
16
|
+
MAX_DRAIN_DEPTH,
|
|
17
|
+
now,
|
|
18
|
+
queueAutonomyObservation,
|
|
19
|
+
queueForExecution,
|
|
20
|
+
reconcileSessionActivityLease,
|
|
21
|
+
scheduleDeferredDrain,
|
|
22
|
+
state,
|
|
23
|
+
syncRunRecord,
|
|
24
|
+
} from './state'
|
|
25
|
+
import type { EnqueueSessionRunInput } from './types'
|
|
26
|
+
|
|
27
|
+
type EnqueueSessionRunFn = (input: EnqueueSessionRunInput) => unknown
|
|
28
|
+
|
|
29
|
+
export async function drainExecution(
|
|
30
|
+
executionKey: string,
|
|
31
|
+
deps: { enqueueSessionRun: EnqueueSessionRunFn },
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const depth = (state.drainDepth.get(executionKey) || 0) + 1
|
|
34
|
+
state.drainDepth.set(executionKey, depth)
|
|
35
|
+
if (depth > MAX_DRAIN_DEPTH) {
|
|
36
|
+
log.error('session-run', 'Drain recursion depth exceeded, deferring', { executionKey, depth, max: MAX_DRAIN_DEPTH })
|
|
37
|
+
state.drainDepth.delete(executionKey)
|
|
38
|
+
scheduleDeferredDrain(executionKey, (nextExecutionKey) => { void drainExecution(nextExecutionKey, deps) }, 500)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (state.runningByExecution.has(executionKey)) return
|
|
43
|
+
const queue = queueForExecution(executionKey)
|
|
44
|
+
const userIdx = queue.findIndex((entry) => !entry.run.internal)
|
|
45
|
+
let next
|
|
46
|
+
if (userIdx >= 0) {
|
|
47
|
+
next = queue.splice(userIdx, 1)[0]
|
|
48
|
+
} else {
|
|
49
|
+
const internalIdx = queue.findIndex((entry) => !isInternalHeartbeatRun(entry.run.internal, entry.run.source))
|
|
50
|
+
next = internalIdx >= 0 ? queue.splice(internalIdx, 1)[0] : queue.shift()
|
|
51
|
+
}
|
|
52
|
+
if (!next) {
|
|
53
|
+
clearDeferredDrain(executionKey)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isInternalHeartbeatRun(next.run.internal, next.run.source) && hasActiveNonHeartbeatSessionLease(next.run.sessionId)) {
|
|
58
|
+
queue.unshift(next)
|
|
59
|
+
scheduleDeferredDrain(executionKey, (nextExecutionKey) => { void drainExecution(nextExecutionKey, deps) }, HEARTBEAT_BUSY_RETRY_MS)
|
|
60
|
+
log.info('session-run', `Deferred heartbeat run ${next.run.id} for shared busy session`, {
|
|
61
|
+
sessionId: next.run.sessionId,
|
|
62
|
+
source: next.run.source,
|
|
63
|
+
})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (hasExternalSessionExecutionHold(next.run.sessionId)) {
|
|
68
|
+
queue.unshift(next)
|
|
69
|
+
scheduleDeferredDrain(executionKey, (nextExecutionKey) => { void drainExecution(nextExecutionKey, deps) }, HEARTBEAT_BUSY_RETRY_MS)
|
|
70
|
+
log.info('session-run', `Deferred run ${next.run.id} for external session hold`, {
|
|
71
|
+
sessionId: next.run.sessionId,
|
|
72
|
+
source: next.run.source,
|
|
73
|
+
mode: next.run.mode,
|
|
74
|
+
})
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clearDeferredDrain(executionKey)
|
|
79
|
+
state.runningByExecution.set(executionKey, next)
|
|
80
|
+
next.run.status = 'running'
|
|
81
|
+
next.run.startedAt = now()
|
|
82
|
+
syncRunRecord(next.run)
|
|
83
|
+
emitRunMeta(next, 'running')
|
|
84
|
+
log.info('session-run', `Run started ${next.run.id}`, {
|
|
85
|
+
sessionId: next.run.sessionId,
|
|
86
|
+
source: next.run.source,
|
|
87
|
+
internal: next.run.internal,
|
|
88
|
+
mode: next.run.mode,
|
|
89
|
+
timeoutMs: next.maxRuntimeMs || null,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
let runtimeTimer: ReturnType<typeof setTimeout> | null = null
|
|
93
|
+
let finishedMissionId: string | null = null
|
|
94
|
+
if (next.maxRuntimeMs && next.maxRuntimeMs > 0) {
|
|
95
|
+
runtimeTimer = setTimeout(() => {
|
|
96
|
+
next.signalController.abort()
|
|
97
|
+
}, next.maxRuntimeMs)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await executeSessionChatTurn({
|
|
102
|
+
sessionId: next.run.sessionId,
|
|
103
|
+
message: next.message,
|
|
104
|
+
imagePath: next.imagePath,
|
|
105
|
+
imageUrl: next.imageUrl,
|
|
106
|
+
attachedFiles: next.attachedFiles,
|
|
107
|
+
internal: next.run.internal,
|
|
108
|
+
source: next.run.source,
|
|
109
|
+
runId: next.run.id,
|
|
110
|
+
signal: next.signalController.signal,
|
|
111
|
+
onEvent: (event) => emitToSubscribers(next, event),
|
|
112
|
+
modelOverride: next.modelOverride,
|
|
113
|
+
heartbeatConfig: next.heartbeatConfig,
|
|
114
|
+
replyToId: next.replyToId,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const failed = !!result.error
|
|
118
|
+
const aborted = next.signalController.signal.aborted
|
|
119
|
+
next.run.status = aborted ? 'cancelled' : (failed ? 'failed' : 'completed')
|
|
120
|
+
next.run.endedAt = next.run.endedAt || now()
|
|
121
|
+
next.run.error = aborted ? (next.run.error || 'Cancelled') : result.error
|
|
122
|
+
next.run.missionId = result.missionId || next.run.missionId || null
|
|
123
|
+
finishedMissionId = next.run.missionId || null
|
|
124
|
+
next.run.resultPreview = result.text?.slice(0, 280)
|
|
125
|
+
if (typeof result.inputTokens === 'number') next.run.totalInputTokens = result.inputTokens
|
|
126
|
+
if (typeof result.outputTokens === 'number') next.run.totalOutputTokens = result.outputTokens
|
|
127
|
+
if (typeof result.estimatedCost === 'number') next.run.estimatedCost = result.estimatedCost
|
|
128
|
+
syncRunRecord(next.run)
|
|
129
|
+
emitRunMeta(next, next.run.status, {
|
|
130
|
+
persisted: result.persisted,
|
|
131
|
+
hasText: !!result.text,
|
|
132
|
+
error: next.run.error || null,
|
|
133
|
+
})
|
|
134
|
+
log.info('session-run', `Run finished ${next.run.id}`, {
|
|
135
|
+
sessionId: next.run.sessionId,
|
|
136
|
+
status: next.run.status,
|
|
137
|
+
persisted: result.persisted,
|
|
138
|
+
hasText: !!result.text,
|
|
139
|
+
error: next.run.error || null,
|
|
140
|
+
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
141
|
+
})
|
|
142
|
+
const followup = handleMainLoopRunResult({
|
|
143
|
+
runId: next.run.id,
|
|
144
|
+
sessionId: next.run.sessionId,
|
|
145
|
+
message: next.message,
|
|
146
|
+
internal: next.run.internal,
|
|
147
|
+
source: next.run.source,
|
|
148
|
+
resultText: result.text,
|
|
149
|
+
error: next.run.error,
|
|
150
|
+
toolEvents: result.toolEvents,
|
|
151
|
+
inputTokens: result.inputTokens,
|
|
152
|
+
outputTokens: result.outputTokens,
|
|
153
|
+
estimatedCost: result.estimatedCost,
|
|
154
|
+
})
|
|
155
|
+
queueAutonomyObservation({
|
|
156
|
+
runId: next.run.id,
|
|
157
|
+
sessionId: next.run.sessionId,
|
|
158
|
+
source: next.run.source,
|
|
159
|
+
status: next.run.status,
|
|
160
|
+
resultText: result.text,
|
|
161
|
+
error: next.run.error || null,
|
|
162
|
+
toolEvents: result.toolEvents,
|
|
163
|
+
sourceMessage: next.message,
|
|
164
|
+
})
|
|
165
|
+
if (followup) {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
try {
|
|
168
|
+
deps.enqueueSessionRun({
|
|
169
|
+
sessionId: next.run.sessionId,
|
|
170
|
+
message: followup.message,
|
|
171
|
+
internal: true,
|
|
172
|
+
source: 'main-loop-followup',
|
|
173
|
+
mode: 'followup',
|
|
174
|
+
dedupeKey: followup.dedupeKey,
|
|
175
|
+
})
|
|
176
|
+
} catch (err: unknown) {
|
|
177
|
+
log.warn('session-run', `Main loop follow-up enqueue failed for ${next.run.sessionId}`, {
|
|
178
|
+
error: errorMessage(err),
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}, Math.max(0, followup.delayMs || 0))
|
|
182
|
+
}
|
|
183
|
+
next.resolve(result)
|
|
184
|
+
} catch (err: unknown) {
|
|
185
|
+
const aborted = next.signalController.signal.aborted
|
|
186
|
+
next.run.status = aborted ? 'cancelled' : 'failed'
|
|
187
|
+
next.run.endedAt = now()
|
|
188
|
+
next.run.error = errorMessage(err)
|
|
189
|
+
finishedMissionId = next.run.missionId || null
|
|
190
|
+
syncRunRecord(next.run)
|
|
191
|
+
emitRunMeta(next, next.run.status, { error: next.run.error })
|
|
192
|
+
log.error('session-run', `Run failed ${next.run.id}`, {
|
|
193
|
+
sessionId: next.run.sessionId,
|
|
194
|
+
status: next.run.status,
|
|
195
|
+
error: next.run.error,
|
|
196
|
+
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
197
|
+
})
|
|
198
|
+
if (err instanceof Error && err.stack) {
|
|
199
|
+
log.error('session-run', `Run failed stack trace ${next.run.id}`, {
|
|
200
|
+
sessionId: next.run.sessionId,
|
|
201
|
+
stack: err.stack,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
queueAutonomyObservation({
|
|
205
|
+
runId: next.run.id,
|
|
206
|
+
sessionId: next.run.sessionId,
|
|
207
|
+
source: next.run.source,
|
|
208
|
+
status: next.run.status,
|
|
209
|
+
error: next.run.error || null,
|
|
210
|
+
sourceMessage: next.message,
|
|
211
|
+
})
|
|
212
|
+
next.reject(err instanceof Error ? err : new Error(next.run.error))
|
|
213
|
+
} finally {
|
|
214
|
+
if (runtimeTimer) clearTimeout(runtimeTimer)
|
|
215
|
+
state.runningByExecution.delete(executionKey)
|
|
216
|
+
decrementNonHeartbeatWork(next)
|
|
217
|
+
reconcileSessionActivityLease(next.run.sessionId)
|
|
218
|
+
notify(`stream-end:${next.run.sessionId}`)
|
|
219
|
+
if (finishedMissionId && next.run.source !== 'chat') {
|
|
220
|
+
const missionId = finishedMissionId
|
|
221
|
+
queueMicrotask(() => {
|
|
222
|
+
import('@/lib/server/missions/mission-service')
|
|
223
|
+
.then(({ loadMissionById, requestMissionTick }) => {
|
|
224
|
+
const mission = loadMissionById(missionId)
|
|
225
|
+
if (!mission) return
|
|
226
|
+
if (mission.status !== 'active') return
|
|
227
|
+
if (mission.phase === 'dispatching' || mission.phase === 'executing') return
|
|
228
|
+
requestMissionTick(missionId, 'run_drained', {
|
|
229
|
+
runId: next.run.id,
|
|
230
|
+
source: next.run.source,
|
|
231
|
+
status: next.run.status,
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
.catch((err: unknown) => {
|
|
235
|
+
log.warn('session-run', 'Mission tick failed', { missionId, runId: next.run.id, error: errorMessage(err) })
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
void drainExecution(executionKey, deps)
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
const currentDepth = state.drainDepth.get(executionKey)
|
|
243
|
+
if (currentDepth && currentDepth > 1) state.drainDepth.set(executionKey, currentDepth - 1)
|
|
244
|
+
else state.drainDepth.delete(executionKey)
|
|
245
|
+
}
|
|
246
|
+
}
|