@swarmclawai/swarmclaw 1.2.0 → 1.2.2

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 (241) hide show
  1. package/README.md +19 -0
  2. package/package.json +5 -2
  3. package/skills/coding-agent/SKILL.md +111 -0
  4. package/skills/github/SKILL.md +140 -0
  5. package/skills/nano-banana-pro/SKILL.md +62 -0
  6. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  7. package/skills/nano-pdf/SKILL.md +53 -0
  8. package/skills/openai-image-gen/SKILL.md +78 -0
  9. package/skills/openai-image-gen/scripts/gen.py +328 -0
  10. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  11. package/skills/skill-creator/SKILL.md +147 -0
  12. package/skills/skill-creator/scripts/init_skill.py +378 -0
  13. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  14. package/skills/summarize/SKILL.md +77 -0
  15. package/src/app/api/auth/route.ts +20 -5
  16. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  17. package/src/app/api/chats/[id]/devserver/route.ts +17 -20
  18. package/src/app/api/chats/[id]/messages/route.ts +15 -11
  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/credentials/[id]/route.ts +4 -1
  24. package/src/app/api/extensions/marketplace/route.ts +5 -2
  25. package/src/app/api/ip/route.ts +2 -2
  26. package/src/app/api/memory/maintenance/route.ts +5 -2
  27. package/src/app/api/preview-server/route.ts +15 -12
  28. package/src/app/api/projects/[id]/route.ts +7 -46
  29. package/src/app/api/system/status/route.ts +11 -0
  30. package/src/app/api/upload/route.ts +4 -1
  31. package/src/cli/index.js +7 -0
  32. package/src/cli/spec.js +1 -0
  33. package/src/components/agents/agent-files-editor.tsx +44 -32
  34. package/src/components/agents/personality-builder.tsx +13 -7
  35. package/src/components/agents/trash-list.tsx +1 -1
  36. package/src/components/chat/chat-area.tsx +45 -23
  37. package/src/components/chat/message-bubble.test.ts +35 -0
  38. package/src/components/chat/message-bubble.tsx +20 -9
  39. package/src/components/chat/message-list.tsx +62 -42
  40. package/src/components/chat/swarm-status-card.tsx +10 -3
  41. package/src/components/input/chat-input.tsx +34 -14
  42. package/src/components/layout/daemon-indicator.tsx +7 -8
  43. package/src/components/layout/update-banner.tsx +8 -13
  44. package/src/components/logs/log-list.tsx +1 -1
  45. package/src/components/memory/memory-card.tsx +3 -1
  46. package/src/components/org-chart/org-chart-view.tsx +4 -0
  47. package/src/components/projects/project-list.tsx +4 -2
  48. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  49. package/src/components/secrets/secret-sheet.tsx +1 -1
  50. package/src/components/secrets/secrets-list.tsx +1 -1
  51. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  52. package/src/components/shared/dir-browser.tsx +22 -18
  53. package/src/components/skills/skill-sheet.tsx +2 -3
  54. package/src/components/tasks/task-list.tsx +1 -1
  55. package/src/components/tasks/task-sheet.tsx +1 -1
  56. package/src/hooks/use-openclaw-gateway.ts +46 -27
  57. package/src/instrumentation.ts +10 -7
  58. package/src/lib/chat/assistant-render-id.ts +3 -0
  59. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  60. package/src/lib/chat/chat-streaming-state.ts +20 -8
  61. package/src/lib/chat/chat.ts +18 -2
  62. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  63. package/src/lib/chat/queued-message-queue.ts +11 -2
  64. package/src/lib/providers/anthropic.ts +6 -3
  65. package/src/lib/providers/claude-cli.ts +9 -3
  66. package/src/lib/providers/cli-utils.test.ts +124 -0
  67. package/src/lib/providers/cli-utils.ts +15 -0
  68. package/src/lib/providers/codex-cli.ts +9 -3
  69. package/src/lib/providers/gemini-cli.ts +6 -2
  70. package/src/lib/providers/index.ts +4 -1
  71. package/src/lib/providers/ollama.ts +5 -2
  72. package/src/lib/providers/openai.ts +8 -5
  73. package/src/lib/providers/opencode-cli.ts +6 -2
  74. package/src/lib/server/activity/activity-log.ts +21 -0
  75. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  76. package/src/lib/server/agents/agent-cascade.ts +79 -59
  77. package/src/lib/server/agents/agent-registry.ts +23 -4
  78. package/src/lib/server/agents/agent-repository.ts +90 -0
  79. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  80. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  81. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  82. package/src/lib/server/agents/guardian.ts +2 -2
  83. package/src/lib/server/agents/main-agent-loop.ts +14 -6
  84. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  85. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  86. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  87. package/src/lib/server/agents/task-session.ts +3 -4
  88. package/src/lib/server/approvals/approval-repository.ts +30 -0
  89. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  90. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  91. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  92. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  93. package/src/lib/server/chat-execution/chat-execution.ts +84 -1914
  94. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  95. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  96. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  97. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  98. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  99. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  100. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  101. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  102. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  103. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  104. package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
  105. package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
  106. package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
  107. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  108. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  109. package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
  110. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  111. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  112. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  113. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  114. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  115. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  116. package/src/lib/server/connectors/connector-repository.ts +58 -0
  117. package/src/lib/server/connectors/discord.ts +10 -7
  118. package/src/lib/server/connectors/email.ts +17 -14
  119. package/src/lib/server/connectors/googlechat.ts +7 -4
  120. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  121. package/src/lib/server/connectors/matrix.ts +6 -3
  122. package/src/lib/server/connectors/openclaw.ts +20 -17
  123. package/src/lib/server/connectors/outbox.ts +4 -1
  124. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  125. package/src/lib/server/connectors/runtime-state.ts +19 -0
  126. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  127. package/src/lib/server/connectors/signal.ts +9 -6
  128. package/src/lib/server/connectors/slack.ts +13 -10
  129. package/src/lib/server/connectors/teams.ts +8 -5
  130. package/src/lib/server/connectors/telegram.ts +15 -12
  131. package/src/lib/server/connectors/whatsapp.ts +32 -29
  132. package/src/lib/server/credentials/credential-repository.ts +7 -0
  133. package/src/lib/server/embeddings.ts +4 -1
  134. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  135. package/src/lib/server/link-understanding.ts +4 -1
  136. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  137. package/src/lib/server/memory/memory-abstract.ts +59 -0
  138. package/src/lib/server/memory/memory-db.ts +40 -14
  139. package/src/lib/server/missions/mission-repository.ts +74 -0
  140. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  141. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  142. package/src/lib/server/missions/mission-service/context.ts +4 -0
  143. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  144. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  145. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  146. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  147. package/src/lib/server/missions/mission-service.test.ts +9 -2
  148. package/src/lib/server/missions/mission-service.ts +6 -2263
  149. package/src/lib/server/openclaw/gateway.ts +8 -5
  150. package/src/lib/server/persistence/repository-utils.ts +154 -0
  151. package/src/lib/server/persistence/storage-context.ts +51 -0
  152. package/src/lib/server/persistence/transaction.ts +1 -0
  153. package/src/lib/server/project-utils.ts +13 -0
  154. package/src/lib/server/projects/project-repository.ts +36 -0
  155. package/src/lib/server/projects/project-service.ts +79 -0
  156. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  157. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  158. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  159. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  160. package/src/lib/server/provider-health.ts +18 -0
  161. package/src/lib/server/query-expansion.ts +4 -1
  162. package/src/lib/server/runtime/alert-dispatch.ts +8 -7
  163. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  164. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  165. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  166. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  167. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  168. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  169. package/src/lib/server/runtime/daemon-state.ts +3 -1331
  170. package/src/lib/server/runtime/estop-repository.ts +4 -0
  171. package/src/lib/server/runtime/estop.ts +3 -1
  172. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  173. package/src/lib/server/runtime/heartbeat-service.ts +78 -34
  174. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  175. package/src/lib/server/runtime/idle-window.ts +6 -3
  176. package/src/lib/server/runtime/network.ts +11 -0
  177. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  178. package/src/lib/server/runtime/perf.ts +4 -1
  179. package/src/lib/server/runtime/process-manager.ts +7 -4
  180. package/src/lib/server/runtime/queue/claims.ts +4 -0
  181. package/src/lib/server/runtime/queue/core.ts +2079 -0
  182. package/src/lib/server/runtime/queue/execution.ts +7 -0
  183. package/src/lib/server/runtime/queue/followups.ts +4 -0
  184. package/src/lib/server/runtime/queue/queries.ts +12 -0
  185. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  186. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  187. package/src/lib/server/runtime/queue-repository.ts +17 -0
  188. package/src/lib/server/runtime/queue.ts +5 -2058
  189. package/src/lib/server/runtime/run-ledger.ts +6 -5
  190. package/src/lib/server/runtime/run-repository.ts +73 -0
  191. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  192. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  193. package/src/lib/server/runtime/runtime-state.ts +99 -0
  194. package/src/lib/server/runtime/scheduler.ts +13 -8
  195. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  196. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  197. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  198. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  199. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  200. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  201. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  202. package/src/lib/server/runtime/session-run-manager.ts +72 -1374
  203. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  204. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  205. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  206. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  207. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  208. package/src/lib/server/session-tools/context.ts +14 -0
  209. package/src/lib/server/session-tools/discovery.ts +9 -6
  210. package/src/lib/server/session-tools/index.ts +3 -1
  211. package/src/lib/server/session-tools/platform.ts +1 -1
  212. package/src/lib/server/session-tools/subagent.ts +23 -2
  213. package/src/lib/server/session-tools/wallet.ts +4 -1
  214. package/src/lib/server/sessions/session-repository.ts +85 -0
  215. package/src/lib/server/settings/settings-repository.ts +25 -0
  216. package/src/lib/server/skills/clawhub-client.ts +4 -1
  217. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  218. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  219. package/src/lib/server/skills/skill-discovery.ts +2 -2
  220. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  221. package/src/lib/server/skills/skill-repository.ts +14 -0
  222. package/src/lib/server/solana.ts +6 -0
  223. package/src/lib/server/storage-auth.ts +5 -5
  224. package/src/lib/server/storage-normalization.ts +4 -0
  225. package/src/lib/server/storage.ts +32 -32
  226. package/src/lib/server/tasks/task-followups.ts +4 -1
  227. package/src/lib/server/tasks/task-repository.ts +54 -0
  228. package/src/lib/server/tool-loop-detection.ts +8 -3
  229. package/src/lib/server/tool-planning.ts +226 -0
  230. package/src/lib/server/tool-retry.ts +4 -3
  231. package/src/lib/server/usage/usage-repository.ts +30 -0
  232. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  233. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  234. package/src/lib/server/ws-hub.ts +5 -2
  235. package/src/lib/strip-internal-metadata.test.ts +78 -37
  236. package/src/lib/strip-internal-metadata.ts +20 -6
  237. package/src/stores/use-approval-store.ts +7 -1
  238. package/src/stores/use-chat-store.test.ts +54 -0
  239. package/src/stores/use-chat-store.ts +26 -6
  240. package/src/types/index.ts +6 -0
  241. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -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
