@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
@@ -6,13 +6,24 @@
6
6
  * The main prompt assembly in stream-agent-chat.ts composes these declaratively.
7
7
  */
8
8
 
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
9
11
  import type { Session, Agent } from '@/types'
12
+ import type { PromptMode } from '@/lib/server/chat-execution/prompt-mode'
13
+ import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
10
14
  import type { ActiveProjectContext } from '@/lib/server/project-context'
15
+ import type { SessionToolPolicyDecision } from '@/lib/server/tool-capability-policy'
11
16
  import { buildIdentityContinuityContext } from '@/lib/server/identity-continuity'
17
+ import {
18
+ buildDelegationTaskProfile,
19
+ formatDelegationRationale,
20
+ resolveDelegationAdvisory,
21
+ } from '@/lib/server/agents/delegation-advisory'
12
22
  import { getAgent, listAgents } from '@/lib/server/agents/agent-repository'
13
23
  import { loadSkills } from '@/lib/server/skills/skill-repository'
14
24
  import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
15
25
  import { resolveTeam } from '@/lib/server/agents/team-resolution'
26
+ import { canonicalizeExtensionId } from '@/lib/server/tool-aliases'
16
27
 
17
28
  // ---------------------------------------------------------------------------
18
29
  // Identity: agent name, description, continuity, soul, systemPrompt, skills
@@ -89,6 +100,148 @@ export function buildThinkingSection(
89
100
  return text ? `## Reasoning Depth\n${text}` : null
90
101
  }
91
102
 
