@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.3 curl ... | bash`
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.3 style)
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.3 Release Readiness Notes
714
+ #### v0.8.4 Release Readiness Notes
715
715
 
716
- Before shipping `v0.8.3`, confirm the following user-facing changes are reflected in docs:
716
+ Before shipping `v0.8.4`, confirm the following user-facing changes are reflected in docs:
717
717
 
718
- 1. Chat/session docs note that the chat index now serves lightweight session summaries instead of full transcript payloads, and full messages are loaded from per-chat endpoints.
719
- 2. Operator/runtime docs note that the daemon once again owns scheduler/queue startup; background services should be described from the daemon controls rather than as unconditional boot behavior.
720
- 3. Local auth/troubleshooting docs mention that development-like runtimes are detected even when `NODE_ENV` is unset, so local rate limits and bootstrap timeouts now behave like dev instead of production.
721
- 4. Site and README install/version strings are updated to `v0.8.3`, including install snippets, release notes index text, and sidebar/footer labels.
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",
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
- // Don't inject effectively-empty HEARTBEAT.md content
240
- const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) ? '' : heartbeatFileContent
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',