better-codex 0.1.3 → 0.2.0
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/apps/backend/README.md +43 -2
- package/apps/backend/src/core/app-server.ts +68 -2
- package/apps/backend/src/server.ts +157 -1
- package/apps/backend/src/services/codex-config.ts +561 -0
- package/apps/backend/src/thread-activity/service.ts +47 -0
- package/apps/web/README.md +18 -2
- package/apps/web/src/components/layout/codex-settings.tsx +1208 -0
- package/apps/web/src/components/layout/settings-dialog.tsx +9 -1
- package/apps/web/src/components/layout/virtualized-message-list.tsx +581 -86
- package/apps/web/src/config.ts +24 -0
- package/apps/web/src/hooks/use-hub-connection.ts +21 -3
- package/apps/web/src/hooks/use-thread-history.ts +94 -5
- package/apps/web/src/services/hub-client.ts +103 -5
- package/apps/web/src/types/index.ts +24 -0
- package/apps/web/src/utils/item-format.ts +55 -9
- package/package.json +1 -1
package/apps/backend/README.md
CHANGED
|
@@ -3,16 +3,31 @@
|
|
|
3
3
|
Local backend that supervises multiple `codex app-server` processes and exposes a
|
|
4
4
|
WebSocket bridge for the web UI.
|
|
5
5
|
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- Bun
|
|
9
|
+
- Codex CLI available in `PATH` (or configure `CODEX_BIN`)
|
|
10
|
+
|
|
6
11
|
## Setup
|
|
7
12
|
|
|
8
13
|
1. Install dependencies
|
|
9
14
|
```bash
|
|
10
15
|
bun install
|
|
11
16
|
```
|
|
12
|
-
2. Run the server
|
|
17
|
+
2. Run the server (hot reload)
|
|
13
18
|
```bash
|
|
14
19
|
bun run dev
|
|
15
20
|
```
|
|
21
|
+
3. Or run a single instance
|
|
22
|
+
```bash
|
|
23
|
+
bun run start
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Data locations
|
|
27
|
+
|
|
28
|
+
- Analytics, reviews, and thread index live in `CODEX_HUB_DATA_DIR` (defaults to `~/.codex-hub`).
|
|
29
|
+
- Profiles metadata stored at `CODEX_HUB_DATA_DIR/profiles.json`.
|
|
30
|
+
- Per-profile Codex homes are created under `CODEX_HUB_PROFILES_DIR` (defaults to `~/.codex/profiles`).
|
|
16
31
|
|
|
17
32
|
## Protocol types
|
|
18
33
|
|
|
@@ -30,17 +45,43 @@ bun run generate:protocol
|
|
|
30
45
|
- `CODEX_HUB_DATA_DIR` (default: `~/.codex-hub`)
|
|
31
46
|
- `CODEX_HUB_PROFILES_DIR` (default: `~/.codex/profiles`)
|
|
32
47
|
- `CODEX_HUB_DEFAULT_CODEX_HOME` (default: `~/.codex`)
|
|
48
|
+
- `CODEX_HUB_DEFAULT_CWD` (default: workspace root or process `cwd`)
|
|
49
|
+
- `CODEX_DEFAULT_CWD` (fallback for default cwd)
|
|
33
50
|
- `CODEX_BIN` (default: `codex`)
|
|
34
51
|
- `CODEX_FLAGS` (space-delimited)
|
|
35
52
|
- `CODEX_FLAGS_JSON` (JSON array, preferred)
|
|
36
53
|
- `CODEX_APP_SERVER_FLAGS`
|
|
37
54
|
- `CODEX_APP_SERVER_FLAGS_JSON`
|
|
55
|
+
- `CODEX_HUB_APP_SERVER_STARTUP_TIMEOUT_MS` (default: `15000`)
|
|
56
|
+
- `CODEX_HUB_DEBUG_ROUTES=1` (log non-404 route errors)
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
- Prefer `CODEX_FLAGS_JSON` and `CODEX_APP_SERVER_FLAGS_JSON` to avoid shell quoting issues.
|
|
60
|
+
- `CODEX_HUB_TOKEN` should be set if you want a stable token across restarts.
|
|
38
61
|
|
|
39
62
|
## Endpoints
|
|
40
63
|
|
|
41
64
|
- `GET /health`
|
|
65
|
+
- `GET /config` (returns `{ token }` for the web UI)
|
|
66
|
+
- `GET /analytics/daily?metric=turns_started&days=365&profileId=...&model=...`
|
|
67
|
+
- `GET /reviews?profileId=...&limit=100&offset=0`
|
|
42
68
|
- `GET /profiles`
|
|
43
|
-
- `POST /profiles`
|
|
69
|
+
- `POST /profiles` (optional `{ name }`)
|
|
44
70
|
- `POST /profiles/:profileId/start`
|
|
45
71
|
- `POST /profiles/:profileId/stop`
|
|
72
|
+
- `DELETE /profiles/:profileId`
|
|
73
|
+
- `GET /profiles/:profileId/prompts` (returns prompt names + optional descriptions)
|
|
74
|
+
- `GET /profiles/:profileId/prompts/:name` (returns file contents)
|
|
75
|
+
- `GET /profiles/:profileId/config` (returns config + parsed MCP servers)
|
|
76
|
+
- `PUT /profiles/:profileId/config` (body `{ content: string }`)
|
|
77
|
+
- `PUT /profiles/:profileId/mcp-servers` (body `{ servers: McpServerConfig[] }`)
|
|
78
|
+
- `GET /threads/search?q=...&profileId=...&model=...&status=active|archived&createdAfter=...&createdBefore=...&limit=...&offset=...`
|
|
79
|
+
- `GET /threads/active?profileId=...`
|
|
80
|
+
- `POST /threads/reindex` (body `{ profileId?, limit?, autoStart? }`)
|
|
46
81
|
- `WS /ws?token=...`
|
|
82
|
+
|
|
83
|
+
## Troubleshooting
|
|
84
|
+
|
|
85
|
+
- "Missing hub token" in the web UI: ensure the backend is running and `GET /config` is reachable.
|
|
86
|
+
- "app-server startup timed out": raise `CODEX_HUB_APP_SERVER_STARTUP_TIMEOUT_MS` or run the CLI once manually to warm caches.
|
|
87
|
+
- Profiles not starting: verify the Codex CLI is in `PATH` or set `CODEX_BIN`.
|
|
@@ -31,12 +31,18 @@ export class CodexAppServer extends EventEmitter {
|
|
|
31
31
|
private connection?: JsonRpcConnection
|
|
32
32
|
private ready: Promise<void>
|
|
33
33
|
private resolveReady?: () => void
|
|
34
|
+
private readonly startupTimeoutMs: number
|
|
35
|
+
private readonly startupStderr: string[] = []
|
|
36
|
+
private readonly maxStartupStderr = 20
|
|
37
|
+
private startupComplete = false
|
|
34
38
|
|
|
35
39
|
constructor(private readonly options: CodexAppServerOptions) {
|
|
36
40
|
super()
|
|
37
41
|
this.ready = new Promise((resolve) => {
|
|
38
42
|
this.resolveReady = resolve
|
|
39
43
|
})
|
|
44
|
+
const parsed = Number(process.env.CODEX_HUB_APP_SERVER_STARTUP_TIMEOUT_MS ?? 15000)
|
|
45
|
+
this.startupTimeoutMs = Number.isFinite(parsed) ? Math.max(parsed, 0) : 15000
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
async start(): Promise<void> {
|
|
@@ -73,6 +79,12 @@ export class CodexAppServer extends EventEmitter {
|
|
|
73
79
|
this.emit('serverRequest', message)
|
|
74
80
|
})
|
|
75
81
|
this.connection.on('stderr', (message) => {
|
|
82
|
+
if (!this.startupComplete) {
|
|
83
|
+
this.startupStderr.push(message)
|
|
84
|
+
if (this.startupStderr.length > this.maxStartupStderr) {
|
|
85
|
+
this.startupStderr.shift()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
76
88
|
this.emit('stderr', message)
|
|
77
89
|
})
|
|
78
90
|
this.connection.on('error', (error) => {
|
|
@@ -82,8 +94,17 @@ export class CodexAppServer extends EventEmitter {
|
|
|
82
94
|
this.emit('exit', code)
|
|
83
95
|
})
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
try {
|
|
98
|
+
await this.initializeWithTimeout()
|
|
99
|
+
this.startupComplete = true
|
|
100
|
+
this.resolveReady?.()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.process?.kill()
|
|
103
|
+
this.process = undefined
|
|
104
|
+
this.connection = undefined
|
|
105
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
async request(method: string, params?: unknown): Promise<unknown> {
|
|
@@ -126,6 +147,51 @@ export class CodexAppServer extends EventEmitter {
|
|
|
126
147
|
})
|
|
127
148
|
this.connection.sendNotification('initialized', {})
|
|
128
149
|
}
|
|
150
|
+
|
|
151
|
+
private async initializeWithTimeout(): Promise<void> {
|
|
152
|
+
if (this.startupTimeoutMs <= 0) {
|
|
153
|
+
await this.initialize()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
if (!this.process) {
|
|
157
|
+
throw new Error('missing app-server process')
|
|
158
|
+
}
|
|
159
|
+
let timeoutId: NodeJS.Timeout | null = null
|
|
160
|
+
const cleanup: Array<() => void> = []
|
|
161
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
162
|
+
timeoutId = setTimeout(() => {
|
|
163
|
+
reject(new Error(`app-server startup timed out after ${this.startupTimeoutMs}ms`))
|
|
164
|
+
}, this.startupTimeoutMs)
|
|
165
|
+
})
|
|
166
|
+
const exitPromise = new Promise<never>((_, reject) => {
|
|
167
|
+
const onExit = (code: number | null) => {
|
|
168
|
+
reject(new Error(`app-server exited before initialize${code !== null ? ` (code ${code})` : ''}`))
|
|
169
|
+
}
|
|
170
|
+
this.process?.once('exit', onExit)
|
|
171
|
+
cleanup.push(() => this.process?.removeListener('exit', onExit))
|
|
172
|
+
})
|
|
173
|
+
const errorPromise = new Promise<never>((_, reject) => {
|
|
174
|
+
const onError = (error: Error) => reject(error)
|
|
175
|
+
this.process?.once('error', onError)
|
|
176
|
+
cleanup.push(() => this.process?.removeListener('error', onError))
|
|
177
|
+
})
|
|
178
|
+
try {
|
|
179
|
+
await Promise.race([this.initialize(), timeoutPromise, exitPromise, errorPromise])
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof Error && error.message.includes('startup timed out')) {
|
|
182
|
+
const stderr = this.startupStderr.join('\n')
|
|
183
|
+
if (stderr) {
|
|
184
|
+
throw new Error(`${error.message}\n\n${stderr}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
throw error
|
|
188
|
+
} finally {
|
|
189
|
+
if (timeoutId) {
|
|
190
|
+
clearTimeout(timeoutId)
|
|
191
|
+
}
|
|
192
|
+
cleanup.forEach((fn) => fn())
|
|
193
|
+
}
|
|
194
|
+
}
|
|
129
195
|
}
|
|
130
196
|
|
|
131
197
|
export type CodexAppServerEventsMap = CodexAppServerEvents
|
|
@@ -5,10 +5,12 @@ import { join, extname, basename } from 'node:path'
|
|
|
5
5
|
import { loadConfig } from './config'
|
|
6
6
|
import { CodexSupervisor } from './services/supervisor'
|
|
7
7
|
import { ProfileStore } from './services/profile-store'
|
|
8
|
+
import { readProfileConfig, updateProfileMcpServers, writeProfileConfig, type McpServerConfig } from './services/codex-config'
|
|
8
9
|
import { AnalyticsStore } from './analytics/store'
|
|
9
10
|
import { AnalyticsService } from './analytics/service'
|
|
10
11
|
import { ThreadIndexStore } from './thread-index/store'
|
|
11
12
|
import { ThreadIndexService, type ThreadListItem } from './thread-index/service'
|
|
13
|
+
import { ThreadActivityService } from './thread-activity/service'
|
|
12
14
|
import { ReviewService } from './reviews/service'
|
|
13
15
|
import { ReviewStore } from './reviews/store'
|
|
14
16
|
import type { WsEvent, WsRequest, WsResponse } from './ws/messages'
|
|
@@ -24,6 +26,7 @@ const analytics = new AnalyticsService(new AnalyticsStore(join(config.dataDir, '
|
|
|
24
26
|
analytics.init()
|
|
25
27
|
const threadIndex = new ThreadIndexService(new ThreadIndexStore(join(config.dataDir, 'threads.sqlite')))
|
|
26
28
|
threadIndex.init()
|
|
29
|
+
const threadActivity = new ThreadActivityService()
|
|
27
30
|
const reviews = new ReviewService(new ReviewStore(join(config.dataDir, 'reviews.sqlite')))
|
|
28
31
|
reviews.init()
|
|
29
32
|
|
|
@@ -36,9 +39,27 @@ const sendWsEvent = (event: WsEvent) => {
|
|
|
36
39
|
clients.forEach((client) => client.send(payload))
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
const shouldLogRouteErrors =
|
|
43
|
+
process.env.CODEX_HUB_DEBUG_ROUTES === '1' || process.env.NODE_ENV !== 'production'
|
|
44
|
+
|
|
45
|
+
const formatError = (error: unknown) => {
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
return error.stack ?? `${error.name}: ${error.message}`
|
|
48
|
+
}
|
|
49
|
+
return String(error)
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
const app = new Elysia()
|
|
40
53
|
.use(cors({ origin: true }))
|
|
54
|
+
.onError(({ code, error, request }) => {
|
|
55
|
+
if (!shouldLogRouteErrors || code === 'NOT_FOUND') {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
console.error(`[HubBackend] ${request.method} ${request.url} -> ${code}`)
|
|
59
|
+
console.error(formatError(error))
|
|
60
|
+
})
|
|
41
61
|
.get('/health', () => ({ ok: true }))
|
|
62
|
+
.get('/config', () => ({ token: config.authToken }))
|
|
42
63
|
.get(
|
|
43
64
|
'/analytics/daily',
|
|
44
65
|
({ query }) => {
|
|
@@ -214,6 +235,94 @@ const app = new Elysia()
|
|
|
214
235
|
}),
|
|
215
236
|
}
|
|
216
237
|
)
|
|
238
|
+
.get(
|
|
239
|
+
'/profiles/:profileId/config',
|
|
240
|
+
async ({ params }) => {
|
|
241
|
+
const profile = profileStore.get(params.profileId)
|
|
242
|
+
if (!profile) {
|
|
243
|
+
return new Response('Profile not found', { status: 404 })
|
|
244
|
+
}
|
|
245
|
+
const snapshot = await readProfileConfig(profile.codexHome)
|
|
246
|
+
return {
|
|
247
|
+
path: snapshot.path,
|
|
248
|
+
codexHome: profile.codexHome,
|
|
249
|
+
content: snapshot.content,
|
|
250
|
+
mcpServers: snapshot.mcpServers,
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
params: t.Object({
|
|
255
|
+
profileId: t.String(),
|
|
256
|
+
}),
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
.put(
|
|
260
|
+
'/profiles/:profileId/config',
|
|
261
|
+
async ({ params, body }) => {
|
|
262
|
+
const profile = profileStore.get(params.profileId)
|
|
263
|
+
if (!profile) {
|
|
264
|
+
return new Response('Profile not found', { status: 404 })
|
|
265
|
+
}
|
|
266
|
+
if (!body || typeof body !== 'object' || typeof (body as { content?: string }).content !== 'string') {
|
|
267
|
+
return new Response('Invalid config payload', { status: 400 })
|
|
268
|
+
}
|
|
269
|
+
const snapshot = await writeProfileConfig(profile.codexHome, (body as { content: string }).content)
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
path: snapshot.path,
|
|
273
|
+
codexHome: profile.codexHome,
|
|
274
|
+
content: snapshot.content,
|
|
275
|
+
mcpServers: snapshot.mcpServers,
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
params: t.Object({
|
|
280
|
+
profileId: t.String(),
|
|
281
|
+
}),
|
|
282
|
+
body: t.Object({
|
|
283
|
+
content: t.String(),
|
|
284
|
+
}),
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
.put(
|
|
288
|
+
'/profiles/:profileId/mcp-servers',
|
|
289
|
+
async ({ params, body }) => {
|
|
290
|
+
const profile = profileStore.get(params.profileId)
|
|
291
|
+
if (!profile) {
|
|
292
|
+
return new Response('Profile not found', { status: 404 })
|
|
293
|
+
}
|
|
294
|
+
if (!body || typeof body !== 'object' || !Array.isArray((body as { servers?: McpServerConfig[] }).servers)) {
|
|
295
|
+
return new Response('Invalid MCP server payload', { status: 400 })
|
|
296
|
+
}
|
|
297
|
+
const servers = (body as { servers: McpServerConfig[] }).servers
|
|
298
|
+
const seenNames = new Set<string>()
|
|
299
|
+
for (const server of servers) {
|
|
300
|
+
if (!server?.name || !/^[a-zA-Z0-9_-]+$/.test(server.name)) {
|
|
301
|
+
return new Response('Invalid MCP server name', { status: 400 })
|
|
302
|
+
}
|
|
303
|
+
if (seenNames.has(server.name)) {
|
|
304
|
+
return new Response('Duplicate MCP server name', { status: 400 })
|
|
305
|
+
}
|
|
306
|
+
seenNames.add(server.name)
|
|
307
|
+
}
|
|
308
|
+
const snapshot = await updateProfileMcpServers(profile.codexHome, servers)
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
path: snapshot.path,
|
|
312
|
+
codexHome: profile.codexHome,
|
|
313
|
+
content: snapshot.content,
|
|
314
|
+
mcpServers: snapshot.mcpServers,
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
params: t.Object({
|
|
319
|
+
profileId: t.String(),
|
|
320
|
+
}),
|
|
321
|
+
body: t.Object({
|
|
322
|
+
servers: t.Array(t.Any()),
|
|
323
|
+
}),
|
|
324
|
+
}
|
|
325
|
+
)
|
|
217
326
|
.get(
|
|
218
327
|
'/threads/search',
|
|
219
328
|
({ query }) => {
|
|
@@ -250,6 +359,18 @@ const app = new Elysia()
|
|
|
250
359
|
}),
|
|
251
360
|
}
|
|
252
361
|
)
|
|
362
|
+
.get(
|
|
363
|
+
'/threads/active',
|
|
364
|
+
({ query }) => {
|
|
365
|
+
const profileId = typeof query.profileId === 'string' ? query.profileId : undefined
|
|
366
|
+
return { threads: threadActivity.list(profileId) }
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
query: t.Object({
|
|
370
|
+
profileId: t.Optional(t.String()),
|
|
371
|
+
}),
|
|
372
|
+
}
|
|
373
|
+
)
|
|
253
374
|
.post(
|
|
254
375
|
'/threads/reindex',
|
|
255
376
|
async ({ body }) => {
|
|
@@ -350,6 +471,7 @@ const app = new Elysia()
|
|
|
350
471
|
|
|
351
472
|
if (payload.type === 'profile.stop') {
|
|
352
473
|
await supervisor.stop(payload.profileId)
|
|
474
|
+
threadActivity.clearProfile(payload.profileId)
|
|
353
475
|
const response: WsResponse = {
|
|
354
476
|
type: 'profile.stopped',
|
|
355
477
|
profileId: payload.profileId,
|
|
@@ -379,13 +501,30 @@ const app = new Elysia()
|
|
|
379
501
|
threadIndex.recordThreadStart(payload.profileId, thread)
|
|
380
502
|
}
|
|
381
503
|
if (payload.method === 'thread/resume') {
|
|
382
|
-
const thread = (result as { thread?: ThreadListItem }).thread
|
|
504
|
+
const thread = (result as { thread?: ThreadListItem & { turns?: Array<{ id?: string; status?: string }> } }).thread
|
|
383
505
|
threadIndex.recordThreadResume(payload.profileId, thread)
|
|
506
|
+
if (thread?.id) {
|
|
507
|
+
let activeTurnId: string | null = null
|
|
508
|
+
if (Array.isArray(thread.turns)) {
|
|
509
|
+
for (let index = thread.turns.length - 1; index >= 0; index -= 1) {
|
|
510
|
+
if (thread.turns[index]?.status === 'inProgress') {
|
|
511
|
+
activeTurnId = thread.turns[index]?.id ?? null
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (activeTurnId) {
|
|
517
|
+
threadActivity.markStarted(payload.profileId, thread.id, activeTurnId)
|
|
518
|
+
} else {
|
|
519
|
+
threadActivity.markCompleted(payload.profileId, thread.id)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
384
522
|
}
|
|
385
523
|
if (payload.method === 'thread/archive') {
|
|
386
524
|
const params = payload.params as { threadId?: string } | undefined
|
|
387
525
|
if (params?.threadId) {
|
|
388
526
|
threadIndex.recordThreadArchive(payload.profileId, params.threadId)
|
|
527
|
+
threadActivity.markCompleted(payload.profileId, params.threadId)
|
|
389
528
|
}
|
|
390
529
|
}
|
|
391
530
|
analytics.trackRpcResponse({
|
|
@@ -423,6 +562,18 @@ const app = new Elysia()
|
|
|
423
562
|
})
|
|
424
563
|
|
|
425
564
|
supervisor.on('notification', (event) => {
|
|
565
|
+
if (event.method === 'turn/started' && event.params && typeof event.params === 'object') {
|
|
566
|
+
const { threadId, turn } = event.params as { threadId?: string; turn?: { id?: string } }
|
|
567
|
+
if (threadId) {
|
|
568
|
+
threadActivity.markStarted(event.profileId, threadId, turn?.id ?? null)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (event.method === 'turn/completed' && event.params && typeof event.params === 'object') {
|
|
572
|
+
const { threadId } = event.params as { threadId?: string }
|
|
573
|
+
if (threadId) {
|
|
574
|
+
threadActivity.markCompleted(event.profileId, threadId)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
426
577
|
if (event.method === 'item/started' && event.params && typeof event.params === 'object') {
|
|
427
578
|
const { threadId, turnId, item } = event.params as {
|
|
428
579
|
threadId?: string
|
|
@@ -502,6 +653,7 @@ supervisor.on('diagnostic', (event) => {
|
|
|
502
653
|
})
|
|
503
654
|
|
|
504
655
|
supervisor.on('exit', (event) => {
|
|
656
|
+
threadActivity.clearProfile(event.profileId)
|
|
505
657
|
const wsEvent: WsEvent = {
|
|
506
658
|
type: 'profile.exit',
|
|
507
659
|
profileId: event.profileId,
|
|
@@ -511,6 +663,10 @@ supervisor.on('exit', (event) => {
|
|
|
511
663
|
})
|
|
512
664
|
|
|
513
665
|
supervisor.on('error', (event) => {
|
|
666
|
+
if (shouldLogRouteErrors) {
|
|
667
|
+
console.error(`[HubBackend] profile ${event.profileId} error`)
|
|
668
|
+
console.error(formatError(event.error))
|
|
669
|
+
}
|
|
514
670
|
const wsEvent: WsEvent = {
|
|
515
671
|
type: 'profile.error',
|
|
516
672
|
profileId: event.profileId,
|