@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
@@ -0,0 +1,276 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { Plugin, PluginHooks } from '@/types'
4
+ import { getPluginManager } from '../plugins'
5
+ import type { ToolBuildContext } from './context'
6
+ import { normalizeToolInputArgs } from './normalize-tool-args'
7
+ import {
8
+ downloadMailboxAttachment,
9
+ fetchMailboxMessageByUid,
10
+ fetchMailboxMessages,
11
+ getMailboxConfig,
12
+ replyMailboxMessage,
13
+ } from '../mailbox-utils'
14
+ import { createWatchJob } from '../watch-jobs'
15
+
16
+ function parseMessageUid(value: unknown): number {
17
+ const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number.parseInt(value, 10) : Number.NaN
18
+ return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : 0
19
+ }
20
+
21
+ async function executeMailboxAction(args: Record<string, unknown>, bctx: { cwd: string; sessionId?: string | null; agentId?: string | null }) {
22
+ const normalized = normalizeToolInputArgs(args)
23
+ const action = String(normalized.action || 'status').trim().toLowerCase()
24
+ const folder = typeof normalized.folder === 'string' ? normalized.folder.trim() : undefined
25
+
26
+ try {
27
+ if (action === 'status') {
28
+ const config = getMailboxConfig()
29
+ return JSON.stringify({
30
+ configured: !!(config.imapHost && config.user && config.password),
31
+ imapHost: config.imapHost || null,
32
+ smtpHost: config.smtpHost || null,
33
+ folder: config.folder || 'INBOX',
34
+ fromAddress: config.fromAddress || null,
35
+ subjectPrefix: config.subjectPrefix || null,
36
+ })
37
+ }
38
+
39
+ if (action === 'list_messages' || action === 'search_messages') {
40
+ const messages = await fetchMailboxMessages({
41
+ folder,
42
+ query: typeof normalized.query === 'string' ? normalized.query : undefined,
43
+ from: typeof normalized.from === 'string' ? normalized.from : undefined,
44
+ subjectContains: typeof normalized.subjectContains === 'string' ? normalized.subjectContains : undefined,
45
+ bodyContains: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
46
+ unreadOnly: normalized.unreadOnly === true,
47
+ hasAttachments: normalized.hasAttachments === true,
48
+ limit: typeof normalized.limit === 'number' ? normalized.limit : undefined,
49
+ })
50
+ return JSON.stringify(messages.map((message) => ({
51
+ uid: message.uid,
52
+ messageId: message.messageId,
53
+ subject: message.subject,
54
+ from: message.from,
55
+ fromName: message.fromName,
56
+ date: message.date,
57
+ snippet: message.snippet,
58
+ hasAttachments: message.hasAttachments,
59
+ attachmentCount: message.attachments.length,
60
+ threadKey: message.threadKey,
61
+ })))
62
+ }
63
+
64
+ if (action === 'list_threads') {
65
+ const messages = await fetchMailboxMessages({
66
+ folder,
67
+ limit: typeof normalized.limit === 'number' ? Math.max(10, normalized.limit * 4) : 80,
68
+ })
69
+ const threads = new Map<string, {
70
+ threadKey: string
71
+ subject: string
72
+ participants: Set<string>
73
+ latestUid: number
74
+ latestDate: string | null
75
+ messageCount: number
76
+ unreadCount: number
77
+ snippet: string
78
+ }>()
79
+ for (const message of messages) {
80
+ const current = threads.get(message.threadKey) || {
81
+ threadKey: message.threadKey,
82
+ subject: message.subject,
83
+ participants: new Set<string>(),
84
+ latestUid: message.uid,
85
+ latestDate: message.date,
86
+ messageCount: 0,
87
+ unreadCount: 0,
88
+ snippet: message.snippet,
89
+ }
90
+ current.messageCount += 1
91
+ current.participants.add(message.from)
92
+ if (!message.flags.includes('\\Seen')) current.unreadCount += 1
93
+ if (message.uid >= current.latestUid) {
94
+ current.latestUid = message.uid
95
+ current.latestDate = message.date
96
+ current.subject = message.subject
97
+ current.snippet = message.snippet
98
+ }
99
+ threads.set(message.threadKey, current)
100
+ }
101
+ return JSON.stringify(Array.from(threads.values())
102
+ .map((thread) => ({
103
+ threadKey: thread.threadKey,
104
+ subject: thread.subject,
105
+ participants: Array.from(thread.participants),
106
+ latestUid: thread.latestUid,
107
+ latestDate: thread.latestDate,
108
+ messageCount: thread.messageCount,
109
+ unreadCount: thread.unreadCount,
110
+ snippet: thread.snippet,
111
+ }))
112
+ .sort((a, b) => b.latestUid - a.latestUid)
113
+ .slice(0, Math.max(1, Math.min(typeof normalized.limit === 'number' ? normalized.limit : 20, 100))))
114
+ }
115
+
116
+ if (action === 'read_message') {
117
+ const uid = parseMessageUid(normalized.uid ?? normalized.id)
118
+ if (!uid) return 'Error: uid is required.'
119
+ const message = await fetchMailboxMessageByUid(uid, folder)
120
+ if (!message) return `Error: mailbox message "${uid}" not found.`
121
+ return JSON.stringify(message)
122
+ }
123
+
124
+ if (action === 'download_attachment') {
125
+ const uid = parseMessageUid(normalized.uid ?? normalized.id)
126
+ if (!uid) return 'Error: uid is required.'
127
+ const result = await downloadMailboxAttachment({
128
+ uid,
129
+ folder,
130
+ attachmentId: typeof normalized.attachmentId === 'string' ? normalized.attachmentId : undefined,
131
+ attachmentName: typeof normalized.attachmentName === 'string' ? normalized.attachmentName : undefined,
132
+ saveTo: typeof normalized.saveTo === 'string' ? normalized.saveTo : undefined,
133
+ cwd: bctx.cwd,
134
+ })
135
+ return JSON.stringify(result)
136
+ }
137
+
138
+ if (action === 'reply') {
139
+ const uid = parseMessageUid(normalized.uid ?? normalized.id)
140
+ if (!uid) return 'Error: uid is required.'
141
+ const text = typeof normalized.text === 'string'
142
+ ? normalized.text
143
+ : typeof normalized.body === 'string'
144
+ ? normalized.body
145
+ : ''
146
+ if (!text.trim()) return 'Error: text is required.'
147
+ const result = await replyMailboxMessage({
148
+ uid,
149
+ folder,
150
+ text,
151
+ html: typeof normalized.html === 'string' ? normalized.html : undefined,
152
+ subject: typeof normalized.subject === 'string' ? normalized.subject : undefined,
153
+ })
154
+ return JSON.stringify({ ok: true, ...result, uid })
155
+ }
156
+
157
+ if (action === 'wait_for_email') {
158
+ if (!bctx.sessionId && !bctx.agentId) return 'Error: email waits require a session or agent context.'
159
+ const resumeMessage = typeof normalized.resumeMessage === 'string' && normalized.resumeMessage.trim()
160
+ ? normalized.resumeMessage.trim()
161
+ : 'A matching email arrived. Read it, decide what to do next, and continue the task.'
162
+ const intervalMs = typeof normalized.intervalSec === 'number'
163
+ ? Math.max(30, normalized.intervalSec) * 1000
164
+ : 60_000
165
+ const timeoutAt = typeof normalized.timeoutMinutes === 'number'
166
+ ? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
167
+ : undefined
168
+ const job = await createWatchJob({
169
+ type: 'email',
170
+ sessionId: bctx.sessionId || null,
171
+ agentId: bctx.agentId || null,
172
+ createdByAgentId: bctx.agentId || null,
173
+ resumeMessage,
174
+ description: typeof normalized.description === 'string' ? normalized.description : 'Wait for email',
175
+ intervalMs,
176
+ timeoutAt,
177
+ target: {
178
+ folder: folder || getMailboxConfig().folder || 'INBOX',
179
+ },
180
+ condition: {
181
+ from: typeof normalized.from === 'string' ? normalized.from : undefined,
182
+ subjectContains: typeof normalized.subjectContains === 'string' ? normalized.subjectContains : undefined,
183
+ containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
184
+ query: typeof normalized.query === 'string' ? normalized.query : undefined,
185
+ unreadOnly: normalized.unreadOnly === true,
186
+ hasAttachments: normalized.hasAttachments === true,
187
+ },
188
+ })
189
+ return JSON.stringify(job)
190
+ }
191
+
192
+ return `Error: Unknown action "${action}".`
193
+ } catch (err: unknown) {
194
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
195
+ }
196
+ }
197
+
198
+ const MailboxPlugin: Plugin = {
199
+ name: 'Mailbox',
200
+ enabledByDefault: false,
201
+ description: 'Read/search/reply to inbox messages over IMAP/SMTP, download attachments, and wait for matching inbound email.',
202
+ hooks: {
203
+ getCapabilityDescription: () =>
204
+ 'I can inspect inboxes with `mailbox`, read and search messages, download attachments, reply to emails, and wait for specific inbound messages.',
205
+ } as PluginHooks,
206
+ tools: [
207
+ {
208
+ name: 'mailbox',
209
+ description: 'Work with email inboxes. Actions: status, list_messages, list_threads, search_messages, read_message, download_attachment, reply, wait_for_email.',
210
+ parameters: {
211
+ type: 'object',
212
+ properties: {
213
+ action: { type: 'string', enum: ['status', 'list_messages', 'list_threads', 'search_messages', 'read_message', 'download_attachment', 'reply', 'wait_for_email'] },
214
+ uid: { type: 'number' },
215
+ query: { type: 'string' },
216
+ from: { type: 'string' },
217
+ subjectContains: { type: 'string' },
218
+ containsText: { type: 'string' },
219
+ attachmentId: { type: 'string' },
220
+ attachmentName: { type: 'string' },
221
+ text: { type: 'string' },
222
+ body: { type: 'string' },
223
+ html: { type: 'string' },
224
+ subject: { type: 'string' },
225
+ folder: { type: 'string' },
226
+ unreadOnly: { type: 'boolean' },
227
+ hasAttachments: { type: 'boolean' },
228
+ limit: { type: 'number' },
229
+ saveTo: { type: 'string' },
230
+ resumeMessage: { type: 'string' },
231
+ intervalSec: { type: 'number' },
232
+ timeoutMinutes: { type: 'number' },
233
+ },
234
+ required: ['action'],
235
+ },
236
+ execute: async (args, context) => executeMailboxAction(args, {
237
+ cwd: context.session.cwd || process.cwd(),
238
+ sessionId: context.session.id,
239
+ agentId: context.session.agentId || null,
240
+ }),
241
+ },
242
+ ],
243
+ ui: {
244
+ settingsFields: [
245
+ { key: 'imapHost', label: 'IMAP Host', type: 'text', placeholder: 'imap.gmail.com', help: 'Inbound mailbox host.' },
246
+ { key: 'imapPort', label: 'IMAP Port', type: 'number', defaultValue: 993, help: '993 for TLS IMAP.' },
247
+ { key: 'smtpHost', label: 'SMTP Host', type: 'text', placeholder: 'smtp.gmail.com', help: 'Outbound mail host for replies.' },
248
+ { key: 'smtpPort', label: 'SMTP Port', type: 'number', defaultValue: 587, help: '587 for STARTTLS, 465 for SSL.' },
249
+ { key: 'user', label: 'Mailbox Username', type: 'text', placeholder: 'agent@example.com' },
250
+ { key: 'password', label: 'Mailbox Password', type: 'secret', help: 'IMAP password or app password.' },
251
+ { key: 'folder', label: 'Folder', type: 'text', defaultValue: 'INBOX', placeholder: 'INBOX' },
252
+ { key: 'fromAddress', label: 'Reply From Address', type: 'text', placeholder: 'agent@example.com' },
253
+ { key: 'fromName', label: 'Reply From Name', type: 'text', defaultValue: 'SwarmClaw Agent' },
254
+ ],
255
+ },
256
+ }
257
+
258
+ getPluginManager().registerBuiltin('mailbox', MailboxPlugin)
259
+
260
+ export function buildMailboxTools(bctx: ToolBuildContext): StructuredToolInterface[] {
261
+ if (!bctx.hasPlugin('mailbox')) return []
262
+ return [
263
+ tool(
264
+ async (args) => executeMailboxAction(args, {
265
+ cwd: bctx.cwd,
266
+ sessionId: bctx.ctx?.sessionId || null,
267
+ agentId: bctx.ctx?.agentId || null,
268
+ }),
269
+ {
270
+ name: 'mailbox',
271
+ description: MailboxPlugin.tools![0].description,
272
+ schema: z.object({}).passthrough(),
273
+ },
274
+ ),
275
+ ]
276
+ }
@@ -15,6 +15,8 @@ import type { MemoryEntry, Plugin, PluginHooks } from '@/types'
15
15
  import type { ToolBuildContext } from './context'
