@swarmclawai/swarmclaw 0.7.1 → 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 (237) hide show
  1. package/README.md +155 -150
  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 +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  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/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,311 @@
1
+ import type { Agent, AppSettings, Session, SessionResetMode, SessionResetType } from '@/types'
2
+
3
+ export interface ResolvedSessionResetPolicy {
4
+ type: SessionResetType
5
+ mode: SessionResetMode
6
+ idleTimeoutSec: number | null
7
+ maxAgeSec: number | null
8
+ dailyResetAt: string | null
9
+ timezone: string | null
10
+ }
11
+
12
+ export interface SessionFreshnessSnapshot {
13
+ fresh: boolean
14
+ reason?: string
15
+ policy: ResolvedSessionResetPolicy
16
+ idleExpiresAt: number | null
17
+ dailyBoundaryKey: string | null
18
+ }
19
+
20
+ const DEFAULT_POLICIES: Record<SessionResetType, ResolvedSessionResetPolicy> = {
21
+ direct: {
22
+ type: 'direct',
23
+ mode: 'idle',
24
+ idleTimeoutSec: 12 * 60 * 60,
25
+ maxAgeSec: 7 * 24 * 60 * 60,
26
+ dailyResetAt: null,
27
+ timezone: null,
28
+ },
29
+ group: {
30
+ type: 'group',
31
+ mode: 'idle',
32
+ idleTimeoutSec: 6 * 60 * 60,
33
+ maxAgeSec: 3 * 24 * 60 * 60,
34
+ dailyResetAt: null,
35
+ timezone: null,
36
+ },
37
+ thread: {
38
+ type: 'thread',
39
+ mode: 'idle',
40
+ idleTimeoutSec: 4 * 60 * 60,
41
+ maxAgeSec: 2 * 24 * 60 * 60,
42
+ dailyResetAt: null,
43
+ timezone: null,
44
+ },
45
+ main: {
46
+ type: 'main',
47
+ mode: 'daily',
48
+ idleTimeoutSec: 24 * 60 * 60,
49
+ maxAgeSec: 14 * 24 * 60 * 60,
50
+ dailyResetAt: '04:00',
51
+ timezone: null,
52
+ },
53
+ }
54
+
55
+ function parseIntBounded(value: unknown, min: number, max: number): number | null {
56
+ if (value === null || value === undefined || value === '') return null
57
+ const parsed = typeof value === 'number'
58
+ ? value
59
+ : typeof value === 'string'
60
+ ? Number.parseInt(value.trim(), 10)
61
+ : Number.NaN
62
+ if (!Number.isFinite(parsed)) return null
63
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
64
+ }
65
+
66
+ function normalizeMode(raw: unknown, fallback: SessionResetMode): SessionResetMode {
67
+ const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
68
+ return value === 'daily' ? 'daily' : value === 'idle' ? 'idle' : fallback
69
+ }
70
+
71
+ function normalizeTimeHHMM(raw: unknown): string | null {
72
+ if (typeof raw !== 'string') return null
73
+ const value = raw.trim()
74
+ const match = value.match(/^(\d{1,2}):(\d{2})$/)
75
+ if (!match) return null
76
+ const hours = Number.parseInt(match[1], 10)
77
+ const minutes = Number.parseInt(match[2], 10)
78
+ if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null
79
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null
80
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
81
+ }
82
+
83
+ function normalizeTimezone(raw: unknown): string | null {
84
+ if (typeof raw !== 'string') return null
85
+ const value = raw.trim()
86
+ return value || null
87
+ }
88
+
89
+ function getClockParts(date: Date, timezone?: string | null): { dateKey: string; minutes: number } | null {
90
+ try {
91
+ const formatter = new Intl.DateTimeFormat('en-CA', {
92
+ year: 'numeric',
93
+ month: '2-digit',
94
+ day: '2-digit',
95
+ hour: '2-digit',
96
+ minute: '2-digit',
97
+ hour12: false,
98
+ timeZone: timezone || undefined,
99
+ })
100
+ const parts = formatter.formatToParts(date)
101
+ const year = parts.find((part) => part.type === 'year')?.value
102
+ const month = parts.find((part) => part.type === 'month')?.value
103
+ const day = parts.find((part) => part.type === 'day')?.value
104
+ const hour = Number.parseInt(parts.find((part) => part.type === 'hour')?.value || '', 10)
105
+ const minute = Number.parseInt(parts.find((part) => part.type === 'minute')?.value || '', 10)
106
+ if (!year || !month || !day || !Number.isFinite(hour) || !Number.isFinite(minute)) return null
107
+ return {
108
+ dateKey: `${year}-${month}-${day}`,
109
+ minutes: hour * 60 + minute,
110
+ }
111
+ } catch {
112
+ return null
113
+ }
114
+ }
115
+
116
+ function boundaryKeyForNow(now: number, boundaryMinutes: number, timezone?: string | null): string | null {
117
+ const current = getClockParts(new Date(now), timezone)
118
+ if (!current) return null
119
+ if (current.minutes >= boundaryMinutes) return current.dateKey
120
+ const previous = getClockParts(new Date(now - 24 * 60 * 60 * 1000), timezone)
121
+ return previous?.dateKey || null
122
+ }
123
+
124
+ function rawField(
125
+ session: Partial<Session> | null | undefined,
126
+ overrides: Record<string, unknown> | undefined,
127
+ agent: Partial<Agent> | null | undefined,
128
+ settings: Partial<AppSettings> | null | undefined,
129
+ key: 'sessionResetMode' | 'sessionIdleTimeoutSec' | 'sessionMaxAgeSec' | 'sessionDailyResetAt' | 'sessionResetTimezone',
130
+ ): unknown {
131
+ if (session && session[key] !== undefined) return session[key]
132
+ if (overrides && overrides[key] !== undefined) return overrides[key]
133
+ if (agent && agent[key] !== undefined) return agent[key]
134
+ if (settings && settings[key] !== undefined) return settings[key]
135
+ return undefined
136
+ }
137
+
138
+ export function inferSessionResetType(
139
+ session: Partial<Session> | null | undefined,
140
+ opts?: { isGroup?: boolean | null; threadId?: string | null },
141
+ ): SessionResetType {
142
+ if ((session?.sessionType as string | undefined) === 'orchestrated') return 'main'
143
+ const threadId = opts?.threadId ?? session?.connectorContext?.threadId ?? null
144
+ if (threadId) return 'thread'
145
+ const isGroup = opts?.isGroup ?? session?.connectorContext?.isGroup ?? false
146
+ return isGroup ? 'group' : 'direct'
147
+ }
148
+
149
+ export function resolveSessionResetPolicy(params: {
150
+ session?: Partial<Session> | null
151
+ agent?: Partial<Agent> | null
152
+ settings?: Partial<AppSettings> | null
153
+ resetType?: SessionResetType
154
+ overrides?: Record<string, unknown>
155
+ }): ResolvedSessionResetPolicy {
156
+ const type = params.resetType ?? inferSessionResetType(params.session)
157
+ const defaults = DEFAULT_POLICIES[type]
158
+ return {
159
+ type,
160
+ mode: normalizeMode(
161
+ rawField(params.session, params.overrides, params.agent, params.settings, 'sessionResetMode'),
162
+ defaults.mode,
163
+ ),
164
+ idleTimeoutSec: parseIntBounded(
165
+ rawField(params.session, params.overrides, params.agent, params.settings, 'sessionIdleTimeoutSec'),
166
+ 0,
167
+ 180 * 24 * 60 * 60,
168
+ ) ?? defaults.idleTimeoutSec,
169
+ maxAgeSec: parseIntBounded(
170
+ rawField(params.session, params.overrides, params.agent, params.settings, 'sessionMaxAgeSec'),
171
+ 0,
172
+ 365 * 24 * 60 * 60,
173
+ ) ?? defaults.maxAgeSec,
174
+ dailyResetAt: normalizeTimeHHMM(
175
+ rawField(params.session, params.overrides, params.agent, params.settings, 'sessionDailyResetAt'),
176
+ ) ?? defaults.dailyResetAt,
177
+ timezone: normalizeTimezone(
178
+ rawField(params.session, params.overrides, params.agent, params.settings, 'sessionResetTimezone'),
179
+ ) ?? defaults.timezone,
180
+ }
181
+ }
182
+
183
+ export function evaluateSessionFreshness(params: {
184
+ session?: Partial<Session> | null
185
+ policy: ResolvedSessionResetPolicy
186
+ now?: number
187
+ }): SessionFreshnessSnapshot {
188
+ const now = typeof params.now === 'number' ? params.now : Date.now()
189
+ const session = params.session
190
+ const policy = params.policy
191
+ const messageCount = Array.isArray(session?.messages) ? session.messages.length : 0
192
+ const createdAt = typeof session?.createdAt === 'number' ? session.createdAt : now
193
+ const lastActiveAt = typeof session?.lastActiveAt === 'number' ? session.lastActiveAt : createdAt
194
+ const idleExpiresAt = typeof policy.idleTimeoutSec === 'number' && policy.idleTimeoutSec > 0
195
+ ? lastActiveAt + policy.idleTimeoutSec * 1000
196
+ : null
197
+
198
+ if (!session || messageCount === 0) {
199
+ return {
200
+ fresh: true,
201
+ policy,
202
+ idleExpiresAt,
203
+ dailyBoundaryKey: null,
204
+ }
205
+ }
206
+
207
+ if (idleExpiresAt !== null && now > idleExpiresAt) {
208
+ return {
209
+ fresh: false,
210
+ reason: `idle_timeout:${policy.idleTimeoutSec}`,
211
+ policy,
212
+ idleExpiresAt,
213
+ dailyBoundaryKey: null,
214
+ }
215
+ }
216
+
217
+ if (typeof policy.maxAgeSec === 'number' && policy.maxAgeSec > 0) {
218
+ const maxAgeMs = policy.maxAgeSec * 1000
219
+ if (now - createdAt > maxAgeMs) {
220
+ return {
221
+ fresh: false,
222
+ reason: `max_age:${policy.maxAgeSec}`,
223
+ policy,
224
+ idleExpiresAt,
225
+ dailyBoundaryKey: null,
226
+ }
227
+ }
228
+ }
229
+
230
+ if (policy.mode === 'daily' && policy.dailyResetAt) {
231
+ const boundary = normalizeTimeHHMM(policy.dailyResetAt)
232
+ if (boundary) {
233
+ const [hours, minutes] = boundary.split(':').map((value) => Number.parseInt(value, 10))
234
+ const boundaryMinutes = hours * 60 + minutes
235
+ const nowBoundaryKey = boundaryKeyForNow(now, boundaryMinutes, policy.timezone)
236
+ const lastActiveParts = getClockParts(new Date(lastActiveAt), policy.timezone)
237
+ if (
238
+ nowBoundaryKey
239
+ && lastActiveParts
240
+ && (
241
+ lastActiveParts.dateKey < nowBoundaryKey
242
+ || (lastActiveParts.dateKey === nowBoundaryKey && lastActiveParts.minutes < boundaryMinutes)
243
+ )
244
+ ) {
245
+ return {
246
+ fresh: false,
247
+ reason: `daily_reset:${policy.dailyResetAt}`,
248
+ policy,
249
+ idleExpiresAt,
250
+ dailyBoundaryKey: nowBoundaryKey,
251
+ }
252
+ }
253
+ return {
254
+ fresh: true,
255
+ policy,
256
+ idleExpiresAt,
257
+ dailyBoundaryKey: nowBoundaryKey,
258
+ }
259
+ }
260
+ }
261
+
262
+ return {
263
+ fresh: true,
264
+ policy,
265
+ idleExpiresAt,
266
+ dailyBoundaryKey: null,
267
+ }
268
+ }
269
+
270
+ export function resetSessionRuntime(
271
+ session: Session,
272
+ reason: string,
273
+ opts?: { now?: number },
274
+ ): number {
275
+ const now = typeof opts?.now === 'number' ? opts.now : Date.now()
276
+ const cleared = Array.isArray(session.messages) ? session.messages.length : 0
277
+
278
+ session.messages = []
279
+ session.claudeSessionId = null
280
+ session.codexThreadId = null
281
+ session.opencodeSessionId = null
282
+ session.delegateResumeIds = {
283
+ claudeCode: null,
284
+ codex: null,
285
+ opencode: null,
286
+ gemini: null,
287
+ }
288
+ session.createdAt = now
289
+ session.lastActiveAt = now
290
+ session.lastAutoMemoryAt = null
291
+ session.lastHeartbeatText = null
292
+ session.lastHeartbeatSentAt = null
293
+ session.conversationTone = undefined
294
+ session.lastSessionResetAt = now
295
+ session.lastSessionResetReason = reason
296
+
297
+ if (session.connectorContext) {
298
+ session.connectorContext = {
299
+ ...session.connectorContext,
300
+ lastResetAt: now,
301
+ lastResetReason: reason,
302
+ lastInboundMessageId: null,
303
+ lastInboundReplyToMessageId: null,
304
+ lastInboundThreadId: null,
305
+ lastOutboundMessageId: null,
306
+ lastOutboundAt: null,
307
+ }
308
+ }
309
+
310
+ return cleared
311
+ }
@@ -4,7 +4,9 @@ import { active, loadSessions } from './storage'
4
4
  import { executeSessionChatTurn, type ExecuteChatTurnResult } from './chat-execution'
