feat(ql3): compare bounded task run outcomes

This commit is contained in:
whyour
2026-08-14 17:18:25 +08:00
parent a55613afaa
commit 828370c60b
22 changed files with 1802 additions and 22 deletions
@@ -0,0 +1,49 @@
import { RUN_STATUSES } from '@qinglong/runtime-core/run';
import {
normalizeTaskRunOutcomeWindowQuery,
type TaskRunOutcomeWindowQuery,
type TaskRunOutcomeWindowReader,
type TaskRunOutcomeWindowRecord,
} from '@qinglong/runtime-core/task-run-outcome-window';
import type { DatabaseSync } from 'node:sqlite';
import {
queryRows,
requiredEnum,
requiredInteger,
requiredString,
} from '../runPersistence';
export class LocalSqliteTaskRunOutcomeWindowReader
implements TaskRunOutcomeWindowReader
{
constructor(private readonly client: DatabaseSync) {}
async listRecentRunsByTask(
value: Readonly<TaskRunOutcomeWindowQuery>,
): Promise<readonly Readonly<TaskRunOutcomeWindowRecord>[]> {
const query = normalizeTaskRunOutcomeWindowQuery(value);
return queryRows(
this.client,
`SELECT
"id" AS "id",
"project_id" AS "projectId",
"task_id" AS "taskId",
"status" AS "status",
"created_at_ms" AS "createdAtMs"
FROM "Runs"
WHERE "project_id" = ? AND "task_id" = ?
ORDER BY "created_at_ms" DESC, "id" DESC
LIMIT ?`,
[query.projectId, query.taskId, query.limit],
).map((row) =>
Object.freeze({
id: requiredString(row, 'id'),
projectId: requiredString(row, 'projectId'),
taskId: requiredString(row, 'taskId'),
status: requiredEnum(row, 'status', RUN_STATUSES),
createdAtMs: requiredInteger(row, 'createdAtMs'),
}),
);
}
}
@@ -5,6 +5,10 @@ import type {
ProjectRunListQuery,
ProjectRunListReader,
} from '@qinglong/runtime-core/project-run-list';
import type {
TaskRunOutcomeWindowQuery,
TaskRunOutcomeWindowReader,
} from '@qinglong/runtime-core/task-run-outcome-window';
import {
RunRepositoryBusyError,
RunRepositoryOperationError,
@@ -26,6 +30,7 @@ import {
type LocalSqliteReadinessEvidence,
} from '../readiness/readiness';
import { LocalSqliteRunReader } from '../run/runReader';
import { LocalSqliteTaskRunOutcomeWindowReader } from '../run/outcome-comparison/taskRunOutcomeWindowReader';
import { LocalSqliteStepRunRepository } from '../run/stepRunRepository';
import {
assertLocalSqliteOptions,
@@ -44,7 +49,8 @@ export interface LocalSqliteMcpReadDatabase {
readonly profile: LocalSqliteProfile;
readonly readiness: LocalSqliteReadinessEvidence;
readonly runs: Pick<RunRepositoryReader, 'findRunById' | 'listEvents'> &
ProjectRunListReader;
ProjectRunListReader &
TaskRunOutcomeWindowReader;
readonly stepRuns: Pick<StepRunRepository, 'listByRun'>;
readonly taskDefinitions: Pick<
TaskDefinitionSource,
@@ -76,13 +82,17 @@ export async function openLocalSqliteMcpReadDatabase(
const readiness = await auditLocalSqliteReadiness(client);
const authority = new LocalSqliteOperationAuthority(client);
const reader = new LocalSqliteRunReader(client);
const outcomeWindowReader = new LocalSqliteTaskRunOutcomeWindowReader(
client,
);
const stepRunRepository = new LocalSqliteStepRunRepository(authority);
const taskRepository = new LocalSqliteTaskDefinitionRepository(authority);
const triggerRepository = new LocalSqliteTriggerRepository(authority);
const approvalSource = new LocalSqliteApprovalRequestSource(authority);
const security = new LocalSqliteSecurityAuthorityStore(authority);
const runs: Pick<RunRepositoryReader, 'findRunById' | 'listEvents'> &
ProjectRunListReader = Object.freeze({
ProjectRunListReader &
TaskRunOutcomeWindowReader = Object.freeze({
listRunsByProject(query: Readonly<ProjectRunListQuery>) {
return authority.enqueue(
() => reader.listRunsByProject(query),
@@ -94,6 +104,17 @@ export async function openLocalSqliteMcpReadDatabase(
),
);
},
listRecentRunsByTask(query: Readonly<TaskRunOutcomeWindowQuery>) {
return authority.enqueue(
() => outcomeWindowReader.listRecentRunsByTask(query),
(reason) =>
reason === 'busy'
? new RunRepositoryBusyError()
: new RunRepositoryOperationError(
new Error('Local SQLite MCP read database is closed'),
),
);
},
findRunById(runId: string) {
return authority.enqueue(
() => reader.findRunById(runId),