From f1c0c8f9ff719254748f4394408d69d18df40695 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 30 May 2026 00:08:23 +0800 Subject: [PATCH] docs(acp): add session sync spec --- docs/features/acp-session-sync/plan.md | 355 ++++++++++++++++++++++++ docs/features/acp-session-sync/spec.md | 146 ++++++++++ docs/features/acp-session-sync/tasks.md | 79 ++++++ 3 files changed, 580 insertions(+) create mode 100644 docs/features/acp-session-sync/plan.md create mode 100644 docs/features/acp-session-sync/spec.md create mode 100644 docs/features/acp-session-sync/tasks.md diff --git a/docs/features/acp-session-sync/plan.md b/docs/features/acp-session-sync/plan.md new file mode 100644 index 000000000..0385796c6 --- /dev/null +++ b/docs/features/acp-session-sync/plan.md @@ -0,0 +1,355 @@ +# ACP Session Sync Plan + +## Design Summary + +DeepChat should keep local session identity and ACP agent identity separate: + +- Local identity: `new_sessions.id` / DeepChat conversation id. +- Remote identity: ACP `sessionId`. +- Stable bridge: `acp_sessions(conversation_id, agent_id, session_id, workdir, status, metadata)`. + +The implementation should add a demand-driven sync path that imports ACP `session/list` metadata +into local sessions, then reuse the stored ACP `sessionId` when opening or sending messages. + +## Current Flow + +```mermaid +sequenceDiagram + participant UI as Renderer + participant S as AgentSessionPresenter + participant L as LLMProviderPresenter + participant A as AcpSessionManager + participant P as ACP Agent + + UI->>S: ensureAcpDraftSession(agentId, projectDir) + S->>L: prepareAcpSession(localSessionId, agentId, projectDir) + L->>A: getOrCreateSession(localSessionId, agent, workdir) + A->>A: read acp_sessions by localSessionId + agentId + alt local ACP sessionId exists and loadSession supported + A->>P: session/load(sessionId) + else no local ACP sessionId + A->>P: session/new(cwd) + end + A->>A: save sessionId to acp_sessions +``` + +Missing pieces: + +- No call to ACP `session/list`. +- No lookup by `(agentId, acpSessionId)` for idempotent import. +- `clearSession()` clears the persisted `sessionId`, so runtime cleanup can erase reuse state. +- `session/load` replay is not imported into DeepChat messages. +- `session_info_update` is ignored. + +## Target Flow + +```mermaid +sequenceDiagram + participant UI as Renderer + participant S as AgentSessionPresenter + participant L as LLMProviderPresenter + participant A as AcpProvider/AcpSessionManager + participant P as ACP Agent + + UI->>S: syncAcpAgentSessions(agentId, workdir?) + S->>L: listAcpAgentSessions(agentId, workdir?) + L->>P: initialize if needed + L->>P: session/list(cwd?, cursor?) + P-->>L: sessions + nextCursor + L-->>S: normalized remote session list + S->>S: upsert new_sessions + deepchat_sessions + acp_sessions + S-->>UI: existing sessions.updated event + + UI->>S: open/send local ACP session + S->>L: prepareAcpSession(localSessionId, agentId, workdir) + L->>A: getOrCreateSession(localSessionId, agent, workdir) + A->>A: find persisted ACP sessionId + alt loadSession supported + A->>A: register replay importer for sessionId + A->>P: session/load(sessionId, cwd, mcpServers) + P-->>A: session/update history replay + A->>S: persist replayed messages + else resume supported + A->>P: session/resume(sessionId, cwd, mcpServers) + else + A->>P: session/new(cwd, mcpServers) + end + A->>A: attach normal live hooks +``` + +## Architecture Decisions + +### 1. Add ACP Session Capabilities + +Extend `AcpProcessHandle` with parsed capability flags: + +- `supportsLoadSession` +- `supportsListSessions` +- `supportsResumeSession` +- `supportsCloseSession` + +`parseLoadSessionCapability` already exists. Add parsing for +`agentCapabilities.sessionCapabilities.list/resume/close`. Use SDK methods +`connection.listSessions(...)` and `connection.unstable_resumeSession(...)` only when advertised. + +### 2. Keep Mapping Ownership Local + +`AgentSessionPresenter` should own the local import/upsert flow because it already owns +`new_sessions`, session-list events, and `deepchat_sessions` state. + +`AcpProvider` / `LLMProviderPresenter` should expose a read-only capability: + +```ts +listAcpAgentSessions(input: { + agentId: string + workdir?: string | null +}): Promise +``` + +It should: + +- resolve aliases through `resolveAcpAgentAlias`, +- warm or start the ACP connection, +- verify `supportsListSessions`, +- call `connection.listSessions({ cwd, cursor })` until pagination completes, +- return normalized metadata without mutating local session tables. + +### 3. Add Idempotent Import Helpers + +Add persistence helpers for ACP session identity: + +- `getAcpSessionByRemoteId(agentId, acpSessionId)` +- `upsertAcpSessionMapping(conversationId, agentId, acpSessionId, workdir, metadata?)` +- `releaseAcpRuntimeSession(conversationId, agentId)` that sets `status='idle'` without clearing + `session_id` +- `deleteAcpSessionMapping(conversationId, agentId)` for local session deletion + +Revisit `session_id TEXT UNIQUE`. Prefer a scoped uniqueness model: + +- unique `(agent_id, session_id)` when `session_id` is not null, +- keep `(conversation_id, agent_id)` unique. + +If changing the unique index is risky in the first pass, preserve the existing unique constraint +but still scope all lookups by agent id in code. + +### 4. Sync Local Session Rows + +`AgentSessionPresenter.syncAcpAgentSessions` should: + +1. Validate ACP is enabled and the agent exists. +2. Call `llmProviderPresenter.listAcpAgentSessions`. +3. For each remote session: + - normalize `cwd` into `projectDir`, + - normalize title to remote title or `New Chat`, + - parse `updatedAt` to milliseconds or use sync time, + - find existing mapping by `(agentId, acpSessionId)`, + - update existing local session metadata when found, + - otherwise create a local `new_sessions` row with `isDraft=false`, + - ensure `deepchat_sessions` has provider/model state for ACP, + - upsert the `acp_sessions` mapping. +4. Emit one session-list update payload containing changed local ids. + +Do not delete local rows just because a filtered `session/list` call omitted them. + +### 5. Handle DeepChat-Only And Stale Remote Sessions + +DeepChat can have an ACP local session whose agent-side `sessionId` is absent because another +client deleted agent history, the agent was reset, sync was filtered by `cwd`, auth failed, or the +agent's `session/list` is incomplete. Local history must remain durable. + +Rules: + +- `session/list` absence is observational, not destructive. Filtered list absence means + "unknown"; unfiltered list absence can update mapping metadata to `remoteStatus: 'not_listed'` + with a `checkedAt` timestamp, but must not clear `session_id`. +- `session/load` / `session/resume` rejection is stronger evidence. If the ACP error clearly + means "session not found" or equivalent, mark the mapping `remoteStatus: 'missing'`, record + `missingSessionId`, `missingAt`, and `lastLoadError` in metadata, and preserve all local + messages. +- Do not immediately overwrite the mapping with a new ACP id on open. Create a new ACP session + only when the user sends/continues, so simply browsing old local history does not mutate remote + state. +- When `session/new` succeeds for a stale mapping, set `session_id` to the new ACP id and append + the missing id to metadata such as `previousAcpSessionIds`. +- The new ACP session can only continue from DeepChat's local message context; agent-private + state that existed only in the old remote session cannot be recovered. The UI should surface + this as a quiet warning/status when practical. + +### 6. Separate Runtime Release From Mapping Deletion + +Replace current "clear means forget ACP id" behavior with two paths: + +- Runtime release: remove listeners, cancel active prompt if needed, unbind process, set status to + `idle`, preserve `session_id`. +- Mapping deletion: used when the local DeepChat session is deleted, removes the `acp_sessions` + row. + +App quit, provider refresh, workdir-change cleanup, and process shutdown should use runtime +release. Local session deletion should use mapping deletion. + +### 7. Keep Delete Semantics Capability-Gated + +Zed avoids full bidirectional delete sync. It treats runtime close, local archive, and remote +delete as different operations: + +- `close_session` releases an active ACP runtime session and sends `CloseSessionRequest` only when + the last local handle is gone. This is not history deletion. +- Imported external sessions can be archived locally, which changes Zed's sidebar visibility + without requiring agent-side deletion. +- ACP remote delete is opt-in: the default session-list abstraction returns "delete_session not + supported"; the ACP implementation only sends `DeleteSessionRequest` when the agent advertises + delete capability and Zed's ACP beta flag is enabled. + +DeepChat should follow the same separation: + +- Do not expose "delete from agent" unless a future ACP capability parser confirms remote delete + support. The first implementation can skip remote delete entirely. +- For synced remote-backed sessions, expose local-only removal/hiding. This should purge the + locally copied chat artifacts but keep a minimal hidden conversation plus ACP mapping, so + `(agentId, acpSessionId)` continues to act as a tombstone. +- The hidden marker can live in a new session visibility field, ACP mapping metadata, or a small + dedicated tombstone table. The important invariant is that normal session-list queries exclude + it, while sync import can still find the remote id and decide not to recreate it. +- During `session/list` import, skip remote sessions with an active local tombstone unless the user + explicitly chooses to show hidden sessions or re-import them. +- If the user deletes a local ACP session whose remote session is already confirmed missing, no + remote action is needed. +- If remote delete is added later, make it a separate, confirmed action. On success, remove the + local session/mapping and clear any tombstone for that remote id. + +Local purge should remove message rows, assistant blocks, search documents, offload files, runtime +caches, and permission/tool approval state for the hidden conversation. It should preserve only the +minimum metadata needed for visibility filtering, diagnostics, and re-import suppression. + +This keeps sync simple: `session/list` discovers/imports visible remote sessions; local hiding is a +DeepChat preference backed by a durable remote-id tombstone; remote deletion only happens when the +agent can actually do it. + +### 8. Reuse Existing Sessions With Load/Resume + +`AcpSessionManager.initializeSession` should choose: + +1. `session/load` if a persisted ACP id exists and `supportsLoadSession`. +2. `session/resume` if a persisted ACP id exists, load is not supported, and + `supportsResumeSession`. +3. `session/new` otherwise. + +For `session/load`, register a replay collector before issuing the RPC. Only attach normal live +hooks after the load completes. This avoids treating replayed history as the response to the next +prompt. + +For `session/resume`, there is no history replay. Attach normal live hooks after the response is +ready. + +### 9. Import Load Replay Into Messages + +Add an `AcpSessionReplayImporter` focused on history import, separate from live stream mapping. +It should consume `session/update` notifications during `session/load` and persist them through +`DeepChatMessageStore` or equivalent helper methods that preserve: + +- `deepchat_messages`, +- `deepchat_user_messages`, +- `deepchat_assistant_blocks`, +- search documents, +- usage stats where possible, +- tape facts where appropriate. + +Message grouping rules: + +- Prefer ACP `messageId` when present. +- Group contiguous `user_message_chunk` notifications into a user message. +- Group contiguous `agent_message_chunk`, `agent_thought_chunk`, `tool_call`, `tool_call_update`, + and `plan` notifications into assistant blocks. +- Store provenance in message metadata: ACP session id, ACP message id when available, import + source, and imported timestamp. +- Make replay idempotent. If message ids are present, use a dedicated mapping table or metadata + lookup. If message ids are absent, avoid reimporting when the local session already has messages + unless the user explicitly requests a rebuild. + +The first implementation can import text, reasoning, plan, and tool status blocks. Richer assets +and usage attribution can be incremental as long as unsupported blocks degrade to readable text. + +### 10. Handle Session Info Updates + +Extend `AcpContentMapper` or add a parallel notification parser to surface: + +```ts +{ + title?: string | null + updatedAt?: string | null + meta?: Record | null +} +``` + +`AcpProvider.handleSessionUpdate` should forward this to a session metadata update port. The +update should: + +- ignore invalid timestamps, +- ignore null or blank title values for the non-null local title field, +- refresh the session search document when title changes, +- emit `sessions.updated` / typed session-list events for renderer refresh. + +### 11. Debug And Inspector Support + +The ACP inspector should gain debug actions for: + +- `listSessions` +- `resumeSession` + +This is useful for manual validation and matches the existing `newSession` / `loadSession` +debug actions. + +## Compatibility + +- Agents without `session/list` continue to behave as today. +- Agents without `loadSession` but with `session/resume` can reconnect to known sessions but will + not import old history. +- Agents without both load and resume keep using `session/new`. +- Existing `acp_sessions` rows remain valid. +- Existing local ACP sessions should not be duplicated during sync; aliases resolve before lookup. +- DeepChat-only ACP sessions remain visible and usable. If the agent no longer has the remote + `sessionId`, DeepChat keeps local history and can rebind to a new ACP session on the next send. + +## Test Strategy + +- Unit test capability parsing for list/resume/close. +- Unit test ACP `session/list` pagination and unsupported capability behavior. +- Unit test idempotent import: + - new remote session creates local rows, + - repeat sync updates metadata only, + - filtered missing remote session does not delete local rows. +- Unit test runtime release preserves `acp_sessions.session_id`. +- Unit test local-only removal of a remote-backed session writes a tombstone and prevents + automatic re-import on the next sync. +- Unit test remote delete actions are hidden/disabled and never called when the ACP agent does not + advertise delete support. +- Unit test `initializeSession` chooses load, resume, or new correctly. +- Unit test confirmed remote-missing errors mark mappings stale without deleting local messages. +- Unit test continuing a stale session creates a new ACP session and preserves the old id in + metadata. +- Unit test replay importer with: + - user + assistant text, + - reasoning and plan blocks, + - tool calls, + - duplicate replay with ACP message ids. +- Unit test `session_info_update` updates title/time and emits session list update. +- Renderer/store test that session-list update refreshes synced ACP sessions. +- Manual validation with: + - `acpx --agent "dim acp" sessions list --filter-cwd `, + - opening a synced DimCode session in DeepChat, + - sending a follow-up and confirming the same ACP `sessionId` is reused. + +## Risks And Mitigations + +- Duplicate local sessions: mitigate with `(agentId, acpSessionId)` lookup before create. +- Replay history shown as live response: mitigate with load-specific replay importer and delayed + live hook attachment. +- Bad remote timestamps: parse strictly and fall back to sync time. +- Optional ACP message ids: use best-effort idempotency and avoid destructive rebuilds by default. +- Session list flood: keep sync scoped and demand-driven. +- Accidental remote deletion: do not delete agent history in this feature. +- Local delete surprise re-import: record local hide/tombstone entries for remote-backed sessions. +- Local history loss from stale remote state: never delete local sessions/messages based on + `session/list` absence or `session/load` failure. +- Silent semantic drift after rebind: surface that a missing remote session was replaced by a new + ACP session backed only by DeepChat's local context. diff --git a/docs/features/acp-session-sync/spec.md b/docs/features/acp-session-sync/spec.md new file mode 100644 index 000000000..f44629980 --- /dev/null +++ b/docs/features/acp-session-sync/spec.md @@ -0,0 +1,146 @@ +# ACP Session Sync + +## Background + +DeepChat stores its own local session rows and already persists an ACP mapping in +`acp_sessions`. Today that mapping is only useful after DeepChat itself created the ACP +session. Sessions that already exist inside the ACP agent are invisible to DeepChat, and +runtime cleanup can clear the saved ACP `sessionId`, preventing future `session/load` +reuse. + +The ACP protocol now has the pieces DeepChat needs: + +- [`session/list`](https://agentclientprotocol.com/protocol/session-list) discovers agent-owned + sessions and returns `sessionId`, `cwd`, `title`, `updatedAt`, and pagination. +- [`session/load`](https://agentclientprotocol.com/protocol/session-setup) restores an existing + session and replays its history via `session/update`. +- `session/resume` reconnects to an existing session without replaying history when the agent + advertises `sessionCapabilities.resume`. + +Zed's current ACP implementation keeps a local `ThreadId` separate from the ACP `SessionId`, +stores the mapping in thread metadata, lists external sessions for import, and registers the +local thread before `session/load` completes so history replay notifications have a target. + +Local validation with `acpx --agent "dim acp"` against `dimcode 0.0.76-beta.0` confirmed that +DimCode advertises `loadSession: true` plus `sessionCapabilities.list/resume/close`, returns +sessions through `session/list`, and accepts both `session/load` and `session/resume` for the +same ACP `sessionId`. + +## Goal + +Keep DeepChat's local ACP sessions and the underlying ACP agent's sessions aligned enough that +users can discover, open, and continue agent-owned sessions from DeepChat without creating +duplicate conversations or losing the ACP session identity. + +## User Stories + +1. As a user who has existing sessions in an ACP agent, I can refresh/sync that agent in + DeepChat and see matching sessions in DeepChat's session list for the relevant workdir. +2. As a user who opens a synced ACP session, DeepChat loads or resumes the original ACP + `sessionId` instead of creating a new agent-side conversation. +3. As a user switching between DeepChat and another ACP client, title and updated-time changes + from the agent stay reflected in DeepChat when the agent sends `session_info_update`. +4. As a user restarting DeepChat, local runtime cleanup does not erase the saved ACP `sessionId`; + the next open can reuse the agent-side session. +5. As a user opening a synced session with `session/load`, DeepChat imports the replayed history + into local messages without showing replayed history as the current prompt response. +6. As a user whose local DeepChat ACP session outlives the agent-side session, I keep my local + history and can continue from it, even if DeepChat must create a fresh ACP session upstream. + +## Acceptance Criteria + +- DeepChat can call ACP `session/list` only after initialization confirms + `agentCapabilities.sessionCapabilities.list`. +- DeepChat paginates `session/list` until `nextCursor` is absent or unchanged. +- Sync is scoped by ACP agent id and optionally by absolute workdir (`cwd` filter). A missing + remote session in a filtered result never deletes a local DeepChat session. +- A DeepChat-only ACP session is treated as local user history, not garbage. It remains visible + in DeepChat unless the user deletes or hides the local session. +- Local removal and remote deletion are separate actions. DeepChat must not call an ACP remote + delete operation unless the agent explicitly advertises that capability. +- When a synced remote-backed ACP session is removed only from DeepChat, DeepChat records a local + hide/tombstone entry keyed by agent id and ACP `sessionId` so the next `session/list` sync does + not silently re-import it. +- ACP-backed local removal keeps a minimal hidden conversation/mapping record and purges local + copied chat artifacts, including messages, assistant blocks, search documents, offload files, + runtime/session caches, and permission state where applicable. The remote ACP session remains + untouched. +- If the agent does not support remote session deletion, the UI must hide or disable "delete from + agent" actions and present the operation as local-only removal/hiding. +- If the agent supports remote session deletion in the future, DeepChat may expose an explicit + confirmed "delete from agent" action, but local removal remains available and must not be + confused with runtime close. +- If an unfiltered `session/list` does not include a known ACP `sessionId`, DeepChat may mark the + remote linkage as `not_listed`/unknown in metadata, but it must not delete local messages or + clear the active mapping solely from list absence. +- For each remote ACP session, DeepChat creates or updates: + - one local `new_sessions` row whose `id` remains a DeepChat conversation id, + - one `deepchat_sessions` row with `provider_id = 'acp'` and `model_id = agentId`, + - one `acp_sessions` row mapping the local conversation id to the remote ACP `sessionId`. +- The ACP `sessionId` is never used as the local DeepChat session id. +- Re-sync is idempotent: an existing `(agentId, acpSessionId)` updates metadata instead of + creating another local session. +- Local session list ordering uses the remote `updatedAt` when it is valid; invalid or missing + timestamps fall back to the sync time without breaking pagination. +- Runtime release, app quit, workdir change cleanup, and provider refresh do not clear a + persisted ACP `sessionId`. Local ACP removal may hide the conversation and purge local copied + chat data, but it preserves enough ACP identity to suppress automatic re-import. +- Opening a local ACP session with a persisted ACP `sessionId` attempts: + 1. `session/load` when `loadSession` is supported, + 2. `session/resume` when load is not supported and resume is supported, + 3. `session/new` only when neither reuse method is available or the persisted id is rejected. +- If `session/load` or `session/resume` confirms the persisted ACP `sessionId` no longer exists, + DeepChat marks the linkage stale, preserves the old ACP id in metadata, keeps all local + messages, and creates a new ACP session only when the user continues the conversation. +- When a stale DeepChat-only session creates a fresh ACP session, DeepChat updates the current + `acp_sessions.session_id` to the new remote id and retains the previous missing id in metadata + for diagnostics. +- During `session/load`, history replay notifications are persisted as historical messages and + are not pushed into the active prompt stream. +- `session_info_update` with a valid title and/or `updatedAt` updates local session metadata, + refreshes search documents, and emits the existing session-list update event. +- Existing locally-created ACP sessions keep working, including fallback to `session/new` when + an older agent does not support load/list/resume. + +## Non-Goals + +- Unconditional remote agent history deletion from DeepChat. This feature syncs and reuses + sessions; remote delete is only a future optional workflow for agents that advertise support. +- Full bidirectional message editing sync. DeepChat imports/reuses history and receives live + updates, but editing local historical messages does not rewrite the ACP agent's storage. +- Syncing every ACP agent at startup. The first implementation should be scoped and demand-driven + to avoid surprising session-list churn. +- Using unstable ACP fields as hard requirements. Optional message IDs and metadata improve + idempotency but must have safe fallbacks. +- Treating ACP as authoritative over local DeepChat history. Agent-side absence does not imply + local deletion. +- Changing non-ACP DeepChat session behavior. + +## Constraints + +- Follow DeepChat's SDD flow and existing Presenter pattern. +- Prefer typed routes/events for new renderer-main API; do not add new `useLegacyPresenter()` + call sites. +- Preserve current ACP workdir requirements: an ACP session needs an absolute project/workdir. +- Keep `new_sessions.id` stable and local. Treat ACP `sessionId` as external identity stored in + `acp_sessions`. +- Do not assume ACP `sessionId` is globally unique across all agents; lookup should be scoped by + agent id where possible. +- History replay import must use structured message storage paths so user-message hot tables, + assistant blocks, search documents, usage stats, and tape facts stay consistent. + +## Research Notes + +- DeepChat current mapping: + - `new_sessions.id` is the local conversation id. + - `acp_sessions` stores `conversation_id`, `agent_id`, `session_id`, `workdir`, and `status`. + - `AcpSessionManager.initializeSession` only checks the local persisted ACP `sessionId`; it + never discovers agent-side sessions. + - `AcpContentMapper` currently ignores `session_info_update` and `user_message_chunk`. +- Zed reference: + - `crates/acp_thread/src/connection.rs` defines session list/load/resume abstractions. + - `crates/agent_servers/src/acp.rs` maps `session/list`, `session/load`, and `session/resume`. + - `AcpConnection::open_or_create_session` de-duplicates active and pending loads by ACP + `SessionId` and registers the session before the load RPC returns. + - `ThreadMetadata` stores local `ThreadId` plus optional ACP `session_id`; external sessions + are imported by creating a new local thread id while preserving the ACP id. diff --git a/docs/features/acp-session-sync/tasks.md b/docs/features/acp-session-sync/tasks.md new file mode 100644 index 000000000..671297225 --- /dev/null +++ b/docs/features/acp-session-sync/tasks.md @@ -0,0 +1,79 @@ +# ACP Session Sync Tasks + +## Phase 1: Capability And Catalog Sync + +- [ ] Add ACP session capability parsing for `sessionCapabilities.list`, `resume`, and `close`. +- [ ] Add `AcpProvider` / `LLMProviderPresenter` API to list remote ACP sessions with cursor + pagination and optional `cwd` filtering. +- [ ] Add debug inspector actions for `listSessions` and `resumeSession`. +- [ ] Add SQLite/persistence helpers to find ACP mappings by `(agentId, acpSessionId)`. +- [ ] Add a local hidden/tombstone persistence model for remote-backed ACP sessions that keeps a + minimal hidden conversation plus ACP mapping after users remove them from DeepChat. +- [ ] Decide and implement safe uniqueness for ACP session ids, scoped by agent id where possible. +- [ ] Add `AgentSessionPresenter.syncAcpAgentSessions` to upsert `new_sessions`, + `deepchat_sessions`, and `acp_sessions`. +- [ ] Track remote observation metadata without deleting local sessions when `session/list` + omits a known ACP `sessionId`. +- [ ] Make `session/list` import skip locally hidden/tombstoned ACP session ids unless the user + explicitly requests re-import. +- [ ] Add typed route/client surface for triggering ACP session sync from renderer code. +- [ ] Emit existing session-list update events after sync. + +## Phase 2: Stable Reuse Semantics + +- [ ] Split ACP runtime release from ACP mapping deletion. +- [ ] Update app quit, provider refresh, workdir-change cleanup, and process release paths to + preserve persisted ACP `sessionId`. +- [ ] Make ACP-backed local removal purge copied chat data while preserving the hidden + conversation/mapping tombstone; do not call remote delete in this feature. +- [ ] Ensure ACP-backed local removal clears messages, assistant blocks, search documents, offload + files, runtime caches, and permission/tool approval state for the conversation. +- [ ] Hide or disable any "delete from agent" UI affordance when the ACP agent does not advertise + remote delete support. +- [ ] Add resume fallback in `AcpSessionManager.initializeSession` when load is unsupported. +- [ ] Mark mappings stale when `session/load` or `session/resume` confirms the remote session is + missing, while preserving local messages. +- [ ] Rebind a stale local ACP session to a fresh ACP `session/new` only when the user continues + the conversation. +- [ ] De-duplicate pending and active sessions by ACP `sessionId` when opening a known remote + session. +- [ ] Register load replay handling before `session/load` can emit history notifications. + +## Phase 3: Metadata And History Replay + +- [ ] Surface `session_info_update` from ACP notifications. +- [ ] Update local session title and `updatedAt` from valid `session_info_update` payloads. +- [ ] Add `AcpSessionReplayImporter` for `session/load` history replay. +- [ ] Persist replayed user messages, assistant blocks, plans, and tool-call updates through + structured message storage. +- [ ] Store ACP provenance for imported messages. +- [ ] Make replay idempotent when ACP message ids are present. +- [ ] Add safe fallback behavior when ACP message ids are absent. + +## Phase 4: UI Integration + +- [ ] Add a renderer entry point to refresh/sync ACP sessions for the selected ACP agent and + workdir. +- [ ] Show synced ACP sessions in the existing session list using local DeepChat session ids. +- [ ] Preserve current draft creation flow when no remote session is selected. +- [ ] Show a quiet warning/status when a local ACP session had to continue with a new remote ACP + session because the old agent-side session was missing. +- [ ] Add loading/error/empty states for unsupported `session/list` and sync failures. + +## Phase 5: Verification + +- [ ] Add unit tests for capability parsing and unsupported-agent behavior. +- [ ] Add unit tests for `session/list` pagination. +- [ ] Add unit tests for idempotent local import and metadata updates. +- [ ] Add unit tests proving runtime release preserves persisted ACP `sessionId`. +- [ ] Add unit tests for load/resume/new selection. +- [ ] Add unit tests for DeepChat-only sessions: list absence does not delete them, confirmed load + miss marks stale, and next send rebinds to a new ACP session. +- [ ] Add unit tests that a local-only removal tombstone prevents automatic re-import. +- [ ] Add unit tests proving unsupported ACP remote delete is not exposed or called. +- [ ] Add unit tests for replay importer message grouping and idempotency. +- [ ] Add renderer/store tests for session-list refresh after sync. +- [ ] Manually validate against DimCode using `acpx --agent "dim acp"`. +- [ ] Run `pnpm run format`. +- [ ] Run `pnpm run i18n`. +- [ ] Run `pnpm run lint`.