5
5
  import { loadRuntimeSettings } from './runtime-settings'
6
6
  import { log } from './logger'
7
- import { handleMainLoopRunResult, type MainLoopFollowupRequest } from './main-agent-loop'
7
+ import { isInternalHeartbeatRun } from './heartbeat-source'
8
+ import { cleanupSessionBrowser } from './session-tools/web'
9
+ import { cancelDelegationJobsForParentSession } from './delegation-jobs'
8
10
 
9
11
  export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
10
12
  export type SessionQueueMode = 'followup' | 'steer' | 'collect'
@@ -122,6 +124,23 @@ function emitRunMeta(entry: QueueEntry, status: SessionRunStatus, extra?: Record
122
124
  })
123
125
  }
124
126
 
127
+ function markRunningEntryCancelled(entry: QueueEntry, reason: string) {
128
+ if (entry.run.status === 'cancelled') return
129
+ entry.run.status = 'cancelled'
130
+ entry.run.endedAt = now()
131
+ entry.run.error = reason
132
+ emitRunMeta(entry, 'cancelled', { reason })
133
+ }
134
+
135
+ function abortSessionRuntime(entry: QueueEntry, reason: string) {
136
+ markRunningEntryCancelled(entry, reason)
137
+ entry.signalController.abort()
138
+ try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
139
+ active.delete(entry.run.sessionId)
140
+ try { cleanupSessionBrowser(entry.run.sessionId) } catch { /* noop */ }
141
+ try { cancelDelegationJobsForParentSession(entry.run.sessionId, reason) } catch { /* noop */ }
142
+ }
143
+
125
144
  function executionKeyForSession(sessionId: string): string {
126
145
  return `session:${sessionId}`
127
146
  }
