@swarmclawai/swarmclaw 1.2.1 → 1.2.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 (149) hide show
  1. package/README.md +16 -85
  2. package/bin/server-cmd.js +64 -1
  3. package/package.json +2 -2
  4. package/skills/coding-agent/SKILL.md +111 -0
  5. package/skills/github/SKILL.md +140 -0
  6. package/skills/nano-banana-pro/SKILL.md +62 -0
  7. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  8. package/skills/nano-pdf/SKILL.md +53 -0
  9. package/skills/openai-image-gen/SKILL.md +78 -0
  10. package/skills/openai-image-gen/scripts/gen.py +328 -0
  11. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  12. package/skills/skill-creator/SKILL.md +147 -0
  13. package/skills/skill-creator/scripts/init_skill.py +378 -0
  14. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  15. package/skills/summarize/SKILL.md +77 -0
  16. package/src/app/api/auth/route.ts +20 -5
  17. package/src/app/api/chats/[id]/devserver/route.ts +13 -19
  18. package/src/app/api/chats/[id]/messages/route.ts +13 -15
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/ip/route.ts +2 -2
  24. package/src/app/api/preview-server/route.ts +1 -1
  25. package/src/app/api/projects/[id]/route.ts +7 -46
  26. package/src/cli/server-cmd.test.js +74 -0
  27. package/src/components/chat/chat-area.tsx +45 -23
  28. package/src/components/chat/message-bubble.test.ts +35 -0
  29. package/src/components/chat/message-bubble.tsx +19 -9
  30. package/src/components/chat/message-list.tsx +37 -3
  31. package/src/components/input/chat-input.tsx +34 -14
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
  33. package/src/instrumentation.ts +1 -1
  34. package/src/lib/chat/assistant-render-id.ts +3 -0
  35. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  36. package/src/lib/chat/chat-streaming-state.ts +20 -8
  37. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  38. package/src/lib/chat/queued-message-queue.ts +11 -2
  39. package/src/lib/providers/cli-utils.test.ts +124 -0
  40. package/src/lib/server/activity/activity-log.ts +21 -0
  41. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  42. package/src/lib/server/agents/agent-cascade.ts +79 -59
  43. package/src/lib/server/agents/agent-registry.ts +3 -1
  44. package/src/lib/server/agents/agent-repository.ts +90 -0
  45. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  46. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  47. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  48. package/src/lib/server/agents/guardian.ts +2 -2
  49. package/src/lib/server/agents/main-agent-loop.ts +10 -3
  50. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  51. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  52. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  53. package/src/lib/server/agents/task-session.ts +3 -4
  54. package/src/lib/server/approvals/approval-repository.ts +30 -0
  55. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  56. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  57. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  58. package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
  59. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  60. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  61. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  62. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  63. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  64. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  65. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  66. package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
  67. package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
  68. package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
  69. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  70. package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
  71. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  72. package/src/lib/server/connectors/connector-repository.ts +58 -0
  73. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  74. package/src/lib/server/credentials/credential-repository.ts +7 -0
  75. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  76. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  77. package/src/lib/server/missions/mission-repository.ts +74 -0
  78. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  79. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  80. package/src/lib/server/missions/mission-service/context.ts +4 -0
  81. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  82. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  83. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  84. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  85. package/src/lib/server/missions/mission-service.test.ts +9 -2
  86. package/src/lib/server/missions/mission-service.ts +6 -2266
  87. package/src/lib/server/openclaw/deploy.test.ts +42 -3
  88. package/src/lib/server/openclaw/deploy.ts +26 -12
  89. package/src/lib/server/persistence/repository-utils.ts +154 -0
  90. package/src/lib/server/persistence/storage-context.ts +51 -0
  91. package/src/lib/server/persistence/transaction.ts +1 -0
  92. package/src/lib/server/projects/project-repository.ts +36 -0
  93. package/src/lib/server/projects/project-service.ts +79 -0
  94. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  95. package/src/lib/server/runtime/alert-dispatch.ts +1 -1
  96. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  97. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  98. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  99. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  100. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  101. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  102. package/src/lib/server/runtime/daemon-state.ts +3 -1470
  103. package/src/lib/server/runtime/estop-repository.ts +4 -0
  104. package/src/lib/server/runtime/estop.ts +3 -1
  105. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  106. package/src/lib/server/runtime/heartbeat-service.ts +55 -34
  107. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  108. package/src/lib/server/runtime/idle-window.ts +2 -2
  109. package/src/lib/server/runtime/network.ts +11 -0
  110. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  111. package/src/lib/server/runtime/queue/claims.ts +4 -0
  112. package/src/lib/server/runtime/queue/core.ts +2079 -0
  113. package/src/lib/server/runtime/queue/execution.ts +7 -0
  114. package/src/lib/server/runtime/queue/followups.ts +4 -0
  115. package/src/lib/server/runtime/queue/queries.ts +12 -0
  116. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  117. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  118. package/src/lib/server/runtime/queue-repository.ts +17 -0
  119. package/src/lib/server/runtime/queue.ts +5 -2061
  120. package/src/lib/server/runtime/run-ledger.ts +6 -5
  121. package/src/lib/server/runtime/run-repository.ts +73 -0
  122. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  123. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  124. package/src/lib/server/runtime/runtime-state.ts +99 -0
  125. package/src/lib/server/runtime/scheduler.ts +4 -2
  126. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  127. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  128. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  129. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  130. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  131. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  132. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  133. package/src/lib/server/runtime/session-run-manager.ts +72 -1377
  134. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  135. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  136. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  137. package/src/lib/server/sessions/session-repository.ts +85 -0
  138. package/src/lib/server/settings/settings-repository.ts +25 -0
  139. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  140. package/src/lib/server/skills/skill-discovery.ts +2 -2
  141. package/src/lib/server/skills/skill-repository.ts +14 -0
  142. package/src/lib/server/storage.ts +13 -24
  143. package/src/lib/server/tasks/task-repository.ts +54 -0
  144. package/src/lib/server/usage/usage-repository.ts +30 -0
  145. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  146. package/src/lib/strip-internal-metadata.test.ts +42 -41
  147. package/src/stores/use-chat-store.test.ts +54 -0
  148. package/src/stores/use-chat-store.ts +21 -5
  149. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Quick validation script for skills - minimal version
