@swarmclawai/swarmclaw 1.3.1 → 1.3.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.
package/README.md CHANGED
@@ -204,6 +204,16 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
204
204
 
205
205
  ## Release Notes
206
206
 
207
+ ### v1.3.3 Highlights
208
+
209
+ - **Bug fix — stale connector status after auto-restart (#31)**: connectors that auto-restart via the daemon health monitor now show "Starting" instead of a stale "Stopped" or "Error" status in the UI until the daemon reports runtime state. Added `starting` to the `ConnectorStatus` type and updated both the connector list and detail views.
210
+ - **Bug fix — stale credentialId after credential rotation (#30)**: when a provider credential is deleted and re-created, connector sessions now fall back to resolving any valid credential for the same provider instead of failing with "Missing credentials."
211
+
212
+ ### v1.3.2 Highlights
213
+
214
+ - **Custom provider fix for standalone builds**: fixed `require('@/lib/server/storage')` path alias resolution failure that caused custom providers to silently break in standalone/npm-global installs with "a is not a function" errors. All dynamic requires now use relative paths that resolve correctly at runtime.
215
+ - **GitHub Copilot CLI provider**: new CLI provider wrapping the `copilot` binary with JSONL streaming, session continuity, system prompt injection, and multi-model support (Claude, GPT, Gemini via GitHub Copilot subscription).
216
+
207
217
  ### v1.3.1 Highlights
208
218
 
209
219
  - **SwarmDock SDK v0.2.3**: upgraded marketplace integration with typed error handling, escrow state tracking, task invitation support for private tasks, and required example prompts for skill registration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -322,7 +322,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
322
322
  {meta.label}
323
323
  </span>
324
324
  <span className="text-[11px] text-text-3">
325
- {isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
325
+ {isRunning ? 'Connected' : c.status === 'error' ? 'Error' : c.status === 'starting' ? 'Starting' : 'Stopped'}
326
326
  </span>
327
327
  </div>
328
328
  </div>
@@ -1270,7 +1270,7 @@ export function ConnectorSheet() {
1270
1270
  runtimeConnector?.status === 'error' ? 'bg-red-400' : 'bg-white/20'
1271
1271
  }`} />
1272
1272
  {effectiveRunning ? (waAuthenticated ? 'Connected and listening' : 'Connecting...') :
1273
- runtimeConnector?.status === 'error' ? 'Error — see below' : 'Not connected'}
1273
+ runtimeConnector?.status === 'error' ? 'Error — see below' : runtimeConnector?.status === 'starting' ? 'Starting...' : 'Not connected'}
1274
1274
  </div>
1275
1275
  </div>
1276
1276
  {effectiveRunning ? (
@@ -1,8 +1,8 @@
1
1
  /** CLI providers that use their own tool execution outside the shared tool-runtime path. */
2
- export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli'])
2
+ export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli'])
3
3
 
4
4
  /** Providers with native tool/capability support (CLI providers + OpenClaw). */
5
- export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw'])
5
+ export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'])
6
6
 
7
7
  /** Providers that can only act as workers — no coordinator role, no heartbeat, no advanced settings. */
8
- export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw'])
8
+ export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'])
@@ -39,6 +39,12 @@ const KNOWN_BINARY_PATHS: Record<string, string[]> = {
39
39
  '/usr/local/bin/gemini',
40
40
  '/opt/homebrew/bin/gemini',
41
41
  ],
42
+ copilot: [
43
+ path.join(os.homedir(), '.local/bin/copilot'),
44
+ '/usr/local/bin/copilot',
45
+ '/opt/homebrew/bin/copilot',
46
+ path.join(os.homedir(), '.npm-global/bin/copilot'),
47
+ ],
42
48
  }
43
49
 
44
50
  function getNvmBinaryPaths(name: string): string[] {
@@ -144,7 +150,7 @@ export interface AuthProbeResult {
144
150
  */
145
151
  export function probeCliAuth(
146
152
  binary: string,
147
- backend: 'claude' | 'codex' | 'opencode' | 'gemini',
153
+ backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot',
148
154
  env: NodeJS.ProcessEnv,
149
155
  cwd?: string,
150
156
  ): AuthProbeResult {
@@ -224,6 +230,37 @@ export function probeCliAuth(
224
230
  return { authenticated: true }
225
231
  }
226
232
 
233
+ if (backend === 'copilot') {
234
+ // Check for GitHub token in env first
235
+ if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN || process.env.COPILOT_GITHUB_TOKEN) {
236
+ return { authenticated: true }
237
+ }
238
+ // Try `gh auth status` as fallback (copilot inherits gh auth)
239
+ try {
240
+ const probe = spawnSync('gh', ['auth', 'status'], {
241
+ cwd, env, encoding: 'utf-8', timeout: 8000,
242
+ })
243
+ const probeText = `${probe.stdout || ''}\n${probe.stderr || ''}`.toLowerCase()
244
+ if ((probe.status ?? 1) === 0 || probeText.includes('logged in')) {
245
+ return { authenticated: true }
246
+ }
247
+ } catch { /* gh may not be installed */ }
248
+
249
+ // Fall back to config file check
250
+ const configPaths = [
251
+ path.join(os.homedir(), '.copilot/config.json'),
252
+ path.join(os.homedir(), '.config/copilot/config.json'),
253
+ ]
254
+ const hasConfig = configPaths.some((p) => fs.existsSync(p))
255
+ if (!hasConfig) {
256
+ return {
257
+ authenticated: false,
258
+ errorMessage: 'Copilot CLI is not authenticated. Run `copilot /login`, `gh auth login`, or set GH_TOKEN and try again.',
259
+ }
260
+ }
261
+ return { authenticated: true }
262
+ }
263
+
227
264
  return { authenticated: true }
228
265
  }
229
266
 
@@ -308,6 +345,7 @@ export const CLI_PROVIDER_CAPABILITIES: Record<string, string> = {
308
345
  'codex-cli': 'code generation, file creation, automated coding tasks',
309
346
  'opencode-cli': 'code analysis, generation across multiple LLM backends',
310
347
  'gemini-cli': 'code generation, analysis with Gemini models',
348
+ 'copilot-cli': 'code generation, analysis, multi-model support via GitHub Copilot',
311
349
  }
312
350
 
313
351
  /** Check if a provider ID is a CLI-based provider. */
@@ -0,0 +1,222 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
4
+ import { spawn } from 'child_process'
5
+ import type { StreamChatOptions } from './index'
6
+ import { log } from '../server/logger'
7
+ import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
8
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise } from './cli-utils'
9
+
10
+ /**
11
+ * GitHub Copilot CLI provider — spawns `copilot -p <message> --output-format=json -s --yolo`.
12
+ * Tracks `session.copilotSessionId` from streamed JSON events to support multi-turn continuity.
13
+ */
14
+ export function streamCopilotCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
15
+ const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
16
+ const binary = resolveCliBinary('copilot')
17
+ if (!binary) {
18
+ const msg = 'Copilot CLI not found. Install it (brew install copilot-cli, npm i -g @github/copilot, or https://gh.io/copilot-install) and ensure it is on your PATH.'
19
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
20
+ return Promise.resolve('')
21
+ }
22
+
23
+ const env = buildCliEnv()
24
+
25
+ // Pass GitHub token if available via session API key
26
+ if (session.apiKey) {
27
+ env.GH_TOKEN = session.apiKey
28
+ }
29
+
30
+ // Auth probe
31
+ if (!session.apiKey) {
32
+ const auth = probeCliAuth(binary, 'copilot', env, session.cwd)
33
+ if (!auth.authenticated) {
34
+ log.error('copilot-cli', auth.errorMessage || 'Auth failed')
35
+ write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Copilot CLI is not authenticated.' })}\n\n`)
36
+ return Promise.resolve('')
37
+ }
38
+ }
39
+
40
+ // Build prompt with optional system instructions
41
+ const promptParts: string[] = []
42
+ if (imagePath) {
43
+ promptParts.push(`[The user has shared an image at: ${imagePath}]`)
44
+ }
45
+ promptParts.push(message)
46
+ const prompt = promptParts.join('\n\n')
47
+
48
+ const args = ['-p', prompt, '--output-format=json', '-s', '--yolo']
49
+ if (session.copilotSessionId) args.push('--resume', session.copilotSessionId)
50
+ if (session.model) args.push('--model', session.model)
51
+
52
+ // System prompt: write temp AGENTS.override.md in a temp config dir
53
+ // Symlink auth files from the real config dir so auth still works
54
+ let tempCopilotHome: string | null = null
55
+ if (systemPrompt && !session.copilotSessionId) {
56
+ const realCopilotHome = process.env.COPILOT_HOME || path.join(os.homedir(), '.copilot')
57
+ tempCopilotHome = path.join(os.tmpdir(), `swarmclaw-copilot-${session.id}`)
58
+ fs.mkdirSync(tempCopilotHome, { recursive: true })
59
+
60
+ // Symlink auth/config files from real home into temp dir
61
+ symlinkConfigFiles(realCopilotHome, tempCopilotHome)
62
+
63
+ // Write system prompt as AGENTS.override.md
64
+ fs.writeFileSync(path.join(tempCopilotHome, 'AGENTS.override.md'), systemPrompt)
65
+ env.COPILOT_HOME = tempCopilotHome
66
+ }
67
+
68
+ log.info('copilot-cli', `Spawning: ${binary}`, {
69
+ args: args.map((a) => a.length > 100 ? a.slice(0, 100) + '...' : a),
70
+ cwd: session.cwd,
71
+ promptLen: prompt.length,
72
+ hasSystemPrompt: !!systemPrompt,
73
+ resumeSessionId: session.copilotSessionId || null,
74
+ })
75
+
76
+ const proc = spawn(binary, args, {
77
+ cwd: session.cwd,
78
+ env,
79
+ stdio: ['ignore', 'pipe', 'pipe'],
80
+ timeout: processTimeoutMs,
81
+ })
82
+
83
+ log.info('copilot-cli', `Process spawned: pid=${proc.pid}`)
84
+ active.set(session.id, proc)
85
+ attachAbortHandler(proc, signal)
86
+
87
+ let fullResponse = ''
88
+ let buf = ''
89
+ let eventCount = 0
90
+ let stderrText = ''
91
+
92
+ proc.stdout!.on('data', (chunk: Buffer) => {
93
+ const raw = chunk.toString()
94
+ buf += raw
95
+
96
+ if (eventCount === 0) {
97
+ log.debug('copilot-cli', `First stdout chunk (${raw.length} bytes)`, raw.slice(0, 500))
98
+ }
99
+
100
+ const lines = buf.split('\n')
101
+ buf = lines.pop()!
102
+
103
+ for (const line of lines) {
104
+ if (!line.trim()) continue
105
+ try {
106
+ const ev = JSON.parse(line) as Record<string, unknown>
107
+ eventCount++
108
+
109
+ // Capture session ID from init event
110
+ if (ev.type === 'init' && typeof ev.session_id === 'string') {
111
+ session.copilotSessionId = ev.session_id
112
+ log.info('copilot-cli', `Got session_id: ${ev.session_id}`)
113
+ }
114
+
115
+ // Streaming text deltas
116
+ if (ev.type === 'content_block_delta') {
117
+ const delta = ev.delta as Record<string, unknown> | undefined
118
+ if (typeof delta?.text === 'string') {
119
+ fullResponse += delta.text
120
+ write(`data: ${JSON.stringify({ t: 'd', text: delta.text })}\n\n`)
121
+ }
122
+ }
123
+
124
+ // Agent message chunks (ACP format)
125
+ else if (ev.type === 'agent_message_chunk' && typeof ev.text === 'string') {
126
+ fullResponse += ev.text
127
+ write(`data: ${JSON.stringify({ t: 'd', text: ev.text })}\n\n`)
128
+ }
129
+
130
+ // Assistant message content
131
+ else if (ev.type === 'message' && ev.role === 'assistant' && typeof ev.content === 'string') {
132
+ fullResponse += ev.content
133
+ write(`data: ${JSON.stringify({ t: 'd', text: ev.content })}\n\n`)
134
+ }
135
+
136
+ // Completed item with agent_message
137
+ else if (ev.type === 'item.completed' && (ev.item as Record<string, unknown>)?.type === 'agent_message') {
138
+ const item = ev.item as Record<string, unknown>
139
+ if (typeof item.text === 'string') {
140
+ fullResponse = item.text
141
+ write(`data: ${JSON.stringify({ t: 'r', text: item.text })}\n\n`)
142
+ log.debug('copilot-cli', `Agent message (${item.text.length} chars)`)
143
+ }
144
+ }
145
+
146
+ // Final result
147
+ else if (ev.type === 'result' && typeof ev.result === 'string') {
148
+ fullResponse = ev.result
149
+ write(`data: ${JSON.stringify({ t: 'r', text: ev.result })}\n\n`)
150
+ log.debug('copilot-cli', `Result event (${ev.result.length} chars)`)
151
+ }
152
+
153
+ // Error result
154
+ else if (ev.type === 'result' && ev.status === 'error') {
155
+ const errMsg = typeof ev.error === 'string' ? ev.error : 'Copilot error'
156
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
157
+ log.warn('copilot-cli', `Error result: ${errMsg}`)
158
+ }
159
+
160
+ // Event error
161
+ else if (ev.type === 'error') {
162
+ const errMsg = typeof ev.message === 'string'
163
+ ? ev.message
164
+ : typeof ev.error === 'string'
165
+ ? ev.error
166
+ : 'Unknown Copilot error'
167
+ write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
168
+ log.warn('copilot-cli', `Event error: ${errMsg}`)
169
+ }
170
+
171
+ else if (eventCount <= 10) {
172
+ log.debug('copilot-cli', `Event: ${String(ev.type)}`)
173
+ }
174
+ } catch {
175
+ if (line.trim()) {
176
+ log.debug('copilot-cli', `Non-JSON stdout line`, line.slice(0, 300))
177
+ fullResponse += line + '\n'
178
+ write(`data: ${JSON.stringify({ t: 'd', text: line + '\n' })}\n\n`)
179
+ }
180
+ }
181
+ }
182
+ })
183
+
184
+ proc.stderr!.on('data', (chunk: Buffer) => {
185
+ const text = chunk.toString()
186
+ stderrText += text
187
+ if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
188
+ if (isStderrNoise(text)) {
189
+ log.debug('copilot-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
190
+ } else {
191
+ log.warn('copilot-cli', `stderr [${session.id}]`, text.slice(0, 500))
192
+ }
193
+ })
194
+
195
+ return new Promise((resolve) => {
196
+ proc.on('close', (code, sig) => {
197
+ log.info('copilot-cli', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
198
+ active.delete(session.id)
199
+ // Clean up temp config dir
200
+ if (tempCopilotHome) {
201
+ try { fs.rmSync(tempCopilotHome, { recursive: true }) } catch { /* ignore */ }
202
+ }
203
+ if ((code ?? 0) !== 0 && !fullResponse.trim()) {
204
+ const msg = stderrText.trim()
205
+ ? `Copilot CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
206
+ : `Copilot CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
207
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
208
+ }
209
+ resolve(fullResponse)
210
+ })
211
+
212
+ proc.on('error', (e) => {
213
+ log.error('copilot-cli', `Process error: ${e.message}`)
214
+ active.delete(session.id)
215
+ if (tempCopilotHome) {
216
+ try { fs.rmSync(tempCopilotHome, { recursive: true }) } catch { /* ignore */ }
217
+ }
218
+ write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
219
+ resolve(fullResponse)
220
+ })
221
+ })
222
+ }
@@ -2,6 +2,7 @@ import { streamClaudeCliChat } from './claude-cli'
2
2
  import { streamCodexCliChat } from './codex-cli'
3
3
  import { streamOpenCodeCliChat } from './opencode-cli'
4
4
  import { streamGeminiCliChat } from './gemini-cli'
5
+ import { streamCopilotCliChat } from './copilot-cli'
5
6
  import { streamOpenAiChat } from './openai'
6
7
  import { streamOllamaChat } from './ollama'
7
8
  import { streamAnthropicChat } from './anthropic'
@@ -102,6 +103,14 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
102
103
  requiresEndpoint: false,
103
104
  handler: { streamChat: streamGeminiCliChat },
104
105
  },
