refactor(schedule): keep reminder delivery conversational

This commit is contained in:
Tianyi Cui
2026-08-09 15:22:53 +08:00
parent 2f3e8974ec
commit 36ef892559
134 changed files with 598 additions and 3139 deletions

View File

@@ -55,7 +55,6 @@ interface MemoryConfig { store?: MemoryStore }
interface CoordinatorInternals {
states: Map<unknown, unknown>
live: Map<unknown, {
init: Promise<void> | undefined
writes: { pending: unknown[]; active: Promise<void> | undefined; hasWork: boolean }
}>
chains: Map<unknown, unknown>
@@ -183,7 +182,6 @@ class ControlledBackend implements PersistenceBackend<never> {
loadAttempts = 0
repairAttempts = 0
beforeAppend?: (attempt: number) => Promise<void>
afterAppend?: (attempt: number) => Promise<void>
beforeLoadStored?: (attempt: number, signal?: AbortSignal) => Promise<void>
/** When set, the declared seek hook delegates here so readFrom exercises it; unset throws (tests set it first). */
seekHook?: (id: SessionId, fromSeq: number, signal?: AbortSignal) => Promise<StoredSuffix | undefined>
@@ -220,7 +218,6 @@ class ControlledBackend implements PersistenceBackend<never> {
} else {
entry.events.push(...structuredClone(events) as SessionEvent[])
}
await this.afterAppend?.(attempt)
}
async commitRepair(m: SessionHeader, _tornMarker: undefined, closers: readonly SessionEvent[]): Promise<void> {
@@ -376,213 +373,6 @@ describe('PersistenceCoordinator bounded writes', () => {
})
})
describe('PersistenceCoordinator retryable live initialization', () => {
it('retries a rejected first storage read for a new empty session', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const backend = new ControlledBackend()
const loadGate = Promise.withResolvers<undefined>()
const retryGate = Promise.withResolvers<undefined>()
backend.beforeLoadStored = async (attempt) => {
if (attempt === 1) {
await loadGate.promise
throw new Error('transient init read failure')
}
if (attempt === 2) await retryGate.promise
}
let coordinator!: PersistenceCoordinator<never>
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
coordinator = new PersistenceCoordinator(inner, backend)
}, { inject: ['sessions'] }))
try {
const session = ctx.sessions.create(SessionId('retry-new-empty'))
await vi.waitFor(() => { expect(backend.loadAttempts).toBe(1) })
const first = ctx.sessions.flush(session)
session.append('turn/start', { turn: 1 })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
loadGate.resolve(undefined)
await expect(first).rejects.toThrow('transient init read failure')
const retries = [ctx.sessions.flush(session), ctx.sessions.flush(session)]
await vi.waitFor(() => { expect(backend.loadAttempts).toBe(2) })
retryGate.resolve(undefined)
await expect(Promise.all(retries)).resolves.toEqual([true, true])
// The one shared retry performs the normal new-session probe and
// createCore's collision recheck; a second initialization would add two
// more reads.
expect(backend.loadAttempts).toBe(3)
expect(backend.store.get(session.id)?.events.map(event => event.seq)).toEqual([0, 1])
const live = [...(coordinator as unknown as CoordinatorInternals).live.values()][0]
if (live === undefined) throw new Error('live controller was not retained')
expect(live.init).toBeInstanceOf(Promise)
expect(live).not.toHaveProperty('initialized')
expect(live).not.toHaveProperty('seed')
expect(live).not.toHaveProperty('seedEnd')
} finally {
loadGate.resolve(undefined)
retryGate.resolve(undefined)
await fiber.dispose()
await ctx.fiber.dispose()
}
})
it('uses the backend cursor when a fork seed committed before initialization rejected', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const backend = new ControlledBackend()
const appendGate = Promise.withResolvers<undefined>()
backend.afterAppend = async (attempt) => {
if (attempt === 1) {
await appendGate.promise
throw new Error('uncertain init write')
}
}
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
new PersistenceCoordinator(inner, backend)
}, { inject: ['sessions'] }))
try {
const seed = oneTurnLog()
const session = ctx.sessions.create(SessionId('retry-fork-seed'), {
seed,
meta: { cwd: '/w', seedLength: seed.length },
})
await vi.waitFor(() => { expect(backend.appendAttempts).toBe(1) })
const first = ctx.sessions.flush(session)
appendGate.resolve(undefined)
await expect(first).rejects.toThrow('uncertain init write')
await expect(ctx.sessions.flush(session)).resolves.toBe(true)
expect(backend.appendAttempts).toBe(1)
expect(backend.store.get(session.id)?.events.map(event => event.seq))
.toEqual([0, 1, 2, 3, 4, 5, 6])
} finally {
appendGate.resolve(undefined)
await fiber.dispose()
await ctx.fiber.dispose()
}
})
it('retries a fork seed when initialization rejects before materialization', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const backend = new ControlledBackend()
const appendGate = Promise.withResolvers<undefined>()
backend.beforeAppend = async (attempt) => {
if (attempt === 1) {
await appendGate.promise
throw new Error('pre-commit init write failure')
}
}
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
new PersistenceCoordinator(inner, backend)
}, { inject: ['sessions'] }))
try {
const seed = oneTurnLog()
const session = ctx.sessions.create(SessionId('retry-unmaterialized-fork-seed'), {
seed,
meta: { cwd: '/w', seedLength: seed.length },
})
await vi.waitFor(() => { expect(backend.appendAttempts).toBe(1) })
const first = ctx.sessions.flush(session)
appendGate.resolve(undefined)
await expect(first).rejects.toThrow('pre-commit init write failure')
await expect(ctx.sessions.flush(session)).resolves.toBe(true)
expect(backend.appendAttempts).toBe(2)
expect(backend.store.get(session.id)?.events.map(event => event.seq))
.toEqual([0, 1, 2, 3, 4, 5, 6])
} finally {
appendGate.resolve(undefined)
await fiber.dispose()
await ctx.fiber.dispose()
}
})
it('rejects a retry when its adopted durable prefix disappears', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const backend = new ControlledBackend()
const id = SessionId('retry-missing-adopted-prefix')
const seed = oneTurnLog()
const stored = seed.slice(0, 1)
const storedMeta = meta(id, '/w')
backend.store.set(id, { meta: storedMeta, events: structuredClone(stored) })
const appendGate = Promise.withResolvers<undefined>()
backend.beforeAppend = async (attempt) => {
if (attempt === 1) {
await appendGate.promise
throw new Error('pre-commit adoption write failure')
}
}
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
new PersistenceCoordinator(inner, backend)
}, { inject: ['sessions'] }))
try {
const session = ctx.sessions.create(id, {
seed,
meta: { cwd: '/w', seedLength: seed.length },
})
await vi.waitFor(() => { expect(backend.appendAttempts).toBe(1) })
const first = ctx.sessions.flush(session)
appendGate.resolve(undefined)
await expect(first).rejects.toThrow('pre-commit adoption write failure')
backend.store.delete(id)
await expect(ctx.sessions.flush(session))
.rejects.toThrow('lost its persisted artifact during live initialization')
expect(backend.appendAttempts).toBe(1)
} finally {
appendGate.resolve(undefined)
if (!backend.store.has(id)) {
backend.store.set(id, { meta: storedMeta, events: structuredClone(stored) })
}
await fiber.dispose()
await ctx.fiber.dispose()
}
})
it('retries only a missing suffix after stored-session adoption rejects', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const backend = new ControlledBackend()
const id = SessionId('retry-resume-adoption')
const stored = oneTurnLog()
backend.store.set(id, { meta: meta(id, '/w'), events: structuredClone(stored) })
const appendGate = Promise.withResolvers<undefined>()
backend.beforeAppend = async (attempt) => {
if (attempt === 1) {
await appendGate.promise
throw new Error('transient adoption write failure')
}
}
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
new PersistenceCoordinator(inner, backend)
}, { inject: ['sessions'] }))
try {
const session = ctx.sessions.create(id, { seed: stored, meta: { cwd: '/w' } })
await vi.waitFor(() => { expect(backend.appendAttempts).toBe(1) })
const first = ctx.sessions.flush(session)
appendGate.resolve(undefined)
await expect(first).rejects.toThrow('transient adoption write failure')
await expect(ctx.sessions.flush(session)).resolves.toBe(true)
expect(backend.appendAttempts).toBe(2)
expect(backend.store.get(id)?.events.map(event => event.seq))
.toEqual([0, 1, 2, 3, 4, 5, 6])
} finally {
appendGate.resolve(undefined)
await fiber.dispose()
await ctx.fiber.dispose()
}
})
})
describe('PersistenceCoordinator stored identity', () => {
it('rejects a mismatched backend header before repair or state publication', async () => {
const ctx = new Context()