perf+persistence: the durable reconcile mirror (cache + disk) #47
@@ -126,6 +126,36 @@ export async function reconcileSnapshot(
|
||||
return { issues, milestones, deps, timelines }
|
||||
}
|
||||
|
||||
type Snapshot = Awaited<ReturnType<typeof reconcileSnapshot>>
|
||||
|
||||
/**
|
||||
* A single in-memory reconcile cache shared across the app. A full reconcile is
|
||||
* ~2N gitea calls (deps + timelines per issue); without this, every agent tool
|
||||
* call refetched the whole repo. Reads within `maxAgeMs` reuse the cache;
|
||||
* `getSnapshot({ maxAgeMs: 0 })` forces a fresh pull (the explicit UI reconcile),
|
||||
* and any write calls `invalidateSnapshot()` so the next read sees it. The cache
|
||||
* is rebuildable — the durable truth stays in gitea (the purity split, D4).
|
||||
*/
|
||||
let snapshotCache: { snap: Snapshot; at: number } | null = null
|
||||
|
||||
export async function getSnapshot(client: GiteaClient, opts?: { maxAgeMs?: number }): Promise<Snapshot> {
|
||||
const maxAgeMs = opts?.maxAgeMs ?? 0
|
||||
if (snapshotCache && maxAgeMs > 0 && Date.now() - snapshotCache.at <= maxAgeMs) {
|
||||
return snapshotCache.snap
|
||||
}
|
||||
const snap = await reconcileSnapshot(client)
|
||||
snapshotCache = { snap, at: Date.now() }
|
||||
return snap
|
||||
}
|
||||
|
||||
/** Drop the cache so the next read reflects a just-made write. */
|
||||
export function invalidateSnapshot(): void {
|
||||
snapshotCache = null
|
||||
}
|
||||
|
||||
/** Agent tool calls tolerate a slightly stale snapshot (seconds) to stay responsive. */
|
||||
export const AGENT_SNAPSHOT_TTL_MS = 30_000
|
||||
|
||||
export function registerGiteaIpc(): void {
|
||||
const client = getGiteaClient()
|
||||
const repo = client ? `${process.env.GITEA_OWNER ?? 'christian'}/${process.env.GITEA_REPO ?? 'commitea'}` : null
|
||||
@@ -134,7 +164,8 @@ export function registerGiteaIpc(): void {
|
||||
|
||||
ipcMain.handle('gitea:reconcile', async () => {
|
||||
if (!client) return { configured: false, issues: [], milestones: [], deps: [], timelines: {} }
|
||||
const snap = await reconcileSnapshot(client)
|
||||
// explicit UI sync — force fresh, and warm the cache for agent tool calls
|
||||
const snap = await getSnapshot(client, { maxAgeMs: 0 })
|
||||
return { configured: true, ...snap }
|
||||
})
|
||||
|
||||
@@ -167,6 +198,7 @@ export function registerGiteaIpc(): void {
|
||||
const ids = await resolveLabelIds(plan.labels)
|
||||
await client.setIssueLabels(change.issue, ids)
|
||||
const issue = await client.getIssue(change.issue)
|
||||
invalidateSnapshot() // the board + forecast must reflect the label change
|
||||
return { ok: true as const, plan, issue }
|
||||
})
|
||||
|
||||
@@ -183,6 +215,7 @@ export function registerGiteaIpc(): void {
|
||||
const issue = await client.createIssue({ title: it.title, body: it.body, labelIds })
|
||||
created.push({ number: issue.number, title: issue.title })
|
||||
}
|
||||
if (created.length) invalidateSnapshot() // new issues enter the board/scope
|
||||
return { ok: true as const, created }
|
||||
},
|
||||
)
|
||||
|
||||
@@ -26,7 +26,13 @@ import {
|
||||
} from '@commitea/core'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { appendDirectiveEntry, getGiteaClient, getPmStateClient, reconcileSnapshot } from './gitea.js'
|
||||
import {
|
||||
AGENT_SNAPSHOT_TTL_MS,
|
||||
appendDirectiveEntry,
|
||||
getGiteaClient,
|
||||
getPmStateClient,
|
||||
getSnapshot,
|
||||
} from './gitea.js'
|
||||
|
||||
/** Small local model for prose + the read tool; big model reserved for later decomposition. */
|
||||
function resolveModelRouter(): ModelRouter | null {
|
||||
@@ -93,7 +99,8 @@ export function registerModelIpc(): void {
|
||||
const execute = async (name: string, args: unknown) => {
|
||||
if (!client) return { error: 'gitea is not configured' }
|
||||
if (name === 'query_project') {
|
||||
const snap = await reconcileSnapshot(client)
|
||||
// reuse a recent reconcile — a multi-tool turn shouldn't refetch the repo each call
|
||||
const snap = await getSnapshot(client, { maxAgeMs: AGENT_SNAPSHOT_TTL_MS })
|
||||
const a = (args ?? {}) as { view: ProjectView; filters?: QueryFilters }
|
||||
return buildProjectView(a.view, a.filters, snap, new Date())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user