|
|
@@ -715,6 +715,30 @@ export class ChatCacheStore {
|
|
|
this.db.prepare("UPDATE chat_snapshots SET sync_state='stale' WHERE owner=? AND kind='list'").run(owner)
|
|
|
}
|
|
|
|
|
|
+ invalidateOwnedMetadata(owner: string): Array<{profile:string;session_id:string}> {
|
|
|
+ return this.db.prepare(`UPDATE chat_sessions SET metadata_complete=0,sync_revision=sync_revision+1
|
|
|
+ WHERE owner=? AND owned_at IS NOT NULL AND source NOT IN ('cron','ios_group','yaoyao_workspace')
|
|
|
+ RETURNING profile,session_id`).all(owner) as Array<{profile:string;session_id:string}>
|
|
|
+ }
|
|
|
+
|
|
|
+ refreshMetadata(owner:string,profile:string,id:string,revision:number,response:UpstreamResponse,startedAt:number):boolean {
|
|
|
+ const payload=parseResponse(response),summary=object(payload?.session)??payload
|
|
|
+ const total=summary?.message_count??summary?.messageCount
|
|
|
+ const row=this.db.prepare('SELECT metadata_complete,complete,message_total,sync_state FROM chat_sessions WHERE owner=? AND profile=? AND session_id=?')
|
|
|
+ .get(owner,profile,id) as {metadata_complete:number;complete:number;message_total:number;sync_state:string}|undefined
|
|
|
+ if(!row||row.metadata_complete||!row.complete||total==null||Number(total)!==row.message_total||this.revision(owner,profile,id)!==revision)return false
|
|
|
+ this.db.exec('SAVEPOINT metadata_refresh')
|
|
|
+ try {
|
|
|
+ const metadata={...summary};delete metadata.messages
|
|
|
+ const headers=new Headers(response.headers);headers.delete('content-length')
|
|
|
+ const metadataResponse={...response,headers,body:Buffer.from(JSON.stringify(metadata))}
|
|
|
+ if(!this.putSnapshot(owner,`detail:${profile}:${id}`,'detail',profile,id,metadataResponse,false,startedAt))throw new Error('Invalid conversation metadata')
|
|
|
+ // A metadata invalidation must not replace or downgrade the durable live tail.
|
|
|
+ this.db.prepare('UPDATE chat_sessions SET sync_state=? WHERE owner=? AND profile=? AND session_id=?').run(row.sync_state,owner,profile,id)
|
|
|
+ this.db.exec('RELEASE metadata_refresh');return true
|
|
|
+ }catch(error){this.db.exec('ROLLBACK TO metadata_refresh; RELEASE metadata_refresh');throw error}
|
|
|
+ }
|
|
|
+
|
|
|
recordAuthoritativeTitle(owner: string, profile: string, sessionID: string, title: string): void {
|
|
|
const normalized = title.trim()
|
|
|
if (!normalized) return
|
|
|
@@ -1292,8 +1316,10 @@ export class ChatCacheCoordinator {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- observeGlobal(owner: string, type: string): void {
|
|
|
- if (type === 'sessions.changed') this.store.markListsStale(owner)
|
|
|
+ observeGlobal(owner: string, type: string, upstreamInvalidation=false): void {
|
|
|
+ if (type !== 'sessions.changed' || this.stopped || this.store.isClosed) return
|
|
|
+ this.store.markListsStale(owner)
|
|
|
+ if(upstreamInvalidation)for(const row of this.store.invalidateOwnedMetadata(owner))this.schedule(owner,row.profile,row.session_id)
|
|
|
}
|
|
|
|
|
|
command(owner: string, profile: string, sessionID: string, method: string,
|
|
|
@@ -1327,6 +1353,10 @@ export class ChatCacheCoordinator {
|
|
|
const detail = await this.upstream.request(detailPath, { search: new URLSearchParams({ profile }), cache: 'reload' })
|
|
|
if (this.stopped || this.store.isClosed) return
|
|
|
if (detail.status !== 200) throw new Error('Unable to synchronize conversation metadata')
|
|
|
+ if(!force&&this.store.refreshMetadata(owner,profile,sessionID,revision,detail,detailStartedAt)) {
|
|
|
+ this.onSynchronized(owner,profile,sessionID)
|
|
|
+ return
|
|
|
+ }
|
|
|
const detailPayload = parseResponse(detail) ?? {}
|
|
|
const detailSummary = object(detailPayload.session) ?? detailPayload
|
|
|
const summaryTotal = Math.max(0, Number(detailSummary.message_count ?? detailSummary.messageCount ?? previousTotal) || 0)
|