@swarmclawai/swarmclaw 1.2.4 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +15 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/query/client.ts +17 -0
  109. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  110. package/src/lib/server/agents/agent-service.ts +429 -0
  111. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  112. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  113. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  114. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  115. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  116. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  117. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  118. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  119. package/src/lib/server/build-llm.ts +7 -15
  120. package/src/lib/server/capability-router.test.ts +70 -1
  121. package/src/lib/server/capability-router.ts +24 -99
  122. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  123. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  124. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  125. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  126. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  127. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  128. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  129. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  130. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  131. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  132. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  133. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  134. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  135. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  136. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  137. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  138. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  139. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  140. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  141. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  142. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  143. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  144. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  145. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  146. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  147. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  148. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  149. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  150. package/src/lib/server/chats/chat-session-service.ts +410 -0
  151. package/src/lib/server/connectors/access.ts +1 -1
  152. package/src/lib/server/connectors/commands.ts +7 -6
  153. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  154. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  155. package/src/lib/server/connectors/connector-service.ts +453 -0
  156. package/src/lib/server/connectors/delivery.ts +17 -12
  157. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  158. package/src/lib/server/connectors/media.ts +1 -1
  159. package/src/lib/server/connectors/response-media.ts +1 -1
  160. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  161. package/src/lib/server/connectors/session.ts +9 -7
  162. package/src/lib/server/connectors/voice-note.ts +2 -1
  163. package/src/lib/server/context-manager.ts +20 -1
  164. package/src/lib/server/cost.ts +2 -3
  165. package/src/lib/server/credentials/credential-repository.ts +43 -4
  166. package/src/lib/server/credentials/credential-service.ts +112 -0
  167. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  168. package/src/lib/server/daemon/controller.ts +577 -0
  169. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  170. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  171. package/src/lib/server/daemon/types.ts +101 -0
  172. package/src/lib/server/embeddings.ts +3 -9
  173. package/src/lib/server/eval/agent-regression.ts +3 -2
  174. package/src/lib/server/eval/runner.ts +2 -2
  175. package/src/lib/server/execution-brief.test.ts +167 -0
  176. package/src/lib/server/execution-brief.ts +295 -0
  177. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  178. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  179. package/src/lib/server/execution-engine/index.ts +35 -0
  180. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  181. package/src/lib/server/execution-engine/types.ts +33 -0
  182. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  183. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  184. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  185. package/src/lib/server/messages/message-repository.ts +330 -0
  186. package/src/lib/server/missions/mission-service/core.ts +8 -6
  187. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  188. package/src/lib/server/openclaw/doctor.ts +1 -1
  189. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  190. package/src/lib/server/openclaw/gateway.ts +5 -14
  191. package/src/lib/server/openclaw/health.ts +3 -11
  192. package/src/lib/server/openclaw/sync.ts +8 -6
  193. package/src/lib/server/persistence/storage-context.ts +3 -0
  194. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  195. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  196. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  197. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  198. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  199. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  200. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  201. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  202. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  203. package/src/lib/server/protocols/protocol-types.ts +10 -7
  204. package/src/lib/server/provider-endpoint.ts +7 -12
  205. package/src/lib/server/provider-model-discovery.ts +2 -11
  206. package/src/lib/server/query-expansion.ts +5 -6
  207. package/src/lib/server/run-context.test.ts +365 -0
  208. package/src/lib/server/run-context.ts +367 -0
  209. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  210. package/src/lib/server/runtime/queue/core.ts +61 -190
  211. package/src/lib/server/runtime/run-ledger.ts +8 -0
  212. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  213. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  214. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  215. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  216. package/src/lib/server/service-result.ts +16 -0
  217. package/src/lib/server/session-note.ts +2 -3
  218. package/src/lib/server/session-reset-policy.ts +4 -3
  219. package/src/lib/server/session-tools/connector.ts +9 -6
  220. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  221. package/src/lib/server/session-tools/crud.ts +162 -10
  222. package/src/lib/server/session-tools/delegate.ts +1 -1
  223. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  224. package/src/lib/server/session-tools/memory.ts +6 -4
  225. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  226. package/src/lib/server/session-tools/session-info.ts +119 -12
  227. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  228. package/src/lib/server/session-tools/skills.ts +15 -15
  229. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  230. package/src/lib/server/session-tools/subagent.ts +125 -7
  231. package/src/lib/server/session-tools/team-context.ts +4 -3
  232. package/src/lib/server/session-tools/wallet.ts +0 -58
  233. package/src/lib/server/sessions/session-lineage.ts +55 -0
  234. package/src/lib/server/sessions/session-repository.ts +2 -2
  235. package/src/lib/server/skills/learned-skills.ts +24 -23
  236. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  237. package/src/lib/server/skills/skill-repository.ts +136 -13
  238. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  239. package/src/lib/server/storage-normalization.test.ts +44 -267
  240. package/src/lib/server/storage-normalization.ts +75 -0
  241. package/src/lib/server/storage.ts +19 -0
  242. package/src/lib/server/structured-extract.ts +3 -14
  243. package/src/lib/server/tasks/task-followups.ts +16 -11
  244. package/src/lib/server/tasks/task-result.test.ts +25 -29
  245. package/src/lib/server/tasks/task-result.ts +5 -9
  246. package/src/lib/server/tasks/task-route-service.ts +449 -0
  247. package/src/lib/server/text-normalization.ts +41 -0
  248. package/src/lib/server/tool-planning.ts +6 -42
  249. package/src/lib/server/upload-path.ts +5 -0
  250. package/src/lib/server/working-state/extraction.ts +614 -0
  251. package/src/lib/server/working-state/normalization.ts +866 -0
  252. package/src/lib/server/working-state/prompt.ts +60 -0
  253. package/src/lib/server/working-state/repository.ts +38 -0
  254. package/src/lib/server/working-state/service.test.ts +253 -0
  255. package/src/lib/server/working-state/service.ts +293 -0
  256. package/src/lib/validation/schemas.ts +1 -0
  257. package/src/lib/ws-client.ts +3 -3
  258. package/src/stores/slices/task-slice.ts +1 -4
  259. package/src/stores/use-chatroom-store.ts +2 -2
  260. package/src/types/index.ts +277 -12
