-
Notifications
You must be signed in to change notification settings - Fork 300
perf(webview): stop task history globalState writes #1664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
f1a0dd5
40450b1
0b1702b
e99bc5c
d2efbfd
f48797b
6be655a
b92f0a3
720f1e9
9a58047
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -243,8 +243,6 @@ | |
| private recentTasksCache?: string[] | ||
| public readonly taskHistoryStore: TaskHistoryStore | ||
| private taskHistoryStoreInitialized = false | ||
| private globalStateWriteThroughTimer: ReturnType<typeof setTimeout> | null = null | ||
| private static readonly GLOBAL_STATE_WRITE_THROUGH_DEBOUNCE_MS = 5000 // 5 seconds | ||
| public static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds | ||
| private providerProfileMutationQueue = Promise.resolve() | ||
| private historyTaskCreationQueue = Promise.resolve() | ||
|
|
@@ -344,14 +342,8 @@ | |
| this.mdmService = mdmService | ||
| void this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES) | ||
|
|
||
| // Initialize the per-task file-based history store. | ||
| // The globalState write-through is debounced separately (not on every mutation) | ||
| // since per-task files are authoritative and globalState is only for downgrade compat. | ||
| this.taskHistoryStore = new TaskHistoryStore(this.contextProxy.globalStorageUri.fsPath, { | ||
| onWrite: async () => { | ||
| this.scheduleGlobalStateWriteThrough() | ||
| }, | ||
| }) | ||
| // Initialize the authoritative per-task file-based history store. | ||
| this.taskHistoryStore = new TaskHistoryStore(this.contextProxy.globalStorageUri.fsPath) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stops new writes, but the old
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @PierrunoYT - after reviewing the recent changes, lets revert this request, and instead file a separate ticket for it, it might not be a simple addition for handling old blobs. |
||
| this.initializeTaskHistoryStore().catch((error) => { | ||
| this.log(`Failed to initialize TaskHistoryStore: ${error}`) | ||
| }) | ||
|
|
@@ -897,7 +889,6 @@ | |
| await this.marketplaceManager?.cleanup() | ||
| this.customModesManager?.dispose() | ||
| this.taskHistoryStore.dispose() | ||
| this.flushGlobalStateWriteThrough() | ||
| this.log("Disposed all disposables") | ||
| ClineProvider.activeInstances.delete(this) | ||
|
|
||
|
|
@@ -1743,9 +1734,7 @@ | |
|
|
||
| try { | ||
| // Update the task history with the new mode first. | ||
| const taskHistoryItem = | ||
| this.taskHistoryStore.get(task.taskId) ?? | ||
| (this.getGlobalState("taskHistory") ?? []).find((item) => item.id === task.taskId) | ||
| const taskHistoryItem = this.getTaskHistoryItem(task.taskId) | ||
|
|
||
| if (taskHistoryItem) { | ||
| await this.updateTaskHistory({ ...taskHistoryItem, mode: newMode }) | ||
|
|
@@ -1983,9 +1972,7 @@ | |
| // been persisted into taskHistory (it will be captured on the next save). | ||
| task.setTaskApiConfigName(apiConfigName) | ||
|
|
||
| const taskHistoryItem = | ||
| this.taskHistoryStore.get(task.taskId) ?? | ||
| (this.getGlobalState("taskHistory") ?? []).find((item) => item.id === task.taskId) | ||
| const taskHistoryItem = this.getTaskHistoryItem(task.taskId) | ||
|
|
||
| if (taskHistoryItem) { | ||
| await this.updateTaskHistory({ ...taskHistoryItem, apiConfigName }) | ||
|
|
@@ -2241,15 +2228,26 @@ | |
|
|
||
| // Task history | ||
|
|
||
| private getTaskHistoryItem(id: string): HistoryItem | undefined { | ||
| const historyItem = this.taskHistoryStore.get(id) | ||
|
|
||
| // Once initialization and migration succeed, the file-backed store is authoritative. | ||
| // Legacy global state is only a fallback while startup is incomplete or has failed. | ||
| if (historyItem || this.taskHistoryStoreInitialized) { | ||
| return historyItem | ||
| } | ||
|
|
||
| return (this.getGlobalState("taskHistory") ?? []).find((item) => item.id === id) | ||
|
Check warning on line 2240 in src/core/webview/ClineProvider.ts
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '2215,2260p' src/core/webview/ClineProvider.ts
sed -n '90,155p' src/core/task-persistence/TaskHistoryStore.ts
rg -n -C 6 'getTaskHistoryItem|getTaskWithId|taskHistoryStoreInitialized|taskHistory.*globalState|Task not found|legacy' src/core/webview/__tests__/ClineProvider.spec.ts src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts
find .. -name AGENTS.md -o -name .cursorrules -o -name CONTRIBUTING.md | head -30Repository: Zoo-Code-Org/Zoo-Code Length of output: 32022 🏁 Script executed: printf '%s\n' '--- focused test setup and cases ---'
sed -n '1,180p' src/core/webview/__tests__/ClineProvider.spec.ts
sed -n '4940,5055p' src/core/webview/__tests__/ClineProvider.spec.ts
printf '%s\n' '--- initialization flag and legacy-state references ---'
rg -n -C 5 'taskHistoryStoreInitialized|taskHistoryStore\s*=|getGlobalState\("taskHistory"\)|globalState\.get' src/core/webview/ClineProvider.ts src/core/webview/__tests__/ClineProvider.spec.ts
printf '%s\n' '--- repository instruction files in the checkout ---'
git ls-files '*AGENTS.md' '*CONTRIBUTING.md' '*cursorrules*' | head -80Repository: Zoo-Code-Org/Zoo-Code Length of output: 25058 🤖 get_repo_knowledge executed:
Length of output: 11446 🏁 Script executed: sed -n '1,180p' src/core/webview/__tests__/ClineProvider.spec.ts
sed -n '4940,5055p' src/core/webview/__tests__/ClineProvider.spec.ts
rg -n -C 5 'taskHistoryStoreInitialized|taskHistoryStore\s*=|getGlobalState\("taskHistory"\)|globalState\.get' src/core/webview/ClineProvider.ts src/core/webview/__tests__/ClineProvider.spec.ts
git ls-files '*AGENTS.md' '*CONTRIBUTING.md' '*cursorrules*' | head -80Repository: Zoo-Code-Org/Zoo-Code Length of output: 24912 Cover the cold-start legacy-history fallback.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| async getTaskWithId(id: string): Promise<{ | ||
| historyItem: HistoryItem | ||
| taskDirPath: string | ||
| apiConversationHistoryFilePath: string | ||
| uiMessagesFilePath: string | ||
| apiConversationHistory: Anthropic.MessageParam[] | ||
| }> { | ||
| const historyItem = | ||
| this.taskHistoryStore.get(id) ?? (this.getGlobalState("taskHistory") ?? []).find((item) => item.id === id) | ||
| const historyItem = this.getTaskHistoryItem(id) | ||
|
|
||
| if (!historyItem) { | ||
| throw new Error("Task not found") | ||
|
|
@@ -3134,44 +3132,6 @@ | |
| return history | ||
| } | ||
|
|
||
| /** | ||
| * Schedule a debounced write-through of task history to globalState. | ||
| * Only used for backward compatibility during the transition period. | ||
| * Per-task files are authoritative; globalState is the downgrade fallback. | ||
| */ | ||
| private scheduleGlobalStateWriteThrough(): void { | ||
| if (this.globalStateWriteThroughTimer) { | ||
| clearTimeout(this.globalStateWriteThroughTimer) | ||
| } | ||
|
|
||
| this.globalStateWriteThroughTimer = setTimeout(async () => { | ||
| this.globalStateWriteThroughTimer = null | ||
| try { | ||
| const items = this.taskHistoryStore.getAll() | ||
| await this.updateGlobalState("taskHistory", items) | ||
| } catch (err) { | ||
| this.log( | ||
| `[scheduleGlobalStateWriteThrough] Failed: ${err instanceof Error ? err.message : String(err)}`, | ||
| ) | ||
| } | ||
| }, ClineProvider.GLOBAL_STATE_WRITE_THROUGH_DEBOUNCE_MS) | ||
| } | ||
|
|
||
| /** | ||
| * Flush any pending debounced globalState write-through immediately. | ||
| */ | ||
| private flushGlobalStateWriteThrough(): void { | ||
| if (this.globalStateWriteThroughTimer) { | ||
| clearTimeout(this.globalStateWriteThroughTimer) | ||
| this.globalStateWriteThroughTimer = null | ||
| } | ||
|
|
||
| const items = this.taskHistoryStore.getAll() | ||
| this.updateGlobalState("taskHistory", items).catch((err) => { | ||
| this.log(`[flushGlobalStateWriteThrough] Failed: ${err instanceof Error ? err.message : String(err)}`) | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * Broadcasts a task history update to the webview. | ||
| * This sends a lightweight message with just the task history, rather than the full state. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -243,6 +243,7 @@ vi.mock("@roo-code/cloud", () => ({ | |
| getOrganizationMemberships: vi.fn().mockResolvedValue([]), | ||
| getUserSettings: vi.fn().mockReturnValue(null), | ||
| isTaskSyncEnabled: vi.fn().mockReturnValue(false), | ||
| off: vi.fn(), | ||
| } | ||
| }, | ||
| }, | ||
|
|
@@ -386,6 +387,32 @@ describe("ClineProvider Task History Synchronization", () => { | |
| return calls.filter((call) => call[0]?.type === type) | ||
| } | ||
|
|
||
| it("uses per-task files without registering a globalState write-through callback", () => { | ||
| expect(provider.taskHistoryStore["onWrite"]).toBeUndefined() | ||
| }) | ||
|
|
||
| it("does not write task history to globalState after a history mutation", async () => { | ||
| vi.mocked(mockContext.globalState.update).mockClear() | ||
|
|
||
| await provider.updateTaskHistory(createHistoryItem({ id: "file-backed-task", task: "File-backed task" }), { | ||
| broadcast: false, | ||
| }) | ||
|
|
||
| expect(mockContext.globalState.update).not.toHaveBeenCalledWith("taskHistory", expect.anything()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Assert that no
As per path instructions, “Require regression coverage at the lowest valid harness with behavior-focused assertions, including relevant negative, error, false/unset, and boundary cases.” Suggested assertion- expect(mockContext.globalState.update).not.toHaveBeenCalledWith("taskHistory", expect.anything())
+ expect(mockContext.globalState.update.mock.calls.filter(([key]) => key === "taskHistory")).toEqual([])Also applies to: 413-413 🤖 Prompt for AI AgentsSources: Path instructions, MCP tools |
||
| }) | ||
|
|
||
| it("does not write task history to globalState during disposal", async () => { | ||
| await provider.updateTaskHistory( | ||
| createHistoryItem({ id: "disposed-file-backed-task", task: "Disposed file-backed task" }), | ||
| { broadcast: false }, | ||
| ) | ||
| vi.mocked(mockContext.globalState.update).mockClear() | ||
|
|
||
| await provider.dispose() | ||
|
|
||
| expect(mockContext.globalState.update).not.toHaveBeenCalledWith("taskHistory", expect.anything()) | ||
| }) | ||
|
|
||
| describe("updateTaskHistory", () => { | ||
| it("broadcasts task history update by default", async () => { | ||
| await provider.resolveWebviewView(mockWebviewView) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.