@swarmclawai/swarmclaw 1.2.1 → 1.2.2

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 (144) hide show
  1. package/README.md +9 -0
  2. package/package.json +2 -2
  3. package/skills/coding-agent/SKILL.md +111 -0
  4. package/skills/github/SKILL.md +140 -0
  5. package/skills/nano-banana-pro/SKILL.md +62 -0
  6. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  7. package/skills/nano-pdf/SKILL.md +53 -0
  8. package/skills/openai-image-gen/SKILL.md +78 -0
  9. package/skills/openai-image-gen/scripts/gen.py +328 -0
  10. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  11. package/skills/skill-creator/SKILL.md +147 -0
  12. package/skills/skill-creator/scripts/init_skill.py +378 -0
  13. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  14. package/skills/summarize/SKILL.md +77 -0
  15. package/src/app/api/auth/route.ts +20 -5
  16. package/src/app/api/chats/[id]/devserver/route.ts +13 -19
  17. package/src/app/api/chats/[id]/messages/route.ts +13 -15
  18. package/src/app/api/chats/[id]/route.ts +9 -10
  19. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  20. package/src/app/api/chats/messages-route.test.ts +8 -6
  21. package/src/app/api/chats/route.ts +9 -10
  22. package/src/app/api/ip/route.ts +2 -2
  23. package/src/app/api/preview-server/route.ts +1 -1
  24. package/src/app/api/projects/[id]/route.ts +7 -46
  25. package/src/components/chat/chat-area.tsx +45 -23
  26. package/src/components/chat/message-bubble.test.ts +35 -0
  27. package/src/components/chat/message-bubble.tsx +19 -9
  28. package/src/components/chat/message-list.tsx +37 -3
  29. package/src/components/input/chat-input.tsx +34 -14
  30. package/src/instrumentation.ts +1 -1
  31. package/src/lib/chat/assistant-render-id.ts +3 -0
  32. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  33. package/src/lib/chat/chat-streaming-state.ts +20 -8
  34. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  35. package/src/lib/chat/queued-message-queue.ts +11 -2
  36. package/src/lib/providers/cli-utils.test.ts +124 -0
  37. package/src/lib/server/activity/activity-log.ts +21 -0
  38. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  39. package/src/lib/server/agents/agent-cascade.ts +79 -59
  40. package/src/lib/server/agents/agent-registry.ts +3 -1
  41. package/src/lib/server/agents/agent-repository.ts +90 -0
  42. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  43. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  44. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  45. package/src/lib/server/agents/guardian.ts +2 -2
  46. package/src/lib/server/agents/main-agent-loop.ts +10 -3
  47. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  48. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  49. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  50. package/src/lib/server/agents/task-session.ts +3 -4
  51. package/src/lib/server/approvals/approval-repository.ts +30 -0
  52. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  53. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  54. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  55. package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
  56. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  57. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  58. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  59. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  60. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  61. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  62. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  63. package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
  64. package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
  65. package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
  66. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  67. package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
  68. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  69. package/src/lib/server/connectors/connector-repository.ts +58 -0
  70. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  71. package/src/lib/server/credentials/credential-repository.ts +7 -0
  72. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  73. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  74. package/src/lib/server/missions/mission-repository.ts +74 -0
  75. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  76. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  77. package/src/lib/server/missions/mission-service/context.ts +4 -0
  78. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  79. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  80. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  81. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  82. package/src/lib/server/missions/mission-service.test.ts +9 -2
  83. package/src/lib/server/missions/mission-service.ts +6 -2266
  84. package/src/lib/server/persistence/repository-utils.ts +154 -0
  85. package/src/lib/server/persistence/storage-context.ts +51 -0
  86. package/src/lib/server/persistence/transaction.ts +1 -0
  87. package/src/lib/server/projects/project-repository.ts +36 -0
  88. package/src/lib/server/projects/project-service.ts +79 -0
  89. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  90. package/src/lib/server/runtime/alert-dispatch.ts +1 -1
  91. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  92. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  93. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  94. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  95. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  96. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  97. package/src/lib/server/runtime/daemon-state.ts +3 -1470
  98. package/src/lib/server/runtime/estop-repository.ts +4 -0
  99. package/src/lib/server/runtime/estop.ts +3 -1
  100. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  101. package/src/lib/server/runtime/heartbeat-service.ts +55 -34
  102. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  103. package/src/lib/server/runtime/idle-window.ts +2 -2
  104. package/src/lib/server/runtime/network.ts +11 -0
  105. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  106. package/src/lib/server/runtime/queue/claims.ts +4 -0
  107. package/src/lib/server/runtime/queue/core.ts +2079 -0
  108. package/src/lib/server/runtime/queue/execution.ts +7 -0
  109. package/src/lib/server/runtime/queue/followups.ts +4 -0
  110. package/src/lib/server/runtime/queue/queries.ts +12 -0
  111. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  112. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  113. package/src/lib/server/runtime/queue-repository.ts +17 -0
  114. package/src/lib/server/runtime/queue.ts +5 -2061
  115. package/src/lib/server/runtime/run-ledger.ts +6 -5
  116. package/src/lib/server/runtime/run-repository.ts +73 -0
  117. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  118. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  119. package/src/lib/server/runtime/runtime-state.ts +99 -0
  120. package/src/lib/server/runtime/scheduler.ts +4 -2
  121. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  122. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  123. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  124. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  125. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  126. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  127. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  128. package/src/lib/server/runtime/session-run-manager.ts +72 -1377
  129. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  130. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  131. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  132. package/src/lib/server/sessions/session-repository.ts +85 -0
  133. package/src/lib/server/settings/settings-repository.ts +25 -0
  134. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  135. package/src/lib/server/skills/skill-discovery.ts +2 -2
  136. package/src/lib/server/skills/skill-repository.ts +14 -0
  137. package/src/lib/server/storage.ts +13 -24
  138. package/src/lib/server/tasks/task-repository.ts +54 -0
  139. package/src/lib/server/usage/usage-repository.ts +30 -0
  140. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  141. package/src/lib/strip-internal-metadata.test.ts +42 -41
  142. package/src/stores/use-chat-store.test.ts +54 -0
  143. package/src/stores/use-chat-store.ts +21 -5
  144. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -1,8 +1,10 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { spawn } from 'child_process'
