Skip to content

Commit 217f5bd

Browse files
feat: add server restart coordination and health status tracking (#300)
* feat: add server restart coordination and health status tracking * refactor: consolidate server restart logic, extract hooks, remove dead code
1 parent 68cd6b5 commit 217f5bd

19 files changed

Lines changed: 802 additions & 259 deletions

backend/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import { migrateGlobalSkills } from './services/skills'
5050
import { installAssistantWorkspace } from './services/assistant-mode'
5151
import { getOpenCodeImportStatus, syncOpenCodeImport } from './services/opencode-import'
5252
import { OpenCodeSupervisor } from './services/opencode-supervisor'
53+
import { OpenCodeRestartCoordinator } from './services/opencode-restart-coordinator'
54+
import { setOpenCodeRestartCoordinator } from './services/opencode-restart'
5355
import { OpenCodeConfigSchema } from '@opencode-manager/shared/schemas'
5456
import { parse as parseJsonc } from 'jsonc-parser'
5557
import { getModelStatePath, ModelStateSchema } from './routes/providers'
@@ -320,6 +322,13 @@ sseAggregator.setPendingActionsFetcher(openCodeClient)
320322
sseAggregator.setPasswordResolver(() => new SettingsService(db).getOpenCodeServerPassword())
321323
sseAggregator.start()
322324

325+
sseAggregator.setScheduledSessionsResolver(
326+
() => scheduleService.getActiveRunSessionIds(),
327+
)
328+
329+
const openCodeRestartCoordinator = new OpenCodeRestartCoordinator(openCodeClient, sseAggregator)
330+
setOpenCodeRestartCoordinator(openCodeRestartCoordinator)
331+
323332
void scheduleRunnerInstance.start()
324333

325334
const settingsService = new SettingsService(db)

backend/src/routes/oauth.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,9 @@ import {
77
OAuthAuthorizeResponseSchema,
88
OAuthCallbackRequestSchema
99
} from '../../../shared/src/schemas/auth'
10-
import { opencodeServerManager } from '../services/opencode-single-server'
10+
import { reloadOpenCodeConfig } from '../services/opencode-restart'
1111
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
1212

13-
async function reloadOpenCodeConfig(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
14-
if (openCodeSupervisor) {
15-
await openCodeSupervisor.reloadConfig('settings_reload')
16-
return
17-
}
18-
19-
await opencodeServerManager.reloadConfig()
20-
}
21-
2213
export function createOAuthRoutes(openCodeClient: OpenCodeClient, openCodeSupervisor?: OpenCodeSupervisor) {
2314
const app = new Hono()
2415

backend/src/routes/providers.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AuthService } from '../services/auth'
55
import { SetCredentialRequestSchema } from '../../../shared/src/schemas/auth'
66
import { logger } from '../utils/logger'
77
import type { OpenCodeClient } from '../services/opencode/client'
8-
import { opencodeServerManager } from '../services/opencode-single-server'
8+
import { reloadOpenCodeConfig } from '../services/opencode-restart'
99
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
1010
import type { Database } from 'bun:sqlite'
1111
import { getWorkspacePath } from '@opencode-manager/shared/config/env'
@@ -54,15 +54,6 @@ async function mirrorModelStateToFile(state: OpenCodeModelStateRecord): Promise<
5454
}
5555
}
5656

57-
async function reloadOpenCodeConfig(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
58-
if (openCodeSupervisor) {
59-
await openCodeSupervisor.reloadConfig('settings_reload')
60-
return
61-
}
62-
63-
await opencodeServerManager.reloadConfig()
64-
}
65-
6657
export function createProvidersRoutes(db: Database, openCodeClient: OpenCodeClient, openCodeSupervisor?: OpenCodeSupervisor) {
6758
const app = new Hono()
6859
const authService = new AuthService()

backend/src/routes/repos.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as repoService from '../services/repo'
88
import * as archiveService from '../services/archive'
99
import { SettingsService } from '../services/settings'
1010
import { writeFileContent } from '../services/file-operations'
11-
import { opencodeServerManager } from '../services/opencode-single-server'
11+
import { restartOpenCode } from '../services/opencode-restart'
1212
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
1313
import type { OpenCodeClient } from '../services/opencode/client'
1414
import { logger } from '../utils/logger'
@@ -22,16 +22,6 @@ import { ScheduleService } from '../services/schedules'
2222
import { ensureAssistantMode, getAssistantModeStatus, buildAssistantRepo } from '../services/assistant-mode'
2323
import path from 'path'
2424

25-
async function restartOpenCode(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
26-
if (openCodeSupervisor) {
27-
await openCodeSupervisor.restart('settings_restart')
28-
return
29-
}
30-
31-
opencodeServerManager.clearStartupError()
32-
await opencodeServerManager.restart()
33-
}
34-
3525
function resolveRepo(database: Database, id: number): Repo | null {
3626
return getRepoById(database, id) ?? (id === ASSISTANT_REPO_ID ? buildAssistantRepo() : null)
3727
}

backend/src/routes/settings.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { opencodeServerManager } from '../services/opencode-single-server'
1111
import type { GitAuthService } from '../services/git-auth'
1212
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
1313
import { createStubOpenCodeClient } from '../../test/helpers/stub-opencode-client'
14+
import type { OpenCodeRestartCoordinator, ResumableSession } from '../services/opencode-restart-coordinator'
15+
import { setOpenCodeRestartCoordinator } from '../services/opencode-restart'
1416

1517
interface TestUserPreferenceRow {
1618
preferences: string
@@ -450,3 +452,67 @@ describe('settings routes — opencode model discovery', () => {
450452
expect(data.models).toEqual([])
451453
})
452454
})
455+
456+
describe('settings routes — restart coordinator wiring', () => {
457+
let db: Database
458+
let app: Hono
459+
460+
beforeEach(() => {
461+
db = createTestDb()
462+
})
463+
464+
afterEach(() => {
465+
db.close()
466+
setOpenCodeRestartCoordinator(null)
467+
})
468+
469+
it('GET /opencode-active-sessions returns count and sessions from coordinator', async () => {
470+
const fakeCoordinator = {
471+
captureResumableSessions: vi.fn(() => [{
472+
sessionID: 's1',
473+
directory: '/a',
474+
} satisfies ResumableSession]),
475+
abortSessions: vi.fn(),
476+
resumeSessions: vi.fn(),
477+
runWithResume: vi.fn(),
478+
} as unknown as OpenCodeRestartCoordinator
479+
setOpenCodeRestartCoordinator(fakeCoordinator)
480+
481+
app = createTestApp(db)
482+
const res = await app.request('/settings/opencode-active-sessions')
483+
expect(res.status).toBe(200)
484+
const body = (await res.json()) as { count: number; sessions: ResumableSession[] }
485+
expect(body.count).toBe(1)
486+
expect(body.sessions).toEqual([{ sessionID: 's1', directory: '/a' }])
487+
expect(fakeCoordinator.captureResumableSessions).toHaveBeenCalledTimes(1)
488+
})
489+
490+
it('GET /opencode-active-sessions returns empty when no coordinator', async () => {
491+
setOpenCodeRestartCoordinator(null)
492+
app = createTestApp(db)
493+
const res = await app.request('/settings/opencode-active-sessions')
494+
expect(res.status).toBe(200)
495+
const body = (await res.json()) as { count: number; sessions: ResumableSession[] }
496+
expect(body.count).toBe(0)
497+
expect(body.sessions).toEqual([])
498+
})
499+
500+
it('POST /opencode-restart routes through coordinator.runWithResume and returns resumedSessions', async () => {
501+
const runWithResume = vi.fn().mockResolvedValue({ healthy: true, resumedSessionIDs: ['s1'] })
502+
const fakeCoordinator = {
503+
captureResumableSessions: vi.fn(() => []),
504+
abortSessions: vi.fn(),
505+
resumeSessions: vi.fn(),
506+
runWithResume,
507+
} as unknown as OpenCodeRestartCoordinator
508+
setOpenCodeRestartCoordinator(fakeCoordinator)
509+
510+
app = createTestApp(db)
511+
const res = await app.request('/settings/opencode-restart', { method: 'POST' })
512+
expect(res.status).toBe(200)
513+
const body = (await res.json()) as { success: boolean; resumedSessions: string[] }
514+
expect(body.success).toBe(true)
515+
expect(body.resumedSessions).toEqual(['s1'])
516+
expect(runWithResume).toHaveBeenCalledTimes(1)
517+
})
518+
})

