@swarmclawai/swarmclaw 0.8.3 → 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/README.md
CHANGED
|
@@ -148,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
151
|
-
To pin a version: `SWARMCLAW_VERSION=v0.8.
|
|
151
|
+
To pin a version: `SWARMCLAW_VERSION=v0.8.4 curl ... | bash`
|
|
152
152
|
|
|
153
153
|
Or run locally from the repo (friendly for non-technical users):
|
|
154
154
|
|
|
@@ -701,7 +701,7 @@ npm run update:easy # safe update helper for local installs
|
|
|
701
701
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
702
702
|
|
|
703
703
|
```bash
|
|
704
|
-
# example minor release (v0.8.
|
|
704
|
+
# example minor release (v0.8.4 style)
|
|
705
705
|
npm version minor
|
|
706
706
|
git push origin main --follow-tags
|
|
707
707
|
```
|
|
@@ -711,14 +711,14 @@ On `v*` tags, GitHub Actions will:
|
|
|
711
711
|
2. Create a GitHub Release
|
|
712
712
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
713
713
|
|
|
714
|
-
#### v0.8.
|
|
714
|
+
#### v0.8.4 Release Readiness Notes
|
|
715
715
|
|
|
716
|
-
Before shipping `v0.8.
|
|
716
|
+
Before shipping `v0.8.4`, confirm the following user-facing changes are reflected in docs:
|
|
717
717
|
|
|
718
|
-
1.
|
|
719
|
-
2.
|
|
720
|
-
3.
|
|
721
|
-
4. Site and README install/version strings are updated to `v0.8.
|
|
718
|
+
1. Connector/runtime docs note that the current patch line includes the connector-manager follow-up fix already shipped in the latest app commit.
|
|
719
|
+
2. Chat/session docs still note that the chat index serves lightweight session summaries instead of full transcript payloads, and full messages are loaded from per-chat endpoints.
|
|
720
|
+
3. Operator/runtime docs still note that the daemon owns scheduler/queue startup; background services should be described from the daemon controls rather than as unconditional boot behavior.
|
|
721
|
+
4. Site and README install/version strings are updated to `v0.8.4`, including install snippets, release notes index text, and sidebar/footer labels.
|
|
722
722
|
|
|
723
723
|
## CLI
|
|
724
724
|
|
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',
|