From 236eba1ecf3bbbebc8d926de8c7d318afcbdb08d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:50:25 -0700 Subject: [PATCH] fix(persistence): folder, group and session-state store writes name their host or why they needn't (R4-2c) (#30797) * fix(persistence): session, automation-label writes take the workspace's host; exempt minted-unique folder and group ids (R4-2c) * fix(persistence): keep the session removal's spill rule; the twin guard was unreachable (review) --- config/id-only-store-write-baseline.txt | 7 ---- .../check-id-only-store-write-ratchet.mjs | 13 ++++++++ src/main/ipc/automations.ts | 14 ++++++-- src/main/persistence-automations.test.ts | 32 ++++++++++++++++++- ...sistence-host-partitioned-sessions.test.ts | 29 +++++++++++++++-- .../loading-store/automation-persistence.ts | 8 +++-- .../loading-store/session-host-partitions.ts | 8 ++--- .../store-prune-gate-signals.test.ts | 5 ++- ...re-runtime-authored-session-writes.test.ts | 3 +- .../workspace-session-write-listeners.spec.ts | 5 ++- .../wsl-move-session-freeze.test.ts | 9 ++++-- .../invalid-workspace-layout-purge.ts | 8 +++-- .../automation-run-operations.ts | 13 ++++++-- ...-removal-session-partition-fencing.test.ts | 3 +- src/preload/api/automation-api.ts | 7 +++- src/preload/api/automations-bridge.ts | 8 +++-- .../worktrees/teardown/remove-worktree.ts | 3 +- 17 files changed, 143 insertions(+), 32 deletions(-) diff --git a/config/id-only-store-write-baseline.txt b/config/id-only-store-write-baseline.txt index edb23958c19..672cc7325ba 100644 --- a/config/id-only-store-write-baseline.txt +++ b/config/id-only-store-write-baseline.txt @@ -1,7 +1,6 @@ # Store writes whose first parameter is a bare repo or worktree id (R4-2), as Class.method. # This is a RATCHET: entries may only be REMOVED. Take RepoHostTarget or HostWorktreeTarget. # Prune after a fix: pnpm check:id-only-store-write-ratchet --prune -AutomationPersistence.snapshotAutomationRunWorkspaceDisplayName MetadataLineageOperations.migrateWorktreeIdentity MetadataLineageOperations.removeWorkspaceLineage MetadataLineageOperations.removeWorktreeLineage @@ -9,9 +8,3 @@ MetadataLineageOperations.removeWorktreeMeta MetadataLineageOperations.setWorktreeLineage MetadataLineageOperations.setWorktreeMeta MetadataLineageOperations.setWorktreeMetaForHost -OrcadMigrationBlobInstallPersistence.installDeferredOrcadMigrationShellHistory -ProjectCollectionOperations.deleteProjectGroup -ProjectCollectionOperations.removeFolderWorkspace -ProjectCollectionOperations.updateFolderWorkspace -ProjectCollectionOperations.updateProjectGroup -SessionHostPartitionOperations.removeWorkspaceSessionStateForWorktree diff --git a/config/scripts/check-id-only-store-write-ratchet.mjs b/config/scripts/check-id-only-store-write-ratchet.mjs index 23aa711cf59..f2802a1947b 100644 --- a/config/scripts/check-id-only-store-write-ratchet.mjs +++ b/config/scripts/check-id-only-store-write-ratchet.mjs @@ -32,13 +32,26 @@ export const NOT_HOST_TWINNED_PARAMS = new Map([ ['SshProfileOperations.oldTargetId', 'the id names the host itself'] ]) +// The store keeps one row per id: minted UUIDs, de-duplicated on load, and a conflicting +// migration import is refused (orcad-catalog-row-identity.ts `selectNewRows`). +const UNIQUE_GROUP_ID = 'group ids are minted UUIDs, one row per id in the store' +const UNIQUE_FOLDER_ID = 'folder workspace ids are minted UUIDs, one row per id in the store' + /** Matches exempt by method, with the reason. Keep each reason true. */ export const NOT_HOST_TWINNED = new Map([ ['AutomationPersistence.advanceAutomationNextRun', 'automation ids are minted UUIDs'], ['AutomationPersistence.deleteAutomation', 'automation ids are minted UUIDs'], ['AutomationPersistence.updateAutomation', 'automation ids are minted UUIDs'], + [ + 'OrcadMigrationBlobInstallPersistence.installDeferredOrcadMigrationShellHistory', + 'a retry trigger: the id only gates a store-wide reinstall; nothing is written under it' + ], ['ProfilePreferences.recordFeatureInteraction', 'a feature id, not a row'], + ['ProjectCollectionOperations.deleteProjectGroup', UNIQUE_GROUP_ID], + ['ProjectCollectionOperations.removeFolderWorkspace', UNIQUE_FOLDER_ID], + ['ProjectCollectionOperations.updateFolderWorkspace', UNIQUE_FOLDER_ID], ['ProjectCollectionOperations.updateProject', 'one Project row spans every host'], + ['ProjectCollectionOperations.updateProjectGroup', UNIQUE_GROUP_ID], [ 'RepoLifecycleOperations.reorderRepos', 'a whole-catalog permutation: a duplicated id fails its check, so it refuses instead of guessing' diff --git a/src/main/ipc/automations.ts b/src/main/ipc/automations.ts index 7d44a522b42..4e0559b4bae 100644 --- a/src/main/ipc/automations.ts +++ b/src/main/ipc/automations.ts @@ -1,3 +1,5 @@ +import { parseExecutionHostId } from '../../shared/execution-host' +import { hostWorktreeTarget } from '../../shared/worktree/host-qualified-identity' import { ipcMain } from 'electron' import type { Store } from '../persistence' import type { AutomationService } from '../automations/service' @@ -110,8 +112,16 @@ export function registerAutomationHandlers(store: Store, service: AutomationServ ) ipcMain.handle( 'automations:snapshotWorkspaceName', - (_event, args: { workspaceId: string; displayName: string }): number => - store.snapshotAutomationRunWorkspaceDisplayName(args.workspaceId, args.displayName) + (_event, args: { workspaceId: string; hostId?: string; displayName: string }): number => { + const host = parseExecutionHostId(args.hostId) + // No host, no label: a bare id would also relabel a same-id twin's runs. + return host + ? store.snapshotAutomationRunWorkspaceDisplayName( + hostWorktreeTarget({ id: args.workspaceId, hostId: host.id }), + args.displayName + ) + : 0 + } ) ipcMain.handle('automations:rendererReady', (): void => { service.setRendererReady() diff --git a/src/main/persistence-automations.test.ts b/src/main/persistence-automations.test.ts index 261a8690af4..3365e2751fd 100644 --- a/src/main/persistence-automations.test.ts +++ b/src/main/persistence-automations.test.ts @@ -12,6 +12,7 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import type { PersistedState } from '../shared/persisted-state-types' import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../shared/execution-host' +import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity' // Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config. const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({ @@ -744,7 +745,10 @@ describe('Store', () => { }) store.createAutomationRun(automation, new Date('2026-05-13T09:00:00Z').getTime()) - const updatedCount = store.snapshotAutomationRunWorkspaceDisplayName('wt1', 'Deleted workspace') + const updatedCount = store.snapshotAutomationRunWorkspaceDisplayName( + hostWorktreeTarget({ id: 'wt1', hostId: 'local' }), + 'Deleted workspace' + ) expect(updatedCount).toBe(1) expect(store.listAutomationRuns(automation.id)[0].workspaceDisplayName).toBe( @@ -752,6 +756,32 @@ describe('Store', () => { ) }) + it("deleting an SSH twin's workspace leaves the local twin's run label", async () => { + const store = await createStore() + store.addRepo(makeRepo()) + const automation = store.createAutomation({ + name: 'Nightly', + prompt: 'Run checks', + agentId: 'claude', + projectId: 'r1', + workspaceMode: 'existing', + workspaceId: 'wt1', + timezone: 'UTC', + rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', + dtstart: new Date('2026-05-13T00:00:00Z').getTime() + }) + store.createAutomationRun(automation, new Date('2026-05-13T09:00:00Z').getTime()) + const before = store.listAutomationRuns(automation.id)[0].workspaceDisplayName + + const updatedCount = store.snapshotAutomationRunWorkspaceDisplayName( + hostWorktreeTarget({ id: 'wt1', hostId: toSshExecutionHostId('ssh-1') }), + 'SSH twin' + ) + + expect(updatedCount).toBe(0) + expect(store.listAutomationRuns(automation.id)[0].workspaceDisplayName).toBe(before) + }) + it('persists automation run output snapshots across later status updates', async () => { const store = await createStore() store.addRepo(makeRepo()) diff --git a/src/main/persistence-host-partitioned-sessions.test.ts b/src/main/persistence-host-partitioned-sessions.test.ts index 887a20a2a37..c73ac216059 100644 --- a/src/main/persistence-host-partitioned-sessions.test.ts +++ b/src/main/persistence-host-partitioned-sessions.test.ts @@ -1,3 +1,5 @@ +import { toSshExecutionHostId } from '../shared/execution-host' +import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity' import { closeTestStores, testState, @@ -421,7 +423,9 @@ describe('Store host-partitioned workspace sessions', () => { store.setWorkspaceSession(session, 'runtime:env-a') store.setWorkspaceSession(session, 'runtime:env-b') - store.removeWorkspaceSessionStateForWorktree(worktreeId, 'runtime:env-a') + store.removeWorkspaceSessionStateForWorktree( + hostWorktreeTarget({ id: worktreeId, hostId: 'runtime:env-a' }) + ) store.setWorkspaceSession(session, 'runtime:env-a') store.flush() @@ -622,6 +626,25 @@ describe('Store host-partitioned workspace sessions', () => { expect(store.getWorkspaceSession('local').tabsByWorktree[worktreeId]).toBeUndefined() }) + it("removing one SSH host's copy leaves a same-id copy on another SSH host", async () => { + const store = await createStore() + const worktreeId = 'repo-twin::/workspace/shared' + const session = { + ...makeHostSession('repo-twin'), + tabsByWorktree: { [worktreeId]: [makeTerminalTab({ id: 'twin-tab', worktreeId })] } + } + store.setWorkspaceSession(session, toSshExecutionHostId('ssh-a')) + store.setWorkspaceSession(session, toSshExecutionHostId('ssh-b')) + + store.removeWorkspaceSessionStateForWorktree( + hostWorktreeTarget({ id: worktreeId, hostId: toSshExecutionHostId('ssh-a') }) + ) + + const tabsOn = (host: string) => store.getWorkspaceSession(host).tabsByWorktree[worktreeId] + expect(tabsOn(toSshExecutionHostId('ssh-a'))).toBeUndefined() + expect(tabsOn(toSshExecutionHostId('ssh-b'))?.[0]?.id).toBe('twin-tab') + }) + // Trade-off: the fence is repo-wide, so claiming authority over a partition main never wrote would // rebase every sibling worktree of that repo onto an empty copy. A delayed write wins here instead. it('does not fence a delayed session write when its host partition was never persisted', async () => { @@ -634,7 +657,9 @@ describe('Store host-partitioned workspace sessions', () => { } } - store.removeWorkspaceSessionStateForWorktree(worktreeId, 'runtime:env-a') + store.removeWorkspaceSessionStateForWorktree( + hostWorktreeTarget({ id: worktreeId, hostId: 'runtime:env-a' }) + ) store.setWorkspaceSession(delayedSession, 'runtime:env-a') const session = store.getWorkspaceSession('runtime:env-a') diff --git a/src/main/persistence/loading-store/automation-persistence.ts b/src/main/persistence/loading-store/automation-persistence.ts index 6b30d0c04fd..ee3457e8075 100644 --- a/src/main/persistence/loading-store/automation-persistence.ts +++ b/src/main/persistence/loading-store/automation-persistence.ts @@ -1,3 +1,4 @@ +import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' import type { Automation, AutomationCreateInput, @@ -188,10 +189,13 @@ export class AutomationPersistence { return updateAutomationRunOperation(getAutomationRunOperations(this), result) } - snapshotAutomationRunWorkspaceDisplayName(workspaceId: string, displayName: string): number { + snapshotAutomationRunWorkspaceDisplayName( + target: HostWorktreeTarget, + displayName: string + ): number { return snapshotAutomationRunWorkspaceDisplayNameOperation( getAutomationRunOperations(this), - workspaceId, + target, displayName ) } diff --git a/src/main/persistence/loading-store/session-host-partitions.ts b/src/main/persistence/loading-store/session-host-partitions.ts index 12c5b7bc241..358c45dbb62 100644 --- a/src/main/persistence/loading-store/session-host-partitions.ts +++ b/src/main/persistence/loading-store/session-host-partitions.ts @@ -7,6 +7,7 @@ import { normalizeExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' +import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' import { getDefaultWorkspaceSession } from '../../../shared/constants' import { pruneLocalTerminalScrollbackBuffers } from '../../../shared/workspace-session-terminal-buffers' import { pruneWorkspaceSessionBrowserHistory } from '../../../shared/workspace-session-browser-history' @@ -112,12 +113,11 @@ export class SessionHostPartitionOperations { } removeWorkspaceSessionStateForWorktree( - worktreeId: string, - hostId?: ExecutionHostId | null, + target: HostWorktreeTarget, options: { advanceTerminalTopologyRevision?: boolean } = {} ): void { - for (const resolved of workspaceSessionPartitionIdsForHost(hostId)) { - removeWorkspaceSessionOwnerInPartition(this, worktreeId, resolved, options) + for (const resolved of workspaceSessionPartitionIdsForHost(target.hostId)) { + removeWorkspaceSessionOwnerInPartition(this, target.id, resolved, options) } } } diff --git a/src/main/persistence/loading-store/store-prune-gate-signals.test.ts b/src/main/persistence/loading-store/store-prune-gate-signals.test.ts index ca459ec3982..fe8b31e82bb 100644 --- a/src/main/persistence/loading-store/store-prune-gate-signals.test.ts +++ b/src/main/persistence/loading-store/store-prune-gate-signals.test.ts @@ -1,3 +1,4 @@ +import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' import { closeTestStores, createSqliteTestStore } from '../../persistence-test-harness' /** * Drives the real `Store`, because the value of the prune gate is entirely in whether the shipping @@ -78,7 +79,9 @@ describe('store signals to the worktree metadata prune gate', () => { } as unknown as WorkspaceSessionState) parkGate() - store.removeWorkspaceSessionStateForWorktree(WORKTREE_ID) + store.removeWorkspaceSessionStateForWorktree( + hostWorktreeTarget({ id: WORKTREE_ID, hostId: 'local' }) + ) expect(isLocalWorktreeMetadataPruneDue(REPO_ID)).toBe(true) }) diff --git a/src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts b/src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts index 1cc673e52b4..5b11f64b16a 100644 --- a/src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts +++ b/src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts @@ -1,3 +1,4 @@ +import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' import { closeTestStores, createSqliteTestStore } from '../../persistence-test-harness' /** * Drives the real `Store`, not the helper and not a fake. @@ -155,7 +156,7 @@ describe('Store keeps runtime-authored session fields across desktop writes', () } }) - store.removeWorkspaceSessionStateForWorktree(WT) + store.removeWorkspaceSessionStateForWorktree(hostWorktreeTarget({ id: WT, hostId: 'local' })) expect( Object.keys(store.getWorkspaceSession().clientHostedBrowserPagesByWorktree ?? {}) diff --git a/src/main/persistence/loading-store/workspace-session-write-listeners.spec.ts b/src/main/persistence/loading-store/workspace-session-write-listeners.spec.ts index 929924ddb9f..43e7fbeb9ef 100644 --- a/src/main/persistence/loading-store/workspace-session-write-listeners.spec.ts +++ b/src/main/persistence/loading-store/workspace-session-write-listeners.spec.ts @@ -1,3 +1,4 @@ +import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' import { describe, expect, it, vi } from 'vitest' import type { ExecutionHostId } from '../../../shared/execution-host' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' @@ -100,7 +101,9 @@ export function registerWorkspaceSessionWriteListenersTests(): void { .catch((error: unknown) => error instanceof ProfileStateWriterError || error) }) await step(() => store.retirePtyBinding(binding)) - await step(() => store.removeWorkspaceSessionStateForWorktree(WT)) + await step(() => + store.removeWorkspaceSessionStateForWorktree(hostWorktreeTarget({ id: WT, hostId: 'local' })) + ) return { saved: withStableFixtureIds({ captures: authority.captures, diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts index 19e82ec6200..d4803066dad 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts @@ -1,3 +1,4 @@ +import { hostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -160,8 +161,12 @@ describe('WSL move session freeze: other session writers', () => { }) it('keeps the frozen rows when the workspace owner is removed', () => { - store.removeWorkspaceSessionStateForWorktree(MOVING) - store.removeWorkspaceSessionStateForWorktree(STAYING) + store.removeWorkspaceSessionStateForWorktree( + hostWorktreeTarget({ id: MOVING, hostId: 'local' }) + ) + store.removeWorkspaceSessionStateForWorktree( + hostWorktreeTarget({ id: STAYING, hostId: 'local' }) + ) expect(titles(store)).toEqual({ moving: ['slept'], staying: undefined }) }) }) diff --git a/src/main/persistence/restoring-sessions/invalid-workspace-layout-purge.ts b/src/main/persistence/restoring-sessions/invalid-workspace-layout-purge.ts index f292b6aff47..ba60afed4c1 100644 --- a/src/main/persistence/restoring-sessions/invalid-workspace-layout-purge.ts +++ b/src/main/persistence/restoring-sessions/invalid-workspace-layout-purge.ts @@ -12,6 +12,10 @@ import { collectPersistedWorktreeIdsForSessionHydration } from '../../../shared/worktree/degraded-repo-worktree-validity' import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' +import { + hostWorktreeTarget, + type HostWorktreeTarget +} from '../../../shared/worktree/host-qualified-identity' import type { DetectedWorktreeListResult } from '../../../shared/worktree/types' import { getLocalWorktreeCatalogVersion } from '../../local-worktree-scan-generation' @@ -21,7 +25,7 @@ type PurgeStore = { getRepos(): readonly { id: string }[] getWorkspaceSession(hostId?: string | null): WorkspaceSessionState getWorkspaceSessionHostIds(): ExecutionHostId[] - removeWorkspaceSessionStateForWorktree(worktreeId: string, hostId?: ExecutionHostId | null): void + removeWorkspaceSessionStateForWorktree(target: HostWorktreeTarget): void } const ownedByPairedHost = (hostId: string | null | undefined): boolean => @@ -54,7 +58,7 @@ function dropInvalidWorkspaces( continue } for (const key of invalidWorkspaceKeys(store.getWorkspaceSession(hostId), catalog, inScope)) { - store.removeWorkspaceSessionStateForWorktree(key, hostId) + store.removeWorkspaceSessionStateForWorktree(hostWorktreeTarget({ id: key, hostId })) } } } diff --git a/src/main/persistence/scheduling-automations/automation-run-operations.ts b/src/main/persistence/scheduling-automations/automation-run-operations.ts index 5b82bdb26b3..7119aa64055 100644 --- a/src/main/persistence/scheduling-automations/automation-run-operations.ts +++ b/src/main/persistence/scheduling-automations/automation-run-operations.ts @@ -17,6 +17,8 @@ import { compareAutomationRunsNewestFirst, paginateAutomationRuns } from '../../../shared/automation-run-cursor' +import type { HostWorktreeTarget } from '../../../shared/worktree/host-qualified-identity' +import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { normalizeAutomationPrecheckResult, normalizeAutomationRunOutputSnapshot, @@ -218,16 +220,23 @@ export function updateAutomationRun( export function snapshotAutomationRunWorkspaceDisplayName( operations: AutomationRunOperations, - workspaceId: string, + target: HostWorktreeTarget, displayName: string ): number { const normalizedDisplayName = normalizeAutomationRunWorkspaceDisplayName(displayName) if (!normalizedDisplayName) { return 0 } + // Folder ids are minted UUIDs (one row per id), and a run's project host may differ from it. + const hostScoped = parseWorkspaceKey(target.id)?.type !== 'folder' let updatedCount = 0 operations.state.automationRuns = (operations.state.automationRuns ?? []).map((run) => { - if (run.workspaceId !== workspaceId || run.workspaceDisplayName === normalizedDisplayName) { + if ( + run.workspaceId !== target.id || + // A same-id twin's run names its own host; a run from before host context has none. + (hostScoped && run.runContext && run.runContext.hostId !== target.hostId) || + run.workspaceDisplayName === normalizedDisplayName + ) { return run } updatedCount += 1 diff --git a/src/main/worktree-removal-session-partition-fencing.test.ts b/src/main/worktree-removal-session-partition-fencing.test.ts index dd04d427f8e..f6d7145f14a 100644 --- a/src/main/worktree-removal-session-partition-fencing.test.ts +++ b/src/main/worktree-removal-session-partition-fencing.test.ts @@ -1,3 +1,4 @@ +import { hostWorktreeTarget } from '../shared/worktree/host-qualified-identity' import { closeTestStores, createSqliteTestStore, testState } from './persistence-test-harness' import { Store as CanonicalStore } from './persistence/loading-store/store' import { initDataPath as initCanonicalDataPath } from './persistence/loading-store/user-data-path' @@ -109,7 +110,7 @@ describe('worktree removal across host session partitions', () => { it('does not materialize an unwritten host partition when removing session state directly', async () => { const store = await createStore() - store.removeWorkspaceSessionStateForWorktree(STALE, ENV_A) + store.removeWorkspaceSessionStateForWorktree(hostWorktreeTarget({ id: STALE, hostId: ENV_A })) expect(store.getWorkspaceSessionHostIds()).toEqual(['local']) }) diff --git a/src/preload/api/automation-api.ts b/src/preload/api/automation-api.ts index 9c5a8a09169..efcd2c8a72f 100644 --- a/src/preload/api/automation-api.ts +++ b/src/preload/api/automation-api.ts @@ -1,3 +1,4 @@ +import type { ExecutionHostId } from '../../shared/execution-host' import type { AutomationDispatchRequest, AutomationDispatchResult, @@ -35,7 +36,11 @@ export type AutomationsApi = { runId: string }) => Promise markDispatchResult: (result: AutomationDispatchResult) => Promise - snapshotWorkspaceName: (args: { workspaceId: string; displayName: string }) => Promise + snapshotWorkspaceName: (args: { + workspaceId: string + hostId: ExecutionHostId + displayName: string + }) => Promise rendererReady: () => Promise onDispatchRequested: (callback: (request: AutomationDispatchRequest) => void) => () => void onChanged: (callback: (payload: AutomationsChangedPayload) => void) => () => void diff --git a/src/preload/api/automations-bridge.ts b/src/preload/api/automations-bridge.ts index b5a4ff3c701..51b90bedb63 100644 --- a/src/preload/api/automations-bridge.ts +++ b/src/preload/api/automations-bridge.ts @@ -1,3 +1,4 @@ +import type { ExecutionHostId } from '../../shared/execution-host' import { ipcRenderer } from 'electron' import type { ExternalAutomationManagerResult, PreloadApi } from '../api-types' import type { @@ -29,8 +30,11 @@ export const automationsApi = { ipcRenderer.invoke('automations:runPrecheck', args), markDispatchResult: (result: AutomationDispatchResult): Promise => ipcRenderer.invoke('automations:markDispatchResult', result), - snapshotWorkspaceName: (args: { workspaceId: string; displayName: string }): Promise => - ipcRenderer.invoke('automations:snapshotWorkspaceName', args), + snapshotWorkspaceName: (args: { + workspaceId: string + hostId: ExecutionHostId + displayName: string + }): Promise => ipcRenderer.invoke('automations:snapshotWorkspaceName', args), rendererReady: (): Promise => ipcRenderer.invoke('automations:rendererReady'), onDispatchRequested: (callback: (request: AutomationDispatchRequest) => void): (() => void) => { const listener = (_event: Electron.IpcRendererEvent, request: AutomationDispatchRequest) => diff --git a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts index 048efa8bc95..29c230be14c 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts @@ -225,10 +225,11 @@ export function createRemoveWorktree( } const worktreeDisplayName = worktreeBeforeRemoval?.displayName?.trim() - if (worktreeDisplayName) { + if (worktreeDisplayName && hostId) { try { await window.api.automations?.snapshotWorkspaceName?.({ workspaceId: worktreeId, + hostId, displayName: worktreeDisplayName }) } catch (error) {