@@ -170,7 +189,7 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
170
189
  if (!queue.length) continue
171
190
  const keep: QueueEntry[] = []
172
191
  for (const entry of queue) {
173
- const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
192
+ const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
174
193
  if (!isHeartbeat) {
175
194
  keep.push(entry)
176
195
  continue
@@ -187,47 +206,15 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
187
206
  }
188
207
 
189
208
  for (const entry of state.runningByExecution.values()) {
190
- const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
209
+ const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
191
210
  if (!isHeartbeat) continue
192
211
  abortedRunning += 1
193
- entry.signalController.abort()
194
- try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
212
+ abortSessionRuntime(entry, reason)
195
213
  }
196
214
 
197
215
  return { cancelledQueued, abortedRunning }
198
216
  }
199
217
 
200
- function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupRequest) {
201
- const delayMs = Math.max(0, Math.trunc(followup.delayMs || 0))
202
- setTimeout(() => {
203
- try {
204
- const sessions = loadSessions()
205
- const session = sessions[sessionId]
206
- if (!session || !isMainMissionSession(session)) return
207
- enqueueSessionRun({
208
- sessionId,
209
- message: followup.message,
210
- internal: true,
211
- source: 'main-loop-followup',
212
- mode: 'collect',
213
- dedupeKey: followup.dedupeKey,
214
- })
215
- } catch (err: any) {
216
- log.warn('session-run', `Failed to enqueue main-loop followup for ${sessionId}`, err?.message || String(err))
217
- }
218
- }, delayMs)
219
- }
220
-
221
- export function isMainMissionSession(session: Record<string, unknown>): boolean {
222
- const id = typeof session.id === 'string' ? session.id.trim() : ''
223
- const name = typeof session.name === 'string' ? session.name.trim() : ''
224
- const sessionType = typeof session.sessionType === 'string' ? session.sessionType : ''
225
- if (id.startsWith('main-') || name === '__main__') return true
226
- // Only orchestrated thread sessions should receive autonomous main-loop followups.
227
- if (sessionType === 'orchestrated') return true
228
- return false
229
- }
230
-
231
218
  async function drainExecution(executionKey: string): Promise<void> {
232
219
  if (state.runningByExecution.has(executionKey)) return
233
220
  const q = queueForExecution(executionKey)
@@ -271,49 +258,25 @@ async function drainExecution(executionKey: string): Promise<void> {
271
258
  })
