@swarmclawai/swarmclaw 0.6.7 → 0.7.0

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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -1,22 +1,40 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import { z } from 'zod'
3
3
  import type { GoalContract, MessageToolEvent } from '@/types'
4
- import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks } from './storage'
4
+ import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks, loadSettings } from './storage'
5
5
  import { log } from './logger'
6
6
  import { getMemoryDb } from './memory-db'
7
7
  import { isProtectedMainSession } from './main-session'
8
+ import { logExecution } from './execution-log'
8
9
  import {
9
10
  mergeGoalContracts,
10
11
  parseGoalContractFromText,
11
12
  parseMainLoopPlan,
12
13
  parseMainLoopReview,
13
14
  } from './autonomy-contract'
14
- const MAX_PENDING_EVENTS = 40
15
- const MAX_TIMELINE_EVENTS = 80
15
+ import { buildIdentityContext } from './heartbeat-service'
16
+
17
+ const MAX_PENDING_EVENTS = 60
18
+ const MAX_TIMELINE_EVENTS = 120
16
19
  const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
17
- const MEMORY_NOTE_MIN_INTERVAL_MS = 90 * 60 * 1000
20
+ const MEMORY_NOTE_MIN_INTERVAL_MS = 30 * 60 * 1000
18
21
  const DEFAULT_FOLLOWUP_DELAY_SEC = 45
19
- const MAX_FOLLOWUP_CHAIN = 6
22
+ const DEFAULT_MAX_FOLLOWUP_CHAIN = 20
23
+ function getMaxFollowupChain(agentId: string | undefined): number {
24
+ if (agentId) {
25
+ const agents = loadAgents()
26
+ const agent = agents[agentId]
27
+ if (typeof agent?.maxFollowupChain === 'number' && agent.maxFollowupChain > 0) {
28
+ return Math.min(agent.maxFollowupChain, 100)
29
+ }
30
+ }
31
+ const settings = loadSettings()
32
+ if (typeof settings?.maxFollowupChain === 'number' && settings.maxFollowupChain > 0) {
33
+ return Math.min(settings.maxFollowupChain, 100)
34
+ }
35
+ return DEFAULT_MAX_FOLLOWUP_CHAIN
36
+ }
37
+
20
38
  const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
21
39
  const AGENT_HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
22
40
  const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
@@ -25,6 +43,14 @@ const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recu
25
43
  const UPLOAD_ARTIFACT_HINT = /(?:sandbox:)?\/api\/uploads\/[^\s)\]]+|https?:\/\/[^\s)\]]+\.(?:png|jpe?g|webp|gif|pdf)\b/i
26
44
  const SENT_ARTIFACT_HINT = /\b(sent|shared|uploaded|returned)\b[^.]*\b(screenshot|snapshot|image|file)\b/i
27
45
 
46
+ const COMPANION_GOAL_PROMPT = `
47
+ ## Identity & Vibe
48
+ You are a persistent companion.
49
+ 1. **Identity**: Embody your creature, theme, and vibe. Your emoji is your signature.
50
+ 2. **Workspace Context**: Respect the current workspace. Read IDENTITY.md and HEARTBEAT.md if they exist.
51
+ 3. **Continuity**: Maintain awareness of the user's long-term journey. Proactively help with open-ended goals without being asked for every step.
52
+ `.trim()
53
+
28
54
  interface MainLoopSessionMessageLike {
29
55
  text?: string
30
56
  }
@@ -45,13 +71,12 @@ export interface MainLoopTimelineEntry {
45
71
  at: number
46
72
  source: string
47
73
  note: string
48
- status?: 'idle' | 'progress' | 'blocked' | 'ok'
74
+ status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection'
49
75
  }
50
76
 
