@swarmclawai/swarmclaw 0.8.1 → 0.8.2

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.1 curl ... | bash`
151
+ To pin a version: `SWARMCLAW_VERSION=v0.8.2 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.1 style)
704
+ # example minor release (v0.8.2 style)
705
705
  npm version minor
706
706
  git push origin main --follow-tags
707
707
  ```
@@ -711,13 +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.1 Release Readiness Notes
714
+ #### v0.8.2 Release Readiness Notes
715
715
 
716
- Before shipping `v0.8.1`, confirm the following user-facing changes are reflected in docs:
716
+ Before shipping `v0.8.2`, confirm the following user-facing changes are reflected in docs:
717
717
 
718
- 1. Release notes call out notification deduping and occurrence counts so operators know repeated health/approval events now refresh one notification instead of piling up duplicates.
719
- 2. Memory/tooling docs mention the new current-thread recall rule: same-thread recall should stay local to the chat, while direct memory writes should go straight to `memory_store` or `memory_update` rather than detouring through agent/task/delegation flows.
720
- 3. Site and README install/version strings are updated to `v0.8.1`, including install snippets, release notes index text, and sidebar/footer labels.
718
+ 1. Runtime/defaults docs mention the higher default agent recursion limit so long-running bounded turns get more headroom without custom tuning.
719
+ 2. Memory/tooling docs mention the narrower direct-memory-write routing: remember-and-confirm turns stay on `memory_store`/`memory_update`, bundled related facts should be stored as one canonical write, and same-thread recall should not steal those turns.
720
+ 3. File-output guidance notes that exact bullet-count and titled-section constraints are now treated as hard structure requirements during deliverable follow-through.
721
+ 4. Site and README install/version strings are updated to `v0.8.2`, including install snippets, release notes index text, and sidebar/footer labels.
721
722
 
722
723
  ## CLI
723
724
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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": {
@@ -22,7 +22,7 @@ export const CLAUDE_CODE_TIMEOUT_SEC_MAX = 7200
22
22
  export const CLI_PROCESS_TIMEOUT_SEC_MIN = 10
23
23
  export const CLI_PROCESS_TIMEOUT_SEC_MAX = 7200
24
24
 
25
- export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 60
25
+ export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 120
26
26
  export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 80
27
27
  export const DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS = 16
28
28
  export const DEFAULT_ONGOING_LOOP_MAX_ITERATIONS = 250
@@ -36,6 +36,10 @@ test('isCurrentThreadRecallRequest detects same-thread recall without matching s
36
36
  isCurrentThreadRecallRequest('Remember that my favorite programming language is Rust and I prefer functional programming patterns.'),
37
37
  false,
38
38
  )
39
+ assert.equal(
40
+ isCurrentThreadRecallRequest('Remember that my favorite programming language is Rust and I prefer functional programming patterns. Then confirm what you just stored.'),
41
+ false,
42
+ )
39
43
  })
40
44
 
41
45
  test('isDirectMemoryWriteRequest detects remember-and-confirm turns without matching recall questions', () => {
@@ -29,6 +29,7 @@ export function isCurrentThreadRecallRequest(message: string): boolean {
29
29
  const trimmed = normalizeWhitespace(message)
30
30
  if (!trimmed) return false
31
31
  if (!CURRENT_THREAD_RECALL_MARKER_RE.test(trimmed)) return false
32
+ if (DIRECT_MEMORY_WRITE_MARKER_RE.test(trimmed) && DIRECT_MEMORY_WRITE_FOLLOWUP_RE.test(trimmed)) return false
32
33
  if (/\b(?:remember|store|save)\b/i.test(trimmed) && !/\?\s*$/.test(trimmed) && !/\b(?:what|which|who|when|where|did|confirm|recap|summarize|repeat|list|tell me|answer|recall)\b/i.test(trimmed)) {
33
34
  return false
34
35
  }
@@ -46,7 +46,7 @@ describe('runtime settings defaults', () => {
46
46
  `)
47
47
 
48
48
  assert.equal(output.settings.loopMode, 'bounded')
49
- assert.equal(output.settings.agentLoopRecursionLimit, 60)
49
+ assert.equal(output.settings.agentLoopRecursionLimit, 120)
50
50
  assert.equal(output.settings.orchestratorLoopRecursionLimit, 80)
