@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 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.34",
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": {
@@ -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
- return resolveCredentialSecret(credentialId) || undefined
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)