103
+ // ---------------------------------------------------------------------------
104
+ // Runtime Orientation
105
+ // ---------------------------------------------------------------------------
106
+
107
+ const WORKSPACE_MARKER_FILES = [
108
+ 'AGENTS.md',
109
+ 'BOOTSTRAP.md',
110
+ 'HEARTBEAT.md',
111
+ 'IDENTITY.md',
112
+ 'TOOLS.md',
113
+ 'USER.md',
114
+ ]
115
+
116
+ function normalizeRuntimeExtensionId(extensionId: string): string {
117
+ const normalized = extensionId.trim().toLowerCase()
118
+ if (!normalized) return ''
119
+ if (normalized === 'delegate_to_claude_code' || normalized === 'claude_code') return 'claude_code'
120
+ if (normalized === 'delegate_to_codex_cli' || normalized === 'codex_cli') return 'codex_cli'
121
+ if (normalized === 'delegate_to_opencode_cli' || normalized === 'opencode_cli') return 'opencode_cli'
122
+ if (normalized === 'delegate_to_gemini_cli' || normalized === 'gemini_cli') return 'gemini_cli'
123
+ if (['session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'].includes(normalized)) return 'manage_sessions'
124
+ return canonicalizeExtensionId(normalized)
125
+ }
126
+
127
+ function canonicalizeEnabledExtensions(enabledExtensions: string[]): string[] {
128
+ const seen = new Set<string>()
129
+ const values: string[] = []
130
+ for (const extensionId of enabledExtensions) {
131
+ const normalized = normalizeRuntimeExtensionId(extensionId)
132
+ if (!normalized || seen.has(normalized)) continue
133
+ seen.add(normalized)
134
+ values.push(normalized)
135
+ }
136
+ return values
137
+ }
138
+
139
+ function formatInlineCodeList(values: string[], maxItems = 8): string {
140
+ if (values.length === 0) return '(none)'
141
+ const head = values.slice(0, maxItems).map((value) => `\`${value}\``)
142
+ if (values.length > maxItems) head.push(`... +${values.length - maxItems} more`)
143
+ return head.join(', ')
144
+ }
145
+
146
+ function formatPolicyBlocks(blocked: SessionToolPolicyDecision['blockedExtensions'], maxItems = 3): string {
147
+ const lines = blocked
148
+ .slice(0, maxItems)
149
+ .map((entry) => `\`${canonicalizeExtensionId(entry.tool)}\` (${entry.reason})`)
150
+ if (blocked.length > maxItems) lines.push(`... +${blocked.length - maxItems} more`)
151
+ return lines.join(', ')
152
+ }
153
+
154
+ function collectWorkspaceMarkers(cwd: string | null | undefined): string[] {
155
+ if (typeof cwd !== 'string' || !cwd.trim()) return []
156
+ return WORKSPACE_MARKER_FILES.filter((filename) => {
157
+ try {
158
+ return fs.existsSync(path.join(cwd, filename))
159
+ } catch {
160
+ return false
161
+ }
162
+ })
163
+ }
164
+
165
+ function resolveRuntimeSessionKind(params: {
166
+ session: Session
167
+ isHeartbeat?: boolean
168
+ isConnectorSession?: boolean
169
+ }): string {
170
+ if (params.isHeartbeat) return 'heartbeat'
171
+ if (params.isConnectorSession) return 'connector'
172
+ if (params.session.parentSessionId) return 'delegated_child'
173
+ return 'root_chat'
174
+ }
175
+
176
+ export function buildRuntimeOrientationSection(params: {
177
+ session: Session
178
+ promptMode: PromptMode
179
+ sessionExtensions: string[]
180
+ toolPolicy?: SessionToolPolicyDecision | null
181
+ agent?: Agent | null
182
+ activeProjectContext?: ActiveProjectContext | null
183
+ rootSessionId?: string | null
184
+ isHeartbeat?: boolean
185
+ isConnectorSession?: boolean
186
+ }): string {
187
+ const { session, agent } = params
188
+ const activeProjectContext = params.activeProjectContext || null
189
+ const enabledExtensions = canonicalizeEnabledExtensions(params.sessionExtensions)
190
+ const workspaceMarkers = collectWorkspaceMarkers(session.cwd)
191
+ const delegationEnabled = enabledExtensions.some((extensionId) =>
192
+ ['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli'].includes(extensionId),
193
+ )
194
+
195
+ const lines = [
196
+ '## Runtime Orientation',
197
+ `Session: ${resolveRuntimeSessionKind({ session, isHeartbeat: params.isHeartbeat, isConnectorSession: params.isConnectorSession })} | prompt=${params.promptMode} | id=${session.id}`,
198
+ session.parentSessionId || params.rootSessionId
199
+ ? `Lineage: parent=${session.parentSessionId || '(none)'} | root=${params.rootSessionId || session.id}`
200
+ : `Lineage: root=${session.id}`,
201
+ `Agent: ${agent?.name || session.agentId || 'Unassigned'}${session.agentId ? ` [id: ${session.agentId}]` : ''}`,
202
+ `Provider/model: ${session.provider} / ${session.model || '(default)'}`,
203
+ `CWD: ${session.cwd || '(none)'}`,
204
+ ]
205
+
206
+ if (workspaceMarkers.length > 0) {
207
+ lines.push(`Workspace markers: ${workspaceMarkers.join(', ')}`)
208
+ }
209
+
210
+ if (activeProjectContext?.projectId) {
211
+ const projectLabel = activeProjectContext.project?.name || activeProjectContext.projectId
212
+ const projectRoot = activeProjectContext.projectRoot ? ` | root=${activeProjectContext.projectRoot}` : ''
213
+ lines.push(`Active project: ${projectLabel}${projectRoot}`)
214
+ }
215
+
216
+ lines.push(`Enabled capabilities now: ${formatInlineCodeList(enabledExtensions)}`)
217
+
218
+ if (params.toolPolicy?.blockedExtensions?.length) {
219
+ lines.push(`Policy blocked: ${formatPolicyBlocks(params.toolPolicy.blockedExtensions)}`)
220
+ }
221
+
222
+ if (delegationEnabled) {
223
+ const delegateMode = agent?.delegationTargetMode === 'selected' ? 'selected' : 'all'
224
+ const targetIds = Array.isArray(agent?.delegationTargetAgentIds)
225
+ ? agent!.delegationTargetAgentIds.filter((value) => typeof value === 'string' && value.trim())
226
+ : []
227
+ lines.push(
228
+ delegateMode === 'selected'
229
+ ? `Delegation: enabled (${delegateMode}) | targets=${targetIds.length ? targetIds.join(', ') : '(none configured)'}`
230
+ : 'Delegation: enabled (all allowed targets)',
231
+ )
232
+ } else {
233
+ lines.push('Delegation: disabled in this runtime')
234
+ }
235
+
236
+ if (enabledExtensions.includes('manage_sessions')) {
237
+ lines.push('Harness inspection: use `sessions_tool` action `identity` for live session/platform context; use action `history` only when you need earlier messages from this same session.')
238
+ }
239
+
240
+ lines.push('Platform routing: prefer direct `manage_*` tools when enabled; use `manage_platform` only as fallback.')
241
+
242
+ return lines.join('\n')
243
+ }
244
+
92
245
  // ---------------------------------------------------------------------------
