@usine/engine 0.3.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 (190) hide show
  1. package/README.md +109 -38
  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 +11 -10
  31. package/dist/database/local-sqlite.js.map +1 -1
  32. package/dist/database/schema.d.ts +533 -22
  33. package/dist/database/schema.d.ts.map +1 -1
  34. package/dist/database/schema.js +100 -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 +55 -26
  45. package/dist/engine/engine.d.ts.map +1 -1
  46. package/dist/engine/engine.js +118 -128
  47. package/dist/engine/engine.js.map +1 -1
  48. package/dist/engine/error.d.ts +29 -10
  49. package/dist/engine/error.d.ts.map +1 -1
  50. package/dist/engine/error.js +28 -8
  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 +64 -10
  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 +207 -42
  117. package/dist/store/codec.d.ts.map +1 -1
  118. package/dist/store/codec.js +52 -14
  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 +342 -35
  129. package/dist/store/queries.d.ts.map +1 -1
  130. package/dist/store/queries.js +302 -107
  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 +468 -190
  135. package/dist/store/store.js.map +1 -1
  136. package/dist/store/transcript.d.ts +3 -3
  137. package/dist/store/transcript.d.ts.map +1 -1
  138. package/dist/store/transcript.js +25 -6
  139. package/dist/store/transcript.js.map +1 -1
  140. package/dist/transcript.d.ts +19 -20
  141. package/dist/transcript.d.ts.map +1 -1
  142. package/dist/transcript.js +23 -21
  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/20260917071139_resolve_run_failure/migration.sql +1 -0
  149. package/drizzle/20260917071139_resolve_run_failure/snapshot.json +1471 -0
  150. package/drizzle/20260918032643_attempt_salvage/migration.sql +1 -0
  151. package/drizzle/20260918032643_attempt_salvage/snapshot.json +1481 -0
  152. package/drizzle/20261011153856_drop_workflow_tables/migration.sql +4 -0
  153. package/drizzle/20261011153856_drop_workflow_tables/snapshot.json +1433 -0
  154. package/drizzle/20261017155043_attempt_salvage_check/migration.sql +41 -0
  155. package/drizzle/20261017155043_attempt_salvage_check/snapshot.json +1421 -0
  156. package/drizzle/20261018162700_discard_attempt_leftovers/migration.sql +1 -0
  157. package/drizzle/20261018162700_discard_attempt_leftovers/snapshot.json +1431 -0
  158. package/drizzle/20261018172605_loop_items_and_reopenings/migration.sql +15 -0
  159. package/drizzle/20261018172605_loop_items_and_reopenings/snapshot.json +1574 -0
  160. package/drizzle/20261018180339_attempt_ended_at/migration.sql +1 -0
  161. package/drizzle/20261018180339_attempt_ended_at/snapshot.json +1584 -0
  162. package/drizzle/20261019104043_run_approval_source/migration.sql +15 -0
  163. package/drizzle/20261019104043_run_approval_source/snapshot.json +1610 -0
  164. package/drizzle/20261019133538_board_moves/migration.sql +14 -0
  165. package/drizzle/20261019133538_board_moves/snapshot.json +1731 -0
  166. package/drizzle/20261019170538_item_projection_gone/migration.sql +1 -0
  167. package/drizzle/20261019170538_item_projection_gone/snapshot.json +1741 -0
  168. package/drizzle/20261019180649_run_again_events/migration.sql +7 -0
  169. package/drizzle/20261019180649_run_again_events/snapshot.json +1804 -0
  170. package/drizzle/20261019185008_trigger_event_permissions/migration.sql +1 -0
  171. package/drizzle/20261019185008_trigger_event_permissions/snapshot.json +1814 -0
  172. package/drizzle/20261020103746_attempt_start/migration.sql +5 -0
  173. package/drizzle/20261020103746_attempt_start/snapshot.json +1844 -0
  174. package/drizzle/20261020125211_attempt_comments/migration.sql +1 -0
  175. package/drizzle/20261020125211_attempt_comments/snapshot.json +1854 -0
  176. package/drizzle/20261020191320_run_closed_at/migration.sql +6 -0
  177. package/drizzle/20261020191320_run_closed_at/snapshot.json +1864 -0
  178. package/drizzle/20261023091428_rename_loop_item_hold_to_suppression/migration.sql +2 -0
  179. package/drizzle/20261023091428_rename_loop_item_hold_to_suppression/snapshot.json +1867 -0
  180. package/package.json +15 -5
  181. package/dist/workflow/admission.d.ts.map +0 -1
  182. package/dist/workflow/admission.js.map +0 -1
  183. package/dist/workflow/command.d.ts +0 -13
  184. package/dist/workflow/command.d.ts.map +0 -1
  185. package/dist/workflow/command.js +0 -17
  186. package/dist/workflow/command.js.map +0 -1
  187. package/dist/workflow/workflow.d.ts +0 -18
  188. package/dist/workflow/workflow.d.ts.map +0 -1
  189. package/dist/workflow/workflow.js +0 -108
  190. package/dist/workflow/workflow.js.map +0 -1
