design session delete - sinrimin/harness-nexus GitHub Wiki
Issue #55 Β· shipped 2026-10-15 (Phase A). The rail finally deletes: desktop hover-trash, phone swipe-left, both rails (platform assistant rows + native agent rows).
ACP: session/delete is a STABILIZED method (RFD completed 2026-06-05) β
params {sessionId}, result {}, idempotent ("deleting an already-deleted
session, or one that never existed, SHOULD succeed silently"). Capability bit
agentCapabilities.sessionCapabilities.delete β an OBJECT ({}), not a
boolean; the daemon's deriveSessionCaps reads presence (!== undefined),
which matches.
| target | delete channel | ground truth |
|---|---|---|
| claude-code | ACP session/delete
|
pinned @agentclientprotocol/[email protected] implements + advertises it; internally calls the Claude Agent SDK's own deleteSession (tears down in-memory state before removing the transcript) |
| opencode | ACP, version-gated |
v2.0.7+ only (v2.0.0β2.0.6 and ALL 1.x lack it; 1.18.31 verified locally). V2 moved persistence to SQLite, so file deletion is not an option. Absent cap β honest "does not support session/delete" error. Current stable line is 2.0.29 under npm @opencode/cli (old @opencode-ai/opencode name is gone) |
| codex | β no ACP support in ANY @zed-industries/codex-acp (0.3.1β0.16.0) | Phase B: shell out to the codex CLI's own codex delete (app-server thread/delete). Do NOT unlink rollout files β codex keeps ~/.codex/session_index.jsonl + SQLite state rows + a rollout reference graph; a raw unlink leaves them stale |
| deepseek (dsh) | file |
~/.dsh/sessions/<slug>/<sessionId>/ β the id IS the directory name (generations session.vN.jsonl.zstd coexist inside); delete the whole directory. dsh's ACP profile explicitly does not implement session/delete (their design note says it "remains unsupported"). Residual: the opt-in derived SQLite FTS index (session-query-sqlite) is not refreshed by an unlink β not enabled in shipped compositions |
| pi | file |
~/.pi/agent/sessions/<group>/<ts>_<id>.jsonl; unlink is officially sanctioned by pi's session-format docs (listing is a plain readdir). Residual: a FORKED child carries a parentSession file pointer β deleting a parent leaves a dangling reference in children (pi behavior undocumented) |
Repo notes: opencode lives at anomalyco/opencode (default branch dev),
the ACP spec at agentclientprotocol/agent-client-protocol, pi at
earendil-works/pi, dsh is public at deepseek-ai/deepseek-harness
(npm @deepseek-ai/dsh, floor 0.2.0-rc.2 per #46).
The SPEC leaves deleting a live/loaded session implementation-defined; we are STRICTER (close-first, below) as a product rule.
-
shared:
sessionsDeleteRequestSchema{requestId, target, sessionId}βsessionsDeleteResultEventSchema{requestId, error?}β same requestId/result round-trip shape assessions:list. -
daemon capability bit
sessions-deleteadded toDAEMON_CAPABILITIES. The server 409sDAEMON_NO_SESSIONS_DELETE(upgrade hint) against pre-#55 daemons instead of timing out on a request they never answer. -
REST:
DELETE /api/agent-instances/:id/sessions/:nativeSessionIdβ owner-or-admin gate like the listing, machine-online + capability checks, live-channel guard, then the /ctl round-trip (SessionsCoordinatorgrew a delete waiter arm;failMachinesweeps both). Error mapping mirrors the listing: 502DAEMON_SESSION_DELETE_FAILED, 409MACHINE_OFFLINE, 504SESSIONS_DELETE_TIMEOUT. -
Assistant REST (
DELETE /api/assistant-sessions/:id, pre-existing): ownership FIRST (404-hides foreign ids), then the live guard (409ASSISTANT_SESSION_LIVEβ close the chat first), then the hard delete (cascade is clean: messages + tool calls ON DELETE CASCADE), thenpushSessions(owner)so every rail drops the row immediately.
sessions:delete mirrors the listing's dispatch table, plus:
-
Live guard, all arms β
liveNativeIdsFor(target)(a newChatHandlersHandleseam over the chat registry'sacpSessionIds) refuses a session an open channel holds: "session is open in a live channel β close it first". The server refuses first; this re-checks against the daemon's own table so a racing open cannot slip under. -
ACP trio (claude-code/codex/opencode): live connection
session/deletefirst (concurrent request, no spawn), else a short-lived adapter spawn.AcpSessionCapsgrew adeletebit; the spawn path is CAPABILITY-GATED β an adapter that did not advertise it (codex today, opencode 1.x) gets a clear "does not support session/delete" error rather than a method-not-found rejection. Upstream shipping it later lights the path up with zero daemon changes. -
dsh:
findSessionDir(new export;findTranscriptrefolds onto it) resolves~/.dsh/sessions/*/<id>;rmSync(recursive, force). -
pi:
piFindSessionFile(the rail's existing resolver) β unlink. - File arms are IDEMPOTENT (absent = success), matching ACP semantics. A successful delete drops the target's listing-cache entry.
-
components/chat/swipe-row.tsxβ hand-written touch swipe (zero-dep, the repo carries no gesture library): translateX driven by touch deltas, 6px slop-locked axis (vertical scrolls never fight it), rubber-band at reveal +24px, half-width snap. The moving layer is opaque (bg-background) so the destructive reveal (w-[72px],bg-destructive) is hidden until swiped. The reveal is inset 2px vertically β rows carry fractional heights and WebKit rounds aninset-y-0absolute layer and the in-flow cover differently, which peeked a 1px red sliver below fractionally-unlucky rows on iOS (post-ship fix, not reproducible in Chromium). -
components/chat/session-row.tsxβ the shared affordance shell: desktop hover trash (opacity-0 β group-hover/row:opacity-100, also focus-visible) + the phone SwipeRow lane (md:hidden);onDelete === undefinedrenders neither (a live/open row must not offer what the server refuses). - Both rails wrap their existing row buttons verbatim (the shell never
restructures the row); confirm rides the kit
ConfirmDialog(danger, irreversible, impact = the row title). Errors toastchat.deleteFailed {error}(the server's message rides through β e.g.DAEMON_NO_SESSIONS_DELETEupgrade hint, adapter-unsupported reason). - Native rows hide the affordance while a channel holds them
(
attached !== undefined); assistant rows while viewed or busy. - i18n:
chat.deleteSessionTitle/Action/Consequence,chat.deleteFailed.
- Unit: cli
sessions-delete.test.ts(7 β fixture adapter with/without the cap, live-connection zero-spawn, dsh dir, pi file, live guard, proto errors), serversessions-route.test.tsdelete describe (5) +assistant-delete.test.ts(3 β foreign 404, live 409 + push, idle delete), webswipe-row.test.tsx(5 β axis lock, clamp/rubber, snap, onDelete, affordance presence). - smoke:
[#55] native session deleteblock (ACP arm round-trip + idempotency, dsh file arm + disk check + empty listing) β 486/486. - Browser (docker rig, real machine + overlaid daemon): desktop hoverβtrashβ
confirm on an assistant row and on a real claude-code row (the REAL adapter
deleted the transcript from
~/.claude/projectsβ verified on disk); 375px drawer delete via the SwipeRow reveal β confirm β row + DB gone; dsh/pi arms via API against the real machine's stores. - Known gaps: the literal finger-drag could not be driven live (synthetic
TouchEvents don't reach React's delegated handlers from a Radix portal;
agent-browser has no CDP touch dispatch) β the gesture math is unit-tested
and the reveal+delete flow was clicked for real. opencode's rail on the
dev rig is down for a PRE-EXISTING config break (its
opencode.jsonlacks a required key), so its degradation toast was not exercised live (covered by unit test).
Rig recon (codex-cli 0.154.0): codex delete --force <SESSION> accepts the
session UUID (exactly what the ACP sessionId is β the rollout filename uuid),
requires --force headless ("cannot confirm session deletion without an
interactive terminal"), and is NOT idempotent (a repeat exits 1 "failed to
delete session"). The daemon arm therefore uses the ROLLOUT FILE as its
truth: absent = already gone = success without spawning the CLI; a nonzero
exit still counts when the file verifiably went away (a race settled in our
favor). findCodexRollout (exported) scans ~/.codex/sessions/<Y>/<M>/<D>
for a filename containing the id. When codex-acp eventually implements
session/delete, the CLI path stays β it is the vendor's own cleanup
(rollout + session index + SQLite state together), equally correct.