mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 08:02:51 +00:00
* refactor(native-chat): remove the records-file and per-chat journal imports Every native chat user is on a build that already moved chat records and history into the app-wide database, so the one-time copies are dead code: the agent-sessions.json import and its owed-copy flag, the per-chat journal.db import and the write-queue hold behind it, and the pre-SQLite log.jsonl notice. * refactor(native-chat): remove what only the deleted imports used - JournalHostDatabase.unsyncedTransaction and the synchronous-pragma reset that undid it; JOURNAL_SYNCHRONOUS is now module-local. The stranded rollback test drives transaction() instead. - deleteUnpublishedJournalRows and its SQL, plus its test. - boundJournalStatusText. - Stale comments naming the removed first-use copy (queued messages, send refusal example, JOURNAL_SYNCHRONOUS doc). - Write-queue and readInOrder comments: a write also lags when issued behind one still waiting in line. - Resolved-append ordering test binds the sink from inside a running read, so a resolver that read at handover now fails it. * test(native-chat): run resolved-append in the SQLite runtime project
141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
/**
|
|
* A transaction applies to a draft of the published store state, and the store publishes the draft
|
|
* only once its rows have committed, so no reader ever sees a change that might still roll back.
|
|
*/
|
|
|
|
import { encodeAgentSessionRecord } from '../../shared/agent-session-record-stored-form'
|
|
import type {
|
|
AgentSessionStoreState,
|
|
RetiredAgentSessionClaimKey
|
|
} from './agent-session-store-state'
|
|
import {
|
|
isReadableAgentSessionStoreOperation,
|
|
isReadableAgentSessionStoreRecord,
|
|
isReadableAgentSessionStoreTab,
|
|
isReadableRetiredAgentSessionClaimKey
|
|
} from './agent-session-store-row-rules'
|
|
import type { PersistedAgentSessionTab } from './agent-session-tab-table'
|
|
|
|
/** Unreadable rows are derived at load and never written, so the draft shares them. */
|
|
export function draftAgentSessionStoreState(state: AgentSessionStoreState): AgentSessionStoreState {
|
|
return {
|
|
...state,
|
|
records: new Map(state.records),
|
|
operations: new Map(state.operations),
|
|
retiredClaimKeys: [...state.retiredClaimKeys],
|
|
sessionTabs: state.sessionTabs?.clone() ?? null
|
|
}
|
|
}
|
|
|
|
type KeyedRowWrites = { upsert: [key: string, json: string][]; remove: string[] }
|
|
|
|
/** Exactly the rows a draft changed, each serialized as it will be stored. */
|
|
export type AgentSessionStoreRowWrites = {
|
|
records: KeyedRowWrites
|
|
operations: KeyedRowWrites
|
|
/** The whole list when it changed, else null. */
|
|
retiredClaimKeys: RetiredAgentSessionClaimKey[] | null
|
|
/** The whole index when it changed: `recorded: false` is a store that has never recorded one. */
|
|
sessionTabs: { recorded: boolean; tabs: PersistedAgentSessionTab[] } | null
|
|
}
|
|
|
|
/** A row as a load parses it back: JSON drops `undefined` members that an in-memory check sees. */
|
|
function serializeChangedRows<V>(
|
|
published: ReadonlyMap<string, V>,
|
|
next: ReadonlyMap<string, V>,
|
|
readable: (key: string, written: unknown) => boolean,
|
|
stored: (value: V) => unknown = (value) => value
|
|
): KeyedRowWrites {
|
|
const writes: KeyedRowWrites = { upsert: [], remove: [] }
|
|
let added = 0
|
|
for (const [key, value] of next) {
|
|
const prior = published.get(key)
|
|
if (prior === value) {
|
|
continue
|
|
}
|
|
const json = JSON.stringify(stored(value))
|
|
if (json === undefined || !readable(key, JSON.parse(json))) {
|
|
throw new Error('agent_session_store_write_invalid')
|
|
}
|
|
writes.upsert.push([key, json])
|
|
added += prior === undefined && !published.has(key) ? 1 : 0
|
|
}
|
|
// Only a size mismatch can hide a removal, so the common write never walks the published rows.
|
|
if (published.size + added !== next.size) {
|
|
for (const key of published.keys()) {
|
|
if (!next.has(key)) {
|
|
writes.remove.push(key)
|
|
}
|
|
}
|
|
}
|
|
return writes
|
|
}
|
|
|
|
function retiredClaimKeysChanged(
|
|
published: readonly RetiredAgentSessionClaimKey[],
|
|
next: readonly RetiredAgentSessionClaimKey[]
|
|
): boolean {
|
|
return published.length !== next.length || published.some((entry, index) => entry !== next[index])
|
|
}
|
|
|
|
/**
|
|
* The rows a draft changed relative to the published state, by identity, or null when it changed
|
|
* none. Each changed row is checked as written against the rules a load applies, so a row a later
|
|
* load would refuse rejects its own transaction instead of living in memory unreadable.
|
|
*/
|
|
export function agentSessionStoreDraftRowWrites(
|
|
published: AgentSessionStoreState,
|
|
draft: AgentSessionStoreState
|
|
): AgentSessionStoreRowWrites | null {
|
|
const records = serializeChangedRows(
|
|
published.records,
|
|
draft.records,
|
|
(sessionId, written) => isReadableAgentSessionStoreRecord(sessionId, written),
|
|
encodeAgentSessionRecord
|
|
)
|
|
const operations = serializeChangedRows(published.operations, draft.operations, (key, written) =>
|
|
isReadableAgentSessionStoreOperation(key, written)
|
|
)
|
|
const retiredClaimKeys = retiredClaimKeysChanged(
|
|
published.retiredClaimKeys,
|
|
draft.retiredClaimKeys
|
|
)
|
|
? draft.retiredClaimKeys
|
|
: null
|
|
if (
|
|
retiredClaimKeys &&
|
|
(!retiredClaimKeys.every((entry) =>
|
|
isReadableRetiredAgentSessionClaimKey(JSON.parse(JSON.stringify(entry)))
|
|
) ||
|
|
new Set(retiredClaimKeys.map((entry) => entry.keyId)).size !== retiredClaimKeys.length)
|
|
) {
|
|
throw new Error('agent_session_store_write_invalid')
|
|
}
|
|
const tabsChanged =
|
|
published.sessionTabs && draft.sessionTabs
|
|
? !published.sessionTabs.equals(draft.sessionTabs)
|
|
: published.sessionTabs !== draft.sessionTabs
|
|
const tabs = tabsChanged
|
|
? (draft.sessionTabs?.entries() ?? []).map(([tabId, sessionId]) => ({ tabId, sessionId }))
|
|
: []
|
|
if (!tabs.every(isReadableAgentSessionStoreTab)) {
|
|
throw new Error('agent_session_store_write_invalid')
|
|
}
|
|
const changed =
|
|
records.upsert.length > 0 ||
|
|
records.remove.length > 0 ||
|
|
operations.upsert.length > 0 ||
|
|
operations.remove.length > 0 ||
|
|
retiredClaimKeys !== null ||
|
|
tabsChanged
|
|
if (!changed) {
|
|
return null
|
|
}
|
|
return {
|
|
records,
|
|
operations,
|
|
retiredClaimKeys,
|
|
sessionTabs: tabsChanged ? { recorded: draft.sessionTabs !== null, tabs } : null
|
|
}
|
|
}
|