@usine/engine 0.2.0 → 0.4.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.
Files changed (192) hide show
  1. package/README.md +109 -36
  2. package/dist/board-moves/board-moves.d.ts +20 -0
  3. package/dist/board-moves/board-moves.d.ts.map +1 -0
  4. package/dist/board-moves/board-moves.js +74 -0
  5. package/dist/board-moves/board-moves.js.map +1 -0
  6. package/dist/board-moves/model.d.ts +21 -0
  7. package/dist/board-moves/model.d.ts.map +1 -0
  8. package/dist/board-moves/model.js +22 -0
  9. package/dist/board-moves/model.js.map +1 -0
  10. package/dist/cursor/cursor.d.ts +2 -6
  11. package/dist/cursor/cursor.d.ts.map +1 -1
  12. package/dist/cursor/cursor.js +2 -6
  13. package/dist/cursor/cursor.js.map +1 -1
  14. package/dist/cycle-budget/cycle-budget.d.ts +6 -6
  15. package/dist/cycle-budget/cycle-budget.d.ts.map +1 -1
  16. package/dist/cycle-budget/cycle-budget.js +5 -5
  17. package/dist/cycle-budget/cycle-budget.js.map +1 -1
  18. package/dist/cycle-budget/model.d.ts +2 -2
  19. package/dist/cycle-budget/model.d.ts.map +1 -1
  20. package/dist/cycle-budget/model.js +2 -2
  21. package/dist/cycle-budget/model.js.map +1 -1
  22. package/dist/cycle-budget/queries.d.ts +1 -1
  23. package/dist/cycle-budget/queries.d.ts.map +1 -1
  24. package/dist/cycle-budget/queries.js +3 -3
  25. package/dist/cycle-budget/queries.js.map +1 -1
  26. package/dist/database/database.d.ts +1 -1
  27. package/dist/database/database.d.ts.map +1 -1
  28. package/dist/database/local-sqlite.d.ts +10 -8
  29. package/dist/database/local-sqlite.d.ts.map +1 -1
  30. package/dist/database/local-sqlite.js +16 -9
  31. package/dist/database/local-sqlite.js.map +1 -1
  32. package/dist/database/schema.d.ts +581 -22
  33. package/dist/database/schema.d.ts.map +1 -1
  34. package/dist/database/schema.js +104 -23
  35. package/dist/database/schema.js.map +1 -1
  36. package/dist/{workflow → engine}/admission.d.ts +2 -2
  37. package/dist/engine/admission.d.ts.map +1 -0
  38. package/dist/{workflow → engine}/admission.js +4 -4
  39. package/dist/engine/admission.js.map +1 -0
  40. package/dist/engine/checkout-recovery.d.ts +69 -0
  41. package/dist/engine/checkout-recovery.d.ts.map +1 -0
  42. package/dist/engine/checkout-recovery.js +181 -0
  43. package/dist/engine/checkout-recovery.js.map +1 -0
  44. package/dist/engine/engine.d.ts +57 -24
  45. package/dist/engine/engine.d.ts.map +1 -1
  46. package/dist/engine/engine.js +144 -120
  47. package/dist/engine/engine.js.map +1 -1
  48. package/dist/engine/error.d.ts +36 -9
  49. package/dist/engine/error.d.ts.map +1 -1
  50. package/dist/engine/error.js +35 -7
  51. package/dist/engine/error.js.map +1 -1
  52. package/dist/engine/lifecycle.d.ts +81 -28
  53. package/dist/engine/lifecycle.d.ts.map +1 -1
  54. package/dist/engine/lifecycle.js +83 -44
  55. package/dist/engine/lifecycle.js.map +1 -1
  56. package/dist/engine/retry.d.ts +16 -0
  57. package/dist/engine/retry.d.ts.map +1 -0
  58. package/dist/engine/retry.js +18 -0
  59. package/dist/engine/retry.js.map +1 -0
  60. package/dist/engine/run-again.d.ts +45 -0
  61. package/dist/engine/run-again.d.ts.map +1 -0
  62. package/dist/engine/run-again.js +56 -0
  63. package/dist/engine/run-again.js.map +1 -0
  64. package/dist/engine/run-driver.d.ts +54 -0
  65. package/dist/engine/run-driver.d.ts.map +1 -0
  66. package/dist/engine/run-driver.js +362 -0
  67. package/dist/engine/run-driver.js.map +1 -0
  68. package/dist/engine/shutdown.d.ts +3 -4
  69. package/dist/engine/shutdown.d.ts.map +1 -1
  70. package/dist/engine/shutdown.js +3 -4
  71. package/dist/engine/shutdown.js.map +1 -1
  72. package/dist/engine/submission.d.ts +31 -11
  73. package/dist/engine/submission.d.ts.map +1 -1
  74. package/dist/engine/submission.js +42 -12
  75. package/dist/engine/submission.js.map +1 -1
  76. package/dist/item-projection/item-projection.d.ts +12 -4
  77. package/dist/item-projection/item-projection.d.ts.map +1 -1
  78. package/dist/item-projection/item-projection.js +41 -6
  79. package/dist/item-projection/item-projection.js.map +1 -1
  80. package/dist/item-projection/model.d.ts +5 -1
  81. package/dist/item-projection/model.d.ts.map +1 -1
  82. package/dist/item-projection/model.js +6 -2
  83. package/dist/item-projection/model.js.map +1 -1
  84. package/dist/loop-control/loop-control.d.ts +5 -5
  85. package/dist/loop-control/loop-control.d.ts.map +1 -1
  86. package/dist/loop-control/loop-control.js +4 -4
  87. package/dist/loop-control/loop-control.js.map +1 -1
  88. package/dist/loop-control/model.d.ts +1 -1
  89. package/dist/loop-control/model.d.ts.map +1 -1
  90. package/dist/loop-control/model.js +1 -1
  91. package/dist/loop-control/model.js.map +1 -1
  92. package/dist/repository-label-projection/model.d.ts +2 -2
  93. package/dist/repository-label-projection/model.d.ts.map +1 -1
  94. package/dist/repository-label-projection/model.js +2 -2
  95. package/dist/repository-label-projection/model.js.map +1 -1
  96. package/dist/repository-label-projection/repository-label-projection.d.ts +1 -1
  97. package/dist/repository-label-projection/repository-label-projection.d.ts.map +1 -1
  98. package/dist/repository-label-projection/repository-label-projection.js +1 -1
  99. package/dist/repository-label-projection/repository-label-projection.js.map +1 -1
  100. package/dist/run-reader/model.d.ts +62 -4
  101. package/dist/run-reader/model.d.ts.map +1 -1
  102. package/dist/run-reader/model.js +66 -7
  103. package/dist/run-reader/model.js.map +1 -1
  104. package/dist/run-reader/run-reader.d.ts +19 -9
  105. package/dist/run-reader/run-reader.d.ts.map +1 -1
  106. package/dist/run-reader/run-reader.js +23 -7
  107. package/dist/run-reader/run-reader.js.map +1 -1
  108. package/dist/step-executor.d.ts +65 -9
  109. package/dist/step-executor.d.ts.map +1 -1
  110. package/dist/step-executor.js +10 -2
  111. package/dist/step-executor.js.map +1 -1
  112. package/dist/store/attempt-windows.d.ts +8 -0
  113. package/dist/store/attempt-windows.d.ts.map +1 -0
  114. package/dist/store/attempt-windows.js +53 -0
  115. package/dist/store/attempt-windows.js.map +1 -0
  116. package/dist/store/codec.d.ts +381 -42
  117. package/dist/store/codec.d.ts.map +1 -1
  118. package/dist/store/codec.js +59 -15
  119. package/dist/store/codec.js.map +1 -1
  120. package/dist/store/leftovers.d.ts +10 -0
  121. package/dist/store/leftovers.d.ts.map +1 -0
  122. package/dist/store/leftovers.js +43 -0
  123. package/dist/store/leftovers.js.map +1 -0
  124. package/dist/store/loop-items.d.ts +16 -0
  125. package/dist/store/loop-items.d.ts.map +1 -0
  126. package/dist/store/loop-items.js +185 -0
  127. package/dist/store/loop-items.js.map +1 -0
  128. package/dist/store/queries.d.ts +345 -37
  129. package/dist/store/queries.d.ts.map +1 -1
  130. package/dist/store/queries.js +333 -160
  131. package/dist/store/queries.js.map +1 -1
  132. package/dist/store/store.d.ts +91 -54
  133. package/dist/store/store.d.ts.map +1 -1
  134. package/dist/store/store.js +481 -190
  135. package/dist/store/store.js.map +1 -1
  136. package/dist/store/transcript.d.ts +9 -0
  137. package/dist/store/transcript.d.ts.map +1 -0
  138. package/dist/store/transcript.js +90 -0
  139. package/dist/store/transcript.js.map +1 -0
  140. package/dist/transcript.d.ts +41 -20
  141. package/dist/transcript.d.ts.map +1 -1
  142. package/dist/transcript.js +45 -25
  143. package/dist/transcript.js.map +1 -1
  144. package/dist/trigger-condition.d.ts +23 -0
  145. package/dist/trigger-condition.d.ts.map +1 -0
  146. package/dist/trigger-condition.js +8 -0
  147. package/dist/trigger-condition.js.map +1 -0
  148. package/drizzle/20260912111855_operator_intervention/migration.sql +42 -0
  149. package/drizzle/20260912111855_operator_intervention/snapshot.json +1461 -0
  150. package/drizzle/20260917071139_resolve_run_failure/migration.sql +1 -0
  151. package/drizzle/20260917071139_resolve_run_failure/snapshot.json +1471 -0
  152. package/drizzle/20260918032643_attempt_salvage/migration.sql +1 -0
  153. package/drizzle/20260918032643_attempt_salvage/snapshot.json +1481 -0
  154. package/drizzle/20261011153856_drop_workflow_tables/migration.sql +4 -0
  155. package/drizzle/20261011153856_drop_workflow_tables/snapshot.json +1433 -0
  156. package/drizzle/20261017155043_attempt_salvage_check/migration.sql +41 -0
  157. package/drizzle/20261017155043_attempt_salvage_check/snapshot.json +1421 -0
  158. package/drizzle/20261018162700_discard_attempt_leftovers/migration.sql +1 -0
  159. package/drizzle/20261018162700_discard_attempt_leftovers/snapshot.json +1431 -0
  160. package/drizzle/20261018172605_loop_items_and_reopenings/migration.sql +15 -0
  161. package/drizzle/20261018172605_loop_items_and_reopenings/snapshot.json +1574 -0
  162. package/drizzle/20261018180339_attempt_ended_at/migration.sql +1 -0
  163. package/drizzle/20261018180339_attempt_ended_at/snapshot.json +1584 -0
  164. package/drizzle/20261019104043_run_approval_source/migration.sql +15 -0
  165. package/drizzle/20261019104043_run_approval_source/snapshot.json +1610 -0
  166. package/drizzle/20261019133538_board_moves/migration.sql +14 -0
  167. package/drizzle/20261019133538_board_moves/snapshot.json +1731 -0
  168. package/drizzle/20261019170538_item_projection_gone/migration.sql +1 -0
  169. package/drizzle/20261019170538_item_projection_gone/snapshot.json +1741 -0
  170. package/drizzle/20261019180649_run_again_events/migration.sql +7 -0
  171. package/drizzle/20261019180649_run_again_events/snapshot.json +1804 -0
  172. package/drizzle/20261019185008_trigger_event_permissions/migration.sql +1 -0
  173. package/drizzle/20261019185008_trigger_event_permissions/snapshot.json +1814 -0
  174. package/drizzle/20261020103746_attempt_start/migration.sql +5 -0
  175. package/drizzle/20261020103746_attempt_start/snapshot.json +1844 -0
  176. package/drizzle/20261020125211_attempt_comments/migration.sql +1 -0
  177. package/drizzle/20261020125211_attempt_comments/snapshot.json +1854 -0
  178. package/drizzle/20261020191320_run_closed_at/migration.sql +6 -0
  179. package/drizzle/20261020191320_run_closed_at/snapshot.json +1864 -0
  180. package/drizzle/20261023091428_rename_loop_item_hold_to_suppression/migration.sql +2 -0
  181. package/drizzle/20261023091428_rename_loop_item_hold_to_suppression/snapshot.json +1867 -0
  182. package/package.json +15 -5
  183. package/dist/workflow/admission.d.ts.map +0 -1
  184. package/dist/workflow/admission.js.map +0 -1
  185. package/dist/workflow/command.d.ts +0 -13
  186. package/dist/workflow/command.d.ts.map +0 -1
  187. package/dist/workflow/command.js +0 -17
  188. package/dist/workflow/command.js.map +0 -1
  189. package/dist/workflow/workflow.d.ts +0 -18
  190. package/dist/workflow/workflow.d.ts.map +0 -1
  191. package/dist/workflow/workflow.js +0 -108
  192. package/dist/workflow/workflow.js.map +0 -1
