Skip to content

Commit 110962b

Browse files
authored
🤖 Add defensive checks for init-output and message processing (#413)
## Problem After #228 merged, encountered two issues: 1. **Init-output crash**: `StreamingMessageAggregator.ts:516` throws `Cannot read properties of undefined (reading 'trimEnd')` when processing init-output events with missing `line` field 2. **Inconsistent message appearance**: User messages sometimes don't appear immediately in chat view ## Solution ### 1. Init-output defensive check Added null check for `data.line` before calling `trimEnd()`: ```typescript if (!data.line) return; // Defensive: skip events with missing line data ``` Follows the defensive pattern from #228 - gracefully handle edge cases during replay. ### 2. Message processing condition tightened Added `'role' in data` check to the caught-up branch: ```typescript } else if (isCaughtUp && "role" in data) { ``` Makes the condition symmetric with the buffering branch and ensures only valid CmuxMessages are processed. ## Testing - ✅ `make typecheck` passes - ✅ All 763 unit tests pass _Generated with `cmux`_
1 parent 824e131 commit 110962b

File tree

2 files changed

+30
-4
lines changed

2 files changed

+30
-4
lines changed

src/stores/WorkspaceStore.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -964,13 +964,24 @@ export class WorkspaceStore {
964964
const historicalMsgs = this.historicalMessages.get(workspaceId) ?? [];
965965
historicalMsgs.push(data);
966966
this.historicalMessages.set(workspaceId, historicalMsgs);
967-
} else if (isCaughtUp) {
967+
} else if (isCaughtUp && "role" in data) {
968968
// Process live events immediately (after history loaded)
969+
// Check for role field to ensure this is a CmuxMessage
969970
aggregator.handleMessage(data);
970971
this.states.bump(workspaceId);
971972
this.checkAndBumpRecencyIfChanged();
973+
} else if ("role" in data || "type" in data) {
974+
// Unexpected: message with role/type field didn't match any condition
975+
console.error("[WorkspaceStore] Message not processed - unexpected state", {
976+
workspaceId,
977+
isCaughtUp,
978+
hasRole: "role" in data,
979+
hasType: "type" in data,
980+
type: "type" in data ? (data as { type: string }).type : undefined,
981+
role: "role" in data ? (data as { role: string }).role : undefined,
982+
});
972983
}
973-
// Note: Init events and stream events are handled by isStreamEvent() buffering above
984+
// Note: Messages without role/type are silently ignored (expected for some IPC events)
974985
}
975986
}
976987

src/utils/messages/StreamingMessageAggregator.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,15 +511,30 @@ export class StreamingMessageAggregator {
511511
}
512512

513513
if (isInitOutput(data)) {
514-
if (!this.initState) return; // Defensive: shouldn't happen but handle gracefully
514+
if (!this.initState) {
515+
console.error("Received init-output without init-start", { data });
516+
return;
517+
}
518+
if (!data.line) {
519+
console.error("Received init-output with missing line field", { data });
520+
return;
521+
}
515522
const line = data.isError ? `ERROR: ${data.line}` : data.line;
523+
// Extra defensive check (should never hit due to check above, but prevents crash if data changes)
524+
if (typeof line !== "string") {
525+
console.error("Init-output line is not a string", { line, data });
526+
return;
527+
}
516528
this.initState.lines.push(line.trimEnd());
517529
this.invalidateCache();
518530
return;
519531
}
520532

521533
if (isInitEnd(data)) {
522-
if (!this.initState) return; // Defensive: shouldn't happen but handle gracefully
534+
if (!this.initState) {
535+
console.error("Received init-end without init-start", { data });
536+
return;
537+
}
523538
this.initState.exitCode = data.exitCode;
524539
this.initState.status = data.exitCode === 0 ? "success" : "error";
525540
this.invalidateCache();

0 commit comments

Comments
 (0)