fix: review follow-ups for the session archive set

- WorkspaceRegistry.archiveSession no longer wraps persistence-listing
  failures as WorkspaceUnknownSessionError: only a definite miss (live
  lookup, header index, then a fresh list) maps to session-not-found;
  storage faults propagate as internal errors, with a negative test.
- The archived-current sweep moves from the unary path into the
  projection: any install path (local echo, another tab's frame, a
  reconnect baseline) clears a selection that landed in the archive set.
- An archive set installed while workspace.list is in flight supersedes
  the stale baseline's set instead of being rolled back by it.
- The workspace-management e2e anchors the archived row by its session
  actions button and asserts the single-stray fixture assumption loudly.
- Drop the stale touchSession rows from the workspace READMEs (the
  method was removed with its Agent Note).
This commit is contained in:
imccyu
2026-07-31 03:44:35 +08:00
committed by imccyu
parent c00f0dccda
commit d3e8f17a54
15 changed files with 103 additions and 26 deletions

View File

@@ -38,6 +38,13 @@ export class WorkspaceManager {
private error: RpcError | null = null
private inflight: Promise<void> | null = null
private refreshFrames: WorkspaceDelta[] | null = null
/**
* True once a frame or unary echo installed the archive set while a list
* request was in flight: that install is newer than the pending baseline,
* so the baseline's (older) set must not roll it back — the archive
* mirror of replaying refreshFrames over the item baseline.
*/
private archivedSupersedesRefresh = false
/**
* Ids this process has seen removed, kept for the connection's lifetime so
* a late changed frame or a stale baseline row cannot resurrect a deleted
@@ -82,7 +89,7 @@ export class WorkspaceManager {
items = items.filter(workspace => !this.removedIds.has(workspace.workspaceId))
for (const delta of frames) items = applyWorkspaceDelta(items, delta)
this.installViews(items)
this.installArchived(result.value.archivedSessionIds)
if (!this.archivedSupersedesRefresh) this.installArchived(result.value.archivedSessionIds)
this.state = 'idle'
this.phase = 'ready'
} else {
@@ -96,6 +103,7 @@ export class WorkspaceManager {
this.error = folded.ok ? null : folded.error
} finally {
this.refreshFrames = null
this.archivedSupersedesRefresh = false
this.inflight = null
this.notifier.markDirty()
}
@@ -224,6 +232,7 @@ export class WorkspaceManager {
/** Replace the archive set when membership actually changed (set identity backs Object.is short-circuits). */
private installArchived(archivedSessionIds: readonly SessionId[]): void {
if (this.refreshFrames !== null) this.archivedSupersedesRefresh = true
if (archivedSessionIds.length === this.archivedSessionIds.size
&& archivedSessionIds.every(id => this.archivedSessionIds.has(id))) return
this.archivedSessionIds = new Set(archivedSessionIds)

View File

@@ -260,15 +260,14 @@ export class WorkspacesService implements IWorkspaces {
}
/**
* Archive a session into the registry-global set. When the archived
* session is the current one, the selection is cleared into the New
* Session view state — a hidden row must not stay open behind the list.
* Archive a session into the registry-global set. Clearing an archived
* current selection is the projection sweep's job (one rule for the local
* echo and a remote tab's frame alike).
* @param sessionId - session to archive.
*/
async archiveSession(sessionId: SessionId): Promise<void> {
const result = await this.manager.archiveSession(sessionId)
if (!result.ok) throw new Error(`session archive failed: ${result.error.code}: ${result.error.message}`)
if (this.sessions.list.getSnapshot().current === sessionId) this.sessions.clear()
}
/**
@@ -313,6 +312,14 @@ export class WorkspacesService implements IWorkspaces {
const workspace = this.manager.getSnapshot()
const sessions = this.sessions.list.getSnapshot()
const baselinesReady = workspace.phase === 'ready' && sessions.phase === 'ready'
// An archived current selection clears into the New Session view state —
// a hidden row must not stay open behind the list. Sweeping here covers
// every install path with one rule: the local unary echo, another tab's
// changed frame, and a reconnect baseline restoring a persisted
// selection that was archived while this client was away.
if (sessions.current !== undefined && workspace.archivedSessionIds.has(sessions.current)) {
this.sessions.clear()
}
this.list.set({
items: workspace.items,
archivedSessionIds: workspace.archivedSessionIds,