@@ -1,24 +1,26 @@
1
- import { Attempt, AttemptId } from "@usine/core/attempt";
2
- import * as AttemptChannel from "@usine/core/attempt/channel";
1
+ import { RepositoryName } from "@usine/core/repository";
2
+ import { WorkerName } from "@usine/core/worker";
3
+ import { Attempt, AttemptId, AttemptContext, AttemptNumber } from "@usine/core/attempt";
3
4
  import { AttemptOutcome } from "@usine/core/attempt/outcome";
5
+ import { AttemptSalvage, salvageRefFor } from "@usine/core/attempt/salvage";
4
6
  import { Event, DedupKey, EventId } from "@usine/core/event";
5
7
  import { ItemKey } from "@usine/core/item";
6
- import { LoopName } from "@usine/core/loop";
8
+ import { LoopName, LoopPermissions } from "@usine/core/loop";
7
9
  import { RunId } from "@usine/core/run";
8
10
  import { StepName } from "@usine/core/step";
9
11
  import * as RunState from "@usine/core/run/state";
10
12
  import { StepResult } from "@usine/core/step/result";
11
- import { and, asc, desc, eq, gt, inArray, isNull, lt, ne, notInArray, sql } from "drizzle-orm";
13
+ import { and, asc, desc, eq, inArray, isNull, ne, notInArray, sql, } from "drizzle-orm";
12
14
  import { alias } from "drizzle-orm/sqlite-core";