@@ -1,22 +1,25 @@
1
- import { Attempt, AttemptId, AttemptContext } from "@usine/core/attempt";
1
+ import { RepositoryName } from "@usine/core/repository";
2
+ import { WorkerName } from "@usine/core/worker";
3
+ import { Attempt, AttemptId, AttemptContext, AttemptNumber } from "@usine/core/attempt";
2
4
  import { AttemptOutcome } from "@usine/core/attempt/outcome";
5
+ import { AttemptSalvage, salvageRefFor } from "@usine/core/attempt/salvage";
3
6
  import { Event, DedupKey, EventId } from "@usine/core/event";
4
7
  import { ItemKey } from "@usine/core/item";
5
- import { LoopName } from "@usine/core/loop";
8
+ import { LoopName, LoopPermissions } from "@usine/core/loop";
6
9
  import { RunId } from "@usine/core/run";
7
10
  import { StepName } from "@usine/core/step";
8
11
  import * as RunState from "@usine/core/run/state";
9
12
  import { StepResult } from "@usine/core/step/result";
10
- import { and, asc, desc, eq, inArray, isNull, ne, notInArray, sql } from "drizzle-orm";
13
+ import { and, asc, desc, eq, inArray, isNull, ne, notInArray, sql, } from "drizzle-orm";
11
14
  import { alias } from "drizzle-orm/sqlite-core";
12
15
  import { Array as EffectArray, DateTime, Effect, Option, Schema } from "effect";
13
- import { approvals, attempts, events, ignoredEvents, projectionRevision, runs, steps, triggerEvents, workflowCommands, } from "../database/schema.js";
16
+ import { approvals, attempts, events, ignoredEvents, itemCycleBudgets, loopItems, projectionRevision, runAgainEvents, runs, steps, triggerEvents, } from "../database/schema.js";
14
17
  import { RunNotFoundError } from "../engine/error.js";
15
18
  import { RunSnapshot, RunSummary } from "../run-reader/model.js";
16
19
  import { Submission } from "../engine/submission.js";
17
- import { WORKFLOW_COMMAND_PRIORITY } from "../workflow/command.js";
18
20
  import { cycleQueries } from "../cycle-budget/queries.js";
19
- import { attemptFromRow, decodeAttemptRows, decodeDiscards, decodeEventRoutes, decodeRecoverableRuns, decodeProjectionRevision, decodeRunIds, decodeRunRows, decodeWorkflowCommands, encodeAgent, encodeBrainSpec, encodeDateTime, encodeOutcome, encodePayload, encodeRunState, encodeStepResult, eventFromRow, runFromRow, runSummaryFromRow, stepFromRow, submissionFromRow, } 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";
20
23
  import { transcriptQueries } from "./transcript.js";
