Skip to content

Commit fdf662b

Browse files
anandgupta42claude
andauthored
fix: resolve TUI worker startup crash from circular dependency (#47)
The TUI showed an empty screen because the worker thread crashed on startup. ModelsDev.refresh() ran at module load time and accessed Installation.USER_AGENT before the Installation module finished initializing (circular import chain: worker.ts → config.ts → models.ts → installation). Deferring the initial refresh via setTimeout(…, 0) ensures all modules are fully loaded first. Also improves worker error logging to extract the actual error message from ErrorEvent (previously logged as useless "[object ErrorEvent]"), and adds an e2e test that spawns the worker and verifies it starts without errors. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dbf8525 commit fdf662b

File tree

3 files changed

+82
-8
lines changed

3 files changed

+82
-8
lines changed

packages/altimate-code/src/cli/cmd/tui/thread.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export const TuiThreadCommand = cmd({
117117
),
118118
})
119119
worker.onerror = (e) => {
120-
Log.Default.error(e)
120+
Log.Default.error(e.message, { error: e.error?.stack ?? e.error ?? String(e) })
121121
}
122122
const client = Rpc.client<typeof rpc>(worker)
123123
process.on("uncaughtException", (e) => {

packages/altimate-code/src/provider/models.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,15 @@ export namespace ModelsDev {
122122
}
123123

124124
if (!Flag.ALTIMATE_CLI_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
125-
ModelsDev.refresh()
126-
setInterval(
127-
async () => {
128-
await ModelsDev.refresh()
129-
},
130-
60 * 1000 * 60,
131-
).unref()
125+
// Defer initial refresh to avoid circular dependency — Installation may not
126+
// be fully initialized when this module is first evaluated.
127+
setTimeout(() => {
128+
ModelsDev.refresh()
129+
setInterval(
130+
async () => {
131+
await ModelsDev.refresh()
132+
},
133+
60 * 1000 * 60,
134+
).unref()
135+
}, 0)
132136
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, test, afterEach } from "bun:test"
2+
import { Rpc } from "../../../src/util/rpc"
3+
import type { rpc } from "../../../src/cli/cmd/tui/worker"
4+
import path from "path"
5+
import { fileURLToPath } from "url"
6+
import { Filesystem } from "../../../src/util/filesystem"
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
9+
const workerSrc = path.resolve(__dirname, "../../../src/cli/cmd/tui/worker.ts")
10+
11+
describe("tui worker", () => {
12+
let worker: Worker | undefined
13+
14+
afterEach(() => {
15+
worker?.terminate()
16+
worker = undefined
17+
})
18+
19+
test("starts without errors", async () => {
20+
const errors: string[] = []
21+
22+
worker = new Worker(workerSrc, {
23+
env: Object.fromEntries(
24+
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
25+
),
26+
})
27+
28+
worker.onerror = (e) => {
29+
errors.push(e.message ?? String(e))
30+
}
31+
32+
// Give the worker time to initialize — module loading,
33+
// top-level awaits, and side effects all run during this window.
34+
await new Promise((r) => setTimeout(r, 3000))
35+
36+
expect(errors).toEqual([])
37+
}, 10_000)
38+
39+
test("responds to RPC calls after startup", async () => {
40+
const errors: string[] = []
41+
42+
worker = new Worker(workerSrc, {
43+
env: Object.fromEntries(
44+
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
45+
),
46+
})
47+
48+
worker.onerror = (e) => {
49+
errors.push(e.message ?? String(e))
50+
}
51+
52+
// Wait for worker to be ready
53+
await new Promise((r) => setTimeout(r, 3000))
54+
expect(errors).toEqual([])
55+
56+
// Verify RPC communication works — the worker exports a `fetch` method
57+
const client = Rpc.client<typeof rpc>(worker)
58+
const result = await Promise.race([
59+
client.call("fetch", {
60+
url: "http://altimate-code.internal/health",
61+
method: "GET",
62+
headers: {},
63+
}),
64+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("RPC timeout")), 5000)),
65+
])
66+
67+
expect(result).toBeDefined()
68+
expect(typeof result.status).toBe("number")
69+
}, 15_000)
70+
})

0 commit comments

Comments
 (0)