@swarmclawai/swarmclaw 1.1.9 → 1.2.1

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 (125) hide show
  1. package/README.md +10 -0
  2. package/next.config.ts +0 -1
  3. package/package.json +6 -3
  4. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  5. package/src/app/api/chats/[id]/devserver/route.ts +5 -2
  6. package/src/app/api/chats/[id]/messages/route.ts +7 -1
  7. package/src/app/api/credentials/[id]/route.ts +4 -1
  8. package/src/app/api/extensions/marketplace/route.ts +5 -2
  9. package/src/app/api/memory/maintenance/route.ts +5 -2
  10. package/src/app/api/preview-server/route.ts +14 -11
  11. package/src/app/api/system/status/route.ts +11 -0
  12. package/src/app/api/upload/route.ts +4 -1
  13. package/src/cli/index.js +7 -0
  14. package/src/cli/spec.js +1 -0
  15. package/src/components/agents/agent-files-editor.tsx +44 -32
  16. package/src/components/agents/personality-builder.tsx +13 -7
  17. package/src/components/agents/trash-list.tsx +1 -1
  18. package/src/components/chat/message-bubble.tsx +1 -0
  19. package/src/components/chat/message-list.tsx +25 -39
  20. package/src/components/chat/swarm-status-card.tsx +10 -3
  21. package/src/components/layout/daemon-indicator.tsx +7 -8
  22. package/src/components/layout/update-banner.tsx +8 -13
  23. package/src/components/logs/log-list.tsx +1 -1
  24. package/src/components/memory/memory-card.tsx +3 -1
  25. package/src/components/org-chart/mini-chat-bubble.tsx +2 -1
  26. package/src/components/org-chart/org-chart-view.tsx +4 -0
  27. package/src/components/projects/project-list.tsx +4 -2
  28. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  29. package/src/components/secrets/secret-sheet.tsx +1 -1
  30. package/src/components/secrets/secrets-list.tsx +1 -1
  31. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  32. package/src/components/shared/dir-browser.tsx +22 -18
  33. package/src/components/skills/skill-sheet.tsx +2 -3
  34. package/src/components/tasks/task-list.tsx +1 -1
  35. package/src/components/tasks/task-sheet.tsx +1 -1
  36. package/src/hooks/use-openclaw-gateway.ts +46 -27
  37. package/src/instrumentation.ts +10 -7
  38. package/src/lib/chat/chat.ts +18 -2
  39. package/src/lib/providers/anthropic.ts +6 -3
  40. package/src/lib/providers/claude-cli.ts +9 -3
  41. package/src/lib/providers/cli-utils.ts +15 -0
  42. package/src/lib/providers/codex-cli.ts +9 -3
  43. package/src/lib/providers/gemini-cli.ts +6 -2
  44. package/src/lib/providers/index.ts +4 -1
  45. package/src/lib/providers/ollama.ts +5 -2
  46. package/src/lib/providers/openai.ts +8 -5
  47. package/src/lib/providers/opencode-cli.ts +6 -2
  48. package/src/lib/server/agents/agent-registry.ts +20 -3
  49. package/src/lib/server/agents/main-agent-loop.ts +4 -3
  50. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  51. package/src/lib/server/chat-execution/chat-execution.ts +14 -2
  52. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  53. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  54. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  55. package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
  56. package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
  57. package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
  58. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  59. package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
  60. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  61. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  62. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  63. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  64. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  65. package/src/lib/server/connectors/discord.ts +10 -7
  66. package/src/lib/server/connectors/email.ts +17 -14
  67. package/src/lib/server/connectors/googlechat.ts +7 -4
  68. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  69. package/src/lib/server/connectors/matrix.ts +6 -3
  70. package/src/lib/server/connectors/openclaw.ts +20 -17
  71. package/src/lib/server/connectors/outbox.ts +4 -1
  72. package/src/lib/server/connectors/runtime-state.ts +19 -0
  73. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  74. package/src/lib/server/connectors/signal.ts +9 -6
  75. package/src/lib/server/connectors/slack.ts +13 -10
  76. package/src/lib/server/connectors/teams.ts +8 -5
  77. package/src/lib/server/connectors/telegram.ts +15 -12
  78. package/src/lib/server/connectors/whatsapp.ts +32 -29
  79. package/src/lib/server/embeddings.ts +4 -1
  80. package/src/lib/server/link-understanding.ts +4 -1
  81. package/src/lib/server/memory/memory-abstract.ts +59 -0
  82. package/src/lib/server/memory/memory-db.ts +40 -14
  83. package/src/lib/server/missions/mission-service.ts +6 -3
  84. package/src/lib/server/openclaw/gateway.ts +8 -5
  85. package/src/lib/server/project-utils.ts +13 -0
  86. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  87. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  88. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  89. package/src/lib/server/provider-health.ts +18 -0
  90. package/src/lib/server/query-expansion.ts +4 -1
  91. package/src/lib/server/runtime/alert-dispatch.ts +7 -6
  92. package/src/lib/server/runtime/daemon-state.ts +189 -50
  93. package/src/lib/server/runtime/heartbeat-service.ts +23 -0
  94. package/src/lib/server/runtime/idle-window.ts +4 -1
  95. package/src/lib/server/runtime/perf.ts +4 -1
  96. package/src/lib/server/runtime/process-manager.ts +7 -4
  97. package/src/lib/server/runtime/queue.ts +31 -28
  98. package/src/lib/server/runtime/scheduler.ts +9 -6
  99. package/src/lib/server/runtime/session-run-manager.ts +3 -0
  100. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  101. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  102. package/src/lib/server/session-tools/context.ts +14 -0
  103. package/src/lib/server/session-tools/discovery.ts +9 -6
  104. package/src/lib/server/session-tools/index.ts +3 -1
  105. package/src/lib/server/session-tools/platform.ts +1 -1
  106. package/src/lib/server/session-tools/subagent.ts +23 -2
  107. package/src/lib/server/session-tools/wallet.ts +4 -1
  108. package/src/lib/server/skills/clawhub-client.ts +4 -1
  109. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  110. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  111. package/src/lib/server/solana.ts +6 -0
  112. package/src/lib/server/storage-auth.ts +5 -5
  113. package/src/lib/server/storage-normalization.ts +4 -0
  114. package/src/lib/server/storage.ts +19 -8
  115. package/src/lib/server/tasks/task-followups.ts +4 -1
  116. package/src/lib/server/tool-loop-detection.ts +8 -3
  117. package/src/lib/server/tool-planning.ts +226 -0
  118. package/src/lib/server/tool-retry.ts +4 -3
  119. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  120. package/src/lib/server/ws-hub.ts +5 -2
  121. package/src/lib/strip-internal-metadata.test.ts +44 -4
  122. package/src/lib/strip-internal-metadata.ts +20 -6
  123. package/src/stores/use-approval-store.ts +7 -1
  124. package/src/stores/use-chat-store.ts +5 -1
  125. package/src/types/index.ts +6 -0
