feat(schedule): add durable after reminders

This commit is contained in:
pku-xht
2026-08-06 03:47:37 +08:00
committed by Tianyi Cui
parent f7e7851e3f
commit 8b69252664
41 changed files with 354 additions and 146 deletions
@@ -254,8 +254,8 @@ export interface CommandNode {
/**
* Host-computed presentation for one durable non-surface event. The generic
* runtime carries the keyed JSON-compatible payload without importing the
* producing domain; a client plugin owns the keyed renderer.
* runtime carries the durable event type and JSON-compatible payload without
* importing the producing domain; a client plugin owns the keyed renderer.
*/
export interface PresentedEventNode {
kind: 'presented-event'
@@ -263,8 +263,8 @@ export interface PresentedEventNode {
seq: number
/** Unix epoch ms from the source Session event. */
time: number
/** Open runtime key selecting an optional domain renderer. */
presentationKey: string
/** Durable event type selecting an optional domain renderer. */
eventType: string
/** Domain-owned JSON-compatible presentation payload. */
view: unknown
}
@@ -392,6 +392,7 @@ export class Session implements SessionFace {
async loadOlder(): Promise<void> {
if (this.openState !== 'open' || !this.hasMore || this.loadingOlder) return
const generation = this.openGeneration
const requestedBaseSeq = this.baseSeq
this.loadingOlder = true
this.notifier.markDirty()
try {
@@ -404,34 +405,27 @@ export class Session implements SessionFace {
return
}
const tail = older[older.length - 1]
if (tail === undefined || tail.event.seq + 1 !== this.baseSeq) {
if (tail === undefined || tail.event.seq + 1 !== requestedBaseSeq) {
// §D.2 continuity assertion: on violation drop the page fail-soft rather than render an out-of-order stream.
console.error(`[web-runtime] history page discontinuous: tail seq ${tail?.event.seq} vs baseSeq ${this.baseSeq}`)
console.error(`[web-runtime] history page discontinuous: tail seq ${tail?.event.seq} vs baseSeq ${requestedBaseSeq}`)
this.hasMore = false
return
}
this.installWindow([
...older,
...this.events.map((event, index): HistoryEntry => {
const view = this.views[index]
return view === undefined ? { event } : { event, view }
}),
], result.value.hasMore)
this.events = [...older.map(entry => entry.event), ...this.events]
this.views = [...older.map(entry => entry.view), ...this.views]
/* v8 ignore next -- the empty-page branch returned above. */
this.baseSeq = older[0]?.event.seq ?? this.baseSeq
this.hasMore = result.value.hasMore
this.transcript.reset(this.events, this.views)
this.rebuildDerivedFromWindow()
} catch (error) {
if (generation === this.openGeneration) {
console.error('[web-runtime] loadOlder failed:', error)
}
} finally {
if (generation === this.openGeneration) {
try {
const { hasGap } = this.mergeWindow()
// oxlint-disable-next-line typescript/no-unnecessary-condition -- resync can close the window while the page request is awaited.
if (hasGap && this.openState === 'open') void this.repairGap()
} catch (error) {
console.error('[web-runtime] loadOlder buffer merge failed:', error)
void this.resync()
}
this.loadingOlder = false
if (this.liveBuffer.length > 0) void this.repairGap()
this.notifier.markDirty()
}
}
@@ -816,6 +810,21 @@ export class Session implements SessionFace {
this.applyEventSideEffects(event, view)
}
/** Verify one retained event and apply a defined late sidecar immediately. */
private upgradeLiveView(event: SessionEvent, view?: SessionEventView): boolean {
const index = this.events.findIndex(candidate => candidate.seq === event.seq)
if (index === -1) return false
const retained = this.events[index]
/* v8 ignore next -- findIndex returned a dense-array position. */
if (retained === undefined) return false
assertSameEvent(retained, event)
if (view === undefined || sameWireValue(this.views[index], view)) return false
this.views[index] = view
this.transcript.reset(this.events, this.views)
this.rebuildDerivedFromWindow()
return true
}
/** Retire the first matching live steering occurrence when its durable message takes over. */
private handoffPendingSteering(event: SessionEvent): void {
if (event.type !== 'user/message') return
@@ -840,9 +849,8 @@ export class Session implements SessionFace {
if (this.openState !== 'open') return // cold/error: no window upkeep (history fully backfills on open)
const tailSeq = this.windowTailSeq()
if (tailSeq !== null && event.seq <= tailSeq) {
this.liveBuffer.push({ event, view })
try {
const { changed } = this.mergeWindow()
const changed = this.upgradeLiveView(event, view)
if (changed) this.notifier.markDirty()
} catch (error) {
console.error('[web-runtime] duplicate session event failed identity validation:', error)
@@ -852,12 +860,12 @@ export class Session implements SessionFace {
}
if (tailSeq !== null && event.seq > tailSeq + 1) {
this.liveBuffer.push({ event, view })
void this.repairGap()
if (!this.loadingOlder) void this.repairGap()
return
}
if (tailSeq === null && event.seq !== 0) {
this.liveBuffer.push({ event, view })
void this.repairGap()
if (!this.loadingOlder) void this.repairGap()
return
}
this.appendLive(event, view)
@@ -125,7 +125,7 @@ function materializePresented(event: SessionEvent, sidecar: PresentedEventView):
kind: 'presented-event',
seq: event.seq,
time: event.time,
presentationKey: sidecar.presentationKey,
eventType: event.type,
view: sidecar.view,
}
}