mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 00:02:05 +00:00
feat(session-search): report how many messages an index holds
The status contract gains an optional messagesIndexed, read from the store beside the file-state counts and cached the same way, so a settings row can say what is searchable rather than how many files were opened. Optional on the wire: a paired server that predates the field degrades to a session count.
This commit is contained in:
@@ -61,6 +61,14 @@ function newIndexer(
|
||||
return indexer
|
||||
}
|
||||
|
||||
/** What the index holds, counted the way `status()` counts it. */
|
||||
function indexedMessageCount(): number {
|
||||
const row = harness.read((db: SyncDatabase) =>
|
||||
db.prepare('SELECT count(*) AS n FROM messages').get()
|
||||
)
|
||||
return row && typeof row === 'object' && 'n' in row && typeof row.n === 'number' ? row.n : -1
|
||||
}
|
||||
|
||||
/** Sessions a published-view read returns for one term, the only legal shape. */
|
||||
function sessionsMatching(term: string): string[] {
|
||||
return harness.read((db: SyncDatabase) =>
|
||||
@@ -256,6 +264,9 @@ it('resumes after close and reopen without re-reading what it already indexed',
|
||||
// `filesIndexed` is the count of rows the index holds at their current stat,
|
||||
// so it stays 2. That nothing was opened again is the read loop's own test.
|
||||
expect(reopened.status()).toMatchObject({ filesIndexed: 2, filesDue: 0 })
|
||||
// The same number the pane shows as "messages searchable", read from the rows rather than counted as they land.
|
||||
expect(indexedMessageCount()).toBeGreaterThan(0)
|
||||
expect(reopened.status().messagesIndexed).toBe(indexedMessageCount())
|
||||
expect(
|
||||
harness.read((db: SyncDatabase) => db.prepare('SELECT count(*) AS n FROM messages').get())
|
||||
).toEqual(indexedRows)
|
||||
|
||||
@@ -36,6 +36,8 @@ export type SessionSearchIndexStatus = {
|
||||
filesDue: number
|
||||
/** Rows whose last read did not commit. */
|
||||
filesFailed: number
|
||||
/** Messages the index holds across every indexed row. */
|
||||
messagesIndexed: number
|
||||
degradedRoots: SessionSearchDegradedRoot[]
|
||||
lastReconcileAt: number | null
|
||||
/** When a whole-machine sweep last finished; null until one has. */
|
||||
@@ -187,13 +189,15 @@ export class SessionSearchIndexer {
|
||||
const settled = (this.closed ? this.lastCounts : this.readCounts()) ?? {
|
||||
current: 0,
|
||||
due: 0,
|
||||
failed: 0
|
||||
failed: 0,
|
||||
messages: 0
|
||||
}
|
||||
return {
|
||||
phase: this.phase(settled),
|
||||
filesIndexed: settled.current,
|
||||
filesDue: settled.due,
|
||||
filesFailed: settled.failed,
|
||||
messagesIndexed: settled.messages,
|
||||
degradedRoots: this.degradedRoots.map((root) => ({ ...root })),
|
||||
lastReconcileAt: this.lastReconcileAt,
|
||||
lastSweepCompletedAt: this.lastSweepCompletedAt
|
||||
|
||||
@@ -24,7 +24,8 @@ async function fixture() {
|
||||
// degradedRoots is re-stated because the contract type leaves `root` optional
|
||||
// for relay redaction, while the indexer always names the root it degraded.
|
||||
const indexer = {
|
||||
status: () => ({ ...status, degradedRoots: [] }),
|
||||
// messagesIndexed is optional on the wire and required of an indexer, which has read the rows.
|
||||
status: () => ({ ...status, messagesIndexed: 0, degradedRoots: [] }),
|
||||
reconcile: vi.fn(async () => {})
|
||||
}
|
||||
const service = createSessionSearchService({ engine: harness.engine, indexer })
|
||||
|
||||
@@ -41,8 +41,13 @@ export type SessionSearchFileRow = {
|
||||
failedMtimeMs: number | null
|
||||
}
|
||||
|
||||
/** How many rows are in each state; the whole of the indexer's progress report. */
|
||||
export type SessionSearchStateCounts = { current: number; due: number; failed: number }
|
||||
/** How many rows are in each state, plus the indexed message total; the whole of the indexer's progress report. */
|
||||
export type SessionSearchStateCounts = {
|
||||
current: number
|
||||
due: number
|
||||
failed: number
|
||||
messages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Owns the index database. PR 2 scope: the write half only — the transcript
|
||||
@@ -266,19 +271,29 @@ export class SessionSearchStore {
|
||||
}
|
||||
}
|
||||
|
||||
/** Rows per state. The status is this query and the pass's own degraded roots. */
|
||||
/** Rows per state and indexed messages. The status is these queries and the pass's own degraded roots. */
|
||||
stateCounts(): SessionSearchStateCounts {
|
||||
const rows = this.db.prepare('SELECT state, count(*) AS n FROM files GROUP BY state').all() as {
|
||||
state: SessionSearchFileState
|
||||
n: number
|
||||
}[]
|
||||
const counts: SessionSearchStateCounts = { current: 0, due: 0, failed: 0 }
|
||||
const counts: SessionSearchStateCounts = { current: 0, due: 0, failed: 0, messages: 0 }
|
||||
for (const row of rows) {
|
||||
counts[row.state] = Number(row.n)
|
||||
}
|
||||
counts.messages = this.messageCount()
|
||||
return counts
|
||||
}
|
||||
|
||||
/** Messages the index holds. Read with the file states so both describe one moment. */
|
||||
private messageCount(): number {
|
||||
const row: unknown = this.db.prepare('SELECT count(*) AS n FROM messages').get()
|
||||
if (row && typeof row === 'object' && 'n' in row && typeof row.n === 'number') {
|
||||
return row.n
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a source's rows. Only a proven deletion may call this: an unreadable
|
||||
* source is `unverifiable`, not `missing`, and keeps its rows
|
||||
|
||||
@@ -111,6 +111,8 @@ export const AiVaultSearchStatusSchema = z.object({
|
||||
filesIndexed: z.number().int().nonnegative(),
|
||||
filesDue: z.number().int().nonnegative(),
|
||||
filesFailed: z.number().int().nonnegative(),
|
||||
// Optional: a host that predates this field degrades to a session count only.
|
||||
messagesIndexed: z.number().int().nonnegative().optional(),
|
||||
// `root` is a host path, withheld over the relay; the array length is the count.
|
||||
degradedRoots: z.array(z.object({ root: z.string().optional(), reason: z.string() })),
|
||||
lastReconcileAt: z.number().nullable(),
|
||||
|
||||
Reference in New Issue
Block a user