21
24
  const newestRunFirst = (left, right) => DateTime.toEpochMillis(right.openedAt) - DateTime.toEpochMillis(left.openedAt) ||
22
25
  right.runId.localeCompare(left.runId);
@@ -29,6 +32,7 @@ const eventRouteSelection = {
29
32
  loop: triggerEvents.loopName,
30
33
  step: triggerEvents.stepName,
31
34
  approval: triggerEvents.approval,
35
+ permissions: triggerEvents.permissions,
32
36
  cycleLimit: triggerEvents.cycleLimit,
33
37
  cycleGeneration: triggerEvents.cycleGeneration,
34
38
  repository: triggerEvents.repository,
@@ -37,13 +41,16 @@ const eventRouteSelection = {
37
41
  agent: triggerEvents.agentJson,
38
42
  parentModel: triggerEvents.parentModel,
39
43
  };
44
+ const approvalEvents = alias(events, "approval_event");
40
45
  const runSelection = {
41
46
  itemKey: triggerEvents.itemKey,
42
47
  runId: runs.runId,
43
48
  eventId: runs.eventId,
44
49
  loop: runs.loopName,
45
50
  state: runs.stateJson,
51
+ resolvedFailureAttemptId: runs.resolvedFailureAttemptId,
46
52
  openedAt: runs.openedAt,
53
+ closedAt: runs.closedAt,
47
54
  repository: runs.repository,
48
55
  revision: runs.revision,
49
56
  task: runs.task,
@@ -51,8 +58,13 @@ const runSelection = {
51
58
  parentModel: runs.parentModel,
52
59
  stepName: steps.stepName,
53
60
  result: steps.result,
61
+ approvedAt: approvalEvents.receivedAt,
62
+ approvalChannel: approvals.channel,
63
+ approver: approvals.approver,
64
+ againFrom: runAgainEvents.fromRunId,
65
+ againRequestedBy: runAgainEvents.requestedBy,
54
66
  };
55
- const attemptSelection = {
67
+ export const attemptSelection = {
56
68
  attemptId: attempts.attemptId,
57
69
  runId: attempts.runId,
58
70
  stepName: attempts.stepName,
@@ -65,8 +77,12 @@ const attemptSelection = {
65
77
  parentModel: attempts.parentModel,
66
78
  startedAt: attempts.startedAt,
67
79
  outcome: attempts.outcomeJson,
80
+ endedAt: attempts.endedAt,
81
+ salvage: attempts.salvageJson,
82
+ start: attempts.startJson,
83
+ reattachedAt: attempts.reattachedAt,
84
+ comments: attempts.commentsJson,
68
85
  };
69
- // The state partitions are owned by @usine/core; these predicates only borrow their tags.
70
86
  const storedStateTag = sql `json_extract(${runs.stateJson}, '$._tag')`;
71
87
  const openState = inArray(storedStateTag, [...RunState.OPEN_STATE_TAGS]);
72
88
  const unstartedState = inArray(storedStateTag, [...RunState.UNSTARTED_STATE_TAGS]);
@@ -74,8 +90,7 @@ const unstartedState = inArray(storedStateTag, [...RunState.UNSTARTED_STATE_TAGS
74
90
  // cannot emit the declaration (TS2883).
75
91
  const storedAgent = (agent) => Schema.decodeUnknownSync(Schema.Json)(encodeAgent(agent));
76
92
  export const queriesFor = (database) => {
77
- // Keep `.execute()` explicit: the yieldable wrapper can cross an Effect
78
- // Workflow suspension before its final mutation is observable (ADR 0020).
93
+ // `.execute()` stays explicit (ADR 0020).
79
94
  const eventRouteByDedup = Effect.fn("RunStoreQueries.eventRouteByDedup")(function* (dedupKey) {
80
95
  const rows = yield* database
81
96
  .select(eventRouteSelection)
@@ -95,15 +110,57 @@ export const queriesFor = (database) => {
95
110
  .execute();
96
111
  return yield* decodeEventRoutes(rows);
97
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));
98
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* () {
99
127
  const rows = yield* database
100
- .select(runSelection)
101
- .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))
102
139
  .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
103
- .innerJoin(steps, eq(steps.runId, runs.runId))
104
- .where(eq(runs.runId, runId))
140
+ .where(and(isNull(attempts.leftoversDiscardedAt), ...filters))
105
141
  .execute();
106
- 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)));
107
164
  });
108
165
  const attemptRows = Effect.fn("RunStoreQueries.attemptRows")(function* (runId) {
109
166
  const rows = yield* database
@@ -131,17 +188,21 @@ export const queriesFor = (database) => {
131
188
  run: runFromRow(storedRun),
132
189
  step: stepFromRow(storedRun),
133
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),
134
195
  repository: storedRun.repository,
135
196
  revision: storedRun.revision,
136
197
  task: storedRun.task,
137
198
  agent: storedRun.agent,
138
199
  parentModel: Option.fromNullishOr(storedRun.parentModel),
200
+ permissions: route.permissions,
139
201
  });
140
202
  });