3
- import { loadSessions, devServers, localIP } from '@/lib/server/storage'
4
3
  import { notFound } from '@/lib/server/collection-helpers'
5
4
  import { resolveDevServerLaunchDir } from '@/lib/server/runtime/devserver-launch'
5
+ import { clearDevServer, getDevServer, hasDevServer, registerDevServer, stopDevServer, updateDevServerUrl } from '@/lib/server/runtime/runtime-state'
6
+ import { localIP } from '@/lib/server/runtime/network'
7
+ import { listSessions } from '@/lib/server/sessions/session-repository'
6
8
  import { safeParseBody } from '@/lib/server/safe-parse-body'
7
9
  import { sleep } from '@/lib/shared-utils'
8
10
  import net from 'net'
@@ -56,22 +58,21 @@ async function startDevServer(id: string, session: { cwd: string }): Promise<Dev
56
58
  if (match) {
57
59
  const detectedPort = match[1]
58
60
  detectedUrl = `http://${localIP()}:${detectedPort}`
59
- const ds = devServers.get(id)
60
- if (ds) ds.url = detectedUrl
61
+ updateDevServerUrl(id, detectedUrl)
61
62
  }
62
63
  }
63
64
  }
64
65
 
65
66
  proc.stdout!.on('data', onData)
66
67
  proc.stderr!.on('data', onData)
67
- proc.on('close', () => { devServers.delete(id); log.info(TAG, `dev server stopped for ${id}`) })
68
- proc.on('error', () => devServers.delete(id))
68
+ proc.on('close', () => { clearDevServer(id); log.info(TAG, `dev server stopped for ${id}`) })
69
+ proc.on('error', () => clearDevServer(id))
69
70
 
70
- devServers.set(id, { proc, url: `http://${localIP()}:${port}` })
71
+ registerDevServer(id, { proc, url: `http://${localIP()}:${port}` })
71
72
  log.info(TAG, `starting dev server in ${launch.launchDir} (session cwd=${session.cwd})`)
72
73
 
73
74
  await sleep(4000)