272
259
 
273
260
  const failed = !!result.error
274
- let followup: MainLoopFollowupRequest | null = null
275
- try {
276
- followup = handleMainLoopRunResult({
277
- sessionId: next.run.sessionId,
278
- message: next.message,
279
- internal: next.run.internal,
280
- source: next.run.source,
281
- resultText: result.text,
282
- error: result.error,
283
- toolEvents: result.toolEvents,
284
- inputTokens: result.inputTokens,
285
- outputTokens: result.outputTokens,
286
- estimatedCost: result.estimatedCost,
287
- })
288
- } catch (mainLoopErr: any) {
289
- log.warn('session-run', `Main-loop update failed for ${next.run.id}`, mainLoopErr?.message || String(mainLoopErr))
290
- }
291
-
292
- next.run.status = failed ? 'failed' : 'completed'
293
- next.run.endedAt = now()
294
- next.run.error = result.error
261
+ const aborted = next.signalController.signal.aborted
262
+ next.run.status = aborted ? 'cancelled' : (failed ? 'failed' : 'completed')
263
+ next.run.endedAt = next.run.endedAt || now()
264
+ next.run.error = aborted ? (next.run.error || 'Cancelled') : result.error
295
265
  next.run.resultPreview = result.text?.slice(0, 280)
296
266
  emitRunMeta(next, next.run.status, {
297
267
  persisted: result.persisted,
298
268
  hasText: !!result.text,
299
- error: result.error || null,
269
+ error: next.run.error || null,
300
270
  })
