@swarmclawai/swarmclaw 1.9.30 → 1.9.32

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
@@ -146,19 +146,34 @@ Then open `http://localhost:3456`.
146
146
  Install the SwarmClaw skill for your [OpenClaw](https://openclaw.ai) agents:
147
147
 
148
148
  ```bash
149
- clawhub install swarmclaw
149
+ openclaw skills install swarmclaw
150
150
  ```
151
151
 
152
- [Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
152
+ [Browse on ClawHub](https://clawhub.ai/waydelyle/swarmclaw)
153
+
154
+ ## v1.9.32 Highlights
155
+
156
+ PR integration release for background model routing, reflection memory controls, and current ClawHub install guidance.
157
+
158
+ - **Background model routing.** Per-agent `dreamConfig` overrides can route dream cycles and daily digests before global dream settings, while `compactionProvider` settings can route live auto-compaction summaries through a cheaper or faster model.
159
+ - **Reflection memory controls.** `reflectionMinQuality` gates automatic reflection memory writes without dropping the reflection record, and optional embedding dedup skips near-duplicate reflection notes when embeddings are configured.
160
+ - **ClawHub install guidance.** OpenClaw skill docs now use `openclaw skills install swarmclaw` and current owner-scoped ClawHub links.
161
+ - **Regression coverage.** Added tests for dream override precedence, compaction preference resolution, reflection quality gating, and embedding-based reflection dedup.
162
+
163
+ ## v1.9.31 Highlights
164
+
165
+ Documentation cleanup release for public release notes and OpenClaw guidance. No runtime behavior changed.
166
+
167
+ - **Public docs cleanup.** Removed an unwanted third-party example from the README and site release notes.
168
+ - **OpenClaw guidance preserved.** The README keeps the SwarmClaw-native OpenClaw gateway, skill, and agent-file guidance without naming unrelated workflows.
153
169
 
154
170
  ## v1.9.30 Highlights
155
171
 
156
- PR integration release for dream-model routing, email bridge TLS opt-outs, installed CLI runtime resolution, and an OpenClaw plugin workflow example.
172
+ PR integration release for dream-model routing, email bridge TLS opt-outs, and installed CLI runtime resolution.
157
173
 
158
174
  - **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
159
175
  - **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
160
176
  - **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
161
- - **OpenClaw plugin workflow.** README guidance now includes a concrete TweetClaw plugin workflow for OpenClaw operators.
162
177
 
163
178
  ## v1.9.29 Highlights
164
179
 
@@ -228,7 +243,7 @@ SwarmClaw is built for OpenClaw operators who need more than one agent or one ga
228
243
  - Deploy official-image OpenClaw runtimes locally, via VPS bundles, or over SSH.
229
244
  - Edit OpenClaw agent files such as `SOUL.md`, `IDENTITY.md`, `USER.md`, `TOOLS.md`, and `AGENTS.md`.
230
245
  - Import OpenClaw `SKILL.md` files and use them in SwarmClaw's runtime skill system.
231
- - Use OpenClaw plugins for domain workflows. For example, `openclaw plugins install @xquik/tweetclaw` installs [TweetClaw](https://github.com/Xquik-dev/tweetclaw) via [ClawHub](https://clawhub.ai/kriptoburak/xquik-tweetclaw) for X/Twitter search, follower export, monitors, webhooks, and approval-gated post/reply actions.
246
+ - Use OpenClaw plugins and skills through the configured gateway workflow without leaving the SwarmClaw control plane.
232
247
 
233
248
  ## Use Cases
234
249
 
@@ -397,7 +412,7 @@ SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network
397
412
  - **Per-agent opt-in**: enable SwarmFeed on any agent with automatic Ed25519 registration
398
413
  - **Richer in-app surface**: feed tabs for For You, Following, Trending, Bookmarks, and Notifications, plus thread detail, profile sheets, suggested follows, and search
399
414
  - **Heartbeat integration**: agents can auto-post, auto-reply to mentions, auto-follow with guardrails, and publish task-completion updates during heartbeat cycles
400
- - **Multiple access methods**: [SDK](https://www.npmjs.com/package/@swarmfeed/sdk), [CLI](https://www.npmjs.com/package/@swarmfeed/cli), [MCP Server](https://www.npmjs.com/package/@swarmfeed/mcp-server), and [ClawHub skill](https://clawhub.ai/skills/swarmfeed)
415
+ - **Multiple access methods**: [SDK](https://www.npmjs.com/package/@swarmfeed/sdk), [CLI](https://www.npmjs.com/package/@swarmfeed/cli), [MCP Server](https://www.npmjs.com/package/@swarmfeed/mcp-server), and [ClawHub skill](https://clawhub.ai/waydelyle/swarmfeed)
401
416
 
402
417
  Read the docs at [swarmclaw.ai/docs/swarmfeed](https://swarmclaw.ai/docs/swarmfeed) and visit [swarmfeed.ai](https://swarmfeed.ai) for the platform itself.
403
418
 
@@ -420,14 +435,29 @@ Operational docs: https://swarmclaw.ai/docs/observability
420
435
 
421
436
  ## Releases
422
437
 
438
+ ### v1.9.32 Highlights
439
+
440
+ PR integration release for background model routing, reflection memory controls, and current ClawHub install guidance.
441
+
442
+ - **Background model routing.** Per-agent `dreamConfig` overrides can route dream cycles and daily digests before global dream settings, while `compactionProvider` settings can route live auto-compaction summaries through a cheaper or faster model.
443
+ - **Reflection memory controls.** `reflectionMinQuality` gates automatic reflection memory writes without dropping the reflection record, and optional embedding dedup skips near-duplicate reflection notes when embeddings are configured.
444
+ - **ClawHub install guidance.** OpenClaw skill docs now use `openclaw skills install swarmclaw` and current owner-scoped ClawHub links.
445
+ - **Regression coverage.** Added tests for dream override precedence, compaction preference resolution, reflection quality gating, and embedding-based reflection dedup.
446
+
447
+ ### v1.9.31 Highlights
448
+
449
+ Documentation cleanup release for public release notes and OpenClaw guidance. No runtime behavior changed.
450
+
451
+ - **Public docs cleanup.** Removed an unwanted third-party example from the README and site release notes.
452
+ - **OpenClaw guidance preserved.** The README keeps the SwarmClaw-native OpenClaw gateway, skill, and agent-file guidance without naming unrelated workflows.
453
+
423
454
  ### v1.9.30 Highlights
424
455
 
425
- PR integration release for dream-model routing, email bridge TLS opt-outs, installed CLI runtime resolution, and an OpenClaw plugin workflow example.
456
+ PR integration release for dream-model routing, email bridge TLS opt-outs, and installed CLI runtime resolution.
426
457
 
427
458
  - **Dream model routing.** Memory dream cycles and daily digests can use optional `dreamProvider` settings so background consolidation can run on a smaller local model.
428
459
  - **Email bridge TLS opt-outs.** `tlsRejectUnauthorized=false` now disables hostname checks too, matching the explicit self-signed-server opt-out.
429
460
  - **Installed CLI stability.** Legacy API-backed CLI commands import the package-local `tsx` runtime instead of resolving `tsx` from the caller's project.
430
- - **OpenClaw plugin workflow.** README guidance now includes a concrete TweetClaw plugin workflow for OpenClaw operators.
431
461
 
432
462
  ### v1.9.29 Highlights
433
463
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.30",
3
+ "version": "1.9.32",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -88,7 +88,7 @@
88
88
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/electron-signing-config.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
89
89
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
90
90
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
91
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/memory/dream-service.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/autonomy/supervisor-settings.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/autonomy/supervisor-reflection.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/provider-diagnostics.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/memory/dream-service.test.ts src/lib/server/memory/memory-consolidation.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/compaction-generation-preference.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/connectors/slack.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/server/schedules/schedule-timing.test.ts src/lib/server/schedules/schedule-preview.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/server/session-tools/web-crawl.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/gateways/control-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/preview/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
92
92
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
93
93
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
94
94
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -0,0 +1,26 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { normalizeSupervisorSettings } from './supervisor-settings'
4
+
5
+ describe('normalizeSupervisorSettings', () => {
6
+ it('preserves and clamps reflection memory quality settings', () => {
7
+ assert.deepEqual(
8
+ {
9
+ minQuality: normalizeSupervisorSettings({ reflectionMinQuality: '0.72' }).reflectionMinQuality,
10
+ minQualityHigh: normalizeSupervisorSettings({ reflectionMinQuality: 2 }).reflectionMinQuality,
11
+ minQualityLow: normalizeSupervisorSettings({ reflectionMinQuality: -1 }).reflectionMinQuality,
12
+ semanticEnabled: normalizeSupervisorSettings({ reflectionSemanticDedupEnabled: 'on' }).reflectionSemanticDedupEnabled,
13
+ semanticThreshold: normalizeSupervisorSettings({ reflectionSemanticDedupThreshold: '0.91' }).reflectionSemanticDedupThreshold,
14
+ semanticThresholdHigh: normalizeSupervisorSettings({ reflectionSemanticDedupThreshold: 1.5 }).reflectionSemanticDedupThreshold,
15
+ },
16
+ {
17
+ minQuality: 0.72,
18
+ minQualityHigh: 1,
19
+ minQualityLow: 0,
20
+ semanticEnabled: true,
21
+ semanticThreshold: 0.91,
22
+ semanticThresholdHigh: 1,
23
+ },
24
+ )
25
+ })
26
+ })
@@ -8,6 +8,9 @@ export const DEFAULT_SUPERVISOR_NO_PROGRESS_LIMIT = 2
8
8
  export const DEFAULT_SUPERVISOR_REPEATED_TOOL_LIMIT = 3
9
9
  export const DEFAULT_REFLECTION_ENABLED = true
10
10
  export const DEFAULT_REFLECTION_AUTO_WRITE_MEMORY = true
11
+ export const DEFAULT_REFLECTION_MIN_QUALITY = 0
12
+ export const DEFAULT_REFLECTION_SEMANTIC_DEDUP_ENABLED = false
13
+ export const DEFAULT_REFLECTION_SEMANTIC_DEDUP_THRESHOLD = 0.88
11
14
 
12
15
  export const SUPERVISOR_NO_PROGRESS_LIMIT_MIN = 1
13
16
  export const SUPERVISOR_NO_PROGRESS_LIMIT_MAX = 8
@@ -24,6 +27,16 @@ function parseIntSetting(value: unknown, fallback: number, min: number, max: num
24
27
  return Math.max(min, Math.min(max, Math.trunc(parsed)))
25
28
  }
26
29
 
30
+ function parseNumberSetting(value: unknown, fallback: number, min: number, max: number): number {
31
+ const parsed = typeof value === 'number'
32
+ ? value
33
+ : typeof value === 'string'
34
+ ? Number.parseFloat(value)
35
+ : Number.NaN
36
+ if (!Number.isFinite(parsed)) return fallback
37
+ return Math.max(min, Math.min(max, parsed))
38
+ }
39
+
27
40
  function parseBoolSetting(value: unknown, fallback: boolean): boolean {
28
41
  if (typeof value === 'boolean') return value
29
42
  if (typeof value === 'string') {
@@ -41,6 +54,9 @@ export interface NormalizedSupervisorSettings {
41
54
  supervisorRepeatedToolLimit: number
42
55
  reflectionEnabled: boolean
43
56
  reflectionAutoWriteMemory: boolean
57
+ reflectionMinQuality: number
58
+ reflectionSemanticDedupEnabled: boolean
59
+ reflectionSemanticDedupThreshold: number
44
60
  }
45
61
 
46
62
  export function normalizeSupervisorSettings(
@@ -69,6 +85,17 @@ export function normalizeSupervisorSettings(
69
85
  ),
70
86
  reflectionEnabled: parseBoolSetting(current.reflectionEnabled, DEFAULT_REFLECTION_ENABLED),
71
87
  reflectionAutoWriteMemory: parseBoolSetting(current.reflectionAutoWriteMemory, DEFAULT_REFLECTION_AUTO_WRITE_MEMORY),
88
+ reflectionMinQuality: parseNumberSetting(current.reflectionMinQuality, DEFAULT_REFLECTION_MIN_QUALITY, 0, 1),
89
+ reflectionSemanticDedupEnabled: parseBoolSetting(
90
+ current.reflectionSemanticDedupEnabled,
91
+ DEFAULT_REFLECTION_SEMANTIC_DEDUP_ENABLED,
92
+ ),
93
+ reflectionSemanticDedupThreshold: parseNumberSetting(
94
+ current.reflectionSemanticDedupThreshold,
95
+ DEFAULT_REFLECTION_SEMANTIC_DEDUP_THRESHOLD,
96
+ 0,
97
+ 1,
98
+ ),
72
99
  }
73
100
  }
74
101
 
@@ -202,6 +202,212 @@ describe('supervisor-reflection', () => {
202
202
  ])
203
203
  })
204
204
 
205
+ it('persists low-quality reflections while skipping auto-written memory', () => {
206
+ const output = runWithTempDataDir(`
207
+ const storageMod = await import('@/lib/server/storage')
208
+ const storage = storageMod.default || storageMod['module.exports'] || storageMod
209
+ const reflectionMod = await import('@/lib/server/autonomy/supervisor-reflection')
210
+ const mod = reflectionMod.default || reflectionMod['module.exports'] || reflectionMod
211
+ const memoryDbMod = await import('@/lib/server/memory/memory-db')
212
+ const memoryMod = memoryDbMod.default || memoryDbMod['module.exports'] || memoryDbMod
213
+
214
+ storage.saveAgents({
215
+ 'agent-a': {
216
+ id: 'agent-a',
217
+ name: 'Agent A',
218
+ provider: 'openai',
219
+ model: 'gpt-test',
220
+ },
221
+ })
222
+
223
+ storage.saveSessions({
224
+ s1: {
225
+ id: 's1',
226
+ name: 'Autonomy Session',
227
+ cwd: process.cwd(),
228
+ user: 'tester',
229
+ provider: 'openai',
230
+ model: 'gpt-test',
231
+ claudeSessionId: null,
232
+ messages: [
233
+ { role: 'user', text: 'Repair the deployment workflow and keep notes for later.', time: 1 },
234
+ { role: 'assistant', text: 'I retried the same shell path and nothing changed.', time: 2 },
235
+ ],
236
+ createdAt: 1,
237
+ lastActiveAt: 2,
238
+ sessionType: 'human',
239
+ agentId: 'agent-a',
240
+ },
241
+ })
242
+
243
+ storage.saveSettings({
244
+ supervisorEnabled: true,
245
+ supervisorRuntimeScope: 'both',
246
+ supervisorNoProgressLimit: 2,
247
+ supervisorRepeatedToolLimit: 3,
248
+ reflectionEnabled: true,
249
+ reflectionAutoWriteMemory: true,
250
+ reflectionMinQuality: 0.8,
251
+ })
252
+
253
+ const result = await mod.observeAutonomyRunOutcome({
254
+ runId: 'run-low-quality',
255
+ sessionId: 's1',
256
+ agentId: 'agent-a',
257
+ source: 'chat',
258
+ status: 'completed',
259
+ resultText: 'I retried the same shell path and nothing changed.',
260
+ toolEvents: [
261
+ { name: 'shell', input: '{"cmd":"npm test"}' },
262
+ { name: 'shell', input: '{"cmd":"npm test"}' },
263
+ { name: 'shell', input: '{"cmd":"npm test"}' },
264
+ ],
265
+ mainLoopState: {
266
+ followupChainCount: 2,
267
+ summary: 'I retried the same shell path and nothing changed.',
268
+ },
269
+ sourceMessage: 'Repair the deployment workflow and keep notes for later.',
270
+ }, {
271
+ generateText: async () => JSON.stringify({
272
+ summary: 'Low quality reflection',
273
+ lessons: ['This weak note should not be written to memory.'],
274
+ quality_score: 0.25,
275
+ quality_reasoning: 'Too generic to keep as durable memory.',
276
+ }),
277
+ })
278
+
279
+ const memories = memoryMod.getMemoryDb().list(undefined, 50)
280
+ .filter((entry) => entry.metadata && entry.metadata.origin === 'autonomy-reflection')
281
+
282
+ console.log(JSON.stringify({
283
+ reflectionSummary: result.reflection?.summary ?? null,
284
+ qualityScore: result.reflection?.qualityScore ?? null,
285
+ autoMemoryCount: result.reflection?.autoMemoryIds?.length ?? 0,
286
+ storedReflectionMemoryCount: memories.length,
287
+ }))
288
+ `)
289
+
290
+ assert.equal(output.reflectionSummary, 'Low quality reflection')
291
+ assert.equal(output.qualityScore, 0.25)
292
+ assert.equal(output.autoMemoryCount, 0)
293
+ assert.equal(output.storedReflectionMemoryCount, 0)
294
+ })
295
+
296
+ it('skips semantically duplicate reflection memory when embedding dedup is enabled', () => {
297
+ const output = runWithTempDataDir(`
298
+ const http = await import('node:http')
299
+ const path = await import('node:path')
300
+ const Database = (await import('better-sqlite3')).default
301
+ const storageMod = await import('@/lib/server/storage')
302
+ const storage = storageMod.default || storageMod['module.exports'] || storageMod
303
+ const reflectionMod = await import('@/lib/server/autonomy/supervisor-reflection')
304
+ const mod = reflectionMod.default || reflectionMod['module.exports'] || reflectionMod
305
+ const memoryDbMod = await import('@/lib/server/memory/memory-db')
306
+ const memoryMod = memoryDbMod.default || memoryDbMod['module.exports'] || memoryDbMod
307
+ const settingsRepositoryMod = await import('@/lib/server/settings/settings-repository')
308
+ const settingsRepository = settingsRepositoryMod.default || settingsRepositoryMod['module.exports'] || settingsRepositoryMod
309
+
310
+ const server = http.createServer((req, res) => {
311
+ res.setHeader('content-type', 'application/json')
312
+ res.end(JSON.stringify({ embedding: [1, 0] }))
313
+ })
314
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
315
+ const endpoint = 'http://127.0.0.1:' + server.address().port
316
+
317
+ try {
318
+ storage.saveAgents({
319
+ 'agent-a': {
320
+ id: 'agent-a',
321
+ name: 'Agent A',
322
+ provider: 'openai',
323
+ model: 'gpt-test',
324
+ },
325
+ })
326
+
327
+ storage.saveSessions({
328
+ s1: {
329
+ id: 's1',
330
+ name: 'Semantic Dedup Session',
331
+ cwd: process.cwd(),
332
+ user: 'tester',
333
+ provider: 'openai',
334
+ model: 'gpt-test',
335
+ claudeSessionId: null,
336
+ messages: [
337
+ { role: 'user', text: 'Release carefully and keep the durable lesson.', time: 1 },
338
+ { role: 'assistant', text: 'I checked the release gates before shipping.', time: 2 },
339
+ ],
340
+ createdAt: 1,
341
+ lastActiveAt: 2,
342
+ sessionType: 'human',
343
+ agentId: 'agent-a',
344
+ },
345
+ })
346
+
347
+ const memDb = memoryMod.getMemoryDb()
348
+ const existing = memDb.add({
349
+ agentId: 'agent-a',
350
+ sessionId: 's1',
351
+ category: 'reflection/lesson',
352
+ title: 'Reflection Lesson',
353
+ content: 'Always verify release gates before shipping.',
354
+ metadata: { origin: 'autonomy-reflection' },
355
+ })
356
+ const rawDb = new Database(path.join(process.env.DATA_DIR, 'memory.db'))
357
+ rawDb.prepare('UPDATE memories SET embedding = ? WHERE id = ?').run(Buffer.from(new Float32Array([1, 0]).buffer), existing.id)
358
+ rawDb.close()
359
+
360
+ settingsRepository.saveSettings({
361
+ supervisorEnabled: true,
362
+ supervisorRuntimeScope: 'both',
363
+ supervisorNoProgressLimit: 2,
364
+ supervisorRepeatedToolLimit: 3,
365
+ reflectionEnabled: true,
366
+ reflectionAutoWriteMemory: true,
367
+ reflectionSemanticDedupEnabled: true,
368
+ reflectionSemanticDedupThreshold: 0.9,
369
+ embeddingProvider: 'ollama',
370
+ embeddingModel: 'test-embedding',
371
+ embeddingEndpoint: endpoint,
372
+ })
373
+
374
+ const result = await mod.observeAutonomyRunOutcome({
375
+ runId: 'run-semantic-dedup',
376
+ sessionId: 's1',
377
+ agentId: 'agent-a',
378
+ source: 'chat',
379
+ status: 'completed',
380
+ resultText: 'I checked the release gates before shipping.',
381
+ toolEvents: [
382
+ { name: 'shell', input: '{"cmd":"npm test"}' },
383
+ ],
384
+ sourceMessage: 'Release carefully and keep the durable lesson.',
385
+ }, {
386
+ generateText: async () => JSON.stringify({
387
+ summary: 'Release gate reflection',
388
+ lessons: ['Confirm release gates before shipping.'],
389
+ quality_score: 0.95,
390
+ }),
391
+ })
392
+
393
+ const reflectionMemories = memDb.list('agent-a', 50)
394
+ .filter((entry) => entry.metadata && entry.metadata.origin === 'autonomy-reflection')
395
+
396
+ console.log(JSON.stringify({
397
+ reflectionSummary: result.reflection?.summary ?? null,
398
+ autoMemoryCount: result.reflection?.autoMemoryIds?.length ?? 0,
399
+ reflectionMemoryContents: reflectionMemories.map((entry) => entry.content).sort(),
400
+ }))
401
+ } finally {
402
+ await new Promise((resolve) => server.close(resolve))
403
+ }
404
+ `)
405
+
406
+ assert.equal(output.reflectionSummary, 'Release gate reflection')
407
+ assert.equal(output.autoMemoryCount, 0)
408
+ assert.deepEqual(output.reflectionMemoryContents, ['Always verify release gates before shipping.'])
409
+ })
410
+
205
411
  it('reflects short human chats when they contain durable personal context', () => {
206
412
  const output = runWithTempDataDir(`
207
413
  const storageMod = await import('@/lib/server/storage')
@@ -744,7 +744,7 @@ function inferFollowUpAt(note: string, createdAt: number): number {
744
744
  return createdAt + 7 * 24 * 3600_000
745
745
  }
746
746
 
747
- function writeReflectionMemories(params: {
747
+ async function writeReflectionMemories(params: {
748
748
  reflectionId: string
749
749
  runId: string
750
750
  sessionId: string
@@ -761,7 +761,7 @@ function writeReflectionMemories(params: {
761
761
  profile: string[]
762
762
  boundaries: string[]
763
763
  openLoops: string[]
764
- }): string[] {
764
+ }): Promise<string[]> {
765
765
  const memoryDb = getMemoryDb()
766
766
  const memoryIds: string[] = []
767
767
  const incidentIds = params.incidents.map((incident) => incident.id)
@@ -809,11 +809,51 @@ function writeReflectionMemories(params: {
809
809
  // dedup only rather than blocking the reflection write.
810
810
  }
811
811
 
812
+ // Semantic dedup (opt-in): on top of the text-equality cross-run dedup
813
+ // above, compare each candidate note's embedding against recent reflection
814
+ // memories' embeddings. Catches near-duplicates the LLM re-derives in
815
+ // different words ("Always verify before acting" / "Confirm state first").
816
+ // Falls back gracefully when embeddings aren't configured.
817
+ let reflectionSettings: NormalizedSupervisorSettings | null = null
818
+ try { reflectionSettings = normalizeSupervisorSettings(loadSettings()) } catch { reflectionSettings = null }
819
+ const semanticDedupEnabled = reflectionSettings?.reflectionSemanticDedupEnabled === true
820
+ const semanticDedupThreshold = reflectionSettings?.reflectionSemanticDedupThreshold ?? 0.88
821
+ const semanticSkip = new Set<string>()
822
+ if (semanticDedupEnabled && params.agentId) {
823
+ try {
824
+ const recentEmb = memoryDb.recentReflectionEmbeddings(params.agentId, crossRunDedupCutoff, 500)
825
+ .filter((r) => Array.isArray(r.embedding) && r.embedding.length > 0) as Array<{ id: string; content: string; embedding: number[] }>
826
+ if (recentEmb.length > 0) {
827
+ const { getEmbedding, cosineSimilarity } = await import('@/lib/server/embeddings')
828
+ for (const group of groups) {
829
+ for (const note of group.notes) {
830
+ const trimmed = (note || '').trim()
831
+ if (!trimmed) continue
832
+ const norm = normalizeNote(trimmed)
833
+ if (!norm || seenNormalized.has(norm) || semanticSkip.has(norm)) continue
834
+ const emb = await getEmbedding(trimmed)
835
+ if (!emb) continue
836
+ for (const r of recentEmb) {
837
+ if (cosineSimilarity(emb, r.embedding) >= semanticDedupThreshold) {
838
+ semanticSkip.add(norm)
839
+ break
840
+ }
841
+ }
842
+ }
843
+ }
844
+ }
845
+ } catch {
846
+ // Best-effort: any failure (embedder offline, DB blip) falls through to
847
+ // the existing text-equality dedup. Never block the write.
848
+ }
849
+ }
850
+
812
851
  for (const group of groups) {
813
852
  for (const note of group.notes) {
814
853
  const norm = normalizeNote(note)
815
854
  if (!norm) continue
816
855
  if (seenNormalized.has(norm)) continue
856
+ if (semanticSkip.has(norm)) continue
817
857
  seenNormalized.add(norm)
818
858
  const metadata: Record<string, unknown> = {
819
859
  origin: 'autonomy-reflection',
@@ -1085,8 +1125,18 @@ export async function observeAutonomyRunOutcome(
1085
1125
  if (parsed.skip) return { incidents, reflection: null }
1086
1126
 
1087
1127
  const reflectionId = genId()
1088
- const autoMemoryIds = settings.reflectionAutoWriteMemory
1089
- ? writeReflectionMemories({
1128
+ const minQuality = typeof settings.reflectionMinQuality === 'number' ? settings.reflectionMinQuality : 0
1129
+ const qualityScore = parsed.qualityScore
1130
+ const qualityGateOpen = minQuality <= 0
1131
+ || qualityScore == null
1132
+ || qualityScore >= minQuality
1133
+ if (!qualityGateOpen) {
1134
+ log.info(TAG,
1135
+ `Reflection ${reflectionId} below quality gate (score=${qualityScore?.toFixed(2) ?? 'null'}, threshold=${minQuality.toFixed(2)}); skipping memory writes`,
1136
+ )
1137
+ }
1138
+ const autoMemoryIds = settings.reflectionAutoWriteMemory && qualityGateOpen
1139
+ ? await writeReflectionMemories({
1090
1140
  reflectionId,
1091
1141
  runId: input.runId,
1092
1142
  sessionId: input.sessionId,
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { resolveCompactionGenerationPreference } from './compaction-generation-preference'
4
+
5
+ describe('resolveCompactionGenerationPreference', () => {
6
+ it('returns no preference when no compaction provider is configured', () => {
7
+ assert.equal(resolveCompactionGenerationPreference({}), undefined)
8
+ assert.equal(resolveCompactionGenerationPreference({ compactionProvider: ' ' }), undefined)
9
+ })
10
+
11
+ it('builds a trimmed compaction model preference from app settings', () => {
12
+ assert.deepEqual(resolveCompactionGenerationPreference({
13
+ compactionProvider: ' ollama ',
14
+ compactionModel: ' llama3.2:3b ',
15
+ compactionCredentialId: ' cred-1 ',
16
+ compactionEndpoint: ' http://localhost:11434 ',
17
+ }), {
18
+ provider: 'ollama',
19
+ model: 'llama3.2:3b',
20
+ credentialId: 'cred-1',
21
+ apiEndpoint: 'http://localhost:11434',
22
+ })
23
+ })
24
+ })
@@ -0,0 +1,25 @@
1
+ import type { GenerationModelPreference } from '@/lib/server/build-llm'
2
+ import type { AppSettings } from '@/types'
3
+
4
+ type CompactionGenerationSettings = Pick<AppSettings, 'compactionProvider' | 'compactionModel' | 'compactionCredentialId' | 'compactionEndpoint'> | Record<string, unknown> | null | undefined
5
+
6
+ function optionalSettingString(value: unknown): string | undefined {
7
+ const normalized = typeof value === 'string' ? value.trim() : ''
8
+ return normalized || undefined
9
+ }
10
+
11
+ /** Mirrors resolveDreamGenerationPreference — returns a model preference for
12
+ * the auto-compaction summarizer if app settings opt into a routing override,
13
+ * otherwise undefined (caller falls back to the session's primary model). */
14
+ export function resolveCompactionGenerationPreference(settings: CompactionGenerationSettings): GenerationModelPreference | undefined {
15
+ const record = (settings || {}) as Record<string, unknown>
16
+ const provider = optionalSettingString(record.compactionProvider)
17
+ if (!provider) return undefined
18
+
19
+ return {
20
+ provider,
21
+ model: optionalSettingString(record.compactionModel),
22
+ credentialId: optionalSettingString(record.compactionCredentialId),
23
+ apiEndpoint: optionalSettingString(record.compactionEndpoint),
24
+ }
25
+ }
@@ -682,8 +682,29 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
682
682
  reserveTokens,
683
683
  includeToolEvents: false,
684
684
  })) {
685
+ // Resolve compaction model: if app settings opt into an override, build
686
+ // a separate LLM for the summarizer (cheap/local model); otherwise reuse
687
+ // the session's primary llm. Mirrors the dream-model override path.
688
+ const { resolveCompactionGenerationPreference } = await import('@/lib/server/chat-execution/compaction-generation-preference')
689
+ const { buildLLM } = await import('@/lib/server/build-llm')
690
+ // loadSettings is imported at the top of this file.
691
+ const settings = loadSettings()
692
+ const compactionPref = resolveCompactionGenerationPreference(settings)
693
+ let summarizerLlm = llm
694
+ let summarizerProvider = session.provider
695
+ let summarizerModel = session.model
696
+ if (compactionPref) {
697
+ try {
698
+ const built = await buildLLM({ preferred: compactionPref, sessionId: session.id, agentId: session.agentId || null })
699
+ summarizerLlm = built.llm
700
+ summarizerProvider = built.provider
701
+ summarizerModel = built.model
702
+ } catch (overrideErr) {
703
+ log.warn(TAG, `Compaction override LLM build failed for ${session.id}; falling back to session model:`, overrideErr)
704
+ }
705
+ }
685
706
  const summarize = async (prompt: string): Promise<string> => {
686
- const response = await llm.invoke([new HumanMessage(prompt)])
707
+ const response = await summarizerLlm.invoke([new HumanMessage(prompt)])
687
708
  if (typeof response.content === 'string') return response.content
688
709
  if (Array.isArray(response.content)) {
689
710
  return response.content
@@ -694,8 +715,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
694
715
  }
695
716
  const result = await llmCompact({
696
717
  messages: recentHistory,
697
- provider: session.provider,
698
- model: session.model,
718
+ provider: summarizerProvider,
719
+ model: summarizerModel,
699
720
  agentId: session.agentId || null,
700
721
  sessionId: session.id,
701
722
  summarize,
@@ -704,6 +725,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
704
725
  log.info(TAG,
705
726
  `Auto-compacted ${session.id}: ${recentHistory.length} → ${effectiveHistory.length} msgs` +
706
727
  ` (prompt history ${promptHistoryTokens} tokens)` +
728
+ (compactionPref ? ` (override ${summarizerProvider}/${summarizerModel})` : '') +
707
729
  (result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
708
730
  )
709
731
  }
@@ -1,14 +1,42 @@
1
1
  import type { GenerationModelPreference } from '@/lib/server/build-llm'
2
- import type { AppSettings } from '@/types'
2
+ import type { AppSettings, DreamConfig } from '@/types'
3
3
 
4
4
  type DreamGenerationSettings = Pick<AppSettings, 'dreamProvider' | 'dreamModel' | 'dreamCredentialId' | 'dreamEndpoint'> | Record<string, unknown> | null | undefined
5
5
 
6
+ type DreamConfigOverride = Pick<DreamConfig, 'provider' | 'model' | 'credentialId' | 'endpoint'> | Partial<DreamConfig> | Record<string, unknown> | null | undefined
7
+
6
8
  function optionalSettingString(value: unknown): string | undefined {
7
9
  const normalized = typeof value === 'string' ? value.trim() : ''
8
10
  return normalized || undefined
9
11
  }
10
12
 
11
- export function resolveDreamGenerationPreference(settings: DreamGenerationSettings): GenerationModelPreference | undefined {
13
+ /**
14
+ * Resolve which model to use for memory consolidation / dream cycles.
15
+ *
16
+ * Precedence:
17
+ * 1. Per-agent override (`dreamConfig.provider` on the Agent record)
18
+ * 2. Global app settings (`dreamProvider` etc.)
19
+ * 3. undefined — caller falls back to the agent's primary generation model
20
+ *
21
+ * The per-agent override lets you route different agents to different dream
22
+ * models (e.g. cheap local for most, but a stronger model for an agent whose
23
+ * memory mix needs more capable structured-output generation).
24
+ */
25
+ export function resolveDreamGenerationPreference(
26
+ settings: DreamGenerationSettings,
27
+ override?: DreamConfigOverride,
28
+ ): GenerationModelPreference | undefined {
29
+ const overrideRecord = (override || {}) as Record<string, unknown>
30
+ const overrideProvider = optionalSettingString(overrideRecord.provider)
31
+ if (overrideProvider) {
32
+ return {
33
+ provider: overrideProvider,
34
+ model: optionalSettingString(overrideRecord.model),
35
+ credentialId: optionalSettingString(overrideRecord.credentialId),
36
+ apiEndpoint: optionalSettingString(overrideRecord.endpoint),
37
+ }
38
+ }
39
+
12
40
  const record = (settings || {}) as Record<string, unknown>
13
41
  const provider = optionalSettingString(record.dreamProvider)
14
42
  if (!provider) return undefined
@@ -22,6 +22,25 @@ describe('resolveDreamGenerationPreference', () => {
22
22
  apiEndpoint: 'http://localhost:11434',
23
23
  })
24
24
  })
25
+
26
+ it('uses a per-agent dream override before global app settings', () => {
27
+ assert.deepEqual(resolveDreamGenerationPreference({
28
+ dreamProvider: 'openai',
29
+ dreamModel: 'gpt-5-mini',
30
+ dreamCredentialId: 'global-cred',
31
+ dreamEndpoint: 'https://global.example/v1',
32
+ }, {
33
+ provider: ' ollama ',
34
+ model: ' qwen3:8b ',
35
+ credentialId: ' agent-cred ',
36
+ endpoint: ' http://127.0.0.1:11434 ',
37
+ }), {
38
+ provider: 'ollama',
39
+ model: 'qwen3:8b',
40
+ credentialId: 'agent-cred',
41
+ apiEndpoint: 'http://127.0.0.1:11434',
42
+ })
43
+ })
25
44
  })
26
45
 
27
46
  describe('parseTier2DreamResponseText', () => {
@@ -216,7 +216,9 @@ ${memoryLines.join('\n')}`
216
216
  try {
217
217
  const { buildLLM } = await import('@/lib/server/build-llm')
218
218
  const { loadSettings } = await import('@/lib/server/settings/settings-repository')
219
- const preferred = resolveDreamGenerationPreference(loadSettings())
219
+ // `config` is the resolved per-agent dream config (defaults + overrides);
220
+ // pass it so a per-agent provider/model takes precedence over global settings.
221
+ const preferred = resolveDreamGenerationPreference(loadSettings(), config)
220
222
  const { llm } = await buildLLM({ agentId, preferred, responseFormat: 'json_object' })
221
223
  const { HumanMessage } = await import('@langchain/core/messages')
222
224
 
@@ -124,3 +124,34 @@ test('canCreateDailyDigestForAgent allows CLI-only agents when a dream model is
124
124
  true,
125
125
  )
126
126
  })
127
+
128
+ test('canCreateDailyDigestForAgent allows CLI-only agents with a per-agent dream model override', async () => {
129
+ const now = Date.now()
130
+ const agentId = 'agent-dream-override-cli'
131
+ storage.saveSettings({})
132
+ storage.saveAgents({
133
+ [agentId]: {
134
+ id: agentId,
135
+ name: 'Per-Agent Dream Routed CLI Agent',
136
+ description: '',
137
+ systemPrompt: '',
138
+ provider: 'claude-cli',
139
+ model: 'claude-sonnet-4-5',
140
+ credentialId: null,
141
+ fallbackCredentialIds: [],
142
+ apiEndpoint: null,
143
+ dreamConfig: {
144
+ provider: 'ollama',
145
+ model: 'llama3.2',
146
+ endpoint: 'http://127.0.0.1:11434',
147
+ },
148
+ createdAt: now,
149
+ updatedAt: now,
150
+ } as Agent,
151
+ })
152
+
153
+ assert.equal(
154
+ consolidation.canCreateDailyDigestForAgent(agentId, storage.loadAgents({ includeTrashed: true }), storage.loadSettings()),
155
+ true,
156
+ )
157
+ })
@@ -48,7 +48,7 @@ export function canCreateDailyDigestForAgent(
48
48
  try {
49
49
  resolveGenerationModelConfig({
50
50
  agentId,
51
- preferred: resolveDreamGenerationPreference(settings),
51
+ preferred: resolveDreamGenerationPreference(settings, agent.dreamConfig),
52
52
  })
53
53
  return true
54
54
  } catch (err: unknown) {
@@ -118,10 +118,11 @@ export async function runDailyConsolidation(): Promise<{
118
118
  ].join('\n')
119
119
 
120
120
  // Use an optional dream-model override before the target agent's generation provider.
121
+ // Precedence: per-agent dreamConfig override → global dream* settings → agent's primary.
121
122
  const { buildLLM } = await import('@/lib/server/build-llm')
122
123
  const { llm } = await buildLLM({
123
124
  agentId,
124
- preferred: resolveDreamGenerationPreference(settings),
125
+ preferred: resolveDreamGenerationPreference(settings, agents[agentId]?.dreamConfig),
125
126
  })
126
127
 
127
128
  const response = await llm.invoke([new HumanMessage(prompt)])
@@ -1270,6 +1270,30 @@ function initDb() {
1270
1270
  return (stmts.listByAgent.all(agentId, safeLimit) as any[]).map(rowToEntry)
1271
1271
  },
1272
1272
 
1273
+ /** Return recent reflection/* memories with their embeddings deserialized
1274
+ * for semantic dedup. Memories without an embedding (older rows, or
1275
+ * embedding still being computed in background) are included with a
1276
+ * null embedding so callers can fall back to text dedup. */
1277
+ recentReflectionEmbeddings(
1278
+ agentId: string,
1279
+ sinceMs: number,
1280
+ limit = 200,
1281
+ ): Array<{ id: string; content: string; embedding: number[] | null }> {
1282
+ const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
1283
+ const rows = db.prepare(
1284
+ `SELECT id, content, embedding FROM memories
1285
+ WHERE (agentId = ? OR sharedWith LIKE ?)
1286
+ AND category LIKE 'reflection/%'
1287
+ AND updatedAt >= ?
1288
+ ORDER BY updatedAt DESC LIMIT ?`,
1289
+ ).all(agentId, `%"${agentId}"%`, sinceMs, safeLimit) as Array<{ id: string; content: string; embedding: Buffer | null }>
1290
+ return rows.map((row) => ({
1291
+ id: row.id,
1292
+ content: row.content || '',
1293
+ embedding: row.embedding ? deserializeEmbedding(row.embedding) : null,
1294
+ }))
1295
+ },
1296
+
1273
1297
  getFrequentlyAccessedByAgent(agentId: string, minAccessCount = 3, sinceDays = 7): MemoryEntry[] {
1274
1298
  const cutoff = Date.now() - sinceDays * 86_400_000
1275
1299
  const rows = stmts.frequentlyAccessedByAgent.all(agentId, minAccessCount, cutoff) as Record<string, unknown>[]
@@ -32,6 +32,14 @@ export interface AppSettings {
32
32
  dreamModel?: string | null
33
33
  dreamCredentialId?: string | null
34
34
  dreamEndpoint?: string | null
35
+ // Optional model override for auto-compaction (live-loop conversation
36
+ // summarization triggered when context usage hits the auto-compact
37
+ // threshold). Lets the user route the summarizer to a cheaper or faster
38
+ // model than the agent's primary generation model.
39
+ compactionProvider?: string | null
40
+ compactionModel?: string | null
41
+ compactionCredentialId?: string | null
42
+ compactionEndpoint?: string | null
35
43
  loopMode?: LoopMode
36
44
  agentLoopRecursionLimit?: number
37
45
  delegationMaxDepth?: number
@@ -103,6 +111,12 @@ export interface AppSettings {
103
111
  autonomyResumeApprovalsEnabled?: boolean
104
112
  reflectionEnabled?: boolean
105
113
  reflectionAutoWriteMemory?: boolean
114
+ /** Minimum reflection quality score (0-1) required to auto-write memories. */
115
+ reflectionMinQuality?: number
116
+ /** Enable embedding-based dedup for reflection memory writes. */
117
+ reflectionSemanticDedupEnabled?: boolean
118
+ /** Cosine threshold above which a reflection note is considered duplicate. */
119
+ reflectionSemanticDedupThreshold?: number
106
120
  memoryReferenceDepth?: number
107
121
  maxMemoriesPerLookup?: number
108
122
  maxLinkedMemoriesExpanded?: number
@@ -33,6 +33,14 @@ export interface DreamConfig {
33
33
  pruneThresholdDays: number
34
34
  tier2Enabled: boolean
35
35
  tier2MaxMemories: number
36
+ // Optional per-agent override for the consolidation/dream LLM. When set,
37
+ // takes precedence over the global `dream*` app settings. When unset, the
38
+ // helper falls back to global settings, then to the agent's primary
39
+ // generation model — same precedence as before.
40
+ provider?: string | null
41
+ model?: string | null
42
+ credentialId?: string | null
43
+ endpoint?: string | null
36
44
  }
37
45
 
38
46
  export const DEFAULT_DREAM_CONFIG: DreamConfig = {