mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(workspace): make deletion recoverable
This commit is contained in:
@@ -109,6 +109,7 @@ export class WorkspaceRegistry extends Service {
|
||||
this.global = domain.global
|
||||
this.state = domain.global.get()
|
||||
|
||||
await this.recoverPendingMutation()
|
||||
this.validateStoredState(this.state)
|
||||
if (!this.state.initialized) {
|
||||
const headers = await this.ctx.sessionPersistence.list()
|
||||
@@ -218,10 +219,28 @@ export class WorkspaceRegistry extends Service {
|
||||
}
|
||||
const entity = new WorkspaceEntity(this.host, id, record)
|
||||
this.entities.set(id, entity)
|
||||
const pendingState: WorkspaceDomainState = {
|
||||
...state,
|
||||
pendingMutation: { operation: 'create', workspaceId: id },
|
||||
}
|
||||
try {
|
||||
await this.setState(pendingState)
|
||||
} catch (error) {
|
||||
this.entities.delete(id)
|
||||
throw error
|
||||
}
|
||||
try {
|
||||
await table.put(id, record)
|
||||
} catch (error) {
|
||||
this.entities.delete(id)
|
||||
try {
|
||||
await this.setState(state)
|
||||
} catch (rollbackError) {
|
||||
throw new AggregateError(
|
||||
[error, rollbackError],
|
||||
`workspace '${id}' record write and pending-marker rollback both failed`,
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -232,10 +251,17 @@ export class WorkspaceRegistry extends Service {
|
||||
try {
|
||||
await table.delete(id)
|
||||
} catch (rollbackError) {
|
||||
this.entities.set(id, entity)
|
||||
throw new AggregateError(
|
||||
[error, rollbackError],
|
||||
`workspace '${id}' was stored but its registry order and rollback both failed`,
|
||||
`workspace '${id}' order write and record rollback both failed; the pending marker remains recoverable`,
|
||||
)
|
||||
}
|
||||
try {
|
||||
await this.setState(state)
|
||||
} catch (rollbackError) {
|
||||
throw new AggregateError(
|
||||
[error, rollbackError],
|
||||
`workspace '${id}' order write and pending-marker rollback both failed`,
|
||||
)
|
||||
}
|
||||
throw error
|
||||
@@ -251,7 +277,10 @@ export class WorkspaceRegistry extends Service {
|
||||
initialized: true,
|
||||
workspaceIds: state.workspaceIds.filter(workspaceId => workspaceId !== id),
|
||||
}
|
||||
await this.setState(nextState)
|
||||
await this.setState({
|
||||
...nextState,
|
||||
pendingMutation: { operation: 'delete', workspaceId: id },
|
||||
})
|
||||
this.entities.delete(id)
|
||||
try {
|
||||
await this.requireTable().delete(id)
|
||||
@@ -260,6 +289,10 @@ export class WorkspaceRegistry extends Service {
|
||||
try {
|
||||
await this.setState(state)
|
||||
} catch (rollbackError) {
|
||||
// The durable marker still says to finish deletion, so the cache must
|
||||
// agree with that recoverable direction rather than republish a row
|
||||
// absent from the persisted order.
|
||||
this.entities.delete(id)
|
||||
throw new AggregateError(
|
||||
[error, rollbackError],
|
||||
`workspace '${id}' record deletion and registry-order rollback both failed`,
|
||||
@@ -267,9 +300,38 @@ export class WorkspaceRegistry extends Service {
|
||||
}
|
||||
throw error
|
||||
}
|
||||
try {
|
||||
await this.setState(nextState)
|
||||
} catch (error) {
|
||||
// The deletion committed at the table write and was already published
|
||||
// to Host streams. Keep the durable marker for startup recovery rather
|
||||
// than reporting failure after the requested state became true.
|
||||
this.ctx.logger.warn(
|
||||
`workspace '${id}' was deleted but its pending marker could not be cleared: ${String(error)}`,
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the one mutation explicitly named by durable state. Unexplained
|
||||
* order/table divergence still reaches {@link validateStoredState} and
|
||||
* fails loud; this path never infers provenance from shape alone.
|
||||
*/
|
||||
private async recoverPendingMutation(): Promise<void> {
|
||||
const state = this.requireState()
|
||||
const pending = state.pendingMutation
|
||||
if (pending === undefined) return
|
||||
if (state.workspaceIds.includes(pending.workspaceId)) {
|
||||
throw new Error(
|
||||
`workspace domain is inconsistent: pending ${pending.operation} workspace `
|
||||
+ `'${pending.workspaceId}' is still present in registry order`,
|
||||
)
|
||||
}
|
||||
await this.requireTable().delete(pending.workspaceId)
|
||||
await this.setState({ initialized: state.initialized, workspaceIds: state.workspaceIds })
|
||||
}
|
||||
|
||||
private async bootstrap(headers: readonly SessionHeader[]): Promise<void> {
|
||||
const table = this.requireTable()
|
||||
const state = this.requireState()
|
||||
@@ -493,7 +555,12 @@ export class WorkspaceRegistry extends Service {
|
||||
}
|
||||
|
||||
private enqueueOperation<T>(operation: () => Promise<T>): Promise<T> {
|
||||
const result = this.operationTail.then(operation)
|
||||
const result = this.operationTail.then(async () => {
|
||||
// A committed delete may leave only its marker cleanup pending. Retry
|
||||
// recovery before another create/delete can overwrite that provenance.
|
||||
await this.recoverPendingMutation()
|
||||
return await operation()
|
||||
})
|
||||
this.operationTail = result.then(() => {}, () => {})
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user