@swarmclawai/swarmclaw 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +16 -85
  2. package/bin/server-cmd.js +64 -1
  3. package/package.json +2 -2
  4. package/skills/coding-agent/SKILL.md +111 -0
  5. package/skills/github/SKILL.md +140 -0
  6. package/skills/nano-banana-pro/SKILL.md +62 -0
  7. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  8. package/skills/nano-pdf/SKILL.md +53 -0
  9. package/skills/openai-image-gen/SKILL.md +78 -0
  10. package/skills/openai-image-gen/scripts/gen.py +328 -0
  11. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  12. package/skills/skill-creator/SKILL.md +147 -0
  13. package/skills/skill-creator/scripts/init_skill.py +378 -0
  14. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  15. package/skills/summarize/SKILL.md +77 -0
  16. package/src/app/api/auth/route.ts +20 -5
  17. package/src/app/api/chats/[id]/devserver/route.ts +13 -19
  18. package/src/app/api/chats/[id]/messages/route.ts +13 -15
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/ip/route.ts +2 -2
  24. package/src/app/api/preview-server/route.ts +1 -1
  25. package/src/app/api/projects/[id]/route.ts +7 -46
  26. package/src/cli/server-cmd.test.js +74 -0
  27. package/src/components/chat/chat-area.tsx +45 -23
  28. package/src/components/chat/message-bubble.test.ts +35 -0
  29. package/src/components/chat/message-bubble.tsx +19 -9
  30. package/src/components/chat/message-list.tsx +37 -3
  31. package/src/components/input/chat-input.tsx +34 -14
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
  33. package/src/instrumentation.ts +1 -1
  34. package/src/lib/chat/assistant-render-id.ts +3 -0
  35. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  36. package/src/lib/chat/chat-streaming-state.ts +20 -8
  37. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  38. package/src/lib/chat/queued-message-queue.ts +11 -2
  39. package/src/lib/providers/cli-utils.test.ts +124 -0
  40. package/src/lib/server/activity/activity-log.ts +21 -0
  41. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  42. package/src/lib/server/agents/agent-cascade.ts +79 -59
  43. package/src/lib/server/agents/agent-registry.ts +3 -1
  44. package/src/lib/server/agents/agent-repository.ts +90 -0
  45. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  46. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  47. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  48. package/src/lib/server/agents/guardian.ts +2 -2
  49. package/src/lib/server/agents/main-agent-loop.ts +10 -3
  50. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  51. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  52. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  53. package/src/lib/server/agents/task-session.ts +3 -4
  54. package/src/lib/server/approvals/approval-repository.ts +30 -0
  55. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  56. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  57. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  58. package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
  59. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  60. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  61. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  62. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  63. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  64. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  65. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  66. package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
  67. package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
  68. package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
  69. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  70. package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
  71. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  72. package/src/lib/server/connectors/connector-repository.ts +58 -0
  73. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  74. package/src/lib/server/credentials/credential-repository.ts +7 -0
  75. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  76. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  77. package/src/lib/server/missions/mission-repository.ts +74 -0
  78. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  79. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  80. package/src/lib/server/missions/mission-service/context.ts +4 -0
  81. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  82. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  83. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  84. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  85. package/src/lib/server/missions/mission-service.test.ts +9 -2
  86. package/src/lib/server/missions/mission-service.ts +6 -2266
  87. package/src/lib/server/openclaw/deploy.test.ts +42 -3
  88. package/src/lib/server/openclaw/deploy.ts +26 -12
  89. package/src/lib/server/persistence/repository-utils.ts +154 -0
  90. package/src/lib/server/persistence/storage-context.ts +51 -0
  91. package/src/lib/server/persistence/transaction.ts +1 -0
  92. package/src/lib/server/projects/project-repository.ts +36 -0
  93. package/src/lib/server/projects/project-service.ts +79 -0
  94. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  95. package/src/lib/server/runtime/alert-dispatch.ts +1 -1
  96. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  97. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  98. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  99. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  100. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  101. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  102. package/src/lib/server/runtime/daemon-state.ts +3 -1470
  103. package/src/lib/server/runtime/estop-repository.ts +4 -0
  104. package/src/lib/server/runtime/estop.ts +3 -1
  105. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  106. package/src/lib/server/runtime/heartbeat-service.ts +55 -34
  107. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  108. package/src/lib/server/runtime/idle-window.ts +2 -2
  109. package/src/lib/server/runtime/network.ts +11 -0
  110. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  111. package/src/lib/server/runtime/queue/claims.ts +4 -0
  112. package/src/lib/server/runtime/queue/core.ts +2079 -0
  113. package/src/lib/server/runtime/queue/execution.ts +7 -0
  114. package/src/lib/server/runtime/queue/followups.ts +4 -0
  115. package/src/lib/server/runtime/queue/queries.ts +12 -0
  116. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  117. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  118. package/src/lib/server/runtime/queue-repository.ts +17 -0
  119. package/src/lib/server/runtime/queue.ts +5 -2061
  120. package/src/lib/server/runtime/run-ledger.ts +6 -5
  121. package/src/lib/server/runtime/run-repository.ts +73 -0
  122. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  123. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  124. package/src/lib/server/runtime/runtime-state.ts +99 -0
  125. package/src/lib/server/runtime/scheduler.ts +4 -2
  126. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  127. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  128. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  129. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  130. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  131. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  132. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  133. package/src/lib/server/runtime/session-run-manager.ts +72 -1377
  134. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  135. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  136. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  137. package/src/lib/server/sessions/session-repository.ts +85 -0
  138. package/src/lib/server/settings/settings-repository.ts +25 -0
  139. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  140. package/src/lib/server/skills/skill-discovery.ts +2 -2
  141. package/src/lib/server/skills/skill-repository.ts +14 -0
  142. package/src/lib/server/storage.ts +13 -24
  143. package/src/lib/server/tasks/task-repository.ts +54 -0
  144. package/src/lib/server/usage/usage-repository.ts +30 -0
  145. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  146. package/src/lib/strip-internal-metadata.test.ts +42 -41
  147. package/src/stores/use-chat-store.test.ts +54 -0
  148. package/src/stores/use-chat-store.ts +21 -5
  149. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -0,0 +1,817 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+
