@swarmclawai/swarmclaw 1.3.6 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +16 -52
  2. package/next.config.ts +9 -4
  3. package/package.json +18 -10
  4. package/scripts/build-bootstrap-env.mjs +24 -0
  5. package/scripts/run-next-build.mjs +74 -0
  6. package/scripts/run-next-typegen.mjs +61 -0
  7. package/src/app/api/.well-known/agent-card/route.ts +46 -0
  8. package/src/app/api/a2a/route.ts +56 -0
  9. package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
  10. package/src/app/api/approvals/route.test.ts +29 -3
  11. package/src/app/api/approvals/route.ts +13 -7
  12. package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
  13. package/src/app/api/chats/[id]/chat/route.ts +24 -8
  14. package/src/app/api/chats/[id]/deploy/route.ts +2 -2
  15. package/src/app/api/chats/chat-route.test.ts +68 -0
  16. package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
  18. package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
  19. package/src/app/api/logs/route.test.ts +61 -0
  20. package/src/app/api/logs/route.ts +35 -0
  21. package/src/app/api/openclaw/sync/route.ts +1 -1
  22. package/src/app/api/swarmfeed/channels/route.ts +14 -0
  23. package/src/app/api/swarmfeed/posts/route.ts +60 -0
  24. package/src/app/api/swarmfeed/route.ts +37 -0
  25. package/src/app/api/tts/route.test.ts +82 -0
  26. package/src/app/api/tts/route.ts +13 -6
  27. package/src/app/api/tts/stream/route.ts +12 -5
  28. package/src/app/error.tsx +32 -0
  29. package/src/app/global-error.tsx +33 -0
  30. package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
  31. package/src/app/protocols/page.tsx +16 -7
  32. package/src/app/swarmfeed/page.tsx +7 -0
  33. package/src/cli/index.js +22 -0
  34. package/src/cli/spec.js +9 -0
  35. package/src/components/agents/agent-avatar.tsx +2 -5
  36. package/src/components/agents/agent-sheet.tsx +10 -0
  37. package/src/components/auth/access-key-gate.tsx +25 -0
  38. package/src/components/layout/error-boundary.tsx +12 -30
  39. package/src/components/layout/error-fallback.tsx +61 -0
  40. package/src/components/layout/sidebar-rail.tsx +52 -0
  41. package/src/components/protocols/builder/edge-editor.tsx +43 -0
  42. package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
  43. package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
  44. package/src/components/protocols/builder/edge-types/index.ts +3 -0
  45. package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
  46. package/src/components/protocols/builder/node-inspector.tsx +227 -0
  47. package/src/components/protocols/builder/node-palette.tsx +97 -0
  48. package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
  49. package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
  50. package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
  51. package/src/components/protocols/builder/node-types/index.ts +9 -0
  52. package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
  53. package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
  54. package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
  55. package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
  56. package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
  57. package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
  58. package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
  59. package/src/components/protocols/builder/run-overlay.tsx +29 -0
  60. package/src/components/protocols/builder/template-gallery.tsx +53 -0
  61. package/src/components/protocols/builder/validation-panel.tsx +57 -0
  62. package/src/components/skills/skills-workspace.tsx +1 -9
  63. package/src/features/protocols/builder/hooks/index.ts +2 -0
  64. package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
  65. package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
  66. package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
  67. package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
  68. package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
  69. package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
  70. package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
  71. package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
  72. package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
  73. package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
  74. package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
  75. package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
  76. package/src/features/swarmfeed/compose-post.tsx +139 -0
  77. package/src/features/swarmfeed/feed-page.tsx +136 -0
  78. package/src/features/swarmfeed/post-card.tsx +114 -0
  79. package/src/features/swarmfeed/queries.ts +28 -0
  80. package/src/lib/a2a/agent-card.ts +61 -0
  81. package/src/lib/a2a/auth.ts +54 -0
  82. package/src/lib/a2a/client.ts +133 -0
  83. package/src/lib/a2a/discovery.ts +116 -0
  84. package/src/lib/a2a/handlers.ts +176 -0
  85. package/src/lib/a2a/json-rpc-router.ts +38 -0
  86. package/src/lib/a2a/types.ts +95 -0
  87. package/src/lib/app/navigation.ts +1 -0
  88. package/src/lib/app/report-client-error.ts +52 -0
  89. package/src/lib/app/view-constants.ts +9 -1
  90. package/src/lib/providers/anthropic.ts +119 -107
  91. package/src/lib/providers/ollama.ts +34 -14
  92. package/src/lib/providers/openai.ts +154 -142
  93. package/src/lib/providers/openclaw.ts +3 -3
  94. package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
  95. package/src/lib/server/agents/main-agent-loop.ts +377 -41
  96. package/src/lib/server/chat-execution/chat-execution.ts +12 -7
  97. package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
  98. package/src/lib/server/connectors/swarmdock.ts +1 -1
  99. package/src/lib/server/extensions.ts +11 -0
  100. package/src/lib/server/messages/message-repository.ts +31 -0
  101. package/src/lib/server/openclaw/sync.ts +4 -4
  102. package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
  103. package/src/lib/server/protocols/protocol-normalization.ts +1 -0
  104. package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
  105. package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
  106. package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
  107. package/src/lib/server/protocols/protocol-types.ts +1 -0
  108. package/src/lib/server/provider-health.ts +19 -3
  109. package/src/lib/server/safe-parse-body.test.ts +32 -0
  110. package/src/lib/server/safe-parse-body.ts +20 -3
  111. package/src/lib/server/session-tools/delegate.ts +151 -77
  112. package/src/lib/server/storage-auth.ts +10 -2
  113. package/src/lib/server/storage-normalization.ts +11 -0
  114. package/src/lib/server/storage.ts +113 -4
  115. package/src/lib/server/working-state/service.test.ts +2 -3
  116. package/src/lib/server/working-state/service.ts +37 -6
  117. package/src/lib/swarmfeed-client.ts +157 -0
  118. package/src/lib/validation/schemas.ts +1 -1
  119. package/src/stores/slices/data-slice.ts +3 -0
  120. package/src/stores/use-approval-store.ts +4 -1
  121. package/src/types/agent.ts +31 -1
  122. package/src/types/index.ts +1 -0
  123. package/src/types/protocol.ts +19 -0
  124. package/src/types/session.ts +1 -1
  125. package/src/types/swarmfeed.ts +30 -0
  126. package/tsconfig.json +1 -2