package/README.md CHANGED
@@ -190,6 +190,16 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
190
190
 
191
191
  ## Release Notes
192
192
 
193
+ ### v1.2.1 Highlights
194
+
195
+ - **System health endpoint**: new `/api/system/status` route returns lightweight health summary for external monitoring and uptime checks.
196
+ - **Memory abstracts**: ~100-token LLM summaries attached to memories for efficient proactive recall without loading full content.
197
+ - **Structured logging**: migrated 40+ files from `console.*` to the `log` module for consistent, level-aware logging across the codebase.
198
+ - **Lint baseline improvements**: reduced lint violations from 440 to 414 (-26) through targeted fixes across server and UI code.
199
+ - **Daemon housekeeping**: pruning for subagent processes, orchestrator state, connector sessions, and usage records to prevent resource leaks.
200
+ - **SKILL.md v2.0.0**: comprehensive CLI documentation covering 40+ command groups with examples and usage patterns.
201
+ - **New dev scripts**: added `type-check`, `test`, and `format` scripts to `package.json` for streamlined development workflows.
202
+
193
203
  ### v1.1.9 Highlights
194
204
 
195
205
  - **Docker build stability**: limit Next.js page data workers to 1 in build mode to prevent `SQLITE_BUSY` contention.
package/next.config.ts CHANGED
@@ -58,7 +58,6 @@ const nextConfig: NextConfig = {
58
58
  root: PROJECT_ROOT,
59
59
  },
