@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
@@ -6,6 +6,8 @@ import {
6
6
  getOpenClawLocalDeployStatus,
7
7
  getOpenClawRemoteDeployCollectionStatus,
8
8
  getOpenClawRemoteDeployStatus,
9
+ sanitizeLocalPort,
10
+ sanitizeSshConfig,
9
11
  } from './deploy'
10
12
 
11
13
  const GLOBAL_KEY = '__swarmclaw_openclaw_deploy__' as const
@@ -42,13 +44,17 @@ test('docker smart deploy bundle uses official image and provider-specific metad
42
44
 
43
45
  const envFile = bundle.files.find((file) => file.name === '.env')
44
46
  assert.ok(envFile)
45
- assert.match(envFile.content, /OPENCLAW_IMAGE=openclaw:latest/)
47
+ assert.match(envFile.content, /OPENCLAW_IMAGE=ghcr\.io\/openclaw\/openclaw:latest/)
46
48
  assert.match(envFile.content, /OPENCLAW_GATEWAY_TOKEN=test-token/)
47
49
 
50
+ const dockerCompose = bundle.files.find((file) => file.name === 'docker-compose.yml')
51
+ assert.ok(dockerCompose)
52
+ assert.match(dockerCompose.content, /image: \$\{OPENCLAW_IMAGE:-ghcr\.io\/openclaw\/openclaw:latest\}/)
53
+
48
54
  const cloudInit = bundle.files.find((file) => file.name === 'cloud-init.yaml')
49
55
  assert.ok(cloudInit)
50
56
  assert.match(cloudInit.content, /docker\.io/)
51
- assert.match(cloudInit.content, /docker pull "\$\{OPENCLAW_IMAGE:-openclaw:latest\}"/)
57
+ assert.match(cloudInit.content, /docker pull "\$\{OPENCLAW_IMAGE:-ghcr\.io\/openclaw\/openclaw:latest\}"/)
52
58
  assert.match(cloudInit.content, /\/opt\/openclaw\/docker-compose\.yml/)
53
59
 
54
60
  const caddyfile = bundle.files.find((file) => file.name === 'Caddyfile')
@@ -74,6 +80,39 @@ test('render bundle stays aligned with the official repo flow', () => {
74
80
  assert.match(bundle.runbook[0] || '', /official OpenClaw GitHub repo/i)
75
81
  })
76
82
 
83
+ test('remote bundle preserves low HTTP ports below 1024', () => {
84
+ const bundle = buildOpenClawDeployBundle({
85
+ template: 'docker',
86
+ target: 'gateway.example.com',
87
+ scheme: 'http',
88
+ port: 81,
89
+ })
90
+
91
+ assert.equal(bundle.endpoint, 'http://gateway.example.com:81/v1')
92
+ assert.equal(bundle.wsUrl, 'ws://gateway.example.com:81')
93
+ })
94
+
95
+ test('ssh config preserves port 22', () => {
96
+ const config = sanitizeSshConfig({
97
+ host: 'gateway.example.com',
98
+ port: 22,
99
+ })
100
+
101
+ assert.deepEqual(config, {
102
+ host: 'gateway.example.com',
103
+ user: 'root',
104
+ port: 22,
105
+ keyPath: null,
106
+ targetDir: '/opt/openclaw',
107
+ })
108
+ })
109
+
110
+ test('local managed deploy ports stay clamped to unprivileged values', () => {
111
+ assert.equal(sanitizeLocalPort(22), 1024)
112
+ assert.equal(sanitizeLocalPort('443'), 1024)
113
+ assert.equal(sanitizeLocalPort(18789), 18789)
114
+ })
115
+
77
116
  test('local deploy status exposes a sensible default endpoint before startup', () => {
78
117
  const status = getOpenClawLocalDeployStatus()
79
118
  const collection = getOpenClawLocalDeployCollectionStatus()
@@ -159,7 +198,7 @@ test('remote deploy collection preserves multiple remotes and targeted lookup',
159
198
  createdAt: 30,
160
199
  updatedAt: 40,
161
200
  lastError: null,
162
- lastSummary: 'Pulling openclaw:latest and recreating the OpenClaw stack on beta.example.com.',
201
+ lastSummary: 'Pulling ghcr.io/openclaw/openclaw:latest and recreating the OpenClaw stack on beta.example.com.',
163
202
  lastCommandPreview: 'ssh root@beta.example.com docker compose up -d',
164
203
  lastBackupPath: null,
165
204
  },
@@ -192,6 +192,7 @@ interface ExposureMeta {
192
192
 
193
193
  const DEFAULT_LOCAL_PORT = 18789
194
194
  const DEFAULT_REMOTE_PORT = 18789
195
+ const DEFAULT_OPENCLAW_IMAGE = 'ghcr.io/openclaw/openclaw:latest'
195
196
  const OC_DEPLOY_KEY = '__swarmclaw_openclaw_deploy__'
196
197
 
197
198
  const REMOTE_PROVIDER_META: Record<OpenClawRemoteDeployProvider, RemoteProviderMeta> = {
@@ -604,14 +605,27 @@ function buildLocalInstallCommand(port: number, token?: string | null, localId =
604
605
  return `${parts.join(' ')} && npx openclaw gateway start`
605
606
  }
606
607
 
607
- function sanitizePort(value: unknown, fallback = DEFAULT_LOCAL_PORT): number {
608
+ function parsePortNumber(value: unknown): number | null {
608
609
  const parsed = typeof value === 'number'
609
610
  ? value
610
611
  : typeof value === 'string'
611
612
  ? Number.parseInt(value, 10)
612
613
  : Number.NaN
613
- if (!Number.isFinite(parsed)) return fallback
614
- return Math.max(1024, Math.min(65535, Math.trunc(parsed)))
614
+ return Number.isFinite(parsed) ? parsed : null
615
+ }
616
+
617
+ function sanitizePortInRange(value: unknown, fallback: number, minimum: number): number {
618
+ const parsed = parsePortNumber(value)
619
+ if (parsed === null) return fallback
620
+ return Math.max(minimum, Math.min(65535, Math.trunc(parsed)))
621
+ }
622
+
623
+ export function sanitizeLocalPort(value: unknown, fallback = DEFAULT_LOCAL_PORT): number {
624
+ return sanitizePortInRange(value, fallback, 1024)
625
+ }
626
+
627
+ function sanitizeRemotePort(value: unknown, fallback = DEFAULT_REMOTE_PORT): number {
628
+ return sanitizePortInRange(value, fallback, 1)
615
629
  }
616
630
 
617
631
  function normalizeToken(value: unknown): string | null {
@@ -666,10 +680,10 @@ function normalizeExposurePreset(value: unknown, fallback?: OpenClawUseCaseTempl
666
680
  return useCase?.defaultExposure || 'private-lan'
667
681
  }
668
682
 
669
- function sanitizeSshConfig(input?: Partial<OpenClawSshConfig> | null): OpenClawSshConfig | null {
683
+ export function sanitizeSshConfig(input?: Partial<OpenClawSshConfig> | null): OpenClawSshConfig | null {
670
684
  const host = typeof input?.host === 'string' && input.host.trim() ? input.host.trim() : ''
671
685
  if (!host) return null
672
- const port = sanitizePort(input?.port, 22)
686
+ const port = sanitizePortInRange(input?.port, 22, 1)
673
687
  return {
674
688
  host,
675
689
  user: typeof input?.user === 'string' && input.user.trim() ? input.user.trim() : 'root',
@@ -1034,7 +1048,7 @@ export async function startOpenClawLocalDeploy(input?: {
1034
1048
  makePrimary?: boolean
1035
1049
  }): Promise<{ local: OpenClawLocalDeployStatus; locals: OpenClawLocalDeployStatus[]; token: string }> {
1036
1050
  const state = getRuntimeState()
1037
- const port = sanitizePort(input?.port, DEFAULT_LOCAL_PORT)
1051
+ const port = sanitizeLocalPort(input?.port, DEFAULT_LOCAL_PORT)
1038
1052
  const requestedLocalId = typeof input?.localId === 'string' && input.localId.trim()
1039
1053
  ? input.localId.trim()
1040
1054
  : null
@@ -1238,7 +1252,7 @@ function resolveHostBindAddress(useCase: OpenClawUseCaseTemplate, exposure: Open
1238
1252
  function buildDockerComposeFile(options: DockerBundleOptions): string {
1239
1253
  return `services:
1240
1254
  openclaw-gateway:
1241
- image: \${OPENCLAW_IMAGE:-openclaw:latest}
1255
+ image: \${OPENCLAW_IMAGE:-${DEFAULT_OPENCLAW_IMAGE}}
1242
1256
  environment:
1243
1257
  HOME: /home/node
1244
1258
  TERM: xterm-256color
@@ -1284,7 +1298,7 @@ function buildDockerComposeFile(options: DockerBundleOptions): string {
1284
1298
  }
1285
1299
 
1286
1300
  function buildDockerEnvFile(options: DockerBundleOptions): string {
1287
- return `OPENCLAW_IMAGE=openclaw:latest
1301
+ return `OPENCLAW_IMAGE=${DEFAULT_OPENCLAW_IMAGE}
1288
1302
  OPENCLAW_GATEWAY_TOKEN=${options.token}
1289
1303
  OPENCLAW_GATEWAY_BIND=lan
1290
1304
  OPENCLAW_HOST_BIND=${resolveHostBindAddress(options.useCase, options.exposure)}
@@ -1314,7 +1328,7 @@ if ! command -v docker >/dev/null 2>&1; then
1314
1328
  exit 1
1315
1329
  fi
1316
1330
 
1317
- docker pull "\${OPENCLAW_IMAGE:-openclaw:latest}"
1331
+ docker pull "\${OPENCLAW_IMAGE:-${DEFAULT_OPENCLAW_IMAGE}}"
1318
1332
  docker compose up -d
1319
1333
  if [ -f docker-compose.proxy.yml ]; then
1320
1334
  docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d
@@ -1365,7 +1379,7 @@ ${extraFiles ? `${extraFiles}
1365
1379
  ` : ''}runcmd:
1366
1380
  - mkdir -p /opt/openclaw/.openclaw /opt/openclaw/workspace /opt/openclaw/backups
1367
1381
  - systemctl enable --now docker
1368
- - bash -lc 'cd /opt/openclaw && docker pull "\${OPENCLAW_IMAGE:-openclaw:latest}"'
1382
+ - bash -lc 'cd /opt/openclaw && docker pull "\${OPENCLAW_IMAGE:-${DEFAULT_OPENCLAW_IMAGE}}"'
1369
1383
  - bash -lc 'cd /opt/openclaw && docker compose up -d'
1370
1384
  - bash -lc 'cd /opt/openclaw && if [ -f docker-compose.proxy.yml ]; then docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d; fi'
1371
1385
  final_message: "OpenClaw gateway bootstrap complete. Run: sudo docker compose -f /opt/openclaw/docker-compose.yml ps"
@@ -1597,7 +1611,7 @@ export function buildOpenClawDeployBundle(input?: {
1597
1611
  const template = input?.template || 'docker'
1598
1612
  const token = normalizeToken(input?.token) || generateOpenClawGatewayToken()
1599
1613
  const scheme = input?.scheme === 'http' ? 'http' : 'https'
1600
- const port = sanitizePort(input?.port, DEFAULT_REMOTE_PORT)
1614
+ const port = sanitizeRemotePort(input?.port, DEFAULT_REMOTE_PORT)
1601
1615
  const rawTarget = typeof input?.target === 'string' ? input.target.trim() : ''
1602
1616
  const endpoint = normalizeOpenClawEndpoint(ensureSchemeAndPort(rawTarget || 'openclaw.example.com', scheme, port))
1603
1617
  const wsUrl = deriveOpenClawWsUrl(endpoint)
@@ -1802,7 +1816,7 @@ export async function runOpenClawRemoteLifecycleAction(input?: {
1802
1816
  const sshConfig = sanitizeSshConfig(input?.ssh)
1803
1817
  if (!sshConfig) throw new Error('SSH host is required for remote lifecycle actions.')
1804
1818
  const remoteDir = sshConfig.targetDir || '/opt/openclaw'
1805
- const image = normalizeText(input?.image) || 'openclaw:latest'
1819
+ const image = normalizeText(input?.image) || DEFAULT_OPENCLAW_IMAGE
1806
1820
  const action = input?.action || 'restart'
1807
1821
  let remoteCommand = ''
1808
1822
  let summary = ''
@@ -0,0 +1,154 @@
1
+ import { perf } from '@/lib/server/runtime/perf'
2
+
3
+ export interface RecordRepository<
4
+ T,
5
+ ListOptions = void,
6
+ UpsertValue = T | Record<string, unknown>,
7
+ > {
8
+ get(id: string, options?: ListOptions): T | null
9
+ getMany(ids: string[], options?: ListOptions): Record<string, T>
10
+ list(options?: ListOptions): Record<string, T>
11
+ upsert(id: string, value: UpsertValue): void
12
+ upsertMany(entries: Array<[string, UpsertValue]>): void
13
+ patch(id: string, updater: (current: T | null) => T | null, options?: ListOptions): T | null
14
+ replace(data: Record<string, UpsertValue>): void
15
+ delete(id: string): void
16
+ }
17
+
18
+ interface RecordRepositoryOps<
19
+ T,
20
+ ListOptions = void,
21
+ UpsertValue = T | Record<string, unknown>,
22
+ > {
23
+ get(id: string, options?: ListOptions): T | null
24
+ list(options?: ListOptions): Record<string, T>
25
+ upsert(id: string, value: UpsertValue): void
26
+ upsertMany?: (entries: Array<[string, UpsertValue]>) => void
27
+ patch?: (id: string, updater: (current: T | null) => T | null) => T | null
28
+ replace?: (data: Record<string, UpsertValue>) => void
29
+ delete?: (id: string) => void
30
+ }
31
+
32
+ export interface SingletonRepository<
33
+ T,
34
+ SaveValue = T | Record<string, unknown>,
35
+ > {
36
+ get(): T
37
+ save(value: SaveValue): void
38
+ patch(updater: (current: T) => SaveValue): T
39
+ }
40
+
41
+ interface SingletonRepositoryOps<
42
+ T,
43
+ SaveValue = T | Record<string, unknown>,
44
+ > {
45
+ get(): T
46
+ save(value: SaveValue): void
47
+ }
48
+
49
+ function uniqueIds(ids: string[]): string[] {
50
+ const out: string[] = []
51
+ const seen = new Set<string>()
52
+ for (const id of ids) {
53
+ const normalized = typeof id === 'string' ? id.trim() : ''
54
+ if (!normalized || seen.has(normalized)) continue
55
+ seen.add(normalized)
56
+ out.push(normalized)
57
+ }
58
+ return out
59
+ }
60
+
61
+ export function createRecordRepository<
62
+ T,
63
+ ListOptions = void,
64
+ UpsertValue = T | Record<string, unknown>,
65
+ >(
66
+ name: string,
67
+ ops: RecordRepositoryOps<T, ListOptions, UpsertValue>,
68
+ ): RecordRepository<T, ListOptions, UpsertValue> {
69
+ return {
70
+ get(id, options) {
71
+ return perf.measureSync('repository', `${name}.get`, () => ops.get(id, options), { id })
72
+ },
73
+ getMany(ids, options) {
74
+ return perf.measureSync('repository', `${name}.getMany`, () => {
75
+ const result: Record<string, T> = {}
76
+ for (const id of uniqueIds(ids)) {
77
+ const item = ops.get(id, options)
78
+ if (item) result[id] = item
79
+ }
80
+ return result
81
+ }, { count: ids.length })
82
+ },
83
+ list(options) {
84
+ return perf.measureSync('repository', `${name}.list`, () => ops.list(options))
85
+ },
86
+ upsert(id, value) {
87
+ perf.measureSync('repository', `${name}.upsert`, () => ops.upsert(id, value), { id })
88
+ },
89
+ upsertMany(entries) {
90
+ perf.measureSync('repository', `${name}.upsertMany`, () => {
91
+ if (ops.upsertMany) {
92
+ ops.upsertMany(entries)
93
+ return
94
+ }
95
+ for (const [id, value] of entries) ops.upsert(id, value)
96
+ }, { count: entries.length })
97
+ },
98
+ patch(id, updater, options) {
99
+ return perf.measureSync('repository', `${name}.patch`, () => {
100
+ if (ops.patch) return ops.patch(id, updater)
101
+ const current = ops.get(id, options)
102
+ const next = updater(current)
103
+ if (next === null) {
104
+ if (!ops.delete) return null
105
+ ops.delete(id)
106
+ return null
107
+ }
108
+ ops.upsert(id, next as UpsertValue)
109
+ return next
110
+ }, { id })
111
+ },
112
+ replace(data) {
113
+ perf.measureSync('repository', `${name}.replace`, () => {
114
+ if (ops.replace) {
115
+ ops.replace(data)
116
+ return
117
+ }
118
+ const entries = Object.entries(data)
119
+ if (ops.upsertMany) ops.upsertMany(entries)
120
+ else for (const [id, value] of entries) ops.upsert(id, value)
121
+ }, { count: Object.keys(data).length })
122
+ },
123
+ delete(id) {
124
+ perf.measureSync('repository', `${name}.delete`, () => {
125
+ if (!ops.delete) return
126
+ ops.delete(id)
127
+ }, { id })
128
+ },
129
+ }
130
+ }
131
+
132
+ export function createSingletonRepository<
133
+ T,
134
+ SaveValue = T | Record<string, unknown>,
135
+ >(
136
+ name: string,
137
+ ops: SingletonRepositoryOps<T, SaveValue>,
138
+ ): SingletonRepository<T, SaveValue> {
139
+ return {
140
+ get() {
141
+ return perf.measureSync('repository', `${name}.get`, () => ops.get())
142
+ },
143
+ save(value) {
144
+ perf.measureSync('repository', `${name}.save`, () => ops.save(value))
145
+ },
146
+ patch(updater) {
147
+ return perf.measureSync('repository', `${name}.patch`, () => {
148
+ const next = updater(ops.get())
149
+ ops.save(next)
150
+ return ops.get()
151
+ })
152
+ },
153
+ }
154
+ }
@@ -0,0 +1,51 @@
1
+ import { withTransaction } from '@/lib/server/storage'
2
+
3
+ import { agentRepository } from '@/lib/server/agents/agent-repository'
4
+ import { approvalRepository } from '@/lib/server/approvals/approval-repository'
5
+ import { chatroomRepository } from '@/lib/server/chatrooms/chatroom-repository'
6
+ import { connectorRepository } from '@/lib/server/connectors/connector-repository'
7
+ import { missionEventRepository, missionRepository } from '@/lib/server/missions/mission-repository'
8
+ import { projectRepository } from '@/lib/server/projects/project-repository'
9
+ import { scheduleRepository } from '@/lib/server/schedules/schedule-repository'
10
+ import { sessionRepository } from '@/lib/server/sessions/session-repository'
11
+ import { settingsRepository } from '@/lib/server/settings/settings-repository'
12
+ import { taskRepository } from '@/lib/server/tasks/task-repository'
13
+ import { runEventRepository, runRepository } from '@/lib/server/runtime/run-repository'
14
+
15
+ export interface StorageTxContext {
16
+ agents: typeof agentRepository
17
+ approvals: typeof approvalRepository
18
+ chatrooms: typeof chatroomRepository
19
+ connectors: typeof connectorRepository
20
+ missions: typeof missionRepository
21
+ missionEvents: typeof missionEventRepository
22
+ projects: typeof projectRepository
23
+ runs: typeof runRepository
24
+ runEvents: typeof runEventRepository
25
+ schedules: typeof scheduleRepository
26
+ sessions: typeof sessionRepository
27
+ settings: typeof settingsRepository
28
+ tasks: typeof taskRepository
29
+ }
30
+
31
+ export function createStorageTxContext(): StorageTxContext {
32
+ return {
33
+ agents: agentRepository,
34
+ approvals: approvalRepository,
35
+ chatrooms: chatroomRepository,
36
+ connectors: connectorRepository,
37
+ missions: missionRepository,
38
+ missionEvents: missionEventRepository,
39
+ projects: projectRepository,
40
+ runs: runRepository,
41
+ runEvents: runEventRepository,
42
+ schedules: scheduleRepository,
43
+ sessions: sessionRepository,
44
+ settings: settingsRepository,
45
+ tasks: taskRepository,
46
+ }
47
+ }
48
+
49
+ export function withStorageTx<T>(fn: (ctx: StorageTxContext) => T): T {
50
+ return withTransaction(() => fn(createStorageTxContext()))
51
+ }
@@ -0,0 +1 @@
1
+ export { withTransaction } from '@/lib/server/storage'
@@ -0,0 +1,36 @@
1
+ import type { Project } from '@/types'
2
+
3
+ import {
4
+ deleteProject as deleteStoredProject,
5
+ loadProjects as loadStoredProjects,
6
+ saveProjects as saveStoredProjects,
7
+ upsertStoredItem,
8
+ } from '@/lib/server/storage'
9
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
10
+
11
+ export const projectRepository = createRecordRepository<Project>(
12
+ 'projects',
13
+ {
14
+ get(id) {
15
+ return (loadStoredProjects() as Record<string, Project>)[id] || null
16
+ },
17
+ list() {
18
+ return loadStoredProjects() as Record<string, Project>
19
+ },
20
+ upsert(id, value) {
21
+ upsertStoredItem('projects', id, value)
22
+ },
23
+ replace(data) {
24
+ saveStoredProjects(data)
25
+ },
26
+ delete(id) {
27
+ deleteStoredProject(id)
28
+ },
29
+ },
30
+ )
31
+
32
+ export const loadProjects = () => projectRepository.list()
33
+ export const loadProject = (id: string) => projectRepository.get(id)
34
+ export const saveProjects = (items: Record<string, Project | Record<string, unknown>>) => projectRepository.replace(items as Record<string, Project>)
35
+ export const upsertProject = (id: string, value: Project | Record<string, unknown>) => projectRepository.upsert(id, value as Project)
36
+ export const deleteProject = (id: string) => projectRepository.delete(id)
@@ -0,0 +1,79 @@
1
+ import type { Agent, BoardTask, Project, Schedule, Skill, StoredSecret } from '@/types'
2
+
3
+ import {
4
+ deleteProject as deleteStoredProject,
5
+ loadAgents,
6
+ loadProjects,
7
+ loadSchedules,
8
+ loadSecrets,
9
+ loadSkills,
10
+ loadTasks,
11
+ saveAgents,
12
+ saveProjects,
13
+ saveSchedules,
14
+ saveSecrets,
15
+ saveSkills,
16
+ saveTasks,
17
+ } from '@/lib/server/storage'
18
+ import { ensureProjectWorkspace, normalizeProjectPatchInput } from '@/lib/server/project-utils'
19
+ import { notify } from '@/lib/server/ws-hub'
20
+
21
+ type ProjectLinkedRecord = {
22
+ projectId?: string
23
+ }
24
+
25
+ function clearProjectId<T extends ProjectLinkedRecord>(
26
+ projectId: string,
27
+ load: () => Record<string, T>,
28
+ save: (items: Record<string, T>) => void,
29
+ topic: string,
30
+ ): void {
31
+ const items = load()
32
+ let changed = false
33
+ for (const item of Object.values(items)) {
34
+ if (item.projectId !== projectId) continue
35
+ item.projectId = undefined
36
+ changed = true
37
+ }
38
+ if (!changed) return
39
+ save(items)
40
+ notify(topic)
41
+ }
42
+
43
+ export function getProject(id: string): Project | null {
44
+ return loadProjects()[id] || null
45
+ }
46
+
47
+ export function updateProject(id: string, input: Record<string, unknown>): Project | null {
48
+ const projects = loadProjects()
49
+ const existing = projects[id]
50
+ if (!existing) return null
51
+
52
+ const patch = normalizeProjectPatchInput(input)
53
+ const nextProject: Project = {
54
+ ...existing,
55
+ ...patch,
56
+ id,
57
+ updatedAt: Date.now(),
58
+ }
59
+ projects[id] = nextProject
60
+ saveProjects(projects)
61
+ ensureProjectWorkspace(id, nextProject.name)
62
+ notify('projects')
63
+ return nextProject
64
+ }
65
+
66
+ export function deleteProjectAndDetachReferences(id: string): boolean {
67
+ if (!getProject(id)) return false
68
+
69
+ deleteStoredProject(id)
70
+ notify('projects')
71
+
72
+ clearProjectId<Agent>(id, loadAgents, saveAgents, 'agents')
73
+ clearProjectId<BoardTask>(id, loadTasks, saveTasks, 'tasks')
74
+ clearProjectId<Schedule>(id, loadSchedules, saveSchedules, 'schedules')
75
+ clearProjectId<Skill>(id, loadSkills, saveSkills, 'skills')
76
+ clearProjectId<StoredSecret>(id, loadSecrets, saveSecrets, 'secrets')
77
+
78
+ return true
79
+ }
@@ -3,13 +3,15 @@ import { after, before, describe, it } from 'node:test'
3
3
 
4
4
  let mod: typeof import('@/lib/server/protocols/protocol-normalization')
5
5
 
6
+ const savedBuildMode = process.env.SWARMCLAW_BUILD_MODE
6
7
  before(async () => {
7
8
  process.env.SWARMCLAW_BUILD_MODE = '1'
8
9
  mod = await import('@/lib/server/protocols/protocol-normalization')
9
10
  })
10
11
 
11
12
  after(() => {
12
- delete process.env.SWARMCLAW_BUILD_MODE
13
+ if (savedBuildMode === undefined) delete process.env.SWARMCLAW_BUILD_MODE
14
+ else process.env.SWARMCLAW_BUILD_MODE = savedBuildMode
13
15
  })
14
16
 
15
17
  describe('protocol-normalization', () => {
@@ -26,8 +28,8 @@ describe('protocol-normalization', () => {
26
28
  })
27
29
 
28
30
  it('artifact_exists with artifactKind', () => {
29
- const result = mod.normalizeCondition({ type: 'artifact_exists', artifactKind: 'report' })
30
- assert.deepEqual(result, { type: 'artifact_exists', artifactKind: 'report' })
31
+ const result = mod.normalizeCondition({ type: 'artifact_exists', artifactKind: 'summary' })
32
+ assert.deepEqual(result, { type: 'artifact_exists', artifactKind: 'summary' })
31
33
  })
32
34
 
33
35
  it('artifact_exists without artifactKind', () => {
@@ -53,7 +55,7 @@ describe('protocol-normalization', () => {
53
55
  type: 'all',
54
56
  conditions: [
55
57
  { type: 'summary_exists' },
56
- { type: 'artifact_exists', artifactKind: 'doc' },
58
+ { type: 'artifact_exists', artifactKind: 'notes' },
57
59
  ],
58
60
  })
59
61
  assert.equal(result!.type, 'all')
@@ -1,4 +1,4 @@
1
- import { loadSettings } from '@/lib/server/storage'
1
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
2
2
  import type { AppNotification } from '@/types'
3
3
  import { errorMessage } from '@/lib/shared-utils'
4
4
  import { log } from '@/lib/server/logger'
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults'
2
- import { loadSettings } from '@/lib/server/storage'
2
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
3
3
  import type { Session } from '@/types'
4
4
 
5
5
  const SYNTHETIC_HEALTH_SESSION_USERS = new Set(['workbench', 'comparison-bench'])