301
271
  log.info('session-run', `Run finished ${next.run.id}`, {
302
272
  sessionId: next.run.sessionId,
303
273
  status: next.run.status,
304
274
  persisted: result.persisted,
305
275
  hasText: !!result.text,
306
- error: result.error || null,
276
+ error: next.run.error || null,
307
277
  durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
308
278
  })
309
279
  next.resolve(result)
310
- if (!failed && followup) {
311
- scheduleMainLoopFollowup(next.run.sessionId, followup)
312
- log.info('session-run', `Queued main-loop followup after ${next.run.id}`, {
313
- sessionId: next.run.sessionId,
314
- delayMs: followup.delayMs,
315
- })
316
- }
317
280
  } catch (err: any) {
318
281
  const aborted = next.signalController.signal.aborted
319
282
  next.run.status = aborted ? 'cancelled' : 'failed'
@@ -326,19 +289,6 @@ async function drainExecution(executionKey: string): Promise<void> {
326
289
  error: next.run.error,
327
290
  durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
328
291
  })
329
- try {
330
- handleMainLoopRunResult({
331
- sessionId: next.run.sessionId,
332
- message: next.message,
333
- internal: next.run.internal,
334
- source: next.run.source,
335
- resultText: '',
336
- error: next.run.error,
337
- toolEvents: [],
338
- })
339
- } catch {
340
- // Main-loop bookkeeping failures should not affect queue execution.
341
- }
342
292
  next.reject(err instanceof Error ? err : new Error(next.run.error))