93
246
  // Workspace Context (async — dynamic import)
94
247
  // ---------------------------------------------------------------------------
@@ -414,6 +567,43 @@ export function buildCoordinatorSection(
414
567
  return lines.join('\n')
415
568
  }
416
569
 
570
+ export function buildDelegationRecommendationSection(params: {
571
+ agent: Agent | null | undefined
572
+ classification?: MessageClassification | null
573
+ }): string | null {
574
+ const agent = params.agent
575
+ if (!agent || agent.delegationEnabled !== true) return null
576
+ const profile = buildDelegationTaskProfile({
577
+ classification: params.classification,
578
+ })
579
+ if (!profile.substantial) return null
580
+
581
+ const advisory = resolveDelegationAdvisory({
582
+ currentAgent: agent,
583
+ agents: listAgents(),
584
+ profile,
585
+ delegationTargetMode: agent.delegationTargetMode === 'selected' ? 'selected' : 'all',
586
+ delegationTargetAgentIds: agent.delegationTargetAgentIds || [],
587
+ })
588
+ if (!advisory.shouldDelegate || !advisory.recommended) return null
589
+
590
+ const recommendation = advisory.recommended
591
+ const workLabel = profile.workType === 'general' ? 'substantial work' : `${profile.workType} work`
592
+ const recommendationLabel = advisory.style === 'managerial'
593
+ ? 'Managerial Delegation Recommendation'
594
+ : 'Delegation Recommendation'
595
+
596
+ return [
597
+ `## ${recommendationLabel}`,
598
+ `This request looks like ${workLabel}.`,
599
+ `Best-fit teammate: **${recommendation.agentName}** [id: ${recommendation.agentId}]`,
600
+ `Why: ${formatDelegationRationale(recommendation)}.`,
601
+ advisory.style === 'managerial'
602
+ ? 'Prefer delegating the execution and keep your direct work to reconnaissance, validation, and synthesis.'
603
+ : 'Prefer delegating the execution and reserve direct tool use for quick lookups or validation.',
604
+ ].join('\n')
605
+ }
606
+
417
607
  // ---------------------------------------------------------------------------
418
608
  // Credential Awareness
419
609
  // ---------------------------------------------------------------------------