@@ -0,0 +1,206 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+ import type { Agent } from '@/types'
7
+ import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
8
+
9
+ const originalEnv = {
10
+ DATA_DIR: process.env.DATA_DIR,
11
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
12
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
13
+ }
14
+
15
+ let tempDir = ''
16
+ let advisory: typeof import('@/lib/server/agents/delegation-advisory')
17
+ let storage: typeof import('@/lib/server/storage')
18
+
19
+ before(async () => {
20
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegation-advisory-'))
21
+ process.env.DATA_DIR = path.join(tempDir, 'data')
22
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
23
+ process.env.SWARMCLAW_BUILD_MODE = '1'
24
+ fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
25
+ fs.mkdirSync(process.env.WORKSPACE_DIR, { recursive: true })
26
+
27
+ advisory = await import('@/lib/server/agents/delegation-advisory')
28
+ storage = await import('@/lib/server/storage')
29
+ })
30
+
31
+ after(() => {
32
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
33
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
34
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
35
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
36
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
37
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
38
+ fs.rmSync(tempDir, { recursive: true, force: true })
39
+ })
40
+
41
+ function makeAgent(params: Partial<Agent> & Pick<Agent, 'id' | 'name'>): Agent {
42
+ const now = Date.now()
43
+ return {
44
+ id: params.id,
45
+ name: params.name,
46
+ role: params.role || 'worker',
47
+ description: params.description || '',
48
+ systemPrompt: params.systemPrompt || '',
49
+ provider: params.provider || 'openai',
50
+ model: params.model || 'gpt-test',
51
+ capabilities: params.capabilities || [],
52
+ delegationEnabled: params.delegationEnabled ?? false,
53
+ delegationTargetMode: params.delegationTargetMode || 'all',
54
+ delegationTargetAgentIds: params.delegationTargetAgentIds || [],
55
+ createdAt: params.createdAt || now,
56
+ updatedAt: params.updatedAt || now,
57
+ } as Agent
58
+ }
59
+
60
+ function makeClassification(overrides: Partial<MessageClassification>): MessageClassification {
61
+ return {
62
+ taskIntent: 'general',
63
+ isDeliverableTask: false,
64
+ isBroadGoal: false,
65
+ walletIntent: 'none',
66
+ hasHumanSignals: false,
67
+ hasSignificantEvent: false,
68
+ isResearchSynthesis: false,
69
+ workType: 'general',
70
+ wantsScreenshots: false,
71
+ wantsOutboundDelivery: false,
72
+ wantsVoiceDelivery: false,
73
+ explicitToolRequests: [],
74
+ confidence: 0.9,
75
+ ...overrides,
76
+ }
77
+ }
78
+
79
+ function saveAgents(agents: Agent[]): Record<string, Agent> {
80
+ const record = Object.fromEntries(agents.map((agent) => [agent.id, agent]))
81
+ storage.saveAgents(record)
82
+ storage.saveTasks({})
83
+ storage.saveSessions({})
84
+ return record
85
+ }
86
+
87
+ describe('delegation-advisory', () => {
88
+ it('prefers a builder over a coordinator for coding work', () => {
89
+ const agents = saveAgents([
90
+ makeAgent({
91
+ id: 'ceo',
92
+ name: 'CEO',
93
+ role: 'coordinator',
94
+ capabilities: ['coordination', 'delegation', 'operations'],
95
+ delegationEnabled: true,
96
+ }),
97
+ makeAgent({
98
+ id: 'builder',
99
+ name: 'Builder',
100
+ role: 'worker',
101
+ capabilities: ['coding', 'implementation', 'debugging'],
102
+ }),
103
+ makeAgent({
104
+ id: 'writer',
105
+ name: 'Writer',
106
+ role: 'worker',
107
+ capabilities: ['writing', 'editing'],
108
+ }),
109
+ ])
110
+
111
+ const profile = advisory.buildDelegationTaskProfile({
112
+ classification: makeClassification({
113
+ isDeliverableTask: true,
114
+ workType: 'coding',
115
+ }),
116
+ })
117
+ const result = advisory.resolveDelegationAdvisory({
118
+ currentAgent: agents.ceo,
119
+ agents,
120
+ profile,
121
+ delegationTargetMode: 'all',
122
+ delegationTargetAgentIds: [],
123
+ })
124
+
125
+ assert.equal(result.shouldDelegate, true)
126
+ assert.equal(result.style, 'managerial')
127
+ assert.equal(result.recommended?.agentId, 'builder')
128
+ assert.match(advisory.formatDelegationRationale(result.recommended), /coding/i)
129
+ })
130
+
131
+ it('prefers a researcher for research work', () => {
132
+ const agents = saveAgents([
133
+ makeAgent({
134
+ id: 'ceo',
135
+ name: 'CEO',
136
+ role: 'coordinator',
137
+ capabilities: ['coordination', 'delegation', 'operations'],
138
+ delegationEnabled: true,
139
+ }),
140
+ makeAgent({
141
+ id: 'builder',
142
+ name: 'Builder',
143
+ role: 'worker',
144
+ capabilities: ['coding', 'implementation', 'debugging'],
145
+ }),
146
+ makeAgent({
147
+ id: 'researcher',
148
+ name: 'Researcher',
149
+ role: 'worker',
150
+ capabilities: ['research', 'analysis', 'summarization'],
151
+ }),
152
+ ])
153
+
154
+ const profile = advisory.buildDelegationTaskProfile({
155
+ classification: makeClassification({
156
+ isResearchSynthesis: true,
157
+ workType: 'research',
158
+ }),
159
+ })
160
+ const result = advisory.resolveDelegationAdvisory({
161
+ currentAgent: agents.ceo,
162
+ agents,
163
+ profile,
164
+ delegationTargetMode: 'all',
165
+ delegationTargetAgentIds: [],
166
+ })
167
+
168
+ assert.equal(result.shouldDelegate, true)
169
+ assert.equal(result.recommended?.agentId, 'researcher')
170
+ })
171
+
172
+ it('does not advise delegation when self is already as capable as peers', () => {
173
+ const agents = saveAgents([
174
+ makeAgent({
175
+ id: 'builder-a',
176
+ name: 'Builder A',
177
+ role: 'worker',
178
+ capabilities: ['coding', 'implementation', 'debugging'],
179
+ delegationEnabled: true,
180
+ }),
181
+ makeAgent({
182
+ id: 'builder-b',
183
+ name: 'Builder B',
184
+ role: 'worker',
185
+ capabilities: ['coding', 'implementation', 'debugging'],
186
+ }),
187
+ ])
188
+
189
+ const profile = advisory.buildDelegationTaskProfile({
190
+ classification: makeClassification({
191
+ isDeliverableTask: true,
192
+ workType: 'coding',
193
+ }),
194
+ })
195
+ const result = advisory.resolveDelegationAdvisory({
196
+ currentAgent: agents['builder-a'],
197
+ agents,
198
+ profile,
199
+ delegationTargetMode: 'all',
200
+ delegationTargetAgentIds: [],
201
+ })
202
+
203
+ assert.equal(result.recommended?.agentId, 'builder-b')
204
+ assert.equal(result.shouldDelegate, false)
205
+ })
206
+ })
@@ -0,0 +1,251 @@
1
+ import type { Agent } from '@/types'
2
+ import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
3
+ import { capabilityMatchScore } from '@/lib/server/agents/capability-match'
4
+ import { getAgentDirectory, type AgentDirectoryEntry } from '@/lib/server/agents/agent-registry'
5
+
6
+ export type DelegationWorkType =
7
+ | 'coding'
8
+ | 'research'
9
+ | 'writing'
10
+ | 'review'
11
+ | 'operations'
12
+ | 'general'
13
+
14
+ export interface DelegationTaskProfile {
15
+ workType: DelegationWorkType
16
+ requiredCapabilities: string[]
17
+ substantial: boolean
18
+ }
19
+
20
+ export interface DelegationCandidateFit {
21
+ agentId: string
22
+ agentName: string
23
+ score: number
24
+ availability: 'idle' | 'working' | 'unknown'
25
+ matchedCapabilities: string[]
26
+ reasons: string[]
27
+ }
28
+
29
+ export interface DelegationAdvisory {
30
+ profile: DelegationTaskProfile
31
+ current: DelegationCandidateFit | null
32
+ recommended: DelegationCandidateFit | null
33
+ shouldDelegate: boolean
34
+ style: 'managerial' | 'advisory'
35
+ }
36
+
37
+ const WORK_TYPE_CAPABILITIES: Record<DelegationWorkType, string[]> = {
38
+ coding: ['coding', 'implementation', 'debugging'],
39
+ research: ['research', 'analysis', 'summarization'],
40
+ writing: ['writing', 'messaging', 'structuring', 'editing'],
41
+ review: ['review', 'testing', 'risk assessment'],
42
+ operations: ['coordination', 'delegation', 'operations'],
43
+ general: [],
44
+ }
45
+
46
+ function normalizeCapabilityList(value: string[] | undefined | null): string[] {
47
+ if (!Array.isArray(value)) return []
48
+ const seen = new Set<string>()
49
+ const out: string[] = []
50
+ for (const entry of value) {
51
+ const trimmed = typeof entry === 'string' ? entry.trim() : ''
52
+ const key = trimmed.toLowerCase()
53
+ if (!trimmed || seen.has(key)) continue
54
+ seen.add(key)
55
+ out.push(trimmed)
56
+ }
57
+ return out
58
+ }
59
+
60
+ function normalizeWorkType(value: unknown): DelegationWorkType {
61
+ if (
62
+ value === 'coding'
63
+ || value === 'research'
64
+ || value === 'writing'
65
+ || value === 'review'
66
+ || value === 'operations'
67
+ ) {
68
+ return value
69
+ }
70
+ return 'general'
71
+ }
72
+
73
+ function matchedCapabilities(agentCapabilities: string[] | undefined, requiredCapabilities: string[]): string[] {
74
+ if (!requiredCapabilities.length || !Array.isArray(agentCapabilities) || !agentCapabilities.length) return []
75
+ const agentSet = new Set(agentCapabilities.map((entry) => entry.toLowerCase()))
76
+ return requiredCapabilities.filter((entry) => agentSet.has(entry.toLowerCase()))
77
+ }
78
+
79
+ function roleAdjustment(agent: Agent, profile: DelegationTaskProfile): number {
80
+ const role = agent.role === 'coordinator' ? 'coordinator' : 'worker'
81
+ if (profile.workType === 'operations') {
82
+ return role === 'coordinator' ? 0.28 : -0.04
83
+ }
84
+ if (profile.workType === 'general') {
85
+ return role === 'coordinator' ? -0.03 : 0
86
+ }
87
+ return role === 'coordinator' ? -0.18 : 0.16
88
+ }
89
+
90
+ function selfExecutionPenalty(agent: Agent, profile: DelegationTaskProfile, isSelf: boolean): number {
91
+ if (!isSelf) return 0
92
+ if (agent.role !== 'coordinator') return 0
93
+ if (!profile.substantial) return 0
94
+ if (profile.workType === 'operations') return 0
95
+ return -0.42
96
+ }
97
+
98
+ function availabilityAdjustment(
99
+ availability: DelegationCandidateFit['availability'],
100
+ isSelf: boolean,
101
+ directoryEntry?: AgentDirectoryEntry,
102
+ ): number {
103
+ if (availability === 'idle') return 0.08
104
+ if (availability === 'working') {
105
+ // The current live chat session should not count as a self-load penalty.
106
+ if (isSelf && !directoryEntry?.statusDetail) return 0.08
107
+ return -0.08
108
+ }
109
+ return 0
110
+ }
111
+
112
+ function buildAvailabilityMap(): Map<string, AgentDirectoryEntry> {
113
+ return new Map(getAgentDirectory().map((entry) => [entry.id, entry]))
114
+ }
115
+
116
+ function buildCandidateFit(
117
+ agent: Agent,
118
+ profile: DelegationTaskProfile,
119
+ directory: Map<string, AgentDirectoryEntry>,
120
+ isSelf = false,
121
+ ): DelegationCandidateFit {
122
+ const directoryEntry = directory.get(agent.id)
123
+ const availability = directoryEntry?.status || 'unknown'
124
+ const matched = matchedCapabilities(agent.capabilities, profile.requiredCapabilities)
125
+ const capabilityScore = profile.requiredCapabilities.length > 0
126
+ ? capabilityMatchScore(agent.capabilities, profile.requiredCapabilities) * 1.45
127
+ : 0
128
+ const score = capabilityScore
129
+ + roleAdjustment(agent, profile)
130
+ + availabilityAdjustment(availability, isSelf, directoryEntry)
131
+ + selfExecutionPenalty(agent, profile, isSelf)
132
+
133
+ const reasons: string[] = []
134
+ if (matched.length > 0) reasons.push(`capability match: ${matched.join(', ')}`)
135
+ if (profile.workType === 'operations' && agent.role === 'coordinator') reasons.push('coordinator role fits operations work')
136
+ if (profile.workType !== 'operations' && profile.workType !== 'general' && agent.role !== 'coordinator') reasons.push('worker role fits execution-heavy work')
137
+ if (availability === 'idle') reasons.push('currently idle')
138
+ if (availability === 'working' && directoryEntry?.statusDetail) reasons.push(directoryEntry.statusDetail)
139
+ if (isSelf && selfExecutionPenalty(agent, profile, true) < 0) reasons.push('coordinator should prefer orchestration over direct execution')
140
+
141
+ return {
142
+ agentId: agent.id,
143
+ agentName: agent.name,
144
+ score,
145
+ availability,
146
+ matchedCapabilities: matched,
147
+ reasons,
148
+ }
149
+ }
150
+
151
+ function isAllowedDelegateTarget(
152
+ agentId: string,
153
+ opts?: { delegationTargetMode?: 'all' | 'selected'; delegationTargetAgentIds?: string[] },
154
+ ): boolean {
155
+ if (opts?.delegationTargetMode !== 'selected') return true
156
+ const allowed = new Set(normalizeCapabilityList(opts.delegationTargetAgentIds))
157
+ return allowed.size === 0 || allowed.has(agentId)
158
+ }
159
+
160
+ export function resolveDelegationWorkType(
161
+ classification: MessageClassification | null | undefined,
162
+ ): DelegationWorkType {
163
+ return normalizeWorkType(classification?.workType)
164
+ }
165
+
166
+ export function buildDelegationTaskProfile(params: {
167
+ classification?: MessageClassification | null
168
+ workType?: DelegationWorkType | null
169
+ requiredCapabilities?: string[] | null
170
+ }): DelegationTaskProfile {
171
+ const workType = params.workType
172
+ ? normalizeWorkType(params.workType)
173
+ : resolveDelegationWorkType(params.classification)
174
+ const explicitRequirements = normalizeCapabilityList(params.requiredCapabilities)
175
+ const requiredCapabilities = explicitRequirements.length > 0
176
+ ? explicitRequirements
177
+ : WORK_TYPE_CAPABILITIES[workType]
178
+ const substantial = explicitRequirements.length > 0
179
+ || Boolean(params.classification?.isBroadGoal)
180
+ || Boolean(params.classification?.isDeliverableTask)
181
+ || Boolean(params.classification?.isResearchSynthesis)
182
+ || workType !== 'general'
183
+ return {
184
+ workType,
185
+ requiredCapabilities,
186
+ substantial,
187
+ }
188
+ }
189
+
190
+ export function resolveBestDelegateTarget(params: {
191
+ currentAgentId?: string | null
192
+ agents: Record<string, Agent>
193
+ profile: DelegationTaskProfile
194
+ delegationTargetMode?: 'all' | 'selected'
195
+ delegationTargetAgentIds?: string[]
196
+ }): DelegationCandidateFit | null {
197
+ const directory = buildAvailabilityMap()
198
+ const candidates = Object.values(params.agents)
199
+ .filter((agent) => agent.id !== params.currentAgentId)
200
+ .filter((agent) => !agent.disabled && !agent.trashedAt)
201
+ .filter((agent) => isAllowedDelegateTarget(agent.id, params))
202
+ .map((agent) => buildCandidateFit(agent, params.profile, directory))
203
+ .sort((left, right) => {
204
+ if (right.score !== left.score) return right.score - left.score
205
+ return left.agentName.localeCompare(right.agentName)
206
+ })
207
+ return candidates[0] || null
208
+ }
209
+
210
+ export function resolveDelegationAdvisory(params: {
211
+ currentAgent: Agent | null | undefined
212
+ agents: Record<string, Agent>
213
+ profile: DelegationTaskProfile
214
+ delegationTargetMode?: 'all' | 'selected'
215
+ delegationTargetAgentIds?: string[]
216
+ }): DelegationAdvisory {
217
+ const directory = buildAvailabilityMap()
218
+ const current = params.currentAgent && !params.currentAgent.disabled && !params.currentAgent.trashedAt
219
+ ? buildCandidateFit(params.currentAgent, params.profile, directory, true)
220
+ : null
221
+ const recommended = resolveBestDelegateTarget({
222
+ currentAgentId: params.currentAgent?.id || null,
223
+ agents: params.agents,
224
+ profile: params.profile,
225
+ delegationTargetMode: params.delegationTargetMode,
226
+ delegationTargetAgentIds: params.delegationTargetAgentIds,
227
+ })
228
+ const currentScore = current?.score ?? 0
229
+ const recommendedScore = recommended?.score ?? Number.NEGATIVE_INFINITY
230
+ const shouldDelegate = Boolean(
231
+ params.profile.substantial
232
+ && recommended
233
+ && recommendedScore >= currentScore + 0.3
234
+ && recommendedScore >= 0.25,
235
+ )
236
+ const style = params.currentAgent?.role === 'coordinator' && params.profile.workType !== 'operations'
237
+ ? 'managerial'
238
+ : 'advisory'
239
+ return {
240
+ profile: params.profile,
241
+ current,
242
+ recommended,
243
+ shouldDelegate,
244
+ style,
245
+ }
246
+ }
247
+
248
+ export function formatDelegationRationale(candidate: DelegationCandidateFit | null | undefined): string {
249
+ if (!candidate || candidate.reasons.length === 0) return 'better fit for this work'
250
+ return candidate.reasons.slice(0, 2).join('; ')
251
+ }
@@ -1,4 +1,5 @@
1
1
  import { hmrSingleton } from '@/lib/shared-utils'
