@swarmclawai/swarmclaw 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -85
- package/bin/server-cmd.js +64 -1
- package/package.json +2 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/devserver/route.ts +13 -19
- package/src/app/api/chats/[id]/messages/route.ts +13 -15
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/preview-server/route.ts +1 -1
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/cli/server-cmd.test.js +74 -0
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +19 -9
- package/src/components/chat/message-list.tsx +37 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
- package/src/instrumentation.ts +1 -1
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +3 -1
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +10 -3
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2266
- package/src/lib/server/openclaw/deploy.test.ts +42 -3
- package/src/lib/server/openclaw/deploy.ts +26 -12
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/runtime/alert-dispatch.ts +1 -1
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1470
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +55 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +2 -2
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2061
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +4 -2
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1377
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/storage.ts +13 -24
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/strip-internal-metadata.test.ts +42 -41
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +21 -5
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -43,16 +43,17 @@ describe('chat-streaming-state', () => {
|
|
|
43
43
|
localStreaming: true,
|
|
44
44
|
hasLiveArtifacts: true,
|
|
45
45
|
assistantRenderId: 'render-1',
|
|
46
|
+
showLiveRow: true,
|
|
47
|
+
syntheticAssistant: messages[1],
|
|
46
48
|
} as StreamingAwareMessageListOptions),
|
|
47
49
|
[
|
|
48
50
|
{ role: 'user', text: 'hello', time: 1 },
|
|
49
51
|
{
|
|
50
52
|
role: 'assistant',
|
|
51
|
-
text: '
|
|
52
|
-
time:
|
|
53
|
+
text: 'partial',
|
|
54
|
+
time: 2,
|
|
53
55
|
kind: 'chat',
|
|
54
56
|
streaming: true,
|
|
55
|
-
thinking: 'working...',
|
|
56
57
|
clientRenderId: 'render-1',
|
|
57
58
|
},
|
|
58
59
|
],
|
|
@@ -69,6 +70,7 @@ describe('chat-streaming-state', () => {
|
|
|
69
70
|
localStreaming: true,
|
|
70
71
|
hasLiveArtifacts: true,
|
|
71
72
|
assistantRenderId: 'render-2',
|
|
73
|
+
showLiveRow: true,
|
|
72
74
|
} as StreamingAwareMessageListOptions),
|
|
73
75
|
[
|
|
74
76
|
{ role: 'user', text: 'hello', time: 1 },
|
|
@@ -84,6 +86,43 @@ describe('chat-streaming-state', () => {
|
|
|
84
86
|
)
|
|
85
87
|
})
|
|
86
88
|
|
|
89
|
+
it('can show a synthetic live row for server-driven runs using the latest persisted streaming artifact', () => {
|
|
90
|
+
const messages: Message[] = [
|
|
91
|
+
{ role: 'user', text: 'queued follow-up', time: 1 },
|
|
92
|
+
{
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
text: 'Drafting the handoff now.',
|
|
95
|
+
time: 2,
|
|
96
|
+
streaming: true,
|
|
97
|
+
thinking: 'Collecting the completion details',
|
|
98
|
+
toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
buildStreamingAwareMessageList(messages, {
|
|
104
|
+
localStreaming: true,
|
|
105
|
+
hasLiveArtifacts: false,
|
|
106
|
+
assistantRenderId: 'render-server',
|
|
107
|
+
showLiveRow: true,
|
|
108
|
+
syntheticAssistant: messages[1],
|
|
109
|
+
}),
|
|
110
|
+
[
|
|
111
|
+
{ role: 'user', text: 'queued follow-up', time: 1 },
|
|
112
|
+
{
|
|
113
|
+
role: 'assistant',
|
|
114
|
+
text: 'Drafting the handoff now.',
|
|
115
|
+
time: 2,
|
|
116
|
+
kind: 'chat',
|
|
117
|
+
streaming: true,
|
|
118
|
+
thinking: 'Collecting the completion details',
|
|
119
|
+
toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
|
|
120
|
+
clientRenderId: 'render-server',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
87
126
|
it('replaces trailing streaming assistant messages with the completed assistant message', () => {
|
|
88
127
|
const messages: Message[] = [
|
|
89
128
|
{ role: 'user', text: 'hello', time: 1 },
|
|
@@ -10,6 +10,8 @@ export interface StreamingAwareMessageListOptions {
|
|
|
10
10
|
localStreaming: boolean
|
|
11
11
|
hasLiveArtifacts: boolean
|
|
12
12
|
assistantRenderId?: string | null
|
|
13
|
+
showLiveRow?: boolean
|
|
14
|
+
syntheticAssistant?: Partial<Message> | null
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function isStreamingAssistantMessage(
|
|
@@ -27,13 +29,13 @@ function isStreamingAssistantMessage(
|
|
|
27
29
|
|
|
28
30
|
export function shouldHidePersistedStreamingAssistantMessage(
|
|
29
31
|
message: Message,
|
|
30
|
-
opts: { localStreaming: boolean; hasLiveArtifacts: boolean },
|
|
32
|
+
opts: { localStreaming: boolean; hasLiveArtifacts: boolean; showLiveRow?: boolean },
|
|
31
33
|
): boolean {
|
|
34
|
+
const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
|
|
32
35
|
return (
|
|
33
|
-
|
|
36
|
+
showLiveRow
|
|
34
37
|
&& message.role === 'assistant'
|
|
35
38
|
&& message.streaming === true
|
|
36
|
-
&& opts.hasLiveArtifacts
|
|
37
39
|
)
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -41,22 +43,32 @@ export function buildStreamingAwareMessageList(
|
|
|
41
43
|
messages: Message[],
|
|
42
44
|
opts: StreamingAwareMessageListOptions,
|
|
43
45
|
): Message[] {
|
|
44
|
-
const
|
|
46
|
+
const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
|
|
47
|
+
const nextMessages = showLiveRow
|
|
45
48
|
? messages.filter((message) => !shouldHidePersistedStreamingAssistantMessage(message, opts))
|
|
46
49
|
: messages
|
|
47
50
|
|
|
48
|
-
if (!
|
|
51
|
+
if (!showLiveRow || !opts.assistantRenderId) {
|
|
49
52
|
return nextMessages
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const syntheticAssistantMessage: Message = {
|
|
53
56
|
role: 'assistant',
|
|
54
|
-
text: '',
|
|
55
|
-
time: 0,
|
|
56
|
-
kind: 'chat',
|
|
57
|
+
text: typeof opts.syntheticAssistant?.text === 'string' ? opts.syntheticAssistant.text : '',
|
|
58
|
+
time: typeof opts.syntheticAssistant?.time === 'number' ? opts.syntheticAssistant.time : 0,
|
|
59
|
+
kind: opts.syntheticAssistant?.kind || 'chat',
|
|
57
60
|
streaming: true,
|
|
58
61
|
clientRenderId: opts.assistantRenderId,
|
|
59
62
|
}
|
|
63
|
+
if (typeof opts.syntheticAssistant?.thinking === 'string') {
|
|
64
|
+
syntheticAssistantMessage.thinking = opts.syntheticAssistant.thinking
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(opts.syntheticAssistant?.toolEvents)) {
|
|
67
|
+
syntheticAssistantMessage.toolEvents = opts.syntheticAssistant.toolEvents
|
|
68
|
+
}
|
|
69
|
+
if (opts.syntheticAssistant?.source) {
|
|
70
|
+
syntheticAssistantMessage.source = opts.syntheticAssistant.source
|
|
71
|
+
}
|
|
60
72
|
|
|
61
73
|
return [
|
|
62
74
|
...nextMessages,
|
|
@@ -27,7 +27,7 @@ describe('queued-message-queue', () => {
|
|
|
27
27
|
it('replaces queued items only for the requested session', () => {
|
|
28
28
|
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
29
29
|
{ runId: 'q4', sessionId: 'session-a', text: 'replacement', queuedAt: 4, position: 1 },
|
|
30
|
-
])
|
|
30
|
+
], { activeRunId: null })
|
|
31
31
|
assert.deepEqual(
|
|
32
32
|
listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
|
|
33
33
|
['q4'],
|
|
@@ -38,6 +38,28 @@ describe('queued-message-queue', () => {
|
|
|
38
38
|
)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
it('keeps only the newly active run as a sending placeholder when it disappears from the queue snapshot', () => {
|
|
42
|
+
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
43
|
+
{ runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
|
|
44
|
+
], { activeRunId: 'q1' })
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
listQueuedMessagesForSession(replaced, 'session-a').map((item) => [item.runId, item.sending === true]),
|
|
48
|
+
[['q1', true], ['q3', false]],
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('drops missing stale queue rows that are not the active run', () => {
|
|
53
|
+
const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
|
|
54
|
+
{ runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
|
|
55
|
+
], { activeRunId: 'run-other' })
|
|
56
|
+
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
|
|
59
|
+
['q3'],
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
41
63
|
it('removes queued items by stable id', () => {
|
|
42
64
|
assert.deepEqual(removeQueuedMessageById(queue, 'q2').map((item) => item.runId), ['q1', 'q3'])
|
|
43
65
|
})
|
|
@@ -41,18 +41,27 @@ export function snapshotToQueuedMessages(snapshot: SessionQueueSnapshot): Queued
|
|
|
41
41
|
return snapshot.items.map((item) => ({ ...item }))
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface ReplaceQueuedMessagesOptions {
|
|
45
|
+
activeRunId?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
export function replaceQueuedMessagesForSession(
|
|
45
49
|
queue: QueuedSessionMessage[],
|
|
46
50
|
sessionId: string,
|
|
47
51
|
nextItems: QueuedSessionMessage[],
|
|
52
|
+
options: ReplaceQueuedMessagesOptions = {},
|
|
48
53
|
): QueuedSessionMessage[] {
|
|
49
54
|
const otherSessions = queue.filter((item) => item.sessionId !== sessionId)
|
|
50
55
|
const previousForSession = queue.filter((item) => item.sessionId === sessionId && !item.sending)
|
|
51
56
|
// Detect consumed messages: items in local state but not in server snapshot.
|
|
52
|
-
// Keep
|
|
57
|
+
// Keep only the run that actually became active visible as "sending" so it
|
|
58
|
+
// doesn't vanish from the UI before the transcript refresh catches up.
|
|
53
59
|
const nextRunIds = new Set(nextItems.map((item) => item.runId))
|
|
60
|
+
const activeRunId = typeof options.activeRunId === 'string' && options.activeRunId.trim()
|
|
61
|
+
? options.activeRunId
|
|
62
|
+
: null
|
|
54
63
|
const consumed = previousForSession
|
|
55
|
-
.filter((item) => !item.optimistic && !nextRunIds.has(item.runId))
|
|
64
|
+
.filter((item) => !item.optimistic && !nextRunIds.has(item.runId) && activeRunId === item.runId)
|
|
56
65
|
.map((item) => ({ ...item, sending: true }))
|
|
57
66
|
return [
|
|
58
67
|
...otherSessions,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { isStderrNoise, buildCliEnv, isCliProvider, CLI_PROVIDER_CAPABILITIES } from './cli-utils'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// isStderrNoise
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
describe('isStderrNoise', () => {
|
|
11
|
+
it('returns true for MallocStackLogging lines', () => {
|
|
12
|
+
assert.equal(isStderrNoise('MallocStackLogging: could not tag MSL'), true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns true for blank/whitespace lines', () => {
|
|
16
|
+
assert.equal(isStderrNoise(''), true)
|
|
17
|
+
assert.equal(isStderrNoise(' '), true)
|
|
18
|
+
assert.equal(isStderrNoise('\t'), true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns false for real error text', () => {
|
|
22
|
+
assert.equal(isStderrNoise('Error: connection refused'), false)
|
|
23
|
+
assert.equal(isStderrNoise('FATAL: segfault'), false)
|
|
24
|
+
assert.equal(isStderrNoise('Permission denied'), false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// buildCliEnv
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
describe('buildCliEnv', () => {
|
|
33
|
+
it('strips SWARMCLAW_ prefixed vars from env', () => {
|
|
34
|
+
const orig = process.env.SWARMCLAW_TEST_VAR
|
|
35
|
+
process.env.SWARMCLAW_TEST_VAR = 'should_be_stripped'
|
|
36
|
+
try {
|
|
37
|
+
const env = buildCliEnv()
|
|
38
|
+
assert.equal(env.SWARMCLAW_TEST_VAR, undefined)
|
|
39
|
+
} finally {
|
|
40
|
+
if (orig === undefined) delete process.env.SWARMCLAW_TEST_VAR
|
|
41
|
+
else process.env.SWARMCLAW_TEST_VAR = orig
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('deletes MallocStackLogging', () => {
|
|
46
|
+
const orig = process.env.MallocStackLogging
|
|
47
|
+
process.env.MallocStackLogging = '1'
|
|
48
|
+
try {
|
|
49
|
+
const env = buildCliEnv()
|
|
50
|
+
assert.equal(env.MallocStackLogging, undefined)
|
|
51
|
+
} finally {
|
|
52
|
+
if (orig === undefined) delete process.env.MallocStackLogging
|
|
53
|
+
else process.env.MallocStackLogging = orig
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('injects provided key-value pairs', () => {
|
|
58
|
+
const env = buildCliEnv({ inject: { MY_CUSTOM_KEY: 'hello' } })
|
|
59
|
+
assert.equal(env.MY_CUSTOM_KEY, 'hello')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('preserves unrelated user env vars', () => {
|
|
63
|
+
const env = buildCliEnv()
|
|
64
|
+
assert.equal(env.PATH, process.env.PATH)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('supports custom stripPrefixes', () => {
|
|
68
|
+
const orig = process.env.CUSTOM_PREFIX_VAR
|
|
69
|
+
process.env.CUSTOM_PREFIX_VAR = 'should_be_stripped'
|
|
70
|
+
try {
|
|
71
|
+
const env = buildCliEnv({ stripPrefixes: ['CUSTOM_PREFIX_'] })
|
|
72
|
+
assert.equal(env.CUSTOM_PREFIX_VAR, undefined)
|
|
73
|
+
} finally {
|
|
74
|
+
if (orig === undefined) delete process.env.CUSTOM_PREFIX_VAR
|
|
75
|
+
else process.env.CUSTOM_PREFIX_VAR = orig
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('sets TERM=dumb and NO_COLOR=1', () => {
|
|
80
|
+
const env = buildCliEnv()
|
|
81
|
+
assert.equal(env.TERM, 'dumb')
|
|
82
|
+
assert.equal(env.NO_COLOR, '1')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// isCliProvider
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
describe('isCliProvider', () => {
|
|
91
|
+
it('returns true for known CLI providers', () => {
|
|
92
|
+
assert.equal(isCliProvider('claude-cli'), true)
|
|
93
|
+
assert.equal(isCliProvider('codex-cli'), true)
|
|
94
|
+
assert.equal(isCliProvider('opencode-cli'), true)
|
|
95
|
+
assert.equal(isCliProvider('gemini-cli'), true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns false for non-CLI providers', () => {
|
|
99
|
+
assert.equal(isCliProvider('openai'), false)
|
|
100
|
+
assert.equal(isCliProvider('anthropic'), false)
|
|
101
|
+
assert.equal(isCliProvider(''), false)
|
|
102
|
+
assert.equal(isCliProvider('google'), false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// CLI_PROVIDER_CAPABILITIES
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('CLI_PROVIDER_CAPABILITIES', () => {
|
|
111
|
+
it('has entries for all 4 CLI providers', () => {
|
|
112
|
+
assert.ok('claude-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
113
|
+
assert.ok('codex-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
114
|
+
assert.ok('opencode-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
115
|
+
assert.ok('gemini-cli' in CLI_PROVIDER_CAPABILITIES)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('each entry is a non-empty string', () => {
|
|
119
|
+
for (const [key, value] of Object.entries(CLI_PROVIDER_CAPABILITIES)) {
|
|
120
|
+
assert.equal(typeof value, 'string', `${key} should be a string`)
|
|
121
|
+
assert.ok(value.length > 0, `${key} should be non-empty`)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadActivity as loadStoredActivity, logActivity as writeActivityLog } from '@/lib/server/storage'
|
|
2
|
+
import { perf } from '@/lib/server/runtime/perf'
|
|
3
|
+
|
|
4
|
+
export function loadActivity() {
|
|
5
|
+
return perf.measureSync('repository', 'activity.list', () => loadStoredActivity())
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function logActivity(entry: {
|
|
9
|
+
entityType: string
|
|
10
|
+
entityId: string
|
|
11
|
+
action: string
|
|
12
|
+
actor: string
|
|
13
|
+
actorId?: string
|
|
14
|
+
summary: string
|
|
15
|
+
detail?: Record<string, unknown>
|
|
16
|
+
}) {
|
|
17
|
+
perf.measureSync('repository', 'activity.log', () => writeActivityLog(entry), {
|
|
18
|
+
entityType: entry.entityType,
|
|
19
|
+
action: entry.action,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { describe, it } from 'node:test'
|
|
3
|
+
import type { Agent, ProviderType } from '@/types'
|
|
3
4
|
import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from './agent-availability'
|
|
4
5
|
|
|
5
6
|
describe('isWorkerOnlyAgent', () => {
|
|
6
|
-
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw']
|
|
7
|
-
const NON_CLI_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'groq', '
|
|
7
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'openclaw'] satisfies ProviderType[]
|
|
8
|
+
const NON_CLI_PROVIDERS = ['openai', 'anthropic', 'google', 'deepseek', 'groq', 'together'] satisfies ProviderType[]
|
|
9
|
+
|
|
10
|
+
function withProvider(provider: unknown): Pick<Agent, 'provider'> {
|
|
11
|
+
return { provider } as Pick<Agent, 'provider'>
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
for (const provider of CLI_PROVIDERS) {
|
|
10
15
|
it(`returns true for ${provider}`, () => {
|
|
11
|
-
assert.equal(isWorkerOnlyAgent(
|
|
16
|
+
assert.equal(isWorkerOnlyAgent(withProvider(provider)), true)
|
|
12
17
|
})
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
for (const provider of NON_CLI_PROVIDERS) {
|
|
16
21
|
it(`returns false for ${provider}`, () => {
|
|
17
|
-
assert.equal(isWorkerOnlyAgent(
|
|
22
|
+
assert.equal(isWorkerOnlyAgent(withProvider(provider)), false)
|
|
18
23
|
})
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -27,7 +32,7 @@ describe('isWorkerOnlyAgent', () => {
|
|
|
27
32
|
})
|
|
28
33
|
|
|
29
34
|
it('returns false for empty provider string', () => {
|
|
30
|
-
assert.equal(isWorkerOnlyAgent(
|
|
35
|
+
assert.equal(isWorkerOnlyAgent(withProvider('')), false)
|
|
31
36
|
})
|
|
32
37
|
})
|
|
33
38
|
|
|
@@ -3,22 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* When an agent is trashed, related entities (tasks, schedules, watch jobs,
|
|
5
5
|
* connectors, webhooks, delegation jobs, chatroom memberships) must be
|
|
6
|
-
* suspended to prevent phantom daemon activity.
|
|
6
|
+
* suspended to prevent phantom daemon activity. On permanent delete the
|
|
7
7
|
* referencing rows are hard-removed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
upsertStoredItems,
|
|
13
|
-
loadSchedules,
|
|
14
|
-
loadWatchJobs,
|
|
15
|
-
loadConnectors,
|
|
11
|
+
deleteDelegationJob,
|
|
16
12
|
loadDelegationJobs,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from '@/lib/server/
|
|
21
|
-
import
|
|
13
|
+
saveDelegationJobRecords,
|
|
14
|
+
} from '@/lib/server/agents/delegation-job-repository'
|
|
15
|
+
import { loadChatrooms, saveChatrooms } from '@/lib/server/chatrooms/chatroom-repository'
|
|
16
|
+
import { loadConnectors, saveConnectors } from '@/lib/server/connectors/connector-repository'
|
|
17
|
+
import { deleteWatchJob, loadWatchJobs, upsertWatchJobs } from '@/lib/server/runtime/watch-job-repository'
|
|
18
|
+
import { deleteSchedule, loadSchedules, upsertSchedules } from '@/lib/server/schedules/schedule-repository'
|
|
19
|
+
import { deleteTask, loadTasks, saveTaskMany } from '@/lib/server/tasks/task-repository'
|
|
20
|
+
import { loadWebhooks, saveWebhooks } from '@/lib/server/webhooks/webhook-repository'
|
|
22
21
|
|
|
23
22
|
interface CascadeCounts {
|
|
24
23
|
tasks: number
|
|
@@ -39,7 +38,7 @@ export function suspendAgentReferences(agentId: string): CascadeCounts {
|
|
|
39
38
|
|
|
40
39
|
// 1. Tasks — cancel active ones
|
|
41
40
|
const tasks = loadTasks()
|
|
42
|
-
const taskUpdates: Array<[string, unknown]> = []
|
|
41
|
+
const taskUpdates: Array<[string, Record<string, unknown>]> = []
|
|
43
42
|
for (const t of Object.values(tasks) as unknown as Array<Record<string, unknown>>) {
|
|
44
43
|
if (!t || t.agentId !== agentId) continue
|
|
45
44
|
const status = t.status as string | undefined
|
|
@@ -50,13 +49,13 @@ export function suspendAgentReferences(agentId: string): CascadeCounts {
|
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
if (taskUpdates.length) {
|
|
53
|
-
|
|
52
|
+
saveTaskMany(taskUpdates)
|
|
54
53
|
counts.tasks = taskUpdates.length
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
// 2. Schedules — pause (with marker for restore)
|
|
58
57
|
const schedules = loadSchedules()
|
|
59
|
-
const schedUpdates: Array<[string, unknown]> = []
|
|
58
|
+
const schedUpdates: Array<[string, Record<string, unknown>]> = []
|
|
60
59
|
for (const s of Object.values(schedules) as unknown as Array<Record<string, unknown>>) {
|
|
61
60
|
if (!s || s.agentId !== agentId) continue
|
|
62
61
|
if (s.enabled === false) continue
|
|
@@ -65,13 +64,13 @@ export function suspendAgentReferences(agentId: string): CascadeCounts {
|
|
|
65
64
|
schedUpdates.push([s.id as string, s])
|
|
66
65
|
}
|
|
67
66
|
if (schedUpdates.length) {
|
|
68
|
-
|
|
67
|
+
upsertSchedules(schedUpdates)
|
|
69
68
|
counts.schedules = schedUpdates.length
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
// 3. Watch jobs — cancel active
|
|
73
72
|
const watchJobs = loadWatchJobs()
|
|
74
|
-
const wjUpdates: Array<[string, unknown]> = []
|
|
73
|
+
const wjUpdates: Array<[string, Record<string, unknown>]> = []
|
|
75
74
|
for (const w of Object.values(watchJobs) as unknown as Array<Record<string, unknown>>) {
|
|
76
75
|
if (!w || w.agentId !== agentId) continue
|
|
77
76
|
if (w.status === 'cancelled') continue
|
|
@@ -79,26 +78,28 @@ export function suspendAgentReferences(agentId: string): CascadeCounts {
|
|
|
79
78
|
wjUpdates.push([w.id as string, w])
|
|
80
79
|
}
|
|
81
80
|
if (wjUpdates.length) {
|
|
82
|
-
|
|
81
|
+
upsertWatchJobs(wjUpdates)
|
|
83
82
|
counts.watchJobs = wjUpdates.length
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
// 4. Connectors — detach agent (keep connector alive but unrouted)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
{
|
|
87
|
+
const connectors = loadConnectors()
|
|
88
|
+
let connectorUpdates = 0
|
|
89
|
+
for (const c of Object.values(connectors)) {
|
|
90
|
+
if (!c || c.agentId !== agentId) continue
|
|
91
|
+
c.agentId = null
|
|
92
|
+
connectorUpdates += 1
|
|
93
|
+
}
|
|
94
|
+
if (connectorUpdates > 0) {
|
|
95
|
+
saveConnectors(connectors)
|
|
96
|
+
counts.connectors = connectorUpdates
|
|
97
|
+
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// 5. Delegation jobs — cancel queued/running
|
|
100
101
|
const delegationJobs = loadDelegationJobs()
|
|
101
|
-
const djUpdates: Array<[string, unknown]> = []
|
|
102
|
+
const djUpdates: Array<[string, Record<string, unknown>]> = []
|
|
102
103
|
for (const d of Object.values(delegationJobs) as unknown as Array<Record<string, unknown>>) {
|
|
103
104
|
if (!d || d.agentId !== agentId) continue
|
|
104
105
|
const status = d.status as string | undefined
|
|
@@ -108,22 +109,22 @@ export function suspendAgentReferences(agentId: string): CascadeCounts {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
if (djUpdates.length) {
|
|
111
|
-
|
|
112
|
+
saveDelegationJobRecords(djUpdates)
|
|
112
113
|
counts.delegationJobs = djUpdates.length
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
// 6. Webhooks — disable
|
|
116
117
|
const webhooks = loadWebhooks()
|
|
117
|
-
|
|
118
|
-
for (const w of Object.values(webhooks)
|
|
118
|
+
let webhookUpdates = 0
|
|
119
|
+
for (const w of Object.values(webhooks)) {
|
|
119
120
|
if (!w || w.agentId !== agentId) continue
|
|
120
121
|
if (w.enabled === false) continue
|
|
121
122
|
w.enabled = false
|
|
122
|
-
|
|
123
|
+
webhookUpdates += 1
|
|
123
124
|
}
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
counts.webhooks =
|
|
125
|
+
if (webhookUpdates > 0) {
|
|
126
|
+
saveWebhooks(webhooks)
|
|
127
|
+
counts.webhooks = webhookUpdates
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
// 7. Chatrooms — remove agent from member arrays
|
|
@@ -138,23 +139,25 @@ export function suspendAgentReferences(agentId: string): CascadeCounts {
|
|
|
138
139
|
export function purgeAgentReferences(agentId: string): CascadeCounts {
|
|
139
140
|
const counts: CascadeCounts = { tasks: 0, schedules: 0, watchJobs: 0, connectors: 0, delegationJobs: 0, webhooks: 0, chatrooms: 0 }
|
|
140
141
|
|
|
141
|
-
counts.tasks = deleteMatching(
|
|
142
|
-
counts.schedules = deleteMatching(
|
|
143
|
-
counts.watchJobs = deleteMatching(
|
|
144
|
-
counts.delegationJobs = deleteMatching(
|
|
145
|
-
counts.webhooks =
|
|
142
|
+
counts.tasks = deleteMatching(loadTasks(), agentId, deleteTask)
|
|
143
|
+
counts.schedules = deleteMatching(loadSchedules(), agentId, deleteSchedule)
|
|
144
|
+
counts.watchJobs = deleteMatching(loadWatchJobs(), agentId, deleteWatchJob)
|
|
145
|
+
counts.delegationJobs = deleteMatching(loadDelegationJobs(), agentId, deleteDelegationJob)
|
|
146
|
+
counts.webhooks = purgeWebhooks(agentId)
|
|
146
147
|
|
|
147
148
|
// Connectors: detach agent but keep the connector record
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
{
|
|
150
|
+
const connectors = loadConnectors()
|
|
151
|
+
let connectorUpdates = 0
|
|
152
|
+
for (const c of Object.values(connectors)) {
|
|
153
|
+
if (!c || c.agentId !== agentId) continue
|
|
154
|
+
c.agentId = null
|
|
155
|
+
connectorUpdates += 1
|
|
156
|
+
}
|
|
157
|
+
if (connectorUpdates > 0) {
|
|
158
|
+
saveConnectors(connectors)
|
|
159
|
+
counts.connectors = connectorUpdates
|
|
160
|
+
}
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
counts.chatrooms = removeAgentFromChatrooms(agentId)
|
|
@@ -167,7 +170,7 @@ export function purgeAgentReferences(agentId: string): CascadeCounts {
|
|
|
167
170
|
/** Re-enable schedules that were paused by trash. */
|
|
168
171
|
export function restoreAgentSchedules(agentId: string): number {
|
|
169
172
|
const schedules = loadSchedules()
|
|
170
|
-
const updates: Array<[string, unknown]> = []
|
|
173
|
+
const updates: Array<[string, Record<string, unknown>]> = []
|
|
171
174
|
for (const s of Object.values(schedules) as unknown as Array<Record<string, unknown>>) {
|
|
172
175
|
if (!s || s.agentId !== agentId) continue
|
|
173
176
|
if (!s.suspendedByTrash) continue
|
|
@@ -176,7 +179,7 @@ export function restoreAgentSchedules(agentId: string): number {
|
|
|
176
179
|
updates.push([s.id as string, s])
|
|
177
180
|
}
|
|
178
181
|
if (updates.length) {
|
|
179
|
-
|
|
182
|
+
upsertSchedules(updates)
|
|
180
183
|
}
|
|
181
184
|
return updates.length
|
|
182
185
|
}
|
|
@@ -184,15 +187,15 @@ export function restoreAgentSchedules(agentId: string): number {
|
|
|
184
187
|
// ── Internals ───────────────────────────────────────────────────────────
|
|
185
188
|
|
|
186
189
|
function deleteMatching<T extends { agentId?: string | null; id?: string | null }>(
|
|
187
|
-
table: StorageCollection,
|
|
188
190
|
collection: Record<string, T>,
|
|
189
191
|
agentId: string,
|
|
192
|
+
deleteItem: (id: string) => void,
|
|
190
193
|
): number {
|
|
191
194
|
let count = 0
|
|
192
195
|
for (const item of Object.values(collection)) {
|
|
193
196
|
if (!item || item.agentId !== agentId) continue
|
|
194
197
|
if (!item.id) continue
|
|
195
|
-
|
|
198
|
+
deleteItem(item.id)
|
|
196
199
|
count++
|
|
197
200
|
}
|
|
198
201
|
return count
|
|
@@ -200,7 +203,7 @@ function deleteMatching<T extends { agentId?: string | null; id?: string | null
|
|
|
200
203
|
|
|
201
204
|
function removeAgentFromChatrooms(agentId: string): number {
|
|
202
205
|
const chatrooms = loadChatrooms()
|
|
203
|
-
|
|
206
|
+
let changedCount = 0
|
|
204
207
|
for (const room of Object.values(chatrooms) as unknown as Array<Record<string, unknown>>) {
|
|
205
208
|
if (!room) continue
|
|
206
209
|
let changed = false
|
|
@@ -220,10 +223,27 @@ function removeAgentFromChatrooms(agentId: string): number {
|
|
|
220
223
|
room.agentIds = agentIds.filter((id) => id !== agentId)
|
|
221
224
|
if ((room.agentIds as string[]).length !== before) changed = true
|
|
222
225
|
}
|
|
223
|
-
if (changed)
|
|
226
|
+
if (changed) changedCount += 1
|
|
224
227
|
}
|
|
225
|
-
if (
|
|
226
|
-
|
|
228
|
+
if (changedCount > 0) {
|
|
229
|
+
saveChatrooms(chatrooms)
|
|
227
230
|
}
|
|
228
|
-
return
|
|
231
|
+
return changedCount
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function purgeWebhooks(agentId: string): number {
|
|
235
|
+
const webhooks = loadWebhooks() as Record<string, Record<string, unknown>>
|
|
236
|
+
const remaining: Record<string, Record<string, unknown>> = {}
|
|
237
|
+
let count = 0
|
|
238
|
+
for (const [id, webhook] of Object.entries(webhooks)) {
|
|
239
|
+
if (webhook && webhook.agentId === agentId) {
|
|
240
|
+
count += 1
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
remaining[id] = webhook
|
|
244
|
+
}
|
|
245
|
+
if (count > 0) {
|
|
246
|
+
saveWebhooks(remaining)
|
|
247
|
+
}
|
|
248
|
+
return count
|
|
229
249
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { loadAgents, loadTasks, loadSessions } from '@/lib/server/storage'
|
|
2
1
|
import type { Agent, BoardTask } from '@/types'
|
|
2
|
+
import { loadAgents } from '@/lib/server/agents/agent-repository'
|
|
3
|
+
import { loadSessions } from '@/lib/server/sessions/session-repository'
|
|
4
|
+
import { loadTasks } from '@/lib/server/tasks/task-repository'
|
|
3
5
|
|
|
4
6
|
export interface AgentDirectoryEntry {
|
|
5
7
|
id: string
|