From b075a95b06307abac4bf42d175cf2c0b9439737f Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:40:01 -0700 Subject: [PATCH] Strip liveness gate from AI Vault session delete (#13279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Strip liveness gate from AI Vault session delete Delete now requires only path validation + user confirmation — no process roster, no liveness check, no quiescence, no ownership ledger. Co-authored-by: Orca * Remove obsolete AI Vault liveness delete reliability gate Session delete no longer checks process liveness, so drop the manifest entry that still referenced the deleted test files. * minor fix --------- Co-authored-by: Orca --- config/reliability-gates.jsonc | 99 +------ .../session-delete-liveness.repro.test.ts | 179 ------------ src/main/ai-vault/session-delete.test.ts | 10 +- src/main/ai-vault/session-delete.ts | 31 +-- src/main/ai-vault/session-liveness.test.ts | 263 ------------------ src/main/ai-vault/session-liveness.ts | 187 ------------- src/main/ipc/ai-vault-delete.ts | 25 +- src/main/ipc/ai-vault.test.ts | 6 +- src/main/ipc/ai-vault.ts | 6 +- src/main/ipc/register-core-handlers.ts | 3 +- src/main/runtime/orca-runtime.ts | 75 +---- .../right-sidebar/AiVaultSessionRow.tsx | 2 +- .../ai-vault-session-deletability.test.ts | 23 -- .../ai-vault-session-deletability.ts | 18 +- src/renderer/src/i18n/locales/en.json | 3 +- src/renderer/src/i18n/locales/es.json | 3 +- src/renderer/src/i18n/locales/ja.json | 3 +- src/renderer/src/i18n/locales/ko.json | 3 +- src/renderer/src/i18n/locales/zh.json | 3 +- src/shared/ai-vault-session-deletion.ts | 7 +- .../terminal-macos-system-key-remap.spec.ts | 7 +- 21 files changed, 32 insertions(+), 924 deletions(-) delete mode 100644 src/main/ai-vault/session-delete-liveness.repro.test.ts delete mode 100644 src/main/ai-vault/session-liveness.test.ts delete mode 100644 src/main/ai-vault/session-liveness.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 3e4a32a0c..3993a178f 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "updatedAt": "2026-08-07", + "updatedAt": "2026-08-09", "policy": { "maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"], "blockingPromotion": { @@ -10,103 +10,6 @@ } }, "gates": [ - { - "id": "ai-vault.live-session-delete-authority", - "title": "AI Vault deletion fails closed against owning-runtime session liveness", - "maturity": "experimental", - "protection": "partial", - "owner": "ai-vault-runtime", - "layer": "main-process-filesystem-mutation", - "surfaces": [ - "AI Vault transcript deletion", - "local and WSL agent sessions", - "paired-client-owned host sessions" - ], - "platforms": ["macos", "linux", "windows"], - "providers": ["local", "wsl", "ssh", "paired-runtime"], - "coveredPlatforms": ["macos", "linux", "windows"], - "coveredProviders": ["local", "wsl", "ssh", "paired-runtime"], - "coverageNotes": "Platform-independent contracts cover local and WSL authority, paired-owned and unmanaged live sessions absent from renderer or managed-PTY state, identity retention across UI dismissal, inventory and foreground-inspection ordering, SSH exclusion, unavailable and ambiguous inventory, bounded inspection concurrency, oversized inventories, returned rejection, and real temporary-file survival. Live headed/headless paired-runtime, physical Windows/WSL, and Docker SSH journeys remain uncollected.", - "motivatingLinks": ["https://github.com/stablyai/orca/releases/tag/v1.4.177-rc.0"], - "invariant": "Main may delete a local AI transcript only after the owning runtime freshly proves that no live local or WSL provider-session identity exists, including exact hook identities absent from managed PTY inventory; live and unknown liveness preserve the transcript regardless of renderer freshness.", - "oracle": "Open confirmation against a stale empty renderer snapshot, transition the authoritative session to live before main authorization and during a controlled foreground-inspection barrier, and separately model paired and unmanaged local owners absent from renderer or managed-PTY state. Dismiss a live row and require its identity-only authority to remain. Require session-live or session-liveness-unknown rejection and the real temporary transcript to survive. Make process inventory unavailable or ambiguous and require unknown with the file preserved; prove known other sessions permit deletion and inspection concurrency stays at or below eight.", - "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/ai-vault/session-delete-liveness.repro.test.ts src/main/ai-vault/session-liveness.test.ts src/main/agent-hooks/server-ai-vault-liveness.test.ts --reporter=dot" - ], - "testFiles": [ - "src/main/ai-vault/session-delete-liveness.repro.test.ts", - "src/main/ai-vault/session-liveness.test.ts", - "src/main/agent-hooks/server-ai-vault-liveness.test.ts" - ], - "assertionRefs": [ - { - "file": "src/main/ai-vault/session-delete-liveness.repro.test.ts", - "assertions": [ - "confirmation TOCTOU returns session-live and preserves the real transcript", - "paired ownership missing from the renderer snapshot returns session-live and preserves the real transcript", - "exact live local hook identity missing from managed PTY inventory returns unknown and preserves the real transcript", - "dismissed live status retained as identity-only authority returns unknown and preserves the real transcript", - "unknown authoritative liveness fails closed and preserves the real transcript" - ] - }, - { - "file": "src/main/ai-vault/session-liveness.test.ts", - "assertions": [ - "local, WSL, paired-runtime, and SSH host partitions resolve without local fallback", - "renderer session identity must match the identity parsed from the validated transcript", - "hook identity is captured after the controlled process-inventory barrier", - "hook identity is captured after the controlled foreground-inspection barrier", - "missing identity, unavailable inventory, unattributed ownership, and oversized inventory remain unknown", - "foreground inspection concurrency never exceeds eight and stops scheduling at the shared deadline" - ] - }, - { - "file": "src/main/agent-hooks/server-ai-vault-liveness.test.ts", - "assertions": [ - "user dismissal retains provider identity without retaining a visible turn-status row", - "restart hydration preserves dismissed identity as unconfirmed liveness-only authority" - ] - } - ], - "evidenceRuns": [ - { - "date": "2026-08-07", - "runner": "local", - "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ai-vault/session-delete-liveness.repro.test.ts src/main/ai-vault/session-liveness.test.ts src/main/agent-hooks/server-ai-vault-liveness.test.ts --reporter=dot", - "result": "passed", - "durationSeconds": 1, - "summary": "Twenty deterministic deletion, identity-binding, inventory/inspection-ordering, dismissal/restart-retention, unmanaged-ownership, unknown-state, filesystem-survival, host-partition, deadline, and bounded-concurrency assertions passed." - } - ], - "runtimeBudget": { - "p95Seconds": 2, - "scope": "focused main-process liveness and temporary-filesystem contracts" - }, - "flakeHistory": { - "status": "unknown", - "evidence": "One local deterministic run is recorded; CI and soak history have not started." - }, - "redGreenEvidence": { - "status": "complete", - "evidence": "The byte-identical two-scenario oracle failed on v1.4.177-rc.0 (9e948fbdf4) and latest main (7a867f12aa): both returned deleted and both real transcripts were removed. On PR commit 8e2fcfd681, an exact live local hook identity absent from empty managed-PTY inventory returned deleted and removed the real transcript. Independent review then proved a session-other to session-live transition during foreground inspection returned not-live, and live-row dismissal erased the only external identity. The corrected candidate passed all 20 assertions, including restart hydration of dismissed identity; temporarily disabling only the original liveness decision made all three original deletion-safety tests red again." - }, - "performanceBudget": { - "required": true, - "evidence": "Deletion parses only the validated target through the incremental parse cache, performs one local-provider process listing, makes no SSH or paired-runtime request, caps inventory at 512 PTYs, bounds foreground inspections to eight concurrent calls under one shared three-second fail-closed deadline, and adds no polling or background work." - }, - "promotionCriteria": [ - "Collect CI soak history for the deterministic gate.", - "Collect headed and headless paired-runtime deletion-refusal evidence.", - "Collect physical Windows/WSL and Docker SSH host-partition evidence." - ], - "knownGaps": [ - "No live headed/headless paired-runtime journey was run.", - "No physical Windows/WSL or Docker SSH journey was run.", - "The authorization check cannot make an external provider process start atomic with filesystem trashing; it closes the reported renderer-confirmation window at the main mutation boundary." - ], - "demotionRule": "Keep experimental or demote if live or unknown ownership can reach trashing, remote ownership falls back locally, process inspection becomes unbounded, or the deterministic filesystem oracle flakes without a product or harness defect." - }, { "id": "editor.restored-sibling-owner-reparent", "title": "Restored sibling tabs migrate filesystem authority before becoming editable", diff --git a/src/main/ai-vault/session-delete-liveness.repro.test.ts b/src/main/ai-vault/session-delete-liveness.repro.test.ts deleted file mode 100644 index 6e4928a84..000000000 --- a/src/main/ai-vault/session-delete-liveness.repro.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { existsSync } from 'node:fs' -import { mkdtemp, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' -import type { AgentStatusIpcPayload } from '../../shared/agent-status-types' -import type { AiVaultDeleteSessionResult } from '../../shared/ai-vault-session-deletion' -import type { ValidateAiVaultSessionDeleteTargetArgs } from './session-delete-target' - -const { trashItemMock } = vi.hoisted(() => ({ trashItemMock: vi.fn() })) - -vi.mock('electron', () => ({ shell: { trashItem: trashItemMock } })) -vi.mock('../wsl-unc-delete', () => ({ tryDeleteWslUncPath: vi.fn().mockResolvedValue(false) })) - -import { deleteAiVaultSessionFile } from './session-delete' -import { resolveAiVaultSessionLiveness } from './session-liveness' - -type AiVaultSessionLiveness = 'live' | 'not-live' | 'unknown' - -type DeleteWithLiveness = ( - args: ValidateAiVaultSessionDeleteTargetArgs & { sessionId: string }, - deps: { getSessionLiveness: () => Promise } -) => Promise - -const deleteWithLiveness = deleteAiVaultSessionFile as unknown as DeleteWithLiveness -const fixtureRoots: string[] = [] - -async function createTranscript(): Promise<{ filePath: string; root: string }> { - const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-live-delete-repro-')) - fixtureRoots.push(root) - const filePath = join(root, 'session-live.json') - await writeFile(filePath, '{"sessionId":"session-live"}\n') - return { filePath, root } -} - -async function attemptDelete( - getSessionLiveness: () => Promise -): Promise<{ filePath: string; result: AiVaultDeleteSessionResult }> { - const { filePath, root } = await createTranscript() - const result = await deleteWithLiveness( - { - agent: 'gemini', - sessionId: 'session-live', - filePath, - executionHostId: 'local', - rootOptions: { geminiSessionsDir: root } - }, - { getSessionLiveness } - ) - return { filePath, result } -} - -describe('live AI Vault session delete safety invariant', () => { - afterEach(async () => { - trashItemMock.mockReset() - await Promise.all( - fixtureRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })) - ) - }) - - it('survives when the session becomes live after confirmation but before main authorization', async () => { - trashItemMock.mockImplementation((path: string) => rm(path)) - const rendererPaneSnapshot = new Set() - expect(rendererPaneSnapshot.has('session-live')).toBe(false) - - let authoritativeLiveness: AiVaultSessionLiveness = 'not-live' - const confirmationOpened = true - authoritativeLiveness = 'live' - expect(confirmationOpened).toBe(true) - - const { filePath, result } = await attemptDelete(async () => authoritativeLiveness) - - expect.soft(result).toEqual({ outcome: 'rejected', agent: 'gemini', reason: 'session-live' }) - expect.soft(existsSync(filePath), 'live transcript must survive on disk').toBe(true) - }) - - it('survives when a paired owner is absent from the renderer pane snapshot', async () => { - trashItemMock.mockImplementation((path: string) => rm(path)) - const rendererPaneSnapshot = new Set() - const authoritativeOwner = { - runtimeId: 'host-runtime', - connectionId: 'paired-client', - generation: 'generation-1', - sessionId: 'session-live', - liveness: 'live' as const - } - expect(rendererPaneSnapshot.has(authoritativeOwner.sessionId)).toBe(false) - - const { filePath, result } = await attemptDelete(async () => authoritativeOwner.liveness) - - expect.soft(result).toEqual({ outcome: 'rejected', agent: 'gemini', reason: 'session-live' }) - expect.soft(existsSync(filePath), 'externally owned transcript must survive on disk').toBe(true) - }) - - it('survives an exact live local hook identity absent from managed PTY inventory', async () => { - trashItemMock.mockImplementation((path: string) => rm(path)) - const inspectForegroundProcess = vi.fn() - const externalLiveStatus: AgentStatusIpcPayload = { - paneKey: 'tab:external-live', - terminalHandle: 'term_external-live', - connectionId: null, - receivedAt: 1, - stateStartedAt: 1, - state: 'working', - prompt: 'test', - agentType: 'gemini', - providerSession: { key: 'session_id', id: 'session-live' } - } - - const { filePath, result } = await attemptDelete(() => - resolveAiVaultSessionLiveness( - { agent: 'gemini', sessionId: 'session-live' }, - { - listProcesses: async () => [], - getStatusSnapshot: () => [externalLiveStatus], - inspectForegroundProcess, - getStatusPtyId: () => 'external-live', - getAgentHint: () => null - } - ) - ) - - expect.soft(result).toEqual({ - outcome: 'rejected', - agent: 'gemini', - reason: 'session-liveness-unknown' - }) - expect.soft(existsSync(filePath), 'external live transcript must survive on disk').toBe(true) - expect(inspectForegroundProcess).not.toHaveBeenCalled() - }) - - it('survives a dismissed live row retained as liveness-only identity', async () => { - trashItemMock.mockImplementation((path: string) => rm(path)) - const retainedIdentity: AgentStatusIpcPayload = { - paneKey: 'tab:external-live', - connectionId: null, - receivedAt: 1, - stateStartedAt: 1, - state: 'working', - prompt: 'test', - agentType: 'gemini', - providerSession: { key: 'session_id', id: 'session-live' }, - providerSessionOnly: true - } - - const { filePath, result } = await attemptDelete(() => - resolveAiVaultSessionLiveness( - { agent: 'gemini', sessionId: 'session-live' }, - { - listProcesses: async () => [], - getStatusSnapshot: () => [retainedIdentity], - inspectForegroundProcess: vi.fn(), - getStatusPtyId: () => null, - getAgentHint: () => null - } - ) - ) - - expect.soft(result).toEqual({ - outcome: 'rejected', - agent: 'gemini', - reason: 'session-liveness-unknown' - }) - expect.soft(existsSync(filePath), 'dismissed live transcript must survive on disk').toBe(true) - }) - - it('fails closed when authoritative liveness is unavailable', async () => { - trashItemMock.mockImplementation((path: string) => rm(path)) - - const { filePath, result } = await attemptDelete(async () => 'unknown') - - expect(result).toEqual({ - outcome: 'rejected', - agent: 'gemini', - reason: 'session-liveness-unknown' - }) - expect(existsSync(filePath), 'unknown-liveness transcript must survive on disk').toBe(true) - }) -}) diff --git a/src/main/ai-vault/session-delete.test.ts b/src/main/ai-vault/session-delete.test.ts index 3c6f86f43..cacb0dbbd 100644 --- a/src/main/ai-vault/session-delete.test.ts +++ b/src/main/ai-vault/session-delete.test.ts @@ -33,21 +33,13 @@ vi.mock('../wsl-unc-delete', () => ({ WslDeleteValidationError: WslDeleteValidationErrorMock })) -import { deleteAiVaultSessionFile as deleteAiVaultSessionFileWithLiveness } from './session-delete' +import { deleteAiVaultSessionFile } from './session-delete' const HOME = join('/tmp', 'orca-ai-vault-delete-exec-fixture-home') const GEMINI_ROOT = join(HOME, '.gemini', 'tmp') const CLAUDE_ROOT = join(HOME, '.claude', 'projects') const ROVO_ROOT = join(HOME, '.rovodev', 'sessions') -function deleteAiVaultSessionFile( - args: Parameters[0] -) { - return deleteAiVaultSessionFileWithLiveness(args, { - getSessionLiveness: async () => 'not-live' - }) -} - function enoent(): NodeJS.ErrnoException { const error = new Error('not found') as NodeJS.ErrnoException error.code = 'ENOENT' diff --git a/src/main/ai-vault/session-delete.ts b/src/main/ai-vault/session-delete.ts index fb6876904..467674906 100644 --- a/src/main/ai-vault/session-delete.ts +++ b/src/main/ai-vault/session-delete.ts @@ -2,10 +2,8 @@ import { lstat, realpath } from 'node:fs/promises' import { shell } from 'electron' import type { AiVaultDeleteSessionResult, - AiVaultSessionDeleteRemoval, - AiVaultSessionLiveness + AiVaultSessionDeleteRemoval } from '../../shared/ai-vault-session-deletion' -import type { AiVaultAgent } from '../../shared/ai-vault-types' import { isPathInsideOrEqual } from '../../shared/cross-platform-path' import { validateAiVaultSessionDeleteTarget, @@ -19,14 +17,7 @@ import { isENOENT } from '../ipc/filesystem-auth' // Never throws: IPC payloads are untyped at runtime, so a bad input and an fs // error both resolve to a discriminated result the handler can render. export async function deleteAiVaultSessionFile( - args: ValidateAiVaultSessionDeleteTargetArgs & { sessionId?: string }, - deps: { - getSessionLiveness: (target: { - agent: AiVaultAgent - sessionId: string | undefined - filePath: string - }) => Promise - } + args: ValidateAiVaultSessionDeleteTargetArgs & { sessionId?: string } ): Promise { const validation = validateAiVaultSessionDeleteTarget(args) if (!validation.allowed) { @@ -34,24 +25,6 @@ export async function deleteAiVaultSessionFile( } const { agent, removals } = validation - let liveness: AiVaultSessionLiveness = 'unknown' - try { - liveness = await deps.getSessionLiveness({ - agent, - sessionId: args.sessionId, - filePath: validation.resolvedPath - }) - } catch { - // Inspection failure is unknown, never permission to delete. - } - if (liveness !== 'not-live') { - return { - outcome: 'rejected', - agent, - reason: liveness === 'live' ? 'session-live' : 'session-liveness-unknown' - } - } - try { for (const removal of removals) { const rejection = await removeOne(removal) diff --git a/src/main/ai-vault/session-liveness.test.ts b/src/main/ai-vault/session-liveness.test.ts deleted file mode 100644 index c4eb2a703..000000000 --- a/src/main/ai-vault/session-liveness.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { describe, expect, it, vi } from 'vitest' -import type { AgentStatusIpcPayload } from '../../shared/agent-status-types' -import type { PtyProcessInfo } from '../providers/pty-process-info' -import { - readAiVaultSessionIdentity, - resolveAiVaultSessionLiveness, - type AiVaultSessionLivenessDependencies -} from './session-liveness' - -function processInfo(id: string, terminalHandle = `term_${id}`): PtyProcessInfo { - return { id, terminalHandle, cwd: '/workspace', title: 'terminal' } -} - -function status(args: { - sessionId: string - ptyId: string - connectionId?: string | null -}): AgentStatusIpcPayload { - return { - paneKey: `tab:${args.ptyId}`, - terminalHandle: `term_${args.ptyId}`, - connectionId: args.connectionId ?? null, - receivedAt: 1, - stateStartedAt: 1, - state: 'working', - prompt: 'test', - agentType: 'gemini', - providerSession: { key: 'session_id', id: args.sessionId } - } -} - -function dependencies( - overrides: Partial = {} -): AiVaultSessionLivenessDependencies { - return { - listProcesses: async () => [], - getStatusSnapshot: () => [], - inspectForegroundProcess: async () => ({ available: true, process: 'zsh' }), - getStatusPtyId: (row) => row.terminalHandle?.slice('term_'.length) ?? null, - getAgentHint: () => null, - ...overrides - } -} - -describe('resolveAiVaultSessionLiveness', () => { - it('binds renderer identity to the session parsed from the validated transcript', async () => { - const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-delete-identity-')) - const filePath = join(root, 'session.json') - await writeFile( - filePath, - JSON.stringify({ - sessionId: 'authoritative-session', - startTime: '2026-08-07T00:00:00.000Z', - lastUpdated: '2026-08-07T00:01:00.000Z', - messages: [{ type: 'user', content: 'test' }] - }) - ) - try { - await expect( - readAiVaultSessionIdentity({ - agent: 'gemini', - sessionId: 'spoofed-session', - filePath - }) - ).resolves.toEqual({ outcome: 'unknown' }) - await expect( - readAiVaultSessionIdentity({ - agent: 'gemini', - sessionId: 'authoritative-session', - filePath - }) - ).resolves.toEqual({ outcome: 'found', sessionId: 'authoritative-session' }) - } finally { - await rm(root, { recursive: true, force: true }) - } - }) - - it('finds a paired-owned live session without consulting a renderer pane snapshot', async () => { - const deps = dependencies({ - listProcesses: async () => [processInfo('paired-pty')], - getStatusSnapshot: () => [status({ sessionId: 'session-live', ptyId: 'paired-pty' })], - inspectForegroundProcess: async () => ({ available: true, process: 'gemini' }) - }) - - await expect( - resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId: 'session-live' }, deps) - ).resolves.toBe('live') - }) - - it('reads hook identity after a controlled process-inventory barrier', async () => { - let releaseInventory: (processes: PtyProcessInfo[]) => void = () => {} - const inventory = new Promise((resolve) => { - releaseInventory = resolve - }) - let statuses = [status({ sessionId: 'session-other', ptyId: 'paired-pty' })] - const deps = dependencies({ - listProcesses: () => inventory, - getStatusSnapshot: () => statuses, - inspectForegroundProcess: async () => ({ available: true, process: 'gemini' }) - }) - - const liveness = resolveAiVaultSessionLiveness( - { agent: 'gemini', sessionId: 'session-live' }, - deps - ) - statuses = [status({ sessionId: 'session-live', ptyId: 'paired-pty' })] - releaseInventory([processInfo('paired-pty')]) - - await expect(liveness).resolves.toBe('live') - }) - - it('reads hook identity after a controlled foreground-inspection barrier', async () => { - let markInspectionStarted: () => void = () => {} - const inspectionStarted = new Promise((resolve) => { - markInspectionStarted = resolve - }) - let releaseInspection: (inspection: { - available: boolean - process: string | null - }) => void = () => {} - const inspection = new Promise<{ available: boolean; process: string | null }>((resolve) => { - releaseInspection = resolve - }) - let statuses = [status({ sessionId: 'session-other', ptyId: 'managed-pty' })] - const deps = dependencies({ - listProcesses: async () => [processInfo('managed-pty')], - getStatusSnapshot: () => statuses, - inspectForegroundProcess: () => { - markInspectionStarted() - return inspection - } - }) - - const liveness = resolveAiVaultSessionLiveness( - { agent: 'gemini', sessionId: 'session-live' }, - deps - ) - await inspectionStarted - statuses = [status({ sessionId: 'session-live', ptyId: 'managed-pty' })] - releaseInspection({ available: true, process: 'gemini' }) - - await expect(liveness).resolves.toBe('live') - }) - - it('treats WSL hook ownership as local authority', async () => { - const deps = dependencies({ - listProcesses: async () => [processInfo('wsl-pty')], - getStatusSnapshot: () => [ - status({ sessionId: 'session-live', ptyId: 'wsl-pty', connectionId: 'wsl:Ubuntu' }) - ], - inspectForegroundProcess: async () => ({ available: true, process: 'gemini' }) - }) - - await expect( - resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId: 'session-live' }, deps) - ).resolves.toBe('live') - }) - - it('does not use SSH identity as local liveness evidence', async () => { - const deps = dependencies({ - listProcesses: async () => [processInfo('local-pty')], - getStatusSnapshot: () => [ - status({ sessionId: 'session-live', ptyId: 'local-pty', connectionId: 'ssh:dev-box' }) - ], - inspectForegroundProcess: async () => ({ available: true, process: 'gemini' }) - }) - - await expect( - resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId: 'session-live' }, deps) - ).resolves.toBe('unknown') - }) - - it('returns not-live when every local Gemini process owns another known session', async () => { - const deps = dependencies({ - listProcesses: async () => [processInfo('other-pty')], - getStatusSnapshot: () => [status({ sessionId: 'session-other', ptyId: 'other-pty' })], - inspectForegroundProcess: async () => ({ available: true, process: 'gemini' }) - }) - - await expect( - resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId: 'session-target' }, deps) - ).resolves.toBe('not-live') - }) - - it.each([ - ['missing session identity', dependencies(), undefined], - [ - 'unavailable process inventory', - dependencies({ - listProcesses: async () => { - throw new Error('daemon offline') - } - }), - 'session-live' - ], - [ - 'unattributed live agent process', - dependencies({ - listProcesses: async () => [processInfo('unknown-pty')], - inspectForegroundProcess: async () => ({ available: true, process: 'gemini' }) - }), - 'session-live' - ] - ])('preserves unknown for %s', async (_name, deps, sessionId) => { - await expect(resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId }, deps)).resolves.toBe( - 'unknown' - ) - }) - - it('bounds foreground inspection concurrency', async () => { - let active = 0 - let maxActive = 0 - const inspectForegroundProcess = vi.fn(async () => { - active += 1 - maxActive = Math.max(maxActive, active) - await new Promise((resolve) => queueMicrotask(resolve)) - active -= 1 - return { available: true, process: 'zsh' } - }) - const deps = dependencies({ - listProcesses: async () => - Array.from({ length: 20 }, (_, index) => processInfo(`shell-${index}`)), - inspectForegroundProcess - }) - - await expect( - resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId: 'session-target' }, deps) - ).resolves.toBe('not-live') - expect(inspectForegroundProcess).toHaveBeenCalledTimes(20) - expect(maxActive).toBeLessThanOrEqual(8) - }) - - it('fails closed before inspecting an oversized inventory', async () => { - const inspectForegroundProcess = vi.fn() - const deps = dependencies({ - listProcesses: async () => - Array.from({ length: 513 }, (_, index) => processInfo(`shell-${index}`)), - inspectForegroundProcess - }) - - await expect( - resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId: 'session-target' }, deps) - ).resolves.toBe('unknown') - expect(inspectForegroundProcess).not.toHaveBeenCalled() - }) - - it('does not start foreground inspections after the shared deadline', async () => { - const inspectForegroundProcess = vi.fn() - const deps = dependencies({ - deadlineMs: 0, - listProcesses: async () => [processInfo('late-pty')], - inspectForegroundProcess - }) - - await expect( - resolveAiVaultSessionLiveness({ agent: 'gemini', sessionId: 'session-target' }, deps) - ).resolves.toBe('unknown') - expect(inspectForegroundProcess).not.toHaveBeenCalled() - }) -}) diff --git a/src/main/ai-vault/session-liveness.ts b/src/main/ai-vault/session-liveness.ts deleted file mode 100644 index 6f7170687..000000000 --- a/src/main/ai-vault/session-liveness.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { isShellProcess } from '../../shared/agent-detection' -import { recognizeAgentProcess } from '../../shared/agent-process-recognition' -import type { AgentStatusIpcPayload } from '../../shared/agent-status-types' -import type { AiVaultSessionLiveness } from '../../shared/ai-vault-session-deletion' -import type { AiVaultAgent } from '../../shared/ai-vault-types' -import type { PtyProcessInfo } from '../providers/pty-process-info' -import { isWslHookRelayConnectionId } from '../../shared/wsl-hook-relay-contract' -import { lstat } from 'node:fs/promises' -import { parseAgentSessionFileCached } from './session-scanner-parse-cache' - -const MAX_LIVENESS_PROCESSES = 512 -const PROCESS_INSPECTION_CONCURRENCY = 8 - -type ProcessInspection = { - available: boolean - process: string | null -} - -type AgentProcessInspection = 'target-agent' | 'unknown' | 'other' - -export type AiVaultSessionLivenessDependencies = { - deadlineMs?: number - listProcesses: () => Promise - getStatusSnapshot: () => AgentStatusIpcPayload[] - inspectForegroundProcess: (ptyId: string) => Promise - getStatusPtyId: (status: AgentStatusIpcPayload) => string | null - getAgentHint: (process: PtyProcessInfo) => string | null -} - -export type AiVaultSessionIdentityRead = - | { outcome: 'found'; sessionId: string } - | { outcome: 'missing' } - | { outcome: 'unknown' } - -/** Binds the requested identity to the validated transcript on disk. */ -export async function readAiVaultSessionIdentity(target: { - agent: AiVaultAgent - sessionId: string | undefined - filePath: string -}): Promise { - try { - const fileStats = await lstat(target.filePath) - if (!fileStats.isFile()) { - return { outcome: 'unknown' } - } - const session = await parseAgentSessionFileCached( - { - agent: target.agent, - file: { - path: target.filePath, - mtimeMs: fileStats.mtimeMs, - modifiedAt: fileStats.mtime.toISOString(), - sizeBytes: fileStats.size - }, - codexHome: null - }, - process.platform - ) - return session?.sessionId && session.sessionId === target.sessionId - ? { outcome: 'found', sessionId: session.sessionId } - : { outcome: 'unknown' } - } catch (error) { - return (error as NodeJS.ErrnoException).code === 'ENOENT' - ? { outcome: 'missing' } - : { outcome: 'unknown' } - } -} - -function isLocalStatus(status: AgentStatusIpcPayload): boolean { - return status.connectionId === null || isWslHookRelayConnectionId(status.connectionId) -} - -function isValidSessionId(sessionId: string | undefined): sessionId is string { - return ( - typeof sessionId === 'string' && - sessionId.length > 0 && - sessionId.length <= 512 && - sessionId.trim() === sessionId - ) -} - -async function inspectInBatches( - values: readonly T[], - inspect: (value: T) => Promise, - deadlineMs?: number -): Promise { - const results: AgentProcessInspection[] = [] - let nextIndex = 0 - const worker = async (): Promise => { - while (nextIndex < values.length) { - const index = nextIndex - nextIndex += 1 - if (deadlineMs !== undefined && Date.now() >= deadlineMs) { - results[index] = 'unknown' - continue - } - results[index] = await inspect(values[index]!) - } - } - await Promise.all( - Array.from({ length: Math.min(PROCESS_INSPECTION_CONCURRENCY, values.length) }, worker) - ) - return results -} - -/** Resolves owning-runtime liveness without treating unavailable evidence as absence. */ -export async function resolveAiVaultSessionLiveness( - target: { agent: AiVaultAgent; sessionId: string | undefined }, - deps: AiVaultSessionLivenessDependencies -): Promise { - if (!isValidSessionId(target.sessionId)) { - return 'unknown' - } - - let processes: PtyProcessInfo[] - try { - processes = await deps.listProcesses() - } catch { - return 'unknown' - } - if (processes.length > MAX_LIVENESS_PROCESSES) { - return 'unknown' - } - - const processInspections = await inspectInBatches( - processes, - async (process) => { - let inspection: ProcessInspection - try { - inspection = await deps.inspectForegroundProcess(process.id) - } catch { - return 'unknown' - } - if (!inspection.available) { - return 'unknown' - } - - const recognizedAgent = recognizeAgentProcess(inspection.process)?.agent ?? null - const hintedAgent = deps.getAgentHint(process) - const ownsTargetAgent = - recognizedAgent === target.agent || - (recognizedAgent === null && - (inspection.process === null || !isShellProcess(inspection.process)) && - hintedAgent === target.agent) - if (!ownsTargetAgent) { - return 'other' - } - return 'target-agent' - }, - deps.deadlineMs - ) - - let localStatuses: AgentStatusIpcPayload[] - try { - localStatuses = deps.getStatusSnapshot().filter(isLocalStatus) - } catch { - return 'unknown' - } - const processIds = new Set(processes.map((process) => process.id)) - const hasUnmatchedTargetStatus = localStatuses.some( - (status) => - status.agentType === target.agent && - status.providerSession?.id === target.sessionId && - !processIds.has(deps.getStatusPtyId(status) ?? '') - ) - if (hasUnmatchedTargetStatus) { - return 'unknown' - } - - const results = processInspections.map((inspection, index) => { - if (inspection !== 'target-agent') { - return inspection - } - const identities = localStatuses.filter( - (status) => - status.agentType === target.agent && deps.getStatusPtyId(status) === processes[index]!.id - ) - if (identities.some((status) => status.providerSession?.id === target.sessionId)) { - return 'live' - } - return identities.some((status) => status.providerSession) ? 'other' : 'unknown' - }) - if (results.includes('live')) { - return 'live' - } - return results.includes('unknown') ? 'unknown' : 'not-live' -} diff --git a/src/main/ipc/ai-vault-delete.ts b/src/main/ipc/ai-vault-delete.ts index 3f3a1c8ab..a221cc28c 100644 --- a/src/main/ipc/ai-vault-delete.ts +++ b/src/main/ipc/ai-vault-delete.ts @@ -8,19 +8,13 @@ import { invalidateSessionParseCacheEntry } from '../ai-vault/session-scanner-pa import type { AiVaultAgent } from '../../shared/ai-vault-types' import type { AiVaultDeleteSessionArgs, - AiVaultDeleteSessionResult, - AiVaultSessionLiveness + AiVaultDeleteSessionResult } from '../../shared/ai-vault-session-deletion' // Which cache backs the multi-host list is ai-vault.ts's concern, so its // invalidation is injected rather than reached into from here. type AiVaultDeleteDeps = { invalidateMultiHostListCache: () => void - getSessionLiveness: (target: { - agent: AiVaultAgent - sessionId: string | undefined - filePath: string - }) => Promise } // Binds the delete orchestration to the caller's cache-invalidation seam. @@ -40,16 +34,13 @@ export async function deleteAiVaultSession( // The validator tolerates a malformed agent/filePath but destructures `args`, // so an absent payload is defaulted here to keep the never-throws boundary. const wslHomeDirs = await getAiVaultWslHomeDirs() - const result = await deleteAiVaultSessionFile( - { - agent: args?.agent as AiVaultAgent, - sessionId: args?.sessionId, - filePath: args?.filePath ?? '', - executionHostId: args?.executionHostId, - wslHomeDirs - }, - deps - ) + const result = await deleteAiVaultSessionFile({ + agent: args?.agent as AiVaultAgent, + sessionId: args?.sessionId, + filePath: args?.filePath ?? '', + executionHostId: args?.executionHostId, + wslHomeDirs + }) if (result.outcome === 'deleted') { // Three caches could otherwise resurrect it: the desktop per-host leg diff --git a/src/main/ipc/ai-vault.test.ts b/src/main/ipc/ai-vault.test.ts index e36708e18..9af91f92a 100644 --- a/src/main/ipc/ai-vault.test.ts +++ b/src/main/ipc/ai-vault.test.ts @@ -800,8 +800,7 @@ describe('deleteAiVaultSession', () => { sessionId: args.sessionId, filePath: args.filePath, executionHostId: 'local' - }), - expect.objectContaining({ getSessionLiveness: expect.any(Function) }) + }) ) expect(mocks.invalidateAiVaultSessionListCache).toHaveBeenCalledTimes(1) expect(mocks.invalidateSessionParseCacheEntry).toHaveBeenCalledWith(args.filePath) @@ -850,8 +849,7 @@ describe('deleteAiVaultSession', () => { reason: 'invalid-path' }) expect(mocks.deleteAiVaultSessionFile).toHaveBeenCalledWith( - expect.objectContaining({ filePath: '' }), - expect.objectContaining({ getSessionLiveness: expect.any(Function) }) + expect.objectContaining({ filePath: '' }) ) expect(mocks.invalidateAiVaultSessionListCache).not.toHaveBeenCalled() }) diff --git a/src/main/ipc/ai-vault.ts b/src/main/ipc/ai-vault.ts index 3f4e2d49a..c2f5be216 100644 --- a/src/main/ipc/ai-vault.ts +++ b/src/main/ipc/ai-vault.ts @@ -71,7 +71,6 @@ type AiVaultHandlerOptions = AiVaultSessionSources & getActiveRuntimeAiVaultHostInfos?: () => readonly RuntimeAiVaultHostInfo[] scanRuntimeAiVaultSessions?: RuntimeAiVaultScanner resolveRuntimeAiVaultSessionTitles?: RuntimeAiVaultSessionTitleResolver - getSessionLiveness?: Parameters[1]['getSessionLiveness'] } let scanCoordinator = new AiVaultScanCoordinator() @@ -80,10 +79,7 @@ const listCancellations = createSenderScopedRequestCancellations() // Shared by the IPC registration and the test internals: a delete must drop // the multi-host leg cache, which this module owns the only caller of. const aiVaultDeleteDeps = { - invalidateMultiHostListCache: invalidateAiVaultHostLegCache, - getSessionLiveness: ( - target: Parameters>[0] - ) => handlerOptions.getSessionLiveness?.(target) ?? Promise.resolve('unknown' as const) + invalidateMultiHostListCache: invalidateAiVaultHostLegCache } const resolveAiVaultSessionTitles = ( diff --git a/src/main/ipc/register-core-handlers.ts b/src/main/ipc/register-core-handlers.ts index b685f470e..607aa08ce 100644 --- a/src/main/ipc/register-core-handlers.ts +++ b/src/main/ipc/register-core-handlers.ts @@ -227,8 +227,7 @@ export function registerCoreHandlers( resolveRuntimeAiVaultSessionTitles: async (environmentId, args) => resolveRuntimeAiVaultSessionTitles(app.getPath('userData'), environmentId, args), prepareRuntimeSessionResume: async (environmentId, args) => - prepareRuntimeAiVaultSessionResume(app.getPath('userData'), environmentId, args), - getSessionLiveness: (target) => runtime.getAiVaultSessionLiveness(target) + prepareRuntimeAiVaultSessionResume(app.getPath('userData'), environmentId, args) }) registerNativeChatHandlers() registerClipboardHandlers(store) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 85888b946..f01277024 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -490,16 +490,11 @@ import { listAiVaultSessions } from '../ai-vault/cached-session-list' import { resolveLocalAiVaultSessionTitles } from '../ai-vault/session-title-resolver' -import { - readAiVaultSessionIdentity, - resolveAiVaultSessionLiveness -} from '../ai-vault/session-liveness' -import type { AiVaultAgent, AiVaultListArgs, AiVaultListResult } from '../../shared/ai-vault-types' +import type { AiVaultListArgs, AiVaultListResult } from '../../shared/ai-vault-types' import type { AiVaultSessionTitleRequest, AiVaultSessionTitlesResult } from '../../shared/ai-vault-session-title' -import type { AiVaultSessionLiveness } from '../../shared/ai-vault-session-deletion' import type { AiVaultPrepareSessionResumeArgs, AiVaultPrepareSessionResumeResult @@ -4990,74 +4985,6 @@ export class OrcaRuntimeService { return resolveLocalAiVaultSessionTitles(requests, signal) } - async getAiVaultSessionLiveness(target: { - agent: AiVaultAgent - sessionId: string | undefined - filePath: string - }): Promise { - const provider = this.getLocalProviderFn?.() - if (!provider || !this.getAgentProviderSessionSnapshotFn) { - return 'unknown' - } - const identity = await readAiVaultSessionIdentity(target) - if (identity.outcome !== 'found') { - return 'unknown' - } - const deadlineMs = Date.now() + 3_000 - return await resolveAiVaultSessionLiveness( - { - agent: target.agent, - sessionId: identity.sessionId - }, - { - deadlineMs, - listProcesses: async () => { - const result = await withTimeoutResult( - provider.listProcesses({ deadlineMs }), - Math.max(1, deadlineMs - Date.now()) - ) - if (!result.ok) { - throw new Error('agent_session_ownership_unknown') - } - return result.value - }, - getStatusSnapshot: this.getAgentProviderSessionSnapshotFn, - inspectForegroundProcess: async (ptyId) => { - const result = await withTimeoutResult( - provider.getForegroundProcess(ptyId), - Math.max(1, deadlineMs - Date.now()) - ) - return result.ok - ? { available: true, process: result.value } - : { available: false, process: null } - }, - getStatusPtyId: (status) => { - if (status.terminalHandle) { - const live = this.getLivePtyForHandle(status.terminalHandle) - if (live) { - return live.pty.ptyId - } - for (const [ptyId, handle] of this.handleByPtyId) { - if (handle === status.terminalHandle) { - return ptyId - } - } - } - return this.getPtyRecordForPaneKey(status.paneKey)?.ptyId ?? null - }, - getAgentHint: (process) => { - const pty = this.ptysById.get(process.id) - const runtimeHint = pty?.foregroundAgent ?? pty?.launchAgent - if (runtimeHint) { - return runtimeHint - } - const ownerAgents = new Set(process.agentSessionOwners?.map((owner) => owner.claim.agent)) - return ownerAgents.size === 1 ? ([...ownerAgents][0] ?? null) : null - } - } - ) - } - prepareAiVaultSessionResume( args: AiVaultPrepareSessionResumeArgs ): Promise { diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx index 9e2f1702f..e4af4f3e9 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx @@ -85,7 +85,7 @@ export function VaultSessionRow({ const detailsId = getSessionDetailsId(session.id) const latestTurn = latestSessionConversationTurn(session) // Computed once so the dropdown menu and the context menu never disagree. - const deleteBlockedReason = aiVaultSessionDeleteBlockedReason(session, liveState) + const deleteBlockedReason = aiVaultSessionDeleteBlockedReason(session) const requestDelete = (): void => onRequestDelete(session) const detailsTooltip = detailsExpanded ? translate('auto.components.right.sidebar.AiVaultSessionRow.hideDetails', 'Hide Details') diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts index 49e8d39e1..3e2a370e8 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts @@ -5,7 +5,6 @@ import { aiVaultSessionDeleteBlockedReason } from './ai-vault-session-deletabili // assertions pin the English copy as well as the gate order. const NON_LOCAL = 'Only sessions on this device can be deleted.' const SYNTHETIC = "This session can't be deleted from Orca." -const LIVE = 'This session is still running — wait for it to finish before deleting.' const localGeminiSession = { agent: 'gemini' as const, @@ -28,28 +27,6 @@ describe('aiVaultSessionDeleteBlockedReason', () => { ).toBeNull() }) - it('tells the user to wait while the agent is still running', () => { - for (const live of ['working', 'blocked', 'waiting'] as const) { - expect(aiVaultSessionDeleteBlockedReason(localGeminiSession, live)).toBe(LIVE) - } - }) - - it('offers Delete for a finished session (done) and one with no live state', () => { - expect(aiVaultSessionDeleteBlockedReason(localGeminiSession, 'done')).toBeNull() - expect(aiVaultSessionDeleteBlockedReason(localGeminiSession, null)).toBeNull() - }) - - it('keeps the permanent reason over "running" for an unsupported live session', () => { - // A live but unsupported agent stays "unsupported" — it would never become - // deletable, so "wait for it to finish" would mislead. - expect( - aiVaultSessionDeleteBlockedReason( - { agent: 'codex', executionHostId: 'local', filePath: '/home/user/.codex/x.jsonl' }, - 'working' - ) - ).toBe("Codex sessions can't be deleted from Orca.") - }) - it('blocks ssh- and runtime-hosted sessions regardless of agent', () => { for (const executionHostId of ['ssh:dev-box', 'runtime:gpu-box'] as const) { expect(aiVaultSessionDeleteBlockedReason({ ...localGeminiSession, executionHostId })).toBe( diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.ts index 033ebd87c..87007c376 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-deletability.ts @@ -1,6 +1,5 @@ import { isAiVaultDeletableAgent } from '../../../../shared/ai-vault-session-deletion' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { AgentStatusState } from '../../../../shared/agent-status-types' import { translate } from '@/i18n/i18n' import { agentLabel } from './ai-vault-session-filters' import { @@ -8,11 +7,6 @@ import { isSyntheticAiVaultSessionPath } from './ai-vault-session-path-actions' -// Matches the active-dot rule in ai-vault-session-row-display. -function isSessionLive(liveState: AgentStatusState | null | undefined): boolean { - return liveState != null && liveState !== 'done' -} - /** * Why Delete is unavailable for this session, as the tooltip text to show — or * null when it is offered. Each message says which sessions are affected, never @@ -25,8 +19,7 @@ function isSessionLive(liveState: AgentStatusState | null | undefined): boolean * it does: both consult the same shared agent set and host/synthetic predicates. */ export function aiVaultSessionDeleteBlockedReason( - session: Pick, - liveState?: AgentStatusState | null + session: Pick ): string | null { if (!canUseLocalAiVaultSessionPathActions(session.executionHostId)) { return translate( @@ -47,14 +40,5 @@ export function aiVaultSessionDeleteBlockedReason( { value0: agentLabel(session.agent) } ) } - // Last, so an otherwise-deletable session reads as "wait for it to finish" - // rather than a permanent reason. Trashing a live transcript would drop the - // writes the agent is still appending. - if (isSessionLive(liveState)) { - return translate( - 'auto.components.right.sidebar.AiVaultSessionRow.deleteReasonSessionLive', - 'This session is still running — wait for it to finish before deleting.' - ) - } return null } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index a1eedc206..08fd9b389 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -11738,8 +11738,7 @@ "delete": "Delete", "deleteReasonNonLocalHost": "Only sessions on this device can be deleted.", "deleteReasonSyntheticPath": "This session can't be deleted from Orca.", - "deleteReasonUnsupportedAgent": "{{value0}} sessions can't be deleted from Orca.", - "deleteReasonSessionLive": "This session is still running — wait for it to finish before deleting." + "deleteReasonUnsupportedAgent": "{{value0}} sessions can't be deleted from Orca." }, "AiVaultSessionDeleteDialog": { "title": "Delete this session?", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index dc1024c8b..d84aa89b1 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -11318,8 +11318,7 @@ "delete": "Eliminar", "deleteReasonNonLocalHost": "Solo se pueden eliminar las sesiones de este dispositivo.", "deleteReasonSyntheticPath": "Esta sesión no se puede eliminar desde Orca.", - "deleteReasonUnsupportedAgent": "Las sesiones de {{value0}} no se pueden eliminar desde Orca.", - "deleteReasonSessionLive": "Esta sesión aún se está ejecutando: espera a que termine antes de eliminarla." + "deleteReasonUnsupportedAgent": "Las sesiones de {{value0}} no se pueden eliminar desde Orca." }, "AiVaultSessionDeleteDialog": { "title": "¿Eliminar esta sesión?", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 3d7991b14..76f154d39 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -11318,8 +11318,7 @@ "delete": "削除", "deleteReasonNonLocalHost": "このデバイス上のセッションのみ削除できます。", "deleteReasonSyntheticPath": "このセッションは Orca では削除できません。", - "deleteReasonUnsupportedAgent": "{{value0}} のセッションは Orca では削除できません。", - "deleteReasonSessionLive": "このセッションはまだ実行中です — 削除する前に完了を待ってください。" + "deleteReasonUnsupportedAgent": "{{value0}} のセッションは Orca では削除できません。" }, "AiVaultSessionDeleteDialog": { "title": "このセッションを削除しますか?", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index c04d93c16..c86caa3e8 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -11318,8 +11318,7 @@ "delete": "삭제", "deleteReasonNonLocalHost": "이 기기에 있는 세션만 삭제할 수 있습니다.", "deleteReasonSyntheticPath": "이 세션은 Orca에서 삭제할 수 없습니다.", - "deleteReasonUnsupportedAgent": "{{value0}} 세션은 Orca에서 삭제할 수 없습니다.", - "deleteReasonSessionLive": "이 세션은 아직 실행 중입니다 — 삭제하려면 완료될 때까지 기다리세요." + "deleteReasonUnsupportedAgent": "{{value0}} 세션은 Orca에서 삭제할 수 없습니다." }, "AiVaultSessionDeleteDialog": { "title": "이 세션을 삭제할까요?", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 85b4cd051..62090dd10 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -11338,8 +11338,7 @@ "delete": "删除", "deleteReasonNonLocalHost": "只能删除此设备上的会话。", "deleteReasonSyntheticPath": "此会话无法在 Orca 中删除。", - "deleteReasonUnsupportedAgent": "{{value0}} 会话无法在 Orca 中删除。", - "deleteReasonSessionLive": "此会话仍在运行 — 请等待其完成后再删除。" + "deleteReasonUnsupportedAgent": "{{value0}} 会话无法在 Orca 中删除。" }, "AiVaultSessionDeleteDialog": { "title": "要删除此会话吗?", diff --git a/src/shared/ai-vault-session-deletion.ts b/src/shared/ai-vault-session-deletion.ts index 7fdde68b0..9e25f632a 100644 --- a/src/shared/ai-vault-session-deletion.ts +++ b/src/shared/ai-vault-session-deletion.ts @@ -4,7 +4,8 @@ import type { ExecutionHostId } from './execution-host' // IPC payload for aiVault:deleteSession. export type AiVaultDeleteSessionArgs = { agent: AiVaultAgent - // Optional for mixed renderer/main versions; main rejects missing identity. + // Optional for mixed renderer/main versions. Main ignores this field on delete + // (path + host + agent validation only; no identity/liveness check). sessionId?: string filePath: string // The session's host; only a local session may be deleted. @@ -71,10 +72,6 @@ export type AiVaultSessionDeleteRejectionCode = // fs-side guard: lstat disagrees with the removal's declared kind (a symlink, // or a file where the plan expects a directory). | 'unexpected-target-kind' - | 'session-live' - | 'session-liveness-unknown' - -export type AiVaultSessionLiveness = 'live' | 'not-live' | 'unknown' // One path the executor removes. A `kind` mismatch on disk is a rejection, // never a coerced delete. `roots` are what the path's realpath must still diff --git a/tests/e2e/terminal-macos-system-key-remap.spec.ts b/tests/e2e/terminal-macos-system-key-remap.spec.ts index 711b7a6b6..f858f2243 100644 --- a/tests/e2e/terminal-macos-system-key-remap.spec.ts +++ b/tests/e2e/terminal-macos-system-key-remap.spec.ts @@ -149,7 +149,12 @@ test.describe('Terminal macOS system key remap', () => { expect(received).toEqual([Buffer.from(`${committed}\n`).toString('hex')]) completed = true } finally { - await closeTerminalImePaneArena(arena, testInfo, `${arm.slug}-${layout.label}`, !completed) + await closeTerminalImePaneArena( + arena, + testInfo, + `${arm.slug}-${layout.label}`, + !completed + ) removeTerminalImeByteReader(reader) } })