74
- const ds = devServers.get(id)
75
+ const ds = getDevServer(id)
75
76
  if (!ds) {
76
77
  return {
77
78
  status: 502,
@@ -99,7 +100,7 @@ async function startDevServer(id: string, session: { cwd: string }): Promise<Dev
99
100
 
100
101
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
101
102
  const { id } = await params
102
- const sessions = loadSessions()
103
+ const sessions = listSessions()
103
104
  const session = sessions[id]
104
105
  if (!session) return notFound()
105
106
 
@@ -108,8 +109,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
108
109
  const { action } = body
109
110
 
110
111
  if (action === 'start') {
111
- if (devServers.has(id)) {
112
- const ds = devServers.get(id)!
112
+ if (hasDevServer(id)) {
113
+ const ds = getDevServer(id)!
113
114
  return NextResponse.json({ running: true, url: ds.url })
114
115
  }
115
116
 
@@ -126,18 +127,11 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
126
127
  return NextResponse.json(result.body, result.status ? { status: result.status } : undefined)
127
128
 
128
129
  } else if (action === 'stop') {
129
- if (devServers.has(id)) {
130
- const ds = devServers.get(id)!
131
- try { ds.proc.kill('SIGTERM') } catch {}
132
- if (typeof ds.proc.pid === 'number') {
133
- try { process.kill(-ds.proc.pid, 'SIGTERM') } catch {}
134
- }
135
- devServers.delete(id)
136
- }
130
+ stopDevServer(id)
137
131
  return NextResponse.json({ running: false })
138
132
 
139
133
  } else if (action === 'status') {
140
- return NextResponse.json({ running: devServers.has(id), url: devServers.get(id)?.url })
134
+ return NextResponse.json({ running: hasDevServer(id), url: getDevServer(id)?.url })
141
135
  }
142
136
 
143
137
  return NextResponse.json({ running: false })
@@ -1,27 +1,25 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadStoredItem, upsertStoredItem, active } from '@/lib/server/storage'
3
2
  import { notFound } from '@/lib/server/collection-helpers'
4
3
  import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
5
4
  import { appendSessionNote } from '@/lib/server/session-note'
6
5
  import { getSessionRunState } from '@/lib/server/runtime/session-run-manager'
7
- import type { Message, Session } from '@/types'
6
+ import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
7
+ import type { Message } from '@/types'
8
8
  import { safeParseBody } from '@/lib/server/safe-parse-body'
9
9
 
10
10
  export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
11
11
  const { id } = await params
12
- const session = loadStoredItem('sessions', id) as Session | null
12
+ const session = getSession(id)
13
13
  if (!session) return notFound()
14
14
  session.messages = Array.isArray(session.messages) ? session.messages : []
15
15
 
16
- // Check both persisted fields AND in-memory runtime state.
17
- // The persisted session doesn't have active/currentRunId set during runs
18
- // those are only computed at runtime from the active map and run ledger.
16
+ // Use persisted fields plus the run ledger. Process-local execution state is
17
+ // intentionally excluded here so stale registry entries do not block cleanup.
19
18
  const sessionClaimsActive = session.active === true
20
19
  || (typeof session.currentRunId === 'string' && session.currentRunId.trim().length > 0)
21
- || active.has(id)
22
20
  || !!getSessionRunState(id).runningRunId
23
21
  if (!sessionClaimsActive && materializeStreamingAssistantArtifacts(session.messages)) {
24
- upsertStoredItem('sessions', id, session)
22
+ saveSession(id, session)
25
23
  }
26
24
 
27
25
  const url = new URL(req.url)
@@ -63,7 +61,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
63
61
  if (error) return error
64
62
 
65
63
  if (body.kind === 'context-clear') {
66
- const session = loadStoredItem('sessions', id) as Session | null
64
+ const session = getSession(id)
67
65
  if (!session) return notFound()
68
66
 
69
67
  session.messages.push({
@@ -72,7 +70,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
72
70
  kind: 'context-clear',
73
71
  time: Date.now(),
74
72
  })
75
- upsertStoredItem('sessions', id, session)
73
+ saveSession(id, session)
76
74
  return NextResponse.json({ ok: true })
77
75
  }
78
76
 
@@ -84,7 +82,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
84
82
  kind: body.messageKind || 'system',
85
83
  })
86
84
  if (!inserted) {
87
- const session = loadStoredItem('sessions', id) as Session | null
85
+ const session = getSession(id)
88
86
  if (!session) return notFound()
89
87
  return NextResponse.json({ error: 'Note text is required' }, { status: 400 })
90
88
  }
@@ -98,7 +96,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
98
96
  const { id } = await params
99
97
  const { data: body, error } = await safeParseBody<{ messageIndex: number; bookmarked: boolean }>(req)
100
98
  if (error) return error
101
- const session = loadStoredItem('sessions', id) as Session | null
99
+ const session = getSession(id)
102
100
  if (!session) return notFound()
103
101
 
104
102
  const { messageIndex, bookmarked } = body
@@ -107,7 +105,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
107
105
  }
108
106
 
109
107
  session.messages[messageIndex].bookmarked = bookmarked
110
- upsertStoredItem('sessions', id, session)
108
+ saveSession(id, session)
111
109
  return NextResponse.json(session.messages[messageIndex])
