diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index f317431..099042b 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -2965,9 +2965,64 @@ function normalizeState(raw: Partial | undefined): BossState { state.projects.unshift(base.projects[0]); } + removeLegacyBossConsoleArtifacts(state); return syncDerivedState(state); } +const LEGACY_BOSS_CONSOLE_PROJECT_ID = "boss-console"; +const LEGACY_BOSS_CONSOLE_PROJECT_NAME = "Boss 移动控制台"; + +function isLegacyBossConsoleRef(value?: string | null) { + const normalized = value?.trim(); + return ( + normalized === LEGACY_BOSS_CONSOLE_PROJECT_ID || + normalized === LEGACY_BOSS_CONSOLE_PROJECT_NAME + ); +} + +function removeLegacyBossConsoleArtifacts(state: BossState) { + state.projects = state.projects.filter((project) => !isLegacyBossConsoleRef(project.id)); + state.devices = state.devices.map((device) => ({ + ...device, + projects: device.projects.filter((project) => !isLegacyBossConsoleRef(project)), + })); + state.masterAgentMemories = state.masterAgentMemories.filter( + (memory) => !isLegacyBossConsoleRef(memory.projectId), + ); + state.userProjectAgentControls = state.userProjectAgentControls.filter( + (item) => !isLegacyBossConsoleRef(item.projectId), + ); + state.threadContextSnapshots = state.threadContextSnapshots.filter( + (snapshot) => !isLegacyBossConsoleRef(snapshot.projectId), + ); + state.threadHandoffPackages = state.threadHandoffPackages.filter( + (item) => !isLegacyBossConsoleRef(item.projectId), + ); + state.threadContextAlerts = state.threadContextAlerts.filter( + (item) => !isLegacyBossConsoleRef(item.projectId), + ); + state.opsFaults = state.opsFaults.filter((item) => !isLegacyBossConsoleRef(item.projectId)); + state.masterAgentTasks = state.masterAgentTasks.filter( + (task) => + !isLegacyBossConsoleRef(task.projectId) && + !isLegacyBossConsoleRef(task.targetProjectId) && + !isLegacyBossConsoleRef(task.projectUnderstandingTargetProjectId), + ); + state.dispatchPlans = state.dispatchPlans.filter( + (plan) => + !isLegacyBossConsoleRef(plan.groupProjectId) && + !(plan.targets ?? []).some((target) => isLegacyBossConsoleRef(target.projectId)) && + !(plan.confirmedTargetProjectIds ?? []).some((projectId) => + isLegacyBossConsoleRef(projectId), + ), + ); + state.dispatchExecutions = state.dispatchExecutions.filter( + (execution) => + !isLegacyBossConsoleRef(execution.groupProjectId) && + !isLegacyBossConsoleRef(execution.targetProjectId), + ); +} + function latestProjectTimestamp(state: BossState, projectId: string) { const project = state.projects.find((item) => item.id === projectId); const messageTimes = (project?.messages ?? []).map((message) => messageTimeValue(message.sentAt)); diff --git a/tests/conversation-home-items.test.ts b/tests/conversation-home-items.test.ts index 8344378..1db1101 100644 --- a/tests/conversation-home-items.test.ts +++ b/tests/conversation-home-items.test.ts @@ -2,7 +2,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import os from "node:os"; import path from "node:path"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; let runtimeRoot = ""; let readState: (typeof import("../src/lib/boss-data"))["readState"]; @@ -246,3 +246,71 @@ test("default seeded conversations no longer expose Boss 移动控制台", async false, ); }); + +test("readState migrates away persisted legacy boss-console conversations", async () => { + await setup(); + const state = await readState(); + + const legacyState = structuredClone(state) as typeof state; + legacyState.projects.push({ + id: "boss-console", + name: "Boss 移动控制台", + pinned: false, + systemPinned: false, + deviceIds: ["mac-studio"], + preview: "历史遗留的 boss-console 会话。", + updatedAt: "2026-04-04T12:20:00+08:00", + lastMessageAt: "2026-04-04T12:20:00+08:00", + isGroup: false, + threadMeta: { + projectId: "boss-console", + threadId: "thread-boss-console", + threadDisplayName: "Boss 移动控制台", + folderName: "Boss", + activityIconCount: 1, + updatedAt: "2026-04-04T12:20:00+08:00", + codexThreadRef: "thread-boss-console", + codexFolderRef: "boss-console", + }, + groupMembers: [], + createdByAgent: true, + collaborationMode: "development", + approvalState: "not_required", + unreadCount: 0, + riskLevel: "low", + messages: [], + goals: [], + versions: [], + }); + legacyState.threadContextSnapshots.push({ + snapshotId: "legacy-boss-console-snapshot", + projectId: "boss-console", + taskId: "legacy-task", + threadId: "thread-boss-console", + title: "Boss 移动控制台线程", + summary: "遗留摘要", + nodeId: "mac-studio", + workerId: "worker-legacy", + sourceKind: "worker_estimator", + status: "running", + contextBudgetRemainingPct: 48, + contextBudgetLevel: "watch", + mustFinishBeforeCompaction: false, + estimatedRemainingTurns: 5, + estimatedRemainingLargeMessages: 2, + compactionCount: 0, + patchPending: false, + testsPending: false, + evidencePending: false, + checklist: [], + capturedAt: "2026-04-04T12:20:00+08:00", + }); + + const stateFile = process.env.BOSS_STATE_FILE; + assert.ok(stateFile, "expected test state file path"); + await writeFile(stateFile, JSON.stringify(legacyState, null, 2), "utf8"); + + const migratedState = await readState(); + assert.equal(migratedState.projects.some((item) => item.id === "boss-console"), false); + assert.equal(migratedState.threadContextSnapshots.some((item) => item.projectId === "boss-console"), false); +});