backend/src/routes/settings.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { opencodeServerManager, ConfigReloadError } from '../services/opencode-s
3131
import { getOrCreateInternalToken, rotateInternalToken } from '../services/internal-token'
3232
import { sseAggregator } from '../services/sse-aggregator'
3333
import type { OpenCodeSupervisor } from '../services/opencode-supervisor'
34+
import { restartOpenCode, reloadOpenCodeConfig, getOpenCodeRestartCoordinator } from '../services/opencode-restart'
3435
import type { GitAuthService } from '../services/git-auth'
3536
import { DEFAULT_AGENTS_MD } from '../constants'
3637
import { validateSSHPrivateKey } from '../utils/ssh-validation'
@@ -92,25 +93,6 @@ function getOpenCodeConfigContentToWrite(
9293
return JSON.stringify(appliedConfig, null, 2)
9394
}
9495

95-
async function reloadOpenCodeConfig(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
96-
if (openCodeSupervisor) {
97-
await openCodeSupervisor.reloadConfig('settings_reload')
98-
return
99-
}
100-
101-
await opencodeServerManager.reloadConfig()
102-
}
103-
104-
async function restartOpenCode(openCodeSupervisor?: OpenCodeSupervisor): Promise<void> {
105-
if (openCodeSupervisor) {
106-
await openCodeSupervisor.restart('settings_restart')
107-
return
108-
}
109-
110-
opencodeServerManager.clearStartupError()
111-
await opencodeServerManager.restart()
112-
}
113-
11496
async function restartOpenCodeSafe(openCodeSupervisor: OpenCodeSupervisor | undefined, context: string): Promise<void> {
11597
try {
11698
await restartOpenCode(openCodeSupervisor)
@@ -728,8 +710,12 @@ export function createSettingsRoutes(db: Database, gitAuthService: GitAuthServic
728710
try {
729711
logger.info('Manual OpenCode server restart requested')
730712
opencodeServerManager.clearStartupError()
731-
await restartOpenCode(openCodeSupervisor)
732-
return c.json({ success: true, message: 'OpenCode server restarted successfully' })
713+
const { resumedSessionIDs } = await restartOpenCode(openCodeSupervisor)
714+
return c.json({
715+
success: true,
716+
message: 'OpenCode server restarted successfully',
717+
resumedSessions: resumedSessionIDs,
718+
})
733719
} catch (error) {
734720
logger.error('Failed to restart OpenCode server:', error)
735721
const startupError = opencodeServerManager.getLastStartupError()
@@ -1879,6 +1865,11 @@ export function createSettingsRoutes(db: Database, gitAuthService: GitAuthServic
18791865
}
18801866
})
18811867

1868+
app.get('/opencode-active-sessions', (c) => {
1869+
const sessions = getOpenCodeRestartCoordinator()?.captureResumableSessions() ?? []
1870+
return c.json({ count: sessions.length, sessions })
1871+
})
1872+
18821873
app.get('/opencode-discover-models', async (c) => {
18831874
try {
18841875
const baseUrl = c.req.query('baseUrl')

0 commit comments

Comments
 (0)