@swarmclawai/swarmclaw 1.5.34 → 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 +8 -0
- package/bin/update-cmd.js +26 -0
- package/package.json +1 -1
- package/scripts/easy-update.mjs +23 -0
- package/src/app/api/setup/doctor/route.ts +31 -1
- package/src/app/api/version/update/route.ts +17 -0
- package/src/lib/server/credentials/credential-service.ts +9 -1
- package/src/lib/server/openclaw/gateway.ts +8 -1
- package/src/lib/server/storage.ts +15 -0
package/README.md
CHANGED
|
@@ -375,6 +375,14 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
375
375
|
|
|
376
376
|
## Releases
|
|
377
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
|
+
|
|
378
386
|
### v1.5.34 Highlights
|
|
379
387
|
|
|
380
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.
|
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": {
|
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) {
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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?: {
|
|
@@ -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)
|