2
+ import { getMessages } from '@/lib/server/messages/message-repository'
2
3
  import type { GoalContract, Message, MessageToolEvent, Session } from '@/types'
3
4
  import { mergeGoalContracts, parseGoalContractFromText, parseMainLoopPlan, parseMainLoopReview } from '@/lib/server/agents/autonomy-contract'
4
5
  import {
@@ -12,6 +13,10 @@ import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
12
13
  import { buildMissionHeartbeatPrompt as buildMissionHeartbeatPromptFromMission, getMissionForSession } from '@/lib/server/missions/mission-service'
13
14
  import { loadSettings } from '@/lib/server/settings/settings-repository'
14
15
  import { getSession, loadSessions } from '@/lib/server/sessions/session-repository'
16
+ import { deleteSessionWorkingState, loadSessionWorkingState, syncWorkingStateFromMainLoopState } from '@/lib/server/working-state/service'
17
+ import { syncMainLoopToRunContext } from '@/lib/server/run-context'
18
+ import { buildExecutionBrief, buildExecutionBriefContextBlock } from '@/lib/server/execution-brief'
19
+ import { cleanText, cleanMultiline } from '@/lib/server/text-normalization'
15
20
 
16
21
  const LEGACY_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
17
22
  const HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
@@ -19,7 +24,7 @@ const MAX_PENDING_EVENTS = 16
19
24
  const MAX_TIMELINE_ITEMS = 40
20
25
  const MAX_WORKING_MEMORY_NOTES = 12
21
26
  const DEFAULT_FOLLOWUP_DELAY_MS = 1500
22
- const DEFAULT_MAX_FOLLOWUP_CHAIN = 4
27
+ const DEFAULT_MAX_FOLLOWUP_CHAIN = 3
23
28
  const MAX_LIFETIME_ITERATIONS = 200
24
29
 
25
30
  export interface MainLoopState {
@@ -111,24 +116,6 @@ function asSession(session: unknown): MainSessionLike | null {
111
116
  return session as MainSessionLike
112
117
  }
113
118
 
114
- function cleanText(value: unknown, maxChars = 320): string | null {
115
- if (typeof value !== 'string') return null
116
- const normalized = value.replace(/\s+/g, ' ').trim()
117
- return normalized ? normalized.slice(0, maxChars) : null
118
- }
119
-
120
- function cleanMultiline(value: unknown, maxChars = 1400): string | null {
121
- if (typeof value !== 'string') return null
122
- const normalized = value
123
- .split('\n')
124
- .map((line) => line.trim())
125
- .filter(Boolean)
126
- .join('\n')
127
- .slice(0, maxChars)
128
- .trim()
129
- return normalized || null
130
- }
131
-
132
119
  function normalizeConfidence(value: unknown): number | null {
133
120
  const raw = typeof value === 'number'
134
121
  ? value
@@ -403,7 +390,7 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
403
390
  const session = sessions[sessionId]
404
391
  if (!session || !isMainSession(session)) return null
405
392
 
406
- const messages = Array.isArray(session.messages) ? session.messages : []
393
+ const messages = getMessages(sessionId)
407
394
  const hydrated = defaultState()
408
395
  hydrated.autonomyMode = session.heartbeatEnabled === true ? 'autonomous' : 'assist'
409
396
  hydrated.updatedAt = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : now()
@@ -440,21 +427,48 @@ function hydrateStateFromSession(sessionId: string): MainLoopState | null {
440
427
  }
441
428
  }
442
429
 
443
- return normalizeState(hydrated)
430
+ return mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(hydrated))
444
431
  }
