@swarmclawai/swarmclaw 1.5.33 → 1.5.35
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 +28 -3
- package/bin/update-cmd.js +26 -0
- package/package.json +2 -2
- package/scripts/easy-update.mjs +23 -0
- package/src/app/api/secrets/[id]/route.ts +4 -2
- package/src/app/api/setup/doctor/route.ts +31 -1
- package/src/app/api/version/update/route.ts +17 -0
- package/src/lib/ollama-mode.test.ts +18 -1
- package/src/lib/ollama-mode.ts +8 -0
- package/src/lib/providers/openai.ts +5 -1
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +10 -8
- package/src/lib/server/chat-execution/prompt-sections.ts +2 -2
- package/src/lib/server/chat-execution/stream-agent-chat.ts +2 -2
- package/src/lib/server/credentials/credential-service.ts +9 -1
- package/src/lib/server/ollama-runtime.ts +2 -2
- package/src/lib/server/openclaw/gateway.ts +8 -1
- package/src/lib/server/provider-health.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.ts +13 -10
- package/src/lib/server/session-tools/index.ts +11 -1
- package/src/lib/server/storage.ts +15 -0
- package/src/proxy.ts +7 -1
package/README.md
CHANGED
|
@@ -39,6 +39,10 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
|
|
|
39
39
|
<td align="center"><img src="doc/assets/logos/codex.svg" width="32" alt="Codex"><br><sub>Codex</sub></td>
|
|
40
40
|
<td align="center"><img src="doc/assets/logos/gemini-cli.svg" width="32" alt="Gemini CLI"><br><sub>Gemini CLI</sub></td>
|
|
41
41
|
<td align="center"><img src="doc/assets/logos/opencode.svg" width="32" alt="OpenCode"><br><sub>OpenCode</sub></td>
|
|
42
|
+
<td align="center"><img src="doc/assets/logos/copilot-cli.svg" width="32" alt="Copilot CLI"><br><sub>Copilot</sub></td>
|
|
43
|
+
<td align="center"><img src="doc/assets/logos/cursor-cli.svg" width="32" alt="Cursor Agent CLI"><br><sub>Cursor</sub></td>
|
|
44
|
+
<td align="center"><img src="doc/assets/logos/qwen-code-cli.svg" width="32" alt="Qwen Code CLI"><br><sub>Qwen Code</sub></td>
|
|
45
|
+
<td align="center"><img src="doc/assets/logos/goose.svg" width="32" alt="Goose"><br><sub>Goose</sub></td>
|
|
42
46
|
<td align="center"><img src="doc/assets/logos/anthropic.svg" width="32" alt="Anthropic"><br><sub>Anthropic</sub></td>
|
|
43
47
|
<td align="center"><img src="doc/assets/logos/openai.svg" width="32" alt="OpenAI"><br><sub>OpenAI</sub></td>
|
|
44
48
|
<td align="center"><img src="public/provider-logos/openrouter.png" width="32" alt="OpenRouter"><br><sub>OpenRouter</sub></td>
|
|
@@ -61,7 +65,7 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
|
|
|
61
65
|
- Node.js 22.6+ (`nvm use` will pick up the repo's `.nvmrc`, which matches CI)
|
|
62
66
|
- npm 10+ or another supported package manager
|
|
63
67
|
- Docker Desktop is recommended for sandbox browser execution
|
|
64
|
-
- Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, or
|
|
68
|
+
- Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, Gemini, Copilot, Cursor Agent, Qwen Code, or Goose
|
|
65
69
|
|
|
66
70
|
## Quick Start
|
|
67
71
|
|
|
@@ -147,10 +151,10 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
|
|
|
147
151
|
|
|
148
152
|
## Core Capabilities
|
|
149
153
|
|
|
150
|
-
- **Providers**:
|
|
154
|
+
- **Providers**: 23 built-in — Claude Code CLI, Codex CLI, OpenCode CLI, Gemini CLI, Copilot CLI, Cursor Agent CLI, Qwen Code CLI, Goose, Anthropic, OpenAI, OpenRouter, Google Gemini, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, Ollama, OpenClaw, and Hermes Agent, plus compatible custom endpoints.
|
|
151
155
|
- **OpenRouter**: <img src="public/provider-logos/openrouter.png" alt="OpenRouter logo" width="20" height="20" /> Use OpenRouter as a first-class built-in provider with its standard OpenAI-compatible endpoint and routed model IDs such as `openai/gpt-4.1-mini`.
|
|
152
156
|
- **Hermes Agent**: <img src="public/provider-logos/hermes-agent.png" alt="Hermes Agent logo" width="20" height="20" /> Connect Hermes through its OpenAI-compatible API server, locally or through a reachable remote `/v1` endpoint.
|
|
153
|
-
- **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, and native SwarmClaw subagents.
|
|
157
|
+
- **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, Cursor Agent CLI, Qwen Code CLI, and native SwarmClaw subagents.
|
|
154
158
|
- **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, and agent wakeups.
|
|
155
159
|
- **Orchestration**: durable structured execution with branching, repeat loops, parallel branches, explicit joins, restart-safe run state, and contextual launch from chats, chatrooms, tasks, schedules, and API flows.
|
|
156
160
|
- **Structured Sessions**: reusable bounded runs with templates, facilitators, participants, hidden live rooms, chatroom `/breakout`, durable transcripts, outputs, operator controls, and a visible protocols template gallery plus visual builder.
|
|
@@ -371,6 +375,20 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
371
375
|
|
|
372
376
|
## Releases
|
|
373
377
|
|
|
378
|
+
### v1.5.35 Highlights
|
|
379
|
+
|
|
380
|
+
- **Update safety: prevent DB corruption on Linux**: `npm run update:easy`, `swarmclaw update`, and the in-app update endpoint now stop the running server (or checkpoint the SQLite WAL) before rebuilding native modules, preventing the WAL journal corruption that forced some Linux users back to the setup wizard.
|
|
381
|
+
- **SQLite graceful shutdown**: the server now checkpoints and closes the database on SIGTERM/SIGINT, eliminating stale WAL state after any clean stop.
|
|
382
|
+
- **Doctor: detect dangling gateway credentials**: the setup doctor now flags gateway profiles that reference deleted or missing credentials, explaining the "gateway token missing" connection errors.
|
|
383
|
+
- **Gateway credential resolution logging**: when a gateway credential can't be resolved, the server now logs a clear warning identifying the missing credential ID.
|
|
384
|
+
- **Credential decryption error logging**: when a stored credential can't be decrypted (e.g. after `CREDENTIAL_SECRET` changes), the server now logs the credential ID and provider so users know which key to re-add.
|
|
385
|
+
|
|
386
|
+
### v1.5.34 Highlights
|
|
387
|
+
|
|
388
|
+
- **Ollama Cloud auth fix**: SwarmClaw now normalizes `api.ollama.com` and `www.ollama.com` to `ollama.com` before making authenticated requests, avoiding the redirect that was dropping authorization headers and causing false provider-health/runtime failures.
|
|
389
|
+
- **Chat execution context hardening**: tool invocation now resolves names case-insensitively, oversized tool results are truncated before they are fed back into the model, and proactive grounding/heartbeat prompts stay smaller under pressure to reduce avoidable context blowouts.
|
|
390
|
+
- **API compatibility fixes**: OpenAI-compatible streaming now captures reasoning deltas from providers that emit them outside `delta.content`, and A2A endpoints are exempt from the main proxy access-key gate so they can rely on their own auth scheme.
|
|
391
|
+
|
|
374
392
|
### v1.5.33 Highlights
|
|
375
393
|
|
|
376
394
|
- **CLI global flag compatibility**: legacy-routed commands now honor the documented `--access-key` and `--base-url` aliases even when they appear after the subcommand, so authenticated CLI automation works the same across binary entry points.
|
|
@@ -380,6 +398,13 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
380
398
|
|
|
381
399
|
- **Fix Docker first-run crash**: resolved `EISDIR: illegal operation on a directory, read` error when running `docker compose up` without a pre-existing `.env.local` file. Docker was creating a directory mount instead of a file, which crashed Next.js on startup. Replaced the file bind mount with `env_file` directive using `required: false`.
|
|
382
400
|
|
|
401
|
+
### v1.5.4 Highlights
|
|
402
|
+
|
|
403
|
+
- **Cursor Agent CLI built-in provider**: Cursor Agent CLI is now a first-class worker provider with session continuity, headless execution, and delegation support.
|
|
404
|
+
- **Qwen Code CLI built-in provider**: Qwen Code CLI is now available as a built-in worker provider and delegation backend with structured headless execution support.
|
|
405
|
+
- **Goose built-in provider**: Goose is now supported as a runtime-managed worker provider, using its own local auth and provider configuration while preserving SwarmClaw session continuity.
|
|
406
|
+
- **CLI setup and health parity**: setup flows, provider checks, setup doctor, and provider-facing UI now recognize Cursor, Qwen Code, and Goose alongside the existing CLI-backed providers.
|
|
407
|
+
|
|
383
408
|
### v1.5.3 Highlights
|
|
384
409
|
|
|
385
410
|
- **Copilot CLI v1.x compatibility**: the `copilot-cli` provider now handles the current event format (`assistant.message_delta`, `assistant.message`, updated `result` payload) while keeping backward compatibility with the legacy format. Also fixes `--resume` flag syntax. (Community contribution by [@borislavnnikolov](https://github.com/borislavnnikolov) -- PR #36)
|
package/bin/update-cmd.js
CHANGED
|
@@ -98,6 +98,29 @@ function runRegistrySelfUpdate(
|
|
|
98
98
|
return 0
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function stopRunningServer(logger = { log, logError }) {
|
|
102
|
+
// Stop the server gracefully before updating to prevent SQLite WAL corruption.
|
|
103
|
+
try {
|
|
104
|
+
const serverCmd = path.join(PKG_ROOT, 'bin', 'swarmclaw.js')
|
|
105
|
+
const status = execFileSync(process.execPath, [serverCmd, 'status'], {
|
|
106
|
+
encoding: 'utf-8',
|
|
107
|
+
cwd: PKG_ROOT,
|
|
108
|
+
timeout: 10_000,
|
|
109
|
+
}).trim()
|
|
110
|
+
if (status.toLowerCase().includes('running')) {
|
|
111
|
+
logger.log('Stopping running server before update...')
|
|
112
|
+
execFileSync(process.execPath, [serverCmd, 'stop'], {
|
|
113
|
+
encoding: 'utf-8',
|
|
114
|
+
cwd: PKG_ROOT,
|
|
115
|
+
timeout: 15_000,
|
|
116
|
+
})
|
|
117
|
+
logger.log('Server stopped.')
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Server may not be running or status command unavailable — continue.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
function main(args = process.argv.slice(3)) {
|
|
102
125
|
if (args.includes('-h') || args.includes('--help')) {
|
|
103
126
|
console.log(`
|
|
@@ -112,9 +135,12 @@ If running from a registry install, update the global package with its owning pa
|
|
|
112
135
|
try {
|
|
113
136
|
run('git rev-parse --git-dir')
|
|
114
137
|
} catch {
|
|
138
|
+
stopRunningServer()
|
|
115
139
|
process.exit(runRegistrySelfUpdate())
|
|
116
140
|
}
|
|
117
141
|
|
|
142
|
+
stopRunningServer()
|
|
143
|
+
|
|
118
144
|
const beforeRef = run('git rev-parse HEAD')
|
|
119
145
|
const beforeSha = run('git rev-parse --short HEAD')
|
|
120
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.35",
|
|
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
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
75
75
|
"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",
|
|
76
76
|
"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/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/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
77
|
-
"test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
|
|
77
|
+
"test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
|
|
78
78
|
"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",
|
|
79
79
|
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
|
|
80
80
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
package/scripts/easy-update.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
3
5
|
import { spawnSync } from 'node:child_process'
|
|
4
6
|
|
|
5
7
|
const args = new Set(process.argv.slice(2))
|
|
@@ -68,12 +70,33 @@ function getLatestStableTag() {
|
|
|
68
70
|
return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function stopRunningServer() {
|
|
74
|
+
// Try to stop the SwarmClaw server gracefully before updating.
|
|
75
|
+
// An unclean shutdown while npm rebuild replaces native modules
|
|
76
|
+
// can corrupt the SQLite WAL journal on Linux.
|
|
77
|
+
const cliPath = path.join(cwd, 'bin', 'swarmclaw.js')
|
|
78
|
+
if (!fs.existsSync(cliPath)) return
|
|
79
|
+
|
|
80
|
+
const status = run('node', [cliPath, 'status'])
|
|
81
|
+
if (!status.ok || !status.out.toLowerCase().includes('running')) return
|
|
82
|
+
|
|
83
|
+
log('Stopping running SwarmClaw server before update...')
|
|
84
|
+
const stop = run('node', [cliPath, 'stop'])
|
|
85
|
+
if (stop.ok) {
|
|
86
|
+
log('Server stopped.')
|
|
87
|
+
} else {
|
|
88
|
+
log('Could not stop the server automatically. Please stop it manually before updating.')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function main() {
|
|
72
93
|
const gitCheck = run('git', ['rev-parse', '--is-inside-work-tree'])
|
|
73
94
|
if (!gitCheck.ok) {
|
|
74
95
|
fail('This folder is not a git repository. Automatic updates require git.')
|
|
75
96
|
}
|
|
76
97
|
|
|
98
|
+
stopRunningServer()
|
|
99
|
+
|
|
77
100
|
const dirty = run('git', ['status', '--porcelain'])
|
|
78
101
|
const isDirty = !!dirty.out
|
|
79
102
|
if (isDirty && !allowDirty) {
|
|
@@ -12,7 +12,8 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
12
12
|
const secret = secrets[id]
|
|
13
13
|
if (!secret) return notFound()
|
|
14
14
|
// Never expose the encrypted value
|
|
15
|
-
const
|
|
15
|
+
const safe = { ...(secret as Record<string, unknown>) }
|
|
16
|
+
delete safe.encryptedValue
|
|
16
17
|
return NextResponse.json(safe)
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -36,6 +37,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
36
37
|
return secret
|
|
37
38
|
})
|
|
38
39
|
if (!result) return notFound()
|
|
39
|
-
const
|
|
40
|
+
const safe = { ...(result as Record<string, unknown>) }
|
|
41
|
+
delete safe.encryptedValue
|
|
40
42
|
return NextResponse.json(safe)
|
|
41
43
|
}
|
|
@@ -3,7 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
4
|
import { NextResponse } from 'next/server'
|
|
5
5
|
import { DATA_DIR } from '@/lib/server/data-dir'
|
|
6
|
-
import { loadAgents, loadCredentials, loadSettings } from '@/lib/server/storage'
|
|
6
|
+
import { loadAgents, loadCredentials, loadSettings, loadCollection } from '@/lib/server/storage'
|
|
7
7
|
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
8
8
|
import { detectDocker } from '@/lib/server/sandbox/docker-detect'
|
|
9
9
|
|
|
@@ -160,6 +160,7 @@ export async function GET(req: Request) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const credentials = Object.values(loadCredentials() || {})
|
|
163
|
+
const credentialIds = new Set(credentials.map((c) => (c as Record<string, unknown>).id as string).filter(Boolean))
|
|
163
164
|
if (credentials.length > 0) {
|
|
164
165
|
pushCheck(checks, 'credentials', 'Credentials', 'pass', `${credentials.length} credential(s) saved.`)
|
|
165
166
|
} else {
|
|
@@ -167,6 +168,35 @@ export async function GET(req: Request) {
|
|
|
167
168
|
actions.push('If using cloud providers, add an API key in the setup wizard or Settings → Providers.')
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
// Check for undecryptable credentials (CREDENTIAL_SECRET may have changed)
|
|
172
|
+
if (credentials.length > 0) {
|
|
173
|
+
try {
|
|
174
|
+
const { resolveCredentialSecret } = await import('@/lib/server/credentials/credential-service')
|
|
175
|
+
let undecryptable = 0
|
|
176
|
+
for (const cred of credentials) {
|
|
177
|
+
const c = cred as Record<string, unknown>
|
|
178
|
+
if (c.encryptedKey && !resolveCredentialSecret(c.id as string)) undecryptable++
|
|
179
|
+
}
|
|
180
|
+
if (undecryptable > 0) {
|
|
181
|
+
pushCheck(checks, 'credential-decrypt', 'Credential decryption', 'fail',
|
|
182
|
+
`${undecryptable} of ${credentials.length} credential(s) cannot be decrypted. CREDENTIAL_SECRET may have changed since these keys were stored.`, true)
|
|
183
|
+
actions.push('Your CREDENTIAL_SECRET changed (possibly from a container restart). Re-add your API keys in Settings → Providers.')
|
|
184
|
+
}
|
|
185
|
+
} catch { /* best-effort */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for dangling credential references in gateway profiles
|
|
189
|
+
try {
|
|
190
|
+
const gateways = Object.values(loadCollection('gateway_profiles') || {}) as Array<Record<string, unknown>>
|
|
191
|
+
const dangling = gateways.filter((gw) => gw.credentialId && !credentialIds.has(gw.credentialId as string))
|
|
192
|
+
if (dangling.length > 0) {
|
|
193
|
+
const names = dangling.map((gw) => gw.name || gw.id).join(', ')
|
|
194
|
+
pushCheck(checks, 'gateway-credentials', 'Gateway credential references', 'warn',
|
|
195
|
+
`${dangling.length} gateway profile(s) reference missing credentials: ${names}. This causes "gateway token missing" errors.`)
|
|
196
|
+
actions.push('Re-add the missing API key in Settings → Gateways, or update the gateway profile to use an existing credential.')
|
|
197
|
+
}
|
|
198
|
+
} catch { /* best-effort */ }
|
|
199
|
+
|
|
170
200
|
const optionalBinaries: Array<{ id: string; label: string; command: string }> = [
|
|
171
201
|
{ id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
|
|
172
202
|
{ id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { execSync } from 'child_process'
|
|
3
|
+
import { getDb } from '@/lib/server/storage'
|
|
3
4
|
|
|
4
5
|
const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/
|
|
5
6
|
|
|
@@ -7,6 +8,19 @@ function run(cmd: string): string {
|
|
|
7
8
|
return execSync(cmd, { encoding: 'utf-8', cwd: process.cwd(), timeout: 60_000 }).trim()
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Checkpoint the SQLite WAL before operations that replace native modules
|
|
13
|
+
* (npm install rebuilds better-sqlite3). Without this, an unclean WAL state
|
|
14
|
+
* combined with a replaced native binary can corrupt the database on Linux.
|
|
15
|
+
*/
|
|
16
|
+
function checkpointDatabase(): void {
|
|
17
|
+
try {
|
|
18
|
+
getDb().pragma('wal_checkpoint(TRUNCATE)')
|
|
19
|
+
} catch {
|
|
20
|
+
// Best-effort — the database may already be in a good state.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
function getLatestStableTag(): string | null {
|
|
11
25
|
const tags = run(`git tag --list 'v*' --sort=-v:refname`)
|
|
12
26
|
.split('\n')
|
|
@@ -67,6 +81,9 @@ export async function POST() {
|
|
|
67
81
|
try {
|
|
68
82
|
const diff = run(`git diff --name-only ${beforeSha}..HEAD`)
|
|
69
83
|
if (diff.includes('package-lock.json') || diff.includes('package.json')) {
|
|
84
|
+
// Checkpoint WAL before npm install — the postinstall hook rebuilds
|
|
85
|
+
// better-sqlite3's native module, which can corrupt an open WAL journal.
|
|
86
|
+
checkpointDatabase()
|
|
70
87
|
run('npm install --omit=dev')
|
|
71
88
|
installedDeps = true
|
|
72
89
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
import { isOllamaCloudEndpoint, normalizeOllamaMode, resolveStoredOllamaMode } from './ollama-mode'
|
|
3
|
+
import { isOllamaCloudEndpoint, normalizeOllamaCloudEndpoint, normalizeOllamaMode, resolveStoredOllamaMode } from './ollama-mode'
|
|
4
4
|
|
|
5
5
|
test('normalizeOllamaMode only accepts explicit local and cloud values', () => {
|
|
6
6
|
assert.equal(normalizeOllamaMode('local'), 'local')
|
|
@@ -31,3 +31,20 @@ test('resolveStoredOllamaMode falls back to endpoint only for legacy records', (
|
|
|
31
31
|
assert.equal(resolveStoredOllamaMode({ apiEndpoint: 'http://localhost:11434' }), 'local')
|
|
32
32
|
assert.equal(resolveStoredOllamaMode({}), 'local')
|
|
33
33
|
})
|
|
34
|
+
|
|
35
|
+
test('normalizeOllamaCloudEndpoint rewrites api.ollama.com to ollama.com', () => {
|
|
36
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://api.ollama.com'), 'https://ollama.com')
|
|
37
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://api.ollama.com/v1'), 'https://ollama.com/v1')
|
|
38
|
+
assert.equal(normalizeOllamaCloudEndpoint('http://api.ollama.com'), 'http://ollama.com')
|
|
39
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://www.ollama.com'), 'https://ollama.com')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('normalizeOllamaCloudEndpoint preserves correct ollama.com URLs', () => {
|
|
43
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://ollama.com'), 'https://ollama.com')
|
|
44
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://ollama.com/v1'), 'https://ollama.com/v1')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('normalizeOllamaCloudEndpoint does not mangle non-Ollama endpoints', () => {
|
|
48
|
+
assert.equal(normalizeOllamaCloudEndpoint('http://localhost:11434'), 'http://localhost:11434')
|
|
49
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://api.openai.com/v1'), 'https://api.openai.com/v1')
|
|
50
|
+
})
|
package/src/lib/ollama-mode.ts
CHANGED
|
@@ -18,6 +18,14 @@ export function isOllamaCloudEndpoint(endpoint: string | null | undefined): bool
|
|
|
18
18
|
return /^https?:\/\/(?:www\.|api\.)?ollama\.com(?:\/|$)/i.test(normalized)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Normalize an Ollama Cloud endpoint to avoid the api.ollama.com -> ollama.com
|
|
23
|
+
* 301 redirect which drops the Authorization header.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeOllamaCloudEndpoint(endpoint: string): string {
|
|
26
|
+
return endpoint.replace(/^(https?:\/\/)(?:www\.|api\.)?ollama\.com/i, '$1ollama.com')
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export function resolveStoredOllamaMode(input: {
|
|
22
30
|
ollamaMode?: string | null
|
|
23
31
|
apiEndpoint?: string | null
|
|
@@ -165,7 +165,11 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
165
165
|
if (data === '[DONE]') continue
|
|
166
166
|
try {
|
|
167
167
|
const parsed = JSON.parse(data)
|
|
168
|
-
const
|
|
168
|
+
const choice = parsed.choices?.[0]?.delta
|
|
169
|
+
const delta = choice?.content
|
|
170
|
+
// Thinking/reasoning models (kimi-k2, etc.) put output in reasoning fields
|
|
171
|
+
|| choice?.reasoning_content
|
|
172
|
+
|| choice?.reasoning
|
|
169
173
|
if (delta) {
|
|
170
174
|
fullResponse += delta
|
|
171
175
|
writeSSE(write, 'd', delta)
|
|
@@ -453,12 +453,13 @@ async function invokeSessionTool(
|
|
|
453
453
|
})
|
|
454
454
|
|
|
455
455
|
try {
|
|
456
|
-
const
|
|
456
|
+
const lowerName = toolName.toLowerCase()
|
|
457
|
+
const directTool = tools.find((t) => t?.name === toolName || t?.name?.toLowerCase() === lowerName) as StructuredToolInterface | undefined
|
|
457
458
|
const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
|
|
458
459
|
const translated = directTool
|
|
459
|
-
? { toolName, args }
|
|
460
|
+
? { toolName: directTool.name, args }
|
|
460
461
|
: translateRequestedToolInvocation(toolName, args, ctx.message, availableToolNames)
|
|
461
|
-
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
|
|
462
|
+
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName || t?.name?.toLowerCase() === translated.toolName.toLowerCase()) as StructuredToolInterface | undefined
|
|
462
463
|
if (!selectedTool?.invoke) {
|
|
463
464
|
const resolvedName = translated.toolName !== toolName ? translated.toolName : null
|
|
464
465
|
const unavailableReason = resolvedName === 'delegate'
|
|
@@ -469,18 +470,19 @@ async function invokeSessionTool(
|
|
|
469
470
|
return { invoked: false, responseOverride: null, unavailableReason }
|
|
470
471
|
}
|
|
471
472
|
|
|
473
|
+
const resolvedToolName = selectedTool.name
|
|
472
474
|
const toolCallId = genId()
|
|
473
|
-
ctx.emit({ t: 'tool_call', toolName, toolInput: JSON.stringify(translated.args), toolCallId })
|
|
475
|
+
ctx.emit({ t: 'tool_call', toolName: resolvedToolName, toolInput: JSON.stringify(translated.args), toolCallId })
|
|
474
476
|
const toolOutput = await selectedTool.invoke(translated.args)
|
|
475
477
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
476
|
-
ctx.emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
|
|
478
|
+
ctx.emit({ t: 'tool_result', toolName: resolvedToolName, toolOutput: outputText, toolCallId })
|
|
477
479
|
|
|
478
480
|
const delegateResponse = (
|
|
479
|
-
|
|
480
|
-
||
|
|
481
|
+
resolvedToolName === 'delegate'
|
|
482
|
+
|| resolvedToolName.startsWith('delegate_to_')
|
|
481
483
|
) ? extractDelegateResponse(outputText) : null
|
|
482
484
|
|
|
483
|
-
calledNames.add(
|
|
485
|
+
calledNames.add(resolvedToolName)
|
|
484
486
|
|
|
485
487
|
if (delegateResponse) {
|
|
486
488
|
return { invoked: true, responseOverride: delegateResponse, toolOutputText: outputText }
|
|
@@ -475,7 +475,7 @@ export async function buildProactiveMemorySection(
|
|
|
475
475
|
)
|
|
476
476
|
sections.push(`## Recalled Context\nRelevant memories from previous interactions:\n${recalledLines.join('\n')}`)
|
|
477
477
|
if (knowledgeTrace?.hits.length) {
|
|
478
|
-
const groundingLines = knowledgeTrace.hits.map((hit) =>
|
|
478
|
+
const groundingLines = knowledgeTrace.hits.slice(0, 10).map((hit) =>
|
|
479
479
|
`- [${hit.chunkIndex + 1}/${hit.chunkCount}] ${hit.sourceTitle}: ${hit.snippet}`,
|
|
480
480
|
)
|
|
481
481
|
sections.push(`## Source Grounding\nSource-backed knowledge retrieved for this turn:\n${groundingLines.join('\n')}`)
|
|
@@ -488,7 +488,7 @@ export async function buildProactiveMemorySection(
|
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
if (knowledgeTrace?.hits.length) {
|
|
491
|
-
const groundingLines = knowledgeTrace.hits.map((hit) =>
|
|
491
|
+
const groundingLines = knowledgeTrace.hits.slice(0, 10).map((hit) =>
|
|
492
492
|
`- [${hit.chunkIndex + 1}/${hit.chunkCount}] ${hit.sourceTitle}: ${hit.snippet}`,
|
|
493
493
|
)
|
|
494
494
|
return {
|
|
@@ -695,7 +695,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
695
695
|
(result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
|
|
696
696
|
)
|
|
697
697
|
}
|
|
698
|
-
} catch {
|
|
698
|
+
} catch (compactErr) { log.warn(TAG, `Auto-compaction failed for ${session.id}:`, compactErr) }
|
|
699
699
|
|
|
700
700
|
// Truncate oversized assistant messages in history to prevent context blowout
|
|
701
701
|
const HISTORY_MSG_MAX_CHARS = 8_000
|
|
@@ -787,7 +787,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
787
787
|
if (warning) {
|
|
788
788
|
prompt = joinPromptSegments(warning, prompt)
|
|
789
789
|
}
|
|
790
|
-
} catch {
|
|
790
|
+
} catch (degradeErr) { log.warn(TAG, `Context degradation check failed for ${session.id}:`, degradeErr) }
|
|
791
791
|
|
|
792
792
|
await runCapabilityHook(
|
|
793
793
|
'llmInput',
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
loadCredentials,
|
|
10
10
|
saveCredential,
|
|
11
11
|
} from '@/lib/server/credentials/credential-repository'
|
|
12
|
+
import { log } from '@/lib/server/logger'
|
|
13
|
+
|
|
14
|
+
const TAG = 'credential-service'
|
|
12
15
|
|
|
13
16
|
export type CredentialSummary = Pick<Credential, 'id' | 'provider' | 'name' | 'createdAt'>
|
|
14
17
|
|
|
@@ -55,7 +58,12 @@ export function resolveCredentialSecret(credentialId: string | null | undefined)
|
|
|
55
58
|
if (!credential?.encryptedKey) return null
|
|
56
59
|
try {
|
|
57
60
|
return decryptKey(credential.encryptedKey)
|
|
58
|
-
} catch {
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.warn(TAG, `Failed to decrypt credential "${id}" — CREDENTIAL_SECRET may have changed since this key was stored. Re-add the API key to fix.`, {
|
|
63
|
+
credentialId: id,
|
|
64
|
+
provider: credential.provider,
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
})
|
|
59
67
|
return null
|
|
60
68
|
}
|
|
61
69
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stripOllamaCloudModelSuffix } from '@/lib/ollama-model'
|
|
2
|
-
import { isOllamaCloudEndpoint, resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
2
|
+
import { isOllamaCloudEndpoint, normalizeOllamaCloudEndpoint, resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
3
3
|
import { PROVIDER_DEFAULTS } from '@/lib/providers/provider-defaults'
|
|
4
4
|
|
|
5
5
|
const OLLAMA_CLOUD_KEY_ENV_VARS = ['OLLAMA_API_KEY', 'OLLAMA_CLOUD_API_KEY'] as const
|
|
@@ -41,7 +41,7 @@ export function resolveOllamaRuntimeConfig(input: {
|
|
|
41
41
|
const cloudApiKey = resolveOllamaCloudApiKey(explicitApiKey)
|
|
42
42
|
const useCloud = ollamaMode === 'cloud'
|
|
43
43
|
const endpoint = useCloud
|
|
44
|
-
? (isOllamaCloudEndpoint(explicitEndpoint) ? explicitEndpoint! : PROVIDER_DEFAULTS.ollamaCloud)
|
|
44
|
+
? normalizeOllamaCloudEndpoint(isOllamaCloudEndpoint(explicitEndpoint) ? explicitEndpoint! : PROVIDER_DEFAULTS.ollamaCloud)
|
|
45
45
|
: (explicitEndpoint && !isOllamaCloudEndpoint(explicitEndpoint) ? explicitEndpoint : PROVIDER_DEFAULTS.ollama)
|
|
46
46
|
|
|
47
47
|
return {
|
|
@@ -81,7 +81,14 @@ function normalizeWsUrl(raw: string): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function resolveTokenForCredential(credentialId?: string | null): string | undefined {
|
|
84
|
-
|
|
84
|
+
if (!credentialId) return undefined
|
|
85
|
+
const secret = resolveCredentialSecret(credentialId)
|
|
86
|
+
if (!secret) {
|
|
87
|
+
log.warn(TAG, `Credential "${credentialId}" is referenced but could not be resolved — gateway connection will lack a token`, {
|
|
88
|
+
credentialId,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
return secret || undefined
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
export function resolveGatewayConfig(target?: {
|
|
@@ -305,8 +305,10 @@ export async function pingAnthropic(apiKey: string): Promise<{ ok: boolean; mess
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
export async function pingOllama(endpoint: string, apiKey?: string): Promise<{ ok: boolean; message: string }> {
|
|
308
|
-
|
|
308
|
+
let normalizedEndpoint = (endpoint || 'http://localhost:11434').replace(/\/+$/, '')
|
|
309
309
|
const useCloud = isOllamaCloudEndpoint(normalizedEndpoint)
|
|
310
|
+
// Normalize api.ollama.com -> ollama.com to avoid 301 redirect that drops auth
|
|
311
|
+
if (useCloud) normalizedEndpoint = normalizedEndpoint.replace(/^(https?:\/\/)(?:www\.|api\.)?ollama\.com/i, '$1ollama.com')
|
|
310
312
|
if (useCloud && !apiKey) {
|
|
311
313
|
return { ok: false, message: 'Ollama Cloud requires an API key.' }
|
|
312
314
|
}
|
|
@@ -319,10 +319,11 @@ export function buildAgentHeartbeatPrompt(
|
|
|
319
319
|
agent: HeartbeatPromptAgent | null | undefined,
|
|
320
320
|
fallbackPrompt: string,
|
|
321
321
|
heartbeatFileContent: string,
|
|
322
|
-
opts?: { approvals?: Record<string, ApprovalRequest>; chatrooms?: Record<string, Chatroom
|
|
322
|
+
opts?: { approvals?: Record<string, ApprovalRequest>; chatrooms?: Record<string, Chatroom>; lightContext?: boolean },
|
|
323
323
|
): string {
|
|
324
324
|
if (!agent) return fallbackPrompt
|
|
325
325
|
|
|
326
|
+
const light = opts?.lightContext === true
|
|
326
327
|
const sections: string[] = []
|
|
327
328
|
|
|
328
329
|
// ── Phase 1: Identity context ──
|
|
@@ -331,11 +332,11 @@ export function buildAgentHeartbeatPrompt(
|
|
|
331
332
|
const identityContext = buildIdentityContext(session, agent)
|
|
332
333
|
const continuityContext = buildIdentityContinuityContext(session, agent)
|
|
333
334
|
if (identityContext) sections.push(identityContext)
|
|
334
|
-
if (continuityContext) sections.push(continuityContext)
|
|
335
|
+
if (!light && continuityContext) sections.push(continuityContext)
|
|
335
336
|
const description = agent.description || ''
|
|
336
337
|
const soul = agent.soul || ''
|
|
337
338
|
if (description) sections.push(`Description: ${description}`)
|
|
338
|
-
if (soul) sections.push(`Persona: ${soul.slice(0, 300)}`)
|
|
339
|
+
if (!light && soul) sections.push(`Persona: ${soul.slice(0, 300)}`)
|
|
339
340
|
|
|
340
341
|
// ── Phase 2: Pending approvals ──
|
|
341
342
|
const agentId = agent.id || session.agentId || ''
|
|
@@ -346,7 +347,7 @@ export function buildAgentHeartbeatPrompt(
|
|
|
346
347
|
(a) => a.status === 'pending' && a.agentId === agentId,
|
|
347
348
|
)
|
|
348
349
|
if (pending.length > 0) {
|
|
349
|
-
const approvalLines = pending.slice(0, 5).map(
|
|
350
|
+
const approvalLines = pending.slice(0, light ? 2 : 5).map(
|
|
350
351
|
(a) => `- [${a.category}] ${a.title}${a.description ? `: ${a.description.slice(0, 100)}` : ''}`,
|
|
351
352
|
)
|
|
352
353
|
sections.push(`### Pending Approvals (${pending.length})\n${approvalLines.join('\n')}`)
|
|
@@ -366,7 +367,7 @@ export function buildAgentHeartbeatPrompt(
|
|
|
366
367
|
const dynamicGoal = agent.heartbeatGoal || ''
|
|
367
368
|
const dynamicNextAction = agent.heartbeatNextAction || ''
|
|
368
369
|
const systemPrompt = agent.systemPrompt || ''
|
|
369
|
-
const goalSummary = systemPrompt.slice(0, 500)
|
|
370
|
+
const goalSummary = systemPrompt.slice(0, light ? 200 : 500)
|
|
370
371
|
|
|
371
372
|
if (dynamicGoal) {
|
|
372
373
|
sections.push(`Current goal (self-set): ${dynamicGoal}`)
|
|
@@ -377,12 +378,13 @@ export function buildAgentHeartbeatPrompt(
|
|
|
377
378
|
|
|
378
379
|
const strippedContent = stripBlockedItems(heartbeatFileContent)
|
|
379
380
|
const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(strippedContent) ? '' : strippedContent
|
|
380
|
-
if (effectiveFileContent) sections.push(`\nHEARTBEAT.md contents:\n${effectiveFileContent.slice(0, 2000)}`)
|
|
381
|
+
if (effectiveFileContent) sections.push(`\nHEARTBEAT.md contents:\n${effectiveFileContent.slice(0, light ? 500 : 2000)}`)
|
|
381
382
|
|
|
383
|
+
const messageCount = light ? 2 : 5
|
|
382
384
|
const recentMessages = (
|
|
383
385
|
Array.isArray(session.messages)
|
|
384
|
-
? session.messages.slice(-
|
|
385
|
-
: (session.id ? getRecentMessages(session.id,
|
|
386
|
+
? session.messages.slice(-messageCount)
|
|
387
|
+
: (session.id ? getRecentMessages(session.id, messageCount) : [])
|
|
386
388
|
) as HeartbeatPromptMessage[]
|
|
387
389
|
const recentContext = recentMessages
|
|
388
390
|
.map((m) => {
|
|
@@ -395,8 +397,8 @@ export function buildAgentHeartbeatPrompt(
|
|
|
395
397
|
.join('\n')
|
|
396
398
|
if (recentContext) sections.push(`Recent conversation:\n${recentContext}`)
|
|
397
399
|
|
|
398
|
-
// ── Phase 4b: Chatroom mentions since last heartbeat ──
|
|
399
|
-
try {
|
|
400
|
+
// ── Phase 4b: Chatroom mentions since last heartbeat (skip in light mode) ──
|
|
401
|
+
if (!light) try {
|
|
400
402
|
const chatrooms = Object.values(opts?.chatrooms ?? loadChatrooms()) as Chatroom[]
|
|
401
403
|
const myChatrooms = chatrooms.filter((c) => !c.archivedAt && c.agentIds?.includes(agentId))
|
|
402
404
|
if (myChatrooms.length > 0) {
|
|
@@ -718,6 +720,7 @@ export async function tickHeartbeats() {
|
|
|
718
720
|
const baseHeartbeatMessage = buildAgentHeartbeatPrompt(session, agent, cfg.prompt, heartbeatFileContent, {
|
|
719
721
|
approvals: sharedApprovals,
|
|
720
722
|
chatrooms: sharedChatrooms,
|
|
723
|
+
lightContext: cfg.lightContext,
|
|
721
724
|
})
|
|
722
725
|
let heartbeatMessage = isMainSession(session)
|
|
723
726
|
? buildMainLoopHeartbeatPrompt(session, baseHeartbeatMessage)
|
|
@@ -6,6 +6,8 @@ import { loadSettings, loadSession, loadAgent, loadMcpServers, patchAgent, patch
|
|
|
6
6
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
7
7
|
import { log } from '../logger'
|
|
8
8
|
import { resolveSessionToolPolicy } from '../tool-capability-policy'
|
|
9
|
+
import { truncateToolResultText, calculateMaxToolResultChars } from '../chat-execution/tool-result-guard'
|
|
10
|
+
import { getContextWindowSize } from '../context-manager'
|
|
9
11
|
import { expandExtensionIds } from '../tool-aliases'
|
|
10
12
|
import type { ToolContext, SessionToolsResult, ToolBuildContext, AbortSignalRef } from './context'
|
|
11
13
|
|
|
@@ -455,9 +457,17 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
455
457
|
}
|
|
456
458
|
const effectiveArgs = hookResult.input ?? guardedArgs
|
|
457
459
|
const result = await candidate.invoke(effectiveArgs ?? {})
|
|
458
|
-
const
|
|
460
|
+
const rawOutput = typeof result === 'string' ? result : JSON.stringify(result)
|
|
461
|
+
// Truncate oversized tool outputs before LangGraph feeds them back to
|
|
462
|
+
// the LLM. Without this, a single large tool result (e.g. shell dump,
|
|
463
|
+
// large web fetch) can blow out the context window inside LangGraph's
|
|
464
|
+
// internal state, which the auto-compaction system cannot observe.
|
|
465
|
+
const currentSession = resolveCurrentSession()
|
|
466
|
+
const maxChars = calculateMaxToolResultChars(getContextWindowSize(currentSession?.provider || '', currentSession?.model || ''))
|
|
467
|
+
const outputText = truncateToolResultText(rawOutput, maxChars)
|
|
459
468
|
setSpanAttributes(span, {
|
|
460
469
|
'swarmclaw.tool.output_bytes': Buffer.byteLength(outputText, 'utf-8'),
|
|
470
|
+
...(rawOutput.length !== outputText.length ? { 'swarmclaw.tool.truncated_from': rawOutput.length } : {}),
|
|
461
471
|
})
|
|
462
472
|
await runCapabilityHook(
|
|
463
473
|
'afterToolExec',
|
|
@@ -94,6 +94,21 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
94
94
|
}
|
|
95
95
|
db.pragma('foreign_keys = ON')
|
|
96
96
|
|
|
97
|
+
// Graceful shutdown: checkpoint WAL and close the database to prevent
|
|
98
|
+
// corruption when the process is killed (e.g. during npm run update:easy).
|
|
99
|
+
if (!IS_BUILD_BOOTSTRAP) {
|
|
100
|
+
const shutdownDb = () => {
|
|
101
|
+
try {
|
|
102
|
+
db.pragma('wal_checkpoint(TRUNCATE)')
|
|
103
|
+
db.close()
|
|
104
|
+
} catch {
|
|
105
|
+
// Best-effort — process is exiting.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
process.on('SIGTERM', shutdownDb)
|
|
109
|
+
process.on('SIGINT', shutdownDb)
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
/** Run a function inside an immediate SQLite transaction for atomicity. */
|
|
98
113
|
export function withTransaction<T>(fn: () => T): T {
|
|
99
114
|
const wrapped = db.transaction(fn)
|
package/src/proxy.ts
CHANGED
|
@@ -87,13 +87,19 @@ export function proxy(request: NextRequest) {
|
|
|
87
87
|
})
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
//
|
|
90
|
+
// A2A endpoints use their own authentication (Authorization: Bearer / x-a2a-access-key)
|
|
91
|
+
const isA2ARoute = pathname === '/api/a2a'
|
|
92
|
+
|| pathname.startsWith('/api/a2a/')
|
|
93
|
+
|| pathname === '/api/.well-known/agent-card'
|
|
94
|
+
|
|
95
|
+
// Only protect API routes (not auth, inbound webhooks, or A2A)
|
|
91
96
|
if (
|
|
92
97
|
!pathname.startsWith('/api/')
|
|
93
98
|
|| pathname === '/api/auth'
|
|
94
99
|
|| pathname === '/api/healthz'
|
|
95
100
|
|| isWebhookTrigger
|
|
96
101
|
|| isConnectorWebhook
|
|
102
|
+
|| isA2ARoute
|
|
97
103
|
) {
|
|
98
104
|
return NextResponse.next()
|
|
99
105
|
}
|