@@ -55,6 +55,29 @@ interface DelegateRuntimeState {
55
55
  cancel?: () => void
56
56
  }
57
57
 
58
+ type DelegateFailureKind = 'auth' | 'unavailable' | 'spawn' | 'permission' | 'runtime' | 'timeout'
59
+
60
+ interface DelegateBackendResult {
61
+ backend: DelegateBackend
62
+ status: 'completed' | 'failed'
63
+ response: string | null
64
+ error: string | null
65
+ failureKind?: DelegateFailureKind
66
+ }
67
+
68
+ interface DelegateBackendAdapter {
69
+ backend: DelegateBackend
70
+ binaryName: string
71
+ run: (
72
+ binary: string,
73
+ task: string,
74
+ resume: boolean,
75
+ resumeId: string,
76
+ bctx: DelegateContext,
77
+ runtime?: DelegateRuntimeState,
78
+ ) => Promise<DelegateBackendResult>
79
+ }
80
+
58
81
  function buildDelegateContextFromSessionish(session: unknown): DelegateContext {
59
82
  const record = session && typeof session === 'object' ? session as Record<string, unknown> : {}
60
83
  const sessionId = typeof record.id === 'string'
@@ -337,25 +360,73 @@ export function resolveDelegateResumeConfig(
337
360
  }
338
361
  }
339
362
 
340
- async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
363
+ function buildDelegateFailure(
364
+ backend: DelegateBackend,
365
+ error: string,
366
+ failureKind: DelegateFailureKind = 'runtime',
367
+ ): DelegateBackendResult {
368
+ return {
369
+ backend,
370
+ status: 'failed',
371
+ response: null,
372
+ error: error.trim() || `Delegate backend "${backend}" failed.`,
373
+ failureKind,
374
+ }
375
+ }
376
+
377
+ function buildDelegateSuccess(
378
+ backend: DelegateBackend,
379
+ response: string,
380
+ ): DelegateBackendResult {
381
+ return {
382
+ backend,
383
+ status: 'completed',
384
+ response: truncate(response, MAX_OUTPUT),
385
+ error: null,
386
+ }
387
+ }
388
+
389
+ function formatDelegateResultText(result: DelegateBackendResult): string {
390
+ if (result.status === 'completed') {
391
+ return truncate(result.response?.trim() || 'Task completed.', MAX_OUTPUT)
392
+ }
393
+ const error = result.error?.trim() || `Delegate backend "${result.backend}" failed.`
394
+ return truncate(`Error: ${error}`, MAX_OUTPUT)
395
+ }
396
+
397
+ const DELEGATE_BACKEND_ADAPTERS: Record<DelegateBackend, DelegateBackendAdapter> = {
398
+ claude: {
399
+ backend: 'claude',
400
+ binaryName: 'claude',
401
+ run: runClaudeDelegate,
402
+ },
403
+ codex: {
404
+ backend: 'codex',
405
+ binaryName: 'codex',
406
+ run: runCodexDelegate,
407
+ },
408
+ opencode: {
409
+ backend: 'opencode',
410
+ binaryName: 'opencode',
411
+ run: runOpenCodeDelegate,
412
+ },
413
+ gemini: {
414
+ backend: 'gemini',
415
+ binaryName: 'gemini',
416
+ run: runGeminiDelegate,
417
+ },
418
+ }
419
+
420
+ async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
341
421
  const normalized = normalizeDelegateArgs(args)
342
422
  const task = normalized.task as string
343
423
  const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
344
424
  const { resume, resumeId } = resolveDelegateResumeConfig(normalized, backend, bctx)
