@swarmclawai/swarmclaw 1.2.6 → 1.2.9

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 (269) hide show
  1. package/README.md +54 -23
  2. package/next.config.ts +1 -0
  3. package/package.json +4 -3
  4. package/scripts/easy-setup.mjs +1 -1
  5. package/scripts/postinstall.mjs +1 -1
  6. package/skills/swarmclaw.md +115 -0
  7. package/skills/tools/browser.md +131 -0
  8. package/skills/tools/execute.md +98 -0
  9. package/skills/tools/files.md +98 -0
  10. package/skills/tools/memory.md +104 -0
  11. package/skills/tools/platform.md +144 -0
  12. package/skills/tools/skills.md +83 -0
  13. package/src/app/agents/[id]/page.tsx +1 -18
  14. package/src/app/api/agents/thread-route.test.ts +0 -1
  15. package/src/app/api/approvals/route.test.ts +6 -22
  16. package/src/app/api/chats/[id]/messages/route.ts +23 -19
  17. package/src/app/api/chats/messages-route.test.ts +105 -51
  18. package/src/app/api/connectors/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
  20. package/src/app/api/openclaw/deploy/route.ts +2 -0
  21. package/src/app/api/portability/export/route.ts +8 -0
  22. package/src/app/api/portability/import/route.test.ts +80 -0
  23. package/src/app/api/portability/import/route.ts +28 -0
  24. package/src/app/api/settings/route.ts +0 -2
  25. package/src/app/api/setup/doctor/route.ts +4 -4
  26. package/src/app/api/wallets/[id]/route.ts +15 -157
  27. package/src/app/api/wallets/generate/route.ts +22 -0
  28. package/src/app/api/wallets/route.test.ts +147 -0
  29. package/src/app/api/wallets/route.ts +13 -95
  30. package/src/app/autonomy/page.tsx +2 -57
  31. package/src/app/protocols/page.tsx +2 -21
  32. package/src/app/settings/page.tsx +0 -9
  33. package/src/app/wallets/page.tsx +105 -5
  34. package/src/cli/index.js +21 -33
  35. package/src/cli/spec.js +19 -30
  36. package/src/components/agents/agent-chat-list.tsx +23 -1
  37. package/src/components/agents/agent-sheet.tsx +2 -40
  38. package/src/components/agents/inspector-panel.tsx +165 -131
  39. package/src/components/chat/chat-area.tsx +38 -9
  40. package/src/components/chat/chat-card.tsx +0 -31
  41. package/src/components/chat/message-bubble.tsx +1 -108
  42. package/src/components/chat/message-list.tsx +33 -19
  43. package/src/components/connectors/connector-sheet.tsx +25 -1
  44. package/src/components/gateways/gateway-sheet.tsx +5 -2
  45. package/src/components/layout/sidebar-rail.tsx +6 -10
  46. package/src/components/projects/project-detail.tsx +3 -35
  47. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  48. package/src/components/projects/tabs/work-tab.tsx +7 -77
  49. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  50. package/src/components/shared/connector-platform-icon.tsx +1 -0
  51. package/src/components/tasks/task-card.tsx +4 -34
  52. package/src/components/tasks/task-sheet.tsx +6 -36
  53. package/src/components/wallets/wallet-list.tsx +150 -0
  54. package/src/lib/agent-execute-defaults.test.ts +24 -0
  55. package/src/lib/agent-execute-defaults.ts +62 -0
  56. package/src/lib/app/navigation.test.ts +0 -13
  57. package/src/lib/app/navigation.ts +2 -7
  58. package/src/lib/app/view-constants.ts +14 -19
  59. package/src/lib/chat/queued-message-queue.test.ts +134 -1
  60. package/src/lib/chat/queued-message-queue.ts +77 -2
  61. package/src/lib/server/agents/agent-service.ts +5 -0
  62. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  63. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  64. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  65. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  66. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  67. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  68. package/src/lib/server/approval-match.ts +0 -85
  69. package/src/lib/server/approvals.test.ts +6 -6
  70. package/src/lib/server/approvals.ts +0 -6
  71. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  72. package/src/lib/server/builtin-extensions.ts +1 -2
  73. package/src/lib/server/capability-router.test.ts +0 -2
  74. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
  75. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +15 -14
  76. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  77. package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -4
  78. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  79. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  80. package/src/lib/server/chat-execution/chat-turn-preparation.ts +81 -64
  81. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
  82. package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
  83. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  84. package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
  85. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  86. package/src/lib/server/chat-execution/message-classifier.ts +11 -16
  87. package/src/lib/server/chat-execution/prompt-builder.test.ts +27 -0
  88. package/src/lib/server/chat-execution/prompt-builder.ts +14 -31
  89. package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
  90. package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
  91. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  92. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  93. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  94. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +13 -126
  95. package/src/lib/server/chat-execution/stream-agent-chat.ts +46 -21
  96. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  97. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  98. package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
  99. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  100. package/src/lib/server/chats/chat-session-service.ts +3 -5
  101. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  102. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  103. package/src/lib/server/connectors/connector-service.ts +39 -9
  104. package/src/lib/server/connectors/discord.ts +2 -2
  105. package/src/lib/server/connectors/matrix.ts +3 -2
  106. package/src/lib/server/connectors/signal.ts +5 -4
  107. package/src/lib/server/connectors/slack.ts +10 -9
  108. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  109. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  110. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  111. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  112. package/src/lib/server/connectors/swarmdock-tasks.ts +119 -0
  113. package/src/lib/server/connectors/swarmdock.ts +255 -0
  114. package/src/lib/server/connectors/teams.ts +3 -2
  115. package/src/lib/server/connectors/telegram.ts +4 -4
  116. package/src/lib/server/connectors/whatsapp.ts +2 -2
  117. package/src/lib/server/daemon/controller.ts +7 -0
  118. package/src/lib/server/execution-brief.test.ts +2 -25
  119. package/src/lib/server/execution-brief.ts +12 -35
  120. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  121. package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
  122. package/src/lib/server/messages/message-repository.test.ts +70 -0
  123. package/src/lib/server/messages/message-repository.ts +11 -6
  124. package/src/lib/server/openclaw/deploy.ts +32 -2
  125. package/src/lib/server/persistence/storage-context.ts +0 -5
  126. package/src/lib/server/plugins-advanced.test.ts +1 -2
  127. package/src/lib/server/portability/export.ts +109 -0
  128. package/src/lib/server/portability/import.ts +159 -0
  129. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  130. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  131. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  132. package/src/lib/server/protocols/protocol-service.ts +0 -1
  133. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  134. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  135. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  136. package/src/lib/server/protocols/protocol-types.ts +0 -2
  137. package/src/lib/server/provider-health.ts +1 -10
  138. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  139. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  140. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  141. package/src/lib/server/runtime/process-manager.ts +13 -9
  142. package/src/lib/server/runtime/queue/core.ts +11 -33
  143. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  144. package/src/lib/server/runtime/scheduler.ts +0 -13
  145. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  146. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  147. package/src/lib/server/runtime/session-run-manager/queries.ts +15 -1
  148. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  149. package/src/lib/server/runtime/session-run-manager.test.ts +58 -28
  150. package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
  151. package/src/lib/server/sandbox/session-runtime.ts +40 -28
  152. package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
  153. package/src/lib/server/session-tools/context.ts +1 -1
  154. package/src/lib/server/session-tools/credential-env.ts +109 -0
  155. package/src/lib/server/session-tools/crud.ts +3 -17
  156. package/src/lib/server/session-tools/delegate.ts +0 -4
  157. package/src/lib/server/session-tools/edit_file.ts +3 -2
  158. package/src/lib/server/session-tools/execute.test.ts +58 -0
  159. package/src/lib/server/session-tools/execute.ts +334 -0
  160. package/src/lib/server/session-tools/files-tool.ts +635 -0
  161. package/src/lib/server/session-tools/index.ts +14 -8
  162. package/src/lib/server/session-tools/memory-tool.ts +242 -0
  163. package/src/lib/server/session-tools/memory.ts +1 -1
  164. package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
  165. package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
  166. package/src/lib/server/session-tools/platform-tool.ts +617 -0
  167. package/src/lib/server/session-tools/session-info.ts +3 -2
  168. package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
  169. package/src/lib/server/session-tools/shell.ts +7 -122
  170. package/src/lib/server/session-tools/skills-tool.ts +396 -0
  171. package/src/lib/server/session-tools/team-context.ts +0 -3
  172. package/src/lib/server/session-tools/web.ts +2 -2
  173. package/src/lib/server/storage-normalization.ts +10 -0
  174. package/src/lib/server/storage.ts +18 -45
  175. package/src/lib/server/tasks/task-checkout.ts +59 -0
  176. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  177. package/src/lib/server/tasks/task-route-service.ts +4 -26
  178. package/src/lib/server/tasks/task-service.ts +0 -7
  179. package/src/lib/server/tool-aliases.ts +2 -2
  180. package/src/lib/server/tool-capability-policy-advanced.test.ts +13 -6
  181. package/src/lib/server/tool-capability-policy.test.ts +2 -1
  182. package/src/lib/server/tool-capability-policy.ts +60 -35
  183. package/src/lib/server/tool-planning.ts +11 -12
  184. package/src/lib/server/universal-tool-access.ts +0 -1
  185. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  186. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  187. package/src/lib/server/wallets/wallet-service.ts +119 -0
  188. package/src/lib/server/working-state/extraction.ts +8 -42
  189. package/src/lib/server/working-state/normalization.ts +10 -103
  190. package/src/lib/server/working-state/service.ts +12 -21
  191. package/src/lib/setup-defaults.ts +5 -0
  192. package/src/lib/strip-internal-metadata.test.ts +1 -1
  193. package/src/lib/strip-internal-metadata.ts +1 -1
  194. package/src/lib/tool-definitions.ts +1 -1
  195. package/src/lib/validation/schemas.test.ts +16 -0
  196. package/src/lib/validation/schemas.ts +49 -2
  197. package/src/stores/slices/data-slice.ts +5 -1
  198. package/src/stores/slices/ui-slice.ts +0 -4
  199. package/src/stores/use-chat-store.test.ts +231 -0
  200. package/src/stores/use-chat-store.ts +62 -13
  201. package/src/types/agent.ts +264 -0
  202. package/src/types/app-settings.ts +173 -0
  203. package/src/types/approval.ts +25 -0
  204. package/src/types/connector.ts +188 -0
  205. package/src/types/extension.ts +386 -0
  206. package/src/types/index.ts +16 -3555
  207. package/src/types/message.ts +56 -0
  208. package/src/types/misc.ts +737 -0
  209. package/src/types/protocol.ts +420 -0
  210. package/src/types/provider.ts +52 -0
  211. package/src/types/run.ts +180 -0
  212. package/src/types/schedule.ts +59 -0
  213. package/src/types/session.ts +215 -0
  214. package/src/types/skill.ts +157 -0
  215. package/src/types/swarmdock.ts +29 -0
  216. package/src/types/task.ts +144 -0
  217. package/src/types/working-state.ts +204 -0
  218. package/src/views/settings/section-heartbeat.tsx +2 -2
  219. package/src/views/settings/section-runtime-loop.tsx +0 -14
  220. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  221. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  222. package/src/app/api/missions/[id]/events/route.ts +0 -14
  223. package/src/app/api/missions/[id]/route.ts +0 -10
  224. package/src/app/api/missions/route.test.ts +0 -244
  225. package/src/app/api/missions/route.ts +0 -57
  226. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  227. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  228. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  229. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  230. package/src/app/missions/[id]/page.tsx +0 -3
  231. package/src/app/missions/page.tsx +0 -685
  232. package/src/components/canvas/canvas-panel.tsx +0 -267
  233. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  234. package/src/components/wallets/wallet-panel.tsx +0 -1010
  235. package/src/components/wallets/wallet-section.tsx +0 -260
  236. package/src/features/missions/queries.ts +0 -23
  237. package/src/lib/canvas-content.test.ts +0 -360
  238. package/src/lib/canvas-content.ts +0 -198
  239. package/src/lib/server/canvas-content.test.ts +0 -32
  240. package/src/lib/server/canvas-content.ts +0 -6
  241. package/src/lib/server/ethereum.ts +0 -591
  242. package/src/lib/server/evm-swap.ts +0 -476
  243. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  244. package/src/lib/server/missions/mission-intent.ts +0 -569
  245. package/src/lib/server/missions/mission-repository.ts +0 -74
  246. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  247. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  248. package/src/lib/server/missions/mission-service/context.ts +0 -4
  249. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  250. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  251. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  252. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  253. package/src/lib/server/missions/mission-service.test.ts +0 -888
  254. package/src/lib/server/missions/mission-service.ts +0 -6
  255. package/src/lib/server/session-tools/canvas.ts +0 -105
  256. package/src/lib/server/session-tools/sandbox.ts +0 -281
  257. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  258. package/src/lib/server/session-tools/wallet.ts +0 -1287
  259. package/src/lib/server/solana.ts +0 -327
  260. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  261. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  262. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  263. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  264. package/src/lib/server/wallet/wallet-service.ts +0 -225
  265. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  266. package/src/lib/wallet/wallet-transactions.ts +0 -43
  267. package/src/lib/wallet/wallet.test.ts +0 -333
  268. package/src/lib/wallet/wallet.ts +0 -183
  269. package/src/views/settings/section-wallets.tsx +0 -35
