@swarmclawai/swarmclaw 1.9.27 → 1.9.29
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 +25 -6
- package/bin/swarmclaw.js +18 -2
- package/package.json +3 -3
- package/src/app/api/providers/[id]/models/route.test.ts +36 -0
- package/src/cli/binary.test.js +18 -1
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/lib/server/build-llm.test.ts +41 -0
- package/src/lib/server/build-llm.ts +16 -3
- package/src/lib/server/connectors/email.test.ts +34 -1
- package/src/lib/server/connectors/email.ts +35 -1
- package/src/lib/server/memory/dream-service.test.ts +42 -0
- package/src/lib/server/memory/dream-service.ts +71 -5
- package/src/lib/server/storage.ts +13 -7
- package/electron-dist/main.js +0 -218
package/README.md
CHANGED
|
@@ -151,14 +151,14 @@ clawhub install swarmclaw
|
|
|
151
151
|
|
|
152
152
|
[Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
|
|
153
153
|
|
|
154
|
-
## v1.9.
|
|
154
|
+
## v1.9.29 Highlights
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
|
|
157
157
|
|
|
158
|
-
- **
|
|
159
|
-
- **
|
|
160
|
-
- **
|
|
161
|
-
- **Regression coverage.**
|
|
158
|
+
- **Edit Agent tooltips.** Help tips in the Edit Agent sheet now render above modal layers instead of being hidden behind the dialog.
|
|
159
|
+
- **Installed package builds.** The npm package now ships the Dagre type declarations needed by `swarmclaw server --build`.
|
|
160
|
+
- **Local Ollama dream output.** Structured dream/reflection calls request Ollama JSON mode and validate balanced JSON before writing memories.
|
|
161
|
+
- **Regression coverage.** CLI/package, model-build, and dream-parser tests cover the reported failure modes.
|
|
162
162
|
|
|
163
163
|
## Hosted Deploys
|
|
164
164
|
|
|
@@ -410,6 +410,25 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
410
410
|
|
|
411
411
|
## Releases
|
|
412
412
|
|
|
413
|
+
### v1.9.29 Highlights
|
|
414
|
+
|
|
415
|
+
Issue-fix release for Edit Agent tooltips, installed package builds, and structured dream output on local Ollama models.
|
|
416
|
+
|
|
417
|
+
- **Edit Agent tooltips.** Help tips in the Edit Agent sheet now render above modal layers instead of being hidden behind the dialog.
|
|
418
|
+
- **Installed package builds.** The npm package now ships the Dagre type declarations needed by `swarmclaw server --build`.
|
|
419
|
+
- **Local Ollama dream output.** Structured dream/reflection calls request Ollama JSON mode and validate balanced JSON before writing memories.
|
|
420
|
+
- **Regression coverage.** CLI/package, model-build, and dream-parser tests cover the reported failure modes.
|
|
421
|
+
|
|
422
|
+
### v1.9.28 Highlights
|
|
423
|
+
|
|
424
|
+
Issue-fix release for installed CLI groups, email bridge TLS handling, built-in model overrides, and Windows desktop native modules.
|
|
425
|
+
|
|
426
|
+
- **Installed CLI groups.** Global npm installs route legacy API-backed group commands through the bundled TS runtime when installed under `node_modules`, avoiding Node 22.6+/25 type-stripping failures.
|
|
427
|
+
- **Email bridge TLS resilience.** The email connector logs IMAP socket errors without crashing the daemon and supports `tlsRejectUnauthorized=false` for local self-signed IMAP/SMTP servers.
|
|
428
|
+
- **Provider model override persistence.** Built-in provider live model saves now reload array-valued overrides instead of falling back to catalog defaults.
|
|
429
|
+
- **Windows desktop native modules.** Desktop packaging syncs rebuilt Electron-native modules into traced `.next/node_modules` aliases so packaged Windows installs start against the correct ABI.
|
|
430
|
+
- **Regression coverage.** CLI, email, provider route, and Electron after-pack tests cover the reported failure modes.
|
|
431
|
+
|
|
413
432
|
### v1.9.27 Highlights
|
|
414
433
|
|
|
415
434
|
Desktop compatibility and provider-save repair for Intel Mac users and OpenRouter setup.
|
package/bin/swarmclaw.js
CHANGED
|
@@ -55,9 +55,19 @@ function hasTsxRuntime() {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function pathIsInsideNodeModules(filePath) {
|
|
59
|
+
return path.resolve(filePath).split(path.sep).includes('node_modules')
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
|
|
63
|
+
const ext = path.extname(cliPath).toLowerCase()
|
|
64
|
+
if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
|
|
65
|
+
return [cliPath, ...argv]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const insideNodeModules = options.insideNodeModules ?? pathIsInsideNodeModules(cliPath)
|
|
59
69
|
const stripTypesSupported = options.supportsStripTypes ?? supportsStripTypes()
|
|
60
|
-
if (stripTypesSupported) {
|
|
70
|
+
if (stripTypesSupported && !insideNodeModules) {
|
|
61
71
|
return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv]
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -69,6 +79,10 @@ function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
|
|
|
69
79
|
return null
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
function resolveLegacyTsCliPath() {
|
|
83
|
+
return path.join(__dirname, '..', 'src', 'cli', 'index.ts')
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
function normalizeLegacyTsCliArgv(argv) {
|
|
73
87
|
const normalized = []
|
|
74
88
|
|
|
@@ -98,7 +112,7 @@ function normalizeLegacyTsCliArgv(argv) {
|
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
function runLegacyTsCli(argv) {
|
|
101
|
-
const cliPath =
|
|
115
|
+
const cliPath = resolveLegacyTsCliPath()
|
|
102
116
|
const args = buildLegacyTsCliArgs(cliPath, normalizeLegacyTsCliArgv(argv))
|
|
103
117
|
const env = normalizeLegacyCliEnv(process.env)
|
|
104
118
|
if (!args) {
|
|
@@ -358,6 +372,8 @@ module.exports = {
|
|
|
358
372
|
buildLegacyTsCliArgs,
|
|
359
373
|
hasTsxRuntime,
|
|
360
374
|
normalizeLegacyTsCliArgv,
|
|
375
|
+
pathIsInsideNodeModules,
|
|
376
|
+
resolveLegacyTsCliPath,
|
|
361
377
|
TS_CLI_ACTIONS,
|
|
362
378
|
normalizeLegacyCliEnv,
|
|
363
379
|
printPackageVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.29",
|
|
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/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/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",
|
|
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",
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"@tailwindcss/postcss": "^4",
|
|
120
120
|
"@tanstack/react-query": "^5.91.0",
|
|
121
121
|
"@types/better-sqlite3": "^7.6.13",
|
|
122
|
+
"@types/dagre": "^0.7.54",
|
|
122
123
|
"@types/mailparser": "^3.4.6",
|
|
123
124
|
"@types/node": "^20",
|
|
124
125
|
"@types/nodemailer": "^7.0.11",
|
|
@@ -179,7 +180,6 @@
|
|
|
179
180
|
},
|
|
180
181
|
"devDependencies": {
|
|
181
182
|
"@electron/rebuild": "^3.7.2",
|
|
182
|
-
"@types/dagre": "^0.7.54",
|
|
183
183
|
"electron": "^33.3.0",
|
|
184
184
|
"electron-builder": "^25.1.8",
|
|
185
185
|
"eslint": "^9",
|
|
@@ -58,3 +58,39 @@ test('provider models route updates custom provider configs without creating mod
|
|
|
58
58
|
hasOverride: false,
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
|
+
|
|
62
|
+
test('provider model overrides preserve built-in provider array rows', () => {
|
|
63
|
+
const output = runWithTempDataDir<{
|
|
64
|
+
overrides: Record<string, string[]>
|
|
65
|
+
providerModels: string[]
|
|
66
|
+
getPayload: { models: string[]; hasOverride: boolean }
|
|
67
|
+
}>(`
|
|
68
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
69
|
+
const providerMod = await import('./src/lib/providers')
|
|
70
|
+
const routeMod = await import('./src/app/api/providers/[id]/models/route')
|
|
71
|
+
const storage = storageMod.default || storageMod
|
|
72
|
+
const providers = providerMod.default || providerMod
|
|
73
|
+
const route = routeMod.default || routeMod
|
|
74
|
+
|
|
75
|
+
storage.saveModelOverrides({ lmstudio: ['qwen3.5-27b'] })
|
|
76
|
+
|
|
77
|
+
const getResponse = await route.GET(
|
|
78
|
+
new Request('http://local/api/providers/lmstudio/models'),
|
|
79
|
+
{ params: Promise.resolve({ id: 'lmstudio' }) },
|
|
80
|
+
)
|
|
81
|
+
const provider = providers.getProviderList().find((entry) => entry.id === 'lmstudio')
|
|
82
|
+
|
|
83
|
+
console.log(JSON.stringify({
|
|
84
|
+
overrides: storage.loadModelOverrides(),
|
|
85
|
+
providerModels: provider?.models || [],
|
|
86
|
+
getPayload: await getResponse.json(),
|
|
87
|
+
}))
|
|
88
|
+
`, { prefix: 'swarmclaw-provider-model-override-test-' })
|
|
89
|
+
|
|
90
|
+
assert.deepEqual(output.overrides, { lmstudio: ['qwen3.5-27b'] })
|
|
91
|
+
assert.deepEqual(output.providerModels, ['qwen3.5-27b'])
|
|
92
|
+
assert.deepEqual(output.getPayload, {
|
|
93
|
+
models: ['qwen3.5-27b'],
|
|
94
|
+
hasOverride: true,
|
|
95
|
+
})
|
|
96
|
+
})
|
package/src/cli/binary.test.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require('node:fs')
|
|
|
7
7
|
const os = require('node:os')
|
|
8
8
|
const path = require('node:path')
|
|
9
9
|
const { spawnSync } = require('node:child_process')
|
|
10
|
-
const { buildLegacyTsCliArgs } = require('../../bin/swarmclaw.js')
|
|
10
|
+
const { buildLegacyTsCliArgs, resolveLegacyTsCliPath } = require('../../bin/swarmclaw.js')
|
|
11
11
|
|
|
12
12
|
const CLI_BIN = path.join(__dirname, '..', '..', 'bin', 'swarmclaw.js')
|
|
13
13
|
const PACKAGE_JSON = require('../../package.json')
|
|
@@ -199,6 +199,12 @@ test('binary -v alias output matches package version', () => {
|
|
|
199
199
|
assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`)
|
|
200
200
|
})
|
|
201
201
|
|
|
202
|
+
test('package ships dagre type declarations required by installed builds', () => {
|
|
203
|
+
assert.equal(PACKAGE_JSON.dependencies.dagre, '^0.8.5')
|
|
204
|
+
assert.equal(PACKAGE_JSON.dependencies['@types/dagre'], '^0.7.54')
|
|
205
|
+
assert.equal(PACKAGE_JSON.devDependencies?.['@types/dagre'], undefined)
|
|
206
|
+
})
|
|
207
|
+
|
|
202
208
|
test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => {
|
|
203
209
|
const cliPath = path.join(APP_ROOT, 'src', 'cli', 'index.ts')
|
|
204
210
|
const args = buildLegacyTsCliArgs(cliPath, ['runs', 'list'], {
|
|
@@ -208,3 +214,14 @@ test('legacy TS launcher falls back to tsx import when strip-types is unavailabl
|
|
|
208
214
|
|
|
209
215
|
assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'runs', 'list'])
|
|
210
216
|
})
|
|
217
|
+
|
|
218
|
+
test('legacy TS launcher uses tsx instead of strip-types inside node_modules', () => {
|
|
219
|
+
const cliPath = path.join(os.tmpdir(), 'node_modules', '@swarmclawai', 'swarmclaw', 'src', 'cli', 'index.ts')
|
|
220
|
+
const args = buildLegacyTsCliArgs(cliPath, ['agents', 'list'], {
|
|
221
|
+
supportsStripTypes: true,
|
|
222
|
+
hasTsxRuntime: true,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'agents', 'list'])
|
|
226
|
+
assert.equal(resolveLegacyTsCliPath(), path.join(APP_ROOT, 'src', 'cli', 'index.ts'))
|
|
227
|
+
})
|
|
@@ -42,7 +42,7 @@ function TooltipContent({
|
|
|
42
42
|
data-slot="tooltip-content"
|
|
43
43
|
sideOffset={sideOffset}
|
|
44
44
|
className={cn(
|
|
45
|
-
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-
|
|
45
|
+
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[1300] w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
46
46
|
className
|
|
47
47
|
)}
|
|
48
48
|
{...props}
|
|
@@ -250,6 +250,47 @@ test('buildChatModel disables parallel_tool_calls for Ollama local to avoid dupl
|
|
|
250
250
|
assert.equal(llm.clientConfig?.baseURL, 'http://localhost:11434/v1')
|
|
251
251
|
})
|
|
252
252
|
|
|
253
|
+
test('buildChatModel passes JSON format mode only to local Ollama structured calls', () => {
|
|
254
|
+
const local = buildChatModel({
|
|
255
|
+
provider: 'ollama',
|
|
256
|
+
model: 'gemma4:e4b',
|
|
257
|
+
ollamaMode: 'local',
|
|
258
|
+
apiKey: null,
|
|
259
|
+
responseFormat: 'json_object',
|
|
260
|
+
}) as ChatOpenAI & { modelKwargs?: Record<string, unknown> }
|
|
261
|
+
|
|
262
|
+
assert.equal(local.modelKwargs?.parallel_tool_calls, false)
|
|
263
|
+
assert.equal(local.modelKwargs?.format, 'json')
|
|
264
|
+
|
|
265
|
+
saveCredentials({
|
|
266
|
+
'cred-ollama-cloud-json': {
|
|
267
|
+
id: 'cred-ollama-cloud-json',
|
|
268
|
+
provider: 'ollama',
|
|
269
|
+
name: 'Ollama Cloud',
|
|
270
|
+
encryptedKey: encryptKey('ollama-cloud-key'),
|
|
271
|
+
createdAt: Date.now(),
|
|
272
|
+
},
|
|
273
|
+
} as Record<string, {
|
|
274
|
+
id: string
|
|
275
|
+
provider: string
|
|
276
|
+
name: string
|
|
277
|
+
encryptedKey: string
|
|
278
|
+
createdAt: number
|
|
279
|
+
}>)
|
|
280
|
+
|
|
281
|
+
const cloud = buildChatModel({
|
|
282
|
+
provider: 'ollama',
|
|
283
|
+
model: 'glm-5:cloud',
|
|
284
|
+
ollamaMode: 'cloud',
|
|
285
|
+
apiKey: null,
|
|
286
|
+
credentialId: 'cred-ollama-cloud-json',
|
|
287
|
+
responseFormat: 'json_object',
|
|
288
|
+
}) as ChatOpenAI & { modelKwargs?: Record<string, unknown> }
|
|
289
|
+
|
|
290
|
+
assert.equal(cloud.modelKwargs?.parallel_tool_calls, false)
|
|
291
|
+
assert.equal(cloud.modelKwargs?.format, undefined)
|
|
292
|
+
})
|
|
293
|
+
|
|
253
294
|
test('buildChatModel uses a reasoning_content-preserving bridge for DeepSeek', () => {
|
|
254
295
|
const llm = buildChatModel({
|
|
255
296
|
provider: 'deepseek',
|
|
@@ -18,6 +18,7 @@ const OLLAMA_CLOUD_URL = 'https://ollama.com/v1'
|
|
|
18
18
|
const OLLAMA_LOCAL_URL = 'http://localhost:11434/v1'
|
|
19
19
|
export const OPENAI_COMPAT_MODEL_TIMEOUT_MS = 180_000
|
|
20
20
|
export const OPENAI_COMPAT_MODEL_MAX_RETRIES = 2
|
|
21
|
+
export type GenerationResponseFormat = 'json_object'
|
|
21
22
|
|
|
22
23
|
export interface GenerationModelPreference {
|
|
23
24
|
provider?: string | null
|
|
@@ -27,6 +28,7 @@ export interface GenerationModelPreference {
|
|
|
27
28
|
apiEndpoint?: string | null
|
|
28
29
|
gatewayProfileId?: string | null
|
|
29
30
|
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high' | null
|
|
31
|
+
responseFormat?: GenerationResponseFormat | null
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
interface ResolvedGenerationModelConfig {
|
|
@@ -36,6 +38,7 @@ interface ResolvedGenerationModelConfig {
|
|
|
36
38
|
apiKey: string | null
|
|
37
39
|
apiEndpoint: string | null
|
|
38
40
|
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
|
|
41
|
+
responseFormat?: GenerationResponseFormat
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
type OpenAiReasoningEffort = 'low' | 'medium' | 'high'
|
|
@@ -43,6 +46,7 @@ type ChatOpenAiConfig = ConstructorParameters<typeof ChatOpenAI>[0] & {
|
|
|
43
46
|
modelKwargs?: {
|
|
44
47
|
reasoning_effort?: OpenAiReasoningEffort
|
|
45
48
|
parallel_tool_calls?: boolean
|
|
49
|
+
format?: 'json'
|
|
46
50
|
}
|
|
47
51
|
configuration?: {
|
|
48
52
|
baseURL?: string
|
|
@@ -68,8 +72,9 @@ export function buildChatModel(opts: {
|
|
|
68
72
|
credentialId?: string | null
|
|
69
73
|
apiEndpoint?: string | null
|
|
70
74
|
thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
|
|
75
|
+
responseFormat?: GenerationResponseFormat
|
|
71
76
|
}) {
|
|
72
|
-
const { provider, model, ollamaMode, apiKey, credentialId, apiEndpoint, thinkingLevel } = opts
|
|
77
|
+
const { provider, model, ollamaMode, apiKey, credentialId, apiEndpoint, thinkingLevel, responseFormat } = opts
|
|
73
78
|
const resolvedCredentialId = resolveProviderCredentialId({ provider, ollamaMode: ollamaMode ?? null, credentialId })
|
|
74
79
|
const resolvedApiKey = apiKey ?? resolveApiKeyFromCredential(resolvedCredentialId)
|
|
75
80
|
const providers = getProviderList()
|
|
@@ -118,7 +123,10 @@ export function buildChatModel(opts: {
|
|
|
118
123
|
timeout: OPENAI_COMPAT_MODEL_TIMEOUT_MS,
|
|
119
124
|
maxRetries: OPENAI_COMPAT_MODEL_MAX_RETRIES,
|
|
120
125
|
configuration: { baseURL },
|
|
121
|
-
modelKwargs: {
|
|
126
|
+
modelKwargs: {
|
|
127
|
+
parallel_tool_calls: false,
|
|
128
|
+
...(responseFormat === 'json_object' && !runtime.useCloud ? { format: 'json' as const } : {}),
|
|
129
|
+
},
|
|
122
130
|
})
|
|
123
131
|
}
|
|
124
132
|
|
|
@@ -222,6 +230,7 @@ function resolvePreferredGenerationConfig(
|
|
|
222
230
|
apiKey,
|
|
223
231
|
apiEndpoint,
|
|
224
232
|
thinkingLevel: candidate.thinkingLevel || undefined,
|
|
233
|
+
responseFormat: candidate.responseFormat || undefined,
|
|
225
234
|
}
|
|
226
235
|
}
|
|
227
236
|
return null
|
|
@@ -232,6 +241,7 @@ export function resolveGenerationModelConfig(options?: {
|
|
|
232
241
|
sessionId?: string | null
|
|
233
242
|
agentId?: string | null
|
|
234
243
|
excludeProviders?: string[]
|
|
244
|
+
responseFormat?: GenerationResponseFormat
|
|
235
245
|
}): ResolvedGenerationModelConfig {
|
|
236
246
|
const providers = getProviderList()
|
|
237
247
|
const excludeProviders = new Set((options?.excludeProviders || []).map((value) => normalizePreferenceValue(value)).filter(Boolean))
|
|
@@ -252,7 +262,9 @@ export function resolveGenerationModelConfig(options?: {
|
|
|
252
262
|
...getAgentGenerationPreferences(sessionAgent),
|
|
253
263
|
...getAgentGenerationPreferences(directAgent),
|
|
254
264
|
], excludeProviders)
|
|
255
|
-
if (resolved) return
|
|
265
|
+
if (resolved) return options?.responseFormat
|
|
266
|
+
? { ...resolved, responseFormat: options.responseFormat }
|
|
267
|
+
: resolved
|
|
256
268
|
|
|
257
269
|
const sessionLabel = options?.sessionId ? `session "${options.sessionId}"` : null
|
|
258
270
|
const agentLabel = options?.agentId ? `agent "${options.agentId}"` : null
|
|
@@ -271,6 +283,7 @@ export async function buildLLM(options?: {
|
|
|
271
283
|
sessionId?: string | null
|
|
272
284
|
agentId?: string | null
|
|
273
285
|
excludeProviders?: string[]
|
|
286
|
+
responseFormat?: GenerationResponseFormat
|
|
274
287
|
}) {
|
|
275
288
|
const resolved = resolveGenerationModelConfig(options)
|
|
276
289
|
return {
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
2
3
|
import fs from 'node:fs'
|
|
3
4
|
import os from 'node:os'
|
|
4
5
|
import path from 'node:path'
|
|
5
6
|
import { describe, it } from 'node:test'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
attachImapErrorHandler,
|
|
9
|
+
buildAttachments,
|
|
10
|
+
buildEmailTlsOptions,
|
|
11
|
+
parseTlsRejectUnauthorized,
|
|
12
|
+
} from './email'
|
|
7
13
|
import { connectorSupportsBinaryMedia } from './response-media'
|
|
8
14
|
|
|
9
15
|
describe('connectorSupportsBinaryMedia — email', () => {
|
|
@@ -63,3 +69,30 @@ describe('email buildAttachments', () => {
|
|
|
63
69
|
}
|
|
64
70
|
})
|
|
65
71
|
})
|
|
72
|
+
|
|
73
|
+
describe('email TLS configuration', () => {
|
|
74
|
+
it('defaults to certificate verification', () => {
|
|
75
|
+
assert.equal(parseTlsRejectUnauthorized(undefined), true)
|
|
76
|
+
assert.equal(parseTlsRejectUnauthorized(''), true)
|
|
77
|
+
assert.deepEqual(buildEmailTlsOptions({ tlsRejectUnauthorized: true }), { rejectUnauthorized: true })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('allows explicit self-signed certificate opt-out', () => {
|
|
81
|
+
assert.equal(parseTlsRejectUnauthorized(false), false)
|
|
82
|
+
assert.equal(parseTlsRejectUnauthorized('false'), false)
|
|
83
|
+
assert.equal(parseTlsRejectUnauthorized('0'), false)
|
|
84
|
+
assert.deepEqual(buildEmailTlsOptions({ tlsRejectUnauthorized: false }), { rejectUnauthorized: false })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('handles IMAP socket errors without leaving the emitter unhandled', () => {
|
|
88
|
+
const imap = new EventEmitter()
|
|
89
|
+
let disconnected = false
|
|
90
|
+
|
|
91
|
+
attachImapErrorHandler(imap, () => {
|
|
92
|
+
disconnected = true
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
assert.doesNotThrow(() => imap.emit('error', new Error('DEPTH_ZERO_SELF_SIGNED_CERT')))
|
|
96
|
+
assert.equal(disconnected, true)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -21,6 +21,7 @@ interface EmailConfig {
|
|
|
21
21
|
folder?: string
|
|
22
22
|
pollIntervalSec?: number
|
|
23
23
|
subjectPrefix?: string
|
|
24
|
+
tlsRejectUnauthorized: boolean
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface MailAttachment {
|
|
@@ -29,6 +30,10 @@ interface MailAttachment {
|
|
|
29
30
|
contentType?: string
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
interface ImapErrorEmitter {
|
|
34
|
+
on(event: 'error', listener: (err: unknown) => void): unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
export function buildAttachments(options?: OutboundSendOptions): MailAttachment[] {
|
|
33
38
|
const source = options?.mediaPath
|
|
34
39
|
if (!source) return []
|
|
@@ -43,6 +48,28 @@ export function buildAttachments(options?: OutboundSendOptions): MailAttachment[
|
|
|
43
48
|
}]
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
export function parseTlsRejectUnauthorized(value: unknown): boolean {
|
|
52
|
+
if (typeof value === 'boolean') return value
|
|
53
|
+
if (typeof value !== 'string') return true
|
|
54
|
+
|
|
55
|
+
const normalized = value.trim().toLowerCase()
|
|
56
|
+
if (!normalized) return true
|
|
57
|
+
if (['false', '0', 'no', 'off', 'disabled'].includes(normalized)) return false
|
|
58
|
+
if (['true', '1', 'yes', 'on', 'enabled'].includes(normalized)) return true
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildEmailTlsOptions(config: Pick<EmailConfig, 'tlsRejectUnauthorized'>): { rejectUnauthorized: boolean } {
|
|
63
|
+
return { rejectUnauthorized: config.tlsRejectUnauthorized !== false }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function attachImapErrorHandler(imap: ImapErrorEmitter, onDisconnected: () => void): void {
|
|
67
|
+
imap.on('error', (err: unknown) => {
|
|
68
|
+
onDisconnected()
|
|
69
|
+
log.error(TAG, `IMAP socket error: ${errorMessage(err)}`)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
function getConfig(connector: Connector): EmailConfig {
|
|
47
74
|
const c = connector.config as Record<string, unknown>
|
|
48
75
|
return {
|
|
@@ -55,6 +82,7 @@ function getConfig(connector: Connector): EmailConfig {
|
|
|
55
82
|
folder: String(c.folder ?? 'INBOX'),
|
|
56
83
|
pollIntervalSec: Number(c.pollIntervalSec ?? 60),
|
|
57
84
|
subjectPrefix: c.subjectPrefix ? String(c.subjectPrefix) : undefined,
|
|
85
|
+
tlsRejectUnauthorized: parseTlsRejectUnauthorized(c.tlsRejectUnauthorized),
|
|
58
86
|
}
|
|
59
87
|
}
|
|
60
88
|
|
|
@@ -68,24 +96,31 @@ const email: PlatformConnector = {
|
|
|
68
96
|
|
|
69
97
|
const folder = config.folder || 'INBOX'
|
|
70
98
|
const pollMs = (config.pollIntervalSec || 60) * 1000
|
|
99
|
+
const tls = buildEmailTlsOptions(config)
|
|
100
|
+
let connected = false
|
|
71
101
|
|
|
72
102
|
// IMAP client for inbound
|
|
73
103
|
const imap = new ImapFlow({
|
|
74
104
|
host: config.imapHost,
|
|
75
105
|
port: config.imapPort || 993,
|
|
76
106
|
secure: (config.imapPort || 993) === 993,
|
|
107
|
+
tls,
|
|
77
108
|
auth: {
|
|
78
109
|
user: config.user,
|
|
79
110
|
pass: config.password,
|
|
80
111
|
},
|
|
81
112
|
logger: false,
|
|
82
113
|
})
|
|
114
|
+
attachImapErrorHandler(imap, () => {
|
|
115
|
+
connected = false
|
|
116
|
+
})
|
|
83
117
|
|
|
84
118
|
// SMTP transport for outbound
|
|
85
119
|
const smtp: Transporter = createTransport({
|
|
86
120
|
host: config.smtpHost,
|
|
87
121
|
port: config.smtpPort || 587,
|
|
88
122
|
secure: (config.smtpPort || 587) === 465,
|
|
123
|
+
tls,
|
|
89
124
|
auth: {
|
|
90
125
|
user: config.user,
|
|
91
126
|
pass: config.password,
|
|
@@ -94,7 +129,6 @@ const email: PlatformConnector = {
|
|
|
94
129
|
|
|
95
130
|
// Track last seen UID to only process new messages
|
|
96
131
|
let highwaterUid = 0
|
|
97
|
-
let connected = false
|
|
98
132
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
99
133
|
|
|
100
134
|
// Map to track original sender per channelId (email address) for replies
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { parseTier2DreamResponseText } from './dream-service'
|
|
4
|
+
|
|
5
|
+
describe('parseTier2DreamResponseText', () => {
|
|
6
|
+
it('parses a plain structured dream response', () => {
|
|
7
|
+
const parsed = parseTier2DreamResponseText(JSON.stringify({
|
|
8
|
+
consolidations: [{
|
|
9
|
+
sourceIds: ['mem-1', 'mem-2'],
|
|
10
|
+
title: 'Shared pattern',
|
|
11
|
+
content: 'Both memories describe the same workflow.',
|
|
12
|
+
}],
|
|
13
|
+
reflections: [{ title: 'Focus', content: 'The agent prefers short release loops.' }],
|
|
14
|
+
flagged: [{ memoryId: 'mem-3', reason: 'Contradicts the current release process.' }],
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
assert.deepEqual(parsed?.consolidations?.[0]?.sourceIds, ['mem-1', 'mem-2'])
|
|
18
|
+
assert.equal(parsed?.reflections?.[0]?.title, 'Focus')
|
|
19
|
+
assert.equal(parsed?.flagged?.[0]?.memoryId, 'mem-3')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('extracts fenced JSON with nested braces inside strings', () => {
|
|
23
|
+
const parsed = parseTier2DreamResponseText([
|
|
24
|
+
'```json',
|
|
25
|
+
'{',
|
|
26
|
+
' "consolidations": [{',
|
|
27
|
+
' "sourceIds": ["mem-1"],',
|
|
28
|
+
' "title": "Payload shape",',
|
|
29
|
+
' "content": "The JSON example was {\\"ok\\":true} and should stay intact."',
|
|
30
|
+
' }]',
|
|
31
|
+
'}',
|
|
32
|
+
'```',
|
|
33
|
+
].join('\n'))
|
|
34
|
+
|
|
35
|
+
assert.equal(parsed?.consolidations?.[0]?.content, 'The JSON example was {"ok":true} and should stay intact.')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('rejects malformed or schema-incompatible responses', () => {
|
|
39
|
+
assert.equal(parseTier2DreamResponseText('no json here'), null)
|
|
40
|
+
assert.equal(parseTier2DreamResponseText('{"consolidations":[{"sourceIds":[123],"title":"Bad","content":"Bad"}]}'), null)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import crypto from 'crypto'
|
|
2
|
+
import { z } from 'zod'
|
|
2
3
|
import type { DreamCycle, DreamCycleResult, DreamConfig, DreamTrigger, Agent } from '@/types'
|
|
3
4
|
import { DEFAULT_DREAM_CONFIG } from '@/types/dream'
|
|
4
5
|
import { getMemoryDb } from '@/lib/server/memory/memory-db'
|
|
5
6
|
import { saveDreamCycle } from '@/lib/server/memory/dream-cycles'
|
|
6
|
-
import { errorMessage
|
|
7
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
7
8
|
import { log } from '@/lib/server/logger'
|
|
8
9
|
|
|
9
10
|
const TAG = 'dream-service'
|
|
@@ -102,6 +103,70 @@ interface Tier2Response {
|
|
|
102
103
|
flagged?: Tier2Flagged[]
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
const Tier2ResponseSchema = z.object({
|
|
107
|
+
consolidations: z.array(z.object({
|
|
108
|
+
sourceIds: z.array(z.string()),
|
|
109
|
+
title: z.string(),
|
|
110
|
+
content: z.string(),
|
|
111
|
+
})).optional(),
|
|
112
|
+
reflections: z.array(z.object({
|
|
113
|
+
title: z.string(),
|
|
114
|
+
content: z.string(),
|
|
115
|
+
})).optional(),
|
|
116
|
+
flagged: z.array(z.object({
|
|
117
|
+
memoryId: z.string(),
|
|
118
|
+
reason: z.string(),
|
|
119
|
+
})).optional(),
|
|
120
|
+
}).passthrough()
|
|
121
|
+
|
|
122
|
+
function findBalancedJsonObjectEnd(text: string, start: number): number {
|
|
123
|
+
let depth = 0
|
|
124
|
+
let inString = false
|
|
125
|
+
let escaped = false
|
|
126
|
+
|
|
127
|
+
for (let index = start; index < text.length; index += 1) {
|
|
128
|
+
const char = text[index]
|
|
129
|
+
if (inString) {
|
|
130
|
+
if (escaped) escaped = false
|
|
131
|
+
else if (char === '\\') escaped = true
|
|
132
|
+
else if (char === '"') inString = false
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (char === '"') {
|
|
136
|
+
inString = true
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (char === '{') depth += 1
|
|
140
|
+
else if (char === '}') depth -= 1
|
|
141
|
+
if (depth === 0) return index + 1
|
|
142
|
+
}
|
|
143
|
+
return -1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractFirstBalancedJsonObject(text: string): string | null {
|
|
147
|
+
const source = String(text || '')
|
|
148
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
149
|
+
if (source[index] !== '{') continue
|
|
150
|
+
const end = findBalancedJsonObjectEnd(source, index)
|
|
151
|
+
if (end === -1) return null
|
|
152
|
+
return source.slice(index, end)
|
|
153
|
+
}
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function parseTier2DreamResponseText(text: string): Tier2Response | null {
|
|
158
|
+
const jsonText = extractFirstBalancedJsonObject(text)
|
|
159
|
+
if (!jsonText) return null
|
|
160
|
+
let raw: unknown
|
|
161
|
+
try {
|
|
162
|
+
raw = JSON.parse(jsonText)
|
|
163
|
+
} catch {
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
const parsed = Tier2ResponseSchema.safeParse(raw)
|
|
167
|
+
return parsed.success ? parsed.data : null
|
|
168
|
+
}
|
|
169
|
+
|
|
105
170
|
export async function runTier2Dream(
|
|
106
171
|
agentId: string,
|
|
107
172
|
config: DreamConfig,
|
|
@@ -149,7 +214,7 @@ ${memoryLines.join('\n')}`
|
|
|
149
214
|
|
|
150
215
|
try {
|
|
151
216
|
const { buildLLM } = await import('@/lib/server/build-llm')
|
|
152
|
-
const { llm } = await buildLLM({ agentId })
|
|
217
|
+
const { llm } = await buildLLM({ agentId, responseFormat: 'json_object' })
|
|
153
218
|
const { HumanMessage } = await import('@langchain/core/messages')
|
|
154
219
|
|
|
155
220
|
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
@@ -159,9 +224,10 @@ ${memoryLines.join('\n')}`
|
|
|
159
224
|
? response.content.map((b) => ('text' in b && typeof b.text === 'string' ? b.text : '')).join('')
|
|
160
225
|
: ''
|
|
161
226
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
227
|
+
const parsed = parseTier2DreamResponseText(text) ?? {}
|
|
228
|
+
if (!parsed.consolidations && !parsed.reflections && !parsed.flagged) {
|
|
229
|
+
errors.push('Tier 2 dream response was not valid structured JSON.')
|
|
230
|
+
}
|
|
165
231
|
|
|
166
232
|
// Process consolidations
|
|
167
233
|
if (Array.isArray(parsed.consolidations)) {
|
|
@@ -187,6 +187,10 @@ const COLLECTIONS = [
|
|
|
187
187
|
|
|
188
188
|
export type StorageCollection = (typeof COLLECTIONS)[number]
|
|
189
189
|
|
|
190
|
+
const ARRAY_VALUE_COLLECTIONS = new Set<StorageCollection>([
|
|
191
|
+
'model_overrides',
|
|
192
|
+
])
|
|
193
|
+
|
|
190
194
|
for (const table of COLLECTIONS) {
|
|
191
195
|
db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
|
|
192
196
|
}
|
|
@@ -246,18 +250,20 @@ function getCollectionRawCache(table: string): LRUMap<string, string> {
|
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
function loadCollectionWithNormalizationState(table: string): {
|
|
249
|
-
result: Record<string, StoredObject>
|
|
253
|
+
result: Record<string, StoredObject | unknown[]>
|
|
250
254
|
normalizedCount: number
|
|
251
255
|
} {
|
|
252
256
|
const endPerf = perf.start('storage', 'loadCollection', { table })
|
|
253
257
|
const raw = getCollectionRawCache(table)
|
|
254
|
-
const result: Record<string, StoredObject> = {}
|
|
258
|
+
const result: Record<string, StoredObject | unknown[]> = {}
|
|
259
|
+
const allowsArrayValues = ARRAY_VALUE_COLLECTIONS.has(table as StorageCollection)
|
|
255
260
|
let normalizedCount = 0
|
|
256
261
|
for (const [id, data] of raw.entries()) {
|
|
257
262
|
try {
|
|
258
263
|
const { value: normalized, changed } = normalize(table, JSON.parse(data))
|
|
259
|
-
if (!normalized || typeof normalized !== 'object'
|
|
260
|
-
|
|
264
|
+
if (!normalized || typeof normalized !== 'object') continue
|
|
265
|
+
if (Array.isArray(normalized) && !allowsArrayValues) continue
|
|
266
|
+
result[id] = normalized as StoredObject | unknown[]
|
|
261
267
|
if (changed) normalizedCount += 1
|
|
262
268
|
} catch (err) {
|
|
263
269
|
const fingerprint = `${table}:${id}`
|
|
@@ -277,8 +283,8 @@ function loadCollectionWithNormalizationState(table: string): {
|
|
|
277
283
|
|
|
278
284
|
export function loadCollection(table: string): Record<string, StoredObject> {
|
|
279
285
|
const { result, normalizedCount } = loadCollectionWithNormalizationState(table)
|
|
280
|
-
if (normalizedCount > 0) saveCollection(table, result)
|
|
281
|
-
return result
|
|
286
|
+
if (normalizedCount > 0) saveCollection(table, result as Record<string, unknown>)
|
|
287
|
+
return result as Record<string, StoredObject>
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
function saveCollection(table: string, data: Record<string, unknown>) {
|
|
@@ -1001,7 +1007,7 @@ export function patchAgent(
|
|
|
1001
1007
|
const schedulesStore = createCollectionStore('schedules', { ttlMs: 10_000 })
|
|
1002
1008
|
export function loadSchedules(): Record<string, Schedule> {
|
|
1003
1009
|
const { result, normalizedCount } = loadCollectionWithNormalizationState('schedules')
|
|
1004
|
-
if (normalizedCount > 0) saveCollection('schedules', result)
|
|
1010
|
+
if (normalizedCount > 0) saveCollection('schedules', result as Record<string, unknown>)
|
|
1005
1011
|
return result as unknown as Record<string, Schedule>
|
|
1006
1012
|
}
|
|
1007
1013
|
export const saveSchedules = schedulesStore.save
|
package/electron-dist/main.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
const electron_1 = require("electron");
|
|
40
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
-
const paths_1 = require("./paths");
|
|
43
|
-
const server_lifecycle_1 = require("./server-lifecycle");
|
|
44
|
-
const menu_1 = require("./menu");
|
|
45
|
-
const DEV_URL_DEFAULT = 'http://127.0.0.1:3456';
|
|
46
|
-
const LOG_TAIL_BYTES = 1500;
|
|
47
|
-
let mainWindow = null;
|
|
48
|
-
let serverHandle = null;
|
|
49
|
-
let serverLogFile = null;
|
|
50
|
-
let isQuitting = false;
|
|
51
|
-
const gotLock = electron_1.app.requestSingleInstanceLock();
|
|
52
|
-
if (!gotLock) {
|
|
53
|
-
electron_1.app.quit();
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
electron_1.app.on('second-instance', () => {
|
|
57
|
-
if (mainWindow) {
|
|
58
|
-
if (mainWindow.isMinimized())
|
|
59
|
-
mainWindow.restore();
|
|
60
|
-
mainWindow.focus();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
electron_1.app.on('ready', () => void onReady());
|
|
64
|
-
electron_1.app.on('window-all-closed', () => {
|
|
65
|
-
if (process.platform !== 'darwin')
|
|
66
|
-
electron_1.app.quit();
|
|
67
|
-
});
|
|
68
|
-
electron_1.app.on('activate', () => {
|
|
69
|
-
if (mainWindow !== null)
|
|
70
|
-
return;
|
|
71
|
-
if (serverHandle) {
|
|
72
|
-
createMainWindow(serverHandle.url);
|
|
73
|
-
}
|
|
74
|
-
else if (!electron_1.app.isPackaged) {
|
|
75
|
-
createMainWindow(process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
electron_1.app.on('before-quit', () => {
|
|
79
|
-
isQuitting = true;
|
|
80
|
-
});
|
|
81
|
-
electron_1.app.on('will-quit', async (event) => {
|
|
82
|
-
if (!serverHandle)
|
|
83
|
-
return;
|
|
84
|
-
event.preventDefault();
|
|
85
|
-
try {
|
|
86
|
-
await serverHandle.stop();
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
serverHandle = null;
|
|
90
|
-
electron_1.app.exit(0);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
async function onReady() {
|
|
95
|
-
const paths = (0, paths_1.resolveRuntimePaths)();
|
|
96
|
-
(0, menu_1.buildAppMenu)(paths, () => mainWindow);
|
|
97
|
-
const iconPath = resolveIconPath();
|
|
98
|
-
if (process.platform === 'darwin' && iconPath && electron_1.app.dock) {
|
|
99
|
-
const img = electron_1.nativeImage.createFromPath(iconPath);
|
|
100
|
-
if (!img.isEmpty())
|
|
101
|
-
electron_1.app.dock.setIcon(img);
|
|
102
|
-
}
|
|
103
|
-
if (!electron_1.app.isPackaged) {
|
|
104
|
-
const devUrl = process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT;
|
|
105
|
-
console.log(`[swarmclaw] dev mode, loading ${devUrl}`);
|
|
106
|
-
createMainWindow(devUrl);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
serverLogFile = node_path_1.default.join(electron_1.app.getPath('userData'), 'logs', 'server.log');
|
|
110
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(serverLogFile), { recursive: true });
|
|
111
|
-
try {
|
|
112
|
-
serverHandle = await (0, server_lifecycle_1.startEmbeddedServer)({
|
|
113
|
-
paths,
|
|
114
|
-
logFile: serverLogFile,
|
|
115
|
-
onStdout: (c) => process.stdout.write(`[swarmclaw] ${c}`),
|
|
116
|
-
onStderr: (c) => process.stderr.write(`[swarmclaw] ${c}`),
|
|
117
|
-
onExit: (code, signal) => {
|
|
118
|
-
if (!isQuitting) {
|
|
119
|
-
console.error(`[swarmclaw] server exited unexpectedly (code=${code}, signal=${signal ?? 'none'})`);
|
|
120
|
-
void showServerCrashDialog(code, signal);
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
await showStartupFailureDialog(err, paths);
|
|
127
|
-
electron_1.app.exit(1);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
createMainWindow(serverHandle.url);
|
|
131
|
-
void Promise.resolve().then(() => __importStar(require('./updater'))).then((m) => m.initAutoUpdater());
|
|
132
|
-
}
|
|
133
|
-
function resolveIconPath() {
|
|
134
|
-
const candidate = electron_1.app.isPackaged
|
|
135
|
-
? node_path_1.default.join(process.resourcesPath, 'icon.png')
|
|
136
|
-
: node_path_1.default.join(__dirname, '..', 'resources', 'icon.png');
|
|
137
|
-
return node_fs_1.default.existsSync(candidate) ? candidate : undefined;
|
|
138
|
-
}
|
|
139
|
-
function createMainWindow(startUrl) {
|
|
140
|
-
const iconPath = resolveIconPath();
|
|
141
|
-
mainWindow = new electron_1.BrowserWindow({
|
|
142
|
-
width: 1440,
|
|
143
|
-
height: 900,
|
|
144
|
-
minWidth: 1024,
|
|
145
|
-
minHeight: 640,
|
|
146
|
-
backgroundColor: '#0b0b0f',
|
|
147
|
-
show: true,
|
|
148
|
-
...(iconPath ? { icon: iconPath } : {}),
|
|
149
|
-
webPreferences: {
|
|
150
|
-
contextIsolation: true,
|
|
151
|
-
nodeIntegration: false,
|
|
152
|
-
sandbox: false,
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
const wc = mainWindow.webContents;
|
|
156
|
-
if (!electron_1.app.isPackaged)
|
|
157
|
-
wc.openDevTools({ mode: 'detach' });
|
|
158
|
-
wc.on('did-start-loading', () => console.log('[swarmclaw] did-start-loading'));
|
|
159
|
-
wc.on('did-finish-load', () => console.log('[swarmclaw] did-finish-load'));
|
|
160
|
-
wc.on('did-fail-load', (_e, code, desc, url) => console.error(`[swarmclaw] did-fail-load code=${code} desc=${desc} url=${url}`));
|
|
161
|
-
wc.on('render-process-gone', (_e, details) => console.error(`[swarmclaw] render-process-gone reason=${details.reason}`));
|
|
162
|
-
wc.on('unresponsive', () => console.error('[swarmclaw] webContents unresponsive'));
|
|
163
|
-
mainWindow.on('closed', () => {
|
|
164
|
-
mainWindow = null;
|
|
165
|
-
});
|
|
166
|
-
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
167
|
-
if (url.startsWith(startUrl))
|
|
168
|
-
return { action: 'allow' };
|
|
169
|
-
void electron_1.shell.openExternal(url);
|
|
170
|
-
return { action: 'deny' };
|
|
171
|
-
});
|
|
172
|
-
void mainWindow.loadURL(startUrl).catch((err) => {
|
|
173
|
-
console.error('[swarmclaw] loadURL rejected:', err);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
async function showServerCrashDialog(code, signal) {
|
|
177
|
-
const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
|
|
178
|
-
const quitButtonId = buttons.length - 1;
|
|
179
|
-
const detail = buildLogDetail(`code=${code ?? 'null'} signal=${signal ?? 'none'}`);
|
|
180
|
-
const res = await electron_1.dialog.showMessageBox({
|
|
181
|
-
type: 'error',
|
|
182
|
-
buttons,
|
|
183
|
-
defaultId: quitButtonId,
|
|
184
|
-
cancelId: quitButtonId,
|
|
185
|
-
title: 'SwarmClaw stopped',
|
|
186
|
-
message: 'The SwarmClaw server exited unexpectedly.',
|
|
187
|
-
detail,
|
|
188
|
-
});
|
|
189
|
-
if (serverLogFile && res.response === 0)
|
|
190
|
-
electron_1.shell.showItemInFolder(serverLogFile);
|
|
191
|
-
electron_1.app.exit(1);
|
|
192
|
-
}
|
|
193
|
-
async function showStartupFailureDialog(err, paths) {
|
|
194
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
-
const base = `${message}\n\nStandalone entry: ${paths.standaloneEntry}\nData dir: ${paths.dataDir}`;
|
|
196
|
-
const detail = buildLogDetail(base);
|
|
197
|
-
const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
|
|
198
|
-
const quitButtonId = buttons.length - 1;
|
|
199
|
-
const res = await electron_1.dialog.showMessageBox({
|
|
200
|
-
type: 'error',
|
|
201
|
-
buttons,
|
|
202
|
-
defaultId: quitButtonId,
|
|
203
|
-
cancelId: quitButtonId,
|
|
204
|
-
title: 'SwarmClaw failed to start',
|
|
205
|
-
message: 'The embedded server did not start.',
|
|
206
|
-
detail,
|
|
207
|
-
});
|
|
208
|
-
if (serverLogFile && res.response === 0)
|
|
209
|
-
electron_1.shell.showItemInFolder(serverLogFile);
|
|
210
|
-
}
|
|
211
|
-
function buildLogDetail(base) {
|
|
212
|
-
if (!serverLogFile)
|
|
213
|
-
return base;
|
|
214
|
-
const tail = (0, server_lifecycle_1.tailLogFile)(serverLogFile, LOG_TAIL_BYTES).trim();
|
|
215
|
-
if (!tail)
|
|
216
|
-
return `${base}\n\nLog file: ${serverLogFile}\n(no output captured yet)`;
|
|
217
|
-
return `${base}\n\nLog tail (${serverLogFile}):\n${tail}`;
|
|
218
|
-
}
|