345
- const backends = {
346
- claude: findBinaryOnPath('claude'),
347
- codex: findBinaryOnPath('codex'),
348
- opencode: findBinaryOnPath('opencode'),
349
- gemini: findBinaryOnPath('gemini'),
350
- }
351
- const binary = backends[backend as keyof typeof backends]
352
- if (!binary) return `Error: Backend "${backend}" unavailable.`
353
-
354
- if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx, runtime)
355
- if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx, runtime)
356
- if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx, runtime)
357
- if (backend === 'gemini') return runGeminiDelegate(binary, task, resume, resumeId, bctx, runtime)
358
- return `Error: Unsupported backend "${backend}".`
425
+ const adapter = DELEGATE_BACKEND_ADAPTERS[backend]
426
+ if (!adapter) return buildDelegateFailure(backend, `Unsupported backend "${backend}".`, 'unavailable')
427
+ const binary = findBinaryOnPath(adapter.binaryName)
428
+ if (!binary) return buildDelegateFailure(backend, `Backend "${backend}" unavailable.`, 'unavailable')
429
+ return adapter.run(binary, task, resume, resumeId, bctx, runtime)
359
430
  }
360
431
 
361
432
  function providerIdForBackend(backend: DelegateBackend): string {
@@ -369,13 +440,13 @@ function fallbackOrderForBackend(requested: DelegateBackend): DelegateBackend[]
369
440
  return [requested, ...DELEGATE_BACKEND_ORDER.filter((backend) => backend !== requested)]
370
441
  }
371
442
 