445
432
 
446
433
  function persistState(sessionId: string, state: MainLoopState): void {
447
- upsertPersistedMainLoopState(sessionId, state as unknown as Record<string, unknown>)
434
+ const normalized = clampState(state)
435
+ upsertPersistedMainLoopState(sessionId, normalized as unknown as Record<string, unknown>)
436
+ const session = getSession(sessionId)
437
+ if (!session) return
438
+ const mission = getMissionForSession(session)
439
+ void syncWorkingStateFromMainLoopState({
440
+ sessionId,
441
+ mission,
442
+ goal: normalized.goal,
443
+ summary: normalized.summary,
444
+ status: normalized.status === 'ok'
445
+ ? 'completed'
446
+ : normalized.status === 'blocked'
447
+ ? 'blocked'
448
+ : normalized.status === 'progress'
449
+ ? 'progress'
450
+ : 'idle',
451
+ nextAction: normalized.nextAction,
452
+ planSteps: normalized.planSteps,
453
+ blockers: normalized.skillBlocker ? [{
454
+ summary: normalized.skillBlocker.summary,
455
+ kind: normalized.skillBlocker.status === 'approval_requested' ? 'approval' : 'other',
456
+ }] : undefined,
457
+ })
448
458
  }
449
459
 
450
460
  function getOrCreateState(sessionId: string): MainLoopState | null {
451
461
  const existing = stateMap.get(sessionId)
452
- if (existing) return existing
462
+ if (existing) {
463
+ const merged = mergeWorkingStateIntoMainLoopState(sessionId, existing)
464
+ stateMap.set(sessionId, merged)
465
+ return merged
466
+ }
453
467
 
454
468
  // Try disk (survives full restart)
455
469
  const persisted = loadPersistedMainLoopState(sessionId) as Partial<MainLoopState> | null
456
470
  if (persisted) {
457
- const restored = normalizeState(persisted)
471
+ const restored = mergeWorkingStateIntoMainLoopState(sessionId, normalizeState(persisted))
458
472
  stateMap.set(sessionId, restored)
459
473
  return restored
460
474
  }
@@ -467,6 +481,43 @@ function getOrCreateState(sessionId: string): MainLoopState | null {
467
481
  return hydrated
468
482
  }
469
483
 
484
+ function mergeWorkingStateIntoMainLoopState(sessionId: string, current: MainLoopState): MainLoopState {
485
+ const workingState = loadSessionWorkingState(sessionId)
486
+ if (!workingState) return clampState(current)
487
+ const next = normalizeState(current)
488
+ if (workingState.objective) next.goal = cleanMultiline(workingState.objective, 900)
489
+ if (workingState.summary) next.summary = cleanText(workingState.summary, 1000)
490
+ if (workingState.nextAction) next.nextAction = cleanText(workingState.nextAction, 240)
491
+ if (workingState.status === 'completed') next.status = 'ok'
492
+ else if (workingState.status === 'blocked' || workingState.status === 'waiting') next.status = 'blocked'
493
+ else if (workingState.status === 'progress') next.status = 'progress'
494
+
495
+ const planSteps = (workingState.planSteps || [])
496
+ .map((step) => cleanText(step.text, 240))
497
+ .filter((step): step is string => Boolean(step))
498
+ if (planSteps.length > 0) {
499
+ next.planSteps = uniqueStrings(planSteps, 8)
500
+ next.completedPlanSteps = uniqueStrings(
501
+ (workingState.planSteps || [])
502
+ .filter((step) => step.status === 'resolved')
503
+ .map((step) => cleanText(step.text, 240))
504
+ .filter((step): step is string => Boolean(step)),
505
+ 16,
506
+ )
507
+ const activeStep = (workingState.planSteps || []).find((step) => step.status === 'active')
508
+ if (activeStep?.text) next.currentPlanStep = cleanText(activeStep.text, 240)
509
+ }
510
+
511
+ const noteCandidates = [
512
+ ...(workingState.confirmedFacts || []).filter((item) => item.status === 'active').map((item) => `Fact: ${item.statement}`),
513
+ ...(workingState.blockers || []).filter((item) => item.status === 'active').map((item) => `Blocker: ${item.summary}`),
514
+ ]
515
+ if (noteCandidates.length > 0) {
516
+ next.workingMemoryNotes = uniqueStrings([...(next.workingMemoryNotes || []), ...noteCandidates], MAX_WORKING_MEMORY_NOTES)
517
+ }
518
+ return clampState(next)
519
+ }
520
+
470
521
  function summarizePendingEvents(events: MainLoopState['pendingEvents']): string {
471
522
  if (!events.length) return ''
472
523
  return events
@@ -754,39 +805,38 @@ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: s
754
805
  const state = getOrCreateState(String(candidate.id))
755
806
  if (!state) return fallbackPrompt
756
807
  const latestExternalGoal = extractLatestGoal(Array.isArray(candidate.messages) ? candidate.messages as Message[] : [])
757
- const effectiveGoal = state.goal || latestExternalGoal.goal
758
808
  const effectiveGoalContract = latestExternalGoal.goalContract
759
809
  ? mergeGoalContracts(state.goalContract, latestExternalGoal.goalContract)
760
810
  : state.goalContract
761
811
 
762
- const completedSet = new Set(state.completedPlanSteps.map((s) => s.toLowerCase()))
763
- const planLines = state.planSteps.length > 0
764
- ? state.planSteps.slice(0, 8).map((step, index) => {
765
- const isDone = completedSet.has(step.toLowerCase())
766
- return `${index + 1}. ${isDone ? '[DONE] ' : ''}${step}`
767
- }).join('\n')
768
- : ''
812
+ const heartbeatSession = (persistedSession || candidate as Session)
813
+ const executionBrief = buildExecutionBrief({
814
+ session: heartbeatSession,
815
+ mission: getMissionForSession(heartbeatSession),
816
+ })
817
+ const executionBriefBlock = buildExecutionBriefContextBlock(executionBrief)
769
818
  const boundedFallbackPrompt = cleanMultiline(fallbackPrompt, 500)
770
- const boundedSummary = cleanMultiline(state.summary, 500)
819
+ const workingState = loadSessionWorkingState(String(candidate.id))
820
+ const activeWorkingBlockers = (workingState?.blockers || [])
821
+ .filter((item) => item.status === 'active')
822
+ .map((item) => item.nextAction ? `${item.summary} | next: ${item.nextAction}` : item.summary)
823
+ .slice(0, 4)
824
+ .join('\n')
771
825
 
772
826
  return [
773
827
  'MAIN_AGENT_HEARTBEAT_TICK',
774
828
  `Time: ${new Date().toISOString()}`,
775
- effectiveGoal ? `Current goal:\n${effectiveGoal}` : '',
829
+ executionBriefBlock,
776
830
  formatGoalContract(effectiveGoalContract),
777
- `Current status: ${state.status}`,
778
- state.nextAction ? `Planned next action: ${state.nextAction}` : '',
779
- state.currentPlanStep ? `Current plan step: ${state.currentPlanStep}` : '',
780
- planLines ? `Plan:\n${planLines}` : '',
781
831
  state.pendingEvents.length > 0 ? `Pending external events:\n${summarizePendingEvents(state.pendingEvents)}` : '',
832
+ activeWorkingBlockers ? `Active blockers:\n${activeWorkingBlockers}` : '',
782
833
  state.skillBlocker ? `Active skill blocker:\n${summarizeSkillBlocker(state.skillBlocker)}` : '',
783
834
  summarizeSelectedSkillRuntime(candidate),
784
- boundedSummary ? `Latest summary:\n${boundedSummary}` : '',
785
835
  boundedFallbackPrompt ? `Base heartbeat instructions:\n${boundedFallbackPrompt}` : '',
786
836
  '',
787
837
  'You are checking the durable main mission thread for this agent.',
788
838
  'Keep this status check brief — 5-10 tool calls maximum. Read key state, summarize progress, and report. Do not attempt fixes or deep investigation during heartbeats.',
789
- 'Use only the current goal, plan, next action, and pending external events shown above.',
839
+ 'Use the execution brief and pending external events shown above as the authoritative state for this tick.',
790
840
  'Do not infer or repeat old tasks from prior heartbeats.',
791
841
  'Prefer taking the single highest-value next step over restating the plan. Do not repeat completed work.',
792
842
  'If you revise the plan, emit exactly one line like:',
@@ -827,6 +877,7 @@ export function getMainLoopStateForSession(sessionId: string): MainLoopState | n
827
877
 
828
878
  export function clearMainLoopStateForSession(sessionId: string): boolean {
829
879
  deletePersistedMainLoopState(sessionId)
880
+ deleteSessionWorkingState(sessionId)
830
881
  return stateMap.delete(sessionId)
831
882
  }
832
883
 
@@ -840,6 +891,7 @@ export function pruneMainLoopState(liveSessionIds: Set<string>): number {
840
891
  if (!liveSessionIds.has(sessionId)) {
841
892
  stateMap.delete(sessionId)
842
893
  deletePersistedMainLoopState(sessionId)
894
+ deleteSessionWorkingState(sessionId)
843
895
  removed++
844
896
  }
845
897
  }
@@ -1096,5 +1148,11 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
1096
1148
  const finalClamped = clampState(state)
1097
1149
  stateMap.set(input.sessionId, finalClamped)
1098
1150
  persistState(input.sessionId, finalClamped)
1151
+
1152
+ // Project orchestrator state into session RunContext (non-critical)
1153
+ try {
1154
+ syncMainLoopToRunContext(input.sessionId, finalClamped)
1155
+ } catch { /* non-critical — main loop continues even if sync fails */ }
1156
+
1099
1157
  return followup
1100
1158
  }