omp-conductor 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.ts CHANGED
@@ -41,6 +41,7 @@ import type {
41
41
  SessionRole,
42
42
  SettlementFlag,
43
43
  Store,
44
+ TurnOverride,
44
45
  VerbDecision,
45
46
  VerbLedgerDraft,
46
47
  VerbLedgerEntry,
@@ -73,6 +74,7 @@ const FRICTION_HOLD_REASONS: ReadonlySet<FrictionAdmissionReason> = new Set([
73
74
  "failed-attempts",
74
75
  "continuations",
75
76
  "parent-lookup-error",
77
+ "issue-state-lookup-error",
76
78
  "open-pr-lookup-error",
77
79
  "unroutable:no-repo-label",
78
80
  "unroutable:multiple-repo-labels",
@@ -107,6 +109,10 @@ const UPDATABLE_COLUMNS: Record<string, true> = {
107
109
  sessionFile: true,
108
110
  prUrl: true,
109
111
  headSha: true,
112
+ mergeSha: true,
113
+ baseRef: true,
114
+ baseCheck: true,
115
+ baseCheckAt: true,
110
116
  salvageSha: true,
111
117
  salvageError: true,
112
118
  salvageAckAt: true,
@@ -139,6 +145,10 @@ interface RunRow {
139
145
  sessionFile: string | null;
140
146
  prUrl: string | null;
141
147
  headSha: string | null;
148
+ mergeSha: string | null;
149
+ baseRef: string | null;
150
+ baseCheck: string | null;
151
+ baseCheckAt: number | null;
142
152
  salvageSha: string | null;
143
153
  salvageError: string | null;
144
154
  salvageAckAt: number | null;
@@ -256,6 +266,10 @@ CREATE TABLE IF NOT EXISTS runs (
256
266
  sessionFile TEXT,
257
267
  prUrl TEXT,
258
268
  headSha TEXT,
269
+ mergeSha TEXT,
270
+ baseRef TEXT,
271
+ baseCheck TEXT,
272
+ baseCheckAt INTEGER,
259
273
  salvageSha TEXT,
260
274
  salvageError TEXT,
261
275
  salvageAckAt INTEGER,
@@ -276,6 +290,28 @@ CREATE TABLE IF NOT EXISTS notifications (
276
290
  at INTEGER NOT NULL
277
291
  );
278
292
 
293
+ -- A one-shot operator-selected ceiling for an issue's next claimed attempt.
294
+ -- The run insert and consumption happen in one transaction.
295
+ CREATE TABLE IF NOT EXISTS turn_overrides (
296
+ project TEXT NOT NULL,
297
+ issue INTEGER NOT NULL CHECK (issue > 0),
298
+ maxTurns INTEGER NOT NULL CHECK (maxTurns > 0),
299
+ setAt INTEGER NOT NULL CHECK (setAt >= 0),
300
+ PRIMARY KEY (project, issue)
301
+ );
302
+
303
+ -- Append-only audit of every operator-selected ceiling. Consuming or replacing
304
+ -- the pending value never erases what was requested.
305
+ CREATE TABLE IF NOT EXISTS turn_override_ledger (
306
+ id TEXT PRIMARY KEY,
307
+ project TEXT NOT NULL,
308
+ issue INTEGER NOT NULL CHECK (issue > 0),
309
+ maxTurns INTEGER NOT NULL CHECK (maxTurns > 0),
310
+ setAt INTEGER NOT NULL CHECK (setAt >= 0)
311
+ );
312
+ CREATE INDEX IF NOT EXISTS turn_override_ledger_project_at
313
+ ON turn_override_ledger (project, setAt);
314
+
279
315
  CREATE TABLE IF NOT EXISTS dispatch_summaries (
280
316
  project TEXT PRIMARY KEY,
281
317
  summary TEXT NOT NULL
@@ -519,6 +555,10 @@ function toRecord(row: RunRow): RunRecord {
519
555
  if (row.sessionFile !== null) record.sessionFile = row.sessionFile;
520
556
  if (row.prUrl !== null) record.prUrl = row.prUrl;
521
557
  if (row.headSha !== null) record.headSha = row.headSha;
558
+ if (row.mergeSha !== null) record.mergeSha = row.mergeSha;
559
+ if (row.baseRef !== null) record.baseRef = row.baseRef;
560
+ if (row.baseCheck !== null) record.baseCheck = row.baseCheck as RunRecord["baseCheck"];
561
+ if (row.baseCheckAt !== null) record.baseCheckAt = row.baseCheckAt;
522
562
  if (row.salvageSha !== null) record.salvageSha = row.salvageSha;
523
563
  if (row.salvageError !== null) record.salvageError = row.salvageError;
524
564
  if (row.salvageAckAt !== null) record.salvageAckAt = row.salvageAckAt;
@@ -744,6 +784,18 @@ export function openStore(dbPath: string): Store {
744
784
  if (!columns.some((column) => column.name === "headSha")) {
745
785
  db.exec("ALTER TABLE runs ADD COLUMN headSha TEXT");
746
786
  }
787
+ // Historical merged rows were never watched after settlement, so NULL is
788
+ // "not observed", never permission to claim their base branch was green.
789
+ for (const [name, type] of [
790
+ ["mergeSha", "TEXT"],
791
+ ["baseRef", "TEXT"],
792
+ ["baseCheck", "TEXT"],
793
+ ["baseCheckAt", "INTEGER"],
794
+ ] as const) {
795
+ if (!columns.some((column) => column.name === name)) {
796
+ db.exec(`ALTER TABLE runs ADD COLUMN ${name} ${type}`);
797
+ }
798
+ }
747
799
  // v0.3.22 and earlier removed a blocked run's dirty tree without saving it
748
800
  // (#118), so no row before this release has anywhere to record where the
749
801
  // work went. A NULL `salvageSha` on a historical row therefore means "never
@@ -878,9 +930,10 @@ export function openStore(dbPath: string): Store {
878
930
  const insertRun = db.query<unknown, SqlValue[]>(
879
931
  `INSERT INTO runs (
880
932
  id, project, issue, repo, branch, worktree, state, attempt, turns,
881
- maxTurns, spendUsd, sessionFile, prUrl, headSha, salvageSha, salvageError,
882
- salvageAckAt, startedAt, endedAt, lastError, settlementFlags, report
883
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
933
+ maxTurns, spendUsd, sessionFile, prUrl, headSha, mergeSha, baseRef,
934
+ baseCheck, baseCheckAt, salvageSha, salvageError, salvageAckAt, startedAt,
935
+ endedAt, lastError, settlementFlags, report
936
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
884
937
  );
885
938
  const selectRun = db.query<RunRow, [string]>(`SELECT * FROM runs WHERE id = ?`);
886
939
  const selectActive = db.query<RunRow, SqlValue[]>(
@@ -910,6 +963,26 @@ export function openStore(dbPath: string): Store {
910
963
  AND (state <> 'merged' OR COALESCE(endedAt, startedAt) >= ?)
911
964
  ORDER BY startedAt DESC`,
912
965
  );
966
+ const selectPendingBaseChecks = db.query<RunRow, [string, number]>(
967
+ `SELECT * FROM runs
968
+ WHERE project = ? AND state = 'merged' AND baseCheck = 'pending'
969
+ ORDER BY COALESCE(endedAt, startedAt) ASC, rowid ASC
970
+ LIMIT ?`,
971
+ );
972
+ const selectLatestBaseChecks = db.query<RunRow & { rank: number }, [string, number]>(
973
+ `SELECT * FROM (
974
+ SELECT runs.*,
975
+ ROW_NUMBER() OVER (
976
+ PARTITION BY repo
977
+ ORDER BY COALESCE(endedAt, startedAt) DESC, runs.rowid DESC
978
+ ) AS rank
979
+ FROM runs
980
+ WHERE project = ? AND state = 'merged' AND baseCheck IS NOT NULL
981
+ AND COALESCE(baseCheckAt, endedAt, startedAt) >= ?
982
+ )
983
+ WHERE rank = 1
984
+ ORDER BY repo ASC`,
985
+ );
913
986
  const countAttempts = db.query<{ n: number }, [string, number]>(
914
987
  `SELECT COUNT(*) AS n FROM runs WHERE project = ? AND issue = ?`,
915
988
  );
@@ -993,6 +1066,38 @@ export function openStore(dbPath: string): Store {
993
1066
  ORDER BY startedAt DESC, rowid DESC
994
1067
  LIMIT 1`,
995
1068
  );
1069
+ const upsertTurnOverride = db.query<unknown, [string, number, number, number]>(
1070
+ `INSERT INTO turn_overrides (project, issue, maxTurns, setAt) VALUES (?, ?, ?, ?)
1071
+ ON CONFLICT(project, issue) DO UPDATE SET
1072
+ maxTurns = excluded.maxTurns,
1073
+ setAt = excluded.setAt`,
1074
+ );
1075
+ const insertTurnOverrideLedger = db.query<
1076
+ unknown,
1077
+ [string, string, number, number, number]
1078
+ >(
1079
+ `INSERT INTO turn_override_ledger (id, project, issue, maxTurns, setAt)
1080
+ VALUES (?, ?, ?, ?, ?)`,
1081
+ );
1082
+ const selectTurnOverride = db.query<{ maxTurns: number }, [string, number]>(
1083
+ `SELECT maxTurns FROM turn_overrides WHERE project = ? AND issue = ?`,
1084
+ );
1085
+ const deleteTurnOverride = db.query<unknown, [string, number]>(
1086
+ `DELETE FROM turn_overrides WHERE project = ? AND issue = ?`,
1087
+ );
1088
+ const selectTurnOverrides = db.query<TurnOverride, [string]>(
1089
+ `SELECT project, issue, maxTurns, setAt FROM turn_overrides
1090
+ WHERE project = ? ORDER BY issue ASC`,
1091
+ );
1092
+ const selectTurnOverrideLedger = db.query<
1093
+ TurnOverride,
1094
+ [string, number, number, number]
1095
+ >(
1096
+ `SELECT project, issue, maxTurns, setAt FROM turn_override_ledger
1097
+ WHERE project = ? AND (? = 0 OR issue = ?)
1098
+ ORDER BY setAt DESC, rowid DESC
1099
+ LIMIT ?`,
1100
+ );
996
1101
  // Every attempt that settled with a report, in attempt order. NULLs are the
997
1102
  // rows written before #199, or one killed before the worker returned a
998
1103
  // report — skipping them makes a continuation read exactly today's priors.
@@ -1347,34 +1452,58 @@ export function openStore(dbPath: string): Store {
1347
1452
  recordGhRefusalTx(at);
1348
1453
  };
1349
1454
 
1455
+ const insertRunRecord = (r: Omit<RunRecord, "id">): RunRecord => {
1456
+ const record: RunRecord = { ...r, id: crypto.randomUUID() };
1457
+ insertRun.run(
1458
+ record.id,
1459
+ record.project,
1460
+ record.issue,
1461
+ record.repo,
1462
+ record.branch,
1463
+ record.worktree,
1464
+ record.state,
1465
+ record.attempt,
1466
+ record.turns,
1467
+ record.maxTurns,
1468
+ record.spendUsd,
1469
+ toSql(record.sessionFile),
1470
+ toSql(record.prUrl),
1471
+ toSql(record.headSha),
1472
+ toSql(record.mergeSha),
1473
+ toSql(record.baseRef),
1474
+ toSql(record.baseCheck),
1475
+ toSql(record.baseCheckAt),
1476
+ toSql(record.salvageSha),
1477
+ toSql(record.salvageError),
1478
+ toSql(record.salvageAckAt),
1479
+ record.startedAt,
1480
+ toSql(record.endedAt),
1481
+ toSql(record.lastError),
1482
+ toSql(record.settlementFlags),
1483
+ toSql(record.report),
1484
+ );
1485
+ return record;
1486
+ };
1487
+ const createRunTx = db.transaction((r: Omit<RunRecord, "id">): RunRecord => {
1488
+ if (r.state !== "claimed") return insertRunRecord(r);
1489
+ const turnOverride = selectTurnOverride.get(r.project, r.issue)?.maxTurns;
1490
+ const record = insertRunRecord({
1491
+ ...r,
1492
+ maxTurns: Math.max(r.maxTurns, turnOverride ?? r.maxTurns),
1493
+ });
1494
+ if (turnOverride !== undefined) deleteTurnOverride.run(r.project, r.issue);
1495
+ return record;
1496
+ });
1497
+ const setTurnOverrideTx = db.transaction(
1498
+ (project: string, issue: number, maxTurns: number, setAt: number): void => {
1499
+ upsertTurnOverride.run(project, issue, maxTurns, setAt);
1500
+ insertTurnOverrideLedger.run(crypto.randomUUID(), project, issue, maxTurns, setAt);
1501
+ },
1502
+ );
1503
+
1350
1504
  return {
1351
1505
  createRun(r: Omit<RunRecord, "id">): RunRecord {
1352
- const record: RunRecord = { ...r, id: crypto.randomUUID() };
1353
- insertRun.run(
1354
- record.id,
1355
- record.project,
1356
- record.issue,
1357
- record.repo,
1358
- record.branch,
1359
- record.worktree,
1360
- record.state,
1361
- record.attempt,
1362
- record.turns,
1363
- record.maxTurns,
1364
- record.spendUsd,
1365
- toSql(record.sessionFile),
1366
- toSql(record.prUrl),
1367
- toSql(record.headSha),
1368
- toSql(record.salvageSha),
1369
- toSql(record.salvageError),
1370
- toSql(record.salvageAckAt),
1371
- record.startedAt,
1372
- toSql(record.endedAt),
1373
- toSql(record.lastError),
1374
- toSql(record.settlementFlags),
1375
- toSql(record.report),
1376
- );
1377
- return record;
1506
+ return createRunTx(r);
1378
1507
  },
1379
1508
 
1380
1509
  updateRun(id: string, patch: Partial<RunRecord>): void {
@@ -1414,6 +1543,14 @@ export function openStore(dbPath: string): Store {
1414
1543
  recentRuns(project: string, mergedSinceEpochMs: number): RunRecord[] {
1415
1544
  return selectRecentRuns.all(project, mergedSinceEpochMs).map(toRecord);
1416
1545
  },
1546
+ runsNeedingBaseCheck(project: string, limit = 20): RunRecord[] {
1547
+ return selectPendingBaseChecks.all(project, limit).map(toRecord);
1548
+ },
1549
+
1550
+ latestBaseChecks(project: string, sinceEpochMs: number): RunRecord[] {
1551
+ return selectLatestBaseChecks.all(project, sinceEpochMs).map(toRecord);
1552
+ },
1553
+
1417
1554
 
1418
1555
  salvagedRuns(project: string): RunRecord[] {
1419
1556
  return selectSalvaged.all(project).map(toRecord);
@@ -1440,6 +1577,30 @@ export function openStore(dbPath: string): Store {
1440
1577
  return row ? toRecord(row) : undefined;
1441
1578
  },
1442
1579
 
1580
+ setTurnOverride(project: string, issue: number, maxTurns: number, setAt = Date.now()): void {
1581
+ setTurnOverrideTx(project, issue, maxTurns, setAt);
1582
+ },
1583
+
1584
+ turnOverride(project: string, issue: number): number | undefined {
1585
+ return selectTurnOverride.get(project, issue)?.maxTurns;
1586
+ },
1587
+
1588
+ clearTurnOverride(project: string, issue: number): void {
1589
+ deleteTurnOverride.run(project, issue);
1590
+ },
1591
+
1592
+ listTurnOverrides(project: string): TurnOverride[] {
1593
+ return selectTurnOverrides.all(project);
1594
+ },
1595
+
1596
+ turnOverrideLedger(
1597
+ project: string,
1598
+ opts: { issue?: number; limit?: number } = {},
1599
+ ): TurnOverride[] {
1600
+ const issue = opts.issue ?? 0;
1601
+ return selectTurnOverrideLedger.all(project, issue, issue, opts.limit ?? 50);
1602
+ },
1603
+
1443
1604
  attemptReports(project: string, issue: number): { attempt: number; report: string }[] {
1444
1605
  return selectAttemptReports.all(project, issue);
1445
1606
  },