112
110
  }
113
111
 
@@ -115,7 +113,7 @@ export async function DELETE(req: Request, { params }: { params: Promise<{ id: s
115
113
  const { id } = await params
116
114
  const { data: body, error } = await safeParseBody<{ messageIndex: number }>(req)
117
115
  if (error) return error
118
- const session = loadStoredItem('sessions', id) as Session | null
116
+ const session = getSession(id)
119
117
  if (!session) return notFound()
120
118
 
121
119
  const { messageIndex } = body
@@ -129,6 +127,6 @@ export async function DELETE(req: Request, { params }: { params: Promise<{ id: s
129
127
  }
130
128
 
131
129
  session.messages.splice(messageIndex, 1)
132
- upsertStoredItem('sessions', id, session)
130
+ saveSession(id, session)
133
131
  return NextResponse.json({ ok: true })
134
132
  }
@@ -1,10 +1,12 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSession, upsertSession, deleteSession, active, loadAgents } from '@/lib/server/storage'
2
+ import { loadAgents } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
5
5
  import { resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
6
6
  import { clearMainLoopStateForSession } from '@/lib/server/agents/main-agent-loop'
7
7
  import { cleanupSessionProcesses } from '@/lib/server/runtime/process-manager'
8
+ import { deleteSession, getSession, saveSession } from '@/lib/server/sessions/session-repository'
9
+ import { stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
8
10
  import { getSessionQueueSnapshot, getSessionRunState } from '@/lib/server/runtime/session-run-manager'
9
11
  import { normalizeCapabilitySelection } from '@/lib/capability-selection'
10
12
  import { enrichSessionWithMissionSummary } from '@/lib/server/missions/mission-service'
@@ -12,12 +14,12 @@ import { safeParseBody } from '@/lib/server/safe-parse-body'
12
14
 
13
15
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
14
16
  const { id } = await params
15
- const session = loadSession(id)
17
+ const session = getSession(id)
16
18
  if (!session) return notFound()
17
19
 
18
20
  const run = getSessionRunState(id)
19
21
  const queue = getSessionQueueSnapshot(id)
20
- session.active = active.has(id) || !!run.runningRunId
22
+ session.active = !!run.runningRunId
21
23
  session.queuedCount = queue.queueLength
22
24
  session.currentRunId = run.runningRunId || null
23
25
 
@@ -28,7 +30,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
28
30
  const { id } = await params
29
31
  const { data: updates, error } = await safeParseBody(req)
30
32
  if (error) return error
31
- const session = loadSession(id) as Record<string, unknown> | null
33
+ const session = getSession(id) as Record<string, unknown> | null
32
34
  if (!session) return notFound()
33
35
 
34
36
  if (updates.resetMainLoopState === true) {
@@ -142,17 +144,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
142
144
  if (updates.delegateResumeIds !== undefined) session.delegateResumeIds = updates.delegateResumeIds
143
145
  if (!Array.isArray(session.messages)) session.messages = []
144
146
 
145
- upsertSession(id, session)
147
+ saveSession(id, session)
146
148
  return NextResponse.json(enrichSessionWithMissionSummary(session as never))
147
149
  }
148
150
 
149
151
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
150
152
  const { id } = await params
151
- if (!loadSession(id)) return notFound()
152
- if (active.has(id)) {
153
- try { active.get(id)?.kill() } catch {}
154
- active.delete(id)
155
- }
153
+ if (!getSession(id)) return notFound()
154
+ stopActiveSessionProcess(id)
156
155
  cleanupSessionProcesses(id)
157
156
  deleteSession(id)
158
157
  return new NextResponse('OK')
@@ -1,19 +1,17 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
3
- import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
4
3
  import { cancelSessionRuns } from '@/lib/server/runtime/session-run-manager'
4
+ import { stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
5
+ import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
5
6
  import type { Session } from '@/types'
6
7
 
7
8
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
9
  const { id } = await params
9
10
  const cancel = cancelSessionRuns(id, 'Stopped by user')
10
- const session = loadStoredItem('sessions', id) as Session | null
11
+ const session = getSession(id) as Session | null
11
12
  if (session && Array.isArray(session.messages) && materializeStreamingAssistantArtifacts(session.messages)) {
12
- upsertStoredItem('sessions', id, session)
13
- }
14
- if (active.has(id)) {
15
- try { active.get(id)?.kill() } catch {}
16
- active.delete(id)
13
+ saveSession(id, session)
17
14
  }
15
+ stopActiveSessionProcess(id)
18
16
  return NextResponse.json({ ok: true, ...cancel })
19
17
  }
@@ -10,11 +10,13 @@ test('chat messages route materializes stale streaming artifacts even if runtime
10
10
  returnedText: string | null
11
11
  persistedStreaming: boolean | null
12
12
  persistedText: string | null
13
- }>(`
14
- const storageMod = await import('./src/lib/server/storage')
15
- const routeMod = await import('./src/app/api/chats/[id]/messages/route')
16
- const storage = storageMod.default || storageMod
17
- const route = routeMod.default || routeMod
13
+ }>(`
14
+ const storageMod = await import('./src/lib/server/storage')
15
+ const routeMod = await import('./src/app/api/chats/[id]/messages/route')
16
+ const runtimeStateMod = await import('./src/lib/server/runtime/runtime-state')
17
+ const storage = storageMod.default || storageMod
18
+ const route = routeMod.default || routeMod
19
+ const runtimeState = runtimeStateMod.default || runtimeStateMod
18
20
 
19
21
  storage.upsertStoredItem('sessions', 'session-stale', {
20
22
  id: 'session-stale',
@@ -37,7 +39,7 @@ test('chat messages route materializes stale streaming artifacts even if runtime
37
39
  ],
38
40
  })
39
41
 
40
- storage.active.set('session-stale', { kill() {} })
42
+ runtimeState.registerActiveSessionProcess('session-stale', { kill() {} })
41
43
 
42
44
  const response = await route.GET(
43
45
  new Request('http://local/api/chats/session-stale/messages'),
@@ -3,9 +3,11 @@ import { genId } from '@/lib/id'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
5
  import { perf } from '@/lib/server/runtime/perf'
6
- import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
6
+ import { loadAgents } from '@/lib/server/storage'
7
7
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
8
8
  import { notify } from '@/lib/server/ws-hub'
9
+ import { deleteSession, listSessions, replaceSessions } from '@/lib/server/sessions/session-repository'
10
+ import { stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
9
11
  import { getSessionQueueSnapshot, getSessionRunState } from '@/lib/server/runtime/session-run-manager'
10
12
  import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
11
13
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
@@ -25,11 +27,11 @@ export async function GET(req: Request) {
25
27
  const endPerf = perf.start('api', 'GET /api/chats')
26
28
  // Note: pruneThreadConnectorMirrors and materializeStreamingAssistantArtifacts
27
29
  // are handled by the daemon periodic health check, not on every list fetch.
28
- const sessions = loadSessions()
30
+ const sessions = listSessions()
29
31
  for (const id of Object.keys(sessions)) {
30
32
  const run = getSessionRunState(id)
31
33
  const queue = getSessionQueueSnapshot(id)
32
- sessions[id].active = active.has(id) || !!run.runningRunId
34
+ sessions[id].active = !!run.runningRunId
33
35
  sessions[id].queuedCount = queue.queueLength
34
36
  sessions[id].currentRunId = run.runningRunId || null
35
37
  }
@@ -59,14 +61,11 @@ export async function DELETE(req: Request) {
59
61
  if (!Array.isArray(ids) || !ids.length) {
60
62
  return new NextResponse('Missing ids', { status: 400 })
61
63
  }
62
- const sessions = loadSessions()
64
+ const sessions = listSessions()
63
65
  let deleted = 0
64
66
  for (const id of ids) {
65
67
  if (!sessions[id]) continue
66
- if (active.has(id)) {
67
- try { active.get(id)?.kill() } catch {}
68
- active.delete(id)
69
- }
68
+ stopActiveSessionProcess(id)
70
69
  deleteSession(id)
71
70
  deleted += 1
72
71
  }
@@ -83,7 +82,7 @@ export async function POST(req: Request) {
83
82
  else if (!cwd) cwd = WORKSPACE_DIR
84
83
 
85
84
  const id = body.id || genId()
86
- const sessions = loadSessions()
85
+ const sessions = listSessions()
87
86
  const agent = body.agentId ? loadAgents()[body.agentId] : null
88
87
  if (isAgentDisabled(agent)) {
89
88
  return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'start chats') }, { status: 409 })
@@ -162,7 +161,7 @@ export async function POST(req: Request) {
162
161
  sessions[id] = (body.provider || body.model || body.credentialId || body.apiEndpoint)
163
162
  ? nextSession
164
163
  : applyResolvedRoute(nextSession, resolvedRoute)
165
- saveSessions(sessions)
164
+ replaceSessions(sessions)
166
165
  notify('sessions')
167
166
  return NextResponse.json(sessions[id])
168
167
  }
@@ -1,8 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { localIP } from '@/lib/server/storage'
2
+ import { localIP } from '@/lib/server/runtime/network'
3
3
  export const dynamic = 'force-dynamic'
4
4
 
5
5
 
6
- export async function GET(_req: Request) {
6
+ export async function GET() {
7
7
  return NextResponse.json({ ip: localIP(), port: parseInt(process.env.PORT || '3000') })
8
8
  }
@@ -3,7 +3,7 @@ import { spawn, type ChildProcess } from 'child_process'
3
3
  import http from 'http'
4
4
  import fs from 'fs'
5
5
  import path from 'path'
6
- import { localIP } from '@/lib/server/storage'
6
+ import { localIP } from '@/lib/server/runtime/network'
7
7
  import { resolveDevServerLaunchDir } from '@/lib/server/runtime/devserver-launch'
8
8
  import { resolvePathWithinBaseDir } from '@/lib/server/path-utils'
9
9
  import { safeParseBody } from '@/lib/server/safe-parse-body'
@@ -1,65 +1,26 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills, loadSecrets, saveSecrets } from '@/lib/server/storage'
3
- import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
- import { ensureProjectWorkspace, normalizeProjectPatchInput } from '@/lib/server/project-utils'
5
- import { notify } from '@/lib/server/ws-hub'
2
+ import { notFound } from '@/lib/server/collection-helpers'
3
+ import { deleteProjectAndDetachReferences, getProject, updateProject } from '@/lib/server/projects/project-service'
6
4
  import { safeParseBody } from '@/lib/server/safe-parse-body'
7
5
 
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- const ops: CollectionOps<any> = { load: loadProjects, save: saveProjects, deleteFn: deleteProject, topic: 'projects' }
10
-
11
6
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
12
7
  const { id } = await params
13
- const projects = loadProjects()
14
- if (!projects[id]) return notFound()
15
- return NextResponse.json(projects[id])
8
+ const project = getProject(id)
9
+ if (!project) return notFound()
10
+ return NextResponse.json(project)
16
11
  }
17
12
 
18
13
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
19
14
  const { id } = await params
20
15
  const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
21
16
  if (error) return error
22
- const result = mutateItem(ops, id, (project) => {
23
- const patch = normalizeProjectPatchInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
24
- Object.assign(project, patch, { updatedAt: Date.now() })
25
- delete (project as Record<string, unknown>).id
26
- project.id = id
27
- return project
28
- })
17
+ const result = updateProject(id, body && typeof body === 'object' ? body : {})
29
18
  if (!result) return notFound()
30
- ensureProjectWorkspace(id, result.name)
31
19
  return NextResponse.json(result)
32
20
  }
33
21
 
34
22
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
35
23
  const { id } = await params
36
- if (!deleteItem(ops, id)) return notFound()
37
-
38
- // Clear projectId from referencing entities
39
- const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void, topic: string) => {
40
- const items = load()
41
- let changed = false
42
- for (const item of Object.values(items)) {
43
- if (item.projectId === id) {
44
- item.projectId = undefined
45
- changed = true
46
- }
47
- }
48
- if (changed) {
49
- save(items)
50
- notify(topic)
51
- }
52
- }
53
-
54
- clearProjectId(
55
- loadAgents as unknown as () => Record<string, Record<string, unknown>>,
56
- saveAgents as unknown as (data: Record<string, Record<string, unknown>>) => void,
57
- 'agents',
58
- )
59
- clearProjectId(loadTasks as unknown as () => Record<string, Record<string, unknown>>, saveTasks as unknown as (data: Record<string, Record<string, unknown>>) => void, 'tasks')
60
- clearProjectId(loadSchedules as unknown as () => Record<string, Record<string, unknown>>, saveSchedules as unknown as (data: Record<string, Record<string, unknown>>) => void, 'schedules')
61
- clearProjectId(loadSkills as unknown as () => Record<string, Record<string, unknown>>, saveSkills as unknown as (data: Record<string, Record<string, unknown>>) => void, 'skills')
62
- clearProjectId(loadSecrets as unknown as () => Record<string, Record<string, unknown>>, saveSecrets as unknown as (data: Record<string, Record<string, unknown>>) => void, 'secrets')
63
-
24
+ if (!deleteProjectAndDetachReferences(id)) return notFound()
64
25
  return NextResponse.json({ ok: true })
65
26
  }
@@ -27,6 +27,7 @@ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
27
27
  import { speak } from '@/lib/tts'
28
28
  import { api } from '@/lib/app/api-client'
29
29
  import { messagesDiffer } from '@/lib/chat/chat-streaming-state'
30
+ import { createAssistantRenderId } from '@/lib/chat/assistant-render-id'
30
31
  import { getSessionLastMessage } from '@/lib/chat/session-summary'
31
32
  import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-selection'
32
33
 
@@ -71,6 +72,36 @@ export function ChatArea() {
71
72
  const setPreviewContent = useChatStore((s) => s.setPreviewContent)
72
73
  const isDesktop = useMediaQuery('(min-width: 768px)')
73
74
 
75
+ const markSessionLocallyIdle = useCallback((targetSessionId: string) => {
76
+ const appState = useAppStore.getState()
77
+ const existing = appState.sessions[targetSessionId]
78
+ if (!existing) return
79
+ appState.updateSessionInStore({
80
+ ...existing,
81
+ active: false,
82
+ currentRunId: null,
83
+ })
84
+ }, [])
85
+
86
+ const startServerStreamingPlaceholder = useCallback((targetSessionId: string, phase: 'queued' | 'thinking' | 'connecting' = 'thinking') => {
87
+ useChatStore.setState((state) => {
88
+ const sameServerStream = state.streaming
89
+ && state.streamSource === 'server'
90
+ && state.streamingSessionId === targetSessionId
91
+
92
+ return {
93
+ streaming: true,
94
+ streamingSessionId: targetSessionId,
95
+ streamSource: 'server',
96
+ streamPhase: sameServerStream ? state.streamPhase : phase,
97
+ streamText: '',
98
+ displayText: '',
99
+ assistantRenderId: sameServerStream && state.assistantRenderId ? state.assistantRenderId : createAssistantRenderId(),
100
+ thinkingStartTime: sameServerStream && state.thinkingStartTime > 0 ? state.thinkingStartTime : Date.now(),
101
+ }
102
+ })
103
+ }, [])
104
+
74
105
  const currentAgent = useAppStore((s) => {
75
106
  const agentId = session?.agentId
76
107
  return agentId ? s.agents[agentId] ?? null : null
@@ -178,14 +209,12 @@ export function ChatArea() {
178
209
  })
179
210
 
180
211
  const sessionAtLoad = useAppStore.getState().sessions[requestedSessionId]
181
- if (sessionAtLoad?.active) {
182
- useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
183
- }
212
+ if (sessionAtLoad?.active) startServerStreamingPlaceholder(requestedSessionId)
184
213
 
185
214
  return () => {
186
215
  cancelled = true
187
216
  }
188
- }, [loadQueuedMessages, refreshSession, sessionId, setDevServer, setMessages])
217
+ }, [loadQueuedMessages, refreshSession, sessionId, setDevServer, setMessages, startServerStreamingPlaceholder])
189
218
 
190
219
  useEffect(() => {
191
220
  if (!sessionId || messagesLoading) return
@@ -195,9 +224,7 @@ export function ChatArea() {
195
224
  void refreshSession(requestedSessionId).then(() => {
196
225
  if (cancelled || selectActiveSessionId(useAppStore.getState()) !== requestedSessionId) return
197
226
  const refreshed = useAppStore.getState().sessions[requestedSessionId]
198
- if (refreshed?.active) {
199
- useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
200
- }
227
+ if (refreshed?.active) startServerStreamingPlaceholder(requestedSessionId)
201
228
  }).catch((err) => console.error('Failed to refresh session:', err))
202
229
 
203
230
  void devServer(requestedSessionId, 'status').then((r) => {
@@ -213,7 +240,7 @@ export function ChatArea() {
213
240
  cancelled = true
214
241
  window.clearTimeout(timer)
215
242
  }
216
- }, [messagesLoading, refreshSession, sessionId, setDevServer])
243
+ }, [messagesLoading, refreshSession, sessionId, setDevServer, startServerStreamingPlaceholder])
217
244
 
218
245
  useEffect(() => {
219
246
  if (!sessionId || messagesLoading) return
@@ -292,19 +319,12 @@ export function ChatArea() {
292
319
  && !chatState.streaming
293
320
  && chatState.streamingSessionId !== sessionId
294
321
  ) {
295
- useChatStore.setState({
296
- streaming: true,
297
- streamingSessionId: sessionId,
298
- streamSource: 'server',
299
- streamPhase: 'thinking',
300
- streamText: '',
301
- thinkingStartTime: Date.now(),
302
- })
322
+ startServerStreamingPlaceholder(sessionId)
303
323
  }
304
324
  } catch (err) {
305
325
  console.error('Failed to refresh queue:', err)
306
326
  }
307
- }, [loadQueuedMessages, sessionId])
327
+ }, [loadQueuedMessages, sessionId, startServerStreamingPlaceholder])
308
328
 
309
329
  // Subscribe to WS messages for this session — always subscribe when session exists,
310
330
  // only enable fallback polling when actively needed
@@ -316,7 +336,7 @@ export function ChatArea() {
316
336
  useWs(
317
337
  sessionId ? 'runs' : '',
318
338
  refreshQueue,
319
- sessionId && (isServerActive || queuedCount > 0) ? 10_000 : undefined,
339
+ sessionId && (isServerActive || queuedCount > 0) ? 2_500 : undefined,
320
340
  )
321
341
 
322
342
  // Listen for stream-end signal from the server — clears streaming state
@@ -325,11 +345,12 @@ export function ChatArea() {
325
345
  if (!sessionId) return
326
346
  const state = useChatStore.getState()
327
347
  if (state.streamSource === 'server' && state.streamingSessionId === sessionId) {
328
- useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
348
+ markSessionLocallyIdle(sessionId)
349
+ useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', assistantRenderId: null, streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
329
350
  void refreshMessages()
330
351
  void refreshSession(sessionId)
331
352
  }
332
- }, [sessionId, refreshMessages, refreshSession])
353
+ }, [markSessionLocallyIdle, sessionId, refreshMessages, refreshSession])
333
354
  useWs(sessionId ? `stream-end:${sessionId}` : '', handleStreamEnd)