+ }
@@ -0,0 +1,287 @@
1
+ import { genId } from '@/lib/id'
2
+ import type { SessionRunRecord } from '@/types'
3
+ import { getSession } from '@/lib/server/sessions/session-repository'
4
+ import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
5
+ import { log } from '@/lib/server/logger'
6
+ import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
7
+ import { getEnabledToolIds } from '@/lib/capability-selection'
8
+ import { isAllEstopEngaged, isAutonomyEstopEngaged } from '@/lib/server/runtime/estop'
9
+ import { getActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
10
+
11
+ import { cancelPendingForSession } from './cancellation'
12
+ import {
13
+ abortSessionRuntime,
14
+ chainCallerSignal,
15
+ COLLECT_COALESCE_WINDOW_MS,
16
+ emitRunMeta,
17
+ executionKeyForSession,
18
+ incrementNonHeartbeatWork,
19
+ messagePreview,
20
+ nextQueuedAt,
21
+ normalizeMode,
22
+ queueForExecution,
23
+ reconcileSessionActivityLease,
24
+ registerRun,
25
+ state,
26
+ syncRunRecord,
27
+ } from './state'
28
+ import type {
29
+ EnqueueSessionRunInput,
30
+ EnqueueSessionRunResult,
31
+ SessionQueueMode,
32
+ SessionRunQueueEntry,
33
+ } from './types'
34
+
35
+ type RepairSessionRunQueueFn = (
36
+ sessionId: string,
37
+ opts?: {
38
+ executionKey?: string
39
+ maxQueuedAgeMs?: number
40
+ reason?: string
41
+ },
42
+ ) => { kickedExecutionKeys: number; recoveredQueuedRuns: number }
43
+
44
+ type DrainExecutionFn = (executionKey: string) => Promise<void>
45
+
46
+ const LONG_TOOL_NAMES: ReadonlySet<string> = new Set(['claude_code', 'codex_cli', 'opencode_cli'])
47
+
48
+ type SessionToolConfig = {
49
+ tools?: string[] | null
50
+ extensions?: string[] | null
51
+ }
52
+
53
+ function computeEffectiveRunTimeoutMs(
54
+ baseTimeoutMs: number,
55
+ sessionTools: string[],
56
+ runtime: { claudeCodeTimeoutMs: number },
57
+ ): number {
58
+ const hasLongTool = sessionTools.some((tool) => LONG_TOOL_NAMES.has(tool))
59
+ if (!hasLongTool) return baseTimeoutMs
60
+ const toolTimeout = runtime.claudeCodeTimeoutMs + 120_000
61
+ return Math.max(baseTimeoutMs, toolTimeout)
62
+ }
63
+
64
+ function isAutonomyManagedEnqueue(source: string, internal: boolean): boolean {
65
+ return !(source === 'chat' && !internal)
66
+ }
67
+
68
+ function buildRecoveryPayload(
69
+ input: EnqueueSessionRunInput,
70
+ source: string,
71
+ mode: SessionQueueMode,
72
+ maxRuntimeMs: number | undefined,
73
+ executionKey: string,
74
+ ) {
75
+ return {
76
+ message: input.message,
77
+ imagePath: input.imagePath,
78
+ imageUrl: input.imageUrl,
79
+ attachedFiles: input.attachedFiles,
80
+ internal: input.internal === true,
81
+ source,
82
+ mode,
83
+ maxRuntimeMs,
84
+ modelOverride: input.modelOverride,
85
+ heartbeatConfig: input.heartbeatConfig,
86
+ replyToId: input.replyToId,
87
+ executionGroupKey: executionKey.startsWith('session:') ? undefined : executionKey,
88
+ }
89
+ }
90
+
91
+ function findDedupeMatch(sessionId: string, dedupeKey?: string) {
92
+ if (!dedupeKey) return null
93
+ const executionKey = executionKeyForSession(sessionId)
94
+ const running = state.runningByExecution.get(executionKey)
95
+ if (running?.run.sessionId === sessionId && running.run.dedupeKey === dedupeKey) return running
96
+ const queue = queueForExecution(executionKey)
97
+ return queue.find((entry) => entry.run.sessionId === sessionId && entry.run.dedupeKey === dedupeKey) || null
98
+ }
99
+
100
+ export function enqueueSessionRun(
101
+ input: EnqueueSessionRunInput,
102
+ deps: {
103
+ repairSessionRunQueue: RepairSessionRunQueueFn
104
+ drainExecution: DrainExecutionFn
105
+ },
106
+ ): EnqueueSessionRunResult {
107
+ const internal = input.internal === true
108
+ const mode = normalizeMode(input.mode, internal)
109
+ const source = input.source || 'chat'
110
+ if (isAllEstopEngaged()) {
111
+ throw new Error('Execution is blocked because all estop is engaged.')
112
+ }
113
+ if (isAutonomyEstopEngaged() && isAutonomyManagedEnqueue(source, internal)) {
114
+ throw new Error(`Autonomy estop is engaged. New ${source} runs are paused.`)
115
+ }
116
+ const executionKey = typeof input.executionGroupKey === 'string' && input.executionGroupKey.trim()
117
+ ? input.executionGroupKey.trim()
118
+ : executionKeyForSession(input.sessionId)
119
+ deps.repairSessionRunQueue(input.sessionId, {
120
+ executionKey,
121
+ reason: 'Recovered stale queued run before enqueue',
122
+ })
123
+ const runtime = loadRuntimeSettings()
124
+ const defaultMaxRuntimeMs = runtime.ongoingLoopMaxRuntimeMs ?? (10 * 60_000)
125
+ const sessionData = getSession(input.sessionId) as SessionToolConfig | null
126
+ const sessionTools = getEnabledToolIds(sessionData)
127
+ const adjustedDefaultMs = computeEffectiveRunTimeoutMs(defaultMaxRuntimeMs, sessionTools, runtime)
128
+ const effectiveMaxRuntimeMs = typeof input.maxRuntimeMs === 'number'
129
+ ? input.maxRuntimeMs
130
+ : adjustedDefaultMs
131
+
132
+ const dedupe = findDedupeMatch(input.sessionId, input.dedupeKey)
133
+ if (dedupe) {
134
+ const cb = input.onEvent
135
+ if (cb) dedupe.onEvents.push(cb)
136
+ if (input.callerSignal) chainCallerSignal(input.callerSignal, dedupe.signalController)
137
+ return {
138
+ runId: dedupe.run.id,
139
+ position: 0,
140
+ deduped: true,
141
+ promise: dedupe.promise,
142
+ abort: () => dedupe.signalController.abort(),
143
+ unsubscribe: () => {
144
+ if (!cb) return
145
+ const idx = dedupe.onEvents.indexOf(cb)
146
+ if (idx >= 0) dedupe.onEvents.splice(idx, 1)
147
+ },
148
+ }
149
+ }
150
+
151
+ if (mode === 'steer') {
152
+ const running = state.runningByExecution.get(executionKey)
153
+ if (running && running.run.sessionId === input.sessionId) {
154
+ running.signalController.abort()
155
+ try { getActiveSessionProcess(input.sessionId)?.kill?.() } catch { /* noop */ }
156
+ }
157
+ cancelPendingForSession(input.sessionId, 'Cancelled by steer mode')
158
+ }
159
+
160
+ if (!internal && source === 'chat') {
161
+ const running = state.runningByExecution.get(executionKey)
162
+ if (running && isInternalHeartbeatRun(running.run.internal, running.run.source)) {
163
+ log.info('session-run', `Preempting heartbeat ${running.run.id} for user chat on ${input.sessionId}`)
164
+ abortSessionRuntime(running, 'Preempted by user chat')
165
+ state.runningByExecution.delete(executionKey)
166
+ }
167
+ }
168
+
169
+ const running = state.runningByExecution.get(executionKey)
170
+ const queue = queueForExecution(executionKey)
171
+ if (mode === 'collect' && !input.imagePath && !input.imageUrl && !input.attachedFiles?.length) {
172
+ const nowMs = nextQueuedAt()
173
+ const candidate = queue.at(-1)
174
+ const canCoalesce = !!candidate
175
+ && candidate.run.mode === 'collect'
176
+ && candidate.run.internal === internal
177
+ && candidate.run.source === source
178
+ && !candidate.imagePath
179
+ && !candidate.imageUrl
180
+ && !candidate.attachedFiles?.length
181
+ && (nowMs - candidate.run.queuedAt) <= COLLECT_COALESCE_WINDOW_MS
182
+
183
+ if (candidate && canCoalesce) {
184
+ const nextChunk = input.message.trim()
185
+ if (nextChunk) {
186
+ const current = candidate.message.trim()
187
+ candidate.message = current
188
+ ? `${current}\n\n[Collected follow-up]\n${nextChunk}`
189
+ : nextChunk
190
+ candidate.run.messagePreview = messagePreview(candidate.message)
191
+ candidate.run.queuedAt = nowMs
192
+ syncRunRecord(candidate.run)
193
+ }
194
+ const coalesceCb = input.onEvent
195
+ if (coalesceCb) candidate.onEvents.push(coalesceCb)
196
+ if (input.callerSignal) chainCallerSignal(input.callerSignal, candidate.signalController)
197
+ emitRunMeta(candidate, 'queued', { position: 0, coalesced: true, mergedIntoRunId: candidate.run.id })
198
+ return {
199
+ runId: candidate.run.id,
200
+ position: 0,
201
+ coalesced: true,
202
+ promise: candidate.promise,
203
+ abort: () => candidate.signalController.abort(),
204
+ unsubscribe: () => {
205
+ if (!coalesceCb) return
206
+ const idx = candidate.onEvents.indexOf(coalesceCb)
207
+ if (idx >= 0) candidate.onEvents.splice(idx, 1)
208
+ },
209
+ }
210
+ }
211
+ }
212
+
213
+ const runId = genId(8)
214
+ const run: SessionRunRecord = {
215
+ id: runId,
216
+ sessionId: input.sessionId,
217
+ missionId: input.missionId ?? getSession(input.sessionId)?.missionId ?? null,
218
+ source,
219
+ internal,
220
+ mode,
221
+ status: 'queued',
222
+ messagePreview: messagePreview(input.message),
223
+ dedupeKey: input.dedupeKey,
224
+ queuedAt: nextQueuedAt(),
225
+ recoveredFromRestart: input.recoveredFromRestart === true,
226
+ recoveredFromRunId: input.recoveredFromRunId,
227
+ recoveryPayload: buildRecoveryPayload(
228
+ input,
229
+ source,
230
+ mode,
231
+ effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
232
+ executionKey,
233
+ ),
234
+ }
235
+ registerRun(run)
236
+
237
+ let resolve!: EnqueueSessionRunResult['promise'] extends Promise<infer T> ? (value: T) => void : never
238
+ let reject!: (error: Error) => void
239
+ const promise = new Promise<import('@/lib/server/chat-execution/chat-execution-types').ExecuteChatTurnResult>((res, rej) => {
240
+ resolve = res
241
+ reject = rej
242
+ })
243
+ promise.catch(() => {})
244
+ state.promises.set(runId, promise)
245
+
246
+ const entry: SessionRunQueueEntry = {
247
+ executionKey,
248
+ run,
249
+ message: input.message,
250
+ imagePath: input.imagePath,
251
+ imageUrl: input.imageUrl,
252
+ attachedFiles: input.attachedFiles,
253
+ onEvents: input.onEvent ? [input.onEvent] : [],
254
+ signalController: new AbortController(),
255
+ maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
256
+ modelOverride: input.modelOverride,
257
+ heartbeatConfig: input.heartbeatConfig,
258
+ replyToId: input.replyToId,
259
+ resolve,
260
+ reject,
261
+ promise,
262
+ }
263
+
264
+ if (input.callerSignal) chainCallerSignal(input.callerSignal, entry.signalController)
265
+
266
+ queue.push(entry)
267
+ incrementNonHeartbeatWork(entry)
268
+ if (entry.nonHeartbeatCounted) {
269
+ reconcileSessionActivityLease(input.sessionId)
270
+ }
271
+ const position = (running ? 1 : 0) + queue.length - 1
272
+ emitRunMeta(entry, 'queued', { position })
273
+ void deps.drainExecution(executionKey)
274
+
275
+ const entryCb = input.onEvent
276
+ return {
277
+ runId,
278
+ position,
279
+ promise,
280
+ abort: () => entry.signalController.abort(),
281
+ unsubscribe: () => {
282
+ if (!entryCb) return
283
+ const idx = entry.onEvents.indexOf(entryCb)
284
+ if (idx >= 0) entry.onEvents.splice(idx, 1)
285
+ },
286
+ }
287
+ }
@@ -0,0 +1,117 @@
1
+ import type {
2
+ RunEventRecord,
3
+ SessionQueueSnapshot,
4
+ SessionQueuedTurn,
5
+ SessionRunRecord,
6
+ SessionRunStatus,
7
+ } from '@/types'
8
+ import {
9
+ listPersistedRunEvents,
10
+ listPersistedRuns,
11
+ loadPersistedRun,
12
+ } from '@/lib/server/runtime/run-ledger'
13
+ import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
14
+
15
+ import { state } from './state'
16
+ import type { SessionRunQueueEntry } from './types'
17
+
18
+ export function getSessionRunState(sessionId: string): {
19
+ runningRunId?: string
20
+ queueLength: number
21
+ } {
22
+ const summary = getSessionExecutionState(sessionId)
23
+ return {
24
+ runningRunId: summary.runningRunId,
25
+ queueLength: summary.queueLength,
26
+ }
27
+ }
28
+
29
+ function visibleQueuedEntriesForSession(sessionId: string): SessionRunQueueEntry[] {
30
+ return Array.from(state.queueByExecution.values())
31
+ .flatMap((queue) => queue)
32
+ .filter((entry) => entry.run.sessionId === sessionId && entry.run.internal !== true)
33
+ .sort((left, right) => left.run.queuedAt - right.run.queuedAt)
34
+ }
35
+
36
+ function toQueuedTurn(entry: SessionRunQueueEntry, index: number): SessionQueuedTurn {
37
+ return {
38
+ runId: entry.run.id,
39
+ sessionId: entry.run.sessionId,
40
+ missionId: entry.run.missionId || null,
41
+ text: entry.message,
42
+ queuedAt: entry.run.queuedAt,
43
+ position: index + 1,
44
+ imagePath: entry.imagePath,
45
+ imageUrl: entry.imageUrl,
46
+ attachedFiles: entry.attachedFiles,
47
+ replyToId: entry.replyToId,
48
+ source: entry.run.source,
49
+ }
50
+ }
51
+
52
+ export function getSessionQueueSnapshot(sessionId: string): SessionQueueSnapshot {
53
+ const execution = getSessionExecutionState(sessionId)
54
+ const visibleQueued = visibleQueuedEntriesForSession(sessionId)
55
+ return {
56
+ sessionId,
57
+ activeRunId: execution.runningRunId || null,
58
+ queueLength: visibleQueued.length,
59
+ items: visibleQueued.map((entry, index) => toQueuedTurn(entry, index)),
60
+ }
61
+ }
62
+
63
+ export function getSessionExecutionState(sessionId: string): {
64
+ runningRunId?: string
65
+ queueLength: number
66
+ hasRunning: boolean
67
+ hasQueued: boolean
68
+ hasRunningHeartbeat: boolean
69
+ hasQueuedHeartbeat: boolean
70
+ hasRunningNonHeartbeat: boolean
71
+ hasQueuedNonHeartbeat: boolean
72
+ } {
73
+ const running = Array.from(state.runningByExecution.values())
74
+ .find((entry) => entry.run.sessionId === sessionId)
75
+ const runningMatchesSession = Boolean(running)
76
+ const runningHeartbeat = Boolean(
77
+ runningMatchesSession
78
+ && running
79
+ && isInternalHeartbeatRun(running.run.internal, running.run.source),
80
+ )
81
+ const runningNonHeartbeat = Boolean(runningMatchesSession && !runningHeartbeat)
82
+ const queuedEntries = Array.from(state.queueByExecution.values())
83
+ .flatMap((queue) => queue)
84
+ .filter((entry) => entry.run.sessionId === sessionId)
85
+ const queuedHeartbeat = queuedEntries.filter((entry) =>
86
+ isInternalHeartbeatRun(entry.run.internal, entry.run.source),
87
+ ).length
88
+ const queuedNonHeartbeat = queuedEntries.length - queuedHeartbeat
89
+ return {
90
+ runningRunId: (runningMatchesSession && running?.run.status === 'running')
91
+ ? running.run.id
92
+ : undefined,
93
+ queueLength: queuedEntries.length,
94
+ hasRunning: Boolean(runningMatchesSession),
95
+ hasQueued: queuedEntries.length > 0,
96
+ hasRunningHeartbeat: runningHeartbeat,
97
+ hasQueuedHeartbeat: queuedHeartbeat > 0,
98
+ hasRunningNonHeartbeat: runningNonHeartbeat,
99
+ hasQueuedNonHeartbeat: queuedNonHeartbeat > 0,
100
+ }
101
+ }
102
+
103
+ export function getRunById(runId: string): SessionRunRecord | null {
104
+ return state.runs.get(runId) || loadPersistedRun(runId)
105
+ }
106
+
107
+ export function listRuns(params?: {
108
+ sessionId?: string
109
+ status?: SessionRunStatus
110
+ limit?: number
111
+ }): SessionRunRecord[] {
112
+ return listPersistedRuns(params)
113
+ }
114
+
115
+ export function listRunEvents(runId: string, limit?: number): RunEventRecord[] {
116
+ return listPersistedRunEvents(runId, limit)
117
+ }