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:
@@ -49,14 +49,16 @@ export class WorkspaceNameConflictError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** An archiveSession request named a session neither live nor in session persistence. */
|
||||
/**
|
||||
* An archiveSession request named a session neither live nor in session
|
||||
* persistence — a definite miss only; storage faults propagate as themselves.
|
||||
*/
|
||||
export class WorkspaceUnknownSessionError extends Error {
|
||||
/**
|
||||
* @param sessionId - The unknown session id.
|
||||
* @param options - Standard error options (the header-read failure as `cause`).
|
||||
*/
|
||||
constructor(readonly sessionId: SessionId, options?: ErrorOptions) {
|
||||
super(`cannot archive session '${sessionId}': live sessions and session persistence hold no such session`, options)
|
||||
constructor(readonly sessionId: SessionId) {
|
||||
super(`cannot archive session '${sessionId}': live sessions and session persistence hold no such session`)
|
||||
this.name = 'WorkspaceUnknownSessionError'
|
||||
}
|
||||
}
|
||||
@@ -215,16 +217,27 @@ export class WorkspaceRegistry extends Service {
|
||||
// The chain slot serializes against every other registry write, so this
|
||||
// check-then-write pair cannot interleave with another archive.
|
||||
if (this.requireState().archivedSessionIds.includes(sessionId)) return
|
||||
try {
|
||||
await this.readSessionHeader(sessionId)
|
||||
} catch (error) {
|
||||
throw new WorkspaceUnknownSessionError(sessionId, { cause: error })
|
||||
if (!(await this.sessionKnown(sessionId))) {
|
||||
throw new WorkspaceUnknownSessionError(sessionId)
|
||||
}
|
||||
const state = this.requireState()
|
||||
await this.setState({ ...state, archivedSessionIds: [...state.archivedSessionIds, sessionId] })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a session is live, header-indexed, or present in a fresh
|
||||
* persistence listing. Only a definite miss returns false — a failing
|
||||
* `sessionPersistence.list()` propagates so storage faults never
|
||||
* masquerade as an unknown session.
|
||||
*/
|
||||
private async sessionKnown(id: SessionId): Promise<boolean> {
|
||||
if (this.ctx.get('sessions')?.get(id) !== undefined) return true
|
||||
if (this.headers.has(id)) return true
|
||||
await this.indexHeaders(await this.ctx.sessionPersistence.list())
|
||||
return this.headers.has(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve by canonical directory path without creating or mutating a
|
||||
* workspace. A missing path rejects during `realpath`; an existing unowned
|
||||
|
||||
Reference in New Issue
Block a user