106
+ 'copilot-cli': {
107
+ id: 'copilot-cli',
108
+ name: 'GitHub Copilot CLI',
109
+ models: ['claude-sonnet-4-5', 'gpt-4.1', 'gemini-3-pro'],
110
+ requiresApiKey: false,
111
+ requiresEndpoint: false,
112
+ handler: { streamChat: streamCopilotCliChat },
113
+ },
105
114
  google: {
106
115
  id: 'google',
107
116
  name: 'Google Gemini',
@@ -281,7 +290,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
281
290
  function getCustomProviders(): Record<string, CustomProviderConfig> {
282
291
  try {
283
292
  // eslint-disable-next-line @typescript-eslint/no-require-imports
284
- const { loadProviderConfigs } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
293
+ const { loadProviderConfigs } = require('../server/storage') as typeof import('@/lib/server/storage')
285
294
  const configs = loadProviderConfigs() as Record<string, CustomProviderConfig>
286
295
  return Object.fromEntries(
287
296
  Object.entries(configs).filter(([, config]) => config?.type === 'custom'),
@@ -295,7 +304,7 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
295
304
  function getModelOverrides(): Record<string, string[]> {
296
305
  try {
297
306
  // eslint-disable-next-line @typescript-eslint/no-require-imports
298
- const { loadModelOverrides } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
307
+ const { loadModelOverrides } = require('../server/storage') as typeof import('@/lib/server/storage')
299
308
  return loadModelOverrides()
300
309
  } catch {
301
310
  return {}
@@ -313,7 +322,7 @@ export function getProviderList(): ProviderInfo[] {
313
322
  ...info,
314
323
  models: overrides[info.id] || info.models,
315
324
  defaultModels: info.models,
316
- supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'fireworks'].includes(info.id),
325
+ supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'fireworks'].includes(info.id),
317
326
  }
318
327
  })
319
328
 
@@ -383,7 +392,7 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
383
392
  if (id.startsWith('custom-') && !custom) {
384
393
  try {
385
394
  // eslint-disable-next-line @typescript-eslint/no-require-imports
386
- const { loadStoredItem } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
395
+ const { loadStoredItem } = require('../server/storage') as typeof import('@/lib/server/storage')
387
396
  const directConfig = loadStoredItem('provider_configs', id) as CustomProviderConfig | null
388
397
  if (directConfig?.type === 'custom' && directConfig.isEnabled) {
389
398
  log.info(TAG, `Resolved custom provider '${id}' via direct DB lookup (batch load missed it)`)
@@ -447,7 +456,7 @@ export async function streamChatWithFailover(
447
456
  if (credId && i > 0) {
448
457
  // Need to decrypt fallback credential
449
458
  // eslint-disable-next-line @typescript-eslint/no-require-imports
450
- const { loadCredentials, decryptKey } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
459
+ const { loadCredentials, decryptKey } = require('../server/storage') as typeof import('@/lib/server/storage')
451
460
  const creds = loadCredentials()
452
461
  const cred = creds[credId]
453
462
  if (cred?.encryptedKey) {
@@ -4,7 +4,7 @@ import type { Agent, ProviderType } from '@/types'
4
4
  import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from './agent-availability'
5
5
 
6
6
  describe('isWorkerOnlyAgent', () => {
7
- const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw'] satisfies ProviderType[]
7
+ const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'openclaw'] satisfies ProviderType[]
8
8
  const NON_CLI_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'groq', 'together'] satisfies ProviderType[]
9
9
 
10
10
  function withProvider(provider: unknown): Pick<Agent, 'provider'> {
@@ -160,7 +160,7 @@ function notifySwarmChanged() {
160
160
  function persistSwarmSnapshot(swarm: SwarmHandle): void {
161
161
  try {
162
162
  // eslint-disable-next-line @typescript-eslint/no-require-imports
163
- const { upsertStoredItem } = require('@/lib/server/storage')
163
+ const { upsertStoredItem } = require('../storage')
164
164
  upsertStoredItem('swarm_snapshots', swarm.swarmId, {
165
165
  swarmId: swarm.swarmId,
166
166
  parentSessionId: swarm.parentSessionId,
@@ -550,7 +550,7 @@ export function getSwarmSnapshot(swarmId: string): SwarmSnapshot | null {
550
550
  // Fallback to persisted store for swarms from previous process lifetimes
551
551
  try {
552
552
  // eslint-disable-next-line @typescript-eslint/no-require-imports
553
- const { loadStoredItem } = require('@/lib/server/storage')
553
+ const { loadStoredItem } = require('../storage')
554
554
  const persisted = loadStoredItem('swarm_snapshots', swarmId)
555
555
  return persisted ? (persisted as SwarmSnapshot) : null
556
556
  } catch { return null }
@@ -641,7 +641,7 @@ function buildSwarmSnapshot(swarm: SwarmHandle): SwarmSnapshot {
641
641
  export function restoreSwarmRegistry(): number {
642
642
  try {
643
643
  // eslint-disable-next-line @typescript-eslint/no-require-imports
644
- const { loadCollection, upsertStoredItem } = require('@/lib/server/storage')
644
+ const { loadCollection, upsertStoredItem } = require('../storage')
645
645
  const persisted = loadCollection('swarm_snapshots') as Record<string, SwarmSnapshot>
646
646
  let lost = 0
647
647
  for (const [id, record] of Object.entries(persisted)) {
@@ -45,9 +45,9 @@ function buildExtensionCapabilityLines(enabledExtensions: string[], opts?: { del
45
45
  if (opts.agentId) {
46
46
  try {
47
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports
48
- const { loadAgents } = require('@/lib/server/storage')
48
+ const { loadAgents } = require('../storage')
49
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports
50
- const { resolveTeam } = require('@/lib/server/agents/team-resolution')
50
+ const { resolveTeam } = require('../agents/team-resolution')
51
51
  const agents = loadAgents() as Record<string, Record<string, unknown>>
52
52
  const team = resolveTeam(opts.agentId, agents)
53
53
  if (team.mode === 'team') {
@@ -28,6 +28,7 @@ import {
28
28
  selectChatroomRecipients,
29
29
  } from '@/lib/server/chatrooms/chatroom-routing'
30
30
  import { markProviderFailure, markProviderSuccess } from '../provider-health'
31
+ import { listCredentialIdsByProvider, resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
31
32
  import { buildIdentityContinuityContext } from '../identity-continuity'
32
33
  import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
33
34
  import { getProvider } from '@/lib/providers'
@@ -1009,6 +1010,18 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
1009
1010
  }
1010
1011
  }
1011
1012
 
1013
+ // Fallback: session credential was deleted — try any credential for this provider
1014
+ if (!apiKey && session.provider) {
1015
+ const providerCredentialIds = listCredentialIdsByProvider(session.provider)
1016
+ for (const id of providerCredentialIds) {
1017
+ const resolved = resolveCredentialSecret(id)
1018
+ if (resolved) {
1019
+ apiKey = resolved
1020
+ break
1021
+ }
1022
+ }
1023
+ }
1024
+
1012
1025
  // Build system prompt: [identity] \n\n [userPrompt] \n\n [soul] \n\n [systemPrompt]
1013
1026
  const settings = loadSettings()
1014
1027
  const promptParts: string[] = []
@@ -62,11 +62,13 @@ function persistConnector(connector: Connector): void {
62
62
  }
63
63
 
64
64
  function applyRuntimeFields(connector: Connector, runtime: DaemonConnectorRuntimeState | null): Connector {
65
- connector.status = runtime?.status
66
- ? runtime.status
67
- : connector.lastError
68
- ? 'error'
69
- : 'stopped'
65
+ if (runtime?.status) {
66
+ connector.status = runtime.status
67
+ } else if (connector.isEnabled) {
68
+ connector.status = 'starting'
69
+ } else {
70
+ connector.status = connector.lastError ? 'error' : 'stopped'
71
+ }
70
72
 
71
73
  if (connector.platform === 'whatsapp') {
72
74
  connector.authenticated = runtime?.authenticated
@@ -71,7 +71,7 @@ export function markProviderFailure(providerId: string, error: string, credentia
71
71
  })
72
72
  try {
73
73
  // eslint-disable-next-line @typescript-eslint/no-require-imports
74
- const { upsertStoredItem } = require('@/lib/server/storage')
74
+ const { upsertStoredItem } = require('./storage')
75
75
  upsertStoredItem('provider_health', key, states.get(key)!)
76
76
  } catch {}
77
77
  }
@@ -89,7 +89,7 @@ export function markProviderSuccess(providerId: string, credentialId?: string |
89
89
  })
90
90
  try {
91
91
  // eslint-disable-next-line @typescript-eslint/no-require-imports
92
- const { upsertStoredItem } = require('@/lib/server/storage')
92
+ const { upsertStoredItem } = require('./storage')
93
93
  upsertStoredItem('provider_health', key, states.get(key)!)
94
94
  } catch {}
95
95
  }
@@ -188,7 +188,7 @@ export function getProviderHealthSnapshot(): Record<string, ProviderHealthState
188
188
  export function restoreProviderHealthState(): number {
189
189
  try {
190
190
  // eslint-disable-next-line @typescript-eslint/no-require-imports
191
- const { loadCollection } = require('@/lib/server/storage')
191
+ const { loadCollection } = require('./storage')
192
192
  const persisted = loadCollection('provider_health') as Record<string, ProviderHealthState>
193
193
  let restored = 0
194
194
  for (const [id, record] of Object.entries(persisted)) {
@@ -323,7 +323,7 @@ export async function pingProvider(
323
323
  apiKey: string | undefined,
324
324
  endpoint: string | undefined,
325
325
  ): Promise<{ ok: boolean; message: string }> {
326
- const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli']
326
+ const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli']
327
327
  if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
328
328
 
329
329
  try {
@@ -620,6 +620,8 @@ function normalizeStoredRecordInner(
620
620
  }
621
621
  // Default geminiSessionId for new field
622
622
  if (session.geminiSessionId === undefined) session.geminiSessionId = null
623
+ // Default copilotSessionId for new field
624
+ if (session.copilotSessionId === undefined) session.copilotSessionId = null
623
625
  // Default injectedMemoryIds for proactive recall dedup
624
626
  if (!session.injectedMemoryIds || typeof session.injectedMemoryIds !== 'object') {
625
627
  session.injectedMemoryIds = {}
@@ -29,7 +29,7 @@ export type ConnectorPlatform =
29
29
  | 'webchat'
30
30
  | 'mockmail'
31
31
  | 'swarmdock'
32
- export type ConnectorStatus = 'stopped' | 'running' | 'error'
32
+ export type ConnectorStatus = 'stopped' | 'running' | 'error' | 'starting'
33
33
 
34
34
  export interface MessageSource {
35
35
  platform: ConnectorPlatform
@@ -1,4 +1,4 @@
1
- export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'openai' | 'ollama' | 'anthropic' | 'openclaw' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
1
+ export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'openai' | 'ollama' | 'anthropic' | 'openclaw' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks' | 'nebius' | 'deepinfra'
2
2
  export type ProviderId = ProviderType | (string & {})
3
3
 
4
4
  export interface ProviderInfo {
@@ -70,11 +70,13 @@ export interface Session {
70
70
  codexThreadId?: string | null
71
71
  opencodeSessionId?: string | null
72
72
  geminiSessionId?: string | null
73
+ copilotSessionId?: string | null
73
74
  delegateResumeIds?: {
74
75
  claudeCode?: string | null
75
76
  codex?: string | null
76
77
  opencode?: string | null
77
78
  gemini?: string | null
79
+ copilot?: string | null
78
80
  }
79
81
  /** @deprecated Messages are stored in session_messages table. Use message-repository. */
80
82
  messages: Message[]