fix(schedule): close concurrent durability gaps

This commit is contained in:
pku-xht
2026-08-07 19:06:34 +08:00
committed by Tianyi Cui
parent 76da9e3f9f
commit 9d73b527de
12 changed files with 160 additions and 71 deletions
@@ -9,6 +9,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { AfterScheduleRecord } from './types.ts'
import { foldScheduleEvents, renderReminderFraming, ScheduleLogError } from './domain.ts'
import { flushSchedulePersistence } from './persistence.ts'
import { runScheduleTransaction } from './transaction.ts'
/** Largest delay that Node timers represent without clamping. */
export const MAX_TIMER_DELAY_MS = 2_147_483_647
@@ -102,7 +103,7 @@ export class ScheduleOwner {
private async runRequested(): Promise<void> {
while (this.requested && !this.stopping && !this.faulted) {
this.requested = false
await this.driveOnce()
await runScheduleTransaction(this.agent, () => this.driveOnce())
}
}
+56 -49
View File
@@ -18,6 +18,7 @@ import {
scheduleView,
} from './domain.ts'
import { flushSchedulePersistence } from './persistence.ts'
import { runScheduleTransaction } from './transaction.ts'
import type {
AfterScheduleRecord,
PersistenceUncertainError,
@@ -255,31 +256,33 @@ export function registerScheduleTools(
if (exec.agent !== agent) return internalError()
const invalid = validateCreateArgs(args)
if (invalid !== undefined) return invalid
const uncertain = await preflight(rootCtx, agent, 'create')
if (uncertain !== undefined) return uncertain
notifyDurableChange()
const folded = foldForTool(agent)
if (isToolError(folded)) return folded
const id = allocateScheduleId(folded)
let record: AfterScheduleRecord
try {
record = createAfterScheduleRecord(id, args.prompt, args.after_seconds, Date.now())
} catch (error: unknown) {
return error instanceof ScheduleInputError ? inputError(error) : internalError()
}
try {
agent.session.append('schedule/change', {
version: 1,
operation: 'create',
schedule: record,
})
} catch {
return internalError()
}
const barrier = await preflight(rootCtx, agent, 'create', id)
if (barrier !== undefined) return barrier
notifyDurableChange()
return scheduleView(record, Date.now())
return runScheduleTransaction(agent, async () => {
const uncertain = await preflight(rootCtx, agent, 'create')
if (uncertain !== undefined) return uncertain
notifyDurableChange()
const folded = foldForTool(agent)
if (isToolError(folded)) return folded
const id = allocateScheduleId(folded)
let record: AfterScheduleRecord
try {
record = createAfterScheduleRecord(id, args.prompt, args.after_seconds, Date.now())
} catch (error: unknown) {
return error instanceof ScheduleInputError ? inputError(error) : internalError()
}
try {
agent.session.append('schedule/change', {
version: 1,
operation: 'create',
schedule: record,
})
} catch {
return internalError()
}
const barrier = await preflight(rootCtx, agent, 'create', id)
if (barrier !== undefined) return barrier
notifyDurableChange()
return scheduleView(record, Date.now())
})
},
presentCall: args => present('Create reminder', 'other', args.prompt),
})))
@@ -291,13 +294,15 @@ export function registerScheduleTools(
output: { schema: LIST_OUTPUT_SCHEMA, render: renderValue },
async execute(_args, exec): Promise<ScheduleListValue> {
if (exec.agent !== agent) return internalError()
const uncertain = await preflight(rootCtx, agent, 'list')
if (uncertain !== undefined) return uncertain
notifyDurableChange()
const folded = foldForTool(agent)
if (isToolError(folded)) return folded
const now = Date.now()
return folded.active.map(record => scheduleView(record, now))
return runScheduleTransaction(agent, async () => {
const uncertain = await preflight(rootCtx, agent, 'list')
if (uncertain !== undefined) return uncertain
notifyDurableChange()
const folded = foldForTool(agent)
if (isToolError(folded)) return folded
const now = Date.now()
return folded.active.map(record => scheduleView(record, now))
})
},
presentCall: () => present('List reminders', 'read'),
})))
@@ -315,23 +320,25 @@ export function registerScheduleTools(
}
const id = ScheduleId(args.id)
if (exec.agent !== agent) return internalError()
const uncertain = await preflight(rootCtx, agent, 'delete', id)
if (uncertain !== undefined) return uncertain
notifyDurableChange()
const folded = foldForTool(agent)
if (isToolError(folded)) return folded
if (!folded.active.some(record => record.id === id)) {
return { id, deleted: false, code: 'schedule_not_found' }
}
try {
agent.session.append('schedule/change', { version: 1, operation: 'delete', id })
} catch {
return internalError()
}
const barrier = await preflight(rootCtx, agent, 'delete', id)
if (barrier !== undefined) return barrier
notifyDurableChange()
return { id, deleted: true }
return runScheduleTransaction(agent, async () => {
const uncertain = await preflight(rootCtx, agent, 'delete', id)
if (uncertain !== undefined) return uncertain
notifyDurableChange()
const folded = foldForTool(agent)
if (isToolError(folded)) return folded
if (!folded.active.some(record => record.id === id)) {
return { id, deleted: false, code: 'schedule_not_found' }
}
try {
agent.session.append('schedule/change', { version: 1, operation: 'delete', id })
} catch {
return internalError()
}
const barrier = await preflight(rootCtx, agent, 'delete', id)
if (barrier !== undefined) return barrier
notifyDurableChange()
return { id, deleted: true }
})
},
presentCall: args => present('Delete reminder', 'other', args.id),
})))
@@ -0,0 +1,23 @@
/** Agent-scoped serialization for Schedule reads and durable mutations. */
import type { Agent } from '@deepseek-ai/dsh-agent'
const tails = new WeakMap<Agent, Promise<void>>()
/**
* Run one complete Schedule transaction after its exact Agent's prior transaction.
* @param agent - Exact Schedule owner and serialization key.
* @param operation - Complete preflight, fold, mutation, and postflight operation.
* @returns The operation result after exclusive execution.
*/
export async function runScheduleTransaction<T>(agent: Agent, operation: () => Promise<T>): Promise<T> {
const prior = tails.get(agent) ?? Promise.resolve()
const run = prior.then(operation)
const tail = run.then(() => undefined, () => undefined)
tails.set(agent, tail)
try {
return await run
} finally {
if (tails.get(agent) === tail) tails.delete(agent)
}
}