From c8ff65f11f66d3dcee19903d2aea7379981f3e5d Mon Sep 17 00:00:00 2001 From: lyon Date: Sat, 20 Jun 2026 12:17:54 +0800 Subject: [PATCH] fix: reuse proven runtime readiness for workbench reads --- internal/cloud/workbench-facts-store.ts | 21 ++++++++++++++++ internal/db/runtime-store.test.ts | 33 +++++++++++++++++++++++++ internal/db/runtime-store.ts | 7 ++++++ 3 files changed, 61 insertions(+) diff --git a/internal/cloud/workbench-facts-store.ts b/internal/cloud/workbench-facts-store.ts index 7e26aa0e..1f890a87 100644 --- a/internal/cloud/workbench-facts-store.ts +++ b/internal/cloud/workbench-facts-store.ts @@ -27,6 +27,7 @@ export function createWorkbenchFactsStore(options = {}, actor = null) { persistence: result?.persistence ?? null }; } catch (error) { + logWorkbenchFactsQueryFailure(options.logger ?? console, params, error); return { facts: emptyWorkbenchFacts(), count: 0, @@ -137,6 +138,26 @@ export function createWorkbenchFactsStore(options = {}, actor = null) { }; } +function logWorkbenchFactsQueryFailure(logger, params = {}, error = null) { + const families = Array.isArray(params.families) ? params.families : []; + const event = { + event: "workbench_facts_query_failed", + ok: false, + families, + hasSessionId: Boolean(params.sessionId), + hasTraceId: Boolean(params.traceId), + hasOwnerUserId: Boolean(params.ownerUserId), + errorName: error?.name ?? "Error", + errorCode: error?.code ?? null, + message: error instanceof Error ? error.message : String(error ?? "unknown"), + valuesRedacted: true + }; + try { + if (typeof logger?.warn === "function") logger.warn(JSON.stringify(event)); + else if (typeof logger?.log === "function") logger.log(JSON.stringify(event)); + } catch {} +} + async function durableTraceSnapshot(runtimeStore, traceId) { if (!runtimeStore || typeof runtimeStore.queryAgentTraceEvents !== "function") return null; const safeId = safeTraceId(traceId); diff --git a/internal/db/runtime-store.test.ts b/internal/db/runtime-store.test.ts index ffd0761f..4d7e59b6 100644 --- a/internal/db/runtime-store.test.ts +++ b/internal/db/runtime-store.test.ts @@ -806,6 +806,33 @@ test("configured postgres runtime persists and queries Workbench durable facts", assert.ok(queryClient.calls.some((call) => call.sql.startsWith("SELECT session_json FROM workbench_sessions WHERE session_id = $1"))); }); +test("configured postgres runtime reuses proven readiness for Workbench durable reads", async () => { + const queryClient = createFakePostgresClient({ migrationReady: true }); + const store = createConfiguredCloudRuntimeStore({ + env: { + HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres", + HWLAB_CLOUD_RUNTIME_DURABLE: "true" + }, + dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab", + queryClient, + now: () => "2026-06-20T10:00:00.000Z" + }); + await store.writeWorkbenchFacts({ + facts: { + sessions: [{ sessionId: "ses_ready_cached", ownerUserId: "usr_ready", status: "running", updatedAt: "2026-06-20T10:00:01.000Z" }] + } + }); + queryClient.calls.length = 0; + + const first = await store.queryWorkbenchFacts({ families: ["sessions"], limit: 10 }); + const second = await store.queryWorkbenchFacts({ families: ["sessions"], limit: 10 }); + + assert.equal(first.count, 1); + assert.equal(second.count, 1); + assert.equal(queryClient.calls.filter((call) => isReadinessQuery(call.sql)).length, 0); + assert.equal(queryClient.calls.filter((call) => call.sql.startsWith("SELECT session_json FROM workbench_sessions")).length, 2); +}); + test("configured postgres runtime queries requested workbench fact families concurrently", async () => { const expectedReads = 4; let started = 0; @@ -930,6 +957,12 @@ test("configured postgres runtime pushes trace event query filters into SQL", as assert.deepEqual(select.params, ["ses_filter_b"]); }); +function isReadinessQuery(sql) { + return sql.includes("information_schema.columns") + || sql.startsWith("SELECT id, schema_version FROM hwlab_schema_migrations") + || sql.startsWith("SELECT COUNT(*)::int AS count FROM "); +} + function createFakePostgresClient({ migrationReady = true, migrationErrorCode = null, diff --git a/internal/db/runtime-store.ts b/internal/db/runtime-store.ts index 97ebaf39..106e5628 100644 --- a/internal/db/runtime-store.ts +++ b/internal/db/runtime-store.ts @@ -1372,7 +1372,10 @@ export class PostgresCloudRuntimeStore { } async assertReadyForDurableReads(method) { + const current = this.summary(); + if (isReadyForDurableRuntimeRead(current)) return; const readiness = await this.readiness(); + if (isReadyForDurableRuntimeRead(readiness)) return; if (readiness.ready !== true || readiness.durable !== true || readiness.liveRuntimeEvidence !== true) { throw durableRuntimeReadBlockedError(method, readiness); } @@ -1697,6 +1700,10 @@ export class PostgresCloudRuntimeStore { } } +function isReadyForDurableRuntimeRead(readiness = {}) { + return readiness.ready === true && readiness.durable === true && readiness.liveRuntimeEvidence === true; +} + function normalizeCapabilityInputs(params) { const value = params.capabilities ?? params.capability ?? params; if (Array.isArray(value)) {