@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.
Files changed (149) hide show
  1. package/README.md +16 -85
  2. package/bin/server-cmd.js +64 -1
  3. package/package.json +2 -2
  4. package/skills/coding-agent/SKILL.md +111 -0
  5. package/skills/github/SKILL.md +140 -0
  6. package/skills/nano-banana-pro/SKILL.md +62 -0
  7. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  8. package/skills/nano-pdf/SKILL.md +53 -0
  9. package/skills/openai-image-gen/SKILL.md +78 -0
  10. package/skills/openai-image-gen/scripts/gen.py +328 -0
  11. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  12. package/skills/skill-creator/SKILL.md +147 -0
  13. package/skills/skill-creator/scripts/init_skill.py +378 -0
  14. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  15. package/skills/summarize/SKILL.md +77 -0
  16. package/src/app/api/auth/route.ts +20 -5
  17. package/src/app/api/chats/[id]/devserver/route.ts +13 -19
  18. package/src/app/api/chats/[id]/messages/route.ts +13 -15
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/ip/route.ts +2 -2
  24. package/src/app/api/preview-server/route.ts +1 -1
  25. package/src/app/api/projects/[id]/route.ts +7 -46
  26. package/src/cli/server-cmd.test.js +74 -0
  27. package/src/components/chat/chat-area.tsx +45 -23
  28. package/src/components/chat/message-bubble.test.ts +35 -0
  29. package/src/components/chat/message-bubble.tsx +19 -9
  30. package/src/components/chat/message-list.tsx +37 -3
  31. package/src/components/input/chat-input.tsx +34 -14
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
  33. package/src/instrumentation.ts +1 -1
  34. package/src/lib/chat/assistant-render-id.ts +3 -0
  35. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  36. package/src/lib/chat/chat-streaming-state.ts +20 -8
  37. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  38. package/src/lib/chat/queued-message-queue.ts +11 -2
  39. package/src/lib/providers/cli-utils.test.ts +124 -0
  40. package/src/lib/server/activity/activity-log.ts +21 -0
  41. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  42. package/src/lib/server/agents/agent-cascade.ts +79 -59
  43. package/src/lib/server/agents/agent-registry.ts +3 -1
  44. package/src/lib/server/agents/agent-repository.ts +90 -0
  45. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  46. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  47. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  48. package/src/lib/server/agents/guardian.ts +2 -2
  49. package/src/lib/server/agents/main-agent-loop.ts +10 -3
  50. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  51. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  52. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  53. package/src/lib/server/agents/task-session.ts +3 -4
  54. package/src/lib/server/approvals/approval-repository.ts +30 -0
  55. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  56. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  57. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  58. package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
  59. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  60. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  61. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  62. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  63. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  64. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  65. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  66. package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
  67. package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
  68. package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
  69. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  70. package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
  71. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  72. package/src/lib/server/connectors/connector-repository.ts +58 -0
  73. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  74. package/src/lib/server/credentials/credential-repository.ts +7 -0
  75. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  76. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  77. package/src/lib/server/missions/mission-repository.ts +74 -0
  78. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  79. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  80. package/src/lib/server/missions/mission-service/context.ts +4 -0
  81. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  82. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  83. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  84. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  85. package/src/lib/server/missions/mission-service.test.ts +9 -2
  86. package/src/lib/server/missions/mission-service.ts +6 -2266
  87. package/src/lib/server/openclaw/deploy.test.ts +42 -3
  88. package/src/lib/server/openclaw/deploy.ts +26 -12
  89. package/src/lib/server/persistence/repository-utils.ts +154 -0
  90. package/src/lib/server/persistence/storage-context.ts +51 -0
  91. package/src/lib/server/persistence/transaction.ts +1 -0
  92. package/src/lib/server/projects/project-repository.ts +36 -0
  93. package/src/lib/server/projects/project-service.ts +79 -0
  94. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  95. package/src/lib/server/runtime/alert-dispatch.ts +1 -1
  96. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  97. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  98. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  99. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  100. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  101. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  102. package/src/lib/server/runtime/daemon-state.ts +3 -1470
  103. package/src/lib/server/runtime/estop-repository.ts +4 -0
  104. package/src/lib/server/runtime/estop.ts +3 -1
  105. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  106. package/src/lib/server/runtime/heartbeat-service.ts +55 -34
  107. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  108. package/src/lib/server/runtime/idle-window.ts +2 -2
  109. package/src/lib/server/runtime/network.ts +11 -0
  110. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  111. package/src/lib/server/runtime/queue/claims.ts +4 -0
  112. package/src/lib/server/runtime/queue/core.ts +2079 -0
  113. package/src/lib/server/runtime/queue/execution.ts +7 -0
  114. package/src/lib/server/runtime/queue/followups.ts +4 -0
  115. package/src/lib/server/runtime/queue/queries.ts +12 -0
  116. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  117. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  118. package/src/lib/server/runtime/queue-repository.ts +17 -0
  119. package/src/lib/server/runtime/queue.ts +5 -2061
  120. package/src/lib/server/runtime/run-ledger.ts +6 -5
  121. package/src/lib/server/runtime/run-repository.ts +73 -0
  122. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  123. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  124. package/src/lib/server/runtime/runtime-state.ts +99 -0
  125. package/src/lib/server/runtime/scheduler.ts +4 -2
  126. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  127. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  128. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  129. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  130. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  131. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  132. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  133. package/src/lib/server/runtime/session-run-manager.ts +72 -1377
  134. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  135. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  136. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  137. package/src/lib/server/sessions/session-repository.ts +85 -0
  138. package/src/lib/server/settings/settings-repository.ts +25 -0
  139. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  140. package/src/lib/server/skills/skill-discovery.ts +2 -2
  141. package/src/lib/server/skills/skill-repository.ts +14 -0
  142. package/src/lib/server/storage.ts +13 -24
  143. package/src/lib/server/tasks/task-repository.ts +54 -0
  144. package/src/lib/server/usage/usage-repository.ts +30 -0
  145. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  146. package/src/lib/strip-internal-metadata.test.ts +42 -41
  147. package/src/stores/use-chat-store.test.ts +54 -0
  148. package/src/stores/use-chat-store.ts +21 -5
  149. /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
- deleteStoredItem,
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/storage'
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
- deleteStoredItem('runtime_runs', id)
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
- deleteStoredItem('runtime_run_events', id)
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
- deleteStoredItem('runtime_run_events', id)
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)
@@ -0,0 +1,8 @@
1
+ export {
2
+ isRuntimeLockActive,
3
+ pruneExpiredLocks,
4
+ readRuntimeLock,
5
+ releaseRuntimeLock,
6
+ renewRuntimeLock,
7
+ tryAcquireRuntimeLock,
8
+ } from '@/lib/server/storage'
@@ -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/storage'
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 { loadSchedules, loadAgents, loadTasks, upsertSchedule, upsertSchedules, upsertTask } from '@/lib/server/storage'
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 = loadAgents()
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
+ }