Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Avoid mock-heavy tests that verify implementation details rather than behavior.
- Use `using` declarations (or equivalent disposables) for processes, file handles, etc., to ensure cleanup even on errors.
- Centralize magic constants under `src/constants/`; share them instead of duplicating values across layers.
- Never repeat constant values (like keybinds) in comments—they become stale when the constant changes.
- **Avoid `void asyncFn()`** - fire-and-forget async calls hide race conditions. When state is observable by other code (in-memory cache, event emitters), ensure visibility order matches invariants. If memory and disk must stay in sync, persist before updating memory so observers see consistent state.

## Component State & Storage

Expand Down
23 changes: 19 additions & 4 deletions src/node/services/initStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export class InitStateManager extends EventEmitter {
/**
* 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 ensures
* the invariant: if init-end is visible (live or replay), the file MUST exist.
*/
async endInit(workspaceId: string, exitCode: number): Promise<void> {
const state = this.store.getState(workspaceId);
Expand All @@ -207,13 +211,24 @@ export class InitStateManager extends EventEmitter {
}

const endTime = Date.now();
state.status = exitCode === 0 ? "success" : "error";
const finalStatus = exitCode === 0 ? "success" : "error";

// Create complete state for persistence (don't mutate in-memory state yet)
const stateToPerist: InitHookState = {
...state,
status: finalStatus,
exitCode,
endTime,
};

// Persist FIRST - ensures file exists before in-memory state shows completion
await this.store.persist(workspaceId, stateToPerist);

// NOW update in-memory state (replay will now see file exists)
state.status = finalStatus;
state.exitCode = exitCode;
state.endTime = endTime;

// Persist to disk (fire-and-forget, errors logged internally by EventStore)
await this.store.persist(workspaceId, state);

log.info(
`Init hook ${state.status} for workspace ${workspaceId} (exit code ${exitCode}, duration ${endTime - state.startTime}ms)`
);
Expand Down