16
16
  import { getPluginManager } from '../plugins'
17
17
  import { normalizeToolInputArgs } from './normalize-tool-args'
18
+ import { partitionMemoriesByTier } from '../memory-tiers'
19
+ import { syncSessionArchiveMemory } from '../session-archive-memory'
18
20
 
19
21
  /**
20
22
  * Advanced Database-Backed Memory logic.
@@ -34,6 +36,12 @@ async function executeMemoryAction(input: any, ctx: any) {
34
36
 
35
37
  const memDb = getMemoryDb()
36
38
  const currentAgentId = ctx?.agentId || null
39
+ const currentSessionId = typeof ctx?.sessionId === 'string'
40
+ ? ctx.sessionId
41
+ : typeof ctx?.id === 'string'
42
+ ? ctx.id
43
+ : null
44
+ const currentSession = ctx && typeof ctx === 'object' && Array.isArray(ctx.messages) ? ctx : null
37
45
  const rawScope = typeof scope === 'string' ? scope : 'auto'
38
46
  const scopeMode = normalizeMemoryScopeMode(rawScope === 'shared' ? 'global' : rawScope)
39
47
  const rerankMode = rerank === 'semantic' || rerank === 'lexical' ? rerank : 'balanced'
@@ -41,7 +49,7 @@ async function executeMemoryAction(input: any, ctx: any) {
41
49
  const scopeFilter = {
42
50
  mode: scopeMode,
43
51
  agentId: currentAgentId,
44
- sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() : (ctx?.sessionId || null),
52
+ sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() : currentSessionId,
45
53
  projectRoot: (typeof projectRoot === 'string' && projectRoot.trim()) ? projectRoot.trim() : ((project && typeof project === 'object' && 'rootPath' in project && typeof (project as Record<string, unknown>).rootPath === 'string') ? (project as Record<string, unknown>).rootPath as string : null),
46
54
  }
47
55
 
@@ -52,6 +60,10 @@ async function executeMemoryAction(input: any, ctx: any) {
52
60
  const limits = getMemoryLookupLimits(loadSettings())
53
61
  const maxPerLookup = limits.maxPerLookup
54
62
 
63
+ if ((action === 'search' || action === 'list') && currentSession) {
64
+ try { syncSessionArchiveMemory(currentSession) } catch { /* archive sync is best-effort */ }
65
+ }
66
+
55
67
  const formatEntry = (m: any) => {
56
68
  let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
57
69
  if (m.reinforcementCount) line += ` (reinforced ×${m.reinforcementCount})`
@@ -132,6 +144,8 @@ const MemoryPlugin: Plugin = {
132
144
  const agentId = ctx.session.agentId
133
145
  if (!agentId) return null
134
146
 
147
+ try { syncSessionArchiveMemory(ctx.session) } catch { /* archive sync is best-effort */ }
148
+
135
149
  const memDb = getMemoryDb()
136
150
  const memoryQuerySeed = [
137
151
  ctx.message,
@@ -159,12 +173,22 @@ const MemoryPlugin: Plugin = {
159
173
  const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, agentId, 1, 10, 14)
160
174
  const relevant = relevantLookup.entries.slice(0, relevantSlice)
161
175
  const recent = memDb.list(agentId, 12).slice(0, 6)
176
+ const relevantByTier = partitionMemoriesByTier(relevant)
177
+ const recentByTier = partitionMemoriesByTier(recent)
178
+
179
+ const relevantLines = relevantByTier.durable
180
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
181
+ .map(formatMemoryLine)
162
182
 
163
- const relevantLines = relevant
183
+ const archiveLines = relevantByTier.archive
164
184
  .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
165
185
  .map(formatMemoryLine)
166
186
 
167
- const recentLines = recent
187
+ const recentLines = recentByTier.durable
188
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
189
+ .map(formatMemoryLine)
190
+
191
+ const recentArchiveLines = recentByTier.archive
168
192
  .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
169
193
  .map(formatMemoryLine)
170
194
 
@@ -175,14 +199,21 @@ const MemoryPlugin: Plugin = {
175
199
  if (relevantLines.length) {
176
200
  parts.push(['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'))
177
201
  }
202
+ if (archiveLines.length) {
203
+ parts.push(['## Session Archive Hits', 'Past conversation snapshots that may restore context from older chats.', ...archiveLines].join('\n'))
204
+ }
178
205
  if (recentLines.length) {
179
206
  parts.push(['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'))
180
207
  }
208
+ if (recentArchiveLines.length) {
209
+ parts.push(['## Recent Session Archives', 'Recently synced conversation archives you can search instead of relying on stale live context.', ...recentArchiveLines].join('\n'))
210
+ }
181
211
 
182
212
  // Memory Policy
183
213
  parts.push([
184
214
  '## My Memory',
185
215
  'I have long-term memory that persists across conversations. I use it naturally — I don\'t wait to be asked to remember things.',
216
+ 'Memory tiers: working memory is short-lived, durable memory stores stable facts and decisions, and session archives capture older conversation context for search.',
186
217
  '',
187
218
  '**Things worth remembering:**',
188
219
  '- What the user likes, dislikes, or has corrected me on',
@@ -201,6 +232,7 @@ const MemoryPlugin: Plugin = {
201
232
  '**Good habits:**',
202
233
  '- Give memories clear titles ("User prefers dark mode" not "Note 1")',
203
234
  '- Use categories: preference, fact, learning, project, identity, decision',
235
+ '- Search session archives before assuming older conversation context is still in the live chat history',
204
236
  '- Check what I already know before storing something new',
205
237
  '- When I learn something that corrects old knowledge, update or remove the old memory',
206
238
  ].join('\n'))
@@ -8,16 +8,98 @@ import { getPluginManager } from '../plugins'
8
8
  import type { Plugin, PluginHooks } from '@/types'
9
9
  import { safePath, truncate } from './context'
10
10
  import { normalizeToolInputArgs } from './normalize-tool-args'
11
+ import { cancelWatchJob, createWatchJob, getWatchJob, listWatchJobs } from '../watch-jobs'
12
+ import { ensureSessionBrowserProfileId, loadBrowserSessionRecord } from '../browser-state'
13
+
14
+ type WatchKind = 'time' | 'http' | 'file' | 'task' | 'webhook' | 'page'
15
+
16
+ async function createDurableWatch(
17
+ normalized: Record<string, unknown>,
18
+ bctx: { cwd: string; sessionId?: string; agentId?: string | null },
19
+ explicitType?: WatchKind,
20
+ ) {
21
+ const watchType = (explicitType || String(normalized.watchType || normalized.type || '').trim().toLowerCase()) as WatchKind
22
+ if (!watchType) return 'Error: watchType is required.'
23
+ if (!['time', 'http', 'file', 'task', 'webhook', 'page'].includes(watchType)) {
24
+ return `Error: Unsupported watchType "${watchType}".`
25
+ }
26
+
27
+ const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
28
+ const agentId = typeof normalized.agentId === 'string' ? normalized.agentId : (bctx.agentId || undefined)
29
+ const resumeMessage = String(normalized.resumeMessage || normalized.message || '').trim()
30
+ if (!resumeMessage) return 'Error: resumeMessage is required.'
31
+
32
+ const target = (normalized.target ?? normalized.url ?? normalized.path) as string | undefined
33
+ const delayMinutes = typeof normalized.delayMinutes === 'number' ? normalized.delayMinutes : undefined
34
+ const runAt = typeof normalized.runAt === 'number'
35
+ ? normalized.runAt
36
+ : delayMinutes !== undefined
37
+ ? Date.now() + Math.max(0, delayMinutes) * 60_000
38
+ : undefined
39
+ const intervalMs = typeof normalized.intervalSec === 'number'
40
+ ? Math.max(15, normalized.intervalSec) * 1000
41
+ : typeof normalized.intervalMs === 'number'
42
+ ? Math.max(15_000, normalized.intervalMs)
43
+ : undefined
44
+ const timeoutAt = typeof normalized.timeoutMinutes === 'number'
45
+ ? Date.now() + Math.max(1, normalized.timeoutMinutes) * 60_000
46
+ : typeof normalized.timeoutAt === 'number'
47
+ ? normalized.timeoutAt
48
+ : undefined
49
+ const browserProfileId = sessionId ? ensureSessionBrowserProfileId(sessionId).profileId : null
50
+ const targetPath = watchType === 'file' && target ? safePath(bctx.cwd, target) : target
51
+ const pageUrl = watchType === 'page' && !target && sessionId
52
+ ? loadBrowserSessionRecord(sessionId)?.currentUrl || undefined
53
+ : undefined
54
+ const pageTarget = target || pageUrl
55
+ if ((watchType === 'http' || watchType === 'page') && !pageTarget) {
56
+ return `Error: ${watchType === 'page' ? 'url or active browser page' : 'url'} is required.`
57
+ }
58
+
59
+ const job = await createWatchJob({
60
+ type: watchType,
61
+ sessionId: sessionId || null,
62
+ agentId: agentId || null,
63
+ createdByAgentId: agentId || null,
64
+ browserProfileId,
65
+ description: typeof normalized.description === 'string' ? normalized.description : null,
66
+ resumeMessage,
67
+ runAt,
68
+ intervalMs,
69
+ timeoutAt,
70
+ target: {
71
+ url: watchType === 'http' || watchType === 'page' ? pageTarget : undefined,
72
+ path: watchType === 'file' ? targetPath : undefined,
73
+ taskId: watchType === 'task' ? String(normalized.taskId || normalized.id || '') : undefined,
74
+ webhookId: watchType === 'webhook' ? String(normalized.webhookId || normalized.id || '') : undefined,
75
+ baselineHash: undefined,
76
+ },
77
+ condition: {
78
+ containsText: typeof normalized.containsText === 'string' ? normalized.containsText : undefined,
79
+ textGone: typeof normalized.textGone === 'string' ? normalized.textGone : undefined,
80
+ regex: typeof normalized.regex === 'string' ? normalized.regex : undefined,
81
+ changed: normalized.changed === true,
82
+ exists: normalized.exists,
83
+ status: typeof normalized.status === 'number' ? normalized.status : undefined,
84
+ statusIn: Array.isArray(normalized.statusIn) ? normalized.statusIn : undefined,
85
+ event: typeof normalized.event === 'string' ? normalized.event : undefined,
86
+ threshold: typeof normalized.threshold === 'number' ? normalized.threshold : undefined,
87
+ },
88
+ })
89
+ return JSON.stringify(job, null, 2)
90
+ }
11
91
 
12
92
  /**
13
93
  * Unified Monitoring Logic
14
94
  */
15
- async function executeMonitorAction(args: any, bctx: { cwd: string }) {
95
+ async function executeMonitorAction(args: any, bctx: { cwd: string; sessionId?: string; agentId?: string | null }) {
16
96
  const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
17
97
  const action = normalized.action as string | undefined
18
98
  const target = (normalized.target ?? normalized.url ?? normalized.path) as string | undefined
19
99
  const limit = normalized.limit as number | undefined
20
100
  const threshold = normalized.threshold as number | undefined
101
+ const sessionId = typeof normalized.sessionId === 'string' ? normalized.sessionId : bctx.sessionId
102
+ const agentId = typeof normalized.agentId === 'string' ? normalized.agentId : (bctx.agentId || undefined)
21
103
 
22
104
  try {
23
105
  switch (action) {
@@ -65,6 +147,7 @@ async function executeMonitorAction(args: any, bctx: { cwd: string }) {
65
147
  status: res.status,
66
148
  ok: res.ok,
67
149
  latency: `${latency}ms`,
150
+ thresholdExceeded: typeof threshold === 'number' ? latency >= threshold : undefined,
68
151
  url
69
152
  }, null, 2)
70
153
  } catch (err: any) {
@@ -76,6 +159,55 @@ async function executeMonitorAction(args: any, bctx: { cwd: string }) {
76
159
  }
77
160
  }
78
161
 
162
+ case 'create_watch': {
163
+ return createDurableWatch(normalized, bctx)
164
+ }
165
+
166
+ case 'wait_until': {
167
+ return createDurableWatch(normalized, bctx, 'time')
168
+ }
169
+
170
+ case 'wait_for_http': {
171
+ return createDurableWatch(normalized, bctx, 'http')
172
+ }
173
+
174
+ case 'wait_for_file': {
175
+ return createDurableWatch(normalized, bctx, 'file')
176
+ }
177
+
178
+ case 'wait_for_task': {
179
+ return createDurableWatch(normalized, bctx, 'task')
180
+ }
181
+
182
+ case 'wait_for_webhook': {
183
+ return createDurableWatch(normalized, bctx, 'webhook')
184
+ }
185
+
186
+ case 'wait_for_page_change': {
187
+ return createDurableWatch(normalized, bctx, 'page')
188
+ }
189
+
190
+ case 'list_watches': {
191
+ const filterSessionId = normalized.all === true ? undefined : sessionId
192
+ return JSON.stringify(listWatchJobs({ sessionId: filterSessionId || null }), null, 2)
193
+ }
194
+
195
+ case 'get_watch': {
196
+ const id = String(normalized.id || '').trim()
197
+ if (!id) return 'Error: id is required.'
198
+ const job = getWatchJob(id)
199
+ if (!job) return `Error: watch job "${id}" not found.`
200
+ return JSON.stringify(job, null, 2)
201
+ }
202
+
203
+ case 'cancel_watch': {
204
+ const id = String(normalized.id || '').trim()
205
+ if (!id) return 'Error: id is required.'
206
+ const job = cancelWatchJob(id)
207
+ if (!job) return `Error: watch job "${id}" not found.`
208
+ return JSON.stringify(job, null, 2)
209
+ }
210
+
79
211
  default:
80
212
  return `Error: Unknown action "${action}"`
81
213
  }
@@ -89,22 +221,29 @@ async function executeMonitorAction(args: any, bctx: { cwd: string }) {
89
221
  */
90
222
  const MonitorPlugin: Plugin = {
91
223
  name: 'Core Monitor',
92
- description: 'System observability: check resource usage, watch logs, and ping endpoints.',
224
+ description: 'System observability and durable watch jobs: inspect system state, monitor files/endpoints/tasks, and resume agents when conditions trigger.',
93
225
  hooks: {} as PluginHooks,
94
226
  tools: [
95
227
  {
96
228
  name: 'monitor_tool',
97
- description: 'Observe system health, log activity, or endpoint availability.',
229
+ description: 'Observe system health, inspect logs/endpoints, or create durable waits like wait_for_http, wait_for_file, wait_for_webhook, and wait_for_page_change.',
98
230
  parameters: {
99
231
  type: 'object',
100
232
  properties: {
101
- action: { type: 'string', enum: ['sys_info', 'watch_log', 'ping'] },
233
+ action: { type: 'string', enum: ['sys_info', 'watch_log', 'ping', 'create_watch', 'wait_until', 'wait_for_http', 'wait_for_file', 'wait_for_task', 'wait_for_webhook', 'wait_for_page_change', 'list_watches', 'get_watch', 'cancel_watch'] },
102
234
  target: { type: 'string', description: 'Log file path (for watch_log) or URL (for ping)' },
103
- limit: { type: 'number', description: 'Number of lines or bytes to retrieve' }
235
+ limit: { type: 'number', description: 'Number of lines or bytes to retrieve' },
236
+ watchType: { type: 'string', enum: ['time', 'http', 'file', 'task', 'webhook', 'page'] },
237
+ resumeMessage: { type: 'string', description: 'Message injected when the watch triggers and the agent wakes up.' },
238
+ regex: { type: 'string', description: 'Regex pattern used by file/page/http watchers.' },
104
239
  },
105
240
  required: ['action']
106
241
  },
107
- execute: async (args, context) => executeMonitorAction(args, { cwd: context.session.cwd || process.cwd() })
242
+ execute: async (args, context) => executeMonitorAction(args, {
243
+ cwd: context.session.cwd || process.cwd(),
244
+ sessionId: context.session.id,
245
+ agentId: context.session.agentId,
246
+ })
108
247
  }
109
248
  ]
110
249
  }
@@ -115,7 +254,11 @@ export function buildMonitorTools(bctx: ToolBuildContext): StructuredToolInterfa
115
254
  if (!bctx.hasPlugin('monitor')) return []
116
255
  return [
117
256
  tool(
118
- async (args) => executeMonitorAction(args, { cwd: bctx.cwd }),
257
+ async (args) => executeMonitorAction(args, {
258
+ cwd: bctx.cwd,
259
+ sessionId: bctx.ctx?.sessionId || undefined,
260
+ agentId: bctx.ctx?.agentId || undefined,
261
+ }),
119
262
  {
120
263
  name: 'monitor_tool',
121
264
  description: MonitorPlugin.tools![0].description,
@@ -1,4 +1,5 @@
1
1
  export type ToolArgsRecord = Record<string, unknown>
2
+ const NESTED_WRAPPER_KEYS = ['input', 'args', 'arguments', 'payload', 'parameters'] as const
2
3
 
3
4
  function parseRecordCandidate(value: unknown): ToolArgsRecord | null {
4
5
  if (!value) return null
@@ -26,22 +27,24 @@ function parseRecordCandidate(value: unknown): ToolArgsRecord | null {
26
27
  * as either objects or JSON strings.
27
28
  */
28
29
  export function normalizeToolInputArgs(rawArgs: ToolArgsRecord): ToolArgsRecord {
29
- const nestedSources: Array<ToolArgsRecord | null> = [
30
- parseRecordCandidate(rawArgs.input),
31
- parseRecordCandidate(rawArgs.args),
32
- parseRecordCandidate(rawArgs.arguments),
33
- parseRecordCandidate(rawArgs.payload),
34
- ]
35
-
36
30
  const normalized: ToolArgsRecord = {}
37
- for (const nested of nestedSources) {
38
- if (!nested) continue
39
- Object.assign(normalized, nested)
40
- }
31
+ const queue: ToolArgsRecord[] = [rawArgs]
32
+ const visited = new Set<ToolArgsRecord>()
33
+
34
+ while (queue.length > 0) {
35
+ const current = queue.shift()
36
+ if (!current || visited.has(current)) continue
37
+ visited.add(current)
41
38
 
42
- for (const [key, value] of Object.entries(rawArgs)) {
43
- if (value === undefined || value === null) continue
44
- normalized[key] = value
39
+ for (const key of NESTED_WRAPPER_KEYS) {
40
+ const nested = parseRecordCandidate(current[key])
41
+ if (nested) queue.push(nested)
42
+ }
43
+
44
+ for (const [key, value] of Object.entries(current)) {
45
+ if (value === undefined || value === null) continue
46
+ normalized[key] = value
47
+ }
45
48
  }
46
49
 
47
50
  return normalized