|
@@ -309,20 +309,31 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
|
|
|
if (titleEvent !== undefined) {
|
|
if (titleEvent !== undefined) {
|
|
|
values['title'] = (titleEvent as unknown as { data: { title: string } }).data.title
|
|
values['title'] = (titleEvent as unknown as { data: { title: string } }).data.title
|
|
|
}
|
|
}
|
|
|
- const todos = backscanTodos(log)
|
|
|
|
|
- if (todos !== undefined) values['todos'] = todos
|
|
|
|
|
|
|
+ // Always present (tool-todo unit composed): null when no plan stands.
|
|
|
|
|
+ values['todos'] = backscanTodos(log) ?? null
|
|
|
return values
|
|
return values
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Host push-frame parallel: emit one session/projection frame per key the given event advanced. */
|
|
/** Host push-frame parallel: emit one session/projection frame per key the given event advanced. */
|
|
|
function projectionFramesOf(id: SessionId, log: readonly SessionEvent[], event: SessionEvent): Extract<MuxFrame, { type: 'session/projection' }>[] {
|
|
function projectionFramesOf(id: SessionId, log: readonly SessionEvent[], event: SessionEvent): Extract<MuxFrame, { type: 'session/projection' }>[] {
|
|
|
const type = (event as { type: string }).type
|
|
const type = (event as { type: string }).type
|
|
|
- const key = type === 'session/title' ? 'title' : type === 'todo/write' ? 'todos' : undefined
|
|
|
|
|
- if (key === undefined) return []
|
|
|
|
|
- const values = projectionValuesOf(log)
|
|
|
|
|
- /* v8 ignore next -- the advancing event is in the log, so its key always has a value. */
|
|
|
|
|
- if (!Object.hasOwn(values, key)) return []
|
|
|
|
|
- return [{ type: 'session/projection', sessionId: id, key, value: values[key], seq: event.seq }]
|
|
|
|
|
|
|
+ if (type === 'session/title') {
|
|
|
|
|
+ const values = projectionValuesOf(log)
|
|
|
|
|
+ /* v8 ignore next -- the advancing title event is in the log, so the key is present. */
|
|
|
|
|
+ if (!Object.hasOwn(values, 'title')) return []
|
|
|
|
|
+ return [{ type: 'session/projection', sessionId: id, key: 'title', value: values['title'], seq: event.seq }]
|
|
|
|
|
+ }
|
|
|
|
|
+ // Standing-plan fold: writes replace the list; turn/start clears it (null).
|
|
|
|
|
+ if (type === 'todo/write' || type === 'turn/start') {
|
|
|
|
|
+ return [{
|
|
|
|
|
+ type: 'session/projection',
|
|
|
|
|
+ sessionId: id,
|
|
|
|
|
+ key: 'todos',
|
|
|
|
|
+ value: backscanTodos(log) ?? null,
|
|
|
|
|
+ seq: event.seq,
|
|
|
|
|
+ }]
|
|
|
|
|
+ }
|
|
|
|
|
+ return []
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -356,11 +367,16 @@ function pageOf(
|
|
|
return { events, hasMore: start > 0 }
|
|
return { events, hasMore: start > 0 }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** Current todo projection over the full log (host parallel: latest todo/write, last write wins). */
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Current plan projection over the full log (host parallel: latest todo/write
|
|
|
|
|
+ * with no later turn/start; a new turn retires the previous plan).
|
|
|
|
|
+ */
|
|
|
function backscanTodos(log: readonly SessionEvent[]): TodoItem[] | undefined {
|
|
function backscanTodos(log: readonly SessionEvent[]): TodoItem[] | undefined {
|
|
|
for (let i = log.length - 1; i >= 0; i--) {
|
|
for (let i = log.length - 1; i >= 0; i--) {
|
|
|
const event = log[i]
|
|
const event = log[i]
|
|
|
- if (event !== undefined && event.type === 'todo/write') return event.data.todos
|
|
|
|
|
|
|
+ if (event === undefined) continue
|
|
|
|
|
+ if (event.type === 'turn/start') return undefined
|
|
|
|
|
+ if (event.type === 'todo/write') return event.data.todos
|
|
|
}
|
|
}
|
|
|
return undefined
|
|
return undefined
|
|
|
}
|
|
}
|