13
- import { Array as EffectArray, DateTime, Effect, Match, Option, Schema } from "effect";
14
- import { approvals, attemptTranscripts, attempts, events, ignoredEvents, projectionRevision, runs, steps, triggerEvents, workflowCommands, } from "../database/schema.js";
15
+ import { Array as EffectArray, DateTime, Effect, Option, Schema } from "effect";
16
+ import { approvals, attempts, events, ignoredEvents, itemCycleBudgets, loopItems, projectionRevision, runAgainEvents, runs, steps, triggerEvents, } from "../database/schema.js";
15
17
  import { RunNotFoundError } from "../engine/error.js";
16
18
  import { RunSnapshot, RunSummary } from "../run-reader/model.js";
17
19
  import { Submission } from "../engine/submission.js";
18
- import { TranscriptQuery } from "../transcript.js";
19
- import { WORKFLOW_COMMAND_PRIORITY } from "../workflow/command.js";
20
20
  import { cycleQueries } from "../cycle-budget/queries.js";
21
- import { attemptFromRow, decodeAttemptRows, decodeDiscards, decodeEventRoutes, decodeRecoverableRuns, decodeProjectionRevision, decodeRunIds, decodeRunRows, decodeWorkflowCommands, decodeTranscriptRows, encodeAgent, encodeBrainSpec, encodeDateTime, encodeEnvelope, encodeOutcome, encodePayload, encodeRunState, encodeStepResult, eventFromRow, runFromRow, runSummaryFromRow, stepFromRow, submissionFromRow, transcriptEntryFromRow, } from "./codec.js";
21
+ import { approvalFromRow, attemptFromRow, decodeAttemptRows, decodeDiscards, decodeNextStarts, decodeEventRoutes, decodeProjectionRevision, decodeReopenedEvents, decodeReopenings, decodeRunEventRows, decodeRunIds, decodeRunRows, encodeAgent, encodeAttemptStart, encodeBrainSpec, encodeDateTime, encodeOutcome, encodeComments, encodeSalvage, encodePayload, encodeRunState, encodeStepResult, eventFromRow, runAgainFromRow, runEventFromRow, runFromRow, runSummaryFromRow, stepFromRow, submissionFromRow, } from "./codec.js";
22
+ import { loopItemQueries } from "./loop-items.js";
23
+ import { transcriptQueries } from "./transcript.js";
22
24
  const newestRunFirst = (left, right) => DateTime.toEpochMillis(right.openedAt) - DateTime.toEpochMillis(left.openedAt) ||
23
25
  right.runId.localeCompare(left.runId);