60
60
  experimental: {
61
- turbopackFileSystemCacheForDev: false,
62
61
  // Limit build workers to 1 inside Docker to avoid SQLITE_BUSY contention
63
62
  // when multiple workers collect page data concurrently.
64
63
  ...(process.env.SWARMCLAW_BUILD_MODE ? { cpus: 1 } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -61,6 +61,9 @@
61
61
  "sandbox:build:browser": "docker build -f Dockerfile.sandbox-browser -t swarmclaw-sandbox-browser:bookworm-slim .",
62
62
  "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
63
63
  "benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
64
+ "type-check": "tsc --noEmit",
65
+ "test": "npm run test:cli && npm run test:setup && npm run test:openclaw",
66
+ "format": "eslint --fix",
64
67
  "lint": "eslint",
65
68
  "lint:fix": "eslint --fix",
66
69
  "lint:baseline": "node ./scripts/lint-baseline.mjs check",
@@ -104,7 +107,7 @@
104
107
  "langchain": "^1.2.30",
105
108
  "lucide-react": "^0.574.0",
106
109
  "mailparser": "^3.9.3",
107
- "next": "16.1.6",
110
+ "next": "16.1.7",
108
111
  "next-themes": "^0.4.6",
109
112
  "nodemailer": "^8.0.1",
110
113
  "openclaw": "^2026.2.26",
@@ -140,7 +143,7 @@
140
143
  },
141
144
  "devDependencies": {
142
145
  "eslint": "^9",
143
- "eslint-config-next": "16.1.6",
146
+ "eslint-config-next": "16.1.7",
144
147
  "tsx": "^4.20.6"
145
148
  },
146
149
  "optionalDependencies": {
@@ -3,6 +3,9 @@ import { execSync } from 'child_process'
3
3
  import { loadSessions } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { safeParseBody } from '@/lib/server/safe-parse-body'
6
+ import { log } from '@/lib/server/logger'
7
+
8
+ const TAG = 'api-deploy'
6
9
 
7
10
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
11
  const { id } = await params
@@ -21,16 +24,18 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
21
24
  try {
22
25
  execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, opts)
23
26
  committed = true
24
- } catch (ce: any) {
25
- if (!(ce.stdout || ce.stderr || '').includes('nothing to commit')) throw ce
27
+ } catch (ce: unknown) {
28
+ const ex = ce as { stdout?: string; stderr?: string }
29
+ if (!(ex.stdout || ex.stderr || '').includes('nothing to commit')) throw ce
26
30
  }
27
31
  execSync('git push 2>&1', opts)
28
- console.log(`[${id}] deployed: ${msg}`)
32
+ log.info(TAG, `deployed: ${msg}`)
29
33
  return NextResponse.json({ ok: true, output: committed ? 'Committed and pushed!' : 'Already committed — pushed to remote!' })
30
- } catch (e: any) {
31
- console.error(`[${id}] deploy error:`, e.message)
34
+ } catch (e: unknown) {
35
+ const ex = e as { stderr?: string; stdout?: string; message?: string }
36
+ log.error(TAG, `deploy error:`, ex.message)
32
37
  return NextResponse.json(
33
- { ok: false, error: (e.stderr || e.stdout || e.message).toString().slice(0, 300) },
38
+ { ok: false, error: (ex.stderr || ex.stdout || ex.message || 'Unknown error').toString().slice(0, 300) },
34
39
  { status: 500 },
35
40
  )
36
41
  }
@@ -6,6 +6,9 @@ import { resolveDevServerLaunchDir } from '@/lib/server/runtime/devserver-launch
6
6
  import { safeParseBody } from '@/lib/server/safe-parse-body'
7
7
  import { sleep } from '@/lib/shared-utils'
8
8
  import net from 'net'
9
+ import { log } from '@/lib/server/logger'
10
+
11
+ const TAG = 'api-devserver'
9
12
 
10
13
  interface DevServerStartResult {
11
14
  status?: number
@@ -61,11 +64,11 @@ async function startDevServer(id: string, session: { cwd: string }): Promise<Dev
61
64
 
62
65
  proc.stdout!.on('data', onData)
63
66
  proc.stderr!.on('data', onData)
64
- proc.on('close', () => { devServers.delete(id); console.log(`[${id}] dev server stopped`) })
67
+ proc.on('close', () => { devServers.delete(id); log.info(TAG, `dev server stopped for ${id}`) })
65
68
  proc.on('error', () => devServers.delete(id))
66
69
 
67
70
  devServers.set(id, { proc, url: `http://${localIP()}:${port}` })
68
- console.log(`[${id}] starting dev server in ${launch.launchDir} (session cwd=${session.cwd})`)
71
+ log.info(TAG, `starting dev server in ${launch.launchDir} (session cwd=${session.cwd})`)
69
72
 
70
73
  await sleep(4000)
71
74
  const ds = devServers.get(id)
@@ -1,8 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
2
+ import { loadStoredItem, upsertStoredItem, active } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
5
5
  import { appendSessionNote } from '@/lib/server/session-note'
6
+ import { getSessionRunState } from '@/lib/server/runtime/session-run-manager'
6
7
  import type { Message, Session } from '@/types'
7
8
  import { safeParseBody } from '@/lib/server/safe-parse-body'
8
9
 
@@ -12,8 +13,13 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
12
13
  if (!session) return notFound()
13
14
  session.messages = Array.isArray(session.messages) ? session.messages : []
14
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.
15
19
  const sessionClaimsActive = session.active === true
16
20
  || (typeof session.currentRunId === 'string' && session.currentRunId.trim().length > 0)
21
+ || active.has(id)
22
+ || !!getSessionRunState(id).runningRunId
17
23
  if (!sessionClaimsActive && materializeStreamingAssistantArtifacts(session.messages)) {
18
24
  upsertStoredItem('sessions', id, session)
19
25
  }
@@ -1,6 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadCredentials, deleteCredential } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
+ import { log } from '@/lib/server/logger'
5
+
6
+ const TAG = 'api-credentials'
4
7
 
