perf(activity): page, summarize and reconcile activity history in Main by index

Schema 57 rebuilds activity_history_records with order_seq (newer = larger;
a move to the front assigns a new maximum) and stored generated columns
(record_id, occurrence, status, status_group, kind, request_id,
conversation_id, project_id, created_at) with indexes. The order is migrated
from record_order_json exactly; legacy duplicates keep their occurrence and
only the first is shown. record_order_json is no longer written.

ActivityHistoryRepository owns the SQL: page({before, limit <= 500, filter,
projectId, conversationId}) newest first by index, summary() with the counts
and conversation titles/status the Activity page derived from the full list,
reconcile() joined with tasks with a renderer-localized detail line, update()
applied directly in SQL without reading the history, replace() for the
legacy migration, clear(). New activity-history:page/summary/reconcile IPC
with zod validation; page and summary run on the readonly worker.
perf:main measures 24,000 records: startup load 106 ms on Main (14 MB) ->
first page + summary 13.5 ms on the worker (0.6 ms loop block, 114 KB).
This commit is contained in:
mesalogo
2026-10-03 21:42:52 +08:00
parent 6aab78b9fa
commit 352da66dbd
14 changed files with 963 additions and 263 deletions
+50
View File
@@ -133,6 +133,56 @@ export const activityHistoryUpdateSchema = z
})
.strict()
/**
* Activity history pages (PERF-15), newest first. A record ID shows once:
* legacy duplicates after the first occurrence are left out. `before` is the
* `nextBefore` of the previous page.
*/
export const activityHistoryFilterSchema = z.enum(['all', 'active', 'failed'])
export const activityHistoryPageRequestSchema = z
.object({
before: z.number().int().optional(),
limit: z.number().int().min(1).max(500),
filter: activityHistoryFilterSchema.optional(),
projectId: z.string().min(1).max(256).optional(),
conversationId: z.string().min(1).max(256).optional()
})
.strict()
export const activityHistorySummaryRequestSchema = z
.object({
/** Conversations whose title and status are needed (the loaded ones). */
conversationIds: z.array(z.string().min(1).max(256)).max(5_000)
})
.strict()
export const activityHistoryReconcileRequestSchema = z
.object({
activeRequestIds: z.array(z.string().min(1).max(256)).max(10_000),
/** The localized line appended to records marked interrupted. */
interruptedDetail: z.string().max(1_000)
})
.strict()
export type ActivityHistoryFilter = z.infer<typeof activityHistoryFilterSchema>
export type ActivityHistoryPageRequest = z.infer<typeof activityHistoryPageRequestSchema>
export type ActivityHistoryPage = {
records: ActivityRecord[]
/** Set when older records may follow. */
nextBefore?: number
}
export type ActivityHistorySummaryRequest = z.infer<typeof activityHistorySummaryRequestSchema>
export type ActivityHistorySummary = {
/** Record counts per filter, over the whole history. */
counts: Record<ActivityHistoryFilter, number>
/**
* Per requested conversation: the title of its newest request record and
* the conversation status (latest request's result, else latest result,
* else latest record), over the whole history.
*/
conversations: Record<string, { title?: string; status: ActivityRecord['status'] }>
legacyHistoryMayBeIncomplete: boolean
}
export type ActivityHistoryReconcileRequest = z.infer<typeof activityHistoryReconcileRequestSchema>
export type ActivityRecord = z.infer<typeof activityRecordSchema>
export type ActivityHistorySnapshot = z.infer<
typeof activityHistorySnapshotSchema