fix(desktop): route remote-profile session mutations + fix unified-list pagination
Follow-up to the read-routing fix: make remote-profile sessions fully
first-class, not just resumable.
Mutations (rename/archive/delete) went through the same hermes:api handler but
never carried the owning profile, so they hit the local primary's state.db --
which has no row for a remote session. Deleting/archiving/renaming a remote
session silently no-op'd or 404'd, and the row reappeared on next refresh.
- hermes.ts: setSessionArchived/deleteSession/renameSession take the owning
profile and pass it as request.profile so Electron routes to that profile's
backend (matching the read path). Callers now forward session.profile.
- main.cjs: generalize the intercept (read -> request) to also reroute
DELETE/PATCH on /api/sessions/{id} for remote profiles, stripping the profile
param (the remote serves its own state.db; no cross-profile semantics there).
- web_server.py: DELETE /api/sessions/{id} gains a profile param for parity with
GET/PATCH (local cross-profile delete).
Also fix the unified-list merge: it concatenated each remote's page onto the
primary's without re-windowing, so a limit=N request could return up to
N*(1+remotes) rows and report the primary's (stale) total. Now it over-fetches
limit+offset from each remote (from offset 0), re-sorts by recency, re-windows
to the page, and recomputes total/profile_totals from the remote counts.
Verified live against a remote backend: rename/archive/delete mutate the remote
db; page 1 windows to limit, profile_totals reflect remote counts, page 2 has no
overlap with page 1. tsc -b clean; connection-config tests pass.
This commit is contained in:
@@ -763,7 +763,7 @@ export function useSessionActions({
|
||||
await requestGateway('session.close', { session_id: closingRuntimeId }).catch(() => undefined)
|
||||
}
|
||||
|
||||
await deleteSession(storedSessionId)
|
||||
await deleteSession(storedSessionId, removed?.profile)
|
||||
clearQueuedPrompts(storedSessionId)
|
||||
|
||||
if (closingRuntimeId) {
|
||||
@@ -839,7 +839,7 @@ export function useSessionActions({
|
||||
}
|
||||
|
||||
try {
|
||||
await setSessionArchived(storedSessionId, true)
|
||||
await setSessionArchived(storedSessionId, true, archived?.profile)
|
||||
notify({ durationMs: 2_000, kind: 'success', message: 'Archived' })
|
||||
} catch (err) {
|
||||
if (archived) {
|
||||
|
||||
@@ -57,7 +57,7 @@ export function SessionsSettings() {
|
||||
setBusyId(session.id)
|
||||
|
||||
try {
|
||||
await setSessionArchived(session.id, false)
|
||||
await setSessionArchived(session.id, false, session.profile)
|
||||
setLocalSessions(prev => prev.filter(s => s.id !== session.id))
|
||||
// Surface it again in the sidebar without waiting for a full refresh.
|
||||
setSessions(prev => [{ ...session, archived: false }, ...prev.filter(s => s.id !== session.id)])
|
||||
@@ -78,7 +78,7 @@ export function SessionsSettings() {
|
||||
setBusyId(session.id)
|
||||
|
||||
try {
|
||||
await deleteSession(session.id)
|
||||
await deleteSession(session.id, session.profile)
|
||||
setLocalSessions(prev => prev.filter(s => s.id !== session.id))
|
||||
triggerHaptic('warning')
|
||||
} catch (err) {
|
||||
|
||||
@@ -166,8 +166,13 @@ export async function listAllProfileSessions(
|
||||
}
|
||||
}
|
||||
|
||||
export function setSessionArchived(id: string, archived: boolean): Promise<{ ok: boolean }> {
|
||||
// Mutations take the owning `profile` so Electron routes them to that profile's
|
||||
// backend (remote pool or local primary) via request.profile — matching the
|
||||
// read path. A remote session's row lives only on its remote host, so a mutation
|
||||
// that hit the local primary would no-op or 404. Omit for the current/default.
|
||||
export function setSessionArchived(id: string, archived: boolean, profile?: string | null): Promise<{ ok: boolean }> {
|
||||
return window.hermesDesktop.api<{ ok: boolean }>({
|
||||
...(profile ? { profile } : {}),
|
||||
path: `/api/sessions/${encodeURIComponent(id)}`,
|
||||
method: 'PATCH',
|
||||
body: { archived }
|
||||
@@ -180,8 +185,10 @@ export function searchSessions(query: string): Promise<SessionSearchResponse> {
|
||||
})
|
||||
}
|
||||
|
||||
// `profile` reads another profile's transcript straight off its state.db via the
|
||||
// primary backend (no spawn). Omit for the current/default profile.
|
||||
// Reads another profile's transcript. For a remote profile Electron reroutes
|
||||
// this GET to the remote backend (which serves its own state.db); for a local
|
||||
// profile the primary opens that profile's state.db via ?profile=. Omit for
|
||||
// the current/default profile.
|
||||
export function getSessionMessages(id: string, profile?: string | null): Promise<SessionMessagesResponse> {
|
||||
const suffix = profile ? `?profile=${encodeURIComponent(profile)}` : ''
|
||||
|
||||
@@ -190,8 +197,9 @@ export function getSessionMessages(id: string, profile?: string | null): Promise
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteSession(id: string): Promise<{ ok: boolean }> {
|
||||
export function deleteSession(id: string, profile?: string | null): Promise<{ ok: boolean }> {
|
||||
return window.hermesDesktop.api<{ ok: boolean }>({
|
||||
...(profile ? { profile } : {}),
|
||||
path: `/api/sessions/${encodeURIComponent(id)}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
@@ -203,6 +211,7 @@ export function renameSession(
|
||||
profile?: string | null
|
||||
): Promise<{ ok: boolean; title: string }> {
|
||||
return window.hermesDesktop.api<{ ok: boolean; title: string }>({
|
||||
...(profile ? { profile } : {}),
|
||||
path: `/api/sessions/${encodeURIComponent(id)}`,
|
||||
method: 'PATCH',
|
||||
body: { title, ...(profile ? { profile } : {}) }
|
||||
|
||||
Reference in New Issue
Block a user