141
203
  const summariesWithAttempts = Effect.fn("RunStoreQueries.summariesWithAttempts")(function* (storedRuns) {
142
204
  const runIds = storedRuns.map(({ runId }) => runId);
143
- // Keep history queries below adapter bind-parameter limits, even for a
144
- // long-lived Task with many Runs.
205
+ // Chunks keep each read below the driver's bind-parameter limit.
145
206
  const storedAttempts = (yield* Effect.forEach(EffectArray.chunksOf(runIds, 100), (ids) => database
146
207
  .select(attemptSelection)
147
208
  .from(attempts)
@@ -155,11 +216,7 @@ export const queriesFor = (database) => {
155
216
  const itemRunSummaries = Effect.fn("RunStoreQueries.itemRunSummaries")(function* (itemKeys) {
156
217
  if (itemKeys.length === 0)
157
218
  return [];
158
- const storedRuns = (yield* Effect.forEach(EffectArray.chunksOf([...new Set(itemKeys)], 100), (keys) => database
159
- .select(runSelection)
160
- .from(runs)
161
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
162
- .innerJoin(steps, eq(steps.runId, runs.runId))
219
+ const storedRuns = (yield* Effect.forEach(EffectArray.chunksOf([...new Set(itemKeys)], 100), (keys) => selectRuns()
163
220
  .where(inArray(triggerEvents.itemKey, keys))
164
221
  .execute()
165
222
  .pipe(Effect.flatMap(decodeRunRows))))
@@ -167,24 +224,15 @@ export const queriesFor = (database) => {
167
224
  .sort(newestRunFirst);
168
225
  return yield* summariesWithAttempts(storedRuns);
169
226
  });
170
- // Every open Run plus the newest `closedLimit` settled ones, so an old
171
- // 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.
172
228
  const runSummaries = Effect.fn("RunStoreQueries.runSummaries")(function* (closedLimit) {
173
229
  const [openRuns, closedRuns] = yield* Effect.all([
174
- database
175
- .select(runSelection)
176
- .from(runs)
177
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
178
- .innerJoin(steps, eq(steps.runId, runs.runId))
230
+ selectRuns()
179
231
  .where(openState)
180
232
  .orderBy(desc(runs.openedAt), desc(runs.runId))
181
233
  .execute()
182
234
  .pipe(Effect.flatMap(decodeRunRows)),
183
- database
184
- .select(runSelection)
185
- .from(runs)
186
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
187
- .innerJoin(steps, eq(steps.runId, runs.runId))
235
+ selectRuns()
188
236
  .where(notInArray(storedStateTag, [...RunState.OPEN_STATE_TAGS]))
189
237
  .orderBy(desc(runs.openedAt), desc(runs.runId))
190
238
  .limit(closedLimit)
@@ -194,6 +242,32 @@ export const queriesFor = (database) => {
194
242
  const storedRuns = [...openRuns, ...closedRuns].sort(newestRunFirst);
195
243
  return yield* summariesWithAttempts(storedRuns);
196
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
+ });
197
271
  const revision = Effect.fn("RunStoreQueries.projectionRevision")(function* () {
198
272
  const rows = yield* decodeProjectionRevision(yield* database
199
273
  .select({ revision: projectionRevision.revision })
@@ -203,10 +277,12 @@ export const queriesFor = (database) => {
203
277
  .execute());
204
278
  return Option.fromNullishOr(rows[0]?.revision);
205
279
  });
206
- 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));
207
283
  const updated = yield* database
208
284
  .update(runs)
209
- .set({ stateJson: encodeRunState(state) })
285
+ .set({ stateJson: encodeRunState(state), closedAt })
210
286
  .where(eq(runs.runId, runId))
211
287
  .returning({ runId: runs.runId })
212
288
  .execute();
@@ -214,6 +290,14 @@ export const queriesFor = (database) => {
214
290
  return yield* new RunNotFoundError({ runId });
215
291
  }
216
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
+ });
217
301
  const addAttempt = Effect.fn("RunStoreQueries.addAttempt")(function* (attempt) {
218
302
  yield* database
219
303
  .insert(attempts)
@@ -230,13 +314,58 @@ export const queriesFor = (database) => {
230
314
  parentModel: Option.getOrNull(attempt.parentModel),
231
315
  startedAt: encodeDateTime(attempt.startedAt),
232
316
  outcomeJson: null,
317
+ endedAt: null,
318
+ salvageJson: null,
319
+ startJson: Option.match(attempt.start, { onNone: () => null, onSome: encodeAttemptStart }),
233
320
  })
234
321
  .execute();
235
322
  });
236
- const closeAttempt = Effect.fn("RunStoreQueries.closeAttempt")(function* (attemptId, outcome) {
323
+ const recordReattach = Effect.fn("RunStoreQueries.recordReattach")(function* (attemptId, reattachedAt) {
324
+ yield* Effect.annotateCurrentSpan({ attemptId });
237
325
  yield* database
238
326
  .update(attempts)
239
- .set({ outcomeJson: encodeOutcome(outcome) })
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 }),
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))
355
+ .execute();
356
+ });
357
+ const recordComments = Effect.fn("RunStoreQueries.recordComments")(function* (attemptId, comments) {
358
+ yield* Effect.annotateCurrentSpan({ attemptId, comments: comments.length });
359
+ yield* database
360
+ .update(attempts)
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) })
240
369
  .where(and(eq(attempts.attemptId, attemptId), isNull(attempts.outcomeJson)))
241
370
  .execute();
242
371
  });
@@ -277,7 +406,7 @@ export const queriesFor = (database) => {
277
406
  const approvalExists = Effect.fn("RunStoreQueries.approvalExists")(function* (runId) {
278
407
  return (yield* database.$count(approvals, eq(approvals.runId, runId)).execute()) > 0;
279
408
  });
280
- const recordApproval = Effect.fn("RunStoreQueries.recordApproval")(function* (runId, approval) {
409
+ const recordApproval = Effect.fn("RunStoreQueries.recordApproval")(function* (runId, approval, source) {
281
410
  yield* database
282
411
  .insert(events)
283
412
  .values({
@@ -288,30 +417,15 @@ export const queriesFor = (database) => {
288
417
  })
289
418
  .onConflictDoNothing()
290
419
  .execute();
291
- yield* database.insert(approvals).values({ runId, eventId: approval.id }).execute();
292
- });
293
- const requestWorkflowCommand = Effect.fn("RunStoreQueries.requestWorkflowCommand")(function* (runId, command) {
294
- yield* database
295
- .insert(workflowCommands)
296
- .values({ runId, command })
297
- .onConflictDoNothing()
298
- .execute();
299
- });
300
- const requestInterrupt = Effect.fn("RunStoreQueries.requestInterrupt")(function* (runId) {
301
420
  yield* database
302
- .delete(workflowCommands)
303
- .where(and(eq(workflowCommands.runId, runId), inArray(workflowCommands.command, ["Approve", "Resume"])))
304
- .execute();
305
- yield* requestWorkflowCommand(runId, "Interrupt");
306
- });
307
- const workflowCommandsFor = Effect.fn("RunStoreQueries.workflowCommandsFor")(function* (runId) {
308
- yield* Effect.annotateCurrentSpan({ runId });
309
- const rows = yield* database
310
- .select({ runId: workflowCommands.runId, command: workflowCommands.command })
311
- .from(workflowCommands)
312
- .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
+ })
313
428
  .execute();
314
- return (yield* decodeWorkflowCommands(rows)).map(({ command }) => command);
315
429
  });
316
430
  const createRun = Effect.fn("RunStoreQueries.createRun")(function* (submission, eventId, runId) {
317
431
  const candidate = alias(triggerEvents, "candidate");
@@ -376,7 +490,48 @@ export const queriesFor = (database) => {
376
490
  .limit(1)
377
491
  .execute());
378
492
  });
379
- 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()) {
380
535
  yield* database
381
536
  .insert(events)
382
537
  .values({
@@ -398,6 +553,7 @@ export const queriesFor = (database) => {
398
553
  loopName: submission.loop,
399
554
  stepName: submission.step,
400
555
  approval: submission.approval,
556
+ permissions: submission.permissions,
401
557
  cycleLimit: Option.getOrNull(submission.cycleLimit),
402
558
  cycleGeneration: submission.cycleGeneration,
403
559
  repository: submission.repository,
@@ -405,6 +561,11 @@ export const queriesFor = (database) => {
405
561
  task: submission.task,
406
562
  agentJson: storedAgent(submission.agent),
407
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
+ }),
408
569
  })
409
570
  .execute();
410
571
  return submission;
@@ -416,6 +577,16 @@ export const queriesFor = (database) => {
416
577
  const current = routes[0];
417
578
  if (current === undefined)
418
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
+ }
419
590
  const queuedRows = yield* database
420
591
  .select(eventRouteSelection)
421
592
  .from(events)
@@ -424,55 +595,53 @@ export const queriesFor = (database) => {
424
595
  .leftJoin(ignoredEvents, eq(ignoredEvents.eventId, events.eventId))
425
596
  .where(and(eq(triggerEvents.loopName, current.loop), eq(triggerEvents.itemKey, current.itemKey), cycleQueries(database).currentEvent, isNull(runs.runId), isNull(ignoredEvents.eventId)))
426
597
  .orderBy(desc(events.receivedAt))
427
- .limit(1)
428
598
  .execute();
429
599
  const queued = yield* decodeEventRoutes(queuedRows);
430
- return Option.fromNullishOr(queued[0]).pipe(Option.map(submissionFromRow));
600
+ return Option.fromNullishOr(queued[0]).pipe(Option.map((newest) => ({
601
+ submission: submissionFromRow(newest),
602
+ eventIds: queued.map((row) => row.eventId),
603
+ })));
431
604
  });
432
- const recoverable = Effect.fn("RunStoreQueries.recoverable")(function* () {
605
+ const runIdsToDrive = Effect.fn("RunStoreQueries.runIdsToDrive")(function* () {
606
+ const waiting = "Waiting";
433
607
  const rows = yield* database
434
- .select({
435
- ...eventRouteSelection,
436
- runId: runs.runId,
437
- state: runs.stateJson,
438
- })
608
+ .select({ runId: runs.runId })
439
609
  .from(runs)
440
- .innerJoin(events, eq(events.eventId, runs.eventId))
441
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, events.eventId))
442
- .where(openState)
610
+ .where(and(openState, ne(storedStateTag, waiting)))
443
611
  .execute();
444
- return yield* decodeRecoverableRuns(rows);
612
+ return (yield* decodeRunIds(rows)).map(({ runId }) => runId);
445
613
  });
446
- const pendingWorkflowCommands = Effect.fn("RunStoreQueries.pendingWorkflowCommands")(function* () {
447
- const rows = yield* database
448
- .select({ runId: workflowCommands.runId, command: workflowCommands.command })
449
- .from(workflowCommands)
450
- .execute();
451
- const decoded = yield* decodeWorkflowCommands(rows);
452
- // Delivery order per run is a replay invariant; WORKFLOW_COMMAND_PRIORITY names it.
453
- const priority = (command) => WORKFLOW_COMMAND_PRIORITY.indexOf(command);
454
- return [...decoded].sort((left, right) => left.runId === right.runId
455
- ? priority(left.command) - priority(right.command)
456
- : left.runId < right.runId
457
- ? -1
458
- : 1);
459
- });
460
- const completeWorkflowCommand = Effect.fn("RunStoreQueries.completeWorkflowCommand")(function* (runId, command) {
461
- yield* database
462
- .delete(workflowCommands)
463
- .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))
464
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);
465
640
  });
