@swarmclawai/swarmclaw 0.8.4 → 0.8.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import {
|
|
4
|
+
stripBlockedItems,
|
|
5
|
+
isHeartbeatContentEffectivelyEmpty,
|
|
6
|
+
buildAgentHeartbeatPrompt,
|
|
7
|
+
} from './heartbeat-service'
|
|
8
|
+
|
|
9
|
+
describe('heartbeat blocked-item suppression', () => {
|
|
10
|
+
describe('stripBlockedItems', () => {
|
|
11
|
+
it('removes checklist items marked (blocked, no update)', () => {
|
|
12
|
+
const input = [
|
|
13
|
+
'# Heartbeat Tasks',
|
|
14
|
+
'## Active',
|
|
15
|
+
'- [ ] Pull SWGOH roster data (blocked, no update)',
|
|
16
|
+
'- [ ] Send daily summary',
|
|
17
|
+
'## Completed',
|
|
18
|
+
'- [x] Do laundry',
|
|
19
|
+
].join('\n')
|
|
20
|
+
|
|
21
|
+
const result = stripBlockedItems(input)
|
|
22
|
+
|
|
23
|
+
assert.ok(!result.includes('SWGOH'), 'blocked item should be stripped')
|
|
24
|
+
assert.ok(result.includes('Send daily summary'), 'non-blocked item should remain')
|
|
25
|
+
assert.ok(result.includes('Do laundry'), 'completed item should remain')
|
|
26
|
+
assert.ok(result.includes('# Heartbeat Tasks'), 'headers should remain')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('removes items with various blocked markers', () => {
|
|
30
|
+
const input = [
|
|
31
|
+
'- [ ] Task A (blocked: awaiting input)',
|
|
32
|
+
'- [ ] Task B (Blocked, pending user decision)',
|
|
33
|
+
'- [ ] Task C (BLOCKED)',
|
|
34
|
+
'- [ ] Task D - normal task',
|
|
35
|
+
'* [ ] Task E (blocked, no update)',
|
|
36
|
+
].join('\n')
|
|
37
|
+
|
|
38
|
+
const result = stripBlockedItems(input)
|
|
39
|
+
|
|
40
|
+
assert.ok(!result.includes('Task A'), 'blocked: variant stripped')
|
|
41
|
+
assert.ok(!result.includes('Task B'), 'Blocked, variant stripped')
|
|
42
|
+
assert.ok(!result.includes('Task C'), 'BLOCKED variant stripped')
|
|
43
|
+
assert.ok(result.includes('Task D'), 'non-blocked task preserved')
|
|
44
|
+
assert.ok(!result.includes('Task E'), 'asterisk list item stripped')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('preserves non-list lines that mention "blocked"', () => {
|
|
48
|
+
const input = [
|
|
49
|
+
'## Notes',
|
|
50
|
+
'Some items are blocked until user responds.',
|
|
51
|
+
'- [ ] Actual blocked task (blocked, no update)',
|
|
52
|
+
].join('\n')
|
|
53
|
+
|
|
54
|
+
const result = stripBlockedItems(input)
|
|
55
|
+
|
|
56
|
+
assert.ok(result.includes('Some items are blocked'), 'prose mentioning blocked should stay')
|
|
57
|
+
assert.ok(!result.includes('Actual blocked task'), 'blocked list item should be stripped')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns empty string for empty input', () => {
|
|
61
|
+
assert.equal(stripBlockedItems(''), '')
|
|
62
|
+
assert.equal(stripBlockedItems(null as unknown as string), '')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns content unchanged when no blocked items', () => {
|
|
66
|
+
const input = '- [ ] Task A\n- [ ] Task B\n'
|
|
67
|
+
assert.equal(stripBlockedItems(input), input)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('blocked items + effectively empty', () => {
|
|
72
|
+
it('treats content with only blocked items as effectively empty', () => {
|
|
73
|
+
const input = [
|
|
74
|
+
'# Heartbeat Tasks',
|
|
75
|
+
'## Active',
|
|
76
|
+
'- [ ] Pull SWGOH data (blocked, no update)',
|
|
77
|
+
'## Completed',
|
|
78
|
+
].join('\n')
|
|
79
|
+
|
|
80
|
+
const stripped = stripBlockedItems(input)
|
|
81
|
+
// After stripping, only headers remain — effectively empty
|
|
82
|
+
assert.equal(isHeartbeatContentEffectivelyEmpty(stripped), true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('treats content with blocked + active items as not empty', () => {
|
|
86
|
+
const input = [
|
|
87
|
+
'# Heartbeat Tasks',
|
|
88
|
+
'## Active',
|
|
89
|
+
'- [ ] Pull SWGOH data (blocked, no update)',
|
|
90
|
+
'- [ ] Send daily summary',
|
|
91
|
+
].join('\n')
|
|
92
|
+
|
|
93
|
+
const stripped = stripBlockedItems(input)
|
|
94
|
+
assert.equal(isHeartbeatContentEffectivelyEmpty(stripped), false)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('buildAgentHeartbeatPrompt integration', () => {
|
|
99
|
+
it('does not include blocked items in the prompt sent to the LLM', () => {
|
|
100
|
+
const session = {
|
|
101
|
+
id: 'test-session',
|
|
102
|
+
cwd: '/tmp',
|
|
103
|
+
messages: [],
|
|
104
|
+
}
|
|
105
|
+
const agent = {
|
|
106
|
+
id: 'test-agent',
|
|
107
|
+
name: 'Test',
|
|
108
|
+
description: 'Test agent',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const heartbeatFileContent = [
|
|
112
|
+
'# Heartbeat Tasks',
|
|
113
|
+
'## Active',
|
|
114
|
+
'- [ ] Pull SWGOH roster data (blocked, no update)',
|
|
115
|
+
'- [ ] Check weather forecast',
|
|
116
|
+
'## Completed',
|
|
117
|
+
'- [x] Laundry done',
|
|
118
|
+
].join('\n')
|
|
119
|
+
|
|
120
|
+
const prompt = buildAgentHeartbeatPrompt(session, agent, 'default prompt', heartbeatFileContent)
|
|
121
|
+
|
|
122
|
+
assert.ok(!prompt.includes('SWGOH'), 'blocked SWGOH task should not appear in prompt')
|
|
123
|
+
assert.ok(prompt.includes('Check weather forecast'), 'non-blocked task should appear')
|
|
124
|
+
assert.ok(prompt.includes('Laundry done'), 'completed task should appear')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('produces no HEARTBEAT.md section when all active items are blocked', () => {
|
|
128
|
+
const session = {
|
|
129
|
+
id: 'test-session',
|
|
130
|
+
cwd: '/tmp',
|
|
131
|
+
messages: [],
|
|
132
|
+
}
|
|
133
|
+
const agent = {
|
|
134
|
+
id: 'test-agent',
|
|
135
|
+
name: 'Test',
|
|
136
|
+
description: 'Test agent',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const heartbeatFileContent = [
|
|
140
|
+
'# Heartbeat Tasks',
|
|
141
|
+
'## Active',
|
|
142
|
+
'- [ ] Task A (blocked, no update)',
|
|
143
|
+
'- [ ] Task B (blocked: awaiting user)',
|
|
144
|
+
].join('\n')
|
|
145
|
+
|
|
146
|
+
const prompt = buildAgentHeartbeatPrompt(session, agent, 'default prompt', heartbeatFileContent)
|
|
147
|
+
|
|
148
|
+
assert.ok(!prompt.includes('HEARTBEAT.md contents:'), 'should not include HEARTBEAT.md section when all items are blocked')
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -194,6 +194,31 @@ export function buildIdentityContext(session: Record<string, unknown> | undefine
|
|
|
194
194
|
return `## Your Identity\n${lines.join('\n')}`
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// ── Blocked-item suppression ────────────────────────────────────────────
|
|
198
|
+
// Ported from OpenClaw's duplicate-suppression pattern: instead of letting
|
|
199
|
+
// the LLM see blocked tasks every tick (and parrot "still blocked"), we
|
|
200
|
+
// strip those lines before they ever reach the prompt. A line is
|
|
201
|
+
// considered blocked if it contains "(blocked" anywhere (case-insensitive),
|
|
202
|
+
// which covers "(blocked, no update)", "(blocked: awaiting …)", etc.
|
|
203
|
+
const BLOCKED_MARKER_RE = /\(blocked\b/i
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove blocked checklist items from HEARTBEAT.md content so the LLM
|
|
207
|
+
* doesn't keep surfacing them. Headers and non-list lines pass through
|
|
208
|
+
* unchanged.
|
|
209
|
+
*/
|
|
210
|
+
export function stripBlockedItems(content: string): string {
|
|
211
|
+
if (!content) return ''
|
|
212
|
+
const lines = content.split('\n')
|
|
213
|
+
const filtered = lines.filter((line) => {
|
|
214
|
+
const trimmed = line.trim()
|
|
215
|
+
// Only filter checklist / list items that are explicitly marked blocked
|
|
216
|
+
if (/^[-*+]\s/.test(trimmed) && BLOCKED_MARKER_RE.test(trimmed)) return false
|
|
217
|
+
return true
|
|
218
|
+
})
|
|
219
|
+
return filtered.join('\n')
|
|
220
|
+
}
|
|
221
|
+
|
|
197
222
|
/** Detect HEARTBEAT.md files that contain only skeleton structure (headers, empty list items) but no real content. */
|
|
198
223
|
export function isHeartbeatContentEffectivelyEmpty(content: string | undefined | null): boolean {
|
|
199
224
|
if (!content || typeof content !== 'string') return true
|
|
@@ -236,8 +261,9 @@ export function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackProm
|
|
|
236
261
|
})
|
|
237
262
|
.join('\n')
|
|
238
263
|
|
|
239
|
-
//
|
|
240
|
-
const
|
|
264
|
+
// Strip blocked items, then check if anything meaningful remains
|
|
265
|
+
const strippedContent = stripBlockedItems(heartbeatFileContent)
|
|
266
|
+
const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(strippedContent) ? '' : strippedContent
|
|
241
267
|
|
|
242
268
|
return [
|
|
243
269
|
'AGENT_HEARTBEAT_TICK',
|