From b225df8fab5b11ded8b7ba8afffc14e72ffd21ca Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 12:44:51 +0500 Subject: [PATCH 01/13] fix: harden xum directory transition --- src/common/constants/paths.test.ts | 60 ++++++++++++- src/common/constants/paths.ts | 68 ++++++++++++--- src/node/compat/xumTransition.test.ts | 119 ++++++++++++++++++++++++++ src/node/compat/xumTransition.ts | 40 ++++++--- 4 files changed, 256 insertions(+), 31 deletions(-) diff --git a/src/common/constants/paths.test.ts b/src/common/constants/paths.test.ts index 9d561af554..e59f9cef85 100644 --- a/src/common/constants/paths.test.ts +++ b/src/common/constants/paths.test.ts @@ -178,6 +178,47 @@ describe("getXumHome", () => { }); }); + test("selects a sole populated legacy tree before startup adopts an empty canonical home", () => { + const homeDir = createTempMuxRoot(); + const canonicalPath = join(homeDir, ".xum"); + const legacyPath = join(homeDir, ".mux"); + mkdirSync(canonicalPath); + mkdirSync(legacyPath); + writeFileSync(join(legacyPath, "config.json"), "legacy", "utf8"); + + withHomeDir(homeDir, () => { + expect(getXumHome()).toBe(legacyPath); + expect(readFileSync(join(getXumHome(), "config.json"), "utf8")).toBe("legacy"); + }); + }); + + test("keeps canonical active when two independent legacy trees are populated", () => { + const homeDir = createTempMuxRoot(); + const canonicalPath = join(homeDir, ".xum"); + const muxPath = join(homeDir, ".mux"); + const cmuxPath = join(homeDir, ".cmux"); + mkdirSync(canonicalPath); + mkdirSync(muxPath); + mkdirSync(cmuxPath); + writeFileSync(join(muxPath, "config.json"), "mux", "utf8"); + writeFileSync(join(cmuxPath, "config.json"), "cmux", "utf8"); + + withHomeDir(homeDir, () => { + expect(getXumHome()).toBe(canonicalPath); + }); + }); + + test("does not treat a legacy alias to empty canonical storage as independent data", () => { + const homeDir = createTempMuxRoot(); + const canonicalPath = join(homeDir, ".xum"); + mkdirSync(canonicalPath); + symlinkSync(canonicalPath, join(homeDir, ".mux"), "dir"); + + withHomeDir(homeDir, () => { + expect(getXumHome()).toBe(canonicalPath); + }); + }); + test("prefers a healthy leftover tree when canonical storage is a regular file", () => { const homeDir = createTempMuxRoot(); writeFileSync(join(homeDir, ".xum"), "not-a-directory", "utf8"); @@ -257,15 +298,16 @@ describe("getXumHome", () => { }); }); - test("ignores a malformed leftover marker and keeps an empty canonical home", () => { + test("ignores a malformed marker while still selecting the sole populated legacy home", () => { const homeDir = createTempMuxRoot(); + const legacyPath = join(homeDir, ".mux"); mkdirSync(join(homeDir, ".xum")); - mkdirSync(join(homeDir, ".mux")); - writeFileSync(join(homeDir, ".mux", "config.json"), "legacy", "utf8"); + mkdirSync(legacyPath); + writeFileSync(join(legacyPath, "config.json"), "legacy", "utf8"); writeFileSync(getXumHomeLegacyFallbackMarkerPath(homeDir), "../.mux\n", "utf8"); withHomeDir(homeDir, () => { - expect(getXumHome()).toBe(join(homeDir, ".xum")); + expect(getXumHome()).toBe(legacyPath); }); }); @@ -275,6 +317,8 @@ describe("getXumHome", () => { mkdirSync(canonicalPath); mkdirSync(join(homeDir, ".mux")); writeFileSync(join(homeDir, ".mux", "config.json"), "legacy", "utf8"); + mkdirSync(join(homeDir, ".cmux")); + writeFileSync(join(homeDir, ".cmux", "config.json"), "other", "utf8"); writeFileSync(getXumHomeLegacyFallbackMarkerPath(homeDir), `.mux\n${"x".repeat(80)}`, "utf8"); withHomeDir(homeDir, () => { @@ -291,6 +335,8 @@ describe("getXumHome", () => { mkdirSync(canonicalPath); mkdirSync(join(homeDir, ".mux")); writeFileSync(join(homeDir, ".mux", "config.json"), "legacy", "utf8"); + mkdirSync(join(homeDir, ".cmux")); + writeFileSync(join(homeDir, ".cmux", "config.json"), "other", "utf8"); const fifo = spawnSync("mkfifo", [markerPath], { encoding: "utf8" }); expect(fifo.status).toBe(0); expect(lstatSync(markerPath).isFIFO()).toBe(true); @@ -307,6 +353,8 @@ describe("getXumHome", () => { mkdirSync(canonicalPath); mkdirSync(join(homeDir, ".mux")); writeFileSync(join(homeDir, ".mux", "config.json"), "legacy", "utf8"); + mkdirSync(join(homeDir, ".cmux")); + writeFileSync(join(homeDir, ".cmux", "config.json"), "other", "utf8"); mkdirSync(getXumHomeLegacyFallbackMarkerPath(homeDir)); withHomeDir(homeDir, () => { @@ -321,6 +369,8 @@ describe("getXumHome", () => { mkdirSync(canonicalPath); mkdirSync(join(homeDir, ".mux")); writeFileSync(join(homeDir, ".mux", "config.json"), "legacy", "utf8"); + mkdirSync(join(homeDir, ".cmux")); + writeFileSync(join(homeDir, ".cmux", "config.json"), "other", "utf8"); writeFileSync(markerTarget, ".mux\n", "utf8"); symlinkSync(markerTarget, getXumHomeLegacyFallbackMarkerPath(homeDir)); @@ -336,6 +386,8 @@ describe("getXumHome", () => { mkdirSync(canonicalPath); mkdirSync(join(homeDir, ".mux")); writeFileSync(join(homeDir, ".mux", "config.json"), "legacy", "utf8"); + mkdirSync(join(homeDir, ".cmux")); + writeFileSync(join(homeDir, ".cmux", "config.json"), "other", "utf8"); writeFileSync(markerPath, `.mux\n${"x".repeat(80)}`, "utf8"); const originalLstat = lstatSync; diff --git a/src/common/constants/paths.ts b/src/common/constants/paths.ts index c65b317f72..529377e211 100644 --- a/src/common/constants/paths.ts +++ b/src/common/constants/paths.ts @@ -98,6 +98,46 @@ function directoryHasEntries(path: string): boolean { } } +function sameHealthyDirectory(leftPath: string, rightPath: string): boolean { + try { + const left = statSync(leftPath); + const right = statSync(rightPath); + return ( + left.isDirectory() && right.isDirectory() && left.dev === right.dev && left.ino === right.ino + ); + } catch { + return false; + } +} + +/** + * Match the mutating transition's empty-canonical adoption without changing disk. + * Standalone readers (notably VS Code) may run before desktop/CLI startup, so they + * must see the sole populated legacy tree rather than an empty Xum directory. + */ +function findSolePopulatedIndependentLegacy( + canonicalPath: string, + legacyPaths: readonly string[] +): string | undefined { + const populatedPaths: string[] = []; + + for (const legacyPath of legacyPaths) { + if ( + !isHealthyDirectory(legacyPath) || + sameHealthyDirectory(legacyPath, canonicalPath) || + !directoryHasEntries(legacyPath) + ) { + continue; + } + + if (!populatedPaths.some((candidate) => sameHealthyDirectory(candidate, legacyPath))) { + populatedPaths.push(legacyPath); + } + } + + return populatedPaths.length === 1 ? populatedPaths[0] : undefined; +} + const MAX_LEGACY_FALLBACK_MARKER_BYTES = 64; /** @@ -178,10 +218,10 @@ function readMarkedLegacyHome(homeDir: string, suffix: string): string | undefin * Appends '-dev' when NODE_ENV=development. * * Prefer a usable populated canonical directory, then a persisted leftover - * fallback marker (written only with a known leftover name), then any healthy - * canonical directory, then the first healthy leftover tree. A file or broken - * symlink at ~/.xum is not a home. A genuinely empty unmarked home still - * returns the canonical future path. + * fallback marker (written only with a known leftover name). When canonical is + * empty, choose a sole populated independent legacy tree so non-mutating readers + * agree with startup migration; true conflicts keep canonical active. A file or + * broken symlink at ~/.xum is not a home. * * Main-process only: this helper lives in constants/ for organization, but it * reads process.env / homedir and must not be imported from renderer code. @@ -198,6 +238,11 @@ export function getXumHome(): string { const suffix = process.env.NODE_ENV === "development" ? "-dev" : ""; const homeDir = os.homedir(); const canonicalPath = join(homeDir, XUM_HOME_DIR_NAME + suffix); + const legacyPaths = [join(homeDir, LEGACY_MUX_HOME_DIR_NAME + suffix)]; + if (!suffix) { + legacyPaths.push(join(homeDir, LEGACY_CMUX_HOME_DIR_NAME)); + } + if (isHealthyDirectory(canonicalPath) && directoryHasEntries(canonicalPath)) { return canonicalPath; } @@ -208,18 +253,13 @@ export function getXumHome(): string { } if (isHealthyDirectory(canonicalPath)) { - return canonicalPath; + const solePopulatedLegacy = findSolePopulatedIndependentLegacy(canonicalPath, legacyPaths); + return solePopulatedLegacy ?? canonicalPath; } - const legacyMuxPath = join(homeDir, LEGACY_MUX_HOME_DIR_NAME + suffix); - if (isHealthyDirectory(legacyMuxPath)) { - return legacyMuxPath; - } - - if (!suffix) { - const legacyCmuxPath = join(homeDir, LEGACY_CMUX_HOME_DIR_NAME); - if (isHealthyDirectory(legacyCmuxPath)) { - return legacyCmuxPath; + for (const legacyPath of legacyPaths) { + if (isHealthyDirectory(legacyPath)) { + return legacyPath; } } diff --git a/src/node/compat/xumTransition.test.ts b/src/node/compat/xumTransition.test.ts index f2683a3266..d965a7be2a 100644 --- a/src/node/compat/xumTransition.test.ts +++ b/src/node/compat/xumTransition.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import { promises as fs } from "node:fs"; import * as os from "node:os"; import { join, resolve } from "node:path"; @@ -83,6 +84,10 @@ async function listQuarantineBackups(dir: string, baseName: string): Promise { await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); }); @@ -144,6 +149,40 @@ describe("initializeXumHomeTransition", () => { expect(await fs.readFile(join(canonicalPath, "from-mux"), "utf8")).toBe("old"); }); + test.skipIf(process.platform === "win32")( + "keeps real Git worktree metadata usable through canonical and downgrade paths", + async () => { + const homeDir = await createTempDir(); + const repositoryPath = join(homeDir, "repository"); + const legacyWorktreePath = join(homeDir, ".mux", "src", "project", "feature"); + await fs.mkdir(repositoryPath); + runGit(repositoryPath, "init", "-q"); + await fs.writeFile(join(repositoryPath, "README.md"), "base\n", "utf8"); + runGit(repositoryPath, "add", "README.md"); + runGit( + repositoryPath, + "-c", + "user.name=Xum Test", + "-c", + "user.email=xum@example.invalid", + "commit", + "-qm", + "initial" + ); + await fs.mkdir(join(homeDir, ".mux", "src", "project"), { recursive: true }); + runGit(repositoryPath, "worktree", "add", "-q", "-b", "feature", legacyWorktreePath); + + const result = await initializeXumHomeTransition({ homeDir, env: {}, platform: "linux" }); + const canonicalWorktreePath = join(homeDir, ".xum", "src", "project", "feature"); + + expect(result.status).toBe("migrated"); + expect(runGit(canonicalWorktreePath, "status", "--porcelain")).toBe(""); + await fs.writeFile(join(canonicalWorktreePath, "from-xum.txt"), "shared\n", "utf8"); + expect(runGit(legacyWorktreePath, "status", "--porcelain")).toBe("?? from-xum.txt"); + expect(await fs.realpath(legacyWorktreePath)).toBe(await fs.realpath(canonicalWorktreePath)); + } + ); + test("moves a cmux-only tree and still creates the mux alias", async () => { const homeDir = await createTempDir(); const cmuxPath = join(homeDir, ".cmux"); @@ -182,6 +221,56 @@ describe("initializeXumHomeTransition", () => { } }); + test("rolls a cmux migration back to its actual source when the primary alias fails", async () => { + const homeDir = await createTempDir(); + const sourcePath = join(homeDir, ".cmux"); + await fs.mkdir(sourcePath); + await fs.writeFile(join(sourcePath, "config.json"), "cmux", "utf8"); + const symlink = spyOn(fs, "symlink").mockImplementation(() => { + throw new Error("EPERM: alias blocked"); + }); + + try { + const result = await initializeXumHomeTransition({ homeDir, env: {}, platform: "linux" }); + + expect(result.status).toBe("legacy-fallback"); + expect(result.activePath).toBe(sourcePath); + expect(await fs.readFile(join(sourcePath, "config.json"), "utf8")).toBe("cmux"); + await expectMissingPath(join(homeDir, ".xum")); + await expectMissingPath(join(homeDir, ".mux")); + } finally { + symlink.mockRestore(); + } + }); + + test("removes an earlier alias before rolling a cmux migration back on a later failure", async () => { + const homeDir = await createTempDir(); + const sourcePath = join(homeDir, ".cmux"); + const primaryAlias = join(homeDir, ".mux"); + await fs.mkdir(sourcePath); + await fs.writeFile(join(sourcePath, "config.json"), "cmux", "utf8"); + const realSymlink = fs.symlink.bind(fs); + const symlink = spyOn(fs, "symlink").mockImplementation(async (target, path, type) => { + if (path === sourcePath) { + throw new Error("EPERM: source alias blocked"); + } + await realSymlink(target, path, type); + }); + + try { + const result = await initializeXumHomeTransition({ homeDir, env: {}, platform: "linux" }); + + expect(result.status).toBe("legacy-fallback"); + expect(result.activePath).toBe(sourcePath); + expect(await fs.readFile(join(sourcePath, "config.json"), "utf8")).toBe("cmux"); + expect((await fs.lstat(sourcePath)).isDirectory()).toBe(true); + await expectMissingPath(join(homeDir, ".xum")); + await expectMissingPath(primaryAlias); + } finally { + symlink.mockRestore(); + } + }); + test("does not treat a broken alias or file as a migratable directory", async () => { const homeDir = await createTempDir(); await fs.symlink(join(homeDir, "missing-target"), join(homeDir, ".mux")); @@ -657,6 +746,36 @@ describe("initializeXumUserDataTransition", () => { ); }); + test.skipIf(process.platform !== "linux")( + "rolls historical Linux userData back to Mux when its alias cannot be created", + async () => { + const appDataDir = await createTempDir(); + const sourcePath = join(appDataDir, "Mux"); + const primaryAlias = join(appDataDir, "mux"); + await fs.mkdir(sourcePath); + await fs.writeFile(join(sourcePath, "window-state.json"), "{}", "utf8"); + const realSymlink = fs.symlink.bind(fs); + const symlink = spyOn(fs, "symlink").mockImplementation(async (target, path, type) => { + if (path === sourcePath) { + throw new Error("EPERM: historical alias blocked"); + } + await realSymlink(target, path, type); + }); + + try { + const result = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + + expect(result.status).toBe("legacy-fallback"); + expect(result.activePath).toBe(sourcePath); + expect(await fs.readFile(join(sourcePath, "window-state.json"), "utf8")).toBe("{}"); + await expectMissingPath(join(appDataDir, "xum")); + await expectMissingPath(primaryAlias); + } finally { + symlink.mockRestore(); + } + } + ); + test("adopts a populated legacy tree into an empty canonical userData directory", async () => { const appDataDir = await createTempDir(); const canonicalPath = join(appDataDir, "xum"); diff --git a/src/node/compat/xumTransition.ts b/src/node/compat/xumTransition.ts index 16cb852f71..6c904863df 100644 --- a/src/node/compat/xumTransition.ts +++ b/src/node/compat/xumTransition.ts @@ -299,6 +299,7 @@ async function applyCompatibilityAliases( options: CompatibilityAliasOptions ): Promise { const canonicalUsableForAliases = await isHealthyDirectory(options.canonicalPath); + const createdAliases: string[] = []; for (const legacyPath of options.legacyPaths) { if (await pathEntryExists(legacyPath)) { @@ -317,29 +318,42 @@ async function applyCompatibilityAliases( try { await createDirectoryAlias(options.canonicalPath, legacyPath, options.platform); + createdAliases.push(legacyPath); } catch (error) { options.issues.push(`Could not create compatibility alias ${legacyPath}: ${String(error)}`); - // If this run just moved or created the canonical directory, roll it back to - // the primary legacy path rather than strand existing users without a path an - // older binary can open. A pre-existing canonical directory is never moved. - const primaryLegacyPath = options.legacyPaths[0]; - if ( - (options.migratedFrom != null || options.createdCanonical) && - legacyPath === primaryLegacyPath - ) { + // A migrated tree must return to the exact name that supplied its data when any + // required alias fails. Rolling a `.cmux`/`Mux` source into the primary `.mux` + // name would make the old installation that owns the data lose its home. + const rollbackPath = + options.migratedFrom ?? + (options.createdCanonical && legacyPath === options.legacyPaths[0] + ? options.legacyPaths[0] + : undefined); + if (rollbackPath != null) { try { - await fs.rename(options.canonicalPath, primaryLegacyPath); + // Remove only aliases created by this transition. In particular, the source + // alias may already point at canonical when a later compatibility alias fails. + await Promise.all( + createdAliases.map(async (createdAlias) => { + try { + await fs.rm(createdAlias, { force: true }); + } catch (cleanupError) { + options.issues.push( + `Could not remove compatibility alias ${createdAlias} during rollback: ${String(cleanupError)}` + ); + } + }) + ); + await fs.rename(options.canonicalPath, rollbackPath); return { canonicalPath: options.canonicalPath, - activePath: await requireHealthyDirectory(primaryLegacyPath, options.issues), + activePath: await requireHealthyDirectory(rollbackPath, options.issues), status: "legacy-fallback", issues: options.issues, }; } catch (rollbackError) { - options.issues.push( - `Could not roll back to ${primaryLegacyPath}: ${String(rollbackError)}` - ); + options.issues.push(`Could not roll back to ${rollbackPath}: ${String(rollbackError)}`); } } } From fad205052a2ba27f783a5e024e802a18299e8bb0 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 12:44:15 +0500 Subject: [PATCH 02/13] refactor: rename internal Mux identifiers to Xum --- src/browser/App.tsx | 4 +- src/browser/components/TitleBar/TitleBar.tsx | 26 +- .../XumGatewaySessionExpiredDialog.tsx} | 6 +- .../contexts/ProviderOptionsContext.tsx | 16 +- src/browser/contexts/WorkspaceContext.tsx | 2 +- src/browser/features/ChatInput/index.tsx | 16 +- .../ChatInput/useCreationWorkspace.ts | 18 +- src/browser/features/ChatInput/utils.ts | 4 +- .../Messages/MessageRenderer.stories.tsx | 8 +- .../features/Messages/MessageWindow.test.tsx | 8 +- .../features/Messages/MessageWindow.tsx | 4 +- .../features/Messages/StreamErrorMessage.tsx | 8 +- .../Settings/Sections/GovernorSection.tsx | 2 +- .../Settings/Sections/ProvidersSection.tsx | 118 +++--- .../SplashScreens/OnboardingWizardSplash.tsx | 114 +++--- .../features/Tools/ProposePlanToolCall.tsx | 4 +- .../Tools/SubagentTranscriptDialog.tsx | 8 +- src/browser/hooks/useCompactAndRetry.ts | 4 +- .../hooks/useMuxGatewayAccountStatus.test.ts | 20 -- src/browser/hooks/useResumeStream.test.tsx | 4 +- src/browser/hooks/useStartHere.ts | 4 +- .../hooks/useXumGatewayAccountStatus.test.ts | 20 ++ ...tatus.ts => useXumGatewayAccountStatus.ts} | 10 +- .../stores/WorkspaceConsumerManager.ts | 4 +- src/browser/stores/WorkspaceStore.ts | 16 +- src/browser/stories/helpers/chatSetup.ts | 10 +- src/browser/stories/mocks/chatHandlers.ts | 6 +- src/browser/stories/mocks/messages.ts | 32 +- src/browser/stories/mocks/orpc.ts | 10 +- src/browser/stories/mocks/tools.ts | 10 +- src/browser/utils/chatCommands.ts | 6 +- .../StreamingMessageAggregator.skills.test.ts | 34 +- .../StreamingMessageAggregator.status.test.ts | 20 +- .../StreamingMessageAggregator.test.ts | 222 ++++++------ .../messages/StreamingMessageAggregator.ts | 88 ++--- .../messages/applyToolOutputRedaction.test.ts | 12 +- .../messages/applyToolOutputRedaction.ts | 8 +- .../applyWorkspaceChatEventToAggregator.ts | 8 +- .../utils/messages/buildSendMessageOptions.ts | 4 +- ...ayedMessageBuilder.bashMonitorWake.test.ts | 8 +- .../displayedMessageBuilder.staged.test.ts | 8 +- .../utils/messages/displayedMessageBuilder.ts | 52 +-- .../messages/modelMessageTransform.test.ts | 60 ++-- .../utils/messages/modelMessageTransform.ts | 28 +- src/browser/utils/messages/recency.test.ts | 90 ++--- src/browser/utils/messages/recency.ts | 4 +- .../utils/messages/sanitizeToolInput.test.ts | 16 +- .../utils/messages/sanitizeToolInput.ts | 6 +- src/browser/utils/messages/sendOptions.ts | 8 +- src/browser/utils/workflowRunMessages.test.ts | 16 +- src/browser/utils/workflowRunMessages.ts | 26 +- src/cli/debug/costs.ts | 6 +- src/cli/debug/replay-history.ts | 12 +- src/cli/debug/send-message.ts | 6 +- src/common/config/schemas/providersConfig.ts | 6 +- src/common/config/schemas/userPreferences.ts | 10 +- .../orpc/onChatCursorFingerprint.test.ts | 14 +- src/common/orpc/onChatCursorFingerprint.ts | 6 +- src/common/orpc/schemas.ts | 16 +- src/common/orpc/schemas/api.ts | 12 +- src/common/orpc/schemas/chatStats.ts | 2 +- src/common/orpc/schemas/message.test.ts | 24 +- src/common/orpc/schemas/message.ts | 28 +- src/common/orpc/schemas/providerOptions.ts | 2 +- src/common/orpc/schemas/stream.ts | 28 +- src/common/orpc/types.ts | 4 +- .../preferences/userPreferencesStorage.ts | 6 +- src/common/schemas/providerOptions.ts | 2 +- src/common/types/durableEvent.ts | 2 +- src/common/types/instructions.ts | 2 +- .../types/message.agentSkillRefs.test.ts | 4 +- .../types/message.mcpPromptSnapshots.test.ts | 32 +- src/common/types/message.ts | 82 ++--- src/common/types/providerOptions.ts | 4 +- src/common/types/stream.ts | 4 +- src/common/utils/ai/providerOptions.test.ts | 20 +- src/common/utils/ai/providerOptions.ts | 22 +- src/common/utils/goalClearedSummaryDisplay.ts | 4 +- .../utils/messages/compactionBoundary.test.ts | 148 ++++---- .../utils/messages/compactionBoundary.ts | 24 +- .../utils/messages/extractEditedFiles.test.ts | 34 +- .../utils/messages/extractEditedFiles.ts | 6 +- .../utils/messages/providerEligibility.ts | 4 +- .../messages/startHerePlanSummary.test.ts | 6 +- .../utils/messages/startHerePlanSummary.ts | 8 +- .../utils/messages/transcriptShare.test.ts | 72 ++-- src/common/utils/messages/transcriptShare.ts | 22 +- src/common/utils/recency.ts | 6 +- .../utils/tokens/tokenStatsCalculator.test.ts | 16 +- .../utils/tokens/tokenStatsCalculator.ts | 10 +- src/common/utils/tools/toolCatalog.test.ts | 8 +- src/common/utils/tools/toolCatalog.ts | 6 +- src/common/utils/tools/tools.ts | 4 +- src/common/utils/workflowRunMessages.ts | 16 +- src/node/acp/adapter.ts | 4 +- src/node/acp/agent.ts | 41 +-- src/node/acp/streamTranslator.test.ts | 6 +- src/node/acp/streamTranslator.ts | 6 +- src/node/orpc/context.ts | 8 +- src/node/orpc/router.ts | 46 +-- src/node/orpc/server.test.ts | 28 +- src/node/orpc/server.ts | 8 +- src/node/runtime/CoderSSHRuntime.test.ts | 24 +- src/node/runtime/CoderSSHRuntime.ts | 2 +- ...ter.test.ts => xumSshConfigWriter.test.ts} | 60 ++-- ...hConfigWriter.ts => xumSshConfigWriter.ts} | 6 +- .../agentSession.agentSkillSnapshot.test.ts | 16 +- .../agentSession.autoCompaction.test.ts | 48 +-- ...gentSession.continueMessageAgentId.test.ts | 18 +- .../agentSession.editMessageId.test.ts | 18 +- ...gentSession.fileChangeNotification.test.ts | 8 +- .../agentSession.goalAutoPause.test.ts | 12 +- .../agentSession.mcpPromptSnapshot.test.ts | 12 +- ...tSession.postCompactionAttachments.test.ts | 18 +- ...agentSession.postCompactionRefresh.test.ts | 12 +- .../agentSession.postCompactionRetry.test.ts | 4 +- .../agentSession.preStreamError.test.ts | 50 +-- .../agentSession.queueDispatch.test.ts | 6 +- .../agentSession.startupAutoRetry.test.ts | 48 +-- src/node/services/agentSession.testHarness.ts | 4 +- .../agentSession.thinkingOverride.test.ts | 4 +- src/node/services/agentSession.ts | 208 +++++------ ...ntSession.workspaceTurnInheritance.test.ts | 34 +- .../agentSkills/loadedSkillSnapshots.test.ts | 8 +- .../agentSkills/loadedSkillSnapshots.ts | 12 +- src/node/services/agentStatusService.test.ts | 94 ++--- src/node/services/agentStatusService.ts | 8 +- src/node/services/aiService.test.ts | 136 +++---- src/node/services/aiService.ts | 102 +++--- src/node/services/backup/payload.test.ts | 4 +- src/node/services/bashMonitorWakeStore.ts | 4 +- src/node/services/coderService.test.ts | 40 +-- src/node/services/coderService.ts | 6 +- src/node/services/compactionHandler.test.ts | 158 ++++---- src/node/services/compactionHandler.ts | 30 +- src/node/services/heartbeatService.test.ts | 66 ++-- src/node/services/heartbeatService.ts | 4 +- src/node/services/historyService.test.ts | 338 +++++++++--------- src/node/services/historyService.ts | 126 +++---- .../services/idleCompactionService.test.ts | 32 +- .../services/mdnsAdvertiserService.test.ts | 14 +- src/node/services/mdnsAdvertiserService.ts | 12 +- .../memoryConsolidationService.test.ts | 8 +- src/node/services/memoryHarvest.test.ts | 14 +- src/node/services/memoryHarvest.ts | 16 +- src/node/services/messagePipeline.test.ts | 34 +- src/node/services/messagePipeline.ts | 4 +- src/node/services/messageQueue.test.ts | 46 +-- src/node/services/mock/mockAiRouter.ts | 12 +- .../services/mock/mockAiStreamPlayer.test.ts | 48 +-- src/node/services/mock/mockAiStreamPlayer.ts | 36 +- src/node/services/partialService.test.ts | 30 +- .../services/providerModelFactory.test.ts | 10 +- src/node/services/providerModelFactory.ts | 12 +- src/node/services/providerService.test.ts | 14 +- src/node/services/providerService.ts | 4 +- src/node/services/replay/replayFixture.ts | 12 +- .../services/replay/replayRequestBuilder.ts | 6 +- .../replay/replayVerify.fixture.test.ts | 4 +- src/node/services/replay/replayVerify.test.ts | 8 +- src/node/services/replay/replayVerify.ts | 20 +- src/node/services/serverService.ts | 4 +- src/node/services/serviceContainer.ts | 22 +- src/node/services/sessionUsageService.test.ts | 18 +- src/node/services/sessionUsageService.ts | 10 +- .../services/streamContextBuilder.test.ts | 10 +- src/node/services/streamContextBuilder.ts | 4 +- src/node/services/streamManager.test.ts | 4 +- src/node/services/streamManager.ts | 24 +- src/node/services/streamSimulation.test.ts | 6 +- src/node/services/streamSimulation.ts | 12 +- src/node/services/systemMessage.ts | 6 +- src/node/services/taskService.test.ts | 206 +++++------ src/node/services/taskService.ts | 102 +++--- src/node/services/timelineMapper.ts | 14 +- src/node/services/timelineService.test.ts | 6 +- src/node/services/timelineService.ts | 10 +- src/node/services/tokenizerService.test.ts | 20 +- src/node/services/tokenizerService.ts | 6 +- src/node/services/turnEnvelope.test.ts | 4 +- src/node/services/turnEnvelope.ts | 4 +- src/node/services/utils/fileChangeTracker.ts | 6 +- src/node/services/voiceService.ts | 10 +- .../services/workspaceGoalService.test.ts | 14 +- src/node/services/workspaceGoalService.ts | 6 +- src/node/services/workspaceService.test.ts | 128 +++---- src/node/services/workspaceService.ts | 74 ++-- ...test.ts => xumGatewayOauthService.test.ts} | 10 +- ...thService.ts => xumGatewayOauthService.ts} | 2 +- ...est.ts => xumGovernorOauthService.test.ts} | 10 +- ...hService.ts => xumGovernorOauthService.ts} | 2 +- .../convertDataUriFilePartsForSdk.test.ts | 14 +- .../messages/convertDataUriFilePartsForSdk.ts | 6 +- .../extractToolMediaAsUserMessages.test.ts | 18 +- .../extractToolMediaAsUserMessages.ts | 16 +- .../inlineSvgAsTextForProvider.test.ts | 12 +- .../messages/inlineSvgAsTextForProvider.ts | 12 +- src/node/utils/messages/legacy.ts | 16 +- .../messages/reasoningProviderOptions.test.ts | 14 +- .../messages/reasoningProviderOptions.ts | 22 +- .../sanitizeAnthropicDocumentFilename.test.ts | 16 +- .../sanitizeAnthropicDocumentFilename.ts | 4 +- src/node/utils/oauthUtils.ts | 2 +- src/node/utils/providerRequirements.ts | 4 +- tests/e2e/utils/historyFixture.ts | 12 +- tests/ipc/acp.disconnectCleanup.test.ts | 10 +- tests/ipc/acp.promptCorrelation.test.ts | 24 +- tests/ipc/acp.sessionMethods.test.ts | 18 +- tests/ipc/agents/planCommands.test.ts | 12 +- .../ipc/compaction1MRetry.integration.test.ts | 6 +- tests/ipc/helpers.ts | 4 +- .../openaiPreviousResponseIdRecovery.test.ts | 4 +- tests/ipc/providers/xaiGrok46.test.ts | 4 +- .../streaming/emptyAssistantMessage.test.ts | 10 +- tests/ipc/streaming/interrupt.test.ts | 4 +- .../queuedMessages.completing.test.ts | 8 +- .../streaming/queuedMessages.starting.test.ts | 6 +- tests/ipc/streaming/resume.test.ts | 4 +- .../ipc/streaming/sendMessage.images.test.ts | 8 +- tests/ipc/streaming/truncate.test.ts | 26 +- .../streaming/websocketHistoryReplay.test.ts | 10 +- .../persistentSubagentCompaction.test.ts | 8 +- tests/ipc/workspace/fork.test.ts | 16 +- tests/runtime/runtime.test.ts | 2 +- tests/ui/chat/forkFromResponse.test.ts | 4 +- tests/ui/chat/truncation.test.ts | 6 +- tests/ui/gateway/sessionExpired.test.tsx | 10 +- tests/ui/tasks/awaitVisualization.test.ts | 6 +- tests/ui/tasks/bestOfProgress.test.ts | 18 +- tests/ui/tasks/reportRelocation.test.ts | 8 +- tests/ui/workspaces/subagents.test.ts | 4 +- 231 files changed, 2722 insertions(+), 2721 deletions(-) rename src/browser/components/{MuxGatewaySessionExpiredDialog/MuxGatewaySessionExpiredDialog.tsx => XumGatewaySessionExpiredDialog/XumGatewaySessionExpiredDialog.tsx} (97%) delete mode 100644 src/browser/hooks/useMuxGatewayAccountStatus.test.ts create mode 100644 src/browser/hooks/useXumGatewayAccountStatus.test.ts rename src/browser/hooks/{useMuxGatewayAccountStatus.ts => useXumGatewayAccountStatus.ts} (86%) rename src/node/runtime/{muxSshConfigWriter.test.ts => xumSshConfigWriter.test.ts} (89%) rename src/node/runtime/{muxSshConfigWriter.ts => xumSshConfigWriter.ts} (98%) rename src/node/services/{muxGatewayOauthService.test.ts => xumGatewayOauthService.test.ts} (96%) rename src/node/services/{muxGatewayOauthService.ts => xumGatewayOauthService.ts} (99%) rename src/node/services/{muxGovernorOauthService.test.ts => xumGovernorOauthService.test.ts} (97%) rename src/node/services/{muxGovernorOauthService.ts => xumGovernorOauthService.ts} (99%) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index 87a92d55ca..4f8f9f25e9 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -103,7 +103,7 @@ import { ConfirmDialogProvider, useConfirmDialog } from "./contexts/ConfirmDialo import { AboutDialog } from "./features/About/AboutDialog"; import { SettingsPage } from "@/browser/features/Settings/SettingsPage"; import { AnalyticsDashboard } from "@/browser/features/Analytics/AnalyticsDashboard"; -import { MuxGatewaySessionExpiredDialog } from "./components/MuxGatewaySessionExpiredDialog/MuxGatewaySessionExpiredDialog"; +import { XumGatewaySessionExpiredDialog } from "./components/XumGatewaySessionExpiredDialog/XumGatewaySessionExpiredDialog"; import { SshPromptDialog } from "./components/SshPromptDialog/SshPromptDialog"; import { SplashScreenProvider } from "./features/SplashScreens/SplashScreenProvider"; import { TutorialProvider } from "./contexts/TutorialContext"; @@ -1498,7 +1498,7 @@ function AppInner() { /> )} - + diff --git a/src/browser/components/TitleBar/TitleBar.tsx b/src/browser/components/TitleBar/TitleBar.tsx index 870d86fddb..27b8169a3e 100644 --- a/src/browser/components/TitleBar/TitleBar.tsx +++ b/src/browser/components/TitleBar/TitleBar.tsx @@ -26,9 +26,9 @@ import { useRouting } from "@/browser/hooks/useRouting"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; import { - formatMuxGatewayBalance, - useMuxGatewayAccountStatus, -} from "@/browser/hooks/useMuxGatewayAccountStatus"; + formatXumGatewayBalance, + useXumGatewayAccountStatus, +} from "@/browser/hooks/useXumGatewayAccountStatus"; import { isDesktopMode, getTitlebarLeftInset, @@ -114,12 +114,12 @@ export function TitleBar(props: TitleBarProps) { : routing.resolveRoute(canonicalActiveModel); const activeRouteProvider = activeRoute.route; const isNonDirectRoute = activeRouteProvider !== "direct"; - const isMuxGatewayRoute = activeRouteProvider === "mux-gateway"; + const isXumGatewayRoute = activeRouteProvider === "mux-gateway"; const { data: muxGatewayAccountStatus, error: muxGatewayAccountError, - refresh: refreshMuxGatewayAccountStatus, - } = useMuxGatewayAccountStatus(); + refresh: refreshXumGatewayAccountStatus, + } = useXumGatewayAccountStatus(); const gitDescribe = getGitDescribe(VERSION satisfies unknown); const [updateStatus, setUpdateStatus] = useState({ type: "idle" }); @@ -263,31 +263,31 @@ export function TitleBar(props: TitleBarProps) { type="button" onClick={handleOpenRouteSettings} onMouseEnter={() => { - if (isMuxGatewayRoute) { - void refreshMuxGatewayAccountStatus(); + if (isXumGatewayRoute) { + void refreshXumGatewayAccountStatus(); } }} className="border-border-light text-muted-foreground hover:border-border-medium/80 hover:bg-toggle-bg/70 flex h-5 w-5 cursor-pointer items-center justify-center rounded border transition-opacity hover:opacity-70" aria-label={`${activeRoute.displayName} routing`} > - {isMuxGatewayRoute ? ( + {isXumGatewayRoute ? ( ) : ( )} - +
{activeRoute.displayName}
- {isMuxGatewayRoute ? ( + {isXumGatewayRoute ? ( <>
Balance - {formatMuxGatewayBalance(muxGatewayAccountStatus?.remaining_microdollars)} + {formatXumGatewayBalance(muxGatewayAccountStatus?.remaining_microdollars)}
@@ -314,7 +314,7 @@ export function TitleBar(props: TitleBarProps) { className={TOOLTIP_CTA_CLASSNAME} onClick={handleOpenRouteSettings} > - {isMuxGatewayRoute + {isXumGatewayRoute ? "Click to open gateway settings" : "Click to open provider settings"} diff --git a/src/browser/components/MuxGatewaySessionExpiredDialog/MuxGatewaySessionExpiredDialog.tsx b/src/browser/components/XumGatewaySessionExpiredDialog/XumGatewaySessionExpiredDialog.tsx similarity index 97% rename from src/browser/components/MuxGatewaySessionExpiredDialog/MuxGatewaySessionExpiredDialog.tsx rename to src/browser/components/XumGatewaySessionExpiredDialog/XumGatewaySessionExpiredDialog.tsx index 97e3d3f8fd..1a9a3b31a1 100644 --- a/src/browser/components/MuxGatewaySessionExpiredDialog/MuxGatewaySessionExpiredDialog.tsx +++ b/src/browser/components/XumGatewaySessionExpiredDialog/XumGatewaySessionExpiredDialog.tsx @@ -11,7 +11,7 @@ function getServerAuthToken(): string | null { return urlToken?.length ? urlToken : getStoredAuthToken(); } -export function MuxGatewaySessionExpiredDialog() { +export function XumGatewaySessionExpiredDialog() { const [isOpen, setIsOpen] = useState(false); const [loginError, setLoginError] = useState(null); const [isStartingLogin, setIsStartingLogin] = useState(false); @@ -42,7 +42,7 @@ export function MuxGatewaySessionExpiredDialog() { setIsStartingLogin(false); }; - const startMuxGatewayLogin = async () => { + const startXumGatewayLogin = async () => { if (isStartingLogin) { return; } @@ -136,7 +136,7 @@ export function MuxGatewaySessionExpiredDialog() { label: isStartingLogin ? "Starting login..." : "Login to Xum Gateway", disabled: isStartingLogin, onClick: () => { - void startMuxGatewayLogin(); + void startXumGatewayLogin(); }, }} dismissLabel="Cancel" diff --git a/src/browser/contexts/ProviderOptionsContext.tsx b/src/browser/contexts/ProviderOptionsContext.tsx index 0334e230cc..227f7f44c7 100644 --- a/src/browser/contexts/ProviderOptionsContext.tsx +++ b/src/browser/contexts/ProviderOptionsContext.tsx @@ -4,15 +4,15 @@ import { PROVIDER_OPTIONS_ANTHROPIC_KEY, PROVIDER_OPTIONS_GOOGLE_KEY, } from "@/common/constants/storage"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import { supports1MContext } from "@/common/utils/ai/models"; import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; interface ProviderOptionsContextType { - options: MuxProviderOptions; - setAnthropicOptions: (options: MuxProviderOptions["anthropic"]) => void; - setGoogleOptions: (options: MuxProviderOptions["google"]) => void; + options: XumProviderOptions; + setAnthropicOptions: (options: XumProviderOptions["anthropic"]) => void; + setGoogleOptions: (options: XumProviderOptions["google"]) => void; /** Check if a specific model has 1M context enabled */ has1MContext: (modelId: string) => boolean; /** Toggle 1M context for a specific model */ @@ -31,8 +31,8 @@ const ProviderOptionsContext = createContext 0) @@ -57,7 +57,7 @@ function migrateGlobalToPerModel( export function ProviderOptionsProvider({ children }: { children: React.ReactNode }) { const { config: providersConfig } = useProvidersConfig(); const [anthropicOptions, setAnthropicOptions] = usePersistedState< - MuxProviderOptions["anthropic"] + XumProviderOptions["anthropic"] >(PROVIDER_OPTIONS_ANTHROPIC_KEY, {}, { listener: true }); // One-time migration from global boolean to per-model set @@ -70,7 +70,7 @@ export function ProviderOptionsProvider({ children }: { children: React.ReactNod } } - const [googleOptions, setGoogleOptions] = usePersistedState( + const [googleOptions, setGoogleOptions] = usePersistedState( PROVIDER_OPTIONS_GOOGLE_KEY, {}, { listener: true } diff --git a/src/browser/contexts/WorkspaceContext.tsx b/src/browser/contexts/WorkspaceContext.tsx index acc1130b36..f1406265ef 100644 --- a/src/browser/contexts/WorkspaceContext.tsx +++ b/src/browser/contexts/WorkspaceContext.tsx @@ -150,7 +150,7 @@ function migrateLocalGatewayPrefsToBackend( if (shouldMigrateEnabled || shouldMigrateModels) { api.config - .updateMuxGatewayPrefs({ + .updateXumGatewayPrefs({ muxGatewayEnabled: cfg.muxGatewayEnabled ?? localEnabled, muxGatewayModels: cfg.muxGatewayModels ?? localModels, }) diff --git a/src/browser/features/ChatInput/index.tsx b/src/browser/features/ChatInput/index.tsx index ca05974833..1f037f9246 100644 --- a/src/browser/features/ChatInput/index.tsx +++ b/src/browser/features/ChatInput/index.tsx @@ -173,7 +173,7 @@ import { import { resolveThinkingInput } from "@/common/utils/thinking/policy"; import { type AgentSkillReference, - type MuxMessageMetadata, + type XumMessageMetadata, type ReviewNoteDataForDisplay, prepareUserMessageForSend, withAgentSkillRefs, @@ -799,8 +799,8 @@ const ChatInputInner: React.FC = (props) => { // Track transcription provider prerequisites from Settings → Providers. const [openAIKeySet, setOpenAIKeySet] = useState(false); const [openAIProviderEnabled, setOpenAIProviderEnabled] = useState(true); - const [muxGatewayCouponSet, setMuxGatewayCouponSet] = useState(false); - const [muxGatewayEnabled, setMuxGatewayEnabled] = useState(true); + const [muxGatewayCouponSet, setXumGatewayCouponSet] = useState(false); + const [muxGatewayEnabled, setXumGatewayEnabled] = useState(true); const isTranscriptionAvailable = (openAIProviderEnabled && openAIKeySet) || (muxGatewayEnabled && muxGatewayCouponSet); @@ -1964,8 +1964,8 @@ const ChatInputInner: React.FC = (props) => { if (!signal.aborted) { setOpenAIKeySet(config?.openai?.apiKeySet ?? false); setOpenAIProviderEnabled(config?.openai?.isEnabled ?? true); - setMuxGatewayCouponSet(config?.["mux-gateway"]?.couponCodeSet ?? false); - setMuxGatewayEnabled(config?.["mux-gateway"]?.isEnabled ?? true); + setXumGatewayCouponSet(config?.["mux-gateway"]?.couponCodeSet ?? false); + setXumGatewayEnabled(config?.["mux-gateway"]?.isEnabled ?? true); } } catch { // Ignore errors fetching config @@ -2925,14 +2925,14 @@ const ChatInputInner: React.FC = (props) => { } } - const skillMuxMetadata = skillInvocation + const skillXumMetadata = skillInvocation ? buildSkillInvocationMetadata( appendStagedAttachmentNotice(messageText, sendAttachments), skillInvocation.descriptor, skillInvocation.argumentText ) : undefined; - const promptMuxMetadata: MuxMessageMetadata | undefined = mcpPromptInvocation + const promptXumMetadata: XumMessageMetadata | undefined = mcpPromptInvocation ? { type: "normal", // Include the staged-attachment notice so edit restoration and @@ -3009,7 +3009,7 @@ const ChatInputInner: React.FC = (props) => { // When editing a /compact command, regenerate the actual summarization request let actualMessageText = messageTextForSend; - let muxMetadata: MuxMessageMetadata | undefined = skillMuxMetadata ?? promptMuxMetadata; + let muxMetadata: XumMessageMetadata | undefined = skillXumMetadata ?? promptXumMetadata; if (combinedSkillRefs.length > 0) { muxMetadata = withAgentSkillRefs(muxMetadata, combinedSkillRefs); } diff --git a/src/browser/features/ChatInput/useCreationWorkspace.ts b/src/browser/features/ChatInput/useCreationWorkspace.ts index 80d9e47583..bfcaf357f1 100644 --- a/src/browser/features/ChatInput/useCreationWorkspace.ts +++ b/src/browser/features/ChatInput/useCreationWorkspace.ts @@ -64,7 +64,7 @@ import { estimatePersistedChatAttachmentsChars, MAX_PERSISTED_ATTACHMENT_DRAFT_CHARS, } from "@/browser/features/ChatInput/draftAttachmentsStorage"; -import type { MuxMessageMetadata } from "@/common/types/message"; +import type { XumMessageMetadata } from "@/common/types/message"; import type { ParsedCommand } from "@/browser/utils/slashCommands/types"; import { processSlashCommand, type SlashCommandContext } from "@/browser/utils/chatCommands"; import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events"; @@ -706,12 +706,12 @@ export function useCreationWorkspace({ // SendMessageOptions.muxMetadata is a black box (z.any); the creation // caller only ever passes XumMessageMetadata built in ChatInput. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const overrideMuxMetadata: MuxMessageMetadata | undefined = optionsOverride?.muxMetadata; + const overrideXumMetadata: XumMessageMetadata | undefined = optionsOverride?.muxMetadata; const overrideRawCommand = - overrideMuxMetadata && - "rawCommand" in overrideMuxMetadata && - typeof overrideMuxMetadata.rawCommand === "string" - ? overrideMuxMetadata.rawCommand + overrideXumMetadata && + "rawCommand" in overrideXumMetadata && + typeof overrideXumMetadata.rawCommand === "string" + ? overrideXumMetadata.rawCommand : null; if (stagingFailed) { @@ -794,12 +794,12 @@ export function useCreationWorkspace({ // over message text for transcript display) lacks the notice; patch it // so the displayed message keeps the staged attachment chips. const muxMetadataWithNotice = - stagingOutcome.staged.length > 0 && overrideMuxMetadata && overrideRawCommand !== null + stagingOutcome.staged.length > 0 && overrideXumMetadata && overrideRawCommand !== null ? { - ...overrideMuxMetadata, + ...overrideXumMetadata, rawCommand: appendStagedAttachmentNotice(overrideRawCommand, stagingOutcome.staged), } - : overrideMuxMetadata; + : overrideXumMetadata; // A transport-level rejection (e.g. oRPC disconnect) must flow through // the same failure branch as success:false: the outer catch would skip diff --git a/src/browser/features/ChatInput/utils.ts b/src/browser/features/ChatInput/utils.ts index df052a4ef5..c85383b098 100644 --- a/src/browser/features/ChatInput/utils.ts +++ b/src/browser/features/ChatInput/utils.ts @@ -21,7 +21,7 @@ import { dedupeMcpPromptRefs, type AgentSkillReference, type MCPPromptReference, - type MuxMessageMetadata, + type XumMessageMetadata, } from "@/common/types/message"; import type { FilePart } from "@/common/orpc/types"; import type { ChatAttachment } from "@/browser/features/ChatInput/ChatAttachments"; @@ -90,7 +90,7 @@ export function buildSkillInvocationMetadata( rawCommand: string, descriptor: AgentSkillDescriptor, argumentText: string -): MuxMessageMetadata { +): XumMessageMetadata { return buildAgentSkillMetadata({ rawCommand, commandPrefix: `/${descriptor.name}`, diff --git a/src/browser/features/Messages/MessageRenderer.stories.tsx b/src/browser/features/Messages/MessageRenderer.stories.tsx index 5689d57998..aacc7602ef 100644 --- a/src/browser/features/Messages/MessageRenderer.stories.tsx +++ b/src/browser/features/Messages/MessageRenderer.stories.tsx @@ -1,4 +1,4 @@ -import type { WorkspaceChatMessage, ChatMuxMessage } from "@/common/orpc/types"; +import type { WorkspaceChatMessage, ChatXumMessage } from "@/common/orpc/types"; import type { AppStory } from "@/browser/stories/meta.js"; import { appMeta, AppWithMocks, PIXEL_DISABLED, PIXEL_DUAL_THEME } from "@/browser/stories/meta.js"; import { @@ -400,9 +400,9 @@ export const Conversation: AppStory = { historySequence: 0, hiddenCount: 42, }, - } as unknown as ChatMuxMessage; + } as unknown as ChatXumMessage; - const messages: ChatMuxMessage[] = [ + const messages: ChatXumMessage[] = [ hiddenIndicator, createUserMessage("msg-1", "Add authentication to the user API endpoint", { historySequence: 1, @@ -523,7 +523,7 @@ export const WorkflowTriggeredCommand: AppStory = { { name: "shallow-review", args: workflowRun.args }, { runId, status: workflowRun.status, result: null, run: workflowRun }, STABLE_TIMESTAMP - 295000 - ) as ChatMuxMessage; + ) as ChatXumMessage; workflowCard.type = "message"; workflowCard.metadata = { historySequence: 2, diff --git a/src/browser/features/Messages/MessageWindow.test.tsx b/src/browser/features/Messages/MessageWindow.test.tsx index 19db2a53b7..4856ba4ae0 100644 --- a/src/browser/features/Messages/MessageWindow.test.tsx +++ b/src/browser/features/Messages/MessageWindow.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { afterEach, beforeEach, describe, expect, test, mock } from "bun:test"; import { cleanup, render } from "@testing-library/react"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { installDom } from "../../../../tests/ui/dom"; import { MessageWindow } from "./MessageWindow"; @@ -21,7 +21,7 @@ function createAssistantMessage(overrides: { isStreaming?: boolean; isLastPartOfMessage?: boolean; isPartial?: boolean; -}): MuxMessage { +}): XumMessage { return { id: "assistant-1", role: "assistant", @@ -29,7 +29,7 @@ function createAssistantMessage(overrides: { parts: [], metadata: { model: "test-model", partial: overrides.isPartial ? true : undefined }, ...(overrides as object), - } as unknown as MuxMessage; + } as unknown as XumMessage; } function renderAssistantWindow(overrides: Parameters[0]) { @@ -112,7 +112,7 @@ describe("MessageWindow meta-row stability", () => { historySequence: 1, parts: [], metadata: {}, - } as unknown as MuxMessage; + } as unknown as XumMessage; const { container } = render( diff --git a/src/browser/features/Messages/MessageWindow.tsx b/src/browser/features/Messages/MessageWindow.tsx index 6a46823c72..b6da9c4889 100644 --- a/src/browser/features/Messages/MessageWindow.tsx +++ b/src/browser/features/Messages/MessageWindow.tsx @@ -1,5 +1,5 @@ import { cn } from "@/common/lib/utils"; -import type { DisplayedMessage, MuxMessage, QueuedMessage } from "@/common/types/message"; +import type { DisplayedMessage, XumMessage, QueuedMessage } from "@/common/types/message"; import { TranscriptQuoteRoot } from "./TranscriptQuoteBoundary"; import { formatTimestamp } from "@/browser/utils/ui/dateTime"; import { Code2Icon } from "lucide-react"; @@ -23,7 +23,7 @@ export interface ButtonConfig { interface MessageWindowProps { label: ReactNode; variant?: "assistant" | "user"; - message: MuxMessage | DisplayedMessage | QueuedMessage; + message: XumMessage | DisplayedMessage | QueuedMessage; buttons?: ButtonConfig[]; children: ReactNode; className?: string; diff --git a/src/browser/features/Messages/StreamErrorMessage.tsx b/src/browser/features/Messages/StreamErrorMessage.tsx index 15840ff822..70d7d2e854 100644 --- a/src/browser/features/Messages/StreamErrorMessage.tsx +++ b/src/browser/features/Messages/StreamErrorMessage.tsx @@ -87,10 +87,10 @@ const StreamErrorMessageBase: React.FC = (props) => const isModelRefusalError = message.errorType === "model_refusal"; // Gateway quota failures need explicit attribution so users know mux gateway credits, // not a provider quota, are blocking the request. - const isMuxGatewayQuotaError = + const isXumGatewayQuotaError = message.errorType === "quota" && message.routedThroughGateway === true; - const title = isMuxGatewayQuotaError + const title = isXumGatewayQuotaError ? "Xum Gateway credits depleted" : isAnthropicOverloaded ? "Service overloaded" @@ -102,11 +102,11 @@ const StreamErrorMessageBase: React.FC = (props) => ? "Model refused to respond" : "Stream Error"; const pill = isAnthropicOverloaded ? "overloaded" : message.errorType; - const body = isMuxGatewayQuotaError + const body = isXumGatewayQuotaError ? "Your Xum Gateway credits have been depleted. Add credits or configure another provider to continue." : message.error; - const ctaAction = isMuxGatewayQuotaError ? ( + const ctaAction = isXumGatewayQuotaError ? ( @@ -2010,7 +2010,7 @@ export function ProvidersSection() { @@ -2048,7 +2048,7 @@ export function ProvidersSection() { variant="outline" size="sm" onClick={() => { - void refreshMuxGatewayAccountStatus(); + void refreshXumGatewayAccountStatus(); }} disabled={muxGatewayAccountLoading} > @@ -2059,7 +2059,7 @@ export function ProvidersSection() {
Balance - {formatMuxGatewayBalance( + {formatXumGatewayBalance( muxGatewayAccountStatus?.remaining_microdollars )} diff --git a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx index 01e6d8c7b7..a2ab27a0ff 100644 --- a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx +++ b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx @@ -33,9 +33,9 @@ import { updatePersistedState } from "@/browser/hooks/usePersistedState"; import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; import { useRouting } from "@/browser/hooks/useRouting"; import { - formatMuxGatewayBalance, - useMuxGatewayAccountStatus, -} from "@/browser/hooks/useMuxGatewayAccountStatus"; + formatXumGatewayBalance, + useXumGatewayAccountStatus, +} from "@/browser/hooks/useXumGatewayAccountStatus"; import { KEYBINDS, formatKeybind } from "@/browser/utils/ui/keybinds"; import { getAgentsInitNudgeKey } from "@/common/constants/storage"; import { PROVIDER_DEFINITIONS, type ProviderName } from "@/common/constants/providers"; @@ -54,7 +54,7 @@ interface OAuthMessage { error?: unknown; } -type MuxGatewayLoginStatus = "idle" | "starting" | "waiting" | "success" | "error"; +type XumGatewayLoginStatus = "idle" | "starting" | "waiting" | "success" | "error"; function getServerAuthToken(): string | null { const urlToken = new URLSearchParams(window.location.search).get("token")?.trim(); @@ -219,8 +219,8 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { data: muxGatewayAccountStatus, error: muxGatewayAccountError, isLoading: muxGatewayAccountLoading, - refresh: refreshMuxGatewayAccountStatus, - } = useMuxGatewayAccountStatus(); + refresh: refreshXumGatewayAccountStatus, + } = useXumGatewayAccountStatus(); const backendBaseUrl = getBrowserBackendBaseUrl(); const backendOrigin = useMemo(() => { @@ -233,16 +233,16 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { const isDesktop = !!window.api; - const [muxGatewayLoginStatus, setMuxGatewayLoginStatus] = useState("idle"); - const [muxGatewayLoginError, setMuxGatewayLoginError] = useState(null); + const [muxGatewayLoginStatus, setXumGatewayLoginStatus] = useState("idle"); + const [muxGatewayLoginError, setXumGatewayLoginError] = useState(null); const muxGatewayLoginAttemptRef = useRef(0); - const [muxGatewayDesktopFlowId, setMuxGatewayDesktopFlowId] = useState(null); - const [muxGatewayServerState, setMuxGatewayServerState] = useState(null); + const [muxGatewayDesktopFlowId, setXumGatewayDesktopFlowId] = useState(null); + const [muxGatewayServerState, setXumGatewayServerState] = useState(null); const routing = useRouting(); - const disableMuxGatewayRoute = useCallback(() => { + const disableXumGatewayRoute = useCallback(() => { const nextPriority = routing.routePriority.filter((route) => route !== "mux-gateway"); if (!nextPriority.includes("direct")) { nextPriority.push("direct"); @@ -264,35 +264,35 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { routing.setRoutePreferences(nextPriority, nextOverrides); }, [routing]); - const cancelMuxGatewayLogin = useCallback(() => { + const cancelXumGatewayLogin = useCallback(() => { muxGatewayLoginAttemptRef.current++; if (isDesktop && api && muxGatewayDesktopFlowId) { void api.muxGatewayOauth.cancelDesktopFlow({ flowId: muxGatewayDesktopFlowId }); } - setMuxGatewayDesktopFlowId(null); - setMuxGatewayServerState(null); - setMuxGatewayLoginStatus("idle"); - setMuxGatewayLoginError(null); + setXumGatewayDesktopFlowId(null); + setXumGatewayServerState(null); + setXumGatewayLoginStatus("idle"); + setXumGatewayLoginError(null); }, [api, isDesktop, muxGatewayDesktopFlowId]); - const startMuxGatewayLogin = useCallback(async () => { + const startXumGatewayLogin = useCallback(async () => { const attempt = ++muxGatewayLoginAttemptRef.current; try { - setMuxGatewayLoginError(null); - setMuxGatewayDesktopFlowId(null); - setMuxGatewayServerState(null); + setXumGatewayLoginError(null); + setXumGatewayDesktopFlowId(null); + setXumGatewayServerState(null); if (isDesktop) { if (!api) { - setMuxGatewayLoginStatus("error"); - setMuxGatewayLoginError("Xum API not connected."); + setXumGatewayLoginStatus("error"); + setXumGatewayLoginError("Xum API not connected."); return; } - setMuxGatewayLoginStatus("starting"); + setXumGatewayLoginStatus("starting"); const startResult = await api.muxGatewayOauth.startDesktopFlow(); if (attempt !== muxGatewayLoginAttemptRef.current) { @@ -303,14 +303,14 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { } if (!startResult.success) { - setMuxGatewayLoginStatus("error"); - setMuxGatewayLoginError(startResult.error); + setXumGatewayLoginStatus("error"); + setXumGatewayLoginError(startResult.error); return; } const { flowId, authorizeUrl } = startResult.data; - setMuxGatewayDesktopFlowId(flowId); - setMuxGatewayLoginStatus("waiting"); + setXumGatewayDesktopFlowId(flowId); + setXumGatewayLoginStatus("waiting"); // Desktop main process intercepts external window.open() calls and routes them via shell.openExternal. window.open(authorizeUrl, "_blank", "noopener"); @@ -326,9 +326,9 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { } if (waitResult.success) { - setMuxGatewayLoginStatus("success"); + setXumGatewayLoginStatus("success"); - const refreshPromise = refreshMuxGatewayAccountStatus(); + const refreshPromise = refreshXumGatewayAccountStatus(); // Time-box the balance check so a stalled gateway endpoint doesn't // block first-time onboarding defaults. const accountStatus = await Promise.race([ @@ -343,7 +343,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { const hasCredits = accountStatus == null || accountStatus.remaining_microdollars > 0; if (!hasCredits) { - disableMuxGatewayRoute(); + disableXumGatewayRoute(); } // If the timeout won the race, the balance check is still in flight. @@ -352,15 +352,15 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { void refreshPromise.then((laterStatus) => { if (muxGatewayLoginAttemptRef.current !== attempt) return; if (laterStatus?.remaining_microdollars === 0) { - disableMuxGatewayRoute(); + disableXumGatewayRoute(); } }); } return; } - setMuxGatewayLoginStatus("error"); - setMuxGatewayLoginError(waitResult.error); + setXumGatewayLoginStatus("error"); + setXumGatewayLoginError(waitResult.error); return; } @@ -371,7 +371,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { throw new Error("Popup blocked - please allow popups and try again."); } - setMuxGatewayLoginStatus("starting"); + setXumGatewayLoginStatus("starting"); const startUrl = new URL(`${backendBaseUrl}/auth/mux-gateway/start`); const authToken = getServerAuthToken(); @@ -414,19 +414,19 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { throw new Error(`Invalid response from ${startUrl.pathname}`); } - setMuxGatewayServerState(json.state); + setXumGatewayServerState(json.state); popup.location.href = json.authorizeUrl; - setMuxGatewayLoginStatus("waiting"); + setXumGatewayLoginStatus("waiting"); } catch (err) { if (attempt !== muxGatewayLoginAttemptRef.current) { return; } const message = getErrorMessage(err); - setMuxGatewayLoginStatus("error"); - setMuxGatewayLoginError(message); + setXumGatewayLoginStatus("error"); + setXumGatewayLoginError(message); } - }, [api, backendBaseUrl, disableMuxGatewayRoute, isDesktop, refreshMuxGatewayAccountStatus]); + }, [api, backendBaseUrl, disableXumGatewayRoute, isDesktop, refreshXumGatewayAccountStatus]); useEffect(() => { const attempt = muxGatewayLoginAttemptRef.current; @@ -445,9 +445,9 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { if (data.state !== muxGatewayServerState) return; if (data.ok === true) { - setMuxGatewayLoginStatus("success"); + setXumGatewayLoginStatus("success"); - const refreshPromise = refreshMuxGatewayAccountStatus(); + const refreshPromise = refreshXumGatewayAccountStatus(); // Time-box the balance check so a stalled gateway endpoint doesn't // block first-time onboarding defaults. void Promise.race([ @@ -459,7 +459,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { const hasCredits = accountStatus == null || accountStatus.remaining_microdollars > 0; if (!hasCredits) { - disableMuxGatewayRoute(); + disableXumGatewayRoute(); } // If the timeout won the race, the balance check is still in flight. @@ -468,7 +468,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { void refreshPromise.then((laterStatus) => { if (muxGatewayLoginAttemptRef.current !== attempt) return; if (laterStatus?.remaining_microdollars === 0) { - disableMuxGatewayRoute(); + disableXumGatewayRoute(); } }); } @@ -477,19 +477,19 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { } const msg = typeof data.error === "string" ? data.error : "Login failed"; - setMuxGatewayLoginStatus("error"); - setMuxGatewayLoginError(msg); + setXumGatewayLoginStatus("error"); + setXumGatewayLoginError(msg); }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [ backendOrigin, - disableMuxGatewayRoute, + disableXumGatewayRoute, isDesktop, muxGatewayLoginStatus, muxGatewayServerState, - refreshMuxGatewayAccountStatus, + refreshXumGatewayAccountStatus, ]); const muxGatewayCouponCodeSet = providersConfig?.["mux-gateway"]?.couponCodeSet ?? false; @@ -623,7 +623,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { variant="secondary" size="sm" onClick={() => { - void refreshMuxGatewayAccountStatus(); + void refreshXumGatewayAccountStatus(); }} disabled={muxGatewayAccountLoading} > @@ -635,7 +635,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
Balance - {formatMuxGatewayBalance(muxGatewayAccountStatus?.remaining_microdollars)} + {formatXumGatewayBalance(muxGatewayAccountStatus?.remaining_microdollars)}
@@ -677,7 +677,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
{muxGatewayLoginInProgress && ( - )} @@ -1000,7 +1000,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { }, [ addProject, agentPickerShortcut, - cancelMuxGatewayLogin, + cancelXumGatewayLogin, commandPaletteActionsShortcut, commandPaletteShortcut, configuredProviders.length, @@ -1019,8 +1019,8 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { projectFormHasError, userProjects.size, providersConfig, - refreshMuxGatewayAccountStatus, - startMuxGatewayLogin, + refreshXumGatewayAccountStatus, + startXumGatewayLogin, onboardingProviders, ]); @@ -1038,9 +1038,9 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { useEffect(() => { if (currentStep?.key !== "mux-gateway" && muxGatewayLoginInProgress) { - cancelMuxGatewayLogin(); + cancelXumGatewayLogin(); } - }, [cancelMuxGatewayLogin, currentStep?.key, muxGatewayLoginInProgress]); + }, [cancelXumGatewayLogin, currentStep?.key, muxGatewayLoginInProgress]); if (!currentStep) { return null; @@ -1079,7 +1079,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) { { - cancelMuxGatewayLogin(); + cancelXumGatewayLogin(); props.onDismiss(); }} dismissLabel={null} diff --git a/src/browser/features/Tools/ProposePlanToolCall.tsx b/src/browser/features/Tools/ProposePlanToolCall.tsx index 4cf57c0c89..bc6a2f45d2 100644 --- a/src/browser/features/Tools/ProposePlanToolCall.tsx +++ b/src/browser/features/Tools/ProposePlanToolCall.tsx @@ -30,7 +30,7 @@ import { } from "@/browser/utils/ui/keybinds"; import { useStartHere } from "@/browser/hooks/useStartHere"; import { useReviews } from "@/browser/hooks/useReviews"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { useCopyToClipboard } from "@/browser/hooks/useCopyToClipboard"; import { TranscriptQuoteRoot } from "../Messages/TranscriptQuoteBoundary"; import { cn } from "@/common/lib/utils"; @@ -442,7 +442,7 @@ export const ProposePlanToolCall: React.FC = (props) = if (!workspaceId || !api) return; try { - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( `${args.idPrefix}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, "assistant", startHereContent, diff --git a/src/browser/features/Tools/SubagentTranscriptDialog.tsx b/src/browser/features/Tools/SubagentTranscriptDialog.tsx index d10b2fc514..fd88c35d7c 100644 --- a/src/browser/features/Tools/SubagentTranscriptDialog.tsx +++ b/src/browser/features/Tools/SubagentTranscriptDialog.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; -import type { DisplayedMessage, MuxMessage } from "@/common/types/message"; +import type { DisplayedMessage, XumMessage } from "@/common/types/message"; import type { ThinkingLevel } from "@/common/types/thinking"; -import type { ChatMuxMessage } from "@/common/orpc/types"; +import type { ChatXumMessage } from "@/common/orpc/types"; import { useAPI } from "@/browser/contexts/API"; import { StreamingMessageAggregator } from "@/browser/utils/messages/StreamingMessageAggregator"; import { @@ -83,7 +83,7 @@ const SubagentTranscriptViewer: React.FC<{ const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [messages, setMessages] = useState(null); + const [messages, setMessages] = useState(null); useEffect(() => { // TaskToolCall renders this dialog component for each completed task even while closed. @@ -146,7 +146,7 @@ const SubagentTranscriptViewer: React.FC<{ aggregator.setShowAllMessages(true); for (const msg of messages) { - const event: ChatMuxMessage = { ...msg, type: "message" }; + const event: ChatXumMessage = { ...msg, type: "message" }; aggregator.handleMessage(event); } diff --git a/src/browser/hooks/useCompactAndRetry.ts b/src/browser/hooks/useCompactAndRetry.ts index 282ec651e3..732543a61e 100644 --- a/src/browser/hooks/useCompactAndRetry.ts +++ b/src/browser/hooks/useCompactAndRetry.ts @@ -26,7 +26,7 @@ import { withMcpPromptRefs, type CompactionFollowUpInput, type DisplayedMessage, - type MuxMessageMetadata, + type XumMessageMetadata, } from "@/common/types/message"; interface CompactAndRetryState { @@ -73,7 +73,7 @@ export function buildFollowUpFromSource( // provider content and preserve slash metadata so retried rows remain editable // as their original invocation. let text = source.content; - let promptMetadata: MuxMessageMetadata | undefined; + let promptMetadata: XumMessageMetadata | undefined; // Trim only for command detection/extraction (the parser accepted the // original send from a trimmed view); rawCommand keeps source.content // verbatim so the retried row displays exactly like the original. diff --git a/src/browser/hooks/useMuxGatewayAccountStatus.test.ts b/src/browser/hooks/useMuxGatewayAccountStatus.test.ts deleted file mode 100644 index 5d27b33e16..0000000000 --- a/src/browser/hooks/useMuxGatewayAccountStatus.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { formatMuxGatewayBalance } from "./useMuxGatewayAccountStatus"; - -describe("formatMuxGatewayBalance", () => { - test("formats zero balance", () => { - expect(formatMuxGatewayBalance(0)).toBe("$0.00"); - }); - - test("formats positive balance", () => { - expect(formatMuxGatewayBalance(5_000_000)).toBe("$5.00"); - }); - - test("returns dash for null", () => { - expect(formatMuxGatewayBalance(null)).toBe("—"); - }); - - test("returns dash for undefined", () => { - expect(formatMuxGatewayBalance(undefined)).toBe("—"); - }); -}); diff --git a/src/browser/hooks/useResumeStream.test.tsx b/src/browser/hooks/useResumeStream.test.tsx index ad3b2445eb..77408e67ed 100644 --- a/src/browser/hooks/useResumeStream.test.tsx +++ b/src/browser/hooks/useResumeStream.test.tsx @@ -9,7 +9,7 @@ import { useWorkspaceStoreRaw, workspaceStore, } from "@/browser/stores/WorkspaceStore"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { useResumeStream } from "./useResumeStream"; @@ -46,7 +46,7 @@ function createWorkspaceMetadata(workspaceId: string): FrontendWorkspaceMetadata function seedWorkspaceWithUserMessage(workspaceId: string): void { workspaceStore.addWorkspace(createWorkspaceMetadata(workspaceId)); - const userMessage: MuxMessage = { + const userMessage: XumMessage = { id: `${workspaceId}-user-1`, role: "user", parts: [{ type: "text", text: "Hi" }], diff --git a/src/browser/hooks/useStartHere.ts b/src/browser/hooks/useStartHere.ts index 9ad99921e3..86871a6173 100644 --- a/src/browser/hooks/useStartHere.ts +++ b/src/browser/hooks/useStartHere.ts @@ -1,7 +1,7 @@ import { useState } from "react"; import React from "react"; import { StartHereModal } from "@/browser/components/StartHereModal/StartHereModal"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { useAPI } from "@/browser/contexts/API"; /** @@ -40,7 +40,7 @@ export function useStartHere( setIsStartingHere(true); try { - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( `start-here-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, "assistant", content, diff --git a/src/browser/hooks/useXumGatewayAccountStatus.test.ts b/src/browser/hooks/useXumGatewayAccountStatus.test.ts new file mode 100644 index 0000000000..21d4702f9b --- /dev/null +++ b/src/browser/hooks/useXumGatewayAccountStatus.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test"; +import { formatXumGatewayBalance } from "./useXumGatewayAccountStatus"; + +describe("formatXumGatewayBalance", () => { + test("formats zero balance", () => { + expect(formatXumGatewayBalance(0)).toBe("$0.00"); + }); + + test("formats positive balance", () => { + expect(formatXumGatewayBalance(5_000_000)).toBe("$5.00"); + }); + + test("returns dash for null", () => { + expect(formatXumGatewayBalance(null)).toBe("—"); + }); + + test("returns dash for undefined", () => { + expect(formatXumGatewayBalance(undefined)).toBe("—"); + }); +}); diff --git a/src/browser/hooks/useMuxGatewayAccountStatus.ts b/src/browser/hooks/useXumGatewayAccountStatus.ts similarity index 86% rename from src/browser/hooks/useMuxGatewayAccountStatus.ts rename to src/browser/hooks/useXumGatewayAccountStatus.ts index 84520c527b..44a432baf6 100644 --- a/src/browser/hooks/useMuxGatewayAccountStatus.ts +++ b/src/browser/hooks/useXumGatewayAccountStatus.ts @@ -5,12 +5,12 @@ import { MUX_GATEWAY_SESSION_EXPIRED_MESSAGE } from "@/common/constants/muxGatew import { formatCostWithDollar } from "@/common/utils/tokens/usageAggregator"; import { getErrorMessage } from "@/common/utils/errors"; -export interface MuxGatewayAccountStatus { +export interface XumGatewayAccountStatus { remaining_microdollars: number; ai_gateway_concurrent_requests_per_user: number; } -export function formatMuxGatewayBalance(remainingMicrodollars: number | null | undefined): string { +export function formatXumGatewayBalance(remainingMicrodollars: number | null | undefined): string { if (remainingMicrodollars === null || remainingMicrodollars === undefined) { return "—"; } @@ -18,13 +18,13 @@ export function formatMuxGatewayBalance(remainingMicrodollars: number | null | u return formatCostWithDollar(remainingMicrodollars / 1_000_000); } -export function useMuxGatewayAccountStatus() { +export function useXumGatewayAccountStatus() { const { api } = useAPI(); - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - const refresh = useCallback(async (): Promise => { + const refresh = useCallback(async (): Promise => { if (!api) { return null; } diff --git a/src/browser/stores/WorkspaceConsumerManager.ts b/src/browser/stores/WorkspaceConsumerManager.ts index 2ffe8b2464..7961fb3a49 100644 --- a/src/browser/stores/WorkspaceConsumerManager.ts +++ b/src/browser/stores/WorkspaceConsumerManager.ts @@ -1,7 +1,7 @@ import type { WorkspaceConsumersState } from "./WorkspaceStore"; import type { StreamingMessageAggregator } from "@/browser/utils/messages/StreamingMessageAggregator"; import type { ChatStats } from "@/common/types/chatStats"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { sliceMessagesForProviderFromLatestContextBoundary } from "@/common/utils/messages/compactionBoundary"; const TOKENIZER_CANCELLED_MESSAGE = "Cancelled by newer request"; @@ -11,7 +11,7 @@ const latestRequestByWorkspace = new Map(); async function calculateTokenStatsLatest( workspaceId: string, - messages: MuxMessage[], + messages: XumMessage[], model: string ): Promise { const orpcClient = window.__ORPC_CLIENT__; diff --git a/src/browser/stores/WorkspaceStore.ts b/src/browser/stores/WorkspaceStore.ts index 527855f91e..f9a19953ae 100644 --- a/src/browser/stores/WorkspaceStore.ts +++ b/src/browser/stores/WorkspaceStore.ts @@ -1,6 +1,6 @@ import assert from "@/common/utils/assert"; import { stripStagedAttachmentNotice } from "@/browser/features/ChatInput/stagedAttachments"; -import type { MuxMessage, DisplayedMessage, QueuedMessage } from "@/common/types/message"; +import type { XumMessage, DisplayedMessage, QueuedMessage } from "@/common/types/message"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { isGoalPendingPersistence, type GoalSnapshot } from "@/common/types/goal"; import type { @@ -49,7 +49,7 @@ import { isBashOutputEvent, isTaskCreatedEvent, isWorkflowRunAttachedEvent, - isMuxMessage, + isXumMessage, isQueuedMessageChanged, isRestoreToInput, isRuntimeStatus, @@ -187,7 +187,7 @@ export interface WorkspaceState { isHydratingTranscript: boolean; hasOlderHistory: boolean; loadingOlderHistory: boolean; - muxMessages: MuxMessage[]; + muxMessages: XumMessage[]; currentModel: string | null; currentThinkingLevel: string | null; recencyTimestamp: number | null; @@ -338,7 +338,7 @@ export interface WorkflowToolLiveRunState { interface WorkspaceChatTransientState { caughtUp: boolean; isHydratingTranscript: boolean; - historicalMessages: MuxMessage[]; + historicalMessages: XumMessage[]; pendingStreamEvents: WorkspaceChatMessage[]; replayingHistory: boolean; queuedMessage: QueuedMessage | null; @@ -575,7 +575,7 @@ function calculateSubscriptionBackoffMs(attempt: number): number { return Math.min(SUBSCRIPTION_RETRY_BASE_MS * 2 ** attempt, SUBSCRIPTION_RETRY_MAX_MS); } -function getMaxHistorySequence(messages: MuxMessage[]): number | undefined { +function getMaxHistorySequence(messages: XumMessage[]): number | undefined { let max: number | undefined; for (const message of messages) { const seq = message.metadata?.historySequence; @@ -2522,7 +2522,7 @@ export class WorkspaceStore { ); } - const historicalMessages = result.messages.filter(isMuxMessage); + const historicalMessages = result.messages.filter(isXumMessage); const ignoredCount = result.messages.length - historicalMessages.length; if (ignoredCount > 0) { console.warn( @@ -4803,7 +4803,7 @@ export class WorkspaceStore { } // Regular messages (XumMessage without type field) - if (isMuxMessage(data)) { + if (isXumMessage(data)) { const transient = this.assertChatTransientState(workspaceId); if (!transient.caughtUp) { @@ -5220,7 +5220,7 @@ export function showAllMessages(workspaceId: string): void { * Add an ephemeral message to a workspace and trigger a re-render. * Used for displaying frontend-only messages like /plan output. */ -export function addEphemeralMessage(workspaceId: string, message: MuxMessage): void { +export function addEphemeralMessage(workspaceId: string, message: XumMessage): void { const store = getStoreInstance(); const aggregator = store.getAggregator(workspaceId); if (aggregator) { diff --git a/src/browser/stories/helpers/chatSetup.ts b/src/browser/stories/helpers/chatSetup.ts index bcdeac4ee1..b1d40882a6 100644 --- a/src/browser/stories/helpers/chatSetup.ts +++ b/src/browser/stories/helpers/chatSetup.ts @@ -1,11 +1,11 @@ import type { AgentSkillDescriptor, AgentSkillIssue } from "@/common/types/agentSkill"; import type { WorkspaceChatMessage, - ChatMuxMessage, + ChatXumMessage, ProvidersConfigMap, WorkspaceStatsSnapshot, } from "@/common/orpc/types"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { ThinkingLevel } from "@/common/types/thinking"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import type { BackgroundProcessInfo } from "@/common/orpc/schemas/api"; @@ -48,7 +48,7 @@ export interface SimpleChatSetupOptions { workspaceName?: string; projectName?: string; projectPath?: string; - messages: ChatMuxMessage[]; + messages: ChatXumMessage[]; /** Additional child workspaces that should appear alongside the selected chat workspace. */ additionalWorkspaces?: FrontendWorkspaceMetadata[]; gitStatus?: GitStatusFixture; @@ -63,7 +63,7 @@ export interface SimpleChatSetupOptions { /** Mock transcripts for workspace.getSubagentTranscript (taskId -> persisted transcript response). */ subagentTranscripts?: Map< string, - { messages: MuxMessage[]; model?: string; thinkingLevel?: ThinkingLevel } + { messages: XumMessage[]; model?: string; thinkingLevel?: ThinkingLevel } >; /** Optional custom chat handler for emitting additional events (e.g., queued-message-changed) */ onChat?: (workspaceId: string, emit: (msg: WorkspaceChatMessage) => void) => void; @@ -195,7 +195,7 @@ export interface StreamingChatSetupOptions { workspaceId?: string; workspaceName?: string; projectName?: string; - messages: ChatMuxMessage[]; + messages: ChatXumMessage[]; streamingMessageId: string; model?: string; historySequence: number; diff --git a/src/browser/stories/mocks/chatHandlers.ts b/src/browser/stories/mocks/chatHandlers.ts index a630677d46..e3e3ee2686 100644 --- a/src/browser/stories/mocks/chatHandlers.ts +++ b/src/browser/stories/mocks/chatHandlers.ts @@ -1,4 +1,4 @@ -import type { WorkspaceChatMessage, ChatMuxMessage } from "@/common/orpc/types"; +import type { WorkspaceChatMessage, ChatXumMessage } from "@/common/orpc/types"; import { STABLE_TIMESTAMP } from "./workspaces"; // ═══════════════════════════════════════════════════════════════════════════════ @@ -9,7 +9,7 @@ import { STABLE_TIMESTAMP } from "./workspaces"; type ChatHandler = (callback: (event: WorkspaceChatMessage) => void) => () => void; /** Creates a chat handler that sends messages then caught-up */ -export function createStaticChatHandler(messages: ChatMuxMessage[]): ChatHandler { +export function createStaticChatHandler(messages: ChatXumMessage[]): ChatHandler { return (callback) => { setTimeout(() => { for (const msg of messages) { @@ -24,7 +24,7 @@ export function createStaticChatHandler(messages: ChatMuxMessage[]): ChatHandler /** Creates a chat handler with streaming state */ export function createStreamingChatHandler(opts: { - messages: ChatMuxMessage[]; + messages: ChatXumMessage[]; streamingMessageId: string; model: string; historySequence: number; diff --git a/src/browser/stories/mocks/messages.ts b/src/browser/stories/mocks/messages.ts index f593da08a7..27d93bc0c5 100644 --- a/src/browser/stories/mocks/messages.ts +++ b/src/browser/stories/mocks/messages.ts @@ -1,11 +1,11 @@ -import type { ChatMuxMessage } from "@/common/orpc/types"; +import type { ChatXumMessage } from "@/common/orpc/types"; import type { BashMonitorWakeDisplayRecord, - MuxMessageMetadata, - MuxTextPart, - MuxReasoningPart, - MuxFilePart, - MuxToolPart, + XumMessageMetadata, + XumTextPart, + XumReasoningPart, + XumFilePart, + XumToolPart, } from "@/common/types/message"; import { formatSubagentReportEnvelope } from "@/common/utils/subagentReportEnvelope"; import type { ThinkingLevel } from "@/common/types/thinking"; @@ -18,7 +18,7 @@ import { import { STABLE_TIMESTAMP } from "./workspaces"; /** Part type for message construction */ -type MuxPart = MuxTextPart | MuxReasoningPart | MuxFilePart | MuxToolPart; +type MuxPart = XumTextPart | XumReasoningPart | XumFilePart | XumToolPart; // ═══════════════════════════════════════════════════════════════════════════════ // MESSAGE FACTORY @@ -30,11 +30,11 @@ export function createUserMessage( historySequence: number; timestamp?: number; images?: string[]; - muxMetadata?: MuxMessageMetadata; + muxMetadata?: XumMessageMetadata; /** Mark as synthetic (auto-generated by system, not user-typed). Shows "AUTO" badge. */ synthetic?: boolean; } -): ChatMuxMessage { +): ChatXumMessage { const parts: MuxPart[] = [{ type: "text", text }]; if (opts.images) { for (const url of opts.images) { @@ -60,7 +60,7 @@ function createGoalSyntheticMessage( text: string, opts: { historySequence: number; timestamp?: number }, kind: GoalSyntheticMessageKind -): ChatMuxMessage { +): ChatXumMessage { return { type: "message", id, @@ -80,7 +80,7 @@ export function createGoalBudgetLimitMessage( id: string, text: string, opts: { historySequence: number; timestamp?: number } -): ChatMuxMessage { +): ChatXumMessage { return createGoalSyntheticMessage(id, text, opts, GOAL_BUDGET_LIMIT_KIND); } @@ -88,7 +88,7 @@ export function createGoalContinuationMessage( id: string, text: string, opts: { historySequence: number; timestamp?: number } -): ChatMuxMessage { +): ChatXumMessage { return createGoalSyntheticMessage(id, text, opts, GOAL_CONTINUATION_KIND); } @@ -105,7 +105,7 @@ export function createBashMonitorWakeMessage( promptText: string; records: BashMonitorWakeDisplayRecord[]; } -): ChatMuxMessage { +): ChatXumMessage { return { type: "message", id, @@ -139,7 +139,7 @@ export function createSubagentReportMessage( thinkingLevel?: ThinkingLevel; structuredOutput?: unknown; } -): ChatMuxMessage { +): ChatXumMessage { return createUserMessage( id, formatSubagentReportEnvelope({ @@ -164,7 +164,7 @@ export function createSubagentReportMessage( export function createCompactionRequestMessage( id: string, opts: { historySequence: number; timestamp?: number; rawCommand?: string } -): ChatMuxMessage { +): ChatXumMessage { const rawCommand = opts.rawCommand ?? "/compact"; return { type: "message", @@ -197,7 +197,7 @@ export function createAssistantMessage( /** Custom context usage for testing context meter display */ contextUsage?: { inputTokens: number; outputTokens: number; totalTokens?: number }; } -): ChatMuxMessage { +): ChatXumMessage { const parts: MuxPart[] = []; if (opts.reasoning) { parts.push({ type: "reasoning", text: opts.reasoning }); diff --git a/src/browser/stories/mocks/orpc.ts b/src/browser/stories/mocks/orpc.ts index f2cca63c9a..22cf9fd65f 100644 --- a/src/browser/stories/mocks/orpc.ts +++ b/src/browser/stories/mocks/orpc.ts @@ -33,7 +33,7 @@ import type { ServerAuthSession, } from "@/common/orpc/types"; import type { ProjectGitStatusResult as ApiProjectGitStatusResult } from "@/common/orpc/schemas/api"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { ThinkingLevel } from "@/common/types/thinking"; import type { DebugLlmRequestSnapshot } from "@/common/types/debugLlmRequest"; import type { NameGenerationError } from "@/common/types/errors"; @@ -225,7 +225,7 @@ export interface MockORPCClientOptions { /** Mock transcripts for workspace.getSubagentTranscript (taskId -> persisted transcript response). */ subagentTranscripts?: Map< string, - { messages: MuxMessage[]; model?: string; thinkingLevel?: ThinkingLevel } + { messages: XumMessage[]; model?: string; thinkingLevel?: ThinkingLevel } >; /** Global MCP server configuration (Settings → MCP) */ globalMcpServers?: Record; @@ -371,7 +371,7 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl lastLlmRequestSnapshots = new Map(), subagentTranscripts = new Map< string, - { messages: MuxMessage[]; model?: string; thinkingLevel?: ThinkingLevel } + { messages: XumMessage[]; model?: string; thinkingLevel?: ThinkingLevel } >(), additionalSystemContexts = new Map(), workspaceStatsSnapshots = new Map(), @@ -808,7 +808,7 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl notifyConfigChanged(); return Promise.resolve(undefined); }, - updateMuxGatewayPrefs: (input: { + updateXumGatewayPrefs: (input: { muxGatewayEnabled: boolean; muxGatewayModels: string[]; }) => { @@ -935,7 +935,7 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl notifyConfigChanged(); return Promise.resolve(undefined); }, - unenrollMuxGovernor: () => Promise.resolve(undefined), + unenrollXumGovernor: () => Promise.resolve(undefined), }, agents: { list: (_input: { diff --git a/src/browser/stories/mocks/tools.ts b/src/browser/stories/mocks/tools.ts index aa1c83e4d6..bfa88b933c 100644 --- a/src/browser/stories/mocks/tools.ts +++ b/src/browser/stories/mocks/tools.ts @@ -1,8 +1,8 @@ import type { - MuxTextPart, - MuxReasoningPart, - MuxFilePart, - MuxToolPart, + XumTextPart, + XumReasoningPart, + XumFilePart, + XumToolPart, } from "@/common/types/message"; import type { CodeExecutionResult, @@ -11,7 +11,7 @@ import type { import type { TodoItem } from "@/common/types/tools"; /** Part type for message construction */ -type MuxPart = MuxTextPart | MuxReasoningPart | MuxFilePart | MuxToolPart; +type MuxPart = XumTextPart | XumReasoningPart | XumFilePart | XumToolPart; // ═══════════════════════════════════════════════════════════════════════════════ // TOOL CALL FACTORY diff --git a/src/browser/utils/chatCommands.ts b/src/browser/utils/chatCommands.ts index 9054eb8579..477640ad83 100644 --- a/src/browser/utils/chatCommands.ts +++ b/src/browser/utils/chatCommands.ts @@ -15,7 +15,7 @@ import type { SendMessageOptions, } from "@/common/orpc/types"; import { - type MuxMessageMetadata, + type XumMessageMetadata, type CompactionRequestData, type CompactionFollowUpRequest, type CompactionFollowUpInput, @@ -1497,7 +1497,7 @@ export interface CompactionResult { */ export function prepareCompactionMessage(options: CompactionOptions): { messageText: string; - metadata: MuxMessageMetadata; + metadata: XumMessageMetadata; sendOptions: SendMessageOptions; } { // followUpContent is the content that will be auto-sent after compaction. @@ -1560,7 +1560,7 @@ export function prepareCompactionMessage(options: CompactionOptions): { // Apply compaction overrides const sendOptions = applyCompactionOverrides(options.sendMessageOptions, compactData); - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: fullRawCommand, commandPrefix: commandLine, diff --git a/src/browser/utils/messages/StreamingMessageAggregator.skills.test.ts b/src/browser/utils/messages/StreamingMessageAggregator.skills.test.ts index f193dabcf3..818c37399c 100644 --- a/src/browser/utils/messages/StreamingMessageAggregator.skills.test.ts +++ b/src/browser/utils/messages/StreamingMessageAggregator.skills.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import type { AgentSkillScope } from "@/common/types/agentSkill"; import { - createMuxMessage, + createXumMessage, type AgentSkillReference, type DisplayedUserMessage, type MCPPromptReference, @@ -131,7 +131,7 @@ const createSkillSnapshotMessage = ({ body?: string; frontmatterYaml?: string; }) => - createMuxMessage( + createXumMessage( id, "user", `\n${body}\n`, @@ -160,7 +160,7 @@ const createSkillInvocationMessage = ({ historySequence: number; }) => { const command = `/${skillName}`; - return createMuxMessage(id, "user", command, { + return createXumMessage(id, "user", command, { historySequence, timestamp: 0, muxMetadata: { @@ -184,7 +184,7 @@ const createInlineSkillMessage = ({ historySequence: number; refs: AgentSkillReference[]; }) => - createMuxMessage(id, "user", content, { + createXumMessage(id, "user", content, { historySequence, timestamp: 0, muxMetadata: { @@ -476,7 +476,7 @@ describe("Skill load error tracking", () => { const aggregator = createAggregator(); aggregator.loadHistoricalMessages([ - createMuxMessage("msg-1", "assistant", "", undefined, [ + createXumMessage("msg-1", "assistant", "", undefined, [ { type: "dynamic-tool", toolCallId: "tc-1", @@ -525,7 +525,7 @@ describe("Agent skill snapshot association", () => { it("attaches MCP prompt snapshots to slash and inline invocation surfaces", () => { const aggregator = createAggregator(); - const snapshot = createMuxMessage("prompt-snapshot", "user", "Expanded prompt body", { + const snapshot = createXumMessage("prompt-snapshot", "user", "Expanded prompt body", { historySequence: 1, timestamp: 0, synthetic: true, @@ -536,7 +536,7 @@ describe("Agent skill snapshot association", () => { invokingMessageId: "prompt-slash", }, }); - const slash = createMuxMessage("prompt-slash", "user", "Using MCP prompt coder/review", { + const slash = createXumMessage("prompt-slash", "user", "Using MCP prompt coder/review", { historySequence: 2, timestamp: 0, muxMetadata: { @@ -554,7 +554,7 @@ describe("Agent skill snapshot association", () => { agentSkillRefs: [{ skillName: "tdd", scope: "global", source: "inline" }], }, }); - const inlineSnapshot = createMuxMessage("prompt-snapshot-2", "user", "Expanded prompt body", { + const inlineSnapshot = createXumMessage("prompt-snapshot-2", "user", "Expanded prompt body", { historySequence: 3, timestamp: 0, synthetic: true, @@ -565,7 +565,7 @@ describe("Agent skill snapshot association", () => { invokingMessageId: "prompt-inline", }, }); - const inline = createMuxMessage("prompt-inline", "user", "Use $mcp__coder__review", { + const inline = createXumMessage("prompt-inline", "user", "Use $mcp__coder__review", { historySequence: 4, timestamp: 0, muxMetadata: { @@ -605,7 +605,7 @@ describe("Agent skill snapshot association", () => { // Simulates corrupted chat.jsonl rows; muxMetadata persists untyped. const corruptMcpRefs: unknown = [null, {}, { serverName: "coder" }, 42]; const corruptSkillRefs: unknown = [null, { skillName: "tdd" }]; - const corrupted = createMuxMessage("prompt-corrupted", "user", "Corrupted refs", { + const corrupted = createXumMessage("prompt-corrupted", "user", "Corrupted refs", { historySequence: 1, timestamp: 0, muxMetadata: { @@ -623,7 +623,7 @@ describe("Agent skill snapshot association", () => { source: "slash", }, ]; - const valid = createMuxMessage("prompt-valid", "user", "Using MCP prompt coder/review", { + const valid = createXumMessage("prompt-valid", "user", "Using MCP prompt coder/review", { historySequence: 2, timestamp: 0, muxMetadata: { @@ -653,7 +653,7 @@ describe("Agent skill snapshot association", () => { commandKey: "mcp__coder__review", source: "slash" as const, }; - const snapshot = createMuxMessage("prompt-snapshot", "user", "Expanded prompt body", { + const snapshot = createXumMessage("prompt-snapshot", "user", "Expanded prompt body", { historySequence: 1, timestamp: 0, synthetic: true, @@ -664,7 +664,7 @@ describe("Agent skill snapshot association", () => { invokingMessageId: "prompt-first", }, }); - const first = createMuxMessage("prompt-first", "user", "Using MCP prompt coder/review", { + const first = createXumMessage("prompt-first", "user", "Using MCP prompt coder/review", { historySequence: 2, timestamp: 0, muxMetadata: { @@ -674,7 +674,7 @@ describe("Agent skill snapshot association", () => { mcpPromptRefs: [promptRef], }, }); - const second = createMuxMessage("prompt-second", "user", "Using MCP prompt coder/review", { + const second = createXumMessage("prompt-second", "user", "Using MCP prompt coder/review", { historySequence: 3, timestamp: 0, muxMetadata: { @@ -698,7 +698,7 @@ describe("Agent skill snapshot association", () => { it("does not attach a crash-orphaned snapshot to a later same-prompt turn", () => { const aggregator = createAggregator(); - const orphan = createMuxMessage("orphan-snapshot", "user", "Stale expansion", { + const orphan = createXumMessage("orphan-snapshot", "user", "Stale expansion", { historySequence: 1, timestamp: 0, synthetic: true, @@ -709,7 +709,7 @@ describe("Agent skill snapshot association", () => { invokingMessageId: "user-crashed", }, }); - const later = createMuxMessage("prompt-later", "user", "Use $mcp__coder__review", { + const later = createXumMessage("prompt-later", "user", "Use $mcp__coder__review", { historySequence: 2, timestamp: 0, muxMetadata: { @@ -975,7 +975,7 @@ describe("Agent skill snapshot association", () => { frontmatterYaml: "name: tdd\ndescription: Test-first changes", }); const command = "/pull-requests use $tdd"; - const invocation = createMuxMessage("mixed-invoke", "user", command, { + const invocation = createXumMessage("mixed-invoke", "user", command, { historySequence: 3, timestamp: 0, muxMetadata: { diff --git a/src/browser/utils/messages/StreamingMessageAggregator.status.test.ts b/src/browser/utils/messages/StreamingMessageAggregator.status.test.ts index e531b7f610..4643c4e4e6 100644 --- a/src/browser/utils/messages/StreamingMessageAggregator.status.test.ts +++ b/src/browser/utils/messages/StreamingMessageAggregator.status.test.ts @@ -1,6 +1,6 @@ import { afterAll, afterEach, beforeEach, describe, expect, it } from "bun:test"; import { getStatusStateKey } from "@/common/constants/storage"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { StreamingMessageAggregator } from "./StreamingMessageAggregator"; const CREATED_AT = "2024-01-01T00:00:00.000Z"; @@ -109,7 +109,7 @@ function statusMessage( input: StatusInput, options: { output?: StatusResult; historySequence?: number; timestamp?: number } = {} ) { - const message = createMuxMessage(id, "assistant", "", { + const message = createXumMessage(id, "assistant", "", { timestamp: options.timestamp ?? options.historySequence ?? 1, historySequence: options.historySequence ?? 1, }); @@ -147,7 +147,7 @@ afterAll(() => { describe("ask_user_question waiting state", () => { it("treats partial ask_user_question as executing (waiting) not interrupted", () => { const aggregator = createAggregator(); - const assistantMessage = createMuxMessage("assistant-1", "assistant", "", { + const assistantMessage = createXumMessage("assistant-1", "assistant", "", { timestamp: 1000, historySequence: 1, partial: true, @@ -252,7 +252,7 @@ describe("StreamingMessageAggregator - Agent Status", () => { aggregator.handleMessage({ type: "message", - ...createMuxMessage("msg2", "user", "What's next?", { + ...createXumMessage("msg2", "user", "What's next?", { timestamp: Date.now(), historySequence: 2, }), @@ -298,7 +298,7 @@ describe("StreamingMessageAggregator - Agent Status", () => { it("should reconstruct agentStatus when loading historical messages", () => { const aggregator = createAggregator(); aggregator.loadHistoricalMessages([ - createMuxMessage("msg1", "user", "Hello", { timestamp: Date.now(), historySequence: 1 }), + createXumMessage("msg1", "user", "Hello", { timestamp: Date.now(), historySequence: 1 }), (() => { const message = statusMessage( "msg2", @@ -362,7 +362,7 @@ describe("StreamingMessageAggregator - Agent Status", () => { { emoji: "🧪", message: "Running tests" }, { timestamp: 1000 } ), - createMuxMessage("assistant2", "assistant", "[compaction summary]", { + createXumMessage("assistant2", "assistant", "[compaction summary]", { timestamp: 2000, historySequence: 2, }), @@ -383,7 +383,7 @@ describe("StreamingMessageAggregator - Agent Status", () => { const aggregator = createAggregator(WORKSPACE_ID); aggregator.loadHistoricalMessages([ - createMuxMessage("assistant2", "assistant", "[compacted history]", { + createXumMessage("assistant2", "assistant", "[compacted history]", { timestamp: 3000, historySequence: 1, }), @@ -469,7 +469,7 @@ describe("StreamingMessageAggregator - Agent Status", () => { aggregator.handleMessage({ type: "message", - ...createMuxMessage("user1", "user", "Continue", { + ...createXumMessage("user1", "user", "Continue", { timestamp: Date.now(), historySequence: 2, }), @@ -494,14 +494,14 @@ describe("StreamingMessageAggregator - Agent Status", () => { const testUrl = "https://github.com/owner/repo/pull/123"; aggregator.loadHistoricalMessages([ - createMuxMessage("user1", "user", "Make a PR", { timestamp: 1000, historySequence: 1 }), + createXumMessage("user1", "user", "Make a PR", { timestamp: 1000, historySequence: 1 }), statusMessage( "assistant1", "tool1", { emoji: "🔗", message: "PR submitted", url: testUrl }, { timestamp: 1001, historySequence: 2 } ), - createMuxMessage("user2", "user", "Continue", { timestamp: 2000, historySequence: 3 }), + createXumMessage("user2", "user", "Continue", { timestamp: 2000, historySequence: 3 }), statusMessage( "assistant2", "tool2", diff --git a/src/browser/utils/messages/StreamingMessageAggregator.test.ts b/src/browser/utils/messages/StreamingMessageAggregator.test.ts index 022114e995..a1a5c7d114 100644 --- a/src/browser/utils/messages/StreamingMessageAggregator.test.ts +++ b/src/browser/utils/messages/StreamingMessageAggregator.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test"; import { CONTEXT_BOUNDARY_KINDS } from "@/common/constants/contextBoundary"; -import { MuxMessageSchema } from "@/common/orpc/schemas/message"; -import { createMuxMessage, type DisplayedMessage } from "@/common/types/message"; +import { XumMessageSchema } from "@/common/orpc/schemas/message"; +import { createXumMessage, type DisplayedMessage } from "@/common/types/message"; import { formatSubagentReportEnvelope } from "@/common/utils/subagentReportEnvelope"; import { buildWorkflowRunCardMessage } from "@/common/utils/workflowRunMessages"; import { getInterruptionContext } from "@/common/utils/messages/retryEligibility"; @@ -46,7 +46,7 @@ const waitForInitThrottle = () => new Promise((r) => setTimeout(r, 120)); function seedPendingStreamState(aggregator: StreamingMessageAggregator): void { aggregator.handleMessage({ - ...createMuxMessage("user-1", "user", "Hello", { + ...createXumMessage("user-1", "user", "Hello", { historySequence: 1, timestamp: Date.now(), muxMetadata: { @@ -235,7 +235,7 @@ function historicalToolMessage( workflowRun?: { runId: string; timestamp: number }; } = {} ) { - const message = createMuxMessage(id, "assistant", "", { + const message = createXumMessage(id, "assistant", "", { partial: options.partial, historySequence: options.historySequence ?? 1, timestamp: Date.now(), @@ -437,7 +437,7 @@ describe("StreamingMessageAggregator", () => { test("propagates modelFallback metadata to displayed assistant rows", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const fallback = createMuxMessage("a1", "assistant", "answer", { + const fallback = createXumMessage("a1", "assistant", "answer", { timestamp: 1, historySequence: 1, model: "anthropic:claude-opus-4-8", @@ -446,7 +446,7 @@ describe("StreamingMessageAggregator", () => { refusedModels: ["openai:gpt-5.5"], }, }); - const plain = createMuxMessage("a2", "assistant", "no fallback", { + const plain = createXumMessage("a2", "assistant", "no fallback", { timestamp: 2, historySequence: 2, model: "anthropic:claude-opus-4-8", @@ -467,12 +467,12 @@ describe("StreamingMessageAggregator", () => { test("should hide synthetic messages by default", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const synthetic = createMuxMessage("s1", "user", "synthetic", { + const synthetic = createXumMessage("s1", "user", "synthetic", { timestamp: 1, historySequence: 1, synthetic: true, }); - const user = createMuxMessage("u1", "user", "hello", { + const user = createXumMessage("u1", "user", "hello", { timestamp: 2, historySequence: 2, }); @@ -488,13 +488,13 @@ describe("StreamingMessageAggregator", () => { test("should show uiVisible synthetic messages by default", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const syntheticVisible = createMuxMessage("s1", "user", "synthetic visible", { + const syntheticVisible = createXumMessage("s1", "user", "synthetic visible", { timestamp: 1, historySequence: 1, synthetic: true, uiVisible: true, }); - const user = createMuxMessage("u1", "user", "hello", { + const user = createXumMessage("u1", "user", "hello", { timestamp: 2, historySequence: 2, }); @@ -513,7 +513,7 @@ describe("StreamingMessageAggregator", () => { test("renders unknown persisted muxMetadata as an ordinary row", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const legacyMessage = MuxMessageSchema.parse({ + const legacyMessage = XumMessageSchema.parse({ id: "legacy-1", role: "user", parts: [{ type: "text", text: "ordinary persisted content" }], @@ -543,7 +543,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage("assistant-1", "assistant", "I incorporated the progress update.", { + createXumMessage("assistant-1", "assistant", "I incorporated the progress update.", { timestamp: 1, historySequence: 1, }), @@ -552,7 +552,7 @@ describe("StreamingMessageAggregator", () => { ); aggregator.handleMessage({ - ...createMuxMessage( + ...createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -583,7 +583,7 @@ describe("StreamingMessageAggregator", () => { }); test("keeps an anchored completed report between reasoning emitted before and after it", () => { - const report = createMuxMessage( + const report = createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -657,7 +657,7 @@ describe("StreamingMessageAggregator", () => { reloaded.loadHistoricalMessages( [ { - ...createMuxMessage("assistant-1", "assistant", "", { + ...createXumMessage("assistant-1", "assistant", "", { timestamp: 1, historySequence: 1, model: "openai:gpt-5", @@ -688,7 +688,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage( + createXumMessage( "progress-1", "user", formatSubagentReportEnvelope({ @@ -700,16 +700,16 @@ describe("StreamingMessageAggregator", () => { }), { timestamp: 1, historySequence: 1, synthetic: true } ), - createMuxMessage("assistant-progress", "assistant", "I incorporated the update.", { + createXumMessage("assistant-progress", "assistant", "I incorporated the update.", { timestamp: 2, historySequence: 2, }), - createMuxMessage("manual-user", "user", "Continue with other work", { + createXumMessage("manual-user", "user", "Continue with other work", { timestamp: 3, historySequence: 3, }), { - ...createMuxMessage("assistant-current", "assistant", "", { + ...createXumMessage("assistant-current", "assistant", "", { timestamp: 4, historySequence: 4, }), @@ -718,7 +718,7 @@ describe("StreamingMessageAggregator", () => { { type: "reasoning", text: "reasoning B" }, ], }, - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -768,7 +768,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage( + createXumMessage( "progress-1", "user", formatSubagentReportEnvelope({ @@ -780,11 +780,11 @@ describe("StreamingMessageAggregator", () => { }), { timestamp: 1, historySequence: 1, synthetic: true } ), - createMuxMessage("assistant-1", "assistant", "Final answer", { + createXumMessage("assistant-1", "assistant", "Final answer", { timestamp: 2, historySequence: 2, }), - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -817,7 +817,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage( + createXumMessage( "progress-1", "user", formatSubagentReportEnvelope({ @@ -829,7 +829,7 @@ describe("StreamingMessageAggregator", () => { }), { timestamp: 1, historySequence: 1, synthetic: true } ), - createMuxMessage( + createXumMessage( "report-2", "user", formatSubagentReportEnvelope({ @@ -841,11 +841,11 @@ describe("StreamingMessageAggregator", () => { }), { timestamp: 2, historySequence: 2, synthetic: true, uiVisible: true } ), - createMuxMessage("assistant-1", "assistant", "Final answer", { + createXumMessage("assistant-1", "assistant", "Final answer", { timestamp: 3, historySequence: 3, }), - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -891,14 +891,14 @@ describe("StreamingMessageAggregator", () => { }, ])("repairs a trailing report before a $label-only assistant", ({ parts, expectedType }) => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const assistant = createMuxMessage( + const assistant = createXumMessage( "assistant-1", "assistant", "", { timestamp: 1, historySequence: 1 }, [...parts] ); - const report = createMuxMessage( + const report = createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -925,7 +925,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage( + createXumMessage( "progress-1", "user", formatSubagentReportEnvelope({ @@ -937,19 +937,19 @@ describe("StreamingMessageAggregator", () => { }), { timestamp: 1, historySequence: 1, synthetic: true } ), - createMuxMessage("assistant-progress", "assistant", "I incorporated the update.", { + createXumMessage("assistant-progress", "assistant", "I incorporated the update.", { timestamp: 2, historySequence: 2, }), - createMuxMessage("manual-user", "user", "One more question", { + createXumMessage("manual-user", "user", "One more question", { timestamp: 3, historySequence: 3, }), - createMuxMessage("assistant-manual", "assistant", "Answering the later question.", { + createXumMessage("assistant-manual", "assistant", "Answering the later question.", { timestamp: 4, historySequence: 4, }), - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -987,16 +987,16 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage("assistant-1", "assistant", "Old answer", { + createXumMessage("assistant-1", "assistant", "Old answer", { timestamp: 1, historySequence: 1, }), - createMuxMessage("reset-1", "assistant", "", { + createXumMessage("reset-1", "assistant", "", { timestamp: 2, historySequence: 2, contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }), - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1022,20 +1022,20 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage("reset-1", "assistant", "", { + createXumMessage("reset-1", "assistant", "", { timestamp: 1, historySequence: 1, contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }), - createMuxMessage("new-user", "user", "New epoch question", { + createXumMessage("new-user", "user", "New epoch question", { timestamp: 2, historySequence: 2, }), - createMuxMessage("new-assistant", "assistant", "New epoch answer", { + createXumMessage("new-assistant", "assistant", "New epoch answer", { timestamp: 3, historySequence: 3, }), - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1063,11 +1063,11 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage("assistant-1", "assistant", "Original answer", { + createXumMessage("assistant-1", "assistant", "Original answer", { timestamp: 1, historySequence: 1, }), - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1079,11 +1079,11 @@ describe("StreamingMessageAggregator", () => { }), { timestamp: 2, historySequence: 2, synthetic: true, uiVisible: true } ), - createMuxMessage("manual-user", "user", "A later question", { + createXumMessage("manual-user", "user", "A later question", { timestamp: 3, historySequence: 3, }), - createMuxMessage("assistant-2", "assistant", "A later answer", { + createXumMessage("assistant-2", "assistant", "A later answer", { timestamp: 4, historySequence: 4, }), @@ -1108,7 +1108,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage( + createXumMessage( "progress-1", "user", formatSubagentReportEnvelope({ @@ -1120,11 +1120,11 @@ describe("StreamingMessageAggregator", () => { }), { timestamp: 1, historySequence: 1, synthetic: true } ), - createMuxMessage("assistant-1", "assistant", "Final answer", { + createXumMessage("assistant-1", "assistant", "Final answer", { timestamp: 2, historySequence: 2, }), - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1164,7 +1164,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( Array.from({ length: 70 }, (_, index) => - createMuxMessage(`older-${index}`, "assistant", `older response ${index}`, { + createXumMessage(`older-${index}`, "assistant", `older response ${index}`, { historySequence: index + 1, timestamp: index + 1, }) @@ -1188,7 +1188,7 @@ describe("StreamingMessageAggregator", () => { timestamp: 71, }); aggregator.handleMessage({ - ...createMuxMessage( + ...createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1243,14 +1243,14 @@ describe("StreamingMessageAggregator", () => { aggregator.loadHistoricalMessages( [ { - ...createMuxMessage("assistant-1", "assistant", "", { + ...createXumMessage("assistant-1", "assistant", "", { timestamp: 1, historySequence: 1, model: "openai:gpt-5", }), parts: [{ type: "text", text: "final answer" }], }, - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1297,7 +1297,7 @@ describe("StreamingMessageAggregator", () => { aggregator.loadHistoricalMessages( [ { - ...createMuxMessage("assistant-1", "assistant", "", { + ...createXumMessage("assistant-1", "assistant", "", { timestamp: 1, historySequence: 1, error: "Provider failed", @@ -1305,7 +1305,7 @@ describe("StreamingMessageAggregator", () => { }), parts: [{ type: "text", text: "before after" }], }, - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1343,7 +1343,7 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage( + createXumMessage( "report-1", "user", formatSubagentReportEnvelope({ @@ -1376,7 +1376,7 @@ describe("StreamingMessageAggregator", () => { test("renders persisted workflow slash invocation before workflow card", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const command = createMuxMessage("workflow-command", "user", "/deep-research mux", { + const command = createXumMessage("workflow-command", "user", "/deep-research mux", { timestamp: 1, historySequence: 1, muxMetadata: { @@ -1398,7 +1398,7 @@ describe("StreamingMessageAggregator", () => { uiVisible: true, muxMetadata: { type: "workflow-run-card-display", runId: "wfr_123" }, }; - const hiddenWorkflowResult = createMuxMessage( + const hiddenWorkflowResult = createXumMessage( "workflow-result", "user", '/deep-research mux\n\n{"reportMarkdown":"hidden"}', @@ -1413,7 +1413,7 @@ describe("StreamingMessageAggregator", () => { }, } ); - const assistant = createMuxMessage("assistant-1", "assistant", "Done", { + const assistant = createXumMessage("assistant-1", "assistant", "Done", { timestamp: 4, historySequence: 4, }); @@ -1446,7 +1446,7 @@ describe("StreamingMessageAggregator", () => { test("should strip legacy goal-cleared label from displayed summaries", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const legacySummary = createMuxMessage( + const legacySummary = createXumMessage( "goal-cleared-1", "assistant", 'Goal cleared: "Ship goal primitive" — spent $0.00 over 0 turns (status: active)', @@ -1470,7 +1470,7 @@ describe("StreamingMessageAggregator", () => { test("should preserve already-concise goal-cleared summaries", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const summary = createMuxMessage( + const summary = createXumMessage( "goal-cleared-2", "assistant", '"Ship goal primitive" — spent $0.00 over 0 turns (status: active)', @@ -1496,12 +1496,12 @@ describe("StreamingMessageAggregator", () => { withDebugLlmRequestEnabled(() => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const synthetic = createMuxMessage("s1", "user", "synthetic", { + const synthetic = createXumMessage("s1", "user", "synthetic", { timestamp: 1, historySequence: 1, synthetic: true, }); - const user = createMuxMessage("u1", "user", "hello", { + const user = createXumMessage("u1", "user", "hello", { timestamp: 2, historySequence: 2, }); @@ -1530,7 +1530,7 @@ describe("StreamingMessageAggregator", () => { const baseSequence = i * 3; // User message (always kept) manyMessages.push( - createMuxMessage(`u${i}`, "user", `msg-${i}`, { + createXumMessage(`u${i}`, "user", `msg-${i}`, { timestamp: baseSequence, historySequence: baseSequence, }) @@ -1558,7 +1558,7 @@ describe("StreamingMessageAggregator", () => { }); // Assistant response message (always kept) manyMessages.push( - createMuxMessage(`a${i}`, "assistant", `response-${i}`, { + createXumMessage(`a${i}`, "assistant", `response-${i}`, { historySequence: baseSequence + 2, timestamp: baseSequence + 2, model: "claude-3-5-sonnet-20241022", @@ -1628,13 +1628,13 @@ describe("StreamingMessageAggregator", () => { for (let i = 0; i < 200; i++) { const baseSequence = i * 2; manyMessages.push( - createMuxMessage(`u${i}`, "user", `msg-${i}`, { + createXumMessage(`u${i}`, "user", `msg-${i}`, { timestamp: baseSequence, historySequence: baseSequence, }) ); manyMessages.push( - createMuxMessage(`a${i}`, "assistant", `response-${i}`, { + createXumMessage(`a${i}`, "assistant", `response-${i}`, { historySequence: baseSequence + 1, timestamp: baseSequence + 1, model: "claude-3-5-sonnet-20241022", @@ -1680,9 +1680,9 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); aggregator.loadHistoricalMessages( [ - createMuxMessage("u1", "user", "first", { historySequence: 1, timestamp: 1 }), - createMuxMessage("u2", "user", "second", { historySequence: 2, timestamp: 2 }), - createMuxMessage("u3", "user", "third", { historySequence: 3, timestamp: 3 }), + createXumMessage("u1", "user", "first", { historySequence: 1, timestamp: 1 }), + createXumMessage("u2", "user", "second", { historySequence: 2, timestamp: 2 }), + createXumMessage("u3", "user", "third", { historySequence: 3, timestamp: 3 }), ], false ); @@ -1695,7 +1695,7 @@ describe("StreamingMessageAggregator", () => { test("should not show history-hidden when only user messages exceed cap", () => { // When all messages are user rows (always-keep type), no filtering occurs const manyMessages = Array.from({ length: 200 }, (_, i) => - createMuxMessage(`u${i}`, "user", `msg-${i}`, { + createXumMessage(`u${i}`, "user", `msg-${i}`, { timestamp: i, historySequence: i, }) @@ -1717,7 +1717,7 @@ describe("StreamingMessageAggregator", () => { const snapshotText = "\nBODY\n"; - const snapshot1 = createMuxMessage("s1", "assistant", snapshotText, { + const snapshot1 = createXumMessage("s1", "assistant", snapshotText, { timestamp: 1, historySequence: 1, synthetic: true, @@ -1729,7 +1729,7 @@ describe("StreamingMessageAggregator", () => { }, }); - const invocation = createMuxMessage("u1", "user", "/test-skill", { + const invocation = createXumMessage("u1", "user", "/test-skill", { timestamp: 2, historySequence: 2, muxMetadata: { @@ -1753,7 +1753,7 @@ describe("StreamingMessageAggregator", () => { } // Update the snapshot frontmatter without changing body or sha256. - const snapshot2 = createMuxMessage("s1", "assistant", snapshotText, { + const snapshot2 = createXumMessage("s1", "assistant", snapshotText, { timestamp: 1, historySequence: 1, synthetic: true, @@ -1907,7 +1907,7 @@ describe("StreamingMessageAggregator", () => { aggregator.loadHistoricalMessages( [ - createMuxMessage("older-user", "user", "Older history", { + createXumMessage("older-user", "user", "Older history", { historySequence: 1, timestamp: 1, }), @@ -1933,7 +1933,7 @@ describe("StreamingMessageAggregator", () => { aggregator.loadHistoricalMessages( [ - createMuxMessage("replayed-user", "user", "Replay window", { + createXumMessage("replayed-user", "user", "Replay window", { historySequence: 21, timestamp: 21, }), @@ -1985,11 +1985,11 @@ describe("StreamingMessageAggregator", () => { test("inserts a boundary row before compaction summary messages", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const before = createMuxMessage("user-before", "user", "Before compaction", { + const before = createXumMessage("user-before", "user", "Before compaction", { historySequence: 1, timestamp: 1, }); - const summary = createMuxMessage("summary-1", "assistant", "Compacted summary", { + const summary = createXumMessage("summary-1", "assistant", "Compacted summary", { historySequence: 2, timestamp: 2, compacted: "user", @@ -1997,7 +1997,7 @@ describe("StreamingMessageAggregator", () => { compactionEpoch: 3, muxMetadata: { type: "compaction-summary" }, }); - const after = createMuxMessage("user-after", "user", "After compaction", { + const after = createXumMessage("user-after", "user", "After compaction", { historySequence: 3, timestamp: 3, }); @@ -2025,11 +2025,11 @@ describe("StreamingMessageAggregator", () => { test("omits malformed compaction epoch values instead of crashing transcript rendering", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const before = createMuxMessage("user-before", "user", "Before compaction", { + const before = createXumMessage("user-before", "user", "Before compaction", { historySequence: 1, timestamp: 1, }); - const summaryWithMalformedEpoch = createMuxMessage( + const summaryWithMalformedEpoch = createXumMessage( "summary-malformed", "assistant", "Compacted summary", @@ -2042,7 +2042,7 @@ describe("StreamingMessageAggregator", () => { muxMetadata: { type: "compaction-summary" }, } ); - const after = createMuxMessage("user-after", "user", "After compaction", { + const after = createXumMessage("user-after", "user", "After compaction", { historySequence: 3, timestamp: 3, }); @@ -2066,7 +2066,7 @@ describe("StreamingMessageAggregator", () => { describe("live compaction boundary pruning", () => { // handleMessage expects ChatXumMessage (type: "message"), matching how the // backend emits events via emitChatEvent({ ...message, type: "message" }). - const asChatMessage = (msg: ReturnType) => ({ + const asChatMessage = (msg: ReturnType) => ({ ...msg, type: "message" as const, }); @@ -2076,13 +2076,13 @@ describe("StreamingMessageAggregator", () => { // Simulate messages accumulated during a live session (no prior compaction) const msg1 = asChatMessage( - createMuxMessage("user-1", "user", "First message", { + createXumMessage("user-1", "user", "First message", { historySequence: 0, timestamp: 1, }) ); const msg2 = asChatMessage( - createMuxMessage("assistant-1", "assistant", "Response", { + createXumMessage("assistant-1", "assistant", "Response", { historySequence: 1, timestamp: 2, }) @@ -2092,7 +2092,7 @@ describe("StreamingMessageAggregator", () => { aggregator.handleMessage(msg2); const summary = asChatMessage( - createMuxMessage("summary-1", "assistant", "Compacted summary", { + createXumMessage("summary-1", "assistant", "Compacted summary", { historySequence: 2, compacted: "user", compactionBoundary: true, @@ -2114,7 +2114,7 @@ describe("StreamingMessageAggregator", () => { // Epoch 0 messages (before any compaction) const epoch0Msg = asChatMessage( - createMuxMessage("epoch0-user", "user", "Old message", { + createXumMessage("epoch0-user", "user", "Old message", { historySequence: 0, timestamp: 1, }) @@ -2123,7 +2123,7 @@ describe("StreamingMessageAggregator", () => { // First compaction boundary (epoch 1) const boundary1 = asChatMessage( - createMuxMessage("boundary-1", "assistant", "Summary epoch 1", { + createXumMessage("boundary-1", "assistant", "Summary epoch 1", { historySequence: 1, compacted: "user", compactionBoundary: true, @@ -2135,7 +2135,7 @@ describe("StreamingMessageAggregator", () => { // Epoch 1 messages const epoch1Msg = asChatMessage( - createMuxMessage("epoch1-user", "user", "Message in epoch 1", { + createXumMessage("epoch1-user", "user", "Message in epoch 1", { historySequence: 2, timestamp: 3, }) @@ -2148,7 +2148,7 @@ describe("StreamingMessageAggregator", () => { // Second compaction boundary (epoch 2): existing messages with sequence < 3 // are pruned, then boundary-2 is appended. const boundary2 = asChatMessage( - createMuxMessage("boundary-2", "assistant", "Summary epoch 2", { + createXumMessage("boundary-2", "assistant", "Summary epoch 2", { historySequence: 3, compacted: "user", compactionBoundary: true, @@ -2169,11 +2169,11 @@ describe("StreamingMessageAggregator", () => { // Simulate initial replay window starting at historySequence 40. aggregator.loadHistoricalMessages( [ - createMuxMessage("history-40", "user", "Historical user", { + createXumMessage("history-40", "user", "Historical user", { historySequence: 40, timestamp: 40, }), - createMuxMessage("history-41", "assistant", "Historical assistant", { + createXumMessage("history-41", "assistant", "Historical assistant", { historySequence: 41, timestamp: 41, }), @@ -2186,7 +2186,7 @@ describe("StreamingMessageAggregator", () => { expect(beforeCompactionCursor?.history?.oldestHistorySequence).toBe(40); const boundary = asChatMessage( - createMuxMessage("boundary-60", "assistant", "Summary epoch 60", { + createXumMessage("boundary-60", "assistant", "Summary epoch 60", { historySequence: 60, compacted: "user", compactionBoundary: true, @@ -2204,13 +2204,13 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); const user = asChatMessage( - createMuxMessage("user-1", "user", "First message", { historySequence: 0 }) + createXumMessage("user-1", "user", "First message", { historySequence: 0 }) ); const assistant = asChatMessage( - createMuxMessage("assistant-1", "assistant", "Normal response", { historySequence: 1 }) + createXumMessage("assistant-1", "assistant", "Normal response", { historySequence: 1 }) ); const resetBoundary = asChatMessage( - createMuxMessage("reset-1", "assistant", "", { + createXumMessage("reset-1", "assistant", "", { historySequence: 2, contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }) @@ -2256,10 +2256,10 @@ describe("StreamingMessageAggregator", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); const msg1 = asChatMessage( - createMuxMessage("user-1", "user", "First message", { historySequence: 0 }) + createXumMessage("user-1", "user", "First message", { historySequence: 0 }) ); const msg2 = asChatMessage( - createMuxMessage("assistant-1", "assistant", "Normal response", { historySequence: 1 }) + createXumMessage("assistant-1", "assistant", "Normal response", { historySequence: 1 }) ); aggregator.handleMessage(msg1); @@ -2436,7 +2436,7 @@ describe("StreamingMessageAggregator", () => { }; aggregator.handleMessage({ - ...createMuxMessage("idle-compaction-request", "user", "/compact", { + ...createXumMessage("idle-compaction-request", "user", "/compact", { historySequence: 1, timestamp: Date.now(), muxMetadata: { @@ -2748,7 +2748,7 @@ describe("StreamingMessageAggregator", () => { test("rebuilds displayed rows when append replay overwrites an existing message id", () => { const aggregator = new StreamingMessageAggregator(TEST_CREATED_AT); - const partialMessage = createMuxMessage("msg-overwrite-1", "assistant", "partial", { + const partialMessage = createXumMessage("msg-overwrite-1", "assistant", "partial", { historySequence: 1, timestamp: 1, }); @@ -2762,7 +2762,7 @@ describe("StreamingMessageAggregator", () => { expect(initialAssistant).toBeDefined(); expect(initialAssistant?.content).toBe("partial"); - const finalizedMessage = createMuxMessage("msg-overwrite-1", "assistant", "finalized", { + const finalizedMessage = createXumMessage("msg-overwrite-1", "assistant", "finalized", { historySequence: 1, timestamp: 2, }); @@ -3013,7 +3013,7 @@ describe("StreamingMessageAggregator", () => { expect(existingMessage).toBeDefined(); expect(existingMessage?.parts.length).toBeGreaterThan(1); - const staleReplayMessage = createMuxMessage("msg-stale-append", "assistant", "placeholder", { + const staleReplayMessage = createXumMessage("msg-stale-append", "assistant", "placeholder", { historySequence: 1, timestamp: 1_050, }); @@ -3195,7 +3195,7 @@ describe("StreamingMessageAggregator", () => { }, }, }; - const compactionSummaryMessage = createMuxMessage( + const compactionSummaryMessage = createXumMessage( "summary-1", "assistant", "Compacted summary", @@ -3230,7 +3230,7 @@ describe("StreamingMessageAggregator", () => { completion = event.completion; }; - const summaryMessage = createMuxMessage("summary-default-continue", "assistant", "Summary", { + const summaryMessage = createXumMessage("summary-default-continue", "assistant", "Summary", { historySequence: 1, timestamp: Date.now(), compactionBoundary: true, @@ -3247,7 +3247,7 @@ describe("StreamingMessageAggregator", () => { aggregator.loadHistoricalMessages([summaryMessage], true); aggregator.handleMessage({ - ...createMuxMessage("synthetic-default-continue", "user", "Continue", { + ...createXumMessage("synthetic-default-continue", "user", "Continue", { historySequence: 2, timestamp: Date.now(), synthetic: true, @@ -3294,7 +3294,7 @@ describe("StreamingMessageAggregator", () => { completion = event.completion; }; - const summaryMessage = createMuxMessage("summary-user-follow-up", "assistant", "Summary", { + const summaryMessage = createXumMessage("summary-user-follow-up", "assistant", "Summary", { historySequence: 1, timestamp: Date.now(), compactionBoundary: true, @@ -3310,7 +3310,7 @@ describe("StreamingMessageAggregator", () => { aggregator.loadHistoricalMessages([summaryMessage], true); aggregator.handleMessage({ - ...createMuxMessage("synthetic-user-follow-up", "user", "Continue", { + ...createXumMessage("synthetic-user-follow-up", "user", "Continue", { historySequence: 2, timestamp: Date.now(), synthetic: true, @@ -3462,7 +3462,7 @@ describe("StreamingMessageAggregator", () => { aggregator.markOptimisticPendingStreamStart("openai:gpt-4o-mini"); aggregator.loadHistoricalMessages([ - createMuxMessage("user-1", "user", "Hello", { + createXumMessage("user-1", "user", "Hello", { historySequence: 1, timestamp: Date.now(), }), @@ -3488,7 +3488,7 @@ describe("StreamingMessageAggregator", () => { aggregator.loadHistoricalMessages( [ - createMuxMessage("assistant-1", "assistant", "Done", { + createXumMessage("assistant-1", "assistant", "Done", { historySequence: 2, timestamp: Date.now(), model: "openai:gpt-4o-mini", @@ -3933,7 +3933,7 @@ describe("StreamingMessageAggregator", () => { }); aggregator.handleMessage({ - ...createMuxMessage("user-1", "user", "Hello", { historySequence: 1 }), + ...createXumMessage("user-1", "user", "Hello", { historySequence: 1 }), type: "message", }); @@ -4215,7 +4215,7 @@ function historicalReviewPaneUpdateMessage( operation: "add" | "replace" = "replace", options: { historySequence?: number; toolCallId?: string } = {} ) { - const message = createMuxMessage(id, "assistant", "", { + const message = createXumMessage(id, "assistant", "", { historySequence: options.historySequence ?? 1, timestamp: Date.now(), muxMetadata: { type: "normal", requestedModel: TEST_MODEL }, @@ -4402,7 +4402,7 @@ describe("notify tool -> browser notifications", () => { // loadHistoricalMessages; historical notify results must stay silent. withFakeNotificationWindow(() => { const aggregator = createTestAggregator(); - const message = createMuxMessage("msg-1", "assistant", "", { + const message = createXumMessage("msg-1", "assistant", "", { historySequence: 1, timestamp: Date.now(), muxMetadata: { type: "normal", requestedModel: TEST_MODEL }, diff --git a/src/browser/utils/messages/StreamingMessageAggregator.ts b/src/browser/utils/messages/StreamingMessageAggregator.ts index 1d24c9a187..48b83b6f8f 100644 --- a/src/browser/utils/messages/StreamingMessageAggregator.ts +++ b/src/browser/utils/messages/StreamingMessageAggregator.ts @@ -1,12 +1,12 @@ import type { - MuxMessage, - MuxMetadata, + XumMessage, + XumMetadata, DisplayedMessage, CompactionRequestData, InlineSkillSnapshotMap, } from "@/common/types/message"; import { - createMuxMessage, + createXumMessage, getMcpPromptReferenceKey, isCompactionSummaryMetadata, sanitizeAgentSkillRefs, @@ -55,7 +55,7 @@ import type { DeleteMessage, OnChatCursor, } from "@/common/orpc/types"; -import { isInitStart, isInitOutput, isInitEnd, isMuxMessage } from "@/common/orpc/types"; +import { isInitStart, isInitOutput, isInitEnd, isXumMessage } from "@/common/orpc/types"; import { buildAggregateResponseCompleteMetadata, buildResponseCompleteMetadata, @@ -86,7 +86,7 @@ import { } from "@/common/utils/messages/compactionBoundary"; import { isWorkflowResultMessage } from "@/common/utils/workflowRunMessages"; -function isDisplayOnlyCompletedSubagentReport(message: MuxMessage): boolean { +function isDisplayOnlyCompletedSubagentReport(message: XumMessage): boolean { if ( message.role !== "user" || message.metadata?.synthetic !== true || @@ -361,7 +361,7 @@ interface MCPPromptSnapshotContent { } function maybeCollectMcpPromptSnapshot( - message: MuxMessage, + message: XumMessage, snapshots: Map ): void { const metadata = message.metadata?.mcpPromptSnapshot; @@ -393,7 +393,7 @@ function getAgentSkillSnapshotKey(scope: AgentSkillScope, skillName: string): st } function maybeCollectAgentSkillSnapshot( - message: MuxMessage, + message: XumMessage, snapshots: Map ): void { const snapshotMeta = message.metadata?.agentSkillSnapshot; @@ -481,7 +481,7 @@ interface TranscriptInsertionPlan { } interface TranscriptInsertion extends MessagePartSplitCut { - insertionMessage: MuxMessage; + insertionMessage: XumMessage; } export interface TranscriptRevealTarget { @@ -490,7 +490,7 @@ export interface TranscriptRevealTarget { } export class StreamingMessageAggregator { - private messages = new Map(); + private messages = new Map(); private activeStreams = new Map(); private backgroundHandoffCompletion: ReturnType = @@ -509,7 +509,7 @@ export class StreamingMessageAggregator { >(); private messageVersions = new Map(); private cache: { - allMessages?: MuxMessage[]; + allMessages?: XumMessage[]; displayedMessages?: DisplayedMessage[]; } = {}; private recencyTimestamp: number | null = null; @@ -1097,7 +1097,7 @@ export class StreamingMessageAggregator { * Extract the final response text from a message (text after the last tool call). * Used for notification body content. */ - private extractFinalResponseText(message: MuxMessage | undefined): string { + private extractFinalResponseText(message: XumMessage | undefined): string { if (!message) return ""; const parts = message.parts; const lastToolIndex = parts.findLastIndex((part) => part.type === "dynamic-tool"); @@ -1105,11 +1105,11 @@ export class StreamingMessageAggregator { return getTextPartContent(textPartsAfterTools).trim(); } - private compactMessageParts(message: MuxMessage): void { + private compactMessageParts(message: XumMessage): void { message.parts = mergeAdjacentParts(message.parts); } - addMessage(message: MuxMessage): void { + addMessage(message: XumMessage): void { const normalizedMessage = normalizeMessageRouteProvider(message); const existing = this.messages.get(normalizedMessage.id); if (existing) { @@ -1150,7 +1150,7 @@ export class StreamingMessageAggregator { * @param opts.skipDerivedState - Skip replaying messages into derived state when appending older history */ loadHistoricalMessages( - messages: MuxMessage[], + messages: XumMessage[], hasActiveStream = false, opts?: { mode?: "replace" | "append"; skipDerivedState?: boolean } ): void { @@ -1182,7 +1182,7 @@ export class StreamingMessageAggregator { } const overwrittenMessageIds: string[] = []; - const appliedMessages: MuxMessage[] = []; + const appliedMessages: XumMessage[] = []; // Add/overwrite messages in the map for (const message of messages) { @@ -1309,7 +1309,7 @@ export class StreamingMessageAggregator { return this.historyEpoch; } - getAllMessages(): MuxMessage[] { + getAllMessages(): XumMessage[] { this.cache.allMessages ??= Array.from(this.messages.values()).sort( (a, b) => (a.metadata?.historySequence ?? 0) - (b.metadata?.historySequence ?? 0) ); @@ -2061,7 +2061,7 @@ export class StreamingMessageAggregator { this.activeStreams.set(data.messageId, context); // Create initial streaming message with empty parts (deltas will append) - const streamingMessage = createMuxMessage(data.messageId, "assistant", "", { + const streamingMessage = createXumMessage(data.messageId, "assistant", "", { historySequence: data.historySequence, timestamp: Date.now(), model: data.model, @@ -2128,7 +2128,7 @@ export class StreamingMessageAggregator { const message = this.messages.get(data.messageId); if (message?.metadata) { // Transparent metadata merge - backend fields flow through automatically - const updatedMetadata: MuxMetadata = { + const updatedMetadata: XumMetadata = { ...message.metadata, ...data.metadata, }; @@ -2210,7 +2210,7 @@ export class StreamingMessageAggregator { data.metadata.routeProvider, data.metadata.routedThroughGateway ); - const message: MuxMessage = { + const message: XumMessage = { id: data.messageId, role: "assistant", metadata: { @@ -2320,7 +2320,7 @@ export class StreamingMessageAggregator { 0, ...Array.from(this.messages.values()).map((m) => m.metadata?.historySequence ?? 0) ); - const errorMessage: MuxMessage = { + const errorMessage: XumMessage = { id: data.messageId, role: "assistant", parts: [], @@ -2898,8 +2898,8 @@ export class StreamingMessageAggregator { return; } - if (isMuxMessage(data)) { - this.handleMuxMessage(data); + if (isXumMessage(data)) { + this.handleXumMessage(data); } } @@ -2992,7 +2992,7 @@ export class StreamingMessageAggregator { return false; } - private handleMuxMessage(data: MuxMessage): void { + private handleXumMessage(data: XumMessage): void { const incomingMessage = normalizeMessageRouteProvider(data); // Smart replacement logic for edits: if history was truncated, remove the @@ -3069,14 +3069,14 @@ export class StreamingMessageAggregator { this.setPendingStreamStartTime(Date.now()); } - private isContextBoundaryMessage(message: MuxMessage): boolean { + private isContextBoundaryMessage(message: XumMessage): boolean { return ( this.isCompactionBoundaryMessage(message) || getContextBoundaryKind(message) === CONTEXT_BOUNDARY_KINDS.RESET ); } - private isCompactionBoundaryMessage(message: MuxMessage): boolean { + private isCompactionBoundaryMessage(message: XumMessage): boolean { const muxMeta = message.metadata?.muxMetadata; return ( message.role === "assistant" && @@ -3094,7 +3094,7 @@ export class StreamingMessageAggregator { * from getHistoryFromLatestBoundary(skip=0). Older epochs remain accessible via * Load More. */ - private pruneBeforeLatestBoundary(incomingBoundary: MuxMessage): void { + private pruneBeforeLatestBoundary(incomingBoundary: XumMessage): void { const incomingBoundarySequence = incomingBoundary.metadata?.historySequence; // Self-healing guard: malformed boundary metadata should not crash live sessions. if (incomingBoundarySequence === undefined) return; @@ -3127,7 +3127,7 @@ export class StreamingMessageAggregator { } private buildDisplayedMessagesForMessage( - message: MuxMessage, + message: XumMessage, agentSkillSnapshot?: { frontmatterYaml?: string; body?: string }, inlineSkillSnapshots?: InlineSkillSnapshotMap ): DisplayedMessage[] { @@ -3151,7 +3151,7 @@ export class StreamingMessageAggregator { ); } - private getAssistantTailCut(message: MuxMessage): MessagePartSplitCut { + private getAssistantTailCut(message: XumMessage): MessagePartSplitCut { const parts = mergeAdjacentParts(message.parts); let textLength = 0; let reasoningLength = 0; @@ -3163,8 +3163,8 @@ export class StreamingMessageAggregator { } private isReportResponseAssistant( - message: MuxMessage, - shouldHideMessageFromTranscript: (message: MuxMessage) => boolean + message: XumMessage, + shouldHideMessageFromTranscript: (message: XumMessage) => boolean ): boolean { return ( message.role === "assistant" && @@ -3175,7 +3175,7 @@ export class StreamingMessageAggregator { ); } - private isDisplayOnlyTailMessage(message: MuxMessage): boolean { + private isDisplayOnlyTailMessage(message: XumMessage): boolean { return ( isDisplayOnlyCompletedSubagentReport(message) || message.metadata?.muxMetadata?.type === "plan-display" @@ -3183,11 +3183,11 @@ export class StreamingMessageAggregator { } private findReportResponseTarget( - allMessages: readonly MuxMessage[], + allMessages: readonly XumMessage[], reportIndex: number, - reportMessage: MuxMessage, - shouldHideMessageFromTranscript: (message: MuxMessage) => boolean - ): MuxMessage | undefined { + reportMessage: XumMessage, + shouldHideMessageFromTranscript: (message: XumMessage) => boolean + ): XumMessage | undefined { const report = parseSubagentReportEnvelope(getTextPartContent(reportMessage.parts)); if (!report) { return undefined; @@ -3258,8 +3258,8 @@ export class StreamingMessageAggregator { } private buildTranscriptInsertionPlan( - allMessages: readonly MuxMessage[], - shouldHideMessageFromTranscript: (message: MuxMessage) => boolean + allMessages: readonly XumMessage[], + shouldHideMessageFromTranscript: (message: XumMessage) => boolean ): TranscriptInsertionPlan { const messagesById = new Map(allMessages.map((message) => [message.id, message])); const messageIndexById = new Map(allMessages.map((message, index) => [message.id, index])); @@ -3337,9 +3337,9 @@ export class StreamingMessageAggregator { /** Split message parts at stable text/reasoning offsets and canonical part indexes. */ private splitMessagePartsAtTranscriptAnchors( - parts: MuxMessage["parts"], + parts: XumMessage["parts"], cutPoints: readonly MessagePartSplitCut[] - ): Array { + ): Array { const sortedCuts = [...cutPoints].sort((a, b) => { const aContentLength = a.textLength + a.reasoningLength; const bContentLength = b.textLength + b.reasoningLength; @@ -3347,7 +3347,7 @@ export class StreamingMessageAggregator { a.partIndex - b.partIndex || aContentLength - bContentLength || a.textLength - b.textLength ); }); - const segments: Array = sortedCuts.map(() => []); + const segments: Array = sortedCuts.map(() => []); segments.push([]); let cumulativeText = 0; @@ -3424,7 +3424,7 @@ export class StreamingMessageAggregator { * The final segment keeps the original id so active-stream state remains attached. */ private buildMessageDisplayWithInsertions( - message: MuxMessage, + message: XumMessage, insertions: readonly TranscriptInsertion[], agentSkillSnapshot?: { frontmatterYaml?: string; body?: string }, inlineSkillSnapshots?: InlineSkillSnapshotMap @@ -3449,7 +3449,7 @@ export class StreamingMessageAggregator { if (segParts.length > 0 || isLastSegment) { const segMessageId = isLastSegment ? message.id : `${message.id}#seg${i}`; - const segMessage: MuxMessage = { + const segMessage: XumMessage = { ...message, id: segMessageId, parts: segParts, @@ -3554,7 +3554,7 @@ export class StreamingMessageAggregator { const showSyntheticMessages = typeof window !== "undefined" && window.api?.debugLlmRequest === true; - const shouldHideMessageFromTranscript = (message: MuxMessage): boolean => + const shouldHideMessageFromTranscript = (message: XumMessage): boolean => !showSyntheticMessages && ((message.metadata?.synthetic === true && message.metadata?.uiVisible !== true) || isWorkflowResultMessage(message)); @@ -3565,7 +3565,7 @@ export class StreamingMessageAggregator { // row only sees the contiguous snapshot block directly before it; a // history-wide map would falsely attach an older turn's expansion. const blockMcpPromptSnapshotByKey = new Map(); - const isSyntheticSnapshotRow = (message: MuxMessage): boolean => + const isSyntheticSnapshotRow = (message: XumMessage): boolean => message.metadata?.synthetic === true && (message.metadata.mcpPromptSnapshot !== undefined || message.metadata.agentSkillSnapshot !== undefined || diff --git a/src/browser/utils/messages/applyToolOutputRedaction.test.ts b/src/browser/utils/messages/applyToolOutputRedaction.test.ts index 70596a4d00..e86c516795 100644 --- a/src/browser/utils/messages/applyToolOutputRedaction.test.ts +++ b/src/browser/utils/messages/applyToolOutputRedaction.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "bun:test"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { applyToolOutputRedaction } from "./applyToolOutputRedaction"; describe("applyToolOutputRedaction", () => { it("strips UI-only fields from provider-bound tool output", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -35,7 +35,7 @@ describe("applyToolOutputRedaction", () => { }); it("strips workflow run attachment hints from provider-bound tool parts", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-workflow", role: "assistant", @@ -89,7 +89,7 @@ describe("applyToolOutputRedaction", () => { }, ], }; - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -168,7 +168,7 @@ describe("applyToolOutputRedaction", () => { source: inlineSource, events: [{ sequence: 1, type: "log", at: "2026-01-01T00:00:00.000Z", message: "noisy" }], }; - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -234,7 +234,7 @@ describe("applyToolOutputRedaction", () => { }); it("sanitizes binary-like provider output strings for top-level and nested tools", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", diff --git a/src/browser/utils/messages/applyToolOutputRedaction.ts b/src/browser/utils/messages/applyToolOutputRedaction.ts index 54151cf88e..3569b645d8 100644 --- a/src/browser/utils/messages/applyToolOutputRedaction.ts +++ b/src/browser/utils/messages/applyToolOutputRedaction.ts @@ -2,7 +2,7 @@ * Strip UI-only tool output before sending to providers. * Produces a cloned array safe for sending to providers without touching persisted history/UI. */ -import type { MuxMessage, MuxToolPart } from "@/common/types/message"; +import type { XumMessage, XumToolPart } from "@/common/types/message"; import { sanitizeUnknownForProviderOutput } from "@/common/utils/providerOutputSanitization"; import { stripToolOutputUiOnly } from "@/common/utils/tools/toolOutputUiOnly"; import { stripWorkflowRunRecordForModel } from "@/common/utils/workflowRunMessages"; @@ -39,7 +39,7 @@ function stripResolvedSourcePath(source: unknown): unknown { return stripped; } -function stripWorkflowRunAttachment(part: MuxToolPart): MuxToolPart { +function stripWorkflowRunAttachment(part: XumToolPart): XumToolPart { if (part.workflowRun == null) { return part; } @@ -85,7 +85,7 @@ function stripLegacyImageToolOutputForModel(output: unknown): unknown { return stripped; } -export function applyToolOutputRedaction(messages: MuxMessage[]): MuxMessage[] { +export function applyToolOutputRedaction(messages: XumMessage[]): XumMessage[] { return messages.map((msg) => { if (msg.role !== "assistant") return msg; @@ -126,6 +126,6 @@ export function applyToolOutputRedaction(messages: MuxMessage[]): MuxMessage[] { return { ...msg, parts: newParts, - } satisfies MuxMessage; + } satisfies XumMessage; }); } diff --git a/src/browser/utils/messages/applyWorkspaceChatEventToAggregator.ts b/src/browser/utils/messages/applyWorkspaceChatEventToAggregator.ts index e476b174e2..4c4097634e 100644 --- a/src/browser/utils/messages/applyWorkspaceChatEventToAggregator.ts +++ b/src/browser/utils/messages/applyWorkspaceChatEventToAggregator.ts @@ -10,7 +10,7 @@ import { isInitEnd, isInitOutput, isInitStart, - isMuxMessage, + isXumMessage, isQueuedMessageChanged, isReasoningDelta, isReasoningEnd, @@ -101,7 +101,7 @@ function dispatchGoalChildBudgetToast(workspaceId: string, message: string): voi ); } -function dispatchMuxGatewaySessionExpired(): void { +function dispatchXumGatewaySessionExpired(): void { if (typeof window === "undefined") return; window.dispatchEvent(createCustomEvent(CUSTOM_EVENTS.MUX_GATEWAY_SESSION_EXPIRED)); } @@ -154,7 +154,7 @@ export function applyWorkspaceChatEventToAggregator( if (allowSideEffects && event.error === MUX_GATEWAY_SESSION_EXPIRED_MESSAGE) { // Dispatch session-expired event; useGateway() listens for it and // optimistically marks the gateway as unconfigured to stop routing. - dispatchMuxGatewaySessionExpired(); + dispatchXumGatewaySessionExpired(); } aggregator.handleStreamError(event); @@ -233,7 +233,7 @@ export function applyWorkspaceChatEventToAggregator( } // init-* and ChatXumMessage are handled via the aggregator's unified handleMessage. - if (isMuxMessage(event) || isInitStart(event) || isInitOutput(event) || isInitEnd(event)) { + if (isXumMessage(event) || isInitStart(event) || isInitOutput(event) || isInitEnd(event)) { aggregator.handleMessage(event); return "immediate"; } diff --git a/src/browser/utils/messages/buildSendMessageOptions.ts b/src/browser/utils/messages/buildSendMessageOptions.ts index f6b41756b5..a285c301fc 100644 --- a/src/browser/utils/messages/buildSendMessageOptions.ts +++ b/src/browser/utils/messages/buildSendMessageOptions.ts @@ -1,6 +1,6 @@ import type { SendMessageOptions } from "@/common/orpc/types"; import type { OpenAIReasoningMode, ThinkingLevel } from "@/common/types/thinking"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import { normalizeSelectedModel } from "@/common/utils/ai/models"; export interface ExperimentValues { @@ -17,7 +17,7 @@ export interface SendMessageOptionsInput { thinkingLevel: ThinkingLevel; reasoningMode: OpenAIReasoningMode; agentId: string; - providerOptions: MuxProviderOptions; + providerOptions: XumProviderOptions; experiments: ExperimentValues; disableWorkspaceAgents?: boolean; } diff --git a/src/browser/utils/messages/displayedMessageBuilder.bashMonitorWake.test.ts b/src/browser/utils/messages/displayedMessageBuilder.bashMonitorWake.test.ts index e34d581bd2..8a3f7ab8d9 100644 --- a/src/browser/utils/messages/displayedMessageBuilder.bashMonitorWake.test.ts +++ b/src/browser/utils/messages/displayedMessageBuilder.bashMonitorWake.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from "bun:test"; -import { createMuxMessage, type MuxMessageMetadata } from "@/common/types/message"; +import { createXumMessage, type XumMessageMetadata } from "@/common/types/message"; import { buildDisplayedMessagesForMessage } from "./displayedMessageBuilder"; -function buildUserRow(muxMetadata: MuxMessageMetadata) { - const message = createMuxMessage("wake-1", "user", "A background bash monitor matched output.", { +function buildUserRow(muxMetadata: XumMessageMetadata) { + const message = createXumMessage("wake-1", "user", "A background bash monitor matched output.", { historySequence: 1, synthetic: true, uiVisible: true, @@ -42,7 +42,7 @@ describe("buildDisplayedMessagesForMessage bash monitor wake metadata", () => { ["empty records", { type: "bash-monitor-wake", records: [] }], ["malformed record entry", { type: "bash-monitor-wake", records: [null, { kind: "match" }] }], ])("falls back to full-text rendering for %s", (_label, malformed) => { - const row = buildUserRow(malformed as unknown as MuxMessageMetadata); + const row = buildUserRow(malformed as unknown as XumMessageMetadata); expect(row.bashMonitorWake).toBeUndefined(); expect(row.content).toBe("A background bash monitor matched output."); }); diff --git a/src/browser/utils/messages/displayedMessageBuilder.staged.test.ts b/src/browser/utils/messages/displayedMessageBuilder.staged.test.ts index 1b56a05259..556b5a43ca 100644 --- a/src/browser/utils/messages/displayedMessageBuilder.staged.test.ts +++ b/src/browser/utils/messages/displayedMessageBuilder.staged.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { appendStagedAttachmentNotice } from "@/browser/features/ChatInput/stagedAttachments"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { buildDisplayedMessagesForMessage } from "./displayedMessageBuilder"; const STAGED_ATTACHMENT = { @@ -16,7 +16,7 @@ const STAGED_ATTACHMENT = { describe("buildDisplayedMessagesForMessage staged attachments", () => { test("preserves staged notices in compaction previews for chip rendering", () => { const followUpText = appendStagedAttachmentNotice("Continue work", [STAGED_ATTACHMENT]); - const message = createMuxMessage("msg-1", "user", "/compact", { + const message = createXumMessage("msg-1", "user", "/compact", { historySequence: 1, muxMetadata: { type: "compaction-request", @@ -48,7 +48,7 @@ describe("buildDisplayedMessagesForMessage staged attachments", () => { test("preserves staged notices in one-shot raw commands for chip rendering", () => { const rawCommand = appendStagedAttachmentNotice("/opus inspect this", [STAGED_ATTACHMENT]); - const message = createMuxMessage("msg-2", "user", "inspect this", { + const message = createXumMessage("msg-2", "user", "inspect this", { historySequence: 2, muxMetadata: { type: "normal", @@ -74,7 +74,7 @@ describe("buildDisplayedMessagesForMessage staged attachments", () => { test("preserves staged notices in skill raw commands for chip rendering", () => { const rawCommand = appendStagedAttachmentNotice("/review inspect this", [STAGED_ATTACHMENT]); - const message = createMuxMessage("msg-3", "user", "Using skill review: inspect this", { + const message = createXumMessage("msg-3", "user", "Using skill review: inspect this", { historySequence: 3, muxMetadata: { type: "agent-skill", diff --git a/src/browser/utils/messages/displayedMessageBuilder.ts b/src/browser/utils/messages/displayedMessageBuilder.ts index 18b3d1741a..214c4c1c34 100644 --- a/src/browser/utils/messages/displayedMessageBuilder.ts +++ b/src/browser/utils/messages/displayedMessageBuilder.ts @@ -3,9 +3,9 @@ import type { CompactionRequestData, DisplayedMessage, InlineSkillSnapshotMap, - MuxFilePart, - MuxMessage, - MuxMessageMetadata, + XumFilePart, + XumMessage, + XumMessageMetadata, } from "@/common/types/message"; import { getCompactionFollowUpContent, @@ -54,7 +54,7 @@ export function resolveRouteProvider( return routeProvider ?? (routedThroughGateway === true ? "mux-gateway" : undefined); } -export function normalizeMessageRouteProvider(message: MuxMessage): MuxMessage { +export function normalizeMessageRouteProvider(message: XumMessage): XumMessage { const routeProvider = resolveRouteProvider( message.metadata?.routeProvider, message.metadata?.routedThroughGateway @@ -78,10 +78,10 @@ export function normalizeMessageRouteProvider(message: MuxMessage): MuxMessage { * Avoids O(n²) string allocations from repeated concatenation. * Tool parts are preserved as-is between merged text/reasoning runs. */ -export function mergeAdjacentParts(parts: MuxMessage["parts"]): MuxMessage["parts"] { +export function mergeAdjacentParts(parts: XumMessage["parts"]): XumMessage["parts"] { if (parts.length <= 1) return parts; - const merged: MuxMessage["parts"] = []; + const merged: XumMessage["parts"] = []; let pendingTexts: string[] = []; let pendingTextTimestamp: number | undefined; let pendingReasonings: string[] = []; @@ -133,7 +133,7 @@ export function mergeAdjacentParts(parts: MuxMessage["parts"]): MuxMessage["part return merged; } -export function getTextPartContent(parts: ReadonlyArray): string { +export function getTextPartContent(parts: ReadonlyArray): string { const content: string[] = []; for (const part of parts) { if (part.type === "text") { @@ -144,7 +144,7 @@ export function getTextPartContent(parts: ReadonlyArray { assert(message.role === "assistant", "compaction boundaries must belong to assistant summaries"); @@ -169,19 +169,19 @@ function createCompactionBoundaryRow( } export interface BuildDisplayedMessagesForMessageOptions { - message: MuxMessage; + message: XumMessage; agentSkillSnapshot?: { frontmatterYaml?: string; body?: string }; inlineSkillSnapshots?: InlineSkillSnapshotMap; hasActiveStream: boolean; streamIsReplay?: boolean; - isContextBoundaryMessage: (message: MuxMessage) => boolean; + isContextBoundaryMessage: (message: XumMessage) => boolean; } type ToolDisplayStatus = Extract["status"]; type NestedToolCalls = NonNullable; function buildPlanDisplayMessages( - message: MuxMessage, + message: XumMessage, historySequence: number ): DisplayedMessage[] | undefined { const muxMeta = message.metadata?.muxMetadata; @@ -209,7 +209,7 @@ function buildPlanDisplayMessages( * transcript (see AGENTS.md self-healing rule). */ function getValidBashMonitorWakeRecords( - muxMeta: MuxMessageMetadata | undefined + muxMeta: XumMessageMetadata | undefined ): BashMonitorWakeDisplayRecord[] | undefined { if (muxMeta?.type !== "bash-monitor-wake") return undefined; const records: unknown = muxMeta.records; @@ -243,7 +243,7 @@ function getRawCommand(muxMetadata: unknown): string | undefined { } function buildUserDisplayedMessages(options: { - message: MuxMessage; + message: XumMessage; agentSkillSnapshot?: { frontmatterYaml?: string; body?: string }; inlineSkillSnapshots?: InlineSkillSnapshotMap; baseTimestamp?: number; @@ -255,7 +255,7 @@ function buildUserDisplayedMessages(options: { const partsContent = getTextPartContent(message.parts); const fileParts = message.parts - .filter((p): p is MuxFilePart => p.type === "file") + .filter((p): p is XumFilePart => p.type === "file") .map((p) => ({ url: typeof p.url === "string" ? p.url : "", mediaType: p.mediaType, @@ -331,7 +331,7 @@ function buildUserDisplayedMessages(options: { ]; } -function isRenderableDisplayPart(part: MuxMessage["parts"][number]): boolean { +function isRenderableDisplayPart(part: XumMessage["parts"][number]): boolean { return ( part.type === "reasoning" || (part.type === "text" && Boolean(part.text)) || @@ -339,7 +339,7 @@ function isRenderableDisplayPart(part: MuxMessage["parts"][number]): boolean { ); } -function getRenderablePartStats(parts: MuxMessage["parts"]): { +function getRenderablePartStats(parts: XumMessage["parts"]): { lastPartIndex: number; isReasoningOnlyMessage: boolean; } { @@ -364,8 +364,8 @@ function getRenderablePartStats(parts: MuxMessage["parts"]): { function appendReasoningRow( displayedMessages: DisplayedMessage[], options: { - message: MuxMessage; - part: Extract; + message: XumMessage; + part: Extract; partIndex: number; historySequence: number; isStreaming: boolean; @@ -396,8 +396,8 @@ function appendReasoningRow( function appendAssistantTextRow( displayedMessages: DisplayedMessage[], options: { - message: MuxMessage; - part: Extract; + message: XumMessage; + part: Extract; partIndex: number; historySequence: number; isStreaming: boolean; @@ -500,7 +500,7 @@ function getNestedCallsForDisplay(part: DynamicToolPart): NestedToolCalls | unde function appendToolRows( displayedMessages: DisplayedMessage[], options: { - message: MuxMessage; + message: XumMessage; part: DynamicToolPart; partIndex: number; historySequence: number; @@ -545,7 +545,7 @@ function getNestedString(value: unknown, path: string[]): string | undefined { // @ai-sdk/anthropic >=3.0.82 maps refusal stop details to this providerMetadata // shape; older persisted turns simply omit it and fall back to the generic row. -function getProviderRefusalExplanation(message: MuxMessage): string | undefined { +function getProviderRefusalExplanation(message: XumMessage): string | undefined { return getNestedString(message.metadata?.providerMetadata, [ "anthropic", "stopDetails", @@ -553,7 +553,7 @@ function getProviderRefusalExplanation(message: MuxMessage): string | undefined ]); } -function buildRefusalFinishMessage(message: MuxMessage): string { +function buildRefusalFinishMessage(message: XumMessage): string { const finishReason = message.metadata?.finishReason ?? "content-filter"; const explanation = getProviderRefusalExplanation(message); const base = @@ -565,7 +565,7 @@ function buildRefusalFinishMessage(message: MuxMessage): string { function appendStreamErrorRows( displayedMessages: DisplayedMessage[], options: { - message: MuxMessage; + message: XumMessage; historySequence: number; hasActiveStream: boolean; baseTimestamp?: number; @@ -619,12 +619,12 @@ function appendStreamErrorRows( } function buildAssistantDisplayedMessages(options: { - message: MuxMessage; + message: XumMessage; baseTimestamp?: number; historySequence: number; hasActiveStream: boolean; streamIsReplay?: boolean; - isContextBoundaryMessage: (message: MuxMessage) => boolean; + isContextBoundaryMessage: (message: XumMessage) => boolean; }): DisplayedMessage[] { const { message, diff --git a/src/browser/utils/messages/modelMessageTransform.test.ts b/src/browser/utils/messages/modelMessageTransform.test.ts index cb6cb51dee..d743796174 100644 --- a/src/browser/utils/messages/modelMessageTransform.test.ts +++ b/src/browser/utils/messages/modelMessageTransform.test.ts @@ -11,7 +11,7 @@ import { stripOrphanedToolCalls, } from "./modelMessageTransform"; import { MAX_POST_COMPACTION_INJECTION_CHARS } from "@/common/constants/attachments"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; describe("modelMessageTransform", () => { describe("transformModelMessages", () => { @@ -634,7 +634,7 @@ describe("modelMessageTransform", () => { describe("addInterruptedSentinel", () => { it("should insert user message after partial assistant message", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -663,7 +663,7 @@ describe("modelMessageTransform", () => { }); it("should not insert sentinel for non-partial assistant messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -686,7 +686,7 @@ describe("modelMessageTransform", () => { }); it("should insert sentinel for reasoning-only partial messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -710,7 +710,7 @@ describe("modelMessageTransform", () => { }); it("should handle multiple partial messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -751,7 +751,7 @@ describe("modelMessageTransform", () => { }); it("should skip sentinel when user message follows partial", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1045,7 +1045,7 @@ describe("stripOrphanedToolCalls", () => { describe("injectAgentTransition", () => { it("should inject transition message when agent changes", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1086,7 +1086,7 @@ describe("injectAgentTransition", () => { }); it("should not inject transition when agent is the same", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1115,7 +1115,7 @@ describe("injectAgentTransition", () => { }); it("should not inject transition when no previous agent exists", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1132,7 +1132,7 @@ describe("injectAgentTransition", () => { }); it("should not inject transition when no agent specified", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1161,7 +1161,7 @@ describe("injectAgentTransition", () => { }); it("should handle conversation with no user messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -1178,7 +1178,7 @@ describe("injectAgentTransition", () => { }); it("should include tool names in transition message when provided", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1215,7 +1215,7 @@ describe("injectAgentTransition", () => { }); it("should handle agent transition without tools parameter (backward compatibility)", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1247,7 +1247,7 @@ describe("injectAgentTransition", () => { }); it("should handle agent transition with empty tool list", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1279,7 +1279,7 @@ describe("injectAgentTransition", () => { }); it("should include plan content when transitioning from plan to exec", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1324,7 +1324,7 @@ describe("injectAgentTransition", () => { }); it("should NOT include plan content when transitioning from exec to plan", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1360,7 +1360,7 @@ describe("injectAgentTransition", () => { }); it("should NOT include plan content when no plan content provided", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1395,7 +1395,7 @@ describe("injectAgentTransition", () => { }); it("should include both tools and plan content in transition message", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1432,7 +1432,7 @@ describe("injectAgentTransition", () => { describe("filterEmptyAssistantMessages", () => { it("should filter out assistant messages with only reasoning when preserveReasoningOnly=false", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1455,7 +1455,7 @@ describe("filterEmptyAssistantMessages", () => { }); it("should filter out assistant messages with empty parts array (placeholder messages)", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1487,7 +1487,7 @@ describe("filterEmptyAssistantMessages", () => { }); it("should preserve assistant messages with only reasoning when preserveReasoningOnly=true", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1511,7 +1511,7 @@ describe("filterEmptyAssistantMessages", () => { }); it("should preserve assistant messages with text content regardless of preserveReasoningOnly", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -1535,7 +1535,7 @@ describe("filterEmptyAssistantMessages", () => { }); it("should filter out assistant messages with only incomplete tool calls (input-available)", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1571,7 +1571,7 @@ describe("filterEmptyAssistantMessages", () => { }); it("should preserve assistant messages with completed tool calls (output-available)", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1602,7 +1602,7 @@ describe("filterEmptyAssistantMessages", () => { const emptyTexts = ["", "\n\n", " "]; for (const text of emptyTexts) { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -1623,7 +1623,7 @@ describe("filterEmptyAssistantMessages", () => { it("should preserve messages interrupted during thinking phase when preserveReasoningOnly=true", () => { // Simulates an interrupted stream during Extended Thinking - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1653,7 +1653,7 @@ describe("filterEmptyAssistantMessages", () => { describe("injectPostCompactionAttachments", () => { it("inserts after the compaction summary and enforces a size budget", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "compaction-summary", role: "assistant", @@ -1702,7 +1702,7 @@ describe("injectPostCompactionAttachments", () => { }); it("falls back to a legacy compacted summary when durable boundary metadata is missing", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "legacy-summary", role: "assistant", @@ -1750,7 +1750,7 @@ describe("injectPostCompactionAttachments", () => { }); it("appends at the end when no compaction indicators are present", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -1793,7 +1793,7 @@ describe("injectPostCompactionAttachments", () => { }); it("inserts after the latest compaction boundary when multiple summaries exist", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "summary-1", role: "assistant", diff --git a/src/browser/utils/messages/modelMessageTransform.ts b/src/browser/utils/messages/modelMessageTransform.ts index 17fe4edb33..1fcdfd9351 100644 --- a/src/browser/utils/messages/modelMessageTransform.ts +++ b/src/browser/utils/messages/modelMessageTransform.ts @@ -4,7 +4,7 @@ */ import type { ModelMessage, AssistantModelMessage, ToolModelMessage } from "ai"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { PostCompactionAttachment } from "@/common/types/attachment"; import { MAX_POST_COMPACTION_INJECTION_CHARS } from "@/common/constants/attachments"; import { hasProviderReplayableContent } from "@/common/utils/messages/providerEligibility"; @@ -32,9 +32,9 @@ import { renderAttachmentsToContentWithBudget } from "./attachmentRenderer"; * @param preserveReasoningOnly - If true, keep reasoning-only messages (for Extended Thinking) */ export function filterEmptyAssistantMessages( - messages: MuxMessage[], + messages: XumMessage[], preserveReasoningOnly = false -): MuxMessage[] { +): XumMessage[] { return messages.filter((msg) => { // Keep all non-assistant messages if (msg.role !== "assistant") { @@ -59,8 +59,8 @@ export function filterEmptyAssistantMessages( * filtered out, and we'd lose the interruption context. A user message always * survives filtering. */ -export function addInterruptedSentinel(messages: MuxMessage[]): MuxMessage[] { - const result: MuxMessage[] = []; +export function addInterruptedSentinel(messages: XumMessage[]): XumMessage[] { + const result: XumMessage[] = []; for (let i = 0; i < messages.length; i++) { const msg = messages[i]; @@ -106,12 +106,12 @@ export function addInterruptedSentinel(messages: MuxMessage[]): MuxMessage[] { * @returns Messages with agent transition context injected if needed */ export function injectAgentTransition( - messages: MuxMessage[], + messages: XumMessage[], currentAgentId?: string, toolNames?: string[], planContent?: string, planFilePath?: string -): MuxMessage[] { +): XumMessage[] { // No agent specified, nothing to do if (!currentAgentId) { return messages; @@ -148,7 +148,7 @@ export function injectAgentTransition( return messages; } - const result: MuxMessage[] = []; + const result: XumMessage[] = []; // Add all messages up to (but not including) the last user message for (let i = 0; i < lastUserIndex; i++) { @@ -181,7 +181,7 @@ ${planContent} `; } - const transitionMessage: MuxMessage = { + const transitionMessage: XumMessage = { id: `agent-transition-${Date.now()}`, role: "user", parts: [ @@ -210,7 +210,7 @@ ${planContent} // turn start (see createFileChangeNotificationMessage in fileChangeTracker.ts), // keeping the provider request a pure function of the session log. -function findLatestLegacyCompactionSummaryIndex(messages: MuxMessage[]): number { +function findLatestLegacyCompactionSummaryIndex(messages: XumMessage[]): number { for (let i = messages.length - 1; i >= 0; i -= 1) { const message = messages[i]; if (message.role !== "assistant") { @@ -241,9 +241,9 @@ function findLatestLegacyCompactionSummaryIndex(messages: MuxMessage[]): number * @returns Messages with attachments injected after compaction summary */ export function injectPostCompactionAttachments( - messages: MuxMessage[], + messages: XumMessage[], attachments?: PostCompactionAttachment[] | null -): MuxMessage[] { +): XumMessage[] { if (!attachments || attachments.length === 0) { return messages; } @@ -259,7 +259,7 @@ export function injectPostCompactionAttachments( if (compactionIndex === -1) { // No compaction message found - this shouldn't happen if attachments are provided, // but append at end as a fallback - const syntheticMessage: MuxMessage = { + const syntheticMessage: XumMessage = { id: `post-compaction-${Date.now()}`, role: "user", parts: [ @@ -279,7 +279,7 @@ export function injectPostCompactionAttachments( } // Insert the synthetic message immediately after the compaction summary - const syntheticMessage: MuxMessage = { + const syntheticMessage: XumMessage = { id: `post-compaction-${Date.now()}`, role: "user", parts: [ diff --git a/src/browser/utils/messages/recency.test.ts b/src/browser/utils/messages/recency.test.ts index a7acfec2a9..e2cbcc1874 100644 --- a/src/browser/utils/messages/recency.test.ts +++ b/src/browser/utils/messages/recency.test.ts @@ -1,5 +1,5 @@ import { computeRecencyTimestamp } from "./recency"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; describe("computeRecencyTimestamp", () => { it("returns null for empty messages array", () => { @@ -8,25 +8,25 @@ describe("computeRecencyTimestamp", () => { it("returns null when no messages have timestamps", () => { const messages = [ - createMuxMessage("1", "user", "hello"), - createMuxMessage("2", "assistant", "hi"), + createXumMessage("1", "user", "hello"), + createXumMessage("2", "assistant", "hi"), ]; expect(computeRecencyTimestamp(messages)).toBeNull(); }); it("returns last user message timestamp", () => { const messages = [ - createMuxMessage("1", "user", "first", { timestamp: 100 }), - createMuxMessage("2", "assistant", "reply", { timestamp: 200 }), - createMuxMessage("3", "user", "second", { timestamp: 300 }), + createXumMessage("1", "user", "first", { timestamp: 100 }), + createXumMessage("2", "assistant", "reply", { timestamp: 200 }), + createXumMessage("3", "user", "second", { timestamp: 300 }), ]; expect(computeRecencyTimestamp(messages)).toBe(300); }); it("returns max of user message and compacted message timestamps", () => { const messages = [ - createMuxMessage("1", "user", "user msg", { timestamp: 100 }), - createMuxMessage("2", "assistant", "compacted", { + createXumMessage("1", "user", "user msg", { timestamp: 100 }), + createXumMessage("2", "assistant", "compacted", { timestamp: 200, compacted: true, }), @@ -37,8 +37,8 @@ describe("computeRecencyTimestamp", () => { it("falls back to compacted message when no user messages", () => { const messages = [ - createMuxMessage("1", "assistant", "response"), - createMuxMessage("2", "assistant", "compacted summary", { + createXumMessage("1", "assistant", "response"), + createXumMessage("2", "assistant", "compacted summary", { timestamp: 150, compacted: true, }), @@ -48,22 +48,22 @@ describe("computeRecencyTimestamp", () => { it("uses most recent user message when multiple exist", () => { const messages = [ - createMuxMessage("1", "user", "old", { timestamp: 100 }), - createMuxMessage("2", "user", "middle", { timestamp: 200 }), - createMuxMessage("3", "assistant", "reply"), - createMuxMessage("4", "user", "newest", { timestamp: 300 }), + createXumMessage("1", "user", "old", { timestamp: 100 }), + createXumMessage("2", "user", "middle", { timestamp: 200 }), + createXumMessage("3", "assistant", "reply"), + createXumMessage("4", "user", "newest", { timestamp: 300 }), ]; expect(computeRecencyTimestamp(messages)).toBe(300); }); it("uses most recent compacted message as fallback", () => { const messages = [ - createMuxMessage("1", "assistant", "old summary", { + createXumMessage("1", "assistant", "old summary", { timestamp: 100, compacted: true, }), - createMuxMessage("2", "assistant", "response"), - createMuxMessage("3", "assistant", "newer summary", { + createXumMessage("2", "assistant", "response"), + createXumMessage("3", "assistant", "newer summary", { timestamp: 200, compacted: true, }), @@ -73,34 +73,34 @@ describe("computeRecencyTimestamp", () => { it("handles messages with metadata but no timestamp", () => { const messages = [ - createMuxMessage("1", "user", "hello", { model: "claude" }), - createMuxMessage("2", "assistant", "hi", { duration: 100 }), + createXumMessage("1", "user", "hello", { model: "claude" }), + createXumMessage("2", "assistant", "hi", { duration: 100 }), ]; expect(computeRecencyTimestamp(messages)).toBeNull(); }); it("ignores assistant messages without compacted flag", () => { const messages = [ - createMuxMessage("1", "assistant", "regular", { timestamp: 100 }), - createMuxMessage("2", "assistant", "another", { timestamp: 200 }), + createXumMessage("1", "assistant", "regular", { timestamp: 100 }), + createXumMessage("2", "assistant", "another", { timestamp: 200 }), ]; expect(computeRecencyTimestamp(messages)).toBeNull(); }); it("handles mixed messages with only some having timestamps", () => { const messages = [ - createMuxMessage("1", "user", "no timestamp"), - createMuxMessage("2", "user", "has timestamp", { timestamp: 150 }), - createMuxMessage("3", "user", "no timestamp again"), + createXumMessage("1", "user", "no timestamp"), + createXumMessage("2", "user", "has timestamp", { timestamp: 150 }), + createXumMessage("3", "user", "no timestamp again"), ]; expect(computeRecencyTimestamp(messages)).toBe(150); }); it("handles user messages in middle of array", () => { const messages = [ - createMuxMessage("1", "assistant", "start"), - createMuxMessage("2", "user", "middle", { timestamp: 250 }), - createMuxMessage("3", "assistant", "end"), + createXumMessage("1", "assistant", "start"), + createXumMessage("2", "user", "middle", { timestamp: 250 }), + createXumMessage("3", "assistant", "end"), ]; expect(computeRecencyTimestamp(messages)).toBe(250); }); @@ -109,7 +109,7 @@ describe("computeRecencyTimestamp", () => { it("manual compaction summary bumps recency", () => { const now = Date.now(); const messages = [ - createMuxMessage("1", "assistant", "summary", { compacted: "user", timestamp: now }), + createXumMessage("1", "assistant", "summary", { compacted: "user", timestamp: now }), ]; const result = computeRecencyTimestamp(messages); @@ -118,7 +118,7 @@ describe("computeRecencyTimestamp", () => { it("idle compaction request user message is ignored", () => { const messages = [ - createMuxMessage("1", "user", "compact", { + createXumMessage("1", "user", "compact", { timestamp: 2000, muxMetadata: { type: "compaction-request", @@ -137,7 +137,7 @@ describe("computeRecencyTimestamp", () => { const requestTime = Date.now(); const backdatedTime = requestTime - 60000; const messages = [ - createMuxMessage("idle-req", "user", "compact", { + createXumMessage("idle-req", "user", "compact", { timestamp: requestTime, muxMetadata: { type: "compaction-request", @@ -146,7 +146,7 @@ describe("computeRecencyTimestamp", () => { source: "idle-compaction", }, }), - createMuxMessage("idle-summary", "assistant", "Summary", { + createXumMessage("idle-summary", "assistant", "Summary", { compacted: "idle", timestamp: backdatedTime, }), @@ -158,7 +158,7 @@ describe("computeRecencyTimestamp", () => { it("manual compaction request user message does bump recency", () => { const messages = [ - createMuxMessage("1", "user", "/compact", { + createXumMessage("1", "user", "/compact", { timestamp: 3000, muxMetadata: { type: "compaction-request", @@ -187,7 +187,7 @@ describe("computeRecencyTimestamp", () => { // Old message (before workspace created) const messages = [ - createMuxMessage("1", "user", "old message", { timestamp: createdTimestamp - 1000 }), + createXumMessage("1", "user", "old message", { timestamp: createdTimestamp - 1000 }), ]; expect(computeRecencyTimestamp(messages, createdAt)).toBe(createdTimestamp); }); @@ -198,7 +198,7 @@ describe("computeRecencyTimestamp", () => { // New message (after workspace created) const messages = [ - createMuxMessage("1", "user", "new message", { timestamp: createdTimestamp + 5000 }), + createXumMessage("1", "user", "new message", { timestamp: createdTimestamp + 5000 }), ]; expect(computeRecencyTimestamp(messages, createdAt)).toBe(createdTimestamp + 5000); }); @@ -208,8 +208,8 @@ describe("computeRecencyTimestamp", () => { const createdTimestamp = new Date(createdAt).getTime(); const messages = [ - createMuxMessage("1", "user", "old user", { timestamp: createdTimestamp - 5000 }), - createMuxMessage("2", "assistant", "old compacted", { + createXumMessage("1", "user", "old user", { timestamp: createdTimestamp - 5000 }), + createXumMessage("2", "assistant", "old compacted", { timestamp: createdTimestamp - 2000, compacted: true, }), @@ -224,8 +224,8 @@ describe("computeRecencyTimestamp", () => { const createdTimestamp = new Date(createdAt).getTime(); const messages = [ - createMuxMessage("1", "user", "newest", { timestamp: createdTimestamp + 10000 }), - createMuxMessage("2", "assistant", "compacted", { + createXumMessage("1", "user", "newest", { timestamp: createdTimestamp + 10000 }), + createXumMessage("2", "assistant", "compacted", { timestamp: createdTimestamp + 5000, compacted: true, }), @@ -236,7 +236,7 @@ describe("computeRecencyTimestamp", () => { }); it("handles invalid createdAt gracefully", () => { - const messages = [createMuxMessage("1", "user", "msg", { timestamp: 100 })]; + const messages = [createXumMessage("1", "user", "msg", { timestamp: 100 })]; // Invalid ISO string should result in NaN timestamp, which gets filtered out expect(computeRecencyTimestamp(messages, "invalid-date")).toBe(100); @@ -252,9 +252,9 @@ describe("computeRecencyTimestamp", () => { describe("with idle compaction requests", () => { it("excludes idle compaction request messages from recency", () => { const messages = [ - createMuxMessage("1", "user", "normal message", { timestamp: 100 }), - createMuxMessage("2", "assistant", "reply", { timestamp: 150 }), - createMuxMessage("3", "user", "compaction request", { + createXumMessage("1", "user", "normal message", { timestamp: 100 }), + createXumMessage("2", "assistant", "reply", { timestamp: 150 }), + createXumMessage("3", "user", "compaction request", { timestamp: 300, muxMetadata: { type: "compaction-request", @@ -270,8 +270,8 @@ describe("computeRecencyTimestamp", () => { it("includes user-initiated compaction requests in recency", () => { const messages = [ - createMuxMessage("1", "user", "normal message", { timestamp: 100 }), - createMuxMessage("2", "user", "compaction request", { + createXumMessage("1", "user", "normal message", { timestamp: 100 }), + createXumMessage("2", "user", "compaction request", { timestamp: 300, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }), @@ -285,7 +285,7 @@ describe("computeRecencyTimestamp", () => { const createdTimestamp = new Date(createdAt).getTime(); const messages = [ - createMuxMessage("1", "user", "idle compaction", { + createXumMessage("1", "user", "idle compaction", { timestamp: createdTimestamp + 10000, muxMetadata: { type: "compaction-request", diff --git a/src/browser/utils/messages/recency.ts b/src/browser/utils/messages/recency.ts index 1b416594f9..5b684d1d10 100644 --- a/src/browser/utils/messages/recency.ts +++ b/src/browser/utils/messages/recency.ts @@ -1,4 +1,4 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { computeRecencyFromMessages } from "@/common/utils/recency"; /** @@ -12,7 +12,7 @@ import { computeRecencyFromMessages } from "@/common/utils/recency"; * - Last compacted message timestamp (fallback for compacted histories) */ export function computeRecencyTimestamp( - messages: MuxMessage[], + messages: XumMessage[], createdAt?: string, unarchivedAt?: string ): number | null { diff --git a/src/browser/utils/messages/sanitizeToolInput.test.ts b/src/browser/utils/messages/sanitizeToolInput.test.ts index ea2471c0d0..0e678bdef8 100644 --- a/src/browser/utils/messages/sanitizeToolInput.test.ts +++ b/src/browser/utils/messages/sanitizeToolInput.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect } from "@jest/globals"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { sanitizeToolInputs } from "./sanitizeToolInput"; describe("sanitizeToolInputs", () => { it("should handle the actual malformed message from httpjail-coder workspace", () => { // This is the actual problematic message that caused the bug - const problematicMessage: MuxMessage = { + const problematicMessage: XumMessage = { id: "assistant-1761527027508-karjrpf3g", role: "assistant", metadata: { @@ -42,7 +42,7 @@ describe("sanitizeToolInputs", () => { }); it("should convert string inputs to empty objects", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "test-1", role: "assistant", @@ -68,7 +68,7 @@ describe("sanitizeToolInputs", () => { }); it("should keep valid object inputs unchanged", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "test-2", role: "assistant", @@ -94,7 +94,7 @@ describe("sanitizeToolInputs", () => { }); it("should not modify non-assistant messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "test-3", role: "user", @@ -108,7 +108,7 @@ describe("sanitizeToolInputs", () => { }); it("should handle messages with multiple parts", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "test-4", role: "assistant", @@ -139,7 +139,7 @@ describe("sanitizeToolInputs", () => { }); it("should handle null input", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "test-null", role: "assistant", @@ -166,7 +166,7 @@ describe("sanitizeToolInputs", () => { }); it("should handle array input", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "test-array", role: "assistant", diff --git a/src/browser/utils/messages/sanitizeToolInput.ts b/src/browser/utils/messages/sanitizeToolInput.ts index 55a76c9738..04ba1ccff6 100644 --- a/src/browser/utils/messages/sanitizeToolInput.ts +++ b/src/browser/utils/messages/sanitizeToolInput.ts @@ -1,4 +1,4 @@ -import type { MuxMessage, MuxToolPart } from "@/common/types/message"; +import type { XumMessage, XumToolPart } from "@/common/types/message"; /** * Sanitizes tool inputs in messages to ensure they are valid objects. @@ -15,7 +15,7 @@ import type { MuxMessage, MuxToolPart } from "@/common/types/message"; * @param messages - Messages to sanitize * @returns New array with sanitized messages (original messages are not modified) */ -export function sanitizeToolInputs(messages: MuxMessage[]): MuxMessage[] { +export function sanitizeToolInputs(messages: XumMessage[]): XumMessage[] { return messages.map((msg) => { // Only process assistant messages with tool parts if (msg.role !== "assistant") { @@ -43,7 +43,7 @@ export function sanitizeToolInputs(messages: MuxMessage[]): MuxMessage[] { // Sanitize the input if it's not a valid object if (typeof part.input !== "object" || part.input === null || Array.isArray(part.input)) { - const sanitized: MuxToolPart = { + const sanitized: XumToolPart = { ...part, input: {}, // Replace with empty object }; diff --git a/src/browser/utils/messages/sendOptions.ts b/src/browser/utils/messages/sendOptions.ts index 56fba3831b..d749c2c270 100644 --- a/src/browser/utils/messages/sendOptions.ts +++ b/src/browser/utils/messages/sendOptions.ts @@ -18,7 +18,7 @@ import { type OpenAIReasoningMode, type ThinkingLevel, } from "@/common/types/thinking"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { isExperimentEnabled } from "@/browser/hooks/useExperiments"; import { EXPERIMENT_IDS } from "@/common/constants/experiments"; @@ -26,12 +26,12 @@ import { EXPERIMENT_IDS } from "@/common/constants/experiments"; /** * Read provider options from localStorage */ -function getProviderOptions(): MuxProviderOptions { - const anthropic = readPersistedState( +function getProviderOptions(): XumProviderOptions { + const anthropic = readPersistedState( "provider_options_anthropic", {} ); - const google = readPersistedState("provider_options_google", {}); + const google = readPersistedState("provider_options_google", {}); return { anthropic, diff --git a/src/browser/utils/workflowRunMessages.test.ts b/src/browser/utils/workflowRunMessages.test.ts index 038581c48b..5bb3b043c8 100644 --- a/src/browser/utils/workflowRunMessages.test.ts +++ b/src/browser/utils/workflowRunMessages.test.ts @@ -7,7 +7,7 @@ import { getWorkflowRunCardProjection, hasWorkflowRunToolCallMessage, } from "./workflowRunMessages"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { WorkflowRunRecord } from "@/common/types/workflow"; function parseWorkflowResultPayload(message: string): Record { @@ -173,7 +173,7 @@ describe("buildWorkflowRunCardMessage", () => { }); test("filters durable workflow UI-only rows while preserving workflow results", () => { - const trigger: MuxMessage = { + const trigger: XumMessage = { id: "workflow-command", role: "user", parts: [{ type: "text", text: "/deep-research mux" }], @@ -195,7 +195,7 @@ describe("buildWorkflowRunCardMessage", () => { historySequence: 2, muxMetadata: { type: "workflow-run-card-display", runId: "wfr_1" }, }; - const result: MuxMessage = { + const result: XumMessage = { id: "workflow-result", role: "user", parts: [{ type: "text", text: "/deep-research mux\n\n" }], @@ -228,7 +228,7 @@ describe("buildWorkflowRunCardMessage", () => { { runId: run.id, status: "completed", result: { reportMarkdown: "done" } }, 123 ); - const inFlightMessage: MuxMessage = { + const inFlightMessage: XumMessage = { id: "assistant_1", role: "assistant", parts: [ @@ -268,7 +268,7 @@ describe("buildWorkflowRunCardMessage", () => { args: { value: "ok" }, status: "running" as const, }; - const inFlightMessage: MuxMessage = { + const inFlightMessage: XumMessage = { id: "assistant_inline", role: "assistant", parts: [ @@ -365,7 +365,7 @@ describe("buildWorkflowRunCardMessage", () => { args: { topic: "trigger" }, status: "completed" as const, }; - const trigger: MuxMessage = { + const trigger: XumMessage = { id: "workflow-run-command-wfr_trigger_anchor", role: "user", parts: [{ type: "text", text: "/deep-research trigger" }], @@ -428,7 +428,7 @@ describe("buildWorkflowRunCardMessage", () => { args: { topic: "reload" }, status: "running" as const, }; - const inFlightMessage: MuxMessage = { + const inFlightMessage: XumMessage = { id: "assistant_1", role: "assistant", parts: [ @@ -441,7 +441,7 @@ describe("buildWorkflowRunCardMessage", () => { }, ], }; - const completedAssistantMessage: MuxMessage = { + const completedAssistantMessage: XumMessage = { id: "assistant_2", role: "assistant", parts: [ diff --git a/src/browser/utils/workflowRunMessages.ts b/src/browser/utils/workflowRunMessages.ts index 6db0149bee..ea34a11621 100644 --- a/src/browser/utils/workflowRunMessages.ts +++ b/src/browser/utils/workflowRunMessages.ts @@ -1,5 +1,5 @@ import { addEphemeralMessage } from "@/browser/stores/WorkspaceStore"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { WorkflowRunRecord } from "@/common/types/workflow"; import { getWorkflowScriptDisplayPath, @@ -78,7 +78,7 @@ function getProjectedWorkflowRunCardMessageId(runId: string): string { return `workflow-run-${runId}`; } -function hasWorkflowRunCardMetadata(message: MuxMessage, runId: string): boolean { +function hasWorkflowRunCardMetadata(message: XumMessage, runId: string): boolean { return ( message.id === getProjectedWorkflowRunCardMessageId(runId) && message.role === "assistant" && @@ -88,9 +88,9 @@ function hasWorkflowRunCardMetadata(message: MuxMessage, runId: string): boolean } function findWorkflowTriggerDisplayMessage( - messages: readonly MuxMessage[], + messages: readonly XumMessage[], runId: string -): MuxMessage | null { +): XumMessage | null { return ( messages.find( (message) => @@ -101,9 +101,9 @@ function findWorkflowTriggerDisplayMessage( } function getWorkflowRunCardMetadata( - metadata: MuxMessage["metadata"] | undefined, + metadata: XumMessage["metadata"] | undefined, runId: string -): MuxMessage["metadata"] { +): XumMessage["metadata"] { return { ...metadata, muxMetadata: { @@ -114,9 +114,9 @@ function getWorkflowRunCardMetadata( } export function findProjectedWorkflowRunCardMessage( - messages: readonly MuxMessage[], + messages: readonly XumMessage[], runId: string -): MuxMessage | null { +): XumMessage | null { assert(runId.length > 0, "findProjectedWorkflowRunCardMessage: run id is required"); const metadataMatch = messages.find((message) => hasWorkflowRunCardMetadata(message, runId)); if (metadataMatch != null) { @@ -140,7 +140,7 @@ export function findProjectedWorkflowRunCardMessage( } export function hasWorkflowRunToolCallMessage( - messages: readonly MuxMessage[], + messages: readonly XumMessage[], run: Pick ): boolean { assert(run.id.length > 0, "hasWorkflowRunToolCallMessage: run id is required"); @@ -164,9 +164,9 @@ export function hasWorkflowRunToolCallMessage( } export function getWorkflowRunCardProjection( - messages: readonly MuxMessage[], + messages: readonly XumMessage[], run: Pick -): { shouldProject: boolean; existingMessage: MuxMessage | null } { +): { shouldProject: boolean; existingMessage: XumMessage | null } { assert(run.id.length > 0, "getWorkflowRunCardProjection: run id is required"); const existingMessage = findProjectedWorkflowRunCardMessage(messages, run.id); if (existingMessage != null) { @@ -194,7 +194,7 @@ export function addWorkflowRunCardMessage( workspaceId: string, input: WorkflowRunCardInput, result: WorkflowRunCardResult, - options?: { existingMessage?: MuxMessage | null } + options?: { existingMessage?: XumMessage | null } ): void { assert(workspaceId.length > 0, "addWorkflowRunCardMessage: workspaceId is required"); const message = buildWorkflowRunCardMessage(input, result); @@ -207,7 +207,7 @@ export function addWorkflowRunCardMessage( export function addWorkflowRunCardMessageForRun( workspaceId: string, run: WorkflowRunRecord, - options?: { existingMessage?: MuxMessage | null } + options?: { existingMessage?: XumMessage | null } ): void { addWorkflowRunCardMessage( workspaceId, diff --git a/src/cli/debug/costs.ts b/src/cli/debug/costs.ts index 8fc258a474..6b62b264e2 100644 --- a/src/cli/debug/costs.ts +++ b/src/cli/debug/costs.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import { defaultConfig } from "@/node/config"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { calculateTokenStats } from "@/common/utils/tokens/tokenStatsCalculator"; import { defaultModel } from "@/common/utils/ai/models"; import { getToolAvailabilityOptions } from "@/common/utils/tools/toolAvailability"; @@ -24,10 +24,10 @@ export async function costsCommand(workspaceId: string) { // Read and parse messages const data = fs.readFileSync(chatHistoryPath, "utf-8"); - const messages: MuxMessage[] = data + const messages: XumMessage[] = data .split("\n") .filter((line) => line.trim()) - .map((line) => JSON.parse(line) as MuxMessage); + .map((line) => JSON.parse(line) as XumMessage); if (messages.length === 0) { console.log("No messages in chat history"); diff --git a/src/cli/debug/replay-history.ts b/src/cli/debug/replay-history.ts index 1a1ebf7b51..0b34702b74 100644 --- a/src/cli/debug/replay-history.ts +++ b/src/cli/debug/replay-history.ts @@ -15,10 +15,10 @@ import * as fs from "fs"; import * as path from "path"; import { parseArgs } from "util"; import { defaultConfig } from "@/node/config"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { ThinkingLevel } from "@/common/types/thinking"; import { enforceThinkingPolicy } from "@/common/utils/thinking/policy"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { InitStateManager } from "@/node/services/initStateManager"; import { AIService } from "@/node/services/aiService"; import { ProviderService } from "@/node/services/providerService"; @@ -59,11 +59,11 @@ async function main() { // Read history const historyContent = fs.readFileSync(historyFile, "utf-8"); - let messages: MuxMessage[]; + let messages: XumMessage[]; try { // Try parsing as JSON array first - messages = JSON.parse(historyContent) as MuxMessage[]; + messages = JSON.parse(historyContent) as XumMessage[]; if (!Array.isArray(messages)) { messages = [messages]; } @@ -72,7 +72,7 @@ async function main() { messages = historyContent .split("\n") .filter((line) => line.trim()) - .map((line) => JSON.parse(line) as MuxMessage); + .map((line) => JSON.parse(line) as XumMessage); } console.log(`📝 Loaded ${messages.length} messages from history\n`); @@ -112,7 +112,7 @@ async function main() { console.log(`\n✓ Created temporary workspace: ${workspaceId}`); // Add new user message to the history - const userMessage = createMuxMessage( + const userMessage = createXumMessage( `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, "user", messageText, diff --git a/src/cli/debug/send-message.ts b/src/cli/debug/send-message.ts index 3be66f82bb..72c7708d2c 100644 --- a/src/cli/debug/send-message.ts +++ b/src/cli/debug/send-message.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import { defaultConfig } from "@/node/config"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { SendMessageOptions } from "@/common/orpc/types"; import { defaultModel } from "@/common/utils/ai/models"; import { getXumSessionsDir } from "@/common/constants/paths"; @@ -41,10 +41,10 @@ export function sendMessageCommand( // Note: We use a more flexible type here because the on-disk format includes workspaceId // which is not part of the XumMessage type (it's metadata that gets stripped) const data = fs.readFileSync(chatHistoryPath, "utf-8"); - const messages: Array = data + const messages: Array = data .split("\n") .filter((line) => line.trim()) - .map((line) => JSON.parse(line) as MuxMessage & { workspaceId?: string }); + .map((line) => JSON.parse(line) as XumMessage & { workspaceId?: string }); if (messages.length === 0) { console.log("❌ No messages in chat history"); diff --git a/src/common/config/schemas/providersConfig.ts b/src/common/config/schemas/providersConfig.ts index 332af7528d..a2dcc3c326 100644 --- a/src/common/config/schemas/providersConfig.ts +++ b/src/common/config/schemas/providersConfig.ts @@ -68,7 +68,7 @@ export const XAIProviderConfigSchema = BaseProviderConfigSchema.extend({ fastModePreviousServiceTier: XAIFastModePreviousServiceTierSchema.optional(), }); -export const MuxGatewayProviderConfigSchema = BaseProviderConfigSchema.extend({ +export const XumGatewayProviderConfigSchema = BaseProviderConfigSchema.extend({ couponCode: z.string().optional(), voucher: z.string().optional(), }); @@ -147,7 +147,7 @@ export const ProvidersConfigSchema = z bedrock: BedrockProviderConfigSchema.optional(), openrouter: OpenRouterProviderConfigSchema.optional(), xai: XAIProviderConfigSchema.optional(), - "mux-gateway": MuxGatewayProviderConfigSchema.optional(), + "mux-gateway": XumGatewayProviderConfigSchema.optional(), google: GoogleProviderConfigSchema.optional(), deepseek: DeepSeekProviderConfigSchema.optional(), moonshotai: MoonshotAIProviderConfigSchema.optional(), @@ -163,7 +163,7 @@ export type OpenAIProviderConfig = z.infer; export type BedrockProviderConfig = z.infer; export type OpenRouterProviderConfig = z.infer; export type XAIProviderConfig = z.infer; -export type MuxGatewayProviderConfig = z.infer; +export type XumGatewayProviderConfig = z.infer; export type GoogleProviderConfig = z.infer; export type DeepSeekProviderConfig = z.infer; export type MoonshotAIProviderConfig = z.infer; diff --git a/src/common/config/schemas/userPreferences.ts b/src/common/config/schemas/userPreferences.ts index f545a13961..8ef3ef994b 100644 --- a/src/common/config/schemas/userPreferences.ts +++ b/src/common/config/schemas/userPreferences.ts @@ -16,7 +16,7 @@ import { type TerminalFontConfig, type TranscriptDensity, } from "@/common/constants/storage"; -import { MuxProviderOptionsSchema } from "@/common/schemas/providerOptions"; +import { XumProviderOptionsSchema } from "@/common/schemas/providerOptions"; import { ThinkingLevelSchema } from "@/common/types/thinking"; import { isRecord, @@ -87,8 +87,8 @@ export const UserPreferencesSchema = z.object({ .optional(), providerOptions: z .object({ - anthropic: MuxProviderOptionsSchema.shape.anthropic, - google: MuxProviderOptionsSchema.shape.google, + anthropic: XumProviderOptionsSchema.shape.anthropic, + google: XumProviderOptionsSchema.shape.google, }) .optional(), autoCompactionThresholdByModel: z @@ -169,12 +169,12 @@ function parseProviderOptions( } const out: NonNullable["providerOptions"]> = {}; - const anthropic = MuxProviderOptionsSchema.shape.anthropic.safeParse(value.anthropic); + const anthropic = XumProviderOptionsSchema.shape.anthropic.safeParse(value.anthropic); if (anthropic.success && anthropic.data && Object.keys(anthropic.data).length > 0) { out.anthropic = anthropic.data; } - const google = MuxProviderOptionsSchema.shape.google.safeParse(value.google); + const google = XumProviderOptionsSchema.shape.google.safeParse(value.google); if (google.success && google.data && Object.keys(google.data).length > 0) { out.google = google.data; } diff --git a/src/common/orpc/onChatCursorFingerprint.test.ts b/src/common/orpc/onChatCursorFingerprint.test.ts index 7345fa46ac..148231577a 100644 --- a/src/common/orpc/onChatCursorFingerprint.test.ts +++ b/src/common/orpc/onChatCursorFingerprint.test.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from "bun:test"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { computePriorHistoryFingerprint } from "./onChatCursorFingerprint"; function withHistoryMetadata( - message: MuxMessage, + message: XumMessage, historySequence: number, timestamp: number -): MuxMessage { +): XumMessage { return { ...message, metadata: { @@ -20,7 +20,7 @@ function withHistoryMetadata( describe("computePriorHistoryFingerprint", () => { test("returns undefined when no rows exist below the anchor", () => { const anchorOnly = withHistoryMetadata( - createMuxMessage("msg-anchor", "assistant", "anchor"), + createXumMessage("msg-anchor", "assistant", "anchor"), 1, 1_000 ); @@ -30,12 +30,12 @@ describe("computePriorHistoryFingerprint", () => { test("changes when a lower-sequence row is rewritten with new content", () => { const originalRow = withHistoryMetadata( - createMuxMessage("msg-rewritten", "assistant", "original"), + createXumMessage("msg-rewritten", "assistant", "original"), 1, 1_001 ); const anchorRow = withHistoryMetadata( - createMuxMessage("msg-anchor", "assistant", "anchor"), + createXumMessage("msg-anchor", "assistant", "anchor"), 2, 1_002 ); @@ -43,7 +43,7 @@ describe("computePriorHistoryFingerprint", () => { const originalFingerprint = computePriorHistoryFingerprint([originalRow, anchorRow], 2); const rewrittenRow = withHistoryMetadata( - createMuxMessage("msg-rewritten", "assistant", "rewritten"), + createXumMessage("msg-rewritten", "assistant", "rewritten"), 1, 1_001 ); diff --git a/src/common/orpc/onChatCursorFingerprint.ts b/src/common/orpc/onChatCursorFingerprint.ts index 38a6eddb11..0a399db47c 100644 --- a/src/common/orpc/onChatCursorFingerprint.ts +++ b/src/common/orpc/onChatCursorFingerprint.ts @@ -1,4 +1,4 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; const FNV_OFFSET_BASIS = 0x811c9dc5; const FNV_PRIME = 0x01000193; @@ -19,14 +19,14 @@ function updateFnv1a(hash: number, value: string): number { * rewrites below the cursor and safely fall back to full replay. */ export function computePriorHistoryFingerprint( - messages: readonly MuxMessage[], + messages: readonly XumMessage[], anchorHistorySequence: number ): string | undefined { const priorEntries: Array<{ id: string; historySequence: number; timestamp: number; - role: MuxMessage["role"]; + role: XumMessage["role"]; partsFingerprint: string; }> = []; diff --git a/src/common/orpc/schemas.ts b/src/common/orpc/schemas.ts index 62df3b741e..6cceb1b5f7 100644 --- a/src/common/orpc/schemas.ts +++ b/src/common/orpc/schemas.ts @@ -191,7 +191,7 @@ export { PolicyRuntimeIdSchema, } from "./schemas/policy"; // Provider options schemas -export { MuxProviderOptionsSchema } from "./schemas/providerOptions"; +export { XumProviderOptionsSchema } from "./schemas/providerOptions"; // MCP schemas export { @@ -231,13 +231,13 @@ export { DynamicToolPartRedactedSchema, DynamicToolPartSchema, FilePartSchema, - MuxFilePartSchema, - MuxMessageSchema, - MuxReasoningPartSchema, - MuxTextPartSchema, - MuxToolPartSchema, + XumFilePartSchema, + XumMessageSchema, + XumReasoningPartSchema, + XumTextPartSchema, + XumToolPartSchema, } from "./schemas/message"; -export type { FilePart, MuxFilePart } from "./schemas/message"; +export type { FilePart, XumFilePart } from "./schemas/message"; // Stream event schemas export { @@ -247,7 +247,7 @@ export { AutoRetryScheduledEventSchema, AutoRetryStartingEventSchema, CaughtUpMessageSchema, - ChatMuxMessageSchema, + ChatXumMessageSchema, CompletedMessagePartSchema, DeleteMessageSchema, ErrorEventSchema, diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index 423d2c1533..94f8166098 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -17,7 +17,7 @@ import { ProjectRemoveErrorSchema, SendMessageErrorSchema, } from "./errors"; -import { BranchListResultSchema, FilePartSchema, MuxMessageSchema } from "./message"; +import { BranchListResultSchema, FilePartSchema, XumMessageSchema } from "./message"; import { GoalClearInputSchema, GoalBoardAddUpcomingInputSchema, @@ -208,7 +208,7 @@ export const tokenizer = { calculateStats: { input: z.object({ workspaceId: z.string(), - messages: z.array(MuxMessageSchema), + messages: z.array(XumMessageSchema), model: z.string(), }), output: ChatStatsSchema, @@ -1511,7 +1511,7 @@ export const workspace = { replaceChatHistory: { input: z.object({ workspaceId: z.string(), - summaryMessage: MuxMessageSchema, + summaryMessage: XumMessageSchema, /** * Replace strategy. * - destructive (default): clear history, then append summary @@ -1590,7 +1590,7 @@ export const workspace = { taskId: z.string(), }), output: z.object({ - messages: z.array(MuxMessageSchema), + messages: z.array(XumMessageSchema), /** Task-level model string used when running the sub-agent (optional for legacy entries). */ model: z.string().optional(), /** Task-level thinking/reasoning level used when running the sub-agent (optional for legacy entries). */ @@ -2353,7 +2353,7 @@ export const config = { }), output: z.void(), }, - updateMuxGatewayPrefs: { + updateXumGatewayPrefs: { input: z.object({ muxGatewayEnabled: z.boolean(), muxGatewayModels: z.array(z.string()), @@ -2442,7 +2442,7 @@ export const config = { .strict(), output: z.void(), }, - unenrollMuxGovernor: { + unenrollXumGovernor: { input: z.void(), output: z.void(), }, diff --git a/src/common/orpc/schemas/chatStats.ts b/src/common/orpc/schemas/chatStats.ts index ebd0cf4496..77f002b1ce 100644 --- a/src/common/orpc/schemas/chatStats.ts +++ b/src/common/orpc/schemas/chatStats.ts @@ -70,7 +70,7 @@ export const SessionUsageTokenStatsCacheSchema = z.object({ maxHistorySequence: z .number() .optional() - .meta({ description: "Max MuxMessage.metadata.historySequence seen in the message list" }), + .meta({ description: "Max XumMessage.metadata.historySequence seen in the message list" }), }), consumers: z.array(TokenConsumerSchema).meta({ description: "Sorted descending by token count" }), totalTokens: z.number(), diff --git a/src/common/orpc/schemas/message.test.ts b/src/common/orpc/schemas/message.test.ts index c0a1cde802..ddd8b6b54b 100644 --- a/src/common/orpc/schemas/message.test.ts +++ b/src/common/orpc/schemas/message.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { MuxMessageSchema } from "./message"; +import { XumMessageSchema } from "./message"; function createMessage() { return { @@ -9,12 +9,12 @@ function createMessage() { }; } -describe("MuxMessageSchema mcpPromptSnapshot parsing", () => { +describe("XumMessageSchema mcpPromptSnapshot parsing", () => { test("strips malformed snapshot metadata instead of failing the history parse", () => { const malformedSnapshotValues: unknown[] = [null, {}, { serverName: 42 }, "snapshot", []]; for (const malformed of malformedSnapshotValues) { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), role: "user" as const, metadata: { @@ -30,7 +30,7 @@ describe("MuxMessageSchema mcpPromptSnapshot parsing", () => { }); test("preserves invokingMessageId across boundary parsing", () => { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), role: "user" as const, metadata: { @@ -48,9 +48,9 @@ describe("MuxMessageSchema mcpPromptSnapshot parsing", () => { }); }); -describe("MuxMessageSchema compactionEpoch parsing", () => { +describe("XumMessageSchema compactionEpoch parsing", () => { test("preserves valid positive integer compactionEpoch", () => { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), metadata: { compactionEpoch: 7, @@ -61,7 +61,7 @@ describe("MuxMessageSchema compactionEpoch parsing", () => { }); test("preserves acpPromptId metadata", () => { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), metadata: { acpPromptId: "acp-prompt-123", @@ -72,7 +72,7 @@ describe("MuxMessageSchema compactionEpoch parsing", () => { }); test("preserves routeProvider metadata", () => { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), metadata: { routeProvider: "openai", @@ -83,7 +83,7 @@ describe("MuxMessageSchema compactionEpoch parsing", () => { }); test("preserves modelFallback metadata", () => { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), metadata: { modelFallback: { @@ -105,7 +105,7 @@ describe("MuxMessageSchema compactionEpoch parsing", () => { rawCommand: "/removed legacy command", nested: { version: 1 }, }; - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), metadata: { muxMetadata: legacyMetadata }, }); @@ -127,7 +127,7 @@ describe("MuxMessageSchema compactionEpoch parsing", () => { ]; for (const malformedModelFallback of malformedModelFallbackValues) { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), metadata: { modelFallback: malformedModelFallback, @@ -153,7 +153,7 @@ describe("MuxMessageSchema compactionEpoch parsing", () => { ]; for (const malformedCompactionEpoch of malformedCompactionEpochValues) { - const parsed = MuxMessageSchema.parse({ + const parsed = XumMessageSchema.parse({ ...createMessage(), metadata: { compactionEpoch: malformedCompactionEpoch, diff --git a/src/common/orpc/schemas/message.ts b/src/common/orpc/schemas/message.ts index 769e8eaa7d..a0423c6e5a 100644 --- a/src/common/orpc/schemas/message.ts +++ b/src/common/orpc/schemas/message.ts @@ -12,13 +12,13 @@ export const FilePartSchema = z.object({ filename: z.string().optional(), }); -export const MuxTextPartSchema = z.object({ +export const XumTextPartSchema = z.object({ type: z.literal("text"), text: z.string(), timestamp: z.number().optional(), }); -export const MuxReasoningPartSchema = z.object({ +export const XumReasoningPartSchema = z.object({ type: z.literal("reasoning"), text: z.string(), timestamp: z.number().optional(), @@ -33,7 +33,7 @@ export const WorkflowRunToolAttachmentSchema = z.object({ export type WorkflowRunToolAttachment = z.infer; // Base schema for tool parts - shared fields -const MuxToolPartBase = z.object({ +const XumToolPartBase = z.object({ type: z.literal("dynamic-tool"), toolCallId: z.string(), toolName: z.string(), @@ -70,17 +70,17 @@ export const NestedToolCallSchema = z.object({ export type NestedToolCall = z.infer; // Discriminated tool part schemas - output required only when state is "output-available" -export const DynamicToolPartPendingSchema = MuxToolPartBase.extend({ +export const DynamicToolPartPendingSchema = XumToolPartBase.extend({ state: z.literal("input-available"), nestedCalls: z.array(NestedToolCallSchema).optional(), }); -export const DynamicToolPartAvailableSchema = MuxToolPartBase.extend({ +export const DynamicToolPartAvailableSchema = XumToolPartBase.extend({ state: z.literal("output-available"), output: z.unknown(), nestedCalls: z.array(NestedToolCallSchema).optional(), }); -export const DynamicToolPartRedactedSchema = MuxToolPartBase.extend({ +export const DynamicToolPartRedactedSchema = XumToolPartBase.extend({ state: z.literal("output-redacted"), failed: z.boolean().optional(), nestedCalls: z.array(NestedToolCallSchema).optional(), @@ -93,15 +93,15 @@ export const DynamicToolPartSchema = z.discriminatedUnion("state", [ ]); // Alias for message schemas -export const MuxToolPartSchema = DynamicToolPartSchema; +export const XumToolPartSchema = DynamicToolPartSchema; -export const MuxFilePartSchema = FilePartSchema.extend({ +export const XumFilePartSchema = FilePartSchema.extend({ type: z.literal("file"), }); // Export types inferred from schemas for reuse across app/test code. export type FilePart = z.infer; -export type MuxFilePart = z.infer; +export type XumFilePart = z.infer; const CompactionEpochSchema = z.optional( z.preprocess( @@ -128,15 +128,15 @@ const TranscriptAnchorSchema = z.object({ }); // XumMessage (simplified) -export const MuxMessageSchema = z.object({ +export const XumMessageSchema = z.object({ id: z.string(), role: z.enum(["system", "user", "assistant"]), parts: z.array( z.discriminatedUnion("type", [ - MuxTextPartSchema, - MuxReasoningPartSchema, - MuxToolPartSchema, - MuxFilePartSchema, + XumTextPartSchema, + XumReasoningPartSchema, + XumToolPartSchema, + XumFilePartSchema, ]) ), createdAt: z.date().optional(), diff --git a/src/common/orpc/schemas/providerOptions.ts b/src/common/orpc/schemas/providerOptions.ts index 5038f7ae29..7f6923d1ac 100644 --- a/src/common/orpc/schemas/providerOptions.ts +++ b/src/common/orpc/schemas/providerOptions.ts @@ -1 +1 @@ -export { MuxProviderOptionsSchema } from "@/common/schemas/providerOptions"; +export { XumProviderOptionsSchema } from "@/common/schemas/providerOptions"; diff --git a/src/common/orpc/schemas/stream.ts b/src/common/orpc/schemas/stream.ts index 4b328676ac..41d8864fd5 100644 --- a/src/common/orpc/schemas/stream.ts +++ b/src/common/orpc/schemas/stream.ts @@ -7,13 +7,13 @@ import { StreamErrorTypeSchema } from "./errors"; import { FilePartSchema, ModelFallbackRecordSchema, - MuxMessageSchema, - MuxReasoningPartSchema, - MuxTextPartSchema, - MuxToolPartSchema, + XumMessageSchema, + XumReasoningPartSchema, + XumTextPartSchema, + XumToolPartSchema, } from "./message"; -import type { MuxMessageMetadata } from "../../types/message"; -import { MuxProviderOptionsSchema } from "./providerOptions"; +import type { XumMessageMetadata } from "../../types/message"; +import { XumProviderOptionsSchema } from "./providerOptions"; import { RuntimeModeSchema } from "./runtime"; import { WorkflowRunIdSchema, WorkflowRunRecordSchema } from "./workflow"; @@ -199,9 +199,9 @@ export const StreamDeltaEventSchema = z.object({ }); export const CompletedMessagePartSchema = z.discriminatedUnion("type", [ - MuxReasoningPartSchema, - MuxTextPartSchema, - MuxToolPartSchema, + XumReasoningPartSchema, + XumTextPartSchema, + XumToolPartSchema, ]); // Match LanguageModelV2Usage from @ai-sdk/provider exactly @@ -259,7 +259,7 @@ export const StreamEndEventSchema = z.object({ duration: z.number().optional(), ttftMs: z.number().optional(), systemMessageTokens: z.number().optional(), - muxMetadata: z.custom().optional(), + muxMetadata: z.custom().optional(), historySequence: z.number().optional().meta({ description: "Present when loading from history", }), @@ -268,7 +268,7 @@ export const StreamEndEventSchema = z.object({ }), }) .meta({ - description: "Structured metadata from backend - directly mergeable with MuxMetadata", + description: "Structured metadata from backend - directly mergeable with XumMetadata", }), parts: z.array(CompletedMessagePartSchema).meta({ description: "Parts array preserves temporal ordering of reasoning, text, and tool calls", @@ -606,7 +606,7 @@ export const WorkspaceInitEventSchema = z.discriminatedUnion("type", [ // Chat message wrapper with type discriminator for streaming events // XumMessageSchema is used for persisted data (chat.jsonl) which doesn't have a type field. // This wrapper adds a type discriminator for real-time streaming events. -export const ChatMuxMessageSchema = MuxMessageSchema.extend({ +export const ChatXumMessageSchema = XumMessageSchema.extend({ type: z.literal("message"), }); @@ -700,7 +700,7 @@ export const WorkspaceChatMessageSchema = z.discriminatedUnion("type", [ // Init events ...WorkspaceInitEventSchema.def.options, // Chat messages with type discriminator - ChatMuxMessageSchema, + ChatXumMessageSchema, ]); // Update Status @@ -774,7 +774,7 @@ export const SendMessageOptionsSchema = z.object({ mode: AgentModeSchema.optional().catch(undefined).meta({ description: "Legacy base mode (plan/exec/compact) for backend fallback", }), - providerOptions: MuxProviderOptionsSchema.optional(), + providerOptions: XumProviderOptionsSchema.optional(), acpPromptId: z .string() .optional() diff --git a/src/common/orpc/types.ts b/src/common/orpc/types.ts index 614e6e6c3d..ef2ed1092c 100644 --- a/src/common/orpc/types.ts +++ b/src/common/orpc/types.ts @@ -47,7 +47,7 @@ export type GoalBudgetLimitedEvent = z.infer; export type UpdateStatus = z.infer; export type DesktopPrereqStatus = z.infer; -export type ChatMuxMessage = z.infer; +export type ChatXumMessage = z.infer; export type WorkspaceStatsSnapshot = z.infer; export type WorkspaceActivitySnapshot = z.infer; export type FrontendWorkspaceMetadataSchemaType = z.infer< @@ -151,7 +151,7 @@ export function isUsageDelta(msg: WorkspaceChatMessage): msg is UsageDeltaEvent return (msg as { type?: string }).type === "usage-delta"; } -export function isMuxMessage(msg: WorkspaceChatMessage): msg is ChatMuxMessage { +export function isXumMessage(msg: WorkspaceChatMessage): msg is ChatXumMessage { return (msg as { type?: string }).type === "message"; } diff --git a/src/common/preferences/userPreferencesStorage.ts b/src/common/preferences/userPreferencesStorage.ts index eefd9f51e8..a4716708f4 100644 --- a/src/common/preferences/userPreferencesStorage.ts +++ b/src/common/preferences/userPreferencesStorage.ts @@ -39,7 +39,7 @@ import { type LaunchBehavior, type TranscriptDensity, } from "@/common/constants/storage"; -import { MuxProviderOptionsSchema } from "@/common/schemas/providerOptions"; +import { XumProviderOptionsSchema } from "@/common/schemas/providerOptions"; import { isRecord, parseAgentId, @@ -346,7 +346,7 @@ export function applyStoredUserPreference( } if (key === PROVIDER_OPTIONS_ANTHROPIC_KEY) { - const parsed = MuxProviderOptionsSchema.shape.anthropic.safeParse(value); + const parsed = XumProviderOptionsSchema.shape.anthropic.safeParse(value); if (!parsed.success || !parsed.data || Object.keys(parsed.data).length === 0) { return removeStoredUserPreference(next, key); } @@ -355,7 +355,7 @@ export function applyStoredUserPreference( } if (key === PROVIDER_OPTIONS_GOOGLE_KEY) { - const parsed = MuxProviderOptionsSchema.shape.google.safeParse(value); + const parsed = XumProviderOptionsSchema.shape.google.safeParse(value); if (!parsed.success || !parsed.data || Object.keys(parsed.data).length === 0) { return removeStoredUserPreference(next, key); } diff --git a/src/common/schemas/providerOptions.ts b/src/common/schemas/providerOptions.ts index ff7e194ef4..89531701cf 100644 --- a/src/common/schemas/providerOptions.ts +++ b/src/common/schemas/providerOptions.ts @@ -6,7 +6,7 @@ import { XAIServiceTierSchema, } from "../config/schemas/providersConfig"; -export const MuxProviderOptionsSchema = z.object({ +export const XumProviderOptionsSchema = z.object({ anthropic: z .object({ // Deprecated: prefer use1MContextModels for per-model control. diff --git a/src/common/types/durableEvent.ts b/src/common/types/durableEvent.ts index 30d5b8e0eb..0138c6a8c0 100644 --- a/src/common/types/durableEvent.ts +++ b/src/common/types/durableEvent.ts @@ -67,7 +67,7 @@ export const TurnEnvelopeDataSchema = z.object({ /** JSON-serialized PostCompactionAttachment[] injected this turn (blob-stored). */ postCompactionAttachmentsHash: BlobRefSchema.optional(), /** - * JSON-serialized MuxMessage: the partial-output continuation a refusal + * JSON-serialized XumMessage: the partial-output continuation a refusal * fallback appended to its request (blob-stored). The turn's eventual * assistant row lands after requestHistorySequence, so replay cannot * recover this message from chat.jsonl alone. diff --git a/src/common/types/instructions.ts b/src/common/types/instructions.ts index 6b06518f35..7a61f7822e 100644 --- a/src/common/types/instructions.ts +++ b/src/common/types/instructions.ts @@ -69,7 +69,7 @@ export function collectInstructionContents(sets: ReadonlyArray ): string[] { return sets diff --git a/src/common/types/message.agentSkillRefs.test.ts b/src/common/types/message.agentSkillRefs.test.ts index 003d19ce16..26d9e59a9d 100644 --- a/src/common/types/message.agentSkillRefs.test.ts +++ b/src/common/types/message.agentSkillRefs.test.ts @@ -5,7 +5,7 @@ import { mergeAgentSkillRefs, withAgentSkillRefs, } from "./message"; -import type { AgentSkillReference, MuxMessageMetadata } from "./message"; +import type { AgentSkillReference, XumMessageMetadata } from "./message"; import type { ReviewNoteData } from "./review"; function skillRef( @@ -66,7 +66,7 @@ describe("agent skill refs metadata helpers", () => { selectedCode: "const value = true;", userNote: "please review", }; - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "agent-skill", rawCommand: "/tdd write tests", commandPrefix: "/tdd", diff --git a/src/common/types/message.mcpPromptSnapshots.test.ts b/src/common/types/message.mcpPromptSnapshots.test.ts index 55844acc1c..1cb979d709 100644 --- a/src/common/types/message.mcpPromptSnapshots.test.ts +++ b/src/common/types/message.mcpPromptSnapshots.test.ts @@ -1,13 +1,13 @@ import { describe, expect, test } from "bun:test"; import { - createMuxMessage, + createXumMessage, filterOrphanedMcpPromptSnapshots, sanitizeMcpPromptRefs, } from "./message"; -import type { MuxMessage } from "./message"; +import type { XumMessage } from "./message"; -function snapshot(id: string, invokingMessageId?: string, promptName = "review"): MuxMessage { - return createMuxMessage(id, "user", `Expanded ${promptName}`, { +function snapshot(id: string, invokingMessageId?: string, promptName = "review"): XumMessage { + return createXumMessage(id, "user", `Expanded ${promptName}`, { historySequence: 0, synthetic: true, mcpPromptSnapshot: { @@ -19,8 +19,8 @@ function snapshot(id: string, invokingMessageId?: string, promptName = "review") }); } -function invokingUser(id: string, promptNames: string[]): MuxMessage { - return createMuxMessage(id, "user", "Using MCP prompt", { +function invokingUser(id: string, promptNames: string[]): XumMessage { + return createXumMessage(id, "user", "Using MCP prompt", { historySequence: 0, muxMetadata: { type: "normal", @@ -41,7 +41,7 @@ describe("filterOrphanedMcpPromptSnapshots", () => { }); test("drops a snapshot whose invoking user row was never persisted", () => { - const assistant = createMuxMessage("assistant-1", "assistant", "done", { + const assistant = createXumMessage("assistant-1", "assistant", "done", { historySequence: 0, }); const messages = [invokingUser("user-1", []), assistant, snapshot("snap-1", "user-never")]; @@ -60,7 +60,7 @@ describe("filterOrphanedMcpPromptSnapshots", () => { }); test("drops a snapshot whose invoking id points at a row without a matching ref", () => { - const unrelated = createMuxMessage("user-unrelated", "user", "Plain message", { + const unrelated = createXumMessage("user-unrelated", "user", "Plain message", { historySequence: 0, }); const messages = [snapshot("snap-1", "user-unrelated"), unrelated]; @@ -78,7 +78,7 @@ describe("filterOrphanedMcpPromptSnapshots", () => { }); test("keeps other synthetic rows with a corrupted snapshot field, stripped", () => { - const fileSnapshotRow = createMuxMessage("file-snap-1", "user", "File contents", { + const fileSnapshotRow = createXumMessage("file-snap-1", "user", "File contents", { historySequence: 0, synthetic: true, fileAtMentionSnapshot: ["@src/foo.ts"], @@ -92,7 +92,7 @@ describe("filterOrphanedMcpPromptSnapshots", () => { }); test("drops a corrupted expansion row identified by its snapshot message id", () => { - const corrupted = createMuxMessage("mcp-prompt-snapshot-123-abc", "user", "Expanded", { + const corrupted = createXumMessage("mcp-prompt-snapshot-123-abc", "user", "Expanded", { historySequence: 0, synthetic: true, }); @@ -102,7 +102,7 @@ describe("filterOrphanedMcpPromptSnapshots", () => { }); test("drops a prefixed expansion row whose snapshot field is entirely absent", () => { - const corrupted = createMuxMessage("mcp-prompt-snapshot-456-def", "user", "Expanded", { + const corrupted = createXumMessage("mcp-prompt-snapshot-456-def", "user", "Expanded", { historySequence: 0, synthetic: true, }); @@ -115,17 +115,17 @@ describe("filterOrphanedMcpPromptSnapshots", () => { }); test("drops a prefixed expansion row with no metadata at all", () => { - const bare = createMuxMessage("mcp-prompt-snapshot-789-ghi", "user", "Expanded"); + const bare = createXumMessage("mcp-prompt-snapshot-789-ghi", "user", "Expanded"); expect(filterOrphanedMcpPromptSnapshots([bare])).toEqual([]); }); test("keeps ordinary rows with a corrupted snapshot field, stripped", () => { - const authored = createMuxMessage("user-authored", "user", "Real user text", { + const authored = createXumMessage("user-authored", "user", "Real user text", { historySequence: 0, }); (authored.metadata as Record).mcpPromptSnapshot = null; - const assistant = createMuxMessage("assistant-1", "assistant", "Model reply", { + const assistant = createXumMessage("assistant-1", "assistant", "Model reply", { historySequence: 0, }); (assistant.metadata as Record).mcpPromptSnapshot = { bogus: true }; @@ -138,8 +138,8 @@ describe("filterOrphanedMcpPromptSnapshots", () => { test("drops raw rows whose snapshot field is present but malformed", () => { // Raw chat.jsonl rows bypass the oRPC sanitizer, so the request-side // filter must treat a present-but-invalid field as corruption. - const corruptRow = (id: string, snapshotValue: unknown): MuxMessage => { - const message = createMuxMessage(id, "user", "Expanded review", { + const corruptRow = (id: string, snapshotValue: unknown): XumMessage => { + const message = createXumMessage(id, "user", "Expanded review", { historySequence: 0, synthetic: true, }); diff --git a/src/common/types/message.ts b/src/common/types/message.ts index d4a24ec468..8742611566 100644 --- a/src/common/types/message.ts +++ b/src/common/types/message.ts @@ -2,7 +2,7 @@ import type { ModelMessage, UIMessage } from "ai"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; import type { StreamErrorType } from "./errors"; import type { ToolPolicy } from "@/common/utils/tools/toolPolicy"; -import type { FilePart, MuxToolPartSchema } from "@/common/orpc/schemas"; +import type { FilePart, XumToolPartSchema } from "@/common/orpc/schemas"; import type { ContextBoundaryKind, PersistedContextBoundaryKind, @@ -42,7 +42,7 @@ export interface UserMessageContent { */ export interface CompactionFollowUpInput extends UserMessageContent { /** Message metadata to apply to the queued follow-up user message (e.g., preserve /skill display) */ - muxMetadata?: MuxMessageMetadata; + muxMetadata?: XumMessageMetadata; } /** @@ -94,7 +94,7 @@ export type StartupRetrySendOptions = Pick< | "allowAgentSetGoal" > & { /** Correlation for a delegated workspace turn that must survive restart recovery. */ - muxMetadata?: Extract; + muxMetadata?: Extract; /** Internal-only Copilot billing override for startup auto-retry. */ agentInitiated?: boolean; /** Internal goal continuation classification for startup auto-retry accounting. */ @@ -110,9 +110,9 @@ export function pickStartupRetrySendOptions( agentInitiated?: boolean, goalKind?: GoalSyntheticMessageKind ): StartupRetrySendOptions { - const typedMuxMetadata = options.muxMetadata as MuxMessageMetadata | undefined; - const workspaceTurnMuxMetadata = - typedMuxMetadata?.type === "workspace-turn-task" ? typedMuxMetadata : undefined; + const typedXumMetadata = options.muxMetadata as XumMessageMetadata | undefined; + const workspaceTurnXumMetadata = + typedXumMetadata?.type === "workspace-turn-task" ? typedXumMetadata : undefined; return { model: options.model, agentId: options.agentId, @@ -125,7 +125,7 @@ export function pickStartupRetrySendOptions( experiments: options.experiments, disableWorkspaceAgents: options.disableWorkspaceAgents, allowAgentSetGoal: options.allowAgentSetGoal, - ...(workspaceTurnMuxMetadata != null ? { muxMetadata: workspaceTurnMuxMetadata } : {}), + ...(workspaceTurnXumMetadata != null ? { muxMetadata: workspaceTurnXumMetadata } : {}), ...(agentInitiated === true ? { agentInitiated: true } : {}), ...(goalKind != null ? { goalKind } : {}), }; @@ -170,7 +170,7 @@ export interface CompactionFollowUpRequest extends CompactionFollowUpInput, Pres * correlation from this stamp on the persisted summary instead (see * AgentSession.inheritOpenWorkspaceTurnMetadata). */ - workspaceTurnMetadata?: Extract; + workspaceTurnMetadata?: Extract; } /** @@ -203,10 +203,10 @@ export interface CompactionRequestData { */ export function prepareUserMessageForSend( content: UserMessageContent, - existingMetadata?: MuxMessageMetadata + existingMetadata?: XumMessageMetadata ): { finalText: string; - metadata: MuxMessageMetadata | undefined; + metadata: XumMessageMetadata | undefined; } { const { text, reviews } = content; @@ -215,7 +215,7 @@ export function prepareUserMessageForSend( const finalText = reviewsText ? reviewsText + (text ? "\n\n" + text : "") : text; // Build metadata with reviews for display - let metadata: MuxMessageMetadata | undefined = existingMetadata; + let metadata: XumMessageMetadata | undefined = existingMetadata; if (reviews?.length) { metadata = metadata ? { ...metadata, reviews } : { type: "normal", reviews }; } @@ -270,16 +270,16 @@ export function mergeAgentSkillRefs( } function getExistingAgentSkillRefs( - metadata: MuxMessageMetadata | undefined + metadata: XumMessageMetadata | undefined ): AgentSkillReference[] | undefined { const refs = metadata?.agentSkillRefs; return Array.isArray(refs) ? refs : undefined; } export function withAgentSkillRefs( - metadata: MuxMessageMetadata | undefined, + metadata: XumMessageMetadata | undefined, refs: AgentSkillReference[] -): MuxMessageMetadata | undefined { +): XumMessageMetadata | undefined { const existingRefs = getExistingAgentSkillRefs(metadata); if (refs.length === 0 && (!existingRefs || existingRefs.length === 0)) { return metadata; @@ -369,7 +369,7 @@ function isMcpPromptSnapshotBaseShape( value: unknown ): value is { serverName: string; promptName: string; invokingMessageId?: string } { if (value === null || typeof value !== "object") return false; - const snapshot = value as Partial>; + const snapshot = value as Partial>; return typeof snapshot.serverName === "string" && typeof snapshot.promptName === "string"; } @@ -378,12 +378,12 @@ function isMcpPromptSnapshotBaseShape( * and the user-row append: a snapshot survives only when its invoking user row * exists and still references the same prompt. */ -export function filterOrphanedMcpPromptSnapshots(messages: MuxMessage[]): MuxMessage[] { +export function filterOrphanedMcpPromptSnapshots(messages: XumMessage[]): XumMessage[] { // Drop only genuine expansion rows: the reserved ID prefix marks one even // when corruption removed its metadata, and synthetic rows with a valid // snapshot shape cover legacy IDs. Other rows may be corrupted with this // field and must survive after it is stripped. - const isMcpSnapshotRow = (message: MuxMessage): boolean => + const isMcpSnapshotRow = (message: XumMessage): boolean => message.role === "user" && (message.id.startsWith(MCP_PROMPT_SNAPSHOT_MESSAGE_ID_PREFIX) || (message.metadata?.synthetic === true && @@ -399,7 +399,7 @@ export function filterOrphanedMcpPromptSnapshots(messages: MuxMessage[]): MuxMes new Set(refs.map((ref) => getMcpPromptReferenceKey(ref.serverName, ref.promptName))) ); } - return messages.flatMap((message): MuxMessage[] => { + return messages.flatMap((message): XumMessage[] => { const snapshot: unknown = message.metadata?.mcpPromptSnapshot; if (!isMcpSnapshotRow(message)) { if (snapshot === undefined || message.metadata === undefined) return [message]; @@ -433,9 +433,9 @@ export function dedupeMcpPromptRefs(refs: MCPPromptReference[]): MCPPromptRefere } export function withMcpPromptRefs( - metadata: MuxMessageMetadata | undefined, + metadata: XumMessageMetadata | undefined, refs: MCPPromptReference[] -): MuxMessageMetadata | undefined { +): XumMessageMetadata | undefined { const existingRefs = sanitizeMcpPromptRefs(metadata?.mcpPromptRefs); if (existingRefs.length === 0 && refs.length === 0) { return metadata; @@ -456,7 +456,7 @@ export interface BuildAgentSkillMetadataOptions { export function buildAgentSkillMetadata( options: BuildAgentSkillMetadataOptions -): MuxMessageMetadata { +): XumMessageMetadata { return { type: "agent-skill", rawCommand: options.rawCommand, @@ -481,7 +481,7 @@ export interface TranscriptAnchor { } /** Base fields common to all metadata types */ -interface MuxMessageMetadataBase { +interface XumMessageMetadataBase { /** Structured review data for rich UI display (orthogonal to message type) */ reviews?: ReviewNoteDataForDisplay[]; /** Command prefix to highlight in UI (e.g., "/compact -m sonnet" or "/react-effects") */ @@ -529,7 +529,7 @@ export interface BashMonitorWakeDisplayRecord { filterExclude: boolean; } -export type MuxMessageMetadata = MuxMessageMetadataBase & +export type XumMessageMetadata = XumMessageMetadataBase & ( | { type: "compaction-request"; @@ -638,7 +638,7 @@ export type MuxMessageMetadata = MuxMessageMetadataBase & ); export function getCompactionFollowUpContent( - metadata?: MuxMessageMetadata + metadata?: XumMessageMetadata ): CompactionRequestData["followUpContent"] | undefined { // Keep follow-up extraction centralized so callers don't duplicate legacy handling. if (!metadata || metadata.type !== "compaction-request") { @@ -659,11 +659,11 @@ export function getCompactionFollowUpContent( } /** Type for compaction-summary metadata variant */ -export type CompactionSummaryMetadata = Extract; +export type CompactionSummaryMetadata = Extract; /** Type guard for compaction-summary metadata */ export function isCompactionSummaryMetadata( - metadata: MuxMessageMetadata | undefined + metadata: XumMessageMetadata | undefined ): metadata is CompactionSummaryMetadata { return metadata?.type === "compaction-summary"; } @@ -691,7 +691,7 @@ export interface ModelFallbackRecord { } // Our custom metadata type -export interface MuxMetadata { +export interface XumMetadata { /** Highest persisted history sequence included in the provider request that produced this assistant. */ requestHistorySequence?: number; historySequence?: number; // Assigned by backend for global message ordering (required when writing to history) @@ -768,8 +768,8 @@ export interface MuxMetadata { /** Snapshot of send options used for this user turn (for startup retry recovery). */ retrySendOptions?: StartupRetrySendOptions; agentId?: string; // Agent id active when this message was sent (assistant messages only) - cmuxMetadata?: MuxMessageMetadata; // Command metadata persisted for legacy message formats - muxMetadata?: MuxMessageMetadata; // Command metadata used by both frontend and backend message flows + cmuxMetadata?: XumMessageMetadata; // Command metadata persisted for legacy message formats + muxMetadata?: XumMessageMetadata; // Command metadata used by both frontend and backend message flows /** Persisted discriminator for synthetic user turns created by the active-goal loop. */ kind?: "goal_continuation" | "goal_budget_limit"; @@ -813,17 +813,17 @@ export interface MuxMetadata { // Extended tool part type that supports interrupted tool calls (input-available state) // Standard AI SDK ToolUIPart only supports output-available (completed tools) // Uses discriminated union: output is required when state is "output-available", absent when "input-available" -export type MuxToolPart = z.infer; +export type XumToolPart = z.infer; // Text part type -export interface MuxTextPart { +export interface XumTextPart { type: "text"; text: string; timestamp?: number; } // Reasoning part type for extended thinking content -export interface MuxReasoningPart { +export interface XumReasoningPart { type: "reasoning"; text: string; timestamp?: number; @@ -862,7 +862,7 @@ export interface MuxReasoningPart { } // File part type for multimodal messages (matches AI SDK FileUIPart) -export interface MuxFilePart { +export interface XumFilePart { type: "file"; mediaType: string; // IANA media type, e.g., "image/png", "application/pdf" url: string; // Data URL (e.g., "data:application/pdf;base64,...") or hosted URL @@ -871,8 +871,8 @@ export interface MuxFilePart { // XumMessage extends UIMessage with our metadata and custom parts // Supports text, reasoning, file, and tool parts (including interrupted tool calls) -export type MuxMessage = Omit, "parts"> & { - parts: Array; +export type XumMessage = Omit, "parts"> & { + parts: Array; }; // DisplayedMessage represents a single UI message block @@ -983,7 +983,7 @@ export type DisplayedMessage = */ executionStartedAt?: number; /** Durable workflow run attachment recovered from partial history. */ - workflowRun?: MuxToolPart["workflowRun"]; + workflowRun?: XumToolPart["workflowRun"]; // Nested tool calls for code_execution (from PTC streaming or reconstructed from result) nestedCalls?: Array<{ toolCallId: string; @@ -1086,7 +1086,7 @@ export interface QueuedMessage { } /** Keep every snapshot kind here so history scans and edits retain it with its user message. */ -export function isSyntheticSnapshotUserMessage(message: MuxMessage): boolean { +export function isSyntheticSnapshotUserMessage(message: XumMessage): boolean { return ( message.role === "user" && message.metadata?.synthetic === true && @@ -1097,13 +1097,13 @@ export function isSyntheticSnapshotUserMessage(message: MuxMessage): boolean { } // Helper to create a simple text message -export function createMuxMessage( +export function createXumMessage( id: string, role: "user" | "assistant", content: string, - metadata?: MuxMetadata, - additionalParts?: MuxMessage["parts"] -): MuxMessage { + metadata?: XumMetadata, + additionalParts?: XumMessage["parts"] +): XumMessage { const textPart = content ? [{ type: "text" as const, text: content, state: "done" as const }] : []; diff --git a/src/common/types/providerOptions.ts b/src/common/types/providerOptions.ts index aaffa0b070..bd2ad848f5 100644 --- a/src/common/types/providerOptions.ts +++ b/src/common/types/providerOptions.ts @@ -1,5 +1,5 @@ import type z from "zod"; -import type { MuxProviderOptionsSchema } from "../orpc/schemas"; +import type { XumProviderOptionsSchema } from "../orpc/schemas"; /** * Xum provider-specific options that get passed through the stack. @@ -12,4 +12,4 @@ import type { MuxProviderOptionsSchema } from "../orpc/schemas"; * configuration level (e.g., custom headers, beta features). */ -export type MuxProviderOptions = z.infer; +export type XumProviderOptions = z.infer; diff --git a/src/common/types/stream.ts b/src/common/types/stream.ts index c1979b0bd4..0c1b2e777f 100644 --- a/src/common/types/stream.ts +++ b/src/common/types/stream.ts @@ -3,7 +3,7 @@ */ import type { z } from "zod"; -import type { MuxReasoningPart, MuxTextPart, MuxToolPart } from "./message"; +import type { XumReasoningPart, XumTextPart, XumToolPart } from "./message"; import type { AutoCompactionCompletedEventSchema, AutoCompactionTriggeredEventSchema, @@ -39,7 +39,7 @@ import type { * Completed message part (reasoning, text, or tool) suitable for serialization * Used in StreamEndEvent and partial message storage */ -export type CompletedMessagePart = MuxReasoningPart | MuxTextPart | MuxToolPart; +export type CompletedMessagePart = XumReasoningPart | XumTextPart | XumToolPart; export type StreamStartEvent = z.infer; export type StreamDeltaEvent = z.infer; diff --git a/src/common/utils/ai/providerOptions.test.ts b/src/common/utils/ai/providerOptions.test.ts index 5ba4cf42f7..1f1b2d0913 100644 --- a/src/common/utils/ai/providerOptions.test.ts +++ b/src/common/utils/ai/providerOptions.test.ts @@ -5,7 +5,7 @@ import { createOpenAI, type OpenAIResponsesProviderOptions } from "@ai-sdk/openai"; import { generateText, streamText } from "ai"; import type { ProvidersConfigMap } from "@/common/orpc/types"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { createOpenAICachedSystemMessage } from "./cacheStrategy"; import { describe, test, expect, mock } from "bun:test"; import { openaiDirectProviderOptionsAvailable } from "./openaiProviderOptionsAvailability"; @@ -17,7 +17,7 @@ import { preserveAnthropic1MContextForFollowUp, resolveProviderOptionsNamespaceKey, ANTHROPIC_1M_CONTEXT_HEADER, - MUX_WORKSPACE_ID_HEADER, + XUM_WORKSPACE_ID_HEADER, } from "./providerOptions"; // Mock the log module to avoid console noise @@ -1564,7 +1564,7 @@ describe("buildProviderOptions - OpenAI", () => { describe("OpenAI conversation state management", () => { test("does not reuse previousResponseId when Xum already sends explicit GPT-5.5 history", () => { const messages = [ - createMuxMessage("assistant-1", "assistant", "", { + createXumMessage("assistant-1", "assistant", "", { model: "mux-gateway:openai/gpt-5.5", providerMetadata: { openai: { responseId: "resp_123" } }, }), @@ -1631,7 +1631,7 @@ describe("buildProviderOptions - OpenAI", () => { test("omits previousResponseId when wireFormat is chatCompletions", () => { const messages = [ - createMuxMessage("assistant-1", "assistant", "", { + createXumMessage("assistant-1", "assistant", "", { model: "openai:gpt-5.2", providerMetadata: { openai: { responseId: "resp_chat_123" } }, }), @@ -2250,14 +2250,14 @@ describe("buildRequestHeaders", () => { model: "openai:gpt-5.2", options: undefined, workspaceId: "a1b2c3d4e5", - expected: { [MUX_WORKSPACE_ID_HEADER]: "a1b2c3d4e5" }, + expected: { [XUM_WORKSPACE_ID_HEADER]: "a1b2c3d4e5" }, }, { name: "should include X-Mux-Workspace-Id for mux-gateway routes", model: "mux-gateway:openai/gpt-5.2", options: undefined, workspaceId: "a1b2c3d4e5", - expected: { [MUX_WORKSPACE_ID_HEADER]: "a1b2c3d4e5" }, + expected: { [XUM_WORKSPACE_ID_HEADER]: "a1b2c3d4e5" }, }, { name: "should encode non-header-safe workspace IDs before attaching request header", @@ -2265,7 +2265,7 @@ describe("buildRequestHeaders", () => { options: undefined, workspaceId: "workspace-😀", expected: { - [MUX_WORKSPACE_ID_HEADER]: `b64:${Buffer.from("workspace-😀", "utf8").toString("base64url")}`, + [XUM_WORKSPACE_ID_HEADER]: `b64:${Buffer.from("workspace-😀", "utf8").toString("base64url")}`, }, }, { @@ -2274,7 +2274,7 @@ describe("buildRequestHeaders", () => { options: { anthropic: { use1MContext: true } }, workspaceId: "a1b2c3d4e5", expected: { - [MUX_WORKSPACE_ID_HEADER]: "a1b2c3d4e5", + [XUM_WORKSPACE_ID_HEADER]: "a1b2c3d4e5", "anthropic-beta": ANTHROPIC_1M_CONTEXT_HEADER, }, }, @@ -2283,7 +2283,7 @@ describe("buildRequestHeaders", () => { model: "anthropic:claude-sonnet-4-20250514", options: undefined, workspaceId: "deadbeef00", - expected: { [MUX_WORKSPACE_ID_HEADER]: "deadbeef00" }, + expected: { [XUM_WORKSPACE_ID_HEADER]: "deadbeef00" }, }, ] as const) { test(name, () => { @@ -2294,7 +2294,7 @@ describe("buildRequestHeaders", () => { } test("workspace correlation header uses the mux wire name, not Xum", () => { - expect(MUX_WORKSPACE_ID_HEADER).toBe("X-Mux-Workspace-Id"); + expect(XUM_WORKSPACE_ID_HEADER).toBe("X-Mux-Workspace-Id"); const headers = buildRequestHeaders("openai:gpt-5.2", undefined, "ws-id"); expect(headers?.["X-Mux-Workspace-Id"]).toBe("ws-id"); expect(headers?.["X-Xum-Workspace-Id"]).toBeUndefined(); diff --git a/src/common/utils/ai/providerOptions.ts b/src/common/utils/ai/providerOptions.ts index 460541ed92..9e5c75e06f 100644 --- a/src/common/utils/ai/providerOptions.ts +++ b/src/common/utils/ai/providerOptions.ts @@ -17,7 +17,7 @@ import type { } from "@ai-sdk/xai"; import type { ProviderName } from "@/common/constants/providers"; import type { ProvidersConfigMap } from "@/common/orpc/types"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import type { OpenAIReasoningMode, ThinkingLevel } from "@/common/types/thinking"; import { getAnthropicEffort, @@ -36,7 +36,7 @@ import { isGeminiFlashThinkingLevelModelName } from "@/common/utils/thinking/pol import { openaiExplicitPromptCachingAvailable } from "@/common/utils/ai/cacheStrategy"; import { resolveModelForMetadata } from "@/common/utils/providers/modelEntries"; import { log } from "@/node/services/log"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { normalizeToCanonical, resolveProviderOptionsNamespaceKey, @@ -172,7 +172,7 @@ function resolveAnthropic1MCapabilityModel( function hasAnthropic1MIntentForModel( modelString: string, capabilityModel: string, - muxProviderOptions?: MuxProviderOptions + muxProviderOptions?: XumProviderOptions ): boolean { const anthropicOptions = muxProviderOptions?.anthropic; if (!anthropicOptions) { @@ -201,7 +201,7 @@ function hasAnthropic1MIntentForModel( */ export function isAnthropic1MEffectivelyEnabled( modelString: string, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, providersConfig?: ProvidersConfigMap | null ): boolean { const anthropicOptions = muxProviderOptions?.anthropic; @@ -232,9 +232,9 @@ export function isAnthropic1MEffectivelyEnabled( export function preserveAnthropic1MContextForFollowUp( sourceModelString: string, targetModelString: string, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, providersConfig?: ProvidersConfigMap | null -): MuxProviderOptions | undefined { +): XumProviderOptions | undefined { if (!muxProviderOptions) { return undefined; } @@ -287,9 +287,9 @@ export function preserveAnthropic1MContextForFollowUp( export function buildProviderOptions( modelString: string, thinkingLevel: ThinkingLevel, - messages?: MuxMessage[], + messages?: XumMessage[], _lostResponseIds?: (id: string) => boolean, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, workspaceId?: string, // Optional for non-OpenAI providers openaiTruncationMode?: OpenAIResponsesProviderOptions["truncation"], providersConfig?: ProvidersConfigMap | null, @@ -710,7 +710,7 @@ export const ANTHROPIC_1M_CONTEXT_HEADER = "context-1m-2025-08-07"; * The JS symbol can stay Xum-branded; the wire value remains X-Mux-Workspace-Id * so mux-gateway and existing correlation pipelines keep matching. */ -export const MUX_WORKSPACE_ID_HEADER = "X-Mux-Workspace-Id"; +export const XUM_WORKSPACE_ID_HEADER = "X-Mux-Workspace-Id"; const HTTP_HEADER_VALUE_SAFE_PATTERN = /^[\t\x20-\x7E\x80-\xFF]+$/; @@ -740,7 +740,7 @@ function toWorkspaceHeaderValue(workspaceId: string): string { */ export function buildRequestHeaders( modelString: string, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, workspaceId?: string, providersConfig?: ProvidersConfigMap | null, routeProvider?: ProviderName @@ -748,7 +748,7 @@ export function buildRequestHeaders( const headers: Record = {}; if (workspaceId != null) { - headers[MUX_WORKSPACE_ID_HEADER] = toWorkspaceHeaderValue(workspaceId); + headers[XUM_WORKSPACE_ID_HEADER] = toWorkspaceHeaderValue(workspaceId); } const normalized = resolveOptionsCanonicalModel(modelString, providersConfig); diff --git a/src/common/utils/goalClearedSummaryDisplay.ts b/src/common/utils/goalClearedSummaryDisplay.ts index 04b1e77b73..3cafd7f8ba 100644 --- a/src/common/utils/goalClearedSummaryDisplay.ts +++ b/src/common/utils/goalClearedSummaryDisplay.ts @@ -1,10 +1,10 @@ -import type { MuxMessageMetadata } from "@/common/types/message"; +import type { XumMessageMetadata } from "@/common/types/message"; const LEGACY_GOAL_CLEARED_SUMMARY_PREFIX = "Goal cleared: "; export function getGoalClearedSummaryDisplayText( content: string, - muxMetadata: MuxMessageMetadata | undefined + muxMetadata: XumMessageMetadata | undefined ): string { if (muxMetadata?.type !== "goal-cleared-summary") { return content; diff --git a/src/common/utils/messages/compactionBoundary.test.ts b/src/common/utils/messages/compactionBoundary.test.ts index 09e634eece..f1500ec344 100644 --- a/src/common/utils/messages/compactionBoundary.test.ts +++ b/src/common/utils/messages/compactionBoundary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { findLatestCompactionBoundaryIndex, @@ -13,19 +13,19 @@ import { describe("findLatestCompactionBoundaryIndex", () => { it("returns the newest compaction boundary via reverse scan", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-1", "assistant", "first summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-1", "assistant", "first summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "middle"), - createMuxMessage("summary-2", "assistant", "second summary", { + createXumMessage("u1", "user", "middle"), + createXumMessage("summary-2", "assistant", "second summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 2, }), - createMuxMessage("u2", "user", "latest"), + createXumMessage("u2", "user", "latest"), ]; expect(findLatestCompactionBoundaryIndex(messages)).toBe(3); @@ -33,13 +33,13 @@ describe("findLatestCompactionBoundaryIndex", () => { it("treats heartbeat reset boundaries as durable compaction boundaries", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("heartbeat-reset", "assistant", "heartbeat reset", { + createXumMessage("u0", "user", "before"), + createXumMessage("heartbeat-reset", "assistant", "heartbeat reset", { compacted: "heartbeat", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; expect(findLatestCompactionBoundaryIndex(messages)).toBe(1); @@ -47,11 +47,11 @@ describe("findLatestCompactionBoundaryIndex", () => { it("returns -1 when only legacy compacted summaries exist", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("legacy-summary", "assistant", "legacy summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("legacy-summary", "assistant", "legacy summary", { compacted: "user", }), - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; expect(findLatestCompactionBoundaryIndex(messages)).toBe(-1); @@ -59,19 +59,19 @@ describe("findLatestCompactionBoundaryIndex", () => { it("ignores boundary markers that are missing compactionEpoch", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-valid", "assistant", "valid summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-valid", "assistant", "valid summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "middle"), - createMuxMessage("summary-missing-epoch", "assistant", "malformed summary", { + createXumMessage("u1", "user", "middle"), + createXumMessage("summary-missing-epoch", "assistant", "malformed summary", { compacted: "user", compactionBoundary: true, // Corrupted/normalized persisted metadata: missing epoch must not be durable. }), - createMuxMessage("u2", "user", "after"), + createXumMessage("u2", "user", "after"), ]; expect(findLatestCompactionBoundaryIndex(messages)).toBe(1); @@ -79,26 +79,26 @@ describe("findLatestCompactionBoundaryIndex", () => { it("skips malformed boundary markers and keeps scanning for the latest durable boundary", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-valid", "assistant", "valid summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-valid", "assistant", "valid summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "middle"), - createMuxMessage("summary-malformed", "assistant", "malformed summary", { + createXumMessage("u1", "user", "middle"), + createXumMessage("summary-malformed", "assistant", "malformed summary", { // Corrupted persisted metadata: looks like a boundary but is not a compacted summary. compacted: false, compactionBoundary: true, compactionEpoch: 2, }), - createMuxMessage("u2", "user", "after"), + createXumMessage("u2", "user", "after"), ]; expect(findLatestCompactionBoundaryIndex(messages)).toBe(1); }); it("ignores boundary markers with malformed compacted values", () => { - const malformedCompactedBoundary = createMuxMessage( + const malformedCompactedBoundary = createXumMessage( "summary-malformed-compacted", "assistant", "malformed summary", @@ -112,14 +112,14 @@ describe("findLatestCompactionBoundaryIndex", () => { } const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-valid", "assistant", "valid summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-valid", "assistant", "valid summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), malformedCompactedBoundary, - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; expect(findLatestCompactionBoundaryIndex(messages)).toBe(1); @@ -127,18 +127,18 @@ describe("findLatestCompactionBoundaryIndex", () => { it("ignores user-role messages with boundary-like metadata", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-valid", "assistant", "valid summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-valid", "assistant", "valid summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "not-a-summary", { + createXumMessage("u1", "user", "not-a-summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 2, }), - createMuxMessage("u2", "user", "after"), + createXumMessage("u2", "user", "after"), ]; expect(findLatestCompactionBoundaryIndex(messages)).toBe(1); @@ -148,9 +148,9 @@ describe("findLatestCompactionBoundaryIndex", () => { describe("context boundary helpers", () => { it("recognizes context reset boundaries as latest context boundary", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), - createMuxMessage("u1", "user", "after"), + createXumMessage("u0", "user", "before"), + createXumMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), + createXumMessage("u1", "user", "after"), ]; expect(findLatestContextBoundaryIndex(messages)).toBe(1); @@ -159,11 +159,11 @@ describe("context boundary helpers", () => { it("excludes reset boundaries and pre-reset messages from provider slices", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("a0", "assistant", "before reply"), - createMuxMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), - createMuxMessage("u1", "user", "after"), - createMuxMessage("a1", "assistant", "after reply"), + createXumMessage("u0", "user", "before"), + createXumMessage("a0", "assistant", "before reply"), + createXumMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), + createXumMessage("u1", "user", "after"), + createXumMessage("a1", "assistant", "after reply"), ]; const sliced = sliceMessagesForProviderFromLatestContextBoundary(messages); @@ -173,13 +173,13 @@ describe("context boundary helpers", () => { it("keeps compaction summaries provider-visible in context slices", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary", "assistant", "summary text", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary", "assistant", "summary text", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; const sliced = sliceMessagesForProviderFromLatestContextBoundary(messages); @@ -189,15 +189,15 @@ describe("context boundary helpers", () => { it("uses the latest boundary across mixed compaction and reset histories", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary", "assistant", "summary text", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary", "assistant", "summary text", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "middle"), - createMuxMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), - createMuxMessage("u2", "user", "latest"), + createXumMessage("u1", "user", "middle"), + createXumMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), + createXumMessage("u2", "user", "latest"), ]; expect(findLatestContextBoundaryIndex(messages)).toBe(3); @@ -209,30 +209,30 @@ describe("context boundary helpers", () => { it("does not count reset boundaries as provider-eligible messages", () => { expect( hasProviderEligibleMessages([ - createMuxMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), + createXumMessage("reset", "assistant", "", { contextBoundaryKind: "reset" }), ]) ).toBe(false); - expect(hasProviderEligibleMessages([createMuxMessage("u1", "user", "after")])).toBe(true); + expect(hasProviderEligibleMessages([createXumMessage("u1", "user", "after")])).toBe(true); }); }); describe("sliceMessagesFromLatestCompactionBoundary", () => { it("slices request payload history from the latest compaction boundary", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-1", "assistant", "first summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-1", "assistant", "first summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "middle"), - createMuxMessage("summary-2", "assistant", "second summary", { + createXumMessage("u1", "user", "middle"), + createXumMessage("summary-2", "assistant", "second summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 2, }), - createMuxMessage("u2", "user", "latest"), - createMuxMessage("a2", "assistant", "reply"), + createXumMessage("u2", "user", "latest"), + createXumMessage("a2", "assistant", "reply"), ]; const sliced = sliceMessagesFromLatestCompactionBoundary(messages); @@ -243,14 +243,14 @@ describe("sliceMessagesFromLatestCompactionBoundary", () => { it("slices from heartbeat reset boundaries", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("heartbeat-reset", "assistant", "heartbeat reset", { + createXumMessage("u0", "user", "before"), + createXumMessage("heartbeat-reset", "assistant", "heartbeat reset", { compacted: "heartbeat", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "after"), - createMuxMessage("a1", "assistant", "reply"), + createXumMessage("u1", "user", "after"), + createXumMessage("a1", "assistant", "reply"), ]; const sliced = sliceMessagesFromLatestCompactionBoundary(messages); @@ -260,11 +260,11 @@ describe("sliceMessagesFromLatestCompactionBoundary", () => { it("falls back to full history when no durable boundary exists", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("legacy-summary", "assistant", "legacy summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("legacy-summary", "assistant", "legacy summary", { compacted: "user", }), - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; const sliced = sliceMessagesFromLatestCompactionBoundary(messages); @@ -275,13 +275,13 @@ describe("sliceMessagesFromLatestCompactionBoundary", () => { it("treats missing compactionEpoch boundary markers as non-boundaries", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-missing-epoch", "assistant", "malformed summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-missing-epoch", "assistant", "malformed summary", { compacted: "user", compactionBoundary: true, // Schema normalization can drop malformed epochs to undefined. }), - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; const sliced = sliceMessagesFromLatestCompactionBoundary(messages); @@ -291,7 +291,7 @@ describe("sliceMessagesFromLatestCompactionBoundary", () => { }); it("treats malformed compacted boundary markers as non-boundaries", () => { - const malformedCompactedBoundary = createMuxMessage( + const malformedCompactedBoundary = createXumMessage( "summary-malformed-compacted", "assistant", "malformed summary", @@ -305,9 +305,9 @@ describe("sliceMessagesFromLatestCompactionBoundary", () => { } const messages = [ - createMuxMessage("u0", "user", "before"), + createXumMessage("u0", "user", "before"), malformedCompactedBoundary, - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; const sliced = sliceMessagesFromLatestCompactionBoundary(messages); @@ -318,18 +318,18 @@ describe("sliceMessagesFromLatestCompactionBoundary", () => { it("does not slice from user-role messages with boundary-like metadata", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-valid", "assistant", "valid summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-valid", "assistant", "valid summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }), - createMuxMessage("u1", "user", "not-a-summary", { + createXumMessage("u1", "user", "not-a-summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 2, }), - createMuxMessage("a1", "assistant", "after"), + createXumMessage("a1", "assistant", "after"), ]; const sliced = sliceMessagesFromLatestCompactionBoundary(messages); @@ -340,14 +340,14 @@ describe("sliceMessagesFromLatestCompactionBoundary", () => { it("treats malformed boundary markers as non-boundaries instead of crashing", () => { const messages = [ - createMuxMessage("u0", "user", "before"), - createMuxMessage("summary-malformed", "assistant", "malformed summary", { + createXumMessage("u0", "user", "before"), + createXumMessage("summary-malformed", "assistant", "malformed summary", { compacted: "user", compactionBoundary: true, // Corrupted persisted metadata: invalid epoch should not brick request assembly. compactionEpoch: 0, }), - createMuxMessage("u1", "user", "after"), + createXumMessage("u1", "user", "after"), ]; const sliced = sliceMessagesFromLatestCompactionBoundary(messages); diff --git a/src/common/utils/messages/compactionBoundary.ts b/src/common/utils/messages/compactionBoundary.ts index 6043cf14e6..66aafcff26 100644 --- a/src/common/utils/messages/compactionBoundary.ts +++ b/src/common/utils/messages/compactionBoundary.ts @@ -6,7 +6,7 @@ import { import { isPositiveInteger } from "@/common/utils/numbers"; import { hasProviderReplayableContent } from "@/common/utils/messages/providerEligibility"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; export { CONTEXT_BOUNDARY_KINDS, type ContextBoundaryKind }; @@ -16,7 +16,7 @@ export function isDurableCompactedMarker( return value === true || value === "user" || value === "idle" || value === "heartbeat"; } -export function isDurableCompactionBoundaryMarker(message: MuxMessage | undefined): boolean { +export function isDurableCompactionBoundaryMarker(message: XumMessage | undefined): boolean { if (message?.metadata?.compactionBoundary !== true) { return false; } @@ -39,7 +39,7 @@ export function isDurableCompactionBoundaryMarker(message: MuxMessage | undefine return true; } -export function isDurableContextResetBoundaryMarker(message: MuxMessage | undefined): boolean { +export function isDurableContextResetBoundaryMarker(message: XumMessage | undefined): boolean { if (message?.metadata?.contextBoundaryKind !== CONTEXT_BOUNDARY_KINDS.RESET) { return false; } @@ -54,7 +54,7 @@ export function isDurableContextResetBoundaryMarker(message: MuxMessage | undefi } export function getContextBoundaryKind( - message: MuxMessage | undefined + message: XumMessage | undefined ): ContextBoundaryKind | null { if (isDurableContextResetBoundaryMarker(message)) { return CONTEXT_BOUNDARY_KINDS.RESET; @@ -67,7 +67,7 @@ export function getContextBoundaryKind( return null; } -export function isDurableContextBoundaryMarker(message: MuxMessage | undefined): boolean { +export function isDurableContextBoundaryMarker(message: XumMessage | undefined): boolean { return getContextBoundaryKind(message) !== null; } @@ -77,7 +77,7 @@ export function isDurableContextBoundaryMarker(message: MuxMessage | undefined): * Returns the index of the newest message tagged with valid boundary metadata, * or `-1` when no durable boundary exists in the provided history. */ -export function findLatestContextBoundaryIndex(messages: MuxMessage[]): number { +export function findLatestContextBoundaryIndex(messages: XumMessage[]): number { assert(Array.isArray(messages), "findLatestContextBoundaryIndex requires a message array"); for (let i = messages.length - 1; i >= 0; i -= 1) { @@ -90,7 +90,7 @@ export function findLatestContextBoundaryIndex(messages: MuxMessage[]): number { } /** Backwards-compatible compaction-only lookup for existing call sites and tests. */ -export function findLatestCompactionBoundaryIndex(messages: MuxMessage[]): number { +export function findLatestCompactionBoundaryIndex(messages: XumMessage[]): number { assert(Array.isArray(messages), "findLatestCompactionBoundaryIndex requires a message array"); for (let i = messages.length - 1; i >= 0; i -= 1) { @@ -107,7 +107,7 @@ export function findLatestCompactionBoundaryIndex(messages: MuxMessage[]): numbe * * This is request-only and must not be used to mutate persisted replay history. */ -export function sliceMessagesFromLatestCompactionBoundary(messages: MuxMessage[]): MuxMessage[] { +export function sliceMessagesFromLatestCompactionBoundary(messages: XumMessage[]): XumMessage[] { const boundaryIndex = findLatestCompactionBoundaryIndex(messages); if (boundaryIndex === -1) { return messages; @@ -128,7 +128,7 @@ export function sliceMessagesFromLatestCompactionBoundary(messages: MuxMessage[] return sliced; } -export function isProviderEligibleMessage(message: MuxMessage): boolean { +export function isProviderEligibleMessage(message: XumMessage): boolean { if (isDurableContextResetBoundaryMarker(message)) { return false; } @@ -136,7 +136,7 @@ export function isProviderEligibleMessage(message: MuxMessage): boolean { return hasProviderReplayableContent(message); } -export function hasProviderEligibleMessages(messages: MuxMessage[]): boolean { +export function hasProviderEligibleMessages(messages: XumMessage[]): boolean { assert(Array.isArray(messages), "hasProviderEligibleMessages requires a message array"); return messages.some(isProviderEligibleMessage); } @@ -149,8 +149,8 @@ export function hasProviderEligibleMessages(messages: MuxMessage[]): boolean { * after the reset marker. */ export function sliceMessagesForProviderFromLatestContextBoundary( - messages: MuxMessage[] -): MuxMessage[] { + messages: XumMessage[] +): XumMessage[] { const boundaryIndex = findLatestContextBoundaryIndex(messages); if (boundaryIndex === -1) { return messages; diff --git a/src/common/utils/messages/extractEditedFiles.test.ts b/src/common/utils/messages/extractEditedFiles.test.ts index 8cecb66f59..c962b4d1fe 100644 --- a/src/common/utils/messages/extractEditedFiles.test.ts +++ b/src/common/utils/messages/extractEditedFiles.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { createPatch } from "diff"; import { FILE_EDIT_DIFF_OMITTED_MESSAGE } from "@/common/types/tools"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { extractEditedFileDiffs, extractEditedFilePaths } from "./extractEditedFiles"; /** @@ -16,7 +16,7 @@ function createAssistantMessage( success?: boolean; inputPathKey?: "path" | "file_path"; }> -): MuxMessage { +): XumMessage { return { id: `msg-${Math.random().toString(36).slice(2)}`, role: "assistant", @@ -52,7 +52,7 @@ function makeDiff(filePath: string, oldContent: string, newContent: string): str describe("extractEditedFilePaths", () => { it("should extract file paths from successful edits", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -74,7 +74,7 @@ describe("extractEditedFilePaths", () => { }); it("should extract file paths from legacy path alias inputs", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -90,7 +90,7 @@ describe("extractEditedFilePaths", () => { }); it("should ignore failed edits", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -106,7 +106,7 @@ describe("extractEditedFilePaths", () => { }); it("should dedupe paths and return most recent first", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -142,7 +142,7 @@ describe("extractEditedFileDiffs", () => { const newContent = "line1\nmodified\nline3"; const diff = makeDiff("/path/to/file.ts", originalContent, newContent); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -165,7 +165,7 @@ describe("extractEditedFileDiffs", () => { const newContent = "line1\nupdated\nline3"; const diff = makeDiff("/path/to/legacy.ts", originalContent, newContent); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -187,7 +187,7 @@ describe("extractEditedFileDiffs", () => { const newContent = "line1\nmodified\nline3"; const diff = makeDiff("/path/to/file.ts", originalContent, newContent); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -213,7 +213,7 @@ describe("extractEditedFileDiffs", () => { const afterEdit2 = "line1\nMODIFIED2\nline3\nMODIFIED4\nline5"; const diff2 = makeDiff("/path/to/file.ts", afterEdit1, afterEdit2); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -249,7 +249,7 @@ describe("extractEditedFileDiffs", () => { const afterEdit2 = "line1\nSECOND_EDIT\nline3"; const diff2 = makeDiff("/path/to/file.ts", afterEdit1, afterEdit2); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", @@ -284,7 +284,7 @@ describe("extractEditedFileDiffs", () => { const diff2 = makeDiff("/path/to/file.ts", v1, v2); const diff3 = makeDiff("/path/to/file.ts", v2, v3); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", filePath: "/path/to/file.ts", diff: diff1 }, ]), @@ -314,7 +314,7 @@ describe("extractEditedFileDiffs", () => { const afterModify = "line1\nMODIFIED\nline3"; const diff2 = makeDiff("/path/to/file.ts", afterInsert, afterModify); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_insert", filePath: "/path/to/file.ts", diff: diff1 }, ]), @@ -342,7 +342,7 @@ describe("extractEditedFileDiffs", () => { const diff2a = makeDiff("/file2.ts", file2Original, file2V1); const diff2b = makeDiff("/file2.ts", file2V1, file2Final); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", filePath: "/file1.ts", diff: diff1 }, ]), @@ -370,7 +370,7 @@ describe("extractEditedFileDiffs", () => { const afterSuccess = "modified"; const successDiff = makeDiff("/file.ts", original, afterSuccess); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", filePath: "/file.ts", diff: successDiff }, ]), @@ -399,7 +399,7 @@ describe("extractEditedFileDiffs", () => { const afterDelete = "start\nmiddle1\nend"; const diff2 = makeDiff("/file.ts", afterAdd, afterDelete); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", filePath: "/file.ts", diff: diff1 }, ]), @@ -433,7 +433,7 @@ describe("extractEditedFileDiffs", () => { const afterEdit2 = linesAfterEdit2.join("\n"); const diff2 = makeDiff("/large-file.ts", afterEdit1, afterEdit2); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createAssistantMessage([ { toolName: "file_edit_replace_string", filePath: "/large-file.ts", diff: diff1 }, ]), diff --git a/src/common/utils/messages/extractEditedFiles.ts b/src/common/utils/messages/extractEditedFiles.ts index b33f0b2d8c..c757151222 100644 --- a/src/common/utils/messages/extractEditedFiles.ts +++ b/src/common/utils/messages/extractEditedFiles.ts @@ -1,4 +1,4 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { getToolOutputUiOnly } from "@/common/utils/tools/toolOutputUiOnly"; import { FILE_EDIT_TOOL_NAMES } from "@/common/types/tools"; import { MAX_EDITED_FILES, MAX_FILE_CONTENT_SIZE } from "@/common/constants/attachments"; @@ -31,7 +31,7 @@ export interface FileEditDiff { * @param messages - The message history to scan * @returns Array of unique absolute file paths that were edited (max MAX_EDITED_FILES) */ -export function extractEditedFilePaths(messages: MuxMessage[]): string[] { +export function extractEditedFilePaths(messages: XumMessage[]): string[] { const editedFiles: string[] = []; const seen = new Set(); @@ -176,7 +176,7 @@ function extractOriginalFromDiffs(diffs: string[]): string { * @param messages - The message history to scan * @returns Array of file diffs (max MAX_EDITED_FILES) */ -export function extractEditedFileDiffs(messages: MuxMessage[]): FileEditDiff[] { +export function extractEditedFileDiffs(messages: XumMessage[]): FileEditDiff[] { // Collect all diffs per file path in chronological order const diffsByPath = new Map(); const editOrder: string[] = []; // Track order of last edit per file diff --git a/src/common/utils/messages/providerEligibility.ts b/src/common/utils/messages/providerEligibility.ts index 684aaa6ba0..0b1f869f49 100644 --- a/src/common/utils/messages/providerEligibility.ts +++ b/src/common/utils/messages/providerEligibility.ts @@ -1,7 +1,7 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; export function hasProviderReplayableContent( - message: MuxMessage, + message: XumMessage, options: { preserveReasoningOnly?: boolean } = {} ): boolean { if (message.role === "system") { diff --git a/src/common/utils/messages/startHerePlanSummary.test.ts b/src/common/utils/messages/startHerePlanSummary.test.ts index 78b8d73024..e6fe3bc359 100644 --- a/src/common/utils/messages/startHerePlanSummary.test.ts +++ b/src/common/utils/messages/startHerePlanSummary.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "bun:test"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { hasStartHerePlanSummary, isStartHerePlanSummaryMessage } from "./startHerePlanSummary"; -function createTextMessage(overrides: Partial): MuxMessage { +function createTextMessage(overrides: Partial): XumMessage { return { id: overrides.id ?? `msg-${Math.random().toString(36).slice(2)}`, role: overrides.role ?? "assistant", @@ -79,7 +79,7 @@ describe("isStartHerePlanSummaryMessage", () => { describe("hasStartHerePlanSummary", () => { it("returns true when a Start Here plan summary exists anywhere in history", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ createTextMessage({ id: "start-here-123", role: "assistant", diff --git a/src/common/utils/messages/startHerePlanSummary.ts b/src/common/utils/messages/startHerePlanSummary.ts index 98fe9ec4c2..0d0d04a334 100644 --- a/src/common/utils/messages/startHerePlanSummary.ts +++ b/src/common/utils/messages/startHerePlanSummary.ts @@ -1,9 +1,9 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; const START_HERE_PLAN_PATH_NOTE_MARKER = "*Plan file preserved at:*"; const START_HERE_PLAN_PLACEHOLDER_PREFIX = "*Plan saved to "; -function getTextContent(message: MuxMessage): string { +function getTextContent(message: XumMessage): string { return ( message.parts ?.filter((part) => part.type === "text") @@ -42,7 +42,7 @@ function getPlanBodyText(startHereText: string): string { * the exec agent to re-read the plan file), which would waste tokens and often * results in redundant file reads. */ -export function isStartHerePlanSummaryMessage(message: MuxMessage): boolean { +export function isStartHerePlanSummaryMessage(message: XumMessage): boolean { if (message.role !== "assistant") return false; // The Start Here summary is stored as a user-compaction-style message so it @@ -67,7 +67,7 @@ export function isStartHerePlanSummaryMessage(message: MuxMessage): boolean { return true; } -export function hasStartHerePlanSummary(messages: MuxMessage[]): boolean { +export function hasStartHerePlanSummary(messages: XumMessage[]): boolean { for (let i = messages.length - 1; i >= 0; i--) { if (isStartHerePlanSummaryMessage(messages[i])) return true; } diff --git a/src/common/utils/messages/transcriptShare.test.ts b/src/common/utils/messages/transcriptShare.test.ts index b6a95dc00e..15cacea7f4 100644 --- a/src/common/utils/messages/transcriptShare.test.ts +++ b/src/common/utils/messages/transcriptShare.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { buildChatJsonlForSharing } from "./transcriptShare"; function splitJsonlLines(jsonl: string): string[] { @@ -8,7 +8,7 @@ function splitJsonlLines(jsonl: string): string[] { describe("buildChatJsonlForSharing", () => { it("strips tool output and sets state to output-redacted when includeToolOutput=false", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -28,7 +28,7 @@ describe("buildChatJsonlForSharing", () => { const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: false }); expect(jsonl.endsWith("\n")).toBe(true); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; expect(part.type).toBe("dynamic-tool"); @@ -49,7 +49,7 @@ describe("buildChatJsonlForSharing", () => { }); it("strips nestedCalls output and sets nestedCalls state to output-redacted when includeToolOutput=false", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -75,7 +75,7 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: false }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool") { @@ -96,7 +96,7 @@ describe("buildChatJsonlForSharing", () => { }); it("leaves messages unchanged when includeToolOutput=true", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -123,13 +123,13 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: true }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; expect(parsed).toEqual(messages[0]); }); it("inlines planContent into propose_plan tool output when planSnapshot matches", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -153,7 +153,7 @@ describe("buildChatJsonlForSharing", () => { planSnapshot, }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool" || part.state !== "output-available") { @@ -182,7 +182,7 @@ describe("buildChatJsonlForSharing", () => { }); it("inlines planContent even when propose_plan planPath uses ~ but planSnapshot.path is resolved", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -213,7 +213,7 @@ describe("buildChatJsonlForSharing", () => { planSnapshot, }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool" || part.state !== "output-available") { @@ -229,7 +229,7 @@ describe("buildChatJsonlForSharing", () => { }); it("inlines planContent even when propose_plan planPath uses Windows-style slashes", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -260,7 +260,7 @@ describe("buildChatJsonlForSharing", () => { planSnapshot, }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool" || part.state !== "output-available") { @@ -276,7 +276,7 @@ describe("buildChatJsonlForSharing", () => { }); it("inlines planContent even when planSnapshot.path differs from propose_plan planPath", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -303,7 +303,7 @@ describe("buildChatJsonlForSharing", () => { planSnapshot, }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool" || part.state !== "output-available") { @@ -319,7 +319,7 @@ describe("buildChatJsonlForSharing", () => { }); it("preserves propose_plan output (with planContent) while stripping other tool outputs when includeToolOutput=false", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -351,7 +351,7 @@ describe("buildChatJsonlForSharing", () => { planSnapshot, }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const planPart = parsed.parts[0]; if (planPart.type !== "dynamic-tool" || planPart.state !== "output-available") { @@ -395,7 +395,7 @@ describe("buildChatJsonlForSharing", () => { expect(originalStrippedPart).toHaveProperty("output"); }); it("preserves task tool outputs while stripping other tool outputs when includeToolOutput=false", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -428,7 +428,7 @@ describe("buildChatJsonlForSharing", () => { includeToolOutput: false, }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; // task output should be preserved const taskPart = parsed.parts[0]; @@ -456,7 +456,7 @@ describe("buildChatJsonlForSharing", () => { }); it("does not overwrite propose_plan planContent when already present", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -483,7 +483,7 @@ describe("buildChatJsonlForSharing", () => { planSnapshot: { path: "/tmp/plan.md", content: "# New Plan" }, }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool" || part.state !== "output-available") { @@ -501,7 +501,7 @@ describe("buildChatJsonlForSharing", () => { }); it("injects workspaceId into each message when provided", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -510,10 +510,10 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { workspaceId: "ws-123" }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage & { workspaceId?: string }; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage & { workspaceId?: string }; expect(parsed.workspaceId).toBe("ws-123"); - expect((messages[0] as MuxMessage & { workspaceId?: string }).workspaceId).toBeUndefined(); + expect((messages[0] as XumMessage & { workspaceId?: string }).workspaceId).toBeUndefined(); }); it("returns empty string for empty messages array", () => { @@ -521,7 +521,7 @@ describe("buildChatJsonlForSharing", () => { }); it("merges adjacent text/reasoning parts to keep transcripts small", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -543,7 +543,7 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: true }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; expect(parsed.parts).toEqual([ { type: "reasoning", text: "ab", timestamp: 1 }, @@ -563,7 +563,7 @@ describe("buildChatJsonlForSharing", () => { }); it("produces valid JSONL (each line parses, trailing newline)", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -582,12 +582,12 @@ describe("buildChatJsonlForSharing", () => { const lines = splitJsonlLines(jsonl); expect(lines).toHaveLength(messages.length); - const parsed = lines.map((line) => JSON.parse(line) as MuxMessage); + const parsed = lines.map((line) => JSON.parse(line) as XumMessage); expect(parsed).toEqual(messages); }); it("sets failed: true on stripped tool part when output indicates failure", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -605,7 +605,7 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: false }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool") { @@ -619,7 +619,7 @@ describe("buildChatJsonlForSharing", () => { }); it("does NOT set failed on stripped tool part when output indicates success", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -637,7 +637,7 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: false }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool") { @@ -650,7 +650,7 @@ describe("buildChatJsonlForSharing", () => { }); it("sets failed: true on stripped nested call when output indicates failure", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -676,7 +676,7 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: false }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool") { @@ -690,7 +690,7 @@ describe("buildChatJsonlForSharing", () => { }); it("does NOT set failed on stripped nested call when output indicates success", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "assistant-1", role: "assistant", @@ -716,7 +716,7 @@ describe("buildChatJsonlForSharing", () => { ]; const jsonl = buildChatJsonlForSharing(messages, { includeToolOutput: false }); - const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as MuxMessage; + const parsed = JSON.parse(splitJsonlLines(jsonl)[0]) as XumMessage; const part = parsed.parts[0]; if (part.type !== "dynamic-tool") { diff --git a/src/common/utils/messages/transcriptShare.ts b/src/common/utils/messages/transcriptShare.ts index 2ea8b36916..2129a14f54 100644 --- a/src/common/utils/messages/transcriptShare.ts +++ b/src/common/utils/messages/transcriptShare.ts @@ -1,4 +1,4 @@ -import type { MuxMessage, MuxToolPart } from "@/common/types/message"; +import type { XumMessage, XumToolPart } from "@/common/types/message"; import type { NestedToolCall } from "@/common/orpc/schemas/message"; function isFailedOutput(output: unknown): boolean { @@ -17,7 +17,7 @@ export interface BuildChatJsonlForSharingOptions { planSnapshot?: { path: string; content: string }; } -interface ChatJsonlEntry extends MuxMessage { +interface ChatJsonlEntry extends XumMessage { workspaceId?: string; } @@ -29,11 +29,11 @@ interface ChatJsonlEntry extends MuxMessage { * reduce file size. */ function mergeAdjacentTextAndReasoningPartsForSharing( - parts: MuxMessage["parts"] -): MuxMessage["parts"] { + parts: XumMessage["parts"] +): XumMessage["parts"] { if (parts.length <= 1) return parts; - const merged: MuxMessage["parts"] = []; + const merged: XumMessage["parts"] = []; let pendingTexts: string[] = []; let pendingTextTimestamp: number | undefined; let pendingReasonings: string[] = []; @@ -94,7 +94,7 @@ function mergeAdjacentTextAndReasoningPartsForSharing( return merged; } -function compactMessagePartsForSharing(messages: MuxMessage[]): MuxMessage[] { +function compactMessagePartsForSharing(messages: XumMessage[]): XumMessage[] { return messages.map((msg) => { const parts = mergeAdjacentTextAndReasoningPartsForSharing(msg.parts); if (parts === msg.parts) { @@ -135,7 +135,7 @@ const PRESERVE_OUTPUT_TOOLS = new Set([ "task_apply_git_patch", ]); -function stripToolPartOutput(part: MuxToolPart): MuxToolPart { +function stripToolPartOutput(part: XumToolPart): XumToolPart { const nestedCalls = part.nestedCalls?.map(stripNestedToolCallOutput); if (PRESERVE_OUTPUT_TOOLS.has(part.toolName)) { @@ -156,7 +156,7 @@ function stripToolPartOutput(part: MuxToolPart): MuxToolPart { }; } -function stripToolOutputsForSharing(messages: MuxMessage[]): MuxMessage[] { +function stripToolOutputsForSharing(messages: XumMessage[]): XumMessage[] { return messages.map((msg) => { if (msg.role !== "assistant") { return msg; @@ -181,9 +181,9 @@ function isRecord(value: unknown): value is Record { } function inlinePlanContentForSharing( - messages: MuxMessage[], + messages: XumMessage[], planSnapshot: { path: string; content: string } -): MuxMessage[] { +): XumMessage[] { return messages.map((msg) => { if (msg.role !== "assistant") { return msg; @@ -242,7 +242,7 @@ function inlinePlanContentForSharing( * compacts adjacent text/reasoning deltas into a single part each to keep shared transcripts small. */ export function buildChatJsonlForSharing( - messages: MuxMessage[], + messages: XumMessage[], options: BuildChatJsonlForSharingOptions = {} ): string { if (messages.length === 0) return ""; diff --git a/src/common/utils/recency.ts b/src/common/utils/recency.ts index 07007d981e..9d13220a0f 100644 --- a/src/common/utils/recency.ts +++ b/src/common/utils/recency.ts @@ -1,11 +1,11 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; /** * Check if a message is an idle compaction request. * Used to exclude these from recency calculation since they shouldn't affect * when the workspace was "last used" by the user. */ -function isIdleCompactionRequest(msg: MuxMessage): boolean { +function isIdleCompactionRequest(msg: XumMessage): boolean { const muxMeta = msg.metadata?.muxMetadata; return muxMeta?.type === "compaction-request" && muxMeta?.source === "idle-compaction"; } @@ -21,7 +21,7 @@ function isIdleCompactionRequest(msg: MuxMessage): boolean { * @param unarchivedAt - When workspace was last unarchived (bumps to top of recency) */ export function computeRecencyFromMessages( - messages: MuxMessage[], + messages: XumMessage[], createdAt?: number, unarchivedAt?: number ): number | null { diff --git a/src/common/utils/tokens/tokenStatsCalculator.test.ts b/src/common/utils/tokens/tokenStatsCalculator.test.ts index 99dead8636..0f1badb930 100644 --- a/src/common/utils/tokens/tokenStatsCalculator.test.ts +++ b/src/common/utils/tokens/tokenStatsCalculator.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; import { collectUniqueToolNames, @@ -194,7 +194,7 @@ describe("countEncryptedWebSearchTokens", () => { describe("collectUniqueToolNames", () => { test("collects tool names from assistant messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "1", role: "assistant", @@ -224,7 +224,7 @@ describe("collectUniqueToolNames", () => { }); test("deduplicates tool names", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "1", role: "assistant", @@ -253,7 +253,7 @@ describe("collectUniqueToolNames", () => { }); test("ignores user messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "1", role: "user", @@ -273,7 +273,7 @@ describe("collectUniqueToolNames", () => { describe("extractSyncMetadata", () => { test("accumulates system message tokens", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "1", role: "assistant", @@ -293,7 +293,7 @@ describe("extractSyncMetadata", () => { }); test("extracts usage history", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "1", role: "assistant", @@ -316,7 +316,7 @@ describe("extractSyncMetadata", () => { }); test("ignores user messages", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "1", role: "user", @@ -331,7 +331,7 @@ describe("extractSyncMetadata", () => { }); test("resolves mapped metadata model for usage history costs", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "1", role: "assistant", diff --git a/src/common/utils/tokens/tokenStatsCalculator.ts b/src/common/utils/tokens/tokenStatsCalculator.ts index 37dcb43fdc..b1e60cf4d1 100644 --- a/src/common/utils/tokens/tokenStatsCalculator.ts +++ b/src/common/utils/tokens/tokenStatsCalculator.ts @@ -6,7 +6,7 @@ * For renderer-safe usage utilities, use displayUsage.ts instead. */ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { ProvidersConfigMap } from "@/common/orpc/types"; import { extractToolFilePath } from "@/common/utils/tools/toolInputFilePath"; import type { ChatStats, TokenConsumer } from "@/common/types/chatStats"; @@ -163,7 +163,7 @@ export interface TokenCountJob { * Creates all token counting jobs from messages * Jobs are executed immediately (promises start running) */ -function createTokenCountingJobs(messages: MuxMessage[], tokenizer: Tokenizer): TokenCountJob[] { +function createTokenCountingJobs(messages: XumMessage[], tokenizer: Tokenizer): TokenCountJob[] { const jobs: TokenCountJob[] = []; for (const message of messages) { @@ -230,7 +230,7 @@ function createTokenCountingJobs(messages: MuxMessage[], tokenizer: Tokenizer): /** * Collects all unique tool names from messages */ -export function collectUniqueToolNames(messages: MuxMessage[]): Set { +export function collectUniqueToolNames(messages: XumMessage[]): Set { const toolNames = new Set(); for (const message of messages) { @@ -283,7 +283,7 @@ interface SyncMetadata { * Extracts synchronous metadata from messages (no token counting needed) */ export function extractSyncMetadata( - messages: MuxMessage[], + messages: XumMessage[], model: string, providersConfig: ProvidersConfigMap | null = null ): SyncMetadata { @@ -402,7 +402,7 @@ export function mergeResults( * @returns ChatStats with token breakdown by consumer and usage history */ export async function calculateTokenStats( - messages: MuxMessage[], + messages: XumMessage[], model: string, providersConfig: ProvidersConfigMap | null, availableToolsOptions: Parameters[3] diff --git a/src/common/utils/tools/toolCatalog.test.ts b/src/common/utils/tools/toolCatalog.test.ts index df63b79219..d09db381c3 100644 --- a/src/common/utils/tools/toolCatalog.test.ts +++ b/src/common/utils/tools/toolCatalog.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import type { Tool } from "ai"; -import type { ModelMessage, MuxMessage } from "@/common/types/message"; +import type { ModelMessage, XumMessage } from "@/common/types/message"; import type { ToolPolicy } from "@/common/utils/tools/toolPolicy"; import { buildToolCatalog, @@ -680,17 +680,17 @@ describe("extractPreActivatedToolNames", () => { expect(names.size).toBe(0); }); - test("reads MuxMessage dynamic-tool parts (pre-conversion seeding path)", () => { + test("reads XumMessage dynamic-tool parts (pre-conversion seeding path)", () => { // aiService seeds from XumMessages (before Xum→Model conversion) so the // agent-transition sentinel can include pre-activated tools. - const muxMessage = (toolName: string, state: string, output?: unknown): MuxMessage => { + const muxMessage = (toolName: string, state: string, output?: unknown): XumMessage => { const raw: unknown = { id: "m1", role: "assistant", metadata: {}, parts: [{ type: "dynamic-tool", toolCallId: "call-1", toolName, input: {}, state, output }], }; - return raw as MuxMessage; + return raw as XumMessage; }; const names = extractPreActivatedToolNames([ muxMessage("tool_catalog_search", "output-available", matchesResult), diff --git a/src/common/utils/tools/toolCatalog.ts b/src/common/utils/tools/toolCatalog.ts index a0af8a8eee..0eb449412c 100644 --- a/src/common/utils/tools/toolCatalog.ts +++ b/src/common/utils/tools/toolCatalog.ts @@ -19,7 +19,7 @@ import type { ToolModelMessage, ToolResultPart, } from "ai"; -import type { ModelMessage, MuxMessage } from "@/common/types/message"; +import type { ModelMessage, XumMessage } from "@/common/types/message"; import { buildRequiredToolPatterns, type ToolPolicy } from "@/common/utils/tools/toolPolicy"; export const TOOL_SEARCH_TOOL_NAME = "tool_catalog_search"; @@ -596,7 +596,7 @@ function renameLegacyToolSearchResultPart(part: ToolResultPart): ToolResultPart * conversion runs. Callers intersect the result with the current deferred set. */ export function extractPreActivatedToolNames( - messages: ReadonlyArray + messages: ReadonlyArray ): Set { const names = new Set(); for (const message of messages) { @@ -692,7 +692,7 @@ export function normalizeLegacyToolSearchMessages(messages: ModelMessage[]): Mod */ export function seedToolSearchActivationsFromMessages( state: ToolSearchStreamState, - messages: ReadonlyArray + messages: ReadonlyArray ): void { for (const name of extractPreActivatedToolNames(messages)) { if (state.deferredToolNames.has(name)) { diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts index 0eb4d14099..f9de75e304 100644 --- a/src/common/utils/tools/tools.ts +++ b/src/common/utils/tools/tools.ts @@ -1,7 +1,7 @@ import { xai } from "@ai-sdk/xai"; import { type LanguageModel, type Tool } from "ai"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import { isGrokFrontierModel } from "@/common/types/thinking"; import type { BackgroundWorkAttentionPolicy } from "@/common/types/backgroundWorkAttention"; import { cloneToolPreservingDescriptors } from "@/common/utils/tools/cloneToolPreservingDescriptors"; @@ -156,7 +156,7 @@ export interface ToolConfiguration { /** Whether the resolved route supports xAI Responses-native tools. */ xaiNativeToolsEnabled?: boolean; /** Legacy xAI Live Search settings translated to Responses native search tools. */ - xaiSearchParameters?: NonNullable["searchParameters"]>; + xaiSearchParameters?: NonNullable["searchParameters"]>; /** Overflow policy for bash tool output (optional, not exposed to AI) */ overflow_policy?: "truncate" | "tmpfile"; /** Background process manager for bash tool (optional, AI-only) */ diff --git a/src/common/utils/workflowRunMessages.ts b/src/common/utils/workflowRunMessages.ts index 9fc393e9f7..44639e20ca 100644 --- a/src/common/utils/workflowRunMessages.ts +++ b/src/common/utils/workflowRunMessages.ts @@ -1,4 +1,4 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { WorkflowRunRecord } from "@/common/types/workflow"; import assert from "@/common/utils/assert"; @@ -205,25 +205,25 @@ export interface WorkflowRunCardResult { run?: WorkflowRunRecord; } -type WorkflowRunToolPart = Extract; +type WorkflowRunToolPart = Extract; -export function isWorkflowTriggerDisplayMessage(message: MuxMessage): boolean { +export function isWorkflowTriggerDisplayMessage(message: XumMessage): boolean { return message.metadata?.muxMetadata?.type === WORKFLOW_TRIGGER_DISPLAY_METADATA_TYPE; } -export function isWorkflowRunCardDisplayMessage(message: MuxMessage): boolean { +export function isWorkflowRunCardDisplayMessage(message: XumMessage): boolean { return message.metadata?.muxMetadata?.type === WORKFLOW_RUN_CARD_DISPLAY_METADATA_TYPE; } -export function isWorkflowResultMessage(message: Pick): boolean { +export function isWorkflowResultMessage(message: Pick): boolean { return message.metadata?.muxMetadata?.type === WORKFLOW_RESULT_METADATA_TYPE; } -export function isWorkflowDisplayOnlyMessage(message: MuxMessage): boolean { +export function isWorkflowDisplayOnlyMessage(message: XumMessage): boolean { return isWorkflowTriggerDisplayMessage(message) || isWorkflowRunCardDisplayMessage(message); } -export function filterWorkflowDisplayOnlyMessages(messages: MuxMessage[]): MuxMessage[] { +export function filterWorkflowDisplayOnlyMessages(messages: XumMessage[]): XumMessage[] { if (!messages.some(isWorkflowDisplayOnlyMessage)) { return messages; } @@ -268,7 +268,7 @@ export function buildWorkflowRunCardMessage( input: WorkflowRunCardInput, result: WorkflowRunCardResult, now = Date.now() -): MuxMessage { +): XumMessage { const toolPart = buildWorkflowRunToolPart(input, result, now); return { id: toolPart.toolCallId, diff --git a/src/node/acp/adapter.ts b/src/node/acp/adapter.ts index b9a7994f3a..974f4676f2 100644 --- a/src/node/acp/adapter.ts +++ b/src/node/acp/adapter.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { Readable, Writable } from "node:stream"; import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; -import { MuxAgent } from "./agent"; +import { XumAgent } from "./agent"; import type { ServerConnection } from "./serverConnection"; /** @@ -35,7 +35,7 @@ export async function runAcpAdapter(server: ServerConnection): Promise { let waitForDisconnectCleanup: () => Promise = () => Promise.resolve(); const connection = new AgentSideConnection((conn) => { - const createdAgent = new MuxAgent(conn, server); + const createdAgent = new XumAgent(conn, server); waitForDisconnectCleanup = () => createdAgent.waitForDisconnectCleanup(); return createdAgent; }, stream); diff --git a/src/node/acp/agent.ts b/src/node/acp/agent.ts index c467b73b92..074aa23d4d 100644 --- a/src/node/acp/agent.ts +++ b/src/node/acp/agent.ts @@ -102,7 +102,7 @@ const DEFAULT_TURN_CORRELATION_TIMEOUT_MS = 30_000; const MAX_BUFFERED_CHAT_EVENTS = 5_000; -interface MuxAgentOptions { +interface XumAgentOptions { disconnectCleanupMaxWaitMs?: number; sessionIdleTtlMs?: number; maxTrackedSessions?: number; @@ -148,7 +148,7 @@ interface TurnCompletion { messageId?: string; } -interface ParsedMuxMeta { +interface ParsedXumMeta { projectPath?: string; branchName?: string; trunkBranch?: string; @@ -167,7 +167,7 @@ type WorkspaceActivityById = Awaited< ReturnType >; -export class MuxAgent implements Agent { +export class XumAgent implements Agent { private readonly sessionManager = new SessionManager(); private readonly streamTranslator: StreamTranslator; private readonly toolRouter: ToolRouter; @@ -216,31 +216,31 @@ export class MuxAgent implements Agent { constructor( private readonly connection: AgentSideConnection, private readonly server: ServerConnection, - options?: MuxAgentOptions + options?: XumAgentOptions ) { - assert(connection != null, "MuxAgent: connection is required"); - assert(server != null, "MuxAgent: server connection is required"); + assert(connection != null, "XumAgent: connection is required"); + assert(server != null, "XumAgent: server connection is required"); const configuredDisconnectCleanupMaxWaitMs = options?.disconnectCleanupMaxWaitMs; assert( configuredDisconnectCleanupMaxWaitMs == null || (Number.isFinite(configuredDisconnectCleanupMaxWaitMs) && configuredDisconnectCleanupMaxWaitMs >= 0), - "MuxAgent: disconnectCleanupMaxWaitMs must be a finite non-negative number" + "XumAgent: disconnectCleanupMaxWaitMs must be a finite non-negative number" ); const configuredSessionIdleTtlMs = options?.sessionIdleTtlMs; assert( configuredSessionIdleTtlMs == null || (Number.isFinite(configuredSessionIdleTtlMs) && configuredSessionIdleTtlMs > 0), - "MuxAgent: sessionIdleTtlMs must be a finite positive number" + "XumAgent: sessionIdleTtlMs must be a finite positive number" ); const configuredMaxTrackedSessions = options?.maxTrackedSessions; assert( configuredMaxTrackedSessions == null || (Number.isInteger(configuredMaxTrackedSessions) && configuredMaxTrackedSessions > 0), - "MuxAgent: maxTrackedSessions must be a positive integer" + "XumAgent: maxTrackedSessions must be a positive integer" ); const configuredTurnCorrelationTimeoutMs = options?.turnCorrelationTimeoutMs; @@ -248,7 +248,7 @@ export class MuxAgent implements Agent { configuredTurnCorrelationTimeoutMs == null || (Number.isFinite(configuredTurnCorrelationTimeoutMs) && configuredTurnCorrelationTimeoutMs > 0), - "MuxAgent: turnCorrelationTimeoutMs must be a finite positive number" + "XumAgent: turnCorrelationTimeoutMs must be a finite positive number" ); this.disconnectCleanupMaxWaitMs = @@ -320,7 +320,7 @@ export class MuxAgent implements Agent { this.inFlightNewSessionCount += 1; try { - const meta = parseMuxMeta(params._meta); + const meta = parseXumMeta(params._meta); const requestedProjectPath = await resolveAcpNewSessionProjectPath( params.cwd, meta.projectPath @@ -522,7 +522,7 @@ export class MuxAgent implements Agent { async unstable_forkSession(params: ForkSessionRequest): Promise { this.assertInitialized("unstable_forkSession"); - const meta = parseMuxMeta(params._meta); + const meta = parseXumMeta(params._meta); const sourceWorkspaceId = this.sessionManager.getWorkspaceId(params.sessionId); const sourceWorkspace = await this.server.client.workspace.getInfo({ workspaceId: sourceWorkspaceId, @@ -767,9 +767,9 @@ export class MuxAgent implements Agent { "attachPromptCorrelationToSendOptions: promptCorrelationId must be non-empty" ); - const existingMuxMetadata = isRecord(options.muxMetadata) ? options.muxMetadata : {}; + const existingXumMetadata = isRecord(options.muxMetadata) ? options.muxMetadata : {}; const muxMetadata: Record = { - ...existingMuxMetadata, + ...existingXumMetadata, [ACP_PROMPT_CORRELATION_MUX_METADATA_KEY]: promptCorrelationId, }; @@ -2492,8 +2492,8 @@ function toSessionRecencyTimestamp( return Number.isFinite(createdAtMs) ? createdAtMs : 0; } -function parseMuxMeta(rawMeta: MetaRecord | null | undefined): ParsedMuxMeta { - const source = getMuxMetaSource(rawMeta); +function parseXumMeta(rawMeta: MetaRecord | null | undefined): ParsedXumMeta { + const source = getXumMetaSource(rawMeta); return { projectPath: readOptionalString(source, "projectPath"), @@ -2509,14 +2509,15 @@ function parseMuxMeta(rawMeta: MetaRecord | null | undefined): ParsedMuxMeta { }; } -function getMuxMetaSource(rawMeta: MetaRecord | null | undefined): MetaRecord { +function getXumMetaSource(rawMeta: MetaRecord | null | undefined): MetaRecord { if (!isRecord(rawMeta)) { return {}; } - const nestedMuxMeta = rawMeta.mux; - if (isRecord(nestedMuxMeta)) { - return nestedMuxMeta; + // ACP clients already send this protocol marker, so the wire key remains `mux`. + const nestedXumMeta = rawMeta.mux; + if (isRecord(nestedXumMeta)) { + return nestedXumMeta; } return rawMeta; diff --git a/src/node/acp/streamTranslator.test.ts b/src/node/acp/streamTranslator.test.ts index 453d5b3406..d9466b62a8 100644 --- a/src/node/acp/streamTranslator.test.ts +++ b/src/node/acp/streamTranslator.test.ts @@ -1,7 +1,7 @@ import { describe, expect, mock, test } from "bun:test"; import type { AgentSideConnection } from "@agentclientprotocol/sdk"; import { StreamTranslator } from "@/node/acp/streamTranslator"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { WorkspaceChatMessage } from "@/common/orpc/types"; async function replayThrough(events: WorkspaceChatMessage[]): Promise { @@ -16,7 +16,7 @@ async function replayThrough(events: WorkspaceChatMessage[]): Promise describe("StreamTranslator MCP prompt replay", () => { test("replays the authored slash command instead of the transformed prompt text", async () => { - const userMessage = createMuxMessage("user-1", "user", "Using MCP prompt coder/review: src", { + const userMessage = createXumMessage("user-1", "user", "Using MCP prompt coder/review: src", { muxMetadata: { type: "normal", rawCommand: "/mcp__coder__review src", @@ -47,7 +47,7 @@ describe("StreamTranslator MCP prompt replay", () => { }); test("suppresses synthetic MCP prompt snapshot rows", async () => { - const snapshotMessage = createMuxMessage("mcp-prompt-snapshot-1", "user", "Expanded prompt", { + const snapshotMessage = createXumMessage("mcp-prompt-snapshot-1", "user", "Expanded prompt", { synthetic: true, mcpPromptSnapshot: { serverName: "coder", diff --git a/src/node/acp/streamTranslator.ts b/src/node/acp/streamTranslator.ts index 96d0a0e000..3bff5e3bad 100644 --- a/src/node/acp/streamTranslator.ts +++ b/src/node/acp/streamTranslator.ts @@ -683,9 +683,9 @@ function extractRawCommand(metadata: MessageMetadataWithFrontendFields | undefin return null; } - const fromMuxMetadata = extractRawCommandFromFrontendMetadata(metadata.muxMetadata); - if (fromMuxMetadata != null) { - return fromMuxMetadata; + const fromXumMetadata = extractRawCommandFromFrontendMetadata(metadata.muxMetadata); + if (fromXumMetadata != null) { + return fromXumMetadata; } return extractRawCommandFromFrontendMetadata(metadata.cmuxMetadata); diff --git a/src/node/orpc/context.ts b/src/node/orpc/context.ts index e89668211d..9222ab941e 100644 --- a/src/node/orpc/context.ts +++ b/src/node/orpc/context.ts @@ -4,8 +4,8 @@ import type { Config } from "@/node/config"; import type { AIService } from "@/node/services/aiService"; import type { ProjectService } from "@/node/services/projectService"; import type { WorkspaceService } from "@/node/services/workspaceService"; -import type { MuxGatewayOauthService } from "@/node/services/muxGatewayOauthService"; -import type { MuxGovernorOauthService } from "@/node/services/muxGovernorOauthService"; +import type { XumGatewayOauthService } from "@/node/services/xumGatewayOauthService"; +import type { XumGovernorOauthService } from "@/node/services/xumGovernorOauthService"; import type { CodexOauthService } from "@/node/services/codexOauthService"; import type { CoderOauthService } from "@/node/services/coderOauthService"; import type { CopilotOauthService } from "@/node/services/copilotOauthService"; @@ -56,8 +56,8 @@ export interface ORPCContext { workspaceService: WorkspaceService; taskService: TaskService; providerService: ProviderService; - muxGatewayOauthService: MuxGatewayOauthService; - muxGovernorOauthService: MuxGovernorOauthService; + xumGatewayOauthService: XumGatewayOauthService; + xumGovernorOauthService: XumGovernorOauthService; codexOauthService: CodexOauthService; coderOauthService: CoderOauthService; copilotOauthService: CopilotOauthService; diff --git a/src/node/orpc/router.ts b/src/node/orpc/router.ts index d2b4d3f2ce..ddc4ee7150 100644 --- a/src/node/orpc/router.ts +++ b/src/node/orpc/router.ts @@ -109,9 +109,9 @@ import * as path from "node:path"; import type { DevToolsEvent } from "@/common/types/devtools"; import type { WorkflowRunStreamEvent } from "@/common/types/workflow"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { coerceThinkingLevel } from "@/common/types/thinking"; -import { normalizeLegacyMuxMetadata } from "@/node/utils/messages/legacy"; +import { normalizeLegacyXumMetadata } from "@/node/utils/messages/legacy"; import { log } from "@/node/services/log"; import { BROWSER_BRIDGE_WS_PATH, DESKTOP_WS_PATH } from "@/node/orpc/wsPaths"; import { SERVER_AUTH_SESSION_COOKIE_NAME } from "@/node/services/serverAuthService"; @@ -613,7 +613,7 @@ function mergeTaskSettingsForConfigSave(current: unknown, input: unknown) { return normalizeTaskSettings({ ...normalizeTaskSettings(current), ...definedInput }); } -function normalizeMuxMessageFromDisk(value: unknown): MuxMessage | null { +function normalizeXumMessageFromDisk(value: unknown): XumMessage | null { if (!value || typeof value !== "object") { return null; } @@ -629,22 +629,22 @@ function normalizeMuxMessageFromDisk(value: unknown): MuxMessage | null { } } - return normalizeLegacyMuxMetadata(value as MuxMessage); + return normalizeLegacyXumMetadata(value as XumMessage); } async function readChatJsonlAllowMissing(params: { chatPath: string; logLabel: string; -}): Promise { +}): Promise { try { const data = await fsPromises.readFile(params.chatPath, "utf-8"); const lines = data.split("\n").filter((line) => line.trim()); - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; for (let i = 0; i < lines.length; i++) { try { const parsed = JSON.parse(lines[i]) as unknown; - const message = normalizeMuxMessageFromDisk(parsed); + const message = normalizeXumMessageFromDisk(parsed); if (message) { messages.push(message); } @@ -668,11 +668,11 @@ async function readChatJsonlAllowMissing(params: { } } -async function readPartialJsonBestEffort(partialPath: string): Promise { +async function readPartialJsonBestEffort(partialPath: string): Promise { try { const raw = await fsPromises.readFile(partialPath, "utf-8"); const parsed = JSON.parse(raw) as unknown; - return normalizeMuxMessageFromDisk(parsed); + return normalizeXumMessageFromDisk(parsed); } catch (error: unknown) { if (isErrnoWithCode(error, "ENOENT")) { return null; @@ -687,7 +687,7 @@ async function readPartialJsonBestEffort(partialPath: string): Promise { }; }); }), - updateMuxGatewayPrefs: t - .input(schemas.config.updateMuxGatewayPrefs.input) - .output(schemas.config.updateMuxGatewayPrefs.output) + updateXumGatewayPrefs: t + .input(schemas.config.updateXumGatewayPrefs.input) + .output(schemas.config.updateXumGatewayPrefs.output) .handler(async ({ context, input }) => { await context.config.editConfig((config) => { const nextModels = Array.from(new Set(input.muxGatewayModels)); @@ -1555,9 +1555,9 @@ export const router = (authToken?: string) => { return config; }); }), - unenrollMuxGovernor: t - .input(schemas.config.unenrollMuxGovernor.input) - .output(schemas.config.unenrollMuxGovernor.output) + unenrollXumGovernor: t + .input(schemas.config.unenrollXumGovernor.input) + .output(schemas.config.unenrollXumGovernor.output) .handler(async ({ context }) => { await context.config.editConfig((config) => { const { muxGovernorUrl: _url, muxGovernorToken: _token, ...rest } = config; @@ -2546,13 +2546,13 @@ export const router = (authToken?: string) => { .input(schemas.muxGatewayOauth.startDesktopFlow.input) .output(schemas.muxGatewayOauth.startDesktopFlow.output) .handler(({ context }) => { - return context.muxGatewayOauthService.startDesktopFlow(); + return context.xumGatewayOauthService.startDesktopFlow(); }), waitForDesktopFlow: t .input(schemas.muxGatewayOauth.waitForDesktopFlow.input) .output(schemas.muxGatewayOauth.waitForDesktopFlow.output) .handler(({ context, input }) => { - return context.muxGatewayOauthService.waitForDesktopFlow(input.flowId, { + return context.xumGatewayOauthService.waitForDesktopFlow(input.flowId, { timeoutMs: input.timeoutMs, }); }), @@ -2560,7 +2560,7 @@ export const router = (authToken?: string) => { .input(schemas.muxGatewayOauth.cancelDesktopFlow.input) .output(schemas.muxGatewayOauth.cancelDesktopFlow.output) .handler(async ({ context, input }) => { - await context.muxGatewayOauthService.cancelDesktopFlow(input.flowId); + await context.xumGatewayOauthService.cancelDesktopFlow(input.flowId); }), }, copilotOauth: { @@ -2590,7 +2590,7 @@ export const router = (authToken?: string) => { .input(schemas.muxGovernorOauth.startDesktopFlow.input) .output(schemas.muxGovernorOauth.startDesktopFlow.output) .handler(({ context, input }) => { - return context.muxGovernorOauthService.startDesktopFlow({ + return context.xumGovernorOauthService.startDesktopFlow({ governorOrigin: input.governorOrigin, }); }), @@ -2598,7 +2598,7 @@ export const router = (authToken?: string) => { .input(schemas.muxGovernorOauth.waitForDesktopFlow.input) .output(schemas.muxGovernorOauth.waitForDesktopFlow.output) .handler(({ context, input }) => { - return context.muxGovernorOauthService.waitForDesktopFlow(input.flowId, { + return context.xumGovernorOauthService.waitForDesktopFlow(input.flowId, { timeoutMs: input.timeoutMs, }); }), @@ -2606,7 +2606,7 @@ export const router = (authToken?: string) => { .input(schemas.muxGovernorOauth.cancelDesktopFlow.input) .output(schemas.muxGovernorOauth.cancelDesktopFlow.output) .handler(async ({ context, input }) => { - await context.muxGovernorOauthService.cancelDesktopFlow(input.flowId); + await context.xumGovernorOauthService.cancelDesktopFlow(input.flowId); }), }, codexOauth: { @@ -4768,7 +4768,7 @@ export const router = (authToken?: string) => { chatArchivePath?: string; partialPath?: string; logLabel: string; - }): Promise => { + }): Promise => { const workspaceSessionDir = context.config.getSessionDir(params.workspaceId); // Defense-in-depth: refuse path traversal from a corrupted index file. diff --git a/src/node/orpc/server.test.ts b/src/node/orpc/server.test.ts index f6aa908bb8..e7261c436a 100644 --- a/src/node/orpc/server.test.ts +++ b/src/node/orpc/server.test.ts @@ -531,13 +531,13 @@ describe("createOrpcServer", () => { test("includes app-proxy base paths in OAuth redirect and callback return URLs", async () => { let muxGatewayRedirectUri = ""; const stubContext: Partial = { - muxGatewayOauthService: { + xumGatewayOauthService: { startServerFlow: (input: { redirectUri: string }) => { muxGatewayRedirectUri = input.redirectUri; return { authorizeUrl: "https://gateway.example.com/auth", state: "state-gateway" }; }, handleServerCallbackAndExchange: () => Promise.resolve({ success: true, data: undefined }), - } as unknown as ORPCContext["muxGatewayOauthService"], + } as unknown as ORPCContext["xumGatewayOauthService"], }; let server: Awaited> | null = null; @@ -782,13 +782,13 @@ describe("createOrpcServer", () => { let muxGovernorRedirectUri = ""; const stubContext: Partial = { - muxGatewayOauthService: { + xumGatewayOauthService: { startServerFlow: (input: { redirectUri: string }) => { muxGatewayRedirectUri = input.redirectUri; return { authorizeUrl: "https://gateway.example.com/auth", state: "state-gateway" }; }, - } as unknown as ORPCContext["muxGatewayOauthService"], - muxGovernorOauthService: { + } as unknown as ORPCContext["xumGatewayOauthService"], + xumGovernorOauthService: { startServerFlow: (input: { governorOrigin: string; redirectUri: string }) => { muxGovernorRedirectUri = input.redirectUri; return { @@ -796,7 +796,7 @@ describe("createOrpcServer", () => { data: { authorizeUrl: "https://governor.example.com/auth", state: "state-governor" }, }; }, - } as unknown as ORPCContext["muxGovernorOauthService"], + } as unknown as ORPCContext["xumGovernorOauthService"], }; let server: Awaited> | null = null; @@ -845,12 +845,12 @@ describe("createOrpcServer", () => { let muxGatewayRedirectUri = ""; const stubContext: Partial = { - muxGatewayOauthService: { + xumGatewayOauthService: { startServerFlow: (input: { redirectUri: string }) => { muxGatewayRedirectUri = input.redirectUri; return { authorizeUrl: "https://gateway.example.com/auth", state: "state-gateway-http" }; }, - } as unknown as ORPCContext["muxGatewayOauthService"], + } as unknown as ORPCContext["xumGatewayOauthService"], }; let server: Awaited> | null = null; @@ -1115,9 +1115,9 @@ describe("createOrpcServer", () => { test("OAuth callback routes accept POST redirects (query + form_post)", async () => { const stubContext: Partial = { - muxGovernorOauthService: { + xumGovernorOauthService: { handleServerCallbackAndExchange: () => Promise.resolve({ success: true, data: undefined }), - } as unknown as ORPCContext["muxGovernorOauthService"], + } as unknown as ORPCContext["xumGovernorOauthService"], }; let server: Awaited> | null = null; @@ -1155,12 +1155,12 @@ describe("createOrpcServer", () => { test("allows cross-origin POST requests on OAuth callback routes", async () => { const handleSuccessfulCallback = () => Promise.resolve({ success: true, data: undefined }); const stubContext: Partial = { - muxGatewayOauthService: { + xumGatewayOauthService: { handleServerCallbackAndExchange: handleSuccessfulCallback, - } as unknown as ORPCContext["muxGatewayOauthService"], - muxGovernorOauthService: { + } as unknown as ORPCContext["xumGatewayOauthService"], + xumGovernorOauthService: { handleServerCallbackAndExchange: handleSuccessfulCallback, - } as unknown as ORPCContext["muxGovernorOauthService"], + } as unknown as ORPCContext["xumGovernorOauthService"], mcpOauthService: { handleServerCallbackAndExchange: handleSuccessfulCallback, } as unknown as ORPCContext["mcpOauthService"], diff --git a/src/node/orpc/server.ts b/src/node/orpc/server.ts index a695c8521b..3a198a9278 100644 --- a/src/node/orpc/server.ts +++ b/src/node/orpc/server.ts @@ -1057,7 +1057,7 @@ export async function createOrpcServer({ res.status(400).json({ error: "Missing or invalid Host header" }); return; } - const { authorizeUrl, state } = context.muxGatewayOauthService.startServerFlow({ redirectUri }); + const { authorizeUrl, state } = context.xumGatewayOauthService.startServerFlow({ redirectUri }); res.json({ authorizeUrl, state }); }); @@ -1073,7 +1073,7 @@ export async function createOrpcServer({ const error = getStringParamFromQueryOrBody(req, "error"); const errorDescription = getStringParamFromQueryOrBody(req, "error_description") ?? undefined; - const result = await context.muxGatewayOauthService.handleServerCallbackAndExchange({ + const result = await context.xumGatewayOauthService.handleServerCallbackAndExchange({ state, code, error, @@ -1204,7 +1204,7 @@ export async function createOrpcServer({ res.status(400).json({ error: "Missing or invalid Host header" }); return; } - const result = context.muxGovernorOauthService.startServerFlow({ + const result = context.xumGovernorOauthService.startServerFlow({ governorOrigin: governorUrl, redirectUri, }); @@ -1236,7 +1236,7 @@ export async function createOrpcServer({ hasError: typeof error === "string" && error.length > 0, }); - const result = await context.muxGovernorOauthService.handleServerCallbackAndExchange({ + const result = await context.xumGovernorOauthService.handleServerCallbackAndExchange({ state, code, error, diff --git a/src/node/runtime/CoderSSHRuntime.test.ts b/src/node/runtime/CoderSSHRuntime.test.ts index 0d1d991733..1aadacccd7 100644 --- a/src/node/runtime/CoderSSHRuntime.test.ts +++ b/src/node/runtime/CoderSSHRuntime.test.ts @@ -31,7 +31,7 @@ function createMockCoderService(overrides?: Partial): CoderService verifyAuthenticatedSession: mock(() => Promise.resolve()), takeProvisioningSession: mock(() => provisioningSession), disposeProvisioningSession: mock(() => Promise.resolve()), - ensureMuxCoderSSHConfig: mock(() => Promise.resolve()), + ensureXumCoderSSHConfig: mock(() => Promise.resolve()), getWorkspaceStatus: mock(() => Promise.resolve({ kind: "ok" as const, status: "running" as const }) ), @@ -604,7 +604,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { it("creates a new Coder workspace and prepares the directory", async () => { const createWorkspace = mock(() => asyncLines(["build line 1", "build line 2"])); - const ensureMuxCoderSSHConfig = mock(() => Promise.resolve()); + const ensureXumCoderSSHConfig = mock(() => Promise.resolve()); const provisioningSession = { token: "token", dispose: mock(() => Promise.resolve()), @@ -623,7 +623,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { const coderService = createMockCoderService({ createWorkspace, - ensureMuxCoderSSHConfig, + ensureXumCoderSSHConfig, getWorkspaceStatus, takeProvisioningSession, }); @@ -670,7 +670,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { provisioningSession ); expect(provisioningSession.dispose).toHaveBeenCalled(); - expect(ensureMuxCoderSSHConfig).toHaveBeenCalled(); + expect(ensureXumCoderSSHConfig).toHaveBeenCalled(); expect(execBufferedSpy).toHaveBeenCalled(); // After postCreateSetup, ensureReady should succeed (workspace exists on server) @@ -724,7 +724,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { it("skips workspace creation when existingWorkspace=true and workspace is running", async () => { const createWorkspace = mock(() => asyncLines(["should not happen"])); const waitForStartupScripts = mock(() => asyncLines(["Already running"])); - const ensureMuxCoderSSHConfig = mock(() => Promise.resolve()); + const ensureXumCoderSSHConfig = mock(() => Promise.resolve()); const getWorkspaceStatus = mock(() => Promise.resolve({ kind: "ok" as const, status: "running" as const }) ); @@ -732,7 +732,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { const coderService = createMockCoderService({ createWorkspace, waitForStartupScripts, - ensureMuxCoderSSHConfig, + ensureXumCoderSSHConfig, getWorkspaceStatus, }); const runtime = createRuntime( @@ -745,7 +745,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { expect(createWorkspace).not.toHaveBeenCalled(); // waitForStartupScripts is called (it handles running workspaces quickly) expect(waitForStartupScripts).toHaveBeenCalled(); - expect(ensureMuxCoderSSHConfig).toHaveBeenCalled(); + expect(ensureXumCoderSSHConfig).toHaveBeenCalled(); expect(execBufferedSpy).toHaveBeenCalled(); }); @@ -754,7 +754,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { const waitForStartupScripts = mock(() => asyncLines(["Starting workspace...", "Build complete", "Startup scripts finished"]) ); - const ensureMuxCoderSSHConfig = mock(() => Promise.resolve()); + const ensureXumCoderSSHConfig = mock(() => Promise.resolve()); const getWorkspaceStatus = mock(() => Promise.resolve({ kind: "ok" as const, status: "stopped" as const }) ); @@ -762,7 +762,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { const coderService = createMockCoderService({ createWorkspace, waitForStartupScripts, - ensureMuxCoderSSHConfig, + ensureXumCoderSSHConfig, getWorkspaceStatus, }); const runtime = createRuntime( @@ -782,7 +782,7 @@ describe("CoderSSHRuntime.postCreateSetup", () => { expect(waitForStartupScripts).toHaveBeenCalled(); expect(loggedStdout).toContain("Starting workspace..."); expect(loggedStdout).toContain("Startup scripts finished"); - expect(ensureMuxCoderSSHConfig).toHaveBeenCalled(); + expect(ensureXumCoderSSHConfig).toHaveBeenCalled(); }); it("polls until stopping workspace becomes stopped before connecting", async () => { @@ -796,12 +796,12 @@ describe("CoderSSHRuntime.postCreateSetup", () => { return Promise.resolve({ kind: "ok" as const, status: "stopped" as const }); }); const waitForStartupScripts = mock(() => asyncLines(["Ready"])); - const ensureMuxCoderSSHConfig = mock(() => Promise.resolve()); + const ensureXumCoderSSHConfig = mock(() => Promise.resolve()); const coderService = createMockCoderService({ getWorkspaceStatus, waitForStartupScripts, - ensureMuxCoderSSHConfig, + ensureXumCoderSSHConfig, }); const runtime = createRuntime( diff --git a/src/node/runtime/CoderSSHRuntime.ts b/src/node/runtime/CoderSSHRuntime.ts index 8aa2f71c8b..fbd641f4a1 100644 --- a/src/node/runtime/CoderSSHRuntime.ts +++ b/src/node/runtime/CoderSSHRuntime.ts @@ -858,7 +858,7 @@ export class CoderSSHRuntime extends SSHRuntime { // Ensure mux-owned SSH config is set up for Coder workspaces. initLogger.logStep("Configuring SSH for Coder..."); try { - await this.coderService.ensureMuxCoderSSHConfig(); + await this.coderService.ensureXumCoderSSHConfig(); } catch (error) { const errorMsg = getErrorMessage(error); log.error("Failed to configure SSH for Coder", { error }); diff --git a/src/node/runtime/muxSshConfigWriter.test.ts b/src/node/runtime/xumSshConfigWriter.test.ts similarity index 89% rename from src/node/runtime/muxSshConfigWriter.test.ts rename to src/node/runtime/xumSshConfigWriter.test.ts index e00a0067ab..cc672feeb2 100644 --- a/src/node/runtime/muxSshConfigWriter.test.ts +++ b/src/node/runtime/xumSshConfigWriter.test.ts @@ -8,7 +8,7 @@ import { MUX_CODER_SSH_BLOCK_END, MUX_CODER_SSH_BLOCK_START, } from "@/constants/coder"; -import { ensureMuxCoderSSHConfigFile } from "./muxSshConfigWriter"; +import { ensureXumCoderSSHConfigFile } from "./xumSshConfigWriter"; function renderExpectedMuxBlock(coderBinaryPath: string): string { const quotedPath = `"${coderBinaryPath.replaceAll('"', String.raw`\"`)}"`; @@ -25,7 +25,7 @@ function renderExpectedMuxBlock(coderBinaryPath: string): string { ].join("\n"); } -describe("ensureMuxCoderSSHConfigFile", () => { +describe("ensureXumCoderSSHConfigFile", () => { const coderBinaryPath = "/usr/local/bin/coder"; let tempDirs: string[] = []; @@ -52,7 +52,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { it("creates block from an empty config", async () => { const sshConfigPath = await makeSSHConfigPath(); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toBe(`${renderExpectedMuxBlock(coderBinaryPath)}\n`); @@ -63,7 +63,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await writeSSHConfig(sshConfigPath, "Host github.com\n"); await fs.chmod(sshConfigPath, 0o644); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const mode = await fs.stat(sshConfigPath).then((stats) => stats.mode & 0o777); expect(mode).toBe(0o644); @@ -72,7 +72,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { it("defaults to 0o600 for a newly created config file", async () => { const sshConfigPath = await makeSSHConfigPath(); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const mode = await fs.stat(sshConfigPath).then((stats) => stats.mode & 0o777); expect(mode).toBe(0o600); @@ -92,7 +92,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await fs.writeFile(targetConfigPath, "Host github.com\n", "utf8"); await fs.symlink(path.relative(sshDir, targetConfigPath), sshConfigPath); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const linkStats = await fs.lstat(sshConfigPath); expect(linkStats.isSymbolicLink()).toBe(true); @@ -116,7 +116,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await fs.mkdir(path.join(tempDir, "dotfiles"), { recursive: true }); await fs.symlink(targetPath, symlinkPath); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath: symlinkPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath: symlinkPath }); // Symlink must still exist and point to the target. const stat = await fs.lstat(symlinkPath); @@ -146,7 +146,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await fs.symlink(targetPath, linkA); await fs.symlink(linkA, configLink); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath: configLink }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath: configLink }); // Both symlinks must survive. expect((await fs.lstat(configLink)).isSymbolicLink()).toBe(true); @@ -169,7 +169,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await fs.symlink(realFile, linkA); await fs.symlink(linkA, configLink); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath: configLink }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath: configLink }); // Both symlinks must remain intact. const configStat = await fs.lstat(configLink); @@ -191,7 +191,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const existingContent = ["Host github.com", " User git", ""].join("\n"); await writeSSHConfig(sshConfigPath, existingContent); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content.slice(0, existingContent.length)).toBe(existingContent); @@ -208,7 +208,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { ].join("\n"); await writeSSHConfig(sshConfigPath, existingContent); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content.slice(0, existingContent.length)).toBe(existingContent); @@ -230,7 +230,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await writeSSHConfig(sshConfigPath, existingContent); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); const expected = [ @@ -252,7 +252,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const existingContent = `${renderExpectedMuxBlock(originalBinaryPath)}\n${userContent}`; await writeSSHConfig(sshConfigPath, existingContent); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toBe(`${renderExpectedMuxBlock(coderBinaryPath)}\n${userContent}`); @@ -265,7 +265,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const existingContent = `${userContent}\n${renderExpectedMuxBlock(originalBinaryPath)}`; await writeSSHConfig(sshConfigPath, existingContent); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toBe(`${userContent}\n${renderExpectedMuxBlock(coderBinaryPath)}`); @@ -274,10 +274,10 @@ describe("ensureMuxCoderSSHConfigFile", () => { it("is idempotent when called repeatedly with the same binary path", async () => { const sshConfigPath = await makeSSHConfigPath(); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const firstWrite = await readSSHConfig(sshConfigPath); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const secondWrite = await readSSHConfig(sshConfigPath); expect(secondWrite).toBe(firstWrite); @@ -286,8 +286,8 @@ describe("ensureMuxCoderSSHConfigFile", () => { it("updates ProxyCommand when binary path changes", async () => { const sshConfigPath = await makeSSHConfigPath(); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); - await ensureMuxCoderSSHConfigFile({ + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath: "/Applications/Coder.app/Contents/MacOS/coder", sshConfigPath, }); @@ -305,7 +305,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const sshConfigPath = await makeSSHConfigPath(); const spacedPath = "/usr/local/my dir/coder"; - await ensureMuxCoderSSHConfigFile({ coderBinaryPath: spacedPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath: spacedPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toContain('ProxyCommand "/usr/local/my dir/coder"'); @@ -315,7 +315,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const sshConfigPath = await makeSSHConfigPath(); const quotedPath = '/path/to/"coder"'; - await ensureMuxCoderSSHConfigFile({ coderBinaryPath: quotedPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath: quotedPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toContain('ProxyCommand "/path/to/\\"coder\\""'); @@ -325,7 +325,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const sshConfigPath = await makeSSHConfigPath(); const trickyPath = "/usr/$HOME/`whoami`/$(id)/coder"; - await ensureMuxCoderSSHConfigFile({ coderBinaryPath: trickyPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath: trickyPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); // Double-quoting preserves the literal path in the SSH config file. @@ -339,7 +339,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const sshConfigPath = await makeSSHConfigPath(); const pathWithQuote = "/usr/local/it's/coder"; - await ensureMuxCoderSSHConfigFile({ coderBinaryPath: pathWithQuote, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath: pathWithQuote, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toContain('ProxyCommand "/usr/local/it\'s/coder"'); @@ -349,7 +349,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const sshConfigPath = await makeSSHConfigPath(); const windowsPath = "C:\\Program Files\\Coder\\bin\\coder.exe"; - await ensureMuxCoderSSHConfigFile({ coderBinaryPath: windowsPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath: windowsPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toContain('ProxyCommand "C:\\Program Files\\Coder\\bin\\coder.exe"'); @@ -360,7 +360,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { // eslint-disable-next-line @typescript-eslint/await-thenable -- Bun's expect().rejects.toThrow() is thenable at runtime await expect( - ensureMuxCoderSSHConfigFile({ coderBinaryPath: "/path\n/coder", sshConfigPath }) + ensureXumCoderSSHConfigFile({ coderBinaryPath: "/path\n/coder", sshConfigPath }) ).rejects.toThrow(/newline/i); }); @@ -369,7 +369,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { const existingContent = ["Host github.com", " User git"].join("\n"); await writeSSHConfig(sshConfigPath, existingContent); - await ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); + await ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }); const content = await readSSHConfig(sshConfigPath); expect(content).toBe(`${existingContent}\n${renderExpectedMuxBlock(coderBinaryPath)}\n`); @@ -388,7 +388,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await writeSSHConfig(sshConfigPath, corruptedConfig); // eslint-disable-next-line @typescript-eslint/await-thenable -- Bun's expect().rejects.toThrow() is thenable at runtime - await expect(ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath })).rejects.toThrow( + await expect(ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath })).rejects.toThrow( /duplicate/i ); }); @@ -406,7 +406,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await writeSSHConfig(sshConfigPath, corruptedConfig); // eslint-disable-next-line @typescript-eslint/await-thenable -- Bun's expect().rejects.toThrow() is thenable at runtime - await expect(ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath })).rejects.toThrow( + await expect(ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath })).rejects.toThrow( /duplicate/i ); }); @@ -425,7 +425,7 @@ describe("ensureMuxCoderSSHConfigFile", () => { await writeSSHConfig(sshConfigPath, config); // eslint-disable-next-line @typescript-eslint/await-thenable -- Bun's expect().rejects.toThrow() is thenable at runtime - await expect(ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath })).rejects.toThrow( + await expect(ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath })).rejects.toThrow( /mismatched/i ); }); @@ -441,8 +441,8 @@ describe("ensureMuxCoderSSHConfigFile", () => { try { await Promise.all([ - ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }), - ensureMuxCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }), + ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }), + ensureXumCoderSSHConfigFile({ coderBinaryPath, sshConfigPath }), ]); const tempPaths = writeFileSpy.mock.calls diff --git a/src/node/runtime/muxSshConfigWriter.ts b/src/node/runtime/xumSshConfigWriter.ts similarity index 98% rename from src/node/runtime/muxSshConfigWriter.ts rename to src/node/runtime/xumSshConfigWriter.ts index 0e54b46601..89d4f4e3eb 100644 --- a/src/node/runtime/muxSshConfigWriter.ts +++ b/src/node/runtime/xumSshConfigWriter.ts @@ -8,7 +8,7 @@ import { MUX_CODER_SSH_BLOCK_START, } from "@/constants/coder"; -interface EnsureMuxCoderSSHConfigFileOptions { +interface EnsureXumCoderSSHConfigFileOptions { coderBinaryPath: string; sshConfigPath?: string; } @@ -219,8 +219,8 @@ async function writeConfigAtomically( } } -export async function ensureMuxCoderSSHConfigFile( - opts: EnsureMuxCoderSSHConfigFileOptions +export async function ensureXumCoderSSHConfigFile( + opts: EnsureXumCoderSSHConfigFileOptions ): Promise { const configuredSSHConfigPath = opts.sshConfigPath ?? path.join(os.homedir(), ".ssh", "config"); // Preserve users' symlinked ~/.ssh/config setups by writing to the symlink target, diff --git a/src/node/services/agentSession.agentSkillSnapshot.test.ts b/src/node/services/agentSession.agentSkillSnapshot.test.ts index 061a1c7309..c6c06774cb 100644 --- a/src/node/services/agentSession.agentSkillSnapshot.test.ts +++ b/src/node/services/agentSession.agentSkillSnapshot.test.ts @@ -6,7 +6,7 @@ import * as path from "node:path"; import { EXPERIMENT_IDS } from "@/common/constants/experiments"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { Ok } from "@/common/types/result"; import type { AIService } from "@/node/services/aiService"; @@ -38,7 +38,7 @@ describe("AgentSession.sendMessage (agent skill snapshots)", () => { await historyCleanup?.(); }); - function getMessageText(message: MuxMessage): string { + function getMessageText(message: XumMessage): string { const textPart = message.parts.find((part) => part.type === "text"); if (textPart?.type !== "text") { throw new Error(`Expected text part for message ${message.id}`); @@ -70,10 +70,10 @@ describe("AgentSession.sendMessage (agent skill snapshots)", () => { }); historyCleanup = cleanup; - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; const realAppend = historyService.appendToHistory.bind(historyService); const appendToHistory = spyOn(historyService, "appendToHistory").mockImplementation( - async (wId: string, message: MuxMessage) => { + async (wId: string, message: XumMessage) => { messages.push(message); return realAppend(wId, message); } @@ -899,13 +899,13 @@ describe("AgentSession.sendMessage (agent skill snapshots)", () => { const skillSnapshotId = "agent-skill-snapshot-0"; const userMessageId = "user-0"; - const historyMessages: MuxMessage[] = [ - createMuxMessage(fileSnapshotId, "user", "...", { + const historyMessages: XumMessage[] = [ + createXumMessage(fileSnapshotId, "user", "...", { historySequence: 0, synthetic: true, fileAtMentionSnapshot: ["@file:foo.txt"], }), - createMuxMessage(skillSnapshotId, "user", "...", { + createXumMessage(skillSnapshotId, "user", "...", { historySequence: 1, synthetic: true, agentSkillSnapshot: { @@ -914,7 +914,7 @@ describe("AgentSession.sendMessage (agent skill snapshots)", () => { sha256: "abc", }, }), - createMuxMessage(userMessageId, "user", "do X", { + createXumMessage(userMessageId, "user", "do X", { historySequence: 2, muxMetadata: { type: "agent-skill", diff --git a/src/node/services/agentSession.autoCompaction.test.ts b/src/node/services/agentSession.autoCompaction.test.ts index ebc36899cd..491670341e 100644 --- a/src/node/services/agentSession.autoCompaction.test.ts +++ b/src/node/services/agentSession.autoCompaction.test.ts @@ -7,9 +7,9 @@ import type { WorkspaceChatMessage, } from "@/common/orpc/types"; import { - createMuxMessage, + createXumMessage, type CompactionFollowUpRequest, - type MuxMessage, + type XumMessage, } from "@/common/types/message"; import { GOAL_CONTINUATION_KIND } from "@/constants/goals"; import { Ok, Err } from "@/common/types/result"; @@ -48,14 +48,14 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { test("does not persist or emit snapshots before forced on-send compaction", async () => { const workspaceId = "ws-auto-compaction-snapshot-deferral"; - const streamMessage = mock((_history: MuxMessage[]) => Promise.resolve(Ok(undefined))); + const streamMessage = mock((_history: XumMessage[]) => Promise.resolve(Ok(undefined))); const { session, historyService, events } = await createSessionHarness({ workspaceId, streamMessage: streamMessage as unknown as AIService["streamMessage"], captureEvents: true, }); - const syntheticSnapshot = createMuxMessage( + const syntheticSnapshot = createXumMessage( "file-snapshot-1", "user", "@foo.ts", @@ -69,7 +69,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { const internals = session as unknown as { materializeFileAtMentionsSnapshot: ( text: string - ) => Promise<{ snapshotMessage: MuxMessage; materializedTokens: string[] } | null>; + ) => Promise<{ snapshotMessage: XumMessage; materializedTokens: string[] } | null>; compactionMonitor: CompactionMonitor; }; @@ -142,7 +142,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { parsed: {}, source: "auto-compaction" as const, }; - const compactionRequest = createMuxMessage( + const compactionRequest = createXumMessage( "compaction-request", "user", "Summarize the conversation", @@ -151,12 +151,12 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { muxMetadata: compactionMetadata, } ); - const snapshot = createMuxMessage("file-change", "user", "", { + const snapshot = createXumMessage("file-change", "user", "", { synthetic: true, }); const internals = session as unknown as { resolveCompactionRequest: ( - history: MuxMessage[], + history: XumMessage[], modelString: string, options: SendMessageOptions ) => { id: string; source?: string } | undefined; @@ -179,14 +179,14 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { test("does not materialize skill snapshots (or run their directives) on deferred on-send compaction turns", async () => { const workspaceId = "ws-auto-compaction-skill-snapshot-deferral"; - const streamMessage = mock((_history: MuxMessage[]) => Promise.resolve(Ok(undefined))); + const streamMessage = mock((_history: XumMessage[]) => Promise.resolve(Ok(undefined))); const { session } = await createSessionHarness({ workspaceId, streamMessage: streamMessage as unknown as AIService["streamMessage"], }); const internals = session as unknown as { - materializeAgentSkillSnapshots: (...args: unknown[]) => Promise; + materializeAgentSkillSnapshots: (...args: unknown[]) => Promise; compactionMonitor: CompactionMonitor; }; @@ -285,7 +285,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { expect(result.success).toBe(true); expect(streamMessage).toHaveBeenCalledTimes(1); - const firstRequest = streamRequests[0] as { messages?: MuxMessage[] } | undefined; + const firstRequest = streamRequests[0] as { messages?: XumMessage[] } | undefined; const requestMessages = Array.isArray(firstRequest?.messages) ? firstRequest.messages : []; const hasCompactionRequest = requestMessages.some( (message) => message.metadata?.muxMetadata?.type === "compaction-request" @@ -338,7 +338,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { expect(result.success).toBe(true); expect(streamMessage).toHaveBeenCalledTimes(1); - const firstRequest = streamRequests[0] as { messages?: MuxMessage[] } | undefined; + const firstRequest = streamRequests[0] as { messages?: XumMessage[] } | undefined; const requestMessages = Array.isArray(firstRequest?.messages) ? firstRequest.messages : []; const compactionRequestMessage = requestMessages.find( (message) => message.metadata?.muxMetadata?.type === "compaction-request" @@ -364,7 +364,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { const appendSeedUsage = await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-1m-routing-usage", "assistant", "existing context", { + createXumMessage("assistant-1m-routing-usage", "assistant", "existing context", { timestamp: Date.now() - 1_000, model: "anthropic:claude-sonnet-4-5", contextUsage: { @@ -391,7 +391,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { expect(result.success).toBe(true); expect(streamMessage).toHaveBeenCalledTimes(1); - const firstRequest = streamRequests[0] as { messages?: MuxMessage[] } | undefined; + const firstRequest = streamRequests[0] as { messages?: XumMessage[] } | undefined; const requestMessages = Array.isArray(firstRequest?.messages) ? firstRequest.messages : []; const hasCompactionRequest = requestMessages.some( (message) => message.metadata?.muxMetadata?.type === "compaction-request" @@ -427,7 +427,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { const appendSeedUsage = await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-disabled-beta-usage", "assistant", "existing context", { + createXumMessage("assistant-disabled-beta-usage", "assistant", "existing context", { timestamp: Date.now() - 1_000, model: "anthropic:claude-sonnet-4-5", contextUsage: { @@ -453,7 +453,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { expect(result.success).toBe(true); expect(streamMessage).toHaveBeenCalledTimes(1); - const firstRequest = streamRequests[0] as { messages?: MuxMessage[] } | undefined; + const firstRequest = streamRequests[0] as { messages?: XumMessage[] } | undefined; const requestMessages = Array.isArray(firstRequest?.messages) ? firstRequest.messages : []; const hasCompactionRequest = requestMessages.some( (message) => message.metadata?.muxMetadata?.type === "compaction-request" @@ -692,7 +692,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { } as unknown as ProvidersConfigMap; const aiEmitter = new EventEmitter(); - const streamMessage = mock((_history: MuxMessage[]) => { + const streamMessage = mock((_history: XumMessage[]) => { const usage = { inputTokens: 42, outputTokens: 1, @@ -802,7 +802,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { const appendOldUser = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-old-before-boundary", "user", "old prompt", { + createXumMessage("user-old-before-boundary", "user", "old prompt", { timestamp: Date.now() - 4_000, }) ); @@ -810,7 +810,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { const appendOldAssistant = await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-old-before-boundary", "assistant", "old reply", { + createXumMessage("assistant-old-before-boundary", "assistant", "old reply", { timestamp: Date.now() - 3_000, model: "openai:gpt-4o", contextUsage: oldUsage, @@ -820,7 +820,7 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { const appendBoundary = await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-compaction-boundary", "assistant", "compacted summary", { + createXumMessage("assistant-compaction-boundary", "assistant", "compacted summary", { timestamp: Date.now() - 2_000, compacted: "user", compactionBoundary: true, @@ -831,14 +831,14 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { const appendCurrentEpochUser = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-after-boundary", "user", "fresh prompt after compaction", { + createXumMessage("user-after-boundary", "user", "fresh prompt after compaction", { timestamp: Date.now() - 1_000, }) ); expect(appendCurrentEpochUser.success).toBe(true); const aiEmitter = new EventEmitter(); - const streamMessage = mock((_history: MuxMessage[]) => Promise.resolve(Ok(undefined))); + const streamMessage = mock((_history: XumMessage[]) => Promise.resolve(Ok(undefined))); const aiService = Object.assign(aiEmitter, { isStreaming: mock((_workspaceId: string) => false), stopStream: mock((_workspaceId: string) => Promise.resolve(Ok(undefined))), @@ -1047,14 +1047,14 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => { historyCleanup = cleanup; const aiEmitter = new EventEmitter(); - const streamHistories: MuxMessage[][] = []; + const streamHistories: XumMessage[][] = []; let streamCallCount = 0; const streamMessage = mock((request: unknown) => { const requestMessages = typeof request === "object" && request !== null && "messages" in request ? (request as { messages?: unknown }).messages : undefined; - streamHistories.push(Array.isArray(requestMessages) ? (requestMessages as MuxMessage[]) : []); + streamHistories.push(Array.isArray(requestMessages) ? (requestMessages as XumMessage[]) : []); streamCallCount += 1; if (streamCallCount === 1) { diff --git a/src/node/services/agentSession.continueMessageAgentId.test.ts b/src/node/services/agentSession.continueMessageAgentId.test.ts index 16c344fa22..e7c7d29870 100644 --- a/src/node/services/agentSession.continueMessageAgentId.test.ts +++ b/src/node/services/agentSession.continueMessageAgentId.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, mock, test } from "bun:test"; -import { createMuxMessage } from "@/common/types/message"; -import type { CompactionFollowUpRequest, MuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; +import type { CompactionFollowUpRequest, XumMessage } from "@/common/types/message"; import type { FilePart, SendMessageOptions } from "@/common/orpc/types"; import type { Config } from "@/node/config"; import { AgentSession } from "./agentSession"; @@ -46,7 +46,7 @@ const idleFollowUp = (): CompactionFollowUpRequest => ({ function compactionSummaryMessage( id: string, pendingFollowUp: CompactionFollowUpRequest -): MuxMessage { +): XumMessage { return { id, role: "assistant", @@ -57,11 +57,11 @@ function compactionSummaryMessage( pendingFollowUp, }, }, - } satisfies MuxMessage; + } satisfies XumMessage; } -function heartbeatBoundaryMessage(pendingFollowUp = idleFollowUp()): MuxMessage { - return createMuxMessage("heartbeat-boundary", "assistant", "Reset boundary", { +function heartbeatBoundaryMessage(pendingFollowUp = idleFollowUp()): XumMessage { + return createXumMessage("heartbeat-boundary", "assistant", "Reset boundary", { compacted: "heartbeat", compactionBoundary: true, compactionEpoch: 1, @@ -122,7 +122,7 @@ describe("AgentSession continue-message agentId fallback", () => { historyCleanup = undefined; }); - const createSession = async (messages: MuxMessage[] = []) => { + const createSession = async (messages: XumMessage[] = []) => { const { historyService, cleanup } = await createTestHistoryService(); historyCleanup = cleanup; for (const message of messages) { @@ -232,7 +232,7 @@ describe("AgentSession continue-message agentId fallback", () => { }); test("dispatchPendingFollowUp removes heartbeat reset boundaries when idle-only follow-ups are skipped", async () => { - const earlierMessage = createMuxMessage("before-reset", "assistant", "Earlier context"); + const earlierMessage = createXumMessage("before-reset", "assistant", "Earlier context"); const { session, historyService, internals } = await createSession([ earlierMessage, heartbeatBoundaryMessage(), @@ -366,7 +366,7 @@ describe("AgentSession continue-message agentId fallback", () => { getLastMessages: ( workspaceId: string, count: number - ) => Promise<{ success: boolean; error?: string; data: MuxMessage[] }>; + ) => Promise<{ success: boolean; error?: string; data: XumMessage[] }>; }; }; historyInternals.historyService.getLastMessages = mock(() => diff --git a/src/node/services/agentSession.editMessageId.test.ts b/src/node/services/agentSession.editMessageId.test.ts index acf9fe587a..9d0682a4c1 100644 --- a/src/node/services/agentSession.editMessageId.test.ts +++ b/src/node/services/agentSession.editMessageId.test.ts @@ -4,7 +4,7 @@ import type { AIService } from "@/node/services/aiService"; import type { InitStateManager } from "@/node/services/initStateManager"; import type { BackgroundProcessManager } from "@/node/services/backgroundProcessManager"; import type { Config } from "@/node/config"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { SendMessageError } from "@/common/types/errors"; import type { Result } from "@/common/types/result"; import { Ok } from "@/common/types/result"; @@ -74,7 +74,7 @@ describe("AgentSession.sendMessage (editMessageId)", () => { const originalImageUrl = "data:image/png;base64,AAAA"; await historyService.appendToHistory( workspaceId, - createMuxMessage(messageId, "user", "original", { historySequence: 0 }, [ + createXumMessage(messageId, "user", "original", { historySequence: 0 }, [ { type: "file", mediaType: "image/png", url: originalImageUrl }, ]) ); @@ -129,11 +129,11 @@ describe("AgentSession.sendMessage (editMessageId)", () => { const { session, historyService, streamMessage } = await createSessionHarness(workspaceId); await historyService.appendToHistory( workspaceId, - createMuxMessage("user-original", "user", "original", { historySequence: 0 }) + createXumMessage("user-original", "user", "original", { historySequence: 0 }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-original", "assistant", "reply", { historySequence: 1 }) + createXumMessage("assistant-original", "assistant", "reply", { historySequence: 1 }) ); const truncateAfterMessage = spyOn(historyService, "truncateAfterMessage"); const appendToHistory = spyOn(historyService, "appendToHistory"); @@ -222,7 +222,7 @@ describe("AgentSession.sendMessage (editMessageId)", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("snapshot-original", "user", "snapshot", { + createXumMessage("snapshot-original", "user", "snapshot", { historySequence: 0, synthetic: true, fileAtMentionSnapshot: ["@src/foo.ts"], @@ -230,17 +230,17 @@ describe("AgentSession.sendMessage (editMessageId)", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage("user-original", "user", "original @src/foo.ts", { + createXumMessage("user-original", "user", "original @src/foo.ts", { historySequence: 1, }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-original", "assistant", "reply", { historySequence: 2 }) + createXumMessage("assistant-original", "assistant", "reply", { historySequence: 2 }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("boundary", "assistant", "summary", { + createXumMessage("boundary", "assistant", "summary", { historySequence: 3, compacted: "user", compactionBoundary: true, @@ -249,7 +249,7 @@ describe("AgentSession.sendMessage (editMessageId)", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-after-boundary", "assistant", "after", { historySequence: 4 }) + createXumMessage("assistant-after-boundary", "assistant", "after", { historySequence: 4 }) ); const activeWindow = await historyService.getHistoryFromLatestBoundary(workspaceId); expect(activeWindow.success).toBe(true); diff --git a/src/node/services/agentSession.fileChangeNotification.test.ts b/src/node/services/agentSession.fileChangeNotification.test.ts index f0af096130..18d13b3234 100644 --- a/src/node/services/agentSession.fileChangeNotification.test.ts +++ b/src/node/services/agentSession.fileChangeNotification.test.ts @@ -3,8 +3,8 @@ import { mkdtemp, rm, stat, utimes, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; -import { createMuxMessage } from "@/common/types/message"; -import type { MuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { Err, Ok } from "@/common/types/result"; import type { Config } from "@/node/config"; import { AgentSession } from "./agentSession"; @@ -41,10 +41,10 @@ describe("AgentSession file-change notification (turn start)", () => { historyCleanup = cleanup; await historyService.appendToHistory( "ws", - createMuxMessage("user-1", "user", "hello", { timestamp: Date.now() }) + createXumMessage("user-1", "user", "hello", { timestamp: Date.now() }) ); - const capturedRequests: MuxMessage[][] = []; + const capturedRequests: XumMessage[][] = []; const streamMessage = mock((opts: StreamMessageOptions) => { capturedRequests.push(opts.messages); return Promise.resolve(Ok(undefined)); diff --git a/src/node/services/agentSession.goalAutoPause.test.ts b/src/node/services/agentSession.goalAutoPause.test.ts index d3b7e37676..e889e4af4a 100644 --- a/src/node/services/agentSession.goalAutoPause.test.ts +++ b/src/node/services/agentSession.goalAutoPause.test.ts @@ -8,7 +8,7 @@ import type { InitStateManager } from "./initStateManager"; import { AgentSession } from "./agentSession"; import { createTestHistoryService } from "./testHistoryService"; import { WorkspaceGoalService } from "./workspaceGoalService"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { Ok } from "@/common/types/result"; import type { SendMessageOptions } from "@/common/orpc/types"; import type { GoalRecordV1, GoalStatus } from "@/common/types/goal"; @@ -358,14 +358,14 @@ describe("AgentSession goal safety hooks", () => { await setGoalOk(goalService, { workspaceId, objective: "Recover safely" }); await historyService.appendToHistory( workspaceId, - createMuxMessage("user-before-crash", "user", "Start risky work", { + createXumMessage("user-before-crash", "user", "Start risky work", { timestamp: 1, retrySendOptions: SEND_OPTIONS, }) ); await historyService.writePartial( workspaceId, - createMuxMessage("assistant-partial", "assistant", "Partial answer", { historySequence: 1 }) + createXumMessage("assistant-partial", "assistant", "Partial answer", { historySequence: 1 }) ); await session.runStartupRecovery(); @@ -386,7 +386,7 @@ describe("AgentSession goal safety hooks", () => { cleanups.push(cleanup); await historyService.writePartial( workspaceId, - createMuxMessage("assistant-partial", "assistant", "Partial answer", { historySequence: 0 }) + createXumMessage("assistant-partial", "assistant", "Partial answer", { historySequence: 0 }) ); await session.runStartupRecovery(); @@ -407,14 +407,14 @@ describe("AgentSession goal safety hooks", () => { await setGoalOk(goalService, { workspaceId, objective: "Continue after restart" }); await historyService.appendToHistory( workspaceId, - createMuxMessage("user-before-crash", "user", "Start work", { + createXumMessage("user-before-crash", "user", "Start work", { timestamp: 1, retrySendOptions: SEND_OPTIONS, }) ); await historyService.writePartial( workspaceId, - createMuxMessage("assistant-partial", "assistant", "Partial answer", { historySequence: 1 }) + createXumMessage("assistant-partial", "assistant", "Partial answer", { historySequence: 1 }) ); await session.runStartupRecovery(); diff --git a/src/node/services/agentSession.mcpPromptSnapshot.test.ts b/src/node/services/agentSession.mcpPromptSnapshot.test.ts index 0da729dd64..3a4f0195fa 100644 --- a/src/node/services/agentSession.mcpPromptSnapshot.test.ts +++ b/src/node/services/agentSession.mcpPromptSnapshot.test.ts @@ -1,5 +1,5 @@ import { describe, expect, mock, spyOn, test } from "bun:test"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { Err, Ok } from "@/common/types/result"; import type { AIService } from "@/node/services/aiService"; import type { MCPServerManager } from "@/node/services/mcpServerManager"; @@ -171,7 +171,7 @@ describe("AgentSession MCP prompt snapshots", () => { try { const realAppend = harness.historyService.appendToHistory.bind(harness.historyService); const appendToHistory = spyOn(harness.historyService, "appendToHistory").mockImplementation( - async (workspaceId: string, message: MuxMessage) => { + async (workspaceId: string, message: XumMessage) => { if (message.metadata?.mcpPromptSnapshot) return realAppend(workspaceId, message); return Err("disk full"); } @@ -228,7 +228,7 @@ describe("AgentSession MCP prompt snapshots", () => { }); test("excludes crash-orphaned snapshots from provider requests", async () => { - const streamMessage = mock((_args: { messages: MuxMessage[] }) => + const streamMessage = mock((_args: { messages: XumMessage[] }) => Promise.resolve(Ok(undefined)) ); const harness = await createAgentSessionHarness({ @@ -241,7 +241,7 @@ describe("AgentSession MCP prompt snapshots", () => { try { await harness.historyService.appendToHistory( "workspace", - createMuxMessage("orphan-snap", "user", "Expanded prompt", { + createXumMessage("orphan-snap", "user", "Expanded prompt", { historySequence: 0, synthetic: true, mcpPromptSnapshot: { @@ -276,7 +276,7 @@ describe("AgentSession MCP prompt snapshots", () => { const userMessageId = "user-0"; await harness.historyService.appendToHistory( "workspace", - createMuxMessage(snapshotId, "user", "Expanded prompt", { + createXumMessage(snapshotId, "user", "Expanded prompt", { historySequence: 0, synthetic: true, mcpPromptSnapshot: { @@ -288,7 +288,7 @@ describe("AgentSession MCP prompt snapshots", () => { ); await harness.historyService.appendToHistory( "workspace", - createMuxMessage(userMessageId, "user", "Using MCP prompt coder/review: src", { + createXumMessage(userMessageId, "user", "Using MCP prompt coder/review: src", { historySequence: 1, muxMetadata: promptMetadata(), }) diff --git a/src/node/services/agentSession.postCompactionAttachments.test.ts b/src/node/services/agentSession.postCompactionAttachments.test.ts index 2040626257..5c7cabbdfa 100644 --- a/src/node/services/agentSession.postCompactionAttachments.test.ts +++ b/src/node/services/agentSession.postCompactionAttachments.test.ts @@ -9,7 +9,7 @@ import type { TodoListAttachment, } from "@/common/types/attachment"; import { TURNS_BETWEEN_ATTACHMENTS } from "@/common/constants/attachments"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import type { Config } from "@/node/config"; import type { AIService } from "./aiService"; @@ -21,7 +21,7 @@ import { DisposableTempDir } from "./tempDir"; import { createTestHistoryService } from "./testHistoryService"; import { createLoadedSkillSnapshot } from "@/node/services/agentSkills/loadedSkillSnapshots"; -function createSuccessfulFileEditMessage(id: string, filePath: string, diff: string): MuxMessage { +function createSuccessfulFileEditMessage(id: string, filePath: string, diff: string): XumMessage { return { id, role: "assistant", @@ -204,13 +204,13 @@ describe("AgentSession post-compaction attachments", () => { test("extracts edited file diffs from the latest durable compaction boundary slice", async () => { using sessionDir = new DisposableTempDir("agent-session-latest-boundary"); - const history: MuxMessage[] = [ + const history: XumMessage[] = [ createSuccessfulFileEditMessage( "stale-before-boundary", "/tmp/stale-before-boundary.ts", "@@ -1 +1 @@\n-old\n+older\n" ), - createMuxMessage("boundary-1", "assistant", "epoch 1 summary", { + createXumMessage("boundary-1", "assistant", "epoch 1 summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, @@ -220,7 +220,7 @@ describe("AgentSession post-compaction attachments", () => { "/tmp/stale-epoch-1.ts", "@@ -1 +1 @@\n-old\n+stale\n" ), - createMuxMessage("boundary-2", "assistant", "epoch 2 summary", { + createXumMessage("boundary-2", "assistant", "epoch 2 summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 2, @@ -252,9 +252,9 @@ describe("AgentSession post-compaction attachments", () => { test("falls back safely when boundary markers are malformed", async () => { using sessionDir = new DisposableTempDir("agent-session-malformed-boundary"); - const history: MuxMessage[] = [ + const history: XumMessage[] = [ createSuccessfulFileEditMessage("stale-edit", "/tmp/stale.ts", "@@ -1 +1 @@\n-old\n+stale\n"), - createMuxMessage("malformed-boundary", "assistant", "malformed summary", { + createXumMessage("malformed-boundary", "assistant", "malformed summary", { compacted: "user", compactionBoundary: true, // Missing compactionEpoch: marker should be ignored without crashing. @@ -331,8 +331,8 @@ describe("AgentSession post-compaction attachments", () => { test("reinjects cached loaded skills on later turns even after pending state is acknowledged", async () => { using sessionDir = new DisposableTempDir("agent-session-periodic-loaded-skills"); - const history: MuxMessage[] = [ - createMuxMessage("boundary-1", "assistant", "epoch 1 summary", { + const history: XumMessage[] = [ + createXumMessage("boundary-1", "assistant", "epoch 1 summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, diff --git a/src/node/services/agentSession.postCompactionRefresh.test.ts b/src/node/services/agentSession.postCompactionRefresh.test.ts index ad7a0e7180..f21c46757b 100644 --- a/src/node/services/agentSession.postCompactionRefresh.test.ts +++ b/src/node/services/agentSession.postCompactionRefresh.test.ts @@ -6,7 +6,7 @@ import type { InitStateManager } from "./initStateManager"; import type { BackgroundProcessManager } from "./backgroundProcessManager"; import { createTestHistoryService } from "./testHistoryService"; import type { CompactionCompletionMetadata } from "@/common/types/compaction"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { StreamEndEvent } from "@/common/types/stream"; import { createAgentSessionHarness } from "./agentSession.testHarness"; @@ -55,19 +55,19 @@ describe("AgentSession post-compaction refresh trigger", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("user-before-compact", "user", "Remember that we prefer concise tests", { + createXumMessage("user-before-compact", "user", "Remember that we prefer concise tests", { timestamp: 1000, }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-before-compact", "assistant", "Noted.", { + createXumMessage("assistant-before-compact", "assistant", "Noted.", { timestamp: 1001, }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("compact-request", "user", "Please compact", { + createXumMessage("compact-request", "user", "Please compact", { timestamp: 1002, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }) @@ -124,11 +124,11 @@ describe("AgentSession post-compaction refresh trigger", () => { historyCleanup = cleanup; await historyService.appendToHistory( workspaceId, - createMuxMessage("before-failed-follow-up", "user", "Preserve this context") + createXumMessage("before-failed-follow-up", "user", "Preserve this context") ); await historyService.appendToHistory( workspaceId, - createMuxMessage("failed-follow-up-request", "user", "Please compact", { + createXumMessage("failed-follow-up-request", "user", "Please compact", { muxMetadata: { type: "compaction-request", rawCommand: "/compact", diff --git a/src/node/services/agentSession.postCompactionRetry.test.ts b/src/node/services/agentSession.postCompactionRetry.test.ts index d4c39fc3b9..e0b9e5f729 100644 --- a/src/node/services/agentSession.postCompactionRetry.test.ts +++ b/src/node/services/agentSession.postCompactionRetry.test.ts @@ -10,7 +10,7 @@ import type { AIService } from "./aiService"; import type { InitStateManager } from "./initStateManager"; import type { BackgroundProcessManager } from "./backgroundProcessManager"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { SendMessageOptions } from "@/common/orpc/types"; import { createTestHistoryService } from "./testHistoryService"; @@ -49,7 +49,7 @@ describe("AgentSession post-compaction context retry", () => { ], }); - const history: MuxMessage[] = [ + const history: XumMessage[] = [ { id: "compaction-summary", role: "assistant", diff --git a/src/node/services/agentSession.preStreamError.test.ts b/src/node/services/agentSession.preStreamError.test.ts index dab6ceb863..b0cc87ef7b 100644 --- a/src/node/services/agentSession.preStreamError.test.ts +++ b/src/node/services/agentSession.preStreamError.test.ts @@ -5,12 +5,12 @@ import type { AIService } from "@/node/services/aiService"; import type { BackgroundProcessManager } from "@/node/services/backgroundProcessManager"; import type { InitStateManager } from "@/node/services/initStateManager"; import type { SendMessageError } from "@/common/types/errors"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import type { Result } from "@/common/types/result"; import { Err, Ok } from "@/common/types/result"; import { computePriorHistoryFingerprint } from "@/common/orpc/onChatCursorFingerprint"; import { - isMuxMessage, + isXumMessage, type StreamErrorMessage, type WorkspaceChatMessage, } from "@/common/orpc/types"; @@ -37,7 +37,7 @@ async function createReplaySessionHarness( const harness = await createAgentSessionHarness({ workspaceId, aiServiceOverrides: { - streamMessage: mock((_history: MuxMessage[]) => + streamMessage: mock((_history: XumMessage[]) => Promise.resolve(Err({ type: "unknown", raw: "unused" })) ) as unknown as AIService["streamMessage"], getStreamInfo: mock((_workspaceId: string) => streamInfo) as AIService["getStreamInfo"], @@ -57,7 +57,7 @@ describe("AgentSession pre-stream errors", () => { it("emits stream-error when stream startup fails", async () => { const workspaceId = "ws-test"; - const streamMessage = mock((_history: MuxMessage[]) => { + const streamMessage = mock((_history: XumMessage[]) => { return Promise.resolve( Err({ type: "api_key_not_found", @@ -169,7 +169,7 @@ describe("AgentSession pre-stream errors", () => { it("acknowledges edited sends immediately and surfaces later startup failure via stream-error", async () => { const workspaceId = "ws-edit-startup-failed"; - const streamMessage = mock((_history: MuxMessage[]) => { + const streamMessage = mock((_history: XumMessage[]) => { return Promise.resolve( Err({ type: "api_key_not_found", @@ -189,7 +189,7 @@ describe("AgentSession pre-stream errors", () => { const originalMessageId = "editable-user-message"; await historyService.appendToHistory( workspaceId, - createMuxMessage(originalMessageId, "user", "original", { historySequence: 0 }) + createXumMessage(originalMessageId, "user", "original", { historySequence: 0 }) ); const result = await session.sendMessage("edited", { @@ -223,7 +223,7 @@ describe("AgentSession pre-stream errors", () => { const { session, cleanup } = await createAgentSessionHarness({ workspaceId, aiServiceOverrides: { - streamMessage: mock((_history: MuxMessage[]) => + streamMessage: mock((_history: XumMessage[]) => Promise.resolve(Err({ type: "api_key_not_found", provider: "anthropic" })) ) as unknown as AIService["streamMessage"], getStreamInfo: mock((_workspaceId: string) => undefined) as AIService["getStreamInfo"], @@ -337,7 +337,7 @@ describe("AgentSession pre-stream errors", () => { it("schedules auto-retry when runtime startup fails before stream events", async () => { const workspaceId = "ws-runtime-start-failed"; - const streamMessage = mock((_history: MuxMessage[]) => { + const streamMessage = mock((_history: XumMessage[]) => { return Promise.resolve( Err({ type: "runtime_start_failed", @@ -376,7 +376,7 @@ describe("AgentSession pre-stream errors", () => { historyCleanup = cleanup; const aiEmitter = new EventEmitter(); - const streamMessage = mock((_history: MuxMessage[]) => { + const streamMessage = mock((_history: XumMessage[]) => { return Promise.resolve( Err({ type: "runtime_start_failed", @@ -450,7 +450,7 @@ describe("AgentSession pre-stream errors", () => { historyCleanup = cleanup; const aiEmitter = new EventEmitter(); - const streamMessage = mock((_history: MuxMessage[]) => { + const streamMessage = mock((_history: XumMessage[]) => { aiEmitter.emit("error", { workspaceId, messageId: "assistant-stream-startup-failed", @@ -549,8 +549,8 @@ describe("AgentSession pre-stream errors", () => { await createReplaySessionHarness(workspaceId); historyCleanup = cleanup; - const firstMessage = createMuxMessage("msg-history-1", "user", "first"); - const secondMessage = createMuxMessage("msg-history-2", "assistant", "second"); + const firstMessage = createXumMessage("msg-history-1", "user", "first"); + const secondMessage = createXumMessage("msg-history-2", "assistant", "second"); const appendFirst = await historyService.appendToHistory(workspaceId, firstMessage); expect(appendFirst.success).toBe(true); @@ -641,7 +641,7 @@ describe("AgentSession pre-stream errors", () => { ); historyCleanup = cleanup; - const seedMessage = createMuxMessage("msg-history-seed", "assistant", "seed"); + const seedMessage = createXumMessage("msg-history-seed", "assistant", "seed"); expect((await historyService.appendToHistory(workspaceId, seedMessage)).success).toBe(true); const historyResult = await historyService.getHistoryFromLatestBoundary(workspaceId); @@ -694,9 +694,9 @@ describe("AgentSession pre-stream errors", () => { const { session, cleanup, historyService } = await createReplaySessionHarness(workspaceId); historyCleanup = cleanup; - const firstMessage = createMuxMessage("msg-history-a", "user", "first"); - const secondMessage = createMuxMessage("msg-history-b", "assistant", "second"); - const thirdMessage = createMuxMessage("msg-history-c", "assistant", "third"); + const firstMessage = createXumMessage("msg-history-a", "user", "first"); + const secondMessage = createXumMessage("msg-history-b", "assistant", "second"); + const thirdMessage = createXumMessage("msg-history-c", "assistant", "third"); expect((await historyService.appendToHistory(workspaceId, firstMessage)).success).toBe(true); expect((await historyService.appendToHistory(workspaceId, secondMessage)).success).toBe(true); @@ -771,7 +771,7 @@ describe("AgentSession pre-stream errors", () => { expect(caughtUp).toBeDefined(); expect(caughtUp?.replay).toBe("full"); - const replayedMessageIds = events.filter(isMuxMessage).map((message) => message.id); + const replayedMessageIds = events.filter(isXumMessage).map((message) => message.id); expect(replayedMessageIds).toContain(persistedFirst.id); expect(replayedMessageIds).toContain(persistedThird.id); expect(replayedMessageIds).not.toContain(persistedSecond.id); @@ -792,7 +792,7 @@ describe("AgentSession pre-stream errors", () => { ); historyCleanup = cleanup; - const seededMessage = createMuxMessage("msg-history-d", "assistant", "seed"); + const seededMessage = createXumMessage("msg-history-d", "assistant", "seed"); expect((await historyService.appendToHistory(workspaceId, seededMessage)).success).toBe(true); const historyResult = await historyService.getHistoryFromLatestBoundary(workspaceId); @@ -851,7 +851,7 @@ describe("AgentSession pre-stream errors", () => { }); historyCleanup = cleanup; - const placeholder = createMuxMessage("msg-history-stream-events", "assistant", "placeholder"); + const placeholder = createXumMessage("msg-history-stream-events", "assistant", "placeholder"); expect((await historyService.appendToHistory(workspaceId, placeholder)).success).toBe(true); const historyResult = await historyService.getHistoryFromLatestBoundary(workspaceId); @@ -871,7 +871,7 @@ describe("AgentSession pre-stream errors", () => { throw new Error("Expected placeholder row to include historySequence"); } - const partial = createMuxMessage("msg-partial-stream-events", "assistant", "partial", { + const partial = createXumMessage("msg-partial-stream-events", "assistant", "partial", { historySequence: placeholderHistorySequence, }); expect((await historyService.writePartial(workspaceId, partial)).success).toBe(true); @@ -930,7 +930,7 @@ describe("AgentSession pre-stream errors", () => { ); historyCleanup = cleanup; - const placeholder = createMuxMessage("msg-history-placeholder", "assistant", "placeholder"); + const placeholder = createXumMessage("msg-history-placeholder", "assistant", "placeholder"); expect((await historyService.appendToHistory(workspaceId, placeholder)).success).toBe(true); const historyResult = await historyService.getHistoryFromLatestBoundary(workspaceId); @@ -950,7 +950,7 @@ describe("AgentSession pre-stream errors", () => { throw new Error("Expected placeholder row to include historySequence"); } - const partial = createMuxMessage("msg-partial-prepayload", "assistant", "partial", { + const partial = createXumMessage("msg-partial-prepayload", "assistant", "partial", { historySequence: placeholderHistorySequence, }); expect((await historyService.writePartial(workspaceId, partial)).success).toBe(true); @@ -989,7 +989,7 @@ describe("AgentSession pre-stream errors", () => { await createReplaySessionHarness(workspaceId); historyCleanup = cleanup; - const inFlightPlaceholder = createMuxMessage("msg-stream-1", "assistant", "partial"); + const inFlightPlaceholder = createXumMessage("msg-stream-1", "assistant", "partial"); const appendPlaceholder = await historyService.appendToHistory( workspaceId, inFlightPlaceholder @@ -1013,7 +1013,7 @@ describe("AgentSession pre-stream errors", () => { throw new Error("Expected persisted placeholder to have historySequence"); } - const finalizedMessage = createMuxMessage("msg-stream-1", "assistant", "finalized", { + const finalizedMessage = createXumMessage("msg-stream-1", "assistant", "finalized", { historySequence: placeholderHistorySequence, }); const updateResult = await historyService.updateHistory(workspaceId, finalizedMessage); @@ -1050,7 +1050,7 @@ describe("AgentSession pre-stream errors", () => { expect(caughtUp?.replay).toBe("since"); const replayedMessages = events - .filter(isMuxMessage) + .filter(isXumMessage) .filter((event) => event.role === "assistant"); expect(replayedMessages).toHaveLength(1); expect(replayedMessages[0].id).toBe("msg-stream-1"); diff --git a/src/node/services/agentSession.queueDispatch.test.ts b/src/node/services/agentSession.queueDispatch.test.ts index fa4a827d7b..3fe3e8a737 100644 --- a/src/node/services/agentSession.queueDispatch.test.ts +++ b/src/node/services/agentSession.queueDispatch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, mock, spyOn, test } from "bun:test"; -import type { MuxMessageMetadata } from "@/common/types/message"; +import type { XumMessageMetadata } from "@/common/types/message"; import { Err, Ok } from "@/common/types/result"; import type { WorkspaceGoalService } from "./workspaceGoalService"; import { createAgentSessionHarness } from "./agentSession.testHarness"; @@ -65,10 +65,10 @@ describe("AgentSession queued message tool-call dispatch", () => { const sessionHolder: { current?: { hasQueuedOrDispatchingEntry( - continuationMetadata?: Extract + continuationMetadata?: Extract ): boolean; hasPendingWorkspaceTurnContinuation( - continuationMetadata: Extract + continuationMetadata: Extract ): boolean; }; } = {}; diff --git a/src/node/services/agentSession.startupAutoRetry.test.ts b/src/node/services/agentSession.startupAutoRetry.test.ts index 80d73889d2..bbec073497 100644 --- a/src/node/services/agentSession.startupAutoRetry.test.ts +++ b/src/node/services/agentSession.startupAutoRetry.test.ts @@ -9,7 +9,7 @@ import type { HistoryService } from "./historyService"; import type { Config } from "@/node/config"; import type { InitStateManager } from "./initStateManager"; import type { WorkspaceChatMessage, SendMessageOptions } from "@/common/orpc/types"; -import { createMuxMessage, pickStartupRetrySendOptions } from "@/common/types/message"; +import { createXumMessage, pickStartupRetrySendOptions } from "@/common/types/message"; import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace"; import type { WorkspaceMetadata } from "@/common/types/workspace"; import { Ok } from "@/common/types/result"; @@ -80,7 +80,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Hello from interrupted turn", { + createXumMessage("user-1", "user", "Hello from interrupted turn", { timestamp: Date.now(), toolPolicy: [{ regex_match: ".*", action: "disable" }], disableWorkspaceAgents: true, @@ -90,7 +90,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendSnapshotResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("snapshot-1", "user", "", { + createXumMessage("snapshot-1", "user", "", { timestamp: Date.now(), synthetic: true, fileAtMentionSnapshot: ["token"], @@ -132,7 +132,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage( + createXumMessage( "completed-subagent-report", "user", formatSubagentReportEnvelope({ @@ -164,13 +164,13 @@ describe("AgentSession startup auto-retry recovery", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("original-user", "user", "Continue the original task", { + createXumMessage("original-user", "user", "Continue the original task", { timestamp: Date.now(), }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage( + createXumMessage( "completed-subagent-report", "user", formatSubagentReportEnvelope({ @@ -185,7 +185,7 @@ describe("AgentSession startup auto-retry recovery", () => { ); await historyService.writePartial( workspaceId, - createMuxMessage("assistant-partial", "assistant", "Interrupted response", { + createXumMessage("assistant-partial", "assistant", "Interrupted response", { timestamp: Date.now(), partial: true, }) @@ -208,13 +208,13 @@ describe("AgentSession startup auto-retry recovery", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("original-user", "user", "Continue the original task", { + createXumMessage("original-user", "user", "Continue the original task", { timestamp: Date.now(), }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage( + createXumMessage( "hidden-completed-subagent-report", "user", formatSubagentReportEnvelope({ @@ -250,7 +250,7 @@ describe("AgentSession startup auto-retry recovery", () => { }; const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Complete the workspace turn", { + createXumMessage("user-1", "user", "Complete the workspace turn", { timestamp: Date.now(), retrySendOptions: { model: "openai:gpt-4o", agentId: "exec" }, muxMetadata, @@ -290,7 +290,7 @@ describe("AgentSession startup auto-retry recovery", () => { }; const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Use the workflow result", { + createXumMessage("user-1", "user", "Use the workflow result", { timestamp: Date.now(), retrySendOptions: pickStartupRetrySendOptions({ model: "openai:gpt-4o", @@ -327,7 +327,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted while startup was busy", { + createXumMessage("user-1", "user", "Interrupted while startup was busy", { timestamp: Date.now(), }) ); @@ -371,7 +371,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted while history read failed", { + createXumMessage("user-1", "user", "Interrupted while history read failed", { timestamp: Date.now(), }) ); @@ -425,7 +425,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted while history is unavailable", { + createXumMessage("user-1", "user", "Interrupted while history is unavailable", { timestamp: Date.now(), }) ); @@ -469,7 +469,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted while history keeps failing", { + createXumMessage("user-1", "user", "Interrupted while history keeps failing", { timestamp: Date.now(), }) ); @@ -565,7 +565,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted with custom send options", { + createXumMessage("user-1", "user", "Interrupted with custom send options", { timestamp: Date.now(), kind: GOAL_CONTINUATION_KIND, retrySendOptions: { @@ -649,7 +649,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted child task turn", { + createXumMessage("user-1", "user", "Interrupted child task turn", { timestamp: Date.now(), retrySendOptions: { model: "openai:gpt-5.5", @@ -691,7 +691,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted before reconnect", { + createXumMessage("user-1", "user", "Interrupted before reconnect", { timestamp: Date.now(), }) ); @@ -734,7 +734,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted before restart", { + createXumMessage("user-1", "user", "Interrupted before restart", { timestamp: Date.now(), }) ); @@ -772,7 +772,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted before migration", { + createXumMessage("user-1", "user", "Interrupted before migration", { timestamp: Date.now(), }) ); @@ -818,7 +818,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted before restart", { + createXumMessage("user-1", "user", "Interrupted before restart", { timestamp: Date.now(), }) ); @@ -865,7 +865,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-1", "user", "Interrupted prompt", { + createXumMessage("user-1", "user", "Interrupted prompt", { timestamp: Date.now(), }) ); @@ -1233,7 +1233,7 @@ describe("AgentSession startup auto-retry recovery", () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("summary-follow-up", "assistant", "Compaction summary", { + createXumMessage("summary-follow-up", "assistant", "Compaction summary", { muxMetadata: { type: "compaction-summary", pendingFollowUp: { @@ -1649,7 +1649,7 @@ describe("AgentSession startup auto-retry recovery", () => { const writePartialResult = await historyService.writePartial( workspaceId, - createMuxMessage( + createXumMessage( "assistant-1", "assistant", "", diff --git a/src/node/services/agentSession.testHarness.ts b/src/node/services/agentSession.testHarness.ts index e4fce6b3fc..2138ade5a2 100644 --- a/src/node/services/agentSession.testHarness.ts +++ b/src/node/services/agentSession.testHarness.ts @@ -2,7 +2,7 @@ import { mock } from "bun:test"; import { EventEmitter } from "events"; import type { WorkspaceChatMessage } from "@/common/orpc/types"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { Ok } from "@/common/types/result"; import type { Config } from "@/node/config"; import type { AIService } from "@/node/services/aiService"; @@ -48,7 +48,7 @@ function createMockAiService(args?: { emitter?: EventEmitter; overrides?: Partia isStreaming: mock((_workspaceId: string) => false), stopStream: mock((_workspaceId: string) => Promise.resolve(Ok(undefined))), getStreamInfo: mock((_workspaceId: string) => null), - streamMessage: mock((_history: MuxMessage[]) => + streamMessage: mock((_history: XumMessage[]) => Promise.resolve(Ok(undefined)) ) as unknown as AIService["streamMessage"], ...args?.overrides, diff --git a/src/node/services/agentSession.thinkingOverride.test.ts b/src/node/services/agentSession.thinkingOverride.test.ts index e4ccca47b7..858314bff0 100644 --- a/src/node/services/agentSession.thinkingOverride.test.ts +++ b/src/node/services/agentSession.thinkingOverride.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, mock } from "bun:test"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { Ok, Err } from "@/common/types/result"; import type { AIService, StreamMessageOptions } from "@/node/services/aiService"; import { createAgentSessionHarness } from "./agentSession.testHarness"; @@ -133,7 +133,7 @@ describe("AgentSession.setActiveTurnThinkingLevel", () => { }); it("clears the holder when an onAccepted failure aborts the turn before streaming", async () => { - const streamMessage = mock((_history: MuxMessage[]) => Promise.resolve(Ok(undefined))); + const streamMessage = mock((_history: XumMessage[]) => Promise.resolve(Ok(undefined))); const { session, cleanup } = await createAgentSessionHarness({ workspaceId: "thinking-override-onaccepted-failure", aiServiceOverrides: { diff --git a/src/node/services/agentSession.ts b/src/node/services/agentSession.ts index 2359feab9d..b844e8defa 100644 --- a/src/node/services/agentSession.ts +++ b/src/node/services/agentSession.ts @@ -76,7 +76,7 @@ import { } from "@/common/utils/thinking/policy"; import type { ActiveTurnThinkingOverride } from "@/node/services/thinkingOverride"; import { - createMuxMessage, + createXumMessage, dedupeAgentSkillRefs, dedupeMcpPromptRefs, filterOrphanedMcpPromptSnapshots, @@ -89,9 +89,9 @@ import { type AgentSkillReference, isSyntheticSnapshotUserMessage, type CompactionFollowUpRequest, - type MuxMessageMetadata, - type MuxFilePart, - type MuxMessage, + type XumMessageMetadata, + type XumFilePart, + type XumMessage, type ReviewNoteDataForDisplay, type StartupRetrySendOptions, } from "@/common/types/message"; @@ -189,7 +189,7 @@ interface CompactionRequestMetadata { text?: string; imageParts?: FilePart[]; reviews?: ReviewNoteDataForDisplay[]; - muxMetadata?: MuxMessageMetadata; + muxMetadata?: XumMessageMetadata; model?: string; agentId?: string; mode?: "exec" | "plan"; // Legacy: older versions stored mode instead of agentId @@ -237,7 +237,7 @@ const PDF_MEDIA_TYPE = "application/pdf"; const ACP_PROMPT_ID_METADATA_KEY = "acpPromptId"; const ACP_DELEGATED_TOOLS_METADATA_KEY = "acpDelegatedTools"; -function extractAgentSkillRefs(metadata: MuxMessageMetadata | undefined): AgentSkillReference[] { +function extractAgentSkillRefs(metadata: XumMessageMetadata | undefined): AgentSkillReference[] { if (!metadata) return []; const refs = sanitizeAgentSkillRefs(metadata.agentSkillRefs); @@ -314,16 +314,16 @@ function extractAcpDelegatedTools(muxMetadata: unknown): string[] | undefined { (muxMetadata as Record)[ACP_DELEGATED_TOOLS_METADATA_KEY] ); } -type WorkspaceTurnMuxMetadata = Extract; +type WorkspaceTurnXumMetadata = Extract; -function getWorkspaceTurnMuxMetadata(muxMetadata: unknown): WorkspaceTurnMuxMetadata | undefined { - const metadata = muxMetadata as MuxMessageMetadata | undefined; +function getWorkspaceTurnXumMetadata(muxMetadata: unknown): WorkspaceTurnXumMetadata | undefined { + const metadata = muxMetadata as XumMessageMetadata | undefined; return metadata?.type === "workspace-turn-task" ? metadata : undefined; } function hasSameWorkspaceTurnCorrelation( - first: WorkspaceTurnMuxMetadata | undefined, - second: WorkspaceTurnMuxMetadata | undefined + first: WorkspaceTurnXumMetadata | undefined, + second: WorkspaceTurnXumMetadata | undefined ): boolean { return ( first != null && @@ -354,8 +354,8 @@ function hasSameWorkspaceTurnCorrelation( * restarts. */ export function inheritOpenWorkspaceTurnMetadata( - messages: readonly MuxMessage[] -): Extract | undefined { + messages: readonly XumMessage[] +): Extract | undefined { for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; const muxMetadata = message.metadata?.muxMetadata; @@ -713,10 +713,10 @@ export class AgentSession { * the dequeue→stream-start window without consulting stale stream context. */ private dispatchingQueuedEntry = false; - private dispatchingQueuedEntryMuxMetadata?: unknown; + private dispatchingQueuedEntryXumMetadata?: unknown; /** Correlation of the direct send currently in the PREPARING phase, if any. */ - private preparingWorkspaceTurnMetadata?: WorkspaceTurnMuxMetadata; + private preparingWorkspaceTurnMetadata?: WorkspaceTurnXumMetadata; /** Context needed to retry the current stream (cleared on stream end/abort/error). */ private activeStreamContext?: { @@ -726,7 +726,7 @@ export class AgentSession { openaiTruncationModeOverride?: "auto" | "disabled"; providersConfig: ProvidersConfigMap | null; goalKind?: GoalSyntheticMessageKind; - workspaceTurnMetadata?: Extract; + workspaceTurnMetadata?: Extract; }; private activeCompactionRequest?: { @@ -1420,7 +1420,7 @@ export class AgentSession { return parsed.success ? parsed.data : undefined; } - private isPendingAskUserQuestion(message: MuxMessage | null | undefined): boolean { + private isPendingAskUserQuestion(message: XumMessage | null | undefined): boolean { if (!message || message.role !== "assistant") { return false; } @@ -1433,7 +1433,7 @@ export class AgentSession { ); } - private isSyntheticGoalPauseBoundaryMessage(message: MuxMessage): boolean { + private isSyntheticGoalPauseBoundaryMessage(message: XumMessage): boolean { return ( message.role === "user" && message.metadata?.synthetic === true && @@ -1442,7 +1442,7 @@ export class AgentSession { } private getEditTruncateTargetFromMessages( - messages: readonly MuxMessage[], + messages: readonly XumMessage[], editMessageId: string ): string | undefined { const editIndex = messages.findIndex((message) => message.id === editMessageId); @@ -1474,7 +1474,7 @@ export class AgentSession { } } - const fullHistory: MuxMessage[] = []; + const fullHistory: XumMessage[] = []; const fullHistoryResult = await this.historyService.iterateFullHistory( this.workspaceId, "forward", @@ -1489,7 +1489,7 @@ export class AgentSession { return this.getEditTruncateTargetFromMessages(fullHistory, editMessageId) ?? editMessageId; } - private getLastNonSystemHistoryMessage(historyTail: MuxMessage[]): MuxMessage | undefined { + private getLastNonSystemHistoryMessage(historyTail: XumMessage[]): XumMessage | undefined { for (let index = historyTail.length - 1; index >= 0; index -= 1) { const candidate = historyTail[index]; if (candidate.role === "system") { @@ -1597,7 +1597,7 @@ export class AgentSession { return metadataResult.data; } - private isVisibleCompletedSubagentReportMessage(message: MuxMessage): boolean { + private isVisibleCompletedSubagentReportMessage(message: XumMessage): boolean { if ( message.role !== "user" || message.metadata?.synthetic !== true || @@ -1612,7 +1612,7 @@ export class AgentSession { return parseSubagentReportEnvelope(text)?.status === "completed"; } - private shouldUseUserMessageForRetry(message: MuxMessage): boolean { + private shouldUseUserMessageForRetry(message: XumMessage): boolean { if (message.role !== "user") { return false; } @@ -1648,12 +1648,12 @@ export class AgentSession { * a fresh choice, and nothing here is promoted into new defaults. */ private async deriveStartupAutoRetryRequest(params: { - partial: MuxMessage | null; - historyTail: MuxMessage[]; + partial: XumMessage | null; + historyTail: XumMessage[]; }): Promise { const lastUserMessage = [...params.historyTail] .reverse() - .find((message): message is MuxMessage & { role: "user" } => + .find((message): message is XumMessage & { role: "user" } => this.shouldUseUserMessageForRetry(message) ); @@ -1663,7 +1663,7 @@ export class AgentSession { : [...params.historyTail] .reverse() .find( - (message): message is MuxMessage & { role: "assistant" } => + (message): message is XumMessage & { role: "assistant" } => message.role === "assistant" ); @@ -1741,10 +1741,10 @@ export class AgentSession { const persistedProviderOptions = persistedRetrySendOptions?.providerOptions; const persistedExperiments = persistedRetrySendOptions?.experiments; - const lastUserMuxMetadata = lastUserMessage?.metadata?.muxMetadata; - if (isCompactionRequestMetadata(lastUserMuxMetadata)) { + const lastUserXumMetadata = lastUserMessage?.metadata?.muxMetadata; + if (isCompactionRequestMetadata(lastUserXumMetadata)) { const compactionModel = - this.normalizeStartupModel(lastUserMuxMetadata.parsed.model) ?? baseModel; + this.normalizeStartupModel(lastUserXumMetadata.parsed.model) ?? baseModel; const requestedThinkingLevel = baseThinkingLevel ?? coerceThinkingLevel(compactSettings?.thinkingLevel) ?? "off"; @@ -1759,8 +1759,8 @@ export class AgentSession { thinkingLevel: requestedThinkingLevel, ...(requestedReasoningMode != null ? { reasoningMode: requestedReasoningMode } : {}), maxOutputTokens: - typeof lastUserMuxMetadata.parsed.maxOutputTokens === "number" - ? lastUserMuxMetadata.parsed.maxOutputTokens + typeof lastUserXumMetadata.parsed.maxOutputTokens === "number" + ? lastUserXumMetadata.parsed.maxOutputTokens : persistedMaxOutputTokens, toolPolicy: [{ regex_match: ".*", action: "disable" }], allowAgentSetGoal: persistedAllowAgentSetGoal, @@ -1786,17 +1786,17 @@ export class AgentSession { return compactionRequest; } - const workspaceTurnMuxMetadata = - lastUserMuxMetadata?.type === "workspace-turn-task" - ? lastUserMuxMetadata + const workspaceTurnXumMetadata = + lastUserXumMetadata?.type === "workspace-turn-task" + ? lastUserXumMetadata : persistedRetrySendOptions?.muxMetadata; const retryRequest: StartupRetrySendOptions = { model: baseModel, agentId: baseAgentId, }; - if (workspaceTurnMuxMetadata != null) { - retryRequest.muxMetadata = workspaceTurnMuxMetadata; + if (workspaceTurnXumMetadata != null) { + retryRequest.muxMetadata = workspaceTurnXumMetadata; } if (baseThinkingLevel) { retryRequest.thinkingLevel = baseThinkingLevel; @@ -1946,7 +1946,7 @@ export class AgentSession { const startupRetryUserMessage = [...historyResult.data] .reverse() - .find((message): message is MuxMessage & { role: "user" } => + .find((message): message is XumMessage & { role: "user" } => this.shouldUseUserMessageForRetry(message) ); @@ -2779,17 +2779,17 @@ export class AgentSession { // preserve the original message's attachments. // Only search the current compaction epoch — edits of pre-boundary messages are // blocked (the frontend only shows post-boundary messages). - let preservedEditFileParts: MuxFilePart[] | undefined; + let preservedEditFileParts: XumFilePart[] | undefined; if (editMessageId && fileParts === undefined) { const historyResult = await this.historyService.getHistoryFromLatestBoundary( this.workspaceId ); if (historyResult.success) { - const targetMessage: MuxMessage | undefined = historyResult.data.find( + const targetMessage: XumMessage | undefined = historyResult.data.find( (msg) => msg.id === editMessageId ); const fileParts = targetMessage?.parts.filter( - (part): part is MuxFilePart => part.type === "file" + (part): part is XumFilePart => part.type === "file" ); if (fileParts && fileParts.length > 0) { preservedEditFileParts = fileParts; @@ -2995,13 +2995,13 @@ export class AgentSession { // toolPolicy is properly typed via Zod schema inference const typedToolPolicy = options?.toolPolicy; // muxMetadata is z.any() in schema - cast to proper type - const typedMuxMetadata = options?.muxMetadata as MuxMessageMetadata | undefined; + const typedXumMetadata = options?.muxMetadata as XumMessageMetadata | undefined; const acpPromptId = - normalizeAcpPromptId(options?.acpPromptId) ?? extractAcpPromptId(typedMuxMetadata); + normalizeAcpPromptId(options?.acpPromptId) ?? extractAcpPromptId(typedXumMetadata); const delegatedToolNames = normalizeDelegatedToolNames(options?.delegatedToolNames) ?? - extractAcpDelegatedTools(typedMuxMetadata); - const isCompactionRequest = isCompactionRequestMetadata(typedMuxMetadata); + extractAcpDelegatedTools(typedXumMetadata); + const isCompactionRequest = isCompactionRequestMetadata(typedXumMetadata); // Internal callers can force Copilot billing attribution for non-user turns // (task orchestration, compaction, auto-resume, etc.). @@ -3014,7 +3014,7 @@ export class AgentSession { ...(delegatedToolNames != null ? { delegatedToolNames } : {}), }); - const userMessage = createMuxMessage( + const userMessage = createXumMessage( messageId, "user", message, @@ -3023,7 +3023,7 @@ export class AgentSession { toolPolicy: typedToolPolicy, disableWorkspaceAgents: options?.disableWorkspaceAgents, retrySendOptions: pickStartupRetrySendOptions(optionsForStream, agentInitiated, goalKind), - muxMetadata: typedMuxMetadata, // Pass through frontend metadata as black-box + muxMetadata: typedXumMetadata, // Pass through frontend metadata as black-box ...(acpPromptId != null ? { acpPromptId } : {}), ...(goalKind != null ? { kind: goalKind } : {}), // Auto-resume and other system-generated messages are synthetic + UI-visible @@ -3054,7 +3054,7 @@ export class AgentSession { // the follow-up content sent after compaction completes. This avoids duplicating the user // turn in model context (the compaction would otherwise summarize a transcript that already // contains the new prompt, then replay it again post-compaction). - let autoCompactionMessage: MuxMessage | null = null; + let autoCompactionMessage: XumMessage | null = null; if (!isCompactionRequest && !editMessageId) { // Seed usage state from persisted history on the first send after restart // so the compaction monitor can detect context limits even before any live @@ -3094,9 +3094,9 @@ export class AgentSession { // history now, because the correlated queue-cut assistant will be // hidden behind the new boundary when the follow-up dispatches. let inheritedWorkspaceTurnMetadata: - | Extract + | Extract | undefined; - if (typedMuxMetadata?.type === "bash-monitor-wake") { + if (typedXumMetadata?.type === "bash-monitor-wake") { const preCompactionHistory = await this.historyService.getHistoryFromLatestBoundary( this.workspaceId ); @@ -3114,7 +3114,7 @@ export class AgentSession { fileParts: followUpFileParts, agentInitiated, goalKind, - muxMetadata: typedMuxMetadata, + muxMetadata: typedXumMetadata, workspaceTurnMetadata: inheritedWorkspaceTurnMetadata, }); @@ -3131,7 +3131,7 @@ export class AgentSession { reason: "on-send", }); - autoCompactionMessage = createMuxMessage( + autoCompactionMessage = createXumMessage( createUserMessageId(), "user", autoCompactionRequest.messageText, @@ -3181,16 +3181,16 @@ export class AgentSession { // On on-send compaction paths, snapshots are deferred with the follow-up turn. const shouldPersistTurnSnapshots = autoCompactionMessage === null; - let skillSnapshotMessages: MuxMessage[] = []; - let mcpPromptSnapshotMessages: MuxMessage[] = []; + let skillSnapshotMessages: XumMessage[] = []; + let mcpPromptSnapshotMessages: XumMessage[] = []; if (shouldPersistTurnSnapshots) { try { skillSnapshotMessages = await this.materializeAgentSkillSnapshots( - typedMuxMetadata, + typedXumMetadata, options?.disableWorkspaceAgents ); mcpPromptSnapshotMessages = await this.materializeMcpPromptSnapshots( - typedMuxMetadata, + typedXumMetadata, userMessage.id, cancelSignal ); @@ -3371,7 +3371,7 @@ export class AgentSession { const preparedTurnAbortController = new AbortController(); this.activePreparedTurnAbortController = preparedTurnAbortController; - this.preparingWorkspaceTurnMetadata = getWorkspaceTurnMuxMetadata(optionsForStream.muxMetadata); + this.preparingWorkspaceTurnMetadata = getWorkspaceTurnXumMetadata(optionsForStream.muxMetadata); this.setTurnPhase(TurnPhase.PREPARING); const startPreparedStream = async (): Promise> => { @@ -3513,7 +3513,7 @@ export class AgentSession { // A resumed attempt becomes the latest live resume request as soon as we // accept its options, even if startup fails before the stream fully begins. this.setAutoRetryResumeState(optionsForStream, internal?.agentInitiated, internal?.goalKind); - this.preparingWorkspaceTurnMetadata = getWorkspaceTurnMuxMetadata(optionsForStream.muxMetadata); + this.preparingWorkspaceTurnMetadata = getWorkspaceTurnXumMetadata(optionsForStream.muxMetadata); this.setTurnPhase(TurnPhase.PREPARING); // Open the mid-turn thinking override window for the resumed turn (after // setTurnPhase(PREPARING), which clears the holder on the IDLE transition). @@ -3706,7 +3706,7 @@ export class AgentSession { return false; } try { - const userMessage = createMuxMessage( + const userMessage = createXumMessage( createUserMessageId(), "user", trimmed, @@ -3790,8 +3790,8 @@ export class AgentSession { fileParts?: FilePart[]; agentInitiated?: boolean; goalKind?: GoalSyntheticMessageKind; - muxMetadata?: MuxMessageMetadata; - workspaceTurnMetadata?: Extract; + muxMetadata?: XumMessageMetadata; + workspaceTurnMetadata?: Extract; }): CompactionFollowUpRequest { const followUp: CompactionFollowUpRequest = { text: params.messageText, @@ -3867,7 +3867,7 @@ export class AgentSession { reason: "on-send" | "mid-stream"; }): { messageText: string; - metadata: MuxMessageMetadata; + metadata: XumMessageMetadata; sendOptions: SendMessageOptions; agentInitiated: boolean; } { @@ -3923,7 +3923,7 @@ export class AgentSession { const messageText = buildCompactionMessageText({ followUpContent }); - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", commandPrefix: "/compact", @@ -4198,7 +4198,7 @@ export class AgentSession { workspaceId: this.workspaceId, messageId: lastMsg.id, }); - const sentinelMessage = createMuxMessage(createUserMessageId(), "user", "[CONTINUE]", { + const sentinelMessage = createXumMessage(createUserMessageId(), "user", "[CONTINUE]", { timestamp: Date.now(), synthetic: true, }); @@ -4275,30 +4275,30 @@ export class AgentSession { // Bind recordFileState to this session for the propose_plan tool const recordFileState = this.fileChangeTracker.record.bind(this.fileChangeTracker); - const optionsMuxMetadata = options?.muxMetadata as MuxMessageMetadata | undefined; - const retryMuxMetadata = lastUserMessage?.metadata?.muxMetadata; + const optionsXumMetadata = options?.muxMetadata as XumMessageMetadata | undefined; + const retryXumMetadata = lastUserMessage?.metadata?.muxMetadata; // Bash-monitor-wake continuations inherit the correlation of a delegated // workspace turn that was cut mid-work by the wake's queued dispatch, so // the turn's eventual terminal stream-end can settle the parent's handle. - const streamMuxMetadata = - optionsMuxMetadata?.type === "workspace-turn-task" - ? optionsMuxMetadata - : retryMuxMetadata?.type === "workspace-turn-task" - ? retryMuxMetadata - : retryMuxMetadata?.type === "bash-monitor-wake" + const streamXumMetadata = + optionsXumMetadata?.type === "workspace-turn-task" + ? optionsXumMetadata + : retryXumMetadata?.type === "workspace-turn-task" + ? retryXumMetadata + : retryXumMetadata?.type === "bash-monitor-wake" ? inheritOpenWorkspaceTurnMetadata(requestMessages) : undefined; // Mid-stream compaction runs after the original send options have already been resolved against // history (notably bash-monitor wakes). Persist the actual correlation used by this stream so the // post-compaction continuation remains the same delegated workspace turn. if (this.activeStreamContext != null) { - this.activeStreamContext.workspaceTurnMetadata = streamMuxMetadata; + this.activeStreamContext.workspaceTurnMetadata = streamXumMetadata; } const acpPromptId = - normalizeAcpPromptId(options?.acpPromptId) ?? extractAcpPromptId(optionsMuxMetadata); + normalizeAcpPromptId(options?.acpPromptId) ?? extractAcpPromptId(optionsXumMetadata); const delegatedToolNames = normalizeDelegatedToolNames(options?.delegatedToolNames) ?? - extractAcpDelegatedTools(optionsMuxMetadata); + extractAcpDelegatedTools(optionsXumMetadata); const streamResult = await this.aiService.streamMessage({ messages: requestMessages, @@ -4317,7 +4317,7 @@ export class AgentSession { agentId: options?.agentId, acpPromptId, delegatedToolNames, - muxMetadata: streamMuxMetadata, + muxMetadata: streamXumMetadata, recordFileState, postCompactionAttachments, // Invoked by AIService after runtime.ensureReady() (project-scope @@ -4374,7 +4374,7 @@ export class AgentSession { } private resolveCompactionRequest( - history: MuxMessage[], + history: XumMessage[], modelString: string, options?: SendMessageOptions ): @@ -4582,7 +4582,7 @@ export class AgentSession { await this.finalizeCompactionRetry(data.messageId); this.setAutoRetryResumeState(retryOptionsForResume, retryAgentInitiated, retryGoalKind); - this.preparingWorkspaceTurnMetadata = getWorkspaceTurnMuxMetadata( + this.preparingWorkspaceTurnMetadata = getWorkspaceTurnXumMetadata( retryOptionsForResume.muxMetadata ); this.setTurnPhase(TurnPhase.PREPARING); @@ -4679,7 +4679,7 @@ export class AgentSession { await this.clearFailedAssistantMessage(data.messageId, "post-compaction-retry"); // Retry the same request, but without post-compaction injection. - this.preparingWorkspaceTurnMetadata = getWorkspaceTurnMuxMetadata(context.options?.muxMetadata); + this.preparingWorkspaceTurnMetadata = getWorkspaceTurnXumMetadata(context.options?.muxMetadata); this.setTurnPhase(TurnPhase.PREPARING); let retryResult: Result; try { @@ -4891,7 +4891,7 @@ export class AgentSession { forward("stream-start", (payload) => { if (payload.type === "stream-start") { this.dispatchingQueuedEntry = false; - this.dispatchingQueuedEntryMuxMetadata = undefined; + this.dispatchingQueuedEntryXumMetadata = undefined; this.preparingWorkspaceTurnMetadata = undefined; this.activeStreamStartedAtMs = payload.startTime; this.queuedProviderToolEndAbortInFlight = false; @@ -5390,7 +5390,7 @@ export class AgentSession { if (next === TurnPhase.IDLE) { this.dispatchingQueuedEntry = false; - this.dispatchingQueuedEntryMuxMetadata = undefined; + this.dispatchingQueuedEntryXumMetadata = undefined; this.preparingWorkspaceTurnMetadata = undefined; // Turn ended: expire any mid-turn thinking override. Safe unconditionally // because a replacement turn (e.g. an edit) only creates its holder after @@ -5647,7 +5647,7 @@ export class AgentSession { * A predecessor with the same workspace-turn correlation remains part of the * continuation chain and does not supersede the proposed report. */ - hasQueuedOrDispatchingEntry(continuationMetadata?: WorkspaceTurnMuxMetadata): boolean { + hasQueuedOrDispatchingEntry(continuationMetadata?: WorkspaceTurnXumMetadata): boolean { const hasDifferentPreparingSend = this.turnPhase === TurnPhase.PREPARING && !hasSameWorkspaceTurnCorrelation(this.preparingWorkspaceTurnMetadata, continuationMetadata); @@ -5656,8 +5656,8 @@ export class AgentSession { } if (this.dispatchingQueuedEntry) { - const dispatchingMetadata = getWorkspaceTurnMuxMetadata( - this.dispatchingQueuedEntryMuxMetadata + const dispatchingMetadata = getWorkspaceTurnXumMetadata( + this.dispatchingQueuedEntryXumMetadata ); if (!hasSameWorkspaceTurnCorrelation(dispatchingMetadata, continuationMetadata)) { return true; @@ -5692,7 +5692,7 @@ export class AgentSession { if (this.messageQueue.isNextEntryBashMonitorWake()) { return true; } - const dispatching = this.dispatchingQueuedEntryMuxMetadata as MuxMessageMetadata | undefined; + const dispatching = this.dispatchingQueuedEntryXumMetadata as XumMessageMetadata | undefined; return dispatching?.type === "bash-monitor-wake"; } @@ -5700,7 +5700,7 @@ export class AgentSession { * Whether a queued or dispatching entry continues the exact workspace-turn correlation. */ hasPendingWorkspaceTurnContinuation( - metadata: Extract + metadata: Extract ): boolean { if (hasSameWorkspaceTurnCorrelation(this.preparingWorkspaceTurnMetadata, metadata)) { return true; @@ -5716,7 +5716,7 @@ export class AgentSession { return true; } - const dispatching = this.dispatchingQueuedEntryMuxMetadata as MuxMessageMetadata | undefined; + const dispatching = this.dispatchingQueuedEntryXumMetadata as XumMessageMetadata | undefined; return ( dispatching?.type === "workspace-turn-task" && dispatching.taskHandleId === metadata.taskHandleId && @@ -5908,7 +5908,7 @@ export class AgentSession { // behind them dispatches on a later drain instead of batching into them. const { message, options, internal } = this.messageQueue.dequeueNext(); this.dispatchingQueuedEntry = true; - this.dispatchingQueuedEntryMuxMetadata = options?.muxMetadata; + this.dispatchingQueuedEntryXumMetadata = options?.muxMetadata; this.emitQueuedMessageChanged(); // Re-arm dispatch signals for the remaining entries so the stream we are @@ -5922,7 +5922,7 @@ export class AgentSession { // Set PREPARING synchronously before the async sendMessage to prevent // incoming messages from bypassing the queue during the await gap. - this.preparingWorkspaceTurnMetadata = getWorkspaceTurnMuxMetadata(options?.muxMetadata); + this.preparingWorkspaceTurnMetadata = getWorkspaceTurnXumMetadata(options?.muxMetadata); this.setTurnPhase(TurnPhase.PREPARING); void this.sendMessage(message, options, internal) @@ -5954,7 +5954,7 @@ export class AgentSession { }) .catch(() => { this.dispatchingQueuedEntry = false; - this.dispatchingQueuedEntryMuxMetadata = undefined; + this.dispatchingQueuedEntryXumMetadata = undefined; if (this.turnPhase === TurnPhase.PREPARING) { this.setTurnPhase(TurnPhase.IDLE); } @@ -6039,7 +6039,7 @@ export class AgentSession { return false; } - let summaryMessage: MuxMessage | undefined; + let summaryMessage: XumMessage | undefined; if (summaryMessageId) { const historyResult = await this.historyService.getHistoryFromLatestBoundary( this.workspaceId @@ -6150,7 +6150,7 @@ export class AgentSession { // when the compaction handoff was staged. Avoid forwarding internal-only recovery flags. const options: SendMessageOptions & { fileParts?: FilePart[]; - muxMetadata?: MuxMessageMetadata; + muxMetadata?: XumMessageMetadata; } = { model: effectiveModel, agentId: effectiveAgentId, @@ -6196,7 +6196,7 @@ export class AgentSession { return true; } - private async clearPendingFollowUpFromSummary(summaryMessage: MuxMessage): Promise { + private async clearPendingFollowUpFromSummary(summaryMessage: XumMessage): Promise { assert( summaryMessage.role === "assistant", "clearPendingFollowUpFromSummary requires an assistant summary message" @@ -6448,7 +6448,7 @@ export class AgentSession { */ private async materializeFileAtMentionsSnapshot( messageText: string - ): Promise<{ snapshotMessage: MuxMessage; materializedTokens: string[] } | null> { + ): Promise<{ snapshotMessage: XumMessage; materializedTokens: string[] } | null> { // Guard for test mocks that may not implement getWorkspaceMetadata if (typeof this.aiService.getWorkspaceMetadata !== "function") { return null; @@ -6493,7 +6493,7 @@ export class AgentSession { const blocks = materialized.map((m) => m.block).join("\n\n"); const snapshotId = createFileSnapshotMessageId(); - const snapshotMessage = createMuxMessage(snapshotId, "user", blocks, { + const snapshotMessage = createXumMessage(snapshotId, "user", blocks, { timestamp: Date.now(), synthetic: true, fileAtMentionSnapshot: tokens, @@ -6503,16 +6503,16 @@ export class AgentSession { } private async materializeMcpPromptSnapshots( - muxMetadata: MuxMessageMetadata | undefined, + muxMetadata: XumMessageMetadata | undefined, invokingMessageId: string, cancelSignal: AbortSignal | undefined - ): Promise { + ): Promise { const mcpServerManager = this.mcpServerManager; if (!mcpServerManager) return []; const refs = dedupeMcpPromptRefs(sanitizeMcpPromptRefs(muxMetadata?.mcpPromptRefs)); const snapshots = await Promise.all( - refs.map(async (ref): Promise => { + refs.map(async (ref): Promise => { try { const prompt = await mcpServerManager.getPrompt( this.workspaceId, @@ -6521,7 +6521,7 @@ export class AgentSession { ref.arguments ?? {}, cancelSignal !== undefined ? { signal: cancelSignal } : undefined ); - return createMuxMessage(createMcpPromptSnapshotMessageId(), "user", prompt.text, { + return createXumMessage(createMcpPromptSnapshotMessageId(), "user", prompt.text, { timestamp: Date.now(), synthetic: true, mcpPromptSnapshot: { @@ -6553,13 +6553,13 @@ export class AgentSession { } }) ); - return snapshots.filter((snapshot): snapshot is MuxMessage => snapshot !== null); + return snapshots.filter((snapshot): snapshot is XumMessage => snapshot !== null); } private async materializeAgentSkillSnapshots( - muxMetadata: MuxMessageMetadata | undefined, + muxMetadata: XumMessageMetadata | undefined, disableWorkspaceAgents: boolean | undefined - ): Promise { + ): Promise { const refs = extractAgentSkillRefs(muxMetadata); if (refs.length === 0) { return []; @@ -6602,7 +6602,7 @@ export class AgentSession { } } - const snapshotMessages: MuxMessage[] = []; + const snapshotMessages: XumMessage[] = []; for (const ref of refs) { const parsedName = SkillNameSchema.safeParse(ref.skillName); if (!parsedName.success) { @@ -6719,7 +6719,7 @@ export class AgentSession { const snapshotText = renderAgentSkillSnapshotText(snapshot); const snapshotId = createAgentSkillSnapshotMessageId(); snapshotMessages.push( - createMuxMessage(snapshotId, "user", snapshotText, { + createXumMessage(snapshotId, "user", snapshotText, { timestamp: Date.now(), synthetic: true, agentSkillSnapshot: { diff --git a/src/node/services/agentSession.workspaceTurnInheritance.test.ts b/src/node/services/agentSession.workspaceTurnInheritance.test.ts index c19efecd02..e0514e5dfb 100644 --- a/src/node/services/agentSession.workspaceTurnInheritance.test.ts +++ b/src/node/services/agentSession.workspaceTurnInheritance.test.ts @@ -1,6 +1,6 @@ import { describe, expect, mock, test } from "bun:test"; -import { createMuxMessage, type MuxMessage, type MuxMessageMetadata } from "@/common/types/message"; +import { createXumMessage, type XumMessage, type XumMessageMetadata } from "@/common/types/message"; import { Ok } from "@/common/types/result"; import type { AIService, StreamMessageOptions } from "@/node/services/aiService"; @@ -14,19 +14,19 @@ const correlation = { turnId: "turn", } as const; -function turnPrompt(id: string): MuxMessage { - return createMuxMessage(id, "user", "Delegated prompt", { muxMetadata: correlation }); +function turnPrompt(id: string): XumMessage { + return createXumMessage(id, "user", "Delegated prompt", { muxMetadata: correlation }); } -function cutAssistant(id: string): MuxMessage { - return createMuxMessage(id, "assistant", "Working...", { +function cutAssistant(id: string): XumMessage { + return createXumMessage(id, "assistant", "Working...", { finishReason: "tool-calls", muxMetadata: correlation, }); } -function wake(id: string): MuxMessage { - return createMuxMessage(id, "user", "A background bash monitor matched output.", { +function wake(id: string): XumMessage { + return createXumMessage(id, "user", "A background bash monitor matched output.", { muxMetadata: { type: "bash-monitor-wake", records: [] }, }); } @@ -51,7 +51,7 @@ describe("inheritOpenWorkspaceTurnMetadata", () => { test("a correlated assistant that finished with stop closes the turn", () => { const messages = [ turnPrompt("prompt"), - createMuxMessage("final", "assistant", "Final report", { + createXumMessage("final", "assistant", "Final report", { finishReason: "stop", muxMetadata: correlation, }), @@ -64,7 +64,7 @@ describe("inheritOpenWorkspaceTurnMetadata", () => { const messages = [ turnPrompt("prompt"), cutAssistant("cut"), - createMuxMessage("manual", "user", "User takes over"), + createXumMessage("manual", "user", "User takes over"), wake("wake"), ]; expect(inheritOpenWorkspaceTurnMetadata(messages)).toBeUndefined(); @@ -72,7 +72,7 @@ describe("inheritOpenWorkspaceTurnMetadata", () => { test("an uncorrelated assistant closes the chain", () => { const messages = [ - createMuxMessage("plain", "assistant", "Unrelated turn", { finishReason: "tool-calls" }), + createXumMessage("plain", "assistant", "Unrelated turn", { finishReason: "tool-calls" }), wake("wake"), ]; expect(inheritOpenWorkspaceTurnMetadata(messages)).toBeUndefined(); @@ -81,7 +81,7 @@ describe("inheritOpenWorkspaceTurnMetadata", () => { test("a partial correlated assistant does not leave the turn open", () => { const messages = [ turnPrompt("prompt"), - createMuxMessage("partial", "assistant", "Crashed mid-work", { + createXumMessage("partial", "assistant", "Crashed mid-work", { finishReason: "tool-calls", partial: true, muxMetadata: correlation, @@ -99,7 +99,7 @@ describe("inheritOpenWorkspaceTurnMetadata", () => { // On-send compaction consumed the wake: post-compaction history starts at // the summary, which carries the pre-compaction correlation stamp. const messages = [ - createMuxMessage("summary", "assistant", "Compacted context", { + createXumMessage("summary", "assistant", "Compacted context", { finishReason: "stop", muxMetadata: { type: "compaction-summary", @@ -118,7 +118,7 @@ describe("inheritOpenWorkspaceTurnMetadata", () => { test("an unstamped compaction summary closes the chain", () => { const messages = [ - createMuxMessage("summary", "assistant", "Compacted context", { + createXumMessage("summary", "assistant", "Compacted context", { finishReason: "stop", muxMetadata: { type: "compaction-summary", @@ -137,11 +137,11 @@ describe("inheritOpenWorkspaceTurnMetadata", () => { describe("AgentSession workspace-turn correlation inheritance", () => { async function sendAfterQueueCut(sendOptions: { - muxMetadata?: MuxMessageMetadata; + muxMetadata?: XumMessageMetadata; }): Promise { - let streamedMuxMetadata: StreamMessageOptions["muxMetadata"]; + let streamedXumMetadata: StreamMessageOptions["muxMetadata"]; const streamMessage = mock((opts: StreamMessageOptions) => { - streamedMuxMetadata = opts.muxMetadata; + streamedXumMetadata = opts.muxMetadata; return Promise.resolve(Ok(undefined)); }); const { session, cleanup, historyService } = await createAgentSessionHarness({ @@ -165,7 +165,7 @@ describe("AgentSession workspace-turn correlation inheritance", () => { }); expect(result.success).toBe(true); expect(streamMessage.mock.calls).toHaveLength(1); - return streamedMuxMetadata; + return streamedXumMetadata; } finally { session.dispose(); await cleanup(); diff --git a/src/node/services/agentSkills/loadedSkillSnapshots.test.ts b/src/node/services/agentSkills/loadedSkillSnapshots.test.ts index 6b125d3b1e..b144075adf 100644 --- a/src/node/services/agentSkills/loadedSkillSnapshots.test.ts +++ b/src/node/services/agentSkills/loadedSkillSnapshots.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { renderAgentSkillSnapshotText } from "@/common/utils/agentSkills/skillSnapshot"; import { extractLoadedSkillSnapshotsFromMessages } from "./loadedSkillSnapshots"; @@ -10,7 +10,7 @@ function createAgentSkillReadToolMessage(args: { skillName: string; body: string; scope?: "project" | "global" | "built-in"; -}): MuxMessage { +}): XumMessage { const scope = args.scope ?? "project"; return { id: args.id, @@ -47,9 +47,9 @@ function createSyntheticSkillSnapshotMessage(args: { skillName: string; body: string; scope?: "project" | "global" | "built-in"; -}): MuxMessage { +}): XumMessage { const scope = args.scope ?? "project"; - return createMuxMessage( + return createXumMessage( args.id, "user", renderAgentSkillSnapshotText({ diff --git a/src/node/services/agentSkills/loadedSkillSnapshots.ts b/src/node/services/agentSkills/loadedSkillSnapshots.ts index e92f337db5..90c65da2db 100644 --- a/src/node/services/agentSkills/loadedSkillSnapshots.ts +++ b/src/node/services/agentSkills/loadedSkillSnapshots.ts @@ -5,7 +5,7 @@ import assert from "@/common/utils/assert"; import { MAX_POST_COMPACTION_LOADED_SKILLS } from "@/common/constants/attachments"; import type { LoadedSkillSnapshot } from "@/common/types/attachment"; import type { AgentSkillFrontmatter, AgentSkillScope } from "@/common/types/agentSkill"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { AgentSkillPackageSchema, AgentSkillScopeSchema } from "@/common/orpc/schemas/agentSkill"; import { extractAgentSkillBodyFromSnapshotText, @@ -103,10 +103,10 @@ export function stringifyAgentSkillFrontmatter(frontmatter: AgentSkillFrontmatte return yaml; } -function getMessageTextContent(message: MuxMessage): string { +function getMessageTextContent(message: XumMessage): string { return message.parts .filter( - (part): part is Extract => part.type === "text" + (part): part is Extract => part.type === "text" ) .map((part) => part.text) .join(""); @@ -137,7 +137,7 @@ function extractLoadedSkillSnapshotFromToolOutput(output: unknown): LoadedSkillS } function extractLoadedSkillSnapshotFromSyntheticMessage( - message: MuxMessage + message: XumMessage ): LoadedSkillSnapshot | null { const snapshotMeta = message.metadata?.agentSkillSnapshot; if (!snapshotMeta) { @@ -167,7 +167,7 @@ function extractLoadedSkillSnapshotFromSyntheticMessage( }); } -function extractLoadedSkillSnapshotsFromMessage(message: MuxMessage): LoadedSkillSnapshot[] { +function extractLoadedSkillSnapshotsFromMessage(message: XumMessage): LoadedSkillSnapshot[] { const snapshots: LoadedSkillSnapshot[] = []; for (const part of message.parts) { @@ -213,7 +213,7 @@ export function mergeLoadedSkillSnapshots(snapshots: LoadedSkillSnapshot[]): Loa } export function extractLoadedSkillSnapshotsFromMessages( - messages: MuxMessage[] + messages: XumMessage[] ): LoadedSkillSnapshot[] { assert(Array.isArray(messages), "extractLoadedSkillSnapshotsFromMessages requires messages"); diff --git a/src/node/services/agentStatusService.test.ts b/src/node/services/agentStatusService.test.ts index c15a2ddd1d..839b51f717 100644 --- a/src/node/services/agentStatusService.test.ts +++ b/src/node/services/agentStatusService.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "os"; import { join } from "path"; import type { ProjectsConfig, ProjectConfig, Workspace } from "@/common/types/project"; import { Ok, Err } from "@/common/types/result"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { AGENT_STATUS_PROVIDER_FAILURE_IDLE_COOLDOWN_MS, AGENT_STATUS_PROVIDER_FAILURE_RETRY_ATTEMPTS, @@ -164,11 +164,11 @@ describe("AgentStatusService", () => { test("generates and persists a fresh AI status when chat history exists", async () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Please run the test suite") + createXumMessage("u1", "user", "Please run the test suite") ); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "Running tests now") + createXumMessage("a1", "assistant", "Running tests now") ); const service = createService(); @@ -190,7 +190,7 @@ describe("AgentStatusService", () => { // "Frozen chat" behavior: identical hash → no further LLM calls. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Idle workspace") + createXumMessage("u1", "user", "Idle workspace") ); const service = createService(); @@ -210,14 +210,14 @@ describe("AgentStatusService", () => { // suppress the very updates the feature exists to surface. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "kick off a long task") + createXumMessage("u1", "user", "kick off a long task") ); const service = createService(); await getInternals(service).runForWorkspace(workspaceId); expect(generateSpy).toHaveBeenCalledTimes(1); - const partial = createMuxMessage("a-partial", "assistant", "Reading config files"); + const partial = createXumMessage("a-partial", "assistant", "Reading config files"); await historyHandle.historyService.writePartial(workspaceId, partial); // Dedup would have suppressed this second call if the partial was missing @@ -237,11 +237,11 @@ describe("AgentStatusService", () => { // bring back the historical past-tense-while-deploying bug. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "deploy the service") + createXumMessage("u1", "user", "deploy the service") ); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "Kicking off deploy", undefined, [ + createXumMessage("a1", "assistant", "Kicking off deploy", undefined, [ { type: "dynamic-tool", toolCallId: "call-running", @@ -278,11 +278,11 @@ describe("AgentStatusService", () => { // for. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "deploy the service") + createXumMessage("u1", "user", "deploy the service") ); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "Deploying now") + createXumMessage("a1", "assistant", "Deploying now") ); const service = createService(); @@ -306,11 +306,11 @@ describe("AgentStatusService", () => { // stale "Deploying service" sidebar bug this PR exists to fix. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "deploy the service") + createXumMessage("u1", "user", "deploy the service") ); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "Deploying now") + createXumMessage("a1", "assistant", "Deploying now") ); const service = createService(); @@ -328,7 +328,7 @@ describe("AgentStatusService", () => { test("re-generates after the trailing transcript changes", async () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Initial request") + createXumMessage("u1", "user", "Initial request") ); const service = createService(); await getInternals(service).runForWorkspace(workspaceId); @@ -336,7 +336,7 @@ describe("AgentStatusService", () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", "Second request") + createXumMessage("u2", "user", "Second request") ); await getInternals(service).runForWorkspace(workspaceId); expect(generateSpy).toHaveBeenCalledTimes(2); @@ -360,7 +360,7 @@ describe("AgentStatusService", () => { ]); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Populated workspace") + createXumMessage("u1", "user", "Populated workspace") ); getAllSnapshotsMock.mockImplementation(() => Promise.resolve( @@ -387,11 +387,11 @@ describe("AgentStatusService", () => { test("idle workspaces regenerate at the idle focused/unfocused intervals", async () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Hello") + createXumMessage("u1", "user", "Hello") ); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "Hi") + createXumMessage("a1", "assistant", "Hi") ); let now = 1_000_000; @@ -405,7 +405,7 @@ describe("AgentStatusService", () => { await internals.runTick(); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", "follow-up A") + createXumMessage("u2", "user", "follow-up A") ); expect(generateSpy).toHaveBeenCalledTimes(1); @@ -424,7 +424,7 @@ describe("AgentStatusService", () => { isFocused = false; await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u3", "user", "follow-up B") + createXumMessage("u3", "user", "follow-up B") ); now += 60_000; await internals.runTick(); @@ -443,7 +443,7 @@ describe("AgentStatusService", () => { // direction. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Initial request") + createXumMessage("u1", "user", "Initial request") ); let recency = 100; @@ -464,7 +464,7 @@ describe("AgentStatusService", () => { recency = 200; await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", "Pivot to new task") + createXumMessage("u2", "user", "Pivot to new task") ); await internals.runTick(); @@ -476,7 +476,7 @@ describe("AgentStatusService", () => { now += 5_000; await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "Acknowledged") + createXumMessage("a1", "assistant", "Acknowledged") ); await internals.runTick(); @@ -487,7 +487,7 @@ describe("AgentStatusService", () => { test("does not consume a user recency bump until the pivot message reaches history", async () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Initial request") + createXumMessage("u1", "user", "Initial request") ); let recency = 100; @@ -516,7 +516,7 @@ describe("AgentStatusService", () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", "Pivot after recency") + createXumMessage("u2", "user", "Pivot after recency") ); now += 10_000; await internals.runTick(); @@ -534,7 +534,7 @@ describe("AgentStatusService", () => { // against the still-old transcript and consume the recency bump. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Initial request") + createXumMessage("u1", "user", "Initial request") ); let recency = 100; @@ -569,7 +569,7 @@ describe("AgentStatusService", () => { test("defers a first recent recency bump so startup cannot settle on stale pre-pivot history", async () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Old request before restart") + createXumMessage("u1", "user", "Old request before restart") ); let now = 1_000_000; @@ -592,7 +592,7 @@ describe("AgentStatusService", () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", "Pivot after restart") + createXumMessage("u2", "user", "Pivot after restart") ); now += 10_000; await internals.runTick(); @@ -609,11 +609,11 @@ describe("AgentStatusService", () => { ]); await historyHandle.historyService.appendToHistory( staleWorkspaceId, - createMuxMessage("u-stale", "user", "Already summarized") + createXumMessage("u-stale", "user", "Already summarized") ); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u-good", "user", "Waiting behind stale recency") + createXumMessage("u-good", "user", "Waiting behind stale recency") ); let now = 1_000_000; @@ -654,7 +654,7 @@ describe("AgentStatusService", () => { // versus the slower 30s/120s cadence for chats that aren't moving. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "kick off a long task") + createXumMessage("u1", "user", "kick off a long task") ); // Mark the workspace as currently streaming so dispatch picks the // active intervals. @@ -674,7 +674,7 @@ describe("AgentStatusService", () => { now += 5_000; await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "step one") + createXumMessage("a1", "assistant", "step one") ); await internals.runTick(); expect(generateSpy).toHaveBeenCalledTimes(1); @@ -683,7 +683,7 @@ describe("AgentStatusService", () => { now += 5_000; await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a2", "assistant", "step two") + createXumMessage("a2", "assistant", "step two") ); await internals.runTick(); expect(generateSpy).toHaveBeenCalledTimes(2); @@ -694,7 +694,7 @@ describe("AgentStatusService", () => { now += 10_000; await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("a3", "assistant", "step three") + createXumMessage("a3", "assistant", "step three") ); await internals.runTick(); expect(generateSpy).toHaveBeenCalledTimes(2); @@ -721,7 +721,7 @@ describe("AgentStatusService", () => { for (const id of ids) { await historyHandle.historyService.appendToHistory( id, - createMuxMessage(`u1-${id}`, "user", `prompt for ${id}`) + createXumMessage(`u1-${id}`, "user", `prompt for ${id}`) ); } @@ -750,7 +750,7 @@ describe("AgentStatusService", () => { // declared lifecycle. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "long-running task") + createXumMessage("u1", "user", "long-running task") ); let releaseCandidates!: () => void; @@ -779,7 +779,7 @@ describe("AgentStatusService", () => { // past the declared lifecycle. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "long-running task") + createXumMessage("u1", "user", "long-running task") ); // Two-stage gate: signal when the generator actually starts (so the @@ -818,7 +818,7 @@ describe("AgentStatusService", () => { test("drops a generated status if workspace recency advances while provider call is in flight", async () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Old task") + createXumMessage("u1", "user", "Old task") ); let recency = 100; @@ -875,7 +875,7 @@ describe("AgentStatusService", () => { // never made it to disk, silently dropping subsequent retries. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "kick off a task") + createXumMessage("u1", "user", "kick off a task") ); setSidebarStatusMock.mockImplementationOnce(() => Promise.reject(new Error("disk full"))); @@ -943,7 +943,7 @@ describe("AgentStatusService", () => { // same placeholder back and burn provider budget. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "kick off a task") + createXumMessage("u1", "user", "kick off a task") ); generateSpy.mockResolvedValueOnce( @@ -971,7 +971,7 @@ describe("AgentStatusService", () => { // After a genuine transcript change, we try again with a fresh result. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", "follow-up message") + createXumMessage("u2", "user", "follow-up message") ); await getInternals(service).runForWorkspace(workspaceId); expect(generateSpy).toHaveBeenCalledTimes(2); @@ -986,7 +986,7 @@ describe("AgentStatusService", () => { // and even repeated transient provider misses must eventually recover. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "kick off a task") + createXumMessage("u1", "user", "kick off a task") ); generateSpy.mockReset(); @@ -1050,7 +1050,7 @@ describe("AgentStatusService", () => { // recovers without requiring a new user message. await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "kick off a task") + createXumMessage("u1", "user", "kick off a task") ); generateSpy.mockResolvedValueOnce( @@ -1105,11 +1105,11 @@ describe("AgentStatusService", () => { ]); await historyHandle.historyService.appendToHistory( misconfiguredWorkspaceId, - createMuxMessage("u-bad", "user", "Misconfigured workspace") + createXumMessage("u-bad", "user", "Misconfigured workspace") ); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u-good", "user", "Healthy workspace") + createXumMessage("u-good", "user", "Healthy workspace") ); getAllSnapshotsMock.mockImplementation(() => Promise.resolve( @@ -1147,7 +1147,7 @@ describe("AgentStatusService", () => { test("pre-provider retry state does not consume a recency bump before history catches up", async () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Old misconfigured request") + createXumMessage("u1", "user", "Old misconfigured request") ); let recency = 100; getAllSnapshotsMock.mockImplementation(() => @@ -1180,7 +1180,7 @@ describe("AgentStatusService", () => { await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", "Pivot after config failure") + createXumMessage("u2", "user", "Pivot after config failure") ); now += 10_000; await internals.runTick(); @@ -1195,7 +1195,7 @@ describe("AgentStatusService", () => { ]); await historyHandle.historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "Archived chat") + createXumMessage("u1", "user", "Archived chat") ); const service = createService(); diff --git a/src/node/services/agentStatusService.ts b/src/node/services/agentStatusService.ts index c7b3e5b162..3b6fdfd900 100644 --- a/src/node/services/agentStatusService.ts +++ b/src/node/services/agentStatusService.ts @@ -16,7 +16,7 @@ import { AGENT_STATUS_TICK_INTERVAL_MS, } from "@/constants/agentStatus"; import type { Config } from "@/node/config"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { isWorkspaceArchived } from "@/common/utils/archive"; import type { AIService } from "./aiService"; import type { ExtensionMetadataService } from "./ExtensionMetadataService"; @@ -505,7 +505,7 @@ export class AgentStatusService { ); if (!result.success) return ""; - const committedMessages: MuxMessage[] = [...result.data]; + const committedMessages: XumMessage[] = [...result.data]; const partial = await this.historyService.readPartial(workspaceId); // Partial messages get an "(in progress)" role suffix so the model sees @@ -538,7 +538,7 @@ export class AgentStatusService { } } -function extractMessageText(message: MuxMessage): string { +function extractMessageText(message: XumMessage): string { return (message.parts ?? []) .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text.trim()) @@ -577,7 +577,7 @@ function summarizeToolPart(part: unknown): string | null { } function formatMessageForTranscript( - message: MuxMessage, + message: XumMessage, opts: { partial: boolean } = { partial: false } ): string { const baseRole = diff --git a/src/node/services/aiService.test.ts b/src/node/services/aiService.test.ts index 4790dce262..bf155a0f12 100644 --- a/src/node/services/aiService.test.ts +++ b/src/node/services/aiService.test.ts @@ -10,7 +10,7 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:te import { AIService, prepareProviderRequestMessages, - resolveMuxProjectRootForHostFs, + resolveXumProjectRootForHostFs, } from "./aiService"; import { discoverAvailableSubagentsForToolContext } from "./streamContextBuilder"; import { @@ -41,8 +41,8 @@ import { CODEX_ENDPOINT } from "@/common/constants/codexOAuth"; import { addInterruptedSentinel } from "@/browser/utils/messages/modelMessageTransform"; import { buildWorkflowRunCardMessage } from "@/common/utils/workflowRunMessages"; import { jsonSchema, tool, type LanguageModel, type Tool } from "ai"; -import { createMuxMessage } from "@/common/types/message"; -import type { ModelMessage, MuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; +import type { ModelMessage, XumMessage } from "@/common/types/message"; import type { XumToolScope } from "@/common/types/toolScope"; import type { WorkspaceMetadata } from "@/common/types/workspace"; import { uniqueSuffix } from "@/common/utils/hasher"; @@ -414,14 +414,14 @@ function stubCommonStreamMessageDependencies(args: { describe("prepareProviderRequestMessages", () => { it("slices at reset boundaries before filtering empty assistant messages", () => { - const oldMessage = createMuxMessage("old-user", "user", "old context", { + const oldMessage = createXumMessage("old-user", "user", "old context", { historySequence: 1, }); - const resetBoundary = createMuxMessage("reset-boundary", "assistant", "", { + const resetBoundary = createXumMessage("reset-boundary", "assistant", "", { historySequence: 2, contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }); - const newMessage = createMuxMessage("new-user", "user", "new context", { + const newMessage = createXumMessage("new-user", "user", "new context", { historySequence: 3, }); @@ -436,7 +436,7 @@ describe("prepareProviderRequestMessages", () => { }); it("filters workflow display rows while keeping provider-visible workflow results", () => { - const trigger = createMuxMessage("workflow-command", "user", "/shallow-review mux", { + const trigger = createXumMessage("workflow-command", "user", "/shallow-review mux", { historySequence: 1, muxMetadata: { type: "workflow-trigger-display", @@ -456,7 +456,7 @@ describe("prepareProviderRequestMessages", () => { uiVisible: true, muxMetadata: { type: "workflow-run-card-display", runId: "wfr_1" }, }; - const result = createMuxMessage( + const result = createXumMessage( "workflow-result", "user", "/shallow-review mux\n\n{}", @@ -470,7 +470,7 @@ describe("prepareProviderRequestMessages", () => { }, } ); - const nextUser = createMuxMessage("next-user", "user", "continue normal work", { + const nextUser = createXumMessage("next-user", "user", "continue normal work", { historySequence: 4, }); @@ -508,7 +508,7 @@ describe("AIService", () => { }); }); -describe("resolveMuxProjectRootForHostFs", () => { +describe("resolveXumProjectRootForHostFs", () => { const projectPath = "/home/user/projects/my-app"; const workspacePath = "/home/user/.mux/src/my-app/feature-branch"; @@ -523,14 +523,14 @@ describe("resolveMuxProjectRootForHostFs", () => { } it("returns workspacePath for local runtime", () => { - expect(resolveMuxProjectRootForHostFs(createMetadata({ type: "local" }), workspacePath)).toBe( + expect(resolveXumProjectRootForHostFs(createMetadata({ type: "local" }), workspacePath)).toBe( workspacePath ); }); it("returns workspacePath for worktree runtime", () => { expect( - resolveMuxProjectRootForHostFs( + resolveXumProjectRootForHostFs( createMetadata({ type: "worktree", srcBaseDir: "/home/user/.mux/src" }), workspacePath ) @@ -539,7 +539,7 @@ describe("resolveMuxProjectRootForHostFs", () => { it("returns workspacePath for devcontainer runtime", () => { expect( - resolveMuxProjectRootForHostFs( + resolveXumProjectRootForHostFs( createMetadata({ type: "devcontainer", configPath: ".devcontainer/devcontainer.json" }), workspacePath ) @@ -548,7 +548,7 @@ describe("resolveMuxProjectRootForHostFs", () => { it("returns projectPath for ssh runtime", () => { expect( - resolveMuxProjectRootForHostFs( + resolveXumProjectRootForHostFs( createMetadata({ type: "ssh", host: "remote", @@ -561,7 +561,7 @@ describe("resolveMuxProjectRootForHostFs", () => { it("returns projectPath for docker runtime", () => { expect( - resolveMuxProjectRootForHostFs( + resolveXumProjectRootForHostFs( createMetadata({ type: "docker", image: "ubuntu:22.04" }), "/src" ) @@ -1336,7 +1336,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { workspaceId: string ): Promise>> { const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -1406,7 +1406,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { } as unknown as WorkspaceGoalService; const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -1432,7 +1432,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { } as unknown as WorkspaceGoalService; const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -1461,7 +1461,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { } as unknown as WorkspaceGoalService; const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -1496,7 +1496,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString: KNOWN_MODELS.SONNET.id, thinkingLevel: "off", @@ -1512,7 +1512,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { throw new Error("Expected modelFallback options on startStream"); } - const continuationAssistant: MuxMessage = { + const continuationAssistant: XumMessage = { id: "assistant-partial", role: "assistant", metadata: { partial: true, historySequence: 2 }, @@ -1594,7 +1594,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { ); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString: sourceModel, thinkingLevel: "off", @@ -1687,7 +1687,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { stubPerModelRouteResolution(harness.service); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId: options.workspaceId, modelString: options.sourceModel, thinkingLevel: "off", @@ -1847,7 +1847,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString: sourceModel, thinkingLevel: "high", @@ -1951,7 +1951,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString, thinkingLevel: "off", @@ -2017,7 +2017,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString, thinkingLevel: "off", @@ -2080,7 +2080,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString, thinkingLevel: "off", @@ -2146,7 +2146,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString, thinkingLevel: "off", @@ -2219,7 +2219,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "fix the issue")], + messages: [createXumMessage("latest-user", "user", "fix the issue")], workspaceId, modelString, thinkingLevel: "off", @@ -2234,7 +2234,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); it("drops reasoning-only continuations before adding interrupted sentinels for non-Anthropic fallbacks", () => { - const continuationAssistant: MuxMessage = { + const continuationAssistant: XumMessage = { id: "assistant-reasoning-only", role: "assistant", metadata: { partial: true, historySequence: 2 }, @@ -2242,7 +2242,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }; const { providerRequestMessages } = prepareProviderRequestMessages( - [createMuxMessage("latest-user", "user", "fix the issue"), continuationAssistant], + [createXumMessage("latest-user", "user", "fix the issue"), continuationAssistant], "openai", "off" ); @@ -2252,7 +2252,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); it("keeps reasoning-only continuations and sentinels for Anthropic thinking fallbacks", () => { - const continuationAssistant: MuxMessage = { + const continuationAssistant: XumMessage = { id: "assistant-reasoning-only", role: "assistant", metadata: { partial: true, historySequence: 2 }, @@ -2265,7 +2265,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }; const { providerRequestMessages } = prepareProviderRequestMessages( - [createMuxMessage("latest-user", "user", "fix the issue"), continuationAssistant], + [createXumMessage("latest-user", "user", "fix the issue"), continuationAssistant], "anthropic", "medium" ); @@ -2293,7 +2293,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2360,7 +2360,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const harness = createHarness(xumHome.path, metadata); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2392,7 +2392,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2426,7 +2426,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const memoryCalls: Array<{ includeHotMemories: boolean }> = []; const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2460,7 +2460,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const memoryCalls: Array<{ includeHotMemories: boolean }> = []; const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2611,7 +2611,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2652,7 +2652,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2676,7 +2676,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2701,18 +2701,18 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const metadata = createLocalWorkspaceMetadata(workspaceId, projectPath); const harness = createHarness(xumHome.path, metadata); - const messages: MuxMessage[] = [ - createMuxMessage("boundary-1", "assistant", "compaction epoch 1", { + const messages: XumMessage[] = [ + createXumMessage("boundary-1", "assistant", "compaction epoch 1", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, model: "openai:gpt-5.2", }), - createMuxMessage("assistant-old-response", "assistant", "older response", { + createXumMessage("assistant-old-response", "assistant", "older response", { model: "openai:gpt-5.2", providerMetadata: { openai: { responseId: "resp_epoch_1" } }, }), - createMuxMessage( + createXumMessage( "start-here-summary", "assistant", "# Start Here\n\n- Existing plan context\n\n*Plan file preserved at:* /tmp/plan.md", @@ -2721,14 +2721,14 @@ describe("AIService.streamMessage compaction boundary slicing", () => { agentId: "plan", } ), - createMuxMessage("mid-user", "user", "mid conversation"), - createMuxMessage("boundary-2", "assistant", "compaction epoch 2", { + createXumMessage("mid-user", "user", "mid conversation"), + createXumMessage("boundary-2", "assistant", "compaction epoch 2", { compacted: "user", compactionBoundary: true, compactionEpoch: 2, model: "openai:gpt-5.2", }), - createMuxMessage("latest-user", "user", "continue", { historySequence: 42 }), + createXumMessage("latest-user", "user", "continue", { historySequence: 42 }), ]; const result = await harness.service.streamMessage({ @@ -2770,7 +2770,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const harness = createHarness(xumHome.path, metadata, { routeProvider: "openrouter" }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "medium", @@ -2799,7 +2799,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const harness = createHarness(xumHome.path, metadata); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "medium", @@ -2839,7 +2839,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const harness = createHarness(xumHome.path, metadata); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "medium", @@ -2882,7 +2882,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -2905,19 +2905,19 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const metadata = createLocalWorkspaceMetadata(workspaceId, projectPath); const harness = createHarness(xumHome.path, metadata); - const messages: MuxMessage[] = [ - createMuxMessage("assistant-before-malformed", "assistant", "response before malformed", { + const messages: XumMessage[] = [ + createXumMessage("assistant-before-malformed", "assistant", "response before malformed", { model: "openai:gpt-5.2", providerMetadata: { openai: { responseId: "resp_before_malformed" } }, }), - createMuxMessage("malformed-boundary", "assistant", "not a durable boundary", { + createXumMessage("malformed-boundary", "assistant", "not a durable boundary", { compacted: "user", compactionBoundary: true, // Invalid durable marker: must not truncate request payload. compactionEpoch: 0, model: "openai:gpt-5.2", }), - createMuxMessage("latest-user", "user", "continue"), + createXumMessage("latest-user", "user", "continue"), ]; const result = await harness.service.streamMessage({ @@ -3146,7 +3146,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -3282,7 +3282,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { })); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -3373,7 +3373,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const harness = createHarness(xumHome.path, metadata, { sessionUsageService }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "continue")], + messages: [createXumMessage("latest-user", "user", "continue")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -3458,7 +3458,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { const sessionHolder: ActiveTurnThinkingOverride = {}; const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, // Budget-token Anthropic model (no adaptive effort): level changes show // up as thinking.budgetTokens differences. @@ -3502,7 +3502,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: KNOWN_MODELS.SONNET.id, thinkingLevel: "medium", @@ -3533,7 +3533,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "anthropic:claude-opus-4-7", thinkingLevel: "high", @@ -3565,7 +3565,7 @@ describe("AIService.streamMessage compaction boundary slicing", () => { }); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("latest-user", "user", "hello")], + messages: [createXumMessage("latest-user", "user", "hello")], workspaceId, modelString: "xai:grok-4-1-fast", thinkingLevel: "off", @@ -3648,7 +3648,7 @@ describe("AIService.streamMessage multi-project trust gating", () => { async function streamOnce(harness: TrustGatingHarness, workspaceId: string): Promise { const result = await harness.service.streamMessage({ - messages: [createMuxMessage("user-message", "user", "hello")], + messages: [createXumMessage("user-message", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -3743,7 +3743,7 @@ describe("AIService.streamMessage multi-project trust gating", () => { const harness = createHarness(xumHome.path, metadata, false); const result = await harness.service.streamMessage({ - messages: [createMuxMessage("user-message", "user", "hello")], + messages: [createXumMessage("user-message", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", @@ -3822,7 +3822,7 @@ describe("AIService.streamMessage model parameter overrides", () => { modelString = ANTHROPIC_MODEL ): Promise { const result = await harness.service.streamMessage({ - messages: [createMuxMessage("user-message", "user", "hello")], + messages: [createXumMessage("user-message", "user", "hello")], workspaceId, modelString, thinkingLevel: "off", @@ -4276,7 +4276,7 @@ describe("AIService.streamMessage model parameter overrides", () => { ); const result = await service.streamMessage({ - messages: [createMuxMessage("user-message", "user", "hello")], + messages: [createXumMessage("user-message", "user", "hello")], workspaceId, modelString: "coder:google/gemini-2.5-pro", thinkingLevel: "medium", @@ -4322,7 +4322,7 @@ describe("AIService.streamMessage turn envelope", () => { async function streamTurn(harness: TurnEnvelopeHarness, workspaceId: string): Promise { const result = await harness.service.streamMessage({ - messages: [createMuxMessage("user-message", "user", "hello")], + messages: [createXumMessage("user-message", "user", "hello")], workspaceId, modelString: "openai:gpt-5.2", thinkingLevel: "off", diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index 771c1af36c..894c2efe91 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -20,8 +20,8 @@ import { import { EXPERIMENT_IDS, type ExperimentId } from "@/common/constants/experiments"; import type { GoalRecordV1 } from "@/common/types/goal"; -import type { ModelMessage, MuxMessage, MuxMessageMetadata } from "@/common/types/message"; -import { createMuxMessage } from "@/common/types/message"; +import type { ModelMessage, XumMessage, XumMessageMetadata } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { Config } from "@/node/config"; import { StreamManager, type ModelFallbackOptions, type StreamTextOnChunk } from "./streamManager"; import { emitTurnEnvelope } from "./turnEnvelope"; @@ -59,7 +59,7 @@ import { getSrcBaseDir, isSSHRuntime } from "@/common/types/runtime"; import { ContainerManager } from "@/node/multiProject/containerManager"; import { secretsToRecord } from "@/common/types/secrets"; import { mergeMultiProjectSecrets } from "@/node/services/utils/multiProjectSecrets"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import type { XumToolScope } from "@/common/types/toolScope"; import type { PolicyService } from "@/node/services/policyService"; import type { ProviderService } from "@/node/services/providerService"; @@ -213,12 +213,12 @@ import { isWorkspaceProjectTrusted } from "@/node/utils/projectTrust"; const STREAM_STARTUP_DIAGNOSTIC_THRESHOLD_MS = 1_000; export function prepareProviderRequestMessages( - messages: MuxMessage[], + messages: XumMessage[], canonicalProviderName: string, effectiveThinkingLevel: ThinkingLevel ): { - activeContextMessages: MuxMessage[]; - providerRequestMessages: MuxMessage[]; + activeContextMessages: XumMessage[]; + providerRequestMessages: XumMessage[]; contextBoundarySlicedCount: number; } { // Workflow display rows are durable UI history, not main-agent context. @@ -243,9 +243,9 @@ export function prepareProviderRequestMessages( // Exported for the replay builder: fallback requests append the refusal's // partial continuation the same way production does. export function replaceOrAppendMessageById( - messages: MuxMessage[], - replacement: MuxMessage -): MuxMessage[] { + messages: XumMessage[], + replacement: XumMessage +): XumMessage[] { const index = messages.findIndex((message) => message.id === replacement.id); if (index === -1) { return [...messages, replacement]; @@ -262,7 +262,7 @@ export function replaceOrAppendMessageById( /** Options bag for {@link AIService.streamMessage}. */ export interface StreamMessageOptions { - messages: MuxMessage[]; + messages: XumMessage[]; workspaceId: string; modelString: string; thinkingLevel?: ThinkingLevel; @@ -274,7 +274,7 @@ export interface StreamMessageOptions { additionalSystemContext?: string; additionalSystemInstructions?: string; maxOutputTokens?: number; - muxProviderOptions?: MuxProviderOptions; + muxProviderOptions?: XumProviderOptions; /** Internal-only flag for Copilot billing attribution; never sourced from IPC schemas. */ agentInitiated?: boolean; agentId?: string; @@ -303,7 +303,7 @@ export interface StreamMessageOptions { workspaceGoalService?: WorkspaceGoalService; disableWorkspaceAgents?: boolean; hasQueuedMessages?: (dispatchMode?: "tool-end" | "turn-end") => boolean; - muxMetadata?: MuxMessageMetadata; + muxMetadata?: XumMessageMetadata; openaiTruncationModeOverride?: "auto" | "disabled"; /** * Model floor already resolved by AgentSession (config.json @@ -401,7 +401,7 @@ function isToolExecutionContext(value: unknown): value is ToolExecutionContext { * path — unusable by host fs. Fall back to metadata.projectPath which is always * host-local. */ -export function resolveMuxProjectRootForHostFs( +export function resolveXumProjectRootForHostFs( metadata: WorkspaceMetadata, workspacePath: string ): string { @@ -434,7 +434,7 @@ function resolveXumToolScope( return { type: "project", xumHome: config.rootDir, - projectRoot: resolveMuxProjectRootForHostFs(metadata, workspacePath), + projectRoot: resolveXumProjectRootForHostFs(metadata, workspacePath), projectStorageAuthority: runtimeType === "ssh" || runtimeType === "docker" ? "runtime" : "host-local", ...(checkoutRoot != null ? { checkoutRoot } : {}), @@ -1009,7 +1009,7 @@ export class AIService extends EventEmitter { */ async createModel( modelString: string, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, opts?: { agentInitiated?: boolean; workspaceId?: string; @@ -1468,7 +1468,7 @@ export class AIService extends EventEmitter { }; // Mode (plan|exec|compact) is derived from the selected agent definition. - const effectiveMuxProviderOptions: MuxProviderOptions = muxProviderOptions ?? {}; + const effectiveXumProviderOptions: XumProviderOptions = muxProviderOptions ?? {}; // Preliminary clamp for the factory call only: the factory reads the // thinking level solely for the xAI Grok variant swap, which never // depends on Coder instance metadata, so a pre-snapshot resolution is @@ -1487,7 +1487,7 @@ export class AIService extends EventEmitter { const modelResult = await this.providerModelFactory.resolveAndCreateModel( modelString, preliminaryThinkingLevel, - effectiveMuxProviderOptions, + effectiveXumProviderOptions, { agentInitiated, workspaceId } ); recordStartupPhaseTiming("resolveAndCreateModelMs", resolveAndCreateModelStartedAt); @@ -1629,14 +1629,14 @@ export class AIService extends EventEmitter { // The user's own wireFormat, captured BEFORE wire injection: the // refusal-fallback prepare() must reset to it when swapping to a model // whose route is not an OpenAI-wire Coder instance. - const userOpenAIWireFormat = effectiveMuxProviderOptions.openai?.wireFormat; + const userOpenAIWireFormat = effectiveXumProviderOptions.openai?.wireFormat; if (toolsIdentity.openaiWireFormat != null) { // Deliberate in-place update: every downstream consumer // (buildProviderOptions, toolsForModelConfig.openaiWireFormat, header // building, mid-turn thinking rebuilds) reads this object, and the // actual request bytes go over Chat Completions. - effectiveMuxProviderOptions.openai = { - ...(effectiveMuxProviderOptions.openai ?? {}), + effectiveXumProviderOptions.openai = { + ...(effectiveXumProviderOptions.openai ?? {}), wireFormat: toolsIdentity.openaiWireFormat, }; } @@ -2450,7 +2450,7 @@ export class AIService extends EventEmitter { toolPolicy: effectiveToolPolicy, additionalSystemInstructions: scratchpadAdditionalSystemInstructions, maxOutputTokens, - providerOptions: effectiveMuxProviderOptions, + providerOptions: effectiveXumProviderOptions, experiments: { ...experiments, dynamicWorkflows: dynamicWorkflowsExperimentEnabled, @@ -2623,9 +2623,9 @@ export class AIService extends EventEmitter { : {}), ...(toolSearchRuntime ? { toolSearchRuntime } : {}), capabilityModelString, - openaiWireFormat: effectiveMuxProviderOptions?.openai?.wireFormat, + openaiWireFormat: effectiveXumProviderOptions?.openai?.wireFormat, xaiNativeToolsEnabled: routeProvider === "xai", - xaiSearchParameters: effectiveMuxProviderOptions.xai?.searchParameters, + xaiSearchParameters: effectiveXumProviderOptions.xai?.searchParameters, backgroundProcessManager: this.backgroundProcessManager, // Plan agent configuration for plan file access. // - read: plan file is readable in all agents (useful context) @@ -2978,7 +2978,7 @@ export class AIService extends EventEmitter { effectiveThinkingLevel, modelString, providersConfig: requestProvidersConfig, - anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl, + anthropicCacheTtl: effectiveXumProviderOptions.anthropic?.cacheTtl, workspaceId, }); recordStartupPhaseTiming("prepareMessagesForProviderMs", prepareMessagesForProviderStartedAt); @@ -3004,7 +3004,7 @@ export class AIService extends EventEmitter { (latest, message) => Math.max(latest, message.metadata?.historySequence ?? -1), -1 ); - const assistantMessage = createMuxMessage(assistantMessageId, "assistant", "", { + const assistantMessage = createXumMessage(assistantMessageId, "assistant", "", { ...(requestHistorySequence >= 0 ? { requestHistorySequence } : {}), timestamp: Date.now(), model: canonicalModelString, @@ -3026,10 +3026,10 @@ export class AIService extends EventEmitter { // These emit synthetic stream events without calling an AI provider. const forceContextLimitError = modelString.startsWith("openai:") && - effectiveMuxProviderOptions.openai?.forceContextLimitError === true; + effectiveXumProviderOptions.openai?.forceContextLimitError === true; const simulateToolPolicyNoopFlag = modelString.startsWith("openai:") && - effectiveMuxProviderOptions.openai?.simulateToolPolicyNoop === true; + effectiveXumProviderOptions.openai?.simulateToolPolicyNoop === true; if (forceContextLimitError || simulateToolPolicyNoopFlag) { const simulationCtx: SimulationContext = { @@ -3067,7 +3067,7 @@ export class AIService extends EventEmitter { effectiveThinkingLevel, providerRequestMessages, (id) => this.streamManager.isResponseIdLost(id), - effectiveMuxProviderOptions, + effectiveXumProviderOptions, workspaceId, truncationMode, requestProvidersConfig, @@ -3084,7 +3084,7 @@ export class AIService extends EventEmitter { const buildRequestConfigStartedAt = Date.now(); let requestHeaders = buildRequestHeaders( optionsModelString, - effectiveMuxProviderOptions, + effectiveXumProviderOptions, workspaceId, requestProvidersConfig, routeProvider @@ -3266,7 +3266,7 @@ export class AIService extends EventEmitter { effective, providerRequestMessages, (id) => this.streamManager.isResponseIdLost(id), - effectiveMuxProviderOptions, + effectiveXumProviderOptions, workspaceId, truncationMode, requestProvidersConfig, @@ -3435,9 +3435,9 @@ export class AIService extends EventEmitter { // resolving the fallback: the fallback's wire is decided by // ITS effective route, and the factory's direct-OpenAI branch // reads this knob for model selection. - if (effectiveMuxProviderOptions.openai?.wireFormat !== userOpenAIWireFormat) { - effectiveMuxProviderOptions.openai = { - ...(effectiveMuxProviderOptions.openai ?? {}), + if (effectiveXumProviderOptions.openai?.wireFormat !== userOpenAIWireFormat) { + effectiveXumProviderOptions.openai = { + ...(effectiveXumProviderOptions.openai ?? {}), wireFormat: userOpenAIWireFormat, }; } @@ -3445,7 +3445,7 @@ export class AIService extends EventEmitter { const nextModelResult = await this.providerModelFactory.resolveAndCreateModel( nextModelString, preliminaryNextThinkingLevel, - effectiveMuxProviderOptions, + effectiveXumProviderOptions, { agentInitiated, workspaceId } ); if (!nextModelResult.success) { @@ -3499,8 +3499,8 @@ export class AIService extends EventEmitter { // stream is dead once a refusal fallback runs, so every // consumer (option/header rebuilds, mid-turn thinking // rebuild closures) must see the fallback's wire. - effectiveMuxProviderOptions.openai = { - ...(effectiveMuxProviderOptions.openai ?? {}), + effectiveXumProviderOptions.openai = { + ...(effectiveXumProviderOptions.openai ?? {}), wireFormat: nextToolsIdentity.openaiWireFormat, }; } @@ -3529,7 +3529,7 @@ export class AIService extends EventEmitter { capabilityModelString: nextCapabilityModelString, // Snapshot from the main path is stale here: the // fallback's wire decides Responses-only tool assembly. - openaiWireFormat: effectiveMuxProviderOptions.openai?.wireFormat, + openaiWireFormat: effectiveXumProviderOptions.openai?.wireFormat, xaiNativeToolsEnabled: next.routeProvider === "xai", }, workspaceId, @@ -3693,7 +3693,7 @@ export class AIService extends EventEmitter { // from cache/option/header builders. modelString: nextModelString, providersConfig: nextProvidersConfig, - anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl, + anthropicCacheTtl: effectiveXumProviderOptions.anthropic?.cacheTtl, workspaceId, }); @@ -3702,7 +3702,7 @@ export class AIService extends EventEmitter { nextThinkingLevel, nextProviderRequestMessages, (id) => this.streamManager.isResponseIdLost(id), - effectiveMuxProviderOptions, + effectiveXumProviderOptions, workspaceId, truncationMode, nextProvidersConfig, @@ -3715,7 +3715,7 @@ export class AIService extends EventEmitter { // so the native option never leaks onto unsupported fallbacks. let nextHeaders = buildRequestHeaders( nextOptionsModelString, - effectiveMuxProviderOptions, + effectiveXumProviderOptions, workspaceId, nextProvidersConfig, next.routeProvider @@ -3815,7 +3815,7 @@ export class AIService extends EventEmitter { effective, nextProviderRequestMessages, (id) => this.streamManager.isResponseIdLost(id), - effectiveMuxProviderOptions, + effectiveXumProviderOptions, workspaceId, truncationMode, nextProvidersConfig, @@ -3837,7 +3837,7 @@ export class AIService extends EventEmitter { next.routeProvider === "xai" ? getForcedXaiSearchToolNames( nextCapabilityModelString, - effectiveMuxProviderOptions.xai?.searchParameters + effectiveXumProviderOptions.xai?.searchParameters )?.filter((toolName) => toolName in nextTools) : undefined; @@ -3876,7 +3876,7 @@ export class AIService extends EventEmitter { sentinelToolNames: nextToolNamesForSentinel, wireProviderName: next.wireProviderName, anthropicCacheTtl: - effectiveMuxProviderOptions.anthropic?.cacheTtl ?? undefined, + effectiveXumProviderOptions.anthropic?.cacheTtl ?? undefined, planContentForTransition, planFilePath, postCompactionAttachments, @@ -3910,7 +3910,7 @@ export class AIService extends EventEmitter { effectiveThinkingLevel: effectiveLevel, modelString: nextModelString, providersConfig: nextProvidersConfig, - anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl, + anthropicCacheTtl: effectiveXumProviderOptions.anthropic?.cacheTtl, workspaceId, }); await emitFallbackEnvelopeWith(effectiveLevel, providerOptionsForEnvelope); @@ -3933,7 +3933,7 @@ export class AIService extends EventEmitter { providerOptions: nextMergedProviderOptions, headers: nextHeaders, callSettingsOverrides: nextOverrides.standard, - anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl ?? undefined, + anthropicCacheTtl: effectiveXumProviderOptions.anthropic?.cacheTtl ?? undefined, thinkingLevel: nextThinkingLevel, forcedFirstStepToolNames: nextForcedFirstStepToolNames, rebuildProviderOptionsForThinkingLevel: @@ -3965,7 +3965,7 @@ export class AIService extends EventEmitter { routeProvider === "xai" ? getForcedXaiSearchToolNames( capabilityModelString, - effectiveMuxProviderOptions.xai?.searchParameters + effectiveXumProviderOptions.xai?.searchParameters )?.filter((toolName) => toolName in toolsForStream) : undefined; @@ -4024,7 +4024,7 @@ export class AIService extends EventEmitter { effectiveThinkingLevel: folded.effectiveLevel, modelString, providersConfig: requestProvidersConfig, - anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl, + anthropicCacheTtl: effectiveXumProviderOptions.anthropic?.cacheTtl, workspaceId, }); streamProviderOptions = folded.providerOptions; @@ -4061,7 +4061,7 @@ export class AIService extends EventEmitter { // set, so replay cannot derive one from the other. sentinelToolNames: toolNamesForSentinel, wireProviderName, - anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl ?? undefined, + anthropicCacheTtl: effectiveXumProviderOptions.anthropic?.cacheTtl ?? undefined, planContentForTransition, planFilePath, postCompactionAttachments, @@ -4094,7 +4094,7 @@ export class AIService extends EventEmitter { effectiveThinkingLevel: effectiveLevel, modelString, providersConfig: requestProvidersConfig, - anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl, + anthropicCacheTtl: effectiveXumProviderOptions.anthropic?.cacheTtl, workspaceId, }); await emitPrimaryEnvelopeWith(effectiveLevel, providerOptions); @@ -4136,7 +4136,7 @@ export class AIService extends EventEmitter { metadata.name, streamThinkingLevel, requestHeaders, - effectiveMuxProviderOptions.anthropic?.cacheTtl ?? undefined, + effectiveXumProviderOptions.anthropic?.cacheTtl ?? undefined, resolvedOverrides.standard, advisorToolEligible ? onAdvisorChunk : undefined, advisorToolEligible @@ -4315,7 +4315,7 @@ export class AIService extends EventEmitter { await this.streamManager.replayStream(workspaceId, opts); } - debugGetLastMockPrompt(workspaceId: string): Result { + debugGetLastMockPrompt(workspaceId: string): Result { if (typeof workspaceId !== "string" || workspaceId.trim().length === 0) { return Err("debugGetLastMockPrompt: workspaceId is required"); } diff --git a/src/node/services/backup/payload.test.ts b/src/node/services/backup/payload.test.ts index b8609aee8e..67c79b5b84 100644 --- a/src/node/services/backup/payload.test.ts +++ b/src/node/services/backup/payload.test.ts @@ -4,7 +4,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import * as jsonc from "jsonc-parser"; -import { MuxProviderOptionsSchema } from "@/common/schemas/providerOptions"; +import { XumProviderOptionsSchema } from "@/common/schemas/providerOptions"; import { execFileAsync } from "@/node/utils/disposableExec"; import { BACKUP_SCHEMA_VERSION, @@ -345,7 +345,7 @@ describe("backup payload", () => { }); it("keeps no undeclared provider option out of the payload", () => { - for (const provider of Object.keys(MuxProviderOptionsSchema.shape)) { + for (const provider of Object.keys(XumProviderOptionsSchema.shape)) { const serialized = serializeBackupPreferences({ ai: { providerOptions: { [provider]: { apiKey: "hunter2" } } }, }).toString("utf-8"); diff --git a/src/node/services/bashMonitorWakeStore.ts b/src/node/services/bashMonitorWakeStore.ts index 20ff7e451b..9210760096 100644 --- a/src/node/services/bashMonitorWakeStore.ts +++ b/src/node/services/bashMonitorWakeStore.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import assert from "@/common/utils/assert"; import { BASH_MONITOR_WAKE_HEADINGS } from "@/common/utils/machineTurnPrompts"; -import type { MuxMessageMetadata } from "@/common/types/message"; +import type { XumMessageMetadata } from "@/common/types/message"; import type { Config } from "@/node/config"; import { log } from "@/node/services/log"; import { isErrnoWithCode } from "@/node/utils/fs"; @@ -171,7 +171,7 @@ function removeDeliveredLineOverlap( */ export function buildBashMonitorWakeMetadata( records: readonly BashMonitorWakeRecord[] -): Extract { +): Extract { assert(records.length > 0, "buildBashMonitorWakeMetadata requires at least one record"); return { type: "bash-monitor-wake", diff --git a/src/node/services/coderService.test.ts b/src/node/services/coderService.test.ts index 3614501c52..38c62fdf5d 100644 --- a/src/node/services/coderService.test.ts +++ b/src/node/services/coderService.test.ts @@ -3,7 +3,7 @@ import { Readable } from "stream"; import { describe, it, expect, vi, beforeEach, afterEach, spyOn } from "bun:test"; import { CoderService, compareVersions } from "./coderService"; import * as childProcess from "child_process"; -import * as muxSshConfigWriter from "@/node/runtime/muxSshConfigWriter"; +import * as xumSshConfigWriter from "@/node/runtime/xumSshConfigWriter"; import * as disposableExec from "@/node/utils/disposableExec"; // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -1401,26 +1401,26 @@ describe("deleteWorkspaceEventually", () => { }); }); -describe("CoderService.ensureMuxCoderSSHConfig", () => { +describe("CoderService.ensureXumCoderSSHConfig", () => { it("skips SSH config writes when coder binary is unavailable", async () => { const service = new CoderService(); const resolveCoderBinaryPathSpy = spyOn( service as unknown as { resolveCoderBinaryPath: () => Promise }, "resolveCoderBinaryPath" ).mockResolvedValue(null); - const ensureMuxCoderSSHConfigFileSpy = spyOn( - muxSshConfigWriter, - "ensureMuxCoderSSHConfigFile" + const ensureXumCoderSSHConfigFileSpy = spyOn( + xumSshConfigWriter, + "ensureXumCoderSSHConfigFile" ).mockResolvedValue(); try { - await service.ensureMuxCoderSSHConfig(); + await service.ensureXumCoderSSHConfig(); expect(resolveCoderBinaryPathSpy).toHaveBeenCalledTimes(1); - expect(ensureMuxCoderSSHConfigFileSpy).not.toHaveBeenCalled(); + expect(ensureXumCoderSSHConfigFileSpy).not.toHaveBeenCalled(); } finally { resolveCoderBinaryPathSpy.mockRestore(); - ensureMuxCoderSSHConfigFileSpy.mockRestore(); + ensureXumCoderSSHConfigFileSpy.mockRestore(); } }); @@ -1430,21 +1430,21 @@ describe("CoderService.ensureMuxCoderSSHConfig", () => { service as unknown as { resolveCoderBinaryPath: () => Promise }, "resolveCoderBinaryPath" ).mockResolvedValue("/usr/local/bin/coder"); - const ensureMuxCoderSSHConfigFileSpy = spyOn( - muxSshConfigWriter, - "ensureMuxCoderSSHConfigFile" + const ensureXumCoderSSHConfigFileSpy = spyOn( + xumSshConfigWriter, + "ensureXumCoderSSHConfigFile" ).mockResolvedValue(); try { - await service.ensureMuxCoderSSHConfig(); + await service.ensureXumCoderSSHConfig(); expect(resolveCoderBinaryPathSpy).toHaveBeenCalledTimes(1); - expect(ensureMuxCoderSSHConfigFileSpy).toHaveBeenCalledWith({ + expect(ensureXumCoderSSHConfigFileSpy).toHaveBeenCalledWith({ coderBinaryPath: "/usr/local/bin/coder", }); } finally { resolveCoderBinaryPathSpy.mockRestore(); - ensureMuxCoderSSHConfigFileSpy.mockRestore(); + ensureXumCoderSSHConfigFileSpy.mockRestore(); } }); @@ -1454,21 +1454,21 @@ describe("CoderService.ensureMuxCoderSSHConfig", () => { service as unknown as { resolveCoderBinaryPath: () => Promise }, "resolveCoderBinaryPath" ).mockResolvedValue("C:\\Users\\me\\bin\\coder.exe"); - const ensureMuxCoderSSHConfigFileSpy = spyOn( - muxSshConfigWriter, - "ensureMuxCoderSSHConfigFile" + const ensureXumCoderSSHConfigFileSpy = spyOn( + xumSshConfigWriter, + "ensureXumCoderSSHConfigFile" ).mockResolvedValue(); try { - await service.ensureMuxCoderSSHConfig(); + await service.ensureXumCoderSSHConfig(); expect(resolveCoderBinaryPathSpy).toHaveBeenCalledTimes(1); - expect(ensureMuxCoderSSHConfigFileSpy).toHaveBeenCalledWith({ + expect(ensureXumCoderSSHConfigFileSpy).toHaveBeenCalledWith({ coderBinaryPath: "C:\\Users\\me\\bin\\coder.exe", }); } finally { resolveCoderBinaryPathSpy.mockRestore(); - ensureMuxCoderSSHConfigFileSpy.mockRestore(); + ensureXumCoderSSHConfigFileSpy.mockRestore(); } }); }); diff --git a/src/node/services/coderService.ts b/src/node/services/coderService.ts index a7a1ce182d..610b851a2d 100644 --- a/src/node/services/coderService.ts +++ b/src/node/services/coderService.ts @@ -2,7 +2,7 @@ * Service for interacting with the Coder CLI. * Used to create/manage Coder workspaces as SSH targets for Xum workspaces. */ -import { ensureMuxCoderSSHConfigFile } from "@/node/runtime/muxSshConfigWriter"; +import { ensureXumCoderSSHConfigFile } from "@/node/runtime/xumSshConfigWriter"; import { execAsync, execFileAsync } from "@/node/utils/disposableExec"; import { getBashPath } from "@/node/utils/main/bashPath"; import { toWindowsPath } from "@/node/utils/paths"; @@ -1531,7 +1531,7 @@ export class CoderService { * Ensure mux-owned SSH config is set up for Coder workspaces. * Run before every Coder workspace connection (idempotent). */ - async ensureMuxCoderSSHConfig(): Promise { + async ensureXumCoderSSHConfig(): Promise { log.debug("Ensuring mux-owned Coder SSH config"); const coderBinary = await this.resolveCoderBinaryPath(); if (coderBinary == null) { @@ -1539,7 +1539,7 @@ export class CoderService { return; } - await ensureMuxCoderSSHConfigFile({ + await ensureXumCoderSSHConfigFile({ coderBinaryPath: coderBinary, }); } diff --git a/src/node/services/compactionHandler.test.ts b/src/node/services/compactionHandler.test.ts index c3b02f399f..b3b2a621b4 100644 --- a/src/node/services/compactionHandler.test.ts +++ b/src/node/services/compactionHandler.test.ts @@ -9,7 +9,7 @@ import * as path from "path"; import type { EventEmitter } from "events"; import { CONTEXT_BOUNDARY_KINDS } from "@/common/constants/contextBoundary"; import { MAX_EDITED_FILES } from "@/common/constants/attachments"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import type { CompactionCompletionMetadata } from "@/common/types/compaction"; import type { StreamEndEvent } from "@/common/types/stream"; import type { TelemetryService } from "./telemetryService"; @@ -38,8 +38,8 @@ const createMockEmitter = (): { emitter: EventEmitter; events: EmittedEvent[] } return { emitter: emitter as EventEmitter, events }; }; -const createCompactionRequest = (id = "req-1"): MuxMessage => - createMuxMessage(id, "user", "Please summarize the conversation", { +const createCompactionRequest = (id = "req-1"): XumMessage => + createXumMessage(id, "user", "Please summarize the conversation", { muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); @@ -47,8 +47,8 @@ const createSuccessfulFileEditMessage = ( id: string, filePath: string, diff: string, - metadata?: MuxMessage["metadata"] -): MuxMessage => ({ + metadata?: XumMessage["metadata"] +): XumMessage => ({ id, role: "assistant", parts: [ @@ -71,8 +71,8 @@ const createSuccessfulAgentSkillReadMessage = ( id: string, skillName: string, body: string, - metadata?: MuxMessage["metadata"] -): MuxMessage => ({ + metadata?: XumMessage["metadata"] +): XumMessage => ({ id, role: "assistant", parts: [ @@ -144,7 +144,7 @@ describe("CompactionHandler", () => { // Helper: seed messages into real history and return spies for tracking handler calls. // Spies are created AFTER seeding so they only track handler-initiated calls. - const seedHistory = async (...messages: MuxMessage[]) => { + const seedHistory = async (...messages: XumMessage[]) => { for (const msg of messages) { const result = await historyService.appendToHistory(workspaceId, msg); if (!result.success) throw new Error(`Seed failed: ${result.error}`); @@ -187,7 +187,7 @@ describe("CompactionHandler", () => { describe("handleCompletion() - Normal Compaction Flow", () => { it("should return false when no compaction request found", async () => { - const normalMsg = createMuxMessage("msg1", "user", "Hello", { + const normalMsg = createXumMessage("msg1", "user", "Hello", { historySequence: 0, muxMetadata: { type: "normal" }, }); @@ -215,7 +215,7 @@ describe("CompactionHandler", () => { model: "openai:gpt-4o", agentId: "exec", }; - const compactionRequest = createMuxMessage( + const compactionRequest = createXumMessage( "correlated-compaction-request", "user", "Please summarize the conversation", @@ -229,7 +229,7 @@ describe("CompactionHandler", () => { }, } ); - const snapshot = createMuxMessage("file-change-snapshot", "user", "", { + const snapshot = createXumMessage("file-change-snapshot", "user", "", { synthetic: true, }); await seedHistory(compactionRequest, snapshot); @@ -263,11 +263,11 @@ describe("CompactionHandler", () => { onCompactionComplete, }); await seedHistory( - createMuxMessage("stale-user", "user", "old preference"), - createMuxMessage("reset", "assistant", "Context reset", { + createXumMessage("stale-user", "user", "old preference"), + createXumMessage("reset", "assistant", "Context reset", { contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }), - createMuxMessage("fresh-user", "user", "new preference"), + createXumMessage("fresh-user", "user", "new preference"), createCompactionRequest("compact-request") ); @@ -280,8 +280,8 @@ describe("CompactionHandler", () => { }); describe("onIdleCompactionOutcome", () => { - const createIdleCompactionRequest = (id = "idle-req"): MuxMessage => - createMuxMessage(id, "user", "Please summarize the conversation", { + const createIdleCompactionRequest = (id = "idle-req"): XumMessage => + createXumMessage(id, "user", "Please summarize the conversation", { muxMetadata: { type: "compaction-request", rawCommand: "/compact", @@ -513,7 +513,7 @@ describe("CompactionHandler", () => { }); it("preserves pre-existing pending diffs when a heartbeat reset boundary is appended", async () => { - const existingBoundary = createMuxMessage( + const existingBoundary = createXumMessage( "summary-existing", "assistant", "Existing summary", @@ -562,7 +562,7 @@ describe("CompactionHandler", () => { }); it("rolls back heartbeat reset boundaries and restores pending state", async () => { - const existingBoundary = createMuxMessage( + const existingBoundary = createXumMessage( "summary-existing", "assistant", "Existing summary", @@ -631,7 +631,7 @@ describe("CompactionHandler", () => { }); it("prioritizes newly extracted diffs over stale pending diffs when the cap is reached", async () => { - const existingBoundary = createMuxMessage( + const existingBoundary = createXumMessage( "summary-existing", "assistant", "Existing summary", @@ -805,7 +805,7 @@ describe("CompactionHandler", () => { "@@ -1 +1 @@\n-old\n+stale\n", { historySequence: 0 } ); - const latestBoundary = createMuxMessage("summary-boundary", "assistant", "Older summary", { + const latestBoundary = createXumMessage("summary-boundary", "assistant", "Older summary", { historySequence: 1, compacted: "user", compactionBoundary: true, @@ -817,7 +817,7 @@ describe("CompactionHandler", () => { "@@ -1 +1 @@\n-before\n+after\n", { historySequence: 2 } ); - const compactionReq = createMuxMessage("req-latest-epoch", "user", "Please summarize", { + const compactionReq = createXumMessage("req-latest-epoch", "user", "Please summarize", { historySequence: 3, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); @@ -843,7 +843,7 @@ describe("CompactionHandler", () => { "@@ -1 +1 @@\n-old\n+stale\n", { historySequence: 0 } ); - const malformedBoundaryMissingEpoch = createMuxMessage( + const malformedBoundaryMissingEpoch = createXumMessage( "summary-malformed-boundary", "assistant", "Malformed summary", @@ -860,7 +860,7 @@ describe("CompactionHandler", () => { "@@ -1 +1 @@\n-before\n+after\n", { historySequence: 2 } ); - const compactionReq = createMuxMessage("req-malformed-boundary", "user", "Please summarize", { + const compactionReq = createXumMessage("req-malformed-boundary", "user", "Please summarize", { historySequence: 3, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); @@ -998,11 +998,11 @@ describe("CompactionHandler", () => { await handler.handleCompletion(event); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.parts !== undefined; }); expect(summaryEvent).toBeDefined(); - const sevt = summaryEvent?.data.message as MuxMessage; + const sevt = summaryEvent?.data.message as XumMessage; // providerMetadata is omitted to avoid inflating context with pre-compaction cacheCreationInputTokens expect(sevt.metadata).toMatchObject({ model: "claude-3-5-sonnet-20241022", @@ -1041,11 +1041,11 @@ describe("CompactionHandler", () => { expect(result).toBe(true); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.metadata?.compactionBoundary === true; }); expect(summaryEvent).toBeDefined(); - const summaryMessage = summaryEvent?.data.message as MuxMessage; + const summaryMessage = summaryEvent?.data.message as XumMessage; expect(summaryMessage.metadata?.contextUsage).toEqual({ // 20 system prompt tokens + (80 output - 30 reasoning) summary tokens inputTokens: 70, @@ -1075,10 +1075,10 @@ describe("CompactionHandler", () => { }); it("should set boundary metadata and keep historySequence monotonic", async () => { - const priorMessage = createMuxMessage("user-1", "user", "Earlier", { + const priorMessage = createXumMessage("user-1", "user", "Earlier", { historySequence: 4, }); - const compactionReq = createMuxMessage("req-1", "user", "Please summarize", { + const compactionReq = createXumMessage("req-1", "user", "Please summarize", { historySequence: 5, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); @@ -1094,7 +1094,7 @@ describe("CompactionHandler", () => { expect(appendedMsg.metadata?.historySequence).toBe(6); }); it("should ignore malformed persisted historySequence values when deriving monotonic bounds", async () => { - const malformedNegativeSequence = createMuxMessage( + const malformedNegativeSequence = createXumMessage( "assistant-malformed-negative-sequence", "assistant", "Corrupted persisted metadata", @@ -1102,7 +1102,7 @@ describe("CompactionHandler", () => { historySequence: -7, } ); - const malformedFractionalSequence = createMuxMessage( + const malformedFractionalSequence = createXumMessage( "assistant-malformed-fractional-sequence", "assistant", "Corrupted persisted metadata", @@ -1110,10 +1110,10 @@ describe("CompactionHandler", () => { historySequence: 99.5, } ); - const priorMessage = createMuxMessage("user-1", "user", "Earlier", { + const priorMessage = createXumMessage("user-1", "user", "Earlier", { historySequence: 4, }); - const compactionReq = createMuxMessage("req-1", "user", "Please summarize", { + const compactionReq = createXumMessage("req-1", "user", "Please summarize", { historySequence: 5, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); @@ -1142,11 +1142,11 @@ describe("CompactionHandler", () => { }); it("should derive next compaction epoch from legacy compacted summaries", async () => { - const legacySummary = createMuxMessage("summary-legacy", "assistant", "Older summary", { + const legacySummary = createXumMessage("summary-legacy", "assistant", "Older summary", { historySequence: 2, compacted: "user", }); - const compactionReq = createMuxMessage("req-epoch", "user", "Please summarize", { + const compactionReq = createXumMessage("req-epoch", "user", "Please summarize", { historySequence: 3, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); @@ -1163,11 +1163,11 @@ describe("CompactionHandler", () => { }); it("should update streamed summaries in-place without carrying stale provider metadata", async () => { - const compactionReq = createMuxMessage("req-streamed", "user", "Please summarize", { + const compactionReq = createXumMessage("req-streamed", "user", "Please summarize", { historySequence: 5, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); - const streamedSummary = createMuxMessage("msg-id", "assistant", "Summary", { + const streamedSummary = createXumMessage("msg-id", "assistant", "Summary", { historySequence: 6, timestamp: Date.now(), model: "claude-3-5-sonnet-20241022", @@ -1199,18 +1199,18 @@ describe("CompactionHandler", () => { expect(updatedSummary.metadata?.contextProviderMetadata).toBeUndefined(); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.id === "msg-id" && m?.metadata?.compactionBoundary === true; }); expect(summaryEvent).toBeDefined(); }); it("should strip stale provider metadata from emitted stream-end when reusing streamed summary ID", async () => { - const compactionReq = createMuxMessage("req-streamed-sanitize", "user", "Please summarize", { + const compactionReq = createXumMessage("req-streamed-sanitize", "user", "Please summarize", { historySequence: 5, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); - const streamedSummary = createMuxMessage("msg-id", "assistant", "Summary", { + const streamedSummary = createXumMessage("msg-id", "assistant", "Summary", { historySequence: 6, timestamp: Date.now(), model: "claude-3-5-sonnet-20241022", @@ -1252,7 +1252,7 @@ describe("CompactionHandler", () => { }); it("omits context usage estimate when stream-end metadata has no visible summary tokens", async () => { - const compactionReq = createMuxMessage( + const compactionReq = createXumMessage( "req-streamed-no-estimate", "user", "Please summarize", @@ -1261,7 +1261,7 @@ describe("CompactionHandler", () => { muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, } ); - const streamedSummary = createMuxMessage("msg-id", "assistant", "Summary", { + const streamedSummary = createXumMessage("msg-id", "assistant", "Summary", { historySequence: 6, timestamp: Date.now(), model: "claude-3-5-sonnet-20241022", @@ -1289,13 +1289,13 @@ describe("CompactionHandler", () => { }); it("should skip malformed compaction boundary markers when deriving next epoch", async () => { - const validBoundary = createMuxMessage("summary-valid", "assistant", "Valid summary", { + const validBoundary = createXumMessage("summary-valid", "assistant", "Valid summary", { historySequence: 1, compacted: "user", compactionBoundary: true, compactionEpoch: 3, }); - const malformedBoundaryMissingEpoch = createMuxMessage( + const malformedBoundaryMissingEpoch = createXumMessage( "summary-malformed-1", "assistant", "Malformed boundary", @@ -1305,7 +1305,7 @@ describe("CompactionHandler", () => { compactionBoundary: true, } ); - const malformedBoundaryMissingCompacted = createMuxMessage( + const malformedBoundaryMissingCompacted = createXumMessage( "summary-malformed-2", "assistant", "Malformed boundary", @@ -1315,7 +1315,7 @@ describe("CompactionHandler", () => { compactionEpoch: 99, } ); - const malformedBoundaryInvalidCompacted = createMuxMessage( + const malformedBoundaryInvalidCompacted = createXumMessage( "summary-malformed-invalid-compacted", "assistant", "Malformed boundary", @@ -1329,7 +1329,7 @@ describe("CompactionHandler", () => { (malformedBoundaryInvalidCompacted.metadata as Record).compacted = "corrupted"; } - const malformedBoundaryInvalidEpoch = createMuxMessage( + const malformedBoundaryInvalidEpoch = createXumMessage( "summary-malformed-3", "assistant", "Malformed boundary", @@ -1340,7 +1340,7 @@ describe("CompactionHandler", () => { compactionEpoch: 0, } ); - const compactionReq = createMuxMessage("req-malformed", "user", "Please summarize", { + const compactionReq = createXumMessage("req-malformed", "user", "Please summarize", { historySequence: 6, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }); @@ -1491,7 +1491,7 @@ describe("CompactionHandler", () => { expect(deleteEvent).toBeUndefined(); }); - it("should emit summary message with proper MuxMessage structure", async () => { + it("should emit summary message with proper XumMessage structure", async () => { const compactionReq = createCompactionRequest(); await seedHistory(compactionReq); @@ -1499,11 +1499,11 @@ describe("CompactionHandler", () => { await handler.handleCompletion(event); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.parts !== undefined; }); expect(summaryEvent).toBeDefined(); - const summaryMsg = summaryEvent?.data.message as MuxMessage; + const summaryMsg = summaryEvent?.data.message as XumMessage; expect(summaryMsg).toMatchObject({ id: expect.stringContaining("summary-") as string, role: "assistant", @@ -1513,7 +1513,7 @@ describe("CompactionHandler", () => { compactionBoundary: true, compactionEpoch: 1, muxMetadata: { type: "compaction-summary" }, - }) as MuxMessage["metadata"], + }) as XumMessage["metadata"], }); }); @@ -1535,11 +1535,11 @@ describe("CompactionHandler", () => { describe("Idle Compaction", () => { it("should preserve original recency timestamp from last user message", async () => { const originalTimestamp = Date.now() - 3600 * 1000; // 1 hour ago - const userMessage = createMuxMessage("user-1", "user", "Hello", { + const userMessage = createXumMessage("user-1", "user", "Hello", { timestamp: originalTimestamp, historySequence: 0, }); - const idleCompactionReq = createMuxMessage("req-1", "user", "Summarize", { + const idleCompactionReq = createXumMessage("req-1", "user", "Summarize", { historySequence: 1, muxMetadata: { type: "compaction-request", @@ -1555,23 +1555,23 @@ describe("CompactionHandler", () => { await handler.handleCompletion(event); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.metadata?.compacted; }); expect(summaryEvent).toBeDefined(); - const summaryMsg = summaryEvent?.data.message as MuxMessage; + const summaryMsg = summaryEvent?.data.message as XumMessage; expect(summaryMsg.metadata?.timestamp).toBe(originalTimestamp); expect(summaryMsg.metadata?.compacted).toBe("idle"); }); it("should preserve recency from last compacted message if no user message", async () => { const compactedTimestamp = Date.now() - 7200 * 1000; // 2 hours ago - const compactedMessage = createMuxMessage("compacted-1", "assistant", "Previous summary", { + const compactedMessage = createXumMessage("compacted-1", "assistant", "Previous summary", { timestamp: compactedTimestamp, compacted: "user", historySequence: 0, }); - const idleCompactionReq = createMuxMessage("req-1", "user", "Summarize", { + const idleCompactionReq = createXumMessage("req-1", "user", "Summarize", { historySequence: 1, muxMetadata: { type: "compaction-request", @@ -1587,27 +1587,27 @@ describe("CompactionHandler", () => { await handler.handleCompletion(event); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.metadata?.compacted === "idle"; }); expect(summaryEvent).toBeDefined(); - const summaryMsg = summaryEvent?.data.message as MuxMessage; + const summaryMsg = summaryEvent?.data.message as XumMessage; expect(summaryMsg.metadata?.timestamp).toBe(compactedTimestamp); }); it("should use max of user and compacted timestamps", async () => { const olderCompactedTimestamp = Date.now() - 7200 * 1000; // 2 hours ago const newerUserTimestamp = Date.now() - 3600 * 1000; // 1 hour ago - const compactedMessage = createMuxMessage("compacted-1", "assistant", "Previous summary", { + const compactedMessage = createXumMessage("compacted-1", "assistant", "Previous summary", { timestamp: olderCompactedTimestamp, compacted: "user", historySequence: 0, }); - const userMessage = createMuxMessage("user-1", "user", "Hello", { + const userMessage = createXumMessage("user-1", "user", "Hello", { timestamp: newerUserTimestamp, historySequence: 1, }); - const idleCompactionReq = createMuxMessage("req-1", "user", "Summarize", { + const idleCompactionReq = createXumMessage("req-1", "user", "Summarize", { historySequence: 2, muxMetadata: { type: "compaction-request", @@ -1623,11 +1623,11 @@ describe("CompactionHandler", () => { await handler.handleCompletion(event); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.metadata?.compacted === "idle"; }); expect(summaryEvent).toBeDefined(); - const summaryMsg = summaryEvent?.data.message as MuxMessage; + const summaryMsg = summaryEvent?.data.message as XumMessage; // Should use the newer timestamp (user message) expect(summaryMsg.metadata?.timestamp).toBe(newerUserTimestamp); }); @@ -1635,12 +1635,12 @@ describe("CompactionHandler", () => { it("should skip compaction-request message when finding timestamp to preserve", async () => { const originalTimestamp = Date.now() - 3600 * 1000; // 1 hour ago - the real user message const freshTimestamp = Date.now(); // The compaction request has a fresh timestamp - const userMessage = createMuxMessage("user-1", "user", "Hello", { + const userMessage = createXumMessage("user-1", "user", "Hello", { timestamp: originalTimestamp, historySequence: 0, }); // Idle compaction request WITH a timestamp (as happens in production) - const idleCompactionReq = createMuxMessage("req-1", "user", "Summarize", { + const idleCompactionReq = createXumMessage("req-1", "user", "Summarize", { timestamp: freshTimestamp, historySequence: 1, muxMetadata: { @@ -1657,11 +1657,11 @@ describe("CompactionHandler", () => { await handler.handleCompletion(event); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.metadata?.compacted; }); expect(summaryEvent).toBeDefined(); - const summaryMsg = summaryEvent?.data.message as MuxMessage; + const summaryMsg = summaryEvent?.data.message as XumMessage; // Should use the OLD user message timestamp, NOT the fresh compaction request timestamp expect(summaryMsg.metadata?.timestamp).toBe(originalTimestamp); expect(summaryMsg.metadata?.compacted).toBe("idle"); @@ -1669,7 +1669,7 @@ describe("CompactionHandler", () => { it("should use current time for non-idle compaction", async () => { const oldTimestamp = Date.now() - 3600 * 1000; // 1 hour ago - const userMessage = createMuxMessage("user-1", "user", "Hello", { + const userMessage = createXumMessage("user-1", "user", "Hello", { timestamp: oldTimestamp, historySequence: 0, }); @@ -1683,11 +1683,11 @@ describe("CompactionHandler", () => { const afterTime = Date.now(); const summaryEvent = emittedEvents.find((_e) => { - const m = _e.data.message as MuxMessage | undefined; + const m = _e.data.message as XumMessage | undefined; return m?.role === "assistant" && m?.metadata?.compacted; }); expect(summaryEvent).toBeDefined(); - const summaryMsg = summaryEvent?.data.message as MuxMessage; + const summaryMsg = summaryEvent?.data.message as XumMessage; // Should use current time, not the old user message timestamp expect(summaryMsg.metadata?.timestamp).toBeGreaterThanOrEqual(beforeTime); expect(summaryMsg.metadata?.timestamp).toBeLessThanOrEqual(afterTime); @@ -1697,7 +1697,7 @@ describe("CompactionHandler", () => { describe("Empty Summary Validation", () => { it("should reject compaction when summary is empty (stream crashed)", async () => { - const compactionRequestMsg = createMuxMessage("compact-req-1", "user", "/compact", { + const compactionRequestMsg = createXumMessage("compact-req-1", "user", "/compact", { historySequence: 0, timestamp: Date.now() - 1000, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, @@ -1716,7 +1716,7 @@ describe("CompactionHandler", () => { }); it("should reject compaction when summary is only whitespace", async () => { - const compactionRequestMsg = createMuxMessage("compact-req-1", "user", "/compact", { + const compactionRequestMsg = createXumMessage("compact-req-1", "user", "/compact", { historySequence: 0, timestamp: Date.now() - 1000, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, @@ -1735,7 +1735,7 @@ describe("CompactionHandler", () => { describe("Raw JSON Object Validation", () => { it("should reject compaction when summary is a raw JSON object", async () => { - const compactionRequestMsg = createMuxMessage("compact-req-1", "user", "/compact", { + const compactionRequestMsg = createXumMessage("compact-req-1", "user", "/compact", { historySequence: 0, timestamp: Date.now() - 1000, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, @@ -1760,7 +1760,7 @@ describe("CompactionHandler", () => { }); it("should reject any JSON object regardless of structure", async () => { - const compactionRequestMsg = createMuxMessage("compact-req-1", "user", "/compact", { + const compactionRequestMsg = createXumMessage("compact-req-1", "user", "/compact", { historySequence: 0, timestamp: Date.now() - 1000, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, @@ -1779,7 +1779,7 @@ describe("CompactionHandler", () => { }); it("should accept valid compaction summary text", async () => { - const compactionRequestMsg = createMuxMessage("compact-req-1", "user", "/compact", { + const compactionRequestMsg = createXumMessage("compact-req-1", "user", "/compact", { historySequence: 0, timestamp: Date.now() - 1000, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, @@ -1798,7 +1798,7 @@ describe("CompactionHandler", () => { }); it("should accept summary with embedded JSON as part of prose", async () => { - const compactionRequestMsg = createMuxMessage("compact-req-1", "user", "/compact", { + const compactionRequestMsg = createXumMessage("compact-req-1", "user", "/compact", { historySequence: 0, timestamp: Date.now() - 1000, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, @@ -1815,7 +1815,7 @@ describe("CompactionHandler", () => { }); it("should not reject JSON arrays (only objects)", async () => { - const compactionRequestMsg = createMuxMessage("compact-req-1", "user", "/compact", { + const compactionRequestMsg = createXumMessage("compact-req-1", "user", "/compact", { historySequence: 0, timestamp: Date.now() - 1000, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, diff --git a/src/node/services/compactionHandler.ts b/src/node/services/compactionHandler.ts index b2d266cc9a..b8cb1c88e1 100644 --- a/src/node/services/compactionHandler.ts +++ b/src/node/services/compactionHandler.ts @@ -15,11 +15,11 @@ import { Ok, Err } from "@/common/types/result"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; import { - createMuxMessage, + createXumMessage, getCompactionFollowUpContent, type CompactionFollowUpRequest, type CompactionSummaryMetadata, - type MuxMessage, + type XumMessage, } from "@/common/types/message"; import { createCompactionSummaryMessageId } from "@/node/services/utils/messageIds"; import type { TelemetryService } from "@/node/services/telemetryService"; @@ -246,11 +246,11 @@ function coercePersistedPostCompactionState(value: unknown): PersistedPostCompac }; } -function isCompactedSummaryMessage(message: MuxMessage): boolean { +function isCompactedSummaryMessage(message: XumMessage): boolean { return isDurableCompactedMarker(message.metadata?.compacted); } -function getLatestBoundaryHistorySequence(messages: readonly MuxMessage[]): number | undefined { +function getLatestBoundaryHistorySequence(messages: readonly XumMessage[]): number | undefined { let latest: number | undefined; for (const message of messages) { if (!isDurableContextBoundaryMarker(message)) continue; @@ -261,7 +261,7 @@ function getLatestBoundaryHistorySequence(messages: readonly MuxMessage[]): numb return latest; } -function getNextCompactionEpoch(messages: MuxMessage[]): number { +function getNextCompactionEpoch(messages: XumMessage[]): number { let epochCursor = 0; for (const message of messages) { @@ -561,7 +561,7 @@ export class CompactionHandler { } } - private async preparePendingStateFromMessages(messages: MuxMessage[]): Promise { + private async preparePendingStateFromMessages(messages: XumMessage[]): Promise { await this.loadPersistedPendingStateIfNeeded(); const latestCompactionEpochMessages = sliceMessagesFromLatestCompactionBoundary(messages); @@ -579,7 +579,7 @@ export class CompactionHandler { await this.persistPendingStateBestEffort(this.cachedFileDiffs, this.cachedLoadedSkills); } - private getMaxExistingHistorySequence(messages: MuxMessage[]): number { + private getMaxExistingHistorySequence(messages: XumMessage[]): number { return messages.reduce((maxSeq, message) => { const sequence = message.metadata?.historySequence; if (sequence === undefined) { @@ -635,7 +635,7 @@ export class CompactionHandler { "heartbeat reset boundary must compute a positive compaction epoch" ); - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( createCompactionSummaryMessageId(), "assistant", params.boundaryText, @@ -694,7 +694,7 @@ export class CompactionHandler { } async rollbackHeartbeatContextResetBoundary( - summaryMessage: MuxMessage + summaryMessage: XumMessage ): Promise> { assert( summaryMessage.role === "assistant", @@ -972,9 +972,9 @@ export class CompactionHandler { } private findPersistedStreamSummaryMessage( - messages: MuxMessage[], + messages: XumMessage[], streamedSummaryMessageId: string - ): MuxMessage | null { + ): XumMessage | null { for (let i = messages.length - 1; i >= 0; i -= 1) { const candidate = messages[i]; if (candidate.id !== streamedSummaryMessageId) { @@ -1030,7 +1030,7 @@ export class CompactionHandler { contextProviderMetadata?: Record; systemMessageTokens?: number; }, - messages: MuxMessage[], + messages: XumMessage[], streamedSummaryMessageId: string, compactionRequestMessageId: string, isIdleCompaction = false, @@ -1089,7 +1089,7 @@ export class CompactionHandler { // The summary's muxMetadata stores the pending follow-up (if any) for crash-safe dispatch. // After compaction, agentSession checks if the last message is a summary with pendingFollowUp // and dispatches it. The user message persisted by that dispatch serves as proof of completion. - const summaryMuxMetadata: CompactionSummaryMetadata = { + const summaryXumMetadata: CompactionSummaryMetadata = { type: "compaction-summary", pendingFollowUp, }; @@ -1111,7 +1111,7 @@ export class CompactionHandler { metadata.contextProviderMetadata ); - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( persistedStreamSummary?.id ?? createCompactionSummaryMessageId(), "assistant", summary, @@ -1133,7 +1133,7 @@ export class CompactionHandler { duration: metadata.duration, systemMessageTokens: metadata.systemMessageTokens, ...(postCompactionContextEstimate && { contextUsage: postCompactionContextEstimate }), - muxMetadata: summaryMuxMetadata, + muxMetadata: summaryXumMetadata, } ); if (persistedSummaryHistorySequence !== undefined) { diff --git a/src/node/services/heartbeatService.test.ts b/src/node/services/heartbeatService.test.ts index 0aed21ebcc..0b5b711708 100644 --- a/src/node/services/heartbeatService.test.ts +++ b/src/node/services/heartbeatService.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect, beforeEach, mock, afterEach } from "bun:test"; -import type { MuxMessage } from "@/common/types/message"; -import { createMuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { ProjectConfig, ProjectsConfig, Workspace } from "@/common/types/project"; import { Ok } from "@/common/types/result"; import type { WorkspaceActivitySnapshot } from "@/common/types/workspace"; @@ -80,7 +80,7 @@ describe("HeartbeatService", () => { let getAllSnapshotsMock: ReturnType< typeof mock<() => Promise>> >; - let getChatHistoryMock: ReturnType Promise>>; + let getChatHistoryMock: ReturnType Promise>>; let executeHeartbeatMock: ReturnType Promise>>; let isBusyForMessageMock: ReturnType boolean>>; let hasActiveDescendantTasksMock: ReturnType boolean>>; @@ -151,15 +151,15 @@ describe("HeartbeatService", () => { return new Map(entries); } - function makeCompletedTurnHistory(timestamp = staleTimestamp): MuxMessage[] { + function makeCompletedTurnHistory(timestamp = staleTimestamp): XumMessage[] { return [ - createMuxMessage("1", "user", "Hello", { timestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp }), + createXumMessage("1", "user", "Hello", { timestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp }), ]; } - function makeInteractiveAssistantMessage(timestamp = staleTimestamp): MuxMessage { - const assistantMessage = createMuxMessage("2", "assistant", "asking", { timestamp }); + function makeInteractiveAssistantMessage(timestamp = staleTimestamp): XumMessage { + const assistantMessage = createXumMessage("2", "assistant", "asking", { timestamp }); (assistantMessage as unknown as { parts: unknown[] }).parts = [ { type: "text", text: "Let me ask...", state: "done" }, { @@ -349,8 +349,8 @@ describe("HeartbeatService", () => { name: "no assistant message in history", setup: () => getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), - createMuxMessage("2", "user", "Still there?", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("2", "user", "Still there?", { timestamp: staleTimestamp }), ]), eligible: false, reason: "no_completed_turn", @@ -359,9 +359,9 @@ describe("HeartbeatService", () => { name: "last message is from user (awaiting response)", setup: () => getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), - createMuxMessage("3", "user", "Another question?", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), + createXumMessage("3", "user", "Another question?", { timestamp: staleTimestamp }), ]), eligible: false, reason: "awaiting_response", @@ -370,7 +370,7 @@ describe("HeartbeatService", () => { name: "last assistant message has interactive tool input", setup: () => getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), makeInteractiveAssistantMessage(), ]), eligible: false, @@ -407,9 +407,9 @@ describe("HeartbeatService", () => { // Streaming carve-out: committed history ends with the user message being answered // (partials are excluded from committed history), which must not gate queue modes. getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), - createMuxMessage("3", "user", "Keep going", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), + createXumMessage("3", "user", "Keep going", { timestamp: staleTimestamp }), ]); const result = await service.checkEligibility(testWorkspaceId, Date.now()); @@ -460,9 +460,9 @@ describe("HeartbeatService", () => { setHeartbeatConfig({ ...queueModeHeartbeat }); isBusyForMessageMock.mockReturnValueOnce(true); getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), - createMuxMessage("3", "user", "Keep going", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), + createXumMessage("3", "user", "Keep going", { timestamp: staleTimestamp }), ]); const result = await service.checkEligibility(testWorkspaceId, Date.now()); @@ -474,9 +474,9 @@ describe("HeartbeatService", () => { setHeartbeatConfig({ enabled: true, intervalMs: defaultHeartbeatIntervalMs }); isBusyForMessageMock.mockReturnValueOnce(true); getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), - createMuxMessage("3", "user", "Keep going", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), + createXumMessage("3", "user", "Keep going", { timestamp: staleTimestamp }), ]); const result = await service.checkEligibility(testWorkspaceId, Date.now()); @@ -487,9 +487,9 @@ describe("HeartbeatService", () => { test("an idle unanswered user message still gates queue modes", async () => { setHeartbeatConfig({ ...queueModeHeartbeat }); getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), - createMuxMessage("3", "user", "Another question?", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: staleTimestamp }), + createXumMessage("3", "user", "Another question?", { timestamp: staleTimestamp }), ]); const result = await service.checkEligibility(testWorkspaceId, Date.now()); @@ -515,7 +515,7 @@ describe("HeartbeatService", () => { setHeartbeatConfig({ ...queueModeHeartbeat }); getChatHistoryMock.mockResolvedValueOnce([ - createMuxMessage("1", "user", "Hello", { timestamp: staleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: staleTimestamp }), makeInteractiveAssistantMessage(), ]); const interactive = await service.checkEligibility(testWorkspaceId, Date.now()); @@ -1587,11 +1587,11 @@ describe("HeartbeatService", () => { ); getChatHistoryMock.mockResolvedValueOnce([ ...makeCompletedTurnHistory(), - createMuxMessage("hb-1", "user", "[Scheduled heartbeat] check in", { + createXumMessage("hb-1", "user", "[Scheduled heartbeat] check in", { timestamp: lastFiredAt, muxMetadata: { type: "heartbeat-request" }, }), - createMuxMessage("hb-1-reply", "assistant", "All good", { timestamp: lastFiredAt + 1 }), + createXumMessage("hb-1-reply", "assistant", "All good", { timestamp: lastFiredAt + 1 }), ]); await internals.resyncFromConfig(now); @@ -1612,7 +1612,7 @@ describe("HeartbeatService", () => { ]); getChatHistoryMock.mockResolvedValueOnce([ ...makeCompletedTurnHistory(), - createMuxMessage("hb-1", "user", "[Scheduled heartbeat] check in", { + createXumMessage("hb-1", "user", "[Scheduled heartbeat] check in", { timestamp: lastFiredAt, muxMetadata: { type: "heartbeat-request" }, }), @@ -1635,7 +1635,7 @@ describe("HeartbeatService", () => { ]); getChatHistoryMock.mockResolvedValueOnce([ ...makeCompletedTurnHistory(), - createMuxMessage("hb-1", "user", "[Scheduled heartbeat] check in", { + createXumMessage("hb-1", "user", "[Scheduled heartbeat] check in", { timestamp: deliveredAt, muxMetadata: { type: "heartbeat-request", firedAt }, }), @@ -1663,7 +1663,7 @@ describe("HeartbeatService", () => { ]); getChatHistoryMock.mockResolvedValueOnce([ ...makeCompletedTurnHistory(), - createMuxMessage("hb-1", "user", "[Scheduled heartbeat] check in", { + createXumMessage("hb-1", "user", "[Scheduled heartbeat] check in", { timestamp: lastFiredAt, muxMetadata: { type: "heartbeat-request" }, }), @@ -1691,7 +1691,7 @@ describe("HeartbeatService", () => { ]); getChatHistoryMock.mockResolvedValueOnce([ ...makeCompletedTurnHistory(), - createMuxMessage("hb-1", "user", "[Scheduled heartbeat] check in", { + createXumMessage("hb-1", "user", "[Scheduled heartbeat] check in", { timestamp: lastFiredAt, muxMetadata: { type: "heartbeat-request" }, }), diff --git a/src/node/services/heartbeatService.ts b/src/node/services/heartbeatService.ts index d8b7a6b68f..4a79306970 100644 --- a/src/node/services/heartbeatService.ts +++ b/src/node/services/heartbeatService.ts @@ -1,5 +1,5 @@ import assert from "@/common/utils/assert"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { ProjectsConfig, Workspace } from "@/common/types/project"; import type { WorkspaceActivitySnapshot, WorkspaceMetadata } from "@/common/types/workspace"; import { isWorkspaceArchived } from "@/common/utils/archive"; @@ -868,7 +868,7 @@ export class HeartbeatService { return workspaceId.length > 0 ? workspaceId : null; } - private hasInteractiveToolInput(message: MuxMessage): boolean { + private hasInteractiveToolInput(message: XumMessage): boolean { if (!Array.isArray(message.parts)) { return false; } diff --git a/src/node/services/historyService.test.ts b/src/node/services/historyService.test.ts index a550efa79a..8c38602391 100644 --- a/src/node/services/historyService.test.ts +++ b/src/node/services/historyService.test.ts @@ -3,7 +3,7 @@ import { CONTEXT_BOUNDARY_KINDS } from "@/common/constants/contextBoundary"; import { HistoryService } from "./historyService"; import type { Config } from "@/node/config"; import { createTestHistoryService } from "./testHistoryService"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import assert from "node:assert"; import { createHash } from "node:crypto"; import * as fs from "fs/promises"; @@ -11,7 +11,7 @@ import * as path from "path"; /** Collect all messages via iterateFullHistory (replaces removed getFullHistory). */ async function collectFullHistory(service: HistoryService, workspaceId: string) { - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; const result = await service.iterateFullHistory(workspaceId, "forward", (chunk) => { messages.push(...chunk); }); @@ -29,7 +29,7 @@ async function writeHistoryLines( await fs.writeFile(path.join(workspaceDir, "chat.jsonl"), lines.join("\n") + "\n"); } -function messageLine(workspaceId: string, message: MuxMessage): string { +function messageLine(workspaceId: string, message: XumMessage): string { return JSON.stringify({ ...message, workspaceId }); } @@ -41,7 +41,7 @@ async function appendNumberedMessages( for (let i = 0; i < count; i++) { await service.appendToHistory( workspaceId, - createMuxMessage(`msg-${i}`, "user", `Message ${i}`) + createXumMessage(`msg-${i}`, "user", `Message ${i}`) ); } } @@ -71,10 +71,10 @@ describe("HistoryService", () => { it("should read messages from chat.jsonl", async () => { const workspaceId = "workspace1"; await writeHistoryLines(config, workspaceId, [ - messageLine(workspaceId, createMuxMessage("msg1", "user", "Hello", { historySequence: 0 })), + messageLine(workspaceId, createXumMessage("msg1", "user", "Hello", { historySequence: 0 })), messageLine( workspaceId, - createMuxMessage("msg2", "assistant", "Hi there", { historySequence: 1 }) + createXumMessage("msg2", "assistant", "Hi there", { historySequence: 1 }) ), ]); @@ -87,9 +87,9 @@ describe("HistoryService", () => { it("should skip malformed JSON lines", async () => { const workspaceId = "workspace1"; await writeHistoryLines(config, workspaceId, [ - messageLine(workspaceId, createMuxMessage("msg1", "user", "Hello", { historySequence: 0 })), + messageLine(workspaceId, createXumMessage("msg1", "user", "Hello", { historySequence: 0 })), "invalid json line", - messageLine(workspaceId, createMuxMessage("msg2", "user", "World", { historySequence: 1 })), + messageLine(workspaceId, createXumMessage("msg2", "user", "World", { historySequence: 1 })), ]); const messages = await collectFullHistory(service, workspaceId); @@ -100,7 +100,7 @@ describe("HistoryService", () => { it("hydrates legacy cmuxMetadata entries", async () => { const workspaceId = "workspace-legacy"; - const legacyMessage = createMuxMessage("msg-legacy", "user", "legacy", { + const legacyMessage = createXumMessage("msg-legacy", "user", "legacy", { historySequence: 0, }); (legacyMessage.metadata as Record).cmuxMetadata = { type: "normal" }; @@ -112,7 +112,7 @@ describe("HistoryService", () => { it("should handle empty lines in history file", async () => { const workspaceId = "workspace1"; await writeHistoryLines(config, workspaceId, [ - messageLine(workspaceId, createMuxMessage("msg1", "user", "Hello", { historySequence: 0 })), + messageLine(workspaceId, createXumMessage("msg1", "user", "Hello", { historySequence: 0 })), "", "", ]); @@ -126,7 +126,7 @@ describe("HistoryService", () => { describe("appendToHistory", () => { it("should create workspace directory if it doesn't exist", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); const result = await service.appendToHistory(workspaceId, msg); @@ -141,7 +141,7 @@ describe("HistoryService", () => { it("should assign historySequence to message without metadata", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); const result = await service.appendToHistory(workspaceId, msg); @@ -153,9 +153,9 @@ describe("HistoryService", () => { it("should assign sequential historySequence numbers", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "Hello"); - const msg2 = createMuxMessage("msg2", "assistant", "Hi"); - const msg3 = createMuxMessage("msg3", "user", "How are you?"); + const msg1 = createXumMessage("msg1", "user", "Hello"); + const msg2 = createXumMessage("msg2", "assistant", "Hi"); + const msg3 = createXumMessage("msg3", "user", "How are you?"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); @@ -170,7 +170,7 @@ describe("HistoryService", () => { it("should preserve existing historySequence if provided", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello", { historySequence: 5 }); + const msg = createXumMessage("msg1", "user", "Hello", { historySequence: 5 }); const result = await service.appendToHistory(workspaceId, msg); @@ -182,7 +182,7 @@ describe("HistoryService", () => { it("should reject malformed provided historySequence values", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello", { historySequence: 5.5 }); + const msg = createXumMessage("msg1", "user", "Hello", { historySequence: 5.5 }); const result = await service.appendToHistory(workspaceId, msg); @@ -194,8 +194,8 @@ describe("HistoryService", () => { it("should update sequence counter when message has higher sequence", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "Hello", { historySequence: 10 }); - const msg2 = createMuxMessage("msg2", "user", "World"); + const msg1 = createXumMessage("msg1", "user", "Hello", { historySequence: 10 }); + const msg2 = createXumMessage("msg2", "user", "World"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); @@ -211,9 +211,9 @@ describe("HistoryService", () => { await fs.mkdir(workspaceDir, { recursive: true }); const messages = [ - createMuxMessage("msg-low", "user", "low", { historySequence: 0 }), - createMuxMessage("msg-high", "assistant", "high", { historySequence: 100 }), - createMuxMessage("msg-stale-tail", "assistant", "stale", { historySequence: 10 }), + createXumMessage("msg-low", "user", "low", { historySequence: 0 }), + createXumMessage("msg-high", "assistant", "high", { historySequence: 100 }), + createXumMessage("msg-stale-tail", "assistant", "stale", { historySequence: 10 }), ]; const chatPath = path.join(workspaceDir, "chat.jsonl"); await fs.writeFile( @@ -222,7 +222,7 @@ describe("HistoryService", () => { ); const restartedService = new HistoryService(config); - const nextMessage = createMuxMessage("msg-next", "user", "next"); + const nextMessage = createXumMessage("msg-next", "user", "next"); const appendResult = await restartedService.appendToHistory(workspaceId, nextMessage); expect(appendResult.success).toBe(true); @@ -233,15 +233,15 @@ describe("HistoryService", () => { const workspaceId = "workspace-stale-provided-sequence"; await service.appendToHistory( workspaceId, - createMuxMessage("msg-low", "user", "low", { historySequence: 0 }) + createXumMessage("msg-low", "user", "low", { historySequence: 0 }) ); await service.appendToHistory( workspaceId, - createMuxMessage("msg-high", "assistant", "high", { historySequence: 100 }) + createXumMessage("msg-high", "assistant", "high", { historySequence: 100 }) ); const restartedService = new HistoryService(config); - const staleMessage = createMuxMessage("msg-stale", "assistant", "stale", { + const staleMessage = createXumMessage("msg-stale", "assistant", "stale", { historySequence: 10, }); const staleResult = await restartedService.appendToHistory(workspaceId, staleMessage); @@ -251,7 +251,7 @@ describe("HistoryService", () => { expect(staleResult.error).toContain("stale historySequence 10"); } - const nextMessage = createMuxMessage("msg-next", "user", "next"); + const nextMessage = createXumMessage("msg-next", "user", "next"); const nextResult = await restartedService.appendToHistory(workspaceId, nextMessage); expect(nextResult.success).toBe(true); expect(nextMessage.metadata?.historySequence).toBe(101); @@ -259,7 +259,7 @@ describe("HistoryService", () => { it("should preserve other metadata fields", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello", { + const msg = createXumMessage("msg1", "user", "Hello", { timestamp: 123456, model: "claude-opus-4", providerMetadata: { test: "data" }, @@ -276,7 +276,7 @@ describe("HistoryService", () => { it("should include workspaceId in persisted message", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); await service.appendToHistory(workspaceId, msg); @@ -296,14 +296,14 @@ describe("HistoryService", () => { describe("updateHistory", () => { it("should update message by historySequence", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "Hello"); - const msg2 = createMuxMessage("msg2", "assistant", "Hi"); + const msg1 = createXumMessage("msg1", "user", "Hello"); + const msg2 = createXumMessage("msg2", "assistant", "Hi"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); const messages = await collectFullHistory(service, workspaceId); - const updatedMsg = createMuxMessage("msg1", "user", "Updated Hello", { + const updatedMsg = createXumMessage("msg1", "user", "Updated Hello", { historySequence: messages[0].metadata?.historySequence, }); @@ -320,7 +320,7 @@ describe("HistoryService", () => { it("should return error if message has no historySequence", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); const result = await service.updateHistory(workspaceId, msg); @@ -332,11 +332,11 @@ describe("HistoryService", () => { it("should return error if message with historySequence not found", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "Hello"); + const msg1 = createXumMessage("msg1", "user", "Hello"); await service.appendToHistory(workspaceId, msg1); - const msg2 = createMuxMessage("msg2", "user", "Not found", { historySequence: 99 }); + const msg2 = createXumMessage("msg2", "user", "Not found", { historySequence: 99 }); const result = await service.updateHistory(workspaceId, msg2); expect(result.success).toBe(false); @@ -347,13 +347,13 @@ describe("HistoryService", () => { it("should preserve historySequence when updating", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); await service.appendToHistory(workspaceId, msg); const messages = await collectFullHistory(service, workspaceId); const originalSequence = messages[0].metadata?.historySequence; - const updatedMsg = createMuxMessage("msg1", "user", "Updated", { + const updatedMsg = createXumMessage("msg1", "user", "Updated", { historySequence: originalSequence, }); @@ -365,7 +365,7 @@ describe("HistoryService", () => { it("preserves durable compaction metadata across late in-place rewrites", async () => { const workspaceId = "workspace1"; - const placeholder = createMuxMessage("summary-msg", "assistant", "", { + const placeholder = createXumMessage("summary-msg", "assistant", "", { model: "openai:gpt-5", }); @@ -380,7 +380,7 @@ describe("HistoryService", () => { } // Simulate compaction finishing first and upgrading the streamed placeholder in place. - const compactionSummary = createMuxMessage("summary-msg", "assistant", "Compacted summary", { + const compactionSummary = createXumMessage("summary-msg", "assistant", "Compacted summary", { historySequence: sequence, compacted: "user", compactionBoundary: true, @@ -392,7 +392,7 @@ describe("HistoryService", () => { // Simulate a late stream rewrite (e.g., simulateToolPolicyNoop path) that omits // compaction metadata. The durable boundary markers must survive this rewrite. - const lateRewrite = createMuxMessage( + const lateRewrite = createXumMessage( "summary-msg", "assistant", "Tool execution skipped because the requested tool is disabled by policy.", @@ -419,7 +419,7 @@ describe("HistoryService", () => { it("self-heals by not preserving malformed compaction boundary metadata", async () => { const workspaceId = "workspace1"; - const placeholder = createMuxMessage("summary-msg", "assistant", "", { + const placeholder = createXumMessage("summary-msg", "assistant", "", { model: "openai:gpt-5", }); @@ -434,7 +434,7 @@ describe("HistoryService", () => { } // Simulate malformed persisted boundary metadata (invalid epoch). - const malformedCompactionSummary = createMuxMessage( + const malformedCompactionSummary = createXumMessage( "summary-msg", "assistant", "Compacted summary", @@ -451,7 +451,7 @@ describe("HistoryService", () => { ); expect(malformedUpdateResult.success).toBe(true); - const lateRewrite = createMuxMessage("summary-msg", "assistant", "Late rewrite", { + const lateRewrite = createXumMessage("summary-msg", "assistant", "Late rewrite", { historySequence: sequence, model: "openai:gpt-5", }); @@ -466,7 +466,7 @@ describe("HistoryService", () => { it("self-heals by not preserving malformed compacted markers in compaction boundaries", async () => { const workspaceId = "workspace1"; - const placeholder = createMuxMessage("summary-msg", "assistant", "", { + const placeholder = createXumMessage("summary-msg", "assistant", "", { model: "openai:gpt-5", }); @@ -480,7 +480,7 @@ describe("HistoryService", () => { return; } - const malformedCompactionSummary = createMuxMessage( + const malformedCompactionSummary = createXumMessage( "summary-msg", "assistant", "Compacted summary", @@ -500,7 +500,7 @@ describe("HistoryService", () => { ); expect(malformedUpdateResult.success).toBe(true); - const lateRewrite = createMuxMessage("summary-msg", "assistant", "Late rewrite", { + const lateRewrite = createXumMessage("summary-msg", "assistant", "Late rewrite", { historySequence: sequence, model: "openai:gpt-5", }); @@ -518,9 +518,9 @@ describe("HistoryService", () => { describe("deleteMessage", () => { it("should remove only the targeted message and preserve subsequent messages", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "First"); - const msg2 = createMuxMessage("msg2", "assistant", "Second"); - const msg3 = createMuxMessage("msg3", "user", "Third"); + const msg1 = createXumMessage("msg1", "user", "First"); + const msg2 = createXumMessage("msg2", "assistant", "Second"); + const msg3 = createXumMessage("msg3", "user", "Third"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); @@ -533,7 +533,7 @@ describe("HistoryService", () => { expect(messages).toHaveLength(2); expect(messages.map((message) => message.id)).toEqual(["msg1", "msg3"]); - const msg4 = createMuxMessage("msg4", "assistant", "Fourth"); + const msg4 = createXumMessage("msg4", "assistant", "Fourth"); await service.appendToHistory(workspaceId, msg4); const messagesAfterAppend = await collectFullHistory(service, workspaceId); @@ -549,7 +549,7 @@ describe("HistoryService", () => { it("should return error if message not found", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); await service.appendToHistory(workspaceId, msg); @@ -565,15 +565,15 @@ describe("HistoryService", () => { describe("deleteMessages", () => { it("atomically removes only targeted rows and preserves later concurrent rows", async () => { const workspaceId = "workspace-delete-messages"; - await service.appendToHistory(workspaceId, createMuxMessage("before", "assistant", "Before")); + await service.appendToHistory(workspaceId, createXumMessage("before", "assistant", "Before")); await service.appendToHistory( workspaceId, - createMuxMessage("wake-snapshot", "user", "Snapshot") + createXumMessage("wake-snapshot", "user", "Snapshot") ); - await service.appendToHistory(workspaceId, createMuxMessage("wake", "user", "Wake")); + await service.appendToHistory(workspaceId, createXumMessage("wake", "user", "Wake")); await service.appendToHistory( workspaceId, - createMuxMessage("pause-boundary", "user", "Goal paused") + createXumMessage("pause-boundary", "user", "Goal paused") ); const result = await service.deleteMessages(workspaceId, ["wake-snapshot", "wake"]); @@ -585,8 +585,8 @@ describe("HistoryService", () => { it("does not rewrite history when any target is missing", async () => { const workspaceId = "workspace-delete-messages-missing"; - await service.appendToHistory(workspaceId, createMuxMessage("wake", "user", "Wake")); - await service.appendToHistory(workspaceId, createMuxMessage("later", "user", "Later")); + await service.appendToHistory(workspaceId, createXumMessage("wake", "user", "Wake")); + await service.appendToHistory(workspaceId, createXumMessage("later", "user", "Later")); const result = await service.deleteMessages(workspaceId, ["wake", "missing"]); expect(result.success).toBe(false); @@ -599,10 +599,10 @@ describe("HistoryService", () => { describe("truncateAfterMessage", () => { it("should remove message and all subsequent messages", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "First"); - const msg2 = createMuxMessage("msg2", "assistant", "Second"); - const msg3 = createMuxMessage("msg3", "user", "Third"); - const msg4 = createMuxMessage("msg4", "assistant", "Fourth"); + const msg1 = createXumMessage("msg1", "user", "First"); + const msg2 = createXumMessage("msg2", "assistant", "Second"); + const msg3 = createXumMessage("msg3", "user", "Third"); + const msg4 = createXumMessage("msg4", "assistant", "Fourth"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); @@ -620,9 +620,9 @@ describe("HistoryService", () => { it("should update sequence counter after truncation", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "First"); - const msg2 = createMuxMessage("msg2", "assistant", "Second"); - const msg3 = createMuxMessage("msg3", "user", "Third"); + const msg1 = createXumMessage("msg1", "user", "First"); + const msg2 = createXumMessage("msg2", "assistant", "Second"); + const msg3 = createXumMessage("msg3", "user", "Third"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); @@ -631,7 +631,7 @@ describe("HistoryService", () => { await service.truncateAfterMessage(workspaceId, "msg2"); // Append a new message and check its sequence - const msg4 = createMuxMessage("msg4", "user", "New message"); + const msg4 = createXumMessage("msg4", "user", "New message"); await service.appendToHistory(workspaceId, msg4); const messages = await collectFullHistory(service, workspaceId); @@ -642,15 +642,15 @@ describe("HistoryService", () => { it("should reset sequence counter when truncating all messages", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "First"); - const msg2 = createMuxMessage("msg2", "assistant", "Second"); + const msg1 = createXumMessage("msg1", "user", "First"); + const msg2 = createXumMessage("msg2", "assistant", "Second"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); await service.truncateAfterMessage(workspaceId, "msg1"); - const msg3 = createMuxMessage("msg3", "user", "New"); + const msg3 = createXumMessage("msg3", "user", "New"); await service.appendToHistory(workspaceId, msg3); const messages = await collectFullHistory(service, workspaceId); @@ -660,7 +660,7 @@ describe("HistoryService", () => { it("should return error if message not found", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); await service.appendToHistory(workspaceId, msg); @@ -676,9 +676,9 @@ describe("HistoryService", () => { describe("truncateAfterMessage keepTargetMessage", () => { it("should retain the target message when requested", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "First"); - const msg2 = createMuxMessage("msg2", "assistant", "Second"); - const msg3 = createMuxMessage("msg3", "user", "Third"); + const msg1 = createXumMessage("msg1", "user", "First"); + const msg2 = createXumMessage("msg2", "assistant", "Second"); + const msg3 = createXumMessage("msg3", "user", "Third"); await service.appendToHistory(workspaceId, msg1); await service.appendToHistory(workspaceId, msg2); @@ -700,7 +700,7 @@ describe("HistoryService", () => { describe("clearHistory", () => { it("should delete chat.jsonl file", async () => { const workspaceId = "workspace1"; - const msg = createMuxMessage("msg1", "user", "Hello"); + const msg = createXumMessage("msg1", "user", "Hello"); await service.appendToHistory(workspaceId, msg); @@ -719,12 +719,12 @@ describe("HistoryService", () => { it("should reset sequence counter", async () => { const workspaceId = "workspace1"; - const msg1 = createMuxMessage("msg1", "user", "Hello"); + const msg1 = createXumMessage("msg1", "user", "Hello"); await service.appendToHistory(workspaceId, msg1); await service.clearHistory(workspaceId); - const msg2 = createMuxMessage("msg2", "user", "New message"); + const msg2 = createXumMessage("msg2", "user", "New message"); await service.appendToHistory(workspaceId, msg2); const messages = await collectFullHistory(service, workspaceId); @@ -744,7 +744,7 @@ describe("HistoryService", () => { await service.clearHistory(workspaceId); - const msg = createMuxMessage("msg1", "user", "First"); + const msg = createXumMessage("msg1", "user", "First"); await service.appendToHistory(workspaceId, msg); const messages = await collectFullHistory(service, workspaceId); @@ -759,8 +759,8 @@ describe("HistoryService", () => { await fs.mkdir(workspaceDir, { recursive: true }); // Manually create history with specific sequences - const msg1 = createMuxMessage("msg1", "user", "Hello", { historySequence: 0 }); - const msg2 = createMuxMessage("msg2", "assistant", "Hi", { historySequence: 1 }); + const msg1 = createXumMessage("msg1", "user", "Hello", { historySequence: 0 }); + const msg2 = createXumMessage("msg2", "assistant", "Hi", { historySequence: 1 }); const chatPath = path.join(workspaceDir, "chat.jsonl"); await fs.writeFile( @@ -775,7 +775,7 @@ describe("HistoryService", () => { const newService = new HistoryService(config); // Append a new message - should get sequence 2 - const msg3 = createMuxMessage("msg3", "user", "How are you?"); + const msg3 = createXumMessage("msg3", "user", "How are you?"); await newService.appendToHistory(workspaceId, msg3); const messages = await collectFullHistory(newService, workspaceId); @@ -788,8 +788,8 @@ describe("HistoryService", () => { const workspaceDir = config.getSessionDir(workspaceId); await fs.mkdir(workspaceDir, { recursive: true }); - const validMessage = createMuxMessage("msg-valid", "user", "Hello", { historySequence: 3 }); - const malformedMessage = createMuxMessage("msg-malformed", "assistant", "Hi", { + const validMessage = createXumMessage("msg-valid", "user", "Hello", { historySequence: 3 }); + const malformedMessage = createXumMessage("msg-malformed", "assistant", "Hi", { historySequence: 42, }); if (malformedMessage.metadata) { @@ -806,7 +806,7 @@ describe("HistoryService", () => { ); const newService = new HistoryService(config); - const msg3 = createMuxMessage("msg3", "user", "How are you?"); + const msg3 = createXumMessage("msg3", "user", "How are you?"); const appendResult = await newService.appendToHistory(workspaceId, msg3); expect(appendResult.success).toBe(true); @@ -818,7 +818,7 @@ describe("HistoryService", () => { it("should start from 0 for new workspace", async () => { const workspaceId = "new-workspace"; - const msg = createMuxMessage("msg1", "user", "First message"); + const msg = createXumMessage("msg1", "user", "First message"); await service.appendToHistory(workspaceId, msg); @@ -853,7 +853,7 @@ describe("HistoryService", () => { preBoundaryIds.push(id); lines.push( JSON.stringify({ - ...createMuxMessage(id, "user", `message ${i}`, { historySequence: seq++ }), + ...createXumMessage(id, "user", `message ${i}`, { historySequence: seq++ }), workspaceId, }) ); @@ -863,7 +863,7 @@ describe("HistoryService", () => { const boundaryId = `boundary-${epoch}`; lines.push( JSON.stringify({ - ...createMuxMessage(boundaryId, "assistant", "Compaction summary", { + ...createXumMessage(boundaryId, "assistant", "Compaction summary", { historySequence: seq++, compactionBoundary: true, compacted: "user", @@ -879,7 +879,7 @@ describe("HistoryService", () => { postBoundaryIds.push(id); lines.push( JSON.stringify({ - ...createMuxMessage(id, "user", `post message ${i}`, { historySequence: seq++ }), + ...createXumMessage(id, "user", `post message ${i}`, { historySequence: seq++ }), workspaceId, }) ); @@ -895,8 +895,8 @@ describe("HistoryService", () => { const workspaceDir = config.getSessionDir(workspaceId); await fs.mkdir(workspaceDir, { recursive: true }); - const msg1 = createMuxMessage("msg1", "user", "Hello", { historySequence: 0 }); - const msg2 = createMuxMessage("msg2", "assistant", "Hi", { historySequence: 1 }); + const msg1 = createXumMessage("msg1", "user", "Hello", { historySequence: 0 }); + const msg2 = createXumMessage("msg2", "assistant", "Hi", { historySequence: 1 }); await fs.writeFile( path.join(workspaceDir, "chat.jsonl"), JSON.stringify({ ...msg1, workspaceId }) + @@ -952,13 +952,13 @@ describe("HistoryService", () => { // Epoch 1 messages + boundary lines.push( JSON.stringify({ - ...createMuxMessage("e1-user", "user", "msg", { historySequence: seq++ }), + ...createXumMessage("e1-user", "user", "msg", { historySequence: seq++ }), workspaceId, }) ); lines.push( JSON.stringify({ - ...createMuxMessage("e1-boundary", "assistant", "Summary 1", { + ...createXumMessage("e1-boundary", "assistant", "Summary 1", { historySequence: seq++, compactionBoundary: true, compacted: "user", @@ -971,13 +971,13 @@ describe("HistoryService", () => { // Epoch 2 messages + boundary lines.push( JSON.stringify({ - ...createMuxMessage("e2-user", "user", "msg", { historySequence: seq++ }), + ...createXumMessage("e2-user", "user", "msg", { historySequence: seq++ }), workspaceId, }) ); lines.push( JSON.stringify({ - ...createMuxMessage("e2-boundary", "assistant", "Summary 2", { + ...createXumMessage("e2-boundary", "assistant", "Summary 2", { historySequence: seq++, compactionBoundary: true, compacted: "idle", @@ -990,7 +990,7 @@ describe("HistoryService", () => { // Post-epoch-2 message lines.push( JSON.stringify({ - ...createMuxMessage("post-e2", "user", "after both", { historySequence: seq++ }), + ...createXumMessage("post-e2", "user", "after both", { historySequence: seq++ }), workspaceId, }) ); @@ -1023,13 +1023,13 @@ describe("HistoryService", () => { const workspaceDir = config.getSessionDir(workspaceId); await fs.mkdir(workspaceDir, { recursive: true }); - const boundary = createMuxMessage("boundary", "assistant", "Summary", { + const boundary = createXumMessage("boundary", "assistant", "Summary", { historySequence: 0, compactionBoundary: true, compacted: "user", compactionEpoch: 1, }); - const post = createMuxMessage("post", "user", "after", { historySequence: 1 }); + const post = createXumMessage("post", "user", "after", { historySequence: 1 }); await fs.writeFile( path.join(workspaceDir, "chat.jsonl"), @@ -1061,13 +1061,13 @@ describe("HistoryService", () => { lines.push( JSON.stringify({ - ...createMuxMessage("e1-user", "user", "epoch 1 user", { historySequence: seq++ }), + ...createXumMessage("e1-user", "user", "epoch 1 user", { historySequence: seq++ }), workspaceId, }) ); lines.push( JSON.stringify({ - ...createMuxMessage("e1-boundary", "assistant", "summary 1", { + ...createXumMessage("e1-boundary", "assistant", "summary 1", { historySequence: seq++, compactionBoundary: true, compacted: "user", @@ -1078,13 +1078,13 @@ describe("HistoryService", () => { ); lines.push( JSON.stringify({ - ...createMuxMessage("e2-user", "user", "epoch 2 user", { historySequence: seq++ }), + ...createXumMessage("e2-user", "user", "epoch 2 user", { historySequence: seq++ }), workspaceId, }) ); lines.push( JSON.stringify({ - ...createMuxMessage("e2-boundary", "assistant", "summary 2", { + ...createXumMessage("e2-boundary", "assistant", "summary 2", { historySequence: seq++, compactionBoundary: true, compacted: "idle", @@ -1095,7 +1095,7 @@ describe("HistoryService", () => { ); lines.push( JSON.stringify({ - ...createMuxMessage("post-e2", "user", "latest message", { historySequence: seq++ }), + ...createXumMessage("post-e2", "user", "latest message", { historySequence: seq++ }), workspaceId, }) ); @@ -1130,7 +1130,7 @@ describe("HistoryService", () => { const lines = [ messageLine( workspaceId, - createMuxMessage("old-boundary", "assistant", "old summary", { + createXumMessage("old-boundary", "assistant", "old summary", { historySequence: 0, compactionBoundary: true, compacted: "user", @@ -1139,18 +1139,18 @@ describe("HistoryService", () => { ), messageLine( workspaceId, - createMuxMessage("kept-user", "user", "durable preference", { historySequence: 1 }) + createXumMessage("kept-user", "user", "durable preference", { historySequence: 1 }) ), messageLine( workspaceId, - createMuxMessage("compact-request", "user", "Please compact", { + createXumMessage("compact-request", "user", "Please compact", { historySequence: 2, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }) ), messageLine( workspaceId, - createMuxMessage("new-summary", "assistant", "new summary", { + createXumMessage("new-summary", "assistant", "new summary", { historySequence: 3, compactionBoundary: true, compacted: "user", @@ -1184,7 +1184,7 @@ describe("HistoryService", () => { const replayedPrefix = [ messageLine( workspaceId, - createMuxMessage("old-boundary", "assistant", "old summary", { + createXumMessage("old-boundary", "assistant", "old summary", { historySequence: 0, compactionBoundary: true, compacted: "user", @@ -1193,11 +1193,11 @@ describe("HistoryService", () => { ), messageLine( workspaceId, - createMuxMessage("kept-user", "user", "durable preference", { historySequence: 1 }) + createXumMessage("kept-user", "user", "durable preference", { historySequence: 1 }) ), messageLine( workspaceId, - createMuxMessage("compact-request", "user", "Please compact", { + createXumMessage("compact-request", "user", "Please compact", { historySequence: 2, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }) @@ -1205,7 +1205,7 @@ describe("HistoryService", () => { ]; const summary = messageLine( workspaceId, - createMuxMessage("new-summary", "assistant", "new summary", { + createXumMessage("new-summary", "assistant", "new summary", { historySequence: 3, compactionBoundary: true, compacted: "user", @@ -1243,7 +1243,7 @@ describe("HistoryService", () => { await writeHistoryLines(config, workspaceId, [ messageLine( workspaceId, - createMuxMessage("old-boundary", "assistant", "old summary", { + createXumMessage("old-boundary", "assistant", "old summary", { historySequence: 0, compactionBoundary: true, compacted: "user", @@ -1252,18 +1252,18 @@ describe("HistoryService", () => { ), messageLine( workspaceId, - createMuxMessage("kept-user", "user", "durable preference", { historySequence: 1 }) + createXumMessage("kept-user", "user", "durable preference", { historySequence: 1 }) ), messageLine( workspaceId, - createMuxMessage("compact-request", "user", "Please compact", { + createXumMessage("compact-request", "user", "Please compact", { historySequence: 2, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }) ), messageLine( workspaceId, - createMuxMessage("summary", "assistant", "summary", { + createXumMessage("summary", "assistant", "summary", { historySequence: 3, compactionBoundary: true, compacted: "user", @@ -1328,29 +1328,29 @@ describe("HistoryService", () => { await writeHistoryLines(config, workspaceId, [ messageLine( workspaceId, - createMuxMessage("stale-user", "user", "old preference", { historySequence: 0 }) + createXumMessage("stale-user", "user", "old preference", { historySequence: 0 }) ), messageLine( workspaceId, - createMuxMessage("reset", "assistant", "Context reset", { + createXumMessage("reset", "assistant", "Context reset", { historySequence: 1, contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }) ), messageLine( workspaceId, - createMuxMessage("kept-user", "user", "new preference", { historySequence: 2 }) + createXumMessage("kept-user", "user", "new preference", { historySequence: 2 }) ), messageLine( workspaceId, - createMuxMessage("compact-request", "user", "Please compact", { + createXumMessage("compact-request", "user", "Please compact", { historySequence: 3, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }) ), messageLine( workspaceId, - createMuxMessage("summary", "assistant", "summary", { + createXumMessage("summary", "assistant", "summary", { historySequence: 4, compactionBoundary: true, compacted: "user", @@ -1379,7 +1379,7 @@ describe("HistoryService", () => { await writeHistoryLines(config, workspaceId, [ messageLine( workspaceId, - createMuxMessage("valid-boundary", "assistant", "old summary", { + createXumMessage("valid-boundary", "assistant", "old summary", { historySequence: 0, compactionBoundary: true, compacted: "user", @@ -1388,33 +1388,33 @@ describe("HistoryService", () => { ), messageLine( workspaceId, - createMuxMessage("before-malformed", "user", "valid evidence before malformed row", { + createXumMessage("before-malformed", "user", "valid evidence before malformed row", { historySequence: 1, }) ), messageLine( workspaceId, - createMuxMessage("malformed-boundary", "user", "corrupt boundary-like row", { + createXumMessage("malformed-boundary", "user", "corrupt boundary-like row", { historySequence: 2, compactionBoundary: true, }) ), messageLine( workspaceId, - createMuxMessage("after-malformed", "user", "valid evidence after malformed row", { + createXumMessage("after-malformed", "user", "valid evidence after malformed row", { historySequence: 3, }) ), messageLine( workspaceId, - createMuxMessage("compact-request", "user", "Please compact", { + createXumMessage("compact-request", "user", "Please compact", { historySequence: 4, muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }) ), messageLine( workspaceId, - createMuxMessage("summary", "assistant", "summary", { + createXumMessage("summary", "assistant", "summary", { historySequence: 5, compactionBoundary: true, compacted: "user", @@ -1484,7 +1484,7 @@ describe("HistoryService", () => { Array.from({ length: testCase.totalMessages }, (_, i) => messageLine( testCase.workspaceId, - createMuxMessage(`msg-${i}`, "user", `message ${i}`, { historySequence: i }) + createXumMessage(`msg-${i}`, "user", `message ${i}`, { historySequence: i }) ) ) ); @@ -1500,11 +1500,11 @@ describe("HistoryService", () => { it("should skip malformed lines", async () => { const workspaceId = "ws-last-malformed"; await writeHistoryLines(config, workspaceId, [ - messageLine(workspaceId, createMuxMessage("msg1", "user", "Hello", { historySequence: 0 })), + messageLine(workspaceId, createXumMessage("msg1", "user", "Hello", { historySequence: 0 })), "BAD LINE", messageLine( workspaceId, - createMuxMessage("msg2", "assistant", "Hi", { historySequence: 1 }) + createXumMessage("msg2", "assistant", "Hi", { historySequence: 1 }) ), ]); @@ -1530,7 +1530,7 @@ describe("HistoryService", () => { // Pre-boundary: message with emoji (4-byte UTF-8 chars) lines.push( JSON.stringify({ - ...createMuxMessage("emoji-msg", "user", "Hello 🌍🔥💻 world", { + ...createXumMessage("emoji-msg", "user", "Hello 🌍🔥💻 world", { historySequence: seq++, }), workspaceId, @@ -1540,7 +1540,7 @@ describe("HistoryService", () => { // Boundary with CJK characters (3-byte UTF-8 chars) lines.push( JSON.stringify({ - ...createMuxMessage("boundary-utf8", "assistant", "要約:会話の概要", { + ...createXumMessage("boundary-utf8", "assistant", "要約:会話の概要", { historySequence: seq++, compactionBoundary: true, compacted: "user", @@ -1553,7 +1553,7 @@ describe("HistoryService", () => { // Post-boundary: message with mixed scripts lines.push( JSON.stringify({ - ...createMuxMessage("post-utf8", "user", "Ñoño café résumé über 日本語", { + ...createXumMessage("post-utf8", "user", "Ñoño café résumé über 日本語", { historySequence: seq++, }), workspaceId, @@ -1591,7 +1591,7 @@ describe("HistoryService", () => { for (let i = 0; i < 5; i++) { lines.push( JSON.stringify({ - ...createMuxMessage(`utf8-${i}`, "user", `メッセージ ${i} 🎯`, { + ...createXumMessage(`utf8-${i}`, "user", `メッセージ ${i} 🎯`, { historySequence: i, }), workspaceId, @@ -1632,7 +1632,7 @@ describe("HistoryService", () => { const workspaceDir = config.getSessionDir(workspaceId); await fs.mkdir(workspaceDir, { recursive: true }); - const msg = createMuxMessage("msg1", "user", "Hello", { historySequence: 0 }); + const msg = createXumMessage("msg1", "user", "Hello", { historySequence: 0 }); await fs.writeFile( path.join(workspaceDir, "chat.jsonl"), JSON.stringify({ ...msg, workspaceId }) + "\n" @@ -1649,7 +1649,7 @@ describe("HistoryService", () => { it("should iterate forward in chronological order", async () => { await appendNumberedMessages(service, wsId, 5); - const collected: MuxMessage[] = []; + const collected: XumMessage[] = []; const result = await service.iterateFullHistory(wsId, "forward", (chunk) => { collected.push(...chunk); }); @@ -1661,7 +1661,7 @@ describe("HistoryService", () => { it("should iterate backward with newest first", async () => { await appendNumberedMessages(service, wsId, 5); - const collected: MuxMessage[] = []; + const collected: XumMessage[] = []; const result = await service.iterateFullHistory(wsId, "backward", (chunk) => { collected.push(...chunk); }); @@ -1674,7 +1674,7 @@ describe("HistoryService", () => { it("should support early exit by returning false", async () => { await appendNumberedMessages(service, wsId, 10); - let found: MuxMessage | undefined; + let found: XumMessage | undefined; await service.iterateFullHistory(wsId, "forward", (chunk) => { for (const msg of chunk) { if (msg.id === "msg-3") { @@ -1691,7 +1691,7 @@ describe("HistoryService", () => { await appendNumberedMessages(service, wsId, 10); // Find the first message encountered when reading backward (should be msg-9) - let firstSeen: MuxMessage | undefined; + let firstSeen: XumMessage | undefined; await service.iterateFullHistory(wsId, "backward", (chunk) => { firstSeen = chunk[0]; return false; // stop after first chunk @@ -1701,7 +1701,7 @@ describe("HistoryService", () => { }); it("should return success for empty history", async () => { - const collected: MuxMessage[] = []; + const collected: XumMessage[] = []; const result = await service.iterateFullHistory(wsId, "forward", (chunk) => { collected.push(...chunk); }); @@ -1712,11 +1712,11 @@ describe("HistoryService", () => { it("should skip malformed lines during iteration", async () => { await writeHistoryLines(config, wsId, [ "not valid json", - messageLine(wsId, createMuxMessage("valid-1", "user", "Valid message")), + messageLine(wsId, createXumMessage("valid-1", "user", "Valid message")), "{malformed", ]); - const collected: MuxMessage[] = []; + const collected: XumMessage[] = []; const result = await service.iterateFullHistory(wsId, "forward", (chunk) => { collected.push(...chunk); }); @@ -1729,20 +1729,20 @@ describe("HistoryService", () => { describe("sealed history rotation", () => { const wsId = "ws-rotation"; - function boundaryMessage(id: string, epoch: number): MuxMessage { - return createMuxMessage(id, "assistant", `Summary ${epoch}`, { + function boundaryMessage(id: string, epoch: number): XumMessage { + return createXumMessage(id, "assistant", `Summary ${epoch}`, { compactionBoundary: true, compacted: "user", compactionEpoch: epoch, }); } - async function readJsonlFile(filePath: string): Promise { + async function readJsonlFile(filePath: string): Promise { const data = await fs.readFile(filePath, "utf-8"); return data .split("\n") .filter((line) => line.trim()) - .map((line) => JSON.parse(line) as MuxMessage); + .map((line) => JSON.parse(line) as XumMessage); } function chatPath(workspaceId: string): string { @@ -1756,7 +1756,7 @@ describe("HistoryService", () => { it("rotates the sealed prefix into the archive when a boundary is appended", async () => { await appendNumberedMessages(service, wsId, 3); // seq 0..2 await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); // seq 3 - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); // seq 4 + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); // seq 4 // Active file holds only the latest epoch; sealed rows moved to the archive. const chatRows = await readJsonlFile(chatPath(wsId)); @@ -1786,12 +1786,12 @@ describe("HistoryService", () => { it("lazily rotates legacy files with a mid-file boundary on first read", async () => { const lines = [ - messageLine(wsId, createMuxMessage("old-0", "user", "old", { historySequence: 0 })), + messageLine(wsId, createXumMessage("old-0", "user", "old", { historySequence: 0 })), messageLine(wsId, { ...boundaryMessage("boundary-1", 1), metadata: { ...boundaryMessage("boundary-1", 1).metadata, historySequence: 1 }, }), - messageLine(wsId, createMuxMessage("post-0", "user", "after", { historySequence: 2 })), + messageLine(wsId, createXumMessage("post-0", "user", "after", { historySequence: 2 })), ]; await writeHistoryLines(config, wsId, lines); @@ -1809,11 +1809,11 @@ describe("HistoryService", () => { }); it("reads boundary windows across the archive seam (skip + paging)", async () => { - await service.appendToHistory(wsId, createMuxMessage("e1-user", "user", "msg")); // seq 0 + await service.appendToHistory(wsId, createXumMessage("e1-user", "user", "msg")); // seq 0 await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); // seq 1 - await service.appendToHistory(wsId, createMuxMessage("e2-user", "user", "msg")); // seq 2 + await service.appendToHistory(wsId, createXumMessage("e2-user", "user", "msg")); // seq 2 await service.appendToHistory(wsId, boundaryMessage("boundary-2", 2)); // seq 3 - await service.appendToHistory(wsId, createMuxMessage("post", "user", "after")); // seq 4 + await service.appendToHistory(wsId, createXumMessage("post", "user", "after")); // seq 4 // Both sealed epochs live in the archive now. const archiveRows = await readJsonlFile(archivePath(wsId)); @@ -1856,7 +1856,7 @@ describe("HistoryService", () => { await fs.rm(chatPath(wsId)); const restarted = new HistoryService(config); - const msg = createMuxMessage("new-msg", "user", "fresh"); + const msg = createXumMessage("new-msg", "user", "fresh"); const appendResult = await restarted.appendToHistory(wsId, msg); expect(appendResult.success).toBe(true); expect(msg.metadata?.historySequence).toBe(3); @@ -1865,7 +1865,7 @@ describe("HistoryService", () => { it("deduplicates rows when a crash replays the sealed prefix", async () => { await appendNumberedMessages(service, wsId, 3); // seq 0..2 → archived after boundary await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); // seq 3 - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); // seq 4 + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); // seq 4 // Simulate a crash between the archive append and the chat.jsonl rewrite: // the sealed prefix reappears at the head of chat.jsonl while the archive @@ -1889,7 +1889,7 @@ describe("HistoryService", () => { it("returns the tail across the archive seam from getLastMessages", async () => { await appendNumberedMessages(service, wsId, 3); // seq 0..2 await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); // seq 3 - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); // seq 4 + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); // seq 4 const result = await service.getLastMessages(wsId, 4); expect(result.success).toBe(true); @@ -1902,7 +1902,7 @@ describe("HistoryService", () => { it("truncates after an archived message and collapses the archive", async () => { await appendNumberedMessages(service, wsId, 3); // msg-0..2, seq 0..2 await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); // seq 3 - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); // seq 4 + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); // seq 4 const truncateResult = await service.truncateAfterMessage(wsId, "msg-1", { keepTargetMessage: true, @@ -1921,7 +1921,7 @@ describe("HistoryService", () => { ).toBe(false); // The sequence counter continues from the cut point. - const msg = createMuxMessage("new-msg", "user", "fresh"); + const msg = createXumMessage("new-msg", "user", "fresh"); await service.appendToHistory(wsId, msg); expect(msg.metadata?.historySequence).toBe(2); }); @@ -1929,14 +1929,14 @@ describe("HistoryService", () => { it("never reuses archived sequences after truncating the whole active epoch", async () => { await appendNumberedMessages(service, wsId, 3); // msg-0..2, seq 0..2 → archived await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); // seq 3 - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); // seq 4 + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); // seq 4 // Truncate at the boundary itself (without keeping it) — the active file // becomes empty while the archive still holds seq 0..2. const truncateResult = await service.truncateAfterMessage(wsId, "boundary-1"); expect(truncateResult.success).toBe(true); - const msg = createMuxMessage("new-msg", "user", "fresh"); + const msg = createXumMessage("new-msg", "user", "fresh"); await service.appendToHistory(wsId, msg); expect(msg.metadata?.historySequence).toBe(3); }); @@ -1985,7 +1985,7 @@ describe("HistoryService", () => { const deleteResult = await restarted.deleteMessage(wsId, "boundary-1"); expect(deleteResult.success).toBe(true); - const msg = createMuxMessage("new-msg", "user", "fresh"); + const msg = createXumMessage("new-msg", "user", "fresh"); await restarted.appendToHistory(wsId, msg); expect(msg.metadata?.historySequence).toBe(3); }); @@ -2004,7 +2004,7 @@ describe("HistoryService", () => { const migrateResult = await restarted.migrateWorkspaceId(wsId, newWsId); expect(migrateResult.success).toBe(true); - const msg = createMuxMessage("new-msg", "user", "fresh"); + const msg = createXumMessage("new-msg", "user", "fresh"); await restarted.appendToHistory(newWsId, msg); expect(msg.metadata?.historySequence).toBe(3); }); @@ -2031,7 +2031,7 @@ describe("HistoryService", () => { await appendNumberedMessages(service, wsId, 8); await service.appendToHistory( wsId, - createMuxMessage("assistant-usage", "assistant", "reply", { + createXumMessage("assistant-usage", "assistant", "reply", { contextUsage: { inputTokens: 95_000, outputTokens: 100, totalTokens: 95_100 }, contextProviderMetadata: { openai: {} }, model: "openai:gpt-4o", @@ -2039,7 +2039,7 @@ describe("HistoryService", () => { ); await service.appendToHistory( wsId, - createMuxMessage("assistant-provider-metadata", "assistant", "reply", { + createXumMessage("assistant-provider-metadata", "assistant", "reply", { contextProviderMetadata: { openai: {} }, model: "openai:gpt-4o", }) @@ -2071,24 +2071,24 @@ describe("HistoryService", () => { await appendNumberedMessages(service, wsId, 12); await service.appendToHistory( wsId, - createMuxMessage("reset-boundary", "assistant", "", { + createXumMessage("reset-boundary", "assistant", "", { contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }) ); } await service.appendToHistory( wsId, - createMuxMessage( + createXumMessage( "workflow-display", "user", `workflow trigger display ${"x".repeat(2_000)}`, { muxMetadata: { type: "workflow-trigger-display", rawCommand: "/wf", runId: "run-1" } } ) ); - await service.appendToHistory(wsId, createMuxMessage("user-active", "user", "prompt")); + await service.appendToHistory(wsId, createXumMessage("user-active", "user", "prompt")); await service.appendToHistory( wsId, - createMuxMessage("assistant-active", "assistant", "active reply", { + createXumMessage("assistant-active", "assistant", "active reply", { contextUsage: { inputTokens: 95_000, outputTokens: 100, totalTokens: 95_100 }, model: "openai:gpt-4o", }) @@ -2117,7 +2117,7 @@ describe("HistoryService", () => { await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); await service.appendToHistory( wsId, - createMuxMessage("active-usage", "assistant", "reply", { + createXumMessage("active-usage", "assistant", "reply", { contextUsage: { inputTokens: 95_000, outputTokens: 100, totalTokens: 95_100 }, model: "openai:gpt-4o", }) @@ -2137,7 +2137,7 @@ describe("HistoryService", () => { it("restores a markerless archive tombstone left by an older truncation", async () => { await appendNumberedMessages(service, wsId, 3); await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); await fs.rename(archivePath(wsId), `${archivePath(wsId)}.truncate`); const restarted = new HistoryService(config); @@ -2154,7 +2154,7 @@ describe("HistoryService", () => { it("restores an interrupted archive tombstone when only the final chat matches", async () => { await appendNumberedMessages(service, wsId, 3); await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); const chatContents = await fs.readFile(chatPath(wsId), "utf-8"); const hash = (contents: string) => createHash("sha256").update(contents).digest("hex"); await fs.writeFile( @@ -2182,7 +2182,7 @@ describe("HistoryService", () => { const targetWorkspaceId = "forked-workspace"; await appendNumberedMessages(service, wsId, 3); await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); const chatContents = await fs.readFile(chatPath(wsId), "utf-8"); const hash = (contents: string) => createHash("sha256").update(contents).digest("hex"); await fs.writeFile( @@ -2204,7 +2204,7 @@ describe("HistoryService", () => { it("does not restore a committed archive tombstone before appending", async () => { await appendNumberedMessages(service, wsId, 3); await service.appendToHistory(wsId, boundaryMessage("boundary-1", 1)); - await service.appendToHistory(wsId, createMuxMessage("post-0", "user", "after")); + await service.appendToHistory(wsId, createXumMessage("post-0", "user", "after")); await fs.writeFile( `${archivePath(wsId)}.truncate.json`, JSON.stringify({ finalArchiveHash: null, finalChatHash: null }) @@ -2213,7 +2213,7 @@ describe("HistoryService", () => { await fs.rm(chatPath(wsId)); const restarted = new HistoryService(config); - const message = createMuxMessage("new-msg", "user", "fresh"); + const message = createXumMessage("new-msg", "user", "fresh"); expect((await restarted.appendToHistory(wsId, message)).success).toBe(true); expect(message.metadata?.historySequence).toBe(0); expect((await collectFullHistory(restarted, wsId)).map((item) => item.id)).toEqual([ diff --git a/src/node/services/historyService.ts b/src/node/services/historyService.ts index 8266b07a2d..78b52343fb 100644 --- a/src/node/services/historyService.ts +++ b/src/node/services/historyService.ts @@ -8,8 +8,8 @@ import type { Result } from "@/common/types/result"; import { Ok, Err } from "@/common/types/result"; import { isCompactionSummaryMetadata, - type MuxMessage, - type MuxMetadata, + type XumMessage, + type XumMetadata, } from "@/common/types/message"; import type { Config } from "@/node/config"; import { ensurePrivateDir } from "@/node/utils/fs"; @@ -18,7 +18,7 @@ import { log } from "./log"; import { getTokenizerForModel } from "@/node/utils/main/tokenizer"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; import { safeStringifyForCounting } from "@/common/utils/tokens/safeStringifyForCounting"; -import { normalizeLegacyMuxMetadata } from "@/node/utils/messages/legacy"; +import { normalizeLegacyXumMetadata } from "@/node/utils/messages/legacy"; import { CONTEXT_BOUNDARY_KINDS } from "@/common/constants/contextBoundary"; import { findLatestContextBoundaryIndex, @@ -33,7 +33,7 @@ import { isRefusalFinishReason } from "@/common/utils/messages/refusalFinishReas import { getErrorMessage } from "@/common/utils/errors"; import { isNonNegativeInteger, isPositiveInteger } from "@/common/utils/numbers"; -function hasDurableCompactionBoundary(metadata: MuxMetadata | undefined): boolean { +function hasDurableCompactionBoundary(metadata: XumMetadata | undefined): boolean { if (metadata?.compactionBoundary !== true) { return false; } @@ -46,7 +46,7 @@ function hasDurableCompactionBoundary(metadata: MuxMetadata | undefined): boolea return isPositiveInteger(metadata.compactionEpoch); } -function prefixCutChangesActiveContext(messages: MuxMessage[], removeCount: number): boolean { +function prefixCutChangesActiveContext(messages: XumMessage[], removeCount: number): boolean { const boundaryIndex = findLatestContextBoundaryIndex(messages); const activeStart = boundaryIndex < 0 @@ -59,7 +59,7 @@ function prefixCutChangesActiveContext(messages: MuxMessage[], removeCount: numb ); } -function stripContextUsage(message: MuxMessage): MuxMessage { +function stripContextUsage(message: XumMessage): XumMessage { if (!message.metadata) { return message; } @@ -75,9 +75,9 @@ function stripContextUsage(message: MuxMessage): MuxMessage { function getCompactionMetadataToPreserve( workspaceId: string, - existingMessage: MuxMessage, - incomingMessage: MuxMessage -): Partial | null { + existingMessage: XumMessage, + incomingMessage: XumMessage +): Partial | null { const existingMetadata = existingMessage.metadata; if (existingMetadata?.compactionBoundary !== true) { return null; @@ -111,7 +111,7 @@ function getCompactionMetadataToPreserve( return null; } - const preserved: Partial = { + const preserved: Partial = { compacted: existingMetadata.compacted, compactionBoundary: true, compactionEpoch: existingMetadata.compactionEpoch, @@ -135,7 +135,7 @@ function getCompactionMetadataToPreserve( * through the headless-usage sidecar instead — exactly one of {chat row, * sidecar row} may carry a turn's usage. */ -export function hasCommitWorthyParts(parts: MuxMessage["parts"] | undefined): boolean { +export function hasCommitWorthyParts(parts: XumMessage["parts"] | undefined): boolean { return (parts ?? []).some((part) => { if (part.type === "text" || part.type === "reasoning") { return part.text.trim().length > 0; @@ -506,7 +506,7 @@ export class HistoryService { const line = buffer.subarray(lineStart, lineEnd).toString("utf-8"); if (HistoryService.BOUNDARY_NEEDLES.some((needle) => line.includes(needle))) { try { - const msg = JSON.parse(line) as MuxMessage; + const msg = JSON.parse(line) as XumMessage; if (isDurableContextBoundaryMarker(msg)) { if (skipped < skip) { skipped++; @@ -528,7 +528,7 @@ export class HistoryService { const line = carryoverBytes.toString("utf-8"); if (HistoryService.BOUNDARY_NEEDLES.some((needle) => line.includes(needle))) { try { - const msg = JSON.parse(line) as MuxMessage; + const msg = JSON.parse(line) as XumMessage; if (isDurableContextBoundaryMarker(msg)) { if (skipped < skip) { // Not enough boundaries in the file to satisfy skip @@ -552,7 +552,7 @@ export class HistoryService { * Read and parse messages from a byte offset to the end of a history file. * Self-healing: skips malformed JSON lines the same way readChatHistory does. */ - private async readHistoryFromOffset(filePath: string, byteOffset: number): Promise { + private async readHistoryFromOffset(filePath: string, byteOffset: number): Promise { const stat = await fs.stat(filePath); const tailSize = stat.size - byteOffset; if (tailSize <= 0) return []; @@ -565,10 +565,10 @@ export class HistoryService { .toString("utf-8") .split("\n") .filter((l) => l.trim()); - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; for (const line of lines) { try { - messages.push(normalizeLegacyMuxMetadata(JSON.parse(line) as MuxMessage)); + messages.push(normalizeLegacyXumMetadata(JSON.parse(line) as XumMessage)); } catch { // Skip malformed lines — same self-healing behavior as readChatHistory } @@ -586,7 +586,7 @@ export class HistoryService { * Uses raw byte scanning for \n positions (same approach as findLastBoundaryByteOffset) * so that chunk boundaries splitting multi-byte UTF-8 sequences don't corrupt lines. */ - private async readLastMessagesFromFile(filePath: string, n: number): Promise { + private async readLastMessagesFromFile(filePath: string, n: number): Promise { let fileSize: number; try { const stat = await fs.stat(filePath); @@ -598,7 +598,7 @@ export class HistoryService { const fh = await fs.open(filePath, "r"); try { - const collected: MuxMessage[] = []; + const collected: XumMessage[] = []; let readEnd = fileSize; let carryoverBytes = Buffer.alloc(0); @@ -636,7 +636,7 @@ export class HistoryService { const line = buffer.subarray(lineStart, lineEnd).toString("utf-8").trim(); if (line.length === 0) continue; try { - collected.push(normalizeLegacyMuxMetadata(JSON.parse(line) as MuxMessage)); + collected.push(normalizeLegacyXumMetadata(JSON.parse(line) as XumMessage)); } catch { // Skip malformed lines } @@ -650,7 +650,7 @@ export class HistoryService { const line = carryoverBytes.toString("utf-8").trim(); if (line.length > 0) { try { - collected.push(normalizeLegacyMuxMetadata(JSON.parse(line) as MuxMessage)); + collected.push(normalizeLegacyXumMetadata(JSON.parse(line) as XumMessage)); } catch { // skip } @@ -670,16 +670,16 @@ export class HistoryService { * Returns empty array if the file doesn't exist. * Skips malformed JSON lines to prevent data loss from corruption. */ - private async readMessagesFromFile(filePath: string, logLabel: string): Promise { + private async readMessagesFromFile(filePath: string, logLabel: string): Promise { try { const data = await fs.readFile(filePath, "utf-8"); const lines = data.split("\n").filter((line) => line.trim()); - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; for (let i = 0; i < lines.length; i++) { try { - const message = JSON.parse(lines[i]) as MuxMessage; - messages.push(normalizeLegacyMuxMetadata(message)); + const message = JSON.parse(lines[i]) as XumMessage; + messages.push(normalizeLegacyXumMetadata(message)); } catch (parseError) { // Skip malformed lines but log error for debugging log.warn( @@ -704,7 +704,7 @@ export class HistoryService { * Read raw messages from the active chat.jsonl (does not include partial.json * or the sealed archive). */ - private async readChatHistory(workspaceId: string): Promise { + private async readChatHistory(workspaceId: string): Promise { return this.readMessagesFromFile( this.getChatHistoryPath(workspaceId), `${workspaceId}/${this.CHAT_FILE}` @@ -714,7 +714,7 @@ export class HistoryService { /** * Read raw messages from the sealed chat-archive.jsonl (pre-boundary history). */ - private async readArchivedHistory(workspaceId: string): Promise { + private async readArchivedHistory(workspaceId: string): Promise { return this.readMessagesFromFile( this.getChatArchivePath(workspaceId), `${workspaceId}/${this.CHAT_ARCHIVE_FILE}` @@ -736,7 +736,7 @@ export class HistoryService { */ private async iterateForward( filePath: string, - visitor: (messages: MuxMessage[]) => boolean | void | Promise + visitor: (messages: XumMessage[]) => boolean | void | Promise ): Promise { let fileSize: number; try { @@ -787,12 +787,12 @@ export class HistoryService { const completeText = buffer.subarray(0, lastNewline).toString("utf-8"); carryoverBytes = Buffer.from(buffer.subarray(lastNewline + 1)); - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; for (const line of completeText.split("\n")) { const trimmed = line.trim(); if (trimmed.length === 0) continue; try { - messages.push(normalizeLegacyMuxMetadata(JSON.parse(trimmed) as MuxMessage)); + messages.push(normalizeLegacyXumMetadata(JSON.parse(trimmed) as XumMessage)); } catch { // Skip malformed lines — same self-healing behavior as readChatHistory } @@ -809,7 +809,7 @@ export class HistoryService { const line = carryoverBytes.toString("utf-8").trim(); if (line.length > 0) { try { - const msg = normalizeLegacyMuxMetadata(JSON.parse(line) as MuxMessage); + const msg = normalizeLegacyXumMetadata(JSON.parse(line) as XumMessage); const shouldContinue = await visitor([msg]); if (shouldContinue === false) return false; } catch { @@ -832,7 +832,7 @@ export class HistoryService { */ private async iterateBackward( filePath: string, - visitor: (messages: MuxMessage[]) => boolean | void | Promise + visitor: (messages: XumMessage[]) => boolean | void | Promise ): Promise { let fileSize: number; try { @@ -876,7 +876,7 @@ export class HistoryService { carryoverBytes = Buffer.from(buffer.subarray(0, newlinePositions[0])); // Parse complete lines in reverse (newest → oldest for backward iteration) - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; for (let nl = newlinePositions.length - 1; nl >= 0; nl--) { const lineStart = newlinePositions[nl] + 1; const lineEnd = @@ -886,7 +886,7 @@ export class HistoryService { const line = buffer.subarray(lineStart, lineEnd).toString("utf-8").trim(); if (line.length === 0) continue; try { - messages.push(normalizeLegacyMuxMetadata(JSON.parse(line) as MuxMessage)); + messages.push(normalizeLegacyXumMetadata(JSON.parse(line) as XumMessage)); } catch { // Skip malformed lines } @@ -905,7 +905,7 @@ export class HistoryService { const line = carryoverBytes.toString("utf-8").trim(); if (line.length > 0) { try { - const msg = normalizeLegacyMuxMetadata(JSON.parse(line) as MuxMessage); + const msg = normalizeLegacyXumMetadata(JSON.parse(line) as XumMessage); const shouldContinue = await visitor([msg]); if (shouldContinue === false) return false; } catch { @@ -937,7 +937,7 @@ export class HistoryService { async iterateFullHistory( workspaceId: string, direction: "forward" | "backward", - visitor: (messages: MuxMessage[]) => boolean | void | Promise + visitor: (messages: XumMessage[]) => boolean | void | Promise ): Promise> { return this.withRecoveredHistoryResultLock(workspaceId, "Failed to iterate history", () => this.iterateFullHistoryUnlocked(workspaceId, direction, visitor) @@ -948,7 +948,7 @@ export class HistoryService { async iterateFullHistoryUnderLock( workspaceId: string, direction: "forward" | "backward", - visitor: (messages: MuxMessage[]) => boolean | void | Promise + visitor: (messages: XumMessage[]) => boolean | void | Promise ): Promise> { try { await this.recoverTruncateTransactionUnlocked(workspaceId); @@ -1000,7 +1000,7 @@ export class HistoryService { private async iterateFullHistoryUnlocked( workspaceId: string, direction: "forward" | "backward", - visitor: (messages: MuxMessage[]) => boolean | void | Promise + visitor: (messages: XumMessage[]) => boolean | void | Promise ): Promise> { const chatPath = this.getChatHistoryPath(workspaceId); const archivePath = this.getChatArchivePath(workspaceId); @@ -1024,7 +1024,7 @@ export class HistoryService { } } - private getOldestHistorySequence(messages: readonly MuxMessage[]): number | undefined { + private getOldestHistorySequence(messages: readonly XumMessage[]): number | undefined { let oldest: number | undefined; for (const message of messages) { @@ -1041,7 +1041,7 @@ export class HistoryService { return oldest; } - private getNewestHistorySequence(messages: readonly MuxMessage[]): number | undefined { + private getNewestHistorySequence(messages: readonly XumMessage[]): number | undefined { let newest: number | undefined; for (const message of messages) { @@ -1117,7 +1117,7 @@ export class HistoryService { beforeHistorySequence: number ): Promise { let hasOlder = false; - const visitor = (messages: MuxMessage[]): boolean | void => { + const visitor = (messages: XumMessage[]): boolean | void => { for (const message of messages) { const sequence = message.metadata?.historySequence; if (!isNonNegativeInteger(sequence)) { @@ -1148,7 +1148,7 @@ export class HistoryService { async getHistoryBoundaryWindow( workspaceId: string, beforeHistorySequence: number - ): Promise> { + ): Promise> { assert( typeof workspaceId === "string" && workspaceId.trim().length > 0, "workspaceId is required" @@ -1158,7 +1158,7 @@ export class HistoryService { "getHistoryBoundaryWindow requires beforeHistorySequence to be a non-negative integer" ); - const operation = async (): Promise> => { + const operation = async (): Promise> => { // Scan boundaries newest→oldest and pick the first window that has rows older // than the cursor. Boundaries newer than the rotation point live in chat.jsonl; // older ones live in the sealed archive. @@ -1234,7 +1234,7 @@ export class HistoryService { async getMessagesForCompactionEpoch( workspaceId: string, metadata: CompactionCompletionMetadata - ): Promise> { + ): Promise> { assert( typeof workspaceId === "string" && workspaceId.trim().length > 0, "workspaceId is required" @@ -1249,8 +1249,8 @@ export class HistoryService { ); try { - const messages: MuxMessage[] = []; - let summary: MuxMessage | undefined; + const messages: XumMessage[] = []; + let summary: XumMessage | undefined; const lowerBound = metadata.previousBoundaryHistorySequence; const seenHistorySequences = new Set(); @@ -1308,8 +1308,8 @@ export class HistoryService { * Prefer this over iterateFullHistory() for provider-request assembly and any path * that only needs the active compaction epoch. */ - async getHistoryFromLatestBoundary(workspaceId: string, skip = 0): Promise> { - const operation = async (): Promise> => { + async getHistoryFromLatestBoundary(workspaceId: string, skip = 0): Promise> { + const operation = async (): Promise> => { // One-time lazy migration: seal any pre-boundary prefix left in chat.jsonl // by older builds so this read (and every later one) stays O(active epoch). await this.ensureSealedHistoryRotatedUnlocked(workspaceId); @@ -1431,7 +1431,7 @@ export class HistoryService { continue; } try { - const message = JSON.parse(trimmed) as MuxMessage; + const message = JSON.parse(trimmed) as XumMessage; const sequence = message.metadata?.historySequence; if (isNonNegativeInteger(sequence) && sequence <= archivedMaxSequence) { continue; // Already archived by a rotation that crashed before the chat rewrite. @@ -1468,7 +1468,7 @@ export class HistoryService { * Much cheaper than iterateFullHistory() when only the tail is needed. * Continues into the sealed archive when the active epoch has fewer than N rows. */ - async getLastMessages(workspaceId: string, n: number): Promise> { + async getLastMessages(workspaceId: string, n: number): Promise> { return this.withRecoveredHistoryResultLock( workspaceId, `Failed to read last ${n} messages`, @@ -1520,12 +1520,12 @@ export class HistoryService { /** * Read the partial message for a workspace, if it exists. */ - async readPartial(workspaceId: string): Promise { + async readPartial(workspaceId: string): Promise { try { const partialPath = this.getPartialPath(workspaceId); const data = await fs.readFile(partialPath, "utf-8"); - const message = JSON.parse(data) as MuxMessage; - return normalizeLegacyMuxMetadata(message); + const message = JSON.parse(data) as XumMessage; + return normalizeLegacyXumMetadata(message); } catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { return null; @@ -1539,14 +1539,14 @@ export class HistoryService { /** * Write a partial message to disk. */ - async writePartial(workspaceId: string, message: MuxMessage): Promise> { + async writePartial(workspaceId: string, message: XumMessage): Promise> { return this.fileLocks.withLock(workspaceId, async () => { try { const workspaceDir = this.config.getSessionDir(workspaceId); await ensurePrivateDir(workspaceDir); const partialPath = this.getPartialPath(workspaceId); - const partialMessage: MuxMessage = { + const partialMessage: XumMessage = { ...message, metadata: { ...message.metadata, @@ -1597,7 +1597,7 @@ export class HistoryService { try { const partialPath = this.getPartialPath(workspaceId); const data = await fs.readFile(partialPath, "utf-8"); - const partialMessage = normalizeLegacyMuxMetadata(JSON.parse(data) as MuxMessage); + const partialMessage = normalizeLegacyXumMetadata(JSON.parse(data) as XumMessage); if (partialMessage.id !== messageId) { return Ok(false); } @@ -1747,7 +1747,7 @@ export class HistoryService { */ private async _appendToHistoryUnlocked( workspaceId: string, - message: MuxMessage + message: XumMessage ): Promise> { try { const workspaceDir = this.config.getSessionDir(workspaceId); @@ -1831,7 +1831,7 @@ export class HistoryService { } /** Serialize messages as JSONL rows tagged with workspace context. */ - private serializeHistoryEntries(messages: readonly MuxMessage[], workspaceId: string): string { + private serializeHistoryEntries(messages: readonly XumMessage[], workspaceId: string): string { return messages.map((msg) => JSON.stringify({ ...msg, workspaceId }) + "\n").join(""); } @@ -1842,7 +1842,7 @@ export class HistoryService { */ private async rotateAfterBoundaryWriteUnlocked( workspaceId: string, - message: MuxMessage + message: XumMessage ): Promise { if (!isDurableContextBoundaryMarker(message)) { return; @@ -1858,7 +1858,7 @@ export class HistoryService { } } - async appendToHistory(workspaceId: string, message: MuxMessage): Promise> { + async appendToHistory(workspaceId: string, message: XumMessage): Promise> { return this.withRecoveredHistoryResultLock( workspaceId, "Failed to append history", @@ -1882,7 +1882,7 @@ export class HistoryService { * always in the active epoch (stream placeholders, compaction summaries), * never in the sealed archive. */ - async updateHistory(workspaceId: string, message: MuxMessage): Promise> { + async updateHistory(workspaceId: string, message: XumMessage): Promise> { return this.withRecoveredHistoryResultLock( workspaceId, "Failed to update history", @@ -1905,7 +1905,7 @@ export class HistoryService { // Find and replace the message with matching historySequence let found = false; - let persistedMessage: MuxMessage | undefined; + let persistedMessage: XumMessage | undefined; for (let i = 0; i < messages.length; i++) { if (messages[i].metadata?.historySequence === targetSequence) { const existingMessage = messages[i]; @@ -2309,7 +2309,7 @@ export class HistoryService { // Count tokens for each message // We stringify the entire message for simplicity - only relative weights matter - const messageTokens: Array<{ message: MuxMessage; tokens: number }> = await Promise.all( + const messageTokens: Array<{ message: XumMessage; tokens: number }> = await Promise.all( messages.map(async (msg) => { const tokens = await tokenizer.countTokens(safeStringifyForCounting(msg)); return { message: msg, tokens }; @@ -2348,7 +2348,7 @@ export class HistoryService { const activeContextChanged = prefixCutChangesActiveContext(messages, removeCount); const sanitize = activeContextChanged ? stripContextUsage - : (message: MuxMessage) => message; + : (message: XumMessage) => message; const remainingMessages = messages.slice(removeCount).map(sanitize); const deletedMessages = messages.slice(0, removeCount); const deletedSequences = deletedMessages diff --git a/src/node/services/idleCompactionService.test.ts b/src/node/services/idleCompactionService.test.ts index 6c8265eef1..a864c46a52 100644 --- a/src/node/services/idleCompactionService.test.ts +++ b/src/node/services/idleCompactionService.test.ts @@ -4,7 +4,7 @@ import type { Config } from "@/node/config"; import type { HistoryService } from "./historyService"; import type { ExtensionMetadataService } from "./ExtensionMetadataService"; import type { ProjectConfig, ProjectsConfig } from "@/common/types/project"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { Ok } from "@/common/types/result"; import { createTestHistoryService } from "./testHistoryService"; @@ -62,11 +62,11 @@ describe("IdleCompactionService", () => { const idleTimestamp = now - 25 * oneHourMs; await historyService.appendToHistory( testWorkspaceId, - createMuxMessage("1", "user", "Hello", { timestamp: idleTimestamp }) + createXumMessage("1", "user", "Hello", { timestamp: idleTimestamp }) ); await historyService.appendToHistory( testWorkspaceId, - createMuxMessage("2", "assistant", "Hi there!", { timestamp: idleTimestamp }) + createXumMessage("2", "assistant", "Hi there!", { timestamp: idleTimestamp }) ); // Create mock extension metadata service @@ -133,7 +133,7 @@ describe("IdleCompactionService", () => { const idleTimestamp = now - 25 * oneHourMs; spyOn(historyService, "getLastMessages").mockResolvedValueOnce( Ok([ - createMuxMessage("1", "assistant", "Summary", { + createXumMessage("1", "assistant", "Summary", { compacted: true, timestamp: idleTimestamp, }), @@ -150,8 +150,8 @@ describe("IdleCompactionService", () => { const recentTimestamp = now - oneHourMs; spyOn(historyService, "getLastMessages").mockResolvedValueOnce( Ok([ - createMuxMessage("1", "user", "Hello", { timestamp: recentTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: recentTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: recentTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: recentTimestamp }), ]) ); @@ -164,9 +164,9 @@ describe("IdleCompactionService", () => { const idleTimestamp = now - 25 * oneHourMs; spyOn(historyService, "getLastMessages").mockResolvedValueOnce( Ok([ - createMuxMessage("1", "user", "Hello", { timestamp: idleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), - createMuxMessage("3", "user", "Another question?", { timestamp: idleTimestamp }), // Last message is user + createXumMessage("1", "user", "Hello", { timestamp: idleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), + createXumMessage("3", "user", "Another question?", { timestamp: idleTimestamp }), // Last message is user ]) ); @@ -178,7 +178,7 @@ describe("IdleCompactionService", () => { test("returns ineligible when messages have no timestamps", async () => { // Messages without timestamps - can't determine recency spyOn(historyService, "getLastMessages").mockResolvedValueOnce( - Ok([createMuxMessage("1", "user", "Hello"), createMuxMessage("2", "assistant", "Hi!")]) + Ok([createXumMessage("1", "user", "Hello"), createXumMessage("2", "assistant", "Hi!")]) ); const result = await service.checkEligibility(testWorkspaceId, threshold24h, now); @@ -245,8 +245,8 @@ describe("IdleCompactionService", () => { } return Promise.resolve( Ok([ - createMuxMessage("1", "user", "Hello", { timestamp: idleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: idleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), ]) ); }); @@ -281,8 +281,8 @@ describe("IdleCompactionService", () => { spyOn(historyService, "getLastMessages").mockResolvedValue( Ok([ - createMuxMessage("1", "user", "Hello", { timestamp: idleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: idleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), ]) ); @@ -358,8 +358,8 @@ describe("IdleCompactionService", () => { // same data for both checks. spyOn(historyService, "getLastMessages").mockResolvedValue( Ok([ - createMuxMessage("1", "user", "Hello", { timestamp: idleTimestamp }), - createMuxMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), + createXumMessage("1", "user", "Hello", { timestamp: idleTimestamp }), + createXumMessage("2", "assistant", "Hi!", { timestamp: idleTimestamp }), ]) ); diff --git a/src/node/services/mdnsAdvertiserService.test.ts b/src/node/services/mdnsAdvertiserService.test.ts index d2cf483232..7ca41e720e 100644 --- a/src/node/services/mdnsAdvertiserService.test.ts +++ b/src/node/services/mdnsAdvertiserService.test.ts @@ -1,9 +1,9 @@ import { describe, expect, test } from "bun:test"; import { Protocol } from "@homebridge/ciao"; import type { NetworkInterfaceInfo } from "node:os"; -import { buildMuxMdnsServiceOptions, MUX_MDNS_SERVICE_TYPE } from "./mdnsAdvertiserService"; +import { buildXumMdnsServiceOptions, XUM_MDNS_SERVICE_TYPE } from "./mdnsAdvertiserService"; -describe("buildMuxMdnsServiceOptions", () => { +describe("buildXumMdnsServiceOptions", () => { test("0.0.0.0 disables IPv6 and avoids advertising loopback addresses", () => { const networkInterfaces: NodeJS.Dict = { lo0: [ @@ -28,7 +28,7 @@ describe("buildMuxMdnsServiceOptions", () => { ], }; - const serviceOptions = buildMuxMdnsServiceOptions({ + const serviceOptions = buildXumMdnsServiceOptions({ bindHost: "0.0.0.0", port: 3000, instanceName: "mux-test", @@ -37,7 +37,7 @@ describe("buildMuxMdnsServiceOptions", () => { networkInterfaces, }); - expect(serviceOptions.type).toBe(MUX_MDNS_SERVICE_TYPE); + expect(serviceOptions.type).toBe(XUM_MDNS_SERVICE_TYPE); expect(serviceOptions.protocol).toBe(Protocol.TCP); expect(serviceOptions.disabledIpv6).toBe(true); expect(serviceOptions.restrictedAddresses).toEqual(["en0"]); @@ -80,7 +80,7 @@ describe("buildMuxMdnsServiceOptions", () => { ], }; - const serviceOptions = buildMuxMdnsServiceOptions({ + const serviceOptions = buildXumMdnsServiceOptions({ bindHost: "::", port: 3000, instanceName: "mux-test", @@ -94,7 +94,7 @@ describe("buildMuxMdnsServiceOptions", () => { }); test("sanitizes dots in instanceName so DNS-SD clients can browse/resolve", () => { - const serviceOptions = buildMuxMdnsServiceOptions({ + const serviceOptions = buildXumMdnsServiceOptions({ bindHost: "192.168.1.10", port: 3000, instanceName: "mux-host.home", @@ -106,7 +106,7 @@ describe("buildMuxMdnsServiceOptions", () => { }); test("specific IP restricts addresses", () => { - const serviceOptions = buildMuxMdnsServiceOptions({ + const serviceOptions = buildXumMdnsServiceOptions({ bindHost: "192.168.1.10", port: 3000, instanceName: "mux-test", diff --git a/src/node/services/mdnsAdvertiserService.ts b/src/node/services/mdnsAdvertiserService.ts index 3dd4b188d5..cd5744568d 100644 --- a/src/node/services/mdnsAdvertiserService.ts +++ b/src/node/services/mdnsAdvertiserService.ts @@ -11,9 +11,9 @@ import * as net from "node:net"; import * as os from "node:os"; import { log } from "./log"; -// NOTE: Avoid "mux" here: it's an IANA-registered service name ("Multiplexing Protocol"), -// and some discovery tools will display/handle it specially. -export const MUX_MDNS_SERVICE_TYPE = "mux-api"; +// Existing clients browse `_mux-api._tcp`, so the discovery value remains stable after the rename. +// Avoid bare "mux": it is an IANA-registered service name that discovery tools may handle specially. +export const XUM_MDNS_SERVICE_TYPE = "mux-api"; type NetworkInterfaces = NodeJS.Dict; @@ -56,7 +56,7 @@ function getNonInternalInterfaceNames( } type ServiceTxtRecord = Record; -export interface BuildMuxMdnsServiceOptions { +export interface BuildXumMdnsServiceOptions { bindHost: string; port: number; instanceName: string; @@ -65,7 +65,7 @@ export interface BuildMuxMdnsServiceOptions { networkInterfaces?: NetworkInterfaces; } -export function buildMuxMdnsServiceOptions(options: BuildMuxMdnsServiceOptions): ServiceOptions { +export function buildXumMdnsServiceOptions(options: BuildXumMdnsServiceOptions): ServiceOptions { const bindHost = options.bindHost.trim(); assert(bindHost, "bindHost is required"); @@ -105,7 +105,7 @@ export function buildMuxMdnsServiceOptions(options: BuildMuxMdnsServiceOptions): const serviceOptions: ServiceOptions = { name: instanceName, - type: MUX_MDNS_SERVICE_TYPE, + type: XUM_MDNS_SERVICE_TYPE, protocol: Protocol.TCP, port: options.port, txt, diff --git a/src/node/services/memoryConsolidationService.test.ts b/src/node/services/memoryConsolidationService.test.ts index a351ad7cbe..e5d4b0c4cc 100644 --- a/src/node/services/memoryConsolidationService.test.ts +++ b/src/node/services/memoryConsolidationService.test.ts @@ -6,7 +6,7 @@ import { MockLanguageModelV3, simulateReadableStream } from "ai/test"; import type { LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider"; import type { CompactionCompletionMetadata } from "@/common/types/compaction"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { MemoryConsolidationStatusChangeEventPayload } from "@/common/orpc/schemas/memory"; import { MULTI_PROJECT_CONFIG_KEY } from "@/common/constants/multiProject"; import { EXPERIMENT_IDS } from "@/common/constants/experiments"; @@ -314,15 +314,15 @@ async function seedCompactionEpoch( ): Promise { await fixture.historyService.appendToHistory( workspaceId, - createMuxMessage("pref-1", "user", "Please remember that I prefer concise tests.") + createXumMessage("pref-1", "user", "Please remember that I prefer concise tests.") ); await fixture.historyService.appendToHistory( workspaceId, - createMuxMessage("compact-request", "user", "Please compact", { + createXumMessage("compact-request", "user", "Please compact", { muxMetadata: { type: "compaction-request", rawCommand: "/compact", parsed: {} }, }) ); - const summary = createMuxMessage("summary-1", "assistant", "The user prefers concise tests.", { + const summary = createXumMessage("summary-1", "assistant", "The user prefers concise tests.", { compactionBoundary: true, compacted: "user", compactionEpoch: 1, diff --git a/src/node/services/memoryHarvest.test.ts b/src/node/services/memoryHarvest.test.ts index 68bda5606f..ce29341ddf 100644 --- a/src/node/services/memoryHarvest.test.ts +++ b/src/node/services/memoryHarvest.test.ts @@ -4,7 +4,7 @@ import { MockLanguageModelV3, simulateReadableStream } from "ai/test"; import type { LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider"; import type { CompactionCompletionMetadata } from "@/common/types/compaction"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { Config } from "@/node/config"; import { MemoryMetaService } from "./memoryMeta"; import { MemoryService, type MemoryScopeContext } from "./memoryService"; @@ -97,8 +97,8 @@ interface Fixture extends Disposable { memoryService: MemoryService; ctx: MemoryScopeContext; metadata: CompactionCompletionMetadata; - messages: MuxMessage[]; - summary: MuxMessage; + messages: XumMessage[]; + summary: XumMessage; } function createFixture(): Fixture { @@ -112,7 +112,7 @@ function createFixture(): Fixture { workspaceId: "ws-harvest", projectPath: "/projects/demo", }; - const summary = createMuxMessage("summary-1", "assistant", "The user prefers concise tests.", { + const summary = createXumMessage("summary-1", "assistant", "The user prefers concise tests.", { historySequence: 2, compactionBoundary: true, compacted: "user", @@ -130,7 +130,7 @@ function createFixture(): Fixture { compactionRequestMessageId: "compact-request", }, messages: [ - createMuxMessage("m1", "user", "Please remember that I prefer concise tests.", { + createXumMessage("m1", "user", "Please remember that I prefer concise tests.", { historySequence: 0, }), ], @@ -301,7 +301,7 @@ describe("runMemoryHarvest", () => { it("serializes transcript evidence as JSON instead of breakable pseudo-XML", async () => { using fixture = createFixture(); fixture.messages = [ - createMuxMessage("m1", "user", 'remember this', { + createXumMessage("m1", "user", 'remember this', { historySequence: 0, }), ]; @@ -325,7 +325,7 @@ describe("runMemoryHarvest", () => { it("chunks oversized epochs before calling the model", async () => { using fixture = createFixture(); fixture.messages = Array.from({ length: 12 }, (_, index) => - createMuxMessage(`m${index}`, "user", `preference ${index} ${"x".repeat(7_000)}`, { + createXumMessage(`m${index}`, "user", `preference ${index} ${"x".repeat(7_000)}`, { historySequence: index, }) ); diff --git a/src/node/services/memoryHarvest.ts b/src/node/services/memoryHarvest.ts index 1df2f4f24b..45b540082b 100644 --- a/src/node/services/memoryHarvest.ts +++ b/src/node/services/memoryHarvest.ts @@ -3,7 +3,7 @@ import type { LanguageModelV2Usage } from "@ai-sdk/provider"; import { z } from "zod"; import type { CompactionCompletionMetadata } from "@/common/types/compaction"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { getErrorMessage } from "@/common/utils/errors"; import { accumulateStepsProviderMetadata } from "@/common/utils/tokens/usageHelpers"; import assert from "@/common/utils/assert"; @@ -33,7 +33,7 @@ interface HarvestChunk { interface HarvestEvidenceMessage { id: string; sequence: number | null; - role: MuxMessage["role"]; + role: XumMessage["role"]; text: string; truncated: boolean; } @@ -46,7 +46,7 @@ export interface MemoryHarvestResult { streamError?: string; } -function partToText(part: MuxMessage["parts"][number]): string { +function partToText(part: XumMessage["parts"][number]): string { if (part.type === "text") return part.text; if (part.type === "dynamic-tool") return `[tool:${part.toolName}]`; return `[${part.type}]`; @@ -64,7 +64,7 @@ function truncateForHarvest(text: string): { text: string; truncated: boolean } }; } -function formatMessageForHarvest(message: MuxMessage): HarvestEvidenceMessage { +function formatMessageForHarvest(message: XumMessage): HarvestEvidenceMessage { const sequence = message.metadata?.historySequence; const joinedText = neutralizeHarvestText(message.parts.map(partToText).join("\n").trim()); const truncated = truncateForHarvest(joinedText); @@ -77,7 +77,7 @@ function formatMessageForHarvest(message: MuxMessage): HarvestEvidenceMessage { }; } -function buildHarvestChunks(messages: MuxMessage[]): HarvestChunk[] { +function buildHarvestChunks(messages: XumMessage[]): HarvestChunk[] { const chunks: HarvestChunk[] = []; let currentMessages: HarvestEvidenceMessage[] = []; let currentIds = new Set(); @@ -120,7 +120,7 @@ function normalizeCandidateKey(candidate: MemoryCandidate): string { function renderInbox(args: { metadata: CompactionCompletionMetadata; - summary: MuxMessage; + summary: XumMessage; candidates: MemoryCandidate[]; }): string { const lines = [ @@ -195,8 +195,8 @@ export async function runMemoryHarvest(args: { memoryService: MemoryService; ctx: MemoryScopeContext; completionMetadata: CompactionCompletionMetadata; - messages: MuxMessage[]; - summary: MuxMessage; + messages: XumMessage[]; + summary: XumMessage; abortSignal?: AbortSignal; /** * Best-effort cost telemetry: headless harvest bypasses the chat cost diff --git a/src/node/services/messagePipeline.test.ts b/src/node/services/messagePipeline.test.ts index 58b563c10f..17e8a8aef6 100644 --- a/src/node/services/messagePipeline.test.ts +++ b/src/node/services/messagePipeline.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import type { AssistantModelMessage, ModelMessage } from "ai"; import { transformModelMessages } from "@/browser/utils/messages/modelMessageTransform"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import type { ThinkingLevel } from "@/common/types/thinking"; import { createTestHistoryService } from "./testHistoryService"; import { prepareMessagesForProvider, sanitizeAssistantModelMessages } from "./messagePipeline"; @@ -72,21 +72,21 @@ describe("prepareMessagesForProvider log purity", () => { // derive the request from them alone (no live disk reads or tracker state), // so building twice from the same log yields byte-identical messages. const messages = [ - createMuxMessage( + createXumMessage( "file-snapshot-1", "user", '\n```ts\nline1\nline2\n```\n', { timestamp: 1000, synthetic: true, fileAtMentionSnapshot: ["src/foo.ts"] } ), - createMuxMessage("user-1", "user", "Please check @src/foo.ts", { timestamp: 1001 }), - createMuxMessage("assistant-1", "assistant", "Looks fine.", { timestamp: 1002 }), - createMuxMessage( + createXumMessage("user-1", "user", "Please check @src/foo.ts", { timestamp: 1001 }), + createXumMessage("assistant-1", "assistant", "Looks fine.", { timestamp: 1002 }), + createXumMessage( "file-change-1", "user", "\nNote: src/foo.ts was modified.\n", { timestamp: 1003, synthetic: true } ), - createMuxMessage("user-2", "user", "Continue", { timestamp: 1004 }), + createXumMessage("user-2", "user", "Continue", { timestamp: 1004 }), ]; const first = await prepareMessagesForProvider({ @@ -111,9 +111,9 @@ describe("prepareMessagesForProvider log purity", () => { // snapshot rows. There is no request-time fallback that reads live disk, so // the mention stays plain text — and the request still builds without error. const messages = [ - createMuxMessage("user-1", "user", "Please check @src/foo.ts", { timestamp: 1000 }), - createMuxMessage("assistant-1", "assistant", "Sure.", { timestamp: 1001 }), - createMuxMessage("user-2", "user", "Continue", { timestamp: 1002 }), + createXumMessage("user-1", "user", "Please check @src/foo.ts", { timestamp: 1000 }), + createXumMessage("assistant-1", "assistant", "Sure.", { timestamp: 1001 }), + createXumMessage("user-2", "user", "Continue", { timestamp: 1002 }), ]; const result = await prepareMessagesForProvider({ @@ -128,23 +128,23 @@ describe("prepareMessagesForProvider log purity", () => { }); describe("reasoning replay in built provider requests", () => { - function historyWith(assistantParts: MuxMessage["parts"]): MuxMessage[] { + function historyWith(assistantParts: XumMessage["parts"]): XumMessage[] { return [ - createMuxMessage("user-1", "user", "solve it", { timestamp: 1000 }), + createXumMessage("user-1", "user", "solve it", { timestamp: 1000 }), { id: "assistant-1", role: "assistant", metadata: { timestamp: 1001 }, parts: assistantParts, }, - createMuxMessage("user-2", "user", "continue", { timestamp: 1002 }), + createXumMessage("user-2", "user", "continue", { timestamp: 1002 }), ]; } function buildRequest( provider: string, thinkingLevel: ThinkingLevel, - messages: MuxMessage[] + messages: XumMessage[] ): Promise { return prepareMessagesForProvider({ messagesWithSentinel: messages, @@ -383,7 +383,7 @@ describe("reasoning replay in built provider requests", () => { text: "corrupt metadata", signature: 999, providerOptions: { openai: { itemId: 42 } }, - } as unknown as MuxMessage["parts"][number], + } as unknown as XumMessage["parts"][number], { type: "text", text: "answer" }, ]) ); @@ -427,7 +427,7 @@ describe("reasoning replay in built provider requests", () => { try { const workspaceId = "reasoning-replay-ws"; const turns = [ - createMuxMessage("user-1", "user", "solve it", { timestamp: 1000 }), + createXumMessage("user-1", "user", "solve it", { timestamp: 1000 }), { id: "assistant-1", role: "assistant", @@ -441,8 +441,8 @@ describe("reasoning replay in built provider requests", () => { }, { type: "text", text: "the answer" }, ], - } satisfies MuxMessage, - createMuxMessage("user-2", "user", "next question", { timestamp: 1002 }), + } satisfies XumMessage, + createXumMessage("user-2", "user", "next question", { timestamp: 1002 }), ]; for (const message of turns) { const appendResult = await historyService.appendToHistory(workspaceId, message); diff --git a/src/node/services/messagePipeline.ts b/src/node/services/messagePipeline.ts index 14cf2ca359..acbb0e99d5 100644 --- a/src/node/services/messagePipeline.ts +++ b/src/node/services/messagePipeline.ts @@ -16,7 +16,7 @@ import { extractToolMediaAsUserMessages } from "@/node/utils/messages/extractToo import { sanitizeAnthropicPdfFilenames } from "@/node/utils/messages/sanitizeAnthropicDocumentFilename"; import { convertDataUriFilePartsForSdk } from "@/node/utils/messages/convertDataUriFilePartsForSdk"; import { attachReasoningReplayMetadata } from "@/node/utils/messages/reasoningProviderOptions"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { PostCompactionAttachment } from "@/common/types/attachment"; import type { ProvidersConfigMap } from "@/common/orpc/types"; import type { ThinkingLevel } from "@/common/types/thinking"; @@ -33,7 +33,7 @@ import { log } from "./log"; /** Options for the full message preparation pipeline. */ export interface PrepareMessagesOptions { /** Pre-filtered messages (with interrupted-sentinel already added). */ - messagesWithSentinel: MuxMessage[]; + messagesWithSentinel: XumMessage[]; /** Active agent ID for transition injection. */ effectiveAgentId: string; /** Tool names for mode-transition sentinel detection. */ diff --git a/src/node/services/messageQueue.test.ts b/src/node/services/messageQueue.test.ts index 0373f37ca5..fe58996fde 100644 --- a/src/node/services/messageQueue.test.ts +++ b/src/node/services/messageQueue.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from "bun:test"; import { MessageQueue } from "./messageQueue"; -import type { MuxMessageMetadata } from "@/common/types/message"; +import type { XumMessageMetadata } from "@/common/types/message"; import type { SendMessageOptions } from "@/common/orpc/types"; describe("MessageQueue", () => { @@ -44,7 +44,7 @@ describe("MessageQueue", () => { }); it("should return rawCommand for compaction request", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact -t 3000", parsed: { maxOutputTokens: 3000 }, @@ -64,7 +64,7 @@ describe("MessageQueue", () => { it("should queue compaction after normal message as its own entry", () => { queue.add("First message"); - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", parsed: {}, @@ -87,12 +87,12 @@ describe("MessageQueue", () => { const second = queue.dequeueNext(); expect(second.message).toBe("Summarize this conversation..."); - expect((second.options?.muxMetadata as MuxMessageMetadata).type).toBe("compaction-request"); + expect((second.options?.muxMetadata as XumMessageMetadata).type).toBe("compaction-request"); expect(queue.isEmpty()).toBe(true); }); it("should return joined messages when metadata type is not compaction-request", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "normal", }; @@ -112,7 +112,7 @@ describe("MessageQueue", () => { }); it("should return joined messages after clearing compaction metadata", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", parsed: {}, @@ -134,7 +134,7 @@ describe("MessageQueue", () => { describe("getMessages", () => { it("should return raw messages even for compaction requests", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", parsed: {}, @@ -166,7 +166,7 @@ describe("MessageQueue", () => { }); it("should return true when compaction request is queued", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", parsed: {}, @@ -182,7 +182,7 @@ describe("MessageQueue", () => { }); it("should return false after clearing", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", parsed: {}, @@ -243,7 +243,7 @@ describe("MessageQueue", () => { }); expect(queue.getQueueDispatchMode()).toBe("turn-end"); - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "agent-skill", rawCommand: "/init", skillName: "init", @@ -393,7 +393,7 @@ describe("MessageQueue", () => { }); describe("workspace turn metadata", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "workspace-turn-task", taskHandleId: "wst_followup", ownerWorkspaceId: "parent-workspace", @@ -416,7 +416,7 @@ describe("MessageQueue", () => { // FIFO: the workspace turn dispatches first with its metadata + callbacks... const first = queue.dequeueNext(); expect(first.message).toBe("Follow up"); - expect((first.options?.muxMetadata as MuxMessageMetadata).type).toBe("workspace-turn-task"); + expect((first.options?.muxMetadata as XumMessageMetadata).type).toBe("workspace-turn-task"); expect(first.internal?.onAccepted).toBe(onAccepted); // ...and the user message dispatches after it, without adopting either. @@ -440,7 +440,7 @@ describe("MessageQueue", () => { expect(queue.hasWorkspaceTurn("wst_followup")).toBe(true); const second = queue.dequeueNext(); - expect((second.options?.muxMetadata as MuxMessageMetadata).type).toBe("workspace-turn-task"); + expect((second.options?.muxMetadata as XumMessageMetadata).type).toBe("workspace-turn-task"); expect(queue.hasWorkspaceTurn("wst_followup")).toBe(false); }); @@ -774,7 +774,7 @@ describe("MessageQueue", () => { }); it("should preserve compaction metadata when follow-up is added", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", parsed: {}, @@ -796,7 +796,7 @@ describe("MessageQueue", () => { // dequeueNext preserves compaction metadata from the entry's first message const { message, options } = queue.dequeueNext(); expect(message).toBe("Summarize...\nAnd then do this follow-up task"); - const muxMeta = options?.muxMetadata as MuxMessageMetadata; + const muxMeta = options?.muxMetadata as XumMessageMetadata; expect(muxMeta.type).toBe("compaction-request"); if (muxMeta.type === "compaction-request") { expect(muxMeta.rawCommand).toBe("/compact"); @@ -806,7 +806,7 @@ describe("MessageQueue", () => { it("should queue an agent-skill invocation after a normal message as its own entry", () => { queue.add("First message"); - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "agent-skill", rawCommand: "/init", skillName: "init", @@ -829,13 +829,13 @@ describe("MessageQueue", () => { expect(first.options?.muxMetadata).toBeUndefined(); const second = queue.dequeueNext(); - expect((second.options?.muxMetadata as MuxMessageMetadata).type).toBe("agent-skill"); + expect((second.options?.muxMetadata as XumMessageMetadata).type).toBe("agent-skill"); }); it("should queue an MCP prompt invocation after a normal message as its own entry", () => { queue.add("First message"); - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "normal", rawCommand: "/mcp__coder__review src", mcpPromptRefs: [ @@ -864,13 +864,13 @@ describe("MessageQueue", () => { expect(first.options?.muxMetadata).toBeUndefined(); const second = queue.dequeueNext(); - expect((second.options?.muxMetadata as MuxMessageMetadata).mcpPromptRefs).toHaveLength(1); + expect((second.options?.muxMetadata as XumMessageMetadata).mcpPromptRefs).toHaveLength(1); }); it("should queue an inline skill reference after a normal message as its own entry", () => { queue.add("First message"); - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "normal", agentSkillRefs: [{ skillName: "tdd", scope: "global", source: "inline" }], }; @@ -887,11 +887,11 @@ describe("MessageQueue", () => { expect(first.options?.muxMetadata).toBeUndefined(); const second = queue.dequeueNext(); - expect((second.options?.muxMetadata as MuxMessageMetadata).agentSkillRefs).toHaveLength(1); + expect((second.options?.muxMetadata as XumMessageMetadata).agentSkillRefs).toHaveLength(1); }); it("should queue a normal message behind an agent-skill invocation without leaking metadata", () => { - const metadata: MuxMessageMetadata = { + const metadata: XumMessageMetadata = { type: "agent-skill", rawCommand: "/init", skillName: "init", @@ -913,7 +913,7 @@ describe("MessageQueue", () => { const first = queue.dequeueNext(); expect(first.message).toBe("Use skill init"); - expect((first.options?.muxMetadata as MuxMessageMetadata).type).toBe("agent-skill"); + expect((first.options?.muxMetadata as XumMessageMetadata).type).toBe("agent-skill"); const second = queue.dequeueNext(); expect(second.message).toBe("Follow-up message"); diff --git a/src/node/services/mock/mockAiRouter.ts b/src/node/services/mock/mockAiRouter.ts index 02aa1f8ddf..a045eacf28 100644 --- a/src/node/services/mock/mockAiRouter.ts +++ b/src/node/services/mock/mockAiRouter.ts @@ -1,11 +1,11 @@ import { getCompactionFollowUpContent } from "@/common/types/message"; -import type { CompactionFollowUpRequest, MuxMessage } from "@/common/types/message"; +import type { CompactionFollowUpRequest, XumMessage } from "@/common/types/message"; import type { StreamErrorType } from "@/common/types/errors"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; export interface MockAiRouterRequest { - messages: MuxMessage[]; - latestUserMessage: MuxMessage; + messages: XumMessage[]; + latestUserMessage: XumMessage; latestUserText: string; } @@ -81,7 +81,7 @@ function hasMockMarker(text: string, marker: string): boolean { return normalized.includes(`${MOCK_MARKER_PREFIX}${marker.toLowerCase()}`); } -function hasCompactionHistory(messages: MuxMessage[]): boolean { +function hasCompactionHistory(messages: XumMessage[]): boolean { return messages.some((message) => { if (readCompactionRequest(message)) { return true; @@ -90,7 +90,7 @@ function hasCompactionHistory(messages: MuxMessage[]): boolean { }); } function readCompactionRequest( - message: MuxMessage + message: XumMessage ): { followUpContent?: CompactionFollowUpRequest } | undefined { const muxMeta = message.metadata?.muxMetadata; if (!muxMeta || muxMeta.type !== "compaction-request") { @@ -108,7 +108,7 @@ function buildUsage(inputTokens: number, outputTokens: number): LanguageModelV2U } function buildMockCompactionSummary(options: { - preCompactionMessages: MuxMessage[]; + preCompactionMessages: XumMessage[]; followUpContent?: CompactionFollowUpRequest; }): string { const userCount = options.preCompactionMessages.filter((m) => m.role === "user").length; diff --git a/src/node/services/mock/mockAiStreamPlayer.test.ts b/src/node/services/mock/mockAiStreamPlayer.test.ts index c6c3c6a1fd..fda9e26787 100644 --- a/src/node/services/mock/mockAiStreamPlayer.test.ts +++ b/src/node/services/mock/mockAiStreamPlayer.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"; import { EventEmitter } from "events"; import { MockAiStreamPlayer } from "./mockAiStreamPlayer"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { Ok } from "@/common/types/result"; import type { HistoryService } from "@/node/services/historyService"; import type { AIService } from "@/node/services/aiService"; @@ -16,14 +16,14 @@ function readWorkspaceId(payload: unknown): string | undefined { return typeof workspaceId === "string" ? workspaceId : undefined; } -function extractText(message: MuxMessage | null | undefined): string { +function extractText(message: XumMessage | null | undefined): string { if (!message) { return ""; } return message.parts .filter( - (part): part is Extract => part.type === "text" + (part): part is Extract => part.type === "text" ) .map((part) => part.text) .join(""); @@ -67,7 +67,7 @@ describe("MockAiStreamPlayer", () => { const workspaceId = "workspace-1"; - const firstTurnUser = createMuxMessage( + const firstTurnUser = createXumMessage( "user-1", "user", "[mock:list-languages] List 3 programming languages", @@ -84,7 +84,7 @@ describe("MockAiStreamPlayer", () => { const historyResult = await historyService.getLastMessages(workspaceId, 100); const historyBeforeSecondTurn = historyResult.success ? historyResult.data : []; - const secondTurnUser = createMuxMessage( + const secondTurnUser = createXumMessage( "user-2", "user", "[mock:error:api] Trigger API error", @@ -127,14 +127,14 @@ describe("MockAiStreamPlayer", () => { appendResolve = resolve; }); - let appendedMessageResolve!: (msg: MuxMessage) => void; - const appendedMessage = new Promise((resolve) => { + let appendedMessageResolve!: (msg: XumMessage) => void; + const appendedMessage = new Promise((resolve) => { appendedMessageResolve = resolve; }); const originalAppend = historyService.appendToHistory.bind(historyService); spyOn(historyService, "appendToHistory").mockImplementation( - async (wId: string, message: MuxMessage) => { + async (wId: string, message: XumMessage) => { // Write to disk so deleteMessage can find it later await originalAppend(wId, message); appendedMessageResolve(message); @@ -152,7 +152,7 @@ describe("MockAiStreamPlayer", () => { const workspaceId = "workspace-abort-startup"; - const userMessage = createMuxMessage( + const userMessage = createXumMessage( "user-1", "user", "[mock:list-languages] List 3 programming languages", @@ -214,7 +214,7 @@ describe("MockAiStreamPlayer", () => { } }); - const firstUserMessage = createMuxMessage( + const firstUserMessage = createXumMessage( "user-abort-replacement-first", "user", "[force] first stream before aborted replacement", @@ -229,7 +229,7 @@ describe("MockAiStreamPlayer", () => { expect(streamStartMessageIds).toHaveLength(1); const abortController = new AbortController(); - const replacementUserMessage = createMuxMessage( + const replacementUserMessage = createXumMessage( "user-abort-replacement-second", "user", "[force] replacement stream should abort before scheduling", @@ -286,7 +286,7 @@ describe("MockAiStreamPlayer", () => { }); }); - const userMessage = createMuxMessage("user-partial", "user", "[force] keep streaming", { + const userMessage = createXumMessage("user-partial", "user", "[force] keep streaming", { timestamp: Date.now(), }); @@ -337,7 +337,7 @@ describe("MockAiStreamPlayer", () => { ); const workspaceId = "workspace-stale-partial-after-stop"; - const userMessage = createMuxMessage("user-stale-partial", "user", "[force] keep streaming", { + const userMessage = createXumMessage("user-stale-partial", "user", "[force] keep streaming", { timestamp: Date.now(), }); @@ -418,7 +418,7 @@ describe("MockAiStreamPlayer", () => { } }); - const firstUserMessage = createMuxMessage( + const firstUserMessage = createXumMessage( "user-stale-delayed-write-first", "user", "[force] first stream before stale delayed-write cleanup", @@ -433,7 +433,7 @@ describe("MockAiStreamPlayer", () => { await waitForCondition(() => writePartialCallCount >= 1, 1000); - const replacementUserMessage = createMuxMessage( + const replacementUserMessage = createXumMessage( "user-stale-delayed-write-second", "user", "[force] replacement stream should keep its partial after stale cleanup", @@ -483,7 +483,7 @@ describe("MockAiStreamPlayer", () => { }); const workspaceId = "workspace-partial-replacement"; - const firstUserMessage = createMuxMessage( + const firstUserMessage = createXumMessage( "user-partial-first", "user", "[force] first-partial-marker keep streaming", @@ -503,7 +503,7 @@ describe("MockAiStreamPlayer", () => { const firstPartial = await historyService.readPartial(workspaceId); expect(firstPartial).not.toBeNull(); - const secondUserMessage = createMuxMessage( + const secondUserMessage = createXumMessage( "user-partial-second", "user", "[force] second-partial-marker keep streaming", @@ -560,7 +560,7 @@ describe("MockAiStreamPlayer", () => { errorEvents.push(payload as { messageId?: string }); }); - const firstUserMessage = createMuxMessage( + const firstUserMessage = createXumMessage( "user-stream-error-first", "user", "[mock:error:api] Trigger API error", @@ -574,7 +574,7 @@ describe("MockAiStreamPlayer", () => { await waitForCondition(() => deletePartialCallCount >= 1, 1000); - const replacementUserMessage = createMuxMessage( + const replacementUserMessage = createXumMessage( "user-stream-error-second", "user", "[force] replacement stream after cancelled error", @@ -619,7 +619,7 @@ describe("MockAiStreamPlayer", () => { }); const workspaceId = "workspace-stale-stream-end"; - const firstUserMessage = createMuxMessage( + const firstUserMessage = createXumMessage( "user-stream-end-first", "user", "[mock:list-languages] List 3 programming languages", @@ -633,7 +633,7 @@ describe("MockAiStreamPlayer", () => { await waitForCondition(() => deletePartialCallCount >= 1, 1000); - const replacementUserMessage = createMuxMessage( + const replacementUserMessage = createXumMessage( "user-stream-end-second", "user", "[force] replacement stream after completed turn", @@ -671,7 +671,7 @@ describe("MockAiStreamPlayer", () => { }); const workspaceId = "workspace-partial-commit"; - const userMessage = createMuxMessage( + const userMessage = createXumMessage( "user-commit", "user", "[mock:list-languages] List 3 programming languages", @@ -717,7 +717,7 @@ describe("MockAiStreamPlayer", () => { if (payload.workspaceId === workspaceId) streamEnd = payload; }); - const userMessage = createMuxMessage("user-metadata", "user", "Continue delegated work", { + const userMessage = createXumMessage("user-metadata", "user", "Continue delegated work", { timestamp: Date.now(), }); const playResult = await player.play([userMessage], workspaceId, { @@ -785,7 +785,7 @@ describe("MockAiStreamPlayer", () => { }); }); - const forceTurnUser = createMuxMessage("user-force", "user", "[force] keep streaming", { + const forceTurnUser = createXumMessage("user-force", "user", "[force] keep streaming", { timestamp: Date.now(), }); diff --git a/src/node/services/mock/mockAiStreamPlayer.ts b/src/node/services/mock/mockAiStreamPlayer.ts index 62193d7cfa..95c59a1ccd 100644 --- a/src/node/services/mock/mockAiStreamPlayer.ts +++ b/src/node/services/mock/mockAiStreamPlayer.ts @@ -1,6 +1,6 @@ import assert from "@/common/utils/assert"; -import type { MuxMessage, MuxMessageMetadata } from "@/common/types/message"; -import { createMuxMessage } from "@/common/types/message"; +import type { XumMessage, XumMessageMetadata } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { HistoryService } from "@/node/services/historyService"; import type { Result } from "@/common/types/result"; import { Ok, Err } from "@/common/types/result"; @@ -129,8 +129,8 @@ interface ActiveStream { mode?: MockStreamStartEvent["mode"]; agentId?: string; thinkingLevel?: MockStreamStartEvent["thinkingLevel"]; - muxMetadata?: MuxMessageMetadata; - parts: MuxMessage["parts"]; + muxMetadata?: XumMessageMetadata; + parts: XumMessage["parts"]; partialWriteTimer: ReturnType | null; eventQueue: Array<() => Promise>; isProcessing: boolean; @@ -141,14 +141,14 @@ export class MockAiStreamPlayer { private readonly streamStartGates = new Map(); private readonly releasedStreamStartGates = new Set(); private readonly router = new MockAiRouter(); - private readonly lastPromptByWorkspace = new Map(); + private readonly lastPromptByWorkspace = new Map(); private readonly lastModelByWorkspace = new Map(); private readonly activeStreams = new Map(); private nextMockMessageId = 0; constructor(private readonly deps: MockPlayerDeps) {} - debugGetLastPrompt(workspaceId: string): MuxMessage[] | null { + debugGetLastPrompt(workspaceId: string): XumMessage[] | null { return this.lastPromptByWorkspace.get(workspaceId) ?? null; } @@ -156,12 +156,12 @@ export class MockAiStreamPlayer { return this.lastModelByWorkspace.get(workspaceId) ?? null; } - private recordLastPrompt(workspaceId: string, messages: MuxMessage[]): void { + private recordLastPrompt(workspaceId: string, messages: XumMessage[]): void { try { const cloned = typeof structuredClone === "function" ? structuredClone(messages) - : (JSON.parse(JSON.stringify(messages)) as MuxMessage[]); + : (JSON.parse(JSON.stringify(messages)) as XumMessage[]); this.lastPromptByWorkspace.set(workspaceId, cloned); } catch { this.lastPromptByWorkspace.set(workspaceId, messages); @@ -272,13 +272,13 @@ export class MockAiStreamPlayer { } async play( - messages: MuxMessage[], + messages: XumMessage[], workspaceId: string, options?: { model?: string; agentId?: string; thinkingLevel?: StreamStartEvent["thinkingLevel"]; - muxMetadata?: MuxMessageMetadata; + muxMetadata?: XumMessageMetadata; abortSignal?: AbortSignal; } ): Promise> { @@ -363,7 +363,7 @@ export class MockAiStreamPlayer { let historySequence = this.computeNextHistorySequence(messages); - const assistantMessage = createMuxMessage(messageId, "assistant", "", { + const assistantMessage = createXumMessage(messageId, "assistant", "", { timestamp: Date.now(), model: streamStart.model, ...(streamStart.mode && { mode: streamStart.mode }), @@ -421,7 +421,7 @@ export class MockAiStreamPlayer { events: MockAssistantEvent[], messageId: string, historySequence: number, - muxMetadata?: MuxMessageMetadata + muxMetadata?: XumMessageMetadata ): void { const timers: Array> = []; const streamStart = events.find( @@ -518,7 +518,7 @@ export class MockAiStreamPlayer { const existingIndex = active.parts.findIndex( (part) => part.type === "dynamic-tool" && part.toolCallId === event.toolCallId ); - const nextPart: MuxMessage["parts"][number] = { + const nextPart: XumMessage["parts"][number] = { type: "dynamic-tool", state: "input-available", toolCallId: event.toolCallId, @@ -544,7 +544,7 @@ export class MockAiStreamPlayer { (part) => part.type === "dynamic-tool" && part.toolCallId === event.toolCallId ); const previousPart = existingIndex >= 0 ? active.parts[existingIndex] : undefined; - const nextPart: MuxMessage["parts"][number] = { + const nextPart: XumMessage["parts"][number] = { type: "dynamic-tool", state: "output-available", toolCallId: event.toolCallId, @@ -595,7 +595,7 @@ export class MockAiStreamPlayer { return; } - const partialMessage: MuxMessage = { + const partialMessage: XumMessage = { id: active.messageId, role: "assistant", metadata: { @@ -836,7 +836,7 @@ export class MockAiStreamPlayer { if (historyResult.success) { const existingMessage = historyResult.data.find((msg) => msg.id === messageId); if (existingMessage?.metadata?.historySequence !== undefined) { - const completedMessage: MuxMessage = { + const completedMessage: XumMessage = { id: messageId, role: "assistant", parts: completedParts, @@ -896,14 +896,14 @@ export class MockAiStreamPlayer { this.activeStreams.delete(workspaceId); } - private extractText(message: MuxMessage): string { + private extractText(message: XumMessage): string { return message.parts .filter((part) => "text" in part) .map((part) => (part as { text: string }).text) .join(""); } - private computeNextHistorySequence(messages: MuxMessage[]): number { + private computeNextHistorySequence(messages: XumMessage[]): number { let maxSequence = 0; for (const message of messages) { const seq = message.metadata?.historySequence; diff --git a/src/node/services/partialService.test.ts b/src/node/services/partialService.test.ts index 5f09183e54..1f24447c52 100644 --- a/src/node/services/partialService.test.ts +++ b/src/node/services/partialService.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"; import type { HistoryService } from "./historyService"; import type { Config } from "@/node/config"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { Ok } from "@/common/types/result"; import { createTestHistoryService } from "./testHistoryService"; import * as fs from "fs/promises"; @@ -22,7 +22,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { test("commitPartial should strip error metadata and commit parts from errored partial", async () => { const workspaceId = "test-workspace"; - const erroredPartial: MuxMessage = { + const erroredPartial: XumMessage = { id: "msg-1", role: "assistant", metadata: { @@ -71,7 +71,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { test("commitPartial should update existing placeholder when errored partial has more parts", async () => { const workspaceId = "test-workspace"; - const erroredPartial: MuxMessage = { + const erroredPartial: XumMessage = { id: "msg-1", role: "assistant", metadata: { @@ -94,7 +94,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { ], }; - const existingPlaceholder: MuxMessage = { + const existingPlaceholder: XumMessage = { id: "msg-1", role: "assistant", metadata: { @@ -142,7 +142,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { test("commitPartial should skip tool-only incomplete partials", async () => { const workspaceId = "test-workspace"; - const toolOnlyPartial: MuxMessage = { + const toolOnlyPartial: XumMessage = { id: "msg-1", role: "assistant", metadata: { @@ -182,7 +182,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { }); test("commitPartial should skip empty errored partial", async () => { const workspaceId = "test-workspace"; - const emptyErrorPartial: MuxMessage = { + const emptyErrorPartial: XumMessage = { id: "msg-1", role: "assistant", metadata: { @@ -225,7 +225,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { await partialService.appendToHistory( workspaceId, - createMuxMessage("msg-1", "assistant", "", { + createXumMessage("msg-1", "assistant", "", { historySequence, timestamp: Date.now(), model: "test-model", @@ -246,7 +246,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { errorType: "empty_output", }, parts: [], - } satisfies MuxMessage) + } satisfies XumMessage) ); const deleteMessageSpy = spyOn(partialService, "deleteMessage"); @@ -265,16 +265,16 @@ describe("HistoryService partial persistence - Error Recovery", () => { test("commitPartial deletes stale pre-boundary partial instead of appending it", async () => { const workspaceId = "test-workspace-stale-partial"; const rows = [ - createMuxMessage("user-before", "user", "before", { historySequence: 0 }), - createMuxMessage("assistant-before", "assistant", "before reply", { historySequence: 1 }), - createMuxMessage("summary", "assistant", "summary", { + createXumMessage("user-before", "user", "before", { historySequence: 0 }), + createXumMessage("assistant-before", "assistant", "before reply", { historySequence: 1 }), + createXumMessage("summary", "assistant", "summary", { historySequence: 2, compacted: "user", compactionBoundary: true, compactionEpoch: 1, muxMetadata: { type: "compaction-summary" }, }), - createMuxMessage("user-after", "user", "after", { historySequence: 3 }), + createXumMessage("user-after", "user", "after", { historySequence: 3 }), ]; for (const row of rows) { @@ -282,7 +282,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { expect(appendResult.success).toBe(true); } - const stalePartial = createMuxMessage("assistant-before", "assistant", "stale partial", { + const stalePartial = createXumMessage("assistant-before", "assistant", "stale partial", { historySequence: 1, partial: true, }); @@ -304,7 +304,7 @@ describe("HistoryService partial persistence - Error Recovery", () => { expect(historyResult.data.map((message) => message.id)).toEqual(["summary", "user-after"]); expect(historyResult.data.at(-1)?.metadata?.historySequence).toBe(3); - const nextMessage = createMuxMessage("next-user", "user", "next"); + const nextMessage = createXumMessage("next-user", "user", "next"); const appendNextResult = await partialService.appendToHistory(workspaceId, nextMessage); expect(appendNextResult.success).toBe(true); expect(nextMessage.metadata?.historySequence).toBe(4); @@ -329,7 +329,7 @@ describe("HistoryService partial persistence - Legacy compatibility", () => { const workspaceDir = config.getSessionDir(workspaceId); await fs.mkdir(workspaceDir, { recursive: true }); - const partialMessage = createMuxMessage("partial-1", "assistant", "legacy", { + const partialMessage = createXumMessage("partial-1", "assistant", "legacy", { historySequence: 0, }); (partialMessage.metadata as Record).cmuxMetadata = { type: "normal" }; diff --git a/src/node/services/providerModelFactory.test.ts b/src/node/services/providerModelFactory.test.ts index b87d64f2d4..26418745a0 100644 --- a/src/node/services/providerModelFactory.test.ts +++ b/src/node/services/providerModelFactory.test.ts @@ -6,7 +6,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { Config } from "@/node/config"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; import { CODEX_ENDPOINT, CODEX_OAUTH_ROUTED_HEADER } from "@/common/constants/codexOAuth"; import { PROVIDER_REGISTRY } from "@/common/constants/providers"; @@ -2383,7 +2383,7 @@ describe("ProviderModelFactory Coder", () => { } as Parameters[0]); factory.coderOauthService = stubCoderOauthService(); - const muxOptions: MuxProviderOptions = {}; + const muxOptions: XumProviderOptions = {}; const result = await factory.createModel("coder:prod-anthropic/claude-opus-4-5", muxOptions); expect(result.success).toBe(true); expect(muxOptions.anthropic?.disableBetaFeatures).toBe(true); @@ -2406,7 +2406,7 @@ describe("ProviderModelFactory Coder", () => { } as Parameters[0]); factory.coderOauthService = stubCoderOauthService(); - const muxOptions: MuxProviderOptions = {}; + const muxOptions: XumProviderOptions = {}; const result = await factory.createModel("coder:anthropic/gpt-5", muxOptions); expect(result.success).toBe(true); expect(muxOptions.anthropic).toBeUndefined(); @@ -2429,7 +2429,7 @@ describe("ProviderModelFactory Coder", () => { } as Parameters[0]); factory.coderOauthService = stubCoderOauthService(); - const muxOptions: MuxProviderOptions = {}; + const muxOptions: XumProviderOptions = {}; const result = await factory.createModel("coder:prod-openai/gpt-5.2", muxOptions); expect(result.success).toBe(true); expect(muxOptions.openai?.store).toBe(false); @@ -2452,7 +2452,7 @@ describe("ProviderModelFactory Coder", () => { } as Parameters[0]); factory.coderOauthService = stubCoderOauthService(); - const muxOptions: MuxProviderOptions = {}; + const muxOptions: XumProviderOptions = {}; const result = await factory.createModel("coder:openai/gpt-5", muxOptions); expect(result.success).toBe(true); expect(muxOptions.openai).toBeUndefined(); diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index 50dad21c6a..d98d46a69c 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -20,7 +20,7 @@ import { } from "@/common/constants/codexOAuth"; import { parseCodexOauthAuth } from "@/node/utils/codexOauthAuth"; import type { Config, ProviderConfig, ProvidersConfig } from "@/node/config"; -import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import type { XumProviderOptions } from "@/common/types/providerOptions"; import type { ServiceTier, XAIServiceTier } from "@/common/config/schemas/providersConfig"; import { resolveConfigBaseUrl } from "@/common/utils/providers/baseUrl"; import { isProviderDisabledInConfig } from "@/common/utils/providers/isProviderDisabled"; @@ -492,7 +492,7 @@ export function wrapFetchWithAnthropicCacheControl( * This ensures the UI immediately reflects that the user has been logged out * when the gateway session expires. */ -function wrapFetchWithMuxGatewayAutoLogout( +function wrapFetchWithXumGatewayAutoLogout( baseFetch: typeof fetch, providerService: ProviderService ): typeof fetch { @@ -1064,7 +1064,7 @@ export class ProviderModelFactory { */ async createModel( modelString: string, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, opts?: { agentInitiated?: boolean; workspaceId?: string; @@ -1110,7 +1110,7 @@ export class ProviderModelFactory { private async _createModelCore( modelString: string, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, opts?: { agentInitiated?: boolean; routeContext?: RouteContext; @@ -1849,7 +1849,7 @@ export class ProviderModelFactory { injectCacheControl: !disableBeta, }) : baseFetch; - const fetchWithAutoLogout = wrapFetchWithMuxGatewayAutoLogout( + const fetchWithAutoLogout = wrapFetchWithXumGatewayAutoLogout( fetchWithCacheControl, this.providerService ); @@ -2281,7 +2281,7 @@ export class ProviderModelFactory { async resolveAndCreateModel( modelString: string, thinkingLevel: ThinkingLevel, - muxProviderOptions?: MuxProviderOptions, + muxProviderOptions?: XumProviderOptions, opts?: { agentInitiated?: boolean; workspaceId?: string } ): Promise< Result< diff --git a/src/node/services/providerService.test.ts b/src/node/services/providerService.test.ts index cdd4b5d882..34fae86b98 100644 --- a/src/node/services/providerService.test.ts +++ b/src/node/services/providerService.test.ts @@ -36,7 +36,7 @@ async function saveRoutePriority( await config.editConfig(() => ({ ...config.loadConfigOrDefault(), ...overrides, routePriority })); } -function saveMuxGatewayConfig(config: Config): void { +function saveXumGatewayConfig(config: Config): void { config.saveProvidersConfig({ "mux-gateway": { couponCode: "gateway-token" } }); } @@ -288,7 +288,7 @@ describe("ProviderService.getConfig", () => { it("marks mux-gateway disabled when muxGatewayEnabled is false in main config", () => { withTempConfig((config, service) => { - saveMuxGatewayConfig(config); + saveXumGatewayConfig(config); const defaultMainConfig = config.loadConfigOrDefault(); const loadConfigSpy = spyOn(config, "loadConfigOrDefault"); @@ -1867,7 +1867,7 @@ describe("ProviderService gateway lifecycle", () => { it("does not auto-insert configured-but-disabled gateways into routePriority", async () => { await withTempConfigAsync(async (config, service) => { await saveRoutePriority(config, ["direct"]); - saveMuxGatewayConfig(config); + saveXumGatewayConfig(config); const result = await service.setConfig("mux-gateway", ["enabled"], false); @@ -1879,7 +1879,7 @@ describe("ProviderService gateway lifecycle", () => { it("auto-removes gateway from routePriority when disabled", async () => { await withTempConfigAsync(async (config, service) => { await saveRoutePriority(config, ["mux-gateway", "direct"]); - saveMuxGatewayConfig(config); + saveXumGatewayConfig(config); const result = await service.setConfig("mux-gateway", ["enabled"], false); @@ -1962,7 +1962,7 @@ describe("ProviderService gateway lifecycle", () => { it("preserves user order when inserting a second gateway before direct", async () => { await withTempConfigAsync(async (config, service) => { await saveRoutePriority(config, ["mux-gateway", "direct"]); - saveMuxGatewayConfig(config); + saveXumGatewayConfig(config); const result = await service.setConfig("openrouter", ["apiKey"], "sk-or-test"); @@ -1978,7 +1978,7 @@ describe("ProviderService gateway lifecycle", () => { it("appends gateway when direct is absent from routePriority", async () => { await withTempConfigAsync(async (config, service) => { await saveRoutePriority(config, ["mux-gateway"]); - saveMuxGatewayConfig(config); + saveXumGatewayConfig(config); const result = await service.setConfig("openrouter", ["apiKey"], "sk-or-test"); @@ -1990,7 +1990,7 @@ describe("ProviderService gateway lifecycle", () => { it("auto-removes gateway from routePriority when deconfigured", async () => { await withTempConfigAsync(async (config, service) => { await saveRoutePriority(config, ["mux-gateway", "direct"]); - saveMuxGatewayConfig(config); + saveXumGatewayConfig(config); const result = await service.setConfig("mux-gateway", ["couponCode"], ""); diff --git a/src/node/services/providerService.ts b/src/node/services/providerService.ts index 909e2fa87b..bacac74ac2 100644 --- a/src/node/services/providerService.ts +++ b/src/node/services/providerService.ts @@ -1439,7 +1439,7 @@ export class ProviderService { const providersConfig = this.config.loadProvidersConfig() ?? {}; // Track if this is first time setting couponCode for mux-gateway - const isFirstMuxGatewayCoupon = + const isFirstXumGatewayCoupon = provider === "mux-gateway" && keyPath.length === 1 && keyPath[0] === "couponCode" && @@ -1487,7 +1487,7 @@ export class ProviderService { } // Add default models when setting up mux-gateway for the first time - if (isFirstMuxGatewayCoupon) { + if (isFirstXumGatewayCoupon) { const providerConfig = providersConfig[provider] as Record; const existingModels = normalizeProviderModelEntries(providerConfig.models); if (existingModels.length === 0) { diff --git a/src/node/services/replay/replayFixture.ts b/src/node/services/replay/replayFixture.ts index a6ba66c2b0..50c61f43bc 100644 --- a/src/node/services/replay/replayFixture.ts +++ b/src/node/services/replay/replayFixture.ts @@ -19,7 +19,7 @@ import type { LanguageModelV2Usage } from "@ai-sdk/provider"; import { z } from "zod"; import type { PostCompactionAttachment } from "@/common/types/attachment"; import type { DevToolsLogEntry } from "@/common/types/devtools"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { applyCacheControlToTools, type AnthropicCacheTtl } from "@/common/utils/ai/cacheStrategy"; import assert from "@/common/utils/assert"; import { HistoryService } from "@/node/services/historyService"; @@ -77,7 +77,7 @@ export interface ReplayFixtureTurnSpec { planFilePath?: string; postCompactionAttachments?: PostCompactionAttachment[]; /** Refusal-fallback partial continuation (envelope-only, never in chat.jsonl). */ - partialContinuation?: MuxMessage; + partialContinuation?: XumMessage; anthropicCacheTtl?: AnthropicCacheTtl; /** Set false to simulate devtools logging being off for this turn. */ recordDevtools?: boolean; @@ -114,7 +114,7 @@ export function createReplayFixtureSessionContext( async function appendOrThrow( ctx: ReplayFixtureSessionContext, - message: MuxMessage + message: XumMessage ): Promise { const result = await ctx.historyService.appendToHistory(ctx.workspaceId, message); assert(result.success, `fixture append failed: ${String(!result.success && result.error)}`); @@ -140,7 +140,7 @@ export async function appendReplayFixtureTurn( const baseTimestamp = 1_700_000_000_000 + turnNumber * 10_000; const agentId = spec.agentId ?? "exec"; - const userMessage = createMuxMessage(`user-${turnNumber}`, "user", spec.userText, { + const userMessage = createXumMessage(`user-${turnNumber}`, "user", spec.userText, { timestamp: baseTimestamp, }); const requestHistorySequence = await appendOrThrow(ctx, userMessage); @@ -241,7 +241,7 @@ export async function appendReplayFixtureTurn( } if (spec.assistantText !== undefined) { - const assistantMessage = createMuxMessage( + const assistantMessage = createXumMessage( `assistant-${turnNumber}`, "assistant", spec.assistantText, @@ -269,7 +269,7 @@ export async function appendReplayFixtureCompactionBoundary( compactionEpoch: number ): Promise { ctx.turnCounter += 1; - const boundaryMessage = createMuxMessage( + const boundaryMessage = createXumMessage( `compaction-${ctx.turnCounter}`, "assistant", summaryText, diff --git a/src/node/services/replay/replayRequestBuilder.ts b/src/node/services/replay/replayRequestBuilder.ts index f7e6c6eda5..8e804c4fb9 100644 --- a/src/node/services/replay/replayRequestBuilder.ts +++ b/src/node/services/replay/replayRequestBuilder.ts @@ -34,7 +34,7 @@ import type { import { addInterruptedSentinel } from "@/browser/utils/messages/modelMessageTransform"; import type { ProvidersConfigMap } from "@/common/orpc/types"; import type { PostCompactionAttachment } from "@/common/types/attachment"; -import { filterOrphanedMcpPromptSnapshots, type MuxMessage } from "@/common/types/message"; +import { filterOrphanedMcpPromptSnapshots, type XumMessage } from "@/common/types/message"; import type { ThinkingLevel } from "@/common/types/thinking"; import { createCachedSystemMessage, @@ -59,7 +59,7 @@ export interface ReplayRequestInputs { * epoch, already sliced to historySequence <= the turn's * requestHistorySequence by the caller). */ - historyMessages: MuxMessage[]; + historyMessages: XumMessage[]; /** Blob-resolved system prompt (turn-envelope systemPromptHash). */ systemPrompt: string; /** From the turn-envelope row. */ @@ -91,7 +91,7 @@ export interface ReplayRequestInputs { * message exists only in the envelope; production appends it to the * fallback request via replaceOrAppendMessageById. */ - partialContinuation?: MuxMessage | null; + partialContinuation?: XumMessage | null; workspaceId: string; } diff --git a/src/node/services/replay/replayVerify.fixture.test.ts b/src/node/services/replay/replayVerify.fixture.test.ts index 6d1169b0a5..2e4a6f73e3 100644 --- a/src/node/services/replay/replayVerify.fixture.test.ts +++ b/src/node/services/replay/replayVerify.fixture.test.ts @@ -12,7 +12,7 @@ */ import { beforeAll, describe, expect, test } from "bun:test"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { createDisplayUsage } from "@/common/utils/tokens/displayUsage"; import { HistoryService } from "@/node/services/historyService"; import { auditCacheBusts } from "./cacheAudit"; @@ -30,7 +30,7 @@ import { } from "./replayVerify"; import { DurableEventJournal } from "@/node/utils/journal/durableEventJournal"; -async function readFixtureHistory(): Promise { +async function readFixtureHistory(): Promise { const historyService = new HistoryService({ getSessionDir: () => REPLAY_FIXTURE_DIR }); const result = await collectFullHistory(historyService, REPLAY_FIXTURE_WORKSPACE_ID); if (!result.success) { diff --git a/src/node/services/replay/replayVerify.test.ts b/src/node/services/replay/replayVerify.test.ts index d9fcca494f..c20b5c86db 100644 --- a/src/node/services/replay/replayVerify.test.ts +++ b/src/node/services/replay/replayVerify.test.ts @@ -13,7 +13,7 @@ import * as fs from "node:fs/promises"; import { describe, expect, test } from "bun:test"; import { tool, type Tool } from "ai"; import { z } from "zod"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { DisposableTempDir } from "@/node/services/tempDir"; import { emitTurnEnvelope } from "@/node/services/turnEnvelope"; import { @@ -183,7 +183,7 @@ describe("replayVerifySession pairing", () => { // Hand-build a turn whose envelope carries an EMPTY system prompt blob: // buildReplayRequest asserts on it, and the assertion must become a FAIL // for this turn instead of crashing the whole verification (and the CLI). - const userMessage = createMuxMessage("user-corrupt", "user", "Corrupted turn.", { + const userMessage = createXumMessage("user-corrupt", "user", "Corrupted turn.", { timestamp: 1_700_000_500_000, }); const appendUser = await ctx.historyService.appendToHistory(ctx.workspaceId, userMessage); @@ -234,7 +234,7 @@ describe("replayVerifySession pairing", () => { }, }) ); - const assistantMessage = createMuxMessage("assistant-corrupt", "assistant", "Oops.", { + const assistantMessage = createXumMessage("assistant-corrupt", "assistant", "Oops.", { timestamp: 1_700_000_502_000, model: REPLAY_FIXTURE_MODEL, agentId: "exec", @@ -307,7 +307,7 @@ describe("replayVerifySession pairing", () => { // chat.jsonl at this turn's requestHistorySequence (the surviving // assistant row lands later), so the envelope blob must be enough to // rebuild the request bytes. - const continuation = createMuxMessage( + const continuation = createXumMessage( "assistant-partial-refusal", "assistant", "Partial output before the refusal.", diff --git a/src/node/services/replay/replayVerify.ts b/src/node/services/replay/replayVerify.ts index 1107a708fd..4f15593168 100644 --- a/src/node/services/replay/replayVerify.ts +++ b/src/node/services/replay/replayVerify.ts @@ -23,7 +23,7 @@ import type { } from "@/common/types/devtools"; import type { PostCompactionAttachment } from "@/common/types/attachment"; import type { DurableEvent } from "@/common/types/durableEvent"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { Result } from "@/common/types/result"; import { Ok } from "@/common/types/result"; import type { ThinkingLevel } from "@/common/types/thinking"; @@ -210,7 +210,7 @@ export interface ReplayVerifySessionResult { } export interface AssistantTurn { - message: MuxMessage; + message: XumMessage; requestHistorySequence: number; } @@ -219,7 +219,7 @@ export interface AssistantTurn { * streamMessage call) — the chat.jsonl side of the pairing with turn-envelope * rows and recorded devtools runs. */ -export function collectAssistantTurns(historyMessages: MuxMessage[]): AssistantTurn[] { +export function collectAssistantTurns(historyMessages: XumMessage[]): AssistantTurn[] { const turns: AssistantTurn[] = []; for (const message of historyMessages) { const requestHistorySequence = message.metadata?.requestHistorySequence; @@ -240,8 +240,8 @@ export function collectAssistantTurns(historyMessages: MuxMessage[]): AssistantT export async function collectFullHistory( historyService: HistoryService, workspaceId: string -): Promise> { - const messages: MuxMessage[] = []; +): Promise> { + const messages: XumMessage[] = []; const result = await historyService.iterateFullHistory(workspaceId, "forward", (chunk) => { messages.push(...chunk); }); @@ -257,9 +257,9 @@ export async function collectFullHistory( * (compaction summaries are included, reset markers are not). */ export function sliceEpochForTurn( - historyMessages: MuxMessage[], + historyMessages: XumMessage[], requestHistorySequence: number -): MuxMessage[] { +): XumMessage[] { const prefix = historyMessages.filter((message) => { const sequence = message.metadata?.historySequence; assert(sequence != null, `history row ${message.id} is missing historySequence`); @@ -477,7 +477,7 @@ function parseAnthropicCacheTtl(value: string | undefined): AnthropicCacheTtl | export async function replayVerifySession(params: { sessionDir: string; workspaceId: string; - historyMessages: MuxMessage[]; + historyMessages: XumMessage[]; providersConfig?: ProvidersConfigMap | null; }): Promise { const journal = new DurableEventJournal(params.sessionDir); @@ -556,7 +556,7 @@ export async function replayVerifySession(params: { postCompactionAttachments = JSON.parse(attachmentsJson) as PostCompactionAttachment[]; } - let partialContinuation: MuxMessage | undefined; + let partialContinuation: XumMessage | undefined; if (envelope.data.partialContinuationHash != null) { const continuationJson = await journal.blobs.getText(envelope.data.partialContinuationHash); if (continuationJson == null) { @@ -565,7 +565,7 @@ export async function replayVerifySession(params: { ); continue; } - partialContinuation = JSON.parse(continuationJson) as MuxMessage; + partialContinuation = JSON.parse(continuationJson) as XumMessage; } // 1) System prompt: blob bytes vs the wire's system message. diff --git a/src/node/services/serverService.ts b/src/node/services/serverService.ts index 3712fe5190..cffe57bf60 100644 --- a/src/node/services/serverService.ts +++ b/src/node/services/serverService.ts @@ -8,7 +8,7 @@ import { log } from "./log"; import * as os from "os"; import * as childProcess from "node:child_process"; import { VERSION } from "@/version"; -import { buildMuxMdnsServiceOptions, MdnsAdvertiserService } from "./mdnsAdvertiserService"; +import { buildXumMdnsServiceOptions, MdnsAdvertiserService } from "./mdnsAdvertiserService"; import type { AppRouter } from "@/node/orpc/router"; export interface ServerInfo { @@ -440,7 +440,7 @@ export class ServerService { // "auto" mode: only advertise when the bind host is reachable from other devices. if (mdnsAdvertisementEnabled !== false && !isLoopbackHost(bindHost)) { const instanceName = options.context.config.getMdnsServiceName() ?? `xum-${os.hostname()}`; - const serviceOptions = buildMuxMdnsServiceOptions({ + const serviceOptions = buildXumMdnsServiceOptions({ bindHost, port: server.port, instanceName, diff --git a/src/node/services/serviceContainer.ts b/src/node/services/serviceContainer.ts index 960a2e3fa7..832a28aa08 100644 --- a/src/node/services/serviceContainer.ts +++ b/src/node/services/serviceContainer.ts @@ -7,8 +7,8 @@ import { createCoreServices, type CoreServices } from "@/node/services/coreServi import { PTYService } from "@/node/services/ptyService"; import type { TerminalWindowManager } from "@/desktop/terminalWindowManager"; import { ProjectService } from "@/node/services/projectService"; -import { MuxGatewayOauthService } from "@/node/services/muxGatewayOauthService"; -import { MuxGovernorOauthService } from "@/node/services/muxGovernorOauthService"; +import { XumGatewayOauthService } from "@/node/services/xumGatewayOauthService"; +import { XumGovernorOauthService } from "@/node/services/xumGovernorOauthService"; import { CodexOauthService } from "@/node/services/codexOauthService"; import { CoderOauthService } from "@/node/services/coderOauthService"; import { CopilotOauthService } from "@/node/services/copilotOauthService"; @@ -103,8 +103,8 @@ export class ServiceContainer { private readonly backgroundProcessManager: CoreServices["backgroundProcessManager"]; // Desktop-only services public readonly projectService: ProjectService; - public readonly muxGatewayOauthService: MuxGatewayOauthService; - public readonly muxGovernorOauthService: MuxGovernorOauthService; + public readonly xumGatewayOauthService: XumGatewayOauthService; + public readonly xumGovernorOauthService: XumGovernorOauthService; public readonly codexOauthService: CodexOauthService; public readonly coderOauthService: CoderOauthService; public readonly copilotOauthService: CopilotOauthService; @@ -286,11 +286,11 @@ export class ServiceContainer { ); this.mcpServerManager.setMcpOauthService(this.mcpOauthService); - this.muxGatewayOauthService = new MuxGatewayOauthService( + this.xumGatewayOauthService = new XumGatewayOauthService( this.providerService, this.windowService ); - this.muxGovernorOauthService = new MuxGovernorOauthService( + this.xumGovernorOauthService = new XumGovernorOauthService( config, this.windowService, this.policyService @@ -547,7 +547,7 @@ export class ServiceContainer { // Refresh xum-owned Coder SSH config in background (handles binary path changes on restart) // Skip getCoderInfo() to avoid caching "unavailable" if coder isn't installed yet - void this.coderService.ensureMuxCoderSSHConfig().catch((error: unknown) => { + void this.coderService.ensureXumCoderSSHConfig().catch((error: unknown) => { log.warn("Background xum SSH config setup failed", { error }); }); @@ -571,8 +571,8 @@ export class ServiceContainer { workspaceService: this.workspaceService, taskService: this.taskService, providerService: this.providerService, - muxGatewayOauthService: this.muxGatewayOauthService, - muxGovernorOauthService: this.muxGovernorOauthService, + xumGatewayOauthService: this.xumGatewayOauthService, + xumGovernorOauthService: this.xumGovernorOauthService, codexOauthService: this.codexOauthService, coderOauthService: this.coderOauthService, copilotOauthService: this.copilotOauthService, @@ -670,8 +670,8 @@ export class ServiceContainer { this.policyService.dispose(); this.mcpServerManager.dispose(); await this.mcpOauthService.dispose(); - await this.muxGatewayOauthService.dispose(); - await this.muxGovernorOauthService.dispose(); + await this.xumGatewayOauthService.dispose(); + await this.xumGovernorOauthService.dispose(); await this.codexOauthService.dispose(); await this.coderOauthService.dispose(); diff --git a/src/node/services/sessionUsageService.test.ts b/src/node/services/sessionUsageService.test.ts index eaa3bd3596..f725a4ccfe 100644 --- a/src/node/services/sessionUsageService.test.ts +++ b/src/node/services/sessionUsageService.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { SessionUsageService, type SessionUsageTokenStatsCacheV1 } from "./sessionUsageService"; import type { HistoryService } from "./historyService"; import type { Config } from "@/node/config"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { getTotalCost, sumUsageHistory, @@ -701,14 +701,14 @@ describe("SessionUsageService", () => { // Seed messages via real historyService await historyService.appendToHistory( workspaceId, - createMuxMessage("msg1", "assistant", "Hello", { + createXumMessage("msg1", "assistant", "Hello", { model: "claude-sonnet-4-20250514", usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 }, }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("msg2", "assistant", "World", { + createXumMessage("msg2", "assistant", "World", { model: "claude-sonnet-4-20250514", usage: { inputTokens: 200, outputTokens: 75, totalTokens: 275 }, }) @@ -732,7 +732,7 @@ describe("SessionUsageService", () => { // Seed messages via real historyService await historyService.appendToHistory( workspaceId, - createMuxMessage("msg1", "assistant", "Hello", { + createXumMessage("msg1", "assistant", "Hello", { model: "claude-sonnet-4-20250514", usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 }, }) @@ -778,7 +778,7 @@ describe("SessionUsageService", () => { }, }; - const assistantMessage = createMuxMessage("msg-tool-usage", "assistant", "Hello", { + const assistantMessage = createXumMessage("msg-tool-usage", "assistant", "Hello", { historySequence: 1, timestamp: Date.now(), model, @@ -838,7 +838,7 @@ describe("SessionUsageService", () => { metadataModel, usage: { inputTokens: 30, outputTokens: 10, totalTokens: 40 }, }; - const assistantMessage = createMuxMessage("msg-coder-rebuild", "assistant", "Hello", { + const assistantMessage = createXumMessage("msg-coder-rebuild", "assistant", "Hello", { historySequence: 1, timestamp: Date.now(), model: rawModel, @@ -892,7 +892,7 @@ describe("SessionUsageService", () => { }, }; - const assistantMessage = createMuxMessage("msg-tool-usage-malformed", "assistant", "Hello", { + const assistantMessage = createXumMessage("msg-tool-usage-malformed", "assistant", "Hello", { historySequence: 1, timestamp: 1_700_000_000_000, }); @@ -944,7 +944,7 @@ describe("SessionUsageService", () => { const workspaceId = "test-workspace"; // Create a compaction summary with historicalUsage (legacy format) - const compactionSummary = createMuxMessage("summary-1", "assistant", "Compacted summary", { + const compactionSummary = createXumMessage("summary-1", "assistant", "Compacted summary", { historySequence: 1, compacted: true, model: "anthropic:claude-sonnet-4-5", @@ -959,7 +959,7 @@ describe("SessionUsageService", () => { ); // Add a post-compaction message - const postCompactionMsg = createMuxMessage("msg2", "assistant", "New response", { + const postCompactionMsg = createXumMessage("msg2", "assistant", "New response", { historySequence: 2, model: "anthropic:claude-sonnet-4-5", usage: { inputTokens: 200, outputTokens: 75, totalTokens: 275 }, diff --git a/src/node/services/sessionUsageService.ts b/src/node/services/sessionUsageService.ts index fdac3e3f39..ba29fdf08d 100644 --- a/src/node/services/sessionUsageService.ts +++ b/src/node/services/sessionUsageService.ts @@ -16,7 +16,7 @@ import { import type { RolledUpChildEntry } from "@/common/orpc/schemas/chatStats"; import type { TokenConsumer } from "@/common/types/chatStats"; import { HEADLESS_USAGE_FILE_NAME } from "@/common/constants/paths"; -import type { MuxMessage, PersistedToolModelUsage } from "@/common/types/message"; +import type { XumMessage, PersistedToolModelUsage } from "@/common/types/message"; import { normalizeUsageModelKey, resolveModelForMetadata, @@ -119,8 +119,8 @@ export class SessionUsageService { this.getProvidersConfig = getProvidersConfig ?? (() => null); } /** Usage rebuild needs every epoch for accurate totals. */ - private async collectFullHistory(workspaceId: string): Promise { - const messages: MuxMessage[] = []; + private async collectFullHistory(workspaceId: string): Promise { + const messages: XumMessage[] = []; const result = await this.historyService.iterateFullHistoryUnderLock( workspaceId, "forward", @@ -598,7 +598,7 @@ export class SessionUsageService { */ private async rebuildFromMessagesInternal( workspaceId: string, - messages: MuxMessage[] + messages: XumMessage[] ): Promise { const result: SessionUsageFile = this.createEmptyUsageFile(); let lastAssistantUsage: { model: string; usage: ChatUsageDisplay } | undefined; @@ -713,7 +713,7 @@ export class SessionUsageService { /** * Public rebuild method (acquires lock). */ - async rebuildFromMessages(workspaceId: string, messages: MuxMessage[]): Promise { + async rebuildFromMessages(workspaceId: string, messages: XumMessage[]): Promise { return this.fileLocks.withLock(workspaceId, async () => { await this.rebuildFromMessagesInternal(workspaceId, messages); }); diff --git a/src/node/services/streamContextBuilder.test.ts b/src/node/services/streamContextBuilder.test.ts index a85668b0b7..1e3b5328fa 100644 --- a/src/node/services/streamContextBuilder.test.ts +++ b/src/node/services/streamContextBuilder.test.ts @@ -5,7 +5,7 @@ import { describe, expect, test } from "bun:test"; import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace"; import { sliceMessagesFromLatestCompactionBoundary } from "@/common/utils/messages/compactionBoundary"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { WorkspaceMetadata } from "@/common/types/workspace"; import type { ProjectsConfig } from "@/common/types/project"; import { DEFAULT_TASK_SETTINGS } from "@/common/types/tasks"; @@ -121,7 +121,7 @@ describe("buildPlanInstructions", () => { }; const runtime = new TestRuntime(projectPath, xumHome); - const requestPayloadMessages = [createMuxMessage("u1", "user", "plan the fix")]; + const requestPayloadMessages = [createXumMessage("u1", "user", "plan the fix")]; const callerInstructions = "Caller-specific plan note"; const expectedPlanFilePath = getPlanFilePath(metadata.name, metadata.projectName, xumHome); @@ -178,7 +178,7 @@ describe("buildPlanInstructions", () => { await fs.mkdir(path.dirname(planFilePath), { recursive: true }); await fs.writeFile(planFilePath, "# Plan\n\n- Keep implementing", "utf-8"); - const startHereSummary = createMuxMessage( + const startHereSummary = createXumMessage( "start-here", "assistant", "# Start Here\n\n- Existing plan context\n\n*Plan file preserved at:* /tmp/plan.md", @@ -188,13 +188,13 @@ describe("buildPlanInstructions", () => { } ); - const compactionBoundary = createMuxMessage("boundary", "assistant", "Compacted summary", { + const compactionBoundary = createXumMessage("boundary", "assistant", "Compacted summary", { compacted: "user", compactionBoundary: true, compactionEpoch: 1, }); - const latestUserMessage = createMuxMessage("u1", "user", "continue implementation"); + const latestUserMessage = createXumMessage("u1", "user", "continue implementation"); const fullHistory = [startHereSummary, compactionBoundary, latestUserMessage]; const requestPayloadMessages = sliceMessagesFromLatestCompactionBoundary(fullHistory); diff --git a/src/node/services/streamContextBuilder.ts b/src/node/services/streamContextBuilder.ts index 0fe0a61e4a..02ca8fe787 100644 --- a/src/node/services/streamContextBuilder.ts +++ b/src/node/services/streamContextBuilder.ts @@ -17,7 +17,7 @@ import * as path from "node:path"; import assert from "@/common/utils/assert"; import { ADVISOR_USAGE_GUIDANCE } from "@/common/constants/advisor"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { DesktopCapability } from "@/common/types/desktop"; import type { ProjectsConfig } from "@/common/types/project"; import type { XumToolScope } from "@/common/types/toolScope"; @@ -76,7 +76,7 @@ export interface BuildPlanInstructionsOptions { * Plan-context derivation must stay aligned with the request payload to avoid pre-boundary * history (e.g., old Start Here summaries) suppressing required plan hints. */ - requestPayloadMessages: MuxMessage[]; + requestPayloadMessages: XumMessage[]; } /** Result of plan instructions assembly. */ diff --git a/src/node/services/streamManager.test.ts b/src/node/services/streamManager.test.ts index b6ea466da9..e38043998d 100644 --- a/src/node/services/streamManager.test.ts +++ b/src/node/services/streamManager.test.ts @@ -11,7 +11,7 @@ import type { ToolCallExecutionStartEvent, WorkflowRunAttachedEvent, } from "@/common/types/stream"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { Ok, Err } from "@/common/types/result"; import type { ToolPolicy } from "@/common/utils/tools/toolPolicy"; import type { ToolSearchStreamState } from "@/common/utils/tools/toolCatalog"; @@ -759,7 +759,7 @@ describe("StreamManager - refusal usage attribution", () => { const buildPartial = Reflect.get(streamManager, "buildPartialAssistantMessage") as ( streamInfo: Record, options?: Record - ) => MuxMessage; + ) => XumMessage; expect(typeof buildPartial).toBe("function"); const message = buildPartial.call(streamManager, { diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index 7aad3f6c59..69227d311d 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -32,7 +32,7 @@ import type { } from "@/common/types/stream"; import type { SendMessageError, StreamErrorType } from "@/common/types/errors"; -import type { MuxMetadata, MuxMessage, PersistedToolModelUsage } from "@/common/types/message"; +import type { XumMetadata, XumMessage, PersistedToolModelUsage } from "@/common/types/message"; import { findFirstReasoningPartIndexInTrailingRun, mergeReasoningProviderOptions, @@ -257,7 +257,7 @@ export interface PreparedModelFallback { anthropicCacheTtl?: AnthropicCacheTtl; thinkingLevel?: string; /** Route attribution corrections (routedThroughGateway, routeProvider, costsIncluded). */ - initialMetadataPatch?: Partial; + initialMetadataPatch?: Partial; /** * Rebuild closure bound to the FALLBACK model so mid-turn thinking changes * keep working after a fallback hop (the source model's closure would build @@ -293,7 +293,7 @@ export interface ModelFallbackPrepareOptions { * mid-turn refusal. This must be cloned from the stream state before prepare() * receives it so provider-message preparation cannot mutate live UI parts. */ - continuation?: { assistantMessage: MuxMessage }; + continuation?: { assistantMessage: XumMessage }; /** * Mid-turn thinking level to fold into the fallback's baseline: pending (not * yet applied) or already-applied override from the refused stream. The @@ -575,7 +575,7 @@ interface WorkspaceStreamInfo { metadataModel: string; /** Effective thinking level after model policy clamping */ thinkingLevel?: string; - initialMetadata?: Partial; + initialMetadata?: Partial; toolModelUsages: PersistedToolModelUsage[]; request: StreamRequestConfig; // Track last prepared step messages for safe retries after tool steps @@ -2027,7 +2027,7 @@ export class StreamManager extends EventEmitter { historySequence: number, messageId: string, tools?: Record, - initialMetadata?: Partial, + initialMetadata?: Partial, providerOptions?: Record, maxOutputTokens?: number, toolPolicy?: ToolPolicy, @@ -2378,7 +2378,7 @@ export class StreamManager extends EventEmitter { // Console events are not streamed (appear in final result only) } - private getStreamMode(initialMetadata?: Partial): "plan" | "exec" | undefined { + private getStreamMode(initialMetadata?: Partial): "plan" | "exec" | undefined { const rawMode = initialMetadata?.mode; // Stats schema only accepts "plan" | "exec". return rawMode === "plan" || rawMode === "exec" ? rawMode : undefined; @@ -2604,8 +2604,8 @@ export class StreamManager extends EventEmitter { private buildPartialAssistantMessage( streamInfo: WorkspaceStreamInfo, - options: { metadata?: Partial; parts?: MuxMessage["parts"] } = {} - ): MuxMessage { + options: { metadata?: Partial; parts?: XumMessage["parts"] } = {} + ): XumMessage { const canonicalModel = metadataModelIdentity(streamInfo.model); const routedThroughGateway = streamInfo.initialMetadata?.routedThroughGateway ?? @@ -2634,9 +2634,9 @@ export class StreamManager extends EventEmitter { private buildPartialRefusalContinuationMessage( streamInfo: WorkspaceStreamInfo, refusalFinishReason: string - ): Result { + ): Result { try { - const parts = structuredClone(streamInfo.parts) as MuxMessage["parts"]; + const parts = structuredClone(streamInfo.parts) as XumMessage["parts"]; return Ok( this.buildPartialAssistantMessage(streamInfo, { metadata: { finishReason: refusalFinishReason }, @@ -3588,7 +3588,7 @@ export class StreamManager extends EventEmitter { // clears history while updateHistory is still running, causing old messages // to be written back after compaction completes. if (streamInfo.parts && streamInfo.parts.length > 0) { - const finalAssistantMessage: MuxMessage = { + const finalAssistantMessage: XumMessage = { id: streamInfo.messageId, role: "assistant", metadata: { @@ -4338,7 +4338,7 @@ export class StreamManager extends EventEmitter { messageId: string, abortSignal?: AbortSignal, tools?: Record, - initialMetadata?: Partial, + initialMetadata?: Partial, providerOptions?: Record, maxOutputTokens?: number, toolPolicy?: ToolPolicy, diff --git a/src/node/services/streamSimulation.test.ts b/src/node/services/streamSimulation.test.ts index b60f88f4e6..4783523b46 100644 --- a/src/node/services/streamSimulation.test.ts +++ b/src/node/services/streamSimulation.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { StreamEndEventSchema } from "@/common/orpc/schemas/stream"; -import { createMuxMessage, type MuxMetadata } from "@/common/types/message"; +import { createXumMessage, type XumMetadata } from "@/common/types/message"; import type { StreamEndEvent, StreamStartEvent } from "@/common/types/stream"; import { createTestHistoryService } from "@/node/services/testHistoryService"; import { @@ -67,7 +67,7 @@ describe("streamSimulation", () => { const appendResult = await historyService.appendToHistory( ctx.workspaceId, - createMuxMessage(ctx.assistantMessageId, "assistant", "", { + createXumMessage(ctx.assistantMessageId, "assistant", "", { historySequence: ctx.historySequence, }) ); @@ -77,7 +77,7 @@ describe("streamSimulation", () => { const streamEnd = getCapturedEvent(events, "stream-end"); const streamEndMetadata = streamEnd.metadata as StreamEndEvent["metadata"] & - Pick; + Pick; expect(streamEndMetadata.agentId).toBe("exec"); expect(streamEndMetadata.mode).toBe("exec"); diff --git a/src/node/services/streamSimulation.ts b/src/node/services/streamSimulation.ts index 34f8971905..9516b0247d 100644 --- a/src/node/services/streamSimulation.ts +++ b/src/node/services/streamSimulation.ts @@ -9,8 +9,8 @@ * - `simulateToolPolicyNoop`: OpenAI SDK testing of tool-policy-disabled handling */ -import type { MuxMessage, MuxTextPart } from "@/common/types/message"; -import { createMuxMessage } from "@/common/types/message"; +import type { XumMessage, XumTextPart } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { ThinkingLevel } from "@/common/types/thinking"; import type { StreamDeltaEvent, StreamEndEvent, StreamStartEvent } from "@/common/types/stream"; import type { ToolPolicy } from "@/common/utils/tools/toolPolicy"; @@ -72,7 +72,7 @@ export async function simulateContextLimitError( const errorMessage = "Context length exceeded: the conversation is too long to send to this OpenAI model. Please shorten the history and try again."; - const errorPartialMessage: MuxMessage = { + const errorPartialMessage: XumMessage = { id: ctx.assistantMessageId, role: "assistant", metadata: { @@ -120,7 +120,7 @@ export async function simulateToolPolicyNoop( effectiveToolPolicy: ToolPolicy | undefined, historyService: HistoryService ): Promise { - const noopMessage = createMuxMessage(ctx.assistantMessageId, "assistant", "", { + const noopMessage = createXumMessage(ctx.assistantMessageId, "assistant", "", { timestamp: Date.now(), model: ctx.canonicalModelString, routedThroughGateway: ctx.routedThroughGateway, @@ -141,7 +141,7 @@ export async function simulateToolPolicyNoop( ctx.emit("stream-start", createSimulatedStreamStart(ctx)); - const textParts = parts.filter((part): part is MuxTextPart => part.type === "text"); + const textParts = parts.filter((part): part is XumTextPart => part.type === "text"); if (textParts.length === 0) { throw new Error("simulateToolPolicyNoop requires at least one text part"); } @@ -179,7 +179,7 @@ export async function simulateToolPolicyNoop( }; ctx.emit("stream-end", streamEndEvent); - const finalAssistantMessage: MuxMessage = { + const finalAssistantMessage: XumMessage = { ...noopMessage, metadata: { ...noopMessage.metadata, diff --git a/src/node/services/systemMessage.ts b/src/node/services/systemMessage.ts index 5e3aa5de7d..cdd65f992e 100644 --- a/src/node/services/systemMessage.ts +++ b/src/node/services/systemMessage.ts @@ -13,7 +13,7 @@ import { getProjects, isMultiProject } from "@/common/utils/multiProject"; import { INSTRUCTION_SCOPE, collectInstructionContents, - collectMuxOnlyInstructionContents, + collectXumOnlyInstructionContents, type InstructionSet, type InstructionSources, } from "@/common/types/instructions"; @@ -666,8 +666,8 @@ export async function buildSystemMessage( // so a "Model: …" heading in a shared AGENTS.md (read by non-Xum agents too) // stays ordinary markdown. Extraction runs per file: a scoped section at the // end of one file must not swallow the next file's unscoped content. - const muxContextContents = collectMuxOnlyInstructionContents(instructionSources.context); - const muxGlobalContents = collectMuxOnlyInstructionContents(instructionSources.global); + const muxContextContents = collectXumOnlyInstructionContents(instructionSources.context); + const muxGlobalContents = collectXumOnlyInstructionContents(instructionSources.global); const agentPromptSections = (options?.agentSystemPromptSections ?? []) .map((section) => section.trim()) diff --git a/src/node/services/taskService.test.ts b/src/node/services/taskService.test.ts index eff2ba2524..e63c9c76c1 100644 --- a/src/node/services/taskService.test.ts +++ b/src/node/services/taskService.test.ts @@ -64,7 +64,7 @@ import { DEFAULT_TASK_SETTINGS } from "@/common/types/tasks"; import type { ThinkingLevel } from "@/common/types/thinking"; import type { SendMessageError } from "@/common/types/errors"; import type { ErrorEvent, StreamAbortEvent, StreamEndEvent } from "@/common/types/stream"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { isDynamicToolPart, type DynamicToolPart } from "@/common/types/toolParts"; import { buildWorkflowRunCardMessage, @@ -90,7 +90,7 @@ function initGitRepo(projectPath: string): void { } async function collectFullHistory(service: HistoryService, workspaceId: string) { - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; const result = await service.iterateFullHistory(workspaceId, "forward", (chunk) => { messages.push(...chunk); }); @@ -2667,11 +2667,11 @@ describe("TaskService", () => { test("uncorrelated stream-end before queued workspace turn prompt does not interrupt it", async () => { const { parentId, taskService, historyService, created } = await startWorkspaceTurnForTest(); - const oldAssistant = createMuxMessage("old-assistant", "assistant", "Previous turn", { + const oldAssistant = createXumMessage("old-assistant", "assistant", "Previous turn", { model: "anthropic:claude-opus-4-6", finishReason: "stop", }); - const queuedPrompt = createMuxMessage("queued-prompt", "user", "Queued follow-up", { + const queuedPrompt = createXumMessage("queued-prompt", "user", "Queued follow-up", { muxMetadata: { type: "workspace-turn-task", taskHandleId: created.taskId, @@ -2709,7 +2709,7 @@ describe("TaskService", () => { const { parentId, taskService, historyService, created } = await startWorkspaceTurnForTest(); const appendResult = await historyService.appendToHistory( created.workspaceId, - createMuxMessage("msg_completed", "assistant", "Recovered final text", { + createXumMessage("msg_completed", "assistant", "Recovered final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -2744,7 +2744,7 @@ describe("TaskService", () => { const { parentId, taskService, historyService, created } = await startWorkspaceTurnForTest(); const appendResult = await historyService.appendToHistory( created.workspaceId, - createMuxMessage("msg_truncated_history", "assistant", "Partial text", { + createXumMessage("msg_truncated_history", "assistant", "Partial text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "length", @@ -3134,9 +3134,9 @@ describe("TaskService", () => { const { historyService, taskService } = createTaskServiceHarness(config); await historyService.appendToHistory( parentId, - createMuxMessage("original-user", "user", "Start work", { timestamp: Date.now() }) + createXumMessage("original-user", "user", "Start work", { timestamp: Date.now() }) ); - const reportMessage = createMuxMessage( + const reportMessage = createXumMessage( "terminal-report", "user", formatSubagentReportEnvelope({ @@ -3154,7 +3154,7 @@ describe("TaskService", () => { await historyService.appendToHistory( parentId, - createMuxMessage("stale-assistant", "assistant", "Response to the earlier request", { + createXumMessage("stale-assistant", "assistant", "Response to the earlier request", { timestamp: Date.now(), requestHistorySequence: reportSequence - 1, }) @@ -3167,7 +3167,7 @@ describe("TaskService", () => { await historyService.appendToHistory( parentId, - createMuxMessage("informed-assistant", "assistant", "Response including the report", { + createXumMessage("informed-assistant", "assistant", "Response including the report", { timestamp: Date.now(), requestHistorySequence: reportSequence, }) @@ -3193,7 +3193,7 @@ describe("TaskService", () => { ); const { workspaceService } = createWorkspaceServiceMocks({ resumeStream }); const { historyService, taskService } = createTaskServiceHarness(config, { workspaceService }); - const userMessage = createMuxMessage("user-request", "user", "Start delegated work", { + const userMessage = createXumMessage("user-request", "user", "Start delegated work", { timestamp: Date.now(), }); await historyService.appendToHistory(parentId, userMessage); @@ -3201,14 +3201,14 @@ describe("TaskService", () => { assert(typeof userSequence === "number", "user history sequence is required"); await historyService.appendToHistory( parentId, - createMuxMessage("parent-final", "assistant", "The requested work is complete.", { + createXumMessage("parent-final", "assistant", "The requested work is complete.", { timestamp: Date.now(), requestHistorySequence: userSequence, }) ); await historyService.appendToHistory( parentId, - createMuxMessage( + createXumMessage( "late-terminal-report", "user", formatSubagentReportEnvelope({ @@ -3251,7 +3251,7 @@ describe("TaskService", () => { ); const { workspaceService } = createWorkspaceServiceMocks({ resumeStream }); const { historyService, taskService } = createTaskServiceHarness(config, { workspaceService }); - const userMessage = createMuxMessage("user-before-compact", "user", "Start delegated work", { + const userMessage = createXumMessage("user-before-compact", "user", "Start delegated work", { timestamp: Date.now(), }); await historyService.appendToHistory(parentId, userMessage); @@ -3259,7 +3259,7 @@ describe("TaskService", () => { assert(typeof userSequence === "number", "user history sequence is required"); await historyService.appendToHistory( parentId, - createMuxMessage("compact-output", "assistant", "Compaction summary", { + createXumMessage("compact-output", "assistant", "Compaction summary", { timestamp: Date.now(), agentId: "compact", requestHistorySequence: userSequence, @@ -3267,7 +3267,7 @@ describe("TaskService", () => { ); await historyService.appendToHistory( parentId, - createMuxMessage( + createXumMessage( "terminal-report-after-compact", "user", formatSubagentReportEnvelope({ @@ -3382,7 +3382,7 @@ describe("TaskService", () => { const { historyService, taskService } = createTaskServiceHarness(config, { workspaceService }); await historyService.appendToHistory( parentId, - createMuxMessage( + createXumMessage( "terminal-report", "user", formatSubagentReportEnvelope({ @@ -3456,7 +3456,7 @@ describe("TaskService", () => { }); await historyService.appendToHistory( parentWorkspaceId, - createMuxMessage( + createXumMessage( "continuation-report", "user", formatSubagentReportEnvelope({ @@ -3548,7 +3548,7 @@ describe("TaskService", () => { }); await historyService.appendToHistory( parentWorkspaceId, - createMuxMessage( + createXumMessage( "old-report", "user", formatSubagentReportEnvelope({ @@ -4507,7 +4507,7 @@ describe("TaskService", () => { disposableWorkspace: false, }); - const reportMessage = createMuxMessage( + const reportMessage = createXumMessage( "existing-terminal-report", "user", formatSubagentReportEnvelope({ @@ -4593,7 +4593,7 @@ describe("TaskService", () => { ownerWorkspaceId: parentId, turnId: "turn-previous", }; - const reportMessage = createMuxMessage( + const reportMessage = createXumMessage( "existing-terminal-report-previous-turn", "user", formatSubagentReportEnvelope({ @@ -4763,14 +4763,14 @@ describe("TaskService", () => { const { historyService, taskService } = createTaskServiceHarness(config, { workspaceService }); await historyService.appendToHistory( parentId, - createMuxMessage("pre-compact-exec", "assistant", "Waiting for delegated work", { + createXumMessage("pre-compact-exec", "assistant", "Waiting for delegated work", { timestamp: Date.now(), agentId: "exec", }) ); await historyService.appendToHistory( parentId, - createMuxMessage("bare-compact-output", "assistant", "Compaction summary", { + createXumMessage("bare-compact-output", "assistant", "Compaction summary", { timestamp: Date.now(), agentId: "compact", }) @@ -5185,7 +5185,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prehandoff", "assistant", "Premature final text", { + createXumMessage("msg_prehandoff", "assistant", "Premature final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -5252,7 +5252,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prehandoff", "assistant", "Recovered final text", { + createXumMessage("msg_prehandoff", "assistant", "Recovered final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -5315,7 +5315,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_recovered_list", "assistant", "Recovered list text", { + createXumMessage("msg_recovered_list", "assistant", "Recovered list text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -5382,7 +5382,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_truncated", "assistant", "Partial text", { + createXumMessage("msg_truncated", "assistant", "Partial text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "length", @@ -5664,7 +5664,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prompt", "user", "Summarize", { muxMetadata }) + createXumMessage("msg_prompt", "user", "Summarize", { muxMetadata }) ) ).success ).toBe(true); @@ -5759,7 +5759,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_queue_cut", "assistant", "Cut mid-work", { + createXumMessage("msg_queue_cut", "assistant", "Cut mid-work", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "tool-calls", @@ -5806,7 +5806,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_required_tool_stop", "assistant", "Stopped on required tool", { + createXumMessage("msg_required_tool_stop", "assistant", "Stopped on required tool", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "tool-calls", @@ -5819,7 +5819,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_unrelated_later_input", "user", "Unrelated later question") + createXumMessage("msg_unrelated_later_input", "user", "Unrelated later question") ) ).success ).toBe(true); @@ -5875,7 +5875,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_selfhealed", "assistant", "Self-healed final text", { + createXumMessage("msg_selfhealed", "assistant", "Self-healed final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -5903,7 +5903,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( parentId, - createMuxMessage( + createXumMessage( "stale-direct-parent-failure", "user", [ @@ -5995,7 +5995,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_consumed_repair", "assistant", "Consumed repaired result", { + createXumMessage("msg_consumed_repair", "assistant", "Consumed repaired result", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -6337,7 +6337,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prompt", "user", "Summarize", { muxMetadata }) + createXumMessage("msg_prompt", "user", "Summarize", { muxMetadata }) ); expect(appendResult.success).toBe(true); await new TaskHandleStore(config).upsertWorkspaceTurn({ @@ -6418,7 +6418,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_blocked_final", "assistant", "Blocked final text", { + createXumMessage("msg_blocked_final", "assistant", "Blocked final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -6495,7 +6495,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prompt", "user", "Summarize", { muxMetadata }) + createXumMessage("msg_prompt", "user", "Summarize", { muxMetadata }) ) ).success ).toBe(true); @@ -6543,7 +6543,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prompt", "user", "Summarize", { muxMetadata }) + createXumMessage("msg_prompt", "user", "Summarize", { muxMetadata }) ) ).success ).toBe(true); @@ -6593,7 +6593,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prompt", "user", "Summarize", { muxMetadata }) + createXumMessage("msg_prompt", "user", "Summarize", { muxMetadata }) ) ).success ).toBe(true); @@ -6649,7 +6649,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prompt", "user", "Summarize", { muxMetadata }) + createXumMessage("msg_prompt", "user", "Summarize", { muxMetadata }) ) ).success ).toBe(true); @@ -6699,7 +6699,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prompt", "user", "Summarize", { muxMetadata }) + createXumMessage("msg_prompt", "user", "Summarize", { muxMetadata }) ) ).success ).toBe(true); @@ -6707,7 +6707,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_manual", "user", "Manual follow-up", {}) + createXumMessage("msg_manual", "user", "Manual follow-up", {}) ) ).success ).toBe(true); @@ -6794,7 +6794,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_selfhealed_final", "assistant", "Self-healed final text", { + createXumMessage("msg_selfhealed_final", "assistant", "Self-healed final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -6807,7 +6807,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_manual_later", "user", "Manual follow-up", {}) + createXumMessage("msg_manual_later", "user", "Manual follow-up", {}) ) ).success ).toBe(true); @@ -6859,7 +6859,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_prehandoff", "assistant", "Premature final text", { + createXumMessage("msg_prehandoff", "assistant", "Premature final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -6938,7 +6938,7 @@ describe("TaskService", () => { }; const appendResult = await historyService.appendToHistory( "childworkspace", - createMuxMessage("msg_workflow_blocked", "assistant", "Workflow-blocked final text", { + createXumMessage("msg_workflow_blocked", "assistant", "Workflow-blocked final text", { model: "anthropic:claude-opus-4-6", agentId: "exec", finishReason: "stop", @@ -8101,7 +8101,7 @@ describe("TaskService", () => { const { historyService, taskService } = createTaskServiceHarness(config, { workspaceService }); const appendAcceptedPrompt = await historyService.appendToHistory( acceptedStartingTaskId, - createMuxMessage("accepted-starting-prompt", "user", acceptedPrompt) + createXumMessage("accepted-starting-prompt", "user", acceptedPrompt) ); expect(appendAcceptedPrompt.success).toBe(true); expect(findWorkspaceInConfig(config, queuedTaskId)?.taskPrompt).toBeUndefined(); @@ -10827,7 +10827,7 @@ describe("TaskService", () => { }); const appendManualUser = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("manual-user", "user", "Ignore the old workflow", { timestamp: 2_000 }) + createXumMessage("manual-user", "user", "Ignore the old workflow", { timestamp: 2_000 }) ); expect(appendManualUser.success).toBe(true); @@ -10885,7 +10885,7 @@ describe("TaskService", () => { }); const appendReset = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("reset-boundary", "assistant", "Context reset", { + createXumMessage("reset-boundary", "assistant", "Context reset", { timestamp: 2_000, contextBoundaryKind: "reset", }) @@ -10946,7 +10946,7 @@ describe("TaskService", () => { }); const appendManualUser = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("manual-user", "user", "Ignore the old workflow", { timestamp: 2_000 }) + createXumMessage("manual-user", "user", "Ignore the old workflow", { timestamp: 2_000 }) ); expect(appendManualUser.success).toBe(true); @@ -11003,7 +11003,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage(assistantMessageId, "assistant", "", { timestamp: 1_000 }) + createXumMessage(assistantMessageId, "assistant", "", { timestamp: 1_000 }) ) ).success ).toBe(true); @@ -11011,7 +11011,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("workflow-slash-trigger", "user", "/research new topic", { + createXumMessage("workflow-slash-trigger", "user", "/research new topic", { timestamp: 2_000, }) ) @@ -11080,7 +11080,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("manual-user", "user", "Ignore the old workflow", { timestamp: 1_000 }) + createXumMessage("manual-user", "user", "Ignore the old workflow", { timestamp: 1_000 }) ) ).success ).toBe(true); @@ -11088,7 +11088,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage(assistantMessageId, "assistant", "", { timestamp: 2_000 }) + createXumMessage(assistantMessageId, "assistant", "", { timestamp: 2_000 }) ) ).success ).toBe(true); @@ -11163,7 +11163,7 @@ describe("TaskService", () => { }); const appendManualUser = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("manual-user", "user", "Ignore the old workflow") + createXumMessage("manual-user", "user", "Ignore the old workflow") ); expect(appendManualUser.success).toBe(true); @@ -11221,7 +11221,7 @@ describe("TaskService", () => { }); const appendCompaction = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("midstream-auto-compaction", "user", "Compacting to continue", { + createXumMessage("midstream-auto-compaction", "user", "Compacting to continue", { timestamp: 2_000, synthetic: true, muxMetadata: { @@ -11300,7 +11300,7 @@ describe("TaskService", () => { }); const appendCompaction = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage("auto-compaction", "user", "Compacting before a new user prompt", { + createXumMessage("auto-compaction", "user", "Compacting before a new user prompt", { timestamp: 2_000, synthetic: true, muxMetadata: { @@ -11378,7 +11378,7 @@ describe("TaskService", () => { const appendTaskAwaitDiscovery = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage( + createXumMessage( "assistant-task-await-discovery", "assistant", "", @@ -12149,7 +12149,7 @@ describe("TaskService", () => { const appendResult = await historyService.appendToHistory( rootWorkspaceId, - createMuxMessage( + createXumMessage( "assistant-root-history", "assistant", "Parent is currently running in plan mode.", @@ -12263,7 +12263,7 @@ describe("TaskService", () => { const appendResult = await historyService.appendToHistory( parentWorkspaceId, - createMuxMessage( + createXumMessage( "assistant-parent-history", "assistant", "Parent is currently running in plan mode.", @@ -17458,7 +17458,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-partial", "assistant", "Waiting on subagent…", @@ -17478,13 +17478,13 @@ describe("TaskService", () => { // Seed child history with the initial prompt + assistant placeholder so committing the final // partial updates the existing assistant message (matching real streaming behavior). - const childPrompt = createMuxMessage("user-child-prompt", "user", "do the thing", { + const childPrompt = createXumMessage("user-child-prompt", "user", "do the thing", { timestamp: Date.now(), }); const appendChildPrompt = await historyService.appendToHistory(childId, childPrompt); expect(appendChildPrompt.success).toBe(true); - const childAssistantPlaceholder = createMuxMessage("assistant-child-partial", "assistant", "", { + const childAssistantPlaceholder = createXumMessage("assistant-child-partial", "assistant", "", { timestamp: Date.now(), }); const appendChildPlaceholder = await historyService.appendToHistory( @@ -17498,7 +17498,7 @@ describe("TaskService", () => { throw new Error("Expected child historySequence to be a number"); } - const childPartial = createMuxMessage( + const childPartial = createXumMessage( "assistant-child-partial", "assistant", "", @@ -17652,9 +17652,9 @@ describe("TaskService", () => { legacyVariants?: string[]; timestamp: number; prompt?: string; - additionalParts?: MuxMessage["parts"]; + additionalParts?: XumMessage["parts"]; }): Promise { - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( params.messageId, "assistant", "Waiting on best-of subagents…", @@ -17682,7 +17682,7 @@ describe("TaskService", () => { } function getTaskToolPart( - message: MuxMessage | null + message: XumMessage | null ): (DynamicToolPart & { state: string; output?: unknown }) | undefined { return message?.parts.find((part) => isDynamicToolPart(part) && part.toolName === "task") as | (DynamicToolPart & { state: string; output?: unknown }) @@ -17728,7 +17728,7 @@ describe("TaskService", () => { title: string; prompt?: string; }): Promise { - const childPrompt = createMuxMessage( + const childPrompt = createXumMessage( `user-${params.childId}-prompt`, "user", params.prompt ?? "compare options", @@ -17740,7 +17740,7 @@ describe("TaskService", () => { true ); - const childAssistantPlaceholder = createMuxMessage( + const childAssistantPlaceholder = createXumMessage( `assistant-${params.childId}-partial`, "assistant", "", @@ -17756,7 +17756,7 @@ describe("TaskService", () => { throw new Error("Expected child historySequence to be a number"); } - const childPartial = createMuxMessage( + const childPartial = createXumMessage( `assistant-${params.childId}-partial`, "assistant", "", @@ -18220,7 +18220,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-fallback", "assistant", "Waiting on best-of subagents…", @@ -18253,12 +18253,12 @@ describe("TaskService", () => { ); expect((await partialService.writePartial(parentId, parentPartial)).success).toBe(true); - const childPrompt = createMuxMessage(`user-${childOneId}-prompt`, "user", "compare options", { + const childPrompt = createXumMessage(`user-${childOneId}-prompt`, "user", "compare options", { timestamp: Date.now(), }); expect((await historyService.appendToHistory(childOneId, childPrompt)).success).toBe(true); - const childAssistantPlaceholder = createMuxMessage( + const childAssistantPlaceholder = createXumMessage( `assistant-${childOneId}-partial`, "assistant", "", @@ -18273,7 +18273,7 @@ describe("TaskService", () => { throw new Error("Expected child historySequence to be a number"); } - const childPartial = createMuxMessage( + const childPartial = createXumMessage( `assistant-${childOneId}-partial`, "assistant", "", @@ -18359,7 +18359,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-deferred-fallback", "assistant", "Waiting on best-of subagents…", @@ -18386,12 +18386,12 @@ describe("TaskService", () => { reportMarkdown: string, title: string ): Promise { - const childPrompt = createMuxMessage(`user-${childId}-prompt`, "user", "compare options", { + const childPrompt = createXumMessage(`user-${childId}-prompt`, "user", "compare options", { timestamp: Date.now(), }); expect((await historyService.appendToHistory(childId, childPrompt)).success).toBe(true); - const childAssistantPlaceholder = createMuxMessage( + const childAssistantPlaceholder = createXumMessage( `assistant-${childId}-partial`, "assistant", "", @@ -18406,7 +18406,7 @@ describe("TaskService", () => { throw new Error("Expected child historySequence to be a number"); } - const childPartial = createMuxMessage( + const childPartial = createXumMessage( `assistant-${childId}-partial`, "assistant", "", @@ -18529,7 +18529,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-partial", "assistant", "Waiting on subagent…", @@ -18547,7 +18547,7 @@ describe("TaskService", () => { const writeParentPartial = await partialService.writePartial(parentId, parentPartial); expect(writeParentPartial.success).toBe(true); - const childPartial = createMuxMessage( + const childPartial = createXumMessage( "assistant-child-partial", "assistant", "", @@ -18711,7 +18711,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-partial", "assistant", "Waiting on subagent…", @@ -18728,7 +18728,7 @@ describe("TaskService", () => { ); expect((await partialService.writePartial(parentId, parentPartial)).success).toBe(true); - const childPartial = createMuxMessage( + const childPartial = createXumMessage( "assistant-child-partial", "assistant", "", @@ -18863,7 +18863,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-partial", "assistant", "Waiting on subagent…", @@ -18881,7 +18881,7 @@ describe("TaskService", () => { const writeParentPartial = await partialService.writePartial(parentId, parentPartial); expect(writeParentPartial.success).toBe(true); - const childPartial = createMuxMessage( + const childPartial = createXumMessage( "assistant-child-partial", "assistant", "", @@ -19001,7 +19001,7 @@ describe("TaskService", () => { workspaceService, }); - const parentHistoryMessage = createMuxMessage( + const parentHistoryMessage = createXumMessage( "assistant-parent-history", "assistant", "Spawned subagent…", @@ -19023,7 +19023,7 @@ describe("TaskService", () => { ); expect(appendParentHistory.success).toBe(true); - const childPartial = createMuxMessage( + const childPartial = createXumMessage( "assistant-child-partial", "assistant", "", @@ -19124,7 +19124,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-partial", "assistant", "Waiting on subagent…", @@ -19932,7 +19932,7 @@ describe("TaskService", () => { for (const [index, agentId] of historyAgentIds.entries()) { await historyService.appendToHistory( parentId, - createMuxMessage(`${idSuffix}-assistant-${index}`, "assistant", "Parent turn output", { + createXumMessage(`${idSuffix}-assistant-${index}`, "assistant", "Parent turn output", { timestamp: Date.now(), agentId, }) @@ -20066,7 +20066,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-partial", "assistant", "Waiting on subagent…", @@ -20208,7 +20208,7 @@ describe("TaskService", () => { aiService, workspaceService, }); - const progressMessage = createMuxMessage( + const progressMessage = createXumMessage( "assistant-progress", "assistant", "", @@ -20379,7 +20379,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( childId, - createMuxMessage("assistant-progress", "assistant", "", { timestamp: Date.now() }, [ + createXumMessage("assistant-progress", "assistant", "", { timestamp: Date.now() }, [ { type: "dynamic-tool", toolCallId: "agent-report-old", @@ -20651,7 +20651,7 @@ describe("TaskService", () => { workspaceService, }); for (const message of [ - createMuxMessage("assistant-old-progress", "assistant", "", { timestamp: Date.now() - 2 }, [ + createXumMessage("assistant-old-progress", "assistant", "", { timestamp: Date.now() - 2 }, [ { type: "dynamic-tool", toolCallId: "agent-report-old", @@ -20661,7 +20661,7 @@ describe("TaskService", () => { output: { success: true }, }, ]), - createMuxMessage( + createXumMessage( "assistant-failed-progress", "assistant", "", @@ -21203,7 +21203,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-partial", "assistant", "Waiting on subagent…", @@ -21420,7 +21420,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-pending-group-target", "assistant", "Waiting on best-of subagents…", @@ -21538,7 +21538,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-stale-single-group", "assistant", "Waiting on best-of subagents…", @@ -21656,7 +21656,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-finalize-ready", "assistant", "Waiting on best-of subagents…", @@ -21796,7 +21796,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-concurrent-deferred-fallback", "assistant", "Waiting on best-of subagents…", @@ -21918,7 +21918,7 @@ describe("TaskService", () => { // An output written by a newer release: extra fields fail the strict result schema, but // the referenced-task bookkeeping must still see these IDs or recovery would append a // duplicate fallback report after a downgrade. - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-future-fields-fallback", "assistant", "Waiting on best-of subagents…", @@ -22023,7 +22023,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( parentId, - createMuxMessage( + createXumMessage( "progress-report", "user", formatSubagentReportEnvelope({ @@ -22108,7 +22108,7 @@ describe("TaskService", () => { ( await historyService.appendToHistory( parentId, - createMuxMessage( + createXumMessage( "completed-report", "user", formatSubagentReportEnvelope({ @@ -22195,7 +22195,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-concurrent-direct-fallback", "assistant", "Waiting on best-of subagents…", @@ -22322,7 +22322,7 @@ describe("TaskService", () => { workspaceService, }); - const parentPartial = createMuxMessage( + const parentPartial = createXumMessage( "assistant-parent-best-of-initialize-finalize-ready", "assistant", "Waiting on best-of subagents…", diff --git a/src/node/services/taskService.ts b/src/node/services/taskService.ts index a519ae0993..6fd35c6a4a 100644 --- a/src/node/services/taskService.ts +++ b/src/node/services/taskService.ts @@ -61,7 +61,7 @@ import { type BackgroundWorkAttentionPolicy, } from "@/common/types/backgroundWorkAttention"; -import { createMuxMessage, type MuxMessage, type MuxMessageMetadata } from "@/common/types/message"; +import { createXumMessage, type XumMessage, type XumMessageMetadata } from "@/common/types/message"; import { createCompactionSummaryMessageId, createTaskFailureMessageId, @@ -526,7 +526,7 @@ export interface WorkspaceTurnWaitResult { finalMessageRef?: WorkspaceTurnFinalMessageRef; } -type WorkspaceTurnMuxMetadata = Extract; +type WorkspaceTurnXumMetadata = Extract; interface BackgroundableForegroundWaiter { taskId: string; @@ -1099,7 +1099,7 @@ function collectAgentReferencedWorkflowRunIdsFromParts( return Array.from(runIds); } -function isInternalResumeAutoCompactionMessage(message: MuxMessage): boolean { +function isInternalResumeAutoCompactionMessage(message: XumMessage): boolean { const muxMetadata = message.metadata?.muxMetadata; if (muxMetadata?.type !== "compaction-request" || muxMetadata.source !== "auto-compaction") { return false; @@ -1107,7 +1107,7 @@ function isInternalResumeAutoCompactionMessage(message: MuxMessage): boolean { return muxMetadata.parsed.followUpContent?.dispatchOptions?.source === "internal-resume"; } -function isSyntheticManualSupersessionMessage(message: MuxMessage): boolean { +function isSyntheticManualSupersessionMessage(message: XumMessage): boolean { const muxMetadata = message.metadata?.muxMetadata; return ( message.metadata?.synthetic === true && @@ -1117,18 +1117,18 @@ function isSyntheticManualSupersessionMessage(message: MuxMessage): boolean { ); } -function isManualUserSupersessionMessage(message: MuxMessage): boolean { +function isManualUserSupersessionMessage(message: XumMessage): boolean { return ( message.role === "user" && (message.metadata?.synthetic !== true || isSyntheticManualSupersessionMessage(message)) ); } -function isResetBoundaryMessage(message: MuxMessage): boolean { +function isResetBoundaryMessage(message: XumMessage): boolean { return message.metadata?.contextBoundaryKind === CONTEXT_BOUNDARY_KINDS.RESET; } -function isWorkflowSupersessionMessage(message: MuxMessage): boolean { +function isWorkflowSupersessionMessage(message: XumMessage): boolean { return isManualUserSupersessionMessage(message) || isResetBoundaryMessage(message); } @@ -1397,7 +1397,7 @@ export class TaskService { const latestSupersession = await this.findLatestWorkflowSupersession(workspaceId); const historyResult = await this.historyService.getHistoryFromLatestBoundary(workspaceId); - let historyMessages: MuxMessage[] = []; + let historyMessages: XumMessage[] = []; let historyScanStartIndex = 0; let trustCurrentParts = true; if (historyResult.success) { @@ -3801,7 +3801,7 @@ export class TaskService { agentId: workspaceTurnAgentId, ...(thinkingLevel != null ? { thinkingLevel } : {}), ...(reasoningMode != null ? { reasoningMode } : {}), - muxMetadata: this.buildWorkspaceTurnMuxMetadata(record), + muxMetadata: this.buildWorkspaceTurnXumMetadata(record), experiments: args.experiments ?? targetTaskExperiments, ...(mode === "existing" ? { queueDispatchMode } : {}), }, @@ -5996,7 +5996,7 @@ export class TaskService { if (!historyResult.success) { return { deliverableNotificationIds, latestMessageTimestampByTaskId }; } - const existingReportMessages = new Map(); + const existingReportMessages = new Map(); const existingTaskIds = new Set(); for (const message of historyResult.data) { if (message.role !== "user" || message.metadata?.synthetic !== true) continue; @@ -6018,32 +6018,32 @@ export class TaskService { } } - const workspaceTurnMuxMetadata = - await this.getActiveWorkspaceTurnMuxMetadataForWorkspace(ownerWorkspaceId); + const workspaceTurnXumMetadata = + await this.getActiveWorkspaceTurnXumMetadataForWorkspace(ownerWorkspaceId); const sessionDir = this.config.getSessionDir(ownerWorkspaceId); for (const notification of notifications) { if (existingTaskIds.has(notification.sourceId)) { const existingMessage = existingReportMessages.get(notification.sourceId); - if (existingMessage != null && workspaceTurnMuxMetadata != null) { + if (existingMessage != null && workspaceTurnXumMetadata != null) { const existingCorrelation = this.getWorkspaceTurnMetadataFromValue( existingMessage.metadata?.muxMetadata ); if (existingCorrelation != null) { const matchesActiveTurn = - existingCorrelation.taskHandleId === workspaceTurnMuxMetadata.taskHandleId && - existingCorrelation.ownerWorkspaceId === workspaceTurnMuxMetadata.ownerWorkspaceId && - existingCorrelation.turnId === workspaceTurnMuxMetadata.turnId; + existingCorrelation.taskHandleId === workspaceTurnXumMetadata.taskHandleId && + existingCorrelation.ownerWorkspaceId === workspaceTurnXumMetadata.ownerWorkspaceId && + existingCorrelation.turnId === workspaceTurnXumMetadata.turnId; if (!matchesActiveTurn) { // Keep the old report visible, but do not let it wake or settle a later turn. await this.terminalAttentionStore.markSuperseded(ownerWorkspaceId, notification.id); continue; } } else { - const updatedMessage: MuxMessage = { + const updatedMessage: XumMessage = { ...existingMessage, metadata: { ...existingMessage.metadata, - muxMetadata: workspaceTurnMuxMetadata, + muxMetadata: workspaceTurnXumMetadata, }, }; const updateResult = await this.historyService.updateHistory( @@ -6109,7 +6109,7 @@ export class TaskService { } const timestamp = Date.now(); - const message = createMuxMessage( + const message = createXumMessage( report != null ? createTaskReportMessageId() : createTaskFailureMessageId(), "user", content, @@ -6117,7 +6117,7 @@ export class TaskService { timestamp, synthetic: true, uiVisible: true, - ...(workspaceTurnMuxMetadata != null ? { muxMetadata: workspaceTurnMuxMetadata } : {}), + ...(workspaceTurnXumMetadata != null ? { muxMetadata: workspaceTurnXumMetadata } : {}), } ); const appendResult = await this.historyService.appendToHistory(ownerWorkspaceId, message); @@ -6335,15 +6335,15 @@ export class TaskService { entry, defaultModel ); - const workspaceTurnMuxMetadata = - await this.getActiveWorkspaceTurnMuxMetadataForWorkspace(ownerWorkspaceId); + const workspaceTurnXumMetadata = + await this.getActiveWorkspaceTurnXumMetadataForWorkspace(ownerWorkspaceId); const sendOptions = { model: resumeOptions.model, agentId: resumeOptions.agentId, thinkingLevel: resumeOptions.thinkingLevel, reasoningMode: resumeOptions.reasoningMode, - ...(workspaceTurnMuxMetadata != null ? { muxMetadata: workspaceTurnMuxMetadata } : {}), + ...(workspaceTurnXumMetadata != null ? { muxMetadata: workspaceTurnXumMetadata } : {}), }; if (prompt.length === 0) { assert(agentNotifications.length > 0, "prompt-free terminal drain requires sub-agent work"); @@ -6760,7 +6760,7 @@ export class TaskService { errorMessage: record.error ?? "Workspace turn failed", }); if (!alreadyDelivered) { - const message = createMuxMessage( + const message = createXumMessage( record.status === "completed" ? createTaskReportMessageId() : createTaskFailureMessageId(), "user", content, @@ -7264,8 +7264,8 @@ export class TaskService { parentEntry, defaultModel ); - const workspaceTurnMuxMetadata = - await this.getActiveWorkspaceTurnMuxMetadataForWorkspace(parentWorkspaceId); + const workspaceTurnXumMetadata = + await this.getActiveWorkspaceTurnXumMetadataForWorkspace(parentWorkspaceId); // A progress report is itself the wake-up message. Unlike terminal attention, it must be // allowed through while this child is still active so review findings and other incremental @@ -7278,22 +7278,22 @@ export class TaskService { agentId: resumeOptions.agentId, thinkingLevel: resumeOptions.thinkingLevel, reasoningMode: resumeOptions.reasoningMode, - ...(workspaceTurnMuxMetadata != null ? { muxMetadata: workspaceTurnMuxMetadata } : {}), + ...(workspaceTurnXumMetadata != null ? { muxMetadata: workspaceTurnXumMetadata } : {}), }, { skipAutoResumeReset: true, synthetic: true, agentInitiated: true, startStreamInBackground: true, - workspaceTurnContinuation: workspaceTurnMuxMetadata != null, + workspaceTurnContinuation: workspaceTurnXumMetadata != null, queueDedupeKey: `agent-report:${childWorkspaceId}:${toolCallId}`, removableQueueDedupeKey: true, - ...(workspaceTurnMuxMetadata != null + ...(workspaceTurnXumMetadata != null ? { onCanceled: async (reason: string) => { await this.settleWorkspaceTurnContinuationFailure( parentWorkspaceId, - workspaceTurnMuxMetadata, + workspaceTurnXumMetadata, "interrupted", reason ); @@ -7301,7 +7301,7 @@ export class TaskService { onAcceptedPreStreamFailure: async (error: SendMessageError) => { await this.settleWorkspaceTurnContinuationFailure( parentWorkspaceId, - workspaceTurnMuxMetadata, + workspaceTurnXumMetadata, "error", formatSendMessageError(error).message ); @@ -7312,10 +7312,10 @@ export class TaskService { ); if (!sendResult.success) { const formattedError = formatSendMessageError(sendResult.error); - if (workspaceTurnMuxMetadata != null) { + if (workspaceTurnXumMetadata != null) { await this.settleWorkspaceTurnContinuationFailure( parentWorkspaceId, - workspaceTurnMuxMetadata, + workspaceTurnXumMetadata, "error", formattedError.message ); @@ -10185,9 +10185,9 @@ export class TaskService { return true; } - private buildWorkspaceTurnMuxMetadata( + private buildWorkspaceTurnXumMetadata( record: Pick - ): WorkspaceTurnMuxMetadata { + ): WorkspaceTurnXumMetadata { return { type: "workspace-turn-task", taskHandleId: record.handleId, @@ -10278,7 +10278,7 @@ export class TaskService { private buildWorkspaceTurnStreamEndEventFromHistory( record: WorkspaceTurnTaskHandleRecord, - message: MuxMessage + message: XumMessage ): StreamEndEvent | null { if (message.role !== "assistant" || message.metadata?.partial === true) { return null; @@ -10435,9 +10435,9 @@ export class TaskService { }); } - private resolveWorkspaceTurnMuxMetadataForStreamEnd( + private resolveWorkspaceTurnXumMetadataForStreamEnd( event: StreamEndEvent - ): WorkspaceTurnMuxMetadata | undefined { + ): WorkspaceTurnXumMetadata | undefined { const metadata = this.getWorkspaceTurnMetadata(event); if (metadata == null) { return undefined; @@ -10848,13 +10848,13 @@ export class TaskService { taskIds: blockingTaskIds, workflowRunIds: activeWorkflowRunIds, }); - const workspaceTurnMuxMetadata = this.resolveWorkspaceTurnMuxMetadataForStreamEnd(event); + const workspaceTurnXumMetadata = this.resolveWorkspaceTurnXumMetadataForStreamEnd(event); const sendOptions = { model: resumeOptions.model, agentId: resumeOptions.agentId, thinkingLevel: resumeOptions.thinkingLevel, reasoningMode: resumeOptions.reasoningMode, - ...(workspaceTurnMuxMetadata != null ? { muxMetadata: workspaceTurnMuxMetadata } : {}), + ...(workspaceTurnXumMetadata != null ? { muxMetadata: workspaceTurnXumMetadata } : {}), }; let sendResult = await this.workspaceService.sendMessage( workspaceId, @@ -11125,9 +11125,9 @@ export class TaskService { return records.toReversed().find((record) => record.workspaceId === workspaceId) ?? null; } - private async getActiveWorkspaceTurnMuxMetadataForWorkspace( + private async getActiveWorkspaceTurnXumMetadataForWorkspace( workspaceId: string - ): Promise { + ): Promise { const candidate = await this.getActiveWorkspaceTurnRecordForWorkspace(workspaceId); if (candidate == null) { return undefined; @@ -11152,7 +11152,7 @@ export class TaskService { return undefined; } - return this.buildWorkspaceTurnMuxMetadata(current); + return this.buildWorkspaceTurnXumMetadata(current); }); } @@ -11160,7 +11160,7 @@ export class TaskService { // exact turn here because no replacement stream-end can arrive. private async settleWorkspaceTurnContinuationFailure( workspaceId: string, - muxMetadata: WorkspaceTurnMuxMetadata, + muxMetadata: WorkspaceTurnXumMetadata, status: "interrupted" | "error", error: string ): Promise { @@ -11516,7 +11516,7 @@ export class TaskService { // Durable context delivery, mirroring deliverReportToParent's synthetic // append: the failure must be visible to the parent's next turn regardless // of whether this settlement resumes it or a later sibling report/failure does. - const failureMessage = createMuxMessage( + const failureMessage = createXumMessage( createTaskFailureMessageId(), "user", formatSubagentFailureUserMessage({ @@ -11779,7 +11779,7 @@ export class TaskService { ? `# Plan\n\n${planSummary.content}\n\nNote: This chat already contains the full plan; no need to re-open the plan file.\n\n---\n\n*Plan file preserved at:* \`${planSummary.path}\`` : `A plan was proposed at ${args.proposePlanResult.planPath}. Read the plan file and implement it.`; - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( createCompactionSummaryMessageId(), "assistant", summaryContent, @@ -13225,14 +13225,14 @@ export class TaskService { : {}), }); - const workspaceTurnMuxMetadata = - await this.getActiveWorkspaceTurnMuxMetadataForWorkspace(parentWorkspaceId); + const workspaceTurnXumMetadata = + await this.getActiveWorkspaceTurnXumMetadataForWorkspace(parentWorkspaceId); const messageId = createTaskReportMessageId(); - const reportMessage = createMuxMessage(messageId, "user", reportContent, { + const reportMessage = createXumMessage(messageId, "user", reportContent, { timestamp: Date.now(), synthetic: true, uiVisible: true, - ...(workspaceTurnMuxMetadata != null ? { muxMetadata: workspaceTurnMuxMetadata } : {}), + ...(workspaceTurnXumMetadata != null ? { muxMetadata: workspaceTurnXumMetadata } : {}), }); const appendResult = await this.historyService.appendToHistory( @@ -13330,7 +13330,7 @@ export class TaskService { } } - const updated: MuxMessage = { + const updated: XumMessage = { ...partial, parts: partial.parts.map((part) => { if (!isDynamicToolPart(part)) return part; diff --git a/src/node/services/timelineMapper.ts b/src/node/services/timelineMapper.ts index 7720f12ba1..dbf040864f 100644 --- a/src/node/services/timelineMapper.ts +++ b/src/node/services/timelineMapper.ts @@ -86,7 +86,7 @@ const MACHINE_AUTHORED_TURN_TYPES = new Set([ ]); // muxMetadata crosses the oRPC boundary as `any`, so read its string fields defensively. -function readMuxMetadataField( +function readXumMetadataField( metadata: Extract["metadata"], field: "type" | "source" | "runId" ): string | undefined { @@ -104,7 +104,7 @@ function isMachineAuthoredTurn( if (metadata?.synthetic === true) { return true; } - const muxType = readMuxMetadataField(metadata, "type"); + const muxType = readXumMetadataField(metadata, "type"); return muxType != null && MACHINE_AUTHORED_TURN_TYPES.has(muxType); } @@ -128,7 +128,7 @@ function isUnloggedMachineTurn( if (metadata.kind === GOAL_CONTINUATION_KIND || metadata.kind === GOAL_BUDGET_LIMIT_KIND) { return true; } - const muxType = readMuxMetadataField(metadata, "type"); + const muxType = readXumMetadataField(metadata, "type"); return muxType === "heartbeat-request" || muxType === "goal-pause-boundary"; } @@ -175,7 +175,7 @@ function classifyMachineTurn( event: Extract, text: string ): MachineTurnRow | null { - const muxType = readMuxMetadataField(event.metadata, "type"); + const muxType = readXumMetadataField(event.metadata, "type"); if (muxType === "bash-monitor-wake") { const processes = readMonitorWakeProcesses(event.metadata); return { @@ -184,7 +184,7 @@ function classifyMachineTurn( }; } if (isWorkflowResultMessage(event)) { - const runId = readMuxMetadataField(event.metadata, "runId"); + const runId = readXumMetadataField(event.metadata, "runId"); return { kind: "workflow.result", status: "completed", @@ -235,8 +235,8 @@ function mapMessage( if (event.role === "user") { // A /compact request is persisted as a user message, but it is Xum asking for a summary, not a // prompt the human wrote, so it must not appear among their prompts. - if (readMuxMetadataField(event.metadata, "type") === "compaction-request") { - const compactionSource = readMuxMetadataField(event.metadata, "source"); + if (readXumMetadataField(event.metadata, "type") === "compaction-request") { + const compactionSource = readXumMetadataField(event.metadata, "source"); return [ { ts, diff --git a/src/node/services/timelineService.test.ts b/src/node/services/timelineService.test.ts index 6c32f2f77a..dfb08eb837 100644 --- a/src/node/services/timelineService.test.ts +++ b/src/node/services/timelineService.test.ts @@ -8,7 +8,7 @@ import { type TimelineEvent, type TimelineEventDraft, } from "@/common/orpc/schemas/timeline"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { createTestHistoryService } from "@/node/services/testHistoryService"; import { TimelineService } from "./timelineService"; @@ -26,8 +26,8 @@ function message( id: string, role: "user" | "assistant", text: string, - metadata: MuxMessage["metadata"] = {} -): MuxMessage { + metadata: XumMessage["metadata"] = {} +): XumMessage { return { id, role, diff --git a/src/node/services/timelineService.ts b/src/node/services/timelineService.ts index 6a49eb3bb3..e953c23d05 100644 --- a/src/node/services/timelineService.ts +++ b/src/node/services/timelineService.ts @@ -21,7 +21,7 @@ import { type TimelinePreview, } from "@/common/orpc/schemas/timeline"; import type { WorkspaceChatMessage } from "@/common/orpc/types"; -import type { MuxMessage, MuxToolPart } from "@/common/types/message"; +import type { XumMessage, XumToolPart } from "@/common/types/message"; import type { Config } from "@/node/config"; import type { ExperimentsService } from "@/node/services/experimentsService"; import type { HistoryService } from "@/node/services/historyService"; @@ -453,7 +453,7 @@ export class TimelineService implements TimelineRecorder { } } - private matchesMessageAnchor(message: MuxMessage, anchor: TimelineAnchor): boolean { + private matchesMessageAnchor(message: XumMessage, anchor: TimelineAnchor): boolean { if (anchor.messageId != null && message.id !== anchor.messageId) { return false; } @@ -471,10 +471,10 @@ export class TimelineService implements TimelineRecorder { return anchor.messageId != null || anchor.historySequence != null; } - private createPreview(message: MuxMessage, anchor: TimelineAnchor): TimelinePreview | null { + private createPreview(message: XumMessage, anchor: TimelineAnchor): TimelinePreview | null { if (anchor.toolCallId != null) { const toolPart = message.parts.find( - (part): part is MuxToolPart => + (part): part is XumToolPart => part.type === "dynamic-tool" && part.toolCallId === anchor.toolCallId ); if (toolPart == null) { @@ -498,7 +498,7 @@ export class TimelineService implements TimelineRecorder { // Serialized tool payloads are unreadable in a preview card, so surface the human-readable // field the call was built around (a task title, a prompt) and show nothing otherwise. - private toolPartText(part: MuxToolPart): string { + private toolPartText(part: XumToolPart): string { const fromInput = readPreviewText(part.input); if (fromInput !== "") { return fromInput; diff --git a/src/node/services/tokenizerService.test.ts b/src/node/services/tokenizerService.test.ts index f107943c61..d721282208 100644 --- a/src/node/services/tokenizerService.test.ts +++ b/src/node/services/tokenizerService.test.ts @@ -4,7 +4,7 @@ import type { SessionUsageService } from "./sessionUsageService"; import * as tokenizerUtils from "@/node/utils/main/tokenizer"; import * as statsUtils from "@/common/utils/tokens/tokenStatsCalculator"; import { CONTEXT_BOUNDARY_KINDS } from "@/common/constants/contextBoundary"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; const GLOBAL_WORKSPACE_ID = "workspace-global"; describe("TokenizerService", () => { @@ -57,8 +57,8 @@ describe("TokenizerService", () => { describe("calculateStats", () => { test("delegates to underlying function and persists token stats cache", async () => { const messages = [ - createMuxMessage("msg1", "user", "Hello", { historySequence: 1 }), - createMuxMessage("msg2", "assistant", "World", { historySequence: 2 }), + createXumMessage("msg1", "user", "Hello", { historySequence: 1 }), + createXumMessage("msg2", "assistant", "World", { historySequence: 2 }), ]; const mockResult = { @@ -103,13 +103,13 @@ describe("TokenizerService", () => { }); test("excludes a leading reset boundary from token stats", async () => { - const resetBoundary = createMuxMessage("reset", "assistant", "", { + const resetBoundary = createXumMessage("reset", "assistant", "", { historySequence: 2, contextBoundaryKind: CONTEXT_BOUNDARY_KINDS.RESET, }); const messages = [ resetBoundary, - createMuxMessage("msg1", "user", "Hello", { historySequence: 3 }), + createXumMessage("msg1", "user", "Hello", { historySequence: 3 }), ]; const mockResult = { consumers: [{ name: "User", tokens: 1, percentage: 100 }], @@ -139,7 +139,7 @@ describe("TokenizerService", () => { }); test("passes tool availability options to calculateTokenStats", async () => { - const messages = [createMuxMessage("msg1", "user", "Hello")]; + const messages = [createXumMessage("msg1", "user", "Hello")]; const mockResult = { consumers: [{ name: "User", tokens: 1, percentage: 100 }], totalTokens: 1, @@ -170,7 +170,7 @@ describe("TokenizerService", () => { }); test("passes enableAgentReport true when parentWorkspaceId is provided", async () => { - const messages = [createMuxMessage("msg1", "user", "Hello")]; + const messages = [createXumMessage("msg1", "user", "Hello")]; const mockResult = { consumers: [{ name: "User", tokens: 1, percentage: 100 }], totalTokens: 1, @@ -193,13 +193,13 @@ describe("TokenizerService", () => { test("skips persisting stale token stats cache when calculations overlap", async () => { const messagesV1 = [ - createMuxMessage("msg1", "user", "Hello", { historySequence: 1 }), - createMuxMessage("msg2", "assistant", "World", { historySequence: 2 }), + createXumMessage("msg1", "user", "Hello", { historySequence: 1 }), + createXumMessage("msg2", "assistant", "World", { historySequence: 2 }), ]; const messagesV2 = [ ...messagesV1, - createMuxMessage("msg3", "assistant", "!!!", { historySequence: 3 }), + createXumMessage("msg3", "assistant", "!!!", { historySequence: 3 }), ]; const deferred = () => { diff --git a/src/node/services/tokenizerService.ts b/src/node/services/tokenizerService.ts index 40e23de0f3..384ff1bebf 100644 --- a/src/node/services/tokenizerService.ts +++ b/src/node/services/tokenizerService.ts @@ -1,6 +1,6 @@ import { countTokens, countTokensBatch } from "@/node/utils/main/tokenizer"; import { calculateTokenStats } from "@/common/utils/tokens/tokenStatsCalculator"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { ChatStats } from "@/common/types/chatStats"; import type { ProvidersConfigMap } from "@/common/orpc/types"; import assert from "@/common/utils/assert"; @@ -10,7 +10,7 @@ import { sliceMessagesForProviderFromLatestContextBoundary } from "@/common/util import type { SessionUsageService, SessionUsageTokenStatsCacheV1 } from "./sessionUsageService"; import { log } from "./log"; -function getMaxHistorySequence(messages: MuxMessage[]): number | undefined { +function getMaxHistorySequence(messages: XumMessage[]): number | undefined { let max: number | undefined; for (const message of messages) { const seq = message.metadata?.historySequence; @@ -66,7 +66,7 @@ export class TokenizerService { */ async calculateStats( workspaceId: string, - messages: MuxMessage[], + messages: XumMessage[], model: string, providersConfig: ProvidersConfigMap | null = null, parentWorkspaceId: string | null = null diff --git a/src/node/services/turnEnvelope.test.ts b/src/node/services/turnEnvelope.test.ts index 0c03ce4a58..60dad1ceab 100644 --- a/src/node/services/turnEnvelope.test.ts +++ b/src/node/services/turnEnvelope.test.ts @@ -10,7 +10,7 @@ import { type Tool, } from "ai"; import { z } from "zod"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { sanitizeToolSchemaForOpenAI } from "@/common/utils/tools/schemaSanitizer"; import { DisposableTempDir } from "@/node/services/tempDir"; import { @@ -247,7 +247,7 @@ describe("emitTurnEnvelope", () => { const journal = new DurableEventJournal(tmp.path); // Refusal-fallback continuation: never persisted to chat.jsonl at the // request's sequence, so the envelope's blob is replay's only source. - const continuation = createMuxMessage( + const continuation = createXumMessage( "assistant-partial-1", "assistant", "partial output before refusal", diff --git a/src/node/services/turnEnvelope.ts b/src/node/services/turnEnvelope.ts index cf0b049841..25f81f87e4 100644 --- a/src/node/services/turnEnvelope.ts +++ b/src/node/services/turnEnvelope.ts @@ -11,7 +11,7 @@ import crypto from "node:crypto"; import { asSchema, type FlexibleSchema, type Tool } from "ai"; import type { PostCompactionAttachment } from "@/common/types/attachment"; import type { BlobRef } from "@/common/types/durableEvent"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { stableStringify } from "@/common/utils/stableStringify"; import { log } from "@/node/services/log"; import type { DurableEventJournal } from "@/node/utils/journal/durableEventJournal"; @@ -165,7 +165,7 @@ export async function emitTurnEnvelope(params: { * Partial-output continuation a refusal fallback appended to its request * (model-visible but never persisted to chat.jsonl at this sequence). */ - partialContinuationMessage?: MuxMessage | null; + partialContinuationMessage?: XumMessage | null; }): Promise { try { // Content-addressed: unchanged prompts across turns dedupe to one blob. diff --git a/src/node/services/utils/fileChangeTracker.ts b/src/node/services/utils/fileChangeTracker.ts index f4457fbf0a..21e7e52f35 100644 --- a/src/node/services/utils/fileChangeTracker.ts +++ b/src/node/services/utils/fileChangeTracker.ts @@ -1,7 +1,7 @@ import { stat, readFile, realpath } from "fs/promises"; import assert from "@/common/utils/assert"; import { computeDiff } from "@/node/utils/diff"; -import { createMuxMessage, type MuxMessage } from "@/common/types/message"; +import { createXumMessage, type XumMessage } from "@/common/types/message"; import { createFileChangeNotificationMessageId } from "@/node/services/utils/messageIds"; /** @@ -52,7 +52,7 @@ interface DetectedChange { */ export function createFileChangeNotificationMessage( changedFileAttachments: EditedFileAttachment[] -): MuxMessage { +): XumMessage { assert(changedFileAttachments.length > 0, "file change notification requires attachments"); const notice = changedFileAttachments @@ -64,7 +64,7 @@ export function createFileChangeNotificationMessage( ) .join("\n\n"); - return createMuxMessage( + return createXumMessage( createFileChangeNotificationMessageId(), "user", `\n${notice}\n`, diff --git a/src/node/services/voiceService.ts b/src/node/services/voiceService.ts index 7069e99a80..4289df02cc 100644 --- a/src/node/services/voiceService.ts +++ b/src/node/services/voiceService.ts @@ -20,7 +20,7 @@ interface OpenAITranscriptionConfig { enabled?: unknown; } -interface MuxGatewayTranscriptionConfig { +interface XumGatewayTranscriptionConfig { couponCode?: string; voucher?: string; baseUrl?: string; @@ -47,7 +47,7 @@ export class VoiceService { try { const providersConfig = this.config.loadProvidersConfig() ?? {}; const gatewayConfig = providersConfig["mux-gateway"] as - | MuxGatewayTranscriptionConfig + | XumGatewayTranscriptionConfig | undefined; const openaiConfig = providersConfig.openai as OpenAITranscriptionConfig | undefined; const mainConfig = this.config.loadConfigOrDefault(); @@ -142,7 +142,7 @@ export class VoiceService { private async transcribeWithGateway( audioBase64: string, couponCode: string, - gatewayConfig: MuxGatewayTranscriptionConfig | undefined + gatewayConfig: XumGatewayTranscriptionConfig | undefined ): Promise> { const forcedBaseUrl = this.policyService?.getForcedBaseUrl("mux-gateway"); const gatewayBase = this.resolveGatewayBase( @@ -157,7 +157,7 @@ export class VoiceService { }); if (response.status === 401) { - await this.clearMuxGatewayCredentials(); + await this.clearXumGatewayCredentials(); return { success: false, error: "You've been logged out of Xum Gateway. Please login again to use voice input.", @@ -261,7 +261,7 @@ export class VoiceService { return errorMessage; } - private async clearMuxGatewayCredentials(): Promise { + private async clearXumGatewayCredentials(): Promise { if (!this.providerService) { return; } diff --git a/src/node/services/workspaceGoalService.test.ts b/src/node/services/workspaceGoalService.test.ts index 622354268a..5fcbfb01e2 100644 --- a/src/node/services/workspaceGoalService.test.ts +++ b/src/node/services/workspaceGoalService.test.ts @@ -14,7 +14,7 @@ import { GOAL_CONTINUATION_IDLE_CONSUMER_NAME, GOAL_CONTINUATION_KIND, } from "@/constants/goals"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; // Shared dispatch helpers live in `./testDispatchHelpers` instead of local // copies so future callers cannot drift. import { drainPendingDispatches, waitForCondition } from "./testDispatchHelpers"; @@ -43,11 +43,11 @@ async function appendUserHistoryMessage( historyService: HistoryService, workspaceId: string, text: string, - metadata: Parameters[3] = { timestamp: Date.now() } + metadata: Parameters[3] = { timestamp: Date.now() } ): Promise { const result = await historyService.appendToHistory( workspaceId, - createMuxMessage(`goal-test-user-${crypto.randomUUID()}`, "user", text, metadata) + createXumMessage(`goal-test-user-${crypto.randomUUID()}`, "user", text, metadata) ); expect(result.success).toBe(true); } @@ -709,7 +709,7 @@ describe("WorkspaceGoalService", () => { test("model-created goals stay active and arm kickoff after a normal user turn", async () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-set-goal-request", "user", "Set yourself a goal and continue", { + createXumMessage("user-set-goal-request", "user", "Set yourself a goal and continue", { timestamp: Date.now(), }) ); @@ -722,7 +722,7 @@ describe("WorkspaceGoalService", () => { executed.push(input); const continuationAppend = await historyService.appendToHistory( workspaceId, - createMuxMessage("model-created-goal-continuation", "user", input.message, { + createXumMessage("model-created-goal-continuation", "user", input.message, { timestamp: Date.now(), kind: GOAL_CONTINUATION_KIND, }) @@ -752,7 +752,7 @@ describe("WorkspaceGoalService", () => { test("preserves model-created kickoff candidate when stream-end continuation is requested", async () => { const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("user-set-goal-stream", "user", "Set yourself a goal and continue", { + createXumMessage("user-set-goal-stream", "user", "Set yourself a goal and continue", { timestamp: Date.now(), }) ); @@ -766,7 +766,7 @@ describe("WorkspaceGoalService", () => { executed.push(input); const continuationAppend = await historyService.appendToHistory( workspaceId, - createMuxMessage("preserved-kickoff-continuation", "user", input.message, { + createXumMessage("preserved-kickoff-continuation", "user", input.message, { timestamp: Date.now(), kind: GOAL_CONTINUATION_KIND, }) diff --git a/src/node/services/workspaceGoalService.ts b/src/node/services/workspaceGoalService.ts index 530aa1eb46..e3461711f4 100644 --- a/src/node/services/workspaceGoalService.ts +++ b/src/node/services/workspaceGoalService.ts @@ -23,7 +23,7 @@ import { } from "@/common/orpc/schemas/goal"; import type { GoalBoardEntry, GoalBoardSnapshot, GoalBoardV1 } from "@/common/types/goal"; import { - createMuxMessage, + createXumMessage, isSyntheticSnapshotUserMessage, pickStartupRetrySendOptions, } from "@/common/types/message"; @@ -591,7 +591,7 @@ export class WorkspaceGoalService { // declarative state model as Resume without rewriting prior continuation // history. The row is model-visible but not rendered unless synthetic debug // messages are enabled, matching other context-only system breadcrumbs. - const message = createMuxMessage( + const message = createXumMessage( `goal-paused-${Date.now()}-${crypto.randomUUID()}`, "user", "Goal paused by the user. Do not continue the goal until a later goal continuation message.", @@ -2868,7 +2868,7 @@ export class WorkspaceGoalService { // in the AI request payload but hides it from the rendered transcript, // which is what we want here. const summary = `Goal cleared: "${goal.objective}" — spent $${formatCentsBare(goal.costCents)} over ${goal.turnsUsed} turns (status: ${goal.status})`; - const message = createMuxMessage( + const message = createXumMessage( `goal-cleared-${Date.now()}-${crypto.randomUUID()}`, "assistant", summary, diff --git a/src/node/services/workspaceService.test.ts b/src/node/services/workspaceService.test.ts index c011142a01..aaaf7138f3 100644 --- a/src/node/services/workspaceService.test.ts +++ b/src/node/services/workspaceService.test.ts @@ -41,7 +41,7 @@ import type { DesktopSessionManager } from "@/node/services/desktop/DesktopSessi import type { WorktreeArchiveSnapshot } from "@/common/schemas/project"; import type { BashToolResult } from "@/common/types/tools"; import type { WorkspaceChatMessage } from "@/common/orpc/types"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { buildStagedAttachmentNotice } from "@/browser/features/ChatInput/stagedAttachments"; import { WORKFLOW_RUN_CARD_DISPLAY_METADATA_TYPE, @@ -2104,7 +2104,7 @@ describe("WorkspaceService bash monitor wakes", () => { timestamp: Date.now(), matchedThroughOffset: 13, }); - const malformedWake = createMuxMessage("malformed-wake", "user", "Malformed wake", { + const malformedWake = createXumMessage("malformed-wake", "user", "Malformed wake", { synthetic: true, }); if (malformedWake.metadata) { @@ -2113,7 +2113,7 @@ describe("WorkspaceService bash monitor wakes", () => { records: null, }; } - const emptyIdentityWake = createMuxMessage( + const emptyIdentityWake = createXumMessage( "empty-identity-wake", "user", "Empty identity wake", @@ -2129,7 +2129,7 @@ describe("WorkspaceService bash monitor wakes", () => { await historyService.appendToHistory(workspaceId, malformedWake); await historyService.appendToHistory( workspaceId, - createMuxMessage("accepted-wake", "user", "Accepted monitor wake", { + createXumMessage("accepted-wake", "user", "Accepted monitor wake", { synthetic: true, muxMetadata: buildBashMonitorWakeMetadata([record]), }) @@ -3209,7 +3209,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ + createXumMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: "workflow-call-1", @@ -3222,7 +3222,7 @@ describe("WorkspaceService workflow invocation events", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage("synthetic-await", "user", "Call task_await", { + createXumMessage("synthetic-await", "user", "Call task_await", { timestamp: 1_100, synthetic: true, }) @@ -3232,7 +3232,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("manual-user", "user", "Never mind, answer something else", { + createXumMessage("manual-user", "user", "Never mind, answer something else", { timestamp: 1_200, }) ); @@ -3274,7 +3274,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ + createXumMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: "workflow-call-1", @@ -3287,7 +3287,7 @@ describe("WorkspaceService workflow invocation events", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage("manual-user", "user", "Never mind, answer something else", { + createXumMessage("manual-user", "user", "Never mind, answer something else", { timestamp: 1_100, }) ); @@ -3297,7 +3297,7 @@ describe("WorkspaceService workflow invocation events", () => { // An unrelated tool output mentioning the run does not re-establish the invocation. await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-unrelated-tool", "assistant", "", { timestamp: 1_200 }, [ + createXumMessage("assistant-unrelated-tool", "assistant", "", { timestamp: 1_200 }, [ { type: "dynamic-tool", toolCallId: "task-list-1", @@ -3315,7 +3315,7 @@ describe("WorkspaceService workflow invocation events", () => { // again and the terminal continuation would be delivered. await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-resume", "assistant", "", { timestamp: 1_300 }, [ + createXumMessage("assistant-workflow-resume", "assistant", "", { timestamp: 1_300 }, [ { type: "dynamic-tool", toolCallId: "workflow-resume-1", @@ -3366,7 +3366,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage(`assistant-${toolName}`, "assistant", "", { timestamp: 1_000 }, [ + createXumMessage(`assistant-${toolName}`, "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: `${toolName}-call-1`, @@ -3423,7 +3423,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ + createXumMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: "workflow-call-1", @@ -3436,7 +3436,7 @@ describe("WorkspaceService workflow invocation events", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage("midstream-auto-compaction", "user", "Compacting to continue", { + createXumMessage("midstream-auto-compaction", "user", "Compacting to continue", { timestamp: 1_100, synthetic: true, muxMetadata: { @@ -3492,7 +3492,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ + createXumMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: "workflow-call-1", @@ -3505,7 +3505,7 @@ describe("WorkspaceService workflow invocation events", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage("auto-compaction", "user", "Compacting before a new user prompt", { + createXumMessage("auto-compaction", "user", "Compacting before a new user prompt", { timestamp: 1_100, synthetic: true, muxMetadata: { @@ -3564,7 +3564,7 @@ describe("WorkspaceService workflow invocation events", () => { expect(persisted).toBe(true); await historyService.appendToHistory( workspaceId, - createMuxMessage("boundary", "assistant", "Compacted summary", { + createXumMessage("boundary", "assistant", "Compacted summary", { timestamp: 2_000, compactionBoundary: true, }) @@ -3607,7 +3607,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ + createXumMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: "workflow-call-1", @@ -3622,7 +3622,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("reset-boundary", "assistant", "Context reset", { + createXumMessage("reset-boundary", "assistant", "Context reset", { timestamp: 1_100, contextBoundaryKind: "reset", }) @@ -3665,7 +3665,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ + createXumMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: "workflow-call-1", @@ -3678,7 +3678,7 @@ describe("WorkspaceService workflow invocation events", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage( + createXumMessage( "assistant-task-await-active-error", "assistant", "", @@ -3709,7 +3709,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage( + createXumMessage( "assistant-task-await-failed-error", "assistant", "", @@ -3773,7 +3773,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ + createXumMessage("assistant-workflow-run", "assistant", "", { timestamp: 1_000 }, [ { type: "dynamic-tool", toolCallId: "workflow-call-1", @@ -3788,7 +3788,7 @@ describe("WorkspaceService workflow invocation events", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-task-await", "assistant", "", { timestamp: 1_100 }, [ + createXumMessage("assistant-task-await", "assistant", "", { timestamp: 1_100 }, [ { type: "dynamic-tool", toolCallId: "task-await-1", @@ -3942,7 +3942,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { }); const appendResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("clear-goal-message", "user", "please remember this", {}) + createXumMessage("clear-goal-message", "user", "please remember this", {}) ); expect(appendResult.success).toBe(true); @@ -3989,7 +3989,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { }); await historyService.appendToHistory( workspaceId, - createMuxMessage("accepted-before-clear", "user", "Accepted wake", { + createXumMessage("accepted-before-clear", "user", "Accepted wake", { synthetic: true, muxMetadata: buildBashMonitorWakeMetadata([record]), }) @@ -4031,7 +4031,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { }); await historyService.appendToHistory( workspaceId, - createMuxMessage("partial-delete-accepted", "user", "Accepted wake", { + createXumMessage("partial-delete-accepted", "user", "Accepted wake", { synthetic: true, muxMetadata: buildBashMonitorWakeMetadata([record]), }) @@ -4148,7 +4148,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { }); await historyService.appendToHistory( workspaceId, - createMuxMessage("partial-clear-accepted", "user", "Accepted wake", { + createXumMessage("partial-clear-accepted", "user", "Accepted wake", { synthetic: true, muxMetadata: buildBashMonitorWakeMetadata([record]), }) @@ -4198,7 +4198,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { const result = await workspaceService.replaceHistory( workspaceId, - createMuxMessage("replacement-summary", "assistant", "Replacement summary", {}) + createXumMessage("replacement-summary", "assistant", "Replacement summary", {}) ); expect(result.success).toBe(true); @@ -4244,7 +4244,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { ( await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-reset-user", "user", "before reset", {}) + createXumMessage("pre-reset-user", "user", "before reset", {}) ) ).success ).toBe(true); @@ -4300,11 +4300,11 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { }); await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-start-here-user", "user", "long conversation", {}) + createXumMessage("pre-start-here-user", "user", "long conversation", {}) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-start-here-assistant", "assistant", "long reply", { + createXumMessage("pre-start-here-assistant", "assistant", "long reply", { model: "openai:gpt-4o", contextUsage: { inputTokens: 95_000, outputTokens: 200, totalTokens: 95_200 }, }) @@ -4326,7 +4326,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { ( await workspaceService.replaceHistory( workspaceId, - createMuxMessage("start-here-summary", "assistant", "Start Here summary", { + createXumMessage("start-here-summary", "assistant", "Start Here summary", { compacted: "user", }), { mode: "append-compaction-boundary" } @@ -4376,7 +4376,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { ( await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-reset-user", "user", "before reset", {}) + createXumMessage("pre-reset-user", "user", "before reset", {}) ) ).success ).toBe(true); @@ -4513,7 +4513,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { ( await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-reset-user", "user", "before reset", {}) + createXumMessage("pre-reset-user", "user", "before reset", {}) ) ).success ).toBe(true); @@ -4545,7 +4545,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { ); const seedResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-reset-user", "user", "before reset", {}) + createXumMessage("pre-reset-user", "user", "before reset", {}) ); expect(seedResult.success).toBe(true); const appendSpy = spyOn(historyService, "appendToHistory").mockResolvedValueOnce( @@ -4584,7 +4584,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { } as unknown as WorkspaceGoalService); const seedResult = await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-reset-user", "user", "before reset", {}) + createXumMessage("pre-reset-user", "user", "before reset", {}) ); expect(seedResult.success).toBe(true); @@ -4668,7 +4668,7 @@ describe("WorkspaceService truncateHistory goal acknowledgment", () => { ( await historyService.appendToHistory( workspaceId, - createMuxMessage("pre-reset-user", "user", "before reset", {}) + createXumMessage("pre-reset-user", "user", "before reset", {}) ) ).success ).toBe(true); @@ -6118,7 +6118,7 @@ describe("WorkspaceService sendMessage status clearing", () => { workspaceId, message: { type: "message", - ...createMuxMessage("user-accepted", "user", "hello"), + ...createXumMessage("user-accepted", "user", "hello"), }, }); @@ -6154,7 +6154,7 @@ describe("WorkspaceService sendMessage status clearing", () => { workspaceId, message: { type: "message", - ...createMuxMessage("user-synthetic", "user", "hello", { synthetic: true }), + ...createXumMessage("user-synthetic", "user", "hello", { synthetic: true }), }, }); @@ -12284,7 +12284,7 @@ describe("WorkspaceService regenerateTitle", () => { test("returns updateTitle error when persisting generated title fails", async () => { const workspaceId = "ws-regenerate-title"; - await historyService.appendToHistory(workspaceId, createMuxMessage("user-1", "user", "Fix CI")); + await historyService.appendToHistory(workspaceId, createXumMessage("user-1", "user", "Fix CI")); const generateIdentitySpy = spyOn( workspaceTitleGenerator, @@ -12322,11 +12322,11 @@ describe("WorkspaceService regenerateTitle", () => { await historyService.appendToHistory( workspaceId, - createMuxMessage("user-before-boundary", "user", "Refactor sidebar loading") + createXumMessage("user-before-boundary", "user", "Refactor sidebar loading") ); await historyService.appendToHistory( workspaceId, - createMuxMessage("summary-boundary", "assistant", "Compacted summary", { + createXumMessage("summary-boundary", "assistant", "Compacted summary", { compacted: true, compactionBoundary: true, compactionEpoch: 1, @@ -12334,7 +12334,7 @@ describe("WorkspaceService regenerateTitle", () => { ); await historyService.appendToHistory( workspaceId, - createMuxMessage("assistant-after-boundary", "assistant", "No new user messages yet") + createXumMessage("assistant-after-boundary", "assistant", "No new user messages yet") ); const iterateSpy = spyOn(historyService, "iterateFullHistory"); @@ -12387,7 +12387,7 @@ describe("WorkspaceService regenerateTitle", () => { const text = `${role === "user" ? "User" : "Assistant"} turn ${turn}`; await historyService.appendToHistory( workspaceId, - createMuxMessage(`${role}-${turn}`, role, text) + createXumMessage(`${role}-${turn}`, role, text) ); } @@ -12684,7 +12684,7 @@ describe("WorkspaceService fork", () => { // before we fork. The fork should keep this history but not inherit its costs. await historyService.appendToHistory( sourceWorkspaceId, - createMuxMessage("assistant-1", "assistant", "Hello", { + createXumMessage("assistant-1", "assistant", "Hello", { model: "claude-sonnet-4-20250514", usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 }, }) @@ -12805,7 +12805,7 @@ describe("WorkspaceService fork", () => { return current; }); - const sourcePartial = createMuxMessage( + const sourcePartial = createXumMessage( "assistant-partial", "assistant", "Waiting on task_await", @@ -14005,11 +14005,11 @@ describe("WorkspaceService.getLastUserPrompt", () => { const prompt = await withService(async (historyService, workspaceId) => { await historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "the prompt before compaction", { historySequence: 1 }) + createXumMessage("u1", "user", "the prompt before compaction", { historySequence: 1 }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("summary", "assistant", "Compacted summary", { + createXumMessage("summary", "assistant", "Compacted summary", { historySequence: 2, compacted: "user", compactionBoundary: true, @@ -14025,15 +14025,15 @@ describe("WorkspaceService.getLastUserPrompt", () => { const prompt = await withService(async (historyService, workspaceId) => { await historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "typed by the user", { historySequence: 1 }) + createXumMessage("u1", "user", "typed by the user", { historySequence: 1 }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", " ", { historySequence: 2 }) + createXumMessage("u2", "user", " ", { historySequence: 2 }) ); await historyService.appendToHistory( workspaceId, - createMuxMessage("u3", "user", "injected turn", { historySequence: 3, synthetic: true }) + createXumMessage("u3", "user", "injected turn", { historySequence: 3, synthetic: true }) ); }); @@ -14044,7 +14044,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { const prompt = await withService(async (historyService, workspaceId) => { await historyService.appendToHistory( workspaceId, - createMuxMessage("a1", "assistant", "hello", { historySequence: 1 }) + createXumMessage("a1", "assistant", "hello", { historySequence: 1 }) ); }); @@ -14053,7 +14053,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { test("prefers the raw slash command over its expanded provider text", async () => { const prompt = await withService(async (historyService, workspaceId) => { - const message = createMuxMessage("u1", "user", "Expanded skill body sent to the model", { + const message = createXumMessage("u1", "user", "Expanded skill body sent to the model", { historySequence: 1, }); await historyService.appendToHistory(workspaceId, { @@ -14067,7 +14067,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { test("reconstructs a compaction command's follow-up text", async () => { const prompt = await withService(async (historyService, workspaceId) => { - const message = createMuxMessage("u1", "user", "Expanded compaction instructions", { + const message = createXumMessage("u1", "user", "Expanded compaction instructions", { historySequence: 1, }); await historyService.appendToHistory(workspaceId, { @@ -14088,7 +14088,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { test("keeps the bare compaction command when the follow-up is the resume sentinel", async () => { const prompt = await withService(async (historyService, workspaceId) => { - const message = createMuxMessage("u1", "user", "Expanded compaction instructions", { + const message = createXumMessage("u1", "user", "Expanded compaction instructions", { historySequence: 1, }); await historyService.appendToHistory(workspaceId, { @@ -14111,7 +14111,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { const prompt = await withService(async (historyService, workspaceId) => { await historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "summarize the attached data", { historySequence: 1 }) + createXumMessage("u1", "user", "summarize the attached data", { historySequence: 1 }) ); const notice = buildStagedAttachmentNotice([ { @@ -14125,7 +14125,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { ]); await historyService.appendToHistory( workspaceId, - createMuxMessage("u2", "user", notice.trimStart(), { historySequence: 2 }) + createXumMessage("u2", "user", notice.trimStart(), { historySequence: 2 }) ); }); @@ -14134,7 +14134,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { test("survives a compaction row whose parsed metadata is missing", async () => { const prompt = await withService(async (historyService, workspaceId) => { - const message = createMuxMessage("u1", "user", "Expanded compaction instructions", { + const message = createXumMessage("u1", "user", "Expanded compaction instructions", { historySequence: 1, }); await historyService.appendToHistory(workspaceId, { @@ -14153,9 +14153,9 @@ describe("WorkspaceService.getLastUserPrompt", () => { const prompt = await withService(async (historyService, workspaceId) => { await historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "the older valid prompt", { historySequence: 1 }) + createXumMessage("u1", "user", "the older valid prompt", { historySequence: 1 }) ); - const broken = createMuxMessage("u2", "user", " ", { historySequence: 2 }); + const broken = createXumMessage("u2", "user", " ", { historySequence: 2 }); await historyService.appendToHistory(workspaceId, { ...broken, metadata: { ...broken.metadata, muxMetadata: "corrupted" }, @@ -14169,9 +14169,9 @@ describe("WorkspaceService.getLastUserPrompt", () => { const prompt = await withService(async (historyService, workspaceId) => { await historyService.appendToHistory( workspaceId, - createMuxMessage("u1", "user", "the older valid prompt", { historySequence: 1 }) + createXumMessage("u1", "user", "the older valid prompt", { historySequence: 1 }) ); - const broken = createMuxMessage("u2", "user", "ignored", { historySequence: 2 }); + const broken = createXumMessage("u2", "user", "ignored", { historySequence: 2 }); await historyService.appendToHistory(workspaceId, { ...broken, parts: undefined, @@ -14186,7 +14186,7 @@ describe("WorkspaceService.getLastUserPrompt", () => { for (const [index, text] of ["oldest prompt", "middle prompt", "newest prompt"].entries()) { await historyService.appendToHistory( workspaceId, - createMuxMessage(`u${index}`, "user", text, { historySequence: index + 1 }) + createXumMessage(`u${index}`, "user", text, { historySequence: index + 1 }) ); } }); diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 2fd9b32137..5237b4fe95 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -155,12 +155,12 @@ import { } from "@/common/utils/tools/toolDefinitions"; import { UIModeSchema, type UIMode } from "@/common/types/mode"; import { - createMuxMessage, + createXumMessage, getCompactionFollowUpContent, pickPreservedSendOptions, type CompactionFollowUpRequest, - type MuxMessageMetadata, - type MuxMessage, + type XumMessageMetadata, + type XumMessage, } from "@/common/types/message"; import { getFollowUpContentText } from "@/browser/utils/compaction/format"; import { stripStagedAttachmentNotice } from "@/browser/features/ChatInput/stagedAttachments"; @@ -352,7 +352,7 @@ interface HeartbeatExecutionRequest { schedulePolicy: HeartbeatSchedulePolicy; sendOptions: SendMessageOptions; heartbeatPrompt: string; - muxMetadata: Extract; + muxMetadata: Extract; followUp: CompactionFollowUpRequest; } @@ -365,7 +365,7 @@ type WorktreeArchiveSnapshotLifecycleService = Pick< >; // Trim and normalize a heartbeat message for storage. Accepts `unknown` so it safely handles // both user input (string | undefined) and persisted config values that may have been corrupted. -function isWorkflowInvocationMessage(message: MuxMessage, runId: string): boolean { +function isWorkflowInvocationMessage(message: XumMessage, runId: string): boolean { if ( message.metadata?.muxMetadata?.type === WORKFLOW_RUN_CARD_DISPLAY_METADATA_TYPE && message.metadata.muxMetadata.runId === runId @@ -391,7 +391,7 @@ function isWorkflowInvocationMessage(message: MuxMessage, runId: string): boolea }); } -function isTerminalWorkflowToolResultMessage(message: MuxMessage, runId: string): boolean { +function isTerminalWorkflowToolResultMessage(message: XumMessage, runId: string): boolean { return message.parts.some( (part) => part.type === "dynamic-tool" && @@ -400,7 +400,7 @@ function isTerminalWorkflowToolResultMessage(message: MuxMessage, runId: string) ); } -function isInternalResumeAutoCompactionMessage(message: MuxMessage): boolean { +function isInternalResumeAutoCompactionMessage(message: XumMessage): boolean { const muxMetadata = message.metadata?.muxMetadata; if (muxMetadata?.type !== "compaction-request" || muxMetadata.source !== "auto-compaction") { return false; @@ -408,7 +408,7 @@ function isInternalResumeAutoCompactionMessage(message: MuxMessage): boolean { return muxMetadata.parsed.followUpContent?.dispatchOptions?.source === "internal-resume"; } -function isSyntheticManualSupersessionMessage(message: MuxMessage): boolean { +function isSyntheticManualSupersessionMessage(message: XumMessage): boolean { const muxMetadata = message.metadata?.muxMetadata; return ( message.metadata?.synthetic === true && @@ -418,21 +418,21 @@ function isSyntheticManualSupersessionMessage(message: MuxMessage): boolean { ); } -function isManualUserSupersessionMessage(message: MuxMessage): boolean { +function isManualUserSupersessionMessage(message: XumMessage): boolean { return ( message.role === "user" && (message.metadata?.synthetic !== true || isSyntheticManualSupersessionMessage(message)) ); } -function isWorkflowResultContinuationMessage(message: MuxMessage, runId: string): boolean { +function isWorkflowResultContinuationMessage(message: XumMessage, runId: string): boolean { return ( message.metadata?.muxMetadata?.type === WORKFLOW_RESULT_METADATA_TYPE && message.metadata.muxMetadata.runId === runId ); } -function isResetBoundaryMessage(message: MuxMessage): boolean { +function isResetBoundaryMessage(message: XumMessage): boolean { return message.metadata?.contextBoundaryKind === CONTEXT_BOUNDARY_KINDS.RESET; } @@ -460,7 +460,7 @@ function isTerminalWorkflowTaskAwaitRecord( return false; } -function isTerminalWorkflowTaskAwaitResultMessage(message: MuxMessage, runId: string): boolean { +function isTerminalWorkflowTaskAwaitResultMessage(message: XumMessage, runId: string): boolean { if (message.role !== "assistant") { return false; } @@ -730,7 +730,7 @@ interface WorkspaceTitleConversationContext { latestUserText: string | undefined; } -function extractMuxMessageText(message: MuxMessage): string { +function extractXumMessageText(message: XumMessage): string { const text = message.parts ?.filter((part) => part.type === "text") @@ -741,7 +741,7 @@ function extractMuxMessageText(message: MuxMessage): string { } function collectWorkspaceTitleContextTurns( - messages: readonly MuxMessage[] + messages: readonly XumMessage[] ): WorkspaceTitleContextTurn[] { const turns: WorkspaceTitleContextTurn[] = []; @@ -750,7 +750,7 @@ function collectWorkspaceTitleContextTurns( continue; } - const text = extractMuxMessageText(message); + const text = extractXumMessageText(message); if (!text) { continue; } @@ -905,7 +905,7 @@ async function resetForkedSessionUsage( async function materializeForkedPartialSnapshot(params: { historyService: HistoryService; - partialSnapshot: MuxMessage | null; + partialSnapshot: XumMessage | null; sourceWorkspaceId: string; targetWorkspaceId: string; }): Promise { @@ -944,9 +944,9 @@ async function materializeForkedPartialSnapshot(params: { } function getOldestSequencedMessage( - messages: readonly MuxMessage[] -): { message: MuxMessage; historySequence: number } | null { - let oldest: { message: MuxMessage; historySequence: number } | null = null; + messages: readonly XumMessage[] +): { message: XumMessage; historySequence: number } | null { + let oldest: { message: XumMessage; historySequence: number } | null = null; for (const message of messages) { const historySequence = message.metadata?.historySequence; @@ -973,13 +973,13 @@ interface WorkspaceHistoryLoadMoreResult { hasOlder: boolean; } -function isCompactedSummaryMessage(message: MuxMessage): boolean { +function isCompactedSummaryMessage(message: XumMessage): boolean { return isDurableCompactedMarker(message.metadata?.compacted); } function getNextCompactionEpochForAppendBoundary( workspaceId: string, - messages: MuxMessage[] + messages: XumMessage[] ): number { let epochCursor = 0; @@ -1692,7 +1692,7 @@ function mergeActiveCount( * `/compact` stores its follow-up separately from `rawCommand`; reconstruct it to match the * transcript display. */ -function appendCompactionFollowUp(rawCommand: string, message: MuxMessage): string { +function appendCompactionFollowUp(rawCommand: string, message: XumMessage): string { const muxMeta: unknown = message.metadata?.muxMetadata; if (rawCommand.includes("\n") || typeof muxMeta !== "object" || muxMeta === null) { return rawCommand; @@ -1707,7 +1707,7 @@ function appendCompactionFollowUp(rawCommand: string, message: MuxMessage): stri * Prefer `rawCommand` so slash commands match the transcript instead of provider-expanded content. * Treat malformed persisted rows as empty so they cannot hide older valid prompts. */ -function extractUserPromptText(message: MuxMessage): string { +function extractUserPromptText(message: XumMessage): string { const muxMeta: unknown = message.metadata?.muxMetadata; const rawCommand = typeof muxMeta === "object" && @@ -8444,7 +8444,7 @@ export class WorkspaceService extends EventEmitter { const now = Date.now(); void this.updateRecencyTimestamp(input.workspaceId, now); const commandPrefix = input.rawCommand.trim().split(/\s+/u)[0] ?? "/workflow"; - const userMessage = createMuxMessage( + const userMessage = createXumMessage( `workflow-run-command-${input.runId}`, "user", input.rawCommand, @@ -8733,9 +8733,9 @@ export class WorkspaceService extends EventEmitter { } const normalizedOptions = this.normalizeSendMessageAgentId(options); - const normalizedMuxMetadata = normalizedOptions.muxMetadata as MuxMessageMetadata | undefined; + const normalizedXumMetadata = normalizedOptions.muxMetadata as XumMessageMetadata | undefined; const workspaceTurnContinuationMetadata = - normalizedMuxMetadata?.type === "workspace-turn-task" ? normalizedMuxMetadata : undefined; + normalizedXumMetadata?.type === "workspace-turn-task" ? normalizedXumMetadata : undefined; const isWorkspaceTurnContinuation = internal?.workspaceTurnContinuation === true; const stripWorkspaceTurnCorrelation = ( @@ -9337,8 +9337,8 @@ export class WorkspaceService extends EventEmitter { try { // Helper: update a message in-place if it contains this ask_user_question tool call. const tryFinalizeMessage = ( - msg: MuxMessage - ): Result<{ updated: MuxMessage; output: AskUserQuestionToolSuccessResult }> => { + msg: XumMessage + ): Result<{ updated: XumMessage; output: AskUserQuestionToolSuccessResult }> => { let foundToolCall = false; let output: AskUserQuestionToolSuccessResult | null = null; let errorMessage: string | null = null; @@ -9439,7 +9439,7 @@ export class WorkspaceService extends EventEmitter { } // Find the newest message containing this tool call. - let best: MuxMessage | null = null; + let best: XumMessage | null = null; let bestSeq = -Infinity; for (const msg of historyResult.data) { const seq = msg.metadata?.historySequence; @@ -9689,7 +9689,7 @@ export class WorkspaceService extends EventEmitter { */ hasPendingWorkspaceTurnContinuation( workspaceId: string, - metadata: Extract + metadata: Extract ): boolean { const session = this.sessions.get(workspaceId.trim()); return session?.hasPendingWorkspaceTurnContinuation(metadata) ?? false; @@ -9938,7 +9938,7 @@ export class WorkspaceService extends EventEmitter { return Ok("noop"); } - const boundaryMessage = createMuxMessage( + const boundaryMessage = createXumMessage( createContextResetBoundaryMessageId(), "assistant", "", @@ -9983,7 +9983,7 @@ export class WorkspaceService extends EventEmitter { async replaceHistory( workspaceId: string, - summaryMessage: MuxMessage, + summaryMessage: XumMessage, options?: { mode?: "destructive" | "append-compaction-boundary" | null; deletePlanFile?: boolean; @@ -10206,7 +10206,7 @@ export class WorkspaceService extends EventEmitter { return {}; } } - async getChatHistory(workspaceId: string): Promise { + async getChatHistory(workspaceId: string): Promise { try { // Only return messages from the latest compaction boundary onward. // Pre-boundary messages are summarized in the boundary marker. @@ -11065,7 +11065,7 @@ export class WorkspaceService extends EventEmitter { const sendOptions = await this.buildIdleCompactionSendOptions(workspaceId); - const muxMetadata: MuxMessageMetadata = { + const muxMetadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", commandPrefix: "/compact", @@ -11328,7 +11328,7 @@ export class WorkspaceService extends EventEmitter { "Heartbeat requests require a resolved agentId" ); - const muxMetadata: Extract = { + const muxMetadata: Extract = { type: "heartbeat-request", source: "heartbeat", requestedModel: sendOptions.model, @@ -11465,7 +11465,7 @@ export class WorkspaceService extends EventEmitter { heartbeatRequest: HeartbeatExecutionRequest ): Promise { const compactionSendOptions = await this.buildIdleCompactionSendOptions(workspaceId); - const compactionMuxMetadata: MuxMessageMetadata = { + const compactionXumMetadata: XumMessageMetadata = { type: "compaction-request", rawCommand: "/compact", commandPrefix: "/compact", @@ -11483,7 +11483,7 @@ export class WorkspaceService extends EventEmitter { buildCompactionMessageText({ followUpContent: heartbeatRequest.followUp }), { ...compactionSendOptions, - muxMetadata: compactionMuxMetadata, + muxMetadata: compactionXumMetadata, }, { skipAutoResumeReset: true, diff --git a/src/node/services/muxGatewayOauthService.test.ts b/src/node/services/xumGatewayOauthService.test.ts similarity index 96% rename from src/node/services/muxGatewayOauthService.test.ts rename to src/node/services/xumGatewayOauthService.test.ts index fa10b09170..14cdb8daf3 100644 --- a/src/node/services/muxGatewayOauthService.test.ts +++ b/src/node/services/xumGatewayOauthService.test.ts @@ -9,7 +9,7 @@ import { } from "@/common/constants/muxGatewayOAuth"; import type { ProviderService } from "@/node/services/providerService"; import type { WindowService } from "@/node/services/windowService"; -import { MuxGatewayOauthService } from "./muxGatewayOauthService"; +import { XumGatewayOauthService } from "./xumGatewayOauthService"; // --------------------------------------------------------------------------- // Helpers @@ -86,8 +86,8 @@ function createMockWindowService(deps: MockDeps): Pick { +describe("XumGatewayOauthService", () => { let deps: MockDeps; - let service: MuxGatewayOauthService; + let service: XumGatewayOauthService; const originalFetch = globalThis.fetch; beforeEach(() => { diff --git a/src/node/services/muxGatewayOauthService.ts b/src/node/services/xumGatewayOauthService.ts similarity index 99% rename from src/node/services/muxGatewayOauthService.ts rename to src/node/services/xumGatewayOauthService.ts index 3f6757981c..fed0cc36be 100644 --- a/src/node/services/muxGatewayOauthService.ts +++ b/src/node/services/xumGatewayOauthService.ts @@ -22,7 +22,7 @@ interface ServerFlow { expiresAtMs: number; } -export class MuxGatewayOauthService { +export class XumGatewayOauthService { private readonly desktopFlows = new OAuthFlowManager(); private readonly serverFlows = new Map(); diff --git a/src/node/services/muxGovernorOauthService.test.ts b/src/node/services/xumGovernorOauthService.test.ts similarity index 97% rename from src/node/services/muxGovernorOauthService.test.ts rename to src/node/services/xumGovernorOauthService.test.ts index 7b794194b7..a94d1508e8 100644 --- a/src/node/services/muxGovernorOauthService.test.ts +++ b/src/node/services/xumGovernorOauthService.test.ts @@ -7,7 +7,7 @@ import type { ProjectsConfig } from "@/common/types/project"; import type { Config } from "@/node/config"; import type { PolicyService } from "@/node/services/policyService"; import type { WindowService } from "@/node/services/windowService"; -import { MuxGovernorOauthService } from "./muxGovernorOauthService"; +import { XumGovernorOauthService } from "./xumGovernorOauthService"; // --------------------------------------------------------------------------- // Helpers @@ -94,8 +94,8 @@ function createMockPolicyService(deps: MockDeps): Pick { +describe("XumGovernorOauthService", () => { let deps: MockDeps; - let service: MuxGovernorOauthService; + let service: XumGovernorOauthService; const originalFetch = globalThis.fetch; beforeEach(() => { diff --git a/src/node/services/muxGovernorOauthService.ts b/src/node/services/xumGovernorOauthService.ts similarity index 99% rename from src/node/services/muxGovernorOauthService.ts rename to src/node/services/xumGovernorOauthService.ts index 5859ea36f5..a9b1320c7a 100644 --- a/src/node/services/muxGovernorOauthService.ts +++ b/src/node/services/xumGovernorOauthService.ts @@ -33,7 +33,7 @@ interface ServerFlow { expiresAtMs: number; } -export class MuxGovernorOauthService { +export class XumGovernorOauthService { private readonly desktopFlows = new OAuthFlowManager(); private readonly serverFlows = new Map(); diff --git a/src/node/utils/messages/convertDataUriFilePartsForSdk.test.ts b/src/node/utils/messages/convertDataUriFilePartsForSdk.test.ts index 1dfcb25a0f..bedd5acc77 100644 --- a/src/node/utils/messages/convertDataUriFilePartsForSdk.test.ts +++ b/src/node/utils/messages/convertDataUriFilePartsForSdk.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "@jest/globals"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { convertDataUriFilePartsForSdk } from "./convertDataUriFilePartsForSdk"; describe("convertDataUriFilePartsForSdk", () => { it("keeps base64 data URI file parts as canonical data URLs", () => { const base64 = Buffer.from("png-bytes", "utf8").toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "u1", role: "user", @@ -34,7 +34,7 @@ describe("convertDataUriFilePartsForSdk", () => { }); it("returns the original array when there are no data URI file parts", () => { - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "u2", role: "user", @@ -51,7 +51,7 @@ describe("convertDataUriFilePartsForSdk", () => { it("does not rewrite assistant messages", () => { const base64 = Buffer.from("assistant", "utf8").toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a1", role: "assistant", @@ -67,7 +67,7 @@ describe("convertDataUriFilePartsForSdk", () => { const pngBase64 = Buffer.from("png", "utf8").toString("base64"); const pdfBase64 = Buffer.from("pdf", "utf8").toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "u3", role: "user", @@ -97,7 +97,7 @@ describe("convertDataUriFilePartsForSdk", () => { const svg = 'hello'; const encodedSvg = encodeURIComponent(svg); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "u4", role: "user", @@ -123,7 +123,7 @@ describe("convertDataUriFilePartsForSdk", () => { }); it("throws for malformed data URIs missing a comma separator", () => { - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "u5", role: "user", diff --git a/src/node/utils/messages/convertDataUriFilePartsForSdk.ts b/src/node/utils/messages/convertDataUriFilePartsForSdk.ts index 2796858a8c..2cced5353e 100644 --- a/src/node/utils/messages/convertDataUriFilePartsForSdk.ts +++ b/src/node/utils/messages/convertDataUriFilePartsForSdk.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; const DATA_URI_PREFIX = "data:"; @@ -56,7 +56,7 @@ function parseDataUriToBase64(dataUri: string): ParsedDataUri { * URL-encoded (non-base64) data URIs would be silently corrupted. Rebuilding the * canonical `data:;base64,` form keeps both cases safe. */ -export function convertDataUriFilePartsForSdk(messages: MuxMessage[]): MuxMessage[] { +export function convertDataUriFilePartsForSdk(messages: XumMessage[]): XumMessage[] { let changedAnyMessage = false; const convertedMessages = messages.map((message) => { @@ -66,7 +66,7 @@ export function convertDataUriFilePartsForSdk(messages: MuxMessage[]): MuxMessag let changedMessage = false; - const convertedParts: MuxMessage["parts"] = message.parts.map((part) => { + const convertedParts: XumMessage["parts"] = message.parts.map((part) => { if (part.type !== "file" || !part.url.toLowerCase().startsWith(DATA_URI_PREFIX)) { return part; } diff --git a/src/node/utils/messages/extractToolMediaAsUserMessages.test.ts b/src/node/utils/messages/extractToolMediaAsUserMessages.test.ts index 37568c9c2d..579d3653f5 100644 --- a/src/node/utils/messages/extractToolMediaAsUserMessages.test.ts +++ b/src/node/utils/messages/extractToolMediaAsUserMessages.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "@jest/globals"; import sharp from "sharp"; import { MAX_IMAGE_DIMENSION } from "@/common/constants/imageAttachments"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { expectContentOutputValue } from "./testToolOutputHelpers"; import { extractToolMediaAsUserMessages } from "./extractToolMediaAsUserMessages"; @@ -20,7 +20,7 @@ describe("extractToolMediaAsUserMessages", () => { .toBuffer() ).toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a1", role: "assistant", @@ -94,7 +94,7 @@ describe("extractToolMediaAsUserMessages", () => { .toBuffer(); const base64 = oversizedPng.toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a1-resize", role: "assistant", @@ -142,7 +142,7 @@ describe("extractToolMediaAsUserMessages", () => { it("rewrites attach_file PDF output into a synthetic user file part", async () => { const base64 = Buffer.from("%PDF-1.7").toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a2", role: "assistant", @@ -187,7 +187,7 @@ describe("extractToolMediaAsUserMessages", () => { it("sanitizes extracted PDF filenames in synthetic user file parts", async () => { const base64 = Buffer.from("%PDF-1.7").toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a3", role: "assistant", @@ -230,7 +230,7 @@ describe("extractToolMediaAsUserMessages", () => { "base64" ); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a4", role: "assistant", @@ -270,7 +270,7 @@ describe("extractToolMediaAsUserMessages", () => { it("strips display-only file bytes without creating a model attachment", async () => { const base64 = Buffer.from("webm bytes").toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a5", role: "assistant", @@ -321,7 +321,7 @@ describe("extractToolMediaAsUserMessages", () => { it("strips display-only file bytes even when metadata is missing", async () => { const base64 = Buffer.from("webm bytes").toString("base64"); - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a6", role: "assistant", @@ -370,7 +370,7 @@ describe("extractToolMediaAsUserMessages", () => { }); it("does not rewrite unrelated tool outputs", async () => { - const input: MuxMessage[] = [ + const input: XumMessage[] = [ { id: "a1", role: "assistant", diff --git a/src/node/utils/messages/extractToolMediaAsUserMessages.ts b/src/node/utils/messages/extractToolMediaAsUserMessages.ts index 6182f5e129..f496da5642 100644 --- a/src/node/utils/messages/extractToolMediaAsUserMessages.ts +++ b/src/node/utils/messages/extractToolMediaAsUserMessages.ts @@ -1,4 +1,4 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { sanitizeAnthropicDocumentFilename } from "@/node/utils/messages/sanitizeAnthropicDocumentFilename"; import { createDataUrlForExtractedAttachment, @@ -24,10 +24,10 @@ import { * convertToModelMessages(...). Persisted history and UI still keep the original tool output. */ export async function extractToolMediaAsUserMessages( - messages: MuxMessage[] -): Promise { + messages: XumMessage[] +): Promise { let didChangeAnyMessage = false; - const result: MuxMessage[] = []; + const result: XumMessage[] = []; for (const message of messages) { if (message.role !== "assistant") { @@ -35,11 +35,11 @@ export async function extractToolMediaAsUserMessages( continue; } - let extractedUserParts: MuxMessage["parts"] = []; + let extractedUserParts: XumMessage["parts"] = []; let extractedAttachmentCount = 0; let changedMessage = false; - const newParts: MuxMessage["parts"] = []; + const newParts: XumMessage["parts"] = []; for (const part of message.parts) { if (part.type !== "dynamic-tool" || part.state !== "output-available") { newParts.push(part); @@ -55,7 +55,7 @@ export async function extractToolMediaAsUserMessages( changedMessage = true; extractedAttachmentCount += extracted.attachments.length; - const nextExtractedUserParts: MuxMessage["parts"] = []; + const nextExtractedUserParts: XumMessage["parts"] = []; for (const attachment of extracted.attachments) { const providerReadyAttachment = await prepareExtractedToolAttachmentForProvider(attachment); if (providerReadyAttachment.type === "text") { @@ -90,7 +90,7 @@ export async function extractToolMediaAsUserMessages( } const rewrittenMessage = changedMessage - ? ({ ...message, parts: newParts } satisfies MuxMessage) + ? ({ ...message, parts: newParts } satisfies XumMessage) : message; if (changedMessage) { didChangeAnyMessage = true; diff --git a/src/node/utils/messages/inlineSvgAsTextForProvider.test.ts b/src/node/utils/messages/inlineSvgAsTextForProvider.test.ts index 2b65fd55bf..cc689d3163 100644 --- a/src/node/utils/messages/inlineSvgAsTextForProvider.test.ts +++ b/src/node/utils/messages/inlineSvgAsTextForProvider.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "@jest/globals"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { inlineSvgAsTextForProvider } from "./inlineSvgAsTextForProvider"; describe("inlineSvgAsTextForProvider", () => { @@ -7,7 +7,7 @@ describe("inlineSvgAsTextForProvider", () => { const svg = ''; const b64 = Buffer.from(svg, "utf8").toString("base64"); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-1", role: "user", @@ -33,7 +33,7 @@ describe("inlineSvgAsTextForProvider", () => { const svg = 'Hello'; const encoded = encodeURIComponent(svg); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-2", role: "user", @@ -55,7 +55,7 @@ describe("inlineSvgAsTextForProvider", () => { const svg = '' + "a".repeat(100) + ""; const b64 = Buffer.from(svg, "utf8").toString("base64"); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-3", role: "user", @@ -78,7 +78,7 @@ describe("inlineSvgAsTextForProvider", () => { const svg = '' + "a".repeat(100) + ""; const b64 = Buffer.from(svg, "utf8").toString("base64"); - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-3b", role: "user", @@ -98,7 +98,7 @@ describe("inlineSvgAsTextForProvider", () => { }); it("returns the same array when there are no SVG parts", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "user-4", role: "user", diff --git a/src/node/utils/messages/inlineSvgAsTextForProvider.ts b/src/node/utils/messages/inlineSvgAsTextForProvider.ts index 860d4943f7..ccab24f12c 100644 --- a/src/node/utils/messages/inlineSvgAsTextForProvider.ts +++ b/src/node/utils/messages/inlineSvgAsTextForProvider.ts @@ -1,5 +1,5 @@ import { MAX_SVG_TEXT_CHARS, SVG_MEDIA_TYPE } from "@/common/constants/imageAttachments"; -import type { MuxMessage, MuxTextPart } from "@/common/types/message"; +import type { XumMessage, XumTextPart } from "@/common/types/message"; // Guardrail: prevent accidentally injecting a multi‑MB SVG into the prompt. const DEFAULT_MAX_SVG_TEXT_BYTES = 200 * 1024; // 200 KiB @@ -90,9 +90,9 @@ function decodeSvgDataUrlToUtf8(svgDataUrl: string, maxBytes: number, maxChars: * - Scope: user message `file` parts only. */ export function inlineSvgAsTextForProvider( - messages: MuxMessage[], + messages: XumMessage[], options?: { maxSvgTextBytes?: number; maxSvgTextChars?: number } -): MuxMessage[] { +): XumMessage[] { const maxSvgTextChars = options?.maxSvgTextChars ?? MAX_SVG_TEXT_CHARS; const maxSvgTextBytes = options?.maxSvgTextBytes ?? DEFAULT_MAX_SVG_TEXT_BYTES; @@ -112,13 +112,13 @@ export function inlineSvgAsTextForProvider( didChange = true; - const newParts: MuxMessage["parts"] = []; + const newParts: XumMessage["parts"] = []; for (const part of msg.parts) { if (part.type === "file" && normalizeMediaType(part.mediaType) === SVG_MEDIA_TYPE) { try { const svgText = decodeSvgDataUrlToUtf8(part.url, maxSvgTextBytes, maxSvgTextChars); - const textPart: MuxTextPart = { + const textPart: XumTextPart = { type: "text", text: `[SVG attachment converted to text (providers generally don't accept ${SVG_MEDIA_TYPE} as an image input).]\n\n` + @@ -128,7 +128,7 @@ export function inlineSvgAsTextForProvider( } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to decode SVG attachment."; - const textPart: MuxTextPart = { + const textPart: XumTextPart = { type: "text", text: `[SVG attachment omitted from provider request: ${errorMessage}]`, }; diff --git a/src/node/utils/messages/legacy.ts b/src/node/utils/messages/legacy.ts index e0bf4178d3..5937b2ba3f 100644 --- a/src/node/utils/messages/legacy.ts +++ b/src/node/utils/messages/legacy.ts @@ -1,7 +1,7 @@ -import type { MuxMessageMetadata, MuxMessage, MuxMetadata } from "@/common/types/message"; +import type { XumMessageMetadata, XumMessage, XumMetadata } from "@/common/types/message"; -interface LegacyMuxMetadata extends MuxMetadata { - cmuxMetadata?: MuxMessageMetadata; +interface LegacyXumMetadata extends XumMetadata { + cmuxMetadata?: XumMessageMetadata; idleCompacted?: boolean; } @@ -12,16 +12,16 @@ interface LegacyMuxMetadata extends MuxMetadata { * - `cmuxMetadata` → `muxMetadata` (mux rename) * - `{ compacted: true, idleCompacted: true }` → `{ compacted: "idle" }` */ -export function normalizeLegacyMuxMetadata(message: MuxMessage): MuxMessage { - const metadata = message.metadata as LegacyMuxMetadata | undefined; +export function normalizeLegacyXumMetadata(message: XumMessage): XumMessage { + const metadata = message.metadata as LegacyXumMetadata | undefined; if (!metadata) return message; - let normalized: MuxMetadata = { ...metadata }; + let normalized: XumMetadata = { ...metadata }; let changed = false; // Migrate cmuxMetadata → muxMetadata if (metadata.cmuxMetadata !== undefined) { - const { cmuxMetadata, ...rest } = normalized as LegacyMuxMetadata; + const { cmuxMetadata, ...rest } = normalized as LegacyXumMetadata; normalized = rest; if (!metadata.muxMetadata) { normalized.muxMetadata = cmuxMetadata; @@ -31,7 +31,7 @@ export function normalizeLegacyMuxMetadata(message: MuxMessage): MuxMessage { // Migrate idleCompacted: true → compacted: "idle" if (metadata.idleCompacted === true) { - const { idleCompacted, ...rest } = normalized as LegacyMuxMetadata; + const { idleCompacted, ...rest } = normalized as LegacyXumMetadata; normalized = { ...rest, compacted: "idle" }; changed = true; } diff --git a/src/node/utils/messages/reasoningProviderOptions.test.ts b/src/node/utils/messages/reasoningProviderOptions.test.ts index f8d0cb8fd9..abb25d90bf 100644 --- a/src/node/utils/messages/reasoningProviderOptions.test.ts +++ b/src/node/utils/messages/reasoningProviderOptions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import type { MuxMessage, MuxReasoningPart } from "@/common/types/message"; +import type { XumMessage, XumReasoningPart } from "@/common/types/message"; import { attachReasoningReplayMetadata, findFirstReasoningPartIndexInTrailingRun, @@ -78,13 +78,13 @@ describe("mergeReasoningProviderOptions", () => { }); describe("attachReasoningReplayMetadata", () => { - function assistantMessage(parts: MuxMessage["parts"]): MuxMessage { + function assistantMessage(parts: XumMessage["parts"]): XumMessage { return { id: "a1", role: "assistant", metadata: { timestamp: 1 }, parts }; } - function reasoningParts(message: MuxMessage): Array> { + function reasoningParts(message: XumMessage): Array> { return message.parts.filter( - (part): part is MuxReasoningPart & Record => part.type === "reasoning" + (part): part is XumReasoningPart & Record => part.type === "reasoning" ); } @@ -136,7 +136,7 @@ describe("attachReasoningReplayMetadata", () => { { type: "reasoning", text: "unsigned" }, { type: "text", text: "answer" }, ]); - const user: MuxMessage = { + const user: XumMessage = { id: "u1", role: "user", metadata: { timestamp: 0 }, @@ -172,7 +172,7 @@ describe("attachReasoningReplayMetadata", () => { // store=false drop rule then removes as unresolvable. xai: { itemId: "rs_ok", reasoningEncryptedContent: { nested: true } }, google: { thoughtSignature: "" }, - } as unknown as MuxReasoningPart["providerOptions"], + } as unknown as XumReasoningPart["providerOptions"], }, ]); @@ -227,7 +227,7 @@ describe("attachReasoningReplayMetadata", () => { }); test("does not mutate the input parts", () => { - const part: MuxReasoningPart = { + const part: XumReasoningPart = { type: "reasoning", text: "thinking", providerOptions: { anthropic: { signature: "sig" } }, diff --git a/src/node/utils/messages/reasoningProviderOptions.ts b/src/node/utils/messages/reasoningProviderOptions.ts index 0bd8dc4ac8..3e9e60445a 100644 --- a/src/node/utils/messages/reasoningProviderOptions.ts +++ b/src/node/utils/messages/reasoningProviderOptions.ts @@ -1,4 +1,4 @@ -import type { MuxMessage, MuxReasoningPart } from "@/common/types/message"; +import type { XumMessage, XumReasoningPart } from "@/common/types/message"; export interface ReasoningProviderMetadata { anthropic?: { @@ -40,11 +40,11 @@ function nonEmptyString(value: unknown): string | undefined { */ export function sanitizeReasoningReplayMetadata( value: unknown -): MuxReasoningPart["providerOptions"] | undefined { +): XumReasoningPart["providerOptions"] | undefined { const record = asRecord(value); if (!record) return undefined; - const options: NonNullable = {}; + const options: NonNullable = {}; const anthropicSignature = nonEmptyString(asRecord(record.anthropic)?.signature); if (anthropicSignature) { @@ -84,18 +84,18 @@ export function sanitizeReasoningReplayMetadata( */ export function reasoningProviderOptionsFromMetadata( providerMetadata: ReasoningProviderMetadata | undefined -): MuxReasoningPart["providerOptions"] | undefined { +): XumReasoningPart["providerOptions"] | undefined { return sanitizeReasoningReplayMetadata(providerMetadata); } export function mergeReasoningProviderOptions( - existing: MuxReasoningPart["providerOptions"] | undefined, - incoming: MuxReasoningPart["providerOptions"] | undefined -): MuxReasoningPart["providerOptions"] | undefined { + existing: XumReasoningPart["providerOptions"] | undefined, + incoming: XumReasoningPart["providerOptions"] | undefined +): XumReasoningPart["providerOptions"] | undefined { if (!existing) return incoming; if (!incoming) return existing; - const merged: NonNullable = { ...existing }; + const merged: NonNullable = { ...existing }; if (incoming.anthropic) { merged.anthropic = { ...existing.anthropic, ...incoming.anthropic }; @@ -116,8 +116,8 @@ export function mergeReasoningProviderOptions( * `providerMetadata` into ModelMessage `providerOptions` and ignores any * `providerOptions` field on the input part. Never persisted to history. */ -type ReasoningPartWithReplayMetadata = MuxReasoningPart & { - providerMetadata?: MuxReasoningPart["providerOptions"]; +type ReasoningPartWithReplayMetadata = XumReasoningPart & { + providerMetadata?: XumReasoningPart["providerOptions"]; }; /** @@ -127,7 +127,7 @@ type ReasoningPartWithReplayMetadata = MuxReasoningPart & { * this bridge, prior-turn reasoning is silently dropped for every provider. * Non-mutating: history objects are reused elsewhere (e.g. debug logging). */ -export function attachReasoningReplayMetadata(messages: MuxMessage[]): MuxMessage[] { +export function attachReasoningReplayMetadata(messages: XumMessage[]): XumMessage[] { return messages.map((message) => { if (message.role !== "assistant") return message; diff --git a/src/node/utils/messages/sanitizeAnthropicDocumentFilename.test.ts b/src/node/utils/messages/sanitizeAnthropicDocumentFilename.test.ts index 567bc6d8a5..4c95d0f4e4 100644 --- a/src/node/utils/messages/sanitizeAnthropicDocumentFilename.test.ts +++ b/src/node/utils/messages/sanitizeAnthropicDocumentFilename.test.ts @@ -3,7 +3,7 @@ import { sanitizeAnthropicDocumentFilename, sanitizeAnthropicPdfFilenames, } from "./sanitizeAnthropicDocumentFilename"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; describe("sanitizeAnthropicDocumentFilename", () => { it("replaces periods with spaces", () => { @@ -50,7 +50,7 @@ describe("sanitizeAnthropicDocumentFilename", () => { }); describe("sanitizeAnthropicPdfFilenames", () => { - const createUserMessageWithPdf = (filename: string): MuxMessage => ({ + const createUserMessageWithPdf = (filename: string): XumMessage => ({ id: "msg-1", role: "user", parts: [ @@ -65,7 +65,7 @@ describe("sanitizeAnthropicPdfFilenames", () => { }); it("sanitizes PDF filenames in user messages", () => { - const messages: MuxMessage[] = [createUserMessageWithPdf("report.pdf")]; + const messages: XumMessage[] = [createUserMessageWithPdf("report.pdf")]; const result = sanitizeAnthropicPdfFilenames(messages); @@ -78,7 +78,7 @@ describe("sanitizeAnthropicPdfFilenames", () => { }); it("does not mutate original messages", () => { - const messages: MuxMessage[] = [createUserMessageWithPdf("original.pdf")]; + const messages: XumMessage[] = [createUserMessageWithPdf("original.pdf")]; const originalFilename = (messages[0].parts[1] as { filename: string }).filename; sanitizeAnthropicPdfFilenames(messages); @@ -87,7 +87,7 @@ describe("sanitizeAnthropicPdfFilenames", () => { }); it("passes through assistant messages unchanged", () => { - const assistantMessage: MuxMessage = { + const assistantMessage: XumMessage = { id: "msg-2", role: "assistant", parts: [{ type: "text", text: "Response" }], @@ -100,7 +100,7 @@ describe("sanitizeAnthropicPdfFilenames", () => { }); it("does not sanitize non-PDF files", () => { - const imageMessage: MuxMessage = { + const imageMessage: XumMessage = { id: "msg-3", role: "user", parts: [ @@ -119,7 +119,7 @@ describe("sanitizeAnthropicPdfFilenames", () => { }); it("handles case-insensitive media type matching", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "msg-4", role: "user", @@ -140,7 +140,7 @@ describe("sanitizeAnthropicPdfFilenames", () => { }); it("returns original array if no changes needed", () => { - const messages: MuxMessage[] = [ + const messages: XumMessage[] = [ { id: "msg-5", role: "user", diff --git a/src/node/utils/messages/sanitizeAnthropicDocumentFilename.ts b/src/node/utils/messages/sanitizeAnthropicDocumentFilename.ts index a5eed78e6f..f85b2214b0 100644 --- a/src/node/utils/messages/sanitizeAnthropicDocumentFilename.ts +++ b/src/node/utils/messages/sanitizeAnthropicDocumentFilename.ts @@ -1,4 +1,4 @@ -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; const PDF_MEDIA_TYPE = "application/pdf"; @@ -47,7 +47,7 @@ export function sanitizeAnthropicDocumentFilename( * @param messages - XumMessage array to process * @returns New array with sanitized PDF filenames (does not mutate input) */ -export function sanitizeAnthropicPdfFilenames(messages: MuxMessage[]): MuxMessage[] { +export function sanitizeAnthropicPdfFilenames(messages: XumMessage[]): XumMessage[] { let didChange = false; const result = messages.map((msg) => { diff --git a/src/node/utils/oauthUtils.ts b/src/node/utils/oauthUtils.ts index 7d22219f4e..7f4d670c13 100644 --- a/src/node/utils/oauthUtils.ts +++ b/src/node/utils/oauthUtils.ts @@ -4,7 +4,7 @@ import type http from "node:http"; * Shared OAuth utility functions extracted from the individual OAuth service files. * * These are verbatim-duplicated across codexOauthService, copilotOauthService, - * muxGatewayOauthService, muxGovernorOauthService, and mcpOauthService. + * xumGatewayOauthService, xumGovernorOauthService, and mcpOauthService. */ /** A deferred promise with an externally-accessible `resolve` handle. */ diff --git a/src/node/utils/providerRequirements.ts b/src/node/utils/providerRequirements.ts index a2a295cedd..c2b4df4b88 100644 --- a/src/node/utils/providerRequirements.ts +++ b/src/node/utils/providerRequirements.ts @@ -19,7 +19,7 @@ import type { BaseProviderConfig, BedrockProviderConfig, CoderProviderConfig, - MuxGatewayProviderConfig, + XumGatewayProviderConfig, OpenAIProviderConfig, } from "@/common/config/schemas/providersConfig"; import type { ProviderConfig, ProvidersConfig } from "@/node/config"; @@ -123,7 +123,7 @@ type ProviderSpecificCredentialFields = Partial< BedrockProviderConfig, "region" | "profile" | "bearerToken" | "accessKeyId" | "secretAccessKey" > & - Pick & + Pick & Pick & Pick & { // Wider than the schema type: callers pass raw (unvalidated) config. diff --git a/tests/e2e/utils/historyFixture.ts b/tests/e2e/utils/historyFixture.ts index 6a68852fa2..f63d679d0e 100644 --- a/tests/e2e/utils/historyFixture.ts +++ b/tests/e2e/utils/historyFixture.ts @@ -1,7 +1,7 @@ import fsPromises from "fs/promises"; import path from "path"; import type { DemoProjectConfig } from "./demoProject"; -import { createMuxMessage, type MuxMessage } from "../../../src/common/types/message"; +import { createXumMessage, type XumMessage } from "../../../src/common/types/message"; import { FILE_EDIT_DIFF_OMITTED_MESSAGE } from "../../../src/common/types/tools"; import { HistoryService } from "../../../src/node/services/historyService"; @@ -123,8 +123,8 @@ function createAssistantParts(args: { toolOutputChars: number; reasoningChars: number; largeDiffLinePairs?: number; -}): MuxMessage["parts"] { - const parts: MuxMessage["parts"] = []; +}): XumMessage["parts"] { + const parts: XumMessage["parts"] = []; if (args.toolOutputChars > 0) { const toolName = args.index % 2 === 0 ? "file_read" : "bash"; @@ -187,7 +187,7 @@ function createAssistantParts(args: { async function appendOrThrow(args: { historyService: HistoryService; workspaceId: string; - message: MuxMessage; + message: XumMessage; profile: HistoryProfileName; role: "user" | "assistant"; }): Promise { @@ -219,7 +219,7 @@ export async function seedWorkspaceHistoryProfile(args: { `${profile}-user-${pairIndex}`, profileConfig.userChars ); - const userMessage = createMuxMessage(`${profile}-user-msg-${pairIndex}`, "user", userText, { + const userMessage = createXumMessage(`${profile}-user-msg-${pairIndex}`, "user", userText, { timestamp: BASE_TIMESTAMP_MS + pairIndex * 2, }); await appendOrThrow({ @@ -242,7 +242,7 @@ export async function seedWorkspaceHistoryProfile(args: { largeDiffLinePairs: profileConfig.largeDiffLinePairs, }); - const assistantMessage = createMuxMessage( + const assistantMessage = createXumMessage( `${profile}-assistant-msg-${pairIndex}`, "assistant", assistantText, diff --git a/tests/ipc/acp.disconnectCleanup.test.ts b/tests/ipc/acp.disconnectCleanup.test.ts index e0186ce44a..45a515d683 100644 --- a/tests/ipc/acp.disconnectCleanup.test.ts +++ b/tests/ipc/acp.disconnectCleanup.test.ts @@ -6,7 +6,7 @@ import { promisify } from "node:util"; import { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk"; import type { ProjectConfig } from "../../src/common/types/project"; import type { OnChatMode, WorkspaceChatMessage } from "../../src/common/orpc/types"; -import { MuxAgent } from "../../src/node/acp/agent"; +import { XumAgent } from "../../src/node/acp/agent"; import type { ORPCClient, ServerConnection } from "../../src/node/acp/serverConnection"; const execFileAsyncForTest = promisify(execFile); @@ -31,7 +31,7 @@ interface HarnessOptions { } interface Harness { - agent: MuxAgent; + agent: XumAgent; createdWorkspaceIds: string[]; createCalls: WorkspaceCreateInput[]; removeCalls: string[]; @@ -252,9 +252,9 @@ function createHarness(options?: HarnessOptions): Harness { const { stream, closeInput } = createControllableAcpStream(); - let agentInstance: MuxAgent | null = null; + let agentInstance: XumAgent | null = null; const connection = new AgentSideConnection((connectionToAgent) => { - const createdAgent = new MuxAgent(connectionToAgent, server, { + const createdAgent = new XumAgent(connectionToAgent, server, { disconnectCleanupMaxWaitMs: options?.disconnectCleanupMaxWaitMs, }); agentInstance = createdAgent; @@ -262,7 +262,7 @@ function createHarness(options?: HarnessOptions): Harness { }, stream); if (agentInstance == null) { - throw new Error("createHarness: failed to construct MuxAgent"); + throw new Error("createHarness: failed to construct XumAgent"); } return { diff --git a/tests/ipc/acp.promptCorrelation.test.ts b/tests/ipc/acp.promptCorrelation.test.ts index 9109ade941..5cb1b9c759 100644 --- a/tests/ipc/acp.promptCorrelation.test.ts +++ b/tests/ipc/acp.promptCorrelation.test.ts @@ -1,12 +1,12 @@ import { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk"; import type { OnChatMode, WorkspaceChatMessage } from "../../src/common/orpc/types"; -import { MuxAgent } from "../../src/node/acp/agent"; +import { XumAgent } from "../../src/node/acp/agent"; import type { ORPCClient, ServerConnection } from "../../src/node/acp/serverConnection"; type WorkspaceInfo = NonNullable>>; interface Harness { - agent: MuxAgent; + agent: XumAgent; sendMessageCalls: Array<{ workspaceId: string; message: string; @@ -285,7 +285,7 @@ interface HarnessOptions { }) => Promise<{ success: boolean; data?: unknown; error?: unknown }>; /** Custom output WritableStream for simulating stdout backpressure. */ acpOutputStream?: WritableStream; - agentOptions?: ConstructorParameters[2]; + agentOptions?: ConstructorParameters[2]; } function createHarness(options?: HarnessOptions): Harness { @@ -405,15 +405,15 @@ function createHarness(options?: HarnessOptions): Harness { output: options?.acpOutputStream, }); - let agentInstance: MuxAgent | null = null; + let agentInstance: XumAgent | null = null; const connection = new AgentSideConnection((connectionToAgent) => { - const createdAgent = new MuxAgent(connectionToAgent, server, options?.agentOptions); + const createdAgent = new XumAgent(connectionToAgent, server, options?.agentOptions); agentInstance = createdAgent; return createdAgent; }, stream); if (agentInstance == null) { - throw new Error("createHarness: failed to construct MuxAgent"); + throw new Error("createHarness: failed to construct XumAgent"); } return { @@ -441,7 +441,7 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -function getLastMuxMetadata(harness: Harness): Record { +function getLastXumMetadata(harness: Harness): Record { const lastSend = harness.sendMessageCalls.at(-1); if (lastSend == null) { throw new Error("Expected prompt send call before reading muxMetadata"); @@ -456,7 +456,7 @@ function getLastMuxMetadata(harness: Harness): Record { } function getPromptCorrelationId(harness: Harness): string { - const promptCorrelationId = getLastMuxMetadata(harness)["acpPromptId"]; + const promptCorrelationId = getLastXumMetadata(harness)["acpPromptId"]; if (typeof promptCorrelationId !== "string") { throw new Error("Expected prompt send options to include acpPromptId"); } @@ -481,7 +481,7 @@ async function startPromptTurn( sessionId: string, text = "hello" ): Promise<{ - promptPromise: ReturnType; + promptPromise: ReturnType; promptCorrelationId: string; }> { const promptPromise = harness.agent.prompt({ @@ -497,8 +497,8 @@ async function createDefaultPromptTurn( harness: Harness, cwd = "/repo/acp-go-sdk" ): Promise<{ - newSessionResponse: Awaited>; - promptPromise: ReturnType; + newSessionResponse: Awaited>; + promptPromise: ReturnType; promptCorrelationId: string; }> { await initializeDefaultAgent(harness); @@ -933,7 +933,7 @@ describe("ACP prompt stream correlation", () => { newSessionResponse.sessionId ); - const muxMetadata = getLastMuxMetadata(harness); + const muxMetadata = getLastXumMetadata(harness); expect(muxMetadata["acpDelegatedTools"]).toEqual([ "file_read", diff --git a/tests/ipc/acp.sessionMethods.test.ts b/tests/ipc/acp.sessionMethods.test.ts index 02395c5e84..07934238d3 100644 --- a/tests/ipc/acp.sessionMethods.test.ts +++ b/tests/ipc/acp.sessionMethods.test.ts @@ -6,7 +6,7 @@ import { } from "@agentclientprotocol/sdk"; import type { ProjectConfig } from "../../src/common/types/project"; import type { OnChatMode, WorkspaceChatMessage } from "../../src/common/orpc/types"; -import { MuxAgent } from "../../src/node/acp/agent"; +import { XumAgent } from "../../src/node/acp/agent"; import type { ORPCClient, ServerConnection } from "../../src/node/acp/serverConnection"; type WorkspaceInfo = NonNullable>>; @@ -36,11 +36,11 @@ interface HarnessOptions { onChatStream?: AsyncIterable; requireTrustedProjectForCreate?: boolean; projectEntries?: Array<[string, ProjectConfig]>; - agentOptions?: ConstructorParameters[2]; + agentOptions?: ConstructorParameters[2]; } interface Harness { - agent: MuxAgent; + agent: XumAgent; onChatCalls: Array<{ workspaceId: string; mode?: OnChatMode }>; setTrustCalls: Array<{ projectPath: string; trusted: boolean }>; createCalls: WorkspaceCreateInput[]; @@ -216,19 +216,19 @@ function createMockServer(options?: HarnessOptions): MockServer { function createHarness(options?: HarnessOptions): Harness { const mockServer = createMockServer(options); - let agentInstance: MuxAgent | null = null; + let agentInstance: XumAgent | null = null; // Use a real ACP connection instead of casting a hand-rolled stub to // AgentSideConnection. This keeps the test harness type-safe and exercises - // the same connection surface MuxAgent uses in production. + // the same connection surface XumAgent uses in production. const _connection = new AgentSideConnection((connectionToAgent) => { - const createdAgent = new MuxAgent(connectionToAgent, mockServer.server, options?.agentOptions); + const createdAgent = new XumAgent(connectionToAgent, mockServer.server, options?.agentOptions); agentInstance = createdAgent; return createdAgent; }, createInMemoryAcpStream()); void _connection; if (agentInstance == null) { - throw new Error("createHarness: failed to construct MuxAgent"); + throw new Error("createHarness: failed to construct XumAgent"); } return { @@ -242,7 +242,7 @@ function createHarness(options?: HarnessOptions): Harness { } // Cross-wired in-memory pipes so a real ClientSideConnection talks to the real -// AgentSideConnection over JSON-RPC. Unlike createHarness (which invokes MuxAgent +// AgentSideConnection over JSON-RPC. Unlike createHarness (which invokes XumAgent // methods directly and therefore renames in lockstep with the implementation), // this exercises the SDK's wire dispatch: a missed Agent-interface rename (the // SDK declares listSessions/resumeSession as optional, e.g. unstable_listSessions @@ -255,7 +255,7 @@ function createWireHarness(options?: HarnessOptions): { client: ClientSideConnec const _agentConnection = new AgentSideConnection( (connectionToAgent) => - new MuxAgent(connectionToAgent, mockServer.server, options?.agentOptions), + new XumAgent(connectionToAgent, mockServer.server, options?.agentOptions), ndJsonStream(agentToClient.writable, clientToAgent.readable) ); void _agentConnection; diff --git a/tests/ipc/agents/planCommands.test.ts b/tests/ipc/agents/planCommands.test.ts index b020da945a..a3bcd344b1 100644 --- a/tests/ipc/agents/planCommands.test.ts +++ b/tests/ipc/agents/planCommands.test.ts @@ -19,7 +19,7 @@ import { } from "../helpers"; import { detectDefaultTrunkBranch } from "../../../src/node/git"; import { getPlanFilePath } from "../../../src/common/utils/planStorage"; -import { createMuxMessage } from "../../../src/common/types/message"; +import { createXumMessage } from "../../../src/common/types/message"; import { expandTilde } from "../../../src/node/runtime/tildeExpansion"; // Skip all tests if TEST_INTEGRATION is not set @@ -177,7 +177,7 @@ describeIntegration("Plan Commands Integration", () => { await fs.mkdir(path.dirname(expandedPlanPath), { recursive: true }); await fs.writeFile(expandedPlanPath, "# Test Plan\n"); - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( `start-here-test-${Date.now()}`, "assistant", "summary", @@ -219,7 +219,7 @@ describeIntegration("Plan Commands Integration", () => { const workspaceId = createResult.metadata.id; try { - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( `start-here-test-${Date.now()}`, "assistant", "summary", @@ -265,7 +265,7 @@ describeIntegration("Plan Commands Integration", () => { const workspaceId = createResult.metadata.id; try { - const legacySummary = createMuxMessage( + const legacySummary = createXumMessage( `legacy-summary-${Date.now()}`, "assistant", "legacy summary", @@ -283,7 +283,7 @@ describeIntegration("Plan Commands Integration", () => { // Inject malformed boundary metadata directly to verify self-healing epoch derivation. const malformedBoundaryId = `malformed-boundary-${Date.now()}`; - const malformedBoundaryMessage = createMuxMessage( + const malformedBoundaryMessage = createXumMessage( malformedBoundaryId, "assistant", "malformed boundary", @@ -301,7 +301,7 @@ describeIntegration("Plan Commands Integration", () => { JSON.stringify({ ...malformedBoundaryMessage, workspaceId }) + "\n" ); - const appendSummary = createMuxMessage( + const appendSummary = createXumMessage( `append-summary-${Date.now()}`, "assistant", "append summary", diff --git a/tests/ipc/compaction1MRetry.integration.test.ts b/tests/ipc/compaction1MRetry.integration.test.ts index 2e754a07a7..06c9204b1e 100644 --- a/tests/ipc/compaction1MRetry.integration.test.ts +++ b/tests/ipc/compaction1MRetry.integration.test.ts @@ -13,7 +13,7 @@ import { setupWorkspace, shouldRunIntegrationTests, validateApiKeys } from "./setup"; import { createStreamCollector, resolveOrpcClient } from "./helpers"; import { HistoryService } from "../../src/node/services/historyService"; -import { createMuxMessage } from "../../src/common/types/message"; +import { createXumMessage } from "../../src/common/types/message"; // Skip all tests if TEST_INTEGRATION is not set const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; @@ -75,13 +75,13 @@ describeIntegration("compaction 1M context retry", () => { const charsPerMessage = Math.ceil(CHARS_NEEDED / pairsNeeded); for (let i = 0; i < pairsNeeded; i++) { - const userMsg = createMuxMessage( + const userMsg = createXumMessage( `filler-user-${i}`, "user", buildFillerText(charsPerMessage), {} ); - const assistantMsg = createMuxMessage( + const assistantMsg = createXumMessage( `filler-asst-${i}`, "assistant", buildFillerText(charsPerMessage), diff --git a/tests/ipc/helpers.ts b/tests/ipc/helpers.ts index 1cac8039b2..5e7869c0d3 100644 --- a/tests/ipc/helpers.ts +++ b/tests/ipc/helpers.ts @@ -34,7 +34,7 @@ import type { ToolPolicy } from "../../src/common/utils/tools/toolPolicy"; import type { WorkspaceSendMessageOutput } from "@/common/orpc/schemas"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { HistoryService } from "../../src/node/services/historyService"; -import { createMuxMessage } from "../../src/common/types/message"; +import { createXumMessage } from "../../src/common/types/message"; const execAsync = promisify(exec); import { ORPCError } from "@orpc/client"; @@ -712,7 +712,7 @@ export async function buildLargeHistory( for (let i = 0; i < messageCount; i++) { const isUser = i % 2 === 0; const role = isUser ? "user" : "assistant"; - const message = createMuxMessage(`history-msg-${i}`, role, largeText, {}); + const message = createXumMessage(`history-msg-${i}`, role, largeText, {}); const result = await historyService.appendToHistory(workspaceId, message); if (!result.success) { diff --git a/tests/ipc/providers/openaiPreviousResponseIdRecovery.test.ts b/tests/ipc/providers/openaiPreviousResponseIdRecovery.test.ts index b7bb251b5e..c7424bd4b1 100644 --- a/tests/ipc/providers/openaiPreviousResponseIdRecovery.test.ts +++ b/tests/ipc/providers/openaiPreviousResponseIdRecovery.test.ts @@ -15,7 +15,7 @@ import { } from "../helpers"; import { KNOWN_MODELS } from "../../../src/common/constants/knownModels"; import type { ToolPolicy } from "../../../src/common/utils/tools/toolPolicy"; -import { createMuxMessage } from "../../../src/common/types/message"; +import { createXumMessage } from "../../../src/common/types/message"; // Skip all tests if TEST_INTEGRATION is not set const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; @@ -42,7 +42,7 @@ describeIntegration("OpenAI response-id metadata", () => { try { const invalidResponseId = createInvalidResponseId(); - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( `summary-${Date.now()}`, "assistant", "Summary placeholder for stale responseId metadata.", diff --git a/tests/ipc/providers/xaiGrok46.test.ts b/tests/ipc/providers/xaiGrok46.test.ts index c25c14168f..0e54f93bf0 100644 --- a/tests/ipc/providers/xaiGrok46.test.ts +++ b/tests/ipc/providers/xaiGrok46.test.ts @@ -1,6 +1,6 @@ import { KNOWN_MODELS } from "@/common/constants/knownModels"; import { isStreamEnd } from "@/common/orpc/types"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import type { ToolPolicy } from "@/common/utils/tools/toolPolicy"; import { HistoryService } from "@/node/services/historyService"; import { @@ -19,7 +19,7 @@ if (shouldRunIntegrationTests()) { const DISABLE_TOOLS: ToolPolicy = [{ regex_match: ".*", action: "disable" }]; -function hasXaiEncryptedReasoning(messages: MuxMessage[]): boolean { +function hasXaiEncryptedReasoning(messages: XumMessage[]): boolean { for (const message of messages) { if (message.role !== "assistant" || !Array.isArray(message.parts)) continue; for (const part of message.parts) { diff --git a/tests/ipc/streaming/emptyAssistantMessage.test.ts b/tests/ipc/streaming/emptyAssistantMessage.test.ts index b557c8740c..5259083d70 100644 --- a/tests/ipc/streaming/emptyAssistantMessage.test.ts +++ b/tests/ipc/streaming/emptyAssistantMessage.test.ts @@ -8,7 +8,7 @@ import { setupWorkspace, shouldRunIntegrationTests, validateApiKeys } from "../setup"; import { sendMessageWithModel, createStreamCollector, HAIKU_MODEL } from "../helpers"; import { HistoryService } from "../../../src/node/services/historyService"; -import { createMuxMessage } from "../../../src/common/types/message"; +import { createXumMessage } from "../../../src/common/types/message"; // Skip all tests if TEST_INTEGRATION is not set const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; @@ -32,9 +32,9 @@ describeIntegration("empty assistant message self-healing", () => { // 3. User follow-up // 4. Empty assistant message (crash during stream start - placeholder persisted) const messages = [ - createMuxMessage("msg-1", "user", "Hello", {}), - createMuxMessage("msg-2", "assistant", "Hi there!", {}), - createMuxMessage("msg-3", "user", "Follow up question", {}), + createXumMessage("msg-1", "user", "Hello", {}), + createXumMessage("msg-2", "assistant", "Hi there!", {}), + createXumMessage("msg-3", "user", "Follow up question", {}), // Corrupted: empty parts array (placeholder message from crash) { id: "msg-4-corrupted", @@ -94,7 +94,7 @@ describeIntegration("empty assistant message self-healing", () => { // Seed history with an assistant message that has only an incomplete tool call // (state: "input-available" means tool was requested but never executed) const messages = [ - createMuxMessage("msg-1", "user", "Run a command", {}), + createXumMessage("msg-1", "user", "Run a command", {}), // Corrupted: tool-only with incomplete state { id: "msg-2-corrupted", diff --git a/tests/ipc/streaming/interrupt.test.ts b/tests/ipc/streaming/interrupt.test.ts index 1ad6fe9aa2..5d96d47566 100644 --- a/tests/ipc/streaming/interrupt.test.ts +++ b/tests/ipc/streaming/interrupt.test.ts @@ -23,7 +23,7 @@ import { HAIKU_MODEL, } from "../helpers"; import { createStreamCollector } from "../streamCollector"; -import { isInitOutput, isMuxMessage } from "@/common/orpc/types"; +import { isInitOutput, isXumMessage } from "@/common/orpc/types"; import * as path from "path"; import * as fs from "fs/promises"; // eslint-disable-next-line local/no-unsafe-child-process @@ -113,7 +113,7 @@ describeIntegration("interruptStream during startup", () => { // Wait until we observe the user message being persisted/emitted. const sawUserMessage = await waitFor(() => { - return activeCollector.getEvents().some((e) => isMuxMessage(e) && e.role === "user"); + return activeCollector.getEvents().some((e) => isXumMessage(e) && e.role === "user"); }, 5000); expect(sawUserMessage).toBe(true); diff --git a/tests/ipc/streaming/queuedMessages.completing.test.ts b/tests/ipc/streaming/queuedMessages.completing.test.ts index 69c1c5e892..082c8b3921 100644 --- a/tests/ipc/streaming/queuedMessages.completing.test.ts +++ b/tests/ipc/streaming/queuedMessages.completing.test.ts @@ -10,7 +10,7 @@ import { createStreamCollector, } from "../helpers"; import { - isMuxMessage, + isXumMessage, isQueuedMessageChanged, isRestoreToInput, type WorkspaceChatMessage, @@ -411,7 +411,7 @@ describe("Queued messages during stream completion", () => { const sawFirstUserMessage = await waitFor(() => { const firstUserMessage = collector .getEvents() - .filter(isMuxMessage) + .filter(isXumMessage) .find( (event) => event.role === "user" && @@ -601,7 +601,7 @@ describe("Queued messages during stream completion", () => { const sawFirstUserMessage = await waitFor(() => { const firstUserMessage = collector .getEvents() - .filter(isMuxMessage) + .filter(isXumMessage) .find( (event) => event.role === "user" && @@ -668,7 +668,7 @@ describe("Queued messages during stream completion", () => { const sawEditedUserMessage = await waitFor(() => { return collector .getEvents() - .filter(isMuxMessage) + .filter(isXumMessage) .some( (event) => event.role === "user" && diff --git a/tests/ipc/streaming/queuedMessages.starting.test.ts b/tests/ipc/streaming/queuedMessages.starting.test.ts index 4715b42bf9..bc44b6ad3d 100644 --- a/tests/ipc/streaming/queuedMessages.starting.test.ts +++ b/tests/ipc/streaming/queuedMessages.starting.test.ts @@ -9,7 +9,7 @@ import { HAIKU_MODEL, createStreamCollector, } from "../helpers"; -import { isMuxMessage } from "@/common/orpc/types"; +import { isXumMessage } from "@/common/orpc/types"; import { buildMockStreamStartGateMessage } from "@/node/services/mock/mockAiRouter"; describe("Queued messages during stream start", () => { @@ -68,7 +68,7 @@ describe("Queued messages during stream start", () => { .getEvents() .some( (event) => - isMuxMessage(event) && + isXumMessage(event) && event.role === "user" && event.parts.some((part) => "text" in part && part.text === gatedMessage) ); @@ -174,7 +174,7 @@ describe("Queued messages during stream start", () => { const userMessages = collector .getEvents() - .filter(isMuxMessage) + .filter(isXumMessage) .filter((event) => event.role === "user") .map((event) => event.parts diff --git a/tests/ipc/streaming/resume.test.ts b/tests/ipc/streaming/resume.test.ts index 0ac3e6ae46..0e51e999bc 100644 --- a/tests/ipc/streaming/resume.test.ts +++ b/tests/ipc/streaming/resume.test.ts @@ -7,7 +7,7 @@ import { configureTestRetries, } from "../helpers"; import { HistoryService } from "../../../src/node/services/historyService"; -import { createMuxMessage } from "../../../src/common/types/message"; +import { createXumMessage } from "../../../src/common/types/message"; import type { WorkspaceChatMessage } from "@/common/orpc/types"; // Skip all tests if TEST_INTEGRATION is not set @@ -158,7 +158,7 @@ describeIntegration("resumeStream", () => { // Simulate post-compaction state: single assistant message with summary // The message promises to say a specific word next, allowing deterministic verification const verificationWord = "ELEPHANT"; - const summaryMessage = createMuxMessage( + const summaryMessage = createXumMessage( "compaction-summary-msg", "assistant", `I previously helped with a task. The conversation has been compacted for token efficiency. My next message will contain the word ${verificationWord} to confirm continuation works correctly.`, diff --git a/tests/ipc/streaming/sendMessage.images.test.ts b/tests/ipc/streaming/sendMessage.images.test.ts index d62a50bbd7..ecc7ba3eab 100644 --- a/tests/ipc/streaming/sendMessage.images.test.ts +++ b/tests/ipc/streaming/sendMessage.images.test.ts @@ -16,7 +16,7 @@ import { configureTestRetries, } from "../sendMessageTestHelpers"; import { HistoryService } from "@/node/services/historyService"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { KNOWN_MODELS } from "../../../src/common/constants/knownModels"; import assert from "node:assert"; @@ -24,8 +24,8 @@ import assert from "node:assert"; async function collectFullHistory( service: HistoryService, workspaceId: string -): Promise { - const messages: MuxMessage[] = []; +): Promise { + const messages: XumMessage[] = []; const result = await service.iterateFullHistory(workspaceId, "forward", (chunk) => { messages.push(...chunk); }); @@ -200,7 +200,7 @@ describeIntegration("sendMessage image handling tests", () => { const messages = await collectFullHistory(historyService, workspaceId); const imageMsg = messages.find( - (msg: MuxMessage) => + (msg: XumMessage) => msg.role === "user" && msg.parts.some( (part) => diff --git a/tests/ipc/streaming/truncate.test.ts b/tests/ipc/streaming/truncate.test.ts index cf484a902e..a87b770e45 100644 --- a/tests/ipc/streaming/truncate.test.ts +++ b/tests/ipc/streaming/truncate.test.ts @@ -7,7 +7,7 @@ import { modelString, } from "../helpers"; import { HistoryService } from "../../../src/node/services/historyService"; -import { createMuxMessage } from "../../../src/common/types/message"; +import { createXumMessage } from "../../../src/common/types/message"; import type { DeleteMessage } from "@/common/orpc/types"; // Skip all tests if TEST_INTEGRATION is not set @@ -30,12 +30,12 @@ describeIntegration("truncateHistory", () => { // Create messages with a unique word in the first message const uniqueWord = `testword-${Date.now()}`; const messages = [ - createMuxMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), - createMuxMessage("msg-2", "assistant", "I will remember that word.", {}), - createMuxMessage("msg-3", "user", "What is 2+2?", {}), - createMuxMessage("msg-4", "assistant", "4", {}), - createMuxMessage("msg-5", "user", "What is 3+3?", {}), - createMuxMessage("msg-6", "assistant", "6", {}), + createXumMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), + createXumMessage("msg-2", "assistant", "I will remember that word.", {}), + createXumMessage("msg-3", "user", "What is 2+2?", {}), + createXumMessage("msg-4", "assistant", "4", {}), + createXumMessage("msg-5", "user", "What is 3+3?", {}), + createXumMessage("msg-6", "assistant", "6", {}), ]; // Append messages to history @@ -115,10 +115,10 @@ describeIntegration("truncateHistory", () => { // Prepopulate chat with messages (avoid API calls) const uniqueWord = `testword-${Date.now()}`; const messages = [ - createMuxMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), - createMuxMessage("msg-2", "assistant", "I will remember that word.", {}), - createMuxMessage("msg-3", "user", "Tell me a fact about cats", {}), - createMuxMessage("msg-4", "assistant", "Cats sleep 12-16 hours a day.", {}), + createXumMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), + createXumMessage("msg-2", "assistant", "I will remember that word.", {}), + createXumMessage("msg-3", "user", "Tell me a fact about cats", {}), + createXumMessage("msg-4", "assistant", "Cats sleep 12-16 hours a day.", {}), ]; // Append messages to history @@ -208,8 +208,8 @@ describeIntegration("truncateHistory", () => { // Prepopulate some history const uniqueWord = `testword-${Date.now()}`; const messages = [ - createMuxMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), - createMuxMessage("msg-2", "assistant", "I will remember that word.", {}), + createXumMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), + createXumMessage("msg-2", "assistant", "I will remember that word.", {}), ]; for (const msg of messages) { diff --git a/tests/ipc/streaming/websocketHistoryReplay.test.ts b/tests/ipc/streaming/websocketHistoryReplay.test.ts index 3592ae68ea..08802c8941 100644 --- a/tests/ipc/streaming/websocketHistoryReplay.test.ts +++ b/tests/ipc/streaming/websocketHistoryReplay.test.ts @@ -6,15 +6,15 @@ import { cleanupTempGitRepo, } from "../helpers"; import { HistoryService } from "@/node/services/historyService"; -import { createMuxMessage } from "@/common/types/message"; -import type { MuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import assert from "node:assert"; async function collectFullHistory( service: HistoryService, workspaceId: string -): Promise { - const messages: MuxMessage[] = []; +): Promise { + const messages: XumMessage[] = []; const result = await service.iterateFullHistory(workspaceId, "forward", (chunk) => { messages.push(...chunk); }); @@ -52,7 +52,7 @@ describe("WebSocket history replay", () => { const workspaceId = createResult.metadata.id; const historyService = new HistoryService(env.config); - const testMessage = createMuxMessage("test-msg-2", "user", "Test message for getHistory"); + const testMessage = createXumMessage("test-msg-2", "user", "Test message for getHistory"); await historyService.appendToHistory(workspaceId, testMessage); await new Promise((resolve) => setTimeout(resolve, 100)); diff --git a/tests/ipc/tasks/persistentSubagentCompaction.test.ts b/tests/ipc/tasks/persistentSubagentCompaction.test.ts index bdf2aa505c..0abbc03ab4 100644 --- a/tests/ipc/tasks/persistentSubagentCompaction.test.ts +++ b/tests/ipc/tasks/persistentSubagentCompaction.test.ts @@ -11,12 +11,12 @@ import { import type { Workspace as WorkspaceConfigEntry } from "@/node/config"; import { HistoryService } from "@/node/services/historyService"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; -function extractText(message: MuxMessage): string { +function extractText(message: XumMessage): string { return message.parts .filter( - (part): part is Extract => part.type === "text" + (part): part is Extract => part.type === "text" ) .map((part) => part.text) .join(""); @@ -187,7 +187,7 @@ describe("Persistent sub-agent compaction", () => { ) ).toBe(true); - const fullHistory: MuxMessage[] = []; + const fullHistory: XumMessage[] = []; const fullHistoryResult = await historyService.iterateFullHistory( childWorkspaceId, "forward", diff --git a/tests/ipc/workspace/fork.test.ts b/tests/ipc/workspace/fork.test.ts index e8d14e4658..dbcd091574 100644 --- a/tests/ipc/workspace/fork.test.ts +++ b/tests/ipc/workspace/fork.test.ts @@ -28,12 +28,12 @@ import { import type { RuntimeConfig } from "../../../src/common/types/runtime"; import { getContainerName } from "../../../src/node/runtime/DockerRuntime"; import { HistoryService } from "../../../src/node/services/historyService"; -import { createMuxMessage, type MuxMessage } from "../../../src/common/types/message"; +import { createXumMessage, type XumMessage } from "../../../src/common/types/message"; import assert from "node:assert"; /** Collect all messages via iterateFullHistory (replaces removed getFullHistory). */ async function collectFullHistory(service: HistoryService, workspaceId: string) { - const messages: MuxMessage[] = []; + const messages: XumMessage[] = []; const result = await service.iterateFullHistory(workspaceId, "forward", (chunk) => { messages.push(...chunk); }); @@ -218,8 +218,8 @@ describeIntegration("Workspace fork", () => { const historyService = new HistoryService(env.config); const uniqueWord = `testword-${Date.now()}`; const historyMessages = [ - createMuxMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), - createMuxMessage( + createXumMessage("msg-1", "user", `Remember this word: ${uniqueWord}`, {}), + createXumMessage( "msg-2", "assistant", `I will remember the word "${uniqueWord}".`, @@ -384,8 +384,8 @@ describeIntegration("Workspace fork", () => { const partial = await historyService.readPartial(sourceWorkspaceId); if (!partial) return false; partialText = (partial.parts ?? []) - .filter((part: MuxMessage["parts"][number]) => part.type === "text") - .map((part: MuxMessage["parts"][number]) => (part.type === "text" ? part.text : "")) + .filter((part: XumMessage["parts"][number]) => part.type === "text") + .map((part: XumMessage["parts"][number]) => (part.type === "text" ? part.text : "")) .join(" ") .trim(); return partialText.length > 0; @@ -689,8 +689,8 @@ describeIntegration("Workspace fork", () => { const historyService = new HistoryService(env.config); const uniqueWord = `localtest-${Date.now()}`; const historyMessages = [ - createMuxMessage("msg-1", "user", `Remember this local word: ${uniqueWord}`, {}), - createMuxMessage( + createXumMessage("msg-1", "user", `Remember this local word: ${uniqueWord}`, {}), + createXumMessage( "msg-2", "assistant", `I will remember the local word "${uniqueWord}".`, diff --git a/tests/runtime/runtime.test.ts b/tests/runtime/runtime.test.ts index 4463676865..2571ffc372 100644 --- a/tests/runtime/runtime.test.ts +++ b/tests/runtime/runtime.test.ts @@ -3052,7 +3052,7 @@ describeIntegration("Runtime integration tests", () => { createWorkspaceCalled = true; yield "should not happen"; }, - ensureMuxCoderSSHConfig: async () => { + ensureXumCoderSSHConfig: async () => { // This SHOULD be called - it's safe and idempotent }, getWorkspaceStatus: () => diff --git a/tests/ui/chat/forkFromResponse.test.ts b/tests/ui/chat/forkFromResponse.test.ts index ea52e377b1..a7902806a7 100644 --- a/tests/ui/chat/forkFromResponse.test.ts +++ b/tests/ui/chat/forkFromResponse.test.ts @@ -1,6 +1,6 @@ import "../dom"; import { fireEvent, waitFor } from "@testing-library/react"; -import type { MuxMessage } from "@/common/types/message"; +import type { XumMessage } from "@/common/types/message"; import { preloadTestModules } from "../../ipc/setup"; import { createAppHarness } from "../harness"; @@ -17,7 +17,7 @@ async function collectFullHistory( ); } -function getMessageText(messages: Array<{ parts: MuxMessage["parts"] }>): string { +function getMessageText(messages: Array<{ parts: XumMessage["parts"] }>): string { return messages .flatMap((message) => message.parts diff --git a/tests/ui/chat/truncation.test.ts b/tests/ui/chat/truncation.test.ts index 6a04acb161..5ac7333dea 100644 --- a/tests/ui/chat/truncation.test.ts +++ b/tests/ui/chat/truncation.test.ts @@ -16,7 +16,7 @@ import { import { detectDefaultTrunkBranch } from "@/node/git"; import { HistoryService } from "@/node/services/historyService"; import { MAX_HISTORY_HIDDEN_SEGMENTS } from "@/browser/utils/messages/transcriptTruncationPlan"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { installDom } from "../dom"; import { renderApp } from "../renderReviewPanel"; @@ -40,7 +40,7 @@ async function seedHistoryWithToolCalls( pairCount: number ): Promise { for (let i = 0; i < pairCount; i++) { - const userMessage = createMuxMessage(`user-${i}`, "user", `user-${i}`); + const userMessage = createXumMessage(`user-${i}`, "user", `user-${i}`); const toolMessage = { id: `assistant-tool-${i}`, role: "assistant" as const, @@ -56,7 +56,7 @@ async function seedHistoryWithToolCalls( }, ], }; - const assistantMessage = createMuxMessage(`assistant-${i}`, "assistant", `assistant-${i}`); + const assistantMessage = createXumMessage(`assistant-${i}`, "assistant", `assistant-${i}`); const userResult = await historyService.appendToHistory(workspaceId, userMessage); if (!userResult.success) { diff --git a/tests/ui/gateway/sessionExpired.test.tsx b/tests/ui/gateway/sessionExpired.test.tsx index be0c6509a9..2ad495a51b 100644 --- a/tests/ui/gateway/sessionExpired.test.tsx +++ b/tests/ui/gateway/sessionExpired.test.tsx @@ -5,11 +5,11 @@ import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"; import { installDom } from "../dom"; -import { MuxGatewaySessionExpiredDialog } from "@/browser/components/MuxGatewaySessionExpiredDialog/MuxGatewaySessionExpiredDialog"; +import { XumGatewaySessionExpiredDialog } from "@/browser/components/XumGatewaySessionExpiredDialog/XumGatewaySessionExpiredDialog"; import { CUSTOM_EVENTS, createCustomEvent } from "@/common/constants/events"; import { MUX_GATEWAY_SESSION_EXPIRED_MESSAGE } from "@/common/constants/muxGatewayOAuth"; -describe("MuxGatewaySessionExpiredDialog", () => { +describe("XumGatewaySessionExpiredDialog", () => { let cleanupDom: (() => void) | null = null; beforeEach(() => { @@ -23,7 +23,7 @@ describe("MuxGatewaySessionExpiredDialog", () => { }); test("shows a Dialog when mux gateway session expires", async () => { - const view = render(); + const view = render(); window.dispatchEvent(createCustomEvent(CUSTOM_EVENTS.MUX_GATEWAY_SESSION_EXPIRED)); @@ -63,7 +63,7 @@ describe("MuxGatewaySessionExpiredDialog", () => { }; try { - const view = render(); + const view = render(); window.dispatchEvent(createCustomEvent(CUSTOM_EVENTS.MUX_GATEWAY_SESSION_EXPIRED)); await waitFor(() => { @@ -89,7 +89,7 @@ describe("MuxGatewaySessionExpiredDialog", () => { window.open = () => null; try { - const view = render(); + const view = render(); window.dispatchEvent(createCustomEvent(CUSTOM_EVENTS.MUX_GATEWAY_SESSION_EXPIRED)); await waitFor(() => { diff --git a/tests/ui/tasks/awaitVisualization.test.ts b/tests/ui/tasks/awaitVisualization.test.ts index 6047ec4afc..5746a79325 100644 --- a/tests/ui/tasks/awaitVisualization.test.ts +++ b/tests/ui/tasks/awaitVisualization.test.ts @@ -18,7 +18,7 @@ import { } from "../../ipc/helpers"; import { detectDefaultTrunkBranch } from "@/node/git"; import { HistoryService } from "@/node/services/historyService"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { installDom } from "../dom"; import { renderApp } from "../renderReviewPanel"; @@ -39,9 +39,9 @@ async function waitForWorkspaceChatToRender(container: HTMLElement): Promise { const awaitedTaskIds = ["task-a", "task-b"]; - const userMessage = createMuxMessage("user-1", "user", "Wait for tasks"); + const userMessage = createXumMessage("user-1", "user", "Wait for tasks"); - const taskAwaitToolMessage = createMuxMessage( + const taskAwaitToolMessage = createXumMessage( "assistant-task-await", "assistant", "", diff --git a/tests/ui/tasks/bestOfProgress.test.ts b/tests/ui/tasks/bestOfProgress.test.ts index 12f2c13fbd..132d70ae5c 100644 --- a/tests/ui/tasks/bestOfProgress.test.ts +++ b/tests/ui/tasks/bestOfProgress.test.ts @@ -24,7 +24,7 @@ import { import { detectDefaultTrunkBranch } from "@/node/git"; import { HistoryService } from "@/node/services/historyService"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { installDom } from "../dom"; @@ -50,8 +50,8 @@ async function seedBestOfParentHistory( historyService: HistoryService, workspaceId: string ): Promise { - const userMessage = createMuxMessage("user-best-of", "user", "Compare the best options"); - const taskToolMessage = createMuxMessage( + const userMessage = createXumMessage("user-best-of", "user", "Compare the best options"); + const taskToolMessage = createXumMessage( "assistant-best-of", "assistant", "", @@ -290,12 +290,12 @@ async function renderCompletedBestOfParentWorkspace(params: { } const historyService = new HistoryService(env.config); - const userMessage = createMuxMessage( + const userMessage = createXumMessage( "user-best-of-completed", "user", "Compare the best options" ); - const taskToolMessage = createMuxMessage( + const taskToolMessage = createXumMessage( "assistant-best-of-completed", "assistant", "", @@ -374,8 +374,8 @@ async function renderPartiallySpawnedBestOfParentWorkspace(params: { } const historyService = new HistoryService(env.config); - const userMessage = createMuxMessage("user-best-of-partial", "user", "Compare the best options"); - const taskToolMessage = createMuxMessage( + const userMessage = createXumMessage("user-best-of-partial", "user", "Compare the best options"); + const taskToolMessage = createXumMessage( "assistant-best-of-partial", "assistant", "", @@ -528,12 +528,12 @@ describe("Best-of parent task progress UI (mock AI router)", () => { } const historyService = new HistoryService(env.config); - const userMessage = createMuxMessage( + const userMessage = createXumMessage( "user-best-of-historical", "user", "Compare the best options" ); - const taskToolMessage = createMuxMessage( + const taskToolMessage = createXumMessage( "assistant-best-of-historical", "assistant", "", diff --git a/tests/ui/tasks/reportRelocation.test.ts b/tests/ui/tasks/reportRelocation.test.ts index ad3d4251f3..20d4077331 100644 --- a/tests/ui/tasks/reportRelocation.test.ts +++ b/tests/ui/tasks/reportRelocation.test.ts @@ -18,7 +18,7 @@ import { } from "../../ipc/helpers"; import { detectDefaultTrunkBranch } from "@/node/git"; import { HistoryService } from "@/node/services/historyService"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { installDom } from "../dom"; import { renderApp } from "../renderReviewPanel"; @@ -39,9 +39,9 @@ async function waitForWorkspaceChatToRender(container: HTMLElement): Promise { const taskId = "task-1"; - const userMessage = createMuxMessage("user-1", "user", "Spawn a background task"); + const userMessage = createXumMessage("user-1", "user", "Spawn a background task"); - const taskToolMessage = createMuxMessage("assistant-task", "assistant", "", undefined, [ + const taskToolMessage = createXumMessage("assistant-task", "assistant", "", undefined, [ { type: "dynamic-tool" as const, toolCallId: "tool-task-1", @@ -60,7 +60,7 @@ async function seedHistory(historyService: HistoryService, workspaceId: string): }, ]); - const taskAwaitToolMessage = createMuxMessage( + const taskAwaitToolMessage = createXumMessage( "assistant-task-await", "assistant", "", diff --git a/tests/ui/workspaces/subagents.test.ts b/tests/ui/workspaces/subagents.test.ts index 2b5a161022..b409d7d74e 100644 --- a/tests/ui/workspaces/subagents.test.ts +++ b/tests/ui/workspaces/subagents.test.ts @@ -28,7 +28,7 @@ import { import { detectDefaultTrunkBranch } from "@/node/git"; import { HistoryService } from "@/node/services/historyService"; -import { createMuxMessage } from "@/common/types/message"; +import { createXumMessage } from "@/common/types/message"; import { getWorkspaceLastReadKey } from "@/common/constants/storage"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { updatePersistedState } from "@/browser/hooks/usePersistedState"; @@ -333,7 +333,7 @@ describe("Workspace sidebar completed sub-agent expansion (UI)", () => { const historyService = new HistoryService(env.config); const appendResult = await historyService.appendToHistory( parentWorkspace.id, - createMuxMessage("parent-unread-message", "user", "Mark this workspace unread") + createXumMessage("parent-unread-message", "user", "Mark this workspace unread") ); if (!appendResult.success) throw new Error(`Failed to seed unread history: ${appendResult.error}`); From 12fb74ab577f5d769fb46e5f2bfbeac32bdd82c7 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 12:43:45 +0500 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=A4=96=20feat:=20create=20a=20disti?= =?UTF-8?q?nct=20Xum=20brand=20mark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the reused x-and-cursor icon with a compact four-part convergence mark, regenerate every tracked square branding asset, and add a non-writing freshness and image-validity check to the canonical generator. Validation: - bun scripts/generate-icons.ts update - bun scripts/generate-icons.ts check - make build-icons - make typecheck - make fmt-check - make lint --- docs/favicon.svg | 5 +- docs/img/logo-black.svg | 5 +- docs/img/logo-white.svg | 5 +- docs/img/logo.webp | Bin 2390 -> 2390 bytes public/apple-touch-icon.png | Bin 1956 -> 1516 bytes public/favicon-dark.ico | Bin 2160 -> 2326 bytes public/favicon.ico | Bin 2087 -> 2223 bytes public/icon-192.png | Bin 2136 -> 2058 bytes public/icon-512.png | Bin 9148 -> 8461 bytes public/icon.png | Bin 9148 -> 8461 bytes public/tray-icon-black.png | Bin 311 -> 300 bytes public/tray-icon-black@2x.png | Bin 500 -> 466 bytes public/tray-icon-black@3x.png | Bin 723 -> 654 bytes public/tray-icon-white.png | Bin 312 -> 301 bytes public/tray-icon-white@2x.png | Bin 518 -> 459 bytes public/tray-icon-white@3x.png | Bin 751 -> 645 bytes scripts/generate-icons.ts | 137 +++++++++++++++++++++++++------ scripts/logoAssets.ts | 15 ++-- src/browser/assets/icons/xum.svg | 5 +- vscode/icon.png | Bin 1043 -> 1143 bytes 20 files changed, 127 insertions(+), 45 deletions(-) diff --git a/docs/favicon.svg b/docs/favicon.svg index e47afe46e3..336b7f9420 100644 --- a/docs/favicon.svg +++ b/docs/favicon.svg @@ -10,7 +10,6 @@ } } - - - + + diff --git a/docs/img/logo-black.svg b/docs/img/logo-black.svg index ec307fc548..0727673900 100644 --- a/docs/img/logo-black.svg +++ b/docs/img/logo-black.svg @@ -1,5 +1,4 @@ - - - + + diff --git a/docs/img/logo-white.svg b/docs/img/logo-white.svg index 0b0f294223..f152fac65a 100644 --- a/docs/img/logo-white.svg +++ b/docs/img/logo-white.svg @@ -1,5 +1,4 @@ - - - + + diff --git a/docs/img/logo.webp b/docs/img/logo.webp index 12eef9a8d1a41a23df7aea3ab32e48a95aa19d7e..e2c1f8efde0d83e83bdd13d08c7561548c592910 100644 GIT binary patch delta 1860 zcmV-K2fO&z64nxsCx09TZQDo*f7tu)2t>pL@ShzkrXfBEn5e~14&{NLgK4*&Oe=6@ax^_imf){jpn+9TiH z^i(OQ5>?7gPn}ED8B49@X~j`nY1%MUO_nPBRF|YGJMGEQ4ma&f(JnK!$WQ|>wMkGD zE4AXUk(1id*UU&g*z4eeTViV{Z(C$d zrEQI?v8?S;HJ3C2Q-GWqh$5s+!4o254w@JVlduHImxUxsx-=YNvgM(OlPu92f^ubg zK~kzz{isa2>RE}BwZrmcE2pJN*Nw}PubP*ng64o6HGdQ*q^P1dB10Xu83`(Bod|y| zrBk7=rgJjv&8KoY>K%e(LFw zM!*b!GX`P=lu__PV2pzn17RerAoyY-ML`!0Ck(cDC~=TQ#0Z2dCPE}sQQ3#W6qj{N zUN2zx-+#X^TFUD*d*P2#$ev&H&({`LzNsuMPZ8HQ#9ttoTxKWWBd~L#w@3o7(@a_gaH1T_}yNbE7l7%9YCa8h086C|oL3z`x~} zK!3lc*TBAK7eOwNtKc@cWl$^BI+z`1A;c1~5?+f}3av(~h1FvfLn@N0;WRnrP^y%A z7+pp|gfgKb`?h^a)^+QeZ2PuFSuR*sWw&8hmeqaDIfjr!{AOQfD&peL2udxQ35 zYK=l`I<>{2EumT>(UMa8vDikeD=%pf-B_|SEZa1+&Qk!>1~awHMwmuwWYQiqH4re$5Wlw_J7c{ zM{4_6+NZP@B()G)8;;uOtQAGAWY+Et+o`O_3-%CMpZ-1?>s8-NV*T3tDQtxD3j!OX z`-Z+oslFnwahmU_>r(M0aoy^@rLAkV*Q9l?^`5d8N*9E+(Yc|kmC6-a?KJMFYN>EZ zR9pTnO|9wIB(-PXQ`;?LED@8%YtfQMtC5n&>QRzNDiV^( zY0{BOsgjY)=u(kPC=-!w-=-nmx=upAZJ&Y)mJ0;bu-g#7iq(qnb!>J-uVk?#crEu^ zVpns$CUiZwdm>kKxhQZ=cbno?b+syN^BU8u+u`n_X|S3xC=C`kG(F z2=qiCU<}%05HAX?Q3w}@wm3wKL`x)s#bQ4ev7)gTjZpEZ$0JfiY7q$(lWk1mL}eY7 zFmXALi{MK6=%WI5KRzY6KVN&{_fp87f?F0kg5#q%LULoIHcDdSq%~4fW2H1&LgS?~ zVlrctc}wgsY3j6%wj09#v47x*afug{a#DQDAsd@PvjY2TgET zlCXq_BnwG^IMQ&0h$0U~kQfp%goz*%L7?VOHGimP&oz6n=1w+uxPQuR<-odi-N>qa z)zF#?nz0o(6oc!n=tWoCQ46oNq!nLjOKAX|HJuSu_Ed(@SkxFpVN+oc{wjYI`YwGK z_A+}M@-}%O?mBlQ>OOTS<`*!>B7OsLFkTAYXtW%(;aEvnvQ<8-p=}b{jBSz8VsQ5<-H&c`oUVsA zHd43a8yTz10*s5+T?IzP>#77}B6d@Q5iz?cLVwhLuR?F!el0^^9zZ8&LcQ)FEp>m)fZ5lt=2 y<%gC?_t0s7`S0+5hyOeL-{Job|9AMm!~Y%r@9=+z|2zEO;s5^iv<0(K2L%GE?vXnH delta 1860 zcma)&`#%#31BY{P6tByKR}9s18FEQ(M~Ol<)imO@l}qtr7-48GBe{g?U{s>H4a3;V zeXfT&$K`c2Y~ERBE6j1(7RN1gPXEFAJkRI*)AQ4psmxUVSszDnm}8nl81mI)Z4q;A z?Ctm2MgQ_aI(aPSGG~#~m2^%}Df;70b#ueU zJjwd$i)}xf=WJ`6c6oZAkqGB6X0H*p*iP9Zjg9OhW}?ex$2d;kAbD{VAS!GATnPm~6eYLj5N(jWBoyI!W2*2E9s@0XjZE+M{ccR5T?U<$F94I=NpH7E4COel*h9%@Y2F( zhjl$#+s)=@~!30rBn-IM}*#RH4#c1&@K?VosXadW%R-K&D5pXjko#f$T(@D z-}Bc}+xh%9K(Z_-IaXrxT%^i zx>mvSZ#Sp|9vd5w1VqV(*LqQ7gt~b$pn4#ZrOixt1J2287w%+uz<$A{P6nl4bJHkM zdL9*`5pJ+uYGK`5sQ+Sr`hzC-$HaxEe~CwR{_JiMlLET0A{x zn+MoMV#Zdw&&Vbe@^GJ%#zQ#6Xp=xElh8oR!TQR_ro-ViON6`$$%rLw7s38>wfFL% zKc|3+pdj=p2y)f%t2WU0gqf$u%lX)VCU9Zn%~_*Ez5ItKIxa^ z10mlxBpfyJFSI1TF5N)|&q>8!bvsoqMwD)0A-$i93T)OD)-alJGWdh+FbTW@wi2#6K&xkrXfiOoYO2cD8qWVFUxYJdOKp4dx7!}d7kPjv zuT2ffA?Q;l8E{rW46+HpA2X-bwWp5KXRPJ2jKpTNgjbA0%APnL52YdL_V`{cB%A7) zO;~0-y}?<+<-#HXx9;$z_41Plj2Oz|$nSV~l`;Qc&@0(Dw(FJn9QO8P`&E?BVBW%6 z-T4Rw=PPh*aG;eiQFkLm-um{wParlB5gH_id{tVE_-Oo~!|`eJ(D{m^vru8~ILqKR zKfhRXk?8j#NW!Cf6gxZMi1+2H`Sie^a+i>fI9e+r3q-^G+emyLPDuq|Y|M8xXs_ zKGXJcC1e&@y#9n6_U^{&*CYVYl>VUGcg+@Bo9SRFe(LE1Bg9L!nx>&&$m*teFW`hf zMNaPbxE^126<>&axoJ-H0F{^SkUg;l&9rPb>uREZk@E74IA*Y+C8r^mX9%`mT0|W| z)*fuH=dv!t zTWUk79v-r()4t3}z$HDpP%a6FV8N+nJXDn`yqi(AnC!eB%-fhptc)D3$)cK3OG>^4 z{DZFE@Vkg`oeoYkSw&|jur-Hth#dz5N%YGHv+2`+zlr{pJA;^;$Xa(44=!M9;o#mk zB0sWMZt~Aj3bkS4b3#>IIy>mzLdusI^5{`-*hDQq>x}GITz#?)%-A9eN5MSWVBQts Q{vqGKMJm3(EhGKIUjWe1qW}N^ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 00820d792872cf9871ef724b4a46080868ff434b..306c5bfe75cab33a39700296d1595b2280a45122 100644 GIT binary patch literal 1516 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD4M^IaWitX&oCO|{#S9G0FF=@aYjsdI0|VKjX*6`ZO0~Tcmd%*tJ49uD$M_@w4BowPgXE_L?>K%%3suZfI>;?gUi9 zzq_Hg<@q-ti+gv&)E4#C!ZKUfcQ?#!Ils@N-~t~ocvWhw0-KtDZR9>O;ls~DPQF*i z^aNZFsn@7k03F~U!#V%poCgd1fPtz~<5dS#xPkk~qz~0TLNZrcV-;pSwmovZdgk2s zs(u9l!0^rxsw<6ZYOVq+I$qn!5KI}OM>09C0^1v`IVv{)raV{>dxKR+#b&{* z2McO%uEChX4~@_7@BeY2_0d_z)DX-u%Fle@S z?mQW|_Xca8Xz}9JK#%O{*%m){+9$3arhVlsf9>5fYSwPQ7C!{St$FEB6UA5bSUp_%UL-v=QQTtcvE@!crOmMqN~GrB&MpaK zD{J)5sM)mZVr1PLCecQfnqL{+oA=yceZ*YYe({Bld|BfQhoD0lCDVY3Ge;pa5t_ss zm$Y4cu_hZBZLE{z+`xGuVfn}JGl0@kz*sf;_iBq<`rQ6lh8+h5Oau*Bk`{J4yv|P$ zXUH>P@nzv@jL1;B@JspzYlAe8qcEehg4isME&tfc7$2}XUvN1f(9x>Kw03{v$kG0r b?LNbbtH*TxLtXv=i*E)`S3j3^P6r>JR6#W4z_&_ONjioqFPS&PbzQzp1Cz+LwyW6IP;<}0(rKTtbKERY}-Lz?3 z(-KA9a5CSL4;IZe%gRka4KxgDRUpkU4GmOW{44gu?uUENr+e?5bMM@9Bl0vBZenKw z002DfcyN>+Iokm<(r45wqa%7SPCFi*0RRx?c7PsHqlf?iJrfoj@LhJ*B9k9phTFwZ z{W7q(&izE=&BP)^Kwwev-odiAkd(sbu-h)M0`Kxa%HJP=Eg)U4+9V31 zoO;6C+nnk*0{m-AEv;lf_$ru}tAn>|L1V(@;ke9)KHM z$Lz9$mE}M`K@dmXX-42+M>Dv=iGIi$2xh+v0|7Z97)D@c8GRinG&f_>fgP|Q2>rHd zAQuRP8VN8oSYa(4^Md>i4b(y)$&MI>y}>z9V43FshCgd;dU*)ek2zQ+Qi902TK z2)er`!bS7n9si|(j+b;@>LRP8w+ZU`*Pphsy7m~eySv55N`kJL_ijl*6m#b)6jkIF z%d7BE@N2q5if!ilvWP??Y4H|5fo6lTojKI|;K7Uj+;tl-@eRv;%Q55yQM0#)hb4}T z#}AYrj0Ow9%DT-EGMSA1^IlaI=jLvoq?pvte?MJS3hPf2Q>45~*K3pMR_YHAnwtKq zMp11Hmvo!!m0b=E5j9;0hd<8@6L42i968Z6YV5H?_k>9FX1EktkElL&<%r_wg1EI? zPas5WJRpyOvJ3?oMcmF#k8$t#arUXsmdlD!rqZr8wy)z6Q4HpspPftPM>&7YUDs+& zq?A3r=|dfRpcW3ljuzA!3ZBv^PZ;pk)kTR*S;J|jmVd?C+S(h9c0j4-Uq7sr1IZv# z`^mQ=QTdu^Myp;BW`u@@Ueqkl0j^@gk6X<&EQLa;qoK2x-mQyf2Ivr>Bu}?7m8Ft0 zHM+wbfLnoSJ>NSPipAEms{P2LvApaZeQ%3QZI`+E+geJ-sH-2_55@IQo-fr?l#JaE z2gT_4y!^%T87!B}l^d)~Oq5-k>Tp)AKh?{s0ghH=rlqyvSqaD?fOEoie||Wnfz5u# zZfICxXrz5qL)?AaZDuRR>Pip7a)Cy@AU+cv9lf*(;s6vh-JG4fJR6q8%D`nC2wM0( zE#!WDR9swY+~d4gW~82+FA-v22#1FZUjx|;QOBaCM4eixR07Ew|2$wb+}I~&x-GCI zq6R%R%JefmP>tH|s;et?KwZf2t@h7P87X&#B)eAhShT%s}JU^e2A{`Q)(>6bUfo8*~Ci|&m zkNwA6UF^|0*&?sO;-N{QOorUF+7VpvomF>0&c}>7eZJ^dq7zL%(8s`RZ!cNP3|p5x1hhGohNnU^MY~ z8>NcQIRwMaK;GDRfV!p273PUQVMWd72$q(X!ptYdk+d`WYi@3KL!>oE43(i=t7)Cu z*`H_aH&`r?%ahDpX>KnQYvFLX@S?20kEOmUTw9db;{0=Ntt|9z+_4%INT2P~`jh^D gj{k}^e4#?OfV1sO0v^DH>1`JX3ppL!5{NJO2j09^*8l(j diff --git a/public/favicon-dark.ico b/public/favicon-dark.ico index d8b515095a9d60c11e40d41017c09ee755e00934..7302ef386b5f8a10225e828470274d442a4032e8 100644 GIT binary patch literal 2326 zcmb7Ge^3*57Ej`~F4`8ph6{))q(Q%`mo*i?p zrD5)s32(2KUfVkLN!7)3H!FIdm!26&cHH^u*BdAM2AA|adbso`4D-Ja9uD$Sss6*R zw|*YrZI5~{&gy8ivIX3uDKT?OzDem8{DID)dk>Fp{R5-JO&lAExsNcBWHJ*eeXGig z-hs=U!QpBnhgYW1I@w0JqQ<$v1-T3OYS|jYUbaTEl`29W!<9IR?-~rTMH7JIIib0tCv3atH~t>cKzAWWF8B5N8lxe(mI z4Xj3zG&@+it&+{7(63J~qK#S^o##PM|CUpwKZ{yj)qr)k4kg4CRus(smhf&#p2ZJ) zdy)oEJ%6ygcS@_3O^&gw*x26mB-{Otu%C4$4L#d#Lb{@)OsSmcULD9bHG?o0Yb-#z zl+YHq=sJHlknKAM!U!ER;6ZcWlhFfl{NGh%ak_!~i6X)=^FLtRM%Iy!*e3*OgylB( zbWF_`()vblvlOAc^SPzdWy3J(Q&qxR+;m&Srvp$_UU~3RXs>HoAOFd>x~1MfPb`aX z|AD~h&OV1pIL040_TT%sV#^F!KTRa+u-|6T(R;wM2;uxm6*(v;6u4;x7uuJP{5+q# zr6e~n`ld!u*2s3ga+u8&6d*S|g1NBM;lAv88Zn(g*NaEHdp_sMN|ZIy&N8Hs)~wwJ zZpQK<>ApCgj#H8={Lasy*_rxp0NXPJhvuH}C^E&bNtWptW7`$9X&iADoJgY@3id>f znyQr@=*kOv#1Esn;|LOj=^v!~uCj7RB|A?qOQD=*gfb2Re_I6XEJKZj@bwAEKi_6X6*uK_bEp4_OVY5rk(_1gx|33ASJ=FNY;)i6-k9C+vdw z=je93;1;P&+t|<-?EAM7-7icR+0vILJ8@oQQ1fTRPX?|+3{uGs!yapimNJ7iVjlEB zB#imT)ba^HI5E_J;hrUbu=CVVe`O(Hl42}2>i|Y;?2kQtPZy?^M*<^YI*PGk*x}0} zU^8b#yTJWwFS;Pkf?bJlf#xg@Z`N2a!!9TQc~BW>TOUZoLPM3KI#52Ov=kho*K8Ux%4Lgpt%0Tr2LQ;D{N9*pF=P*WmnC zK4IV_2Ngl`z;B!-8%24QkjA+dd3~E}x~#mHD^{Dx-|KNiIde%?qatOD&WM|G{zB_FAY(jGD{h6`gzaE*5T3sA$}xkfpU*nDos9u#|DcSoT>zR;OS2R&5e`7@yGp#2R@*^v;yR% zQGI~v->+YjZJd)?KK&?n8;(*scBLD&>PtD?^d9Eh@p5BU6b(C|a(|wWP|n#FtbG?q z+_sc2hT4sr5^24T-&#EA8dY&x9!5N3Yux#`Y3n83aA@E}f(Rb5Hv5llyc<3FzihtO z8ZNyfxM9IB+uWn4-G~QF;A!#RX}m5X`LM2+&}m9tx1CMTzs{+$y3#0ONz}vdvn%cr z-JOpsWSZltl=tYSC5MzG<0s0N;J)*ecU{M6xiT5{!s!N9m-{ZJErWMm7S9ZrJ8W(r z>;m8KX)m)TT{AzJJ;h519Nrf&t4Y;<=|SK^E)RBtg{Zg{zRv z?!3>TcWB%*Gz$=4LWurg3#MJn^`x7C;H#@~Z)f5skZIh!Nh~R>Mi(h?@YhFanNdAearEJyFW^sUTy0;+mH< z#(PjoL}&e>EpV2DF2bFX5#TSgWOOeS@PdDds4)F0s5b$q%eOX&1XS-4F{0X10+qZ^ zC1pDM#k8!>6=DJg?w2H){+Y_M2KNppLm#y#jP4Eoi}}BvKV5$k89vRCP<_Jl QY0z649-l?p~nO=-QY60JSFP13W{__tqDTL%1SyNxV1*MigXt{tZy9}VXrsXNi^Y$O z<5d#{;t?`Hv`C;G=Q0#Jc$)K8mI_@B_>bt| zV|3zeXrO8Y>(IQN0j566OEVH1v=Rx7f=C6%+WtQl$9&hwU2tfeItkZUiJoe?*n#dg*XxLG>o9}X)#{rFj}jPL#_GTuC16B@s++%V zpHmoB(L7yE+bx*IA~877{2|3;eXakv9X@>CX{;2Tc+*mvc|eFkuzqO>cZ&Y;rTA)h zS8ahwzG-W1CQ_&$R0%OkGPK1gX++~Nd7cX(?27t|{Lo+Ng(4uYRQtS7&?qi!x5>2! z2Gd<RM9_MvZG$yayb%T1o^eFobmUj4GAvz7Wbcq_tvY z^C&Z^s{g#zMncr6?%Dpof96zOfewzVE_-|ooajR+Q4jT+=yj+jdcB%pUpty)H<7YG zy_{AxP^!@LD{l9+*_Be5SdZJ#xlZ`V>iO>uY}>)5);ZvlMmTLZ#L6=S%5VZ2D>!aw z?+p*!SHu|G#?SO_{~;mqT7)GMH$SF8nYm@E zFsP01Mb2AHY*2Lb@^Q*aun-zBR9T3T7gTI=N&=RIHz^n0i0N*s`$n3kWX?Tg^@e+TOjR1QBPetH>lv<4JkBtnfw2xgV2^yM*Q8#5*H;@kLIV`5Z#mFe zaXty)oV5&q5%v}XkJ`8CE=)y>Y^`Sy_A#}?gCh-{(Sr*6kgh_#7Z&2@(?25sk3psv zsB^kUw9nSs!tgmJhn1auYV&919|L#DYSNRcNb@zQ1h<7#uq;~NhjCU21!Ztej<1r} z*uOF}V%9EbE{`TyMv!K1kx3}Hv0$4EH1{+!fQBwD6=2Fm0?(mz^gxP?d+W4azplm#5*o#w%t}0?1aW%_e>SF~jGT%8FQqJa`?lq#$W6h4k^m|F+|IfX$ z2gSdi*JSYV16!-IY+t#ae5s-kp?8JnbCi5tJ*J6;Uwyy^$+G}E?H0$>Y z*r89I=!h9iQcT{}al6lq#s)$b@;M7_1np~W4=BhD;Rc0`a}vB(N-&Cla}zqMX|wv? z`H0^?A@>qeR-;ab`_5*MJS+FPi?sgHaO5(rP+W0ZnH9tL9G77bink46Z?v|<|FFs^ zT5QP}DSrI-0#5!MXwhFxae}0UcEdT%j diff --git a/public/favicon.ico b/public/favicon.ico index 325b9bd99ec2291ec83f3023e17eff4b926a1139..496f3b40d0891693b6e551cc22882d199041101c 100644 GIT binary patch literal 2223 zcma)74Nw#584gS8t+uuVDnj@P*VTI7dGgmge$-3+OAw~QP;*EtBo1?8K?Rgvupult zJxS`4(7E&PM8XwMG;rZMv6UXy>`Vi>0tTVdR1kxS96u+QTd8J&Ww+lh=-kZBw9QOr zpLxIMeV%u|o#$PB)^6F#6~Yxf9&hEYv>n+zUI2grJa`#+>^pk(0C+4fP0KyT<9$Db z{Ri+qst@Dw;QhOHY~52)KYd?pSTh#ocqlUTr>kG;3Ah$wFOx)s%?6#^ho|xHp9rb= zV9Tjo`N>CthgNT!N{>`txkBG`yXQA&zuz)BY3aMsQe1e2wg1jfndROyc(FsoXIvM@ zi@#ShYcc=RJeMUe+0wS%!O*k6)S&(oALmfR`}ti)bQ8v_=_=tL^&~;{!8XAcdN*8} zPDx|dRU!C#B0^hF*7BO|{}(}Fsu6{LbT^ml%0c3qt==Rkg=mv1H|BJ*3W{1h(qeGK zkL)&~_QBV`TS5(AKdB(G z(IZ}80+h(kRD3%aor8OS3?wY^zoM=oaRs0++0{xk*W$e_InwGdB|x57r1WP*Ay!TH zoWc9;rn0p$^pmpDgPuMzIfg!($)fgk02O@0qWD+nC$>P@yjs3xS^IFqyVQL6Bl&~P zwl7EAsv)AH!Aoh|TBiQNi+y{jZ17CAiXMt$ZgexpF#J3cLkBZ*4Bv~yR1pd%jKu?U zAEkgip@OS)IbD#%Z7m4$21 zb=u;gwe&^|@A0_9v`?Jw_XQQXR0NW>alRuCiam`M@Vg6NoFDyL+v$6<(NBny10ror z$AYFe6;gXm8h*6LSNLZ^#bL^7z_(MH+X;}YQ#=fBw!0O6L2@dcD;<5AK3*QBwR(JS zK|dn;nup=~DAXmW(4qNx(or+nQHwX3>9U72UDX4!(J8V6145ULcplMgtS0p$ZHG5A zDu4(PLrr$mn~-N+6<>N<{0`rqNx_(qnEWBa+2R2AJmMnmtXIesbA9+@ z!nbJWaVK#j*=;2pMTfdMuOQ|YrxHVJ*Z~);=3Af7Z{^QT&EDVInk0XE%UoqP+wLzY z->ZCEF5WX(WB#q$e?sfB+can2V6R{O0OJuLh8iLwU6GRrUEJ*3QoB)VW3s@Z2 z^|l^(Uld25L`rkGvt|Q6!u5fBZvrzM3_-hLER-WP8S$%&Hd8Pa&DfSzosccSDhX^ht9g54ugSUb_XaBnDJ%w2ecb{X(i5a~waJ+FHc zWOV?IO74xsB6Yi|c?-yeg+HK2-Hrg_oH20HZu2#Oz;Dr(d)%smENZMvnM(LNdKSDh z?ej;}Lj--?8P6kpoAe_2)5CI2p4_xd_v$nWHm~ok*}`?Si;c}Y`~e+vo=lQ9Vv-ev zD1zyNKNN&rBK!1gXkaXz8t>w0QiLuB=?XyNen2QV+H2YlJ=4Io;(2zogbkc@w8$eu z=vu)mNa-rVxQdfYzDPPSPOg3@Rp8*{oZq<2bL}&Kzm!6&e)CsVUz^77H(Nt!y>`@OK<>OwJCV~9oTyHz`#^;SrCLe;SC;N z!O7=n-1!`RWq6K!-Al;iShdtEHYG0Uq516QKL$L+GO%F31te5tD&*-44bG~O@}mjBv!KJNdcaP?GGAU5`_nW{)w%~E&m M+`YrHP5It`0L9ftRsaA1 literal 2087 zcmcIl2Uk}3SLudg9TNp&7cbFKEu7Y3$hR`IT3C~vOU=SiaARsM-5F~Ri{)V}0-F5EWYn^X@`#ay+JKe1j9*y z|AZD9Z@q?2_R|qKk02)tIgbgWI>5}qGD4qr^D|nql?Ps!ieRZG>~zEw^JGC*C!+nl zl#1Q2@P%?qdw0mv)s0qShs2B}6`j_X%P;H<%v^rZZmHehhC+&v{S~n^97xNdXO_HM zL3`4|o)>QZhOjQ|H2E3Yr0u7AmN^hxwGom+NGR_!leCfO9L2kD>xKI(^5DfBPZaWF z?k&+K8p0S%#)KLM)*0eIR0v%|vAB>a=XBkto}eN(9+u?Xip5nFw`aC|bh+e4Y_ai(O#{2@4?{_bE^9RwOQu6dVuB$# z8n{2qxA33zwf1vjg;V zbhQQiH)6w6=}qUsD)p&R=-|C2GM9QAQqyPBmrK<$5w(#D(ynQOg5{5R=j?_|1JURS zEkL64FSC%&IniEPt^!{TM;A1UA1j8xBx zsM*xSzMCf_nZBhjv>`@4=%fC7i_7hop;OYomva=aIK5jhjRI~F6OYx3zwiDp<`r^y z^;8IKy*0b97Y=GQ=`mu{__|JE%pMY|sI5L(hy8cNLfWhrTE=$4&0DhX5e&{OA&u;3Y-9F9$iJ`6*3M`->NCWaQABE(t?@2F)=5g3 zaku_Ch#Vp8MBtuKbxE3=;fO-U{SnQD3eAPnw_|F`1w+?ApYdj&s+>ip%<6(k|EQ!R zMG?5aJvvguX0lF95LaIz(5e|vpd6JOBua-6wlbbk{p0pjkH-ZzgrrPRXlbL38{+59 zWmw8gg451E&Ri0r!zi$Us@f>anhij?Wq&ao^MVv~7<~4S(sBI^S76DEK8MR-ZCPMl zpg87pYKa>E$~@T0klShN9T51XOQ8#Ggf&W7sgvHdv?d{W{b@J=SVDW%9Hy9?0ZC@dg)^~{RJ)k6>k&MVs2!3pq5qB zcor0AxIuxW7FnrI6S_vo6sRy0g)=@+UqbiTfL>Q8u(qBiXcTeYqgWA4IZ__i0NPSH zZ`i(8<4;*(-S3iWFZn^Pi@O|DwcprjLd?n>BXxliaezvy@vGkVfeop$q>w&)3LFvM zs$u+}2fv#new!U1ims0B5)r01IZLr_pLp^9c`u=hmLH67btHO^M7V3^P|m7(uoW;R v?Qb*O=onPyF8fH4>vrP5FbP4(B>grK47+uKe8Z;H8%lveIoZ{mzj5~;TjF<< diff --git a/public/icon-192.png b/public/icon-192.png index 86b052d5ce8019bde7ff0018bb8f4eb92bbe3173..cd51b49f1dbb643742d6411cd6cb9cc0ff1e557b 100644 GIT binary patch literal 2058 zcmcIl`&$!d7M=_PBp@UWRuEP)+Ju6F(3KUaU=o2XfpSr(Qh_A8RS^&`$fckp83e7M zAePJH1z4?63PSpTfixmXrM7jgupk8iNw`?-0upbSKnVMN>F)FFKd?W{H)q~CXTEpN zyz`zfq9Q)Bv2wIR5X6QT60`%HmGB^2fOW@6iX}Kvj)Z)gh9G9o;6b{Tvm+TnEN}CI zxG`B*X0LXHuHiZq2)x3Xd~x{M?C2c1U*2WZOG29yS`^cNE1cOSB z@8sl_rOTQ7r&d>Plyo1io_f-v*rX_3`EhnlMvl-^%%h?tu^6WvKyaK)RC72S+L>fi zTqp!cR{{?4;TnifAz;O*-^2V!LL$b=<=YSv7ylI{k+?MX0yC80G+LOW1RiT^gJF_E zDuQ8L=~@di8U6OX6=X6Y3}e!0*xpDl+8Jl5n=|>^3+F*=VQ(G>?%1^&0V$gIX&`0X z(G;(90HsIm$q?V{3UNOKtg1XOn12y6ByP|kw_A`oRyYQ^HE>Pb0?h!oc{9L}mU4y! zxXq9OLwt}PCMJA+eWe2*9M8CwDaupW zmIch3uk>S!jDPCPT{=5FJgn1><#kl8)d~cHAN%^EPv~F%z%RakbsjyN&ptQA4AIs} zux;YMYX@9ZE{2ergn(C!*50w^VB(PWJqbS(Q%40+!TX>Y$q{#qAB}LWQ2f?b=8WZmQ1dOC?`9uBi@1 z0nP*X1#NMe(nb_WAGEIA$4^KIcAHpyJJ*p>p1v$wWZx)iXxOm$B1l{n?(6Sgw79tF zx^E`O#g(P+%Q|tQD6)F1d~L9J6%)x_%W0q^oxWzR>q*+eYR=72#4zrN*I`sDb^HBx zxm>>~Y&D2Q*)huegAWgS*8IFlwYZoQA0OYP*MEMyqeK3WFD#zV;~Y0SQx_eON^u^} z@4P&ZUMdyZ<+fqPhwiGI5L=2&HCL*6M>o6*JX)dY_8@6DycwbHq0mR3Jb66U%~Ew& zv*VT@+lj(+NWl+^8SIDaxB}f&Iikx)H*k ztp}IO)6I-_7SHQ1hQG0p+}VHKWXQnRur~z4tG-?A#IO=!s%{ayUC!G_)K6#WlY~eB z>P{~FZrQk>a_nwr7^>N&<}ZwW^9gA2FzAfU@7EcPj;tehJKady z!s4|G*5D}y#k;k__?gGa=h^AJRM44XAifdpRJqU|i0@9ooA*=dTjs(Vek&IWfQHB= zPCy#Ztl9KL_2|MyS*zcCdVX}+A4;1n0L{4feK9t^0;JV5E3J&)SrIj&H;Rp6k=pmT8|t@$Kcdx&P5)_436RBU2IUlT(}jHimFM;HmF{V` zb_d@S2t8*D=NtO(=lZ`>bHFYlB4S*xfBefsjgPGC*q+<`=84y@AKAE}k6cxQ1Mhyv ziuGg|Q-`c$zrvzB8v0nO?`(7xp5j2FUJ!FRj!564&FteVuQ4m9J${ihW82I{KC|ZT ze3PjuZHG8Gq-1)@kj9gy+QDai%_l$~Dt8m<3RunQr?=mE@aHmYS~do= zlUPSY<%x}0vgUA{_ycpUD9gvSNQEkMP=vz4`3_?dhE?~?r3MuUARu&DBGf8tg6s%|NFSw20bxX02Fea$35%da1S*XR zSP-;;jYmD>z*SuVnd-f1*fa$KMNtbgf$)ZpyRMl#!?j_WQ zOu^YrPV)F)w$mJ;n3I_pV961UrFM1+tpi?|>gk>~p!o&d0;GY!Bd=STqls)vq;RPE6w{rh!@4~Lw~a3dlPOC;bslq%N_g%hM4 z2oKYND4wE7bfGTE6v>EM732x}ZuL~is=FPjHF$vbMIwo%%1QuUKwvhD0z`GV9Dof< z0DKMV38tv`<*pTK_uX`0;Hm@$U-#=gx*=`AQ*cI?Vk@w8ujm=sa#kYm3AFcyx^LcC*k+W&s%~En!1Hr>rhbRj+!QU&iTpRS9u! zqABA0w%8l^>#qdz#x9&}JfJ5X{ zTE4Q@;|sQ&)g=RN-KN+TyGkqV<82Rftu(QWpYBM}k~SOoP?=K(7tI4Z2d&L|aJa?^ zy@5Auj`S^ey*ZGupiX$$r57${jyx>;Dx%;&W?q+0Me7~c;4`6`ZijzNwx(@L)60Fj zGB&L9m3H!9{2I6^s(((RCP+k!Nz?5$9?_jYym2YHWqz#rUBAsmLoXAP+KxogcRw&F z1k`!%@Z*LB*QSh&=*98d&f=(v-_K#yr_qV+XgvAU{?$Dtw)_6CQ(Vx@lTij=FcXH;Exvj?&X~*>A*PL!VJ%ANR(t`xroNl6LzE=-py@_ zc=1Kkfv{&4)xw?WQpx9{z!ErQJ{*uzVYqP|tmu8Z)hJy^%JC4`r+td?y;u89r6ISj z@(f?SyFFpCfPT9m_)(FLu%Veup)KpH)XmiMtux2-P~R>6QlchX?;I2!ZH)=`T_3w0 zm#}^a^KCI6pU%p2{;9k%;zh&0kSBaoQTzRk&7)lz>3<~gp`84r4Wkq|fUV9>Ul#t~ z7KA3k4M#56?Qvw7r4%yw)m`M0Y-=lIw=%!l!T`UzrbhwrPfjoqa?66KxeOn5mF&p( z3}{0yZ8rgNXd(|ZMQ+!tjl!sGWVgmtAhMR<0uH*0OmSXDG>Im{bw3>0?N;Q?KfXev zWgl`$k9?R2_(ue;r^^G8UoIM2Ke_?fM~#wgAz$#CRcvZ6-;~6pPb5yGgtnm;PRbT=1 zeyR!p4$o#$A2EGx@w4(^A04rbCQL)U3%2=cbHS%Uw_r>G5^On9FEu0C5u*aI6N5gI376sJlJ`441PS*maIwV zaIo9xANNA$$`lSq$XU8*fqZAt-J7p{A?iFgK|dijAUlGS(LGqy9lFWq^R(kh`uuys zjXqcZ_>sT=`=S21#OTF+){>Htf;)zy_#^Y`y}ln#DmE=zRDATg>CMMBYB?AYs+FBo zMpLR!xoFJ6&T(XA|A7WXs1M~fCV48^G=++b`zeGaT-P{|x8G^+aq~&Q9QN58Ib1%2 zO+1B#l_$!Y85EUzHAi^G!;isyKhbS0<+>4I zSH)$HDTZ)-HrY9RBX%2?x`j=48|QMSI;mB=**l%~Ndg(ip0Xw_ac6S0#GafdIUcj@ z9R72%+jxa5>`t7+``Dg@ogO}}@kBSfm%}b}O-eE|du+mC!k)7UE^o8@_;C*3-%jlw z;WqXWy6jK7+5H`M?NrP|$WBA=SGd5+TZXgi_6?7tUd-+m)&I;&Ey8_d_U-pf((kqX zme2aVB0bvZObhuY%(dyid0FME!L@Go6sJ9x5UT8Du|a`9ocOH3-iRi;?N;~kbhr8v zxQ(aETs8QDn?2lNSHJAsf7+R$|K~*|v$zUOX7O~@^?$EZ=nr!FK5WX;h_cHn6)@OW*`9g21XJLfp1+KMY$%#+Ydga+_wReEi@vrjscG+#owN7o* zBr-}n0N%IBq~PQvVVYoIvwoE;$UIGz#OF(5IBc*6LWE|T*Yd_nkF^K{9&h5Af}K}^ z2@vQvn}EZfuATDT@|})F}_dJ5QvwATRPA2+n%!ZuLhWN zEyu%rz95XL`Yz!H}eSR3D~&z>pkweCDAk?xb%>kqf8THaRN8!lCrnBDg`b6 z`llRvZoZ18O}$jUXBoclP(uU}za51toP)?e$lU+I1kOHdy+>S-8$`mfp_u}Y<#^|d zX$9^P1;KDW8R=Ip-f$;3N#ortieKWP zJ$mFT{&FTEV^4G;s~I3VJV(N4~Vpbq!HjgPNoptK|5T>9ANx#0I@ro zY==wFgPBE+1G_u_5Lk>E)I8h6O0u|{S6oTi#+n*#*er?0!EKWb-S>xzGe?aVq@<=c z84k&>cHxOxqbF?A!2%9FWGb#s)%{G3y!{~(I#V_(JPR*CKI%4@N5z*tvwP0lji~zqmy1N zvouS9N~V*ZW(`pBKdct%uREwnl?f0wM+tDZ9B@%b)&QIu;LbSVVs5NeQk-yy zyAWKFT2xl+NXm-CKEm7nJMZ6m?VMLf1Z{4TW`!6m_uop;&CL9v12txh;ur0)Sxsb6 z;+}io-a93={$}pY4QjWQ2JN-9CU$gemmsCAe-*Pmm1Z1FG=^{u0gZ`N#|73-dadTQ zqXv5aKyPKIVK*ao2Wi4?H3T&#mUe_QYFjULS^*)iV>@e|>_BJ&U$Wx@Yo+|p@{8s{ zqt2U`L-myOqs{PXL#ExA)0nuTI3kUi+REcICo2n|W6LDUS{(0@2Od$&lxOqQHw#o1Ui4D5GcryN-6r-2XV(z} zU-6;ou2>BvS})clZxpT;3?wV6Mfy99qyg>S+LgQBz`xcsl;Ato@Qe~mMm%zzu7M*= zAs9%}8>6()7hh*k|1vZ{><`JV^XlI2d9i;`w;mD5lDB^+yCS)!)85QiL06myy^7R* z+_FaCu@(;_x|N>Slx7{nH9_9qCeup_H|Xy~Y2R(Mqkg_^2fnbNAq0zC$J-HL%V4;A zYv80SXqb=1t>x_qvc;huQ$JAyR0|rGV{vPECkgub87NZrvM?-2Mcne=BM( za80rk#mvb+tw43Uk$w!Zr!BE(4l>4H@>RbzQZ=yWtsU$7f|(r{`ZmlZlkqD#9seAY z=zpL}Nkwzii8U3>U9;hRlrvZ*lFmG_?OSF|uVpzOjXWJ6&OG`h;@g7?l6o~buANad zTTn}>Mo~?9Ek7~4R?+H$JH9QKg?rZtS6|*OV&UF&!c7m(Xkp>dL|M4pmYd9uQGDov zJGQNffxBo$ERZj~H@b%1uHCzxAp*<0=P)7OP0@?V(bjtgDP5D5xB20xqfx_mGoKaw z+QZ~0EefklsGAG=s^S+DOx`-<$2*mzwf~;+X6-Kz`#T@jf@?kNdIO!?P&bSSSr=WC z&eTZvZYYJhjJ%pr-pv-+yHeQk#f)g0E;kl2tFx9Mqh!fQ9_hR^wagW(;YIYA0Gfs^ zV^QA$Cd%2LKry|CPDOfMOwC#*#5*rJ;gaRX!z>)=GCN#=rgkIK6~1KQ7_mh~Io9WI za2DG(EHJPt$#l3SmR+N9_vzeRS@RYX>mxrTTA-651*V(n*dK-N&g)jytYn=T{lz*z zh>v&)UA6at%1R#cy~5y(l~OHFuxt41VCg0PELIm_njP%(0k+2rRL>Jga_sdEzD(@l zND7tuw;RbHeA7-ai8d`fp-~SiLz}d*x%7UK{sTrb>T;J&!x~x@IqC{;x)>lB)v|Yb zaB{NnJ;6Y#enunNB+^e}oj+_jBJhaEGpB&a(rxy<_#>m3GmdYAvdeF%$BlC>P@Trv zjC`|}7k#2GQ=UDgW{W-p)GMUofV`wweNcI}RJ|^T4k%CsFrv!YP?U=5Uh)({)0QHLti5-|{2Pvooq%y`qal}Mr*C$AqHOr60!#N;{4J9@O6 z*#B(yxPWZ}sh*oi+Wx#4W1E20_lnJCSJaU>TixU7vhndsw%2C*vCq=|O<`PX;4u`2 z67TEPt%ch4233|-89iD^@Sn*}nPT_62Lj3D5Vm&PZ^#x99Q3KWY-8GM_SjMyG8?f7 zNl(zrET5nZ(DDhN57iU=r?XS+#A!1q`H?>&$=*r-36u^r1V+MTe%K;b3jGA}jnE1a zBSTP>jA|-}S6LT(CO0rWjoyV~hX+e9%Y>5KaGL_D_05X|mx?Is-Bf9Qx^dSzn@b6V z&j!iK2&_O*T*`ZRdR`)LtZw=p5w_qv?O^F_OF?Fo%&?l*R`LK9l2@CSvQ*BrK<}Q5 z;=FQX9jKJ`AQn6QZ51KhV#NF}ZAd|ufgnwpM91c<+R;5>RT9!JOF%+i$fbiB%&~~z z`ykcAg;*mU*hp$bWuQr|35-CxJ|Bz;PGgB?4ia#)!C2xn_Gla`m+o;I1M{s&tFDJY zZkYLX$hZcKZkYLdkue60ZkYK_sq2PWU`3L00eF7YOSSl30kNA^*_r^9K&s}@e{~u= zS|gEhD;VMaX7@C;2C_zK^m6Kf)cVnrS8+qhOQ}6qrQ@;UjIHevVqHyvDPg=St|k*& z1t{pjAKtf(c95TOvu%C!{h=uBF2jvfDk5wAhVEx+x+Va18PMJ$1&A}H=T6&9%XoWL z9MZ1MTHnwNbLDydd=SHXbzPN6GlQ}$oQD=Ikc1Pse*j6#%aXJFwg9wfAht+dIwSjY zKyTh+7Og`H#;wC=U?opFjBx&JiSFAg@IZ>TqBsB=H>1BsZYC{8u{*S4ZaS9T@&44Sxq0=6Yob@TWn!UfTjkFuK-})TweyHL-tMcE8{ny4A5p$6fU^ z+*l7OLH8+2_p&_e1#D`w{wv)0mD4y+9{Du3pjm$%H-70fhRGvmVe6aqW4Q6W)A+nR z@)=CstPjM`xG<;5BVWXhHR~hsGcL?2@<z%Sict1LoAwXF z>?3$W-R@W_`z;k$m;En%W;ocS6fU>FY&^J&5k~fx)Bj!{4j#Qv4b}cDA1VIV-lt;h z$0ZOY&W~PMZettY`JF^IA9EY?JsjUpWS1tm?3ZwdJ?%RV&;BiJHvYSP5P9^O;?ZXc z`!~TyWyO5*{`ddP{qtSpH24F7D)hGiK>7d4C+}bHOUyx+-dHv*({#KF=yR67^7^7P I3l;nR30fI48vpzw}Jg6raWpXXkF_wWAQ z4_rBX$We8*_G$nCRQK=u<~smT06!`K$}sS4D!gqLd|MT?&n*}Lpo8KUgoOPL0{{lV z{%>|23r}PBMAtEo^OeT7r3VBXXBY20y5Hzq{|5y{+c7V{-?Gm=u|xmTY9F^_iF*}! z={#FG^5Ec6Oh5n%X+I5jYK&zwo>2L?FqCE* z*={<7T+Q*q*|VZJKmS}ZMS#Gx(1e)5e5Gat`cDW{#gqa-PXGi5b@2xkQ}qnM!9W>Y z0%R-F_0XJHb{fWtWkq}y<|<9oHZDN)S8$!sPf{2HY2+x zRlkpvSU_>ml3Q*lXZ$umX1t2bnjqyx<0%g1a@Kf4FG^!`PtO66dfj;fo)SoT1C_=D zkQr|)XFLkB6v`l&%vVcAr{sKl=7keAsq7X|<@xg+N0tA#3HJ{XYWV zcQWveJo?MQ7xUl563V`EmA0E8BU~f7#A+{AngJJ3kTP|h8unEbG*M5=;V`-7t7JGB zTP>?JOQ@{UbQKmU&Qv`>##zerrAm9Kr0yW4)zFR$|IU&^FZuMd-hYZupJ?Kfn2@w{ zg9R~Rx*r;=dv2dz^wevYXL`D!9VcuX?oHWAh{bl`>M%^bx^@#b>rUjWmlOe0B=i#{amQ-l ztl#%s+dB~!J^N?2Z}U@D26ln~RZDI|sRI3t8U$YEv%xTPCZYW&cm}T|yC069smVKz zq2<+g7W*PZPxPw>TGCU)7!kg-yw*cD{%-{4z-<$#gMfNsssdC_L*+^Wc9}OLdb-Cg z*SKox%>Z)pE`6=5=M8fVf1>{_$s6jx{|YD(>Fy=x@?m)`*r0|j*{7OaA%)RwOsc4< z#`@hwjiS>%I05?!Xj}k2Lu?rEN?5ElUm)fugCrx5M7yfh$>tarM(A7rcHDrGN9l+0 zT77blz(Q;c0S^Vkwu^d!GFrjOC}TuoXv9rUi96>bG)+)x=EHs0OpJ>H7wn*BO5DA# zqydYw<8^dbMg)%z7AnM94J7*^S%(pf@RKdcJMPvoBv$k?382(KtN%Rr!qEN(X(-NS zw9YDc;22vl^=GT0a96=`xcv-T@9c}e{lNIXtr-ET<}>H%4il$jTNX zXGFaD7`G5_hiOl$p1Qw|MFc$Nit(TMM^ zMLg&WGo5>vF5r;KUDWYk1EWqhY+nyFBtaZ_v7p3*QbgKXLIQOxqnegWV9!6OtQBCD z=$^p`6XuZUG*!vy!(UCYGlX~`hxU6UC#uXI{lN!xCj zv4QBB&r^@*y7ZSp?Iz2cQ(K#3e&->CC)}wLfNbn>^QV5=iH~zjx~pv6Ox=)uR8J_gvpr+_HE% zM<)q=kpedwQN!8x?04i_AV5y`nWFNoMRSw6ojW90lRA}c@kk^TxMBf?b*lK`U)r0!+P5_;dx+ z%jgkv>@s3baP10?-{|ea4^(5VLCwBNvOx5$5_3wO5UkBt*4?!cs`$_{8uAdxjNY43 zpQQ!2Sh?k1rmN&*&j86OAo+xCSfG-u4sWL5EmN3i?x$sO*`)8`5pe;{`s1BTB|sqX z3hpx7#q#U#W@EE-vVGXSNU*LZBB)qKUDRGl4qh=rB*gdT!tXFJ07czLhVqJAv>#Gq zmvj2zHcFmb@kF9WK>gW5P87@Li|I~N33X!WuQ`6=LHeprG7^sq=qlM#OS)XFA{Z&A z)&&HF=n`^`;GdwFIj#5^SP z@PW=le_oGq#tbnANx2C7*)^4>Z-2F!X^OnwX%jkk42Sty^)-R@gdY&ty1q zE-21j6<;4TlY9PsFJ<{oIj4JXIvLj=$t|YZW6#)wop8bj_>p5h>m|PC*kulF&32~h z1e9-dykAf~J9LG7%F-CN;v6DO4LLU^hFEDq#pu&*ncO_zUlZ`lLzne5CYWl6@SvwM zrXD;lme98>Y&-dh6mQWiy_1+|na!NAZz^9&Hj&;-fuxcKuu)n-52XB+L z;yWK$i}i4xZnid;3}3nPm6!`~drNWu(=F5PC#(OsJc55mnEH2>cs~ANPaLLd*o&iG zPrbB!_ru*3N962lU6!`cb9{~W%<~(9Gb?=DquP{-L?ZmVZofy71wEW|bg!@tAD3&> zQn_xpZ_lrHj%#DpEJx;-r_uE{H@V#vf=#s_Bre;2QF?PKg0&4be)R_bmzBq=qy;@+ z)n3i!R8F=uei3h5)R24mZ9s)lrf@1d{srsB^36K}pw#e}W)p?@c?>NlVVmor?yt0j z6`I~rXA?vNVE5SD7yQHW=$t8w;ozn#-0ogRZn6zzRN_Tp+0L06Ag1(*b7joA0R%#n zC$Wk`hJfNBE2KPrIFe<<{Eg&Sj+$YL=3U9N?Z;7edER7uxMeQ@rKq`p%(4FS1-3-L zfn^A~Sp)OaOz%xOuKDiVi}VSx#M_E3A4tKZ;GD7y#VTS^Bz0IoYp^pC(bKjYZg<-x zqTVSZ$fRat6-Wy%osC$=q#k(0u_cl8_H^&_H70rM-e`LL^YzhD)(Z{iJf2ZZynSr% zDoVIq6V_#@_t(wZK(VtHo;i6dt@MGzHG2$Ml3T z?EwJrGKeb`UhsVGXTGpK<42GKo5o ziZ=<2X!Ibe8m?KleM}MEBvVNbRnwv->f6spG^dzbVR<9_=TX)3>_FGf_v|ue26psG zwkvSbbICjB05o}?*9c;};@dCb)3cgJ?j>H>Dp2DRM?yep3~D6frIu%CVcH!ndIrZG zHoBc;?De#V91rqeMHkN5hF!81r>n4CrY)V%^j2}A18KyNVcOb=JbM)*B#2kyNJMy7 z5ZJA>ftQb#y0jjXawk^LdYl!8X%A}2YUA`}T~KRK(hJXOsK(E|*LUWZDMl*D>rpyo zv8~gHg*a~RA9y$`6OSX!lR42GGvF7urAW^e#<=4H!g3@G3o4LxBD)a~>-<#a+=B+{ zSnmb+z~3Bo->sr(B`gWDld3A-l%vCMg%4&8ddA`>9vn<1&gn{3cg;4!jx9?e5ovXt zjJMl7`i@QnPsZA`F^y-YvXTCP)~%(?<%rT}cZ=mOf5aDwZ4e-%XQ!@+YznzXF@+J$ zn5!ZcHAo^jAV@`<2r>2&QN060Tko58B!9YAH~0XFu6=aeC`Q7*Qi-juAu<$#qZ=rk z!O5+43)y;_ISgu+C{iY8n>YU|1pPt^id`Hi<&X_~=Q#XazV;|fKWgk*JHDkS^sSL= zA&4go67;)vxEIqZEq}_vzOUgde-b`$6dV=%!kjfG5%3# zhJ0;3*<_Z>J%O#>^lHHcBwiB4+#0<~wtrw_3dygX*d0RmN)>fGmx}d(-@KNZ{wO$t zysdPU+BC99GI^?D$Ife38nCFD-u`T`xSH_;+3q#SI&CGU@xAQ-=BKGom(E(BO6pTd{Us&!=F*u60D%4= y{x86S0so1=>3k}$Pv!NOl-I(cSk5yMgop^3_%SiU4g5O}uz$~?ZwkIfpZ^yfe7^($ diff --git a/public/icon.png b/public/icon.png index b56124d114902b12185cc39ef3fe1d2e12e5a144..96fcaab0095a540a1f8e0b4e6af72cd1a2fffb19 100644 GIT binary patch literal 8461 zcmeHNeN4(^A04rbCQL)U3%2=cbHS%Uw_r>G5^On9FEu0C5u*aI6N5gI376sJlJ`441PS*maIwV zaIo9xANNA$$`lSq$XU8*fqZAt-J7p{A?iFgK|dijAUlGS(LGqy9lFWq^R(kh`uuys zjXqcZ_>sT=`=S21#OTF+){>Htf;)zy_#^Y`y}ln#DmE=zRDATg>CMMBYB?AYs+FBo zMpLR!xoFJ6&T(XA|A7WXs1M~fCV48^G=++b`zeGaT-P{|x8G^+aq~&Q9QN58Ib1%2 zO+1B#l_$!Y85EUzHAi^G!;isyKhbS0<+>4I zSH)$HDTZ)-HrY9RBX%2?x`j=48|QMSI;mB=**l%~Ndg(ip0Xw_ac6S0#GafdIUcj@ z9R72%+jxa5>`t7+``Dg@ogO}}@kBSfm%}b}O-eE|du+mC!k)7UE^o8@_;C*3-%jlw z;WqXWy6jK7+5H`M?NrP|$WBA=SGd5+TZXgi_6?7tUd-+m)&I;&Ey8_d_U-pf((kqX zme2aVB0bvZObhuY%(dyid0FME!L@Go6sJ9x5UT8Du|a`9ocOH3-iRi;?N;~kbhr8v zxQ(aETs8QDn?2lNSHJAsf7+R$|K~*|v$zUOX7O~@^?$EZ=nr!FK5WX;h_cHn6)@OW*`9g21XJLfp1+KMY$%#+Ydga+_wReEi@vrjscG+#owN7o* zBr-}n0N%IBq~PQvVVYoIvwoE;$UIGz#OF(5IBc*6LWE|T*Yd_nkF^K{9&h5Af}K}^ z2@vQvn}EZfuATDT@|})F}_dJ5QvwATRPA2+n%!ZuLhWN zEyu%rz95XL`Yz!H}eSR3D~&z>pkweCDAk?xb%>kqf8THaRN8!lCrnBDg`b6 z`llRvZoZ18O}$jUXBoclP(uU}za51toP)?e$lU+I1kOHdy+>S-8$`mfp_u}Y<#^|d zX$9^P1;KDW8R=Ip-f$;3N#ortieKWP zJ$mFT{&FTEV^4G;s~I3VJV(N4~Vpbq!HjgPNoptK|5T>9ANx#0I@ro zY==wFgPBE+1G_u_5Lk>E)I8h6O0u|{S6oTi#+n*#*er?0!EKWb-S>xzGe?aVq@<=c z84k&>cHxOxqbF?A!2%9FWGb#s)%{G3y!{~(I#V_(JPR*CKI%4@N5z*tvwP0lji~zqmy1N zvouS9N~V*ZW(`pBKdct%uREwnl?f0wM+tDZ9B@%b)&QIu;LbSVVs5NeQk-yy zyAWKFT2xl+NXm-CKEm7nJMZ6m?VMLf1Z{4TW`!6m_uop;&CL9v12txh;ur0)Sxsb6 z;+}io-a93={$}pY4QjWQ2JN-9CU$gemmsCAe-*Pmm1Z1FG=^{u0gZ`N#|73-dadTQ zqXv5aKyPKIVK*ao2Wi4?H3T&#mUe_QYFjULS^*)iV>@e|>_BJ&U$Wx@Yo+|p@{8s{ zqt2U`L-myOqs{PXL#ExA)0nuTI3kUi+REcICo2n|W6LDUS{(0@2Od$&lxOqQHw#o1Ui4D5GcryN-6r-2XV(z} zU-6;ou2>BvS})clZxpT;3?wV6Mfy99qyg>S+LgQBz`xcsl;Ato@Qe~mMm%zzu7M*= zAs9%}8>6()7hh*k|1vZ{><`JV^XlI2d9i;`w;mD5lDB^+yCS)!)85QiL06myy^7R* z+_FaCu@(;_x|N>Slx7{nH9_9qCeup_H|Xy~Y2R(Mqkg_^2fnbNAq0zC$J-HL%V4;A zYv80SXqb=1t>x_qvc;huQ$JAyR0|rGV{vPECkgub87NZrvM?-2Mcne=BM( za80rk#mvb+tw43Uk$w!Zr!BE(4l>4H@>RbzQZ=yWtsU$7f|(r{`ZmlZlkqD#9seAY z=zpL}Nkwzii8U3>U9;hRlrvZ*lFmG_?OSF|uVpzOjXWJ6&OG`h;@g7?l6o~buANad zTTn}>Mo~?9Ek7~4R?+H$JH9QKg?rZtS6|*OV&UF&!c7m(Xkp>dL|M4pmYd9uQGDov zJGQNffxBo$ERZj~H@b%1uHCzxAp*<0=P)7OP0@?V(bjtgDP5D5xB20xqfx_mGoKaw z+QZ~0EefklsGAG=s^S+DOx`-<$2*mzwf~;+X6-Kz`#T@jf@?kNdIO!?P&bSSSr=WC z&eTZvZYYJhjJ%pr-pv-+yHeQk#f)g0E;kl2tFx9Mqh!fQ9_hR^wagW(;YIYA0Gfs^ zV^QA$Cd%2LKry|CPDOfMOwC#*#5*rJ;gaRX!z>)=GCN#=rgkIK6~1KQ7_mh~Io9WI za2DG(EHJPt$#l3SmR+N9_vzeRS@RYX>mxrTTA-651*V(n*dK-N&g)jytYn=T{lz*z zh>v&)UA6at%1R#cy~5y(l~OHFuxt41VCg0PELIm_njP%(0k+2rRL>Jga_sdEzD(@l zND7tuw;RbHeA7-ai8d`fp-~SiLz}d*x%7UK{sTrb>T;J&!x~x@IqC{;x)>lB)v|Yb zaB{NnJ;6Y#enunNB+^e}oj+_jBJhaEGpB&a(rxy<_#>m3GmdYAvdeF%$BlC>P@Trv zjC`|}7k#2GQ=UDgW{W-p)GMUofV`wweNcI}RJ|^T4k%CsFrv!YP?U=5Uh)({)0QHLti5-|{2Pvooq%y`qal}Mr*C$AqHOr60!#N;{4J9@O6 z*#B(yxPWZ}sh*oi+Wx#4W1E20_lnJCSJaU>TixU7vhndsw%2C*vCq=|O<`PX;4u`2 z67TEPt%ch4233|-89iD^@Sn*}nPT_62Lj3D5Vm&PZ^#x99Q3KWY-8GM_SjMyG8?f7 zNl(zrET5nZ(DDhN57iU=r?XS+#A!1q`H?>&$=*r-36u^r1V+MTe%K;b3jGA}jnE1a zBSTP>jA|-}S6LT(CO0rWjoyV~hX+e9%Y>5KaGL_D_05X|mx?Is-Bf9Qx^dSzn@b6V z&j!iK2&_O*T*`ZRdR`)LtZw=p5w_qv?O^F_OF?Fo%&?l*R`LK9l2@CSvQ*BrK<}Q5 z;=FQX9jKJ`AQn6QZ51KhV#NF}ZAd|ufgnwpM91c<+R;5>RT9!JOF%+i$fbiB%&~~z z`ykcAg;*mU*hp$bWuQr|35-CxJ|Bz;PGgB?4ia#)!C2xn_Gla`m+o;I1M{s&tFDJY zZkYLX$hZcKZkYLdkue60ZkYK_sq2PWU`3L00eF7YOSSl30kNA^*_r^9K&s}@e{~u= zS|gEhD;VMaX7@C;2C_zK^m6Kf)cVnrS8+qhOQ}6qrQ@;UjIHevVqHyvDPg=St|k*& z1t{pjAKtf(c95TOvu%C!{h=uBF2jvfDk5wAhVEx+x+Va18PMJ$1&A}H=T6&9%XoWL z9MZ1MTHnwNbLDydd=SHXbzPN6GlQ}$oQD=Ikc1Pse*j6#%aXJFwg9wfAht+dIwSjY zKyTh+7Og`H#;wC=U?opFjBx&JiSFAg@IZ>TqBsB=H>1BsZYC{8u{*S4ZaS9T@&44Sxq0=6Yob@TWn!UfTjkFuK-})TweyHL-tMcE8{ny4A5p$6fU^ z+*l7OLH8+2_p&_e1#D`w{wv)0mD4y+9{Du3pjm$%H-70fhRGvmVe6aqW4Q6W)A+nR z@)=CstPjM`xG<;5BVWXhHR~hsGcL?2@<z%Sict1LoAwXF z>?3$W-R@W_`z;k$m;En%W;ocS6fU>FY&^J&5k~fx)Bj!{4j#Qv4b}cDA1VIV-lt;h z$0ZOY&W~PMZettY`JF^IA9EY?JsjUpWS1tm?3ZwdJ?%RV&;BiJHvYSP5P9^O;?ZXc z`!~TyWyO5*{`ddP{qtSpH24F7D)hGiK>7d4C+}bHOUyx+-dHv*({#KF=yR67^7^7P I3l;nR30fI48vpzw}Jg6raWpXXkF_wWAQ z4_rBX$We8*_G$nCRQK=u<~smT06!`K$}sS4D!gqLd|MT?&n*}Lpo8KUgoOPL0{{lV z{%>|23r}PBMAtEo^OeT7r3VBXXBY20y5Hzq{|5y{+c7V{-?Gm=u|xmTY9F^_iF*}! z={#FG^5Ec6Oh5n%X+I5jYK&zwo>2L?FqCE* z*={<7T+Q*q*|VZJKmS}ZMS#Gx(1e)5e5Gat`cDW{#gqa-PXGi5b@2xkQ}qnM!9W>Y z0%R-F_0XJHb{fWtWkq}y<|<9oHZDN)S8$!sPf{2HY2+x zRlkpvSU_>ml3Q*lXZ$umX1t2bnjqyx<0%g1a@Kf4FG^!`PtO66dfj;fo)SoT1C_=D zkQr|)XFLkB6v`l&%vVcAr{sKl=7keAsq7X|<@xg+N0tA#3HJ{XYWV zcQWveJo?MQ7xUl563V`EmA0E8BU~f7#A+{AngJJ3kTP|h8unEbG*M5=;V`-7t7JGB zTP>?JOQ@{UbQKmU&Qv`>##zerrAm9Kr0yW4)zFR$|IU&^FZuMd-hYZupJ?Kfn2@w{ zg9R~Rx*r;=dv2dz^wevYXL`D!9VcuX?oHWAh{bl`>M%^bx^@#b>rUjWmlOe0B=i#{amQ-l ztl#%s+dB~!J^N?2Z}U@D26ln~RZDI|sRI3t8U$YEv%xTPCZYW&cm}T|yC069smVKz zq2<+g7W*PZPxPw>TGCU)7!kg-yw*cD{%-{4z-<$#gMfNsssdC_L*+^Wc9}OLdb-Cg z*SKox%>Z)pE`6=5=M8fVf1>{_$s6jx{|YD(>Fy=x@?m)`*r0|j*{7OaA%)RwOsc4< z#`@hwjiS>%I05?!Xj}k2Lu?rEN?5ElUm)fugCrx5M7yfh$>tarM(A7rcHDrGN9l+0 zT77blz(Q;c0S^Vkwu^d!GFrjOC}TuoXv9rUi96>bG)+)x=EHs0OpJ>H7wn*BO5DA# zqydYw<8^dbMg)%z7AnM94J7*^S%(pf@RKdcJMPvoBv$k?382(KtN%Rr!qEN(X(-NS zw9YDc;22vl^=GT0a96=`xcv-T@9c}e{lNIXtr-ET<}>H%4il$jTNX zXGFaD7`G5_hiOl$p1Qw|MFc$Nit(TMM^ zMLg&WGo5>vF5r;KUDWYk1EWqhY+nyFBtaZ_v7p3*QbgKXLIQOxqnegWV9!6OtQBCD z=$^p`6XuZUG*!vy!(UCYGlX~`hxU6UC#uXI{lN!xCj zv4QBB&r^@*y7ZSp?Iz2cQ(K#3e&->CC)}wLfNbn>^QV5=iH~zjx~pv6Ox=)uR8J_gvpr+_HE% zM<)q=kpedwQN!8x?04i_AV5y`nWFNoMRSw6ojW90lRA}c@kk^TxMBf?b*lK`U)r0!+P5_;dx+ z%jgkv>@s3baP10?-{|ea4^(5VLCwBNvOx5$5_3wO5UkBt*4?!cs`$_{8uAdxjNY43 zpQQ!2Sh?k1rmN&*&j86OAo+xCSfG-u4sWL5EmN3i?x$sO*`)8`5pe;{`s1BTB|sqX z3hpx7#q#U#W@EE-vVGXSNU*LZBB)qKUDRGl4qh=rB*gdT!tXFJ07czLhVqJAv>#Gq zmvj2zHcFmb@kF9WK>gW5P87@Li|I~N33X!WuQ`6=LHeprG7^sq=qlM#OS)XFA{Z&A z)&&HF=n`^`;GdwFIj#5^SP z@PW=le_oGq#tbnANx2C7*)^4>Z-2F!X^OnwX%jkk42Sty^)-R@gdY&ty1q zE-21j6<;4TlY9PsFJ<{oIj4JXIvLj=$t|YZW6#)wop8bj_>p5h>m|PC*kulF&32~h z1e9-dykAf~J9LG7%F-CN;v6DO4LLU^hFEDq#pu&*ncO_zUlZ`lLzne5CYWl6@SvwM zrXD;lme98>Y&-dh6mQWiy_1+|na!NAZz^9&Hj&;-fuxcKuu)n-52XB+L z;yWK$i}i4xZnid;3}3nPm6!`~drNWu(=F5PC#(OsJc55mnEH2>cs~ANPaLLd*o&iG zPrbB!_ru*3N962lU6!`cb9{~W%<~(9Gb?=DquP{-L?ZmVZofy71wEW|bg!@tAD3&> zQn_xpZ_lrHj%#DpEJx;-r_uE{H@V#vf=#s_Bre;2QF?PKg0&4be)R_bmzBq=qy;@+ z)n3i!R8F=uei3h5)R24mZ9s)lrf@1d{srsB^36K}pw#e}W)p?@c?>NlVVmor?yt0j z6`I~rXA?vNVE5SD7yQHW=$t8w;ozn#-0ogRZn6zzRN_Tp+0L06Ag1(*b7joA0R%#n zC$Wk`hJfNBE2KPrIFe<<{Eg&Sj+$YL=3U9N?Z;7edER7uxMeQ@rKq`p%(4FS1-3-L zfn^A~Sp)OaOz%xOuKDiVi}VSx#M_E3A4tKZ;GD7y#VTS^Bz0IoYp^pC(bKjYZg<-x zqTVSZ$fRat6-Wy%osC$=q#k(0u_cl8_H^&_H70rM-e`LL^YzhD)(Z{iJf2ZZynSr% zDoVIq6V_#@_t(wZK(VtHo;i6dt@MGzHG2$Ml3T z?EwJrGKeb`UhsVGXTGpK<42GKo5o ziZ=<2X!Ibe8m?KleM}MEBvVNbRnwv->f6spG^dzbVR<9_=TX)3>_FGf_v|ue26psG zwkvSbbICjB05o}?*9c;};@dCb)3cgJ?j>H>Dp2DRM?yep3~D6frIu%CVcH!ndIrZG zHoBc;?De#V91rqeMHkN5hF!81r>n4CrY)V%^j2}A18KyNVcOb=JbM)*B#2kyNJMy7 z5ZJA>ftQb#y0jjXawk^LdYl!8X%A}2YUA`}T~KRK(hJXOsK(E|*LUWZDMl*D>rpyo zv8~gHg*a~RA9y$`6OSX!lR42GGvF7urAW^e#<=4H!g3@G3o4LxBD)a~>-<#a+=B+{ zSnmb+z~3Bo->sr(B`gWDld3A-l%vCMg%4&8ddA`>9vn<1&gn{3cg;4!jx9?e5ovXt zjJMl7`i@QnPsZA`F^y-YvXTCP)~%(?<%rT}cZ=mOf5aDwZ4e-%XQ!@+YznzXF@+J$ zn5!ZcHAo^jAV@`<2r>2&QN060Tko58B!9YAH~0XFu6=aeC`Q7*Qi-juAu<$#qZ=rk z!O5+43)y;_ISgu+C{iY8n>YU|1pPt^id`Hi<&X_~=Q#XazV;|fKWgk*JHDkS^sSL= zA&4go67;)vxEIqZEq}_vzOUgde-b`$6dV=%!kjfG5%3# zhJ0;3*<_Z>J%O#>^lHHcBwiB4+#0<~wtrw_3dygX*d0RmN)>fGmx}d(-@KNZ{wO$t zysdPU+BC99GI^?D$Ife38nCFD-u`T`xSH_;+3q#SI&CGU@xAQ-=BKGom(E(BO6pTd{Us&!=F*u60D%4= y{x86S0so1=>3k}$Pv!NOl-I(cSk5yMgop^3_%SiU4g5O}uz$~?ZwkIfpZ^yfe7^($ diff --git a/public/tray-icon-black.png b/public/tray-icon-black.png index 75c3da2146be2eb6b390150c7cd50d63b5b09005..89c5d28472eb13e14b45aede967c32253751fe2e 100644 GIT binary patch delta 251 zcmVPy7ByixOS6GuOPJG6(Y(+ zYMN>#?H3V^&gvn@V;aw9Ya9jpJDPs1^#@ zU3vYhp`h$@ufG@yl_B!_ze3jWZ=C+OcKzvZswWYr(r+5#dHDbU002ovPDHLkV1l^R Be)s?Y delta 262 zcmV+h0r~!{0=EK?Ie+O%L_t(|ob8b@YC=I6gg+IrQn5Eknw%kpphr+pn=}?`s@SKr z_69ajLA-+sww^+;vI!&*uo6)g{$f}Z@x52Rfnj%NhJV;yNRS{d6o8U3>v=ixT+X2n z%pY*7L%xBFG4KkO;(eJGh<1-B9g+q%Vm3_ginan$VGA6}z<;lq2jC6(vGX7BBH2&{ z4j&TumwnsIg)VSdc){6?Or(Ob7aYY|PzTNuxY+t0M?n$Tn|TDP_POr71M*P|^zDbH zeTp`TQqU4(3uH`B0c-JEVGBCI!1(IjtRi01w3=x5*t_+~2@>!$t~X?)ijtJ8IRF3v M07*qoM6N<$f)N&_(vMK4I{$K}a+@((M5`r^lxAVdlY=$cI? zlevL~9oR?Dy{CnCrUK`jvvedshvW-@2;dvQ%hC9f;VD1fA;RQu;oc#_LgmXKd^Cj07b9F7!sa6YxK9MMLT1aS{P)kjcYlE)v8+5pVtM&89>K5q znJq6c&dfXU4&Vad3BYaTEAJsu%9pW7M33?oiEVj{MD#2_BC%I~L?SZEYb1K*H4>3o zz9pe8-;#(c%GV@D%hx30%JO9-f5uzNm$_sH^UmI#N9ZSj>!iRN>Bd%nOMe+99_2qW zgY`p!dc_!(_yw;~B{Op`bd98+Mt55D)Mr5@9l=w^g$FQz6=wn9VnZjPCPn{!obZ z7-92OA+wiMAoH6cUT0t+^Q$4=H*6sD|Aep(@IdAt{~RCP=bVH8^8*)9NyCw$t|A3(#;ft^&PYak++h@2M3lfg0dU z2)noRlQ5t4-XF0MSX07FKF4Z4E&^%bO9-BO-;8z(eFOTU7Dy?NO?ST^4DdhT5~zz( zU{?vP3bl($?M74r<4WL4`BbZvAG*8rjfE}H20RGG-#*^r8t%-02)R3;IZT0d;8YkZ vlxIQ7W=ceJ6&fc2_GB1_VHk#C5OaP3<6M%$t4G(l00000NkvXXu0mjf#V5!W diff --git a/public/tray-icon-black@3x.png b/public/tray-icon-black@3x.png index 5354b7643d86a30a8e9605118dfa35c1adc19669..974daf1b31d8c0c41e5b14ffa08d4aa17dc0477c 100644 GIT binary patch delta 608 zcmV-m0-ycU1&#%fIe$P&L_t(|obBArO2a@D#qk5>;KRAHE8kENQa3(a5g{T>poTP^ zk9+T&z+A|-{`rwEoI4a4GiJ<~;}6rLGEzMRZ~^cI;Hh_vS`SmqoSxoMQaxP7^p2A1 z;hmUXQBpm85EC6G)x*q~s3@r(7RGo-N%gQY#w$vyhmA4TQGZfBG>Wl`lIo#xOly=> zk5|lG870-@9Wxh3srBFl;4L6%&WMV_`Fp04695Gys^TahQ5~lz5|4tj4ATci1iz=} zFgTHK(l>w?0M7ufN5Y9fT5y)}L~_CLL|Wr`BDvytAl<|9KnlUJM0$i{i4=-sfs~75 zfz$=31*sIL1%Ih4P7P8mP7RU-P6<*gP6?6}P7ab4P7abJ&K{CA&K{C9&K8mv&KA-k zaF${Er}GT|Ij6sM7#yV74*;)-0s2(9>XJLR=Q%&lNayr5R=9eTHCu;WUP!k%^AV7^ za}sEcw1p#$#EfH7E2KRfNhB7WA-5vs;7B2{;()ytsec4V0*MXVhf<^(oUTYTSk2@j zwcvC?qT$}^5t0Q?C=wl(diRhlaYB&jxba*gdEmGrF<>3MAbH}rAaSgo`@T8z>O4o` z>P_~Wa|y;-MjlDw3d%_pNdZS1DJp8pJSJ7^35l8t&ydx6T%)GS19r6@m#Eg&j9QOr zl-1r!ty#w;%6h4%*3%T_weeKzsfzMm2dniIMMdq()q1j`qAwDt_3TFVx@Do(vl-R< u3W{3KzbJ_q!5+PzqY>;GGiJ;f)XXoMGiBG!9arlB0000t-Who8j?SzLq_IY?_l!LOh*_*4f z2PY>=(H?A4PD)$rK)Gt=;Nar8+MBX!{VR53mrX?e>sxc!*8YC;ec#`y+ zvj`Y=WL>Y5QAk0nf$>PtZ(v^}=e%P;CSZALf{df#)sIazTYzgV_}?P!e**21aTHb% z;nj}?_VGUpfPY7hwik#~XhF-uTN0aU`)9Q4aSY@!(44v;V<+%E5;`(%sD-jB}1)+v^y29d%FPCka6d9czkhAg?+Ga1UrIn;^n5;A4ZGzX#TmjbsEFCmZa% zi|iyN$hecU<9_myoS;>}>x`W~2U@Bk$mj@*vZq7aCx0CvKS>H|0mhsij)X&Nr3Br~ z*y+V83F^$zsU1r9RzXl}xVO#Iao&YG89`0J{RTVlFSDRCj__xWI-O+{v=#Uo34R&& zNJEi&6TrGM32JscwB0-&3H7%-u16kZIItF8P@g08#*8+X9Bq2aAgIH!V}2W^)^rvF zPa^H?zJIzcWkD-}SB}8rHQKj3{O;J#nBWNOgKZ{-Zk2(&s z2U8NX9{Au0I^i)59CCbLvp*_Uq#($?XFYU;43dojN9I5Zg3dYurovm61zYgnII_Eo zE@&IDD^fO*kByG(*}j#vIoi!-vA2qA&_Ujpr|jYd6{S3PJ?0@Fb8( zh?8VS8XU-~@Bg3DndvnBqkuV_Av|yd9IyeWUHJ@7wY5YhUVxh-EPMkNMOHot4#2%3 z_=ptd#OJfr2|eK&kIh{9CLUOTGw?8*cS3wH&loABZ(a%UxpjFZL~mYvlT|X$m?bhc z^C%A8as@7(fSYFGCG*r#FDWFNKO>`BE0Q@vXUODPrgnhGnVt#*PMCZ_nfEsv8+&`{vyBuQZg|ki>u5LuMNz; z%tRkD;3dxhEL7GL&*QtU`j7~)R$)c_rt+46?F%5A(!f!|J%5k~4sw42GOGJKKsGj1 z;jGp*qvsDGo99Mx`lb^IFfxk6zX5C9mE0wu^0d=4wlQW(ytFo$iCFOmZ*4w`*R4D6L-2oG$BsIRB)w%!x O002ovPDHLkU;%;!mw8qI diff --git a/public/tray-icon-white@2x.png b/public/tray-icon-white@2x.png index 73d8a7ab65a253bf7b494aa47d65da5becc5d94f..e7cc1b214091f1832558a91931770d9bb520eac4 100644 GIT binary patch delta 412 zcmV;N0b~A#1j_@EIe&dgL_t(|ob8!GO2j}AMFTQ;I9JZf8#)N;#=}V?JW&%ILz1rQ zu3s2PL6-CWf0M;@^$f!?$K(xg{(5%F=LcSZ58z=}e1756godExFTg`XQ1Un6At5OF z26$J9l#dfYp$I@{FIquYbLTaEY|?3W@ac(*y-y z#;3RZd1#r=H}MRdfE(a3?VAsh_%46NXpt!0*Vzaza zqR7fuBuwQi5=CBqMZ#5nMWUpYpZ!i;TFPHBP9sCMXV(N?f!j`jC&iBK`IUZakr>Or zj10*|f#-RP%d7Oh;lhzcs{lo>%j|Ax+DOnr{v1 z+N2QkYat;`4t4kwYuzFM_T>>0000wjwh*!0e>JTh%bO@pXNJ&XTkRl z^oAC|0jw*ARZjp&0tbp`zCasFfJ)$0Fyy3t2TUppWP$R~12}*#;8ieP0yTNM7T{jc zJ-Ti6s|!7V1IQ@GwLHy^qM7gn!VZuG_O-19j4OqAfkfm196$=VQ?}BLFz@CXJ%!K% zIDoM~6dmz=h<_x21K0{saKkG?lmY61E1zQSpsS5@fCRATQ*1k6oyY=A1t>Tg%K$*9 z69*{h-d3qt1pq35W5MiB=?7unog2wG1z1wdb3WT@Iu-%=UhG3Kp8Px+?H2k1q$3T0 z{@Qf+`n59dl(zM&$tZhSZq;g{%$V>C%$ zs;W<*3uc|e=VhGk!f6^aX3Us_kEF}_H9-c|qpz4JNiUMFM$i8^-_sQd)uX>LU87Jv zT94@xh3e6MOlTCUNBJ=!QK%l3$GAqJdQ>0d5{2qfdyHums((jzF(y%{9*3BwC{&MQ zOjQ)BCm^OMiqiH_3~Nk_q1d^J;0@WbhK44mNa zq&G=7lCCAa9tI}}=@aMdS)NGtIL=7FaGa6+aGa3-#c@LN$1z6Q#4$#S!!bh2!ZAXM z$7w_=!f8b6hksLvRE1NC)E}o1sR^eLiG!1gWP+24#KYM^GR4_J;^M3zx!|lI@o^R( zaOV%5x0CbC&p18n7&vD@kCN`D7@%ju>P^-x&U4;9kxn@CkFhBGNt?5B*yVz>z!4rv z+b4mhNGmv8B;_5Gnjr1q@Q_q?hTMddiNirs-2rRBBY2?(n%386UsdYYrA*UWqV_r99$x$pCC<X>{RZ|=Q(rDYp{ZdZ z=)92A3fvd62I^##QqUscbtLE)uq~2vN@&Og%u7m8Xl!42^iWc zoPt7g143Zm6n}NIfIC9%X`zQw3z{3=lGs$+UQ^dAG;$AUOInZtYy`eVLLdE|YQQpJ zJaYeMn}V4@=>-|UNg=pD+`_`xhENeME5YXt`$%nFdApoPHmoP$0AI)9r&kOAxnA7uqwC+*A%lB%Fi z;FUO_t&v)rP0;0>gPv&`K|oJ|LG7n>Yvlzo@f%_LwnZrPp-x5+&;s0SaPYw@3py@@ zKNjxkshl8SE$}50{4}hQ?3kt=W5CkN2m;!Khqj%^z2W{1!uH55^@lYDL488#rJ9Hf~@;k_xl)X~O{^zXBmI6ZE|Gc`2JfGt@ oj^j9v<2a7vIF92uj+4ds18ca^(cRpVCIA2c07*qoM6N<$f(A%Yq5uE@ diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts index 4f2da640f7..d37a2f0129 100644 --- a/scripts/generate-icons.ts +++ b/scripts/generate-icons.ts @@ -7,6 +7,7 @@ * * Commands: * update - Regenerate all logo sources and derived assets + * check - Verify tracked assets without writing files * png - Generate build/icon.png (512x512) * icns - Generate build/icon.icns (macOS app icon) * linux-icons - Generate build/icons/{16x16..512x512}.png (Linux icon set) @@ -90,11 +91,10 @@ type LogoTargetConfig = RasterTargetConfig | SvgTargetConfig; const MONO_ICON = { source: SOURCE_BLACK, bg: false } as const; const APP_ICON = { source: SOURCE_WHITE, bg: true } as const; -// Crop the centered square source to the outlined "x" + cursor bounds so the -// mark fills small tray images without clipping the glyph or cursor. -// Content bounds: x 13.13…58.88, y 24.5…47.5 → 45.75×23 units. Keep -// roughly one output pixel of horizontal breathing room at the 24px tray size. -const TRAY_MARK_CROP = "11 24 50 24.5"; +// Crop to the centered ribbon-mark bounds so it fills small tray images while +// retaining enough breathing room to preserve the open center at 24px. +// Content bounds: x 10…62, y 6…58. The 60-unit crop yields 1.6px margins at 24px. +const TRAY_MARK_CROP = "6 2 60 60"; // Targets to update (path -> config) const LOGO_TARGETS = { @@ -115,14 +115,10 @@ const LOGO_TARGETS = { // iOS Safari uses apple-touch-icon for home screen installs. "public/apple-touch-icon.png": { size: 180, ...APP_ICON }, - // Electron Tray Icons – Wide Xum Mark (Monochrome on Transparent) - // - // The source SVGs have heavy internal padding (mark uses ~32% of canvas). - // We crop to TRAY_MARK_CROP before rendering so the mark fills the output. + // Electron Tray Icons – Xum Mark (Monochrome on Transparent) // + // Crop the source's outer padding so the open-center mark stays clear at 24px. // Pixel dimensions: 24×24 @1x → 48×48 @2x → 72×72 @3x. - // Square canvas; the mark (aspect ≈ 1.84:1) is height-constrained and - // centered horizontally with transparent side padding. // // macOS treats the black variant as a template image (adapts to light/dark // menu bar automatically). Windows/Linux switch between black/white at @@ -232,19 +228,17 @@ async function generateFavicon(source: string, output: string) { console.warn(" ⚠ ImageMagick not found, favicon.ico is single-resolution"); } +function renderThemeFaviconSvg(svg: string) { + const withCurrentColor = svg.replace(/fill="(black|white)"/g, 'fill="currentColor"'); + return withCurrentColor.replace(/]*>/, (match) => `${match}\n${THEME_FAVICON_STYLE}`); +} + async function generateThemeFaviconSvg(output: string) { const svg = await readFile(SOURCE_BLACK, "utf8"); - const withCurrentColor = svg.replace(/fill="(black|white)"/g, 'fill="currentColor"'); - const themedSvg = withCurrentColor.replace( - /]*>/, - (match) => `${match}\n${THEME_FAVICON_STYLE}` - ); - await writeFile(output, themedSvg); + await writeFile(output, renderThemeFaviconSvg(svg)); } -async function replaceInlineWordmark(relativePath: string, className: string) { - const outputPath = path.join(ROOT, relativePath); - const html = await readFile(outputPath, "utf8"); +function replaceInlineWordmarkContent(html: string, relativePath: string, className: string) { const pattern = new RegExp( String.raw`^([ \t]*)`, "m" @@ -259,7 +253,13 @@ async function replaceInlineWordmark(relativePath: string, className: string) { .split("\n") .map((line) => `${indent}${line}`) .join("\n"); - await writeFile(outputPath, html.replace(pattern, svg)); + return html.replace(pattern, svg); +} + +async function replaceInlineWordmark(relativePath: string, className: string) { + const outputPath = path.join(ROOT, relativePath); + const html = await readFile(outputPath, "utf8"); + await writeFile(outputPath, replaceInlineWordmarkContent(html, relativePath, className)); } async function generateSourceLogos() { @@ -310,6 +310,92 @@ async function updateAllLogos() { console.log("\n✅ All logos updated successfully!"); } +function assertFresh(relativePath: string, actual: string | Buffer, expected: string | Buffer) { + const matches = + typeof actual === "string" && typeof expected === "string" + ? actual === expected + : Buffer.from(actual).equals(Buffer.from(expected)); + if (!matches) { + throw new Error(`${relativePath} is stale; run bun scripts/generate-icons.ts update`); + } +} + +async function validateImage( + relativePath: string, + expectedDimensions: readonly (readonly [number, number])[] +) { + const outputPath = path.join(ROOT, relativePath); + const metadata = await sharp(outputPath, { page: 0 }).metadata(); + const dimensionsMatch = expectedDimensions.some( + ([width, height]) => metadata.width === width && metadata.height === height + ); + if (!dimensionsMatch) { + throw new Error( + `${relativePath} has unexpected dimensions ${metadata.width}x${metadata.height}` + ); + } + + const stats = await sharp(outputPath, { page: 0 }).ensureAlpha().stats(); + const alpha = stats.channels[3]; + const hasPixelVariation = stats.channels.some((channel) => channel.min !== channel.max); + if (!alpha || alpha.max === 0 || !hasPixelVariation) { + throw new Error(`${relativePath} has no visible, non-uniform pixels`); + } +} + +async function checkGeneratedAssets() { + console.log("Checking generated logo assets...\n"); + + const squareBlack = renderSquareLogo("black"); + const squareWhite = renderSquareLogo("white"); + const textTargets = { + "docs/img/logo-black.svg": squareBlack, + "docs/img/logo-white.svg": squareWhite, + ...WORDMARK_TARGETS, + "src/browser/assets/icons/xum.svg": squareBlack, + "docs/favicon.svg": renderThemeFaviconSvg(squareBlack), + } as const; + + for (const [relativePath, expected] of Object.entries(textTargets)) { + const actual = await readFile(path.join(ROOT, relativePath), "utf8"); + assertFresh(relativePath, actual, expected); + await validateImage(relativePath, [ + [ + Math.round(Number(expected.match(/width="([\d.]+)"/)?.[1])), + Math.round(Number(expected.match(/height="([\d.]+)"/)?.[1])), + ], + ]); + console.log(`✓ ${relativePath}`); + } + + for (const [relativePath, className] of Object.entries(INLINE_WORDMARK_TARGETS)) { + const actual = await readFile(path.join(ROOT, relativePath), "utf8"); + const expected = replaceInlineWordmarkContent(actual, relativePath, className); + assertFresh(relativePath, actual, expected); + console.log(`✓ ${relativePath}`); + } + + for (const [relativePath, config] of Object.entries(LOGO_TARGETS)) { + if ("svg" in config) continue; + + const outputPath = path.join(ROOT, relativePath); + const actual = await readFile(outputPath); + const expected = await (await generateRasterIcon(config)).toBuffer(); + assertFresh(relativePath, actual, expected); + const [width, height] = Array.isArray(config.size) ? config.size : [config.size, config.size]; + await validateImage(relativePath, [[width, height]]); + console.log(`✓ ${relativePath}`); + } + + const faviconDimensions = FAVICON_SIZES.map((size) => [size, size] as const); + await validateImage("public/favicon.ico", faviconDimensions); + console.log("✓ public/favicon.ico"); + await validateImage("public/favicon-dark.ico", faviconDimensions); + console.log("✓ public/favicon-dark.ico"); + + console.log("\n✅ All tracked logo assets are fresh and parseable!"); +} + async function generateBuildPng() { // Build PNG is App Icon (White on Black) const img = await generateRasterIcon({ size: 512, ...APP_ICON }); @@ -383,6 +469,10 @@ if (commands.has("update")) { await updateAllLogos(); } +if (commands.has("check")) { + await checkGeneratedAssets(); +} + if (commands.has("linux-icons")) { await mkdir(BUILD_DIR, { recursive: true }); await generateLinuxIcons(); @@ -402,8 +492,9 @@ if (commands.has("png") || commands.has("icns")) { await generateIcns(); } catch (e) { console.warn("Failed to generate ICNS:", e); + } finally { + // Only the ICNS process owns this directory; parallel PNG builds must not remove it. + await rm(ICONSET_DIR, { recursive: true, force: true }); } } - - await rm(ICONSET_DIR, { recursive: true, force: true }); } diff --git a/scripts/logoAssets.ts b/scripts/logoAssets.ts index ca00057d81..755534380a 100644 --- a/scripts/logoAssets.ts +++ b/scripts/logoAssets.ts @@ -1,17 +1,13 @@ /** * Canonical Xum logo geometry. * - * Xum is an anagram of Mux, so the outlined paths reuse the exact Geist Sans Bold - * glyphs from the prior Mux logo in x-u-m order. This preserves the established - * baseline, scale, and cursor spacing without relying on platform font rendering. + * The square mark uses four sturdy ribbons converging on an open center: parallel + * agent workflows meeting in one shared workspace. The broken-X silhouette is + * cursor-free, remains recognizable at favicon scale, and inverts as one color. */ const XUM_MARK_PATH = - "M13.125 47.5 20.9101 36.4133 13.3288 25.6526H19.8096L24.497 32.6634L29.0214 25.6526H35.6652L28.1246 36.454L35.869 47.5H29.3882L24.5785 40.0817L19.7281 47.5H13.125Z"; -const XUM_MARK_CURSOR_X = 38.875; -const XUM_MARK_CURSOR_Y = 24.5; -const XUM_MARK_CURSOR_WIDTH = 20; -const XUM_MARK_CURSOR_HEIGHT = 23; + "M10 17 21 6 36 21 25 32ZM51 6 62 17 47 32 36 21ZM47 32 62 47 51 58 36 43ZM25 32 36 43 21 58 10 47Z"; const XUM_WORDMARK_PATH = "M2.208 48 11.376 34.944 2.448 22.272H10.08L15.6 30.528L20.928 22.272H28.752L19.872 34.992L28.992 48H21.36L15.696 39.264L9.984 48H2.208ZM41.856 48.576C39.2 48.576 37.12 47.728 35.616 46.032C34.144 44.304 33.408 41.904 33.408 38.832V22.272H40.608V37.152C40.608 39.136 40.912 40.592 41.52 41.52C42.128 42.416 43.088 42.864 44.4 42.864C45.872 42.864 47.008 42.368 47.808 41.376C48.64 40.352 49.056 38.832 49.056 36.816V22.272H56.256V48H49.68L49.488 40.608L50.4 40.8C50.016 43.36 49.104 45.296 47.664 46.608C46.224 47.92 44.288 48.576 41.856 48.576ZM62.544 48V22.272H69.024L69.264 28.464L68.592 28.176C68.944 26.8 69.472 25.632 70.176 24.672C70.912 23.712 71.792 22.976 72.816 22.464C73.84 21.952 74.96 21.696 76.176 21.696C78.32 21.696 80.048 22.32 81.36 23.568C82.704 24.816 83.568 26.496 83.952 28.608L83.04 28.656C83.328 27.152 83.824 25.888 84.528 24.864C85.264 23.808 86.16 23.024 87.216 22.512C88.272 21.968 89.456 21.696 90.768 21.696C92.56 21.696 94.096 22.064 95.376 22.8C96.656 23.536 97.648 24.64 98.352 26.112C99.056 27.552 99.408 29.328 99.408 31.44V48H92.208V33.456C92.208 31.44 91.904 29.936 91.296 28.944C90.688 27.92 89.696 27.408 88.32 27.408C87.456 27.408 86.72 27.648 86.112 28.128C85.504 28.608 85.024 29.312 84.672 30.24C84.352 31.136 84.192 32.24 84.192 33.552V48H77.712V33.552C77.712 31.568 77.424 30.048 76.848 28.992C76.272 27.936 75.28 27.408 73.872 27.408C73.008 27.408 72.256 27.648 71.616 28.128C71.008 28.608 70.544 29.312 70.224 30.24C69.904 31.168 69.744 32.272 69.744 33.552V48H62.544Z"; @@ -26,9 +22,8 @@ export type LogoFill = "black" | "white" | "currentColor"; export function renderSquareLogo(fill: LogoFill): string { return ` - + - `; } diff --git a/src/browser/assets/icons/xum.svg b/src/browser/assets/icons/xum.svg index ec307fc548..0727673900 100644 --- a/src/browser/assets/icons/xum.svg +++ b/src/browser/assets/icons/xum.svg @@ -1,5 +1,4 @@ - - - + + diff --git a/vscode/icon.png b/vscode/icon.png index f8e59ca23207592d1bee22340c8db49ea24b196a..a0d2b7458644e5269fd8be50c2a2e83c8bb731c1 100644 GIT binary patch delta 1102 zcmZXTZA?>l7{xF30@uNm|5gkgRLX7Hpj2?61C$ppmXPFh)|F+;mhAa(az30qUw-E_ zBMw9#r-H-N`Pi=N&V{Zk4RnkyNWPa_u>MfqdxN7DZ$!KsOB#HS0ozzs)&_vtOB^2X z`cQeOfyM*Z7J5b;%H7YI{g?Zu=DQiEM^+#qb$%&-3!nb*^$%Zu{LJ(2kD1Uk z)crZL5>k3p`yUJ0XtC)A3`pe{&UuwQ9A}VXwX{lTLPad|5>hE7B5+T#?|I;8%-yYP z$FD5P<*zE&3zemSXd^G_q-y70)Z#N&6*%@5^%M{W@=~L;QSl2x3aI=1+>|6Sd{OUw zzzz%V3B?OU$_qeb(P>$MR#_x1liCi7_fwPnnGoA%{3*x$LWLyE)uigX=JErvI>i&@ zBfax6THhk`$5t67+hAWZQ>afT?g&RY=3Q`GR#GXv+M~el$ix|WV#ASCm-5#q9f^JW zAiF_Yo{_LJ`n)p}(qS1spxC+?i}R?L0)432hI~sn0x_dPJXLQ-O^oU8qlKE9X;_nk zJ5*4%M*?Km&U|u?IyapHH#=8;g|xTaq>mL+SzyDuo)COL$v_{aD2ri-Rj28Vpa7UK z1Ou#|jo=FHd$%4l6C?iNSmBD8TF`q?U11&~LG|d{T9nDax5sod6 zc9;^Qdc`bqnHW-UAy2BYLV@1JmH@ics%9=Cw|yzzIB%d>HCeW92)Rw88l;}$smFVS`aDZM>=Ra& zdxwh;V{2PC?>`Y`^)%a(e9_6gzO}XaXlZNYK}Jw^g%=w6T2!hUIZK= zRVeU_Z+>V(b{uyOqS63M1rMi(nH!M+BDj$SEV50sx2Y<28ec_rXpV1p4gaP+Y`fJ` z&OJRR(T+0$yz)0voPNwzhdvVhUqqJ4;DAC<9~m$ILsNa7@}KPyBzoKX$j#4~5%p6AtWg?HPiZor=;7{2U-8Kwj9&nw!zYTdPv0c=>&h_o+5T| literal 1043 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9GG!XV7ZFl&wk0|WC> zPZ!6KiaBrZ_;&aO${hbV|6qmx6Vsa&`r69^lr+71g*Js`1(Zc-Bx*!^dL^YMHu6T^ z(9jMNNm?u^w7gT1Tfb6%SE=~qJC*1D-!nch^SsW!)I9k7|9RE__Rsrm^L$>q3eaY} zV9}P~OZzBV^uV%&b=t+Vtvk}C9!SEaYsMv8qdoQbChKgxS;BMOWs2&@{`SE z)xBnOUT<6Skaa7E!-Mk1>21ekv?}Luw-wh+GO2HfVo}&oI_a0q?9Z{6f|CDkZTod| z<6=i!@q>HqJI^!xoh!2>`e20Td7iE7tFDv?n%M1R=$Z88M8U&$+t>4quGA=J?sE4P zcYHjV+aYb^68(AN|D1!Oo2yQ%{{6D%dioE)w@Mi`lNe9DF^~DvVU`lHE0$?1F6w6PN$k;UI;Jko@RZx&z1AHrG0$IF6}4rnHxvLZ ze#wwxHhquimM7g7`_|hACFOvW$}_xvboSyO(`w%JJL8K|FS=g%0rb#r{c_2Br?z|i z%7}Oy9Z(blRC!&AVdm6h>x=dYZ{K}rMhr{udS3>k4XV*5p#V3l<;~QW=b{d-+`{_7rC7 zJqR@67F$BrKhbkmfvcJNp#18Som#smzli5JATU$+W$j6ZDUqM`ofz62wjHZq{pfb; z^p7j!1(^(3_}F%(N6q^!uE^lK&E?W|2gVI={%ATe%*xoZCsQ--ThB)c?ZEU)A2J z{nE)y1f}y6S)S}X$Imc4zmsfnzpY8?&tea}xd)iXXFcIhTXN|Fv)b_ Date: Sat, 22 Aug 2026 12:56:36 +0500 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20finish=20devel?= =?UTF-8?q?oper=20tooling=20Xum=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make Xum canonical for the Make CLI, Terminal-Bench harness, service-worker cache, local logs, and shared developer environment while preserving explicit legacy aliases and environment inputs. Add a scoped, Make-backed branding audit with behavioral coverage for stale copy and filenames. --- .envrc | 12 +- .github/workflows/nightly-terminal-bench.yml | 37 +- .github/workflows/terminal-bench.yml | 50 +- .xum/skills/tbench/SKILL.md | 36 +- Makefile | 29 +- benchmarks/terminal_bench/__init__.py | 9 +- .../terminal_bench/analyze_failure_rates.py | 117 ++--- .../terminal_bench/download_run_logs.py | 2 +- benchmarks/terminal_bench/mux-run.sh | 210 +------- benchmarks/terminal_bench/mux_agent.py | 463 +---------------- benchmarks/terminal_bench/mux_run_contract.py | 13 - benchmarks/terminal_bench/mux_setup.sh.j2 | 68 --- .../prepare_leaderboard_submission.py | 30 +- benchmarks/terminal_bench/tbench_utils.py | 2 +- benchmarks/terminal_bench/xum-run.sh | 222 ++++++++ benchmarks/terminal_bench/xum_agent.py | 481 ++++++++++++++++++ .../{mux_agent_test.py => xum_agent_test.py} | 211 +++++--- .../{mux_payload.py => xum_payload.py} | 4 +- benchmarks/terminal_bench/xum_run_contract.py | 13 + benchmarks/terminal_bench/xum_setup.sh.j2 | 78 +++ public/service-worker.js | 9 +- scripts/audit_xum_branding.py | 195 +++++++ scripts/audit_xum_branding_test.py | 58 +++ scripts/check-bench-agent.sh | 18 +- scripts/check_tbench_results.py | 12 +- src/browser/serviceWorker.test.ts | 42 ++ src/cli/server.ts | 4 +- src/cli/serverCrashLogging.test.ts | 8 +- src/common/constants/paths.ts | 4 +- src/desktop/main.ts | 2 +- src/node/services/log.test.ts | 37 +- src/node/services/log.ts | 18 +- 32 files changed, 1478 insertions(+), 1016 deletions(-) delete mode 100644 benchmarks/terminal_bench/mux_run_contract.py delete mode 100644 benchmarks/terminal_bench/mux_setup.sh.j2 create mode 100644 benchmarks/terminal_bench/xum-run.sh create mode 100644 benchmarks/terminal_bench/xum_agent.py rename benchmarks/terminal_bench/{mux_agent_test.py => xum_agent_test.py} (62%) rename benchmarks/terminal_bench/{mux_payload.py => xum_payload.py} (83%) create mode 100644 benchmarks/terminal_bench/xum_run_contract.py create mode 100644 benchmarks/terminal_bench/xum_setup.sh.j2 create mode 100644 scripts/audit_xum_branding.py create mode 100644 scripts/audit_xum_branding_test.py create mode 100644 src/browser/serviceWorker.test.ts diff --git a/.envrc b/.envrc index c4067ebec5..59a87c8a00 100644 --- a/.envrc +++ b/.envrc @@ -8,9 +8,13 @@ nix_direnv_manual_reload use flake . -# Optional: shared per-user env vars for all mux worktrees -MUX_SHARED_ENVRC="$HOME/.mux/.envrc" -if [[ -f "$MUX_SHARED_ENVRC" ]]; then +# Optional: shared per-user env vars for all Xum worktrees. +XUM_SHARED_ENVRC="$HOME/.xum/.envrc" +LEGACY_MUX_SHARED_ENVRC="$HOME/.mux/.envrc" +if [[ -f "$XUM_SHARED_ENVRC" ]]; then # source_env() also calls watch_file(), so edits trigger direnv reloads. - source_env "$MUX_SHARED_ENVRC" + source_env "$XUM_SHARED_ENVRC" +elif [[ -f "$LEGACY_MUX_SHARED_ENVRC" ]]; then + # Existing developer machines may not have moved their shared env file yet. + source_env "$LEGACY_MUX_SHARED_ENVRC" fi diff --git a/.github/workflows/nightly-terminal-bench.yml b/.github/workflows/nightly-terminal-bench.yml index d6d7975721..ca69703561 100644 --- a/.github/workflows/nightly-terminal-bench.yml +++ b/.github/workflows/nightly-terminal-bench.yml @@ -18,8 +18,13 @@ on: description: "Experiments to enable (comma-separated)" required: false type: string + xum_run_as_goal: + description: "Run nightly smoke/matrix tasks as strict Xum CLI Goal Runs" + required: false + default: false + type: boolean mux_run_as_goal: - description: "Run nightly smoke/matrix tasks as strict mux CLI Goal Runs" + description: "Deprecated alias for xum_run_as_goal" required: false default: false type: boolean @@ -32,13 +37,13 @@ jobs: uses: ./.github/workflows/terminal-bench.yml with: model_name: "anthropic/claude-sonnet-4-5" - mux_run_args: "--thinking high" + xum_run_args: "--thinking high" dataset: "terminal-bench@2.0" concurrency: "1" env: "daytona" task_names: "chess-best-move" experiments: ${{ inputs.experiments }} - mux_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && inputs.mux_run_as_goal || false }} + xum_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && (inputs.xum_run_as_goal || inputs.mux_run_as_goal) || false }} # Keep least-privilege secret scope for reusable workflow calls. secrets: TERMINAL_BENCH_ANTHROPIC_API_KEY: ${{ secrets.TERMINAL_BENCH_ANTHROPIC_API_KEY }} @@ -48,23 +53,23 @@ jobs: GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} # Smoke tests for Harbor SWE-style datasets (single task each). - # These validate that mux can run inside the dataset's repo checkout (--runtime local) - # and that we target the verifier's expected working directory (MUX_PROJECT_PATH). + # These validate that Xum can run inside the dataset's repo checkout (--runtime local) + # and that we target the verifier's expected working directory (XUM_PROJECT_PATH). swebench-verified-smoke-test: name: "Smoke test (swebench-verified: django__django-10097)" needs: smoke-test uses: ./.github/workflows/terminal-bench.yml with: model_name: "anthropic/claude-sonnet-4-5" - mux_run_args: "--thinking high --runtime local" + xum_run_args: "--thinking high --runtime local" dataset: "swebench-verified@1.0" concurrency: "1" env: "daytona" task_names: "django__django-10097" - mux_project_path: "/testbed" + xum_project_path: "/testbed" timeout: "3000" experiments: ${{ inputs.experiments }} - mux_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && inputs.mux_run_as_goal || false }} + xum_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && (inputs.xum_run_as_goal || inputs.mux_run_as_goal) || false }} secrets: TERMINAL_BENCH_ANTHROPIC_API_KEY: ${{ secrets.TERMINAL_BENCH_ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -78,15 +83,15 @@ jobs: uses: ./.github/workflows/terminal-bench.yml with: model_name: "anthropic/claude-sonnet-4-5" - mux_run_args: "--thinking high --runtime local" + xum_run_args: "--thinking high --runtime local" dataset: "swe-gen-js@1.0" concurrency: "1" env: "daytona" task_names: "biomejs__biome-7314" - mux_project_path: "/app/src" + xum_project_path: "/app/src" timeout: "600" experiments: ${{ inputs.experiments }} - mux_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && inputs.mux_run_as_goal || false }} + xum_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && (inputs.xum_run_as_goal || inputs.mux_run_as_goal) || false }} secrets: TERMINAL_BENCH_ANTHROPIC_API_KEY: ${{ secrets.TERMINAL_BENCH_ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -100,15 +105,15 @@ jobs: uses: ./.github/workflows/terminal-bench.yml with: model_name: "anthropic/claude-sonnet-4-5" - mux_run_args: "--thinking high --runtime local" + xum_run_args: "--thinking high --runtime local" dataset: "aider-polyglot@1.0" concurrency: "1" env: "daytona" task_names: "polyglot_cpp_all-your-base" - mux_project_path: "/app" + xum_project_path: "/app" timeout: "1800" experiments: ${{ inputs.experiments }} - mux_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && inputs.mux_run_as_goal || false }} + xum_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && (inputs.xum_run_as_goal || inputs.mux_run_as_goal) || false }} secrets: TERMINAL_BENCH_ANTHROPIC_API_KEY: ${{ secrets.TERMINAL_BENCH_ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -147,12 +152,12 @@ jobs: uses: ./.github/workflows/terminal-bench.yml with: model_name: ${{ matrix.model }} - mux_run_args: "--thinking high" + xum_run_args: "--thinking high" dataset: "terminal-bench@2.0" concurrency: "48" env: "daytona" experiments: ${{ inputs.experiments }} - mux_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && inputs.mux_run_as_goal || false }} + xum_run_as_goal: ${{ github.event_name == 'workflow_dispatch' && (inputs.xum_run_as_goal || inputs.mux_run_as_goal) || false }} allow_transient_agent_errors: true secrets: TERMINAL_BENCH_ANTHROPIC_API_KEY: ${{ secrets.TERMINAL_BENCH_ANTHROPIC_API_KEY }} diff --git a/.github/workflows/terminal-bench.yml b/.github/workflows/terminal-bench.yml index 1c5247434d..33419d28e4 100644 --- a/.github/workflows/terminal-bench.yml +++ b/.github/workflows/terminal-bench.yml @@ -44,18 +44,34 @@ on: required: false type: string default: "" - mux_project_path: + xum_project_path: description: "Project path inside the task container (e.g., /testbed, /app/src)" required: false type: string default: "" + xum_run_args: + description: "Additional CLI flags passed to Xum run (e.g., --thinking high --use-1m --budget 5.00; with goal mode, add --goal-turns/--goal-budget)" + required: false + type: string + default: "" + xum_run_as_goal: + description: "Run each task instruction as a Xum CLI Goal Run" + required: false + type: boolean + default: false + # Compatibility inputs for reusable-workflow callers created before the rename. + mux_project_path: + description: "Deprecated alias for xum_project_path" + required: false + type: string + default: "" mux_run_args: - description: "Additional CLI flags passed to mux run (e.g., --thinking high --use-1m --budget 5.00; with goal mode, add --goal-turns/--goal-budget)" + description: "Deprecated alias for xum_run_args" required: false type: string default: "" mux_run_as_goal: - description: "Run each task instruction as a mux CLI Goal Run" + description: "Deprecated alias for xum_run_as_goal" required: false type: boolean default: false @@ -101,12 +117,14 @@ on: description: "Model to use (e.g., anthropic/claude-opus-5, openai/gpt-5.6-sol)" required: false type: string + # GitHub caps workflow_dispatch at ten inputs. Keep these established + # external names while reusable workflow calls use canonical xum_* inputs. mux_run_args: - description: "Additional CLI flags passed to mux run (e.g., --thinking high --use-1m; with goal mode, add --goal-turns/--goal-budget)" + description: "Additional CLI flags passed to Xum run (legacy input name)" required: false type: string mux_run_as_goal: - description: "Run each task instruction as a mux CLI Goal Run" + description: "Run each task instruction as a Xum CLI Goal Run (legacy input name)" required: false default: false type: boolean @@ -192,15 +210,15 @@ jobs: TB_ENV: ${{ inputs.env }} TB_TASK_NAMES: ${{ inputs.task_names }} TB_MODEL: ${{ inputs.model_name }} - MUX_MODEL: ${{ inputs.model_name }} + XUM_MODEL: ${{ inputs.model_name }} TB_TIMEOUT: ${{ inputs.timeout }} - MUX_PROJECT_PATH: ${{ inputs.mux_project_path }} + XUM_PROJECT_PATH: ${{ inputs.xum_project_path || inputs.mux_project_path }} TB_ARGS: >- ${{ inputs.max_tasks && format('--n-tasks {0}', inputs.max_tasks) || '' }} ${{ inputs.extra_args || '' }} - MUX_EXPERIMENTS: ${{ inputs.experiments }} - MUX_RUN_ARGS: ${{ inputs.mux_run_args }} - MUX_RUN_AS_GOAL: ${{ inputs.mux_run_as_goal && '1' || '' }} + XUM_EXPERIMENTS: ${{ inputs.experiments }} + XUM_RUN_ARGS: ${{ inputs.xum_run_args || inputs.mux_run_args }} + XUM_RUN_AS_GOAL: ${{ (inputs.xum_run_as_goal || inputs.mux_run_as_goal) && '1' || '' }} ANTHROPIC_API_KEY: ${{ secrets.TERMINAL_BENCH_ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} @@ -280,13 +298,13 @@ jobs: echo "Artifact name: $ARTIFACT_NAME" - name: Upload Terminal-Bench results to BigQuery - if: always() && github.repository == 'coder/mux' && startsWith(inputs.dataset, 'terminal-bench@') + if: always() && github.repository == 'coder/xum' && startsWith(inputs.dataset, 'terminal-bench@') env: GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} GCP_PROJECT_ID: mux-benchmarks BQ_DATASET: benchmarks - MUX_EXPERIMENTS: ${{ inputs.experiments }} - MUX_RUN_AS_GOAL: ${{ inputs.mux_run_as_goal && '1' || '' }} + XUM_EXPERIMENTS: ${{ inputs.experiments }} + XUM_RUN_AS_GOAL: ${{ inputs.xum_run_as_goal && '1' || '' }} run: | if [ -z "$GCP_SA_KEY" ]; then echo "GCP_SA_KEY not set, skipping BigQuery upload" @@ -299,13 +317,13 @@ jobs: rm -f /tmp/gcp-sa.json - name: Upload Harbor results to BigQuery - if: always() && github.repository == 'coder/mux' && !startsWith(inputs.dataset, 'terminal-bench@') + if: always() && github.repository == 'coder/xum' && !startsWith(inputs.dataset, 'terminal-bench@') env: GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} GCP_PROJECT_ID: mux-benchmarks BQ_DATASET: benchmarks - MUX_EXPERIMENTS: ${{ inputs.experiments }} - MUX_RUN_AS_GOAL: ${{ inputs.mux_run_as_goal && '1' || '' }} + XUM_EXPERIMENTS: ${{ inputs.experiments }} + XUM_RUN_AS_GOAL: ${{ inputs.xum_run_as_goal && '1' || '' }} run: | if [ -z "$GCP_SA_KEY" ]; then echo "GCP_SA_KEY not set, skipping BigQuery upload" diff --git a/.xum/skills/tbench/SKILL.md b/.xum/skills/tbench/SKILL.md index 32d7757800..687fa6e0ea 100644 --- a/.xum/skills/tbench/SKILL.md +++ b/.xum/skills/tbench/SKILL.md @@ -19,7 +19,7 @@ make benchmark-terminal make benchmark-terminal TB_TASK_NAMES="hello-world chess-best-move" # Run with specific model and xhigh thinking -MUX_RUN_ARGS="--thinking xhigh" make benchmark-terminal TB_ARGS="--agent-kwarg model_name=anthropic/claude-opus-5" +XUM_RUN_ARGS="--thinking xhigh" make benchmark-terminal TB_ARGS="--agent-kwarg model_name=anthropic/claude-opus-5" # Run on Daytona cloud (high parallelism) TB_ENV=daytona TB_CONCURRENCY=48 make benchmark-terminal @@ -58,8 +58,8 @@ make benchmark-terminal TB_ENV=daytona TB_CONCURRENCY=48 TB_TASK_NAMES="chess-be - `TB_ENV`: Environment to run in (`local` or `daytona`) - `TB_TASK_NAMES`: Space-separated task names to run (default: all tasks) - `TB_ARGS`: Additional arguments passed to harbor -- `MUX_RUN_ARGS`: CLI flags passed directly to `mux run` inside the container (e.g., `--thinking high --use-1m --budget 5.00`). This is the primary mechanism for all `mux run` flags — avoids per-flag plumbing. -- `MUX_RUN_AS_GOAL`: When set to `1`, runs each task instruction as a strict `mux run --goal` objective while still piping the instruction to stdin. Use `MUX_RUN_ARGS` for goal limits such as `--goal-turns` and `--goal-budget`. Incomplete strict-goal exits are left scoreable so Harbor can verify the workspace. +- `XUM_RUN_ARGS`: CLI flags passed directly to `xum run` inside the container (e.g., `--thinking high --use-1m --budget 5.00`). This is the primary mechanism for all `xum run` flags — avoids per-flag plumbing. +- `XUM_RUN_AS_GOAL`: When set to `1`, runs each task instruction as a strict `xum run --goal` objective while still piping the instruction to stdin. Use `XUM_RUN_ARGS` for goal limits such as `--goal-turns` and `--goal-budget`. Incomplete strict-goal exits are left scoreable so Harbor can verify the workspace. ### Timeout Handling @@ -94,7 +94,7 @@ The agent adapter accepts a few Harbor kwargs (passed via `--agent-kwarg`): - `model_name`: Model to use (e.g., `anthropic/claude-opus-5`, `openai/gpt-5.6-sol`) - `experiments`: Experiments to enable, comma-separated (e.g., `programmatic-tool-calling`) -All other `mux run` CLI flags (thinking level, mode, runtime, budget, etc.) are passed via `MUX_RUN_ARGS` — no per-flag plumbing needed. +All other `xum run` CLI flags (thinking level, mode, runtime, budget, etc.) are passed via `XUM_RUN_ARGS` — no per-flag plumbing needed. **CI dispatch (primary method):** @@ -114,8 +114,8 @@ gh workflow run terminal-bench.yml \ ```bash # Run a single task as a strict CLI Goal Run -MUX_RUN_AS_GOAL=1 \ -MUX_RUN_ARGS="--thinking high --goal-turns 30 --goal-budget 10.00" \ +XUM_RUN_AS_GOAL=1 \ +XUM_RUN_ARGS="--thinking high --goal-turns 30 --goal-budget 10.00" \ make benchmark-terminal TB_TASK_NAMES="chess-best-move" # CI dispatch @@ -129,11 +129,11 @@ gh workflow run terminal-bench.yml \ **Local runs:** ```bash -# Pass flags via MUX_RUN_ARGS env var -MUX_RUN_ARGS="--thinking high --use-1m" make benchmark-terminal +# Pass flags via XUM_RUN_ARGS env var +XUM_RUN_ARGS="--thinking high --use-1m" make benchmark-terminal # Model and experiments via TB_ARGS -MUX_RUN_ARGS="--thinking high" make benchmark-terminal TB_ARGS="--agent-kwarg model_name=openai/gpt-5.6-sol --agent-kwarg experiments=programmatic-tool-calling" +XUM_RUN_ARGS="--thinking high" make benchmark-terminal TB_ARGS="--agent-kwarg model_name=openai/gpt-5.6-sol --agent-kwarg experiments=programmatic-tool-calling" ``` ## Monitoring local benchmark output @@ -172,7 +172,7 @@ Results are saved to `runs/YYYY-MM-DD__HH-MM-SS/`: ## Querying Results from BigQuery -Mux Terminal-Bench results are uploaded to BigQuery after CI runs. Query via `bq` CLI after authenticating with `gcloud auth login` and setting project to `mux-benchmarks`. +Xum Terminal-Bench results are uploaded to BigQuery after CI runs. Query via `bq` CLI after authenticating with `gcloud auth login` and setting project to `mux-benchmarks`. **Table:** `mux-benchmarks.benchmarks.tbench_results` @@ -211,7 +211,7 @@ python3 benchmarks/terminal_bench/prepare_leaderboard_submission.py --n-runs 5 - This creates a properly structured submission folder at `leaderboard_submission/` containing: ``` -submissions/terminal-bench/2.0/Mux__/ +submissions/terminal-bench/2.0/Xum__/ metadata.yaml # Agent and model info / # Results from run 1 config.json @@ -271,10 +271,10 @@ The PR will be automatically validated by the leaderboard bot. Once merged, resu ## Files -- `mux_agent.py`: Main agent adapter implementing Harbor's `BaseInstalledAgent` interface -- `mux-run.sh`: Shell script that sets up environment and invokes xum CLI -- `mux_payload.py`: Helper to package xum app for containerized execution -- `mux_setup.sh.j2`: Jinja2 template for agent installation script +- `xum_agent.py`: Main agent adapter implementing Harbor's `BaseInstalledAgent` interface +- `xum-run.sh`: Shell script that sets up environment and invokes xum CLI +- `xum_payload.py`: Helper to package xum app for containerized execution +- `xum_setup.sh.j2`: Jinja2 template for agent installation script - `prepare_leaderboard_submission.py`: Script to prepare results for leaderboard submission - `analyze_failure_rates.py`: Analyze failure rates to find optimization opportunities - `download_run_logs.py`: Download and inspect raw agent logs from nightly runs @@ -357,7 +357,7 @@ python benchmarks/terminal_bench/analyze_failure_rates.py python benchmarks/terminal_bench/analyze_failure_rates.py --top 50 # Filter to specific Xum model -python benchmarks/terminal_bench/analyze_failure_rates.py --mux-model sonnet +python benchmarks/terminal_bench/analyze_failure_rates.py --xum-model sonnet # Force refresh of cached data python benchmarks/terminal_bench/analyze_failure_rates.py --refresh @@ -382,8 +382,8 @@ OPTIMIZATION OPPORTUNITIES (sorted by M/O ratio) ================================================================================ Task ID Xum Fail% Avg Other% M/O Ratio Agent -------------------------------------------------------------------------------- -some-difficult-task 100.0% 10.0% 9.09 Mux__Claude-Sonnet-4.5 -another-task 80.0% 20.0% 3.64 Mux__Claude-Sonnet-4.5 +some-difficult-task 100.0% 10.0% 9.09 Xum__Claude-Sonnet-4.5 +another-task 80.0% 20.0% 3.64 Xum__Claude-Sonnet-4.5 ... ================================================================================ diff --git a/Makefile b/Makefile index 69621d46cc..5a1796cd47 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Build System # ============ -# This Makefile orchestrates the mux build process. +# This Makefile orchestrates the Xum build process. # # Quick Start: # make help - Show all available targets @@ -85,8 +85,8 @@ include fmt.mk .PHONY: docs-server check-docs-links .PHONY: storybook storybook-run storybook-build test-storybook .PHONY: benchmark-terminal -.PHONY: ensure-deps rebuild-native mux -.PHONY: check-eager-imports check-bundle-size check-startup +.PHONY: ensure-deps rebuild-native xum mux +.PHONY: check-eager-imports check-bundle-size check-startup check-xum-branding # Use the package binary instead of its internal path so native-preview can change wrappers safely. TSGO := bun run tsgo @@ -139,13 +139,16 @@ rebuild-native: node_modules/.installed ## Rebuild native modules (node-pty, Duc @npx @electron/rebuild -f -m node_modules/@duckdb/node-bindings @echo "Native modules rebuilt successfully" -# Run compiled CLI with trailing arguments (builds only if missing) -mux: ## Run the compiled mux CLI (e.g., make mux server --port 3000) +# Run compiled CLI with trailing arguments (builds only if missing). +xum: ## Run the compiled Xum CLI (e.g., make xum server --port 3000) @test -f dist/cli/index.js -a -f dist/cli/api.mjs || $(MAKE) build-main - @node dist/cli/index.js $(filter-out $@,$(MAKECMDGOALS)) + @node dist/cli/index.js $(filter-out xum mux,$(MAKECMDGOALS)) -# Catch unknown targets passed to mux (prevents "No rule to make target" errors) -ifneq ($(filter mux,$(MAKECMDGOALS)),) +# Compatibility target for scripts written before the CLI target rename. +mux: xum ## Legacy alias for `make xum` + +# Catch trailing CLI arguments so make forwards them instead of treating them as targets. +ifneq ($(filter xum mux,$(MAKECMDGOALS)),) %: @: endif @@ -348,7 +351,11 @@ build/icon.png: docs/img/logo-white.svg scripts/generate-icons.ts # verification stay in static-check-full so local validation remains responsive. static-check: lint typecheck fmt-check check-eager-imports check-code-docs-links lint-shellcheck lint-hadolint ## Run fast local static checks -static-check-full: static-check check-bench-agent check-docs-links ## Run the full CI static check suite +static-check-full: static-check check-bench-agent check-docs-links check-xum-branding ## Run the full CI static check suite + +check-xum-branding: ## Audit project-owned rename surfaces against the explicit compatibility allowlist + @python3 -m unittest scripts.audit_xum_branding_test + @python3 scripts/audit_xum_branding.py check-bench-agent: node_modules/.installed src/version.ts $(BUILTIN_SKILLS_GENERATED) $(BUILTIN_WORKFLOWS_GENERATED) $(WORKFLOW_RUNTIME_SOURCES_GENERATED) ## Verify terminal-bench agent configuration and imports @./scripts/check-bench-agent.sh @@ -565,11 +572,11 @@ benchmark-terminal: ## Run Terminal-Bench 2.0 with Harbor (use TB_HARBOR_PACKAGE echo "Using Daytona package constraint: $$HARBOR_DAYTONA_PACKAGE"; \ echo "Using timeout: $$TB_TIMEOUT seconds"; \ echo "Running Terminal-Bench with dataset $$TB_DATASET (concurrency: $$TB_CONCURRENCY)"; \ - export MUX_TIMEOUT_MS=$$((TB_TIMEOUT * 1000)); \ + export XUM_TIMEOUT_MS=$$((TB_TIMEOUT * 1000)); \ uvx --from "$$HARBOR_PACKAGE" --with "$$HARBOR_DAYTONA_PACKAGE" python -c 'import importlib.metadata as m; print("Resolved Harbor package:", m.version("harbor")); print("Resolved Daytona package:", m.version("daytona"))'; \ uvx --from "$$HARBOR_PACKAGE" --with "$$HARBOR_DAYTONA_PACKAGE" harbor run \ --dataset "$$TB_DATASET" \ - --agent-import-path benchmarks.terminal_bench.mux_agent:MuxAgent \ + --agent-import-path benchmarks.terminal_bench.xum_agent:XumAgent \ --agent-kwarg timeout=$$TB_TIMEOUT \ --n-concurrent $$TB_CONCURRENCY \ $$ENV_FLAG \ diff --git a/benchmarks/terminal_bench/__init__.py b/benchmarks/terminal_bench/__init__.py index fdc0bf27e3..b48c7333b3 100644 --- a/benchmarks/terminal_bench/__init__.py +++ b/benchmarks/terminal_bench/__init__.py @@ -1,11 +1,12 @@ from __future__ import annotations -__all__ = ["MuxAgent"] +__all__ = ["XumAgent", "MuxAgent"] def __getattr__(name: str): - if name == "MuxAgent": - from .mux_agent import MuxAgent + if name in {"XumAgent", "MuxAgent"}: + from .xum_agent import XumAgent - return MuxAgent + # MuxAgent remains a lazy alias for existing Harbor configurations. + return XumAgent raise AttributeError(name) diff --git a/benchmarks/terminal_bench/analyze_failure_rates.py b/benchmarks/terminal_bench/analyze_failure_rates.py index ca9158b2a2..02da925e53 100755 --- a/benchmarks/terminal_bench/analyze_failure_rates.py +++ b/benchmarks/terminal_bench/analyze_failure_rates.py @@ -2,11 +2,11 @@ """ Analyze Terminal-Bench failure rates to identify optimization opportunities. -Pulls Mux results from BigQuery and other agents from HuggingFace leaderboard. +Pulls Xum results from BigQuery and other agents from HuggingFace leaderboard. Computes: - M/O ratio = Mux failure rate / Average failure rate of top 10 agents + M/O ratio = Xum failure rate / Average failure rate of top 10 agents -Tasks with high M/O ratio are where Mux underperforms relative to competitors, +Tasks with high M/O ratio are where Xum underperforms relative to competitors, representing the best optimization opportunities. Usage: @@ -16,15 +16,15 @@ # Show more results python benchmarks/terminal_bench/analyze_failure_rates.py --top 50 - # Filter to specific Mux model - python benchmarks/terminal_bench/analyze_failure_rates.py --mux-model "claude-sonnet" + # Filter to specific Xum model + python benchmarks/terminal_bench/analyze_failure_rates.py --xum-model "claude-sonnet" # Force re-download of data python benchmarks/terminal_bench/analyze_failure_rates.py --refresh Requirements: git (for cloning from HuggingFace) - bq CLI (for querying Mux results from BigQuery) + bq CLI (for querying Xum results from BigQuery) """ import argparse @@ -129,12 +129,12 @@ def download_leaderboard_data(refresh: bool = False) -> Path: raise -def query_mux_results_from_bq() -> list[TaskResult]: +def query_xum_results_from_bq() -> list[TaskResult]: """ - Query Mux results from BigQuery. + Query Xum results from BigQuery. Uses the bq CLI to query mux-benchmarks.benchmarks.tbench_results. - Returns TaskResult objects for all Mux benchmark runs. + Returns TaskResult objects for all Xum benchmark runs. """ import csv import subprocess @@ -149,7 +149,7 @@ def query_mux_results_from_bq() -> list[TaskResult]: WHERE dataset = 'terminal-bench@2.0' """ - print("Querying Mux results from BigQuery...", file=sys.stderr) + print("Querying Xum results from BigQuery...", file=sys.stderr) try: result = subprocess.run( [ @@ -178,7 +178,7 @@ def query_mux_results_from_bq() -> list[TaskResult]: results: list[TaskResult] = [] lines = result.stdout.strip().split("\n") if len(lines) < 2: - print("No Mux results found in BigQuery", file=sys.stderr) + print("No Xum results found in BigQuery", file=sys.stderr) return results reader = csv.DictReader(lines) @@ -202,19 +202,19 @@ def query_mux_results_from_bq() -> list[TaskResult]: TaskResult( task_id=task_id, passed=passed_str == "true", - agent_name="Mux", + agent_name="Xum", model_name=f"{model}@{thinking}", ) ) - print(f"Found {len(results)} Mux results from BigQuery", file=sys.stderr) + print(f"Found {len(results)} Xum results from BigQuery", file=sys.stderr) if skipped: print(f" (skipped {skipped} incomplete runs)", file=sys.stderr) return results def parse_leaderboard_results( - repo_path: Path, exclude_mux: bool = True + repo_path: Path, exclude_xum: bool = True ) -> list[TaskResult]: """ Parse all agent results from the leaderboard repo structure. @@ -227,7 +227,7 @@ def parse_leaderboard_results( result.json # contains "passed" or "score" Args: - exclude_mux: If True, skip Mux agents (we get those from BigQuery) + exclude_xum: If True, skip Xum agents (we get those from BigQuery) """ results: list[TaskResult] = [] submissions_dir = repo_path / "submissions" / "terminal-bench" / DATASET_VERSION @@ -240,13 +240,14 @@ def parse_leaderboard_results( if not agent_dir.is_dir(): continue - # Parse agent name and model from folder name (e.g., "Mux__Claude-Sonnet-4.5") + # Parse agent name and model from folder name (e.g., "Xum__Claude-Sonnet-4.5") parts = agent_dir.name.split("__", 1) agent_name = parts[0] model_name = parts[1] if len(parts) > 1 else "unknown" - # Skip Mux agents if requested (we get those from BigQuery) - if exclude_mux and agent_name.lower() == "mux": + # Skip Xum agents if requested (we get those from BigQuery) + # Keep historical Mux submissions grouped with canonical Xum results. + if exclude_xum and agent_name.lower() in {"xum", "mux"}: continue # Find all result.json files in trial folders @@ -306,9 +307,9 @@ def compute_agent_stats(results: list[TaskResult]) -> dict[str, AgentStats]: def get_top_agents(stats: dict[str, AgentStats], n: int = 10) -> list[str]: - """Get the top N agents by pass rate (excluding Mux).""" + """Get the top N agents by pass rate (excluding Xum).""" sorted_agents = sorted( - [(k, v) for k, v in stats.items() if not k.startswith("Mux__")], + [(k, v) for k, v in stats.items() if not k.startswith("Xum__")], key=lambda x: x[1].pass_rate, reverse=True, ) @@ -346,44 +347,44 @@ def compute_task_failure_rates( @dataclass class OptimizationOpportunity: - """A task where Mux underperforms relative to competitors.""" + """A task where Xum underperforms relative to competitors.""" task_id: str - mux_fail_rate: float + xum_fail_rate: float avg_other_fail_rate: float ratio: float # M/O ratio - mux_agent: str + xum_agent: str n_other_agents: int def find_optimization_opportunities( results: list[TaskResult], - mux_filter: str | None = None, + xum_filter: str | None = None, top_n_agents: int = 10, ) -> list[OptimizationOpportunity]: """ - Find tasks where Mux has high failure rate relative to top agents. + Find tasks where Xum has high failure rate relative to top agents. Returns opportunities sorted by M/O ratio (descending). """ stats = compute_agent_stats(results) - # Find Mux agents - xum_agents = [k for k in stats.keys() if k.startswith("Mux__")] - if mux_filter: - xum_agents = [k for k in xum_agents if mux_filter.lower() in k.lower()] + # Find Xum agents + xum_agents = [k for k in stats.keys() if k.startswith("Xum__")] + if xum_filter: + xum_agents = [k for k in xum_agents if xum_filter.lower() in k.lower()] if not xum_agents: - print("Warning: No Mux agents found in results", file=sys.stderr) + print("Warning: No Xum agents found in results", file=sys.stderr) return [] - # Get top N non-Mux agents + # Get top N non-Xum agents top_agents = get_top_agents(stats, top_n_agents) if not top_agents: - print("Warning: No non-Mux agents found", file=sys.stderr) + print("Warning: No non-Xum agents found", file=sys.stderr) return [] - print(f"\nAnalyzing Mux agents: {', '.join(xum_agents)}", file=sys.stderr) + print(f"\nAnalyzing Xum agents: {', '.join(xum_agents)}", file=sys.stderr) print(f"Comparing against top {len(top_agents)} agents:", file=sys.stderr) for agent in top_agents[:5]: s = stats[agent] @@ -398,15 +399,15 @@ def find_optimization_opportunities( all_relevant_agents = set(xum_agents) | set(top_agents) task_rates = compute_task_failure_rates(results, all_relevant_agents) - # Find opportunities for each Mux agent + # Find opportunities for each Xum agent opportunities: list[OptimizationOpportunity] = [] - for mux_agent in xum_agents: + for xum_agent in xum_agents: for task_id, agent_rates in task_rates.items(): - if mux_agent not in agent_rates: + if xum_agent not in agent_rates: continue - mux_fail_rate = agent_rates[mux_agent] + xum_fail_rate = agent_rates[xum_agent] # Compute average failure rate of top agents on this task other_rates = [ @@ -419,17 +420,17 @@ def find_optimization_opportunities( # Compute M/O ratio (add small epsilon to avoid div by zero) epsilon = 0.01 - ratio = mux_fail_rate / (avg_other_fail_rate + epsilon) + ratio = xum_fail_rate / (avg_other_fail_rate + epsilon) - # Only include if Mux actually fails sometimes - if mux_fail_rate > 0: + # Only include if Xum actually fails sometimes + if xum_fail_rate > 0: opportunities.append( OptimizationOpportunity( task_id=task_id, - mux_fail_rate=mux_fail_rate, + xum_fail_rate=xum_fail_rate, avg_other_fail_rate=avg_other_fail_rate, ratio=ratio, - mux_agent=mux_agent, + xum_agent=xum_agent, n_other_agents=len(other_rates), ) ) @@ -447,17 +448,17 @@ def print_opportunities( print("OPTIMIZATION OPPORTUNITIES (sorted by M/O ratio)") print(f"{'=' * 80}") print( - f"{'Task ID':<40} {'Mux Fail%':>10} {'Avg Other%':>11} {'M/O Ratio':>10} {'Agent':<20}" + f"{'Task ID':<40} {'Xum Fail%':>10} {'Avg Other%':>11} {'M/O Ratio':>10} {'Agent':<20}" ) print("-" * 80) for opp in opportunities[:top_n]: print( f"{opp.task_id:<40} " - f"{opp.mux_fail_rate * 100:>9.1f}% " + f"{opp.xum_fail_rate * 100:>9.1f}% " f"{opp.avg_other_fail_rate * 100:>10.1f}% " f"{opp.ratio:>10.2f} " - f"{opp.mux_agent:<20}" + f"{opp.xum_agent:<20}" ) if len(opportunities) > top_n: @@ -471,7 +472,7 @@ def print_opportunities( total_tasks = len(opportunities) high_ratio = sum(1 for o in opportunities if o.ratio > 2.0) medium_ratio = sum(1 for o in opportunities if 1.0 < o.ratio <= 2.0) - print(f"Total tasks with Mux failures: {total_tasks}") + print(f"Total tasks with Xum failures: {total_tasks}") print(f" High priority (M/O > 2.0): {high_ratio}") print(f" Medium priority (1.0 < M/O ≤ 2.0): {medium_ratio}") @@ -487,10 +488,12 @@ def main() -> None: help="Number of top opportunities to show (default: 20)", ) parser.add_argument( + "--xum-model", "--mux-model", + dest="xum_model", type=str, default=None, - help="Filter to specific Mux model (substring match)", + help="Filter to specific Xum model (substring match)", ) parser.add_argument( "--top-agents", @@ -510,22 +513,22 @@ def main() -> None: ) args = parser.parse_args() - # Get Mux results from BigQuery - mux_results = query_mux_results_from_bq() - if not mux_results: + # Get Xum results from BigQuery + xum_results = query_xum_results_from_bq() + if not xum_results: print( - "Warning: No Mux results from BigQuery. Ensure bq CLI is configured.", + "Warning: No Xum results from BigQuery. Ensure bq CLI is configured.", file=sys.stderr, ) # Download/load other agents from HuggingFace leaderboard repo_path = download_leaderboard_data(refresh=args.refresh) - print("Parsing leaderboard results (excluding Mux)...", file=sys.stderr) - other_results = parse_leaderboard_results(repo_path, exclude_mux=True) + print("Parsing leaderboard results (excluding Xum)...", file=sys.stderr) + other_results = parse_leaderboard_results(repo_path, exclude_xum=True) print(f"Found {len(other_results)} results from other agents", file=sys.stderr) # Merge results - results = mux_results + other_results + results = xum_results + other_results if not results: print("No results to analyze.", file=sys.stderr) sys.exit(1) @@ -533,7 +536,7 @@ def main() -> None: # Find opportunities opportunities = find_optimization_opportunities( results, - mux_filter=args.mux_model, + xum_filter=args.xum_model, top_n_agents=args.top_agents, ) @@ -541,10 +544,10 @@ def main() -> None: output = [ { "task_id": o.task_id, - "mux_fail_rate": o.mux_fail_rate, + "xum_fail_rate": o.xum_fail_rate, "avg_other_fail_rate": o.avg_other_fail_rate, "ratio": o.ratio, - "mux_agent": o.mux_agent, + "xum_agent": o.xum_agent, } for o in opportunities[: args.top] ] diff --git a/benchmarks/terminal_bench/download_run_logs.py b/benchmarks/terminal_bench/download_run_logs.py index cd0b44be16..50c604f83d 100755 --- a/benchmarks/terminal_bench/download_run_logs.py +++ b/benchmarks/terminal_bench/download_run_logs.py @@ -26,7 +26,7 @@ Prerequisites: - GitHub CLI (gh) installed and authenticated - - Access to coder/mux repository + - Access to coder/xum repository Output structure: .run_logs// diff --git a/benchmarks/terminal_bench/mux-run.sh b/benchmarks/terminal_bench/mux-run.sh index 05615ab06c..baf48af12b 100644 --- a/benchmarks/terminal_bench/mux-run.sh +++ b/benchmarks/terminal_bench/mux-run.sh @@ -1,211 +1,5 @@ #!/usr/bin/env bash - set -euo pipefail -log() { - printf '[mux-run] %s\n' "$1" -} - -fatal() { - printf '[mux-run] ERROR: %s\n' "$1" >&2 - exit 1 -} - -instruction=${1:-} -if [[ -z "${instruction}" ]]; then - fatal "instruction argument is required" -fi - -export BUN_INSTALL="${BUN_INSTALL:-/root/.bun}" -export PATH="${BUN_INSTALL}/bin:${PATH}" - -MUX_APP_ROOT="${MUX_APP_ROOT:-/opt/mux-app}" - -# Prefer an explicit MUX_CONFIG_ROOT, but fall back to MUX_ROOT for callers that -# only override the mux home via MUX_ROOT. -MUX_CONFIG_ROOT="${MUX_CONFIG_ROOT:-${MUX_ROOT:-/root/.mux}}" - -# Export MUX_ROOT so mux's getMuxHome() finds providers.jsonc and other config. -# Don't clobber caller-provided MUX_ROOT (e.g. local runs/tests with a custom root). -export MUX_ROOT="${MUX_ROOT:-${MUX_CONFIG_ROOT}}" -MUX_PROJECT_PATH="${MUX_PROJECT_PATH:-}" -MUX_PROJECT_CANDIDATES="${MUX_PROJECT_CANDIDATES:-/workspace:/app:/workspaces:/root/project}" -MUX_MODEL="${MUX_MODEL:-anthropic:claude-sonnet-4-5}" -MUX_TIMEOUT_MS="${MUX_TIMEOUT_MS:-}" -MUX_WORKSPACE_ID="${MUX_WORKSPACE_ID:-mux-bench}" -MUX_EXPERIMENTS="${MUX_EXPERIMENTS:-}" -MUX_RUN_AS_GOAL="${MUX_RUN_AS_GOAL:-}" - -mux_run_as_goal_normalized="${MUX_RUN_AS_GOAL,,}" -mux_run_as_goal_normalized="${mux_run_as_goal_normalized#"${mux_run_as_goal_normalized%%[![:space:]]*}"}" -mux_run_as_goal_normalized="${mux_run_as_goal_normalized%"${mux_run_as_goal_normalized##*[![:space:]]}"}" -case "${mux_run_as_goal_normalized}" in - "" | "0" | "false") mux_run_as_goal_enabled=0 ;; - "1" | "true") mux_run_as_goal_enabled=1 ;; - *) fatal "MUX_RUN_AS_GOAL must be one of: 1, true, 0, false" ;; -esac - -resolve_project_path() { - if [[ -n "${MUX_PROJECT_PATH}" ]]; then - if [[ -d "${MUX_PROJECT_PATH}" ]]; then - printf '%s\n' "${MUX_PROJECT_PATH}" - return 0 - fi - fatal "MUX_PROJECT_PATH=${MUX_PROJECT_PATH} not found" - fi - - IFS=":" read -r -a candidates <<<"${MUX_PROJECT_CANDIDATES}" - for candidate in "${candidates[@]}"; do - if [[ -d "${candidate}" ]]; then - printf '%s\n' "${candidate}" - return 0 - fi - done - - fatal "no project path located (searched ${MUX_PROJECT_CANDIDATES})" -} - -command -v bun >/dev/null 2>&1 || fatal "bun is not installed" -project_path=$(resolve_project_path) - -log "starting mux agent session for ${project_path}" -cd "${MUX_APP_ROOT}" - -cmd=(bun src/cli/run.ts - --dir "${project_path}" - --model "${MUX_MODEL}" - --keep-background-processes - --json) - -# Add experiment flags (comma-separated → repeated --experiment flags) -if [[ -n "${MUX_EXPERIMENTS}" ]]; then - IFS=',' read -r -a experiments <<<"${MUX_EXPERIMENTS}" - for exp in "${experiments[@]}"; do - # Trim whitespace - exp="${exp#"${exp%%[![:space:]]*}"}" - exp="${exp%"${exp##*[![:space:]]}"}" - if [[ -n "${exp}" ]]; then - cmd+=(--experiment "${exp}") - fi - done -fi - -if [[ "${mux_run_as_goal_enabled}" == "1" ]]; then - log "strict mux goal mode enabled" - cmd+=(--goal "${instruction}") -fi - -mux_run_args=() -# Append arbitrary mux run flags (e.g., --thinking high --mode exec --use-1m --budget 5.00) -if [[ -n "${MUX_RUN_ARGS:-}" ]]; then - # Word-split intentional: MUX_RUN_ARGS contains space-separated CLI flags. - # shellcheck disable=SC2206 - mux_run_args=(${MUX_RUN_ARGS}) - if [[ "${mux_run_as_goal_enabled}" == "1" ]]; then - for arg in "${mux_run_args[@]}"; do - if [[ "${arg}" == "--goal" || "${arg}" == --goal=* ]]; then - fatal "MUX_RUN_ARGS must not include --goal when MUX_RUN_AS_GOAL is enabled" - fi - done - fi - cmd+=("${mux_run_args[@]}") -fi - -# NOTE: Harbor only automatically collects /logs/agent on timeouts. -# Persist stdout/stderr there so partial agent output survives cancellation. -MUX_LOG_DIR="${MUX_LOG_DIR:-/logs/agent/command-0}" -mkdir -p "${MUX_LOG_DIR}" -MUX_OUTPUT_FILE="${MUX_LOG_DIR}/stdout.txt" -MUX_STDERR_FILE="${MUX_LOG_DIR}/stderr.txt" -MUX_TOKEN_FILE="${MUX_TOKEN_FILE:-/tmp/mux-tokens.json}" - -# Let Harbor classify task timeouts; GNU timeout would surface as exit 124. -if [[ -n "${MUX_TIMEOUT_MS}" ]]; then - if [[ ! "${MUX_TIMEOUT_MS}" =~ ^[0-9]+$ ]]; then - fatal "MUX_TIMEOUT_MS must be an integer" - fi - log "MUX_TIMEOUT_MS=${MUX_TIMEOUT_MS} forwarded; Harbor remains timeout authority" -fi - -# Capture output to file while streaming to terminal for token extraction. -# Keep stderr separate so the stdout log stays valid JSONL. -set +e -printf '%s' "${instruction}" \ - | "${cmd[@]}" \ - 2> >(tee "${MUX_STDERR_FILE}" >&2) \ - | tee "${MUX_OUTPUT_FILE}" -pipeline_status=("${PIPESTATUS[@]}") -set -e -stdin_status="${pipeline_status[0]}" -mux_status="${pipeline_status[1]}" -tee_status="${pipeline_status[2]}" - -# Extract usage and cost from the JSONL output. -# Prefer the run-complete event (emitted at end of --json run) which has aggregated -# totals. Fall back to summing usage-delta + session-usage-delta events when -# run-complete is missing (e.g. process killed by timeout, stdout not flushed). -python3 -c ' -import json, sys -result = {"input": 0, "output": 0, "cost_usd": None} -# Track cumulative usage from usage-delta events (keyed by messageId). -# Each usage-delta contains cumulative totals for its message, so we keep the -# latest per message and sum across messages at the end. -cumulative_by_msg = {} -# Track sub-agent usage from session-usage-delta events. These carry per-model -# byModelDelta dicts with {input: {tokens, cost_usd}, output: {tokens, cost_usd}, ...}. -# Each event is an incremental delta, so we sum them all. -subagent_input = 0 -subagent_output = 0 -for line in open(sys.argv[1]): - try: - obj = json.loads(line) - if obj.get("type") == "run-complete": - usage = obj.get("usage") or {} - result["input"] = usage.get("inputTokens", 0) or 0 - result["output"] = usage.get("outputTokens", 0) or 0 - result["cost_usd"] = obj.get("cost_usd") - print(json.dumps(result)) - sys.exit(0) - # Nested event wrapper: {"type":"event","payload":{"type":"usage-delta",...}} - payload = obj.get("payload") or obj - if payload.get("type") == "usage-delta": - msg_id = payload.get("messageId", "") - # Prefer cumulativeUsage (running total across all steps in a message) - # over usage (per-step delta). Keeping the latest cumulative per message - # gives the correct total when summed across messages. - usage = payload.get("cumulativeUsage") or payload.get("usage") or {} - cumulative_by_msg[msg_id] = usage - elif payload.get("type") == "session-usage-delta": - for model_usage in (payload.get("byModelDelta") or {}).values(): - subagent_input += (model_usage.get("input") or {}).get("tokens", 0) - subagent_output += (model_usage.get("output") or {}).get("tokens", 0) - except Exception: - pass -# No run-complete found — aggregate the last usage-delta per message + sub-agent totals -for usage in cumulative_by_msg.values(): - result["input"] += (usage.get("inputTokens", 0) or 0) - result["output"] += (usage.get("outputTokens", 0) or 0) -result["input"] += subagent_input -result["output"] += subagent_output -print(json.dumps(result)) -' "${MUX_OUTPUT_FILE}" >"${MUX_TOKEN_FILE}" 2>/dev/null || true - -if [[ "${mux_status}" -eq 3 && "${mux_run_as_goal_enabled}" == "1" ]]; then - printf '[mux-run] WARNING: mux goal run stopped incomplete (exit 3); leaving workspace for verifier scoring\n' >&2 - mux_status=0 -fi - -if [[ "${mux_status}" -ne 0 ]]; then - printf '[mux-run] ERROR: mux agent session failed (exit %s)\n' "${mux_status}" >&2 - exit "${mux_status}" -fi - -if [[ "${tee_status}" -ne 0 ]]; then - printf '[mux-run] ERROR: failed to capture mux stdout (exit %s)\n' "${tee_status}" >&2 - exit "${tee_status}" -fi - -if [[ "${stdin_status}" -ne 0 ]]; then - printf '[mux-run] ERROR: failed to send instruction to mux (exit %s)\n' "${stdin_status}" >&2 - exit "${stdin_status}" -fi +# Compatibility entrypoint for staged benchmark jobs created before the Xum rename. +exec bash "$(dirname "$0")/xum-run.sh" "$@" diff --git a/benchmarks/terminal_bench/mux_agent.py b/benchmarks/terminal_bench/mux_agent.py index a166711e33..f1af9e5dc4 100644 --- a/benchmarks/terminal_bench/mux_agent.py +++ b/benchmarks/terminal_bench/mux_agent.py @@ -1,461 +1,8 @@ -from __future__ import annotations +"""Compatibility import for benchmark configurations created before the Xum rename.""" -import asyncio -import json -import os -import shlex -import time -from collections.abc import Sequence -from dataclasses import dataclass -from pathlib import Path -from typing import Any +from .xum_agent import XumAgent -from harbor.agents.installed.base import BaseInstalledAgent -from harbor.environments.base import BaseEnvironment, ExecResult -from harbor.models.agent.context import AgentContext -from harbor.trial.trial import AgentTimeoutError +# Harbor configurations may still reference benchmarks.terminal_bench.mux_agent:MuxAgent. +MuxAgent = XumAgent -from .mux_run_contract import ( - MUX_RUN_TIMEOUT_FAILURE_MARKER, - RUN_COMPLETE_MARKER, - TIMEOUT_RETURN_CODE, -) -from .mux_payload import build_app_archive - - -@dataclass(frozen=True) -class _AgentCommand: - command: str - env: dict[str, str] - cwd: str | None = None - timeout_sec: float | None = None - - -class MuxAgent(BaseInstalledAgent): - """ - Minimal Terminal-Bench adapter that installs mux into the task container and - forwards the benchmark instruction to the mux headless runner. - """ - - _ARCHIVE_NAME = "mux-app.tar.gz" - _RUNNER_NAME = "mux-run.sh" - _SETUP_SCRIPT_NAME = "mux_setup.sh" - _COMMAND_STDOUT_NAME = "stdout.txt" - _COMMAND_STDERR_NAME = "stderr.txt" - _DEFAULT_MODEL = "anthropic:claude-sonnet-4-5" - _DEFAULT_PROJECT_CANDIDATES = "/workspace:/app:/workspaces:/root/project" - _INCLUDE_PATHS: Sequence[str] = ( - "package.json", - "bun.lock", - "bunfig.toml", - "tsconfig.json", - "tsconfig.main.json", - "src", - "dist", - "scripts/postinstall.sh", - ) - - _PROVIDER_ENV_KEYS: Sequence[str] = ( - "ANTHROPIC_API_KEY", - "ANTHROPIC_BASE_URL", - "OPENAI_API_KEY", - "OPENAI_BASE_URL", - "OPENAI_API_BASE", - "OPENAI_ORG_ID", - "AZURE_OPENAI_API_KEY", - "AZURE_OPENAI_ENDPOINT", - "AZURE_OPENAI_DEPLOYMENT", - "AZURE_OPENAI_API_VERSION", - # Google provider uses either GOOGLE_GENERATIVE_AI_API_KEY or the legacy - # GOOGLE_API_KEY env var. Forward both (and base URL override) into the - # sandbox to avoid confusing "api_key_not_found" failures. - "GOOGLE_GENERATIVE_AI_API_KEY", - "GOOGLE_API_KEY", - "GOOGLE_BASE_URL", - ) - - _CONFIG_ENV_KEYS: Sequence[str] = ( - "MUX_AGENT_GIT_URL", - "MUX_BUN_INSTALL_URL", - "MUX_PROJECT_PATH", - "MUX_PROJECT_CANDIDATES", - "MUX_MODEL", - "MUX_TIMEOUT_MS", - "MUX_CONFIG_ROOT", - "MUX_APP_ROOT", - "MUX_WORKSPACE_ID", - "MUX_EXPERIMENTS", - # Generic pass-through for arbitrary mux run CLI flags (e.g., --thinking - # high --use-1m --budget 5.00). Avoids per-flag plumbing. - "MUX_RUN_ARGS", - "MUX_RUN_AS_GOAL", - ) - - def __init__( - self, - logs_dir: Path, - model_name: str = "anthropic:claude-sonnet-4-5", - experiments: str | None = None, - timeout: float | int | str | None = None, - **kwargs: Any, - ) -> None: - super().__init__(logs_dir=logs_dir, **kwargs) - self._timeout_sec = self._parse_timeout_sec(timeout) - self._timeout_ms = ( - str(round(self._timeout_sec * 1000)) - if self._timeout_sec is not None - else None - ) - repo_root_env = os.environ.get("MUX_AGENT_REPO_ROOT") - repo_root = ( - Path(repo_root_env).resolve() - if repo_root_env - else Path(__file__).resolve().parents[2] - ) - if not repo_root.exists(): - raise RuntimeError(f"mux repo root {repo_root} does not exist") - - runner_path = Path(__file__).with_name(self._RUNNER_NAME) - if not runner_path.is_file(): - raise RuntimeError(f"mux runner script missing at {runner_path}") - - self._runner_path = runner_path - self._repo_root = repo_root - self._archive_bytes: bytes | None = None - self._model_name = (model_name or "").strip() - self._experiments = (experiments or "").strip() if experiments else None - self._last_environment: BaseEnvironment | None = None - - @staticmethod - def name() -> str: - return "mux" - - @property - def _env(self) -> dict[str, str]: - env: dict[str, str] = {} - - for key in (*self._PROVIDER_ENV_KEYS, *self._CONFIG_ENV_KEYS): - value = os.environ.get(key) - if value: - env[key] = value - - env.setdefault("MUX_MODEL", self._DEFAULT_MODEL) - env.setdefault("MUX_CONFIG_ROOT", "/root/.mux") - env.setdefault("MUX_APP_ROOT", "/opt/mux-app") - env.setdefault("MUX_WORKSPACE_ID", "mux-bench") - env.setdefault("MUX_PROJECT_CANDIDATES", self._DEFAULT_PROJECT_CANDIDATES) - if self._timeout_ms is not None: - env["MUX_TIMEOUT_MS"] = self._timeout_ms - - model_value = self._model_name or env["MUX_MODEL"] - model_value = model_value.strip() - if not model_value: - raise ValueError("MUX_MODEL must be a non-empty string") - if "/" in model_value and ":" not in model_value: - provider, model_name = model_value.split("/", 1) - model_value = f"{provider}:{model_name}" - - # Fail fast for Google models if credentials weren't forwarded into the - # sandbox env. Otherwise Harbor/mux will fail later with a less actionable - # "api_key_not_found" error. - if model_value.startswith("google:") and not ( - env.get("GOOGLE_GENERATIVE_AI_API_KEY") or env.get("GOOGLE_API_KEY") - ): - raise ValueError( - "Google models require GOOGLE_GENERATIVE_AI_API_KEY (preferred) or GOOGLE_API_KEY" - ) - env["MUX_MODEL"] = model_value - - # These env vars are all set with defaults above, no need to validate - for key in ( - "MUX_CONFIG_ROOT", - "MUX_APP_ROOT", - "MUX_WORKSPACE_ID", - "MUX_PROJECT_CANDIDATES", - ): - env[key] = env[key].strip() - - if timeout_value := env.get("MUX_TIMEOUT_MS"): - self._validate_timeout_ms(timeout_value) - - if project_path := env.get("MUX_PROJECT_PATH"): - if not project_path.strip(): - raise ValueError("MUX_PROJECT_PATH must be non-empty when provided") - - mux_run_as_goal = self._normalize_mux_run_as_goal(env.get("MUX_RUN_AS_GOAL")) - if mux_run_as_goal is None: - env.pop("MUX_RUN_AS_GOAL", None) - else: - env["MUX_RUN_AS_GOAL"] = mux_run_as_goal - - # Set experiments from kwarg (takes precedence over env var) - if self._experiments: - env["MUX_EXPERIMENTS"] = self._experiments - - return env - - @staticmethod - def _parse_timeout_sec(value: float | int | str | None) -> float | None: - if value is None: - return None - - timeout_sec = float(value) - if timeout_sec <= 0: - raise ValueError("timeout must be a positive number") - return timeout_sec - - @staticmethod - def _validate_timeout_ms(value: str) -> None: - if not value.strip().isdigit(): - raise ValueError("MUX_TIMEOUT_MS must be an integer") - - @staticmethod - def _normalize_mux_run_as_goal(value: str | None) -> str | None: - if value is None: - return None - - normalized = value.strip().lower() - if normalized in ("", "0", "false"): - return None - if normalized in ("1", "true"): - return "1" - - raise ValueError("MUX_RUN_AS_GOAL must be one of: 1, true, 0, false") - - @property - def _install_agent_template_path(self) -> Path: - return Path(__file__).with_name("mux_setup.sh.j2") - - _PROVIDERS_FILE_ENV_KEY = "MUX_PROVIDERS_FILE" - _TOKEN_FILE_PATH = "/tmp/mux-tokens.json" - - async def _stage_providers_config( - self, environment: BaseEnvironment, env: dict[str, str] - ) -> None: - """Upload host providers.jsonc into the sandbox when explicitly requested.""" - providers_file_raw = os.environ.get(self._PROVIDERS_FILE_ENV_KEY) - if not providers_file_raw: - return - - providers_path = Path(providers_file_raw).expanduser().resolve() - if not providers_path.is_file(): - raise RuntimeError( - f"{self._PROVIDERS_FILE_ENV_KEY}={providers_path} is not a readable file" - ) - - xum_config_root = ( - env.get("MUX_CONFIG_ROOT") or "/root/.mux" - ).strip() or "/root/.mux" - target_path = f"{xum_config_root.rstrip('/')}/providers.jsonc" - - await environment.upload_file( - source_path=providers_path, - target_path=target_path, - ) - - def _agent_version(self) -> str: - version_method = getattr(self, "version", None) - if callable(version_method): - return version_method() or "" - version_value = getattr(self, "_version", "") - return version_value if isinstance(version_value, str) else "" - - def _write_setup_script(self) -> Path: - setup_script = self._install_agent_template_path.read_text().replace( - "{{ version if version is not none else '' }}", - self._agent_version(), - ) - setup_path = self.logs_dir / self._SETUP_SCRIPT_NAME - setup_path.write_text(setup_script) - return setup_path - - async def install(self, environment: BaseEnvironment) -> None: - """Run the staged mux setup script inside the task environment.""" - # The setup script may install apt packages and writes under /opt, so run - # it as root even if Harbor's default agent user changes. - result = await environment.exec( - command=f"bash /installed-agent/{self._SETUP_SCRIPT_NAME}", - env=self._env, - user="root", - ) - if result.return_code != 0: - raise RuntimeError( - "mux setup failed " - f"(exit {result.return_code}):\nstdout: {result.stdout}\nstderr: {result.stderr}" - ) - - async def setup(self, environment: BaseEnvironment) -> None: - """Stage the mux payload before installing it in the task environment.""" - env = self._env - - # Harbor no longer renders installed-agent templates for custom agents. - # Stage the rendered script ourselves so scheduled tbench runs are not - # coupled to Harbor internals that have changed over time. - await environment.exec(command="mkdir -p /installed-agent", user="root") - - if not self._archive_bytes: - self._archive_bytes = build_app_archive( - self._repo_root, self._INCLUDE_PATHS - ) - - archive_path = self.logs_dir / self._ARCHIVE_NAME - archive_path.write_bytes(self._archive_bytes) - await environment.upload_file( - source_path=archive_path, - target_path=f"/installed-agent/{self._ARCHIVE_NAME}", - ) - - await environment.upload_file( - source_path=self._runner_path, - target_path=f"/installed-agent/{self._RUNNER_NAME}", - ) - - await environment.upload_file( - source_path=self._write_setup_script(), - target_path=f"/installed-agent/{self._SETUP_SCRIPT_NAME}", - ) - - await self.install(environment) - - # Optionally seed the sandbox with providers.jsonc from the host machine. - # This is required for OAuth-only configs where env var API keys are absent. - await self._stage_providers_config(environment, env) - - # Store environment reference for token extraction later. - self._last_environment = environment - - def create_run_agent_commands(self, instruction: str) -> list[_AgentCommand]: - escaped = shlex.quote(instruction) - command = f"bash /installed-agent/{self._RUNNER_NAME} {escaped}" - return [ - _AgentCommand(command=command, env=self._env, timeout_sec=self._timeout_sec) - ] - - async def _exec_agent_command( - self, - environment: BaseEnvironment, - exec_input: _AgentCommand, - ) -> ExecResult: - try: - return await environment.exec( - command=exec_input.command, - cwd=exec_input.cwd, - env=exec_input.env, - timeout_sec=exec_input.timeout_sec, - ) - except asyncio.TimeoutError as exc: - if exec_input.timeout_sec is None: - raise - raise self._agent_timeout_error(exec_input.timeout_sec) from exc - except RuntimeError as exc: - if exec_input.timeout_sec is not None and "timed out" in str(exc).lower(): - raise self._agent_timeout_error(exec_input.timeout_sec) from exc - raise - - @staticmethod - def _agent_timeout_error(timeout_sec: float) -> AgentTimeoutError: - return AgentTimeoutError( - f"Agent execution timed out after {timeout_sec:g} seconds" - ) - - @staticmethod - def _is_exec_timeout_return( - result: ExecResult, - timeout_sec: float | None, - elapsed_sec: float, - ) -> bool: - if timeout_sec is None or result.return_code != TIMEOUT_RETURN_CODE: - return False - - assert timeout_sec > 0, "timeout_sec is validated when MuxAgent is constructed" - timeout_threshold = max(timeout_sec * 0.95, timeout_sec - 10) - if elapsed_sec < timeout_threshold: - return False - - stdout = result.stdout or "" - stderr = result.stderr or "" - if RUN_COMPLETE_MARKER in stdout: - return False - if MUX_RUN_TIMEOUT_FAILURE_MARKER in stderr: - return False - - return True - - async def run( - self, - instruction: str, - environment: BaseEnvironment, - context: AgentContext, - ) -> None: - """Run agent commands, download token file, then populate context.""" - # Execute commands (from base class logic, but without calling populate_context) - failed_command: tuple[int, int] | None = None - timeout_error: AgentTimeoutError | None = None - for i, exec_input in enumerate(self.create_run_agent_commands(instruction)): - command_dir = self.logs_dir / f"command-{i}" - command_dir.mkdir(parents=True, exist_ok=True) - (command_dir / "command.txt").write_text(exec_input.command) - - # /logs is bind-mounted; pre-create files so sandbox tee output - # does not leave root-owned files that host-side log writes cannot replace. - stdout_path = command_dir / self._COMMAND_STDOUT_NAME - stderr_path = command_dir / self._COMMAND_STDERR_NAME - for output_path in (stdout_path, stderr_path): - output_path.write_text("") - - started_at = time.monotonic() - try: - result = await self._exec_agent_command(environment, exec_input) - except AgentTimeoutError as exc: - timeout_error = exc - break - elapsed_sec = time.monotonic() - started_at - - (command_dir / "return-code.txt").write_text(str(result.return_code)) - if result.stdout: - stdout_path.write_text(result.stdout) - if result.stderr: - stderr_path.write_text(result.stderr) - if self._is_exec_timeout_return( - result, exec_input.timeout_sec, elapsed_sec - ): - assert exec_input.timeout_sec is not None - timeout_error = self._agent_timeout_error(exec_input.timeout_sec) - break - if result.return_code != 0: - failed_command = (i, result.return_code) - break - - # Download token file from container BEFORE populating context - # Clear any stale token file first to avoid reading outdated data if download fails - token_file = self.logs_dir / "mux-tokens.json" - token_file.unlink(missing_ok=True) - try: - await environment.download_file(self._TOKEN_FILE_PATH, token_file) - except Exception: - pass # Token file may not exist if agent crashed early - - self.populate_context_post_run(context) - - if timeout_error is not None: - raise timeout_error - - if failed_command is not None: - command_index, return_code = failed_command - raise RuntimeError( - f"mux agent command failed (command {command_index}, exit {return_code})" - ) - - def populate_context_post_run(self, context: AgentContext) -> None: - """Extract token usage and cost from the token file written by mux-run.sh.""" - token_file = self.logs_dir / "mux-tokens.json" - if token_file.exists(): - try: - data = json.loads(token_file.read_text()) - context.n_input_tokens = data.get("input", 0) - context.n_output_tokens = data.get("output", 0) - # cost_usd is computed by mux CLI from model pricing - if data.get("cost_usd") is not None: - context.cost_usd = data["cost_usd"] - except Exception: - pass # Token/cost extraction is best-effort +__all__ = ["MuxAgent"] diff --git a/benchmarks/terminal_bench/mux_run_contract.py b/benchmarks/terminal_bench/mux_run_contract.py deleted file mode 100644 index de9f1c4acb..0000000000 --- a/benchmarks/terminal_bench/mux_run_contract.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations - -TIMEOUT_RETURN_CODE = 124 -OOM_LIKE_RETURN_CODE = 137 -RUN_COMPLETE_MARKER = "run-complete" -MUX_RUN_FAILURE_MARKER = "[mux-run] ERROR: mux agent session failed" - - -def mux_run_failure_marker(return_code: int) -> str: - return f"{MUX_RUN_FAILURE_MARKER} (exit {return_code})" - - -MUX_RUN_TIMEOUT_FAILURE_MARKER = mux_run_failure_marker(TIMEOUT_RETURN_CODE) diff --git a/benchmarks/terminal_bench/mux_setup.sh.j2 b/benchmarks/terminal_bench/mux_setup.sh.j2 deleted file mode 100644 index 9beb3331b3..0000000000 --- a/benchmarks/terminal_bench/mux_setup.sh.j2 +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -log() { - printf '[mux-setup] %s\n' "$1" -} - -ensure_tool() { - if command -v "$1" >/dev/null 2>&1; then - return 0 - fi - - if ! command -v apt-get >/dev/null 2>&1; then - printf 'Required tool "%s" missing and apt-get unavailable\n' "$1" >&2 - return 1 - fi - - log "installing missing dependency: $1" - export DEBIAN_FRONTEND=noninteractive - apt-get update - apt-get install -y "$1" -} - -ensure_tool curl -ensure_tool git -ensure_tool unzip -ensure_tool python3 -export BUN_INSTALL="${BUN_INSTALL:-/root/.bun}" -export PATH="${BUN_INSTALL}/bin:${PATH}" - -if ! command -v bun >/dev/null 2>&1; then - log "installing bun" - curl -fsSL "${MUX_BUN_INSTALL_URL:-https://bun.sh/install}" | bash -fi - -MUX_APP_ROOT="${MUX_APP_ROOT:-/opt/mux-app}" -MUX_CONFIG_ROOT="${MUX_CONFIG_ROOT:-/root/.mux}" -MUX_AGENT_VERSION="{{ version if version is not none else '' }}" - -rm -rf "${MUX_APP_ROOT}" -if [[ -n "${MUX_AGENT_VERSION}" ]]; then - : "${MUX_AGENT_GIT_URL:?MUX_AGENT_GIT_URL required when version is set}" - log "cloning mux from ${MUX_AGENT_GIT_URL} @ ${MUX_AGENT_VERSION}" - git clone --depth 1 --branch "${MUX_AGENT_VERSION}" "${MUX_AGENT_GIT_URL}" "${MUX_APP_ROOT}" -else - log "extracting mux archive" - mkdir -p "${MUX_APP_ROOT}" - tar -xzf "/installed-agent/mux-app.tar.gz" -C "${MUX_APP_ROOT}" -fi - -cd "${MUX_APP_ROOT}" - -# Use --production to skip devDependencies (electron-builder, storybook, etc.) -# which cuts install size from 1.9GB → 728MB and avoids OOM in memory-constrained -# Daytona sandboxes (2GB). The headless CLI only needs production deps. -if [[ -d "node_modules" ]]; then - log "node_modules already present, skipping bun install" -else - log "installing mux production dependencies via bun" - MUX_HEADLESS=1 bun install --production --frozen-lockfile -fi - -mkdir -p "${MUX_CONFIG_ROOT}" - -chmod +x /installed-agent/mux-run.sh - -log "setup complete" diff --git a/benchmarks/terminal_bench/prepare_leaderboard_submission.py b/benchmarks/terminal_bench/prepare_leaderboard_submission.py index 4b94e293b8..4faa486654 100755 --- a/benchmarks/terminal_bench/prepare_leaderboard_submission.py +++ b/benchmarks/terminal_bench/prepare_leaderboard_submission.py @@ -28,10 +28,10 @@ # Then submit with hf CLI: hf upload alexgshaw/terminal-bench-2-leaderboard \\ ./leaderboard_submission/submissions submissions \\ - --repo-type dataset --create-pr --commit-message "Mux submission" + --repo-type dataset --create-pr --commit-message "Xum submission" Output structure (per leaderboard requirements): - submissions/terminal-bench/2.0/Mux__/ + submissions/terminal-bench/2.0/Xum__/ metadata.yaml / # From run 1 (e.g., 2026-02-01__00-15-05) config.json @@ -71,15 +71,15 @@ LEADERBOARD_REPO = "alexgshaw/terminal-bench-2-leaderboard" -# Agent metadata for Mux -MUX_METADATA = { - "agent_url": "https://github.com/coder/mux", - "agent_display_name": "Mux", +# Agent metadata for Xum +XUM_METADATA = { + "agent_url": "https://github.com/coder/xum", + "agent_display_name": "Xum", "agent_org_display_name": "Coder", } # Model metadata lookup -# folder_name: Used in submission folder path (e.g., Mux__Claude-Opus-4.5) +# folder_name: Used in submission folder path (e.g., Xum__Claude-Opus-4.5) MODEL_METADATA = { "anthropic/claude-sonnet-4-5": { "model_name": "claude-sonnet-4-5", @@ -193,9 +193,9 @@ def create_metadata_yaml(model: str) -> str: } lines = [ - f'agent_url: "{MUX_METADATA["agent_url"]}"', - f'agent_display_name: "{MUX_METADATA["agent_display_name"]}"', - f'agent_org_display_name: "{MUX_METADATA["agent_org_display_name"]}"', + f'agent_url: "{XUM_METADATA["agent_url"]}"', + f'agent_display_name: "{XUM_METADATA["agent_display_name"]}"', + f'agent_org_display_name: "{XUM_METADATA["agent_org_display_name"]}"', "", "models:", f' - model_name: "{model_info["model_name"]}"', @@ -334,10 +334,10 @@ def prepare_submission( # Create submissions for each model for model, trials in model_trials.items(): - # Create submission directory: Mux__ + # Create submission directory: Xum__ model_info = MODEL_METADATA.get(model, {}) model_folder_name = model_info.get("folder_name", model.split("/")[-1].title()) - submission_name = f"Mux__{model_folder_name}" + submission_name = f"Xum__{model_folder_name}" submission_dir = ( output_dir / "submissions" / "terminal-bench" / "2.0" / submission_name @@ -379,8 +379,8 @@ def prepare_submission( trial_src, dest_trial_dir, ignore=shutil.ignore_patterns( - "mux-app.tar.gz", # Large agent binary (~5MB each) - "mux-tokens.json", # Token usage (not needed for leaderboard) + "xum-app.tar.gz", # Large agent binary (~5MB each) + "xum-tokens.json", # Token usage (not needed for leaderboard) "*.log", # Log files trigger HF LFS and cause upload timeouts ), ) @@ -562,7 +562,7 @@ def main(): print(f" hf upload {LEADERBOARD_REPO} \\") print(f" {args.output_dir}/submissions submissions \\") print(" --repo-type dataset --create-pr \\") - print(f' --commit-message "Mux submission ({run_date})"') + print(f' --commit-message "Xum submission ({run_date})"') # Clean up temp directories if we created any if temp_dirs: diff --git a/benchmarks/terminal_bench/tbench_utils.py b/benchmarks/terminal_bench/tbench_utils.py index 94ed2c9a02..1081d91e09 100644 --- a/benchmarks/terminal_bench/tbench_utils.py +++ b/benchmarks/terminal_bench/tbench_utils.py @@ -15,7 +15,7 @@ from pathlib import Path # GitHub repository for fetching artifacts -GITHUB_REPO = "coder/mux" +GITHUB_REPO = "coder/xum" # Smoke test model - excluded from submissions by default SMOKE_TEST_MODEL = "anthropic/claude-sonnet-4-5" diff --git a/benchmarks/terminal_bench/xum-run.sh b/benchmarks/terminal_bench/xum-run.sh new file mode 100644 index 0000000000..2d608406f6 --- /dev/null +++ b/benchmarks/terminal_bench/xum-run.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash + +set -euo pipefail + +log() { + printf '[xum-run] %s\n' "$1" +} + +fatal() { + printf '[xum-run] ERROR: %s\n' "$1" >&2 + exit 1 +} + +instruction=${1:-} +if [[ -z "${instruction}" ]]; then + fatal "instruction argument is required" +fi + +export BUN_INSTALL="${BUN_INSTALL:-/root/.bun}" +export PATH="${BUN_INSTALL}/bin:${PATH}" + +# External benchmark jobs may still provide MUX_* variables. Canonical XUM_* +# values always win; legacy names are copied only when their replacement is unset. +for suffix in APP_ROOT CONFIG_ROOT ROOT PROJECT_PATH PROJECT_CANDIDATES MODEL TIMEOUT_MS WORKSPACE_ID EXPERIMENTS RUN_AS_GOAL RUN_ARGS LOG_DIR OUTPUT_FILE STDERR_FILE TOKEN_FILE; do + canonical_name="XUM_${suffix}" + legacy_name="MUX_${suffix}" + if [[ ! -v "${canonical_name}" && -v "${legacy_name}" ]]; then + printf -v "${canonical_name}" '%s' "${!legacy_name}" + export "${canonical_name}" + fi +done + +XUM_APP_ROOT="${XUM_APP_ROOT:-/opt/xum-app}" + +# Prefer an explicit XUM_CONFIG_ROOT, but fall back to XUM_ROOT for callers that +# only override the Xum home via XUM_ROOT. +XUM_CONFIG_ROOT="${XUM_CONFIG_ROOT:-${XUM_ROOT:-/root/.xum}}" + +# Export XUM_ROOT so Xum's getXumHome() finds providers.jsonc and other config. +# Don't clobber caller-provided XUM_ROOT (e.g. local runs/tests with a custom root). +export XUM_ROOT="${XUM_ROOT:-${XUM_CONFIG_ROOT}}" +XUM_PROJECT_PATH="${XUM_PROJECT_PATH:-}" +XUM_PROJECT_CANDIDATES="${XUM_PROJECT_CANDIDATES:-/workspace:/app:/workspaces:/root/project}" +XUM_MODEL="${XUM_MODEL:-anthropic:claude-sonnet-4-5}" +XUM_TIMEOUT_MS="${XUM_TIMEOUT_MS:-}" +XUM_WORKSPACE_ID="${XUM_WORKSPACE_ID:-xum-bench}" +XUM_EXPERIMENTS="${XUM_EXPERIMENTS:-}" +XUM_RUN_AS_GOAL="${XUM_RUN_AS_GOAL:-}" + +xum_run_as_goal_normalized="${XUM_RUN_AS_GOAL,,}" +xum_run_as_goal_normalized="${xum_run_as_goal_normalized#"${xum_run_as_goal_normalized%%[![:space:]]*}"}" +xum_run_as_goal_normalized="${xum_run_as_goal_normalized%"${xum_run_as_goal_normalized##*[![:space:]]}"}" +case "${xum_run_as_goal_normalized}" in + "" | "0" | "false") xum_run_as_goal_enabled=0 ;; + "1" | "true") xum_run_as_goal_enabled=1 ;; + *) fatal "XUM_RUN_AS_GOAL must be one of: 1, true, 0, false" ;; +esac + +resolve_project_path() { + if [[ -n "${XUM_PROJECT_PATH}" ]]; then + if [[ -d "${XUM_PROJECT_PATH}" ]]; then + printf '%s\n' "${XUM_PROJECT_PATH}" + return 0 + fi + fatal "XUM_PROJECT_PATH=${XUM_PROJECT_PATH} not found" + fi + + IFS=":" read -r -a candidates <<<"${XUM_PROJECT_CANDIDATES}" + for candidate in "${candidates[@]}"; do + if [[ -d "${candidate}" ]]; then + printf '%s\n' "${candidate}" + return 0 + fi + done + + fatal "no project path located (searched ${XUM_PROJECT_CANDIDATES})" +} + +command -v bun >/dev/null 2>&1 || fatal "bun is not installed" +project_path=$(resolve_project_path) + +log "starting xum agent session for ${project_path}" +cd "${XUM_APP_ROOT}" + +cmd=(bun src/cli/run.ts + --dir "${project_path}" + --model "${XUM_MODEL}" + --keep-background-processes + --json) + +# Add experiment flags (comma-separated → repeated --experiment flags) +if [[ -n "${XUM_EXPERIMENTS}" ]]; then + IFS=',' read -r -a experiments <<<"${XUM_EXPERIMENTS}" + for exp in "${experiments[@]}"; do + # Trim whitespace + exp="${exp#"${exp%%[![:space:]]*}"}" + exp="${exp%"${exp##*[![:space:]]}"}" + if [[ -n "${exp}" ]]; then + cmd+=(--experiment "${exp}") + fi + done +fi + +if [[ "${xum_run_as_goal_enabled}" == "1" ]]; then + log "strict xum goal mode enabled" + cmd+=(--goal "${instruction}") +fi + +xum_run_args=() +# Append arbitrary Xum run flags (e.g., --thinking high --mode exec --use-1m --budget 5.00) +if [[ -n "${XUM_RUN_ARGS:-}" ]]; then + # Word-split intentional: XUM_RUN_ARGS contains space-separated CLI flags. + # shellcheck disable=SC2206 + xum_run_args=(${XUM_RUN_ARGS}) + if [[ "${xum_run_as_goal_enabled}" == "1" ]]; then + for arg in "${xum_run_args[@]}"; do + if [[ "${arg}" == "--goal" || "${arg}" == --goal=* ]]; then + fatal "XUM_RUN_ARGS must not include --goal when XUM_RUN_AS_GOAL is enabled" + fi + done + fi + cmd+=("${xum_run_args[@]}") +fi + +# NOTE: Harbor only automatically collects /logs/agent on timeouts. +# Persist stdout/stderr there so partial agent output survives cancellation. +XUM_LOG_DIR="${XUM_LOG_DIR:-/logs/agent/command-0}" +mkdir -p "${XUM_LOG_DIR}" +XUM_OUTPUT_FILE="${XUM_LOG_DIR}/stdout.txt" +XUM_STDERR_FILE="${XUM_LOG_DIR}/stderr.txt" +XUM_TOKEN_FILE="${XUM_TOKEN_FILE:-/tmp/xum-tokens.json}" + +# Let Harbor classify task timeouts; GNU timeout would surface as exit 124. +if [[ -n "${XUM_TIMEOUT_MS}" ]]; then + if [[ ! "${XUM_TIMEOUT_MS}" =~ ^[0-9]+$ ]]; then + fatal "XUM_TIMEOUT_MS must be an integer" + fi + log "XUM_TIMEOUT_MS=${XUM_TIMEOUT_MS} forwarded; Harbor remains timeout authority" +fi + +# Capture output to file while streaming to terminal for token extraction. +# Keep stderr separate so the stdout log stays valid JSONL. +set +e +printf '%s' "${instruction}" \ + | "${cmd[@]}" \ + 2> >(tee "${XUM_STDERR_FILE}" >&2) \ + | tee "${XUM_OUTPUT_FILE}" +pipeline_status=("${PIPESTATUS[@]}") +set -e +stdin_status="${pipeline_status[0]}" +xum_status="${pipeline_status[1]}" +tee_status="${pipeline_status[2]}" + +# Extract usage and cost from the JSONL output. +# Prefer the run-complete event (emitted at end of --json run) which has aggregated +# totals. Fall back to summing usage-delta + session-usage-delta events when +# run-complete is missing (e.g. process killed by timeout, stdout not flushed). +python3 -c ' +import json, sys +result = {"input": 0, "output": 0, "cost_usd": None} +# Track cumulative usage from usage-delta events (keyed by messageId). +# Each usage-delta contains cumulative totals for its message, so we keep the +# latest per message and sum across messages at the end. +cumulative_by_msg = {} +# Track sub-agent usage from session-usage-delta events. These carry per-model +# byModelDelta dicts with {input: {tokens, cost_usd}, output: {tokens, cost_usd}, ...}. +# Each event is an incremental delta, so we sum them all. +subagent_input = 0 +subagent_output = 0 +for line in open(sys.argv[1]): + try: + obj = json.loads(line) + if obj.get("type") == "run-complete": + usage = obj.get("usage") or {} + result["input"] = usage.get("inputTokens", 0) or 0 + result["output"] = usage.get("outputTokens", 0) or 0 + result["cost_usd"] = obj.get("cost_usd") + print(json.dumps(result)) + sys.exit(0) + # Nested event wrapper: {"type":"event","payload":{"type":"usage-delta",...}} + payload = obj.get("payload") or obj + if payload.get("type") == "usage-delta": + msg_id = payload.get("messageId", "") + # Prefer cumulativeUsage (running total across all steps in a message) + # over usage (per-step delta). Keeping the latest cumulative per message + # gives the correct total when summed across messages. + usage = payload.get("cumulativeUsage") or payload.get("usage") or {} + cumulative_by_msg[msg_id] = usage + elif payload.get("type") == "session-usage-delta": + for model_usage in (payload.get("byModelDelta") or {}).values(): + subagent_input += (model_usage.get("input") or {}).get("tokens", 0) + subagent_output += (model_usage.get("output") or {}).get("tokens", 0) + except Exception: + pass +# No run-complete found — aggregate the last usage-delta per message + sub-agent totals +for usage in cumulative_by_msg.values(): + result["input"] += (usage.get("inputTokens", 0) or 0) + result["output"] += (usage.get("outputTokens", 0) or 0) +result["input"] += subagent_input +result["output"] += subagent_output +print(json.dumps(result)) +' "${XUM_OUTPUT_FILE}" >"${XUM_TOKEN_FILE}" 2>/dev/null || true + +if [[ "${xum_status}" -eq 3 && "${xum_run_as_goal_enabled}" == "1" ]]; then + printf '[xum-run] WARNING: xum goal run stopped incomplete (exit 3); leaving workspace for verifier scoring\n' >&2 + xum_status=0 +fi + +if [[ "${xum_status}" -ne 0 ]]; then + printf '[xum-run] ERROR: xum agent session failed (exit %s)\n' "${xum_status}" >&2 + exit "${xum_status}" +fi + +if [[ "${tee_status}" -ne 0 ]]; then + printf '[xum-run] ERROR: failed to capture xum stdout (exit %s)\n' "${tee_status}" >&2 + exit "${tee_status}" +fi + +if [[ "${stdin_status}" -ne 0 ]]; then + printf '[xum-run] ERROR: failed to send instruction to Xum (exit %s)\n' "${stdin_status}" >&2 + exit "${stdin_status}" +fi diff --git a/benchmarks/terminal_bench/xum_agent.py b/benchmarks/terminal_bench/xum_agent.py new file mode 100644 index 0000000000..356e1ff19b --- /dev/null +++ b/benchmarks/terminal_bench/xum_agent.py @@ -0,0 +1,481 @@ +from __future__ import annotations + +import asyncio +import json +import os +import shlex +import time +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from harbor.agents.installed.base import BaseInstalledAgent +from harbor.environments.base import BaseEnvironment, ExecResult +from harbor.models.agent.context import AgentContext +from harbor.trial.trial import AgentTimeoutError + +from .xum_run_contract import ( + XUM_RUN_TIMEOUT_FAILURE_MARKER, + RUN_COMPLETE_MARKER, + TIMEOUT_RETURN_CODE, +) +from .xum_payload import build_app_archive + + +@dataclass(frozen=True) +class _AgentCommand: + command: str + env: dict[str, str] + cwd: str | None = None + timeout_sec: float | None = None + + +class XumAgent(BaseInstalledAgent): + """ + Minimal Terminal-Bench adapter that installs Xum into the task container and + forwards the benchmark instruction to the Xum headless runner. + """ + + _ARCHIVE_NAME = "xum-app.tar.gz" + _RUNNER_NAME = "xum-run.sh" + _SETUP_SCRIPT_NAME = "xum_setup.sh" + _COMMAND_STDOUT_NAME = "stdout.txt" + _COMMAND_STDERR_NAME = "stderr.txt" + _DEFAULT_MODEL = "anthropic:claude-sonnet-4-5" + _DEFAULT_PROJECT_CANDIDATES = "/workspace:/app:/workspaces:/root/project" + _INCLUDE_PATHS: Sequence[str] = ( + "package.json", + "bun.lock", + "bunfig.toml", + "tsconfig.json", + "tsconfig.main.json", + "src", + "dist", + "scripts/postinstall.sh", + ) + + _PROVIDER_ENV_KEYS: Sequence[str] = ( + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + "OPENAI_API_KEY", + "OPENAI_BASE_URL", + "OPENAI_API_BASE", + "OPENAI_ORG_ID", + "AZURE_OPENAI_API_KEY", + "AZURE_OPENAI_ENDPOINT", + "AZURE_OPENAI_DEPLOYMENT", + "AZURE_OPENAI_API_VERSION", + # Google provider uses either GOOGLE_GENERATIVE_AI_API_KEY or the legacy + # GOOGLE_API_KEY env var. Forward both (and base URL override) into the + # sandbox to avoid confusing "api_key_not_found" failures. + "GOOGLE_GENERATIVE_AI_API_KEY", + "GOOGLE_API_KEY", + "GOOGLE_BASE_URL", + ) + + _CONFIG_ENV_SUFFIXES: Sequence[str] = ( + "AGENT_GIT_URL", + "BUN_INSTALL_URL", + "PROJECT_PATH", + "PROJECT_CANDIDATES", + "MODEL", + "TIMEOUT_MS", + "CONFIG_ROOT", + "APP_ROOT", + "WORKSPACE_ID", + "EXPERIMENTS", + # Generic pass-through for arbitrary Xum run CLI flags (e.g., --thinking + # high --use-1m --budget 5.00). Avoids per-flag plumbing. + "RUN_ARGS", + "RUN_AS_GOAL", + ) + _CONFIG_ENV_KEYS: Sequence[str] = tuple( + f"XUM_{suffix}" for suffix in _CONFIG_ENV_SUFFIXES + ) + # Terminal-Bench jobs may still inject the pre-rename names. Read them only + # as fallback and emit canonical XUM_* names into the task environment. + _LEGACY_CONFIG_ENV_KEYS: Sequence[str] = tuple( + f"MUX_{suffix}" for suffix in _CONFIG_ENV_SUFFIXES + ) + + def __init__( + self, + logs_dir: Path, + model_name: str = "anthropic:claude-sonnet-4-5", + experiments: str | None = None, + timeout: float | int | str | None = None, + **kwargs: Any, + ) -> None: + super().__init__(logs_dir=logs_dir, **kwargs) + self._timeout_sec = self._parse_timeout_sec(timeout) + self._timeout_ms = ( + str(round(self._timeout_sec * 1000)) + if self._timeout_sec is not None + else None + ) + repo_root_env = os.environ.get("XUM_AGENT_REPO_ROOT") or os.environ.get( + "MUX_AGENT_REPO_ROOT" + ) + repo_root = ( + Path(repo_root_env).resolve() + if repo_root_env + else Path(__file__).resolve().parents[2] + ) + if not repo_root.exists(): + raise RuntimeError(f"xum repo root {repo_root} does not exist") + + runner_path = Path(__file__).with_name(self._RUNNER_NAME) + if not runner_path.is_file(): + raise RuntimeError(f"xum runner script missing at {runner_path}") + + self._runner_path = runner_path + self._repo_root = repo_root + self._archive_bytes: bytes | None = None + self._model_name = (model_name or "").strip() + self._experiments = (experiments or "").strip() if experiments else None + self._last_environment: BaseEnvironment | None = None + + @staticmethod + def name() -> str: + return "xum" + + @property + def _env(self) -> dict[str, str]: + env: dict[str, str] = {} + + for key in self._PROVIDER_ENV_KEYS: + value = os.environ.get(key) + if value: + env[key] = value + + for suffix in self._CONFIG_ENV_SUFFIXES: + canonical_key = f"XUM_{suffix}" + legacy_key = f"MUX_{suffix}" + value = os.environ.get(canonical_key) or os.environ.get(legacy_key) + if value: + env[canonical_key] = value + + env.setdefault("XUM_MODEL", self._DEFAULT_MODEL) + env.setdefault("XUM_CONFIG_ROOT", "/root/.xum") + env.setdefault("XUM_APP_ROOT", "/opt/xum-app") + env.setdefault("XUM_WORKSPACE_ID", "xum-bench") + env.setdefault("XUM_PROJECT_CANDIDATES", self._DEFAULT_PROJECT_CANDIDATES) + if self._timeout_ms is not None: + env["XUM_TIMEOUT_MS"] = self._timeout_ms + + model_value = self._model_name or env["XUM_MODEL"] + model_value = model_value.strip() + if not model_value: + raise ValueError("XUM_MODEL must be a non-empty string") + if "/" in model_value and ":" not in model_value: + provider, model_name = model_value.split("/", 1) + model_value = f"{provider}:{model_name}" + + # Fail fast for Google models if credentials weren't forwarded into the + # sandbox env. Otherwise Harbor/Xum will fail later with a less actionable + # "api_key_not_found" error. + if model_value.startswith("google:") and not ( + env.get("GOOGLE_GENERATIVE_AI_API_KEY") or env.get("GOOGLE_API_KEY") + ): + raise ValueError( + "Google models require GOOGLE_GENERATIVE_AI_API_KEY (preferred) or GOOGLE_API_KEY" + ) + env["XUM_MODEL"] = model_value + + # These env vars are all set with defaults above, no need to validate + for key in ( + "XUM_CONFIG_ROOT", + "XUM_APP_ROOT", + "XUM_WORKSPACE_ID", + "XUM_PROJECT_CANDIDATES", + ): + env[key] = env[key].strip() + + if timeout_value := env.get("XUM_TIMEOUT_MS"): + self._validate_timeout_ms(timeout_value) + + if project_path := env.get("XUM_PROJECT_PATH"): + if not project_path.strip(): + raise ValueError("XUM_PROJECT_PATH must be non-empty when provided") + + xum_run_as_goal = self._normalize_xum_run_as_goal(env.get("XUM_RUN_AS_GOAL")) + if xum_run_as_goal is None: + env.pop("XUM_RUN_AS_GOAL", None) + else: + env["XUM_RUN_AS_GOAL"] = xum_run_as_goal + + # Set experiments from kwarg (takes precedence over env var) + if self._experiments: + env["XUM_EXPERIMENTS"] = self._experiments + + return env + + @staticmethod + def _parse_timeout_sec(value: float | int | str | None) -> float | None: + if value is None: + return None + + timeout_sec = float(value) + if timeout_sec <= 0: + raise ValueError("timeout must be a positive number") + return timeout_sec + + @staticmethod + def _validate_timeout_ms(value: str) -> None: + if not value.strip().isdigit(): + raise ValueError("XUM_TIMEOUT_MS must be an integer") + + @staticmethod + def _normalize_xum_run_as_goal(value: str | None) -> str | None: + if value is None: + return None + + normalized = value.strip().lower() + if normalized in ("", "0", "false"): + return None + if normalized in ("1", "true"): + return "1" + + raise ValueError("XUM_RUN_AS_GOAL must be one of: 1, true, 0, false") + + @property + def _install_agent_template_path(self) -> Path: + return Path(__file__).with_name("xum_setup.sh.j2") + + _PROVIDERS_FILE_ENV_KEY = "XUM_PROVIDERS_FILE" + _LEGACY_PROVIDERS_FILE_ENV_KEY = "MUX_PROVIDERS_FILE" + _TOKEN_FILE_PATH = "/tmp/xum-tokens.json" + + async def _stage_providers_config( + self, environment: BaseEnvironment, env: dict[str, str] + ) -> None: + """Upload host providers.jsonc into the sandbox when explicitly requested.""" + providers_file_raw = os.environ.get( + self._PROVIDERS_FILE_ENV_KEY + ) or os.environ.get(self._LEGACY_PROVIDERS_FILE_ENV_KEY) + if not providers_file_raw: + return + + providers_path = Path(providers_file_raw).expanduser().resolve() + if not providers_path.is_file(): + raise RuntimeError( + f"{self._PROVIDERS_FILE_ENV_KEY}={providers_path} is not a readable file" + ) + + xum_config_root = ( + env.get("XUM_CONFIG_ROOT") or "/root/.xum" + ).strip() or "/root/.xum" + target_path = f"{xum_config_root.rstrip('/')}/providers.jsonc" + + await environment.upload_file( + source_path=providers_path, + target_path=target_path, + ) + + def _agent_version(self) -> str: + version_method = getattr(self, "version", None) + if callable(version_method): + return version_method() or "" + version_value = getattr(self, "_version", "") + return version_value if isinstance(version_value, str) else "" + + def _write_setup_script(self) -> Path: + setup_script = self._install_agent_template_path.read_text().replace( + "{{ version if version is not none else '' }}", + self._agent_version(), + ) + setup_path = self.logs_dir / self._SETUP_SCRIPT_NAME + setup_path.write_text(setup_script) + return setup_path + + async def install(self, environment: BaseEnvironment) -> None: + """Run the staged xum setup script inside the task environment.""" + # The setup script may install apt packages and writes under /opt, so run + # it as root even if Harbor's default agent user changes. + result = await environment.exec( + command=f"bash /installed-agent/{self._SETUP_SCRIPT_NAME}", + env=self._env, + user="root", + ) + if result.return_code != 0: + raise RuntimeError( + "xum setup failed " + f"(exit {result.return_code}):\nstdout: {result.stdout}\nstderr: {result.stderr}" + ) + + async def setup(self, environment: BaseEnvironment) -> None: + """Stage the Xum payload before installing it in the task environment.""" + env = self._env + + # Harbor no longer renders installed-agent templates for custom agents. + # Stage the rendered script ourselves so scheduled tbench runs are not + # coupled to Harbor internals that have changed over time. + await environment.exec(command="mkdir -p /installed-agent", user="root") + + if not self._archive_bytes: + self._archive_bytes = build_app_archive( + self._repo_root, self._INCLUDE_PATHS + ) + + archive_path = self.logs_dir / self._ARCHIVE_NAME + archive_path.write_bytes(self._archive_bytes) + await environment.upload_file( + source_path=archive_path, + target_path=f"/installed-agent/{self._ARCHIVE_NAME}", + ) + + await environment.upload_file( + source_path=self._runner_path, + target_path=f"/installed-agent/{self._RUNNER_NAME}", + ) + + await environment.upload_file( + source_path=self._write_setup_script(), + target_path=f"/installed-agent/{self._SETUP_SCRIPT_NAME}", + ) + + await self.install(environment) + + # Optionally seed the sandbox with providers.jsonc from the host machine. + # This is required for OAuth-only configs where env var API keys are absent. + await self._stage_providers_config(environment, env) + + # Store environment reference for token extraction later. + self._last_environment = environment + + def create_run_agent_commands(self, instruction: str) -> list[_AgentCommand]: + escaped = shlex.quote(instruction) + command = f"bash /installed-agent/{self._RUNNER_NAME} {escaped}" + return [ + _AgentCommand(command=command, env=self._env, timeout_sec=self._timeout_sec) + ] + + async def _exec_agent_command( + self, + environment: BaseEnvironment, + exec_input: _AgentCommand, + ) -> ExecResult: + try: + return await environment.exec( + command=exec_input.command, + cwd=exec_input.cwd, + env=exec_input.env, + timeout_sec=exec_input.timeout_sec, + ) + except asyncio.TimeoutError as exc: + if exec_input.timeout_sec is None: + raise + raise self._agent_timeout_error(exec_input.timeout_sec) from exc + except RuntimeError as exc: + if exec_input.timeout_sec is not None and "timed out" in str(exc).lower(): + raise self._agent_timeout_error(exec_input.timeout_sec) from exc + raise + + @staticmethod + def _agent_timeout_error(timeout_sec: float) -> AgentTimeoutError: + return AgentTimeoutError( + f"Agent execution timed out after {timeout_sec:g} seconds" + ) + + @staticmethod + def _is_exec_timeout_return( + result: ExecResult, + timeout_sec: float | None, + elapsed_sec: float, + ) -> bool: + if timeout_sec is None or result.return_code != TIMEOUT_RETURN_CODE: + return False + + assert timeout_sec > 0, "timeout_sec is validated when XumAgent is constructed" + timeout_threshold = max(timeout_sec * 0.95, timeout_sec - 10) + if elapsed_sec < timeout_threshold: + return False + + stdout = result.stdout or "" + stderr = result.stderr or "" + if RUN_COMPLETE_MARKER in stdout: + return False + if XUM_RUN_TIMEOUT_FAILURE_MARKER in stderr: + return False + + return True + + async def run( + self, + instruction: str, + environment: BaseEnvironment, + context: AgentContext, + ) -> None: + """Run agent commands, download token file, then populate context.""" + # Execute commands (from base class logic, but without calling populate_context) + failed_command: tuple[int, int] | None = None + timeout_error: AgentTimeoutError | None = None + for i, exec_input in enumerate(self.create_run_agent_commands(instruction)): + command_dir = self.logs_dir / f"command-{i}" + command_dir.mkdir(parents=True, exist_ok=True) + (command_dir / "command.txt").write_text(exec_input.command) + + # /logs is bind-mounted; pre-create files so sandbox tee output + # does not leave root-owned files that host-side log writes cannot replace. + stdout_path = command_dir / self._COMMAND_STDOUT_NAME + stderr_path = command_dir / self._COMMAND_STDERR_NAME + for output_path in (stdout_path, stderr_path): + output_path.write_text("") + + started_at = time.monotonic() + try: + result = await self._exec_agent_command(environment, exec_input) + except AgentTimeoutError as exc: + timeout_error = exc + break + elapsed_sec = time.monotonic() - started_at + + (command_dir / "return-code.txt").write_text(str(result.return_code)) + if result.stdout: + stdout_path.write_text(result.stdout) + if result.stderr: + stderr_path.write_text(result.stderr) + if self._is_exec_timeout_return( + result, exec_input.timeout_sec, elapsed_sec + ): + assert exec_input.timeout_sec is not None + timeout_error = self._agent_timeout_error(exec_input.timeout_sec) + break + if result.return_code != 0: + failed_command = (i, result.return_code) + break + + # Download token file from container BEFORE populating context + # Clear any stale token file first to avoid reading outdated data if download fails + token_file = self.logs_dir / "xum-tokens.json" + token_file.unlink(missing_ok=True) + try: + await environment.download_file(self._TOKEN_FILE_PATH, token_file) + except Exception: + pass # Token file may not exist if agent crashed early + + self.populate_context_post_run(context) + + if timeout_error is not None: + raise timeout_error + + if failed_command is not None: + command_index, return_code = failed_command + raise RuntimeError( + f"xum agent command failed (command {command_index}, exit {return_code})" + ) + + def populate_context_post_run(self, context: AgentContext) -> None: + """Extract token usage and cost from the token file written by xum-run.sh.""" + token_file = self.logs_dir / "xum-tokens.json" + if token_file.exists(): + try: + data = json.loads(token_file.read_text()) + context.n_input_tokens = data.get("input", 0) + context.n_output_tokens = data.get("output", 0) + # cost_usd is computed by xum CLI from model pricing + if data.get("cost_usd") is not None: + context.cost_usd = data["cost_usd"] + except Exception: + pass # Token/cost extraction is best-effort diff --git a/benchmarks/terminal_bench/mux_agent_test.py b/benchmarks/terminal_bench/xum_agent_test.py similarity index 62% rename from benchmarks/terminal_bench/mux_agent_test.py rename to benchmarks/terminal_bench/xum_agent_test.py index fd181841aa..288ac81861 100644 --- a/benchmarks/terminal_bench/mux_agent_test.py +++ b/benchmarks/terminal_bench/xum_agent_test.py @@ -15,12 +15,21 @@ from harbor.trial.trial import AgentTimeoutError from .mux_agent import MuxAgent -from .mux_payload import build_app_archive +from .xum_agent import XumAgent +from .xum_payload import build_app_archive @pytest.fixture(autouse=True) -def _clear_mux_env(monkeypatch: pytest.MonkeyPatch) -> None: - keys = (*MuxAgent._PROVIDER_ENV_KEYS, *MuxAgent._CONFIG_ENV_KEYS) +def _clear_xum_env(monkeypatch: pytest.MonkeyPatch) -> None: + keys = ( + *XumAgent._PROVIDER_ENV_KEYS, + *XumAgent._CONFIG_ENV_KEYS, + *XumAgent._LEGACY_CONFIG_ENV_KEYS, + "XUM_AGENT_REPO_ROOT", + "MUX_AGENT_REPO_ROOT", + XumAgent._PROVIDERS_FILE_ENV_KEY, + XumAgent._LEGACY_PROVIDERS_FILE_ENV_KEY, + ) for key in keys: monkeypatch.delenv(key, raising=False) @@ -43,19 +52,20 @@ def _write_executable(path: Path, content: str) -> None: path.chmod(0o755) -def _run_mux_runner_smoke( +def _run_xum_runner_smoke( tmp_path: Path, *, exit_code: int, goal_mode: str | None = None, timeout_ms: str | None = None, + runner_name: str = "xum-run.sh", ) -> _RunnerSmokeResult: app_root = tmp_path / "app" project_path = tmp_path / "project" fake_bun_root = tmp_path / "bun-root" fake_bin = fake_bun_root / "bin" log_dir = tmp_path / "logs" / "agent" / "command-0" - token_file = tmp_path / "mux-tokens.json" + token_file = tmp_path / "xum-tokens.json" args_file = tmp_path / "bun-args.txt" timeout_marker = tmp_path / "timeout-invoked.txt" @@ -70,7 +80,7 @@ def _run_mux_runner_smoke( printf '%s\n' "$*" >"${FAKE_BUN_ARGS_FILE}" cat >/dev/null printf '{"type":"run-complete","usage":{"inputTokens":7,"outputTokens":11},"cost_usd":0.42}\n' -exit "${FAKE_MUX_EXIT_CODE}" +exit "${FAKE_XUM_EXIT_CODE}" """, ) _write_executable( @@ -87,21 +97,21 @@ def _run_mux_runner_smoke( { "BUN_INSTALL": str(fake_bun_root), "FAKE_BUN_ARGS_FILE": str(args_file), - "FAKE_MUX_EXIT_CODE": str(exit_code), + "FAKE_XUM_EXIT_CODE": str(exit_code), "FAKE_TIMEOUT_MARKER": str(timeout_marker), - "MUX_APP_ROOT": str(app_root), - "MUX_LOG_DIR": str(log_dir), - "MUX_PROJECT_PATH": str(project_path), - "MUX_TOKEN_FILE": str(token_file), + "XUM_APP_ROOT": str(app_root), + "XUM_LOG_DIR": str(log_dir), + "XUM_PROJECT_PATH": str(project_path), + "XUM_TOKEN_FILE": str(token_file), "PATH": f"{fake_bin}{os.pathsep}{env.get('PATH', '')}", } ) if goal_mode is not None: - env["MUX_RUN_AS_GOAL"] = goal_mode + env["XUM_RUN_AS_GOAL"] = goal_mode if timeout_ms is not None: - env["MUX_TIMEOUT_MS"] = timeout_ms + env["XUM_TIMEOUT_MS"] = timeout_ms - runner_path = _repo_root() / "benchmarks/terminal_bench/mux-run.sh" + runner_path = _repo_root() / "benchmarks/terminal_bench" / runner_name completed = subprocess.run( ["bash", str(runner_path), "solve it"], capture_output=True, @@ -120,57 +130,89 @@ def _run_mux_runner_smoke( ) +def test_legacy_agent_import_resolves_to_canonical_adapter() -> None: + assert MuxAgent is XumAgent + assert MuxAgent.name() == "xum" + + def test_env_defaults_are_normalized( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path, model_name="anthropic/claude-sonnet-4-5") + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path, model_name="anthropic/claude-sonnet-4-5") env = agent._env - assert env["MUX_MODEL"] == "anthropic:claude-sonnet-4-5" - assert env["MUX_PROJECT_CANDIDATES"] == agent._DEFAULT_PROJECT_CANDIDATES + assert env["XUM_MODEL"] == "anthropic:claude-sonnet-4-5" + assert env["XUM_PROJECT_CANDIDATES"] == agent._DEFAULT_PROJECT_CANDIDATES -def test_goal_mode_env_is_forwarded( +def test_legacy_environment_falls_back_to_canonical_task_names( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - monkeypatch.setenv("MUX_RUN_AS_GOAL", "true") + monkeypatch.setenv("MUX_MODEL", "openai/gpt-5") + monkeypatch.setenv("MUX_PROJECT_PATH", "/legacy-project") + + env = XumAgent(logs_dir=tmp_path, model_name="")._env + + assert env["XUM_MODEL"] == "openai:gpt-5" + assert env["XUM_PROJECT_PATH"] == "/legacy-project" + assert "MUX_MODEL" not in env + assert "MUX_PROJECT_PATH" not in env + + +def test_canonical_environment_wins_over_legacy_alias( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + monkeypatch.setenv("XUM_MODEL", "anthropic/claude-sonnet-4-5") + monkeypatch.setenv("MUX_MODEL", "openai/gpt-5") + + env = XumAgent(logs_dir=tmp_path, model_name="")._env + + assert env["XUM_MODEL"] == "anthropic:claude-sonnet-4-5" + + +def test_goal_mode_env_is_forwarded( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + monkeypatch.setenv("XUM_RUN_AS_GOAL", "true") - agent = MuxAgent(logs_dir=tmp_path) + agent = XumAgent(logs_dir=tmp_path) - assert agent._env["MUX_RUN_AS_GOAL"] == "1" + assert agent._env["XUM_RUN_AS_GOAL"] == "1" def test_goal_mode_defaults_to_disabled( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path) + agent = XumAgent(logs_dir=tmp_path) - assert "MUX_RUN_AS_GOAL" not in agent._env + assert "XUM_RUN_AS_GOAL" not in agent._env def test_goal_mode_rejects_invalid_values( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - monkeypatch.setenv("MUX_RUN_AS_GOAL", "yes") + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + monkeypatch.setenv("XUM_RUN_AS_GOAL", "yes") - agent = MuxAgent(logs_dir=tmp_path) - with pytest.raises(ValueError, match="MUX_RUN_AS_GOAL"): + agent = XumAgent(logs_dir=tmp_path) + with pytest.raises(ValueError, match="XUM_RUN_AS_GOAL"): _ = agent._env def test_timeout_must_be_numeric( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - monkeypatch.setenv("MUX_TIMEOUT_MS", "not-a-number") + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + monkeypatch.setenv("XUM_TIMEOUT_MS", "not-a-number") - agent = MuxAgent(logs_dir=tmp_path) + agent = XumAgent(logs_dir=tmp_path) with pytest.raises(ValueError): _ = agent._env @@ -178,20 +220,31 @@ def test_timeout_must_be_numeric( def test_timeout_kwarg_is_instance_local( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + + agent = XumAgent(logs_dir=tmp_path, timeout=1) - agent = MuxAgent(logs_dir=tmp_path, timeout=1) + assert os.environ.get("XUM_TIMEOUT_MS") is None + assert agent._env["XUM_TIMEOUT_MS"] == "1000" + assert "XUM_TIMEOUT_MS" not in XumAgent(logs_dir=tmp_path / "other")._env - assert os.environ.get("MUX_TIMEOUT_MS") is None - assert agent._env["MUX_TIMEOUT_MS"] == "1000" - assert "MUX_TIMEOUT_MS" not in MuxAgent(logs_dir=tmp_path / "other")._env + +def test_legacy_runner_entrypoint_forwards_to_xum(tmp_path: Path) -> None: + result = _run_xum_runner_smoke( + tmp_path, + exit_code=0, + runner_name="mux-run.sh", + ) + + assert result.completed.returncode == 0, result.completed.stderr + assert result.token_file.exists() -def test_mux_runner_scores_goal_mode_incomplete_exit(tmp_path: Path) -> None: - result = _run_mux_runner_smoke(tmp_path, exit_code=3, goal_mode="1") +def test_xum_runner_scores_goal_mode_incomplete_exit(tmp_path: Path) -> None: + result = _run_xum_runner_smoke(tmp_path, exit_code=3, goal_mode="1") assert result.completed.returncode == 0, result.completed.stderr - assert "WARNING: mux goal run stopped incomplete" in result.completed.stderr + assert "WARNING: xum goal run stopped incomplete" in result.completed.stderr args = result.args_file.read_text() assert "--goal" in args assert "solve it" in args @@ -204,20 +257,20 @@ def test_mux_runner_scores_goal_mode_incomplete_exit(tmp_path: Path) -> None: assert stdout_event["type"] == "run-complete" -def test_mux_runner_preserves_incomplete_exit_outside_goal_mode(tmp_path: Path) -> None: - result = _run_mux_runner_smoke(tmp_path, exit_code=3) +def test_xum_runner_preserves_incomplete_exit_outside_goal_mode(tmp_path: Path) -> None: + result = _run_xum_runner_smoke(tmp_path, exit_code=3) assert result.completed.returncode == 3 - assert "mux agent session failed (exit 3)" in result.completed.stderr + assert "xum agent session failed (exit 3)" in result.completed.stderr assert result.token_file.exists() -def test_mux_runner_preserves_fatal_exit(tmp_path: Path) -> None: - result = _run_mux_runner_smoke(tmp_path, exit_code=1, goal_mode="1") +def test_xum_runner_preserves_fatal_exit(tmp_path: Path) -> None: + result = _run_xum_runner_smoke(tmp_path, exit_code=1, goal_mode="1") assert result.completed.returncode == 1 - assert "mux agent session failed (exit 1)" in result.completed.stderr - assert "WARNING: mux goal run stopped incomplete" not in result.completed.stderr + assert "xum agent session failed (exit 1)" in result.completed.stderr + assert "WARNING: xum goal run stopped incomplete" not in result.completed.stderr assert json.loads(result.token_file.read_text()) == { "input": 7, "output": 11, @@ -225,8 +278,8 @@ def test_mux_runner_preserves_fatal_exit(tmp_path: Path) -> None: } -def test_mux_runner_leaves_timeout_to_harbor(tmp_path: Path) -> None: - result = _run_mux_runner_smoke(tmp_path, exit_code=0, timeout_ms="1000") +def test_xum_runner_leaves_timeout_to_harbor(tmp_path: Path) -> None: + result = _run_xum_runner_smoke(tmp_path, exit_code=0, timeout_ms="1000") assert result.completed.returncode == 0, result.completed.stderr assert "Harbor remains timeout authority" in result.completed.stdout @@ -260,8 +313,8 @@ async def exec(self, **_kwargs: object) -> _ExecResult: raise RuntimeError(f"Command timed out after {timeout_sec} seconds") await asyncio.sleep(self.delay_sec) if self.command_dir is not None: - stdout_path = self.command_dir / MuxAgent._COMMAND_STDOUT_NAME - stderr_path = self.command_dir / MuxAgent._COMMAND_STDERR_NAME + stdout_path = self.command_dir / XumAgent._COMMAND_STDOUT_NAME + stderr_path = self.command_dir / XumAgent._COMMAND_STDERR_NAME assert stdout_path.exists() assert stderr_path.exists() stdout_path.write_text("sandbox out") @@ -276,22 +329,22 @@ async def download_file(self, source_path: str, target_path: Path) -> None: def test_run_raises_after_preserving_logs_for_nonzero_exit( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path) environment = _FakeEnvironment( _ExecResult(return_code=7, stdout="out", stderr="err") ) context = SimpleNamespace() - with pytest.raises(RuntimeError, match="mux agent command failed"): + with pytest.raises(RuntimeError, match="xum agent command failed"): asyncio.run(agent.run("do the task", environment, context)) command_dir = tmp_path / "command-0" assert (command_dir / "return-code.txt").read_text() == "7" - assert (command_dir / MuxAgent._COMMAND_STDOUT_NAME).read_text() == "out" - assert (command_dir / MuxAgent._COMMAND_STDERR_NAME).read_text() == "err" + assert (command_dir / XumAgent._COMMAND_STDOUT_NAME).read_text() == "out" + assert (command_dir / XumAgent._COMMAND_STDERR_NAME).read_text() == "err" assert environment.download_attempts == [ - (agent._TOKEN_FILE_PATH, tmp_path / "mux-tokens.json") + (agent._TOKEN_FILE_PATH, tmp_path / "xum-tokens.json") ] assert getattr(context, "n_input_tokens") == 7 assert getattr(context, "n_output_tokens") == 11 @@ -301,8 +354,8 @@ def test_run_raises_after_preserving_logs_for_nonzero_exit( def test_run_timeout_surfaces_agent_timeout_error( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path, timeout=0.01) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path, timeout=0.01) environment = _FakeEnvironment( _ExecResult(return_code=0, stdout="out", stderr="err"), delay_sec=0.05, @@ -313,7 +366,7 @@ def test_run_timeout_surfaces_agent_timeout_error( asyncio.run(agent.run("do the task", environment, context)) assert environment.download_attempts == [ - (agent._TOKEN_FILE_PATH, tmp_path / "mux-tokens.json") + (agent._TOKEN_FILE_PATH, tmp_path / "xum-tokens.json") ] assert getattr(context, "n_input_tokens") == 7 assert getattr(context, "n_output_tokens") == 11 @@ -323,8 +376,8 @@ def test_run_timeout_surfaces_agent_timeout_error( def test_run_maps_near_timeout_return_code_124_to_agent_timeout( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path, timeout=0.01) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path, timeout=0.01) environment = _FakeEnvironment( _ExecResult(return_code=124, stdout="partial event", stderr=""), delay_sec=0.01, @@ -336,14 +389,14 @@ def test_run_maps_near_timeout_return_code_124_to_agent_timeout( command_dir = tmp_path / "command-0" assert (command_dir / "return-code.txt").read_text() == "124" - assert (command_dir / MuxAgent._COMMAND_STDOUT_NAME).read_text() == "partial event" + assert (command_dir / XumAgent._COMMAND_STDOUT_NAME).read_text() == "partial event" def test_run_keeps_fast_return_code_124_strict( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path, timeout=10) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path, timeout=10) environment = _FakeEnvironment(_ExecResult(return_code=124)) context = SimpleNamespace() @@ -355,8 +408,8 @@ def test_run_keeps_fast_return_code_124_strict( def test_run_keeps_non_timeout_agent_exits_strict( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, return_code: int ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path, timeout=0.01) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path, timeout=0.01) environment = _FakeEnvironment( _ExecResult(return_code=return_code, stdout="partial event", stderr=""), delay_sec=0.01, @@ -370,13 +423,13 @@ def test_run_keeps_non_timeout_agent_exits_strict( def test_run_keeps_explicit_return_code_124_failure_strict( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path, timeout=0.01) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path, timeout=0.01) environment = _FakeEnvironment( _ExecResult( return_code=124, stdout="partial event", - stderr="[mux-run] ERROR: mux agent session failed (exit 124)", + stderr="[xum-run] ERROR: xum agent session failed (exit 124)", ), delay_sec=0.01, ) @@ -389,8 +442,8 @@ def test_run_keeps_explicit_return_code_124_failure_strict( def test_run_preseeds_command_logs_before_sandbox_exec( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path) command_dir = tmp_path / "command-0" environment = _FakeEnvironment( _ExecResult(return_code=0, stdout="out", stderr="err"), @@ -400,15 +453,15 @@ def test_run_preseeds_command_logs_before_sandbox_exec( asyncio.run(agent.run("do the task", environment, context)) - assert (command_dir / MuxAgent._COMMAND_STDOUT_NAME).read_text() == "out" - assert (command_dir / MuxAgent._COMMAND_STDERR_NAME).read_text() == "err" + assert (command_dir / XumAgent._COMMAND_STDOUT_NAME).read_text() == "out" + assert (command_dir / XumAgent._COMMAND_STDERR_NAME).read_text() == "err" def test_run_populates_context_for_successful_exit( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(_repo_root())) - agent = MuxAgent(logs_dir=tmp_path) + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + agent = XumAgent(logs_dir=tmp_path) environment = _FakeEnvironment( _ExecResult(return_code=0, stdout="out", stderr="err") ) @@ -418,15 +471,15 @@ def test_run_populates_context_for_successful_exit( command_dir = tmp_path / "command-0" assert (command_dir / "return-code.txt").read_text() == "0" - assert (command_dir / MuxAgent._COMMAND_STDOUT_NAME).read_text() == "out" - assert (command_dir / MuxAgent._COMMAND_STDERR_NAME).read_text() == "err" + assert (command_dir / XumAgent._COMMAND_STDOUT_NAME).read_text() == "out" + assert (command_dir / XumAgent._COMMAND_STDERR_NAME).read_text() == "err" assert getattr(context, "n_input_tokens") == 7 assert getattr(context, "n_output_tokens") == 11 assert getattr(context, "cost_usd") == 0.42 def test_app_archive_includes_postinstall_script() -> None: - assert "scripts/postinstall.sh" in MuxAgent._INCLUDE_PATHS + assert "scripts/postinstall.sh" in XumAgent._INCLUDE_PATHS repo_root = _repo_root() postinstall = repo_root / "scripts/postinstall.sh" diff --git a/benchmarks/terminal_bench/mux_payload.py b/benchmarks/terminal_bench/xum_payload.py similarity index 83% rename from benchmarks/terminal_bench/mux_payload.py rename to benchmarks/terminal_bench/xum_payload.py index c7cd2368e9..694a330d6d 100644 --- a/benchmarks/terminal_bench/mux_payload.py +++ b/benchmarks/terminal_bench/xum_payload.py @@ -7,9 +7,9 @@ def build_app_archive(repo_root: Path, include_paths: Iterable[str]) -> bytes: - """Pack the mux workspace into a gzipped tarball.""" + """Pack the xum workspace into a gzipped tarball.""" if not repo_root.exists(): - raise FileNotFoundError(f"mux repo root {repo_root} not found") + raise FileNotFoundError(f"xum repo root {repo_root} not found") buffer = io.BytesIO() with tarfile.open(fileobj=buffer, mode="w:gz") as archive: diff --git a/benchmarks/terminal_bench/xum_run_contract.py b/benchmarks/terminal_bench/xum_run_contract.py new file mode 100644 index 0000000000..965d0b5fc2 --- /dev/null +++ b/benchmarks/terminal_bench/xum_run_contract.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +TIMEOUT_RETURN_CODE = 124 +OOM_LIKE_RETURN_CODE = 137 +RUN_COMPLETE_MARKER = "run-complete" +XUM_RUN_FAILURE_MARKER = "[xum-run] ERROR: xum agent session failed" + + +def xum_run_failure_marker(return_code: int) -> str: + return f"{XUM_RUN_FAILURE_MARKER} (exit {return_code})" + + +XUM_RUN_TIMEOUT_FAILURE_MARKER = xum_run_failure_marker(TIMEOUT_RETURN_CODE) diff --git a/benchmarks/terminal_bench/xum_setup.sh.j2 b/benchmarks/terminal_bench/xum_setup.sh.j2 new file mode 100644 index 0000000000..06b4de3008 --- /dev/null +++ b/benchmarks/terminal_bench/xum_setup.sh.j2 @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +log() { + printf '[xum-setup] %s\n' "$1" +} + +ensure_tool() { + if command -v "$1" >/dev/null 2>&1; then + return 0 + fi + + if ! command -v apt-get >/dev/null 2>&1; then + printf 'Required tool "%s" missing and apt-get unavailable\n' "$1" >&2 + return 1 + fi + + log "installing missing dependency: $1" + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y "$1" +} + +ensure_tool curl +ensure_tool git +ensure_tool unzip +ensure_tool python3 +export BUN_INSTALL="${BUN_INSTALL:-/root/.bun}" +export PATH="${BUN_INSTALL}/bin:${PATH}" + +# Preserve benchmark configurations created before the environment rename. +for suffix in AGENT_GIT_URL BUN_INSTALL_URL APP_ROOT CONFIG_ROOT; do + canonical_name="XUM_${suffix}" + legacy_name="MUX_${suffix}" + if [[ ! -v "${canonical_name}" && -v "${legacy_name}" ]]; then + printf -v "${canonical_name}" '%s' "${!legacy_name}" + export "${canonical_name}" + fi +done + +if ! command -v bun >/dev/null 2>&1; then + log "installing bun" + curl -fsSL "${XUM_BUN_INSTALL_URL:-https://bun.sh/install}" | bash +fi + +XUM_APP_ROOT="${XUM_APP_ROOT:-/opt/xum-app}" +XUM_CONFIG_ROOT="${XUM_CONFIG_ROOT:-/root/.xum}" +XUM_AGENT_VERSION="{{ version if version is not none else '' }}" + +rm -rf "${XUM_APP_ROOT}" +if [[ -n "${XUM_AGENT_VERSION}" ]]; then + : "${XUM_AGENT_GIT_URL:?XUM_AGENT_GIT_URL required when version is set}" + log "cloning Xum from ${XUM_AGENT_GIT_URL} @ ${XUM_AGENT_VERSION}" + git clone --depth 1 --branch "${XUM_AGENT_VERSION}" "${XUM_AGENT_GIT_URL}" "${XUM_APP_ROOT}" +else + log "extracting Xum archive" + mkdir -p "${XUM_APP_ROOT}" + tar -xzf "/installed-agent/xum-app.tar.gz" -C "${XUM_APP_ROOT}" +fi + +cd "${XUM_APP_ROOT}" + +# Use --production to skip devDependencies (electron-builder, storybook, etc.) +# which cuts install size from 1.9GB → 728MB and avoids OOM in memory-constrained +# Daytona sandboxes (2GB). The headless CLI only needs production deps. +if [[ -d "node_modules" ]]; then + log "node_modules already present, skipping bun install" +else + log "installing xum production dependencies via bun" + XUM_HEADLESS=1 bun install --production --frozen-lockfile +fi + +mkdir -p "${XUM_CONFIG_ROOT}" + +chmod +x /installed-agent/xum-run.sh + +log "setup complete" diff --git a/public/service-worker.js b/public/service-worker.js index 49c99e6684..b95a3ec968 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,5 +1,7 @@ -// mux Service Worker for PWA support -const CACHE_NAME = "mux-v2"; +// Xum Service Worker for PWA support +const CACHE_NAME = "xum-v3"; +// Explicit compatibility cleanup prevents the pre-rename cache from becoming orphaned. +const LEGACY_MUX_CACHE_NAME = "mux-v2"; const urlsToCache = ["./", "./index.html"]; // Install event - cache core assets @@ -20,6 +22,9 @@ self.addEventListener("activate", (event) => { .then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { + if (cacheName === LEGACY_MUX_CACHE_NAME) { + return caches.delete(cacheName); + } if (cacheName !== CACHE_NAME) { return caches.delete(cacheName); } diff --git a/scripts/audit_xum_branding.py b/scripts/audit_xum_branding.py new file mode 100644 index 0000000000..bac14b95ec --- /dev/null +++ b/scripts/audit_xum_branding.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Audit the project-owned rename boundary without rejecting compatibility contracts.""" + +from __future__ import annotations + +import argparse +import fnmatch +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + +BRANDING_PATTERN = re.compile(r"(?i)cmux|mux") + +# This is intentionally a boundary audit, not a blind repository-wide zero-match rule. +# Generated dependencies, docs/history, icon assets, and core TS Mux* types are outside +# this change set; adding a project-owned surface requires an explicit review here. +BOUNDARY_GLOBS = ( + "Makefile", + ".envrc", + "public/service-worker.js", + "benchmarks/terminal_bench/**", + ".github/workflows/terminal-bench.yml", + ".github/workflows/nightly-terminal-bench.yml", + "scripts/check-bench-agent.sh", + "scripts/check_tbench_results.py", + "src/node/services/log.ts", + "src/common/constants/paths.ts", + "src/cli/server.ts", + "src/cli/serverCrashLogging.test.ts", +) + +EXCLUDED_GLOBS = ( + "**/__pycache__/**", + "benchmarks/terminal_bench/.leaderboard_cache/**", +) + + +@dataclass(frozen=True) +class AllowRule: + path_glob: str + content_pattern: re.Pattern[str] + reason: str + + +# Each retained spelling maps to a compatibility or historical contract. Keep rules +# narrow enough that a new user-facing "Mux" sentence on a canonical surface fails. +ALLOW_RULES = ( + AllowRule( + "Makefile", + re.compile(r"MUX_(?:\*|[A-Z0-9_]+)"), + "legacy developer environment input", + ), + AllowRule( + "Makefile", + re.compile(r"^mux:|\.PHONY:.*\bmux\b|filter.*\bmux\b"), + "legacy Make CLI alias", + ), + AllowRule("Makefile", re.compile(r"smoke-test-mux-compat\.sh"), "legacy package smoke test"), + AllowRule(".envrc", re.compile(r"LEGACY_MUX_SHARED_ENVRC|\.mux/"), "legacy shared env fallback"), + AllowRule("public/service-worker.js", re.compile(r"LEGACY_MUX_CACHE_NAME|mux-v2"), "superseded cache cleanup"), + AllowRule("benchmarks/terminal_bench/mux_agent.py", re.compile(r".*"), "legacy Harbor import path"), + AllowRule("benchmarks/terminal_bench/mux-run.sh", re.compile(r".*"), "legacy staged runner entrypoint"), + AllowRule("benchmarks/terminal_bench/__init__.py", re.compile(r"MuxAgent"), "legacy lazy class alias"), + AllowRule("benchmarks/terminal_bench/xum_agent.py", re.compile(r"MUX_(?:\*|[A-Z0-9_]+|\$?\{)"), "legacy benchmark environment input"), + AllowRule( + "benchmarks/terminal_bench/xum_agent_test.py", + re.compile(r"MUX_[A-Z0-9_]+|MuxAgent|mux_agent|mux-run\.sh"), + "compatibility behavior coverage", + ), + AllowRule("benchmarks/terminal_bench/xum-run.sh", re.compile(r"MUX_(?:\*|[A-Z0-9_]+|\$?\{)"), "legacy runner environment input"), + AllowRule("benchmarks/terminal_bench/xum_setup.sh.j2", re.compile(r"MUX_(?:\*|[A-Z0-9_]+|\$?\{)"), "legacy setup environment input"), + AllowRule( + "benchmarks/terminal_bench/README.md", + re.compile(r"mux_run_(?:args|as_goal)"), + "legacy workflow-dispatch input required by GitHub's input limit", + ), + AllowRule( + "benchmarks/terminal_bench/README.md", + re.compile(r"mux-benchmarks"), + "existing BigQuery project ID", + ), + AllowRule("benchmarks/terminal_bench/analyze_failure_rates.py", re.compile(r"mux-benchmarks"), "existing BigQuery project ID"), + AllowRule("benchmarks/terminal_bench/analyze_failure_rates.py", re.compile(r"--mux-model|[\"']mux[\"']|historical Mux"), "legacy CLI alias and leaderboard history"), + AllowRule(".github/workflows/terminal-bench.yml", re.compile(r"inputs\.mux_|^\s*mux_"), "legacy reusable-workflow input"), + AllowRule( + ".github/workflows/nightly-terminal-bench.yml", + re.compile(r"inputs\.mux_|^\s*mux_"), + "legacy workflow-dispatch input", + ), + AllowRule(".github/workflows/terminal-bench.yml", re.compile(r"mux-benchmarks"), "existing BigQuery project ID"), + AllowRule( + "src/node/services/log.ts", + re.compile(r"common/compat/legacyMux"), + "centralized compatibility resolver import", + ), + AllowRule("src/node/services/log.ts", re.compile(r"MUX_(?:DEBUG|LOG_LEVEL)"), "legacy logging environment input"), + AllowRule( + "src/common/constants/paths.ts", + re.compile(r"LEGACY_(?:C?MUX)|legacyC?Mux|MUX_ROOT", re.IGNORECASE), + "centralized storage compatibility", + ), + AllowRule( + "src/cli/server.ts", + re.compile(r"common/compat/legacyMux"), + "centralized compatibility resolver import", + ), + AllowRule("src/cli/server.ts", re.compile(r"MUX_SERVER_(?:URL|AUTH_TOKEN)"), "legacy server environment output"), + AllowRule("src/cli/serverCrashLogging.test.ts", re.compile(r"[\"']mux[\"']"), "legacy CLI argv redaction coverage"), +) + + +@dataclass(frozen=True) +class Violation: + path: str + line_number: int + line: str + + +def _matches_any(path: str, globs: Iterable[str]) -> bool: + return any(fnmatch.fnmatch(path, pattern) for pattern in globs) + + +def _is_allowed(path: str, text: str) -> bool: + return any( + fnmatch.fnmatch(path, rule.path_glob) and rule.content_pattern.search(text) + for rule in ALLOW_RULES + ) + + +def audit_paths(root: Path, paths: Iterable[str]) -> list[Violation]: + violations: list[Violation] = [] + for relative_path in sorted(set(paths)): + if not _matches_any(relative_path, BOUNDARY_GLOBS): + continue + if _matches_any(relative_path, EXCLUDED_GLOBS): + continue + + # Filenames are branding too. Compatibility entrypoint names need their own rule. + if BRANDING_PATTERN.search(relative_path) and not _is_allowed(relative_path, relative_path): + violations.append(Violation(relative_path, 0, "project-owned path contains legacy branding")) + + path = root / relative_path + try: + lines = path.read_text(encoding="utf-8").splitlines() + except (OSError, UnicodeDecodeError): + continue + + for line_number, line in enumerate(lines, start=1): + if BRANDING_PATTERN.search(line) and not _is_allowed(relative_path, line): + violations.append(Violation(relative_path, line_number, line.strip())) + return violations + + +def _tracked_paths(root: Path) -> list[str]: + completed = subprocess.run( + ["git", "ls-files", "--cached", "--others", "--exclude-standard"], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.splitlines() + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--verbose", action="store_true", help="Print boundary and allowlist counts") + args = parser.parse_args() + + root = args.root.resolve() + violations = audit_paths(root, _tracked_paths(root)) + if violations: + print("Non-allowlisted legacy branding found in the Xum rename boundary:", file=sys.stderr) + for violation in violations: + location = f"{violation.path}:{violation.line_number}" if violation.line_number else violation.path + print(f" {location}: {violation.line}", file=sys.stderr) + print("Update the branding or add a narrow ALLOW_RULES entry with a compatibility reason.", file=sys.stderr) + return 1 + + if args.verbose: + print( + f"Xum branding audit passed ({len(BOUNDARY_GLOBS)} boundary globs, " + f"{len(ALLOW_RULES)} explicit allowlist rules)." + ) + else: + print("Xum branding audit passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/audit_xum_branding_test.py b/scripts/audit_xum_branding_test.py new file mode 100644 index 0000000000..f353a12684 --- /dev/null +++ b/scripts/audit_xum_branding_test.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from scripts.audit_xum_branding import audit_paths + + +class AuditXumBrandingTest(unittest.TestCase): + def test_reports_stale_copy_and_project_owned_filenames(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + (root / "Makefile").write_text("# Mux build instructions\n") + stale_path = root / "benchmarks/terminal_bench/mux_new_adapter.py" + stale_path.parent.mkdir(parents=True) + stale_path.write_text("class Adapter:\n pass\n") + + violations = audit_paths( + root, + ["Makefile", "benchmarks/terminal_bench/mux_new_adapter.py"], + ) + + self.assertEqual( + [(item.path, item.line_number) for item in violations], + [ + ("Makefile", 1), + ("benchmarks/terminal_bench/mux_new_adapter.py", 0), + ], + ) + + def test_accepts_reviewed_compatibility_and_ignores_generated_history(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + (root / "Makefile").write_text( + "XUM_VITE_PORT ?= $(or $(MUX_VITE_PORT),5173)\n" + "mux: xum ## Legacy alias for `make xum`\n" + ) + history_path = ( + root + / "benchmarks/terminal_bench/.leaderboard_cache/Mux__Historical/result.json" + ) + history_path.parent.mkdir(parents=True) + history_path.write_text('{"agent": "Mux"}\n') + + violations = audit_paths( + root, + [ + "Makefile", + "benchmarks/terminal_bench/.leaderboard_cache/Mux__Historical/result.json", + ], + ) + + self.assertEqual(violations, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/check-bench-agent.sh b/scripts/check-bench-agent.sh index 0dce41b971..aed2ea115f 100755 --- a/scripts/check-bench-agent.sh +++ b/scripts/check-bench-agent.sh @@ -2,24 +2,24 @@ set -euo pipefail # This script verifies that the terminal-bench agent entry point -# referenced in mux-run.sh is valid and can be executed (imports resolve). +# referenced in xum-run.sh is valid and can be executed (imports resolve). REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -MUX_RUN_SH="$REPO_ROOT/benchmarks/terminal_bench/mux-run.sh" +XUM_RUN_SH="$REPO_ROOT/benchmarks/terminal_bench/xum-run.sh" echo "Checking terminal-bench agent configuration..." -if [[ ! -f "$MUX_RUN_SH" ]]; then - echo "❌ Error: $MUX_RUN_SH not found" +if [[ ! -f "$XUM_RUN_SH" ]]; then + echo "❌ Error: $XUM_RUN_SH not found" exit 1 fi -# Extract the agent CLI path from mux-run.sh +# Extract the agent CLI path from xum-run.sh # Looks for line like: cmd=(bun src/cli/run.ts -CLI_PATH_MATCH=$(grep -o "bun src/.*\.ts" "$MUX_RUN_SH" | head -1 | cut -d' ' -f2) +CLI_PATH_MATCH=$(grep -o "bun src/.*\.ts" "$XUM_RUN_SH" | head -1 | cut -d' ' -f2) if [[ -z "$CLI_PATH_MATCH" ]]; then - echo "❌ Error: Could not find agent CLI path in $MUX_RUN_SH" + echo "❌ Error: Could not find agent CLI path in $XUM_RUN_SH" exit 1 fi @@ -86,7 +86,7 @@ fi # Verify that CLI subcommands boot WITHOUT a lockfile using bun's resolver. # npm and bun resolve pre-release caret ranges differently — bun includes the # stable release (e.g. ^0.1.0-main.28 → 0.1.0) while npm does not. Since users -# run `bun x mux@latest`, we must test with bun to catch resolution mismatches. +# run `bun x xum@latest`, we must test with bun to catch resolution mismatches. echo "" echo "Checking CLI subcommand imports (bun, lockfile-free)..." @@ -115,7 +115,7 @@ CLI_SUBCMDS=(run server) for subcmd in "${CLI_SUBCMDS[@]}"; do if ! output=$(node "$CHECK_DIR/dist/cli/index.js" "$subcmd" --help 2>&1); then if echo "$output" | grep -qE "Cannot find module|MODULE_NOT_FOUND|not defined by \"exports\""; then - echo "❌ Error: 'mux $subcmd --help' failed (bun lockfile-free resolution):" + echo "❌ Error: 'xum $subcmd --help' failed (bun lockfile-free resolution):" echo "$output" echo "" echo "A dependency likely resolved to a version missing a required export." diff --git a/scripts/check_tbench_results.py b/scripts/check_tbench_results.py index 15a587213f..e0b1b09b73 100755 --- a/scripts/check_tbench_results.py +++ b/scripts/check_tbench_results.py @@ -14,12 +14,12 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) -from benchmarks.terminal_bench.mux_run_contract import ( # noqa: E402 - MUX_RUN_FAILURE_MARKER, +from benchmarks.terminal_bench.xum_run_contract import ( # noqa: E402 + XUM_RUN_FAILURE_MARKER, OOM_LIKE_RETURN_CODE, RUN_COMPLETE_MARKER, TIMEOUT_RETURN_CODE, - mux_run_failure_marker, + xum_run_failure_marker, ) Category = Literal["infra", "soft", "hard"] @@ -230,7 +230,7 @@ def classify_exception( if ( return_code == TIMEOUT_RETURN_CODE and RUN_COMPLETE_MARKER not in stdout - and mux_run_failure_marker(TIMEOUT_RETURN_CODE) not in stderr + and xum_run_failure_marker(TIMEOUT_RETURN_CODE) not in stderr ): return ClassifiedException( path=exception_path, @@ -243,8 +243,8 @@ def classify_exception( reason = "non-infrastructure exception" if return_code is not None: reason = f"agent exit {return_code}" - if MUX_RUN_FAILURE_MARKER in stderr: - reason = f"mux-run failure ({reason})" + if XUM_RUN_FAILURE_MARKER in stderr: + reason = f"xum-run failure ({reason})" return ClassifiedException( path=exception_path, diff --git a/src/browser/serviceWorker.test.ts b/src/browser/serviceWorker.test.ts new file mode 100644 index 0000000000..2fc9851c44 --- /dev/null +++ b/src/browser/serviceWorker.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { runInNewContext } from "node:vm"; + +interface WorkerEvent { + waitUntil(promise: Promise): void; +} + +test("activation removes the pre-rename cache without deleting the current cache", async () => { + const listeners = new Map void>(); + const deletedCaches: string[] = []; + let activation: Promise | undefined; + + const self = { + addEventListener: (name: string, listener: (event: WorkerEvent) => void) => { + listeners.set(name, listener); + }, + skipWaiting: () => Promise.resolve(), + clients: { claim: () => Promise.resolve() }, + }; + const caches = { + keys: () => Promise.resolve(["xum-v3", "mux-v2"]), + delete: (name: string) => { + deletedCaches.push(name); + return Promise.resolve(true); + }, + }; + + const source = readFileSync(new URL("../../public/service-worker.js", import.meta.url), "utf8"); + runInNewContext(source, { self, caches }); + + const activate = listeners.get("activate"); + expect(activate).toBeDefined(); + activate?.({ + waitUntil: (promise) => { + activation = promise; + }, + }); + await activation; + + expect(deletedCaches).toEqual(["mux-v2"]); +}); diff --git a/src/cli/server.ts b/src/cli/server.ts index bdd4316952..9a1c07cbe7 100644 --- a/src/cli/server.ts +++ b/src/cli/server.ts @@ -21,7 +21,7 @@ import { appendServerCrashLogSync } from "./serverCrashLogging"; import { shouldExposeLaunchProject } from "./launchProject"; // Server-mode crashes can terminate the process before the async logger flushes, -// so these top-level hooks mirror fatal details into mux.log synchronously. +// so these top-level hooks mirror fatal details into xum.log synchronously. process.on("warning", (warning) => { log.warn("Server process warning", warning); }); @@ -30,7 +30,7 @@ process.on("uncaughtExceptionMonitor", (error, origin) => { // Use the monitor hook instead of adding our own unhandledRejection listener. // In Node, installing an unhandledRejection handler changes fatal promise // rejections into non-fatal events; the monitor preserves the default crash - // while still giving server-mode users a synchronous breadcrumb in mux.log. + // while still giving server-mode users a synchronous breadcrumb in xum.log. appendServerCrashLogSync({ event: "Fatal process error", detail: error, diff --git a/src/cli/serverCrashLogging.test.ts b/src/cli/serverCrashLogging.test.ts index 4e27ad9722..6be577580c 100644 --- a/src/cli/serverCrashLogging.test.ts +++ b/src/cli/serverCrashLogging.test.ts @@ -56,8 +56,8 @@ describe("serverCrashLogging", () => { }); test("falls back when crash entry construction throws", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-server-crash-log-fallback-")); - const logFilePath = path.join(tempDir, "logs", "mux.log"); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "xum-server-crash-log-fallback-")); + const logFilePath = path.join(tempDir, "logs", "xum.log"); const cwdSpy = spyOn(process, "cwd").mockImplementation(() => { throw new Error("cwd missing"); }); @@ -82,8 +82,8 @@ describe("serverCrashLogging", () => { }); test("appends crash entries to disk synchronously", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-server-crash-log-")); - const logFilePath = path.join(tempDir, "logs", "mux.log"); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "xum-server-crash-log-")); + const logFilePath = path.join(tempDir, "logs", "xum.log"); try { appendServerCrashLogSync({ diff --git a/src/common/constants/paths.ts b/src/common/constants/paths.ts index 529377e211..a0449dab2e 100644 --- a/src/common/constants/paths.ts +++ b/src/common/constants/paths.ts @@ -289,8 +289,8 @@ export function getXumSessionsDir(rootDir?: string): string { } /** - * Get the directory where mux backend logs are stored. - * Example: ~/.xum/logs/mux.log + * Get the directory where Xum backend logs are stored. + * Example: ~/.xum/logs/xum.log * * @param rootDir - Optional root directory (defaults to getXumHome()) */ diff --git a/src/desktop/main.ts b/src/desktop/main.ts index d37a19ac14..c7087088e2 100644 --- a/src/desktop/main.ts +++ b/src/desktop/main.ts @@ -1122,7 +1122,7 @@ function createWindow() { }); // Forward renderer console errors to the log service so they reach the log - // file (~/.xum/logs/mux.log) and Output Tab even when the UI is white/blank. + // file (~/.xum/logs/xum.log) and Output Tab even when the UI is white/blank. // The renderer's global error handlers (window.addEventListener("error")) log // to console.error, but that stays in renderer memory only — the main process // never sees it without this hook. diff --git a/src/node/services/log.test.ts b/src/node/services/log.test.ts index 10987731f8..1570185f45 100644 --- a/src/node/services/log.test.ts +++ b/src/node/services/log.test.ts @@ -13,16 +13,16 @@ import * as fs from "node:fs"; import * as fsPromises from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; -import { __resetFileSinkForTests, clearLogFiles, closeLogFile, log } from "./log"; +import { __resetFileSinkForTests, clearLogFiles, closeLogFile, getLogFilePath, log } from "./log"; describe("log file sink state machine", () => { - let tempMuxRoot: string; - let originalMuxRoot: string | undefined; + let tempXumRoot: string; + let originalXumRoot: string | undefined; beforeAll(async () => { - originalMuxRoot = process.env.MUX_ROOT; - tempMuxRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), "mux-log-test-")); - process.env.MUX_ROOT = tempMuxRoot; + originalXumRoot = process.env.XUM_ROOT; + tempXumRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), "xum-log-test-")); + process.env.XUM_ROOT = tempXumRoot; }); beforeEach(() => { @@ -36,13 +36,30 @@ describe("log file sink state machine", () => { afterAll(async () => { closeLogFile(); - if (originalMuxRoot === undefined) { - delete process.env.MUX_ROOT; + if (originalXumRoot === undefined) { + delete process.env.XUM_ROOT; } else { - process.env.MUX_ROOT = originalMuxRoot; + process.env.XUM_ROOT = originalXumRoot; } - await fsPromises.rm(tempMuxRoot, { recursive: true, force: true }); + await fsPromises.rm(tempXumRoot, { recursive: true, force: true }); + }); + + test("writes only the canonical Xum log file", () => { + const originalCreateWriteStream = fs.createWriteStream; + const createWriteStreamSpy = spyOn(fs, "createWriteStream").mockImplementation((...args) => + originalCreateWriteStream(...args) + ); + + log.error("canonical log path"); + + const canonicalPath = path.join(tempXumRoot, "logs", "xum.log"); + expect(getLogFilePath()).toBe(canonicalPath); + expect(createWriteStreamSpy).toHaveBeenCalledWith(canonicalPath, { flags: "a" }); + expect(createWriteStreamSpy).not.toHaveBeenCalledWith( + path.join(tempXumRoot, "logs", "mux.log"), + { flags: "a" } + ); }); test("transitions to degraded after stream error and suppresses immediate retries", async () => { diff --git a/src/node/services/log.ts b/src/node/services/log.ts index 887d12fc6b..21269f4ce7 100644 --- a/src/node/services/log.ts +++ b/src/node/services/log.ts @@ -191,7 +191,7 @@ function ensureSinkOpen(): void { try { const logsDir = getXumLogsDir(); - const activeLogPath = path.join(logsDir, "mux.log"); + const activeLogPath = path.join(logsDir, "xum.log"); fs.mkdirSync(logsDir, { recursive: true }); @@ -225,10 +225,10 @@ function rotateSink(): void { const logsDir = path.dirname(openSink.path); - // Shift: mux.3.log → deleted, mux.2.log → mux.3.log, etc. + // Shift: xum.3.log → deleted, xum.2.log → xum.3.log, etc. for (let i = MAX_LOG_FILES; i >= 1; i--) { - const from = path.join(logsDir, i === 1 ? "mux.log" : `mux.${i - 1}.log`); - const to = path.join(logsDir, `mux.${i}.log`); + const from = path.join(logsDir, i === 1 ? "xum.log" : `xum.${i - 1}.log`); + const to = path.join(logsDir, `xum.${i}.log`); try { fs.renameSync(from, to); } catch { @@ -275,12 +275,12 @@ function writeSink(cleanLineWithNewline: string): void { } export function getLogFilePath(): string { - return path.join(getXumLogsDir(), "mux.log"); + return path.join(getXumLogsDir(), "xum.log"); } function clearSink(): Promise { const logsDir = getXumLogsDir(); - const activeLogPath = path.join(logsDir, "mux.log"); + const activeLogPath = path.join(logsDir, "xum.log"); const openSink = fileSinkState.status === "open" ? fileSinkState : null; const transitionEpoch = sinkLifecycleEpoch; @@ -302,7 +302,7 @@ function clearSink(): Promise { // Remove rotated files — missing files are fine. for (let i = 1; i <= MAX_LOG_FILES; i++) { - const rotatedPath = path.join(logsDir, `mux.${i}.log`); + const rotatedPath = path.join(logsDir, `xum.${i}.log`); try { fs.unlinkSync(rotatedPath); } catch { @@ -749,8 +749,8 @@ function createLogger(boundFields?: LogFields): Logger { * Default levels: * - CLI mode: error (quiet by default) * - Desktop mode: info - * - MUX_DEBUG=1: debug - * - MUX_LOG_LEVEL=: explicit override + * - XUM_DEBUG=1: debug (legacy MUX_DEBUG is accepted) + * - XUM_LOG_LEVEL=: explicit override (legacy MUX_LOG_LEVEL is accepted) * * Use log.withFields({ workspaceId }) to create a sub-logger that * automatically includes fields in every log entry. From 77e91db208803e5a2467254b9404f7a19399258a Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 13:00:41 +0500 Subject: [PATCH 05/13] docs: finish visible Xum branding --- .design-sync/conventions.md | 16 +- .design-sync/previews/PRLinkBadge.tsx | 2 +- Dockerfile | 2 +- README.md | 4 +- dev-app-update.yml | 2 +- docs/docs.json | 2 +- docs/getting-started/mux-gateway.mdx | 2 +- docs/guides/github-actions.mdx | 4 +- docs/index.mdx | 2 +- docs/install.mdx | 10 +- docs/integrations/vscode-extension.mdx | 4 +- docs/reference/telemetry.mdx | 10 +- flake.nix | 2 +- .../PRLinkBadge/PRLinkBadge.stories.tsx | 6 +- .../PRLinkBadge/PRLinkBadge.test.ts | 2 +- .../PRStackBadge/PRStackBadge.stories.tsx | 10 +- .../PRStackBadge/PRStackBadge.test.tsx | 8 +- src/browser/features/About/AboutDialog.tsx | 2 +- .../Settings/Sections/InstructionsSection.tsx | 2 +- src/browser/hooks/useDesktopTitlebar.ts | 2 +- .../stores/PRStatusStore.stack.test.ts | 8 +- .../stories/App.phoneViewports.stories.tsx | 2 +- .../utils/highlighting/shiki-shared.test.ts | 4 +- src/cli/index.ts | 2 +- src/desktop/updater.test.ts | 2 +- src/desktop/updater.ts | 4 +- src/node/acp/agent.ts | 2 +- src/node/builtinSkills/xum-docs.md | 2 +- src/node/compat/xumTransition.test.ts | 214 ++++++++++-------- .../builtInSkillContent.generated.ts | 36 +-- src/node/services/tools/web_fetch.ts | 2 +- tests/e2e/scenarios/terminal.spec.ts | 2 +- tests/ipc/runtime/runtimeExecuteBash.test.ts | 2 +- vscode/.vscode/tasks.json | 2 +- vscode/CHANGELOG.md | 2 +- vscode/Makefile | 10 +- vscode/README.md | 6 +- vscode/esbuild.config.js | 13 +- vscode/media/xumChatView.js | 35 ++- vscode/package.json | 2 +- .../api/orpcConnection.integration.test.ts | 2 +- vscode/src/webview/webview.css | 3 +- 42 files changed, 236 insertions(+), 215 deletions(-) diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md index 51823408b7..da51a34d33 100644 --- a/.design-sync/conventions.md +++ b/.design-sync/conventions.md @@ -1,4 +1,4 @@ -# Mux design system — how to build with it +# Xum design system — how to build with it These components are Mux's real, compiled React components (the desktop app's UI: chat tool-call cards, message states, settings sections, banners, modals, small @@ -30,14 +30,14 @@ There are **no CSS-module class maps**; style layout with Tailwind utility class built from Mux's semantic color tokens (NOT raw hex). Real families (all in the shipped stylesheet): -| Purpose | Utilities | -| ----------- | -------------------------------------------------------------------------------------------- | -| Surfaces | `bg-background`, `bg-background-secondary`, `bg-surface-primary`, `bg-surface-secondary` | -| Text | `text-foreground`, `text-muted-foreground`, `text-content-primary`, `text-content-secondary` | -| Borders | `border-border` | -| Accent | `bg-accent`, `text-accent` | +| Purpose | Utilities | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Surfaces | `bg-background`, `bg-background-secondary`, `bg-surface-primary`, `bg-surface-secondary` | +| Text | `text-foreground`, `text-muted-foreground`, `text-content-primary`, `text-content-secondary` | +| Borders | `border-border` | +| Accent | `bg-accent`, `text-accent` | | Agent modes | `text-plan-mode` / `bg-plan-mode` — same for `edit` / `exec` / `thinking` / `task`. `ask` & `debug` ship only as `--color--mode` tokens (no utility): use `style={{ color: "var(--color-ask-mode)" }}` | -| Radius | `rounded-md` | +| Radius | `rounded-md` | For a token Tailwind doesn't expose as a utility, reference it directly: `style={{ color: "var(--color-content-primary)" }}`. Token names live in the diff --git a/.design-sync/previews/PRLinkBadge.tsx b/.design-sync/previews/PRLinkBadge.tsx index 6c39159532..0b14650367 100644 --- a/.design-sync/previews/PRLinkBadge.tsx +++ b/.design-sync/previews/PRLinkBadge.tsx @@ -8,7 +8,7 @@ import type { GitHubPRLinkWithStatus } from "@/common/types/links"; // PRLinkBadge only needs theme + tooltip from the shell. const PR_LINK: GitHubPRLinkWithStatus = { type: "github-pr", - url: "https://github.com/coder/mux/pull/1623", + url: "https://github.com/coder/xum/pull/1623", owner: "coder", repo: "mux", number: 1623, diff --git a/Dockerfile b/Dockerfile index afed988ebd..4fef5d4310 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,7 +84,7 @@ FROM node:22-slim # OCI image metadata — allows registries (GHCR, Docker Hub) to link the image # back to the source repository and display version/description. ARG VERSION=dev -LABEL org.opencontainers.image.source="https://github.com/coder/mux" +LABEL org.opencontainers.image.source="https://github.com/coder/xum" LABEL org.opencontainers.image.version="${VERSION}" LABEL org.opencontainers.image.description="Xum server — parallel AI agent workflows" LABEL org.opencontainers.image.licenses="AGPL-3.0" diff --git a/README.md b/README.md index f5be5bca72..e7a4b8e85e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # Xum - Coding Agent Multiplexer -[![Download](https://img.shields.io/badge/Download-Releases-purple)](https://github.com/coder/mux/releases) +[![Download](https://img.shields.io/badge/Download-Releases-purple)](https://github.com/coder/xum/releases) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL%203.0-blue.svg)](LICENSE) [![Discord](https://img.shields.io/discord/1446553342699507907?logo=discord&label=Discord)](https://cdr.co/mux-discord) [![X (formerly Twitter)](https://img.shields.io/badge/Follow-%40codermux-black?logo=x)](https://x.com/codermux) @@ -40,7 +40,7 @@ like [opportunistic compaction](https://mux.coder.com/workspaces/compaction) and ## Install -Download pre-built binaries from [the releases page](https://github.com/coder/mux/releases) for +Download pre-built binaries from [the releases page](https://github.com/coder/xum/releases) for macOS and Linux. [More on installation →](https://mux.coder.com/install) diff --git a/dev-app-update.yml b/dev-app-update.yml index 663a4d7bff..a2ae7b8175 100644 --- a/dev-app-update.yml +++ b/dev-app-update.yml @@ -1,4 +1,4 @@ provider: github owner: coder -repo: mux +repo: xum releaseType: release diff --git a/docs/docs.json b/docs/docs.json index 2866285f75..97c17f6c04 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -132,7 +132,7 @@ ] }, "footerSocials": { - "github": "https://github.com/coder/mux" + "github": "https://github.com/coder/xum" }, "integrations": { "ga4": { diff --git a/docs/getting-started/mux-gateway.mdx b/docs/getting-started/mux-gateway.mdx index 5f5afbb13d..9c42adbc43 100644 --- a/docs/getting-started/mux-gateway.mdx +++ b/docs/getting-started/mux-gateway.mdx @@ -51,4 +51,4 @@ the Gateway website and the credits will be applied to your GitHub account. As Xum is in its early development stage, we highly value user feedback. Please let us know of any issues you encounter or feature requests in [our -tracker](https://github.com/coder/mux/issues). +tracker](https://github.com/coder/xum/issues). diff --git a/docs/guides/github-actions.mdx b/docs/guides/github-actions.mdx index f8f0ac4e56..d9ecd85f64 100644 --- a/docs/guides/github-actions.mdx +++ b/docs/guides/github-actions.mdx @@ -34,9 +34,9 @@ Here's a minimal example that runs Xum in a GitHub Action: ## Example: Auto-Cleanup Workflow -This is the exact workflow used live in the Xum repo (see [`.github/workflows/auto-cleanup.yml`](https://github.com/coder/mux/blob/main/.github/workflows/auto-cleanup.yml)). It runs periodically to identify low-risk cleanup opportunities and maintains a refactor PR with improvements. +This is the exact workflow used live in the Xum repo (see [`.github/workflows/auto-cleanup.yml`](https://github.com/coder/xum/blob/main/.github/workflows/auto-cleanup.yml)). It runs periodically to identify low-risk cleanup opportunities and maintains a refactor PR with improvements. -The prompt is stored in a separate file ([`.github/prompts/auto-cleanup.md`](https://github.com/coder/mux/blob/main/.github/prompts/auto-cleanup.md)) and piped via stdin, keeping the workflow file clean and the prompt easy to iterate on. +The prompt is stored in a separate file ([`.github/prompts/auto-cleanup.md`](https://github.com/coder/xum/blob/main/.github/prompts/auto-cleanup.md)) and piped via stdin, keeping the workflow file clean and the prompt easy to iterate on. Xum's repo uses a workflow-specific repository secret (`AUTO_CLEANUP_ANTHROPIC_API_KEY`) for this job so auto-cleanup spend stays isolated from terminal-bench, while the runtime environment still uses the standard `ANTHROPIC_API_KEY` name. diff --git a/docs/index.mdx b/docs/index.mdx index a43acaf6af..638f129695 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -45,6 +45,6 @@ Xum helps you work with multiple coding agents more effectively via: ## License -Xum is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://github.com/coder/mux/blob/main/LICENSE). +Xum is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://github.com/coder/xum/blob/main/LICENSE). Copyright (C) 2026 Coder Technologies, Inc. diff --git a/docs/install.mdx b/docs/install.mdx index 77245e38f4..f8e3b060fc 100644 --- a/docs/install.mdx +++ b/docs/install.mdx @@ -7,7 +7,7 @@ description: Download and install Xum for macOS, Linux, and Windows ### Release Builds - + Download pre-built binaries from the releases page. @@ -17,7 +17,7 @@ description: Download and install Xum for macOS, Linux, and Windows ### Development Builds -Download pre-built binaries of `main` from [GitHub Actions](https://github.com/coder/mux/actions/workflows/pr.yml?query=event:push+branch:main): +Download pre-built binaries of `main` from [GitHub Actions](https://github.com/coder/xum/actions/workflows/pr.yml?query=event:push+branch:main): - **macOS**: Signed and notarized DMG - `build-macos-x64` (Intel Macs) @@ -27,7 +27,7 @@ Download pre-built binaries of `main` from [GitHub Actions](https://github.com/c To download: -1. Go to the [PR workflow (main branch)](https://github.com/coder/mux/actions/workflows/pr.yml?query=event:push+branch:main) +1. Go to the [PR workflow (main branch)](https://github.com/coder/xum/actions/workflows/pr.yml?query=event:push+branch:main) 2. Click on the latest successful run 3. Scroll down to "Artifacts" section 4. Download the appropriate artifact for your platform @@ -60,14 +60,14 @@ Prerequisites: - Install **Git for Windows** (includes Git Bash). **WSL is not supported.** - Restart Xum after installing Git for Windows. -1. Download the installer exe from [releases](https://github.com/coder/mux/releases) (e.g., `xum-x.x.x-x64.exe`) +1. Download the installer exe from [releases](https://github.com/coder/xum/releases) (e.g., `xum-x.x.x-x64.exe`) 2. Run the installer 3. Follow the installation prompts 4. Launch Xum from the Start menu or desktop shortcut Windows support is currently in alpha. Please [report any - issues](https://github.com/coder/mux/issues) you encounter. + issues](https://github.com/coder/xum/issues) you encounter. ### Testing Pre-Release Builds diff --git a/docs/integrations/vscode-extension.mdx b/docs/integrations/vscode-extension.mdx index 27eeeda968..ee5eca0cb1 100644 --- a/docs/integrations/vscode-extension.mdx +++ b/docs/integrations/vscode-extension.mdx @@ -36,13 +36,13 @@ You can find it in the **Secondary Sidebar** under the `xum` container: **Chat ( To send messages, Xum must be connected in server/API mode. -If you hit issues, please report them on the [Xum GitHub issues page](https://github.com/coder/mux/issues). +If you hit issues, please report them on the [Xum GitHub issues page](https://github.com/coder/xum/issues). ## Installation ### Download -Download the latest `.vsix` file from the [GitHub releases page](https://github.com/coder/mux/releases). +Download the latest `.vsix` file from the [GitHub releases page](https://github.com/coder/xum/releases). ### Install diff --git a/docs/reference/telemetry.mdx b/docs/reference/telemetry.mdx index 03fc8dfc91..1df8f7dfae 100644 --- a/docs/reference/telemetry.mdx +++ b/docs/reference/telemetry.mdx @@ -10,7 +10,7 @@ Xum collects anonymous usage telemetry to help improve the product. - **No personal information**: Xum does not collect usernames, project names, file paths, or code content. - **Random IDs only**: Only randomly generated workspace IDs are sent. - **No hashing**: Hashing is vulnerable to rainbow table attacks. -- **Transparent payload**: See exactly what is sent in [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts). +- **Transparent payload**: See exactly what is sent in [`src/common/telemetry/payload.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/payload.ts). ## What Xum tracks @@ -48,7 +48,7 @@ This disables telemetry collection at the backend level. ## Source code -- **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts) -- **Backend service**: [`src/node/services/telemetryService.ts`](https://github.com/coder/mux/blob/main/src/node/services/telemetryService.ts) -- **Frontend client**: [`src/common/telemetry/client.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/client.ts) -- **Privacy utilities**: [`src/common/telemetry/utils.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/utils.ts) +- **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/payload.ts) +- **Backend service**: [`src/node/services/telemetryService.ts`](https://github.com/coder/xum/blob/main/src/node/services/telemetryService.ts) +- **Frontend client**: [`src/common/telemetry/client.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/client.ts) +- **Privacy utilities**: [`src/common/telemetry/utils.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/utils.ts) diff --git a/flake.nix b/flake.nix index a5cec6295a..bffd70aeab 100644 --- a/flake.nix +++ b/flake.nix @@ -166,7 +166,7 @@ meta = with pkgs.lib; { description = "xum - coding agent multiplexer"; - homepage = "https://github.com/coder/mux"; + homepage = "https://github.com/coder/xum"; license = licenses.agpl3Only; platforms = platforms.linux ++ platforms.darwin; mainProgram = "xum"; diff --git a/src/browser/components/PRLinkBadge/PRLinkBadge.stories.tsx b/src/browser/components/PRLinkBadge/PRLinkBadge.stories.tsx index 9ab41f99d2..f0c4535cc3 100644 --- a/src/browser/components/PRLinkBadge/PRLinkBadge.stories.tsx +++ b/src/browser/components/PRLinkBadge/PRLinkBadge.stories.tsx @@ -31,7 +31,7 @@ function makePRLink( ): GitHubPRLinkWithStatus { return { type: "github-pr", - url: `https://github.com/coder/mux/pull/${number}`, + url: `https://github.com/coder/xum/pull/${number}`, owner: "coder", repo: "mux", number, @@ -145,8 +145,8 @@ export const LinksDropdownContext: Story = { const links = [ "https://docs.example.com/links", "https://api.example.com/v1/docs", - "https://github.com/coder/mux/issues/1500", - "https://github.com/coder/mux/actions/runs/12345", + "https://github.com/coder/xum/issues/1500", + "https://github.com/coder/xum/actions/runs/12345", ]; return ( diff --git a/src/browser/components/PRLinkBadge/PRLinkBadge.test.ts b/src/browser/components/PRLinkBadge/PRLinkBadge.test.ts index f4409a527a..cd5509de6f 100644 --- a/src/browser/components/PRLinkBadge/PRLinkBadge.test.ts +++ b/src/browser/components/PRLinkBadge/PRLinkBadge.test.ts @@ -6,7 +6,7 @@ import { getStatusColorClass, getTooltipContent } from "./PRLinkBadge"; function makePRLink(statusOverrides: Partial = {}): GitHubPRLinkWithStatus { return { type: "github-pr", - url: "https://github.com/coder/mux/pull/1", + url: "https://github.com/coder/xum/pull/1", owner: "coder", repo: "mux", number: 1, diff --git a/src/browser/components/PRStackBadge/PRStackBadge.stories.tsx b/src/browser/components/PRStackBadge/PRStackBadge.stories.tsx index e4ef869319..de5e9ae4fa 100644 --- a/src/browser/components/PRStackBadge/PRStackBadge.stories.tsx +++ b/src/browser/components/PRStackBadge/PRStackBadge.stories.tsx @@ -14,7 +14,7 @@ const STACK: WorkspaceStackInfo = { needsRebase: false, pr: { number: 28051, - url: "https://github.com/coder/mux/pull/28051", + url: "https://github.com/coder/xum/pull/28051", state: "MERGED", title: "feat: add the stack foundation", }, @@ -25,7 +25,7 @@ const STACK: WorkspaceStackInfo = { needsRebase: false, pr: { number: 28052, - url: "https://github.com/coder/mux/pull/28052", + url: "https://github.com/coder/xum/pull/28052", state: "OPEN", title: "feat: cache stack metadata for visible workspaces", }, @@ -36,7 +36,7 @@ const STACK: WorkspaceStackInfo = { needsRebase: false, pr: { number: 28053, - url: "https://github.com/coder/mux/pull/28053", + url: "https://github.com/coder/xum/pull/28053", state: "OPEN", title: "feat: add a pull request stack dropdown with long titles", isDraft: true, @@ -48,7 +48,7 @@ const STACK: WorkspaceStackInfo = { needsRebase: true, pr: { number: 28054, - url: "https://github.com/coder/mux/pull/28054", + url: "https://github.com/coder/xum/pull/28054", state: "OPEN", title: "fix: keep the stack menu inside narrow viewports", }, @@ -64,7 +64,7 @@ const STACK: WorkspaceStackInfo = { needsRebase: false, pr: { number: 28056, - url: "https://github.com/coder/mux/pull/28056", + url: "https://github.com/coder/xum/pull/28056", state: "QUEUED", title: "feat: finish stack awareness", }, diff --git a/src/browser/components/PRStackBadge/PRStackBadge.test.tsx b/src/browser/components/PRStackBadge/PRStackBadge.test.tsx index 503db59921..4c7ae5823a 100644 --- a/src/browser/components/PRStackBadge/PRStackBadge.test.tsx +++ b/src/browser/components/PRStackBadge/PRStackBadge.test.tsx @@ -14,7 +14,7 @@ const STACK: WorkspaceStackInfo = { needsRebase: false, pr: { number: 101, - url: "https://github.com/coder/mux/pull/101", + url: "https://github.com/coder/xum/pull/101", state: "MERGED", title: "First layer", }, @@ -30,7 +30,7 @@ const STACK: WorkspaceStackInfo = { needsRebase: true, pr: { number: 103, - url: "https://github.com/coder/mux/pull/103", + url: "https://github.com/coder/xum/pull/103", state: "OPEN", title: "Top layer", }, @@ -64,10 +64,10 @@ describe("PRStackBadge", () => { "mike/feat-b", "mike/feat-a", ]); - expect(rows[0].getAttribute("href")).toBe("https://github.com/coder/mux/pull/103"); + expect(rows[0].getAttribute("href")).toBe("https://github.com/coder/xum/pull/103"); expect(rows[1].tagName).toBe("DIV"); expect(rows[1].getAttribute("aria-current")).toBe("true"); - expect(rows[2].getAttribute("href")).toBe("https://github.com/coder/mux/pull/101"); + expect(rows[2].getAttribute("href")).toBe("https://github.com/coder/xum/pull/101"); const menuItems = view.getAllByRole("menuitem"); expect(menuItems[menuItems.length - 1]).toBe(view.getByTestId("stack-trunk-row")); diff --git a/src/browser/features/About/AboutDialog.tsx b/src/browser/features/About/AboutDialog.tsx index fc682f054f..b155b15223 100644 --- a/src/browser/features/About/AboutDialog.tsx +++ b/src/browser/features/About/AboutDialog.tsx @@ -374,7 +374,7 @@ export function AboutDialog() { )} {

Custom instructions are appended to the system prompt of every workspace in the selected - project. They are stored in ~/.mux/config.json (kept + project. They are stored in ~/.xum/config.json (kept out of source control).

diff --git a/src/browser/hooks/useDesktopTitlebar.ts b/src/browser/hooks/useDesktopTitlebar.ts index 45a84d8a1d..be144fcd5b 100644 --- a/src/browser/hooks/useDesktopTitlebar.ts +++ b/src/browser/hooks/useDesktopTitlebar.ts @@ -5,7 +5,7 @@ * 1. Drag regions for window dragging * 2. Insets for native window controls (traffic lights on mac, overlay on win/linux) * - * In browser/mux server mode, these are no-ops. + * In browser/Xum server mode, these are no-ops. * * ## Architecture * diff --git a/src/browser/stores/PRStatusStore.stack.test.ts b/src/browser/stores/PRStatusStore.stack.test.ts index 12f2efa96a..468e8431bc 100644 --- a/src/browser/stores/PRStatusStore.stack.test.ts +++ b/src/browser/stores/PRStatusStore.stack.test.ts @@ -17,7 +17,7 @@ const STACK_VIEW_FIXTURE = JSON.stringify({ needsRebase: false, pr: { number: 101, - url: "https://github.com/coder/mux/pull/101", + url: "https://github.com/coder/xum/pull/101", state: "OPEN", }, }, @@ -31,7 +31,7 @@ const STACK_VIEW_FIXTURE = JSON.stringify({ needsRebase: true, pr: { number: 102, - url: "https://github.com/coder/mux/pull/102", + url: "https://github.com/coder/xum/pull/102", state: "OPEN", }, }, @@ -62,7 +62,7 @@ describe("parseStackViewOutput", () => { needsRebase: false, pr: { number: 101, - url: "https://github.com/coder/mux/pull/101", + url: "https://github.com/coder/xum/pull/101", state: "OPEN", }, }, @@ -72,7 +72,7 @@ describe("parseStackViewOutput", () => { needsRebase: true, pr: { number: 102, - url: "https://github.com/coder/mux/pull/102", + url: "https://github.com/coder/xum/pull/102", state: "QUEUED", }, }, diff --git a/src/browser/stories/App.phoneViewports.stories.tsx b/src/browser/stories/App.phoneViewports.stories.tsx index 7ec8e039a8..19c71a50a4 100644 --- a/src/browser/stories/App.phoneViewports.stories.tsx +++ b/src/browser/stories/App.phoneViewports.stories.tsx @@ -103,7 +103,7 @@ index 1111111..2222222 100644 `; const TOUCH_REVIEW_IMMERSIVE_NUMSTAT = "2\t0\tsrc/mobile/review.tsx"; -const PR_LINK_URL = "https://github.com/coder/mux/pull/3753"; +const PR_LINK_URL = "https://github.com/coder/xum/pull/3753"; const PR_DETECTION_JSON = JSON.stringify({ number: 3753, url: PR_LINK_URL, diff --git a/src/browser/utils/highlighting/shiki-shared.test.ts b/src/browser/utils/highlighting/shiki-shared.test.ts index d97f9bbc36..5f5f2df4df 100644 --- a/src/browser/utils/highlighting/shiki-shared.test.ts +++ b/src/browser/utils/highlighting/shiki-shared.test.ts @@ -12,12 +12,12 @@ describe("mapToShikiLang", () => { describe("extractShikiLines", () => { test("removes trailing visually-empty Shiki line (e.g. )", () => { - const html = `

https://github.com/coder/mux/pull/new/chat-autocomplete-b24r
+    const html = `
https://github.com/coder/xum/pull/new/chat-autocomplete-b24r
 
 
`; expect(extractShikiLines(html)).toEqual([ - `https://github.com/coder/mux/pull/new/chat-autocomplete-b24r`, + `https://github.com/coder/xum/pull/new/chat-autocomplete-b24r`, ]); }); }); diff --git a/src/cli/index.ts b/src/cli/index.ts index ad6c2dea65..9d7a2097bb 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -126,7 +126,7 @@ function startCli(): void { if (!isCommandAvailable("desktop", env)) { console.error("The 'desktop' command requires Electron to be installed."); console.error("When installed via npm, use the packaged desktop app instead."); - console.error("Download from: https://github.com/coder/mux/releases"); + console.error("Download from: https://github.com/coder/xum/releases"); process.exit(1); } launchDesktop(); diff --git a/src/desktop/updater.test.ts b/src/desktop/updater.test.ts index d0fdaf01e2..82e759f40a 100644 --- a/src/desktop/updater.test.ts +++ b/src/desktop/updater.test.ts @@ -427,7 +427,7 @@ describe("UpdaterService", () => { describe("transient error backoff", () => { const missingNightlyManifestError = - 'Cannot find latest-mac.yml in the latest release artifacts (https://github.com/coder/mux/releases/download/v0.18.1-nightly.16/latest-mac.yml): HttpError: 404 "method: GET url: https://github.com/coder/mux/releases/download/v0.18.1-nightly.16/latest-mac.yml\n\nPlease double check that your authentication token is correct.\n" Headers: { ... }\n at createHttpError (/path/httpExecutor.ts:31:10)'; + 'Cannot find latest-mac.yml in the latest release artifacts (https://github.com/coder/xum/releases/download/v0.18.1-nightly.16/latest-mac.yml): HttpError: 404 "method: GET url: https://github.com/coder/xum/releases/download/v0.18.1-nightly.16/latest-mac.yml\n\nPlease double check that your authentication token is correct.\n" Headers: { ... }\n at createHttpError (/path/httpExecutor.ts:31:10)'; const nightlyManifestPendingMessage = "Update metadata isn't available yet. The latest release may still be publishing; please try again in a few minutes."; diff --git a/src/desktop/updater.ts b/src/desktop/updater.ts index 676c8b8330..ebe643cb04 100644 --- a/src/desktop/updater.ts +++ b/src/desktop/updater.ts @@ -21,8 +21,8 @@ function getGitHubRepo(): { owner: string; repo: string } { if (url) { // Matches github.com/owner/repo in URLs like: - // git+https://github.com/coder/mux.git - // https://github.com/coder/mux + // git+https://github.com/coder/xum.git + // https://github.com/coder/xum // git@github.com:coder/mux.git // git+https://github.com/acme/mux.desktop.git const match = /github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url); diff --git a/src/node/acp/agent.ts b/src/node/acp/agent.ts index 074aa23d4d..e4d5ee0c8d 100644 --- a/src/node/acp/agent.ts +++ b/src/node/acp/agent.ts @@ -676,7 +676,7 @@ export class XumAgent implements Agent { authenticate(_params: AuthenticateRequest): Promise { this.assertInitialized("authenticate"); - // Local mux server connections do not currently require ACP-level auth. + // Local Xum server connections do not currently require ACP-level auth. return Promise.resolve({}); } diff --git a/src/node/builtinSkills/xum-docs.md b/src/node/builtinSkills/xum-docs.md index c4bdb2c037..423834ed18 100644 --- a/src/node/builtinSkills/xum-docs.md +++ b/src/node/builtinSkills/xum-docs.md @@ -142,5 +142,5 @@ Use this skill when the user asks how xum works (workspaces, runtimes, agents, m ## Links -- **GitHub**: https://github.com/coder/mux +- **GitHub**: https://github.com/coder/xum - **Documentation**: https://mux.coder.com diff --git a/src/node/compat/xumTransition.test.ts b/src/node/compat/xumTransition.test.ts index d965a7be2a..1001cf9568 100644 --- a/src/node/compat/xumTransition.test.ts +++ b/src/node/compat/xumTransition.test.ts @@ -818,115 +818,129 @@ describe("initializeXumUserDataTransition", () => { } }); - test("does not merge or delete independent populated userData trees", async () => { - const appDataDir = await createTempDir(); - const canonicalPath = join(appDataDir, "xum"); - const muxPath = join(appDataDir, "mux"); - const productNamePath = join(appDataDir, "Mux"); - await fs.mkdir(canonicalPath); - await fs.mkdir(muxPath); - await fs.mkdir(productNamePath); - await fs.writeFile(join(canonicalPath, "from-xum"), "new", "utf8"); - await fs.writeFile(join(muxPath, "from-mux"), "old", "utf8"); - await fs.writeFile(join(productNamePath, "from-Mux"), "older", "utf8"); - - const result = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); - - expect(result.status).toBe("conflict"); - expect(await fs.readFile(join(canonicalPath, "from-xum"), "utf8")).toBe("new"); - expect(await fs.readFile(join(muxPath, "from-mux"), "utf8")).toBe("old"); - expect(await fs.readFile(join(productNamePath, "from-Mux"), "utf8")).toBe("older"); - expect((await fs.lstat(muxPath)).isDirectory()).toBe(true); - expect((await fs.lstat(productNamePath)).isDirectory()).toBe(true); - }); - - test("quarantines obstructing userData files and recovers a persistent canonical directory", async () => { - const appDataDir = await createTempDir(); - const canonicalPath = join(appDataDir, "xum"); - const muxPath = join(appDataDir, "mux"); - const productNamePath = join(appDataDir, "Mux"); - await fs.writeFile(canonicalPath, "canonical-bytes", "utf8"); - await fs.writeFile(muxPath, "mux-bytes", "utf8"); - await fs.writeFile(productNamePath, "Mux-bytes", "utf8"); - - const first = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + test.skipIf(process.platform !== "linux")( + "does not merge or delete independent populated userData trees", + async () => { + const appDataDir = await createTempDir(); + const canonicalPath = join(appDataDir, "xum"); + const muxPath = join(appDataDir, "mux"); + const productNamePath = join(appDataDir, "Mux"); + await fs.mkdir(canonicalPath); + await fs.mkdir(muxPath); + await fs.mkdir(productNamePath); + await fs.writeFile(join(canonicalPath, "from-xum"), "new", "utf8"); + await fs.writeFile(join(muxPath, "from-mux"), "old", "utf8"); + await fs.writeFile(join(productNamePath, "from-Mux"), "older", "utf8"); - expect(first.status).toBe("canonical"); - expect(first.activePath).toBe(canonicalPath); - expect((await fs.stat(first.activePath)).isDirectory()).toBe(true); - expect(await fs.realpath(muxPath)).toBe(await fs.realpath(canonicalPath)); - expect(await fs.realpath(productNamePath)).toBe(await fs.realpath(canonicalPath)); - expect(await fs.readFile((await listQuarantineBackups(appDataDir, "xum"))[0], "utf8")).toBe( - "canonical-bytes" - ); - expect(await fs.readFile((await listQuarantineBackups(appDataDir, "mux"))[0], "utf8")).toBe( - "mux-bytes" - ); - expect(await fs.readFile((await listQuarantineBackups(appDataDir, "Mux"))[0], "utf8")).toBe( - "Mux-bytes" - ); + const result = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); - const second = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); - expect(second.activePath).toBe(first.activePath); - expect((await fs.stat(second.activePath)).isDirectory()).toBe(true); - expect(await fs.readFile((await listQuarantineBackups(appDataDir, "xum"))[0], "utf8")).toBe( - "canonical-bytes" - ); - }); + expect(result.status).toBe("conflict"); + expect(await fs.readFile(join(canonicalPath, "from-xum"), "utf8")).toBe("new"); + expect(await fs.readFile(join(muxPath, "from-mux"), "utf8")).toBe("old"); + expect(await fs.readFile(join(productNamePath, "from-Mux"), "utf8")).toBe("older"); + expect((await fs.lstat(muxPath)).isDirectory()).toBe(true); + expect((await fs.lstat(productNamePath)).isDirectory()).toBe(true); + } + ); - test("quarantines broken userData aliases and recovers a persistent canonical directory", async () => { - const appDataDir = await createTempDir(); - const canonicalPath = join(appDataDir, "xum"); - const muxPath = join(appDataDir, "mux"); - const productNamePath = join(appDataDir, "Mux"); - const missingCanonical = join(appDataDir, "missing-canonical"); - const missingMux = join(appDataDir, "missing-mux"); - const missingMuxName = join(appDataDir, "missing-Mux"); - await fs.symlink(missingCanonical, canonicalPath); - await fs.symlink(missingMux, muxPath); - await fs.symlink(missingMuxName, productNamePath); + test.skipIf(process.platform !== "linux")( + "quarantines obstructing userData files and recovers a persistent canonical directory", + async () => { + const appDataDir = await createTempDir(); + const canonicalPath = join(appDataDir, "xum"); + const muxPath = join(appDataDir, "mux"); + const productNamePath = join(appDataDir, "Mux"); + await fs.writeFile(canonicalPath, "canonical-bytes", "utf8"); + await fs.writeFile(muxPath, "mux-bytes", "utf8"); + await fs.writeFile(productNamePath, "Mux-bytes", "utf8"); + + const first = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + + expect(first.status).toBe("canonical"); + expect(first.activePath).toBe(canonicalPath); + expect((await fs.stat(first.activePath)).isDirectory()).toBe(true); + expect(await fs.realpath(muxPath)).toBe(await fs.realpath(canonicalPath)); + expect(await fs.realpath(productNamePath)).toBe(await fs.realpath(canonicalPath)); + expect(await fs.readFile((await listQuarantineBackups(appDataDir, "xum"))[0], "utf8")).toBe( + "canonical-bytes" + ); + expect(await fs.readFile((await listQuarantineBackups(appDataDir, "mux"))[0], "utf8")).toBe( + "mux-bytes" + ); + expect(await fs.readFile((await listQuarantineBackups(appDataDir, "Mux"))[0], "utf8")).toBe( + "Mux-bytes" + ); - const first = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + const second = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + expect(second.activePath).toBe(first.activePath); + expect((await fs.stat(second.activePath)).isDirectory()).toBe(true); + expect(await fs.readFile((await listQuarantineBackups(appDataDir, "xum"))[0], "utf8")).toBe( + "canonical-bytes" + ); + } + ); - expect(first.status).toBe("canonical"); - expect(first.activePath).toBe(canonicalPath); - expect((await fs.stat(first.activePath)).isDirectory()).toBe(true); - expect(await fs.realpath(muxPath)).toBe(await fs.realpath(canonicalPath)); - expect(await fs.readlink((await listQuarantineBackups(appDataDir, "xum"))[0])).toBe( - missingCanonical - ); - expect(await fs.readlink((await listQuarantineBackups(appDataDir, "mux"))[0])).toBe(missingMux); - expect(await fs.readlink((await listQuarantineBackups(appDataDir, "Mux"))[0])).toBe( - missingMuxName - ); + test.skipIf(process.platform !== "linux")( + "quarantines broken userData aliases and recovers a persistent canonical directory", + async () => { + const appDataDir = await createTempDir(); + const canonicalPath = join(appDataDir, "xum"); + const muxPath = join(appDataDir, "mux"); + const productNamePath = join(appDataDir, "Mux"); + const missingCanonical = join(appDataDir, "missing-canonical"); + const missingMux = join(appDataDir, "missing-mux"); + const missingMuxName = join(appDataDir, "missing-Mux"); + await fs.symlink(missingCanonical, canonicalPath); + await fs.symlink(missingMux, muxPath); + await fs.symlink(missingMuxName, productNamePath); + + const first = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + + expect(first.status).toBe("canonical"); + expect(first.activePath).toBe(canonicalPath); + expect((await fs.stat(first.activePath)).isDirectory()).toBe(true); + expect(await fs.realpath(muxPath)).toBe(await fs.realpath(canonicalPath)); + expect(await fs.readlink((await listQuarantineBackups(appDataDir, "xum"))[0])).toBe( + missingCanonical + ); + expect(await fs.readlink((await listQuarantineBackups(appDataDir, "mux"))[0])).toBe( + missingMux + ); + expect(await fs.readlink((await listQuarantineBackups(appDataDir, "Mux"))[0])).toBe( + missingMuxName + ); - const second = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); - expect(second.activePath).toBe(first.activePath); - expect((await fs.stat(second.activePath)).isDirectory()).toBe(true); - expect(await fs.readlink((await listQuarantineBackups(appDataDir, "xum"))[0])).toBe( - missingCanonical - ); - }); + const second = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + expect(second.activePath).toBe(first.activePath); + expect((await fs.stat(second.activePath)).isDirectory()).toBe(true); + expect(await fs.readlink((await listQuarantineBackups(appDataDir, "xum"))[0])).toBe( + missingCanonical + ); + } + ); - test("prefers a healthy mux userData tree when the canonical entry is a regular file", async () => { - const appDataDir = await createTempDir(); - const canonicalPath = join(appDataDir, "xum"); - const legacyPath = join(appDataDir, "mux"); - await fs.writeFile(canonicalPath, "not-a-directory", "utf8"); - await fs.mkdir(legacyPath); - await fs.writeFile(join(legacyPath, "window-state.json"), "{}", "utf8"); + test.skipIf(process.platform !== "linux")( + "prefers a healthy mux userData tree when the canonical entry is a regular file", + async () => { + const appDataDir = await createTempDir(); + const canonicalPath = join(appDataDir, "xum"); + const legacyPath = join(appDataDir, "mux"); + await fs.writeFile(canonicalPath, "not-a-directory", "utf8"); + await fs.mkdir(legacyPath); + await fs.writeFile(join(legacyPath, "window-state.json"), "{}", "utf8"); - const result = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); + const result = await initializeXumUserDataTransition({ appDataDir, platform: "linux" }); - expect(result.status).toBe("legacy-fallback"); - expect(result.activePath).toBe(legacyPath); - expect(result.canonicalPath).toBe(canonicalPath); - expect(result.issues.length).toBeGreaterThan(0); - expect((await fs.lstat(canonicalPath)).isFile()).toBe(true); - expect(await fs.readFile(join(legacyPath, "window-state.json"), "utf8")).toBe("{}"); - expect((await fs.lstat(legacyPath)).isDirectory()).toBe(true); - await expectMissingPath(join(appDataDir, "Mux")); - }); + expect(result.status).toBe("legacy-fallback"); + expect(result.activePath).toBe(legacyPath); + expect(result.canonicalPath).toBe(canonicalPath); + expect(result.issues.length).toBeGreaterThan(0); + expect((await fs.lstat(canonicalPath)).isFile()).toBe(true); + expect(await fs.readFile(join(legacyPath, "window-state.json"), "utf8")).toBe("{}"); + expect((await fs.lstat(legacyPath)).isDirectory()).toBe(true); + await expectMissingPath(join(appDataDir, "Mux")); + } + ); test("targets the lowercase slug even when the Electron app name is display-cased", async () => { const appDataDir = await createTempDir(); diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 74dd53ef93..416211cad5 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -5123,7 +5123,7 @@ export const BUILTIN_SKILL_FILES: Record> = { " ]", " },", ' "footerSocials": {', - ' "github": "https://github.com/coder/mux"', + ' "github": "https://github.com/coder/xum"', " },", ' "integrations": {', ' "ga4": {', @@ -5229,7 +5229,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "As Xum is in its early development stage, we highly value user feedback. Please", "let us know of any issues you encounter or feature requests in [our", - "tracker](https://github.com/coder/mux/issues).", + "tracker](https://github.com/coder/xum/issues).", "", ].join("\n"), "references/docs/getting-started/why-parallelize.mdx": [ @@ -5287,9 +5287,9 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "## Example: Auto-Cleanup Workflow", "", - "This is the exact workflow used live in the Xum repo (see [`.github/workflows/auto-cleanup.yml`](https://github.com/coder/mux/blob/main/.github/workflows/auto-cleanup.yml)). It runs periodically to identify low-risk cleanup opportunities and maintains a refactor PR with improvements.", + "This is the exact workflow used live in the Xum repo (see [`.github/workflows/auto-cleanup.yml`](https://github.com/coder/xum/blob/main/.github/workflows/auto-cleanup.yml)). It runs periodically to identify low-risk cleanup opportunities and maintains a refactor PR with improvements.", "", - "The prompt is stored in a separate file ([`.github/prompts/auto-cleanup.md`](https://github.com/coder/mux/blob/main/.github/prompts/auto-cleanup.md)) and piped via stdin, keeping the workflow file clean and the prompt easy to iterate on.", + "The prompt is stored in a separate file ([`.github/prompts/auto-cleanup.md`](https://github.com/coder/xum/blob/main/.github/prompts/auto-cleanup.md)) and piped via stdin, keeping the workflow file clean and the prompt easy to iterate on.", "", "Xum's repo uses a workflow-specific repository secret (`AUTO_CLEANUP_ANTHROPIC_API_KEY`) for this job so auto-cleanup spend stays isolated from terminal-bench, while the runtime environment still uses the standard `ANTHROPIC_API_KEY` name.", "", @@ -6501,7 +6501,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "## License", "", - "Xum is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://github.com/coder/mux/blob/main/LICENSE).", + "Xum is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://github.com/coder/xum/blob/main/LICENSE).", "", "Copyright (C) 2026 Coder Technologies, Inc.", "", @@ -6516,7 +6516,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "### Release Builds", "", - '', + '', " Download pre-built binaries from the releases page.", "", "", @@ -6526,7 +6526,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "### Development Builds", "", - "Download pre-built binaries of `main` from [GitHub Actions](https://github.com/coder/mux/actions/workflows/pr.yml?query=event:push+branch:main):", + "Download pre-built binaries of `main` from [GitHub Actions](https://github.com/coder/xum/actions/workflows/pr.yml?query=event:push+branch:main):", "", "- **macOS**: Signed and notarized DMG", " - `build-macos-x64` (Intel Macs)", @@ -6536,7 +6536,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "To download:", "", - "1. Go to the [PR workflow (main branch)](https://github.com/coder/mux/actions/workflows/pr.yml?query=event:push+branch:main)", + "1. Go to the [PR workflow (main branch)](https://github.com/coder/xum/actions/workflows/pr.yml?query=event:push+branch:main)", "2. Click on the latest successful run", '3. Scroll down to "Artifacts" section', "4. Download the appropriate artifact for your platform", @@ -6569,14 +6569,14 @@ export const BUILTIN_SKILL_FILES: Record> = { "- Install **Git for Windows** (includes Git Bash). **WSL is not supported.**", "- Restart Xum after installing Git for Windows.", "", - "1. Download the installer exe from [releases](https://github.com/coder/mux/releases) (e.g., `xum-x.x.x-x64.exe`)", + "1. Download the installer exe from [releases](https://github.com/coder/xum/releases) (e.g., `xum-x.x.x-x64.exe`)", "2. Run the installer", "3. Follow the installation prompts", "4. Launch Xum from the Start menu or desktop shortcut", "", "", " Windows support is currently in alpha. Please [report any", - " issues](https://github.com/coder/mux/issues) you encounter.", + " issues](https://github.com/coder/xum/issues) you encounter.", "", "", "### Testing Pre-Release Builds", @@ -6831,13 +6831,13 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "To send messages, Xum must be connected in server/API mode.", "", - "If you hit issues, please report them on the [Xum GitHub issues page](https://github.com/coder/mux/issues).", + "If you hit issues, please report them on the [Xum GitHub issues page](https://github.com/coder/xum/issues).", "", "## Installation", "", "### Download", "", - "Download the latest `.vsix` file from the [GitHub releases page](https://github.com/coder/mux/releases).", + "Download the latest `.vsix` file from the [GitHub releases page](https://github.com/coder/xum/releases).", "", "### Install", "", @@ -7529,7 +7529,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "- **No personal information**: Xum does not collect usernames, project names, file paths, or code content.", "- **Random IDs only**: Only randomly generated workspace IDs are sent.", "- **No hashing**: Hashing is vulnerable to rainbow table attacks.", - "- **Transparent payload**: See exactly what is sent in [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts).", + "- **Transparent payload**: See exactly what is sent in [`src/common/telemetry/payload.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/payload.ts).", "", "## What Xum tracks", "", @@ -7567,10 +7567,10 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "## Source code", "", - "- **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts)", - "- **Backend service**: [`src/node/services/telemetryService.ts`](https://github.com/coder/mux/blob/main/src/node/services/telemetryService.ts)", - "- **Frontend client**: [`src/common/telemetry/client.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/client.ts)", - "- **Privacy utilities**: [`src/common/telemetry/utils.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/utils.ts)", + "- **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/payload.ts)", + "- **Backend service**: [`src/node/services/telemetryService.ts`](https://github.com/coder/xum/blob/main/src/node/services/telemetryService.ts)", + "- **Frontend client**: [`src/common/telemetry/client.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/client.ts)", + "- **Privacy utilities**: [`src/common/telemetry/utils.ts`](https://github.com/coder/xum/blob/main/src/common/telemetry/utils.ts)", "", ].join("\n"), "references/docs/runtime/coder.mdx": [ @@ -8520,7 +8520,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "## Links", "", - "- **GitHub**: https://github.com/coder/mux", + "- **GitHub**: https://github.com/coder/xum", "- **Documentation**: https://mux.coder.com", "", ].join("\n"), diff --git a/src/node/services/tools/web_fetch.ts b/src/node/services/tools/web_fetch.ts index db27d3cc83..715a59c27e 100644 --- a/src/node/services/tools/web_fetch.ts +++ b/src/node/services/tools/web_fetch.ts @@ -16,7 +16,7 @@ import { EXIT_CODE_TIMEOUT } from "@/common/constants/exitCodes"; import * as runtimeHelpers from "@/node/utils/runtime/helpers"; import { getErrorMessage } from "@/common/utils/errors"; -const USER_AGENT = "Xum/1.0 (https://github.com/coder/mux; web-fetch tool)"; +const USER_AGENT = "Xum/1.0 (https://github.com/coder/xum; web-fetch tool)"; const WEB_FETCH_MAX_REDIRECTS = 10; const WEB_FETCH_RESOLVE_TIMEOUT_SECS = 5; const WEB_FETCH_RUNTIME_TIMEOUT_GRACE_SECS = 1; diff --git a/tests/e2e/scenarios/terminal.spec.ts b/tests/e2e/scenarios/terminal.spec.ts index 6098d8ed97..b8088fe808 100644 --- a/tests/e2e/scenarios/terminal.spec.ts +++ b/tests/e2e/scenarios/terminal.spec.ts @@ -36,7 +36,7 @@ test("terminal tab handles workspace switching", async ({ ui, page: _page }) => }); /** - * Regression test for: https://github.com/coder/mux/pull/1586 + * Regression test for: https://github.com/coder/xum/pull/1586 * * The bug: attachCustomKeyEventHandler in TerminalView.tsx had inverted return values. * ghostty-web's API expects: diff --git a/tests/ipc/runtime/runtimeExecuteBash.test.ts b/tests/ipc/runtime/runtimeExecuteBash.test.ts index 9129257ed4..8b149ac077 100644 --- a/tests/ipc/runtime/runtimeExecuteBash.test.ts +++ b/tests/ipc/runtime/runtimeExecuteBash.test.ts @@ -319,7 +319,7 @@ describeIntegration("Runtime Bash Execution", () => { try { // Test command that pipes a file through a stdin-reading command (grep) // This would hang forever if stdin.close() was used instead of stdin.abort() - // Regression test for: https://github.com/coder/mux/issues/503 + // Regression test for: https://github.com/coder/xum/issues/503 const events = await sendMessageAndWait( env, workspaceId, diff --git a/vscode/.vscode/tasks.json b/vscode/.vscode/tasks.json index 4a74c561b3..a7b8fe2c7e 100644 --- a/vscode/.vscode/tasks.json +++ b/vscode/.vscode/tasks.json @@ -19,7 +19,7 @@ "background": { "activeOnStart": true, "beginsPattern": ".+", - "endsPattern": "mux VS Code extension: watching for changes\\.\\.\\." + "endsPattern": "Xum VS Code extension: watching for changes\\.\\.\\." } }, "presentation": { diff --git a/vscode/CHANGELOG.md b/vscode/CHANGELOG.md index c6e376b762..cce0efc1ba 100644 --- a/vscode/CHANGELOG.md +++ b/vscode/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to the "mux" extension will be documented in this file. ### Added - Initial release -- Command to open mux workspaces from VS Code and Cursor +- Command to open Xum workspaces from VS Code and Cursor - Support for local workspaces - Support for SSH workspaces via Remote-SSH extension - Automatically detects VS Code Remote-SSH (`ms-vscode-remote.remote-ssh`) diff --git a/vscode/Makefile b/vscode/Makefile index 24fc429ec4..be198df25f 100644 --- a/vscode/Makefile +++ b/vscode/Makefile @@ -1,6 +1,6 @@ # VS Code Extension Build System # ============================== -# Isolated build system for the mux VS Code/Cursor extension +# Isolated build system for the Xum VS Code/Cursor extension .PHONY: all build install clean test test-integration test-orpc help @@ -50,12 +50,12 @@ install: build ## Build and install extension locally test: node_modules/.installed ## Run extension unit tests @bun test -## Run extension integration tests (requires running mux server) -test-integration: node_modules/.installed ## Run extension integration tests (requires mux server) +## Run extension integration tests (requires running Xum server) +test-integration: node_modules/.installed ## Run extension integration tests (requires Xum server) @TEST_INTEGRATION=1 bun test -## Run oRPC connection smoke test (requires running mux server) -test-orpc: ## Connect to mux server and list workspaces via oRPC +## Run oRPC connection smoke test (requires running Xum server) +test-orpc: ## Connect to Xum server and list workspaces via oRPC @TEST_INTEGRATION=1 bun test ./src/api/orpcConnection.integration.test.ts ## Clean build artifacts diff --git a/vscode/README.md b/vscode/README.md index 4a2b4b4413..effff9e552 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -1,10 +1,10 @@ -# mux VS Code Extension +# Xum VS Code Extension -Open [mux](https://mux.coder.com) workspaces from VS Code or Cursor. +Open [Xum](https://mux.coder.com) workspaces from VS Code or Cursor. ## Installation -Download the latest `.vsix` from [mux releases](https://github.com/coder/mux/releases) and install: +Download the latest `.vsix` from [Xum releases](https://github.com/coder/xum/releases) and install: ```bash code --install-extension mux-0.1.0.vsix diff --git a/vscode/esbuild.config.js b/vscode/esbuild.config.js index f557c6cf77..0853152b40 100644 --- a/vscode/esbuild.config.js +++ b/vscode/esbuild.config.js @@ -11,13 +11,7 @@ function resolveXumImport(subpath) { const base = path.resolve(__dirname, "..", "src", subpath); // Prefer explicit source extensions. - const candidates = [ - `${base}.ts`, - `${base}.tsx`, - `${base}.js`, - `${base}.jsx`, - `${base}.json`, - ]; + const candidates = [`${base}.ts`, `${base}.tsx`, `${base}.js`, `${base}.jsx`, `${base}.json`]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { @@ -66,7 +60,6 @@ function ensureOutDir() { let webviewCssBuildPromise = null; - function copySetiFont() { const src = path.resolve(__dirname, "..", "public", "seti.woff"); const dest = path.resolve(__dirname, "out", "seti.woff"); @@ -104,8 +97,6 @@ function copyKatexAssets() { } } - - function buildWebviewCss() { if (webviewCssBuildPromise) { return webviewCssBuildPromise; @@ -284,7 +275,7 @@ async function main() { // Keep process alive. // eslint-disable-next-line no-console - console.log("mux VS Code extension: watching for changes..."); + console.log("Xum VS Code extension: watching for changes..."); return; } diff --git a/vscode/media/xumChatView.js b/vscode/media/xumChatView.js index 5bd144187f..4ffb1a53dc 100644 --- a/vscode/media/xumChatView.js +++ b/vscode/media/xumChatView.js @@ -10,7 +10,8 @@ (function () { "use strict"; - const traceId = (document.body && document.body.dataset && document.body.dataset.muxTraceId) || "unknown"; + const traceId = + (document.body && document.body.dataset && document.body.dataset.muxTraceId) || "unknown"; const startedAtMs = Date.now(); const statusEl = document.getElementById("status"); @@ -139,7 +140,9 @@ openBtn.disabled = !hasSelection; } - const canChat = Boolean(state.connectionStatus && state.connectionStatus.mode === "api" && hasSelection); + const canChat = Boolean( + state.connectionStatus && state.connectionStatus.mode === "api" && hasSelection + ); if (sendBtn) { sendBtn.disabled = !canChat; @@ -150,8 +153,8 @@ inputEl.placeholder = canChat ? "Message mux…" : hasSelection - ? "Chat requires mux server connection." - : "Select a mux workspace to chat."; + ? "Chat requires Xum server connection." + : "Select an Xum workspace to chat."; } } @@ -166,7 +169,8 @@ const placeholder = document.createElement("option"); placeholder.value = ""; - placeholder.textContent = state.workspaces.length > 0 ? "Select workspace…" : "No workspaces found"; + placeholder.textContent = + state.workspaces.length > 0 ? "Select workspace…" : "No workspaces found"; workspaceSelectEl.appendChild(placeholder); for (const ws of state.workspaces) { @@ -185,7 +189,7 @@ const parts = []; if (status.mode === "api") { - parts.push("Connected to mux server"); + parts.push("Connected to Xum server"); if (status.baseUrl) { parts.push(status.baseUrl); } @@ -315,7 +319,12 @@ // --- Error handlers window.addEventListener("error", (ev) => { - appendDebug("window.error", { message: ev.message, filename: ev.filename, lineno: ev.lineno, colno: ev.colno }); + appendDebug("window.error", { + message: ev.message, + filename: ev.filename, + lineno: ev.lineno, + colno: ev.colno, + }); postToExtension({ type: "debugLog", message: "window.error", @@ -356,7 +365,11 @@ clearInterval(readyInterval); appendDebug("handshake complete", { reason, attempts: readyAttempts }); - postToExtension({ type: "debugLog", message: "handshake complete", data: { reason, attempts: readyAttempts } }); + postToExtension({ + type: "debugLog", + message: "handshake complete", + data: { reason, attempts: readyAttempts }, + }); } // Initial ready @@ -383,7 +396,11 @@ return; } - if (msg.type === "connectionStatus" || msg.type === "workspaces" || msg.type === "setSelectedWorkspace") { + if ( + msg.type === "connectionStatus" || + msg.type === "workspaces" || + msg.type === "setSelectedWorkspace" + ) { markHandshakeComplete(msg.type); } diff --git a/vscode/package.json b/vscode/package.json index c04ea87bf6..ab30c1c3be 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -101,7 +101,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/coder/mux.git", + "url": "https://github.com/coder/xum.git", "directory": "vscode" }, "license": "AGPL-3.0-only", diff --git a/vscode/src/api/orpcConnection.integration.test.ts b/vscode/src/api/orpcConnection.integration.test.ts index 11057c240f..8477722084 100644 --- a/vscode/src/api/orpcConnection.integration.test.ts +++ b/vscode/src/api/orpcConnection.integration.test.ts @@ -19,7 +19,7 @@ integrationTestOrSkip( assert( lock, - `No running mux server found (missing/stale lockfile at ${lockfile.getLockPath()}). ` + + `No running Xum server found (missing/stale lockfile at ${lockfile.getLockPath()}). ` + `Start mux and re-run with TEST_INTEGRATION=1.` ); diff --git a/vscode/src/webview/webview.css b/vscode/src/webview/webview.css index 376992456f..a89cd514b5 100644 --- a/vscode/src/webview/webview.css +++ b/vscode/src/webview/webview.css @@ -13,7 +13,7 @@ @source "../../../src/browser/**/*.{ts,tsx}"; /* - * Minimal CSS bundle for the mux VS Code webview. + * Minimal CSS bundle for the Xum VS Code webview. * * This intentionally reuses mux's existing design tokens and markdown/code styles so * the transcript matches the desktop app as closely as possible. @@ -403,7 +403,6 @@ border-color: var(--surface-task-divider); } - html, body { padding: 0; From 6a47e86cd70f2ff78b8aa462ea141cea20a4ff6f Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 13:03:37 +0500 Subject: [PATCH 06/13] test: use canonical Xum trust paths --- src/cli/trust.test.ts | 34 +++++++++++++++--------------- src/node/services/systemMessage.ts | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/cli/trust.test.ts b/src/cli/trust.test.ts index 02bec5459c..0b71b5d0b3 100644 --- a/src/cli/trust.test.ts +++ b/src/cli/trust.test.ts @@ -17,19 +17,19 @@ describe("xum trust CLI", () => { await fs.mkdir(nested, { recursive: true }); await Bun.$`git init`.cwd(repo).quiet(); - expect(await resolveProjectDir({ cwd: nested })).toBe(repo); + expect(await resolveProjectDir({ cwd: nested })).toBe(await fs.realpath(repo)); expect(await resolveProjectDir({ cwd: tmp.path, explicitDir: nested })).toBe(nested); }); test("grants and revokes project trust headlessly", async () => { using tmp = new DisposableTempDir("trust-cli-cycle"); const repo = path.join(tmp.path, "repo"); - const muxRoot = path.join(tmp.path, "mux-root"); + const xumRoot = path.join(tmp.path, "xum-root"); await fs.mkdir(repo, { recursive: true }); - await fs.mkdir(muxRoot, { recursive: true }); - const env = { ...process.env, MUX_ROOT: muxRoot }; + await fs.mkdir(xumRoot, { recursive: true }); + const env = { ...process.env, XUM_ROOT: xumRoot }; - // Grant trust for a project that was never added to mux (no desktop/server + // Grant trust for a project that was never added to Xum (no desktop/server // involved). Route through index.ts to cover top-level subcommand dispatch; // no experiment flag is required for trust. const trustResult = await Bun.$`${BUN_EXECUTABLE} ${INDEX_ENTRY} trust --dir ${repo} --json` @@ -55,10 +55,10 @@ describe("xum trust CLI", () => { using tmp = new DisposableTempDir("trust-cli-worktree-revoke"); const base = await fs.realpath(tmp.path); const repo = path.join(base, "repo"); - const muxRoot = path.join(base, "mux-root"); + const xumRoot = path.join(base, "xum-root"); const worktree = path.join(base, "worktree"); await fs.mkdir(repo, { recursive: true }); - await fs.mkdir(muxRoot, { recursive: true }); + await fs.mkdir(xumRoot, { recursive: true }); await Bun.$`git init`.cwd(repo).quiet(); await Bun.$`git config user.email dogfood@example.com`.cwd(repo).quiet(); await Bun.$`git config user.name Dogfood`.cwd(repo).quiet(); @@ -72,7 +72,7 @@ describe("xum trust CLI", () => { // Revoke must clear both; the direct entry alone would keep the checkout // trusted via resolveProjectTrusted's exact-path lookup. await fs.writeFile( - path.join(muxRoot, "config.json"), + path.join(xumRoot, "config.json"), JSON.stringify({ projects: [ [repo, { workspaces: [], trusted: true }], @@ -81,7 +81,7 @@ describe("xum trust CLI", () => { }), "utf-8" ); - const env = { ...process.env, MUX_ROOT: muxRoot }; + const env = { ...process.env, XUM_ROOT: xumRoot }; const revokeResult = await Bun.$`${BUN_EXECUTABLE} ${TRUST_ENTRY} --revoke --dir ${worktree} --json` @@ -89,7 +89,7 @@ describe("xum trust CLI", () => { .quiet(); expect(revokeResult.exitCode).toBe(0); - const config = JSON.parse(await fs.readFile(path.join(muxRoot, "config.json"), "utf-8")) as { + const config = JSON.parse(await fs.readFile(path.join(xumRoot, "config.json"), "utf-8")) as { projects: Array<[string, { trusted?: boolean }]>; }; const trustByPath = new Map(config.projects.map(([p, c]) => [p, c.trusted])); @@ -101,14 +101,14 @@ describe("xum trust CLI", () => { using tmp = new DisposableTempDir("trust-cli-unwritable"); const repo = path.join(tmp.path, "repo"); await fs.mkdir(repo, { recursive: true }); - // MUX_ROOT pointing at a regular file makes config.json unwritable; + // XUM_ROOT pointing at a regular file makes config.json unwritable; // Config.saveConfig swallows the write error, so only the post-write // verification can surface the failure. - const muxRootFile = path.join(tmp.path, "mux-root-file"); - await fs.writeFile(muxRootFile, "not a directory\n", "utf-8"); + const xumRootFile = path.join(tmp.path, "xum-root-file"); + await fs.writeFile(xumRootFile, "not a directory\n", "utf-8"); const result = await Bun.$`${BUN_EXECUTABLE} ${TRUST_ENTRY} --dir ${repo} --json` - .env({ ...process.env, MUX_ROOT: muxRootFile }) + .env({ ...process.env, XUM_ROOT: xumRootFile }) .nothrow() .quiet(); @@ -123,10 +123,10 @@ describe("xum trust CLI", () => { // entry written to config must match what trust resolution compares against. const base = await fs.realpath(tmp.path); const repo = path.join(base, "repo"); - const muxRoot = path.join(base, "mux-root"); + const xumRoot = path.join(base, "xum-root"); const worktree = path.join(base, "worktree"); await fs.mkdir(repo, { recursive: true }); - await fs.mkdir(muxRoot, { recursive: true }); + await fs.mkdir(xumRoot, { recursive: true }); await Bun.$`git init`.cwd(repo).quiet(); await Bun.$`git config user.email dogfood@example.com`.cwd(repo).quiet(); await Bun.$`git config user.name Dogfood`.cwd(repo).quiet(); @@ -136,7 +136,7 @@ describe("xum trust CLI", () => { await Bun.$`git worktree add ${worktree} -b feature`.cwd(repo).quiet(); const trustResult = await Bun.$`${BUN_EXECUTABLE} ${TRUST_ENTRY} --dir ${worktree} --json` - .env({ ...process.env, MUX_ROOT: muxRoot }) + .env({ ...process.env, XUM_ROOT: xumRoot }) .quiet(); expect(trustResult.exitCode).toBe(0); // Trust must land on the main repository path, not the ephemeral worktree path. diff --git a/src/node/services/systemMessage.ts b/src/node/services/systemMessage.ts index cdd65f992e..dcacf2de92 100644 --- a/src/node/services/systemMessage.ts +++ b/src/node/services/systemMessage.ts @@ -477,7 +477,7 @@ function deriveSubProjectRelativePath(projectPath: string, subProjectPath: strin * @param metadata - Workspace metadata (contains projectPath) * @param runtime - Runtime for reading workspace files (supports SSH) * @param workspacePath - Workspace directory path - * @param projectConfigs - Project configs from ~/.mux/config.json for per-project customInstructions + * @param projectConfigs - Project configs from ~/.xum/config.json for per-project customInstructions * @param claudeSkillsCompatEnabled - Whether to include ~/.claude/CLAUDE.md before native globals * @returns Structured instruction sources (ordered global and context entries) */ @@ -612,7 +612,7 @@ export async function buildSystemMessage( */ modes?: readonly string[]; /** - * Project configs from ~/.mux/config.json, used to append per-project + * Project configs from ~/.xum/config.json, used to append per-project * `customInstructions` (Settings → Instructions) to the prompt. */ projectConfigs?: Map; From 4a6489ff6fdd58b08c6a662b59f23d2f43c52f7d Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 13:57:32 +0500 Subject: [PATCH 07/13] chore: use exec form for Docker healthcheck --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4fef5d4310..92fe4f716c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -141,8 +141,9 @@ ENV MUX_ROOT=/root/.mux EXPOSE 3000 # Health check +# Exec form avoids invoking a shell and preserves the JavaScript probe as one argument. HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD node -e "fetch('http://localhost:3000/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" + CMD ["node", "-e", "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] # Run bundled xum server # --host 0.0.0.0: bind to all interfaces (required for Docker networking) From c5043ef444818281772af05896f613fc4a674ae3 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 13:58:32 +0500 Subject: [PATCH 08/13] fix: export benchmark compatibility variables --- benchmarks/terminal_bench/xum-run.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/benchmarks/terminal_bench/xum-run.sh b/benchmarks/terminal_bench/xum-run.sh index 2d608406f6..5189204381 100644 --- a/benchmarks/terminal_bench/xum-run.sh +++ b/benchmarks/terminal_bench/xum-run.sh @@ -25,8 +25,7 @@ for suffix in APP_ROOT CONFIG_ROOT ROOT PROJECT_PATH PROJECT_CANDIDATES MODEL TI canonical_name="XUM_${suffix}" legacy_name="MUX_${suffix}" if [[ ! -v "${canonical_name}" && -v "${legacy_name}" ]]; then - printf -v "${canonical_name}" '%s' "${!legacy_name}" - export "${canonical_name}" + export "${canonical_name}=${!legacy_name}" fi done From c93dcda778e8e9aace46f4de9cdb784a51ddb288 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 14:18:18 +0500 Subject: [PATCH 09/13] fix: preserve benchmark rename compatibility --- .../prepare_leaderboard_submission.py | 8 ++++++-- benchmarks/terminal_bench/xum_agent.py | 8 +++++++- benchmarks/terminal_bench/xum_agent_test.py | 15 +++++++++++++++ scripts/upload-harbor-results.py | 15 +++++++++++---- scripts/upload-tbench-results.py | 15 +++++++++++---- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/benchmarks/terminal_bench/prepare_leaderboard_submission.py b/benchmarks/terminal_bench/prepare_leaderboard_submission.py index 4faa486654..3127180799 100755 --- a/benchmarks/terminal_bench/prepare_leaderboard_submission.py +++ b/benchmarks/terminal_bench/prepare_leaderboard_submission.py @@ -379,8 +379,12 @@ def prepare_submission( trial_src, dest_trial_dir, ignore=shutil.ignore_patterns( - "xum-app.tar.gz", # Large agent binary (~5MB each) - "xum-tokens.json", # Token usage (not needed for leaderboard) + # Exclude both generations because submissions may combine new + # trials with artifacts downloaded from pre-rename runs. + "xum-app.tar.gz", + "mux-app.tar.gz", + "xum-tokens.json", + "mux-tokens.json", "*.log", # Log files trigger HF LFS and cause upload timeouts ), ) diff --git a/benchmarks/terminal_bench/xum_agent.py b/benchmarks/terminal_bench/xum_agent.py index 356e1ff19b..17a332ed20 100644 --- a/benchmarks/terminal_bench/xum_agent.py +++ b/benchmarks/terminal_bench/xum_agent.py @@ -152,7 +152,13 @@ def _env(self) -> dict[str, str]: for suffix in self._CONFIG_ENV_SUFFIXES: canonical_key = f"XUM_{suffix}" legacy_key = f"MUX_{suffix}" - value = os.environ.get(canonical_key) or os.environ.get(legacy_key) + # Presence, not truthiness, determines precedence: an explicit empty + # canonical value must clear a legacy variable left in the developer shell. + value = ( + os.environ[canonical_key] + if canonical_key in os.environ + else os.environ.get(legacy_key) + ) if value: env[canonical_key] = value diff --git a/benchmarks/terminal_bench/xum_agent_test.py b/benchmarks/terminal_bench/xum_agent_test.py index 288ac81861..530d2fdd96 100644 --- a/benchmarks/terminal_bench/xum_agent_test.py +++ b/benchmarks/terminal_bench/xum_agent_test.py @@ -174,6 +174,21 @@ def test_canonical_environment_wins_over_legacy_alias( assert env["XUM_MODEL"] == "anthropic:claude-sonnet-4-5" +def test_empty_canonical_environment_clears_legacy_alias( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", str(_repo_root())) + monkeypatch.setenv("XUM_RUN_ARGS", "") + monkeypatch.setenv("MUX_RUN_ARGS", "--goal keep-this-from-leaking") + monkeypatch.setenv("XUM_RUN_AS_GOAL", "") + monkeypatch.setenv("MUX_RUN_AS_GOAL", "1") + + env = XumAgent(logs_dir=tmp_path)._env + + assert "XUM_RUN_ARGS" not in env + assert "XUM_RUN_AS_GOAL" not in env + + def test_goal_mode_env_is_forwarded( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/scripts/upload-harbor-results.py b/scripts/upload-harbor-results.py index 878d465171..26b439bc3e 100644 --- a/scripts/upload-harbor-results.py +++ b/scripts/upload-harbor-results.py @@ -44,9 +44,16 @@ def load_json(path: Path) -> dict | None: return None -def env_flag(name: str) -> bool: +def env_value(canonical_name: str, legacy_name: str) -> str | None: + """Prefer a canonical env key by presence, with a pre-rename fallback.""" + if canonical_name in os.environ: + return os.environ[canonical_name] + return os.environ.get(legacy_name) + + +def env_flag(canonical_name: str, legacy_name: str) -> bool: """Return True for the env boolean spellings emitted by workflows.""" - return (os.environ.get(name) or "").strip().lower() in {"1", "true"} + return (env_value(canonical_name, legacy_name) or "").strip().lower() in {"1", "true"} def extract_trial_score(trial_result: dict) -> float | None: @@ -189,8 +196,8 @@ def build_rows(job_folder: Path) -> list[dict]: if dataset is None: dataset = job_config.get("dataset") - experiments = os.environ.get("MUX_EXPERIMENTS") - mux_run_as_goal = env_flag("MUX_RUN_AS_GOAL") + experiments = env_value("XUM_EXPERIMENTS", "MUX_EXPERIMENTS") + mux_run_as_goal = env_flag("XUM_RUN_AS_GOAL", "MUX_RUN_AS_GOAL") # Raw JSON for future-proofing run_result_json = json.dumps(job_result) if job_result else None diff --git a/scripts/upload-tbench-results.py b/scripts/upload-tbench-results.py index 549a95f82b..45694ea516 100755 --- a/scripts/upload-tbench-results.py +++ b/scripts/upload-tbench-results.py @@ -42,9 +42,16 @@ def load_json(path: Path) -> dict | None: return None -def env_flag(name: str) -> bool: +def env_value(canonical_name: str, legacy_name: str) -> str | None: + """Prefer a canonical env key by presence, with a pre-rename fallback.""" + if canonical_name in os.environ: + return os.environ[canonical_name] + return os.environ.get(legacy_name) + + +def env_flag(canonical_name: str, legacy_name: str) -> bool: """Return True for the env boolean spellings emitted by workflows.""" - return (os.environ.get(name) or "").strip().lower() in {"1", "true"} + return (env_value(canonical_name, legacy_name) or "").strip().lower() in {"1", "true"} def extract_thinking_from_config(config: dict) -> str | None: @@ -200,8 +207,8 @@ def build_rows(job_folder: Path) -> list[dict]: if dataset is None: dataset = job_config.get("dataset") - experiments = os.environ.get("MUX_EXPERIMENTS") - mux_run_as_goal = env_flag("MUX_RUN_AS_GOAL") + experiments = env_value("XUM_EXPERIMENTS", "MUX_EXPERIMENTS") + mux_run_as_goal = env_flag("XUM_RUN_AS_GOAL", "MUX_RUN_AS_GOAL") # Raw JSON for future-proofing run_result_json = json.dumps(job_result) if job_result else None From 74edb2682d1cbee1bf9df06d03aaf2779af977fc Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 22 Aug 2026 14:22:19 +0500 Subject: [PATCH 10/13] fix: allow legacy leaderboard artifact filters --- scripts/audit_xum_branding.py | 5 +++++ scripts/audit_xum_branding_test.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/scripts/audit_xum_branding.py b/scripts/audit_xum_branding.py index bac14b95ec..d6c024bf63 100644 --- a/scripts/audit_xum_branding.py +++ b/scripts/audit_xum_branding.py @@ -70,6 +70,11 @@ class AllowRule: re.compile(r"MUX_[A-Z0-9_]+|MuxAgent|mux_agent|mux-run\.sh"), "compatibility behavior coverage", ), + AllowRule( + "benchmarks/terminal_bench/prepare_leaderboard_submission.py", + re.compile(r"[\"']mux-(?:app\.tar\.gz|tokens\.json)[\"']"), + "pre-rename trial artifacts must remain excluded from mixed leaderboard submissions", + ), AllowRule("benchmarks/terminal_bench/xum-run.sh", re.compile(r"MUX_(?:\*|[A-Z0-9_]+|\$?\{)"), "legacy runner environment input"), AllowRule("benchmarks/terminal_bench/xum_setup.sh.j2", re.compile(r"MUX_(?:\*|[A-Z0-9_]+|\$?\{)"), "legacy setup environment input"), AllowRule( diff --git a/scripts/audit_xum_branding_test.py b/scripts/audit_xum_branding_test.py index f353a12684..ccf1344846 100644 --- a/scripts/audit_xum_branding_test.py +++ b/scripts/audit_xum_branding_test.py @@ -42,12 +42,17 @@ def test_accepts_reviewed_compatibility_and_ignores_generated_history(self) -> N ) history_path.parent.mkdir(parents=True) history_path.write_text('{"agent": "Mux"}\n') + leaderboard_script = root / "benchmarks/terminal_bench/prepare_leaderboard_submission.py" + leaderboard_script.write_text( + 'ignore_patterns("mux-app.tar.gz", "mux-tokens.json")\n' + ) violations = audit_paths( root, [ "Makefile", "benchmarks/terminal_bench/.leaderboard_cache/Mux__Historical/result.json", + "benchmarks/terminal_bench/prepare_leaderboard_submission.py", ], ) From e199f37f99c3e9e3479f413c6a762efb5b087ba5 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 24 Aug 2026 09:37:37 +0500 Subject: [PATCH 11/13] fix: preserve MuxMessage public type alias --- src/common/types/message.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/types/message.ts b/src/common/types/message.ts index 8742611566..5626bee817 100644 --- a/src/common/types/message.ts +++ b/src/common/types/message.ts @@ -875,6 +875,10 @@ export type XumMessage = Omit, "parts"> & { parts: Array; }; +// @coder/mux-chat-components is a separately published compatibility package. Keep +// its public type name stable while Xum-owned code uses the canonical identifier. +export type MuxMessage = XumMessage; + // DisplayedMessage represents a single UI message block // This is what the UI components consume, splitting complex messages into separate visual blocks export type DisplayedMessage = From 8cac1f750497a775bc8bb33f5934f8111d974d08 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 24 Aug 2026 09:48:34 +0500 Subject: [PATCH 12/13] fix: honor empty canonical providers override --- benchmarks/terminal_bench/xum_agent.py | 20 ++++++++++++-------- benchmarks/terminal_bench/xum_agent_test.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/benchmarks/terminal_bench/xum_agent.py b/benchmarks/terminal_bench/xum_agent.py index 17a332ed20..13af8925f4 100644 --- a/benchmarks/terminal_bench/xum_agent.py +++ b/benchmarks/terminal_bench/xum_agent.py @@ -140,6 +140,13 @@ def __init__( def name() -> str: return "xum" + @staticmethod + def _environment_alias_value(canonical_key: str, legacy_key: str) -> str | None: + """Prefer the canonical key by presence so an empty value clears legacy state.""" + if canonical_key in os.environ: + return os.environ[canonical_key] + return os.environ.get(legacy_key) + @property def _env(self) -> dict[str, str]: env: dict[str, str] = {} @@ -154,11 +161,7 @@ def _env(self) -> dict[str, str]: legacy_key = f"MUX_{suffix}" # Presence, not truthiness, determines precedence: an explicit empty # canonical value must clear a legacy variable left in the developer shell. - value = ( - os.environ[canonical_key] - if canonical_key in os.environ - else os.environ.get(legacy_key) - ) + value = self._environment_alias_value(canonical_key, legacy_key) if value: env[canonical_key] = value @@ -257,9 +260,10 @@ async def _stage_providers_config( self, environment: BaseEnvironment, env: dict[str, str] ) -> None: """Upload host providers.jsonc into the sandbox when explicitly requested.""" - providers_file_raw = os.environ.get( - self._PROVIDERS_FILE_ENV_KEY - ) or os.environ.get(self._LEGACY_PROVIDERS_FILE_ENV_KEY) + providers_file_raw = self._environment_alias_value( + self._PROVIDERS_FILE_ENV_KEY, + self._LEGACY_PROVIDERS_FILE_ENV_KEY, + ) if not providers_file_raw: return diff --git a/benchmarks/terminal_bench/xum_agent_test.py b/benchmarks/terminal_bench/xum_agent_test.py index 530d2fdd96..196d56d81b 100644 --- a/benchmarks/terminal_bench/xum_agent_test.py +++ b/benchmarks/terminal_bench/xum_agent_test.py @@ -189,6 +189,21 @@ def test_empty_canonical_environment_clears_legacy_alias( assert "XUM_RUN_AS_GOAL" not in env +def test_empty_canonical_providers_file_clears_legacy_alias( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("XUM_PROVIDERS_FILE", "") + monkeypatch.setenv("MUX_PROVIDERS_FILE", "/legacy/providers.jsonc") + + assert ( + XumAgent._environment_alias_value( + XumAgent._PROVIDERS_FILE_ENV_KEY, + XumAgent._LEGACY_PROVIDERS_FILE_ENV_KEY, + ) + == "" + ) + + def test_goal_mode_env_is_forwarded( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: From dfc29c0b0588deb42b8c3ec21905fbc94f406da5 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 24 Aug 2026 09:56:02 +0500 Subject: [PATCH 13/13] fix: honor empty canonical benchmark repo root --- benchmarks/terminal_bench/xum_agent.py | 5 +++-- benchmarks/terminal_bench/xum_agent_test.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/benchmarks/terminal_bench/xum_agent.py b/benchmarks/terminal_bench/xum_agent.py index 13af8925f4..a44c46741e 100644 --- a/benchmarks/terminal_bench/xum_agent.py +++ b/benchmarks/terminal_bench/xum_agent.py @@ -114,8 +114,9 @@ def __init__( if self._timeout_sec is not None else None ) - repo_root_env = os.environ.get("XUM_AGENT_REPO_ROOT") or os.environ.get( - "MUX_AGENT_REPO_ROOT" + repo_root_env = self._environment_alias_value( + "XUM_AGENT_REPO_ROOT", + "MUX_AGENT_REPO_ROOT", ) repo_root = ( Path(repo_root_env).resolve() diff --git a/benchmarks/terminal_bench/xum_agent_test.py b/benchmarks/terminal_bench/xum_agent_test.py index 196d56d81b..a2cc4407d8 100644 --- a/benchmarks/terminal_bench/xum_agent_test.py +++ b/benchmarks/terminal_bench/xum_agent_test.py @@ -174,6 +174,18 @@ def test_canonical_environment_wins_over_legacy_alias( assert env["XUM_MODEL"] == "anthropic:claude-sonnet-4-5" +def test_empty_canonical_repo_root_clears_legacy_alias( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + stale_legacy_root = tmp_path / "missing-legacy-checkout" + monkeypatch.setenv("XUM_AGENT_REPO_ROOT", "") + monkeypatch.setenv("MUX_AGENT_REPO_ROOT", str(stale_legacy_root)) + + agent = XumAgent(logs_dir=tmp_path / "logs") + + assert agent._repo_root == _repo_root() + + def test_empty_canonical_environment_clears_legacy_alias( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: