* Finalize init hook execution. * Updates state, persists to disk, emits init-end event, and resolves completion promise. * * IMPORTANT: We persist BEFORE updating in-memory exitCode to prevent a race condition * where replay() sees exitCode !== null but the file doesn't exist yet. This
(workspaceId: string, exitCode: number)
| 283 | * the invariant: if init-end is visible (live or replay), the file MUST exist. |
| 284 | */ |
| 285 | async endInit(workspaceId: string, exitCode: number): Promise<void> { |
| 286 | const state = this.store.getState(workspaceId); |
| 287 | |
| 288 | if (!state) { |
| 289 | log.error(`endInit called for workspace ${workspaceId} with no active init state`); |
| 290 | return; |
| 291 | } |
| 292 | |
| 293 | const endTime = Date.now(); |
| 294 | const finalStatus = exitCode === 0 ? "success" : "error"; |
| 295 | |
| 296 | // Create complete state for persistence (don't mutate in-memory state yet) |
| 297 | const stateToPerist: InitHookState = { |
| 298 | ...state, |
| 299 | status: finalStatus, |
| 300 | exitCode, |
| 301 | endTime, |
| 302 | }; |
| 303 | |
| 304 | // Persist FIRST - ensures file exists before in-memory state shows completion |
| 305 | await this.store.persist(workspaceId, stateToPerist, { |
| 306 | // If WorkspaceService.remove() cleared init state, do not recreate ~/.mux/sessions/<id>/ |
| 307 | shouldWrite: () => this.store.hasState(workspaceId), |
| 308 | }); |
| 309 | |
| 310 | // NOW update in-memory state (replay will now see file exists) |
| 311 | state.status = finalStatus; |
| 312 | state.exitCode = exitCode; |
| 313 | state.endTime = endTime; |
| 314 | |
| 315 | log.info( |
| 316 | `Init hook ${state.status} for workspace ${workspaceId} (exit code ${exitCode}, duration ${endTime - state.startTime}ms)` |
| 317 | ); |
| 318 | |
| 319 | // Emit init-end event |
| 320 | this.emit("init-end", { |
| 321 | type: "init-end", |
| 322 | workspaceId, |
| 323 | exitCode, |
| 324 | timestamp: endTime, |
| 325 | // Include truncation info so frontend can show indicator |
| 326 | ...(state.truncatedLines ? { truncatedLines: state.truncatedLines } : {}), |
| 327 | } satisfies WorkspaceInitEvent & { workspaceId: string }); |
| 328 | |
| 329 | // Resolve completion promise for waiting tools |
| 330 | const promiseEntry = this.initPromises.get(workspaceId); |
| 331 | if (promiseEntry) { |
| 332 | promiseEntry.resolve(); |
| 333 | this.initPromises.delete(workspaceId); |
| 334 | } |
| 335 | |
| 336 | // Keep state in memory for replay (unlike streams which delete immediately) |
| 337 | } |
| 338 | |
| 339 | /** |
| 340 | * Get current in-memory init state for a workspace. |
no test coverage detected