Skip to content

Commit a02f7a2

Browse files
Bump version to 0.8.21 (#99)
* Add timeout handling and recovery for OpenCode upgrade/install operations - Extract DEFAULT_AGENTS_MD to constants file - Add 90s timeout for opencode upgrade/install commands - Implement server recovery when upgrade/install fails - Update frontend error handling for recovery status - Add settings route tests * Add centralized query cache invalidation utility * Bump version to 0.8.21
1 parent 3661f0a commit a02f7a2

14 files changed

Lines changed: 1100 additions & 126 deletions

File tree

backend/src/constants.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export const DEFAULT_AGENTS_MD = `# OpenCode Manager - Global Agent Instructions
2+
3+
## Critical System Constraints
4+
5+
- **DO NOT** use ports 5003 or 5551 - these are reserved for OpenCode Manager
6+
- **DO NOT** kill or stop processes on ports 5003 or 5551
7+
- **DO NOT** modify files in the \`.config/opencode\` directory unless explicitly requested
8+
9+
## Dev Server Ports
10+
11+
When starting dev servers, use the pre-allocated ports 5100-5103:
12+
- Port 5100: Primary dev server (frontend)
13+
- Port 5101: Secondary dev server (API/backend)
14+
- Port 5102: Additional service
15+
- Port 5103: Additional service
16+
17+
Always bind to \`0.0.0.0\` to allow external access from the Docker host.
18+
19+
## Package Management
20+
21+
### Node.js Packages
22+
Prefer **pnpm** or **bun** over npm for installing dependencies to save disk space:
23+
- Use \`pnpm install\` instead of \`npm install\`
24+
- Use \`bun install\` as an alternative
25+
- Both are pre-installed in the container
26+
27+
### Python Packages
28+
Always create a virtual environment in the repository directory before installing packages:
29+
30+
1. Create virtual environment in repo:
31+
\`cd \`<repo_path>\`
32+
\`uv venv .venv\`
33+
34+
2. Activate the virtual environment:
35+
\`source .venv/bin/activate\` # or \`uv pip sync\` for project-based workflows
36+
37+
3. Install packages into activated environment:
38+
\`uv pip install \`<package>\`
39+
\`uv pip install -r requirements.txt\`
40+
41+
4. Run Python commands:
42+
\`python script.py\` # Uses activated .venv
43+
44+
Alternative: Use \`uv run python script.py\` to skip explicit activation
45+
46+
**Important:**
47+
- Always create .venv in the repository directory (not workspace root)
48+
- Activate the environment before running pip operations
49+
- uv is pre-installed in the container and provides faster package installation
50+
- .venv directories created in repos will persist but can be removed safely
51+
52+
## General Guidelines
53+
54+
- This file is merged with any AGENTS.md files in individual repositories
55+
- Repository-specific instructions take precedence for their respective codebases
56+
`

backend/src/index.ts

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -60,66 +60,11 @@ const db = initializeDatabase(DB_PATH)
6060
const auth = createAuth(db)
6161
const requireAuth = createAuthMiddleware(auth)
6262

63+
import { DEFAULT_AGENTS_MD } from './constants'
64+
6365
let ipcServer: IPCServer | undefined
6466
const gitAuthService = new GitAuthService()
6567

66-
export const DEFAULT_AGENTS_MD = `# OpenCode Manager - Global Agent Instructions
67-
68-
## Critical System Constraints
69-
70-
- **DO NOT** use ports 5003 or 5551 - these are reserved for OpenCode Manager
71-
- **DO NOT** kill or stop processes on ports 5003 or 5551
72-
- **DO NOT** modify files in the \`.config/opencode\` directory unless explicitly requested
73-
74-
## Dev Server Ports
75-
76-
When starting dev servers, use the pre-allocated ports 5100-5103:
77-
- Port 5100: Primary dev server (frontend)
78-
- Port 5101: Secondary dev server (API/backend)
79-
- Port 5102: Additional service
80-
- Port 5103: Additional service
81-
82-
Always bind to \`0.0.0.0\` to allow external access from the Docker host.
83-
84-
## Package Management
85-
86-
### Node.js Packages
87-
Prefer **pnpm** or **bun** over npm for installing dependencies to save disk space:
88-
- Use \`pnpm install\` instead of \`npm install\`
89-
- Use \`bun install\` as an alternative
90-
- Both are pre-installed in the container
91-
92-
### Python Packages
93-
Always create a virtual environment in the repository directory before installing packages:
94-
95-
1. Create virtual environment in repo:
96-
\`cd \`<repo_path>\`
97-
\`uv venv .venv\`
98-
99-
2. Activate the virtual environment:
100-
\`source .venv/bin/activate\` # or \`uv pip sync\` for project-based workflows
101-
102-
3. Install packages into activated environment:
103-
\`uv pip install \`<package>\`
104-
\`uv pip install -r requirements.txt\`
105-
106-
4. Run Python commands:
107-
\`python script.py\` # Uses activated .venv
108-
109-
Alternative: Use \`uv run python script.py\` to skip explicit activation
110-
111-
**Important:**
112-
- Always create .venv in the repository directory (not workspace root)
113-
- Activate the environment before running pip operations
114-
- uv is pre-installed in the container and provides faster package installation
115-
- .venv directories created in repos will persist but can be removed safely
116-
117-
## General Guidelines
118-
119-
- This file is merged with any AGENTS.md files in individual repositories
120-
- Repository-specific instructions take precedence for their respective codebases
121-
`
122-
12368
async function ensureDefaultConfigExists(): Promise<void> {
12469
const settingsService = new SettingsService(db)
12570
const workspaceConfigPath = getOpenCodeConfigFilePath()

backend/src/routes/settings.ts

Lines changed: 126 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '../types/settings'
1313
import { logger } from '../utils/logger'
1414
import { opencodeServerManager } from '../services/opencode-single-server'
15-
import { DEFAULT_AGENTS_MD } from '../index'
15+
import { DEFAULT_AGENTS_MD } from '../constants'
1616

1717
function compareVersions(v1: string, v2: string): number {
1818
const parts1 = v1.split('.').map(s => Number(s))
@@ -27,6 +27,22 @@ function compareVersions(v1: string, v2: string): number {
2727
return 0
2828
}
2929

30+
function execWithTimeout(command: string, timeoutMs: number): { output: string; timedOut: boolean } {
31+
try {
32+
const output = execSync(command, {
33+
encoding: 'utf8',
34+
timeout: timeoutMs,
35+
killSignal: 'SIGKILL'
36+
})
37+
return { output, timedOut: false }
38+
} catch (error) {
39+
if (error && typeof error === 'object' && 'status' in error && (error as { status: number }).status === null) {
40+
return { output: '', timedOut: true }
41+
}
42+
throw error
43+
}
44+
}
45+
3046
const UpdateSettingsSchema = z.object({
3147
preferences: UserPreferencesSchema.partial(),
3248
})
@@ -376,20 +392,20 @@ export function createSettingsRoutes(db: Database) {
376392
})
377393

378394
app.post('/opencode-upgrade', async (c) => {
379-
try {
380-
logger.info('OpenCode upgrade requested')
381-
382-
const oldVersion = opencodeServerManager.getVersion()
383-
logger.info(`Current OpenCode version: ${oldVersion}`)
395+
const oldVersion = opencodeServerManager.getVersion()
396+
logger.info(`Current OpenCode version: ${oldVersion}`)
384397

385-
logger.info('Running opencode upgrade...')
386-
const upgradeOutput = execSync('opencode upgrade 2>&1', { encoding: 'utf8' })
398+
try {
399+
logger.info('Running opencode upgrade with 90s timeout...')
400+
const { output: upgradeOutput, timedOut } = execWithTimeout('opencode upgrade 2>&1', 90000)
387401
logger.info(`Upgrade output: ${upgradeOutput}`)
388402

389-
await new Promise(r => setTimeout(r, 2000))
403+
if (timedOut) {
404+
logger.warn('OpenCode upgrade timed out after 90 seconds')
405+
throw new Error('Upgrade command timed out after 90 seconds')
406+
}
390407

391408
const newVersion = opencodeServerManager.getVersion() || await opencodeServerManager.fetchVersion()
392-
393409
logger.info(`New OpenCode version: ${newVersion}`)
394410

395411
const upgraded = oldVersion && newVersion && compareVersions(newVersion, oldVersion) > 0
@@ -419,16 +435,58 @@ export function createSettingsRoutes(db: Database) {
419435
success: true,
420436
message: 'OpenCode is already up to date',
421437
oldVersion,
422-
newVersion: oldVersion,
438+
newVersion,
423439
upgraded: false
424440
})
425441
}
426442
} catch (error) {
427443
logger.error('Failed to upgrade OpenCode:', error)
428-
return c.json({
429-
error: 'Failed to upgrade OpenCode',
430-
details: error instanceof Error ? error.message : 'Unknown error'
431-
}, 500)
444+
logger.warn('Attempting to recover OpenCode server...')
445+
446+
let recovered = false
447+
let recoveryMessage = ''
448+
449+
opencodeServerManager.clearStartupError()
450+
try {
451+
await opencodeServerManager.restart()
452+
logger.warn('OpenCode server restarted after upgrade failure')
453+
recovered = true
454+
recoveryMessage = 'Server recovered'
455+
} catch (recoveryError) {
456+
logger.error('Failed to recover OpenCode server:', recoveryError)
457+
recovered = false
458+
recoveryMessage = recoveryError instanceof Error ? recoveryError.message : 'Unknown error'
459+
}
460+
461+
let currentVersion: string | null | undefined = oldVersion
462+
try {
463+
currentVersion = opencodeServerManager.getVersion() || oldVersion
464+
} catch (versionError) {
465+
logger.error('Failed to get version after recovery:', versionError)
466+
currentVersion = oldVersion
467+
}
468+
469+
return c.json(
470+
recovered ? {
471+
success: false,
472+
error: 'Upgrade failed but server recovered',
473+
details: error instanceof Error ? error.message : 'Unknown error',
474+
oldVersion,
475+
newVersion: currentVersion,
476+
upgraded: false,
477+
recovered: true,
478+
recoveryMessage
479+
} : {
480+
error: 'Failed to upgrade OpenCode and could not recover',
481+
details: error instanceof Error ? error.message : 'Unknown error',
482+
oldVersion,
483+
newVersion: currentVersion,
484+
upgraded: false,
485+
recovered: false,
486+
recoveryMessage
487+
},
488+
recovered ? 400 : 500
489+
)
432490
}
433491
})
434492

@@ -479,30 +537,32 @@ export function createSettingsRoutes(db: Database) {
479537
})
480538

