@swarmclawai/swarmclaw 0.7.2 → 0.7.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 (197) hide show
  1. package/README.md +81 -22
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -156,8 +156,13 @@ const COLLECTIONS = [
156
156
  'souls',
157
157
  'benchmarks',
158
158
  'approvals',
159
+ 'browser_sessions',
160
+ 'watch_jobs',
161
+ 'delegation_jobs',
159
162
  ] as const
160
163
 
164
+ export type StorageCollection = (typeof COLLECTIONS)[number]
165
+
161
166
  for (const table of COLLECTIONS) {
162
167
  db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
163
168
  }
@@ -185,12 +190,34 @@ function getCollectionRawCache(table: string): LRUMap<string, string> {
185
190
  return loaded
186
191
  }
187
192
 
193
+ function normalizeStoredRecord(table: string, value: any): any {
194
+ if (table !== 'sessions') return value
195
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value
196
+
197
+ const session = value as Record<string, any>
198
+ if (session.sessionType !== 'human') session.sessionType = 'human'
199
+ const isLegacyShortcut = (
200
+ (typeof session.id === 'string' && session.id.startsWith('agent-thread-'))
201
+ || (typeof session.name === 'string' && session.name.startsWith('agent-thread:'))
202
+ )
203
+ if (
204
+ isLegacyShortcut
205
+ && typeof session.agentId === 'string'
206
+ && session.agentId.trim()
207
+ && (!session.shortcutForAgentId || session.shortcutForAgentId !== session.agentId)
208
+ ) {
209
+ session.shortcutForAgentId = session.agentId
210
+ }
211
+ if ('mainLoopState' in session) delete session.mainLoopState
212
+ return session
213
+ }
214
+
188
215
  function loadCollection(table: string): Record<string, any> {
189
216
  const raw = getCollectionRawCache(table)
190
217
  const result: Record<string, any> = {}
191
218
  for (const [id, data] of raw.entries()) {
192
219
  try {
193
- result[id] = JSON.parse(data)
220
+ result[id] = normalizeStoredRecord(table, JSON.parse(data))
194
221
  } catch {
195
222
  // Ignore malformed records instead of crashing list endpoints.
196
223
  }
@@ -205,7 +232,8 @@ function saveCollection(table: string, data: Record<string, any>) {
205
232
  const toDelete: string[] = []
206
233
 
207
234
  for (const [id, val] of Object.entries(data)) {
208
- const serialized = JSON.stringify(val)
235
+ const normalized = normalizeStoredRecord(table, val)
236
+ const serialized = JSON.stringify(normalized)
209
237
  if (typeof serialized !== 'string') continue
210
238
  next.set(id, serialized)
211
239
  if (current.get(id) !== serialized) {
@@ -251,7 +279,7 @@ function deleteCollectionItem(table: string, id: string) {
251
279
  * concurrent processes are modifying different items.
252
280
  */
253
281
  function upsertCollectionItem(table: string, id: string, value: any) {
254
- const serialized = JSON.stringify(value)
282
+ const serialized = JSON.stringify(normalizeStoredRecord(table, value))
255
283
  db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`).run(id, serialized)
256
284
  // Update the in-memory cache
257
285
  const cached = collectionCache.get(table)
@@ -260,6 +288,51 @@ function upsertCollectionItem(table: string, id: string, value: any) {
260
288
  }
261
289
  }
262
290
 
291
+ function loadCollectionItem(table: string, id: string): any | null {
292
+ const row = db.prepare(`SELECT data FROM ${table} WHERE id = ?`).get(id) as { data: string } | undefined
293
+ if (!row) return null
294
+ try {
295
+ return normalizeStoredRecord(table, JSON.parse(row.data))
296
+ } catch {
297
+ return null
298
+ }
299
+ }
300
+
301
+ function upsertCollectionItems(table: string, entries: Array<[string, any]>): void {
302
+ if (!entries.length) return
303
+ const prepared = entries
304
+ .map(([id, value]) => [id, JSON.stringify(normalizeStoredRecord(table, value))] as const)
305
+ .filter(([, serialized]) => typeof serialized === 'string')
306
+ if (!prepared.length) return
307
+
308
+ const transaction = db.transaction(() => {
309
+ const upsert = db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`)
310
+ for (const [id, serialized] of prepared) {
311
+ upsert.run(id, serialized)
312
+ }
313
+ })
314
+ transaction()
315
+
316
+ const cached = collectionCache.get(table)
317
+ if (cached) {
318
+ for (const [id, serialized] of prepared) {
319
+ cached.set(id, serialized)
320
+ }
321
+ }
322
+ }
323
+
324
+ export function loadStoredItem(table: StorageCollection, id: string): any | null {
325
+ return loadCollectionItem(table, id)
326
+ }
327
+
328
+ export function upsertStoredItem(table: StorageCollection, id: string, value: any): void {
329
+ upsertCollectionItem(table, id, value)
330
+ }
331
+
332
+ export function upsertStoredItems(table: StorageCollection, entries: Array<[string, any]>): void {
333
+ upsertCollectionItems(table, entries)
334
+ }
335
+
263
336
  function loadSingleton(table: string, fallback: any): any {
264
337
  const row = db.prepare(`SELECT data FROM ${table} WHERE id = 1`).get() as { data: string } | undefined
265
338
  return row ? JSON.parse(row.data) : fallback
@@ -392,7 +465,7 @@ if (!IS_BUILD_BOOTSTRAP) {
392
465
 
393
466
  ## Platform
394
467
 
395
- - **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Toggle "Orchestrator" to let an agent delegate work to others.
468
+ - **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Enable cross-agent delegation when an agent should assign work to others.
396
469
  - **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
397
470
  - **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
398
471
  - **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
@@ -419,7 +492,7 @@ Use your platform management tools proactively:
419
492
  You have opinions about good agent design. You suggest creative approaches, warn about common pitfalls, and get excited when someone gets something cool working. You're not a manual — you're a collaborator.
420
493
 
421
494
  Be concise but not curt. Warmth doesn't require verbosity. When someone asks "how do I...?", give them the direct steps. Offer to do things rather than just explaining — if someone wants an agent created, create it. Use your tools when actions speak louder than words. If you don't know something, say so honestly.`,
422
- isOrchestrator: false,
495
+ isOrchestrator: true,
423
496
  plugins: defaultStarterTools,
424
497
  heartbeatEnabled: true,
425
498
  platformAssignScope: 'all',
@@ -440,6 +513,15 @@ Be concise but not curt. Warmth doesn't require verbosity. When someone asks "ho
440
513
  existing.plugins = mergedPlugins
441
514
  delete existing.tools
442
515
  existing.updatedAt = Date.now()
516
+ }
517
+ if (existing.platformAssignScope === 'all' || existing.platformAssignScope === 'self') {
518
+ const derivedIsOrchestrator = existing.platformAssignScope === 'all'
519
+ if (existing.isOrchestrator !== derivedIsOrchestrator) {
520
+ existing.isOrchestrator = derivedIsOrchestrator
521
+ existing.updatedAt = Date.now()
522
+ }
523
+ }
524
+ if (JSON.stringify(JSON.parse(row.data)) !== JSON.stringify(existing)) {
443
525
  db.prepare('UPDATE agents SET data = ? WHERE id = ?').run(JSON.stringify(existing), 'default')
444
526
  }
445
527
  } catch {
@@ -685,12 +767,106 @@ export function saveQueue(q: string[]) {
685
767
  }
686
768
 
687
769
  // --- Settings ---
770
+ const APP_SETTINGS_SECRET_FIELDS = [
771
+ 'elevenLabsApiKey',
772
+ 'tavilyApiKey',
773
+ 'braveApiKey',
774
+ ] as const
775
+
776
+ const ENCRYPTED_APP_SETTINGS_KEY = '__encryptedAppSettings'
777
+
778
+ type PersistedSettingsRecord = Record<string, any> & {
779
+ [ENCRYPTED_APP_SETTINGS_KEY]?: Record<string, string>
780
+ }
781
+
782
+ function cloneRecord<T extends Record<string, any>>(value: T): T {
783
+ return JSON.parse(JSON.stringify(value || {})) as T
784
+ }
785
+
786
+ function isPlainRecord(value: unknown): value is Record<string, any> {
787
+ return !!value && typeof value === 'object' && !Array.isArray(value)
788
+ }
789
+
790
+ function getEncryptedAppSettings(settings: PersistedSettingsRecord): Record<string, string> {
791
+ return isPlainRecord(settings[ENCRYPTED_APP_SETTINGS_KEY])
792
+ ? { ...(settings[ENCRYPTED_APP_SETTINGS_KEY] as Record<string, string>) }
793
+ : {}
794
+ }
795
+
796
+ function isClearedSecretValue(value: unknown): boolean {
797
+ return value === null || (typeof value === 'string' && value.trim() === '')
798
+ }
799
+
800
+ function isProvidedSecretValue(value: unknown): value is string {
801
+ return typeof value === 'string' && value.trim().length > 0
802
+ }
803
+
804
+ function buildPersistedSettings(input: Record<string, any>, existing?: PersistedSettingsRecord): PersistedSettingsRecord {
805
+ const next = cloneRecord(input) as PersistedSettingsRecord
806
+ const encrypted = {
807
+ ...(existing ? getEncryptedAppSettings(existing) : {}),
808
+ ...getEncryptedAppSettings(next),
809
+ }
810
+
811
+ delete next[ENCRYPTED_APP_SETTINGS_KEY]
812
+
813
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
814
+ const raw = next[field]
815
+ if (isClearedSecretValue(raw)) {
816
+ delete encrypted[field]
817
+ delete next[field]
818
+ continue
819
+ }
820
+ if (isProvidedSecretValue(raw)) {
821
+ encrypted[field] = encryptKey(raw)
822
+ delete next[field]
823
+ }
824
+ }
825
+
826
+ if (Object.keys(encrypted).length > 0) next[ENCRYPTED_APP_SETTINGS_KEY] = encrypted
827
+ return next
828
+ }
829
+
830
+ function resolveSettingsSecrets(settings: PersistedSettingsRecord): Record<string, any> {
831
+ const resolved = cloneRecord(settings)
832
+ delete resolved[ENCRYPTED_APP_SETTINGS_KEY]
833
+
834
+ const encrypted = getEncryptedAppSettings(settings)
835
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
836
+ if (isProvidedSecretValue(resolved[field])) continue
837
+ const value = encrypted[field]
838
+ if (typeof value !== 'string' || !value) continue
839
+ try {
840
+ resolved[field] = decryptKey(value)
841
+ } catch {
842
+ // Ignore malformed encrypted settings instead of breaking all settings reads.
843
+ }
844
+ }
845
+
846
+ return resolved
847
+ }
848
+
688
849
  export function loadSettings(): Record<string, any> {
689
- return loadSingleton('settings', {})
850
+ const persisted = loadSingleton('settings', {}) as PersistedSettingsRecord
851
+ const normalized = buildPersistedSettings(persisted, persisted)
852
+ if (JSON.stringify(persisted) !== JSON.stringify(normalized)) {
853
+ saveSingleton('settings', normalized)
854
+ }
855
+ return resolveSettingsSecrets(normalized)
690
856
  }
691
857
 
692
858
  export function saveSettings(s: Record<string, any>) {
693
- saveSingleton('settings', s)
859
+ const existing = loadSingleton('settings', {}) as PersistedSettingsRecord
860
+ saveSingleton('settings', buildPersistedSettings(s, existing))
861
+ }
862
+
863
+ export function loadPublicSettings(): Record<string, any> {
864
+ const settings = cloneRecord(loadSettings())
865
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
866
+ settings[`${field}Configured`] = isProvidedSecretValue(settings[field])
867
+ settings[field] = null
868
+ }
869
+ return settings
694
870
  }
695
871
 
696
872
  // --- Secrets (service keys for orchestrators) ---
@@ -1026,6 +1202,49 @@ export function loadApprovals(): Record<string, unknown> {
1026
1202
  return loadCollection('approvals')
1027
1203
  }
1028
1204
 
1205
+ // --- Browser Sessions ---
1206
+ export function loadBrowserSessions(): Record<string, unknown> {
1207
+ return loadCollection('browser_sessions')
1208
+ }
1209
+
1210
+ export function upsertBrowserSession(id: string, data: unknown) {
1211
+ upsertCollectionItem('browser_sessions', id, data)
1212
+ }
1213
+
1214
+ export function deleteBrowserSession(id: string) {
1215
+ deleteCollectionItem('browser_sessions', id)
1216
+ }
1217
+
1218
+ // --- Watch Jobs ---
1219
+ export function loadWatchJobs(): Record<string, unknown> {
1220
+ return loadCollection('watch_jobs')
1221
+ }
1222
+
1223
+ export function upsertWatchJob(id: string, data: unknown) {
1224
+ upsertCollectionItem('watch_jobs', id, data)
1225
+ }
1226
+
1227
+ export function upsertWatchJobs(entries: Array<[string, unknown]>) {
1228
+ upsertCollectionItems('watch_jobs', entries)
1229
+ }
1230
+
1231
+ export function deleteWatchJob(id: string) {
1232
+ deleteCollectionItem('watch_jobs', id)
1233
+ }
1234
+
1235
+ // --- Delegation Jobs ---
1236
+ export function loadDelegationJobs(): Record<string, unknown> {
1237
+ return loadCollection('delegation_jobs')
1238
+ }
1239
+
1240
+ export function upsertDelegationJob(id: string, data: unknown) {
1241
+ upsertCollectionItem('delegation_jobs', id, data)
1242
+ }
1243
+
1244
+ export function deleteDelegationJob(id: string) {
1245
+ deleteCollectionItem('delegation_jobs', id)
1246
+ }
1247
+
1029
1248
  export function upsertApproval(id: string, approval: unknown) {
1030
1249
  upsertCollectionItem('approvals', id, approval)
1031
1250
  }
@@ -13,6 +13,7 @@ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
13
13
  import { expandPluginIds } from './tool-aliases'
14
14
  import type { Session, Message, UsageRecord, PluginInvocationRecord } from '@/types'
15
15
  import { extractSuggestions } from './suggestions'
16
+ import { buildIdentityContinuityContext } from './identity-continuity'
16
17
 
17
18
  /** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
18
19
  interface StreamAgentChatOpts {
@@ -59,7 +60,7 @@ const GOAL_DECOMPOSITION_BLOCK = [
59
60
  'When you receive a broad, open-ended goal:',
60
61
  '1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
61
62
  '2. If manage_tasks is available, create a task for each subtask to track progress.',
62
- '3. Output your plan in a [MAIN_LOOP_PLAN] JSON line: {"steps":["step1","step2",...],"current_step":"step1"}',
63
+ '3. Present the plan as a short checklist or numbered list in plain language.',
63
64
  '4. Execute the first subtask immediately — do not stop after planning.',
64
65
  '5. After each subtask, update progress and move to the next.',
65
66
  ].join('\n')
@@ -71,7 +72,6 @@ function buildAgenticExecutionPolicy(opts: {
71
72
  heartbeatIntervalSec: number
72
73
  platformAssignScope?: 'self' | 'all'
73
74
  userMessage?: string
74
- hasExistingPlan?: boolean
75
75
  }) {
76
76
  const hasTooling = opts.enabledPlugins.length > 0
77
77
  const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
@@ -102,13 +102,15 @@ function buildAgenticExecutionPolicy(opts: {
102
102
  'Always reply to: questions, tasks, emotional sharing, or when you have something useful to add.',
103
103
  'Execute by default — only ask for confirmation on high-risk/irreversible actions. Do not end every response with a question.',
104
104
  'Never repeat completed side effects. Verify state first.',
105
+ 'If a tool returns an error or validation failure, do not claim the task succeeded. Retry with corrected arguments or explain the blocker plainly.',
106
+ 'Delegation is optional, not a stopping condition. If one delegate backend is unavailable or unauthenticated, try another delegate backend or continue with your other tools.',
107
+ 'Only mention files, screenshots, URLs, or download links that were actually returned by tools. Copy returned links exactly; do not rewrite them or prepend extra prefixes like "sandbox:".',
105
108
  `Heartbeat: if message is "${opts.heartbeatPrompt}", reply "HEARTBEAT_OK" unless you have a progress update.`,
106
109
  opts.heartbeatIntervalSec > 0 ? `Heartbeat cadence: ~${opts.heartbeatIntervalSec}s.` : '',
107
- 'For SWARM_MAIN_MISSION_TICK / SWARM_MAIN_AUTO_FOLLOWUP messages, follow the response contract and include [MAIN_LOOP_META] JSON.',
108
110
  )
109
111
 
110
112
  if (pluginLines.length) parts.push('What I can do:\n' + pluginLines.join('\n'))
111
- if (opts.userMessage && !opts.hasExistingPlan && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
113
+ if (opts.userMessage && isBroadGoal(opts.userMessage)) parts.push(GOAL_DECOMPOSITION_BLOCK)
112
114
 
113
115
  return parts.filter(Boolean).join('\n')
114
116
  }
@@ -136,7 +138,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
136
138
 
137
139
  // Resolve agent's thinking level for provider-native params
138
140
  let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
139
- if (session.agentId) {
141
+ if (session.thinkingLevel) {
142
+ agentThinkingLevel = session.thinkingLevel
143
+ } else if (session.agentId) {
140
144
  const agentsForThinking = loadAgents()
141
145
  agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
142
146
  }
@@ -192,6 +196,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
192
196
  if (agent?.description) identityLines.push(agent.description)
193
197
  identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
194
198
  stateModifierParts.push(identityLines.join(' '))
199
+ const continuityBlock = buildIdentityContinuityContext(session, agent)
200
+ if (continuityBlock) stateModifierParts.push(continuityBlock)
195
201
  if (agent?.soul) stateModifierParts.push(agent.soul)
196
202
  if (agent?.systemPrompt) stateModifierParts.push(agent.systemPrompt)
197
203
  if (agent?.skillIds?.length) {
@@ -290,9 +296,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
290
296
  )
291
297
  }
292
298
 
293
- // Check for existing plan in mainLoopState to skip decomposition injection
294
- const hasExistingPlan = Array.isArray(session.mainLoopState?.planSteps) && session.mainLoopState.planSteps.length > 0
295
-
296
299
  stateModifierParts.push(
297
300
  buildAgenticExecutionPolicy({
298
301
  enabledPlugins: sessionPlugins,
@@ -301,7 +304,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
301
304
  heartbeatIntervalSec,
302
305
  platformAssignScope: agentPlatformAssignScope,
303
306
  userMessage: message,
304
- hasExistingPlan,
305
307
  }),
306
308
  )
307
309
 
@@ -480,14 +482,13 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
480
482
  let needsTextSeparator = false
481
483
  let totalInputTokens = 0
482
484
  let totalOutputTokens = 0
483
- let lastToolInput: unknown = null
484
485
  let accumulatedThinking = ''
485
486
  const pluginInvocations: PluginInvocationRecord[] = []
486
487
  let currentToolInputTokens = 0
487
488
 
488
489
  // Plugin hooks: beforeAgentStart
489
490
  const pluginMgr = getPluginManager()
490
- await pluginMgr.runHook('beforeAgentStart', { session, message })
491
+ await pluginMgr.runHook('beforeAgentStart', { session, message }, { enabledIds: sessionPlugins })
491
492
 
492
493
  const abortController = new AbortController()
493
494
  const abortFromSignal = () => abortController.abort()
@@ -512,6 +513,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
512
513
  const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES
513
514
  for (let iteration = 0; iteration <= maxIterations; iteration++) {
514
515
  let shouldContinue: 'recursion' | 'transient' | false = false
516
+ let waitingForToolResult = false
517
+ let idleTimedOut = false
518
+ let idleTimer: ReturnType<typeof setTimeout> | null = null
515
519
 
516
520
  // Fresh per-iteration controller so an internal LangGraph abort doesn't poison subsequent iterations.
517
521
  // Linked to the parent so client disconnect / timeout still propagates.
@@ -520,7 +524,24 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
520
524
  if (abortController.signal.aborted) iterationController.abort()
521
525
  else abortController.signal.addEventListener('abort', onParentAbort)
522
526
 
527
+ const clearIdleWatchdog = () => {
528
+ if (idleTimer) {
529
+ clearTimeout(idleTimer)
530
+ idleTimer = null
531
+ }
532
+ }
533
+
534
+ const armIdleWatchdog = () => {
535
+ clearIdleWatchdog()
536
+ if (waitingForToolResult || iterationController.signal.aborted) return
537
+ idleTimer = setTimeout(() => {
538
+ idleTimedOut = true
539
+ iterationController.abort()
540
+ }, 90_000)
541
+ }
542
+
523
543
  try {
544
+ armIdleWatchdog()
524
545
  const eventStream = agent.streamEvents(
525
546
  { messages: langchainMessages },
526
547
  { version: 'v2', recursionLimit, signal: iterationController.signal },
@@ -530,6 +551,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
530
551
  const kind = event.event
531
552
 
532
553
  if (kind === 'on_chat_model_stream') {
554
+ armIdleWatchdog()
533
555
  const chunk = event.data?.chunk
534
556
  if (chunk?.content) {
535
557
  // content can be string or array of content blocks
@@ -569,6 +591,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
569
591
  }
570
592
  }
571
593
  } else if (kind === 'on_llm_end') {
594
+ armIdleWatchdog()
572
595
  // Track token usage from LLM responses — check all known LangChain event shapes
573
596
  const output = event.data?.output
574
597
  const usage = output?.llmOutput?.tokenUsage
@@ -581,17 +604,16 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
581
604
  totalOutputTokens += usage.completionTokens || usage.output_tokens || usage.completion_tokens || 0
582
605
  }
583
606
  } else if (kind === 'on_tool_start') {
607
+ clearIdleWatchdog()
608
+ waitingForToolResult = true
584
609
  hasToolCalls = true
585
610
  needsTextSeparator = true
586
611
  lastSegment = ''
587
612
  const toolName = event.name || 'unknown'
588
613
  const input = event.data?.input
589
- lastToolInput = input
590
614
  // Estimate input tokens for plugin invocation tracking
591
615
  const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
592
616
  currentToolInputTokens = Math.ceil((inputStr?.length || 0) / 4)
593
- // Plugin hooks: beforeToolExec
594
- await pluginMgr.runHook('beforeToolExec', { toolName, input })
595
617
  logExecution(session.id, 'tool_call', `${toolName} invoked`, {
596
618
  agentId: session.agentId,
597
619
  detail: { toolName, input: inputStr?.slice(0, 4000) },
@@ -602,6 +624,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
602
624
  toolInput: inputStr,
603
625
  })}\n\n`)
604
626
  } else if (kind === 'on_tool_end') {
627
+ waitingForToolResult = false
628
+ armIdleWatchdog()
605
629
  const toolName = event.name || 'unknown'
606
630
  const output = event.data?.output
607
631
  const outputStr = typeof output === 'string'
@@ -609,9 +633,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
609
633
  : output?.content
610
634
  ? String(output.content)
611
635
  : JSON.stringify(output)
612
- // Plugin hooks: afterToolExec
613
- await pluginMgr.runHook('afterToolExec', { session, toolName, input: lastToolInput as Record<string, unknown> | null, output: outputStr })
614
- lastToolInput = null
615
636
  logExecution(session.id, 'tool_result', `${toolName} returned`, {
616
637
  agentId: session.agentId,
617
638
  detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
@@ -654,7 +675,9 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
654
675
  }
655
676
  } catch (innerErr: unknown) {
656
677
  const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
657
- const errMsg = innerErr instanceof Error ? innerErr.message : String(innerErr)
678
+ const errMsg = idleTimedOut
679
+ ? 'Model stream stalled without emitting text or tool results for 90 seconds.'
680
+ : innerErr instanceof Error ? innerErr.message : String(innerErr)
658
681
  const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
659
682
 
660
683
  // Classify the error:
@@ -662,9 +685,10 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
662
685
  // 2. Transient abort/timeout — LLM API failure, not from client disconnect
663
686
  const isRecursionError = errName === 'GraphRecursionError'
664
687
  || /recursion limit|maximum recursion/i.test(errMsg)
665
- const isTransientAbort = !isRecursionError
688
+ const isTransientAbort = (!isRecursionError && idleTimedOut)
689
+ || (!isRecursionError
666
690
  && /abort|timed?\s*out|ECONNRESET|ECONNREFUSED|socket hang up|network/i.test(errMsg)
667
- && !abortController.signal.aborted
691
+ && !abortController.signal.aborted)
668
692
 
669
693
  // Log diagnostic details for every error so we can trace root causes
670
694
  console.error(`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
@@ -695,6 +719,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
695
719
  throw innerErr
696
720
  }
697
721
  } finally {
722
+ clearIdleWatchdog()
698
723
  abortController.signal.removeEventListener('abort', onParentAbort)
699
724
  }
700
725
 
@@ -775,7 +800,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
775
800
  }
776
801
 
777
802
  // Plugin hooks: afterAgentComplete
778
- await pluginMgr.runHook('afterAgentComplete', { session, response: fullText })
803
+ await pluginMgr.runHook('afterAgentComplete', { session, response: fullText }, { enabledIds: sessionPlugins })
779
804
 
780
805
  // OpenClaw auto-sync: push memory if enabled
781
806
  try {
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert/strict'
2
+ import { afterEach, describe, it } from 'node:test'
3
+ import { PROVIDERS } from '../providers'
4
+ import { runStructuredExtraction } from './structured-extract'
5
+
6
+ const originalOllamaHandler = PROVIDERS.ollama.handler.streamChat
7
+
8
+ afterEach(() => {
9
+ PROVIDERS.ollama.handler.streamChat = originalOllamaHandler
10
+ })
11
+
12
+ describe('runStructuredExtraction', () => {
13
+ it('parses fenced JSON output from the current provider', async () => {
14
+ PROVIDERS.ollama.handler.streamChat = async () => '```json\n{"name":"Ada","score":10}\n```'
15
+
16
+ const result = await runStructuredExtraction({
17
+ session: {
18
+ id: 'session-1',
19
+ provider: 'ollama',
20
+ model: 'qwen3.5',
21
+ credentialId: null,
22
+ fallbackCredentialIds: [],
23
+ apiEndpoint: 'http://localhost:11434',
24
+ },
25
+ text: 'Ada scored 10.',
26
+ schema: {
27
+ type: 'object',
28
+ properties: {
29
+ name: { type: 'string' },
30
+ score: { type: 'number' },
31
+ },
32
+ required: ['name', 'score'],
33
+ },
34
+ instruction: 'Extract the person and score.',
35
+ })
36
+
37
+ assert.deepEqual(result.object, { name: 'Ada', score: 10 })
38
+ assert.deepEqual(result.validationErrors, [])
39
+ })
40
+
41
+ it('repairs invalid JSON with a second pass', async () => {
42
+ let callCount = 0
43
+ PROVIDERS.ollama.handler.streamChat = async () => {
44
+ callCount += 1
45
+ return callCount === 1 ? 'name: Ada' : '{"name":"Ada"}'
46
+ }
47
+
48
+ const result = await runStructuredExtraction({
49
+ session: {
50
+ id: 'session-2',
51
+ provider: 'ollama',
52
+ model: 'qwen3.5',
53
+ credentialId: null,
54
+ fallbackCredentialIds: [],
55
+ apiEndpoint: 'http://localhost:11434',
56
+ },
57
+ text: 'Ada',
58
+ schema: {
59
+ type: 'object',
60
+ properties: {
61
+ name: { type: 'string' },
62
+ },
63
+ required: ['name'],
64
+ },
65
+ instruction: 'Extract the name.',
66
+ })
67
+
68
+ assert.equal(callCount, 2)
69
+ assert.deepEqual(result.object, { name: 'Ada' })
70
+ assert.deepEqual(result.validationErrors, [])
71
+ })
72
+ })