343
293
  } finally {
344
294
  if (runtimeTimer) clearTimeout(runtimeTimer)
@@ -530,7 +480,9 @@ export function getSessionRunState(sessionId: string): {
530
480
  const running = state.runningByExecution.get(executionKey)
531
481
  const queued = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId).length
532
482
  return {
533
- runningRunId: running?.run.sessionId === sessionId ? running.run.id : undefined,
483
+ runningRunId: (running?.run.sessionId === sessionId && running.run.status === 'running')
484
+ ? running.run.id
485
+ : undefined,
534
486
  queueLength: queued,
535
487
  }
536
488
  }
@@ -564,8 +516,7 @@ export function cancelSessionRuns(sessionId: string, reason = 'Cancelled'): { ca
564
516
  let cancelledRunning = false
565
517
  if (running && running.run.sessionId === sessionId) {
566
518
  cancelledRunning = true
567
- running.signalController.abort()
568
- try { active.get(sessionId)?.kill?.() } catch { /* noop */ }
519
+ abortSessionRuntime(running, reason)
569
520
  }
570
521
  const cancelledQueued = cancelPendingForSession(sessionId, reason)
571
522
  return { cancelledQueued, cancelledRunning }
@@ -0,0 +1,105 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { describe, it } from 'node:test'
5
+
6
+ const thisFile = new URL(import.meta.url).pathname
7
+ const toolsDir = path.dirname(thisFile)
8
+ const serverDir = path.resolve(toolsDir, '..')
9
+
10
+ function readToolSource(fileName: string): string {
11
+ return fs.readFileSync(path.join(toolsDir, fileName), 'utf-8')
12
+ }
13
+
14
+ function readServerSource(fileName: string): string {
15
+ return fs.readFileSync(path.join(serverDir, fileName), 'utf-8')
16
+ }
17
+
18
+ describe('browser workflow surface', () => {
19
+ it('advertises the higher-level browser actions in web.ts', () => {
20
+ const src = readToolSource('web.ts')
21
+ for (const action of ['read_page', 'extract_links', 'extract_form_fields', 'extract_table', 'fill_form', 'submit_form', 'scroll_until', 'download_file', 'complete_web_task']) {
22
+ assert.equal(src.includes(`'${action}'`), true, `web.ts should expose ${action}`)
23
+ }
24
+ })
25
+ })
26
+
27
+ describe('durable wait surface', () => {
28
+ it('advertises the durable wait actions in monitor.ts', () => {
29
+ const src = readToolSource('monitor.ts')
30
+ for (const action of ['wait_until', 'wait_for_http', 'wait_for_file', 'wait_for_task', 'wait_for_webhook', 'wait_for_page_change']) {
31
+ assert.equal(src.includes(`'${action}'`), true, `monitor.ts should expose ${action}`)
32
+ }
33
+ assert.equal(src.includes('createDurableWatch'), true)
34
+ })
35
+
36
+ it('routes schedule_wake through durable watch storage', () => {
37
+ const src = readToolSource('schedule.ts')
38
+ assert.equal(src.includes('createWatchJob'), true)
39
+ assert.equal(src.includes("type: 'time'"), true)
40
+ })
41
+ })
42
+
43
+ describe('delegation job handles', () => {
44
+ it('exposes subagent control actions', () => {
45
+ const src = readToolSource('subagent.ts')
46
+ for (const action of ['status', 'list', 'wait', 'cancel']) {
47
+ assert.equal(src.includes(`action === '${action}'`), true, `subagent.ts should handle ${action}`)
48
+ }
49
+ assert.equal(src.includes('createDelegationJob'), true)
50
+ })
51
+
52
+ it('builds delegate context from the invoking session and uses job records', () => {
53
+ const src = readToolSource('delegate.ts')
54
+ assert.equal(src.includes('buildDelegateContextFromSessionish'), true)
55
+ assert.equal(src.includes('createDelegationJob'), true)
56
+ assert.equal(src.includes('waitForDelegateJob'), true)
57
+ })
58
+
59
+ it('scheduler and daemon recover the durable autonomy jobs', () => {
60
+ const schedulerSrc = readServerSource('scheduler.ts')
61
+ const daemonSrc = readServerSource('daemon-state.ts')
62
+ assert.equal(schedulerSrc.includes('processDueWatchJobs'), true)
63
+ assert.equal(daemonSrc.includes('recoverStaleDelegationJobs'), true)
64
+ })
65
+ })
66
+
67
+ describe('primitive plugin surfaces', () => {
68
+ it('advertises mailbox and human-loop actions', () => {
69
+ const mailboxSrc = readToolSource('mailbox.ts')
70
+ const humanSrc = readToolSource('human-loop.ts')
71
+ for (const action of ['list_messages', 'list_threads', 'search_messages', 'read_message', 'download_attachment', 'reply', 'wait_for_email']) {
72
+ assert.equal(mailboxSrc.includes(`'${action}'`), true, `mailbox.ts should expose ${action}`)
73
+ }
74
+ for (const action of ['request_input', 'request_approval', 'wait_for_reply', 'wait_for_approval', 'list_mailbox', 'ack_mailbox', 'status']) {
75
+ assert.equal(humanSrc.includes(`'${action}'`), true, `human-loop.ts should expose ${action}`)
76
+ }
77
+ })
78
+
79
+ it('advertises document, extract, table, and crawl actions', () => {
80
+ const documentSrc = readToolSource('document.ts')
81
+ const extractSrc = readToolSource('extract.ts')
82
+ const tableSrc = readToolSource('table.ts')
83
+ const crawlSrc = readToolSource('crawl.ts')
84
+
85
+ for (const action of ['read', 'metadata', 'ocr', 'extract_tables', 'store', 'list', 'search', 'get', 'delete']) {
86
+ assert.equal(documentSrc.includes(`'${action}'`), true, `document.ts should expose ${action}`)
87
+ }
88
+ for (const action of ['extract_structured', 'summarize', 'status']) {
89
+ assert.equal(extractSrc.includes(`'${action}'`), true, `extract.ts should expose ${action}`)
90
+ }
91
+ for (const action of ['read', 'load_csv', 'load_xlsx', 'summarize', 'filter', 'sort', 'group', 'pivot', 'dedupe', 'join', 'write']) {
92
+ assert.equal(tableSrc.includes(`'${action}'`), true, `table.ts should expose ${action}`)
93
+ }
94
+ for (const action of ['crawl_site', 'follow_pagination', 'extract_sitemap', 'dedupe_pages', 'batch_extract']) {
95
+ assert.equal(crawlSrc.includes(`'${action}'`), true, `crawl.ts should expose ${action}`)
96
+ }
97
+ })
98
+
99
+ it('registers the primitive plugins in builtin-plugins.ts', () => {
100
+ const src = readServerSource('builtin-plugins.ts')
101
+ for (const moduleName of ['mailbox', 'human-loop', 'document', 'extract', 'table', 'crawl']) {
102
+ assert.equal(src.includes(`session-tools/${moduleName}`), true, `builtin-plugins.ts should import ${moduleName}`)
103
+ }
104
+ })
105
+ })