51
77
  export interface MainLoopState {
52
78
  goal: string | null
53
79
  goalContract: GoalContract | null
54
- status: 'idle' | 'progress' | 'blocked' | 'ok'
55
80
  summary: string | null
56
81
  nextAction: string | null
57
82
  planSteps: string[]
@@ -61,9 +86,12 @@ export interface MainLoopState {
61
86
  missionTaskId: string | null
62
87
  momentumScore: number
63
88
  paused: boolean
89
+ status: 'idle' | 'progress' | 'blocked' | 'ok'
64
90
  autonomyMode: 'assist' | 'autonomous'
65
91
  pendingEvents: MainLoopEvent[]
66
92
  timeline: MainLoopTimelineEntry[]
93
+ missionTokens: number
94
+ missionCostUsd: number
67
95
  followupChainCount: number
68
96
  metaMissCount: number
69
97
  workingMemoryNotes: string[]
@@ -104,6 +132,9 @@ export interface HandleMainLoopRunResultInput {
104
132
  resultText: string
105
133
  error?: string
106
134
  toolEvents?: MessageToolEvent[]
135
+ inputTokens?: number
136
+ outputTokens?: number
137
+ estimatedCost?: number
107
138
  }
108
139
 
109
140
  function toOneLine(value: string, max = 240): string {
@@ -143,7 +174,7 @@ function appendTimeline(
143
174
  source: string,
144
175
  note: string,
145
176
  now = Date.now(),
146
- status?: 'idle' | 'progress' | 'blocked' | 'ok',
177
+ status?: 'idle' | 'progress' | 'blocked' | 'ok' | 'reflection',
147
178
  ) {
148
179
  const normalizedNote = toOneLine(note, 400)
149
180
  if (!normalizedNote) return
@@ -278,6 +309,8 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
278
309
  autonomyMode: raw?.autonomyMode === 'assist' ? 'assist' : 'autonomous',
279
310
  pendingEvents,
280
311
  timeline,
312
+ missionTokens: typeof raw?.missionTokens === 'number' && Number.isFinite(raw.missionTokens) ? raw.missionTokens : 0,
313
+ missionCostUsd: typeof raw?.missionCostUsd === 'number' && Number.isFinite(raw.missionCostUsd) ? raw.missionCostUsd : 0,
281
314
  followupChainCount: clampInt(raw?.followupChainCount, 0, 0, 100),
282
315
  metaMissCount: clampInt(raw?.metaMissCount, 0, 0, 100),
283
316
  workingMemoryNotes: normalizeStringList(raw?.workingMemoryNotes, 24, 260),
@@ -489,6 +522,7 @@ function upsertMissionTask(session: any, state: MainLoopState, now: number): str
489
522
  const statusMap = {
490
523
  idle: 'backlog',
491
524
  progress: 'running',
525
+ reflection: 'running',
492
526
  blocked: 'failed',
493
527
  ok: 'completed',
494
528
  } as const
@@ -603,6 +637,9 @@ function maybeStoreMissionMemoryNote(
603
637
  state.reviewNote ? `review: ${state.reviewNote}` : '',
604
638
  typeof state.reviewConfidence === 'number' ? `review_confidence: ${state.reviewConfidence}` : '',
605
639
  state.missionTaskId ? `mission_task_id: ${state.missionTaskId}` : '',
640
+ typeof state.missionTokens === 'number' ? `mission_tokens: ${state.missionTokens}` : '',
641
+ typeof state.missionCostUsd === 'number' ? `mission_cost_usd: $${state.missionCostUsd.toFixed(4)}` : '',
642
+ state.workingMemoryNotes?.length ? `working_notes: ${state.workingMemoryNotes.slice(-5).join('; ')}` : '',
606
643
  ].filter(Boolean).join('\n')
607
644
 
608
645
  try {
@@ -622,20 +659,27 @@ function maybeStoreMissionMemoryNote(
622
659
  category: 'mission',
623
660
  title,
624
661
  content,
625
- } as any)
662
+ })
626
663
  state.lastMemoryNoteAt = now
627
- } catch (err: any) {
628
- appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err?.message || String(err), 240)}`, now)
664
+ logExecution(session.id, 'mission_checkpoint', `Checkpoint: ${toOneLine(state.goal || '', 120)}`, {
665
+ agentId: session.agentId,
666
+ detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
667
+ })
668
+ } catch (err: unknown) {
669
+ appendEvent(state, 'memory_note_error', `Failed to store mission memory note: ${toOneLine(err instanceof Error ? err.message : String(err), 240)}`, now)
629
670
  }
630
671
  }
631
672
 
632
- function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: boolean }): string {
673
+ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: boolean; agent?: Record<string, unknown> | null; session?: Record<string, unknown> | null }): string {
633
674
  const hasMemoryTool = opts?.hasMemoryTool === true
675
+ const identityContext = buildIdentityContext(opts?.session, opts?.agent)
634
676
  const goal = state.goal || 'No explicit goal yet. Continue with the strongest actionable objective from recent context.'
635
677
  const nextAction = state.nextAction || 'Determine the next highest-impact action and execute it.'
636
678
  const contractLines = buildGoalContractLines(state)
637
679
  return [
638
680
  'SWARM_MAIN_AUTO_FOLLOWUP',
681
+ identityContext,
682
+ COMPANION_GOAL_PROMPT,
639
683
  `Mission goal: ${goal}`,
640
684
  `Next action to execute now: ${nextAction}`,
641
685
  `Current status: ${state.status}`,
@@ -647,6 +691,9 @@ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: bool
647
691
  state.reviewNote ? `Last review: ${state.reviewNote}` : '',
648
692
  buildPendingEventLines(state),
649
693
  buildTimelineLines(state),
694
+ state.planSteps.length === 0 && state.followupChainCount === 0
695
+ ? 'Before executing, break the mission goal into 3-7 concrete subtasks. Output a [MAIN_LOOP_PLAN] JSON line with your plan, then execute the first step immediately.'
696
+ : '',
650
697
  'Act autonomously. Use available tools to execute work, verify results, and keep momentum.',
651
698
  state.autonomyMode === 'assist'
652
699
  ? 'Assist mode: execute safe internal analysis by default, and ask before irreversible external side effects (sending messages, purchases, account mutations).'
@@ -673,6 +720,9 @@ export function isMainSession(session: any): boolean {
673
720
 
674
721
  export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
675
722
  const now = Date.now()
723
+ const agents = loadAgents()
724
+ const agent = session.agentId ? agents[session.agentId] : null
725
+ const identityContext = buildIdentityContext(session, agent)
676
726
  const state = normalizeState(session?.mainLoopState, now)
677
727
  const goal = state.goal || inferGoalFromSessionMessages(session) || null
678
728
  const hasMemoryTool = Array.isArray(session?.tools) && session.tools.includes('memory')
@@ -684,6 +734,8 @@ export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: strin
684
734
 
685
735
  return [
686
736
  'SWARM_MAIN_MISSION_TICK',
737
+ identityContext,
738
+ COMPANION_GOAL_PROMPT,
687
739
  `Time: ${new Date(now).toISOString()}`,
688
740
  `Mission goal: ${promptGoal}`,
689
741
  `Current status: ${state.status}`,
@@ -883,7 +935,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
883
935
  const sessions = loadSessions()
884
936
  const session = sessions[input.sessionId]
885
937
  if (!session) return null
886
- if (!isMainSession(session)) return handleAgentHeartbeatResult(session, input)
938
+ if (!isProtectedMainSession(session)) return handleAgentHeartbeatResult(session, input)
887
939
 
888
940
  const now = Date.now()
889
941
  const state = normalizeState(session.mainLoopState, now)
@@ -902,6 +954,10 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
902
954
  appendTimeline(state, 'user_goal', `Goal updated: ${userGoal}`, now, state.status)
903
955
  appendWorkingMemoryNote(state, `goal:${userGoal}`)
904
956
  forceMemoryNote = true
957
+ logExecution(input.sessionId, 'mission_start', `New goal: ${toOneLine(userGoal, 200)}`, {
958
+ agentId: session.agentId,
959
+ detail: { goal: userGoal, planSteps: state.planSteps },
960
+ })
905
961
  } else if (userGoalContract?.objective) {
906
962
  state.goal = userGoalContract.objective
907
963
  state.goalContract = mergeGoalContracts(state.goalContract, userGoalContract)
@@ -909,8 +965,22 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
909
965
  appendTimeline(state, 'user_goal_contract', `Goal contract updated: ${userGoalContract.objective}`, now, state.status)
910
966
  appendWorkingMemoryNote(state, `contract:${userGoalContract.objective}`)
911
967
  forceMemoryNote = true
968
+ logExecution(input.sessionId, 'mission_start', `New goal contract: ${toOneLine(userGoalContract.objective, 200)}`, {
969
+ agentId: session.agentId,
970
+ detail: { goal: userGoalContract.objective, contract: userGoalContract, planSteps: state.planSteps },
971
+ })
912
972
  }
913
973
  state.followupChainCount = 0
974
+ state.missionTokens = 0
975
+ state.missionCostUsd = 0
976
+ }
977
+
978
+ // Accumulate per-mission token/cost tracking
979
+ if (typeof input.inputTokens === 'number') {
980
+ state.missionTokens = (state.missionTokens || 0) + input.inputTokens + (input.outputTokens || 0)
981
+ }
982
+ if (typeof input.estimatedCost === 'number') {
983
+ state.missionCostUsd = (state.missionCostUsd || 0) + input.estimatedCost
914
984
  }
915
985
 
916
986
  if (state.paused && input.internal) {
@@ -958,7 +1028,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
958
1028
 
959
1029
  if (shouldAutoKickFromUserGoal) {
960
1030
  followup = {
961
- message: buildFollowupPrompt(state, { hasMemoryTool }),
1031
+ message: buildFollowupPrompt(state, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
962
1032
  delayMs: 1500,
963
1033
  dedupeKey: `main-loop-user-kickoff:${input.sessionId}`,
964
1034
  }
@@ -1020,11 +1090,33 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
1020
1090
  )
1021
1091
  consumeEvents(state, meta.consume_event_ids)
1022
1092
 
1023
- if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < MAX_FOLLOWUP_CHAIN) {
1093
+ // Budget enforcement: check mission cost against goalContract.budgetUsd
1094
+ const budgetUsd = state.goalContract?.budgetUsd
1095
+ if (typeof budgetUsd === 'number' && budgetUsd > 0 && typeof state.missionCostUsd === 'number') {
1096
+ const usageRatio = state.missionCostUsd / budgetUsd
1097
+ if (usageRatio >= 1.0 && !state.paused) {
1098
+ state.paused = true
1099
+ state.status = 'blocked'
1100
+ appendTimeline(state, 'budget_exceeded', `Mission budget exceeded ($${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}). Mission paused.`, now, 'blocked')
1101
+ appendEvent(state, 'budget_exceeded', `Budget limit reached: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)}`, now)
1102
+ logExecution(input.sessionId, 'budget_warning', `Budget exceeded: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
1103
+ agentId: session.agentId,
1104
+ detail: { missionCostUsd: state.missionCostUsd, budgetUsd, missionTokens: state.missionTokens },
1105
+ })
1106
+ } else if (usageRatio >= 0.8) {
1107
+ appendEvent(state, 'budget_warning', `Mission approaching budget limit: $${state.missionCostUsd.toFixed(4)} of $${budgetUsd.toFixed(4)} (${Math.round(usageRatio * 100)}%)`, now)
1108
+ logExecution(input.sessionId, 'budget_warning', `Budget at ${Math.round(usageRatio * 100)}%: $${state.missionCostUsd.toFixed(4)} / $${budgetUsd.toFixed(4)}`, {
1109
+ agentId: session.agentId,
1110
+ detail: { missionCostUsd: state.missionCostUsd, budgetUsd, usagePercent: Math.round(usageRatio * 100) },
1111
+ })
1112
+ }
1113
+ }
1114
+
1115
+ if (meta.follow_up === true && !input.error && !isHeartbeatOk && !state.paused && state.followupChainCount < getMaxFollowupChain(session.agentId)) {
1024
1116
  state.followupChainCount += 1
1025
1117
  const delaySec = clampInt(meta.delay_sec, DEFAULT_FOLLOWUP_DELAY_SEC, 5, 900)
1026
1118
  followup = {
1027
- message: buildFollowupPrompt(state, { hasMemoryTool }),
1119
+ message: buildFollowupPrompt(state, { hasMemoryTool, agent: session.agentId ? loadAgents()[session.agentId] : null, session }),
1028
1120
  delayMs: delaySec * 1000,
1029
1121
  dedupeKey: `main-loop-followup:${input.sessionId}`,
1030
1122
  }
@@ -1034,6 +1126,12 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
1034
1126
  }
