Skip to content

Commit d31f978

Browse files
committed
fix: log friendly message when OpenCode server is offline
1 parent b34cea3 commit d31f978

2 files changed

Lines changed: 107 additions & 4 deletions

File tree

src/session/cache-manager.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ const SYNC_SAFETY_WINDOW_MS = 60_000;
3131
const SYNC_COOLDOWN_MS = 60_000;
3232
const STORAGE_FALLBACK_SCAN_LIMIT = 200;
3333
const SQLITE_FALLBACK_QUERY_LIMIT = 200;
34+
const SERVER_UNAVAILABLE_ERROR_MARKERS = [
35+
"fetch failed",
36+
"econnrefused",
37+
"connection refused",
38+
"connect refused",
39+
];
3440

3541
const EMPTY_CACHE: SessionDirectoryCacheData = {
3642
version: CACHE_VERSION,
@@ -203,6 +209,69 @@ function createVirtualProjectId(worktree: string): string {
203209
return `dir_${hash}`;
204210
}
205211

212+
function hasServerUnavailableMarker(value: string): boolean {
213+
const lower = value.toLowerCase();
214+
return SERVER_UNAVAILABLE_ERROR_MARKERS.some((marker) => lower.includes(marker));
215+
}
216+
217+
function isServerUnavailableError(error: unknown): boolean {
218+
const queue: unknown[] = [error];
219+
const seen = new Set<unknown>();
220+
221+
while (queue.length > 0) {
222+
const current = queue.pop();
223+
224+
if (!current || seen.has(current)) {
225+
continue;
226+
}
227+
228+
seen.add(current);
229+
230+
if (typeof current === "string") {
231+
if (hasServerUnavailableMarker(current)) {
232+
return true;
233+
}
234+
235+
continue;
236+
}
237+
238+
if (current instanceof Error) {
239+
if (hasServerUnavailableMarker(`${current.name}: ${current.message}`)) {
240+
return true;
241+
}
242+
243+
const errorWithCause = current as Error & { cause?: unknown };
244+
if (errorWithCause.cause) {
245+
queue.push(errorWithCause.cause);
246+
}
247+
248+
continue;
249+
}
250+
251+
if (typeof current === "object") {
252+
const value = current as {
253+
code?: unknown;
254+
message?: unknown;
255+
cause?: unknown;
256+
};
257+
258+
if (typeof value.code === "string" && hasServerUnavailableMarker(value.code)) {
259+
return true;
260+
}
261+
262+
if (typeof value.message === "string" && hasServerUnavailableMarker(value.message)) {
263+
return true;
264+
}
265+
266+
if (value.cause) {
267+
queue.push(value.cause);
268+
}
269+
}
270+
}
271+
272+
return false;
273+
}
274+
206275
async function runSync(): Promise<void> {
207276
await ensureCacheLoaded();
208277

@@ -494,7 +563,12 @@ export async function syncSessionDirectoryCache(options?: { force?: boolean }):
494563
lastSyncAttemptAt = Date.now();
495564
})
496565
.catch((error) => {
497-
logger.warn("[SessionCache] Failed to sync sessions cache", error);
566+
if (isServerUnavailableError(error)) {
567+
logger.warn("[SessionCache] OpenCode server is not running. Start it with: opencode serve");
568+
} else {
569+
logger.warn("[SessionCache] Failed to sync sessions cache", error);
570+
}
571+
498572
lastSyncAttemptAt = 0;
499573
})
500574
.finally(() => {

tests/session/cache-manager.test.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import {
1212
warmupSessionDirectoryCache,
1313
} from "../../src/session/cache-manager.js";
1414

15-
const { sessionListMock } = vi.hoisted(() => ({
16-
sessionListMock: vi.fn(),
17-
}));
15+
const { sessionListMock, loggerWarnMock, loggerDebugMock, loggerInfoMock, loggerErrorMock } =
16+
vi.hoisted(() => ({
17+
sessionListMock: vi.fn(),
18+
loggerWarnMock: vi.fn(),
19+
loggerDebugMock: vi.fn(),
20+
loggerInfoMock: vi.fn(),
21+
loggerErrorMock: vi.fn(),
22+
}));
1823

1924
vi.mock("../../src/opencode/client.js", () => ({
2025
opencodeClient: {
@@ -24,6 +29,15 @@ vi.mock("../../src/opencode/client.js", () => ({
2429
},
2530
}));
2631

32+
vi.mock("../../src/utils/logger.js", () => ({
33+
logger: {
34+
debug: loggerDebugMock,
35+
info: loggerInfoMock,
36+
warn: loggerWarnMock,
37+
error: loggerErrorMock,
38+
},
39+
}));
40+
2741
function createSession(directory: string, updated: number) {
2842
return {
2943
id: `ses_${updated}`,
@@ -48,6 +62,7 @@ describe("session/cache-manager", () => {
4862
setRuntimeMode("installed");
4963
await loadSettings();
5064
sessionListMock.mockReset();
65+
loggerWarnMock.mockReset();
5166
__resetSessionDirectoryCacheForTests();
5267
});
5368

@@ -118,6 +133,20 @@ describe("session/cache-manager", () => {
118133
expect(directories.map((item) => item.worktree)).toEqual(["D:/repo-c", "D:/repo-a"]);
119134
});
120135

136+
it("logs friendly message when server is not running during warmup sync", async () => {
137+
sessionListMock.mockResolvedValueOnce({
138+
data: null,
139+
error: new TypeError("fetch failed"),
140+
});
141+
142+
await warmupSessionDirectoryCache();
143+
144+
expect(loggerWarnMock).toHaveBeenCalledTimes(1);
145+
expect(loggerWarnMock).toHaveBeenCalledWith(
146+
"[SessionCache] OpenCode server is not running. Start it with: opencode serve",
147+
);
148+
});
149+
121150
it("updates existing directory with newer timestamp", async () => {
122151
await upsertSessionDirectory("D:/repo-a", 1_700_000_000_100);
123152
await upsertSessionDirectory("D:/repo-a", 1_700_000_000_900);

0 commit comments

Comments
 (0)