@@ -6,7 +6,6 @@ import type {
6
6
  BoardTask,
7
7
  Chatroom,
8
8
  MessageToolEvent,
9
- Mission,
10
9
  ProtocolBranchCase,
11
10
  ProtocolPhaseDefinition,
12
11
  ProtocolRepeatConfig,
@@ -31,7 +30,6 @@ export interface ProtocolRunDetail {
31
30
  template: ProtocolTemplate | null
32
31
  transcript: Chatroom | null
33
32
  parentChatroom: Chatroom | null
34
- linkedMission: Mission | null
35
33
  linkedTask: BoardTask | null
36
34
  events: ProtocolRunEvent[]
37
35
  }
@@ -42,7 +42,7 @@ function commandExists(binary: string): boolean {
42
42
  pruneTTLCache(cliCheckCache, CLI_CHECK_TTL_MS, CLI_CHECK_CACHE_MAX)
43
43
  const probe = isWindows
44
44
  ? spawnSync('where', [binary], { timeout: 2000, stdio: 'pipe' })
45
- : spawnSync('/bin/zsh', ['-lc', `command -v ${binary} >/dev/null 2>&1`], { timeout: 2000 })
45
+ : spawnSync(process.env.SHELL || '/bin/bash', ['-lc', `command -v ${binary} >/dev/null 2>&1`], { timeout: 2000 })
46
46
  const ok = (probe.status ?? 1) === 0
47
47
  cliCheckCache.set(binary, { at: now, ok })
48
48
  return ok
@@ -92,15 +92,6 @@ export function markProviderSuccess(providerId: string, credentialId?: string |
92
92
  const { upsertStoredItem } = require('@/lib/server/storage')
93
93
  upsertStoredItem('provider_health', key, states.get(key)!)
94
94
  } catch {}
95
- queueMicrotask(() => {
96
- import('@/lib/server/missions/mission-service')
97
- .then(({ requestMissionTicksForProviderRecovery }) => {
98
- requestMissionTicksForProviderRecovery(providerId)
99
- })
100
- .catch(() => {
101
- // Mission recovery is best-effort only.
102
- })
103
- })
104
95
  }
105
96
 
106
97
  export function isProviderCoolingDown(providerId: string, credentialId?: string | null): boolean {
@@ -66,7 +66,6 @@ import { loadEstopState } from '@/lib/server/runtime/estop'
66
66
  import { classifyRuntimeFailure, recordSupervisorIncident } from '@/lib/server/autonomy/supervisor-reflection'
67
67
  import { getMemoryDb } from '@/lib/server/memory/memory-db'
68
68
  import { clearLogsByAge } from '@/lib/server/execution-log'
69
- import { runMissionControllerStartupRecovery } from '@/lib/server/missions/mission-service'
70
69
 
71
70
  const TAG = 'daemon-state'
72
71
 
@@ -293,14 +292,6 @@ export function startDaemon(options?: { source?: string; manualStart?: boolean }
293
292
  if (lost > 0) log.info(TAG, `[daemon] Marked ${lost} in-flight swarm(s) as lost after restart`)
294
293
  } catch { /* best-effort */ }
295
294
  resumeQueue()
296
- const missionRecovery = runMissionControllerStartupRecovery()
297
- if (missionRecovery.recovered > 0 || missionRecovery.rerunVerification > 0) {
298
- log.info(
299
- TAG,
300
- `[daemon] Recovered ${missionRecovery.recovered} mission(s) on startup`
301
- + ` (${missionRecovery.rerunVerification} queued for verification replay)`,
302
- )
303
- }
304
295
  startScheduler()
305
296
  startQueueProcessor()
306
297
  startBrowserSweep()
@@ -245,41 +245,6 @@ describe('daemon start/stop lifecycle', () => {
245
245
  }
246
246
  })
247
247
 
248
- it('startDaemon runs mission startup recovery once the daemon owns startup', async () => {
249
- const storage = await import('@/lib/server/storage')
250
- storage.saveMissions({
251
- missionA: {
252
- id: 'missionA',
253
- source: 'chat',
254
- sourceRef: { kind: 'chat', sessionId: 'sessionA' },
255
- objective: 'Recover after daemon start',
256
- status: 'active',
257
- phase: 'executing',
258
- sessionId: 'sessionA',
259
- taskIds: [],
260
- controllerState: {
261
- activeRunId: 'run-stale',
262
- currentTaskId: 'task-stale',
263
- },
264
- createdAt: 1,
265
- updatedAt: 1,
266
- },
267
- })
268
-
269
- await mod.stopDaemon({ source: 'test-prestart' })
270
- mod.startDaemon({ source: 'test-mission-recovery', manualStart: true })
271
- try {
272
- const missions = await import('@/lib/server/missions/mission-service')
273
- const mission = missions.loadMissionById('missionA')
274
- assert.equal(mission?.status, 'active')
275
- assert.equal(mission?.phase, 'planning')
276
- const events = missions.listMissionEventsForMission('missionA')
277
- assert.ok(events.some((event) => event.type === 'interrupted'))
278
- } finally {
279
- await mod.stopDaemon({ source: 'test-mission-recovery' })
280
- }
281
- })
282
-
283
248
  it('stopDaemon sets running to false', async () => {
284
249
  mod.startDaemon({ source: 'test', manualStart: true })
285
250
  await mod.stopDaemon({ source: 'test' })
@@ -10,17 +10,15 @@ import { logActivity } from '@/lib/server/activity/activity-log'
10
10
  import { loadApprovals } from '@/lib/server/approvals/approval-repository'
11
11
  import { loadAgents, patchAgent } from '@/lib/server/agents/agent-repository'
12
12
  import { loadChatrooms } from '@/lib/server/chatrooms/chatroom-repository'
13
- import { loadMission } from '@/lib/server/missions/mission-repository'
14
13
  import { loadSessions, patchSession } from '@/lib/server/sessions/session-repository'
15
14
  import { loadSettings } from '@/lib/server/settings/settings-repository'
16
- import { buildGoalAncestrySection, buildPlatformStatusSummary } from '@/lib/server/chat-execution/situational-awareness'
15
+ import { buildPlatformStatusSummary } from '@/lib/server/chat-execution/situational-awareness'
17
16
  import { drainDeferredWakes, hasDeferredWakes } from '@/lib/server/runtime/wake-dispatcher'
18
17
  import { buildWakeTriggerContext } from '@/lib/server/runtime/heartbeat-wake'
19
18
  import { enqueueSessionRun, getSessionRunState } from '@/lib/server/runtime/session-run-manager'
20
19
  import { log } from '@/lib/server/logger'
21
20
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
22
21
  import { drainSystemEvents, drainOrchestratorEvents } from '@/lib/server/runtime/system-events'
23
- import { buildMissionContextBlock } from '@/lib/server/missions/mission-service'
24
22
  import { getMessages, getRecentMessages, clearMessages } from '@/lib/server/messages/message-repository'
25
23
  import type { Agent, AppSettings, ApprovalRequest, Chatroom, Message, Session } from '@/types'
26
24
  import { isOrchestratorEligible } from '@/lib/orchestrator-config'
@@ -357,12 +355,7 @@ export function buildAgentHeartbeatPrompt(
357
355
  }
358
356
  }
359
357
 
360
- // ── Phase 3: Goal ancestry ──
361
- const missionId = (session.missionId || (agent as Record<string, unknown>).missionId || null) as string | null
362
- const goalAncestry = buildGoalAncestrySection(missionId)
363
- if (goalAncestry) sections.push(goalAncestry)
364
-
365
- // ── Phase 4: Active task checkout & events ──
358
+ // ── Phase 3: Active task checkout & events ──
366
359
  const events = drainSystemEvents(session.id!)
367
360
  if (events.length > 0) {
368
361
  const eventBlock = events.map((e) => `- [${new Date(e.timestamp).toISOString()}] ${e.text}`).join('\n')
@@ -983,20 +976,7 @@ export function buildOrchestratorWakePrompt(session: any, agent: Agent): string
983
976
  addSection(`## System Events\n${eventBlock}`)
984
977
  }
985
978
 
986
- // 7. Active mission state
987
- const missionId = session.missionId || null
988
- if (missionId) {
989
- try {
990
- const missionBlock = buildMissionContextBlock(loadMission(missionId))
991
- if (missionBlock) addSection(missionBlock)
992
- } catch { /* ignore */ }
993
- }
994
-
995
- // 8. Goal ancestry
996
- const goalAncestry = buildGoalAncestrySection(missionId)
997
- if (goalAncestry) addSection(goalAncestry)
998
-
999
- // 9. Chatroom membership
979
+ // 7. Chatroom membership
1000
980
  try {
1001
981
  const chatrooms = Object.values(loadChatrooms()) as Chatroom[]
1002
982
  const myChatrooms = chatrooms.filter((c) => !c.archivedAt && c.agentIds?.includes(agent.id))
@@ -1,5 +1,5 @@
1
1
  import { genId } from '@/lib/id'
2
- import { hmrSingleton, sleep } from '@/lib/shared-utils'
2
+ import { hmrSingleton, sleep, errorMessage } from '@/lib/shared-utils'
3
3
  import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
4
4
  import { detectDocker } from '@/lib/server/sandbox/docker-detect'
5
5
  import { log } from '@/lib/server/logger'
@@ -169,7 +169,8 @@ export function buildDockerExecArgs(params: {
169
169
 
170
170
  export function getShellCommand(command: string, processId?: string, sandbox?: SandboxOptions): { shell: string; args: string[]; containerName?: string } {
171
171
  if (!sandbox) {
172
- return { shell: '/bin/zsh', args: ['-lc', command] }
172
+ const shell = process.env.SHELL || '/bin/bash'
173
+ return { shell, args: ['-lc', command] }
173
174
  }
174
175
 
175
176
  if (sandbox.kind === 'persistent') {
@@ -247,9 +248,12 @@ export async function startManagedProcess(opts: StartProcessOptions): Promise<St
247
248
  state.records.set(id, record)
248
249
 
249
250
  const { shell, args } = getShellCommand(opts.command, id, opts.sandbox)
250
- // Strip SwarmClaw-specific env vars so agent child processes don't inherit
251
- // our port binding or internal keys (e.g. npm dev servers defaulting to :3456)
252
- const stripKeys = new Set(['PORT', 'ACCESS_KEY', 'NEXTAUTH_SECRET'])
251
+ // Strip env vars that should not leak into child processes:
252
+ // - PORT, ACCESS_KEY, NEXTAUTH_SECRET: SwarmClaw-specific bindings
253
+ // - npm_config_prefix: injected by npm when running scripts; conflicts with nvm
254
+ // in login shell subprocesses (nvm refuses to initialize when this is set,
255
+ // breaking node PATH resolution for all nvm users)
256
+ const stripKeys = new Set(['PORT', 'ACCESS_KEY', 'NEXTAUTH_SECRET', 'npm_config_prefix'])
253
257
  const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([k]) => !stripKeys.has(k))) as NodeJS.ProcessEnv
254
258
  const child = spawn(shell, args, {
255
259
  cwd: opts.sandbox ? undefined : opts.cwd,
@@ -405,8 +409,8 @@ export function writeManagedProcessStdin(processId: string, data: string, eof?:
405
409
  if (data) child.stdin.write(data)
406
410
  if (eof) child.stdin.end()
407
411
  return { ok: true }
408
- } catch (err: any) {
409
- return { ok: false, error: err.message || String(err) }
412
+ } catch (err: unknown) {
413
+ return { ok: false, error: errorMessage(err) || String(err) }
410
414
  }
411
415
  }
412
416
 
@@ -418,8 +422,8 @@ export function killManagedProcess(processId: string, signal: NodeJS.Signals = '
418
422
  rec.status = 'killed'
419
423
  child.kill(signal)
420
424
  return { ok: true }
421
- } catch (err: any) {
422
- return { ok: false, error: err.message || String(err) }
425
+ } catch (err: unknown) {
426
+ return { ok: false, error: errorMessage(err) || String(err) }
423
427
  }
424
428
  }
425
429
 
@@ -23,6 +23,7 @@ import type { ExecuteChatTurnResult } from '@/lib/server/chat-execution/chat-exe
23
23
  import { checkAgentBudgetLimits } from '@/lib/server/cost'
24
24
  import { enqueueExecution } from '@/lib/server/execution-engine'
25
25
  import { extractTaskResult, formatResultBody } from '@/lib/server/tasks/task-result'
26
+ import { checkoutTask } from '@/lib/server/tasks/task-checkout'
26
27
  import {
27
28
  classifyRuntimeFailure,
28
29
  observeAutonomyRunOutcome,
@@ -51,7 +52,6 @@ import {
51
52
  markValidatedTaskCompleted,
52
53
  refreshTaskCompletionValidation,
53
54
  } from '@/lib/server/tasks/task-lifecycle'
54
- import { noteMissionTaskFinished, noteMissionTaskStarted } from '@/lib/server/missions/mission-service'
55
55
 
56
56
  const TAG = 'queue'
57
57
 
@@ -1210,25 +1210,16 @@ export async function processNext() {
1210
1210
  } catch {}
1211
1211
  }
1212
1212
 
1213
- const beforeStartTasks = loadTasks() as Record<string, BoardTask>
1214
- task = beforeStartTasks[taskId] as BoardTask | undefined
1215
- if (!task || task.status !== 'queued') {
1216
- return
1217
- }
1218
-
1219
- // Mark as running
1213
+ // Atomic checkout prevents two runners from starting the same task
1214
+ const runId = genId()
1215
+ task = checkoutTask(taskId, runId) as BoardTask | undefined
1216
+ if (!task) return
1220
1217
  applyTaskPolicyDefaults(task)
1221
- task.status = 'running'
1222
- task.startedAt = Date.now()
1223
- task.lastActivityAt = Date.now()
1224
- task.retryScheduledAt = null
1225
- task.deadLetteredAt = null
1226
- // Clear transient failure fields so validation/error state reflects only this attempt.
1227
- task.error = null
1228
- task.validation = null
1229
- task.updatedAt = Date.now()
1230
1218
  logActivity({ entityType: 'task', entityId: taskId, action: 'running', actor: 'system', actorId: task.agentId, summary: `Task started: "${task.title}"` })
1231
1219
 
1220
+ // Reload tasks map for resolution functions and final save (checkoutTask already saved the running status)
1221
+ const allTasks = loadTasks() as Record<string, BoardTask>
1222
+ allTasks[taskId] = task
1232
1223
  const sessionsForCwd = loadSessions() as Record<string, SessionLike>
1233
1224
  const taskCwd = resolveTaskExecutionCwd(task as ScheduleTaskMeta, sessionsForCwd)
1234
1225
  task.cwd = taskCwd
@@ -1238,8 +1229,8 @@ export async function processNext() {
1238
1229
  const sourceScheduleId = typeof scheduleTask.sourceScheduleId === 'string'
1239
1230
  ? scheduleTask.sourceScheduleId
1240
1231
  : ''
1241
- const reusableTaskSessionId = resolveReusableTaskSessionId(task, beforeStartTasks, sessionsForCwd)
1242
- const resumeContext = resolveTaskResumeContext(task, beforeStartTasks, sessionsForCwd as Record<string, SessionLike | Session>)
1232
+ const reusableTaskSessionId = resolveReusableTaskSessionId(task, allTasks, sessionsForCwd)
1233
+ const resumeContext = resolveTaskResumeContext(task, allTasks, sessionsForCwd as Record<string, SessionLike | Session>)
1243
1234
 
1244
1235
  // Resolve the agent's persistent thread session to use as parentSessionId
1245
1236
  const agentThreadSessionId = agent.threadSessionId || null
@@ -1307,8 +1298,7 @@ export async function processNext() {
1307
1298
  note: `Attempt ${(task.attempts || 0) + 1}/${task.maxAttempts || '?'} started${continuationBits.length ? ` (${continuationBits.join('; ')})` : ''}`,
1308
1299
  updatedAt: Date.now(),
1309
1300
  }
1310
- saveTasks(beforeStartTasks)
1311
- noteMissionTaskStarted(task, task.id)
1301
+ saveTasks(allTasks)
1312
1302
  pushMainLoopEventToMainSessions({
1313
1303
  type: 'task_running',
1314
1304
  text: `Task running: "${task.title}" (${task.id}) with ${agent.name}`,
@@ -1485,13 +1475,6 @@ export async function processNext() {
1485
1475
  disableSessionHeartbeat(t2[taskId].sessionId)
1486
1476
  }
1487
1477
  const doneTask = t2[taskId]
1488
- if (doneTask?.status === 'completed') {
1489
- noteMissionTaskFinished(doneTask, 'completed', taskRunId)
1490
- } else if (doneTask?.status === 'failed') {
1491
- noteMissionTaskFinished(doneTask, 'failed', taskRunId)
1492
- } else if (doneTask?.status === 'cancelled') {
1493
- noteMissionTaskFinished(doneTask, 'cancelled', taskRunId)
1494
- }
1495
1478
  queueTaskAutonomyObservation({
1496
1479
  runId: taskRunId,
1497
1480
  sessionId,
@@ -1636,11 +1619,6 @@ export async function processNext() {
1636
1619
  })
1637
1620
  }
1638
1621
  saveTasks(t3)
1639
- if (t3[taskId].status === 'failed') {
1640
- noteMissionTaskFinished(t3[taskId], 'failed', taskRunId)
1641
- } else if (t3[taskId].status === 'cancelled') {
1642
- noteMissionTaskFinished(t3[taskId], 'cancelled', taskRunId)
1643
- }
1644
1622
  notify('tasks')
1645
1623
  notify('runs')
1646
1624
  disableSessionHeartbeat(t3[taskId].sessionId)
@@ -9,7 +9,7 @@ function readRepoSource(relativePath: string): string {
9
9
  return fs.readFileSync(path.join(repoRoot, relativePath), 'utf-8')
10
10
  }
11
11
 
12
- test('runtime hot paths use row-level task, schedule, and agent writes', () => {
12
+ test('runtime hot paths use row-level task, schedule, and wallet writes', () => {
13
13
  const expectations = [
14
14
  {
15
15
  file: 'src/lib/server/runtime/scheduler.ts',
@@ -17,14 +17,14 @@ test('runtime hot paths use row-level task, schedule, and agent writes', () => {
17
17
  forbidden: ['saveTasks(', 'saveSchedules('],
18
18
  },
19
19
  {
20
- file: 'src/app/api/schedules/[id]/run/route.ts',
21
- required: ['upsertTask(', 'upsertSchedule('],
20
+ file: 'src/lib/server/schedules/schedule-route-service.ts',
21
+ required: ['saveTask(', 'upsertSchedule('],
22
22
  forbidden: ['saveTasks(', 'saveSchedules('],
23
23
  },
24
24
  {
25
- file: 'src/lib/server/wallet/wallet-service.ts',
26
- required: ['loadAgent(', 'upsertAgent('],
27
- forbidden: ['saveAgents('],
25
+ file: 'src/lib/server/wallets/wallet-service.ts',
26
+ required: ['saveWallet(', 'deleteWallet('],
27
+ forbidden: ['saveWallets('],
28
28
  },
29
29
  ] as const
30
30
 
@@ -10,7 +10,6 @@ import { processDueWatchJobs } from '@/lib/server/runtime/watch-jobs'
10
10
  import { isAgentDisabled } from '@/lib/server/agents/agent-availability'
11
11
  import { prepareScheduledTaskRun } from '@/lib/server/tasks/task-lifecycle'
12
12
  import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-session'
13
- import { ensureMissionForTask, noteScheduleMissionTriggered } from '@/lib/server/missions/mission-service'
14
13
  import { hasActiveProtocolRunForSchedule, launchProtocolRunForSchedule } from '@/lib/server/protocols/protocol-service'
15
14
  import { hmrSingleton } from '@/lib/shared-utils'
16
15
  import { log } from '@/lib/server/logger'
@@ -168,10 +167,6 @@ async function tick(now = Date.now()) {
168
167
  // Wake-only: no board task, just heartbeat the agent
169
168
  upsertSchedule(schedule.id, schedule)
170
169
  const wakeSessionId = resolveScheduleWakeSessionId(schedule, agents as Record<string, unknown>)
171
- noteScheduleMissionTriggered(schedule, {
172
- wakeOnly: true,
173
- sessionId: wakeSessionId || schedule.createdInSessionId || null,
174
- })
175
170
 
176
171
  const wakeMessage = schedule.message || `Schedule triggered: ${schedule.name}`
177
172
  pushMainLoopEventToMainSessions({
@@ -205,16 +200,8 @@ async function tick(now = Date.now()) {
205
200
  now,
206
201
  scheduleSignature,
207
202
  })
208
- const mission = noteScheduleMissionTriggered(schedule, {
209
- taskId,
210
- sessionId: schedule.createdInSessionId || null,
211
- })
212
- if (mission) {
213
- tasks[taskId].missionId = mission.id
214
- }
215
203
 
216
204
  upsertTask(taskId, tasks[taskId])
217
- ensureMissionForTask(tasks[taskId], { source: 'schedule' })
218
205
  upsertSchedule(schedule.id, schedule)
219
206
 
220
207
  enqueueTask(taskId)
@@ -90,7 +90,6 @@ export async function drainExecution(
90
90
  })
91
91
 
92
92
  let runtimeTimer: ReturnType<typeof setTimeout> | null = null
93
- let finishedMissionId: string | null = null
94
93
  if (next.maxRuntimeMs && next.maxRuntimeMs > 0) {
95
94
  runtimeTimer = setTimeout(() => {
96
95
  next.signalController.abort()
@@ -119,8 +118,6 @@ export async function drainExecution(
119
118
  next.run.status = aborted ? 'cancelled' : (failed ? 'failed' : 'completed')
120
119
  next.run.endedAt = next.run.endedAt || now()
121
120
  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
121
  next.run.resultPreview = result.text?.slice(0, 280)
125
122
  if (typeof result.inputTokens === 'number') next.run.totalInputTokens = result.inputTokens
126
123
  if (typeof result.outputTokens === 'number') next.run.totalOutputTokens = result.outputTokens
@@ -186,7 +183,6 @@ export async function drainExecution(
186
183
  next.run.status = aborted ? 'cancelled' : 'failed'
187
184
  next.run.endedAt = now()
188
185
  next.run.error = errorMessage(err)
189
- finishedMissionId = next.run.missionId || null
190
186
  syncRunRecord(next.run)
191
187
  emitRunMeta(next, next.run.status, { error: next.run.error })
192
188
  log.error('session-run', `Run failed ${next.run.id}`, {
@@ -216,26 +212,6 @@ export async function drainExecution(
216
212
  decrementNonHeartbeatWork(next)
217
213
  reconcileSessionActivityLease(next.run.sessionId)
218
214
  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
215
  void drainExecution(executionKey, deps)
240
216
  }
241
217
  } finally {
@@ -215,7 +215,6 @@ export function enqueueSessionRun(
215
215
  const run: SessionRunRecord = {
216
216
  id: runId,
217
217
  sessionId: input.sessionId,
218
- missionId: input.missionId ?? getSession(input.sessionId)?.missionId ?? null,
219
218
  kind: 'session_turn',
220
219
  ownerType: 'session',
221
220
  ownerId: input.sessionId,
@@ -37,7 +37,6 @@ function toQueuedTurn(entry: SessionRunQueueEntry, index: number): SessionQueued
37
37
  return {
38
38
  runId: entry.run.id,
39
39
  sessionId: entry.run.sessionId,
40
- missionId: entry.run.missionId || null,
41
40
  text: entry.message,
42
41
  queuedAt: entry.run.queuedAt,
43
42
  position: index + 1,
@@ -49,12 +48,27 @@ function toQueuedTurn(entry: SessionRunQueueEntry, index: number): SessionQueued
49
48
  }
50
49
  }
51
50
 
51
+ function toActiveTurn(entry: SessionRunQueueEntry): SessionQueuedTurn {
52
+ return {
53
+ ...toQueuedTurn(entry, 0),
54
+ position: 0,
55
+ }
56
+ }
57
+
58
+ function visibleActiveTurnForSession(sessionId: string): SessionQueuedTurn | null {
59
+ const running = Array.from(state.runningByExecution.values())
60
+ .find((entry) => entry.run.sessionId === sessionId && entry.run.status === 'running')
61
+ if (!running || running.run.internal === true) return null
62
+ return toActiveTurn(running)
63
+ }
64
+
52
65
  export function getSessionQueueSnapshot(sessionId: string): SessionQueueSnapshot {
53
66
  const execution = getSessionExecutionState(sessionId)
54
67
  const visibleQueued = visibleQueuedEntriesForSession(sessionId)
55
68
  return {
56
69
  sessionId,
57
70
  activeRunId: execution.runningRunId || null,
71
+ activeTurn: visibleActiveTurnForSession(sessionId),
58
72
  queueLength: visibleQueued.length,
59
73
  items: visibleQueued.map((entry, index) => toQueuedTurn(entry, index)),
60
74
  }
@@ -48,7 +48,6 @@ function resolveRecoveredQueuedEntry(entry: SessionRunQueueEntry, reason: string
48
48
  entry.resolve({
49
49
  runId: entry.run.id,
50
50
  sessionId: entry.run.sessionId,
51
- ...(entry.run.missionId ? { missionId: entry.run.missionId } : {}),
52
51
  text: '',
53
52
  persisted: false,
54
53
  toolEvents: [],
@@ -249,34 +249,6 @@ describe('session-run-manager', () => {
249
249
  assert.ok(run.queuedAt > 0)
250
250
  })
251
251
 
252
- it('copies the session mission id into queued run snapshots', () => {
253
- storage.upsertSession('sess-mission', {
254
- id: 'sess-mission',
255
- cwd: process.cwd(),
256
- user: 'tester',
257
- provider: 'ollama',
258
- model: 'test-model',
259
- claudeSessionId: null,
260
- messages: [],
261
- createdAt: Date.now(),
262
- lastActiveAt: Date.now(),
263
- agentId: 'test-agent',
264
- missionId: 'mission-123',
265
- })
266
- const release = mgr.acquireExternalSessionExecutionHold('sess-mission')
267
-
268
- const result = enqueue({
269
- sessionId: 'sess-mission',
270
- message: 'Continue the release mission',
271
- })
272
-
273
- const run = mgr.getRunById(result.runId)
274
- const snapshot = mgr.getSessionQueueSnapshot('sess-mission')
275
- assert.equal(run?.missionId, 'mission-123')
276
- assert.equal(snapshot.items[0]?.missionId, 'mission-123')
277
- release()
278
- })
279
-
280
252
  it('persists run records and replay events in storage', () => {
281
253
  const result = enqueue({
282
254
  sessionId: 'sess-persisted',
@@ -521,6 +493,37 @@ describe('session-run-manager', () => {
521
493
  })
522
494
  })
523
495
 
496
+ it('exposes the active user-visible turn separately from queued follow-ups', () => {
497
+ const running = enqueue({
498
+ sessionId: 'sess-active-turn',
499
+ message: 'running now',
500
+ source: 'chat',
501
+ })
502
+ const queued = enqueue({
503
+ sessionId: 'sess-active-turn',
504
+ message: 'queued next',
505
+ source: 'chat',
506
+ })
507
+
508
+ const snapshot = mgr.getSessionQueueSnapshot('sess-active-turn')
509
+ assert.equal(snapshot.activeRunId, running.runId)
510
+ assert.deepEqual(snapshot.activeTurn, {
511
+ runId: running.runId,
512
+ sessionId: 'sess-active-turn',
513
+ missionId: null,
514
+ text: 'running now',
515
+ queuedAt: snapshot.activeTurn?.queuedAt,
516
+ position: 0,
517
+ imagePath: undefined,
518
+ imageUrl: undefined,
519
+ attachedFiles: undefined,
520
+ replyToId: undefined,
521
+ source: 'chat',
522
+ })
523
+ assert.deepEqual(snapshot.items.map((item) => item.runId), [queued.runId])
524
+ assert.equal(snapshot.queueLength, 1)
525
+ })
526
+
524
527
  it('hides internal queued runs from the user-visible queue snapshot', () => {
525
528
  enqueue({ sessionId: 'sess-visible-queue', message: 'running' })
526
529
  enqueue({
@@ -538,9 +541,36 @@ describe('session-run-manager', () => {
538
541
 
539
542
  const snapshot = mgr.getSessionQueueSnapshot('sess-visible-queue')
540
543
  assert.equal(snapshot.queueLength, 1)
544
+ assert.equal(snapshot.activeTurn?.runId, snapshot.activeRunId)
541
545
  assert.deepEqual(snapshot.items.map((item) => item.runId), [visible.runId])
542
546
  })
543
547
 
548
+ it('hides internal active runs from the active-turn snapshot field', () => {
549
+ const { entry, promise } = makeManualQueuedEntry({
550
+ sessionId: 'sess-hidden-active',
551
+ runId: 'run-hidden',
552
+ message: 'heartbeat hidden',
553
+ internal: true,
554
+ source: 'heartbeat',
555
+ })
556
+ entry.run.status = 'running'
557
+ entry.run.startedAt = Date.now()
558
+ const state = getRuntimeState()
559
+ state.runningByExecution.set(entry.executionKey, entry as unknown)
560
+ state.runs.set(entry.run.id, entry.run)
561
+ state.promises.set(entry.run.id, promise)
562
+ pendingPromises.push(promise.catch(() => {}))
563
+
564
+ try {
565
+ const snapshot = mgr.getSessionQueueSnapshot('sess-hidden-active')
566
+ assert.equal(snapshot.activeRunId, 'run-hidden')
567
+ assert.equal(snapshot.activeTurn, null)
568
+ assert.equal(snapshot.queueLength, 0)
569
+ } finally {
570
+ entry.resolve(undefined)
571
+ }
572
+ })
573
+
544
574
  it('reports heartbeat vs non-heartbeat queued runs', () => {
545
575
  enqueue({ sessionId: 'sess-hb-state', message: 'occupier' })
546
576
  enqueue({
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import test from 'node:test'
3
- import { resolveSandboxRuntimeStatus, resolveSandboxWorkdir } from '@/lib/server/sandbox/session-runtime'
3
+ import { resolveSandboxRuntimeStatus, resolveSandboxSessionContext, resolveSandboxWorkdir } from '@/lib/server/sandbox/session-runtime'
4
4
  import type { Session } from '@/types'
5
5
 
6
6
  test('resolveSandboxRuntimeStatus defaults enabled sandboxes to all sessions', () => {
@@ -49,6 +49,23 @@ test('resolveSandboxRuntimeStatus sandboxes child sessions in non-main mode', ()
49
49
  assert.equal(status.scopeKey, 'agent:agent-1')
50
50
  })
51
51
 
52
+ test('resolveSandboxSessionContext resolves browser-compatible workspace context without starting containers', () => {
53
+ const context = resolveSandboxSessionContext({
54
+ config: { enabled: true, scope: 'agent', workdir: '/workspace' },
55
+ session: {
56
+ id: 'child-session',
57
+ agentId: 'agent-1',
58
+ parentSessionId: 'main-session',
59
+ } as Session,
60
+ workspaceDir: '/tmp/project',
61
+ })
62
+
63
+ assert.ok(context)
64
+ assert.equal(context?.scopeKey, 'agent:agent-1')
65
+ assert.equal(context?.workspaceDir, '/tmp/project')
66
+ assert.equal(context?.containerWorkdir, '/workspace')
67
+ })
68
+
52
69
  test('resolveSandboxWorkdir maps nested host paths into the container workspace', () => {
53
70
  const resolved = resolveSandboxWorkdir({
54
71
  workspaceDir: '/tmp/project',