5
8
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
9
  const { id: credId } = await params
@@ -9,6 +12,6 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
9
12
  return notFound()
10
13
  }
11
14
  deleteCredential(credId)
12
- console.log(`[credentials] deleted ${credId}`)
15
+ log.info(TAG, `deleted ${credId}`)
13
16
  return new NextResponse('OK')
14
17
  }
@@ -3,6 +3,9 @@ import { inferExtensionPublisherSourceFromUrl } from '@/lib/extension-sources'
3
3
  import { searchClawHub } from '@/lib/server/skills/clawhub-client'
4
4
  import type { ExtensionCatalogSource } from '@/types'
5
5
  import { errorMessage } from '@/lib/shared-utils'
6
+ import { log } from '@/lib/server/logger'
7
+
8
+ const TAG = 'api-extensions-marketplace'
6
9
 
7
10
  export const dynamic = 'force-dynamic'
8
11
 
@@ -71,7 +74,7 @@ export async function GET(req: Request) {
71
74
  })
72
75
  }
73
76
  } catch (err: unknown) {
74
- console.warn('[extensions-marketplace] Registry failed:', {
77
+ log.warn(TAG, 'Registry failed:', {
75
78
  registryUrl: registry.url,
76
79
  error: errorMessage(err),
77
80
  })
@@ -93,7 +96,7 @@ export async function GET(req: Request) {
93
96
  catalogSource: 'clawhub',
94
97
  })))
95
98
  } catch (err: unknown) {
96
- console.warn('[extensions-marketplace] ClawHub failed:', errorMessage(err))
99
+ log.warn(TAG, 'ClawHub failed:', errorMessage(err))
97
100
  }
98
101
 
99
102
  allExtensions.sort((a, b) => {
@@ -4,6 +4,9 @@ import { getMemoryDb } from '@/lib/server/memory/memory-db'
4
4
  import { loadSettings } from '@/lib/server/storage'
5
5
  import { syncAllSessionArchiveMemories } from '@/lib/server/memory/session-archive-memory'
6
6
  import { DATA_DIR } from '@/lib/server/data-dir'
7
+ import { log } from '@/lib/server/logger'
8
+
9
+ const TAG = 'api-memory-maintenance'
7
10
 
8
11
  function parseBool(value: unknown, fallback: boolean): boolean {
9
12
  if (typeof value === 'boolean') return value
@@ -46,7 +49,7 @@ export async function GET(req: Request) {
46
49
  archiveExportDir: path.join(DATA_DIR, 'session-archives'),
47
50
  })
48
51
  } catch (err: unknown) {
49
- console.error('[memory/maintenance] GET failed:', err)
52
+ log.error(TAG, 'GET failed:', err)
50
53
  return NextResponse.json({ ok: false, error: String((err as Error)?.message || err) }, { status: 500 })
51
54
  }
52
55
  }
@@ -77,7 +80,7 @@ export async function POST(req: Request) {
77
80
  ...result,
78
81
  })
79
82
  } catch (err: unknown) {
80
- console.error('[memory/maintenance] POST failed:', err)
83
+ log.error(TAG, 'POST failed:', err)
81
84
  return NextResponse.json({ ok: false, error: String((err as Error)?.message || err) }, { status: 500 })
82
85
  }
83
86
  }
@@ -8,6 +8,9 @@ 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'
10
10
  import { hmrSingleton, sleep } from '@/lib/shared-utils'
11
+ import { log } from '@/lib/server/logger'
12
+
13
+ const TAG = 'api-preview-server'
11
14
 
12
15
  // ---------------------------------------------------------------------------
13
16
  // MIME types for static server