334
355
 
335
356
  // Keep the local typing indicator aligned with the server's active state
@@ -338,7 +359,7 @@ export function ChatArea() {
338
359
  const state = useChatStore.getState()
339
360
  if (isServerActive) {
340
361
  if (!state.streaming && !state.streamText) {
341
- useChatStore.setState({ streaming: true, streamingSessionId: sessionId, streamSource: 'server', streamText: '' })
362
+ startServerStreamingPlaceholder(sessionId)
342
363
  }
343
364
  return
344
365
  }
@@ -349,9 +370,10 @@ export function ChatArea() {
349
370
  ) {
350
371
  // Server finished — clear all streaming state and fetch final messages
351
372
  fetchMessages(sessionId).then(setMessages).catch(() => {})
352
- useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
373
+ markSessionLocallyIdle(sessionId)
374
+ useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', assistantRenderId: null, streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
353
375
  }
354
- }, [isServerActive, sessionId, setMessages])
376
+ }, [isServerActive, markSessionLocallyIdle, sessionId, setMessages, startServerStreamingPlaceholder])
355
377
 
356
378
  // Poll browser status while session has browser tools
357
379
  const hasBrowserTool = getEnabledToolIds(session).includes('browser')
@@ -37,6 +37,41 @@ describe('MessageBubble', () => {
37
37
  assert.doesNotMatch(html, /streaming-cursor/)
38
38
  })