4
+ """
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ try:
12
+ import yaml
13
+ except ModuleNotFoundError:
14
+ yaml = None
15
+
16
+ MAX_SKILL_NAME_LENGTH = 64
17
+
18
+
19
+ def _extract_frontmatter(content: str) -> Optional[str]:
20
+ lines = content.splitlines()
21
+ if not lines or lines[0].strip() != "---":
22
+ return None
23
+ for i in range(1, len(lines)):
24
+ if lines[i].strip() == "---":
25
+ return "\n".join(lines[1:i])
26
+ return None
27
+
28
+
29
+ def _parse_simple_frontmatter(frontmatter_text: str) -> Optional[dict[str, str]]:
30
+ """
31
+ Minimal fallback parser used when PyYAML is unavailable.
32
+ Supports simple `key: value` mappings used by SKILL.md frontmatter.
33
+ """
34
+ parsed: dict[str, str] = {}
35
+ current_key: Optional[str] = None
36
+ for raw_line in frontmatter_text.splitlines():
37
+ stripped = raw_line.strip()
38
+ if not stripped or stripped.startswith("#"):
39
+ continue
40
+
41
+ is_indented = raw_line[:1].isspace()
42
+ if is_indented:
43
+ if current_key is None:
44
+ return None
45
+ current_value = parsed[current_key]
46
+ parsed[current_key] = (
47
+ f"{current_value}\n{stripped}" if current_value else stripped
48
+ )
49
+ continue
50
+
51
+ if ":" not in stripped:
52
+ return None
53
+ key, value = stripped.split(":", 1)
54
+ key = key.strip()
55
+ value = value.strip()
56
+ if not key:
57
+ return None
58
+ if (value.startswith('"') and value.endswith('"')) or (
59
+ value.startswith("'") and value.endswith("'")
60
+ ):
61
+ value = value[1:-1]
62
+ parsed[key] = value
63
+ current_key = key
64
+ return parsed
65
+
66
+
67
+ def validate_skill(skill_path):
68
+ """Basic validation of a skill"""
69
+ skill_path = Path(skill_path)
70
+
71
+ skill_md = skill_path / "SKILL.md"
72
+ if not skill_md.exists():
73
+ return False, "SKILL.md not found"
74
+
75
+ try:
76
+ content = skill_md.read_text(encoding="utf-8")
77
+ except OSError as e:
78
+ return False, f"Could not read SKILL.md: {e}"
79
+
80
+ frontmatter_text = _extract_frontmatter(content)
81
+ if frontmatter_text is None:
82
+ return False, "Invalid frontmatter format"
83
+ if yaml is not None:
84
+ try:
85
+ frontmatter = yaml.safe_load(frontmatter_text)
86
+ if not isinstance(frontmatter, dict):
87
+ return False, "Frontmatter must be a YAML dictionary"
88
+ except yaml.YAMLError as e:
89
+ return False, f"Invalid YAML in frontmatter: {e}"
90
+ else:
91
+ frontmatter = _parse_simple_frontmatter(frontmatter_text)
92
+ if frontmatter is None:
93
+ return (
94
+ False,
95
+ "Invalid YAML in frontmatter: unsupported syntax without PyYAML installed",
96
+ )
97
+
98
+ allowed_properties = {"name", "description", "license", "allowed-tools", "metadata"}
99
+
100
+ unexpected_keys = set(frontmatter.keys()) - allowed_properties
101
+ if unexpected_keys:
102
+ allowed = ", ".join(sorted(allowed_properties))
103
+ unexpected = ", ".join(sorted(unexpected_keys))
104
+ return (
105
+ False,
106
+ f"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}",
107
+ )
108
+
109
+ if "name" not in frontmatter:
110
+ return False, "Missing 'name' in frontmatter"
111
+ if "description" not in frontmatter:
112
+ return False, "Missing 'description' in frontmatter"
113
+
114
+ name = frontmatter.get("name", "")
115
+ if not isinstance(name, str):
116
+ return False, f"Name must be a string, got {type(name).__name__}"
117
+ name = name.strip()
118
+ if name:
119
+ if not re.match(r"^[a-z0-9-]+$", name):
120
+ return (
121
+ False,
122
+ f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)",
123
+ )
124
+ if name.startswith("-") or name.endswith("-") or "--" in name:
125
+ return (
126
+ False,
127
+ f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens",
128
+ )
129
+ if len(name) > MAX_SKILL_NAME_LENGTH:
130
+ return (
131
+ False,
132
+ f"Name is too long ({len(name)} characters). "
133
+ f"Maximum is {MAX_SKILL_NAME_LENGTH} characters.",
134
+ )
135
+
136
+ description = frontmatter.get("description", "")
137
+ if not isinstance(description, str):
138
+ return False, f"Description must be a string, got {type(description).__name__}"
139
+ description = description.strip()
140
+ if description:
141
+ if "<" in description or ">" in description:
142
+ return False, "Description cannot contain angle brackets (< or >)"
143
+ if len(description) > 1024:
144
+ return (
145
+ False,
146
+ f"Description is too long ({len(description)} characters). Maximum is 1024 characters.",
147
+ )
148
+
149
+ return True, "Skill is valid!"
150
+
151
+
152
+ if __name__ == "__main__":
153
+ if len(sys.argv) != 2:
154
+ print("Usage: python quick_validate.py <skill_directory>")
155
+ sys.exit(1)
156
+
157
+ valid, message = validate_skill(sys.argv[1])
158
+ print(message)
159
+ sys.exit(0 if valid else 1)
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: summarize
3
+ description: Summarize or extract text/transcripts from URLs, podcasts, YouTube videos, and local files using the summarize CLI. Use when asked to summarize a link, article, video, or file, or to transcribe a YouTube video.
4
+ metadata:
5
+ {
6
+ "openclaw":
7
+ {
8
+ "emoji": "🧾",
9
+ "requires": { "bins": ["summarize"] },
10
+ "install":
11
+ [
12
+ {
13
+ "id": "brew",
14
+ "kind": "brew",
15
+ "formula": "steipete/tap/summarize",
16
+ "bins": ["summarize"],
17
+ "label": "Install summarize (brew)",
18
+ },
19
+ ],
20
+ },
21
+ }
22
+ ---
23
+
24
+ # Summarize
25
+
26
+ Fast CLI to summarize URLs, local files, and YouTube links.
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ summarize "https://example.com" --model google/gemini-3-flash-preview
32
+ summarize "/path/to/file.pdf" --model google/gemini-3-flash-preview
33
+ summarize "https://youtu.be/dQw4w9WgXcQ" --youtube auto
34
+ ```
35
+
36
+ ## YouTube: Summary vs Transcript
37
+
38
+ Best-effort transcript extraction (URLs only):
39
+
40
+ ```bash
41
+ summarize "https://youtu.be/dQw4w9WgXcQ" --youtube auto --extract-only
42
+ ```
43
+
44
+ If the user asked for a transcript but it's very long, return a tight summary first, then ask which section or time range to expand.
45
+
46
+ ## Model + Keys
47
+
48
+ Set the API key for your chosen provider:
49
+
50
+ - OpenAI: `OPENAI_API_KEY`
51
+ - Anthropic: `ANTHROPIC_API_KEY`
52
+ - xAI: `XAI_API_KEY`
53
+ - Google: `GEMINI_API_KEY` (aliases: `GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY`)
54
+
55
+ Default model is `google/gemini-3-flash-preview` if none is set.
56
+
57
+ ## Useful Flags
58
+
59
+ - `--length short|medium|long|xl|xxl|<chars>` — control summary length
60
+ - `--max-output-tokens <count>` — hard token limit
61
+ - `--extract-only` — extract raw text without summarizing (URLs only)
62
+ - `--json` — machine-readable output
63
+ - `--firecrawl auto|off|always` — fallback extraction for blocked sites
64
+ - `--youtube auto` — Apify fallback if `APIFY_API_TOKEN` is set
65
+
66
+ ## Config
67
+
68
+ Optional config file: `~/.summarize/config.json`
69
+
70
+ ```json
71
+ { "model": "openai/gpt-5.2" }
72
+ ```
73
+
74
+ Optional services:
75
+
76
+ - `FIRECRAWL_API_KEY` for blocked sites
77
+ - `APIFY_API_TOKEN` for YouTube fallback
@@ -1,11 +1,14 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { safeParseBody } from '@/lib/server/safe-parse-body'
3
- import { getAccessKey, validateAccessKey, isFirstTimeSetup, markSetupComplete, replaceAccessKey } from '@/lib/server/storage'
3
+ import { log } from '@/lib/server/logger'
4
+ import { getAccessKey, validateAccessKey, isFirstTimeSetup, markSetupComplete, replaceAccessKey } from '@/lib/server/storage-auth'
4
5
  import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
5
6
  import { isProductionRuntime } from '@/lib/runtime/runtime-env'
6
7
  import { hmrSingleton } from '@/lib/shared-utils'
7
8
  export const dynamic = 'force-dynamic'
8
9
 
10
+ const TAG = 'auth-route'
11
+
9
12
  interface AuthAttemptEntry {
10
13
  count: number
11
14
  lockedUntil: number
@@ -75,6 +78,20 @@ function pruneExpiredEntries() {
75
78
  }
76
79
  }
77
80
 
81
+ function startDaemonAfterAuth() {
82
+ void import('@/lib/server/runtime/daemon-state')
83
+ .then(({ ensureDaemonStarted }) => {
84
+ try {
85
+ ensureDaemonStarted('api/auth:post')
86
+ } catch (err: unknown) {
87
+ log.error(TAG, 'Deferred daemon start failed', err)
88
+ }
89
+ })
90
+ .catch((err: unknown) => {
91
+ log.error(TAG, 'Failed to load daemon-state for deferred start', err)
92
+ })
93
+ }
94
+
78
95
  /** POST /api/auth — validate an access key */
79
96
  export async function POST(req: Request) {
80
97
  const rateLimitEnabled = isRateLimitEnabled()
@@ -98,8 +115,7 @@ export async function POST(req: Request) {
98
115
  replaceAccessKey(key.trim())
99
116
  markSetupComplete()
100
117
  if (rateLimitEnabled) authRateLimitMap.delete(clientIp)
101
- const { ensureDaemonStarted } = await import('@/lib/server/runtime/daemon-state')
102
- ensureDaemonStarted('api/auth:post')
118
+ startDaemonAfterAuth()
103
119
  return setAuthCookie(NextResponse.json({ ok: true }), req, key.trim())
104
120
  }
105
121
 
@@ -128,7 +144,6 @@ export async function POST(req: Request) {
128
144
  if (isFirstTimeSetup()) {
129
145
  markSetupComplete()
130
146
  }
131
- const { ensureDaemonStarted } = await import('@/lib/server/runtime/daemon-state')
132
- ensureDaemonStarted('api/auth:post')
147
+ startDaemonAfterAuth()
133
148
  return setAuthCookie(NextResponse.json({ ok: true }), req, key)
134
149
  }
@@ -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'