4
+ import { getProvider } from '@/lib/providers'
5
+ import type { Message, Session } from '@/types'
6
+ import {
7
+ decryptKey,
8
+ loadCredentials,
9
+ } from '@/lib/server/credentials/credential-repository'
10
+ import { getAgent } from '@/lib/server/agents/agent-repository'
11
+ import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
12
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
13
+ import { loadSkills } from '@/lib/server/skills/skill-repository'
14
+ import { resolveImagePath } from '@/lib/server/resolve-image'
15
+ import { resolveSessionToolPolicy } from '@/lib/server/tool-capability-policy'
16
+ import { listUniversalToolAccessExtensionIds } from '@/lib/server/universal-tool-access'
17
+ import {
18
+ buildAgentDisabledMessage,
19
+ isAgentDisabled,
20
+ } from '@/lib/server/agents/agent-availability'
21
+ import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
22
+ import { buildWorkspaceContext } from '@/lib/server/workspace-context'
23
+ import {
24
+ buildRuntimeSkillPromptBlocks,
25
+ resolveRuntimeSkills,
26
+ } from '@/lib/server/skills/runtime-skill-resolver'
27
+ import {
28
+ applyResolvedRoute,
29
+ resolvePrimaryAgentRoute,
30
+ } from '@/lib/server/agents/agent-runtime-config'
31
+ import {
32
+ runCapabilityBeforeMessageWrite,
33
+ runCapabilityBeforeModelResolve,
34
+ runCapabilityHook,
35
+ runCapabilityToolResultPersist,
36
+ transformCapabilityText,
37
+ collectCapabilityDescriptions,
38
+ collectCapabilityOperatingGuidance,
39
+ } from '@/lib/server/native-capabilities'
40
+ import {
41
+ getEnabledCapabilityIds,
42
+ getEnabledCapabilitySelection,
43
+ splitCapabilityIds,
44
+ } from '@/lib/capability-selection'
45
+ import { normalizeProviderEndpoint, isLocalOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
46
+ import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
47
+ import {
48
+ buildMissionContextBlock,
49
+ resolveMissionForTurn,
50
+ } from '@/lib/server/missions/mission-service'
51
+ import {
52
+ bridgeHumanReplyFromChat,
53
+ } from '@/lib/server/chatrooms/session-mailbox'
54
+ import { runLinkUnderstanding } from '@/lib/server/link-understanding'
55
+ import {
56
+ guardUntrustedText,
57
+ guardUntrustedToolEvents,
58
+ getUntrustedContentGuardMode,
59
+ } from '@/lib/server/untrusted-content'
60
+ import {
61
+ buildIdentityContinuityContext,
62
+ } from '@/lib/server/identity-continuity'
63
+ import {
64
+ resolveEffectiveSessionMemoryScopeMode,
65
+ } from '@/lib/server/memory/session-memory-scope'
66
+ import { syncSessionArchiveMemory } from '@/lib/server/memory/session-archive-memory'
67
+ import {
68
+ evaluateSessionFreshness,
69
+ resetSessionRuntime,
70
+ resolveSessionResetPolicy,
71
+ } from '@/lib/server/session-reset-policy'
72
+ import { checkAgentBudgetLimits } from '@/lib/server/cost'
73
+ import {
74
+ filterRuntimeCapabilityIds,
75
+ getTodaySpendUsd,
76
+ parseUsdLimit,
77
+ shouldApplySessionFreshnessReset,
78
+ shouldPersistInboundUserMessage,
79
+ } from '@/lib/server/chat-execution/chat-execution-utils'
80
+ import { loadEstopState } from '@/lib/server/runtime/estop'
81
+ import { buildToolSection, joinPromptSegments } from '@/lib/server/chat-execution/prompt-builder'
82
+ import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
83
+ import type { ExecuteChatTurnInput } from '@/lib/server/chat-execution/chat-execution'
84
+
85
+ export function buildAgentRuntimeCapabilities(enabledExtensions: string[]): string[] {
86
+ const capabilities = ['heartbeats', 'autonomous_loop', 'multi_agent_chat']
87
+ if (enabledExtensions.length > 0) capabilities.unshift('tools')
88
+ return capabilities
89
+ }
90
+
91
+ export function buildNoToolsGuidance(): string[] {
92
+ return [
93
+ '## Tool Availability',
94
+ 'No runtime tools are available in this chat after policy filtering.',
95
+ 'Do not imply that a normal read-only action is waiting on user permission when the real blocker is missing tool access.',
96
+ 'If browsing, web fetches, file edits, or other actions are unavailable, state that the capability is blocked by runtime policy in this session.',
97
+ 'Only mention confirmation or approval when a real runtime tool explicitly returned that boundary for a concrete action.',
98
+ ]
99
+ }
100
+
101
+ export function buildEnabledToolsAutonomyGuidance(): string[] {
102
+ return [
103
+ '## Tool Autonomy',
104
+ 'Runtime tools are already available for normal use in this chat.',
105
+ 'Do not request that a tool be enabled or switched on before using it.',
106
+ 'Do not ask the user for permission before using enabled tools for ordinary read-only work, routine diagnostics, or reversible execution steps that are clearly part of the request.',
107
+ 'If the user asks you to use an enabled tool or to perform a task that clearly maps to an enabled tool, attempt that tool path before asking the user to do the work manually.',
108
+ 'If the task depends on current or external information and web tools are enabled, use them instead of answering from stale memory.',
109
+ 'If the task asks for a file, report, dashboard, JSON, or other workspace artifact to be saved, use file-writing or shell tools to actually create it and mention the resulting path.',
110
+ 'If the task asks you to inspect the local repository, runtime, or filesystem state, use shell or file tools instead of guessing.',
111
+ 'Treat capability policy blocks and explicit platform feature gates as the real boundaries. Do not invent an approval queue when none exists.',
112
+ 'When asked to create a file in a format you don\'t have a dedicated tool for (PDF, image, spreadsheet, etc.), check available skills first, then use shell tools to install and run a CLI tool that handles it.',
113
+ 'If no skill or tool exists for a task, write a script and run it with shell tools. Install packages with pip/npm/brew as needed.',
114
+ 'Never say "I can\'t do that" or "I don\'t have a tool for that" when shell tools are available. Attempt a code-based approach first. Only report inability after genuinely trying and failing.',
115
+ 'When you solve a novel task with code or shell, consider using the extension_creator tool to save the solution as a reusable extension.',
116
+ ]
117
+ }
118
+
119
+ export type PersistPhase = 'user' | 'system' | 'assistant_partial' | 'assistant_final' | 'heartbeat'
120
+
121
+ export async function applyMessageLifecycleHooks(params: {
122
+ session: Session
123
+ message: Message
124
+ enabledIds: string[]
125
+ phase: PersistPhase
126
+ runId?: string
127
+ isSynthetic?: boolean
128
+ }): Promise<Message | null> {
129
+ let currentMessage = params.message
130
+ const guardMode = getUntrustedContentGuardMode(loadSettings())
131
+ if (Array.isArray(currentMessage.toolEvents) && currentMessage.toolEvents.length > 0) {
132
+ currentMessage = {
133
+ ...currentMessage,
134
+ toolEvents: guardUntrustedToolEvents({
135
+ toolEvents: currentMessage.toolEvents,
136
+ mode: guardMode,
137
+ }),
138
+ }
139
+ }
140
+ const toolEvents = Array.isArray(currentMessage.toolEvents)
141
+ ? currentMessage.toolEvents.filter((event) => typeof event.output === 'string' || event.error === true)
142
+ : []
143
+
144
+ for (const event of toolEvents) {
145
+ currentMessage = await runCapabilityToolResultPersist(
146
+ {
147
+ session: params.session,
148
+ message: currentMessage,
149
+ toolName: event.name,
150
+ toolCallId: event.toolCallId,
151
+ isSynthetic: params.isSynthetic,
152
+ },
153
+ { enabledIds: params.enabledIds },
154
+ )
155
+ }
156
+
157
+ const writeResult = await runCapabilityBeforeMessageWrite(
158
+ {
159
+ session: params.session,
160
+ message: currentMessage,
161
+ phase: params.phase,
162
+ runId: params.runId,
163
+ },
164
+ { enabledIds: params.enabledIds },
165
+ )
166
+
167
+ if (writeResult.block) return null
168
+ return writeResult.message
169
+ }
170
+
171
+ interface SessionWithCredentials {
172
+ credentialId?: string | null
173
+ }
174
+
175
+ interface ProviderApiKeyConfig {
176
+ requiresApiKey?: boolean
177
+ optionalApiKey?: boolean
178
+ }
179
+
180
+ function joinSystemPromptBlocks(...blocks: Array<string | null | undefined>): string | undefined {
181
+ const joined = joinPromptSegments(...blocks)
182
+ return joined || undefined
183
+ }
184
+
185
+ function syncSessionFromAgent(sessionId: string): void {
186
+ const session = getSession(sessionId)
187
+ if (!session?.agentId) return
188
+ const agent = getAgent(session.agentId)
189
+ if (!agent) return
190
+
191
+ let changed = false
192
+ const route = resolvePrimaryAgentRoute(agent, undefined, {
193
+ preferredGatewayTags: session.routePreferredGatewayTags || [],
194
+ preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
195
+ })
196
+ if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
197
+ if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
198
+ session.model = agent.model
199
+ changed = true
200
+ }
201
+ if (route) {
202
+ const resolved = applyResolvedRoute({ ...session }, route)
203
+ if (session.provider !== resolved.provider) { session.provider = resolved.provider; changed = true }
204
+ if (session.model !== resolved.model) { session.model = resolved.model; changed = true }
205
+ if ((session.credentialId || null) !== (resolved.credentialId || null)) {
206
+ session.credentialId = resolved.credentialId ?? null
207
+ changed = true
208
+ }
209
+ if (JSON.stringify(session.fallbackCredentialIds || []) !== JSON.stringify(resolved.fallbackCredentialIds || [])) {
210
+ session.fallbackCredentialIds = [...(resolved.fallbackCredentialIds || [])]
211
+ changed = true
212
+ }
213
+ if ((session.apiEndpoint || null) !== (resolved.apiEndpoint || null)) {
214
+ session.apiEndpoint = resolved.apiEndpoint ?? null
215
+ changed = true
216
+ }
217
+ if ((session.gatewayProfileId || null) !== (resolved.gatewayProfileId || null)) {
218
+ session.gatewayProfileId = resolved.gatewayProfileId ?? null
219
+ changed = true
220
+ }
221
+ } else {
222
+ if (session.credentialId === undefined && agent.credentialId !== undefined) {
223
+ session.credentialId = agent.credentialId ?? null
224
+ changed = true
225
+ }
226
+ if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
227
+ const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
228
+ if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
229
+ }
230
+ }
231
+ const agentSelection = getEnabledCapabilitySelection(agent)
232
+ if (!session.parentSessionId) {
233
+ const currentSelection = getEnabledCapabilitySelection(session)
234
+ if (
235
+ JSON.stringify(currentSelection.tools) !== JSON.stringify(agentSelection.tools)
236
+ || JSON.stringify(currentSelection.extensions) !== JSON.stringify(agentSelection.extensions)
237
+ ) {
238
+ session.tools = agentSelection.tools
239
+ session.extensions = agentSelection.extensions
240
+ changed = true
241
+ }
242
+ }
243
+ const desiredMemoryScopeMode = resolveEffectiveSessionMemoryScopeMode(session, agent.memoryScopeMode ?? null)
244
+ if ((((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? null) !== desiredMemoryScopeMode) {
245
+ ;(session as unknown as Record<string, unknown>).memoryScopeMode = desiredMemoryScopeMode
246
+ changed = true
247
+ }
248
+ const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
249
+ if (isShortcutChat) {
250
+ const desiredSelection = agentSelection
251
+ const currentShortcutSelection = getEnabledCapabilitySelection(session)
252
+ if (
253
+ JSON.stringify(currentShortcutSelection.tools) !== JSON.stringify(desiredSelection.tools)
254
+ || JSON.stringify(currentShortcutSelection.extensions) !== JSON.stringify(desiredSelection.extensions)
255
+ ) {
256
+ session.tools = desiredSelection.tools
257
+ session.extensions = desiredSelection.extensions
258
+ changed = true
259
+ }
260
+ if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
261
+ if (session.name !== agent.name) { session.name = agent.name; changed = true }
262
+ const desiredHeartbeatEnabled = agent.heartbeatEnabled ?? false
263
+ if ((session.heartbeatEnabled ?? false) !== desiredHeartbeatEnabled) {
264
+ session.heartbeatEnabled = desiredHeartbeatEnabled
265
+ changed = true
266
+ }
267
+ const desiredHeartbeatIntervalSec = agent.heartbeatIntervalSec ?? null
268
+ if ((session.heartbeatIntervalSec ?? null) !== desiredHeartbeatIntervalSec) {
269
+ session.heartbeatIntervalSec = desiredHeartbeatIntervalSec
270
+ changed = true
271
+ }
272
+ const desiredMemoryTierPreference = agent.memoryTierPreference ?? null
273
+ if ((((session as unknown as Record<string, unknown>).memoryTierPreference as string | null | undefined) ?? null) !== desiredMemoryTierPreference) {
274
+ ;(session as unknown as Record<string, unknown>).memoryTierPreference = desiredMemoryTierPreference
275
+ changed = true
276
+ }
277
+ const desiredProjectId = agent.projectId ?? null
278
+ if ((session.projectId ?? null) !== desiredProjectId) {
279
+ session.projectId = desiredProjectId
280
+ changed = true
281
+ }
282
+ const desiredOpenClawAgentId = agent.openclawAgentId ?? null
283
+ if ((session.openclawAgentId ?? null) !== desiredOpenClawAgentId) {
284
+ session.openclawAgentId = desiredOpenClawAgentId
285
+ changed = true
286
+ }
287
+ if (session.connectorContext) {
288
+ session.connectorContext = undefined
289
+ changed = true
290
+ }
291
+ }
292
+
293
+ if (changed) {
294
+ saveSession(sessionId, session)
295
+ }
296
+ }
297
+
298
+ function buildLightHeartbeatSystemPrompt(session: Session): string | undefined {
299
+ if (!session.agentId) return undefined
300
+ const agent = getAgent(session.agentId)
301
+ if (!agent) return undefined
302
+
303
+ const parts: string[] = []
304
+ parts.push(`## Identity\nName: ${agent.name}`)
305
+ if (agent.description) parts.push(`Description: ${agent.description}`)
306
+ parts.push(buildCurrentDateTimePromptContext())
307
+ if (agent.soul) parts.push(`## Soul\n${agent.soul.slice(0, 300)}`)
308
+ parts.push([
309
+ '## Heartbeats',
310
+ 'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
311
+ ].join('\n'))
312
+ return parts.join('\n\n')
313
+ }
314
+
315
+ function buildAgentSystemPrompt(session: Session): string | undefined {
316
+ if (!session.agentId) return undefined
317
+ const agent = getAgent(session.agentId)
318
+ if (!agent) return undefined
319
+
320
+ const settings = loadSettings()
321
+ const allowSilentReplies = isDirectConnectorSession(session)
322
+ const parts: string[] = []
323
+ const enabledExtensions = listUniversalToolAccessExtensionIds(
324
+ getEnabledCapabilityIds(session).length > 0 ? getEnabledCapabilityIds(session) : getEnabledCapabilityIds(agent),
325
+ )
326
+
327
+ const identityLines = ['## My Identity']
328
+ identityLines.push(`Name: ${agent.name}`)
329
+ if (agent.emoji) identityLines.push(`Emoji: ${agent.emoji}`)
330
+ if (agent.creature) identityLines.push(`Creature: ${agent.creature}`)
331
+ if (agent.vibe) identityLines.push(`Vibe: ${agent.vibe}`)
332
+ if (agent.theme) identityLines.push(`Theme: ${agent.theme}`)
333
+ if (agent.description) identityLines.push(`Description: ${agent.description}`)
334
+ identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
335
+ parts.push(identityLines.join('\n'))
336
+ const continuityBlock = buildIdentityContinuityContext(session, agent)
337
+ if (continuityBlock) parts.push(continuityBlock)
338
+
339
+ const runtimeLines = [
340
+ '## Runtime',
341
+ `os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${session.provider} | model=${session.model}`,
342
+ `capabilities=${buildAgentRuntimeCapabilities(enabledExtensions).join(',')}`,
343
+ 'tool_access=universal',
344
+ ]
345
+ parts.push(runtimeLines.join('\n'))
346
+
347
+ if (typeof settings.userPrompt === 'string' && settings.userPrompt.trim()) parts.push(`## User Instructions\n${settings.userPrompt}`)
348
+ parts.push(buildCurrentDateTimePromptContext())
349
+
350
+ if (agent.soul) parts.push(`## Soul\n${agent.soul}`)
351
+ if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
352
+
353
+ try {
354
+ const runtimeSkills = resolveRuntimeSkills({
355
+ cwd: session.cwd,
356
+ enabledExtensions,
357
+ agentId: agent.id,
358
+ sessionId: session.id,
359
+ userId: session.user,
360
+ agentSkillIds: agent.skillIds || [],
361
+ storedSkills: loadSkills(),
362
+ selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
363
+ })
364
+ parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
365
+ } catch {
366
+ // Runtime skills are non-critical during prompt assembly.
367
+ }
368
+
369
+ try {
370
+ const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
371
+ if (wsCtx.block) parts.push(wsCtx.block)
372
+ } catch {
373
+ // Workspace context is non-critical.
374
+ }
375
+
376
+ const thinkingHint = [
377
+ '## Output Format',
378
+ 'If your model supports internal reasoning/thinking, put all internal analysis inside <think>...</think> tags.',
379
+ 'Your final response to the user should be clear and concise.',
380
+ allowSilentReplies
381
+ ? 'When you truly have nothing to say, respond with ONLY: NO_MESSAGE'
382
+ : 'For direct user chats, always send a visible reply. Never answer with NO_MESSAGE or HEARTBEAT_OK unless this is an explicit heartbeat poll.',
383
+ ]
384
+ parts.push(thinkingHint.join('\n'))
385
+
386
+ if (enabledExtensions.length === 0) {
387
+ parts.push(buildNoToolsGuidance().join('\n'))
388
+ } else {
389
+ parts.push(buildEnabledToolsAutonomyGuidance().join('\n'))
390
+ }
391
+ const toolSectionLines = buildToolSection(enabledExtensions)
392
+ if (toolSectionLines.length > 0) parts.push(['## Tool Discipline', ...toolSectionLines].join('\n'))
393
+ const operatingGuidance = collectCapabilityOperatingGuidance(enabledExtensions)
394
+ if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
395
+ const capabilityLines = collectCapabilityDescriptions(enabledExtensions)
396
+ if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
397
+
398
+ parts.push([
399
+ '## Heartbeats',
400
+ 'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
401
+ ].join('\n'))
402
+
403
+ return parts.join('\n\n')
404
+ }
405
+
406
+ function resolveApiKeyForSession(session: SessionWithCredentials, provider: ProviderApiKeyConfig): string | null {
407
+ if (provider.requiresApiKey) {
408
+ if (!session.credentialId) throw new Error('No API key configured for this session')
409
+ const creds = loadCredentials()
410
+ const cred = creds[session.credentialId]
411
+ if (!cred) throw new Error('API key not found. Please add one in Settings.')
412
+ return decryptKey(cred.encryptedKey)
413
+ }
414
+ if (provider.optionalApiKey && session.credentialId) {
415
+ const creds = loadCredentials()
416
+ const cred = creds[session.credentialId]
417
+ if (cred) {
418
+ try { return decryptKey(cred.encryptedKey) } catch { return null }
419
+ }
420
+ }
421
+ return null
422
+ }
423
+
424
+ export interface PreparedBlockedChatTurn {
425
+ kind: 'blocked'
426
+ sessionId: string
427
+ session: Session
428
+ lifecycleRunId: string
429
+ blockedMessage: string
430
+ internal: boolean
431
+ runId?: string
432
+ syntheticEnabledIds: string[]
433
+ }
434
+
435
+ export interface PreparedExecutableChatTurn {
436
+ kind: 'ready'
437
+ sessionId: string
438
+ message: string
439
+ internal: boolean
440
+ source: string
441
+ runId?: string
442
+ session: Session
443
+ sessionForRun: Session
444
+ appSettings: ReturnType<typeof loadSettings>
445
+ lifecycleRunId: string
446
+ agentForSession: ReturnType<typeof getAgent>
447
+ mission: Awaited<ReturnType<typeof resolveMissionForTurn>>
448
+ missionContextBlock?: string
449
+ extensionsForRun: string[]
450
+ effectiveMessage: string
451
+ providerType: string
452
+ provider: NonNullable<ReturnType<typeof getProvider>>
453
+ apiKey: string | null
454
+ hideAssistantTranscript: boolean
455
+ isHeartbeatRun: boolean
456
+ heartbeatLightContext: boolean
457
+ isAutoRunNoHistory: boolean
458
+ hasExtensions: boolean
459
+ systemPrompt?: string
460
+ resolvedImagePath?: string
461
+ runStartedAt: number
462
+ runMessageStartIndex: number
463
+ toolPolicy: ReturnType<typeof resolveSessionToolPolicy>
464
+ }
465
+
466
+ export type PreparedChatTurn = PreparedBlockedChatTurn | PreparedExecutableChatTurn
467
+
468
+ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<PreparedChatTurn> {
469
+ const estop = loadEstopState()
470
+ if (estop.level === 'all') {
471
+ throw new Error(estop.reason
472
+ ? `Execution is blocked because all estop is engaged: ${estop.reason}`
473
+ : 'Execution is blocked because all estop is engaged.')
474
+ }
475
+
476
+ const { message } = input
477
+ const {
478
+ sessionId,
479
+ imagePath,
480
+ imageUrl,
481
+ attachedFiles,
482
+ missionId: explicitMissionId,
483
+ internal = false,
484
+ runId,
485
+ source = 'chat',
486
+ onEvent,
487
+ } = input
488
+
489
+ const resolvedImagePath = resolveImagePath(imagePath, imageUrl) ?? undefined
490
+
491
+ syncSessionFromAgent(sessionId)
492
+
493
+ const session = getSession(sessionId)
494
+ if (!session) throw new Error(`Session not found: ${sessionId}`)
495
+ session.messages = Array.isArray(session.messages) ? session.messages : []
496
+ const runStartedAt = Date.now()
497
+ const runMessageStartIndex = session.messages.length
498
+
499
+ const appSettings = loadSettings()
500
+ const lifecycleRunId = runId || `${sessionId}:${runStartedAt}`
501
+ const agentForSession = session.agentId ? getAgent(session.agentId) : null
502
+ if (isAgentDisabled(agentForSession)) {
503
+ const blockedMessage = buildAgentDisabledMessage(agentForSession, 'run chats')
504
+ onEvent?.({ t: 'err', text: blockedMessage })
505
+ return {
506
+ kind: 'blocked',
507
+ sessionId,
508
+ session,
509
+ lifecycleRunId,
510
+ blockedMessage,
511
+ internal,
512
+ runId,
513
+ syntheticEnabledIds: getEnabledCapabilityIds(session),
514
+ }
515
+ }
516
+
517
+ const runtimeCapabilityIds = filterRuntimeCapabilityIds(getEnabledCapabilityIds(session), {
518
+ delegationEnabled: agentForSession?.delegationEnabled === true,
519
+ })
520
+ const requestedCapabilityIds = runtimeCapabilityIds.length > 0
521
+ ? listUniversalToolAccessExtensionIds(runtimeCapabilityIds)
522
+ : []
523
+ const toolPolicy = resolveSessionToolPolicy(requestedCapabilityIds, appSettings)
524
+ const isHeartbeatRun = input.internal === true && source === 'heartbeat'
525
+ const isAutonomousInternalRun = internal && source !== 'chat'
526
+ const heartbeatLightContext = isHeartbeatRun && !!input.heartbeatConfig?.lightContext
527
+ const isAutoRunNoHistory = isHeartbeatRun
528
+
529
+ if (shouldApplySessionFreshnessReset(source)) {
530
+ const freshness = evaluateSessionFreshness({
531
+ session,
532
+ policy: resolveSessionResetPolicy({
533
+ session,
534
+ agent: agentForSession,
535
+ settings: appSettings,
536
+ }),
537
+ })
538
+ if (!freshness.fresh) {
539
+ try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* best-effort */ }
540
+ await runCapabilityHook(
541
+ 'sessionEnd',
542
+ {
543
+ sessionId: session.id,
544
+ session,
545
+ messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
546
+ durationMs: Date.now() - (session.createdAt || runStartedAt),
547
+ reason: freshness.reason || 'session_reset',
548
+ },
549
+ { enabledIds: runtimeCapabilityIds },
550
+ )
551
+ resetSessionRuntime(session, freshness.reason || 'session_reset')
552
+ onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
553
+ saveSession(sessionId, session)
554
+ }
555
+ }
556
+ if (isAutonomousInternalRun) {
557
+ try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* best-effort */ }
558
+ }
559
+
560
+ const mission = await resolveMissionForTurn({
561
+ session,
562
+ message,
563
+ source,
564
+ internal,
565
+ runId: lifecycleRunId,
566
+ explicitMissionId: explicitMissionId || null,
567
+ })
568
+ if (mission?.id) {
569
+ session.missionId = mission.id
570
+ }
571
+ const extensionsForRun = toolPolicy.enabledExtensions
572
+ if (runMessageStartIndex === 0) {
573
+ await runCapabilityHook(
574
+ 'sessionStart',
575
+ {
576
+ session,
577
+ resumedFrom: session.parentSessionId || null,
578
+ },
579
+ { enabledIds: extensionsForRun },
580
+ )
581
+ }
582
+ const sessionForRunSelection = splitCapabilityIds(extensionsForRun)
583
+ let sessionForRun = JSON.stringify(runtimeCapabilityIds) === JSON.stringify(extensionsForRun)
584
+ ? session
585
+ : { ...session, tools: sessionForRunSelection.tools, extensions: sessionForRunSelection.extensions }
586
+ if (mission?.id) {
587
+ sessionForRun = {
588
+ ...sessionForRun,
589
+ missionId: mission.id,
590
+ }
591
+ }
592
+ if (agentForSession) {
593
+ const preferredRoute = resolvePrimaryAgentRoute(agentForSession, undefined, {
594
+ preferredGatewayTags: session.routePreferredGatewayTags || [],
595
+ preferredGatewayUseCase: session.routePreferredGatewayUseCase || null,
596
+ })
597
+ if (preferredRoute) {
598
+ sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
599
+ }
600
+ }
601
+ let effectiveMessage = message
602
+
603
+ if (extensionsForRun.length > 0) {
604
+ try {
605
+ effectiveMessage = await transformCapabilityText(
606
+ 'transformInboundMessage',
607
+ { session: sessionForRun, text: message },
608
+ { enabledIds: extensionsForRun },
609
+ )
610
+ } catch {
611
+ effectiveMessage = message
612
+ }
613
+ }
614
+
615
+ if (isHeartbeatRun && input.modelOverride) {
616
+ sessionForRun = { ...sessionForRun, model: input.modelOverride }
617
+ }
618
+ const missionContextBlock = buildMissionContextBlock(mission)
619
+
620
+ if (extensionsForRun.length > 0) {
621
+ const modelResolvePrompt = heartbeatLightContext
622
+ ? (joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock) || '')
623
+ : (joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock) || '')
624
+ const modelResolve = await runCapabilityBeforeModelResolve(
625
+ {
626
+ session: sessionForRun,
627
+ prompt: modelResolvePrompt,
628
+ message: effectiveMessage,
629
+ provider: sessionForRun.provider,
630
+ model: sessionForRun.model,
631
+ apiEndpoint: sessionForRun.apiEndpoint || null,
632
+ },
633
+ { enabledIds: extensionsForRun },
634
+ )
635
+ if (modelResolve) {
636
+ sessionForRun = {
637
+ ...sessionForRun,
638
+ provider: modelResolve.providerOverride ?? sessionForRun.provider,
639
+ model: modelResolve.modelOverride ?? sessionForRun.model,
640
+ ...(modelResolve.apiEndpointOverride !== undefined ? { apiEndpoint: modelResolve.apiEndpointOverride } : {}),
641
+ }
642
+ }
643
+ }
644
+
645
+ if (toolPolicy.blockedExtensions.length > 0) {
646
+ const blockedSummary = toolPolicy.blockedExtensions
647
+ .map((entry) => `${entry.tool} (${entry.reason})`)
648
+ .join(', ')
649
+ onEvent?.({ t: 'err', text: `Capability policy blocked extensions for this run: ${blockedSummary}` })
650
+ }
651
+
652
+ if (session.agentId) {
653
+ const agent = getAgent(session.agentId)
654
+ if (agent) {
655
+ const budgetCheck = checkAgentBudgetLimits(agent)
656
+ const action = agent.budgetAction || 'warn'
657
+
658
+ if (budgetCheck.exceeded.length > 0) {
659
+ const blockedMessage = budgetCheck.exceeded.map((entry) => entry.message).join(' ')
660
+ if (action === 'block') {
661
+ onEvent?.({ t: 'err', text: blockedMessage })
662
+ return {
663
+ kind: 'blocked',
664
+ sessionId,
665
+ session,
666
+ lifecycleRunId,
667
+ blockedMessage,
668
+ internal,
669
+ runId,
670
+ syntheticEnabledIds: getEnabledCapabilityIds(session),
671
+ }
672
+ }
673
+ onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: blockedMessage }) })
674
+ } else if (budgetCheck.warnings.length > 0) {
675
+ const warningText = budgetCheck.warnings.map((entry) => entry.message).join(' ')
676
+ onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: warningText }) })
677
+ }
678
+ }
679
+ }
680
+
681
+ const dailySpendLimitUsd = parseUsdLimit(appSettings.safetyMaxDailySpendUsd)
682
+ if (dailySpendLimitUsd !== null) {
683
+ const todaySpendUsd = getTodaySpendUsd()
684
+ if (todaySpendUsd >= dailySpendLimitUsd) {
685
+ const blockedMessage = `Safety budget reached: today's spend is $${todaySpendUsd.toFixed(4)} (limit $${dailySpendLimitUsd.toFixed(4)}). Increase safetyMaxDailySpendUsd to continue autonomous runs.`
686
+ onEvent?.({ t: 'err', text: blockedMessage })
687
+ return {
688
+ kind: 'blocked',
689
+ sessionId,
690
+ session,
691
+ lifecycleRunId,
692
+ blockedMessage,
693
+ internal,
694
+ runId,
695
+ syntheticEnabledIds: getEnabledCapabilityIds(session),
696
+ }
697
+ }
698
+ }
699
+
700
+ const providerType = sessionForRun.provider || 'claude-cli'
701
+ const provider = getProvider(providerType)
702
+ if (!provider) throw new Error(`Unknown provider: ${providerType}`)
703
+
704
+ if (providerType === 'claude-cli' && !fs.existsSync(session.cwd)) {
705
+ throw new Error(`Directory not found: ${session.cwd}`)
706
+ }
707
+
708
+ const apiKey = resolveApiKeyForSession(sessionForRun, provider)
709
+ const hideAssistantTranscript = internal && source === 'main-loop-followup'
710
+
711
+ const shouldPersistUserMessage = shouldPersistInboundUserMessage(internal, source)
712
+ if (shouldPersistUserMessage) {
713
+ const linkAnalysis = !internal ? await runLinkUnderstanding(message) : []
714
+ const guardedUserText = guardUntrustedText({
715
+ text: message,
716
+ source,
717
+ mode: getUntrustedContentGuardMode(appSettings),
718
+ trusted: (source === 'chat' && !internal) || internal,
719
+ }).text
720
+ const nextUserMessage = await applyMessageLifecycleHooks({
721
+ session,
722
+ message: {
723
+ role: 'user',
724
+ text: guardedUserText,
725
+ time: Date.now(),
726
+ imagePath: imagePath || undefined,
727
+ imageUrl: imageUrl || undefined,
728
+ attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
729
+ replyToId: input.replyToId || undefined,
730
+ },
731
+ enabledIds: extensionsForRun,
732
+ phase: 'user',
733
+ runId: lifecycleRunId,
734
+ })
735
+ if (nextUserMessage) {
736
+ session.messages.push(nextUserMessage)
737
+ if (linkAnalysis.length > 0) {
738
+ const linkAnalysisMessage = await applyMessageLifecycleHooks({
739
+ session,
740
+ message: {
741
+ role: 'assistant',
742
+ kind: 'system',
743
+ text: `[Automated Link Analysis]\n${linkAnalysis.join('\n\n')}`,
744
+ time: Date.now(),
745
+ },
746
+ enabledIds: extensionsForRun,
747
+ phase: 'system',
748
+ runId: lifecycleRunId,
749
+ isSynthetic: true,
750
+ })
751
+ if (linkAnalysisMessage) {
752
+ session.messages.push(linkAnalysisMessage)
753
+ }
754
+ }
755
+ session.lastActiveAt = Date.now()
756
+ saveSession(sessionId, session)
757
+ if (!internal && source === 'chat') {
758
+ try {
759
+ bridgeHumanReplyFromChat({
760
+ sessionId,
761
+ payload: nextUserMessage.text,
762
+ })
763
+ } catch {
764
+ // Best-effort mailbox bridge only.
765
+ }
766
+ }
767
+ if (!internal) {
768
+ try {
769
+ await runCapabilityHook('onMessage', { session, message: nextUserMessage }, { enabledIds: extensionsForRun })
770
+ } catch {
771
+ // onMessage hooks are non-critical.
772
+ }
773
+ }
774
+ }
775
+ }
776
+
777
+ const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
778
+ const enabledSessionExtensions = getEnabledCapabilityIds(sessionForRun)
779
+ const hasExtensions = enabledSessionExtensions.length > 0
780
+ && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
781
+ && !useLocalOpenClawNativeRuntime
782
+
783
+ const systemPrompt = heartbeatLightContext
784
+ ? joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock)
785
+ : (hasExtensions ? undefined : joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock))
786
+
787
+ return {
788
+ kind: 'ready',
789
+ sessionId,
790
+ message,
791
+ internal,
792
+ source,
793
+ runId,
794
+ session,
795
+ sessionForRun,
796
+ appSettings,
797
+ lifecycleRunId,
798
+ agentForSession,
799
+ mission,
800
+ missionContextBlock: missionContextBlock || undefined,
801
+ extensionsForRun,
802
+ effectiveMessage,
803
+ providerType,
804
+ provider,
805
+ apiKey,
806
+ hideAssistantTranscript,
807
+ isHeartbeatRun,
808
+ heartbeatLightContext,
809
+ isAutoRunNoHistory,
810
+ hasExtensions,
811
+ systemPrompt,
812
+ resolvedImagePath,
813
+ runStartedAt,
814
+ runMessageStartIndex,
815
+ toolPolicy,
816
+ }
817
+ }