Order the session list by when each agent was turned on

A running session no longer moves: the ones with a process come first,
oldest start first, so starting one appends it to the bottom of that
group and nothing it goes on to do -- beginning a turn, finishing one,
asking a question -- can shift it. Sorting by activity with the
awaiting-answer ones floated to the top is what this replaces; the
status word and its colour already say which session wants something
without the row having to move to say it. Stopped sessions are a group
below, most recently active first.

The order is the server's: `SessionConfig::started` is written each time
a process is started for a session and reported as `started`, so it is
the same on every device and survives a backend restart -- which adopts
processes rather than starting them, and so could not work the times out
for itself. Applied on the phone, because presentation order is a
display decision.

`LiveSession::info` takes the session's config entry rather than a
parameter per field read from it, which is what `AutoResumeView` existed
to bundle; that goes.

Verified on the emulator against the sandbox: three echo sessions kept
their order while the newest-active one was messaged; a stopped and
restarted session moved below one started after it; a stopped session
dropped below every running one; and after a backend restart the
recorded times came back unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-15 23:54:36 -04:00
1 parent a9cfea89e5
commit cbae7ee8c0
6 files changed
+255 -64

No files matched your search

@@ -225,6 +225,15 @@ data class SessionSummary(
val usageProvider: String?,
val status: String,
val lastActivity: Double,
/**
* Epoch seconds a process was last started for this session -- when this agent was last turned
* on. What the list orders the running sessions by; see [sessionsInListOrder].
*
* The server's own, so the order is the same on every device and across a backend restart. An
* older server does not send it, and those sessions fall back to when the session was last
* active, which is the best this app can do without inventing a time.
*/
val started: Double,
/** Latest measured number of live background tasks; zero also covers older servers. */
val backgroundTasks: Int,
/**
@@ -265,6 +274,7 @@ private fun parseSession(session: JSONObject) =
usageProvider = session.optString("usageProvider").ifEmpty { null },
status = session.getString("status"),
lastActivity = session.getDouble("lastActivity"),
started = session.optDouble("started", session.getDouble("lastActivity")),
backgroundTasks = session.optInt("backgroundTasks", 0),
subagents = session.optInt("subagents", 0),
)
@@ -257,12 +257,7 @@ fun SessionListScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
// Awaiting-answer first (the point of the screen), then most recently active.
val ordered =
state.value.sortedWith(
compareByDescending<SessionSummary> { it.status == "awaitingInput" }
.thenByDescending { it.lastActivity }
)
val ordered = sessionsInListOrder(state.value)
LazyColumn {
uniqueItems(ordered, key = { it.id }) { session ->
SessionCard(
@@ -472,6 +467,27 @@ fun SessionListScreen(
}
}
/**
* The order sessions are drawn in: the ones with a process, oldest agent first, then the stopped
* ones, most recently active first. A display decision, made here rather than on the server, which
* answers in config order -- see UI_RULES.
*
* The running group is ordered by when each agent was *turned on* rather than by what it is doing,
* so a session cannot move by beginning a turn, finishing one or asking a question, and a list a
* reader has their place in stays still. That replaces sorting by activity and floating the
* awaiting-answer ones to the top: the status word and its colour already say which session wants
* something, without the row having to move to say it. [SessionSummary.started] is the server's, so
* the order is the same on every device and survives a backend restart.
*
* Stopped sessions are a group of their own because "turned on" is what the order above is made of.
* A session nobody could ask about is not one of them -- it has a process, and dropping it out of
* the running group the moment a machine goes quiet is the move this exists to prevent.
*/
fun sessionsInListOrder(sessions: List<SessionSummary>): List<SessionSummary> {
val (running, stopped) = sessions.partition { it.status != "exited" }
return running.sortedBy { it.started } + stopped.sortedByDescending { it.lastActivity }
}
/**
* The subagents picked out inside one session's card, and which card that is.
*