@@ -181,7 +184,7 @@ function createStaticServer(dir: string): http.Server {
181
184
  async function startNpmServer(dir: string, command: string[], port: number, framework?: string): Promise<PreviewServer> {
182
185
  // Install deps if node_modules missing
183
186
  if (!fs.existsSync(path.join(dir, 'node_modules'))) {
184
- console.log(`[preview] Installing dependencies in ${dir}`)
187
+ log.info(TAG, `Installing dependencies in ${dir}`)
185
188
  await new Promise<void>((resolve, reject) => {
186
189
  const install = spawn('npm', ['install'], { cwd: dir, stdio: 'pipe' })
187
190
  install.on('close', (code) => code === 0 ? resolve() : reject(new Error(`npm install exited ${code}`)))
@@ -206,14 +209,14 @@ async function startNpmServer(dir: string, command: string[], port: number, fram
206
209
  env,
207
210
  })
208
211
 
209
- let log = ''
212
+ let processOutput = ''
210
213
  let detectedPort = port
211
214
  const urlRe = /https?:\/\/(?:localhost|0\.0\.0\.0|127\.0\.0\.1|[\d.]+):(\d+)/
212
215
 
213
216
  const onData = (chunk: Buffer) => {
214
217
  const text = chunk.toString()
215
- log += text
216
- if (log.length > 10000) log = log.slice(-5000)
218
+ processOutput += text
219
+ if (processOutput.length > 10000) processOutput = processOutput.slice(-5000)
217
220
  const match = text.match(urlRe)
218
221
  if (match) {
219
222
  detectedPort = parseInt(match[1], 10)
@@ -236,7 +239,7 @@ async function startNpmServer(dir: string, command: string[], port: number, fram
236
239
 
237
240
  proc.on('close', () => {
238
241
  servers.delete(dirKey(dir))
239
- console.log(`[preview] npm server stopped for ${dir}`)
242
+ log.info(TAG, `npm server stopped for ${dir}`)
240
243
  })
241
244
  proc.on('error', () => servers.delete(dirKey(dir)))
242
245
 
@@ -246,10 +249,10 @@ async function startNpmServer(dir: string, command: string[], port: number, fram
246
249
  await sleep(5000)
247
250
  if (proc.exitCode !== null) {
248
251
  servers.delete(dirKey(dir))
249
- throw new Error(`npm dev server exited early with code ${proc.exitCode}\n${log.slice(-4000)}`)
252
+ throw new Error(`npm dev server exited early with code ${proc.exitCode}\n${processOutput.slice(-4000)}`)
250
253
  }
251
254
  entry.port = detectedPort
252
- entry.log = log
255
+ entry.log = processOutput
253
256
 
254
257
  return entry
255
258
  }
@@ -295,7 +298,7 @@ export async function POST(req: Request) {
295
298
  const port = await findFreePort()
296
299
 
297
300
  if (project.type === 'npm' && project.devCommand) {
298
- console.log(`[preview] Detected ${project.framework} project in ${launch.launchDir}, running: ${project.devCommand.join(' ')}`)
301
+ log.info(TAG, `Detected ${project.framework} project in ${launch.launchDir}, running: ${project.devCommand.join(' ')}`)
299
302
  try {
300
303
  const entry = await startNpmServer(launch.launchDir, project.devCommand, port, project.framework)
301
304
  return NextResponse.json({
@@ -305,7 +308,7 @@ export async function POST(req: Request) {
305
308
  launchDir: launch.launchDir,
306
309
  })
307
310
  } catch (err: unknown) {
308
- console.error(`[preview] npm server failed, falling back to static:`, err)
311
+ log.error(TAG, 'npm server failed, falling back to static:', err)
309
312
  // Fall through to static server
310
313
  }
311
314
  }
@@ -319,7 +322,7 @@ export async function POST(req: Request) {
319
322
 
320
323
  const entry: PreviewServer = { type: 'static', server, port, dir, startedAt: Date.now(), log: '' }
321
324
  servers.set(key, entry)
322
- console.log(`[preview] Started static server for ${dir} on port ${port}`)
325
+ log.info(TAG, `Started static server for ${dir} on port ${port}`)
323
326
 
324
327
  return NextResponse.json(buildResponse(entry))
325
328
 
@@ -332,7 +335,7 @@ export async function POST(req: Request) {
332
335
  }
333
336
  if (srv.server) srv.server.close()
334
337
  servers.delete(key)
335
- console.log(`[preview] Stopped server for ${launch.launchDir}`)
338
+ log.info(TAG, `Stopped server for ${launch.launchDir}`)
336
339
  }
337
340
  return NextResponse.json({ running: false, dir: launch.launchDir })
338
341
 
@@ -0,0 +1,11 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getDaemonHealthSummary } from '@/lib/server/runtime/daemon-state'
3
+ import packageJson from '../../../../../package.json'
4
+
5
+ export async function GET() {
6
+ const summary = getDaemonHealthSummary()
7
+ return NextResponse.json({
8
+ ...summary,
9
+ version: packageJson.version,
10
+ })
11
+ }
@@ -3,6 +3,9 @@ import fs from 'fs'
3
3
  import path from 'path'
4
4
  import { genId } from '@/lib/id'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
+ import { log } from '@/lib/server/logger'
7
+
8
+ const TAG = 'api-upload'
6
9
 
7
10
  export async function POST(req: Request) {
8
11
  const filename = req.headers.get('x-filename') || 'image.png'
@@ -12,7 +15,7 @@ export async function POST(req: Request) {
12
15
 
13
16
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
14
17
  fs.writeFileSync(filePath, buf)
15
- console.log(`[upload] saved ${buf.length} bytes to ${filePath}`)
18
+ log.info(TAG, `saved ${buf.length} bytes to ${filePath}`)
16
19
 
17
20
  return NextResponse.json({ path: filePath, size: buf.length, url: `/api/uploads/${name}` })
18
21
  }
package/src/cli/index.js CHANGED
@@ -730,6 +730,13 @@ const COMMAND_GROUPS = [
730
730
  cmd('delete-many', 'DELETE', '/uploads', 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', { expectsJsonBody: true }),
731
731
  ],
732
732
  },
733
+ {
734
+ name: 'system-status',
735
+ description: 'Lightweight system health summary',
736
+ commands: [
737
+ cmd('get', 'GET', '/system/status', 'Get system health summary (safe for external monitors)'),
738
+ ],
739
+ },
733
740
  {
734
741
  name: 'usage',
735
742
  description: 'Usage and cost summary',
package/src/cli/spec.js CHANGED
@@ -496,6 +496,7 @@ const COMMAND_GROUPS = {
496
496
  description: 'System and version endpoints',
497
497
  commands: {
498
498
  ip: { description: 'Get local bind IP/port', method: 'GET', path: '/ip' },
499
+ status: { description: 'Get lightweight system health summary (safe for external monitors)', method: 'GET', path: '/system/status' },
499
500
  usage: { description: 'Get usage summary', method: 'GET', path: '/usage' },
500
501
  version: { description: 'Get local/remote git version info', method: 'GET', path: '/version' },
501
502
  update: { description: 'Update to latest stable release tag (fallback: main)', method: 'POST', path: '/version/update' },
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect, useState } from 'react'
3
+ import { useEffect, useState } from 'react'
4
4
  import { api } from '@/lib/app/api-client'
5
5
  import { errorMessage } from '@/lib/shared-utils'
6
6
  import { PersonalityBuilder } from './personality-builder'
@@ -8,6 +8,14 @@ import { PersonalityBuilder } from './personality-builder'
8
8
  const FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
9
9
  const GUIDED_FILES = new Set(['SOUL.md', 'IDENTITY.md', 'USER.md'])
10
10
 
11
+ function makeInitialFiles(): Record<string, FileState> {
12
+ const initial: Record<string, FileState> = {}
13
+ for (const f of FILES) {
14
+ initial[f] = { content: '', original: '', loading: true, saving: false }
15
+ }
16
+ return initial
17
+ }
18
+
11
19
  interface FileState {
12
20
  content: string
13
21
  original: string
@@ -22,45 +30,49 @@ interface Props {
22
30
 
23
31
  export function AgentFilesEditor({ agentId }: Props) {
24
32
  const [activeTab, setActiveTab] = useState<string>(FILES[0])
25
- const [files, setFiles] = useState<Record<string, FileState>>({})
33
+ const [files, setFiles] = useState<Record<string, FileState>>(makeInitialFiles)
26
34
  const [guidedMode, setGuidedMode] = useState(false)
27
35
 
28
- const loadFiles = useCallback(async () => {
29
- const initial: Record<string, FileState> = {}
30
- for (const f of FILES) {
31
- initial[f] = { content: '', original: '', loading: true, saving: false }
32
- }
33
- setFiles(initial)
36
+ // Reset to loading state when agentId changes
37
+ const [prevAgentId, setPrevAgentId] = useState(agentId)
38
+ if (agentId !== prevAgentId) {
39
+ setPrevAgentId(agentId)
40
+ setFiles(makeInitialFiles())
41
+ }
34
42
 
35
- try {
36
- const result = await api<Record<string, { content: string; error?: string }>>('GET', `/openclaw/agent-files?agentId=${agentId}`)
37
- setFiles((prev) => {
38
- const next = { ...prev }
39
- for (const [name, data] of Object.entries(result)) {
40
- next[name] = {
41
- content: data.content,
42
- original: data.content,
43
- loading: false,
44
- saving: false,
45
- error: data.error,
43
+ useEffect(() => {
44
+ let cancelled = false
45
+ api<Record<string, { content: string; error?: string }>>('GET', `/openclaw/agent-files?agentId=${agentId}`)
46
+ .then((result) => {
47
+ if (cancelled) return
48
+ setFiles((prev) => {
49
+ const next = { ...prev }
50
+ for (const [name, data] of Object.entries(result)) {
51
+ next[name] = {
52
+ content: data.content,
53
+ original: data.content,
54
+ loading: false,
55
+ saving: false,
56
+ error: data.error,
57
+ }
46
58
  }
47
- }
48
- return next
59
+ return next
60
+ })
49
61
  })
50
- } catch (err: unknown) {
51
- const message = errorMessage(err)
52
- setFiles((prev) => {
53
- const next = { ...prev }
54
- for (const f of FILES) {
55
- next[f] = { ...next[f], loading: false, error: message }
56
- }
57
- return next
62
+ .catch((err: unknown) => {
63
+ if (cancelled) return
64
+ const message = errorMessage(err)
65
+ setFiles((prev) => {
66
+ const next = { ...prev }
67
+ for (const f of FILES) {
68
+ next[f] = { ...next[f], loading: false, error: message }
69
+ }
70
+ return next
71
+ })
58
72
  })
59
- }
73
+ return () => { cancelled = true }
60
74
  }, [agentId])
61
75
 
62
- useEffect(() => { loadFiles() }, [loadFiles])
63
-
64
76
  const handleContentChange = (filename: string, content: string) => {
65
77
  setFiles((prev) => ({
66
78
  ...prev,
@@ -1,8 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useMemo, useState } from 'react'
3
+ import { useMemo, useState } from 'react'
4
4
  import type { PersonalityDraft } from '@/types'
5
- import { api } from '@/lib/app/api-client'
6
5
  import {
7
6
  parseIdentityMd, serializeIdentityMd,
8
7
  parseUserMd, serializeUserMd,
@@ -19,12 +18,12 @@ interface Props {
19
18
  const inputClass = 'w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors'
20
19
  const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1'
21
20
 
22
- export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSave }: Props) {
21
+ export function PersonalityBuilder({ fileType, content, onSave }: Props) {
23
22
  const [draft, setDraft] = useState<Record<string, string>>({})
24
23
  const [initialDraft, setInitialDraft] = useState<Record<string, string>>({})
25
24
  const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle')
26
25
 
27
- useEffect(() => {
26
+ const parsedContent = useMemo(() => {
28
27
  let parsed: Record<string, string> = {}
29
28
  if (fileType === 'IDENTITY.md') {
30
29
  const p = parseIdentityMd(content)
@@ -36,11 +35,18 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
36
35
  const p = parseSoulMd(content)
37
36
  parsed = { coreTruths: p.coreTruths || '', boundaries: p.boundaries || '', vibe: p.vibe || '', continuity: p.continuity || '' }
38
37
  }
39
- setDraft(parsed)
40
- setInitialDraft(parsed)
41
- setSaveState('idle')
38
+ return parsed
42
39
  }, [content, fileType])
43
40
 
41
+ // Reset form when content/fileType changes (render-time state adjustment)
42
+ const [syncKey, setSyncKey] = useState({ content, fileType })
43
+ if (content !== syncKey.content || fileType !== syncKey.fileType) {
44
+ setSyncKey({ content, fileType })
45
+ setDraft(parsedContent)
46
+ setInitialDraft(parsedContent)
47
+ setSaveState('idle')
48
+ }
49
+
44
50
  const isDirty = useMemo(() => {
45
51
  return Object.keys(draft).some((k) => draft[k] !== (initialDraft[k] ?? ''))
46
52
  }, [draft, initialDraft])
@@ -12,7 +12,7 @@ export function TrashList() {
12
12
  const loadAgents = useAppStore((s) => s.loadAgents)
13
13
  const [confirmPermanent, setConfirmPermanent] = useState<Agent | null>(null)
14
14
 
15
- useEffect(() => { loadTrashedAgents() }, [])
15
+ useEffect(() => { loadTrashedAgents() }, [loadTrashedAgents])
16
16
 
17
17
  const handleRestore = async (id: string) => {
18
18
  await api('POST', '/agents/trash', { id })
@@ -418,6 +418,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
418
418
  const effectiveThinking = !isUser
419
419
  ? (liveStreamActive ? (liveStream?.thinking?.trim() ? liveStream.thinking : undefined) : message.thinking)
420
420
  : undefined
421
+
421
422
  const sourceText = liveStreamActive ? (liveStream?.text || '') : message.text
422
423
  const connectorDeliveryTranscript = !isUser && message.kind === 'connector-delivery'
423
424
  ? (message.source?.deliveryTranscript?.trim() || '')
@@ -100,13 +100,6 @@ const LiveStreamBubble = memo(function LiveStreamBubble(props: LiveStreamBubbleP
100
100
  return <MessageBubble {...props} liveStream={liveStream} />
101
101
  })
102
102
 
103
- interface LastMessageMomentOverlayProps {
104
- messages: Message[]
105
- sessionId: string | null
106
- agentId?: string | null
107
- streaming: boolean
108
- children: (momentOverlay: React.ReactNode, currentMoment: { kind: string } | null) => React.ReactNode
109
- }
110
103
 
111
104
  function useLastMessageMoment(messages: Message[], sessionId: string | null, agentId: string | null | undefined, streaming: boolean) {
112
105
  type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; key: string; name: string; input: string }
@@ -117,31 +110,24 @@ function useLastMessageMoment(messages: Message[], sessionId: string | null, age
117
110
  setCurrentMoment({ kind: 'heartbeat' })
118
111
  })
119
112
 
120
- const prevToolKeyRef = useRef<string | null>(null)
121
- const seededMomentSessionRef = useRef<string | null>(null)
122
-
123
- useEffect(() => {
124
- if (!sessionId) {
125
- seededMomentSessionRef.current = null
126
- prevToolKeyRef.current = null
127
- setCurrentMoment(null)
128
- return
129
- }
130
-
131
- if (seededMomentSessionRef.current === sessionId) return
132
- seededMomentSessionRef.current = sessionId
133
- prevToolKeyRef.current = getLatestAssistantToolMoment(messages)?.key || null
134
- setCurrentMoment(null)
135
- }, [messages, sessionId])
136
-
137
- useEffect(() => {
138
- if (!sessionId || seededMomentSessionRef.current !== sessionId) return
113
+ const [trackedSessionId, setTrackedSessionId] = useState<string | null>(null)
114
+ const [trackedToolKey, setTrackedToolKey] = useState<string | null>(null)
115
+
116
+ // Render-time: respond to sessionId/messages changes (avoids set-state-in-effect)
117
+ if (sessionId !== trackedSessionId) {
118
+ setTrackedSessionId(sessionId)
119
+ const initialKey = sessionId
120
+ ? getLatestAssistantToolMoment(messages)?.key || null
121
+ : null
122
+ setTrackedToolKey(initialKey)
123
+ if (currentMoment !== null) setCurrentMoment(null)
124
+ } else if (sessionId) {
139
125
  const moment = getLatestAssistantToolMoment(messages)
140
- if (!moment) return
141
- if (moment.key === prevToolKeyRef.current) return
142
- prevToolKeyRef.current = moment.key
143
- setCurrentMoment({ kind: 'tool', key: moment.key, name: moment.name, input: moment.input })
144
- }, [messages, sessionId])
126
+ if (moment && moment.key !== trackedToolKey) {
127
+ setTrackedToolKey(moment.key)
128
+ setCurrentMoment({ kind: 'tool', key: moment.key, name: moment.name, input: moment.input })
129
+ }
130
+ }
145
131
 
146
132
  const momentOverlay = useMemo(() => {
147
133
  if (streaming || !currentMoment) return null
@@ -161,13 +147,6 @@ function useLastMessageMoment(messages: Message[], sessionId: string | null, age
161
147
  return { currentMoment, momentOverlay }
162
148
  }
163
149
 
164
- interface Props {
165
- messages: Message[]
166
- streaming: boolean
167
- connectorFilter?: string | null
168
- loading?: boolean
169
- }
170
-
171
150
  interface LiveThinkingLaneProps {
172
151
  show: boolean
173
152
  assistantName?: string
@@ -195,6 +174,13 @@ const LiveThinkingLane = memo(function LiveThinkingLane({
195
174
  )
196
175
  })
197
176
 
177
+ interface Props {
178
+ messages: Message[]
179
+ streaming: boolean
180
+ connectorFilter?: string | null
181
+ loading?: boolean
182
+ }
183
+
198
184
  export function MessageList({ messages, streaming, connectorFilter = null, loading = false }: Props) {
199
185
  const scrollRef = useRef<HTMLDivElement>(null)
200
186
  const [showScrollToBottom, setShowScrollToBottom] = useState(false)
@@ -232,7 +218,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
232
218
  const showGatewayOverlay = isOpenClaw && gatewayStatus === 'disconnected'
233
219
 
234
220
  // Moment overlay for last assistant message (heartbeat or tool events)
235
- const { currentMoment, momentOverlay: lastMomentOverlay } = useLastMessageMoment(messages, sessionId, agent?.id, streaming)
221
+ const { momentOverlay: lastMomentOverlay } = useLastMessageMoment(messages, sessionId, agent?.id, streaming)
236
222
 
237
223
  // Unread count tracking
238
224
  const unreadRef = useRef(0)