372
- function isRecoverableDelegateFailure(result: string): boolean {
373
- const normalized = String(result || '').trim().toLowerCase()
374
- if (!normalized.startsWith('error:')) return false
443
+ function isRecoverableDelegateFailure(result: DelegateBackendResult): boolean {
444
+ if (result.status !== 'failed') return false
445
+ if (result.failureKind === 'auth' || result.failureKind === 'unavailable' || result.failureKind === 'spawn' || result.failureKind === 'permission') {
446
+ return true
447
+ }
448
+ const normalized = String(result.error || '').trim().toLowerCase()
375
449
  return [
376
- 'not authenticated',
377
- 'backend "',
378
- 'unavailable',
379
450
  'enoent',
380
451
  'not found',
381
452
  'command not found',
@@ -387,12 +458,16 @@ function isRecoverableDelegateFailure(result: string): boolean {
387
458
 
388
459
  function summarizeDelegateAttempts(
389
460
  requested: DelegateBackend,
390
- attempts: Array<{ backend: DelegateBackend; result: string }>,
391
- ): string {
461
+ attempts: Array<{ backend: DelegateBackend; result: DelegateBackendResult }>,
462
+ ): DelegateBackendResult {
392
463
  const summary = attempts
393
- .map(({ backend, result }) => `${backend}: ${result.replace(/^Error:\s*/i, '').trim() || result.trim()}`)
464
+ .map(({ backend, result }) => `${backend}: ${result.error?.trim() || formatDelegateResultText(result).replace(/^Error:\s*/i, '').trim()}`)
394
465
  .join(' | ')
395
- return `Error: Delegate backend "${requested}" could not complete the task. ${summary}. Continue with another available tool instead of stopping.`
466
+ return buildDelegateFailure(
467
+ requested,
468
+ `Delegate backend "${requested}" could not complete the task. ${summary}. Continue with another available tool instead of stopping.`,
469
+ 'runtime',
470
+ )
396
471
  }
397
472
 
398
473
  async function runDelegateBackendWithFallback(
@@ -400,26 +475,25 @@ async function runDelegateBackendWithFallback(
400
475
  bctx: DelegateContext,
401
476
  runtime?: DelegateRuntimeState,
402
477
  opts?: { onAttempt?: (backend: DelegateBackend, attemptIndex: number) => void; onFallback?: (from: DelegateBackend, to: DelegateBackend, reason: string) => void },
403
- ): Promise<{ backend: DelegateBackend; result: string; attempts: Array<{ backend: DelegateBackend; result: string }> }> {
478
+ ): Promise<{ backend: DelegateBackend; result: DelegateBackendResult; attempts: Array<{ backend: DelegateBackend; result: DelegateBackendResult }> }> {
404
479
  const normalized = normalizeDelegateArgs(args)
405
480
  const requested = ((normalized.backend as string) || 'claude') as DelegateBackend
406
481
  const orderedBackends = fallbackOrderForBackend(requested)
407
- const attempts: Array<{ backend: DelegateBackend; result: string }> = []
482
+ const attempts: Array<{ backend: DelegateBackend; result: DelegateBackendResult }> = []
408
483
 
409
484
  for (const [index, backend] of orderedBackends.entries()) {
410
485
  opts?.onAttempt?.(backend, index)
411
486
  const result = await runDelegateBackend({ ...normalized, backend }, bctx, runtime)
412
487
  attempts.push({ backend, result })
413
- if (/^Error:/i.test(result.trim())) {
414
- markProviderFailure(providerIdForBackend(backend), result)
415
- } else {
488
+ if (result.status === 'completed') {
416
489
  markProviderSuccess(providerIdForBackend(backend))
417
490
  return { backend, result, attempts }
418
491
  }
492
+ markProviderFailure(providerIdForBackend(backend), formatDelegateResultText(result))
419
493
 
420
494
  const nextBackend = orderedBackends[index + 1]
421
495
  if (nextBackend && isRecoverableDelegateFailure(result)) {
422
- opts?.onFallback?.(backend, nextBackend, result)
496
+ opts?.onFallback?.(backend, nextBackend, result.error || formatDelegateResultText(result))
423
497
  continue
424
498
  }
425
499
  return {
@@ -520,7 +594,7 @@ async function executeDelegateAction(args: Record<string, unknown>, bctx: Delega
520
594
  onFallback: (from, to, reason) => {
521
595
  appendDelegationCheckpoint(
522
596
  job.id,
523
- `Delegate ${from} failed: ${reason.replace(/^Error:\s*/i, '').trim()}. Falling back to ${to}.`,
597
+ `Delegate ${from} failed: ${reason.trim()}. Falling back to ${to}.`,
524
598
  'running',
525
599
  )
526
600
  },
@@ -529,22 +603,22 @@ async function executeDelegateAction(args: Record<string, unknown>, bctx: Delega
529
603
  const latest = getDelegationJob(job.id)
530
604
  if (latest?.status === 'cancelled') return { backend, result }
531
605
  const resumePatch = buildDelegateResumePatch(bctx)
532
- if (/^Error:/i.test(result.trim())) {
606
+ if (result.status === 'failed') {
533
607
  appendDelegationCheckpoint(job.id, `Delegate failed on ${backend}`, 'failed')
534
- failDelegationJob(job.id, result.replace(/^Error:\s*/i, '').trim() || result, { ...resumePatch, backend })
608
+ failDelegationJob(job.id, result.error || `Delegate backend "${backend}" failed.`, { ...resumePatch, backend })
535
609
  } else {
536
610
  appendDelegationCheckpoint(job.id, `Delegate completed on ${backend}`, 'completed')
537
- completeDelegationJob(job.id, result, { ...resumePatch, backend })
611
+ completeDelegationJob(job.id, result.response || 'Task completed.', { ...resumePatch, backend })
538
612
  }
539
613
  return { backend, result }
540
614
  })
541
615
  .catch((err: unknown) => {
542
616
  const message = errorMessage(err)
543
617
  const latest = getDelegationJob(job.id)
544
- if (latest?.status === 'cancelled') return { backend: requestedBackend, result: `Error: ${message}` }
618
+ if (latest?.status === 'cancelled') return { backend: requestedBackend, result: buildDelegateFailure(requestedBackend, message) }
545
619
  appendDelegationCheckpoint(job.id, `Delegate crashed on ${requestedBackend}: ${message}`, 'failed')
546
620
  failDelegationJob(job.id, message, { ...buildDelegateResumePatch(bctx), backend: requestedBackend })
547
- return { backend: requestedBackend, result: `Error: ${message}` }
621
+ return { backend: requestedBackend, result: buildDelegateFailure(requestedBackend, message, 'runtime') }
548
622
  })
549
623
 
550
624
  if (!waitForCompletion) {
@@ -560,9 +634,9 @@ async function executeDelegateAction(args: Record<string, unknown>, bctx: Delega
560
634
  const latest = getDelegationJob(job.id)
561
635
  return JSON.stringify({
562
636
  jobId: job.id,
563
- status: latest?.status || (/^Error:/i.test(result.trim()) ? 'failed' : 'completed'),
637
+ status: latest?.status || result.status,
564
638
  backend: latest?.backend || backend,
565
- response: result,
639
+ response: formatDelegateResultText(result),
566
640
  })
567
641
  }
568
642
 
@@ -591,7 +665,7 @@ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
591
665
  return null
592
666
  }
593
667
 
594
- async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
668
+ async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
595
669
  try {
596
670
  // Build clean env — preserves user's CODEX_HOME for auth
597
671
  const env = buildCliEnv()
@@ -599,13 +673,13 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
599
673
  // Auth probe BEFORE any temp CODEX_HOME override
600
674
  const auth = probeCliAuth(binary, 'codex', env, bctx.cwd)
601
675
  if (!auth.authenticated) {
602
- return `Error: ${auth.errorMessage || 'Codex CLI is not authenticated. Run `codex login` and retry.'}`
676
+ return buildDelegateFailure('codex', auth.errorMessage || 'Codex CLI is not authenticated. Run `codex login` and retry.', 'auth')
603
677
  }
604
678
 
605
679
  const storedResumeId = bctx.readStoredDelegateResumeId?.('codex')
606
680
  const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
607
681
 
608
- return await new Promise<string>((resolve) => {
682
+ return await new Promise<DelegateBackendResult>((resolve) => {
609
683
  const args: string[] = ['exec']
610
684
  if (resumeIdToUse) args.push('resume', resumeIdToUse)
611
685
  args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
@@ -618,10 +692,10 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
618
692
  let discoveredId: string | null = null
619
693
  let settled = false
620
694
 
621
- const finish = (text: string) => {
695
+ const finish = (result: DelegateBackendResult) => {
622
696
  if (settled) return
623
697
  settled = true
624
- resolve(truncate(text, MAX_OUTPUT))
698
+ resolve(result)
625
699
  }
626
700
 
627
701
  const timeoutHandle = setTimeout(() => {
@@ -655,39 +729,39 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
655
729
  clearTimeout(timeoutHandle)
656
730
  if (discoveredId) bctx.persistDelegateResumeId?.('codex', discoveredId)
657
731
  const output = responseText.trim()
658
- if (output) return finish(output)
732
+ if (output) return finish(buildDelegateSuccess('codex', output))
659
733
  const stderr = stderrBuf.trim()
660
- if (stderr) return finish(`Error: ${stderr}`)
661
- return finish(`Error: Codex exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`)
734
+ if (stderr) return finish(buildDelegateFailure('codex', stderr))
735
+ return finish(buildDelegateFailure('codex', `Codex exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
662
736
  })
663
737
 
664
738
  child.on('error', (err) => {
665
739
  clearTimeout(timeoutHandle)
666
- finish(`Error: ${err.message}`)
740
+ finish(buildDelegateFailure('codex', err.message, 'spawn'))
667
741
  })
668
742
 
669
743
  child.stdin?.write(task)
670
744
  child.stdin?.end()
671
745
  })
672
746
  } catch (err: unknown) {
673
- return `Error: ${errorMessage(err)}`
747
+ return buildDelegateFailure('codex', errorMessage(err), 'runtime')
674
748
  }
675
749
  }
676
750
 
677
- async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
751
+ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
678
752
  try {
679
753
  const env = buildCliEnv()
680
754
 
681
755
  // Auth probe
682
756
  const auth = probeCliAuth(binary, 'opencode', env, bctx.cwd)
683
757
  if (!auth.authenticated) {
684
- return `Error: ${auth.errorMessage || 'OpenCode CLI is not authenticated.'}`
758
+ return buildDelegateFailure('opencode', auth.errorMessage || 'OpenCode CLI is not authenticated.', 'auth')
685
759
  }
686
760
 
687
761
  const storedResumeId = bctx.readStoredDelegateResumeId?.('opencode')
688
762
  const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
689
763
 
690
- return await new Promise<string>((resolve) => {
764
+ return await new Promise<DelegateBackendResult>((resolve) => {
691
765
  const args = ['run', task, '--format', 'json']
692
766
  if (resumeIdToUse) args.push('--session', resumeIdToUse)
693
767
 
@@ -699,10 +773,10 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
699
773
  let discoveredId: string | null = null
700
774
  let settled = false
701
775
 
702
- const finish = (text: string) => {
776
+ const finish = (result: DelegateBackendResult) => {
703
777
  if (settled) return
704
778
  settled = true
705
- resolve(truncate(text, MAX_OUTPUT))
779
+ resolve(result)
706
780
  }
707
781
 
708
782
  const timeoutHandle = setTimeout(() => {
@@ -742,36 +816,36 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
742
816
  clearTimeout(timeoutHandle)
743
817
  if (discoveredId) bctx.persistDelegateResumeId?.('opencode', discoveredId)
744
818
  const output = responseText.trim()
745
- if (output) return finish(output)
819
+ if (output) return finish(buildDelegateSuccess('opencode', output))
746
820
  const stderr = stderrBuf.trim()
747
- if (stderr) return finish(`Error: ${stderr}`)
748
- return finish(`Error: OpenCode exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`)
821
+ if (stderr) return finish(buildDelegateFailure('opencode', stderr))
822
+ return finish(buildDelegateFailure('opencode', `OpenCode exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
749
823
  })
750
824
 
751
825
  child.on('error', (err) => {
752
826
  clearTimeout(timeoutHandle)
753
- finish(`Error: ${err.message}`)
827
+ finish(buildDelegateFailure('opencode', err.message, 'spawn'))
754
828
  })
755
829
  })
756
830
  } catch (err: unknown) {
757
- return `Error: ${errorMessage(err)}`
831
+ return buildDelegateFailure('opencode', errorMessage(err), 'runtime')
758
832
  }
759
833
  }
760
834
 
761
- async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
835
+ async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
762
836
  try {
763
837
  const env = buildCliEnv()
764
838
 
765
839
  // Auth probe
766
840
  const auth = probeCliAuth(binary, 'gemini', env, bctx.cwd)
767
841
  if (!auth.authenticated) {
768
- return `Error: ${auth.errorMessage || 'Gemini CLI is not authenticated.'}`
842
+ return buildDelegateFailure('gemini', auth.errorMessage || 'Gemini CLI is not authenticated.', 'auth')
769
843
  }
770
844
 
771
845
  const storedResumeId = bctx.readStoredDelegateResumeId?.('gemini')
772
846
  const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
773
847
 
774
- return await new Promise<string>((resolve) => {
848
+ return await new Promise<DelegateBackendResult>((resolve) => {
775
849
  const args = ['--prompt', task, '--output-format', 'stream-json', '--yolo']
776
850
  if (resumeIdToUse) args.push('--resume', resumeIdToUse)
777
851
 
@@ -783,10 +857,10 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
783
857
  let discoveredId: string | null = null
784
858
  let settled = false
785
859
 
786
- const finish = (text: string) => {
860
+ const finish = (result: DelegateBackendResult) => {
787
861
  if (settled) return
788
862
  settled = true
789
- resolve(truncate(text, MAX_OUTPUT))
863
+ resolve(result)
790
864
  }
791
865
 
792
866
  const timeoutHandle = setTimeout(() => {
@@ -830,32 +904,32 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
830
904
  clearTimeout(timeoutHandle)
831
905
  if (discoveredId) bctx.persistDelegateResumeId?.('gemini', discoveredId)
832
906
  const output = responseText.trim()
833
- if (output) return finish(output)
907
+ if (output) return finish(buildDelegateSuccess('gemini', output))
834
908
  const stderr = stderrBuf.trim()
835
- if (stderr) return finish(`Error: ${stderr}`)
836
- return finish(`Error: Gemini exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`)
909
+ if (stderr) return finish(buildDelegateFailure('gemini', stderr))
910
+ return finish(buildDelegateFailure('gemini', `Gemini exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
837
911
  })
838
912
 
839
913
  child.on('error', (err) => {
840
914
  clearTimeout(timeoutHandle)
841
- finish(`Error: ${err.message}`)
915
+ finish(buildDelegateFailure('gemini', err.message, 'spawn'))
842
916
  })
843
917
  })
844
918
  } catch (err: unknown) {
845
- return `Error: ${errorMessage(err)}`
919
+ return buildDelegateFailure('gemini', errorMessage(err), 'runtime')
846
920
  }
847
921
  }
848
922
 
849
- async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
923
+ async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
850
924
  try {
851
925
  const env = buildCliEnv()
852
926
  const auth = probeCliAuth(binary, 'claude', env, bctx.cwd)
853
- if (!auth.authenticated) return `Error: ${auth.errorMessage || 'Claude Code not authenticated.'}`
927
+ if (!auth.authenticated) return buildDelegateFailure('claude', auth.errorMessage || 'Claude Code not authenticated.', 'auth')
854
928
 
855
929
  const storedResumeId = bctx.readStoredDelegateResumeId?.('claudeCode')
856
930
  const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
857
931
 
858
- return new Promise<string>((resolve) => {
932
+ return new Promise<DelegateBackendResult>((resolve) => {
859
933
  const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
860
934
  if (resumeIdToUse) args.push('--resume', resumeIdToUse)
861
935
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
@@ -865,7 +939,7 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
865
939
  let discoveredId: string | null = null
866
940
  let settled = false
867
941
 
868
- const finish = (res: string) => { if (!settled) { settled = true; resolve(truncate(res, MAX_OUTPUT)) } }
942
+ const finish = (result: DelegateBackendResult) => { if (!settled) { settled = true; resolve(result) } }
869
943
  const timeoutHandle = setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, bctx.claudeTimeoutMs || 300000)
870
944
 
871
945
  child.stdout?.on('data', (c) => {
@@ -890,17 +964,17 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
890
964
  clearTimeout(timeoutHandle)
891
965
  if (discoveredId) bctx.persistDelegateResumeId?.('claudeCode', discoveredId)
892
966
  const output = assistantText.trim()
893
- if (code === 0) finish(output || 'Task completed.')
894
- else finish(output ? output : `Error: Code ${code}. ${stderr.trim()}`)
967
+ if (code === 0) finish(buildDelegateSuccess('claude', output || 'Task completed.'))
968
+ else finish(buildDelegateFailure('claude', output || `Code ${code}. ${stderr.trim()}`))
895
969
  })
896
970
  child.on('error', (err) => {
897
971
  clearTimeout(timeoutHandle)
898
- finish(`Error: ${err.message}`)
972
+ finish(buildDelegateFailure('claude', err.message, 'spawn'))
899
973
  })
900
974
  child.stdin?.write(task)
901
975
  child.stdin?.end()
902
976
  })
903
- } catch (err: unknown) { return `Error: ${errorMessage(err)}` }
977
+ } catch (err: unknown) { return buildDelegateFailure('claude', errorMessage(err), 'runtime') }
904
978
  }
905
979
 
906
980
  /**
@@ -21,11 +21,19 @@ if (!IS_BUILD_BOOTSTRAP) {
21
21
  loadEnv()
22
22
  }
23
23
 
24
+ /** Append a key=value to .env.local only if the key doesn't already exist in the file. */
25
+ function appendEnvKeyIfMissing(envPath: string, key: string, value: string): void {
26
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''
27
+ const keyPattern = new RegExp(`^${key}=`, 'm')
28
+ if (keyPattern.test(existing)) return
29
+ fs.appendFileSync(envPath, `\n${key}=${value}\n`)
30
+ }
31
+
24
32
  // Auto-generate CREDENTIAL_SECRET if missing
25
33
  if (!IS_BUILD_BOOTSTRAP && !process.env.CREDENTIAL_SECRET) {
26
34
  const secret = crypto.randomBytes(32).toString('hex')
27
35
  const envPath = path.join(process.cwd(), '.env.local')
28
- fs.appendFileSync(envPath, `\nCREDENTIAL_SECRET=${secret}\n`)
36
+ appendEnvKeyIfMissing(envPath, 'CREDENTIAL_SECRET', secret)
29
37
  process.env.CREDENTIAL_SECRET = secret
30
38
  log.info(TAG, 'Generated CREDENTIAL_SECRET in .env.local')
31
39
  }
@@ -35,7 +43,7 @@ const SETUP_FLAG = path.join(DATA_DIR, '.setup_pending')
35
43
  if (!IS_BUILD_BOOTSTRAP && !process.env.ACCESS_KEY) {
36
44
  const key = crypto.randomBytes(16).toString('hex')
37
45
  const envPath = path.join(process.cwd(), '.env.local')
38
- fs.appendFileSync(envPath, `\nACCESS_KEY=${key}\n`)
46
+ appendEnvKeyIfMissing(envPath, 'ACCESS_KEY', key)
39
47
  process.env.ACCESS_KEY = key
40
48
  fs.writeFileSync(SETUP_FLAG, key)
41
49
  log.info(TAG, `ACCESS KEY: ${key} — Use this key to connect from the browser.`)
@@ -517,6 +517,17 @@ function normalizeStoredRecordInner(
517
517
  if (typeof agent.spentDailyCents !== 'number') agent.spentDailyCents = 0
518
518
  if (typeof agent.spentHourlyCents !== 'number') agent.spentHourlyCents = 0
519
519
  if (typeof agent.lastSpendRollupAt !== 'number') agent.lastSpendRollupAt = 0
520
+ // SwarmFeed defaults
521
+ if (typeof agent.swarmfeedEnabled !== 'boolean') agent.swarmfeedEnabled = false
522
+ if (agent.swarmfeedJoinedAt === undefined) agent.swarmfeedJoinedAt = null
523
+ if (typeof agent.swarmfeedBio !== 'string' && agent.swarmfeedBio !== null) agent.swarmfeedBio = null
524
+ if (agent.swarmfeedPinnedPostId === undefined) agent.swarmfeedPinnedPostId = null
525
+ if (typeof agent.swarmfeedAutoPost !== 'boolean') agent.swarmfeedAutoPost = false
526
+ if (!Array.isArray(agent.swarmfeedAutoPostChannels)) agent.swarmfeedAutoPostChannels = []
527
+ if (typeof agent.swarmfeedApiKey !== 'string' && agent.swarmfeedApiKey !== null) agent.swarmfeedApiKey = null
528
+ if (typeof agent.swarmfeedAgentId !== 'string' && agent.swarmfeedAgentId !== null) agent.swarmfeedAgentId = null
529
+ if (!agent.origin) agent.origin = 'swarmclaw'
530
+ if (agent.swarmfeedHeartbeat === undefined) agent.swarmfeedHeartbeat = null
520
531
  // Org chart normalization
521
532
  if (agent.orgChart && typeof agent.orgChart === 'object' && !Array.isArray(agent.orgChart)) {
522
533
  const oc = agent.orgChart as Record<string, unknown>
@@ -6,12 +6,13 @@ import Database from 'better-sqlite3'
6
6
  import { perf } from '@/lib/server/runtime/perf'
7
7
  import { log } from '@/lib/server/logger'
8
8
  import { notify } from '@/lib/server/ws-hub'
9
-
10
- const TAG = 'storage'
11
9
  import { DATA_DIR, IS_BUILD_BOOTSTRAP, WORKSPACE_DIR } from './data-dir'
12
10
  import { normalizeHeartbeatSettingFields } from '@/lib/runtime/heartbeat-defaults'
13
11
  import { normalizeRuntimeSettingFields } from '@/lib/runtime/runtime-loop'
14
12
  import { normalizeCapabilitySelection } from '@/lib/capability-selection'
13
+
14
+ const TAG = 'storage'
15
+ const malformedRecordWarnings = new Set<string>()
15
16
  import type {
16
17
  Agent,
17
18
  AppNotification,
@@ -236,8 +237,16 @@ function loadCollectionWithNormalizationState(table: string): {
236
237
  if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) continue
237
238
  result[id] = normalized as StoredObject
238
239
  if (changed) normalizedCount += 1
239
- } catch {
240
- // Ignore malformed records instead of crashing list endpoints.
240
+ } catch (err) {
241
+ const fingerprint = `${table}:${id}`
242
+ if (!malformedRecordWarnings.has(fingerprint)) {
243
+ malformedRecordWarnings.add(fingerprint)
244
+ log.warn(TAG, 'Ignoring malformed stored record during collection load', {
245
+ table,
246
+ id,
247
+ error: err instanceof Error ? err.message : String(err),
248
+ })
249
+ }
241
250
  }
242
251
  }
243
252
  endPerf({ count: raw.size, normalizedCount })
@@ -1658,6 +1667,106 @@ export const loadGoal = goalsStore.loadItem
1658
1667
  export const upsertGoal = goalsStore.upsert
1659
1668
  export const deleteGoalItem = goalsStore.deleteItem
1660
1669
 
1670
+ function legacyMissionStatusToWorkingStatus(value: unknown): 'idle' | 'progress' | 'blocked' | 'completed' {
1671
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
1672
+ if (normalized === 'achieved' || normalized === 'completed' || normalized === 'ok') return 'completed'
1673
+ if (normalized === 'blocked' || normalized === 'waiting' || normalized === 'paused') return 'blocked'
1674
+ if (normalized === 'active' || normalized === 'executing' || normalized === 'progress') return 'progress'
1675
+ return 'idle'
1676
+ }
1677
+
1678
+ function buildGoalFromLegacyMission(id: string, mission: StoredObject): StoredObject {
1679
+ const objective = typeof mission.objective === 'string' && mission.objective.trim()
1680
+ ? mission.objective.trim()
1681
+ : typeof mission.title === 'string' && mission.title.trim()
1682
+ ? mission.title.trim()
1683
+ : 'Legacy mission objective'
1684
+ const title = typeof mission.title === 'string' && mission.title.trim()
1685
+ ? mission.title.trim()
1686
+ : objective.slice(0, 120)
1687
+ const status = typeof mission.status === 'string' && mission.status.trim().toLowerCase() === 'achieved'
1688
+ ? 'achieved'
1689
+ : typeof mission.status === 'string' && mission.status.trim().toLowerCase() === 'abandoned'
1690
+ ? 'abandoned'
1691
+ : 'active'
1692
+
1693
+ return {
1694
+ id,
1695
+ title,
1696
+ description: typeof mission.plannerSummary === 'string' && mission.plannerSummary.trim()
1697
+ ? mission.plannerSummary.trim()
1698
+ : typeof mission.description === 'string' && mission.description.trim()
1699
+ ? mission.description.trim()
1700
+ : undefined,
1701
+ level: mission.taskId ? 'task' : mission.agentId ? 'agent' : mission.projectId ? 'project' : 'organization',
1702
+ parentGoalId: typeof mission.parentMissionId === 'string' && mission.parentMissionId.trim() ? mission.parentMissionId.trim() : null,
1703
+ projectId: typeof mission.projectId === 'string' && mission.projectId.trim() ? mission.projectId.trim() : null,
1704
+ agentId: typeof mission.agentId === 'string' && mission.agentId.trim() ? mission.agentId.trim() : null,
1705
+ taskId: typeof mission.taskId === 'string' && mission.taskId.trim() ? mission.taskId.trim() : null,
1706
+ objective,
1707
+ constraints: Array.isArray(mission.constraints)
1708
+ ? mission.constraints.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
1709
+ : [],
1710
+ successMetric: typeof mission.successMetric === 'string' && mission.successMetric.trim() ? mission.successMetric.trim() : null,
1711
+ budgetUsd: typeof mission.budgetUsd === 'number' && Number.isFinite(mission.budgetUsd) ? mission.budgetUsd : null,
1712
+ deadlineAt: typeof mission.deadlineAt === 'number' && Number.isFinite(mission.deadlineAt) ? mission.deadlineAt : null,
1713
+ status,
1714
+ createdAt: typeof mission.createdAt === 'number' && Number.isFinite(mission.createdAt) ? mission.createdAt : Date.now(),
1715
+ updatedAt: typeof mission.updatedAt === 'number' && Number.isFinite(mission.updatedAt) ? mission.updatedAt : Date.now(),
1716
+ }
1717
+ }
1718
+
1719
+ function buildWorkingStateFromLegacyMission(mission: StoredObject): StoredObject | null {
1720
+ const sessionId = typeof mission.sessionId === 'string' && mission.sessionId.trim()
1721
+ ? mission.sessionId.trim()
1722
+ : ''
1723
+ if (!sessionId) return null
1724
+ const currentStep = typeof mission.currentStep === 'string' && mission.currentStep.trim()
1725
+ ? mission.currentStep.trim()
1726
+ : ''
1727
+ const blockerSummary = typeof mission.blockerSummary === 'string' && mission.blockerSummary.trim()
1728
+ ? mission.blockerSummary.trim()
1729
+ : ''
1730
+ return {
1731
+ sessionId,
1732
+ objective: typeof mission.objective === 'string' && mission.objective.trim() ? mission.objective.trim() : null,
1733
+ summary: typeof mission.plannerSummary === 'string' && mission.plannerSummary.trim() ? mission.plannerSummary.trim() : null,
1734
+ status: legacyMissionStatusToWorkingStatus(mission.status ?? mission.phase),
1735
+ nextAction: currentStep || null,
1736
+ planSteps: currentStep
1737
+ ? [{ id: `legacy-mission-${sessionId}`, text: currentStep, status: 'active', createdAt: Date.now(), updatedAt: Date.now() }]
1738
+ : [],
1739
+ blockers: blockerSummary
1740
+ ? [{ id: `legacy-mission-blocker-${sessionId}`, summary: blockerSummary, status: 'active', createdAt: Date.now(), updatedAt: Date.now() }]
1741
+ : [],
1742
+ confirmedFacts: [],
1743
+ artifacts: [],
1744
+ decisions: [],
1745
+ openQuestions: [],
1746
+ hypotheses: [],
1747
+ evidenceRefs: [],
1748
+ constraints: Array.isArray(mission.constraints)
1749
+ ? mission.constraints.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
1750
+ : [],
1751
+ successCriteria: Array.isArray(mission.successCriteria)
1752
+ ? mission.successCriteria.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
1753
+ : [],
1754
+ updatedAt: typeof mission.updatedAt === 'number' && Number.isFinite(mission.updatedAt) ? mission.updatedAt : Date.now(),
1755
+ }
1756
+ }
1757
+
1758
+ export function saveMissions(missions: Record<string, StoredObject>): void {
1759
+ for (const [id, mission] of Object.entries(missions || {})) {
1760
+ upsertGoal(id, buildGoalFromLegacyMission(id, mission))
1761
+ const workingState = buildWorkingStateFromLegacyMission(mission)
1762
+ if (workingState) upsertPersistedWorkingState(String(workingState.sessionId), workingState)
1763
+ }
1764
+ }
1765
+
1766
+ export function loadMissions(): Record<string, StoredObject> {
1767
+ return loadGoals() as Record<string, StoredObject>
1768
+ }
1769
+
1661
1770
  export function getSessionMessages(sessionId: string): Message[] {
1662
1771
  const session = loadSession(sessionId)
1663
1772
  return Array.isArray(session?.messages) ? session.messages : []