24
26
  const eventRouteSelection = {
@@ -30,6 +32,7 @@ const eventRouteSelection = {
30
32
  loop: triggerEvents.loopName,
31
33
  step: triggerEvents.stepName,
32
34
  approval: triggerEvents.approval,
35
+ permissions: triggerEvents.permissions,
33
36
  cycleLimit: triggerEvents.cycleLimit,
34
37
  cycleGeneration: triggerEvents.cycleGeneration,
35
38
  repository: triggerEvents.repository,
@@ -38,13 +41,16 @@ const eventRouteSelection = {
38
41
  agent: triggerEvents.agentJson,
39
42
  parentModel: triggerEvents.parentModel,
40
43
  };
44
+ const approvalEvents = alias(events, "approval_event");
41
45
  const runSelection = {
42
46
  itemKey: triggerEvents.itemKey,
43
47
  runId: runs.runId,
44
48
  eventId: runs.eventId,
45
49
  loop: runs.loopName,
46
50
  state: runs.stateJson,
51
+ resolvedFailureAttemptId: runs.resolvedFailureAttemptId,
47
52
  openedAt: runs.openedAt,
53
+ closedAt: runs.closedAt,
48
54
  repository: runs.repository,
49
55
  revision: runs.revision,
50
56
  task: runs.task,
@@ -52,19 +58,31 @@ const runSelection = {
52
58
  parentModel: runs.parentModel,
53
59
  stepName: steps.stepName,
54
60
  result: steps.result,
61
+ approvedAt: approvalEvents.receivedAt,
62
+ approvalChannel: approvals.channel,
63
+ approver: approvals.approver,
64
+ againFrom: runAgainEvents.fromRunId,
65
+ againRequestedBy: runAgainEvents.requestedBy,
55
66
  };
56
- const attemptSelection = {
67
+ export const attemptSelection = {
57
68
  attemptId: attempts.attemptId,
58
69
  runId: attempts.runId,
59
70
  stepName: attempts.stepName,
60
71
  worker: attempts.workerName,
61
72
  number: attempts.attemptNumber,
62
73
  brain: attempts.brainJson,
74
+ task: attempts.task,
75
+ agent: attempts.agent,
76
+ operatorIntervened: attempts.operatorIntervened,
63
77
  parentModel: attempts.parentModel,
64
78
  startedAt: attempts.startedAt,
65
79
  outcome: attempts.outcomeJson,
80
+ endedAt: attempts.endedAt,
81
+ salvage: attempts.salvageJson,
82
+ start: attempts.startJson,
83
+ reattachedAt: attempts.reattachedAt,
84
+ comments: attempts.commentsJson,
66
85
  };
67
- // The state partitions are owned by @usine/core; these predicates only borrow their tags.
68
86
  const storedStateTag = sql `json_extract(${runs.stateJson}, '$._tag')`;
69
87
  const openState = inArray(storedStateTag, [...RunState.OPEN_STATE_TAGS]);
70
88
  const unstartedState = inArray(storedStateTag, [...RunState.UNSTARTED_STATE_TAGS]);
@@ -72,8 +90,7 @@ const unstartedState = inArray(storedStateTag, [...RunState.UNSTARTED_STATE_TAGS
72
90
  // cannot emit the declaration (TS2883).
73
91
  const storedAgent = (agent) => Schema.decodeUnknownSync(Schema.Json)(encodeAgent(agent));
74
92
  export const queriesFor = (database) => {
75
- // Keep `.execute()` explicit: the yieldable wrapper can cross an Effect
76
- // Workflow suspension before its final mutation is observable (ADR 0020).
93
+ // `.execute()` stays explicit (ADR 0020).
77
94
  const eventRouteByDedup = Effect.fn("RunStoreQueries.eventRouteByDedup")(function* (dedupKey) {
78
95
  const rows = yield* database
79
96
  .select(eventRouteSelection)
@@ -93,15 +110,57 @@ export const queriesFor = (database) => {
93
110
  .execute();
94
111
  return yield* decodeEventRoutes(rows);
95
112
  });
113
+ const selectRuns = () => database
114
+ .select(runSelection)
115
+ .from(runs)
116
+ .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
117
+ .innerJoin(steps, eq(steps.runId, runs.runId))
118
+ .leftJoin(approvals, eq(approvals.runId, runs.runId))
119
+ .leftJoin(approvalEvents, eq(approvalEvents.eventId, approvals.eventId))
120
+ .leftJoin(runAgainEvents, eq(runAgainEvents.eventId, runs.eventId));
96
121
  const runRows = Effect.fn("RunStoreQueries.runRows")(function* (runId) {
122
+ const rows = yield* selectRuns().where(eq(runs.runId, runId)).execute();
123
+ return yield* decodeRunRows(rows);
124
+ });
125
+ /** Returns the Attempts that match `filters` and whose leftovers weren't discarded. */
126
+ const recoveries = (...filters) => Effect.gen(function* () {
97
127
  const rows = yield* database
98
- .select(runSelection)
99
- .from(runs)
128
+ .select({
129
+ attemptId: attempts.attemptId,
130
+ worker: attempts.workerName,
131
+ repository: runs.repository,
132
+ runId: attempts.runId,
133
+ number: attempts.attemptNumber,
134
+ permissions: triggerEvents.permissions,
135
+ outcome: sql `coalesce(json_extract(${attempts.outcomeJson}, '$._tag'), '')`,
136
+ })
137
+ .from(attempts)
138
+ .innerJoin(runs, eq(runs.runId, attempts.runId))
100
139
  .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
101
- .innerJoin(steps, eq(steps.runId, runs.runId))
102
- .where(eq(runs.runId, runId))
140
+ .where(and(isNull(attempts.leftoversDiscardedAt), ...filters))
103
141
  .execute();
104
- return yield* decodeRunRows(rows);
142
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({
143
+ attemptId: AttemptId,
144
+ worker: WorkerName,
145
+ repository: RepositoryName,
146
+ runId: RunId,
147
+ number: AttemptNumber,
148
+ permissions: LoopPermissions,
149
+ outcome: Schema.String,
150
+ })))(rows);
151
+ return decoded.map(({ runId, number, outcome, ...attempt }) => ({
152
+ ...attempt,
153
+ ref: salvageRefFor(runId, number),
154
+ succeeded: outcome === "Succeeded",
155
+ open: outcome === "",
156
+ }));
157
+ });
158
+ const salvageCandidates = Effect.fn("RunStoreQueries.salvageCandidates")(function* () {
159
+ return yield* recoveries();
160
+ });
161
+ const salvageCandidate = Effect.fn("RunStoreQueries.salvageCandidate")(function* (attemptId) {
162
+ yield* Effect.annotateCurrentSpan({ attemptId });
163
+ return EffectArray.head(yield* recoveries(eq(attempts.attemptId, attemptId)));
105
164
  });
106
165
  const attemptRows = Effect.fn("RunStoreQueries.attemptRows")(function* (runId) {
107
166
  const rows = yield* database
@@ -129,17 +188,21 @@ export const queriesFor = (database) => {
129
188
  run: runFromRow(storedRun),
130
189
  step: stepFromRow(storedRun),
131
190
  attempts: storedAttempts.map(attemptFromRow),
191
+ resolvedFailureAttemptId: Option.fromNullishOr(storedRun.resolvedFailureAttemptId),
192
+ closedAt: Option.fromNullishOr(storedRun.closedAt),
193
+ approval: approvalFromRow(storedRun),
194
+ runAgain: runAgainFromRow(storedRun),
132
195
  repository: storedRun.repository,
133
196
  revision: storedRun.revision,
134
197
  task: storedRun.task,
135
198
  agent: storedRun.agent,
136
199
  parentModel: Option.fromNullishOr(storedRun.parentModel),
200
+ permissions: route.permissions,
137
201
  });
138
202
  });
139
203
  const summariesWithAttempts = Effect.fn("RunStoreQueries.summariesWithAttempts")(function* (storedRuns) {
140
204
  const runIds = storedRuns.map(({ runId }) => runId);
141
- // Keep history queries below adapter bind-parameter limits, even for a
142
- // long-lived Task with many Runs.
205
+ // Chunks keep each read below the driver's bind-parameter limit.
143
206
  const storedAttempts = (yield* Effect.forEach(EffectArray.chunksOf(runIds, 100), (ids) => database
144
207
  .select(attemptSelection)
145
208
  .from(attempts)
@@ -153,11 +216,7 @@ export const queriesFor = (database) => {
153
216
  const itemRunSummaries = Effect.fn("RunStoreQueries.itemRunSummaries")(function* (itemKeys) {
154
217
  if (itemKeys.length === 0)
155
218
  return [];
156
- const storedRuns = (yield* Effect.forEach(EffectArray.chunksOf([...new Set(itemKeys)], 100), (keys) => database
157
- .select(runSelection)
158
- .from(runs)
159
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
160
- .innerJoin(steps, eq(steps.runId, runs.runId))
219
+ const storedRuns = (yield* Effect.forEach(EffectArray.chunksOf([...new Set(itemKeys)], 100), (keys) => selectRuns()
161
220
  .where(inArray(triggerEvents.itemKey, keys))
162
221
  .execute()
163
222
  .pipe(Effect.flatMap(decodeRunRows))))
@@ -165,24 +224,15 @@ export const queriesFor = (database) => {
165
224
  .sort(newestRunFirst);
166
225
  return yield* summariesWithAttempts(storedRuns);
167
226
  });
168
- // Every open Run plus the newest `closedLimit` settled ones, so an old
169
- // Waiting run never falls out of the operator's view while history stays bounded.
227
+ // Every open Run plus the newest `closedLimit` closed ones, so an old waiting Run stays listed.
170
228
  const runSummaries = Effect.fn("RunStoreQueries.runSummaries")(function* (closedLimit) {
171
229
  const [openRuns, closedRuns] = yield* Effect.all([
172
- database
173
- .select(runSelection)
174
- .from(runs)
175
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
176
- .innerJoin(steps, eq(steps.runId, runs.runId))
230
+ selectRuns()
177
231
  .where(openState)
178
232
  .orderBy(desc(runs.openedAt), desc(runs.runId))
179
233
  .execute()
180
234
  .pipe(Effect.flatMap(decodeRunRows)),
181
- database
182
- .select(runSelection)
183
- .from(runs)
184
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
185
- .innerJoin(steps, eq(steps.runId, runs.runId))
235
+ selectRuns()
186
236
  .where(notInArray(storedStateTag, [...RunState.OPEN_STATE_TAGS]))
187
237
  .orderBy(desc(runs.openedAt), desc(runs.runId))
188
238
  .limit(closedLimit)
@@ -192,6 +242,32 @@ export const queriesFor = (database) => {
192
242
  const storedRuns = [...openRuns, ...closedRuns].sort(newestRunFirst);
193
243
  return yield* summariesWithAttempts(storedRuns);
194
244
  });
245
+ const runEvents = Effect.fn("RunStoreQueries.runEvents")(function* (runIds) {
246
+ // Chunks keep each read below the driver's bind-parameter limit.
247
+ const rows = yield* Effect.forEach(EffectArray.chunksOf([...new Set(runIds)], 100), (ids) => database
248
+ .select({
249
+ runId: runs.runId,
250
+ eventId: events.eventId,
251
+ dedupKey: events.dedupKey,
252
+ payload: events.payloadJson,
253
+ receivedAt: events.receivedAt,
254
+ })
255
+ .from(runs)
256
+ .innerJoin(events, eq(events.eventId, runs.eventId))
257
+ .where(inArray(runs.runId, ids))
258
+ .execute()
259
+ .pipe(Effect.flatMap(decodeRunEventRows)));
260
+ return rows.flat().map(runEventFromRow);
261
+ });
262
+ const closedRunSummaries = Effect.fn("RunStoreQueries.closedRunSummaries")(function* (limit) {
263
+ const closedRuns = yield* selectRuns()
264
+ .where(notInArray(storedStateTag, [...RunState.OPEN_STATE_TAGS]))
265
+ .orderBy(desc(runs.closedAt), desc(runs.runId))
266
+ .limit(limit)
267
+ .execute()
268
+ .pipe(Effect.flatMap(decodeRunRows));
269
+ return yield* summariesWithAttempts(closedRuns);
270
+ });
195
271
  const revision = Effect.fn("RunStoreQueries.projectionRevision")(function* () {
196
272
  const rows = yield* decodeProjectionRevision(yield* database
197
273
  .select({ revision: projectionRevision.revision })
@@ -201,10 +277,12 @@ export const queriesFor = (database) => {
201
277
  .execute());
202
278
  return Option.fromNullishOr(rows[0]?.revision);
203
279
  });
204
- const updateState = Effect.fn("RunStoreQueries.updateState")(function* (runId, state) {
280
+ // A closed state sets `closedAt` to `at` or now; an open state clears it.
281
+ const updateState = Effect.fn("RunStoreQueries.updateState")(function* (runId, state, at) {
282
+ const closedAt = RunState.isOpen(state) ? null : encodeDateTime(at ?? (yield* DateTime.now));
205
283
  const updated = yield* database
206
284
  .update(runs)
207
- .set({ stateJson: encodeRunState(state) })
285
+ .set({ stateJson: encodeRunState(state), closedAt })
208
286
  .where(eq(runs.runId, runId))
209
287
  .returning({ runId: runs.runId })
210
288
  .execute();
@@ -212,6 +290,14 @@ export const queriesFor = (database) => {
212
290
  return yield* new RunNotFoundError({ runId });
213
291
  }
214
292
  });
293
+ const resolveFailure = Effect.fn("RunStoreQueries.resolveFailure")(function* (runId, attemptId) {
294
+ yield* Effect.annotateCurrentSpan({ runId, attemptId });
295
+ yield* database
296
+ .update(runs)
297
+ .set({ resolvedFailureAttemptId: attemptId })
298
+ .where(eq(runs.runId, runId))
299
+ .execute();
300
+ });
215
301
  const addAttempt = Effect.fn("RunStoreQueries.addAttempt")(function* (attempt) {
216
302
  yield* database
217
303
  .insert(attempts)
@@ -222,19 +308,87 @@ export const queriesFor = (database) => {
222
308
  workerName: attempt.worker,
223
309
  attemptNumber: attempt.number,
224
310
  brainJson: encodeBrainSpec(attempt.brain),
311
+ task: attempt.task,
312
+ agent: attempt.agent,
313
+ operatorIntervened: attempt.operatorIntervened,
225
314
  parentModel: Option.getOrNull(attempt.parentModel),
226
315
  startedAt: encodeDateTime(attempt.startedAt),
227
316
  outcomeJson: null,
317
+ endedAt: null,
318
+ salvageJson: null,
319
+ startJson: Option.match(attempt.start, { onNone: () => null, onSome: encodeAttemptStart }),
320
+ })
321
+ .execute();
322
+ });
323
+ const recordReattach = Effect.fn("RunStoreQueries.recordReattach")(function* (attemptId, reattachedAt) {
324
+ yield* Effect.annotateCurrentSpan({ attemptId });
325
+ yield* database
326
+ .update(attempts)
327
+ .set({ reattachedAt: encodeDateTime(reattachedAt) })
328
+ .where(eq(attempts.attemptId, attemptId))
329
+ .execute();
330
+ });
331
+ /** Returns the operator retry for the Run's next Attempt, or `None`. */
332
+ const nextStart = Effect.fn("RunStoreQueries.nextStart")(function* (runId) {
333
+ const rows = yield* database
334
+ .select({ nextStart: runs.nextStartJson })
335
+ .from(runs)
336
+ .where(eq(runs.runId, runId))
337
+ .execute();
338
+ return Option.fromNullishOr((yield* decodeNextStarts(rows))[0]?.nextStart);
339
+ });
340
+ const setNextStart = Effect.fn("RunStoreQueries.setNextStart")(function* (runId, start) {
341
+ yield* database
342
+ .update(runs)
343
+ .set({
344
+ nextStartJson: Option.match(start, { onNone: () => null, onSome: encodeAttemptStart }),
228
345
  })
346
+ .where(eq(runs.runId, runId))
347
+ .execute();
348
+ });
349
+ const recordSalvage = Effect.fn("RunStoreQueries.recordSalvage")(function* (attemptId, salvage) {
350
+ yield* Effect.annotateCurrentSpan({ attemptId, salvage: salvage._tag });
351
+ yield* database
352
+ .update(attempts)
353
+ .set({ salvageJson: encodeSalvage(salvage) })
354
+ .where(eq(attempts.attemptId, attemptId))
229
355
  .execute();
230
356
  });
231
- const closeAttempt = Effect.fn("RunStoreQueries.closeAttempt")(function* (attemptId, outcome) {
357
+ const recordComments = Effect.fn("RunStoreQueries.recordComments")(function* (attemptId, comments) {
358
+ yield* Effect.annotateCurrentSpan({ attemptId, comments: comments.length });
232
359
  yield* database
233
360
  .update(attempts)
234
- .set({ outcomeJson: encodeOutcome(outcome) })
361
+ .set({ commentsJson: encodeComments(comments) })
362
+ .where(eq(attempts.attemptId, attemptId))
363
+ .execute();
364
+ });
365
+ const closeAttempt = Effect.fn("RunStoreQueries.closeAttempt")(function* (attemptId, outcome, endedAt) {
366
+ yield* database
367
+ .update(attempts)
368
+ .set({ outcomeJson: encodeOutcome(outcome), endedAt: encodeDateTime(endedAt) })
235
369
  .where(and(eq(attempts.attemptId, attemptId), isNull(attempts.outcomeJson)))
236
370
  .execute();
237
371
  });
372
+ const amend = Effect.fn("RunStoreQueries.amend")(function* (runId, context) {
373
+ yield* Effect.annotateCurrentSpan({ runId, agent: context.agent.name });
374
+ yield* database
375
+ .update(runs)
376
+ .set({
377
+ task: context.task,
378
+ agentJson: storedAgent(context.agent),
379
+ parentModel: Option.getOrNull(context.parentModel),
380
+ })
381
+ .where(eq(runs.runId, runId))
382
+ .execute();
383
+ });
384
+ const markIntervened = Effect.fn("RunStoreQueries.markIntervened")(function* (attemptId) {
385
+ yield* Effect.annotateCurrentSpan({ attemptId });
386
+ yield* database
387
+ .update(attempts)
388
+ .set({ operatorIntervened: true })
389
+ .where(eq(attempts.attemptId, attemptId))
390
+ .execute();
391
+ });
238
392
  const updateAttemptParentModel = Effect.fn("RunStoreQueries.updateAttemptParentModel")(function* (attemptId, parentModel) {
239
393
  yield* database
240
394
  .update(attempts)
@@ -252,7 +406,7 @@ export const queriesFor = (database) => {
252
406
  const approvalExists = Effect.fn("RunStoreQueries.approvalExists")(function* (runId) {
253
407
  return (yield* database.$count(approvals, eq(approvals.runId, runId)).execute()) > 0;
254
408
  });
255
- const recordApproval = Effect.fn("RunStoreQueries.recordApproval")(function* (runId, approval) {
409
+ const recordApproval = Effect.fn("RunStoreQueries.recordApproval")(function* (runId, approval, source) {
256
410
  yield* database
257
411
  .insert(events)
258
412
  .values({
@@ -263,30 +417,15 @@ export const queriesFor = (database) => {
263
417
  })
264
418
  .onConflictDoNothing()
265
419
  .execute();
266
- yield* database.insert(approvals).values({ runId, eventId: approval.id }).execute();
267
- });
268
- const requestWorkflowCommand = Effect.fn("RunStoreQueries.requestWorkflowCommand")(function* (runId, command) {
269
420
  yield* database
270
- .insert(workflowCommands)
271
- .values({ runId, command })
272
- .onConflictDoNothing()
273
- .execute();
274
- });
275
- const requestInterrupt = Effect.fn("RunStoreQueries.requestInterrupt")(function* (runId) {
276
- yield* database
277
- .delete(workflowCommands)
278
- .where(and(eq(workflowCommands.runId, runId), inArray(workflowCommands.command, ["Approve", "Resume"])))
279
- .execute();
280
- yield* requestWorkflowCommand(runId, "Interrupt");
281
- });
282
- const workflowCommandsFor = Effect.fn("RunStoreQueries.workflowCommandsFor")(function* (runId) {
283
- yield* Effect.annotateCurrentSpan({ runId });
284
- const rows = yield* database
285
- .select({ runId: workflowCommands.runId, command: workflowCommands.command })
286
- .from(workflowCommands)
287
- .where(eq(workflowCommands.runId, runId))
421
+ .insert(approvals)
422
+ .values({
423
+ runId,
424
+ eventId: approval.id,
425
+ channel: source.channel,
426
+ approver: Option.getOrNull(source.approver),
427
+ })
288
428
  .execute();
289
- return (yield* decodeWorkflowCommands(rows)).map(({ command }) => command);
290
429
  });
291
430
  const createRun = Effect.fn("RunStoreQueries.createRun")(function* (submission, eventId, runId) {
292
431
  const candidate = alias(triggerEvents, "candidate");
@@ -351,7 +490,48 @@ export const queriesFor = (database) => {
351
490
  .limit(1)
352
491
  .execute());
353
492
  });
354
- const recordTriggerEvent = Effect.fn("RunStoreQueries.recordTriggerEvent")(function* (submission) {
493
+ /** Returns the newest reopening of `eventId` (#267), with its reopening number. */
494
+ const newestReopening = Effect.fn("RunStoreQueries.newestReopening")(function* (eventId) {
495
+ const rows = yield* decodeReopenings(yield* database
496
+ .select({ ...eventRouteSelection, reopening: triggerEvents.reopening })
497
+ .from(events)
498
+ .innerJoin(triggerEvents, eq(triggerEvents.eventId, events.eventId))
499
+ .where(eq(triggerEvents.reopensEventId, eventId))
500
+ .orderBy(desc(triggerEvents.reopening))
501
+ .limit(1)
502
+ .execute());
503
+ return Option.map(Option.fromNullishOr(rows[0]), (row) => ({
504
+ submission: submissionFromRow(row),
505
+ reopening: row.reopening,
506
+ }));
507
+ });
508
+ /** Returns the trigger Event that `eventId` reopens or runs again, or `None` for a trigger's own Event. */
509
+ const reopenedEvent = Effect.fn("RunStoreQueries.reopenedEvent")(function* (eventId) {
510
+ const reopened = alias(events, "reopened_event");
511
+ const rows = yield* decodeReopenedEvents(yield* database
512
+ .select({ eventId: reopened.eventId, dedupKey: reopened.dedupKey })
513
+ .from(triggerEvents)
514
+ .innerJoin(reopened, eq(reopened.eventId, triggerEvents.reopensEventId))
515
+ .where(eq(triggerEvents.eventId, eventId))
516
+ .execute());
517
+ return Option.fromNullishOr(rows[0]);
518
+ });
519
+ /** Records that `eventId` came from running a closed Run again, and the requesting login. */
520
+ const recordRunAgain = Effect.fn("RunStoreQueries.recordRunAgain")(function* (eventId, again) {
521
+ yield* database
522
+ .insert(runAgainEvents)
523
+ .values({
524
+ eventId,
525
+ fromRunId: again.from,
526
+ requestedBy: Option.getOrNull(again.requestedBy),
527
+ })
528
+ .execute();
529
+ });
530
+ /**
531
+ * Records a trigger Event and its route once, and returns the route as first recorded.
532
+ * `reopens` is the earlier Event that this one reopens.
533
+ */
534
+ const recordTriggerEvent = Effect.fn("RunStoreQueries.recordTriggerEvent")(function* (submission, reopens = Option.none()) {
355
535
  yield* database
356
536
  .insert(events)
357
537
  .values({
@@ -373,6 +553,7 @@ export const queriesFor = (database) => {
373
553
  loopName: submission.loop,
374
554
  stepName: submission.step,
375
555
  approval: submission.approval,
556
+ permissions: submission.permissions,
376
557
  cycleLimit: Option.getOrNull(submission.cycleLimit),
377
558
  cycleGeneration: submission.cycleGeneration,
378
559
  repository: submission.repository,
@@ -380,6 +561,11 @@ export const queriesFor = (database) => {
380
561
  task: submission.task,
381
562
  agentJson: storedAgent(submission.agent),
382
563
  parentModel: Option.getOrNull(submission.parentModel),
564
+ reopensEventId: Option.getOrNull(Option.map(reopens, ({ eventId }) => eventId)),
565
+ reopening: Option.match(reopens, {
566
+ onNone: () => 0,
567
+ onSome: ({ reopening }) => reopening,
568
+ }),
383
569
  })
384
570
  .execute();
385
571
  return submission;
@@ -391,6 +577,16 @@ export const queriesFor = (database) => {
391
577
  const current = routes[0];
392
578
  if (current === undefined)
393
579
  return Option.none();
580
+ const suppression = yield* loopItemQueries(database).suppression(current.loop, current.itemKey);
581
+ if (Option.isSome(suppression)) {
582
+ yield* Effect.logInfo("Queued Events wait while a suppression keeps the Item", {
583
+ runId,
584
+ suppressedBy: suppression.value.runId,
585
+ loop: current.loop,
586
+ itemKey: current.itemKey,
587
+ });
588
+ return Option.none();
589
+ }
394
590
  const queuedRows = yield* database
395
591
  .select(eventRouteSelection)
396
592
  .from(events)
@@ -399,103 +595,53 @@ export const queriesFor = (database) => {
399
595
  .leftJoin(ignoredEvents, eq(ignoredEvents.eventId, events.eventId))
400
596
  .where(and(eq(triggerEvents.loopName, current.loop), eq(triggerEvents.itemKey, current.itemKey), cycleQueries(database).currentEvent, isNull(runs.runId), isNull(ignoredEvents.eventId)))
401
597
  .orderBy(desc(events.receivedAt))
402
- .limit(1)
403
598
  .execute();
404
599
  const queued = yield* decodeEventRoutes(queuedRows);
405
- return Option.fromNullishOr(queued[0]).pipe(Option.map(submissionFromRow));
406
- });
407
- const appendTranscript = Effect.fn("RunStoreQueries.appendTranscript")(function* (direction, envelope, receivedAt) {
408
- yield* database
409
- .insert(attemptTranscripts)
410
- .values({
411
- attemptId: envelope.attemptId,
412
- direction,
413
- channelSequence: envelope.sequence,
414
- envelope: encodeEnvelope(envelope),
415
- receivedAt: encodeDateTime(receivedAt),
416
- })
417
- .onConflictDoNothing()
418
- .execute();
600
+ return Option.fromNullishOr(queued[0]).pipe(Option.map((newest) => ({
601
+ submission: submissionFromRow(newest),
602
+ eventIds: queued.map((row) => row.eventId),
603
+ })));
419
604
  });
420
- const transcript = Effect.fn("RunStoreQueries.transcript")(function* (query) {
421
- yield* Effect.annotateCurrentSpan({
422
- attemptId: query.attemptId,
423
- query: query._tag,
424
- });
425
- const cursor = Match.valueTags(query, {
426
- TranscriptAllQuery: ({ attemptId }) => eq(attemptTranscripts.attemptId, attemptId),
427
- TranscriptTailQuery: ({ attemptId, after }) => and(eq(attemptTranscripts.attemptId, attemptId), gt(attemptTranscripts.sequence, after)),
428
- TranscriptPageQuery: ({ attemptId, before }) => Option.match(before, {
429
- onNone: () => eq(attemptTranscripts.attemptId, attemptId),
430
- onSome: (sequence) => and(eq(attemptTranscripts.attemptId, attemptId), lt(attemptTranscripts.sequence, sequence)),
431
- }),
432
- });
433
- const selection = database
434
- .select({
435
- sequence: attemptTranscripts.sequence,
436
- attemptId: attemptTranscripts.attemptId,
437
- direction: attemptTranscripts.direction,
438
- envelope: attemptTranscripts.envelope,
439
- receivedAt: attemptTranscripts.receivedAt,
440
- })
441
- .from(attemptTranscripts)
442
- .where(cursor);
443
- // A limited read walks the tail index backwards for the newest N rows and
444
- // re-sorts ascending, so callers always receive append order.
445
- const rows = yield* Match.valueTags(query, {
446
- TranscriptAllQuery: () => selection.orderBy(asc(attemptTranscripts.sequence)).execute(),
447
- TranscriptTailQuery: ({ limit }) => selection.orderBy(asc(attemptTranscripts.sequence)).limit(limit).execute(),
448
- TranscriptPageQuery: ({ limit }) => selection.orderBy(desc(attemptTranscripts.sequence)).limit(limit).execute(),
449
- });
450
- // Transcript rows are usine-owned, so an unknown key is corruption.
451
- return (yield* decodeTranscriptRows(rows, { onExcessProperty: "error" }))
452
- .map(transcriptEntryFromRow)
453
- .sort((left, right) => left.sequence - right.sequence);
454
- });
455
- const recoverable = Effect.fn("RunStoreQueries.recoverable")(function* () {
605
+ const runIdsToDrive = Effect.fn("RunStoreQueries.runIdsToDrive")(function* () {
606
+ const waiting = "Waiting";
456
607
  const rows = yield* database
457
- .select({
458
- ...eventRouteSelection,
459
- runId: runs.runId,
460
- state: runs.stateJson,
461
- })
608
+ .select({ runId: runs.runId })
462
609
  .from(runs)
463
- .innerJoin(events, eq(events.eventId, runs.eventId))
464
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, events.eventId))
465
- .where(openState)
610
+ .where(and(openState, ne(storedStateTag, waiting)))
466
611
  .execute();
467
- return yield* decodeRecoverableRuns(rows);
612
+ return (yield* decodeRunIds(rows)).map(({ runId }) => runId);
468
613
  });
469
- const pendingWorkflowCommands = Effect.fn("RunStoreQueries.pendingWorkflowCommands")(function* () {
470
- const rows = yield* database
471
- .select({ runId: workflowCommands.runId, command: workflowCommands.command })
472
- .from(workflowCommands)
473
- .execute();
474
- const decoded = yield* decodeWorkflowCommands(rows);
475
- // Delivery order per run is a replay invariant; WORKFLOW_COMMAND_PRIORITY names it.
476
- const priority = (command) => WORKFLOW_COMMAND_PRIORITY.indexOf(command);
477
- return [...decoded].sort((left, right) => left.runId === right.runId
478
- ? priority(left.command) - priority(right.command)
479
- : left.runId < right.runId
480
- ? -1
481
- : 1);
482
- });
483
- const completeWorkflowCommand = Effect.fn("RunStoreQueries.completeWorkflowCommand")(function* (runId, command) {
484
- yield* database
485
- .delete(workflowCommands)
486
- .where(and(eq(workflowCommands.runId, runId), eq(workflowCommands.command, command)))
614
+ // The newest closed Run of each Item that has a current queued Event and no open Run, excluding
615
+ // Items waiting for a cycle-budget reset or under a suppression.
616
+ const pendingContinuations = Effect.fn("RunStoreQueries.pendingContinuations")(function* () {
617
+ const items = yield* database
618
+ .selectDistinct({ loop: triggerEvents.loopName, itemKey: triggerEvents.itemKey })
619
+ .from(triggerEvents)
620
+ .leftJoin(runs, eq(runs.eventId, triggerEvents.eventId))
621
+ .leftJoin(ignoredEvents, eq(ignoredEvents.eventId, triggerEvents.eventId))
622
+ .leftJoin(itemCycleBudgets, eq(itemCycleBudgets.itemKey, triggerEvents.itemKey))
623
+ .leftJoin(loopItems, and(eq(loopItems.loopName, triggerEvents.loopName), eq(loopItems.itemKey, triggerEvents.itemKey)))
624
+ .where(and(isNull(runs.runId), isNull(ignoredEvents.eventId), isNull(itemCycleBudgets.askJson), isNull(loopItems.suppressedByRunId), cycleQueries(database).currentEvent))
487
625
  .execute();
626
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ loop: LoopName, itemKey: ItemKey })))(items);
627
+ const closed = yield* Effect.forEach(decoded, ({ loop, itemKey }) => Effect.gen(function* () {
628
+ if ((yield* blockingRun(loop, itemKey)).length > 0)
629
+ return [];
630
+ return yield* decodeRunIds(yield* database
631
+ .select({ runId: runs.runId })
632
+ .from(runs)
633
+ .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
634
+ .where(and(eq(triggerEvents.loopName, loop), eq(triggerEvents.itemKey, itemKey)))
635
+ .orderBy(desc(runs.openedAt), desc(runs.runId))
636
+ .limit(1)
637
+ .execute());
638
+ }));
639
+ return closed.flat().map(({ runId }) => runId);
488
640
  });
489
- const revokeUnstarted = Effect.fn("RunStoreQueries.revokeUnstarted")(function* (loop, itemKey) {
490
- const rows = yield* decodeRunIds(yield* database
491
- .select({ runId: runs.runId })
492
- .from(runs)
493
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
494
- .where(and(eq(triggerEvents.loopName, loop), eq(triggerEvents.itemKey, itemKey), unstartedState))
495
- .execute());
641
+ const revokeQueued = Effect.fn("RunStoreQueries.revokeQueued")(function* (loop, itemKey, eventIds) {
496
642
  const candidate = alias(triggerEvents, "candidate");
497
643
  const candidateRun = alias(runs, "candidate_run");
498
- yield* database
644
+ const ignored = yield* database
499
645
  .insert(ignoredEvents)
500
646
  .select(database
501
647
  .select({
@@ -504,40 +650,67 @@ export const queriesFor = (database) => {
504
650
  })
505
651
  .from(candidate)
506
652
  .leftJoin(candidateRun, eq(candidateRun.eventId, candidate.eventId))
507
- .where(and(eq(candidate.loopName, loop), eq(candidate.itemKey, itemKey), isNull(candidateRun.runId))))
653
+ .where(and(eq(candidate.loopName, loop), eq(candidate.itemKey, itemKey), inArray(candidate.eventId, eventIds), isNull(candidateRun.runId))))
508
654
  .onConflictDoNothing()
655
+ .returning({ eventId: ignoredEvents.eventId })
509
656
  .execute();
657
+ return ignored.length;
658
+ });
659
+ const revokeUnstarted = Effect.fn("RunStoreQueries.revokeUnstarted")(function* (loop, itemKey) {
660
+ const rows = yield* decodeRunIds(yield* database
661
+ .select({ runId: runs.runId })
662
+ .from(runs)
663
+ .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
664
+ .where(and(eq(triggerEvents.loopName, loop), eq(triggerEvents.itemKey, itemKey), unstartedState))
665
+ .execute());
666
+ const queued = database
667
+ .select({ eventId: triggerEvents.eventId })
668
+ .from(triggerEvents)
669
+ .where(and(eq(triggerEvents.loopName, loop), eq(triggerEvents.itemKey, itemKey)));
670
+ yield* revokeQueued(loop, itemKey, queued);
510
671
  return rows;
511
672
  });
512
673
  return {
674
+ salvageCandidates,
675
+ salvageCandidate,
513
676
  cycles: cycleQueries(database),
677
+ loopItems: loopItemQueries(database),
678
+ ...transcriptQueries(database),
514
679
  addAttempt,
515
- appendTranscript,
680
+ recordReattach,
681
+ nextStart,
682
+ setNextStart,
683
+ recordSalvage,
684
+ recordComments,
685
+ amend,
686
+ markIntervened,
516
687
  approvalExists,
517
688
  blockingRun,
518
689
  closeAttempt,
519
- completeWorkflowCommand,
520
690
  createRun,
521
691
  eventRouteByRun,
522
- workflowCommandsFor,
523
692
  ignoredEvent,
693
+ newestReopening,
524
694
  nextQueued,
525
- pendingWorkflowCommands,
695
+ reopenedEvent,
696
+ recordRunAgain,
697
+ pendingContinuations,
526
698
  recordTriggerEvent,
527
- recoverable,
699
+ runIdsToDrive,
528
700
  recordApproval,
529
- requestInterrupt,
530
- requestWorkflowCommand,
531
701
  revision,
702
+ revokeQueued,
532
703
  revokeUnstarted,
533
704
  runSummaries,
705
+ closedRunSummaries,
534
706
  itemRunSummaries,
707
+ runEvents,
535
708
  runForEvent,
536
709
  snapshot,
537
- transcript,
538
710
  updateStepResult,
539
711
  updateAttemptParentModel,
540
712
  updateState,
713
+ resolveFailure,
541
714
  };
542
715
  };
543
716
  //# sourceMappingURL=queries.js.map