mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
[comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -24,14 +24,11 @@ function getOpenCodeDatabaseOverride(environment: NodeJS.ProcessEnv): OpenCodeDa
|
||||
}
|
||||
}
|
||||
|
||||
// Why gated: the AI Vault's primary OpenCode source delegates here from inside
|
||||
// its discovery fan-out, so a UNC data dir or a UNC OPENCODE_DB on a stalled
|
||||
// distro would otherwise hang the whole scan on a raw syscall (STA-4049).
|
||||
// Gate UNC access so stalled WSL paths cannot hang the AI Vault discovery fan-out.
|
||||
export async function listOpenCodeDatabases(
|
||||
/** Lets a caller report the refusal; an empty list otherwise reads as
|
||||
* "OpenCode not used" rather than "we could not look". */
|
||||
/** Distinguish refused access from an unused provider. */
|
||||
onRefusal?: (path: string, error: WslTranscriptFsError) => void,
|
||||
/** Every other stat/readdir failure, including ENOENT; also read as an empty list. */
|
||||
/** Includes ENOENT; discovery otherwise returns an empty list. */
|
||||
onFsError?: (path: string, error: unknown) => void,
|
||||
signal?: AbortSignal,
|
||||
environment: NodeJS.ProcessEnv = process.env
|
||||
@@ -89,9 +86,7 @@ function reportFailure(
|
||||
}
|
||||
|
||||
export function compareOpenCodeClaimPriority(left: string, right: string): number {
|
||||
// Why: the canonical opencode.db is the live database; it must claim
|
||||
// duplicated sessions ahead of stale sibling copies. Remaining ties use
|
||||
// path order so ownership is deterministic across rescans.
|
||||
// Prefer the live canonical database over stale copies; path order breaks remaining ties.
|
||||
const leftRank = basename(left).toLowerCase() === 'opencode.db' ? 0 : 1
|
||||
const rightRank = basename(right).toLowerCase() === 'opencode.db' ? 0 : 1
|
||||
if (leftRank !== rightRank) {
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { UsageProvider } from '../usage/usage-provider-contract'
|
||||
import { scanOpenCodeUsageDatabasesViaWorker } from '../usage/usage-scan-worker-spawn'
|
||||
import type { OpenCodeUsageDailyAggregate, OpenCodeUsageSession } from './types'
|
||||
|
||||
// Why: v4 reads OpenCode 2's `session_v2` table; v3 caches miss every v2 session.
|
||||
// v5 merges a migrated session's two rows per column instead of picking one, so
|
||||
// v4 caches hold zeroed costs and pre-migration metadata.
|
||||
// v6 normalizes input; v7 preserves writes separately for telemetry.
|
||||
// Invalidate older caches: merge migrated rows per column, normalize input and preserve cache writes.
|
||||
export const OPENCODE_USAGE_SCHEMA_VERSION = 7
|
||||
|
||||
export const openCodeUsageProvider = {
|
||||
|
||||
@@ -30,16 +30,10 @@ type OpenCodeSessionUsageRow = {
|
||||
tokens_cache_write: number
|
||||
}
|
||||
|
||||
// Why: OpenCode 2 copies every v1 `session` row into `session_v2` and then only
|
||||
// writes there, so a migrated opencode.db holds both tables and the same session
|
||||
// id in each. Reading `session` alone loses every OpenCode 2 session (#15841);
|
||||
// reading both unfiltered would double-count the migrated ones. First entry is
|
||||
// the generation OpenCode still writes to.
|
||||
// OpenCode 2 freezes session after copying to session_v2; prioritize live rows without double-counting.
|
||||
const SESSION_TABLES_BY_PRIORITY = ['session_v2', 'session'] as const
|
||||
|
||||
// What the session *is*, with the SQL literal to substitute when no generation
|
||||
// carries the column. The live generation answers these; an older twin only
|
||||
// fills in what the live one left NULL.
|
||||
// Prefer live metadata; legacy rows fill NULLs using these defaults for absent columns.
|
||||
const SESSION_METADATA_COLUMNS: Record<string, string> = {
|
||||
project_id: 'NULL',
|
||||
directory: 'NULL',
|
||||
@@ -57,12 +51,11 @@ const SESSION_TOKEN_COLUMNS = [
|
||||
'tokens_cache_write'
|
||||
] as const
|
||||
|
||||
// What the session *spent*. Merged per column, never row-at-a-time.
|
||||
// Merge usage per column, not per row.
|
||||
const SESSION_USAGE_COLUMNS = ['cost', ...SESSION_TOKEN_COLUMNS] as const
|
||||
|
||||
const SESSION_TOKEN_TOTAL = SESSION_TOKEN_COLUMNS.map((name) => `s.${name}`).join(' + ')
|
||||
|
||||
/** One generation's row for a session id, merged into the select that owns it. */
|
||||
type SessionContributor = { table: string; alias: string }
|
||||
|
||||
function columnRef(
|
||||
@@ -73,14 +66,7 @@ function columnRef(
|
||||
return columnExists(db, contributor.table, name) ? `${contributor.alias}.${name}` : null
|
||||
}
|
||||
|
||||
// Why the live generation rather than whichever row has the bigger numbers:
|
||||
// after the v1 import, `session` is frozen while `session_v2` keeps being
|
||||
// written, so a pre-migration directory, title or timestamp survives in the
|
||||
// legacy twin indefinitely. The import also derives `session_v2.model` from the
|
||||
// last user message when the v1 row had none (`transformSession` in upstream
|
||||
// `v1-migration.bun.ts`), so the legacy row is the one that can be NULL here —
|
||||
// 23 of 234 shared ids on a real migrated database. Older generations only fill
|
||||
// NULLs.
|
||||
// Frozen legacy metadata can outlive migration; use it only where live values are NULL.
|
||||
function buildMetadataExpression(
|
||||
db: Database.Database,
|
||||
contributors: readonly SessionContributor[],
|
||||
@@ -97,14 +83,8 @@ function buildMetadataExpression(
|
||||
return `COALESCE(${[...refs, ...tail].join(', ')})`
|
||||
}
|
||||
|
||||
// Why per column rather than picking a winning row: both rows aggregate the same
|
||||
// assistant messages of the same session. The import re-derives every v2 total
|
||||
// from decoded messages and drops the ones that fail to decode, so each v2
|
||||
// column starts at or below its frozen legacy twin and then grows as the session
|
||||
// keeps running. Neither side can invent usage, so each column's MAX is a
|
||||
// strictly tighter lower bound on the truth than either row alone and can never
|
||||
// exceed it. Choosing a row instead lets a token comparison zero a recorded
|
||||
// cost, or a cost comparison zero recorded tokens.
|
||||
// Migration may drop undecodable messages, then live totals grow: each column’s MAX preserves
|
||||
// the strongest recorded lower bound without a winning row zeroing other cost/token columns.
|
||||
function buildUsageExpression(
|
||||
db: Database.Database,
|
||||
contributors: readonly SessionContributor[],
|
||||
@@ -116,8 +96,7 @@ function buildUsageExpression(
|
||||
if (refs.length === 0 || contributors.length === 1) {
|
||||
return refs[0] ?? '0'
|
||||
}
|
||||
// An outer-joined generation is NULL for ids it never held, and SQLite's
|
||||
// scalar MAX() returns NULL if any argument is.
|
||||
// Outer joins yield NULL for absent generations; SQLite scalar MAX propagates NULL.
|
||||
const guarded = refs.map((ref) => `COALESCE(${ref}, 0)`)
|
||||
return guarded.length === 1 ? (guarded[0] ?? '0') : `MAX(${guarded.join(', ')})`
|
||||
}
|
||||
@@ -134,8 +113,7 @@ function buildSessionTableSelect(
|
||||
index: number
|
||||
): string {
|
||||
const table = tables[index] ?? ''
|
||||
// Only lower-priority generations join in: a higher-priority one holding this
|
||||
// id would have excluded the row outright, so it has nothing to contribute.
|
||||
// Higher-priority holders exclude this row, so only lower generations can contribute.
|
||||
const contributors: SessionContributor[] = [
|
||||
{ table, alias: 't' },
|
||||
...tables
|
||||
@@ -154,9 +132,7 @@ function buildSessionTableSelect(
|
||||
.slice(1)
|
||||
.map((other) => `LEFT JOIN ${other.table} ${other.alias} ON ${other.alias}.id = t.id`)
|
||||
.join(' ')
|
||||
// Exactly one select claims each id: the highest-priority generation holding
|
||||
// it. Exclusive because every lower select rejects an id a higher one has,
|
||||
// exhaustive because the highest one holding it never rejects it.
|
||||
// Each ID belongs to its highest-priority holder; lower selects exclude higher holders.
|
||||
const exclusions = tables
|
||||
.slice(0, index)
|
||||
.map((other) => `NOT EXISTS (SELECT 1 FROM ${other} o WHERE o.id = t.id)`)
|
||||
@@ -164,7 +140,6 @@ function buildSessionTableSelect(
|
||||
return `SELECT t.id, ${columns.join(', ')} FROM ${table} t${joins ? ` ${joins}` : ''}${exclusions ? ` WHERE ${exclusions}` : ''}`
|
||||
}
|
||||
|
||||
/** A single deduplicated session relation spanning every session table generation. */
|
||||
function buildSessionSource(db: Database.Database, tables: readonly string[]): string {
|
||||
const selects = tables.map((_table, index) => buildSessionTableSelect(db, tables, index))
|
||||
return `(${selects.join(' UNION ALL ')})`
|
||||
@@ -190,8 +165,7 @@ function getAssistantSessionMessageCount(db: Database.Database): number {
|
||||
return row?.count ?? 0
|
||||
}
|
||||
|
||||
// `some`, not `every`: the merged row takes each usage column from whichever
|
||||
// generation carries it, so one table missing them costs nothing.
|
||||
// One generation’s usage columns suffice because merging fills each column independently.
|
||||
function hasSessionUsageColumns(db: Database.Database, tables: readonly string[]): boolean {
|
||||
return tables.some((table) =>
|
||||
['cost', 'tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read'].every(
|
||||
@@ -264,8 +238,7 @@ export function selectUsageRows(db: Database.Database): OpenCodeUsageRow[] {
|
||||
}
|
||||
const sessionSource = buildSessionSource(db, sessionTables)
|
||||
|
||||
// Why: newer OpenCode DBs maintain session-level token/cost totals. Reading
|
||||
// one aggregate row per session is faster than parsing every message blob.
|
||||
// Session aggregates avoid parsing every message blob.
|
||||
if (hasSessionUsageColumns(db, sessionTables) && getSessionUsageRowCount(db, sessionSource) > 0) {
|
||||
return selectSessionUsageRows(db, sessionSource)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { mkdirSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
import SyncDatabase from '../sqlite/sync-database'
|
||||
|
||||
// The OpenCode schemas as the app itself creates them, written out in full
|
||||
// rather than trimmed to the columns a reader names: every read probes for its
|
||||
// columns, so a trimmed fixture would pass a probe the real database fails.
|
||||
// Foreign keys are dropped so a generation can be built without its siblings.
|
||||
// Use complete upstream schemas so column probes match real databases; omit foreign keys
|
||||
// to construct generations independently.
|
||||
|
||||
const PROJECT_TABLE = `
|
||||
CREATE TABLE project (
|
||||
@@ -15,7 +13,7 @@ const PROJECT_TABLE = `
|
||||
);
|
||||
`
|
||||
|
||||
// OpenCode 1.17.x — and still present, frozen, after an OpenCode 2 migration.
|
||||
// OpenCode 1.17.x; frozen after OpenCode 2 migration.
|
||||
const LEGACY_SESSION_TABLE = `
|
||||
CREATE TABLE session (
|
||||
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, parent_id TEXT, slug TEXT NOT NULL,
|
||||
@@ -39,7 +37,6 @@ const LEGACY_SESSION_TABLE = `
|
||||
);
|
||||
`
|
||||
|
||||
// OpenCode 2.0.x — session rows move to `session_v2`, messages to `session_message`.
|
||||
const V2_SESSION_TABLE = `
|
||||
CREATE TABLE session_v2 (
|
||||
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, workspace_id TEXT, parent_id TEXT,
|
||||
@@ -57,8 +54,7 @@ const V2_SESSION_TABLE = `
|
||||
);
|
||||
`
|
||||
|
||||
// A `session_v2` that never grew the token/cost columns. Reachable on databases
|
||||
// whose v2 lineage predates them, and the shape that must not erase a legacy row.
|
||||
// Older v2 schemas lack totals and must not erase legacy usage.
|
||||
const V2_SESSION_TABLE_WITHOUT_TOKENS = `
|
||||
CREATE TABLE session_v2 (
|
||||
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, workspace_id TEXT, parent_id TEXT,
|
||||
@@ -82,7 +78,7 @@ export type OpenCodeUsageFixtureSession = {
|
||||
id: string
|
||||
directory: string
|
||||
title?: string
|
||||
/** `null` writes a NULL `model`, the shape a v1 row has before the import derives one. */
|
||||
/** NULL matches v1 rows before migration derives a model. */
|
||||
model?: string | null
|
||||
cost?: number
|
||||
tokensInput?: number
|
||||
@@ -94,25 +90,17 @@ export type OpenCodeUsageFixtureSession = {
|
||||
timeUpdated?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Which session tables the database carries.
|
||||
*
|
||||
* - `v1`: OpenCode 1 only.
|
||||
* - `migrated`: what an OpenCode 2 upgrade leaves behind — both tables, with the
|
||||
* pre-upgrade sessions copied into `session_v2` and `session` frozen.
|
||||
* - `v2-only`: `session_v2` without the legacy table at all.
|
||||
*/
|
||||
/** Migrated databases retain frozen v1 rows alongside their live v2 copies. */
|
||||
export type OpenCodeUsageFixtureGeneration = 'v1' | 'migrated' | 'v2-only'
|
||||
|
||||
export type OpenCodeUsageFixtureSpec = {
|
||||
generation: OpenCodeUsageFixtureGeneration
|
||||
/** Rows in the legacy `session` table; ignored for `v2-only`. */
|
||||
/** Ignored for v2-only. */
|
||||
legacySessions?: readonly OpenCodeUsageFixtureSession[]
|
||||
/** Rows in `session_v2`; ignored for `v1`. */
|
||||
/** Ignored for v1. */
|
||||
v2Sessions?: readonly OpenCodeUsageFixtureSession[]
|
||||
/** Build `session_v2` without the token/cost columns; ignored for `v1`. */
|
||||
/** Omit v2 totals; ignored for v1. */
|
||||
v2WithoutTokenColumns?: boolean
|
||||
/** `project.worktree`, the repo root OpenCode recorded for the project. */
|
||||
worktree?: string
|
||||
}
|
||||
|
||||
@@ -158,11 +146,6 @@ function insertSession(
|
||||
).run(...identity, ...usage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OpenCode usage database at `dbPath` for one schema generation.
|
||||
* @param dbPath - Where to create the database; parent directories are created.
|
||||
* @param spec - The generation to build and the session rows to write.
|
||||
*/
|
||||
export function writeOpenCodeUsageDatabase(dbPath: string, spec: OpenCodeUsageFixtureSpec): void {
|
||||
mkdirSync(dirname(dbPath), { recursive: true })
|
||||
const db = new SyncDatabase(dbPath)
|
||||
|
||||
@@ -13,11 +13,6 @@ import {
|
||||
} from './opencode-usage-sqlite-fixture'
|
||||
import { parseOpenCodeUsageDatabase } from './scanner'
|
||||
|
||||
// Why: OpenCode 2 copies v1 `session` rows into `session_v2` and then writes
|
||||
// only there. A reader that knows just `session` reports zero usage for every
|
||||
// OpenCode 2 session, and a reader that unions both double-counts the migrated
|
||||
// ones (#15841). These tests pin both halves against the real v2 schema.
|
||||
|
||||
const WORKTREE = '/workspace/repo'
|
||||
|
||||
let tempDirs: string[] = []
|
||||
@@ -108,7 +103,6 @@ describe('OpenCode 2 session_v2 usage', () => {
|
||||
const [event] = readEvents(path)
|
||||
expect(event?.cachedInputTokens).toBe(7_000)
|
||||
expect(event?.inputTokens).toBe(7_010)
|
||||
// Both cache buckets contribute to input; total counts them once.
|
||||
expect(event?.totalTokens).toBe(7_011)
|
||||
})
|
||||
|
||||
@@ -121,7 +115,6 @@ describe('OpenCode 2 session_v2 usage', () => {
|
||||
}
|
||||
const path = createFixture({
|
||||
generation: 'migrated',
|
||||
// The frozen pre-upgrade copy, still carrying the smaller snapshot totals.
|
||||
legacySessions: [shared],
|
||||
v2Sessions: [
|
||||
{ ...shared, tokensInput: 180, tokensCacheRead: 60 },
|
||||
@@ -152,8 +145,7 @@ describe('OpenCode 2 session_v2 usage', () => {
|
||||
it('keeps the legacy totals when the migration recomputed session_v2 lower', () => {
|
||||
const path = createFixture({
|
||||
generation: 'migrated',
|
||||
// Upstream recomputes v2 totals from decoded messages; undecodable ones
|
||||
// are dropped, landing the v2 row below its frozen legacy copy.
|
||||
// Migration drops undecodable messages, so v2 totals may be lower than frozen v1 totals.
|
||||
legacySessions: [{ id: 'ses_shared', directory: WORKTREE, tokensInput: 900 }],
|
||||
v2Sessions: [{ id: 'ses_shared', directory: WORKTREE, tokensInput: 120 }]
|
||||
})
|
||||
@@ -174,7 +166,6 @@ describe('OpenCode 2 session_v2 usage', () => {
|
||||
const events = readEvents(path)
|
||||
expect(events.map((event) => event.sessionId)).toEqual(['ses_shared'])
|
||||
expect(events[0]?.inputTokens).toBe(64)
|
||||
// Equal totals: the tie goes to the newer table, so its row supplies metadata.
|
||||
expect(events[0]?.model).toContain('v2')
|
||||
})
|
||||
|
||||
@@ -299,11 +290,6 @@ describe('OpenCode 2 session_v2 usage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Why: a migrated session has two rows for one session, and neither is complete.
|
||||
// `session_v2` is the row OpenCode still writes, so it says what the session is;
|
||||
// each usage column is a lossy re-derivation, so each column takes the larger of
|
||||
// the two. Ranking whole rows by one number let that number decide cost,
|
||||
// directory and model too.
|
||||
describe('OpenCode 2 migrated session column merge', () => {
|
||||
const SHARED = 'ses_shared'
|
||||
|
||||
@@ -333,7 +319,6 @@ describe('OpenCode 2 migrated session column merge', () => {
|
||||
|
||||
const events = readEvents(path)
|
||||
expect(events).toHaveLength(1)
|
||||
// The legacy row still wins the token column; it must not drag metadata with it.
|
||||
expect(events[0]?.inputTokens).toBe(900)
|
||||
expect(events[0]?.cwd).toBe('/new/current-path')
|
||||
})
|
||||
@@ -341,8 +326,7 @@ describe('OpenCode 2 migrated session column merge', () => {
|
||||
it('keeps the model session_v2 derived when the legacy row has none', () => {
|
||||
const path = createFixture({
|
||||
generation: 'migrated',
|
||||
// The import fills session_v2.model from the last user message when the v1
|
||||
// row had none: 23 of 234 shared ids on a real migrated database.
|
||||
// Migration derives a missing v1 model from the last user message.
|
||||
legacySessions: [{ id: SHARED, directory: WORKTREE, model: null, tokensInput: 900 }],
|
||||
v2Sessions: [
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
OpenCodeUsagePersistedState
|
||||
} from './types'
|
||||
|
||||
/** Fills cost fields older OpenCode caches never wrote. Electron-free: the scan worker runs it too. */
|
||||
/** Normalize legacy cache cost fields without Electron so scan workers can use it. */
|
||||
export function normalizeOpenCodeUsagePersistedDatabases(
|
||||
databases: OpenCodeUsagePersistedDatabase[] | undefined
|
||||
): OpenCodeUsagePersistedDatabase[] {
|
||||
|
||||
@@ -28,8 +28,7 @@ import {
|
||||
WslTranscriptFsError
|
||||
} from '../native-chat/wsl-transcript-fs-gate'
|
||||
|
||||
// A stalled task holds the gate's single scan slot until it settles, so every
|
||||
// case releases its stall before the next one runs.
|
||||
// Release stalled tasks between cases: each holds the gate’s single scan slot until settlement.
|
||||
let releaseStall: (() => void) | undefined
|
||||
|
||||
function stalls<T>(): Promise<T> {
|
||||
@@ -38,8 +37,7 @@ function stalls<T>(): Promise<T> {
|
||||
})
|
||||
}
|
||||
|
||||
// Complete: UNC readdir results pass through the child dispatcher's dirent
|
||||
// serializer, which reads every kind flag.
|
||||
// The child serializer reads every dirent kind flag.
|
||||
function dirent(name: string) {
|
||||
return {
|
||||
name,
|
||||
@@ -53,8 +51,6 @@ function dirent(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// The point of the gate: an ungated syscall on a stalled 9P mount never returns,
|
||||
// so the call must still be pending at the deadline and settle just after it.
|
||||
async function settlesOnlyAtTheScanDeadline(pending: Promise<string[]>): Promise<string[]> {
|
||||
let settled = false
|
||||
const tracked = pending.then((value) => {
|
||||
@@ -70,8 +66,7 @@ async function settlesOnlyAtTheScanDeadline(pending: Promise<string[]>): Promise
|
||||
let originalDatabaseOverride: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
// blockedRoutes is persistent gate state: a prior stall must not quarantine
|
||||
// this test's route.
|
||||
// Persistent route quarantine must not leak between cases.
|
||||
resetWslTranscriptFsGateForTests()
|
||||
originalDatabaseOverride = process.env.OPENCODE_DB
|
||||
delete process.env.OPENCODE_DB
|
||||
@@ -95,14 +90,11 @@ afterEach(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Why this file exists: the AI Vault's primary OpenCode source reaches these
|
||||
// syscalls transitively, so the import-guard that pins direct `node:fs` use in
|
||||
// the transcript modules never saw them (STA-4049).
|
||||
// These transitive discovery syscalls escape direct filesystem import guards.
|
||||
describe('OpenCode database discovery on a stalled WSL data directory', () => {
|
||||
it('gates the data-directory listing instead of hanging the scan', async () => {
|
||||
mocks.readdir.mockImplementation(stalls)
|
||||
// Asserted because an empty list alone also matches a missing data dir; the
|
||||
// AI Vault's OpenCode source turns this callback into the scan issue.
|
||||
// Empty results alone cannot distinguish missing data from gated refusal.
|
||||
const onRefusal = vi.fn()
|
||||
|
||||
expect(await settlesOnlyAtTheScanDeadline(listOpenCodeDatabases(onRefusal))).toEqual([])
|
||||
@@ -116,8 +108,7 @@ describe('OpenCode database discovery on a stalled WSL data directory', () => {
|
||||
|
||||
expect(await settlesOnlyAtTheScanDeadline(listOpenCodeDatabases(onRefusal))).toEqual([])
|
||||
expect(mocks.readdir).not.toHaveBeenCalled()
|
||||
// Loose on the path: `isAbsolute` is host-flavoured, so a UNC override only
|
||||
// stays verbatim on Windows. The refusal reaching the caller is the contract.
|
||||
// UNC paths remain verbatim only on Windows; assert refusal across hosts.
|
||||
expect(onRefusal).toHaveBeenCalledWith(
|
||||
expect.stringContaining('opencode.db'),
|
||||
expect.any(WslTranscriptFsError)
|
||||
|
||||
@@ -442,8 +442,7 @@ describe('scanOpenCodeUsageDatabases', () => {
|
||||
const db = new Database(path)
|
||||
insertSessionTotalsRow(db, sessionId, inputTokens)
|
||||
db.close()
|
||||
// Why: the row fits the existing page and can land in the same coarse mtime
|
||||
// tick, so pin a later mtime to change the scanner's mtime+size fingerprint.
|
||||
// A same-page write may retain coarse mtime; force the scanner’s mtime/size fingerprint to change.
|
||||
const changedAt = new Date(previousMtimeMs + 1000)
|
||||
utimesSync(path, changedAt, changedAt)
|
||||
}
|
||||
@@ -462,8 +461,6 @@ describe('scanOpenCodeUsageDatabases', () => {
|
||||
})
|
||||
|
||||
it('counts a session duplicated into a stale backup database exactly once', async () => {
|
||||
// The backup holds a stale snapshot of session-1; the canonical db has
|
||||
// grown since. The canonical totals must win and be counted once.
|
||||
writeSessionTotalsDb('opencode.db', [
|
||||
['session-1', 1000],
|
||||
['session-2', 300]
|
||||
@@ -483,8 +480,6 @@ describe('scanOpenCodeUsageDatabases', () => {
|
||||
|
||||
it('still counts backup-only sessions and stays stable across cached rescans', async () => {
|
||||
const canonicalPath = writeSessionTotalsDb('opencode.db', [['session-1', 1000]])
|
||||
// The backup duplicates session-1 (stale) and preserves session-9, which
|
||||
// no longer exists in the canonical database.
|
||||
writeSessionTotalsDb('opencode-backup.db', [
|
||||
['session-1', 400],
|
||||
['session-9', 50]
|
||||
@@ -500,8 +495,6 @@ describe('scanOpenCodeUsageDatabases', () => {
|
||||
second.dailyAggregates.reduce((total, aggregate) => total + aggregate.inputTokens, 0)
|
||||
).toBe(1050)
|
||||
|
||||
// The canonical db keeps growing while the backup stays cached; session-1
|
||||
// must stay owned by the canonical db.
|
||||
appendLiveSessionRow(canonicalPath, 'session-2', 200)
|
||||
|
||||
const third = await scanOpenCodeUsageDatabases([], second.processedDatabases)
|
||||
@@ -514,9 +507,6 @@ describe('scanOpenCodeUsageDatabases', () => {
|
||||
})
|
||||
|
||||
it('reuses a fully-duplicate backup instead of reparsing it when the live db changes', async () => {
|
||||
// The backup only holds stale copies of sessions the canonical db already
|
||||
// owns, so it owns nothing. When the live db changes it must not be
|
||||
// demoted back into the parse set — there is no claim to reclaim.
|
||||
const canonicalPath = writeSessionTotalsDb('opencode.db', [['session-1', 1000]])
|
||||
writeSessionTotalsDb('opencode-backup.db', [['session-1', 400]])
|
||||
|
||||
@@ -532,7 +522,7 @@ describe('scanOpenCodeUsageDatabases', () => {
|
||||
const secondBackup = second.processedDatabases.find((database) =>
|
||||
database.path.endsWith('opencode-backup.db')
|
||||
)
|
||||
// A reused cache entry is the same object; a reparse would produce a new one.
|
||||
// Cache reuse preserves object identity.
|
||||
expect(secondBackup).toBe(firstBackup)
|
||||
expect(
|
||||
second.dailyAggregates.reduce((total, aggregate) => total + aggregate.inputTokens, 0)
|
||||
@@ -540,9 +530,6 @@ describe('scanOpenCodeUsageDatabases', () => {
|
||||
})
|
||||
|
||||
it('lets the live database reclaim sessions after a sticky backup-only claim', async () => {
|
||||
// First scan only has the backup (e.g. live db temporarily missing), so it
|
||||
// owns session-1 at the stale snapshot. When opencode.db reappears with
|
||||
// higher totals it must reclaim the session instead of staying frozen.
|
||||
writeSessionTotalsDb('opencode-backup.db', [['session-1', 400]])
|
||||
|
||||
const first = await scanOpenCodeUsageDatabases([], [])
|
||||
|
||||
@@ -76,8 +76,7 @@ export async function parseOpenCodeUsageDatabase(
|
||||
if (!parsed) {
|
||||
continue
|
||||
}
|
||||
// Why: a stale sibling copy of opencode.db carries the same sessions, so
|
||||
// each session must be counted from exactly one database (#8006).
|
||||
// Sibling backups duplicate sessions; count each from one owner.
|
||||
let owned = claimedBySessionId.get(parsed.sessionId)
|
||||
if (owned === undefined) {
|
||||
owned = options.claimSession ? options.claimSession(parsed.sessionId) : true
|
||||
@@ -118,13 +117,11 @@ export async function scanOpenCodeUsageDatabases(
|
||||
const previousByPath = new Map(
|
||||
previousProcessedDatabases.map((database) => [database.path, database])
|
||||
)
|
||||
// Why: one resolver for the whole scan so every database shares the per-cwd memo.
|
||||
// Share per-cwd resolution memoization across databases.
|
||||
const resolveWorktree = await createUsageWorktreeResolver(worktrees)
|
||||
|
||||
const currentPaths = new Set(dbPaths)
|
||||
// Why: when a database that owned sessions is deleted, remaining siblings
|
||||
// still contain those sessions but their caches record them as unowned.
|
||||
// Only databases that previously deferred claims can reclaim.
|
||||
// When an owner disappears, only caches with deferred claims can reclaim its sessions.
|
||||
const lostOwnerPath = previousProcessedDatabases.some(
|
||||
(database) =>
|
||||
!currentPaths.has(database.path) &&
|
||||
@@ -137,7 +134,6 @@ export async function scanOpenCodeUsageDatabases(
|
||||
for (const dbPath of dbPaths) {
|
||||
const databaseInfo = await getProcessedDatabaseInfo(dbPath)
|
||||
const previous = previousByPath.get(dbPath)
|
||||
// When an owner disappears, only deferred-claim databases need reparse.
|
||||
const mustReclaimDeferred = lostOwnerPath && previous?.hasDeferredClaims !== false
|
||||
const canReuse =
|
||||
!mustReclaimDeferred &&
|
||||
@@ -153,13 +149,8 @@ export async function scanOpenCodeUsageDatabases(
|
||||
}
|
||||
}
|
||||
|
||||
// Why: a sticky backup claim from a scan where opencode.db was missing would
|
||||
// otherwise freeze a still-growing session at the backup snapshot when the
|
||||
// live db reappears. Reparse a lower-priority sibling only when it still owns
|
||||
// sessions a higher-priority path could reclaim; a sibling that owns nothing
|
||||
// (the common case once the live db has claimed every shared session) has no
|
||||
// claim to give back, so reparsing it every time the live db changes is pure
|
||||
// work.
|
||||
// A returning live database must reclaim sticky backup ownership; backups owning nothing
|
||||
// need no reparse when live data changes.
|
||||
const demotedReusePaths: string[] = []
|
||||
for (const [dbPath, reused] of reusedByPath) {
|
||||
if ((reused.ownedSessionIds?.length ?? 0) === 0) {
|
||||
@@ -177,11 +168,7 @@ export async function scanOpenCodeUsageDatabases(
|
||||
pathsToParse.push(dbPath)
|
||||
}
|
||||
|
||||
// Why: `opencode-*.db` siblings are typically stale copies of `opencode.db`
|
||||
// (backups), so mergeSessions would double every duplicated session (#8006).
|
||||
// Each session is counted from exactly one database. The canonical live db
|
||||
// claims first so a stale backup cannot freeze a still-growing session at
|
||||
// its snapshot totals; cached databases keep the claims they persisted.
|
||||
// The live database claims before stale backups; reused caches retain persisted ownership.
|
||||
const sessionOwnerById = new Map<string, string>()
|
||||
for (const dbPath of [...reusedByPath.keys()].sort(compareOpenCodeClaimPriority)) {
|
||||
const previous = reusedByPath.get(dbPath)
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import type SyncDatabase from '../sqlite/sync-database'
|
||||
|
||||
// Why: OpenCode's usage scanner and the AI Vault session scanner both need to
|
||||
// probe the opencode.db schema shape across multiple DB generations. Centralizing
|
||||
// the probes here avoids two private copies and keeps the contract testable.
|
||||
type Database = SyncDatabase.Database
|
||||
|
||||
/**
|
||||
* Check whether a table exists in the given SQLite database.
|
||||
* @param db - A readonly or read-write SyncDatabase instance.
|
||||
* @param tableName - The table name to look up in sqlite_master.
|
||||
* @returns `true` if the table exists, `false` otherwise.
|
||||
*/
|
||||
export function tableExists(db: Database, tableName: string): boolean {
|
||||
const row = db
|
||||
.prepare("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = ?")
|
||||
@@ -18,13 +9,6 @@ export function tableExists(db: Database, tableName: string): boolean {
|
||||
return row?.found === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a column exists on a table in the given SQLite database.
|
||||
* @param db - A readonly or read-write SyncDatabase instance.
|
||||
* @param tableName - The table to inspect via PRAGMA table_info.
|
||||
* @param columnName - The column name to find.
|
||||
* @returns `true` if the column exists on the table, `false` otherwise.
|
||||
*/
|
||||
export function columnExists(db: Database, tableName: string, columnName: string): boolean {
|
||||
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all() as { name?: string }[]
|
||||
return rows.some((row) => row.name === columnName)
|
||||
|
||||
@@ -91,12 +91,9 @@ export type OpenCodeUsageDailyAggregate = {
|
||||
export type OpenCodeUsagePersistedDatabase = OpenCodeUsageProcessedDatabase & {
|
||||
sessions: OpenCodeUsageSession[]
|
||||
dailyAggregates: OpenCodeUsageDailyAggregate[]
|
||||
/** Session ids this database counted. Sibling copies (opencode-backup.db)
|
||||
* duplicate sessions; ownership keeps each session counted by exactly one
|
||||
* cached database across incremental scans. */
|
||||
/** Persist ownership so sibling backups cannot double-count sessions across scans. */
|
||||
ownedSessionIds: string[]
|
||||
/** True when this database saw sessions already claimed by another database.
|
||||
* When that owner disappears, only deferred databases need reparse. */
|
||||
/** Only databases with deferred claims need reparse when an owner disappears. */
|
||||
hasDeferredClaims: boolean
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user