481539
app.post('/opencode-install-version', async (c) => {
540+
const oldVersion = opencodeServerManager.getVersion()
541+
logger.info(`Current OpenCode version: ${oldVersion}`)
542+
482543
try {
483544
const body = await c.req.json()
484545
const { version } = z.object({ version: z.string().min(1) }).parse(body)
485-
546+
486547
logger.info(`Installing OpenCode version: ${version}`)
487-
488-
const oldVersion = opencodeServerManager.getVersion()
489-
logger.info(`Current OpenCode version: ${oldVersion}`)
490-
491548
const versionArg = version.startsWith('v') ? version : `v${version}`
492-
logger.info(`Running opencode upgrade ${versionArg}...`)
493-
494-
const upgradeOutput = execSync(`opencode upgrade ${versionArg} 2>&1`, { encoding: 'utf8' })
549+
logger.info(`Running opencode upgrade ${versionArg} with 90s timeout...`)
550+
551+
const { output: upgradeOutput, timedOut } = execWithTimeout(`opencode upgrade ${versionArg} 2>&1`, 90000)
495552
logger.info(`Upgrade output: ${upgradeOutput}`)
496-
497-
await new Promise(r => setTimeout(r, 2000))
498-
553+
554+
if (timedOut) {
555+
logger.warn('OpenCode version install timed out after 90 seconds')
556+
throw new Error('Version install command timed out after 90 seconds')
557+
}
558+
499559
const newVersion = await opencodeServerManager.fetchVersion()
500560
logger.info(`New OpenCode version: ${newVersion}`)
501-
561+
502562
opencodeServerManager.clearStartupError()
503563
await opencodeServerManager.restart()
504564
logger.info('OpenCode server restarted after version change')
505-
565+
506566
return c.json({
507567
success: true,
508568
message: `OpenCode ${oldVersion ? `changed from v${oldVersion} to` : 'installed as'} v${newVersion}`,
@@ -511,10 +571,44 @@ export function createSettingsRoutes(db: Database) {
511571
})
512572
} catch (error) {
513573
logger.error('Failed to install OpenCode version:', error)
514-
return c.json({
515-
error: 'Failed to install OpenCode version',
516-
details: error instanceof Error ? error.message : 'Unknown error'
517-
}, 500)
574+
logger.warn('Attempting to recover OpenCode server...')
575+
576+
let recovered = false
577+
let recoveryMessage = ''
578+
579+
opencodeServerManager.clearStartupError()
580+
try {
581+
await opencodeServerManager.restart()
582+
logger.warn('OpenCode server restarted after install failure')
583+
recovered = true
584+
recoveryMessage = 'Server recovered'
585+
} catch (recoveryError) {
586+
logger.error('Failed to recover OpenCode server:', recoveryError)
587+
recovered = false
588+
recoveryMessage = recoveryError instanceof Error ? recoveryError.message : 'Unknown error'
589+
}
590+
591+
const currentVersion = opencodeServerManager.getVersion() || oldVersion
592+
593+
return c.json(
594+
recovered ? {
595+
success: false,
596+
error: 'Version install failed but server recovered',
597+
details: error instanceof Error ? error.message : 'Unknown error',
598+
oldVersion,
599+
newVersion: currentVersion,
600+
recovered: true,
601+
recoveryMessage
602+
} : {
603+
error: 'Failed to install OpenCode version and could not recover',
604+
details: error instanceof Error ? error.message : 'Unknown error',
605+
oldVersion,
606+
newVersion: currentVersion,
607+
recovered: false,
608+
recoveryMessage
609+
},
610+
recovered ? 400 : 500
611+
)
518612
}
519613
})
520614

0 commit comments

Comments
 (0)