Stop claiming a terminal, and stop calling a recoverable delete final

Two bugs Bryan hit, with one shape between them: a claim stronger than
the thing that was measured.

**The delete warning branched on `imported`.** It told him deleting
`ai-app` could be undone and deleting `manager` could not, when both are
claude-cli sessions whose conversations survive equally. `imported`
records how a session got into the app; what decides recoverability is
whether the *driver* keeps its own record -- the Claude Code CLI does,
under ~/.claude/projects, however the session started; echo and llama.cpp
do not, and for those the app's transcript is the only copy. So the fact
now sits on DriverKind and rides on SessionInfo, decided by the server
from the provider's kind rather than by the phone from its name, which a
person can change.

The comment above the branch asserted "a session started here has no copy
anywhere". That sentence was the bug written down and reasoned from, and
it is gone.

Neither branch promises a restore, which it should not: nothing here
checks the file is still on disk, and re-importing was never a restore
anyway -- this app's transcript holds images, peer messages and command
events the CLI's record never had. So the recoverable text says what is
known and names what goes either way. "Can't be undone" is now said only
where it is true, which is the point of saying it at all.

**"open in a terminal -- close it there first" named a place that need
not exist.** The detection is right and worth keeping: something live
holds that session, and importing it would reproduce the double-resume
incident. But which something was never measured. The live descriptors
here include two of this backend's own adopted sessions and a peer
agent's; none is a terminal, so the instruction sent the reader looking
for a window that was not there.

**And this app did not recognise its own spawned sessions.** The import
list filters out what the app is already driving, but it matched only the
import cursor -- which exists solely for imported sessions. Every session
the app spawned therefore stayed in the list, marked in use, telling the
reader to go and close it somewhere: here. Matching the resume token too,
which both kinds have, is the fix; `session_importing` is now
`session_driving`, because that is what it was always being asked.

Verified on the emulator against a scratch backend: a spawned claude-cli
session reports keepsOwnTranscript true with imported false -- Bryan's
`manager` case exactly -- and draws the recoverable warning; the echo
session draws "can't be undone"; and once the CLI named itself, the
spawned session's id was absent from the import list, where the old match
would have listed it.

75 tests, clippy and rustfmt clean; ktfmt, compileDebugKotlin and
lintDebug clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VETa8afmpWaYezLCqJhDB8
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-29 20:34:03 -04:00
1 parent 5f6fd34aba
commit 71067275e2
7 files changed
+181 -29

No files matched your search

@@ -125,6 +125,16 @@ data class SessionSummary(
val provider: String,
val title: String,
val model: String?,
/**
* Whether the conversation would outlive deleting this session, decided by the server from the
* provider's kind rather than here from its name.
*
* What it licenses is narrow, and the delete dialog is worded to match: the driver keeps its
* own record of the conversation somewhere this app's delete does not reach. It is not a
* promise that the file is still there, and re-importing is not a restore -- this app's
* transcript holds things that record does not.
*/
val keepsOwnTranscript: Boolean,
/** How much the session asks before acting; null when it was never set. */
val permissionMode: String?,
/**
@@ -139,6 +149,7 @@ private fun parseSession(session: JSONObject) =
SessionSummary(
id = session.getString("id"),
setup = session.getString("setup"),
keepsOwnTranscript = session.optBoolean("keepsOwnTranscript", false),
setupName = session.getString("setupName"),
provider = session.getString("provider"),
title = session.getString("title"),
@@ -374,7 +374,12 @@ private fun statsOf(session: Importable, importing: String?): String =
*/
private fun warningOf(session: Importable): String? =
when (session.inUse) {
"yes" -> "open in a terminal — close it there first"
// What was measured is that a live process on that machine holds this session open. Which
// process is not measured, so it isn't claimed: "a terminal — close it there first" sent
// people looking for a window that need not exist. It is just as likely another agent, or
// this app on a session it spawned. Naming a place the reader then can't find turns a
// correct refusal into a wrong instruction.
"yes" -> "something on that machine is running it"
"unknown" -> "can't tell if it's open"
else -> null
}
@@ -168,16 +168,33 @@ fun SessionListScreen(
onDismissRequest = { confirmingDelete = null },
title = { Text("Delete \"${session.title}\"?") },
text = {
// Two different acts behind one button, so it says which one this is. An
// imported session's real transcript belongs to Claude Code and outlives
// this: removing it here undoes a view. A session started here has no copy
// anywhere, and removing it ends the conversation. Warning "this cannot be
// undone" of both would make the warning worthless where it is true.
// Two different acts behind one button, so it says which one this is. What
// separates them is whether the *driver* keeps its own record of the
// conversation -- the Claude Code CLI does, under ~/.claude/projects, whether
// this app spawned the session or imported it; echo and llama.cpp do not, and
// for those the app's transcript is the only copy there is.
//
// This used to branch on `imported`, above a comment asserting that "a session
// started here has no copy anywhere". That was simply false for every
// claude-cli session this app spawned, and the two warnings disagreed about
// sessions that were equally recoverable. Getting it wrong in that direction
// is the expensive one: "this can't be undone", said of something that can,
// spends the credibility the sentence needs on the sessions where it is true.
//
// Neither branch promises a restore. The recoverable one says what is known --
// the driver keeps its own record -- rather than that the file is still there,
// which nothing here checked; and it names what goes either way, because this
// app's transcript holds images, peer messages and commands that the CLI's own
// record never had.
Text(
if (session.imported)
"Stops the process and removes this app's copy. The conversation " +
"itself stays on the machine and can be imported again."
else "Kills the process and deletes its transcript. This can't be undone."
if (session.keepsOwnTranscript)
"Stops the process and deletes this app's copy of the conversation, " +
"including any images, peer messages and commands recorded only " +
"here. Claude Code keeps its own transcript on the machine, so the " +
"conversation itself should still be there to import again."
else
"Kills the process and deletes the conversation. Nothing else keeps a " +
"copy, so this can't be undone."
)
},
confirmButton = {