@@ -527,3 +717,6 @@ export function buildCliDelegationContext(opts: {
527
717
 
528
718
  return parts.join('\n')
529
719
  }
720
+
721
+ // Re-export RunContext prompt builder from its canonical home
722
+ export { buildRunContextSection } from '@/lib/server/run-context'
@@ -3,7 +3,7 @@ import { HumanMessage, AIMessage } from '@langchain/core/messages'
3
3
  import { createReactAgent } from '@langchain/langgraph/prebuilt'
4
4
  import { MemorySaver } from '@langchain/langgraph'
5
5
  import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults'
6
- import { getAgent } from '@/lib/server/agents/agent-repository'
6
+ import { getAgent, listAgents } from '@/lib/server/agents/agent-repository'
7
7
  import { buildSessionTools } from '@/lib/server/session-tools'
8
8
  import { buildChatModel } from '@/lib/server/build-llm'
9
9
  import { loadSettings } from '@/lib/server/settings/settings-repository'
@@ -18,6 +18,7 @@ import { truncateToolResultText } from '@/lib/server/chat-execution/tool-result-
18
18
  import {
19
19
  buildIdentitySection,
20
20
  buildThinkingSection,
21
+ buildRuntimeOrientationSection,
21
22
  buildWorkspaceSection,
22
23
  buildAgentAwarenessSection,
23
24
  buildSituationalSection,
@@ -27,13 +28,15 @@ import {
27
28
  buildProactiveMemorySection,
28
29
  buildCoordinatorSection,
29
30
  buildCredentialAwarenessSection,
31
+ buildDelegationRecommendationSection,
32
+ buildRunContextSection,
30
33
  } from '@/lib/server/chat-execution/prompt-sections'
31
34
 
32
35
  import { log } from '@/lib/server/logger'
33
36
  import { logExecution } from '@/lib/server/execution-log'
34
37
  import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
35
38
  import { expandExtensionIds } from '@/lib/server/tool-aliases'
36
- import type { Session, Message } from '@/types'
39
+ import type { ExecutionBrief, Session, Message } from '@/types'
37
40
  import { getEnabledCapabilityIds } from '@/lib/capability-selection'
38
41
  import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
39
42
  import { resolveActiveProjectContext } from '@/lib/server/project-context'
@@ -89,6 +92,7 @@ import {
89
92
  DEFAULT_PROMPT_BUDGET,
90
93
  MINIMAL_PROMPT_BUDGET,
91
94
  } from '@/lib/server/chat-execution/prompt-budget'
95
+ import { resolveSessionLineageIds } from '@/lib/server/sessions/session-lineage'
92
96
  import { IterationTimers } from '@/lib/server/chat-execution/iteration-timers'
93
97
  import { processIterationEvents } from '@/lib/server/chat-execution/iteration-event-handler'
94
98
  import { evaluateContinuation } from '@/lib/server/chat-execution/continuation-evaluator'
@@ -101,6 +105,11 @@ import {
101
105
  isResearchSynthesis as classifiedIsResearchSynthesis,
102
106
  type MessageClassification,
103
107
  } from '@/lib/server/chat-execution/message-classifier'
108
+ import {
109
+ buildDelegationTaskProfile,
110
+ formatDelegationRationale,
111
+ resolveDelegationAdvisory,
112
+ } from '@/lib/server/agents/delegation-advisory'
104
113
 
105
114
  const TAG = 'stream-agent-chat'
106
115
 
@@ -174,6 +183,7 @@ interface StreamAgentChatOpts {
174
183
  attachedFiles?: string[]
175
184
  apiKey: string | null
176
185
  systemPrompt?: string
186
+ executionBrief?: ExecutionBrief | null
177
187
  extraSystemContext?: string[]
178
188
  write: (data: string) => void
179
189
  history: Message[]
@@ -213,7 +223,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
213
223
 
214
224
  async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
215
225
  const startTs = Date.now()
216
- const { session, message, imagePath, imageUrl, attachedFiles, apiKey, systemPrompt, extraSystemContext, write, history, fallbackCredentialIds, signal } = opts
226
+ const { session, message, imagePath, imageUrl, attachedFiles, apiKey, systemPrompt, executionBrief, extraSystemContext, write, history, fallbackCredentialIds, signal } = opts
217
227
  const isHeartbeat = isHeartbeatSource(opts.source)
218
228
  const promptMode: PromptMode = opts.promptMode ?? resolvePromptMode(session)
219
229
  const isMinimalPrompt = promptMode === 'minimal'
@@ -221,7 +231,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
221
231
  const rawExtensions = getEnabledCapabilityIds(session)
222
232
  const hasShellCapability = rawExtensions.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
223
233
  const extensionManager = getExtensionManager()
224
- const sessionExtensions = expandExtensionIds([
234
+ const requestedExtensions = expandExtensionIds([
225
235
  ...rawExtensions,
226
236
  ...(hasShellCapability ? ['process'] : []),
227
237
  ]).filter((id) => !extensionManager.isExplicitlyDisabled(id))
@@ -249,10 +259,15 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
249
259
 
250
260
  // Build agent prompt
251
261
  const settings = loadSettings()
262
+ const toolPolicy = resolveSessionToolPolicy(requestedExtensions, settings)
263
+ const blockedExtensionIds = new Set(expandExtensionIds(toolPolicy.blockedExtensions.map((entry) => entry.tool)))
264
+ const sessionExtensions = expandExtensionIds(toolPolicy.enabledExtensions)
265
+ .filter((id) => !blockedExtensionIds.has(id))
266
+ .filter((id) => !extensionManager.isExplicitlyDisabled(id))
252
267
  const requestedToolPreflightResponse = resolveRequestedToolPreflightResponse({
253
268
  message,
254
269
  enabledExtensions: sessionExtensions,
255
- toolPolicy: resolveSessionToolPolicy(sessionExtensions, settings),
270
+ toolPolicy,
256
271
  appSettings: settings,
257
272
  internal: false,
258
273
  source: 'chat',
@@ -296,6 +311,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
296
311
  // -------------------------------------------------------------------------
297
312
  const promptParts: string[] = []
298
313
  const hasProvidedSystemPrompt = typeof systemPrompt === 'string' && systemPrompt.trim().length > 0
314
+ const hasCanonicalExecutionBrief = Boolean(executionBrief)
299
315
  const currentThreadRecallRequest = isCurrentThreadRecallRequest(message)
300
316
  const hasAttachmentContext = Boolean(
301
317
  imagePath
@@ -347,18 +363,37 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
347
363
  // Composable prompt sections — each builder returns string | null (or string[])
348
364
  const thinkingBlock = buildThinkingSection(agentThinkingLevel, isMinimalPrompt)
349
365
  if (thinkingBlock) promptParts.push(thinkingBlock)
366
+ const { rootSessionId } = resolveSessionLineageIds(session)
350
367
 
351
368
  // Async sections — run concurrently where possible
352
- const [workspaceBlock, awarenessBlock, situationalBlock, extensionAuditBlock] = await Promise.all([
369
+ const [runtimeOrientationBlock, workspaceBlock, awarenessBlock, situationalBlock, extensionAuditBlock] = await Promise.all([
370
+ buildRuntimeOrientationSection({
371
+ session,
372
+ promptMode,
373
+ sessionExtensions,
374
+ toolPolicy,
375
+ agent: sessionAgent,
376
+ activeProjectContext,
377
+ rootSessionId,
378
+ isHeartbeat,
379
+ isConnectorSession,
380
+ }),
353
381
  !hasProvidedSystemPrompt ? buildWorkspaceSection(session, isMinimalPrompt, agentHeartbeatEnabled) : null,
354
382
  buildAgentAwarenessSection(session, sessionExtensions, isMinimalPrompt),
355
383
  buildSituationalSection(session, isMinimalPrompt),
356
384
  buildExtensionAccessAuditSection(sessionExtensions, agentMcpDisabledTools, isMinimalPrompt),
357
385
  ])
386
+ if (runtimeOrientationBlock) promptParts.push(runtimeOrientationBlock)
358
387
  if (workspaceBlock) promptParts.push(workspaceBlock)
359
388
  if (awarenessBlock) promptParts.push(awarenessBlock)
360
389
  if (situationalBlock) promptParts.push(situationalBlock)
361
390
 
391
+ // RunContext — structured working memory that survives compaction
392
+ if (!hasProvidedSystemPrompt && !hasCanonicalExecutionBrief) {
393
+ const runContextBlock = buildRunContextSection(session.runContext, isMinimalPrompt)
394
+ if (runContextBlock) promptParts.push(runContextBlock)
395
+ }
396
+
362
397
  // Extra system context — always included (caller-provided context is always relevant)
363
398
  if (Array.isArray(extraSystemContext)) {
364
399
  for (const block of extraSystemContext) {
@@ -393,6 +428,22 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
393
428
 
394
429
  // Await classification before building the agentic execution policy
395
430
  const classification = await classificationPromise
431
+ const delegationAdvisory = sessionAgent && agentDelegationEnabled
432
+ ? resolveDelegationAdvisory({
433
+ currentAgent: sessionAgent,
434
+ agents: listAgents(),
435
+ profile: buildDelegationTaskProfile({ classification }),
436
+ delegationTargetMode: agentDelegationTargetMode,
437
+ delegationTargetAgentIds: agentDelegationTargetAgentIds,
438
+ })
439
+ : null
440
+ const delegationRecommendationBlock = !isMinimalPrompt
441
+ ? buildDelegationRecommendationSection({
442
+ agent: sessionAgent,
443
+ classification,
444
+ })
445
+ : null
446
+ if (delegationRecommendationBlock) promptParts.push(delegationRecommendationBlock)
396
447
 
397
448
  promptParts.push(
398
449
  buildAgenticExecutionPolicy({
@@ -813,7 +864,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
813
864
  // -------------------------------------------------------------------------
814
865
  const state = new ChatTurnState()
815
866
  const limits = new ContinuationLimits(isConnectorSession, isHeartbeat)
816
- const routingDecision = routeTaskIntent(message, sessionExtensions, null)
867
+ const routingDecision = routeTaskIntent(message, sessionExtensions, null, classification)
817
868
  const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionExtensions)
818
869
 
819
870
  const boundedExternalExecutionTask = classifiedHasTransactionalWalletIntent(classification, message)
@@ -1071,6 +1122,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
1071
1122
  likelyResearchSynthesisTask,
1072
1123
  abortControllerAborted: abortController.signal.aborted,
1073
1124
  classification,
1125
+ delegationEnabled: agentDelegationEnabled,
1126
+ delegationPreferenceActive: delegationAdvisory?.shouldDelegate === true,
1074
1127
  })
1075
1128
  shouldContinue = decision.type
1076
1129
  if (decision.requiredToolReminderNames.length > 0) {
@@ -1123,6 +1176,9 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
1123
1176
  cwd: session.cwd,
1124
1177
  frequencyLimitedToolName,
1125
1178
  sessionExtensions,
1179
+ isCoordinatorAgent,
1180
+ recommendedDelegateName: delegationAdvisory?.recommended?.agentName || null,
1181
+ delegationRationale: formatDelegationRationale(delegationAdvisory?.recommended),
1126
1182
  })
1127
1183
 
1128
1184
  if (continuationPrompt) {
@@ -290,5 +290,41 @@ describe('stream-continuation', () => {
290
290
  assert.equal(typeof prompt, 'string')
291
291
  assert.ok(prompt!.includes('memory write'))
292
292
  })
293
+
294
+ it('renders a coordinator delegation nudge with the recommended delegate', () => {
295
+ const prompt = mod.buildContinuationPrompt({
296
+ type: 'coordinator_delegation_nudge',
297
+ message: 'test',
298
+ fullText: '',
299
+ toolEvents: [],
300
+ requiredToolReminderNames: [],
301
+ isCoordinatorAgent: true,
302
+ recommendedDelegateName: 'Builder',
303
+ delegationRationale: 'capability match: coding; worker role fits execution-heavy work',
304
+ })
305
+
306
+ assert.ok(prompt)
307
+ assert.ok(prompt!.includes('Builder'))
308
+ assert.ok(prompt!.includes('orchestrate'))
309
+ assert.ok(prompt!.includes('Reason: capability match: coding; worker role fits execution-heavy work.'))
310
+ })
311
+
312
+ it('renders an advisory delegation nudge for non-coordinator agents', () => {
313
+ const prompt = mod.buildContinuationPrompt({
314
+ type: 'coordinator_delegation_nudge',
315
+ message: 'test',
316
+ fullText: '',
317
+ toolEvents: [],
318
+ requiredToolReminderNames: [],
319
+ isCoordinatorAgent: false,
320
+ recommendedDelegateName: 'Reviewer',
321
+ delegationRationale: 'capability match: review; currently idle',
322
+ })
323
+
324
+ assert.ok(prompt)
325
+ assert.ok(prompt!.includes('materially better fit'))
326
+ assert.ok(prompt!.includes('Reviewer'))
327
+ assert.ok(prompt!.includes('reconnaissance, validation, or synthesis'))
328
+ })
293
329
  })
294
330
  })
@@ -8,7 +8,7 @@
8
8
  import fs from 'node:fs'
9
9
  import os from 'node:os'
10
10
  import path from 'node:path'
11
- import type { MessageToolEvent } from '@/types'
11
+ import type { Message, MessageToolEvent } from '@/types'
12
12
  import { extractSuggestions } from '@/lib/server/suggestions'
13
13
  import { isSuccessfulMemoryMutationToolEvent } from '@/lib/server/chat-execution/memory-mutation-tools'
14
14
  import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
@@ -108,7 +108,7 @@ function looksLikeIncompleteDeliverableResponse(text: string): boolean {
108
108
  }
109
109
 
110
110
  function hasRecentDeliverableContext(
111
- history: Array<{ role?: string; text?: string }> | undefined,
111
+ history: Message[] | undefined,
112
112
  userMessage: string,
113
113
  ): boolean {
114
114
  if (!Array.isArray(history) || history.length === 0) return false
@@ -116,7 +116,7 @@ function hasRecentDeliverableContext(
116
116
  if (!trimmed || trimmed.length > 160) return false
117
117
  return history
118
118
  .slice(-8)
119
- .some((entry) => entry.role === 'user' && looksLikeOpenEndedDeliverableTask((entry.text || '').trim()))
119
+ .some((entry) => entry.role === 'user' && entry.semantics?.isDeliverableTask === true)
120
120
  }
121
121
 
122
122
  const ARTIFACT_PATH_EXT_RE = /\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|ts|tsx|js|jsx|mjs|cjs|py|sql|sh)$/i
@@ -245,7 +245,7 @@ export function shouldForceExternalExecutionFollowthrough(params: {
245
245
  toolEvents: MessageToolEvent[]
246
246
  classification?: MessageClassification | null
247
247
  }): boolean {
248
- const isTransactional = params.classification ? params.classification.walletIntent === 'transactional' : looksLikeBoundedExternalExecutionTask(params.userMessage)
248
+ const isTransactional = params.classification?.walletIntent === 'transactional'
249
249
  if (!isTransactional) return false
250
250
  if (!params.hasToolCalls || params.toolEvents.length < 4) return false
251
251
  if (hasStateChangingWalletEvidence(params.toolEvents)) return false
@@ -266,7 +266,7 @@ export function shouldForceExternalExecutionKickoffFollowthrough(params: {
266
266
  toolEvents: MessageToolEvent[]
267
267
  classification?: MessageClassification | null
268
268
  }): boolean {
269
- const isTransactional = params.classification ? params.classification.walletIntent === 'transactional' : looksLikeBoundedExternalExecutionTask(params.userMessage)
269
+ const isTransactional = params.classification?.walletIntent === 'transactional'
270
270
  if (!isTransactional) return false
271
271
  if (params.hasToolCalls || params.toolEvents.length > 0) return false
272
272
 
@@ -289,11 +289,11 @@ export function shouldForceDeliverableFollowthrough(params: {
289
289
  hasToolCalls: boolean
290
290
  toolEvents: MessageToolEvent[]
291
291
  cwd?: string
292
- history?: Array<{ role?: string; text?: string }>
292
+ history?: Message[]
293
293
  classification?: MessageClassification | null
294
294
  }): boolean {
295
295
  const recentDeliverableContext = hasRecentDeliverableContext(params.history, params.userMessage)
296
- const isDeliverable = params.classification ? params.classification.isDeliverableTask : looksLikeOpenEndedDeliverableTask(params.userMessage)
296
+ const isDeliverable = params.classification?.isDeliverableTask === true
297
297
  const deliverableIntent = isDeliverable || recentDeliverableContext
298
298
  const requestedArtifacts = getRequestedArtifactStatus({
299
299
  userMessage: params.userMessage,
@@ -744,6 +744,9 @@ export function buildContinuationPrompt(params: {
744
744
  cwd?: string
745
745
  frequencyLimitedToolName?: string
746
746
  sessionExtensions?: string[]
747
+ isCoordinatorAgent?: boolean
748
+ recommendedDelegateName?: string | null
749
+ delegationRationale?: string | null
747
750
  }): string | null {
748
751
  switch (params.type) {
749
752
  case 'memory_write_followthrough':
@@ -819,12 +822,24 @@ export function buildContinuationPrompt(params: {
819
822
  ].filter(Boolean).join('\n')
820
823
 
821
824
  case 'coordinator_delegation_nudge':
822
- return [
823
- 'IMPORTANT: You have specialist workers available but you have been doing substantial work directly with tools.',
824
- 'You MUST delegate the remaining work via `spawn_subagent` to the appropriate specialist worker NOW.',
825
- 'As a coordinator, your job is to orchestrate — not to do the work yourself. Direct tool use is only for quick lookups and validation.',
826
- 'Review the workers listed in your system prompt and delegate immediately.',
827
- ].join('\n')
825
+ return params.isCoordinatorAgent
826
+ ? [
827
+ 'IMPORTANT: You have specialist workers available but you have been doing substantial work directly with tools.',
828
+ params.recommendedDelegateName
829
+ ? `Delegate the remaining execution via \`spawn_subagent\` to ${params.recommendedDelegateName} now.`
830
+ : 'You MUST delegate the remaining work via `spawn_subagent` to the appropriate specialist worker NOW.',
831
+ 'As a coordinator, your job is to orchestrate — not to do the work yourself. Direct tool use is only for quick lookups and validation.',
832
+ params.delegationRationale ? `Reason: ${params.delegationRationale}.` : '',
833
+ 'Review the workers listed in your system prompt and delegate immediately.',
834
+ ].filter(Boolean).join('\n')
835
+ : [
836
+ 'You have delegation available and a teammate is a materially better fit for the remaining work.',
837
+ params.recommendedDelegateName
838
+ ? `Use \`spawn_subagent\` to hand the execution to ${params.recommendedDelegateName} now.`
839
+ : 'Use `spawn_subagent` to hand the execution to the best-fit teammate now.',
840
+ params.delegationRationale ? `Reason: ${params.delegationRationale}.` : '',
841
+ 'Keep your direct tool use to reconnaissance, validation, or synthesis unless delegation is blocked.',
842
+ ].filter(Boolean).join('\n')
828
843
 
829
844
  case 'loop_recovery': {
830
845
  const freqTool = params.frequencyLimitedToolName
@@ -1,5 +1,5 @@
1
1
  import type { Chatroom, Agent } from '@/types'
2
- import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
2
+ import { patchChatroom } from '@/lib/server/chatrooms/chatroom-repository'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
 
5
5
  /**
@@ -35,25 +35,33 @@ export function isImplicitlyMentioned(text: string, agent: Agent): boolean {
35
35
  * Useful for acknowledging tasks or agreeing with teammates.
36
36
  */
37
37
  export function addAgentReaction(chatroomId: string, messageId: string, agentId: string, emoji: string) {
38
- const chatrooms = loadChatrooms()
39
- const chatroom = chatrooms[chatroomId] as Chatroom | undefined
40
- if (!chatroom) return
38
+ const updated = patchChatroom(chatroomId, (current) => {
39
+ const chatroom = current as Chatroom | null
40
+ if (!chatroom) return null
41
+ const message = chatroom.messages.find(m => m.id === messageId)
42
+ if (!message) return chatroom
43
+ if (message.reactions.some(r => r.reactorId === agentId && r.emoji === emoji)) return chatroom
41
44
 
42
- const message = chatroom.messages.find(m => m.id === messageId)
43
- if (!message) return
44
-
45
- // Prevent duplicate reactions from the same agent
46
- if (message.reactions.some(r => r.reactorId === agentId && r.emoji === emoji)) return
47
-
48
- message.reactions.push({
49
- emoji,
50
- reactorId: agentId,
51
- time: Date.now()
45
+ return {
46
+ ...chatroom,
47
+ messages: chatroom.messages.map((entry) => (
48
+ entry.id !== messageId
49
+ ? entry
50
+ : {
51
+ ...entry,
52
+ reactions: [
53
+ ...entry.reactions,
54
+ {
55
+ emoji,
56
+ reactorId: agentId,
57
+ time: Date.now(),
58
+ },
59
+ ],
60
+ }
61
+ )),
62
+ }
52
63
  })
53
-
54
- chatrooms[chatroomId] = chatroom
55
- saveChatrooms(chatrooms)
56
- notify(`chatroom:${chatroomId}`)
64
+ if (updated) notify(`chatroom:${chatroomId}`)
57
65
  }
58
66
 
59
67
  /**