@swarmclawai/swarmclaw 1.5.3 → 1.5.33
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 +167 -147
- package/bin/swarmclaw.js +35 -1
- package/package.json +2 -2
- package/scripts/run-next-build.mjs +87 -2
- package/skills/swarmclaw/SKILL.md +151 -0
- package/src/app/api/agents/[id]/route.ts +8 -0
- package/src/app/api/agents/agents-route.test.ts +114 -0
- package/src/app/api/agents/route.ts +10 -0
- package/src/app/api/connectors/route.ts +25 -13
- package/src/app/api/credentials/[id]/route.ts +8 -1
- package/src/app/api/schedules/[id]/route.ts +8 -0
- package/src/app/api/secrets/[id]/route.ts +10 -0
- package/src/app/api/setup/check-provider/route.ts +45 -0
- package/src/app/api/setup/doctor/route.ts +5 -0
- package/src/cli/binary.test.js +11 -0
- package/src/cli/index.js +4 -4
- package/src/cli/index.test.js +5 -2
- package/src/cli/index.ts +1 -1
- package/src/components/agents/agent-sheet.tsx +16 -4
- package/src/components/agents/inspector-panel.tsx +5 -0
- package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
- package/src/components/chat/activity-moment.tsx +3 -0
- package/src/components/chat/chat-header.tsx +23 -2
- package/src/components/chat/tool-call-bubble.tsx +20 -0
- package/src/components/layout/sidebar-rail.tsx +29 -0
- package/src/hooks/setup-done-detection.test.ts +4 -2
- package/src/hooks/setup-done-detection.ts +1 -1
- package/src/lib/orchestrator-config.ts +5 -0
- package/src/lib/provider-sets.ts +4 -4
- package/src/lib/providers/acp-client.ts +116 -0
- package/src/lib/providers/cli-utils.test.ts +9 -1
- package/src/lib/providers/cli-utils.ts +89 -4
- package/src/lib/providers/cursor-cli.ts +172 -0
- package/src/lib/providers/goose.ts +149 -0
- package/src/lib/providers/index.ts +29 -1
- package/src/lib/providers/qwen-code-cli.ts +152 -0
- package/src/lib/server/agents/agent-availability.ts +2 -2
- package/src/lib/server/agents/agent-thread-session.ts +8 -0
- package/src/lib/server/agents/task-session.ts +8 -0
- package/src/lib/server/capability-router.ts +8 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +3 -0
- package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
- package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +3 -1
- package/src/lib/server/chat-execution/stream-agent-chat.ts +5 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
- package/src/lib/server/chats/chat-session-service.ts +18 -0
- package/src/lib/server/connectors/session.ts +8 -0
- package/src/lib/server/context-manager.ts +5 -0
- package/src/lib/server/provider-health.ts +13 -3
- package/src/lib/server/provider-model-discovery.test.ts +8 -0
- package/src/lib/server/provider-model-discovery.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +2 -2
- package/src/lib/server/runtime/queue/core.ts +30 -4
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
- package/src/lib/server/session-reset-policy.test.ts +16 -0
- package/src/lib/server/session-reset-policy.ts +9 -1
- package/src/lib/server/session-tools/context.ts +2 -2
- package/src/lib/server/session-tools/delegate.ts +334 -14
- package/src/lib/server/session-tools/index.ts +5 -2
- package/src/lib/server/session-tools/session-info.ts +4 -1
- package/src/lib/server/storage-auth-docker.test.ts +244 -0
- package/src/lib/server/storage-auth.test.ts +150 -0
- package/src/lib/server/storage-auth.ts +57 -22
- package/src/lib/server/storage-normalization.ts +19 -0
- package/src/lib/server/storage.ts +3 -0
- package/src/lib/server/tasks/task-resume.ts +23 -1
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy.ts +4 -1
- package/src/lib/setup-defaults.test.ts +6 -0
- package/src/lib/setup-defaults.ts +146 -0
- package/src/lib/tool-definitions.ts +1 -1
- package/src/types/misc.ts +4 -1
- package/src/types/provider.ts +1 -1
- package/src/types/session.ts +9 -0
|
@@ -616,7 +616,11 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
|
|
|
616
616
|
}
|
|
617
617
|
|
|
618
618
|
const turnHistory = getMessages(sessionId)
|
|
619
|
-
|
|
619
|
+
// Skip message classification for local/CLI providers to avoid sending
|
|
620
|
+
// a blocking LLM call to the same single-request provider (e.g. Ollama)
|
|
621
|
+
const isLocalProvider = sessionForRun.provider === 'ollama' && sessionForRun.ollamaMode !== 'cloud'
|
|
622
|
+
const skipClassifier = internal || NON_LANGGRAPH_PROVIDER_IDS.has(sessionForRun.provider) || isLocalProvider
|
|
623
|
+
const classification = !skipClassifier
|
|
620
624
|
? await classifyMessage({
|
|
621
625
|
sessionId,
|
|
622
626
|
agentId: session.agentId || null,
|
|
@@ -503,6 +503,9 @@ const FORCED_DELEGATION_TOOLS: DelegateTool[] = [
|
|
|
503
503
|
'delegate_to_codex_cli',
|
|
504
504
|
'delegate_to_opencode_cli',
|
|
505
505
|
'delegate_to_gemini_cli',
|
|
506
|
+
'delegate_to_copilot_cli',
|
|
507
|
+
'delegate_to_cursor_cli',
|
|
508
|
+
'delegate_to_qwen_code_cli',
|
|
506
509
|
]
|
|
507
510
|
|
|
508
511
|
export async function runPostLlmToolRouting(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { IterationTimers } from './iteration-timers'
|
|
4
|
+
|
|
5
|
+
function makeTimers(opts?: { streamIdleStallMs?: number; initialPrefillStallMs?: number }) {
|
|
6
|
+
const controller = new AbortController()
|
|
7
|
+
const timers = new IterationTimers(controller, {
|
|
8
|
+
streamIdleStallMs: opts?.streamIdleStallMs ?? 100,
|
|
9
|
+
initialPrefillStallMs: opts?.initialPrefillStallMs,
|
|
10
|
+
requiredToolKickoffMs: 5000,
|
|
11
|
+
shouldEnforceEarlyRequiredToolKickoff: false,
|
|
12
|
+
})
|
|
13
|
+
return { timers, controller }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('IterationTimers', () => {
|
|
17
|
+
it('first arm uses initialPrefillStallMs when provided', async () => {
|
|
18
|
+
const { timers, controller } = makeTimers({
|
|
19
|
+
streamIdleStallMs: 50,
|
|
20
|
+
initialPrefillStallMs: 200,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
timers.armIdleWatchdog(false)
|
|
24
|
+
|
|
25
|
+
// After 50ms (streamIdleStallMs), should NOT have timed out
|
|
26
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
27
|
+
assert.equal(timers.idleTimedOut, false, 'should not time out at streamIdleStallMs')
|
|
28
|
+
|
|
29
|
+
// After 200ms (initialPrefillStallMs), should have timed out
|
|
30
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
31
|
+
assert.equal(timers.idleTimedOut, true, 'should time out at initialPrefillStallMs')
|
|
32
|
+
assert.equal(controller.signal.aborted, true)
|
|
33
|
+
|
|
34
|
+
timers.clearAll()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('second arm uses streamIdleStallMs (not prefill)', async () => {
|
|
38
|
+
const { timers, controller } = makeTimers({
|
|
39
|
+
streamIdleStallMs: 50,
|
|
40
|
+
initialPrefillStallMs: 500,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// First arm — uses prefill timeout
|
|
44
|
+
timers.armIdleWatchdog(false)
|
|
45
|
+
// Immediately re-arm (simulates receiving a stream token)
|
|
46
|
+
timers.armIdleWatchdog(false)
|
|
47
|
+
|
|
48
|
+
// After 80ms (> streamIdleStallMs), should have timed out
|
|
49
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
50
|
+
assert.equal(timers.idleTimedOut, true, 'second arm should use streamIdleStallMs')
|
|
51
|
+
assert.equal(controller.signal.aborted, true)
|
|
52
|
+
|
|
53
|
+
timers.clearAll()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('defaults initialPrefillStallMs to 2x streamIdleStallMs when not set', async () => {
|
|
57
|
+
const { timers } = makeTimers({
|
|
58
|
+
streamIdleStallMs: 60,
|
|
59
|
+
// initialPrefillStallMs not set — should default to 120
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
timers.armIdleWatchdog(false)
|
|
63
|
+
|
|
64
|
+
// After 80ms (> 60ms but < 120ms), should NOT have timed out
|
|
65
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
66
|
+
assert.equal(timers.idleTimedOut, false, 'should not time out before 2x default')
|
|
67
|
+
|
|
68
|
+
// After 60 more ms (total ~140ms, > 120ms), should have timed out
|
|
69
|
+
await new Promise((r) => setTimeout(r, 60))
|
|
70
|
+
assert.equal(timers.idleTimedOut, true, 'should time out at 2x default')
|
|
71
|
+
|
|
72
|
+
timers.clearAll()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not arm when waitingForToolResult is true', () => {
|
|
76
|
+
const { timers } = makeTimers({ streamIdleStallMs: 10 })
|
|
77
|
+
|
|
78
|
+
timers.armIdleWatchdog(true)
|
|
79
|
+
|
|
80
|
+
// Timer should not be set
|
|
81
|
+
assert.equal(timers.idleTimedOut, false)
|
|
82
|
+
timers.clearAll()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
export interface IterationTimerOpts {
|
|
7
7
|
/** Milliseconds of idle streaming before aborting the iteration. */
|
|
8
8
|
streamIdleStallMs: number
|
|
9
|
+
/**
|
|
10
|
+
* Milliseconds before aborting the initial model prefill phase (before any
|
|
11
|
+
* streaming token arrives). Defaults to `streamIdleStallMs * 2` when unset.
|
|
12
|
+
* Local models with large prompts may need extra time for the prefill pass.
|
|
13
|
+
*/
|
|
14
|
+
initialPrefillStallMs?: number
|
|
9
15
|
/** Milliseconds before forcing a required-tool kickoff reminder. */
|
|
10
16
|
requiredToolKickoffMs: number
|
|
11
17
|
/** Whether the early kickoff enforcement is enabled at all. */
|
|
@@ -17,6 +23,7 @@ export class IterationTimers {
|
|
|
17
23
|
private requiredToolKickoffTimer: ReturnType<typeof setTimeout> | null = null
|
|
18
24
|
private _idleTimedOut = false
|
|
19
25
|
private _requiredToolKickoffTimedOut = false
|
|
26
|
+
private _receivedFirstStreamEvent = false
|
|
20
27
|
|
|
21
28
|
constructor(
|
|
22
29
|
private readonly iterationController: AbortController,
|
|
@@ -26,13 +33,23 @@ export class IterationTimers {
|
|
|
26
33
|
get idleTimedOut(): boolean { return this._idleTimedOut }
|
|
27
34
|
get requiredToolKickoffTimedOut(): boolean { return this._requiredToolKickoffTimedOut }
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Arm the idle watchdog. On the first call (before any streaming tokens
|
|
38
|
+
* arrive), use the longer `initialPrefillStallMs` timeout to allow for
|
|
39
|
+
* model prefill on local providers. Subsequent re-arms use the standard
|
|
40
|
+
* `streamIdleStallMs` timeout.
|
|
41
|
+
*/
|
|
29
42
|
armIdleWatchdog(waitingForToolResult: boolean): void {
|
|
30
43
|
this.clearIdleWatchdog()
|
|
31
44
|
if (waitingForToolResult || this.iterationController.signal.aborted) return
|
|
45
|
+
const timeoutMs = this._receivedFirstStreamEvent
|
|
46
|
+
? this.opts.streamIdleStallMs
|
|
47
|
+
: (this.opts.initialPrefillStallMs ?? this.opts.streamIdleStallMs * 2)
|
|
48
|
+
this._receivedFirstStreamEvent = true
|
|
32
49
|
this.idleTimer = setTimeout(() => {
|
|
33
50
|
this._idleTimedOut = true
|
|
34
51
|
this.iterationController.abort()
|
|
35
|
-
},
|
|
52
|
+
}, timeoutMs)
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
clearIdleWatchdog(): void {
|
|
@@ -120,6 +120,8 @@ function normalizeRuntimeExtensionId(extensionId: string): string {
|
|
|
120
120
|
if (normalized === 'delegate_to_codex_cli' || normalized === 'codex_cli') return 'codex_cli'
|
|
121
121
|
if (normalized === 'delegate_to_opencode_cli' || normalized === 'opencode_cli') return 'opencode_cli'
|
|
122
122
|
if (normalized === 'delegate_to_gemini_cli' || normalized === 'gemini_cli') return 'gemini_cli'
|
|
123
|
+
if (normalized === 'delegate_to_cursor_cli' || normalized === 'cursor_cli') return 'cursor_cli'
|
|
124
|
+
if (normalized === 'delegate_to_qwen_code_cli' || normalized === 'qwen_code_cli') return 'qwen_code_cli'
|
|
123
125
|
if (['session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'].includes(normalized)) return 'manage_sessions'
|
|
124
126
|
return canonicalizeExtensionId(normalized)
|
|
125
127
|
}
|
|
@@ -189,7 +191,7 @@ export function buildRuntimeOrientationSection(params: {
|
|
|
189
191
|
const enabledExtensions = canonicalizeEnabledExtensions(params.sessionExtensions)
|
|
190
192
|
const workspaceMarkers = collectWorkspaceMarkers(session.cwd)
|
|
191
193
|
const delegationEnabled = enabledExtensions.some((extensionId) =>
|
|
192
|
-
['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli'].includes(extensionId),
|
|
194
|
+
['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'cursor_cli', 'qwen_code_cli'].includes(extensionId),
|
|
193
195
|
)
|
|
194
196
|
|
|
195
197
|
const lines = [
|
|
@@ -929,8 +929,13 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
929
929
|
if (abortController.signal.aborted) iterationController.abort()
|
|
930
930
|
else abortController.signal.addEventListener('abort', onParentAbort)
|
|
931
931
|
|
|
932
|
+
// Local models (e.g. Ollama) need extra time for prompt prefill before
|
|
933
|
+
// the first streaming token arrives. Use 2x the idle stall timeout for
|
|
934
|
+
// the initial prefill phase.
|
|
935
|
+
const isLocalModel = session.provider === 'ollama' && session.ollamaMode !== 'cloud'
|
|
932
936
|
const timers = new IterationTimers(iterationController, {
|
|
933
937
|
streamIdleStallMs: runtime.streamIdleStallMs,
|
|
938
|
+
initialPrefillStallMs: isLocalModel ? runtime.streamIdleStallMs * 2 : undefined,
|
|
934
939
|
requiredToolKickoffMs: runtime.requiredToolKickoffMs,
|
|
935
940
|
shouldEnforceEarlyRequiredToolKickoff,
|
|
936
941
|
})
|
|
@@ -281,6 +281,9 @@ function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']
|
|
|
281
281
|
codex: null,
|
|
282
282
|
opencode: null,
|
|
283
283
|
gemini: null,
|
|
284
|
+
copilot: null,
|
|
285
|
+
cursor: null,
|
|
286
|
+
qwen: null,
|
|
284
287
|
}
|
|
285
288
|
}
|
|
286
289
|
|
|
@@ -302,6 +305,11 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
|
|
|
302
305
|
claudeSessionId: null,
|
|
303
306
|
codexThreadId: null,
|
|
304
307
|
opencodeSessionId: null,
|
|
308
|
+
geminiSessionId: null,
|
|
309
|
+
copilotSessionId: null,
|
|
310
|
+
cursorSessionId: null,
|
|
311
|
+
qwenSessionId: null,
|
|
312
|
+
acpSessionId: null,
|
|
305
313
|
delegateResumeIds: buildEmptyDelegateResumeIds(),
|
|
306
314
|
messages: [],
|
|
307
315
|
createdAt: now,
|
|
@@ -352,6 +360,11 @@ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Sessio
|
|
|
352
360
|
}
|
|
353
361
|
if (session.codexThreadId === undefined) session.codexThreadId = null
|
|
354
362
|
if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
|
|
363
|
+
if (session.geminiSessionId === undefined) session.geminiSessionId = null
|
|
364
|
+
if (session.copilotSessionId === undefined) session.copilotSessionId = null
|
|
365
|
+
if (session.cursorSessionId === undefined) session.cursorSessionId = null
|
|
366
|
+
if (session.qwenSessionId === undefined) session.qwenSessionId = null
|
|
367
|
+
if (session.acpSessionId === undefined) session.acpSessionId = null
|
|
355
368
|
saveSession(sessionId, session)
|
|
356
369
|
return session
|
|
357
370
|
}
|
|
@@ -46,6 +46,9 @@ function emptyDelegateResumeIds() {
|
|
|
46
46
|
codex: null,
|
|
47
47
|
opencode: null,
|
|
48
48
|
gemini: null,
|
|
49
|
+
copilot: null,
|
|
50
|
+
cursor: null,
|
|
51
|
+
qwen: null,
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -124,6 +127,11 @@ export function createChatSession(input: Record<string, unknown>): ServiceResult
|
|
|
124
127
|
claudeSessionId: null,
|
|
125
128
|
codexThreadId: null,
|
|
126
129
|
opencodeSessionId: null,
|
|
130
|
+
geminiSessionId: null,
|
|
131
|
+
copilotSessionId: null,
|
|
132
|
+
cursorSessionId: null,
|
|
133
|
+
qwenSessionId: null,
|
|
134
|
+
acpSessionId: null,
|
|
127
135
|
delegateResumeIds: emptyDelegateResumeIds(),
|
|
128
136
|
messages: Array.isArray(input.messages) ? input.messages : [],
|
|
129
137
|
createdAt: now,
|
|
@@ -279,6 +287,11 @@ export function updateChatSession(sessionId: string, updates: Record<string, unk
|
|
|
279
287
|
if (updates.claudeSessionId !== undefined) session.claudeSessionId = updates.claudeSessionId
|
|
280
288
|
if (updates.codexThreadId !== undefined) session.codexThreadId = updates.codexThreadId
|
|
281
289
|
if (updates.opencodeSessionId !== undefined) session.opencodeSessionId = updates.opencodeSessionId
|
|
290
|
+
if (updates.geminiSessionId !== undefined) session.geminiSessionId = updates.geminiSessionId
|
|
291
|
+
if (updates.copilotSessionId !== undefined) session.copilotSessionId = updates.copilotSessionId
|
|
292
|
+
if (updates.cursorSessionId !== undefined) session.cursorSessionId = updates.cursorSessionId
|
|
293
|
+
if (updates.qwenSessionId !== undefined) session.qwenSessionId = updates.qwenSessionId
|
|
294
|
+
if (updates.acpSessionId !== undefined) session.acpSessionId = updates.acpSessionId
|
|
282
295
|
if (updates.delegateResumeIds !== undefined) session.delegateResumeIds = updates.delegateResumeIds
|
|
283
296
|
if (!Array.isArray(session.messages)) session.messages = []
|
|
284
297
|
|
|
@@ -360,6 +373,11 @@ export function clearChatMessages(sessionId: string): boolean {
|
|
|
360
373
|
session.claudeSessionId = null
|
|
361
374
|
session.codexThreadId = null
|
|
362
375
|
session.opencodeSessionId = null
|
|
376
|
+
session.geminiSessionId = null
|
|
377
|
+
session.copilotSessionId = null
|
|
378
|
+
session.cursorSessionId = null
|
|
379
|
+
session.qwenSessionId = null
|
|
380
|
+
session.acpSessionId = null
|
|
363
381
|
session.delegateResumeIds = emptyDelegateResumeIds()
|
|
364
382
|
saveSession(sessionId, session)
|
|
365
383
|
notify('sessions')
|
|
@@ -341,11 +341,19 @@ export function resolveDirectSession(params: {
|
|
|
341
341
|
claudeSessionId: null,
|
|
342
342
|
codexThreadId: null,
|
|
343
343
|
opencodeSessionId: null,
|
|
344
|
+
geminiSessionId: null,
|
|
345
|
+
copilotSessionId: null,
|
|
346
|
+
cursorSessionId: null,
|
|
347
|
+
qwenSessionId: null,
|
|
348
|
+
acpSessionId: null,
|
|
344
349
|
delegateResumeIds: {
|
|
345
350
|
claudeCode: null,
|
|
346
351
|
codex: null,
|
|
347
352
|
opencode: null,
|
|
348
353
|
gemini: null,
|
|
354
|
+
copilot: null,
|
|
355
|
+
cursor: null,
|
|
356
|
+
qwen: null,
|
|
349
357
|
},
|
|
350
358
|
messages: [],
|
|
351
359
|
createdAt: Date.now(),
|
|
@@ -77,6 +77,10 @@ const PROVIDER_DEFAULT_WINDOWS: Record<string, number> = {
|
|
|
77
77
|
openai: 128_000,
|
|
78
78
|
'codex-cli': 1_047_576,
|
|
79
79
|
'opencode-cli': 200_000,
|
|
80
|
+
'gemini-cli': 1_048_576,
|
|
81
|
+
'copilot-cli': 200_000,
|
|
82
|
+
'cursor-cli': 200_000,
|
|
83
|
+
'qwen-code-cli': 1_048_576,
|
|
80
84
|
google: 1_048_576,
|
|
81
85
|
deepseek: 64_000,
|
|
82
86
|
groq: 32_768,
|
|
@@ -85,6 +89,7 @@ const PROVIDER_DEFAULT_WINDOWS: Record<string, number> = {
|
|
|
85
89
|
xai: 131_072,
|
|
86
90
|
fireworks: 32_768,
|
|
87
91
|
ollama: 32_768,
|
|
92
|
+
goose: 200_000,
|
|
88
93
|
openclaw: 128_000,
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -5,7 +5,7 @@ import { log } from './logger'
|
|
|
5
5
|
|
|
6
6
|
const TAG = 'provider-health'
|
|
7
7
|
|
|
8
|
-
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
8
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
|
|
9
9
|
|
|
10
10
|
interface ProviderHealthState {
|
|
11
11
|
failures: number
|
|
@@ -119,6 +119,9 @@ function delegateBinary(delegateTool: DelegateTool): string {
|
|
|
119
119
|
if (delegateTool === 'delegate_to_claude_code') return 'claude'
|
|
120
120
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex'
|
|
121
121
|
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini'
|
|
122
|
+
if (delegateTool === 'delegate_to_copilot_cli') return 'copilot'
|
|
123
|
+
if (delegateTool === 'delegate_to_cursor_cli') return 'cursor-agent'
|
|
124
|
+
if (delegateTool === 'delegate_to_qwen_code_cli') return 'qwen'
|
|
122
125
|
return 'opencode'
|
|
123
126
|
}
|
|
124
127
|
|
|
@@ -146,6 +149,10 @@ function delegateToolReady(delegateTool: DelegateTool): boolean {
|
|
|
146
149
|
const probe = spawnSync(binary, ['login', 'status'], { encoding: 'utf-8', timeout: 8000 })
|
|
147
150
|
const probeText = `${probe.stdout || ''}\n${probe.stderr || ''}`.toLowerCase()
|
|
148
151
|
ok = (probe.status ?? 1) === 0 && probeText.includes('logged in')
|
|
152
|
+
} else if (ok && delegateTool === 'delegate_to_cursor_cli') {
|
|
153
|
+
const probe = spawnSync(binary, ['status'], { encoding: 'utf-8', timeout: 8000 })
|
|
154
|
+
const probeText = `${probe.stdout || ''}\n${probe.stderr || ''}`.toLowerCase()
|
|
155
|
+
ok = (probe.status ?? 1) === 0 || probeText.includes('authenticated') || probeText.includes('logged in')
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
delegateReadyCache.set(delegateTool, { at: now, ok })
|
|
@@ -156,6 +163,9 @@ function delegateProviderId(delegateTool: DelegateTool): string {
|
|
|
156
163
|
if (delegateTool === 'delegate_to_claude_code') return 'claude-cli'
|
|
157
164
|
if (delegateTool === 'delegate_to_codex_cli') return 'codex-cli'
|
|
158
165
|
if (delegateTool === 'delegate_to_gemini_cli') return 'gemini-cli'
|
|
166
|
+
if (delegateTool === 'delegate_to_copilot_cli') return 'copilot-cli'
|
|
167
|
+
if (delegateTool === 'delegate_to_cursor_cli') return 'cursor-cli'
|
|
168
|
+
if (delegateTool === 'delegate_to_qwen_code_cli') return 'qwen-code-cli'
|
|
159
169
|
return 'opencode-cli'
|
|
160
170
|
}
|
|
161
171
|
|
|
@@ -331,14 +341,14 @@ export async function pingOpenClaw(
|
|
|
331
341
|
|
|
332
342
|
/**
|
|
333
343
|
* Ping a provider to check reachability. Returns `{ ok, message }`.
|
|
334
|
-
* Skips CLI-based providers
|
|
344
|
+
* Skips CLI-based providers — returns ok.
|
|
335
345
|
*/
|
|
336
346
|
export async function pingProvider(
|
|
337
347
|
provider: string,
|
|
338
348
|
apiKey: string | undefined,
|
|
339
349
|
endpoint: string | undefined,
|
|
340
350
|
): Promise<{ ok: boolean; message: string }> {
|
|
341
|
-
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli']
|
|
351
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose']
|
|
342
352
|
const OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS = new Set(['hermes'])
|
|
343
353
|
if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
|
|
344
354
|
|
|
@@ -228,6 +228,14 @@ test('resolveDescriptor uses Hermes as an OpenAI-compatible provider with option
|
|
|
228
228
|
assert.equal(descriptor?.optionalApiKey, true)
|
|
229
229
|
})
|
|
230
230
|
|
|
231
|
+
test('resolveDescriptor disables model discovery for local CLI-backed providers without live model catalogs', () => {
|
|
232
|
+
for (const providerId of ['copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose']) {
|
|
233
|
+
const descriptor = resolveDescriptor({ providerId })
|
|
234
|
+
assert.equal(descriptor?.supportsDiscovery, false, `${providerId} should not support discovery`)
|
|
235
|
+
assert.equal(descriptor?.endpoint, undefined, `${providerId} should not expose a discovery endpoint`)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
231
239
|
// ---------------------------------------------------------------------------
|
|
232
240
|
// ttlForDescriptor
|
|
233
241
|
// ---------------------------------------------------------------------------
|
|
@@ -55,7 +55,7 @@ function toOpenAiCompatibleEndpoint(raw: string | null | undefined, fallback: st
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function supportsBuiltInModelDiscovery(providerId: string): boolean {
|
|
58
|
-
return !['claude-cli', 'codex-cli', 'opencode-cli'].includes(providerId)
|
|
58
|
+
return !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(providerId)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function normalizeGoogleModelsEndpoint(raw: string | null | undefined): string {
|
|
@@ -704,7 +704,7 @@ async function runProviderHealthChecks() {
|
|
|
704
704
|
if (!agent?.id || typeof agent.id !== 'string') continue
|
|
705
705
|
if (shouldSuppressSyntheticAgentHealthAlert(agent.id)) continue
|
|
706
706
|
const provider = typeof agent.provider === 'string' ? agent.provider : ''
|
|
707
|
-
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli'].includes(provider)) continue
|
|
707
|
+
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(provider)) continue
|
|
708
708
|
|
|
709
709
|
const credentialId = typeof agent.credentialId === 'string' ? agent.credentialId : ''
|
|
710
710
|
const apiEndpoint = typeof agent.apiEndpoint === 'string' ? agent.apiEndpoint : ''
|
|
@@ -1508,7 +1508,7 @@ export function getDaemonHealthSummary(): {
|
|
|
1508
1508
|
for (const agent of agentEntries) {
|
|
1509
1509
|
if (!agent?.id || typeof agent.id !== 'string') continue
|
|
1510
1510
|
const provider = typeof agent.provider === 'string' ? agent.provider : ''
|
|
1511
|
-
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli'].includes(provider)) continue
|
|
1511
|
+
if (!provider || ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'].includes(provider)) continue
|
|
1512
1512
|
const credentialId = typeof agent.credentialId === 'string' ? agent.credentialId : ''
|
|
1513
1513
|
const apiEndpoint = typeof agent.apiEndpoint === 'string' ? agent.apiEndpoint : ''
|
|
1514
1514
|
providerKeys.add(`${provider}:${credentialId || 'no-cred'}:${apiEndpoint}`)
|
|
@@ -128,6 +128,8 @@ export interface TaskResumeState {
|
|
|
128
128
|
claudeSessionId: string | null
|
|
129
129
|
codexThreadId: string | null
|
|
130
130
|
opencodeSessionId: string | null
|
|
131
|
+
cursorSessionId?: string | null
|
|
132
|
+
qwenSessionId?: string | null
|
|
131
133
|
delegateResumeIds: NonNullable<Session['delegateResumeIds']>
|
|
132
134
|
}
|
|
133
135
|
|
|
@@ -149,6 +151,9 @@ function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']
|
|
|
149
151
|
codex: null,
|
|
150
152
|
opencode: null,
|
|
151
153
|
gemini: null,
|
|
154
|
+
copilot: null,
|
|
155
|
+
cursor: null,
|
|
156
|
+
qwen: null,
|
|
152
157
|
}
|
|
153
158
|
}
|
|
154
159
|
|
|
@@ -166,6 +171,8 @@ function hasResumeState(state: TaskResumeState | null | undefined): state is Tas
|
|
|
166
171
|
|| state.delegateResumeIds.codex
|
|
167
172
|
|| state.delegateResumeIds.opencode
|
|
168
173
|
|| state.delegateResumeIds.gemini
|
|
174
|
+
|| state.delegateResumeIds.cursor
|
|
175
|
+
|| state.delegateResumeIds.qwen
|
|
169
176
|
)
|
|
170
177
|
}
|
|
171
178
|
|
|
@@ -182,16 +189,22 @@ export function extractTaskResumeState(task: Partial<BoardTask> | null | undefin
|
|
|
182
189
|
|| (legacyProvider === 'opencode-cli' ? legacyResumeId : null)
|
|
183
190
|
const geminiSessionId = normalizeResumeHandle(task.geminiResumeId)
|
|
184
191
|
|| (legacyProvider === 'gemini-cli' ? legacyResumeId : null)
|
|
192
|
+
const cursorSessionId = legacyProvider === 'cursor-cli' ? legacyResumeId : null
|
|
193
|
+
const qwenSessionId = legacyProvider === 'qwen-code-cli' ? legacyResumeId : null
|
|
185
194
|
|
|
186
195
|
const resume = {
|
|
187
196
|
claudeSessionId,
|
|
188
197
|
codexThreadId,
|
|
189
198
|
opencodeSessionId,
|
|
199
|
+
cursorSessionId,
|
|
200
|
+
qwenSessionId,
|
|
190
201
|
delegateResumeIds: {
|
|
191
202
|
claudeCode: claudeSessionId,
|
|
192
203
|
codex: codexThreadId,
|
|
193
204
|
opencode: opencodeSessionId,
|
|
194
205
|
gemini: geminiSessionId,
|
|
206
|
+
cursor: cursorSessionId,
|
|
207
|
+
qwen: qwenSessionId,
|
|
195
208
|
},
|
|
196
209
|
} satisfies TaskResumeState
|
|
197
210
|
|
|
@@ -204,6 +217,8 @@ export function extractSessionResumeState(session: Partial<Session> | null | und
|
|
|
204
217
|
const claudeSessionId = normalizeResumeHandle(session.claudeSessionId)
|
|
205
218
|
const codexThreadId = normalizeResumeHandle(session.codexThreadId)
|
|
206
219
|
const opencodeSessionId = normalizeResumeHandle(session.opencodeSessionId)
|
|
220
|
+
const cursorSessionId = normalizeResumeHandle(session.cursorSessionId)
|
|
221
|
+
const qwenSessionId = normalizeResumeHandle(session.qwenSessionId)
|
|
207
222
|
const delegateResumeIds = session.delegateResumeIds && typeof session.delegateResumeIds === 'object'
|
|
208
223
|
? { ...buildEmptyDelegateResumeIds(), ...session.delegateResumeIds }
|
|
209
224
|
: buildEmptyDelegateResumeIds()
|
|
@@ -212,11 +227,16 @@ export function extractSessionResumeState(session: Partial<Session> | null | und
|
|
|
212
227
|
claudeSessionId,
|
|
213
228
|
codexThreadId,
|
|
214
229
|
opencodeSessionId,
|
|
230
|
+
cursorSessionId,
|
|
231
|
+
qwenSessionId,
|
|
215
232
|
delegateResumeIds: {
|
|
216
233
|
claudeCode: normalizeResumeHandle(delegateResumeIds.claudeCode) || claudeSessionId,
|
|
217
234
|
codex: normalizeResumeHandle(delegateResumeIds.codex) || codexThreadId,
|
|
218
235
|
opencode: normalizeResumeHandle(delegateResumeIds.opencode) || opencodeSessionId,
|
|
219
236
|
gemini: normalizeResumeHandle(delegateResumeIds.gemini),
|
|
237
|
+
cursor: normalizeResumeHandle(delegateResumeIds.cursor) || cursorSessionId,
|
|
238
|
+
qwen: normalizeResumeHandle(delegateResumeIds.qwen) || qwenSessionId,
|
|
239
|
+
copilot: normalizeResumeHandle(delegateResumeIds.copilot),
|
|
220
240
|
},
|
|
221
241
|
} satisfies TaskResumeState
|
|
222
242
|
|
|
@@ -263,10 +283,12 @@ export function applyTaskResumeStateToSession(session: Session, resume: TaskResu
|
|
|
263
283
|
if (!hasResumeState(resume)) return false
|
|
264
284
|
|
|
265
285
|
let changed = false
|
|
266
|
-
const directFields: Array<['claudeSessionId' | 'codexThreadId' | 'opencodeSessionId', string | null]> = [
|
|
286
|
+
const directFields: Array<['claudeSessionId' | 'codexThreadId' | 'opencodeSessionId' | 'cursorSessionId' | 'qwenSessionId', string | null]> = [
|
|
267
287
|
['claudeSessionId', resume.claudeSessionId],
|
|
268
288
|
['codexThreadId', resume.codexThreadId],
|
|
269
289
|
['opencodeSessionId', resume.opencodeSessionId],
|
|
290
|
+
['cursorSessionId', resume.cursorSessionId ?? null],
|
|
291
|
+
['qwenSessionId', resume.qwenSessionId ?? null],
|
|
270
292
|
]
|
|
271
293
|
for (const [key, value] of directFields) {
|
|
272
294
|
if (!value || session[key] === value) continue
|
|
@@ -1444,27 +1466,31 @@ export async function processNext() {
|
|
|
1444
1466
|
const execSession = execSessions[sessionId] as unknown as Record<string, unknown> | undefined
|
|
1445
1467
|
if (execSession) {
|
|
1446
1468
|
const delegateIds = execSession.delegateResumeIds as
|
|
1447
|
-
| { claudeCode?: string | null; codex?: string | null; opencode?: string | null; gemini?: string | null }
|
|
1469
|
+
| { claudeCode?: string | null; codex?: string | null; opencode?: string | null; gemini?: string | null; cursor?: string | null; qwen?: string | null }
|
|
1448
1470
|
| undefined
|
|
1449
1471
|
// Store each CLI resume ID separately
|
|
1450
1472
|
const claudeId = (execSession.claudeSessionId as string) || delegateIds?.claudeCode || null
|
|
1451
1473
|
const codexId = (execSession.codexThreadId as string) || delegateIds?.codex || null
|
|
1452
1474
|
const opencodeId = (execSession.opencodeSessionId as string) || delegateIds?.opencode || null
|
|
1453
1475
|
const geminiId = delegateIds?.gemini || null
|
|
1476
|
+
const cursorId = (execSession.cursorSessionId as string) || delegateIds?.cursor || null
|
|
1477
|
+
const qwenId = (execSession.qwenSessionId as string) || delegateIds?.qwen || null
|
|
1454
1478
|
if (claudeId) t2[taskId].claudeResumeId = claudeId
|
|
1455
1479
|
if (codexId) t2[taskId].codexResumeId = codexId
|
|
1456
1480
|
if (opencodeId) t2[taskId].opencodeResumeId = opencodeId
|
|
1457
1481
|
if (geminiId) t2[taskId].geminiResumeId = geminiId
|
|
1458
1482
|
// Keep backward-compat single field (first available)
|
|
1459
|
-
const primaryId = claudeId || codexId || opencodeId || geminiId
|
|
1483
|
+
const primaryId = claudeId || codexId || opencodeId || geminiId || cursorId || qwenId
|
|
1460
1484
|
if (primaryId) {
|
|
1461
1485
|
t2[taskId].cliResumeId = primaryId
|
|
1462
1486
|
if (claudeId) t2[taskId].cliProvider = 'claude-cli'
|
|
1463
1487
|
else if (codexId) t2[taskId].cliProvider = 'codex-cli'
|
|
1464
1488
|
else if (opencodeId) t2[taskId].cliProvider = 'opencode-cli'
|
|
1465
1489
|
else if (geminiId) t2[taskId].cliProvider = 'gemini-cli'
|
|
1490
|
+
else if (cursorId) t2[taskId].cliProvider = 'cursor-cli'
|
|
1491
|
+
else if (qwenId) t2[taskId].cliProvider = 'qwen-code-cli'
|
|
1466
1492
|
}
|
|
1467
|
-
log.info(TAG, `[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}, gemini=${geminiId}`)
|
|
1493
|
+
log.info(TAG, `[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}, gemini=${geminiId}, cursor=${cursorId}, qwen=${qwenId}`)
|
|
1468
1494
|
}
|
|
1469
1495
|
} catch (e) {
|
|
1470
1496
|
log.warn(TAG, `[queue] Failed to extract CLI resume IDs for task ${taskId}:`, e)
|
|
@@ -44,7 +44,7 @@ type RepairSessionRunQueueFn = (
|
|
|
44
44
|
|
|
45
45
|
type DrainExecutionFn = (executionKey: string) => Promise<void>
|
|
46
46
|
|
|
47
|
-
const LONG_TOOL_NAMES: ReadonlySet<string> = new Set(['claude_code', 'codex_cli', 'opencode_cli'])
|
|
47
|
+
const LONG_TOOL_NAMES: ReadonlySet<string> = new Set(['claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'cursor_cli', 'qwen_code_cli'])
|
|
48
48
|
|
|
49
49
|
type SessionToolConfig = {
|
|
50
50
|
tools?: string[] | null
|
|
@@ -102,6 +102,22 @@ describe('session-reset-policy', () => {
|
|
|
102
102
|
})
|
|
103
103
|
assert.equal(policy.idleTimeoutSec, 1800)
|
|
104
104
|
})
|
|
105
|
+
|
|
106
|
+
it('isolated mode is preserved and not replaced by fallback', () => {
|
|
107
|
+
const policy = mod.resolveSessionResetPolicy({
|
|
108
|
+
resetType: 'direct',
|
|
109
|
+
session: { sessionResetMode: 'isolated' } as never,
|
|
110
|
+
})
|
|
111
|
+
assert.equal(policy.mode, 'isolated')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('isolated mode from agent config is preserved', () => {
|
|
115
|
+
const policy = mod.resolveSessionResetPolicy({
|
|
116
|
+
resetType: 'direct',
|
|
117
|
+
agent: { sessionResetMode: 'isolated' } as never,
|
|
118
|
+
})
|
|
119
|
+
assert.equal(policy.mode, 'isolated')
|
|
120
|
+
})
|
|
105
121
|
})
|
|
106
122
|
|
|
107
123
|
// ---- evaluateSessionFreshness ----
|
|
@@ -67,7 +67,7 @@ function parseIntBounded(value: unknown, min: number, max: number): number | nul
|
|
|
67
67
|
|
|
68
68
|
function normalizeMode(raw: unknown, fallback: SessionResetMode): SessionResetMode {
|
|
69
69
|
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
|
70
|
-
return value === 'daily' ? 'daily' : value === 'idle' ? 'idle' : fallback
|
|
70
|
+
return value === 'daily' ? 'daily' : value === 'idle' ? 'idle' : value === 'isolated' ? 'isolated' : fallback
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function normalizeTimeHHMM(raw: unknown): string | null {
|
|
@@ -282,11 +282,19 @@ export function resetSessionRuntime(
|
|
|
282
282
|
session.claudeSessionId = null
|
|
283
283
|
session.codexThreadId = null
|
|
284
284
|
session.opencodeSessionId = null
|
|
285
|
+
session.geminiSessionId = null
|
|
286
|
+
session.copilotSessionId = null
|
|
287
|
+
session.cursorSessionId = null
|
|
288
|
+
session.qwenSessionId = null
|
|
289
|
+
session.acpSessionId = null
|
|
285
290
|
session.delegateResumeIds = {
|
|
286
291
|
claudeCode: null,
|
|
287
292
|
codex: null,
|
|
288
293
|
opencode: null,
|
|
289
294
|
gemini: null,
|
|
295
|
+
copilot: null,
|
|
296
|
+
cursor: null,
|
|
297
|
+
qwen: null,
|
|
290
298
|
}
|
|
291
299
|
session.createdAt = now
|
|
292
300
|
session.lastActiveAt = now
|
|
@@ -86,8 +86,8 @@ export interface ToolBuildContext {
|
|
|
86
86
|
commandTimeoutMs: number
|
|
87
87
|
claudeTimeoutMs: number
|
|
88
88
|
cliProcessTimeoutMs: number
|
|
89
|
-
persistDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
|
|
90
|
-
readStoredDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
|
|
89
|
+
persistDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen', id: string | null | undefined) => void
|
|
90
|
+
readStoredDelegateResumeId: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen') => string | null
|
|
91
91
|
resolveCurrentSession: () => any | null
|
|
92
92
|
activeExtensions: string[]
|
|
93
93
|
/** Agent's file access policy — passed to shell for command-level enforcement */
|