466
- const revokeUnstarted = Effect.fn("RunStoreQueries.revokeUnstarted")(function* (loop, itemKey) {
467
- const rows = yield* decodeRunIds(yield* database
468
- .select({ runId: runs.runId })
469
- .from(runs)
470
- .innerJoin(triggerEvents, eq(triggerEvents.eventId, runs.eventId))
471
- .where(and(eq(triggerEvents.loopName, loop), eq(triggerEvents.itemKey, itemKey), unstartedState))
472
- .execute());
641
+ const revokeQueued = Effect.fn("RunStoreQueries.revokeQueued")(function* (loop, itemKey, eventIds) {
473
642
  const candidate = alias(triggerEvents, "candidate");
474
643
  const candidateRun = alias(runs, "candidate_run");
475
- yield* database
644
+ const ignored = yield* database
476
645
  .insert(ignoredEvents)
477
646
  .select(database
478
647
  .select({
@@ -481,41 +650,67 @@ export const queriesFor = (database) => {
481
650
  })
482
651
  .from(candidate)
483
652
  .leftJoin(candidateRun, eq(candidateRun.eventId, candidate.eventId))
484
- .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))))
485
654
  .onConflictDoNothing()
655
+ .returning({ eventId: ignoredEvents.eventId })
486
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);
487
671
  return rows;
488
672
  });