51
51
  assert.equal(output.settings.legacyOrchestratorMaxTurns, 16)
52
52
  assert.equal(output.settings.ongoingLoopMaxIterations, 250)
@@ -61,7 +61,7 @@ describe('runtime settings defaults', () => {
61
61
  assert.equal(output.settings.heartbeatShowAlerts, true)
62
62
  assert.equal(output.settings.heartbeatTarget, null)
63
63
  assert.equal(output.settings.heartbeatPrompt, null)
64
- assert.equal(output.runtime.agentLoopRecursionLimit, 60)
64
+ assert.equal(output.runtime.agentLoopRecursionLimit, 120)
65
65
  assert.equal(output.runtime.orchestratorLoopRecursionLimit, 80)
66
66
  assert.equal(output.runtime.legacyOrchestratorMaxTurns, 16)
67
67
  })
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict'
3
3
  import fs from 'node:fs'
4
4
  import os from 'node:os'
5
5
  import path from 'node:path'
6
+ import type { Agent } from '@/types'
6
7
 
7
8
  const originalEnv = {
8
9
  DATA_DIR: process.env.DATA_DIR,
@@ -30,11 +31,12 @@ before(async () => {
30
31
  loadAgents = storageMod.loadAgents
31
32
  saveAgents = storageMod.saveAgents
32
33
 
33
- const agents = loadAgents({ includeTrashed: true })
34
+ const agents = loadAgents({ includeTrashed: true }) as Record<string, Agent>
34
35
  agents['agent-soul-test'] = {
35
36
  id: 'agent-soul-test',
36
37
  name: 'Soul Test Agent',
37
38
  description: 'Agent used for CRUD soul validation tests',
39
+ systemPrompt: '',
38
40
  provider: 'ollama',
39
41
  model: 'glm-5:cloud',
40
42
  plugins: ['manage_agents'],
@@ -42,7 +44,7 @@ before(async () => {
42
44
  platformAssignScope: 'self',
43
45
  createdAt: Date.now(),
44
46
  updatedAt: Date.now(),
45
- } as any
47
+ }
46
48
  saveAgents(agents)
47
49
  })
48
50
 
@@ -124,8 +126,8 @@ describe('manage_agents soul validation', () => {
124
126
 
125
127
  const first = JSON.parse(String(firstRaw)) as Record<string, unknown>
126
128
  const second = JSON.parse(String(secondRaw)) as Record<string, unknown>
127
- const created = Object.values(loadAgents({ includeTrashed: true }))
128
- .filter((agent: any) => agent.createdInSessionId === 'agent-dedupe-session')
129
+ const created = Object.values(loadAgents({ includeTrashed: true }) as Record<string, Agent & { createdInSessionId?: string }>)
130
+ .filter((agent) => agent.createdInSessionId === 'agent-dedupe-session')
129
131
 
130
132
  assert.equal(created.length, 1)
131
133
  assert.equal(second.id, first.id)
@@ -788,6 +788,7 @@ const MemoryPlugin: Plugin = {
788
788
  'For info already in the current conversation, respond directly without calling any memory tool.',
789
789
  'For questions about prior work, decisions, dates, people, preferences, or todos from earlier conversations: start with one durable `memory_search`, then use `memory_get` only if you need a more targeted read. Only use archive/session history when the user explicitly needs transcript-level detail or the durable search is insufficient.',
790
790
  'When the user directly says to remember, store, or correct a fact, do one `memory_store` or `memory_update` call immediately. Treat the newest direct user statement as authoritative.',
791
+ 'When one user message contains multiple related facts to remember, prefer one canonical `memory_store` write that captures the full set instead of many near-duplicate store calls.',
791
792
  'If someone says "remember this", write it down; do not rely on RAM alone.',
792
793
  'Memory writes merge canonical memories and retire superseded variants. After a successful store/update, do not keep re-searching unless the user explicitly asked you to verify.',
793
794
  'By default, memory searches focus on durable memories. Only include archives or working execution notes when you explicitly need transcript or run-history context.',
@@ -867,7 +868,7 @@ const MemoryPlugin: Plugin = {
867
868
  },
868
869
  {
869
870
  name: 'memory_store',
870
- description: 'Store a durable fact, preference, decision, or correction from the user. Use this immediately when the user says to remember something.',
871
+ description: 'Store a durable fact, preference, decision, or correction from the user. Use this immediately when the user says to remember something. If several related facts arrive in one request, prefer one canonical write over many near-duplicate calls.',
871
872
  parameters: {
872
873
  type: 'object',
873
874
  properties: {
@@ -884,6 +885,7 @@ const MemoryPlugin: Plugin = {
884
885
  capabilities: ['memory.write'],
885
886
  disciplineGuidance: [
886
887
  'When the user says to remember or store a fact, call `memory_store` immediately. Do not delegate or use platform-management tools first.',
888
+ 'If the user bundled multiple related facts into one remember request, store them together in one canonical write unless they asked for separate memories.',
887
889
  ],
888
890
  },
889
891
  execute: async (args, context) => executeNamedMemoryAction('store', args, context),
@@ -7,6 +7,7 @@ import {
7
7
  buildExternalWalletExecutionBlock,
8
8
  buildToolDisciplineLines,
9
9
  getExplicitRequiredToolNames,
10
+ isNarrowDirectMemoryWriteTurn,
10
11
  isWalletSimulationResult,
11
12
  looksLikeOpenEndedDeliverableTask,
12
13
  resolveContinuationAssistantText,
@@ -39,6 +40,8 @@ describe('buildToolDisciplineLines', () => {
39
40
  const lines = buildToolDisciplineLines(['files'])
40
41
 
41
42
  assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
43
+ assert.ok(lines.some((line) => line.includes('exactly N bullet points')))
44
+ assert.ok(lines.some((line) => line.includes('Lower-priority logistics belong in FYI')))
42
45
  })
43
46
 
44
47
  it('adds schedule reuse and stop guidance when schedule tools are enabled', () => {
@@ -137,10 +140,12 @@ describe('buildToolDisciplineLines', () => {
137
140
  assert.ok(streamAgentChatSource.includes('call `memory_store` or `memory_update` immediately before any planning, delegation, task creation, or agent management'))
138
141
  assert.ok(streamAgentChatSource.includes('Do not inspect skills, browse the workspace, request capabilities, manage tasks, manage agents, or delegate before the direct memory write is complete.'))
139
142
  assert.ok(streamAgentChatSource.includes('Do NOT call memory tools, web search, or session-history tools'))
140
- assert.ok(streamAgentChatSource.includes('const currentThreadRecallRequest = isCurrentThreadRecallRequest(message)'))
141
- assert.ok(streamAgentChatSource.includes('const directMemoryWriteOnlyTurn = isDirectMemoryWriteRequest(message)'))
143
+ assert.ok(streamAgentChatSource.includes('const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)'))
144
+ assert.ok(streamAgentChatSource.includes('const directMemoryWriteOnlyTurn = isNarrowDirectMemoryWriteTurn(message)'))
142
145
  assert.ok(streamAgentChatSource.includes('shouldAllowToolForDirectMemoryWrite(toolName)'))
143
146
  assert.ok(streamAgentChatSource.includes('shouldAllowToolForCurrentThreadRecall(toolName)'))
147
+ assert.ok(streamAgentChatSource.includes('Preserve hard structural constraints from the original request'))
148
+ assert.ok(streamAgentChatSource.includes('## Exact Structural Constraints'))
144
149
  })
145
150
 
146
151
  it('blocks memory, session-history, web, and context tools during same-thread recall turns', () => {
@@ -164,6 +169,21 @@ describe('buildToolDisciplineLines', () => {
164
169
  assert.equal(shouldAllowToolForDirectMemoryWrite('files'), false)
165
170
  })
166
171
 
172
+ it('treats long remember-and-confirm turns as narrow direct memory writes', () => {
173
+ assert.equal(
174
+ isNarrowDirectMemoryWriteTurn('Remember that my favorite programming language is Rust and I prefer functional programming patterns. Then confirm what you just stored.'),
175
+ true,
176
+ )
177
+ assert.equal(
178
+ isNarrowDirectMemoryWriteTurn('Remember these facts for future conversations: My favorite programming language is Rust. My deploy target is Fly.io. My team size is 7 people. The project is codenamed "Neptune".'),
179
+ true,
180
+ )
181
+ assert.equal(
182
+ isNarrowDirectMemoryWriteTurn('Remember that my favorite programming language is Rust, then write a file summarizing it and send it to me.'),
183
+ false,
184
+ )
185
+ })
186
+
167
187
  it('canonicalizes required tool names when checking completion', () => {
168
188
  // The requiredToolsPending filter must canonicalize tool names so that
169
189
  // alias names (e.g. ask_human) match canonical names from LangGraph events.
@@ -133,6 +133,11 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
133
133
  lines.push('Use `manage_capabilities` only when a needed tool is actually unavailable. If a direct tool for the job is already enabled in this session, call that tool immediately instead of requesting access or re-running discovery.')
134
134
  }
135
135
 
136
+ if (uniqueTools.includes('files') || uniqueTools.includes('edit_file')) {
137
+ lines.push('When the user specifies exact counts or exact section titles for file content, treat those as hard constraints. If a file must have exactly N bullet points, keep the total bullet count at N and put extra required detail into short prose under titled sections unless the user explicitly asked for more bullets.')
138
+ lines.push('When summarizing or restructuring a source document into named sections, make sure each top-level source section is represented somewhere in the output. Lower-priority logistics belong in FYI rather than being dropped.')
139
+ }
140
+
136
141
  if (uniqueTools.includes('delegate') && (uniqueTools.includes('shell') || uniqueTools.includes('files') || uniqueTools.includes('edit_file'))) {
137
142
  lines.push('When local workspace tools like `shell`, `files`, or `edit_file` are already enabled, prefer using them directly for straightforward coding and verification. Use `delegate` when you need a specialist backend, a second implementation pass, or parallel work.')
138
143
  }
@@ -524,6 +529,7 @@ function buildDeliverableFollowthroughPrompt(params: {
524
529
  'Do not stop after one partial batch. Finish every requested deliverable that is still outstanding before concluding.',
525
530
  'If a requested artifact cannot be produced, say exactly which artifact is missing, what blocked it, and what you already completed.',
526
531
  'Use the existing files, screenshots, and generated outputs first. Inspect them if needed, then complete the remaining work.',
532
+ 'Preserve hard structural constraints from the original request: exact counts stay exact, required titled sections stay present, and source coverage gaps should be filled instead of skipped.',
527
533
  'End with a concise grouped completion summary that lists exact file paths, upload URLs, localhost URLs/ports, and screenshots you produced.',
528
534
  ]
529
535
 
@@ -553,6 +559,18 @@ function buildDeliverableFollowthroughPrompt(params: {
553
559
  return lines.join('\n')
554
560
  }
555
561
 
562
+ function buildExactStructureBlock(userMessage: string): string {
563
+ const exactBulletMatch = userMessage.match(/\bexactly\s+(\d+)\s+bullet points?\b/i)
564
+ if (!exactBulletMatch) return ''
565
+ const bulletCount = exactBulletMatch[1]
566
+ return [
567
+ '## Exact Structural Constraints',
568
+ `The user required exactly ${bulletCount} bullet points.`,
569
+ 'Treat that as a hard file-wide constraint unless the user explicitly says later sections get their own separate bullets.',
570
+ 'If the file also needs titled sections such as Owners or Risks, use short prose under those headings instead of adding more bullet lines.',
571
+ ].join('\n')
572
+ }
573
+
556
574
  /** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
557
575
  function isBroadGoal(text: string): boolean {
558
576
  if (text.length < 50) return false
@@ -591,10 +609,7 @@ function buildAgenticExecutionPolicy(opts: {
591
609
  const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
592
610
  const toolDisciplineLines = buildToolDisciplineLines(opts.enabledPlugins)
593
611
  const hasMemoryTools = opts.enabledPlugins.some((toolId) => (canonicalizePluginId(toolId) || toolId) === 'memory')
594
- const directMemoryWriteRequest = Boolean(opts.userMessage && isDirectMemoryWriteRequest(opts.userMessage))
595
- const directMemoryWriteOnlyTurn = directMemoryWriteRequest
596
- && !isBroadGoal(opts.userMessage || '')
597
- && !looksLikeOpenEndedDeliverableTask(opts.userMessage || '')
612
+ const directMemoryWriteOnlyTurn = Boolean(opts.userMessage && isNarrowDirectMemoryWriteTurn(opts.userMessage))
598
613
 
599
614
  const parts: string[] = []
600
615
 
@@ -657,6 +672,10 @@ function buildAgenticExecutionPolicy(opts: {
657
672
  if (opts.userMessage && looksLikeOpenEndedDeliverableTask(opts.userMessage) && opts.enabledPlugins.some((toolId) => toolId === 'files' || toolId === 'edit_file')) {
658
673
  parts.push(OPEN_ENDED_REVISION_BLOCK)
659
674
  }
675
+ if (opts.userMessage) {
676
+ const exactStructureBlock = buildExactStructureBlock(opts.userMessage)
677
+ if (exactStructureBlock) parts.push(exactStructureBlock)
678
+ }
660
679
  if (opts.userMessage && isCurrentThreadRecallRequest(opts.userMessage)) {
661
680
  parts.push(buildCurrentThreadRecallBlock(opts.history || []))
662
681
  }
@@ -711,10 +730,24 @@ function buildDirectMemoryWriteBlock(): string {
711
730
  '## Direct Memory Write',
712
731
  'This turn is a direct request to remember, store, or correct a durable fact.',
713
732
  'Call `memory_store` or `memory_update` immediately, then confirm the stored value succinctly.',
733
+ 'If the user bundled several related facts into one remember request, store them together in one canonical memory write unless they explicitly asked for separate entries.',
714
734
  'Do not inspect skills, browse the workspace, request capabilities, manage tasks, manage agents, or delegate before the direct memory write is complete.',
715
735
  ].join('\n')
716
736
  }
717
737
 
738
+ const DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE = /\b(?:then|and then|after that)?\s*(?:confirm|recap|repeat|summarize|tell me|say)\b[\s\S]{0,120}\b(?:stored|saved|updated|remembered|wrote|write)\b/i
739
+ const DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE = /\b(?:then|and then|after that|also)\b[\s\S]{0,160}\b(?:write|create|send|email|message|delegate|research|search|browse|open|edit|build|schedule|plan|review|analy[sz]e)\b/i
740
+
741
+ export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
742
+ const trimmed = String(message || '').trim()
743
+ if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
744
+ if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
745
+ if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
746
+ return false
747
+ }
748
+ return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
749
+ }
750
+
718
751
  const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
719
752
  'memory',
720
753
  'manage_sessions',
@@ -840,7 +873,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
840
873
 
841
874
  const stateModifierParts: string[] = []
842
875
  const hasProvidedSystemPrompt = typeof systemPrompt === 'string' && systemPrompt.trim().length > 0
843
- const currentThreadRecallRequest = isCurrentThreadRecallRequest(message)
876
+ const directMemoryWriteOnlyTurn = isNarrowDirectMemoryWriteTurn(message)
877
+ const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)
844
878
 
845
879
  if (hasProvidedSystemPrompt) {
846
880
  stateModifierParts.push(systemPrompt!.trim())
@@ -1054,9 +1088,6 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1054
1088
  projectDescription: activeProjectContext.project?.description || null,
1055
1089
  memoryScopeMode: agentMemoryScopeMode,
1056
1090
  })
1057
- const directMemoryWriteOnlyTurn = isDirectMemoryWriteRequest(message)
1058
- && !isBroadGoal(message)
1059
- && !looksLikeOpenEndedDeliverableTask(message)
1060
1091
  const toolsForTurn = currentThreadRecallRequest
1061
1092
  ? tools.filter((tool) => {
1062
1093
  const toolName = typeof (tool as { name?: unknown }).name === 'string'
@@ -1268,7 +1299,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1268
1299
  const MAX_REQUIRED_TOOL_CONTINUES = 2
1269
1300
  const MAX_EXECUTION_FOLLOWTHROUGHS = 1
1270
1301
  const MAX_DELIVERABLE_FOLLOWTHROUGHS = 2
1271
- const MAX_TOOL_SUMMARY_RETRIES = 1
1302
+ const MAX_TOOL_SUMMARY_RETRIES = 2
1272
1303
  let autoContinueCount = 0
1273
1304
  let transientRetryCount = 0
1274
1305
  let requiredToolContinueCount = 0