feat(sqlite): make worker deadlines adjustable with recovery guidance (#5818)

Expose a per-call SQLite worker deadline with exact CLI/MCP recovery controls,
parent-owned execution metadata and historical unknown preservation. Keep fixed
resource limits and cleanup admission intact. Share typed cleanup composition
with one captured-output owner and preserve the timeout after cleanup resolves.

Fixes #5812. Follow-up tracking: #5808. Existing CLI test-oracle failure: #5845.

Verified 155 SQLite tests (3 skipped), static/docs/discovery checks, six real
CLI/MCP pairs and 13 actual Codex sessions including four corrected deadline
recoveries. Three pre-dispatch discovery failures remain explicit, not passes.
Review findings include real retained-cleanup and fixture ownership regressions.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
This commit is contained in:
gensokyo-dev
2026-10-21 16:45:46 +08:00
committed by GitHub
parent 5241597594
commit f801b2fbfd
15 changed files with 496 additions and 49 deletions
@@ -4,11 +4,13 @@ import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCo
import { err } from "../../domain/result.js";
import { ok } from "../../domain/result.js";
import { createAnalysisExecution } from "../AnalysisProvider.js";
import { sqliteDatabaseSchema } from "../../domain/sqlite/sqliteDatabase.js";
const fixture = () => ({
artifact: { path: "/selected.db", sha256: "a".repeat(64), bytes: 4096 },
wal: null,
engine: { name: "SQLite", version: "3.50.2" },
worker_timeout_ms: 30_000,
database_encoding: "UTF-8",
schema: {
scope: "inventory",
@@ -43,6 +45,10 @@ it.each([
table: "data",
row_limit: Number.MAX_SAFE_INTEGER + 1,
},
...[0, -1, 1.5, 2_147_483_648, "1000", null].map((worker_timeout_ms) => ({
path: "/selected.db",
worker_timeout_ms,
})),
])(
"rejects invalid SQLite selection before provider effects: %j",
async (input) => {
@@ -58,6 +64,70 @@ it.each([
},
);
it("preserves the selected worker budget in provider input and Evidence", async () => {
const report = { ...fixture(), worker_timeout_ms: 1234 };
const service = new SqliteDatabaseService({
inspect: async (input) => {
expect(input.worker_timeout_ms).toBe(1234);
return ok(execution(report));
},
});
const result = await service.inspect({
path: "/selected.db",
worker_timeout_ms: 1234,
});
if (!result.ok) throw result.error;
expect(result.value.parameters).toMatchObject({ worker_timeout_ms: 1234 });
expect(result.value.normalized_result).toMatchObject({
worker_timeout_ms: 1234,
});
});
it.each([
{ selected: undefined, reported: undefined },
{ selected: undefined, reported: 1234 },
{ selected: 1234, reported: undefined },
{ selected: 1234, reported: 30_000 },
])(
"rejects a missing or changed fresh producer budget: %j",
async ({ selected, reported }) => {
const { worker_timeout_ms: _default, ...report } = fixture();
const service = new SqliteDatabaseService({
inspect: async () =>
ok(
execution({
...report,
...(reported === undefined ? {} : { worker_timeout_ms: reported }),
}),
),
});
expect(
await service.inspect({
path: "/selected.db",
...(selected === undefined ? {} : { worker_timeout_ms: selected }),
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisOutputError" } });
},
);
it("retains the actual default without inventing an explicit caller selection", async () => {
const service = new SqliteDatabaseService({
inspect: async () => ok(execution(fixture())),
});
const result = await service.inspect({ path: "/selected.db" });
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
worker_timeout_ms: 30_000,
});
expect(result.value.parameters).not.toHaveProperty("worker_timeout_ms");
});
it("parses historical report omissions as unknown without supplying a default", () => {
const { worker_timeout_ms: _default, ...historical } = fixture();
const parsed = sqliteDatabaseSchema.parse(historical);
expect(parsed).not.toHaveProperty("worker_timeout_ms");
});
it("preserves producer failures and rejects pre-cancelled requests before effects", async () => {
const failure = new AnalysisCapabilityUnavailableError(
"rea-sqlite",
@@ -18,6 +18,7 @@ import {
type InspectSqliteDatabaseInput,
type SqliteDatabase,
SQLITE_ROW_LIMIT_DEFAULT,
DEFAULT_SQLITE_WORKER_TIMEOUT_MS,
sqliteSchemaScope,
sqliteInspectionParameters,
} from "../../domain/sqlite/sqliteDatabase.js";
@@ -36,6 +37,9 @@ const matchesSelectedDatabase = (
subject.path === input.path &&
subject.sha256 === report.artifact.sha256 &&
subject.format === "file" &&
// Fresh executions must report known settings; only historical parsing permits omission.
report.worker_timeout_ms ===
(input.worker_timeout_ms ?? DEFAULT_SQLITE_WORKER_TIMEOUT_MS) &&
(report.wal === null || report.wal.path === `${input.path}-wal`) &&
report.schema.scope === sqliteSchemaScope(input) &&
(report.schema.scope !== "selected_table" ||
@@ -105,7 +109,7 @@ export class SqliteDatabaseService {
return err(
new AnalysisOutputError(
OPERATION,
"SQLite adapter returned malformed data or changed the selected database identity/table/row limit.",
"SQLite adapter returned malformed data or changed the selected database identity/table/row limit/worker budget.",
),
);
return ok(