39
39
 
40
+ it('falls back to persisted streaming content when the live stream payload is temporarily empty', async () => {
41
+ const messageBubbleModule = await import('./message-bubble') as Record<string, unknown>
42
+ const MessageBubble = (
43
+ messageBubbleModule.MessageBubble
44
+ || (messageBubbleModule.default as { MessageBubble?: unknown } | undefined)?.MessageBubble
45
+ || (messageBubbleModule['module.exports'] as { MessageBubble?: unknown } | undefined)?.MessageBubble
46
+ ) as typeof import('./message-bubble').MessageBubble | undefined
47
+ assert.ok(MessageBubble)
48
+
49
+ const html = renderToStaticMarkup(
50
+ React.createElement(MessageBubble, {
51
+ message: {
52
+ role: 'assistant',
53
+ text: 'Recovered persisted partial text',
54
+ time: Date.now(),
55
+ kind: 'chat',
56
+ streaming: true,
57
+ },
58
+ assistantName: 'Hal2k-3',
59
+ agentName: 'Hal2k-3',
60
+ liveStream: {
61
+ active: true,
62
+ phase: 'responding',
63
+ toolName: '',
64
+ text: '',
65
+ thinking: '',
66
+ toolEvents: [],
67
+ },
68
+ }),
69
+ )
70
+
71
+ assert.match(html, /Recovered persisted partial text/)
72
+ assert.match(html, /streaming-cursor/)
73
+ })
74
+
40
75
  it('renders upload-linked screenshots inline without duplicating them at the bottom', async () => {
41
76
  const messageBubbleModule = await import('./message-bubble') as Record<string, unknown>
42
77
  const MessageBubble = (