489
673
  return {
674
+ salvageCandidates,
675
+ salvageCandidate,
490
676
  cycles: cycleQueries(database),
677
+ loopItems: loopItemQueries(database),
491
678
  ...transcriptQueries(database),
492
679
  addAttempt,
680
+ recordReattach,
681
+ nextStart,
682
+ setNextStart,
683
+ recordSalvage,
684
+ recordComments,
493
685
  amend,
494
686
  markIntervened,
495
687
  approvalExists,
496
688
  blockingRun,
497
689
  closeAttempt,
498
- completeWorkflowCommand,
499
690
  createRun,
500
691
  eventRouteByRun,
501
- workflowCommandsFor,
502
692
  ignoredEvent,
693
+ newestReopening,
503
694
  nextQueued,
504
- pendingWorkflowCommands,
695
+ reopenedEvent,
696
+ recordRunAgain,
697
+ pendingContinuations,
505
698
  recordTriggerEvent,
506
- recoverable,
699
+ runIdsToDrive,
507
700
  recordApproval,
508
- requestInterrupt,
509
- requestWorkflowCommand,
510
701
  revision,
702
+ revokeQueued,
511
703
  revokeUnstarted,
512
704
  runSummaries,
705
+ closedRunSummaries,
513
706
  itemRunSummaries,
707
+ runEvents,
514
708
  runForEvent,
515
709
  snapshot,
516
710
  updateStepResult,
517
711
  updateAttemptParentModel,
518
712
  updateState,
713
+ resolveFailure,
519
714
  };
520
715
  };
521
716
  //# sourceMappingURL=queries.js.map