1035
1127
  if (state.status === 'ok' || state.status === 'blocked') {
1036
1128
  forceMemoryNote = true
1129
+ if (state.status === 'ok') {
1130
+ logExecution(input.sessionId, 'mission_complete', `Mission completed: ${toOneLine(state.goal || 'unknown', 200)}`, {
1131
+ agentId: session.agentId,
1132
+ detail: { momentumScore: state.momentumScore, followupChainCount: state.followupChainCount, missionTokens: state.missionTokens, missionCostUsd: state.missionCostUsd },
1133
+ })
1134
+ }
1037
1135
  }
1038
1136
  } else if (!isHeartbeatOk && trimmedText) {
1039
1137
  state.metaMissCount = Math.min(100, state.metaMissCount + 1)
@@ -1064,6 +1162,7 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
1064
1162
  state.missionTaskId = upsertMissionTask(session, state, now)
1065
1163
  const shouldWritePeriodicMemory = !!state.summary && state.status === 'progress'
1066
1164
  maybeStoreMissionMemoryNote(session, state, now, input.source, forceMemoryNote || shouldWritePeriodicMemory)
1165
+
1067
1166
  state.momentumScore = computeMomentumScore(state)
1068
1167
 
1069
1168
  state.updatedAt = now
@@ -3,12 +3,15 @@ const MAIN_SESSION_NAME = '__main__'
3
3
  export function isProtectedMainSession(session: any): boolean {
4
4
  if (!session || typeof session !== 'object') return false
5
5
  if (session.mainSession === true) return true
6
-
7
- const name = typeof session.name === 'string' ? session.name.trim() : ''
8
- if (name === MAIN_SESSION_NAME) return true
6
+ if (session.sessionType === 'orchestrated') return true
9
7
 
10
8
  const id = typeof session.id === 'string' ? session.id.trim() : ''
11
9
  if (id.startsWith('main-')) return true
10
+ if (id.startsWith('agent-thread-')) return true
11
+
12
+ const name = typeof session.name === 'string' ? session.name.trim() : ''
13
+ if (name === MAIN_SESSION_NAME) return true
14
+ if (name.startsWith('agent-thread:')) return true
12
15
 
13
16
  return false
14
17
  }
@@ -0,0 +1,18 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { runMcpConformanceCheck } from './mcp-conformance.ts'
4
+
5
+ test('runMcpConformanceCheck reports connect/list failure for unsupported transport', async () => {
6
+ const result = await runMcpConformanceCheck({
7
+ id: 'bad',
8
+ name: 'Bad MCP',
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ transport: 'invalid-transport' as any,
11
+ createdAt: Date.now(),
12
+ updatedAt: Date.now(),
13
+ })
14
+
15
+ assert.equal(result.ok, false)
16
+ assert.equal(result.toolsCount, 0)
17
+ assert.ok(result.issues.some((issue) => issue.code === 'connect_or_list_failed'))
18
+ })
@@ -0,0 +1,233 @@
1
+ import type { McpServerConfig } from '@/types'
2
+ import { connectMcpServer, disconnectMcpServer } from './mcp-client'
3
+
4
+ export interface McpConformanceIssue {
5
+ level: 'error' | 'warning'
6
+ code: string
7
+ message: string
8
+ toolName?: string
9
+ }
10
+
11
+ export interface McpConformanceOptions {
12
+ timeoutMs?: number
13
+ smokeToolName?: string | null
14
+ smokeToolArgs?: Record<string, unknown> | null
15
+ }
16
+
17
+ export interface McpConformanceResult {
18
+ ok: boolean
19
+ serverId: string
20
+ serverName: string
21
+ checkedAt: number
22
+ toolsCount: number
23
+ smokeToolName: string | null
24
+ issues: McpConformanceIssue[]
25
+ timings: {
26
+ connectMs: number
27
+ listToolsMs: number
28
+ smokeInvokeMs: number | null
29
+ }
30
+ }
31
+
32
+ const DEFAULT_TIMEOUT_MS = 12_000
33
+ const MIN_TIMEOUT_MS = 1_000
34
+ const MAX_TIMEOUT_MS = 120_000
35
+
36
+ function normalizeTimeoutMs(value: unknown): number {
37
+ const parsed = typeof value === 'number'
38
+ ? value
39
+ : typeof value === 'string'
40
+ ? Number.parseInt(value, 10)
41
+ : Number.NaN
42
+ if (!Number.isFinite(parsed)) return DEFAULT_TIMEOUT_MS
43
+ return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.trunc(parsed)))
44
+ }
45
+
46
+ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
47
+ return new Promise<T>((resolve, reject) => {
48
+ const timer = setTimeout(() => {
49
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`))
50
+ }, timeoutMs)
51
+ promise.then(
52
+ (value) => {
53
+ clearTimeout(timer)
54
+ resolve(value)
55
+ },
56
+ (error) => {
57
+ clearTimeout(timer)
58
+ reject(error)
59
+ },
60
+ )
61
+ })
62
+ }
63
+
64
+ function isRecord(value: unknown): value is Record<string, unknown> {
65
+ return !!value && typeof value === 'object' && !Array.isArray(value)
66
+ }
67
+
68
+ function normalizedRequired(schema: Record<string, unknown>): string[] {
69
+ const required = schema.required
70
+ if (!Array.isArray(required)) return []
71
+ return required.filter((entry): entry is string => typeof entry === 'string')
72
+ }
73
+
74
+ function findSmokeTool(tools: Array<Record<string, unknown>>, preferredName?: string | null): string | null {
75
+ const preferred = typeof preferredName === 'string' ? preferredName.trim() : ''
76
+ if (preferred) {
77
+ const found = tools.find((tool) => tool.name === preferred)
78
+ if (found) return preferred
79
+ }
80
+
81
+ const noArg = tools.find((tool) => {
82
+ const schema = isRecord(tool.inputSchema) ? tool.inputSchema : {}
83
+ const required = normalizedRequired(schema)
84
+ return required.length === 0
85
+ })
86
+ if (noArg && typeof noArg.name === 'string' && noArg.name.trim()) return noArg.name
87
+ return null
88
+ }
89
+
90
+ function validateToolSchemas(tools: Array<Record<string, unknown>>, issues: McpConformanceIssue[]): void {
91
+ const seenNames = new Set<string>()
92
+ for (const tool of tools) {
93
+ const toolName = typeof tool.name === 'string' ? tool.name.trim() : ''
94
+ if (!toolName) {
95
+ issues.push({
96
+ level: 'error',
97
+ code: 'tool_name_missing',
98
+ message: 'Tool is missing a valid name.',
99
+ })
100
+ continue
101
+ }
102
+ if (seenNames.has(toolName)) {
103
+ issues.push({
104
+ level: 'error',
105
+ code: 'tool_name_duplicate',
106
+ message: `Duplicate tool name "${toolName}" detected.`,
107
+ toolName,
108
+ })
109
+ continue
110
+ }
111
+ seenNames.add(toolName)
112
+
113
+ const schema = isRecord(tool.inputSchema) ? tool.inputSchema : null
114
+ if (!schema) {
115
+ issues.push({
116
+ level: 'warning',
117
+ code: 'tool_schema_missing',
118
+ message: `Tool "${toolName}" is missing an input schema.`,
119
+ toolName,
120
+ })
121
+ continue
122
+ }
123
+
124
+ const schemaType = typeof schema.type === 'string' ? schema.type : 'object'
125
+ if (schemaType !== 'object') {
126
+ issues.push({
127
+ level: 'warning',
128
+ code: 'tool_schema_non_object',
129
+ message: `Tool "${toolName}" schema type is "${schemaType}" (expected "object").`,
130
+ toolName,
131
+ })
132
+ }
133
+
134
+ const properties = isRecord(schema.properties) ? schema.properties : {}
135
+ const required = normalizedRequired(schema)
136
+ for (const req of required) {
137
+ if (!Object.prototype.hasOwnProperty.call(properties, req)) {
138
+ issues.push({
139
+ level: 'warning',
140
+ code: 'tool_schema_required_missing_property',
141
+ message: `Tool "${toolName}" marks "${req}" as required but it is not present in schema.properties.`,
142
+ toolName,
143
+ })
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ export async function runMcpConformanceCheck(
150
+ server: McpServerConfig,
151
+ options: McpConformanceOptions = {},
152
+ ): Promise<McpConformanceResult> {
153
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs)
154
+ const issues: McpConformanceIssue[] = []
155
+ const checkedAt = Date.now()
156
+ const result: McpConformanceResult = {
157
+ ok: false,
158
+ serverId: server.id,
159
+ serverName: server.name,
160
+ checkedAt,
161
+ toolsCount: 0,
162
+ smokeToolName: null,
163
+ issues,
164
+ timings: {
165
+ connectMs: 0,
166
+ listToolsMs: 0,
167
+ smokeInvokeMs: null,
168
+ },
169
+ }
170
+
171
+ let client: unknown
172
+ let transport: unknown
173
+ const connectStart = Date.now()
174
+ try {
175
+ const conn = await withTimeout(connectMcpServer(server), timeoutMs, 'MCP connect')
176
+ client = conn.client
177
+ transport = conn.transport
178
+ result.timings.connectMs = Date.now() - connectStart
179
+
180
+ const mcpClient = client as { listTools: () => Promise<Record<string, unknown>>; callTool: (opts: Record<string, unknown>) => Promise<unknown> }
181
+ const listStart = Date.now()
182
+ const listResponse = await withTimeout(mcpClient.listTools(), timeoutMs, 'MCP listTools') as Record<string, unknown>
183
+ result.timings.listToolsMs = Date.now() - listStart
184
+ const tools = Array.isArray(listResponse?.tools) ? listResponse.tools as Array<Record<string, unknown>> : []
185
+ result.toolsCount = tools.length
186
+
187
+ validateToolSchemas(tools, issues)
188
+
189
+ const smokeToolName = findSmokeTool(tools, options.smokeToolName)
190
+ result.smokeToolName = smokeToolName
191
+ if (!smokeToolName) {
192
+ issues.push({
193
+ level: 'warning',
194
+ code: 'smoke_tool_missing',
195
+ message: 'No smoke-testable tool found (no no-arg tool and no explicit smokeToolName).',
196
+ })
197
+ } else {
198
+ const smokeArgs = options.smokeToolArgs && isRecord(options.smokeToolArgs)
199
+ ? options.smokeToolArgs
200
+ : {}
201
+ const smokeStart = Date.now()
202
+ try {
203
+ await withTimeout(
204
+ mcpClient.callTool({ name: smokeToolName, arguments: smokeArgs }),
205
+ timeoutMs,
206
+ `MCP callTool(${smokeToolName})`,
207
+ )
208
+ } catch (err) {
209
+ issues.push({
210
+ level: 'error',
211
+ code: 'smoke_tool_failed',
212
+ message: err instanceof Error ? err.message : String(err),
213
+ toolName: smokeToolName,
214
+ })
215
+ } finally {
216
+ result.timings.smokeInvokeMs = Date.now() - smokeStart
217
+ }
218
+ }
219
+ } catch (err) {
220
+ issues.push({
221
+ level: 'error',
222
+ code: 'connect_or_list_failed',
223
+ message: err instanceof Error ? err.message : String(err),
224
+ })
225
+ } finally {
226
+ if (client && transport) {
227
+ await disconnectMcpServer(client, transport)
228
+ }
229
+ }
230
+
231
+ result.ok = issues.every((issue) => issue.level !== 'error')
232
+ return result
233
+ }