Skip to content

Commit 0f5c1c1

Browse files
committed
🤖 fix: normalize legacy mux metadata
_Generated with `mux`_
1 parent 3bcaab9 commit 0f5c1c1

File tree

6 files changed

+102
-9
lines changed

6 files changed

+102
-9
lines changed

src/constants/events.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,12 @@ export const CUSTOM_EVENTS = {
4949
* Event to execute a command from the command palette
5050
* Detail: { commandId: string }
5151
*/
52-
<<<<<<< HEAD
5352
EXECUTE_COMMAND: "mux:executeCommand",
5453
/**
5554
* Event to enter the chat-based workspace creation experience.
5655
* Detail: { projectPath: string, startMessage?: string, model?: string, trunkBranch?: string, runtime?: string }
5756
*/
5857
START_WORKSPACE_CREATION: "mux:startWorkspaceCreation",
59-
=======
60-
EXECUTE_COMMAND: "mux:executeCommand",
61-
>>>>>>> a3cb9358 (🤖 chore: rename cmux refs to mux)
6258
} as const;
6359

6460
/**

src/services/historyService.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ describe("HistoryService", () => {
9999
}
100100
});
101101

102+
it("hydrates legacy cmuxMetadata entries", async () => {
103+
const workspaceId = "workspace-legacy";
104+
const workspaceDir = config.getSessionDir(workspaceId);
105+
await fs.mkdir(workspaceDir, { recursive: true });
106+
107+
const legacyMessage = createMuxMessage("msg-legacy", "user", "legacy", {
108+
historySequence: 0,
109+
});
110+
(legacyMessage.metadata as Record<string, unknown>).cmuxMetadata = { type: "normal" };
111+
112+
const chatPath = path.join(workspaceDir, "chat.jsonl");
113+
await fs.writeFile(chatPath, JSON.stringify({ ...legacyMessage, workspaceId }) + "\n");
114+
115+
const result = await service.getHistory(workspaceId);
116+
expect(result.success).toBe(true);
117+
if (result.success) {
118+
expect(result.data[0].metadata?.muxMetadata?.type).toBe("normal");
119+
}
120+
});
102121
it("should handle empty lines in history file", async () => {
103122
const workspaceId = "workspace1";
104123
const workspaceDir = config.getSessionDir(workspaceId);

src/services/historyService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { workspaceFileLocks } from "@/utils/concurrency/workspaceFileLocks";
88
import { log } from "./log";
99
import { getTokenizerForModel } from "@/utils/main/tokenizer";
1010
import { KNOWN_MODELS } from "@/constants/knownModels";
11+
import { normalizeLegacyMuxMetadata } from "@/utils/messages/legacy";
1112

1213
/**
1314
* HistoryService - Manages chat history persistence and sequence numbering
@@ -49,7 +50,7 @@ export class HistoryService {
4950
for (let i = 0; i < lines.length; i++) {
5051
try {
5152
const message = JSON.parse(lines[i]) as MuxMessage;
52-
messages.push(message);
53+
messages.push(normalizeLegacyMuxMetadata(message));
5354
} catch (parseError) {
5455
// Skip malformed lines but log error for debugging
5556
console.error(

src/services/partialService.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
/* eslint-disable @typescript-eslint/unbound-method */
2-
import { describe, test, expect, beforeEach, mock } from "bun:test";
2+
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
33
import { PartialService } from "./partialService";
44
import type { HistoryService } from "./historyService";
5-
import type { Config } from "@/config";
6-
import type { MuxMessage } from "@/types/message";
5+
import { Config } from "@/config";
6+
import { createMuxMessage, type MuxMessage } from "@/types/message";
77
import { Ok } from "@/types/result";
8+
import * as fs from "fs/promises";
9+
import * as path from "path";
10+
import * as os from "os";
811

912
// Mock Config
1013
const createMockConfig = (): Config => {
@@ -194,3 +197,36 @@ describe("PartialService - Error Recovery", () => {
194197
expect(deletePartial).toHaveBeenCalledWith(workspaceId);
195198
});
196199
});
200+
201+
describe("PartialService - Legacy compatibility", () => {
202+
let tempDir: string;
203+
let config: Config;
204+
let partialService: PartialService;
205+
206+
beforeEach(async () => {
207+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-partial-legacy-"));
208+
config = new Config(tempDir);
209+
partialService = new PartialService(config, createMockHistoryService());
210+
});
211+
212+
afterEach(async () => {
213+
await fs.rm(tempDir, { recursive: true, force: true });
214+
});
215+
216+
test("readPartial upgrades legacy cmuxMetadata", async () => {
217+
const workspaceId = "legacy-ws";
218+
const workspaceDir = config.getSessionDir(workspaceId);
219+
await fs.mkdir(workspaceDir, { recursive: true });
220+
221+
const partialMessage = createMuxMessage("partial-1", "assistant", "legacy", {
222+
historySequence: 0,
223+
});
224+
(partialMessage.metadata as Record<string, unknown>).cmuxMetadata = { type: "normal" };
225+
226+
const partialPath = path.join(workspaceDir, "partial.json");
227+
await fs.writeFile(partialPath, JSON.stringify(partialMessage));
228+
229+
const result = await partialService.readPartial(workspaceId);
230+
expect(result?.metadata?.muxMetadata?.type).toBe("normal");
231+
});
232+
});

src/services/partialService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { MuxMessage } from "@/types/message";
66
import type { Config } from "@/config";
77
import type { HistoryService } from "./historyService";
88
import { workspaceFileLocks } from "@/utils/concurrency/workspaceFileLocks";
9+
import { normalizeLegacyMuxMetadata } from "@/utils/messages/legacy";
910

1011
/**
1112
* PartialService - Manages partial message persistence for interrupted streams
@@ -48,7 +49,8 @@ export class PartialService {
4849
try {
4950
const partialPath = this.getPartialPath(workspaceId);
5051
const data = await fs.readFile(partialPath, "utf-8");
51-
return JSON.parse(data) as MuxMessage;
52+
const message = JSON.parse(data) as MuxMessage;
53+
return normalizeLegacyMuxMetadata(message);
5254
} catch (error) {
5355
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
5456
return null; // No partial exists

src/utils/messages/legacy.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { MuxFrontendMetadata, MuxMessage, MuxMetadata } from "@/types/message";
2+
3+
interface LegacyMuxMetadata extends MuxMetadata {
4+
cmuxMetadata?: MuxFrontendMetadata;
5+
}
6+
7+
/**
8+
* Normalize persisted messages that were stored before the mux rename.
9+
*
10+
* Older builds recorded frontend metadata under `metadata.cmuxMetadata`.
11+
* After the rename, the field lives under `metadata.muxMetadata`.
12+
*
13+
* This helper upgrades the legacy field on read so UI code keeps working.
14+
*/
15+
export function normalizeLegacyMuxMetadata(message: MuxMessage): MuxMessage {
16+
const metadata = message.metadata as LegacyMuxMetadata | undefined;
17+
if (metadata?.cmuxMetadata === undefined) {
18+
return message;
19+
}
20+
21+
const { cmuxMetadata, ...rest } = metadata;
22+
const normalizedMetadata: MuxMetadata = rest;
23+
24+
if (metadata.muxMetadata) {
25+
// Message already has the new field; just drop the legacy copy.
26+
return {
27+
...message,
28+
metadata: normalizedMetadata,
29+
};
30+
}
31+
32+
return {
33+
...message,
34+
metadata: {
35+
...normalizedMetadata,
36+
muxMetadata: cmuxMetadata,
37+
},
38+
};
39+
}

0 commit comments

Comments
 (0)