diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index 8cc039944..adffcf858 100644
--- a/packages/plugin/package.json
+++ b/packages/plugin/package.json
@@ -39,7 +39,7 @@
"build:tui": "bun scripts/build-tui.ts",
"check:tui-compiled": "bun run build:tui && git diff --exit-code -- src/tui-compiled && test -z \"$(git status --porcelain -- src/tui-compiled)\"",
"typecheck": "tsc -p ../retina-local-fs/tsconfig.build.json && tsc --noEmit && tsc -p tsconfig.scripts.json",
- "test": "bun test",
+ "test": "bun test --timeout 30000",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
diff --git a/packages/plugin/src/features/magic-context/message-index-async.test.ts b/packages/plugin/src/features/magic-context/message-index-async.test.ts
index 9f9c82203..467f53a9d 100644
--- a/packages/plugin/src/features/magic-context/message-index-async.test.ts
+++ b/packages/plugin/src/features/magic-context/message-index-async.test.ts
@@ -1,6 +1,6 @@
///
-import { afterEach, beforeEach, describe, expect, it } from "bun:test";
+import { afterEach, beforeEach, describe, expect, it, jest } from "bun:test";
import type { RawMessage } from "../../hooks/magic-context/read-session-raw";
import { BOOT_QUIET_MS, setBootQuietPeriodForTests } from "../../plugin/boot-quiet";
@@ -114,6 +114,7 @@ describe("message-index-async", () => {
afterEach(() => {
setBootQuietPeriodForTests(null);
+ jest.useRealTimers();
closeQuietly(db);
__resetMessageIndexAsyncForTests();
});
@@ -359,6 +360,8 @@ describe("message-index-async", () => {
});
it("rebuilds when removal overtakes a boot-quiet reconciliation", async () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(0);
const sessionId = "ses-boot-clear";
const surviving = [message("m-survivor", 1, "surviving searchable bytes")];
let reads = 0;
@@ -366,13 +369,15 @@ describe("message-index-async", () => {
reads++;
return surviving;
};
- setBootQuietPeriodForTests(Date.now() - BOOT_QUIET_MS + 20);
+ setBootQuietPeriodForTests(0);
- scheduleReconciliation(db, sessionId, readSurviving);
- scheduleClearAndReindex(db, sessionId, readSurviving);
+ const reconciliationFinished = scheduleReconciliation(db, sessionId, readSurviving);
+ const rebuildFinished = scheduleClearAndReindex(db, sessionId, readSurviving);
expect(isSessionReconciled(sessionId)).toBe(false);
- await wait(80);
+ jest.advanceTimersByTime(BOOT_QUIET_MS);
+ jest.runAllTimers();
+ await Promise.all([reconciliationFinished, rebuildFinished]);
expect(reads).toBe(2);
expect(countMessageRows(db, sessionId, "m-survivor")).toBe(1);
diff --git a/packages/plugin/src/features/magic-context/message-index-async.ts b/packages/plugin/src/features/magic-context/message-index-async.ts
index 7b5b6a2fc..a33c50942 100644
--- a/packages/plugin/src/features/magic-context/message-index-async.ts
+++ b/packages/plugin/src/features/magic-context/message-index-async.ts
@@ -73,7 +73,7 @@ const INCREMENTAL_DEBOUNCE_MS = 100;
const RECONCILIATION_BATCH_SIZE = 100;
const reconciledSessions = new Set();
-const reconciliationScheduledSessions = new Set();
+const reconciliationScheduledSessions = new Map>();
const sessionLocks = new Map>();
const incrementalTimers = new Map>();
const pendingIncrementalKeys = new Set();
@@ -199,23 +199,29 @@ export function scheduleReconciliation(
db: Database,
sessionId: string,
readMessages: ReadMessages,
-): void {
- if (reconciledSessions.has(sessionId) || reconciliationScheduledSessions.has(sessionId)) {
- return;
- }
- reconciliationScheduledSessions.add(sessionId);
-
- scheduleAfterBootQuiet(() => {
- defer(() => {
- void reconcileSessionIndex(db, sessionId, readMessages)
- .catch((error) => {
- logIndexingError(sessionId, "reconciliation", error);
- })
- .finally(() => {
- reconciliationScheduledSessions.delete(sessionId);
- });
+): Promise {
+ if (reconciledSessions.has(sessionId)) return Promise.resolve();
+ const scheduled = reconciliationScheduledSessions.get(sessionId);
+ if (scheduled) return scheduled;
+
+ const completion = new Promise((resolve) => {
+ scheduleAfterBootQuiet(() => {
+ defer(() => {
+ void reconcileSessionIndex(db, sessionId, readMessages)
+ .catch((error) => {
+ logIndexingError(sessionId, "reconciliation", error);
+ })
+ .finally(() => {
+ if (reconciliationScheduledSessions.get(sessionId) === completion) {
+ reconciliationScheduledSessions.delete(sessionId);
+ }
+ resolve();
+ });
+ });
});
});
+ reconciliationScheduledSessions.set(sessionId, completion);
+ return completion;
}
export function scheduleIncrementalIndex(
@@ -275,26 +281,29 @@ export function scheduleClearAndReindex(
db: Database,
sessionId: string,
readMessages: ReadMessages,
-): void {
+): Promise {
reconciledSessions.delete(sessionId);
reconciliationScheduledSessions.delete(sessionId);
clearCompletedIncrementalKeys(sessionId);
- scheduleAfterBootQuiet(() => {
- defer(() => {
- void runWithSessionLock(sessionId, () => {
- // An older boot-quiet reconciliation can finish after this clear was
- // scheduled, so invalidate process state under the same session lock
- // that clears the durable index.
- reconciledSessions.delete(sessionId);
- clearCompletedIncrementalKeys(sessionId);
- clearIndexedMessages(db, sessionId);
- })
- .then(() => reconcileSessionIndex(db, sessionId, readMessages))
- .catch((error) => {
+ return new Promise((resolve) => {
+ scheduleAfterBootQuiet(() => {
+ defer(() => {
+ void runWithSessionLock(sessionId, () => {
+ // An older boot-quiet reconciliation can finish after this clear was
+ // scheduled, so invalidate process state under the same session lock
+ // that clears the durable index.
reconciledSessions.delete(sessionId);
- logIndexingError(sessionId, "clear and reindex", error);
- });
+ clearCompletedIncrementalKeys(sessionId);
+ clearIndexedMessages(db, sessionId);
+ })
+ .then(() => reconcileSessionIndex(db, sessionId, readMessages))
+ .catch((error) => {
+ reconciledSessions.delete(sessionId);
+ logIndexingError(sessionId, "clear and reindex", error);
+ })
+ .finally(resolve);
+ });
});
});
}
diff --git a/packages/plugin/src/features/magic-context/storage-embedding-measurements.test.ts b/packages/plugin/src/features/magic-context/storage-embedding-measurements.test.ts
index 8b4a64ff3..759d07221 100644
--- a/packages/plugin/src/features/magic-context/storage-embedding-measurements.test.ts
+++ b/packages/plugin/src/features/magic-context/storage-embedding-measurements.test.ts
@@ -62,30 +62,32 @@ describe("embedding measurement corpus", () => {
const db = openDatabase();
const overflow = 5;
const total = MEASUREMENT_CORPUS_SESSION_ROW_CAP + overflow;
- for (let i = 0; i < total; i++) {
- recordEmbeddingMeasurement(db, {
- sessionId: "ses-cap",
- projectPath: "/repo",
- // Unique query text per row: dedup is on (query hash, cohort), so
- // distinct queries simulate the cohort-transition growth.
- queryText: `query ${i}`,
- cohortKey: "fp-a:0|fp-b:0",
- primaryResultIds: [],
- shadowResultIds: [],
- primaryLatencyMs: 1,
- shadowLatencyMs: 1,
- primaryFailed: false,
- shadowFailed: false,
- primaryModelId: "local-id",
- shadowModelId: "synapse-id",
- primaryFingerprint: "",
- shadowFingerprint: "fp-b",
- primaryEpoch: 0,
- shadowEpoch: 0,
- corpusHash: `corpus-${i}`,
- coverage: {},
- });
- }
+ db.transaction(() => {
+ for (let i = 0; i < total; i++) {
+ recordEmbeddingMeasurement(db, {
+ sessionId: "ses-cap",
+ projectPath: "/repo",
+ // Unique query text per row: dedup is on (query hash, cohort), so
+ // distinct queries simulate the cohort-transition growth.
+ queryText: `query ${i}`,
+ cohortKey: "fp-a:0|fp-b:0",
+ primaryResultIds: [],
+ shadowResultIds: [],
+ primaryLatencyMs: 1,
+ shadowLatencyMs: 1,
+ primaryFailed: false,
+ shadowFailed: false,
+ primaryModelId: "local-id",
+ shadowModelId: "synapse-id",
+ primaryFingerprint: "",
+ shadowFingerprint: "fp-b",
+ primaryEpoch: 0,
+ shadowEpoch: 0,
+ corpusHash: `corpus-${i}`,
+ coverage: {},
+ });
+ }
+ })();
const rows = listEmbeddingMeasurements(db, "ses-cap");
expect(rows).toHaveLength(MEASUREMENT_CORPUS_SESSION_ROW_CAP);
diff --git a/packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts b/packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts
index e763d2ac5..2a81f672b 100644
--- a/packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts
+++ b/packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts
@@ -49,6 +49,9 @@ describe("compiled TUI runtime imports", () => {
* Built from the shared list so the test cannot cover fewer modules than the
* build rewrites. */
async function loadExportSets(): Promise>> {
+ // @opentui/core/testing subclasses a core export during module initialization;
+ // warm core before the parallel imports so Bun cannot expose its TDZ.
+ await import("@opentui/core");
const entries = await Promise.all(
TUI_RUNTIME_SPECIFIERS.map(
async (specifier) =>