@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.
- package/README.md +109 -38
- package/dist/board-moves/board-moves.d.ts +20 -0
- package/dist/board-moves/board-moves.d.ts.map +1 -0
- package/dist/board-moves/board-moves.js +74 -0
- package/dist/board-moves/board-moves.js.map +1 -0
- package/dist/board-moves/model.d.ts +21 -0
- package/dist/board-moves/model.d.ts.map +1 -0
- package/dist/board-moves/model.js +22 -0
- package/dist/board-moves/model.js.map +1 -0
- package/dist/cursor/cursor.d.ts +2 -6
- package/dist/cursor/cursor.d.ts.map +1 -1
- package/dist/cursor/cursor.js +2 -6
- package/dist/cursor/cursor.js.map +1 -1
- package/dist/cycle-budget/cycle-budget.d.ts +6 -6
- package/dist/cycle-budget/cycle-budget.d.ts.map +1 -1
- package/dist/cycle-budget/cycle-budget.js +5 -5
- package/dist/cycle-budget/cycle-budget.js.map +1 -1
- package/dist/cycle-budget/model.d.ts +2 -2
- package/dist/cycle-budget/model.d.ts.map +1 -1
- package/dist/cycle-budget/model.js +2 -2
- package/dist/cycle-budget/model.js.map +1 -1
- package/dist/cycle-budget/queries.d.ts +1 -1
- package/dist/cycle-budget/queries.d.ts.map +1 -1
- package/dist/cycle-budget/queries.js +3 -3
- package/dist/cycle-budget/queries.js.map +1 -1
- package/dist/database/database.d.ts +1 -1
- package/dist/database/database.d.ts.map +1 -1
- package/dist/database/local-sqlite.d.ts +10 -8
- package/dist/database/local-sqlite.d.ts.map +1 -1
- package/dist/database/local-sqlite.js +11 -10
- package/dist/database/local-sqlite.js.map +1 -1
- package/dist/database/schema.d.ts +533 -22
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +100 -23
- package/dist/database/schema.js.map +1 -1
- package/dist/{workflow → engine}/admission.d.ts +2 -2
- package/dist/engine/admission.d.ts.map +1 -0
- package/dist/{workflow → engine}/admission.js +4 -4
- package/dist/engine/admission.js.map +1 -0
- package/dist/engine/checkout-recovery.d.ts +69 -0
- package/dist/engine/checkout-recovery.d.ts.map +1 -0
- package/dist/engine/checkout-recovery.js +181 -0
- package/dist/engine/checkout-recovery.js.map +1 -0
- package/dist/engine/engine.d.ts +55 -26
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +118 -128
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/error.d.ts +29 -10
- package/dist/engine/error.d.ts.map +1 -1
- package/dist/engine/error.js +28 -8
- package/dist/engine/error.js.map +1 -1
- package/dist/engine/lifecycle.d.ts +81 -28
- package/dist/engine/lifecycle.d.ts.map +1 -1
- package/dist/engine/lifecycle.js +83 -44
- package/dist/engine/lifecycle.js.map +1 -1
- package/dist/engine/retry.d.ts +16 -0
- package/dist/engine/retry.d.ts.map +1 -0
- package/dist/engine/retry.js +18 -0
- package/dist/engine/retry.js.map +1 -0
- package/dist/engine/run-again.d.ts +45 -0
- package/dist/engine/run-again.d.ts.map +1 -0
- package/dist/engine/run-again.js +56 -0
- package/dist/engine/run-again.js.map +1 -0
- package/dist/engine/run-driver.d.ts +54 -0
- package/dist/engine/run-driver.d.ts.map +1 -0
- package/dist/engine/run-driver.js +362 -0
- package/dist/engine/run-driver.js.map +1 -0
- package/dist/engine/shutdown.d.ts +3 -4
- package/dist/engine/shutdown.d.ts.map +1 -1
- package/dist/engine/shutdown.js +3 -4
- package/dist/engine/shutdown.js.map +1 -1
- package/dist/engine/submission.d.ts +31 -11
- package/dist/engine/submission.d.ts.map +1 -1
- package/dist/engine/submission.js +42 -12
- package/dist/engine/submission.js.map +1 -1
- package/dist/item-projection/item-projection.d.ts +12 -4
- package/dist/item-projection/item-projection.d.ts.map +1 -1
- package/dist/item-projection/item-projection.js +41 -6
- package/dist/item-projection/item-projection.js.map +1 -1
- package/dist/item-projection/model.d.ts +5 -1
- package/dist/item-projection/model.d.ts.map +1 -1
- package/dist/item-projection/model.js +6 -2
- package/dist/item-projection/model.js.map +1 -1
- package/dist/loop-control/loop-control.d.ts +5 -5
- package/dist/loop-control/loop-control.d.ts.map +1 -1
- package/dist/loop-control/loop-control.js +4 -4
- package/dist/loop-control/loop-control.js.map +1 -1
- package/dist/loop-control/model.d.ts +1 -1
- package/dist/loop-control/model.d.ts.map +1 -1
- package/dist/loop-control/model.js +1 -1
- package/dist/loop-control/model.js.map +1 -1
- package/dist/repository-label-projection/model.d.ts +2 -2
- package/dist/repository-label-projection/model.d.ts.map +1 -1
- package/dist/repository-label-projection/model.js +2 -2
- package/dist/repository-label-projection/model.js.map +1 -1
- package/dist/repository-label-projection/repository-label-projection.d.ts +1 -1
- package/dist/repository-label-projection/repository-label-projection.d.ts.map +1 -1
- package/dist/repository-label-projection/repository-label-projection.js +1 -1
- package/dist/repository-label-projection/repository-label-projection.js.map +1 -1
- package/dist/run-reader/model.d.ts +62 -4
- package/dist/run-reader/model.d.ts.map +1 -1
- package/dist/run-reader/model.js +66 -7
- package/dist/run-reader/model.js.map +1 -1
- package/dist/run-reader/run-reader.d.ts +19 -9
- package/dist/run-reader/run-reader.d.ts.map +1 -1
- package/dist/run-reader/run-reader.js +23 -7
- package/dist/run-reader/run-reader.js.map +1 -1
- package/dist/step-executor.d.ts +64 -10
- package/dist/step-executor.d.ts.map +1 -1
- package/dist/step-executor.js +10 -2
- package/dist/step-executor.js.map +1 -1
- package/dist/store/attempt-windows.d.ts +8 -0
- package/dist/store/attempt-windows.d.ts.map +1 -0
- package/dist/store/attempt-windows.js +53 -0
- package/dist/store/attempt-windows.js.map +1 -0
- package/dist/store/codec.d.ts +207 -42
- package/dist/store/codec.d.ts.map +1 -1
- package/dist/store/codec.js +52 -14
- package/dist/store/codec.js.map +1 -1
- package/dist/store/leftovers.d.ts +10 -0
- package/dist/store/leftovers.d.ts.map +1 -0
- package/dist/store/leftovers.js +43 -0
- package/dist/store/leftovers.js.map +1 -0
- package/dist/store/loop-items.d.ts +16 -0
- package/dist/store/loop-items.d.ts.map +1 -0
- package/dist/store/loop-items.js +185 -0
- package/dist/store/loop-items.js.map +1 -0
- package/dist/store/queries.d.ts +342 -35
- package/dist/store/queries.d.ts.map +1 -1
- package/dist/store/queries.js +302 -107
- package/dist/store/queries.js.map +1 -1
- package/dist/store/store.d.ts +91 -54
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +468 -190
- package/dist/store/store.js.map +1 -1
- package/dist/store/transcript.d.ts +3 -3
- package/dist/store/transcript.d.ts.map +1 -1
- package/dist/store/transcript.js +25 -6
- package/dist/store/transcript.js.map +1 -1
- package/dist/transcript.d.ts +19 -20
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +23 -21
- package/dist/transcript.js.map +1 -1
- package/dist/trigger-condition.d.ts +23 -0
- package/dist/trigger-condition.d.ts.map +1 -0
- package/dist/trigger-condition.js +8 -0
- package/dist/trigger-condition.js.map +1 -0
- package/drizzle/20260917071139_resolve_run_failure/migration.sql +1 -0
- package/drizzle/20260917071139_resolve_run_failure/snapshot.json +1471 -0
- package/drizzle/20260918032643_attempt_salvage/migration.sql +1 -0
- package/drizzle/20260918032643_attempt_salvage/snapshot.json +1481 -0
- package/drizzle/20261011153856_drop_workflow_tables/migration.sql +4 -0
- package/drizzle/20261011153856_drop_workflow_tables/snapshot.json +1433 -0
- package/drizzle/20261017155043_attempt_salvage_check/migration.sql +41 -0
- package/drizzle/20261017155043_attempt_salvage_check/snapshot.json +1421 -0
- package/drizzle/20261018162700_discard_attempt_leftovers/migration.sql +1 -0
- package/drizzle/20261018162700_discard_attempt_leftovers/snapshot.json +1431 -0
- package/drizzle/20261018172605_loop_items_and_reopenings/migration.sql +15 -0
- package/drizzle/20261018172605_loop_items_and_reopenings/snapshot.json +1574 -0
- package/drizzle/20261018180339_attempt_ended_at/migration.sql +1 -0
- package/drizzle/20261018180339_attempt_ended_at/snapshot.json +1584 -0
- package/drizzle/20261019104043_run_approval_source/migration.sql +15 -0
- package/drizzle/20261019104043_run_approval_source/snapshot.json +1610 -0
- package/drizzle/20261019133538_board_moves/migration.sql +14 -0
- package/drizzle/20261019133538_board_moves/snapshot.json +1731 -0
- package/drizzle/20261019170538_item_projection_gone/migration.sql +1 -0
- package/drizzle/20261019170538_item_projection_gone/snapshot.json +1741 -0
- package/drizzle/20261019180649_run_again_events/migration.sql +7 -0
- package/drizzle/20261019180649_run_again_events/snapshot.json +1804 -0
- package/drizzle/20261019185008_trigger_event_permissions/migration.sql +1 -0
- package/drizzle/20261019185008_trigger_event_permissions/snapshot.json +1814 -0
- package/drizzle/20261020103746_attempt_start/migration.sql +5 -0
- package/drizzle/20261020103746_attempt_start/snapshot.json +1844 -0
- package/drizzle/20261020125211_attempt_comments/migration.sql +1 -0
- package/drizzle/20261020125211_attempt_comments/snapshot.json +1854 -0
- package/drizzle/20261020191320_run_closed_at/migration.sql +6 -0
- package/drizzle/20261020191320_run_closed_at/snapshot.json +1864 -0
- package/drizzle/20261023091428_rename_loop_item_hold_to_suppression/migration.sql +2 -0
- package/drizzle/20261023091428_rename_loop_item_hold_to_suppression/snapshot.json +1867 -0
- package/package.json +15 -5
- package/dist/workflow/admission.d.ts.map +0 -1
- package/dist/workflow/admission.js.map +0 -1
- package/dist/workflow/command.d.ts +0 -13
- package/dist/workflow/command.d.ts.map +0 -1
- package/dist/workflow/command.js +0 -17
- package/dist/workflow/command.js.map +0 -1
- package/dist/workflow/workflow.d.ts +0 -18
- package/dist/workflow/workflow.d.ts.map +0 -1
- package/dist/workflow/workflow.js +0 -108
- package/dist/workflow/workflow.js.map +0 -1
package/dist/store/store.js
CHANGED
|
@@ -1,75 +1,138 @@
|
|
|
1
|
-
import { Attempt, AttemptId
|
|
1
|
+
import { Attempt, AttemptId } from "@usine/core/attempt";
|
|
2
2
|
import * as AttemptChannel from "@usine/core/attempt/channel";
|
|
3
3
|
import { AttemptFailure, InfraFailure } from "@usine/core/attempt/failure";
|
|
4
|
-
import {
|
|
4
|
+
import { StartedByEditAndRetry, StartedByRetry } from "@usine/core/attempt/start";
|
|
5
|
+
import { Event, EventId } from "@usine/core/event";
|
|
5
6
|
import { ItemKey } from "@usine/core/item";
|
|
6
7
|
import { LoopName } from "@usine/core/loop";
|
|
8
|
+
import { CycleGeneration } from "@usine/core/cycle-budget";
|
|
7
9
|
import { RunId, Run } from "@usine/core/run";
|
|
10
|
+
import { RunAgain } from "@usine/core/run/again";
|
|
11
|
+
import { ApprovalSource } from "@usine/core/run/approval";
|
|
8
12
|
import { WorkerName } from "@usine/core/worker";
|
|
9
13
|
import * as RunState from "@usine/core/run/state";
|
|
10
14
|
import { StepResult } from "@usine/core/step/result";
|
|
11
|
-
import {
|
|
15
|
+
import { Context, Crypto, DateTime, Effect, Layer, Match, Option, Result, Schema } from "effect";
|
|
12
16
|
import { RunDatabase } from "../database/database.js";
|
|
13
|
-
import { Claimed, CycleBlocked, Ignored, Queued, Submission, SubmissionResult, } from "../engine/submission.js";
|
|
14
|
-
import { RunOperationRejectedError } from "../engine/error.js";
|
|
17
|
+
import { Claimed, CycleBlocked, Ignored, Queued, reopeningOf, runIdFor, Submission, SubmissionResult, Suppressed, } from "../engine/submission.js";
|
|
18
|
+
import { RunNotFoundError, RunOperationRejectedError, RunRetryBlockedError, } from "../engine/error.js";
|
|
15
19
|
import { RunSnapshot } from "../run-reader/model.js";
|
|
20
|
+
import { runAgainSubmission, } from "../engine/run-again.js";
|
|
21
|
+
import { plainRetry } from "../engine/retry.js";
|
|
16
22
|
import { StepExecution } from "../step-executor.js";
|
|
17
23
|
import { TranscriptEntry, TranscriptQuery } from "../transcript.js";
|
|
24
|
+
import { attemptWindowQueries } from "./attempt-windows.js";
|
|
25
|
+
import { leftoverQueries } from "./leftovers.js";
|
|
18
26
|
import { persistence, submissionFromRow } from "./codec.js";
|
|
19
27
|
import { queriesFor } from "./queries.js";
|
|
20
|
-
import { beginAttempt as decideBeginAttempt, cancelRun, claimRun, finishAttempt as decideFinishAttempt,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
runId: RunId,
|
|
24
|
-
}) {
|
|
25
|
-
}
|
|
26
|
-
export class RecoverableRun extends Schema.Class("@usine/engine/internal/RecoverableRun")({
|
|
27
|
-
submission: Submission,
|
|
28
|
-
runId: RunId,
|
|
29
|
-
state: RunState.RunState,
|
|
30
|
-
}) {
|
|
31
|
-
}
|
|
28
|
+
import { beginAttempt as decideBeginAttempt, cameBack, cancelRun, claimRun, finishAttempt as decideFinishAttempt, operatorStop, resightEvent, retryRun, suppressionDecision, } from "../engine/lifecycle.js";
|
|
29
|
+
const OptionalRunId = Schema.Option(RunId).pipe(Schema.withConstructorDefault(Effect.succeed(Option.none())));
|
|
30
|
+
/** The result of recording an Event, with the Run it opened and the unstarted Run it superseded. */
|
|
32
31
|
export class Claim extends Schema.Class("@usine/engine/internal/Claim")({
|
|
33
|
-
submission: Submission,
|
|
34
32
|
result: SubmissionResult,
|
|
35
|
-
|
|
33
|
+
/** The Run that this claim opened, not yet driven; `None` for a joined Run. */
|
|
34
|
+
openedRun: OptionalRunId,
|
|
35
|
+
supersededRun: OptionalRunId,
|
|
36
36
|
}) {
|
|
37
37
|
}
|
|
38
38
|
export class AttemptStarted extends Schema.TaggedClass("@usine/engine/internal/AttemptStarted")("AttemptStarted", {
|
|
39
39
|
execution: StepExecution,
|
|
40
40
|
}) {
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}) {
|
|
42
|
+
/** A Run that failed because failing the interrupted Attempt used its last automatic retry. */
|
|
43
|
+
export class AttemptRecoveryFailed extends Schema.TaggedClass("@usine/engine/internal/AttemptRecoveryFailed")("AttemptRecoveryFailed", {}) {
|
|
45
44
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
* worker slot. Routine once capacity queues exist, never an error.
|
|
49
|
-
*/
|
|
50
|
-
export class RunClosed extends Schema.TaggedClass("@usine/engine/internal/RunClosed")("RunClosed", {
|
|
51
|
-
state: Schema.String,
|
|
52
|
-
}) {
|
|
45
|
+
/** A Run that closed (canceled, superseded, or revoked) while waiting for a Worker slot; not an error. */
|
|
46
|
+
export class RunClosed extends Schema.TaggedClass("@usine/engine/internal/RunClosed")("RunClosed", {}) {
|
|
53
47
|
}
|
|
54
48
|
const makeRunStore = Effect.gen(function* () {
|
|
55
49
|
const database = yield* RunDatabase;
|
|
50
|
+
const crypto = yield* Crypto.Crypto;
|
|
56
51
|
const queries = queriesFor(database);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
result: new Claimed({ eventId: submission.event.id, runId }),
|
|
60
|
-
supersededRun,
|
|
61
|
-
});
|
|
62
|
-
const openRun = Effect.fn("RunStore.openRun")(function* (tx, submission, runId, supersededRun = Option.none()) {
|
|
52
|
+
/** Opens the Submission's Run as `runId` unless the Item's cycle budget is used up. */
|
|
53
|
+
const admitRun = Effect.fn("RunStore.admitRun")(function* (tx, submission, runId) {
|
|
63
54
|
const blocked = yield* tx.cycles.admit(submission);
|
|
64
55
|
if (Option.isSome(blocked))
|
|
65
|
-
return new
|
|
66
|
-
submission,
|
|
67
|
-
supersededRun,
|
|
68
|
-
result: new CycleBlocked({ eventId: submission.event.id, ask: blocked.value }),
|
|
69
|
-
});
|
|
56
|
+
return new CycleBlocked({ eventId: submission.event.id, ask: blocked.value });
|
|
70
57
|
yield* tx.createRun(submission, submission.event.id, runId);
|
|
71
58
|
yield* tx.cycles.reserve(submission, runId);
|
|
72
|
-
return
|
|
59
|
+
return new Claimed({ eventId: submission.event.id, runId });
|
|
60
|
+
});
|
|
61
|
+
const openRun = Effect.fn("RunStore.openRun")(function* (tx, submission, runId, supersededRun = Option.none()) {
|
|
62
|
+
const result = yield* admitRun(tx, submission, runId);
|
|
63
|
+
return new Claim({
|
|
64
|
+
result,
|
|
65
|
+
openedRun: result._tag === "Claimed" ? Option.some(runId) : Option.none(),
|
|
66
|
+
supersededRun,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
const runIdOf = (submission) => runIdFor(submission).pipe(Effect.provideService(Crypto.Crypto, crypto));
|
|
70
|
+
/** Returns the Event's Run or ignore reason, or `None` if the Event isn't claimed yet. */
|
|
71
|
+
const settlementOf = Effect.fn("RunStore.settlementOf")(function* (tx, eventId) {
|
|
72
|
+
const owner = (yield* tx.runForEvent(eventId))[0];
|
|
73
|
+
if (owner !== undefined) {
|
|
74
|
+
const run = (yield* tx.snapshot(owner.runId)).run;
|
|
75
|
+
return Option.some({ _tag: "Owned", run });
|
|
76
|
+
}
|
|
77
|
+
const ignored = (yield* tx.ignoredEvent(eventId))[0];
|
|
78
|
+
return ignored === undefined
|
|
79
|
+
? Option.none()
|
|
80
|
+
: Option.some({ _tag: "Ignored", reason: ignored.reason });
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Claims a recorded Event that has no Run and wasn't ignored: keeps it waiting during a
|
|
84
|
+
* suppression, queues it, or opens its Run as `runId`.
|
|
85
|
+
*/
|
|
86
|
+
const claimEvent = Effect.fn("RunStore.claimEvent")(function* (tx, recorded, runId) {
|
|
87
|
+
if (!(yield* tx.cycles.isCurrentEvent(recorded))) {
|
|
88
|
+
return new Claim({
|
|
89
|
+
result: new Ignored({ eventId: recorded.event.id, reason: "superseded" }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const suppression = yield* tx.loopItems.suppression(recorded.loop, recorded.itemKey);
|
|
93
|
+
if (Option.isSome(suppression)) {
|
|
94
|
+
const decision = suppressionDecision(suppression.value, recorded.event.id);
|
|
95
|
+
const fields = {
|
|
96
|
+
eventId: recorded.event.id,
|
|
97
|
+
suppressedBy: suppression.value.runId,
|
|
98
|
+
loop: recorded.loop,
|
|
99
|
+
itemKey: recorded.itemKey,
|
|
100
|
+
};
|
|
101
|
+
switch (decision._tag) {
|
|
102
|
+
case "SuppressFrom":
|
|
103
|
+
yield* tx.loopItems.suppressFrom(recorded.loop, recorded.itemKey, recorded.event.id);
|
|
104
|
+
yield* Effect.logInfo("First Item state after the Agent stopped for the operator; it waits for the operator", fields);
|
|
105
|
+
return new Claim({
|
|
106
|
+
result: new Suppressed({ eventId: recorded.event.id, runId: suppression.value.runId }),
|
|
107
|
+
});
|
|
108
|
+
case "Suppress":
|
|
109
|
+
yield* Effect.logInfo("Item unchanged since the Agent stopped for the operator; no Run opens", fields);
|
|
110
|
+
return new Claim({
|
|
111
|
+
result: new Suppressed({ eventId: recorded.event.id, runId: suppression.value.runId }),
|
|
112
|
+
});
|
|
113
|
+
case "Lift":
|
|
114
|
+
yield* tx.loopItems.lift(recorded.loop, recorded.itemKey);
|
|
115
|
+
yield* Effect.logInfo("Item changed after the Agent stopped for the operator; the Loop acts on its new state", fields);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const blocking = yield* tx.blockingRun(recorded.loop, recorded.itemKey);
|
|
119
|
+
const blockedBy = blocking[0];
|
|
120
|
+
const decision = claimRun(blockedBy === undefined
|
|
121
|
+
? Option.none()
|
|
122
|
+
: Option.some((yield* tx.snapshot(blockedBy.runId)).run), recorded.event.id);
|
|
123
|
+
yield* Effect.logInfo("Event claim decided", {
|
|
124
|
+
eventId: recorded.event.id,
|
|
125
|
+
decision: decision._tag,
|
|
126
|
+
});
|
|
127
|
+
switch (decision._tag) {
|
|
128
|
+
case "QueueEvent":
|
|
129
|
+
return new Claim({ result: new Queued({ eventId: recorded.event.id }) });
|
|
130
|
+
case "SupersedeRun":
|
|
131
|
+
yield* tx.updateState(decision.runId, decision.state);
|
|
132
|
+
return yield* openRun(tx, recorded, runId, Option.some(decision.runId));
|
|
133
|
+
case "OpenRun":
|
|
134
|
+
return yield* openRun(tx, recorded, runId);
|
|
135
|
+
}
|
|
73
136
|
});
|
|
74
137
|
const recordAndClaim = Effect.fn("RunStore.recordAndClaim")(function* (submission, requestedRunId) {
|
|
75
138
|
yield* Effect.annotateCurrentSpan({
|
|
@@ -81,72 +144,57 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
81
144
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
82
145
|
const tx = queriesFor(transaction);
|
|
83
146
|
const recorded = yield* tx.recordTriggerEvent(submission);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
147
|
+
const previous = yield* tx.loopItems.sighted(recorded.loop, recorded.itemKey, recorded.event.id);
|
|
148
|
+
// After a reopening, later sightings of the trigger's Event join the newest reopening.
|
|
149
|
+
const current = Option.getOrElse(yield* tx.newestReopening(recorded.event.id), () => ({
|
|
150
|
+
submission: recorded,
|
|
151
|
+
reopening: 0,
|
|
152
|
+
}));
|
|
153
|
+
const eventId = current.submission.event.id;
|
|
154
|
+
const settled = yield* settlementOf(tx, eventId);
|
|
155
|
+
if (Option.isNone(settled)) {
|
|
156
|
+
return yield* claimEvent(tx, current.submission, current.reopening === 0 ? requestedRunId : yield* runIdOf(current.submission));
|
|
91
157
|
}
|
|
92
|
-
const
|
|
93
|
-
if (ignored[0] !== undefined) {
|
|
94
|
-
yield* Effect.logInfo("Event remains ignored", {
|
|
95
|
-
eventId: recorded.event.id,
|
|
96
|
-
reason: ignored[0].reason,
|
|
97
|
-
});
|
|
98
|
-
return new Claim({
|
|
99
|
-
submission: recorded,
|
|
100
|
-
result: new Ignored({
|
|
101
|
-
eventId: recorded.event.id,
|
|
102
|
-
reason: ignored[0].reason,
|
|
103
|
-
}),
|
|
104
|
-
supersededRun: Option.none(),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
if (!(yield* tx.cycles.isCurrentEvent(recorded))) {
|
|
108
|
-
return new Claim({
|
|
109
|
-
submission: recorded,
|
|
110
|
-
supersededRun: Option.none(),
|
|
111
|
-
result: new Ignored({ eventId: recorded.event.id, reason: "superseded" }),
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
const blocking = yield* tx.blockingRun(recorded.loop, recorded.itemKey);
|
|
115
|
-
const blockedBy = blocking[0];
|
|
116
|
-
const decision = claimRun(blockedBy === undefined
|
|
117
|
-
? Option.none()
|
|
118
|
-
: Option.some((yield* tx.snapshot(blockedBy.runId)).run), recorded.event.id);
|
|
119
|
-
yield* Effect.logInfo("Event claim decided", {
|
|
120
|
-
eventId: recorded.event.id,
|
|
121
|
-
decision: decision._tag,
|
|
122
|
-
});
|
|
158
|
+
const decision = resightEvent(settled.value, cameBack(previous, recorded.event.id));
|
|
123
159
|
switch (decision._tag) {
|
|
124
|
-
case "
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
case "JoinRun":
|
|
161
|
+
yield* Effect.logInfo("Event joined its existing Run", {
|
|
162
|
+
eventId,
|
|
163
|
+
runId: decision.run.id,
|
|
164
|
+
state: decision.run.state._tag,
|
|
129
165
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
yield*
|
|
133
|
-
return
|
|
134
|
-
case "
|
|
135
|
-
|
|
166
|
+
return new Claim({ result: new Claimed({ eventId, runId: decision.run.id }) });
|
|
167
|
+
case "StayIgnored":
|
|
168
|
+
yield* Effect.logInfo("Event remains ignored", { eventId, reason: decision.reason });
|
|
169
|
+
return new Claim({ result: new Ignored({ eventId, reason: decision.reason }) });
|
|
170
|
+
case "ReopenEvent": {
|
|
171
|
+
const reopening = current.reopening + 1;
|
|
172
|
+
const reopened = yield* tx.recordTriggerEvent(reopeningOf(submission, recorded.event.dedupKey, reopening), Option.some({ eventId: recorded.event.id, reopening }));
|
|
173
|
+
yield* Effect.logInfo("The Item came back to the state of an Event no Attempt worked on; it opens again", {
|
|
174
|
+
eventId,
|
|
175
|
+
settled: settled.value._tag,
|
|
176
|
+
reopenedEventId: reopened.event.id,
|
|
177
|
+
reopening,
|
|
178
|
+
});
|
|
179
|
+
return yield* claimEvent(tx, reopened, yield* runIdOf(reopened));
|
|
180
|
+
}
|
|
136
181
|
}
|
|
137
182
|
}));
|
|
138
|
-
}, persistence("submit
|
|
183
|
+
}, persistence("submit event"));
|
|
139
184
|
const parkForApproval = Effect.fn("RunStore.parkForApproval")(function* (runId) {
|
|
140
|
-
yield*
|
|
185
|
+
yield* Effect.annotateCurrentSpan({ runId });
|
|
186
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
141
187
|
const tx = queriesFor(transaction);
|
|
142
188
|
const current = yield* tx.snapshot(runId);
|
|
143
|
-
if (current.run.state._tag
|
|
144
|
-
|
|
145
|
-
|
|
189
|
+
if (current.run.state._tag !== "Pending")
|
|
190
|
+
return current.run.state;
|
|
191
|
+
const waiting = RunState.awaitApproval(current.run.state);
|
|
192
|
+
yield* tx.updateState(runId, waiting);
|
|
193
|
+
return waiting;
|
|
146
194
|
}));
|
|
147
195
|
}, persistence("park run for approval"));
|
|
148
|
-
//
|
|
149
|
-
//
|
|
196
|
+
// Runs in the caller's transaction, so Attempt, Run, Step, and outbox writes commit or roll
|
|
197
|
+
// back together (ADR 0010, ADR 0020).
|
|
150
198
|
const applyFinish = Effect.fn("RunStore.applyFinish")(function* (tx, runId, decision) {
|
|
151
199
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
152
200
|
if (decision._tag === "Unchanged") {
|
|
@@ -165,17 +213,52 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
165
213
|
Failed: ({ failure }) => ({ ...fields, failure: failure._tag }),
|
|
166
214
|
Succeeded: () => fields,
|
|
167
215
|
}));
|
|
168
|
-
yield*
|
|
216
|
+
const endedAt = yield* DateTime.now;
|
|
217
|
+
yield* tx.closeAttempt(attempt.id, outcome, endedAt);
|
|
169
218
|
if (decision._tag === "RetryAttempt")
|
|
170
219
|
return;
|
|
171
220
|
if (decision._tag === "RunSucceeded") {
|
|
172
221
|
yield* tx.updateStepResult(runId, attempt.stepName, decision.result);
|
|
173
222
|
}
|
|
174
|
-
yield* tx.updateState(runId, decision.state);
|
|
175
|
-
yield* tx.requestWorkflowCommand(runId, "Continue");
|
|
223
|
+
yield* tx.updateState(runId, decision.state, endedAt);
|
|
176
224
|
});
|
|
177
|
-
|
|
178
|
-
|
|
225
|
+
// The Event queued during the Attempt is usually the Agent's own question or blocker comment,
|
|
226
|
+
// so it becomes the suppressed state.
|
|
227
|
+
const suppressForOperator = Effect.fn("RunStore.suppressForOperator")(function* (tx, current, stop) {
|
|
228
|
+
const runId = current.run.id;
|
|
229
|
+
const queued = Option.map(yield* tx.nextQueued(runId), ({ submission }) => submission.event.id);
|
|
230
|
+
const fields = { runId, loop: current.run.loop, itemKey: current.itemKey, stop };
|
|
231
|
+
if (yield* tx.loopItems.startSuppression(current.run.loop, current.itemKey, runId, current.event.id, queued)) {
|
|
232
|
+
yield* Effect.logInfo("Agent stopped for the operator; its Loop suppresses the Item", {
|
|
233
|
+
...fields,
|
|
234
|
+
suppressedEventId: Option.getOrElse(queued, () => "none yet"),
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
yield* Effect.logInfo("Agent stopped for the operator, but nothing is suppressed; the Loop last saw the Item not matching", fields);
|
|
239
|
+
});
|
|
240
|
+
const executionOf = (current, { attempt, state, resume, reattach, }) => new StepExecution({
|
|
241
|
+
attempt,
|
|
242
|
+
event: current.event,
|
|
243
|
+
run: new Run({
|
|
244
|
+
id: current.run.id,
|
|
245
|
+
eventId: current.run.eventId,
|
|
246
|
+
loop: current.run.loop,
|
|
247
|
+
state,
|
|
248
|
+
openedAt: current.run.openedAt,
|
|
249
|
+
}),
|
|
250
|
+
step: current.step,
|
|
251
|
+
repository: current.repository,
|
|
252
|
+
revision: current.revision,
|
|
253
|
+
task: current.task,
|
|
254
|
+
agent: current.agent,
|
|
255
|
+
parentModel: current.parentModel,
|
|
256
|
+
permissions: current.permissions,
|
|
257
|
+
resume,
|
|
258
|
+
reattach,
|
|
259
|
+
});
|
|
260
|
+
const beginAttempt = Effect.fn("RunStore.beginAttempt")(function* (runId, worker, live) {
|
|
261
|
+
yield* Effect.annotateCurrentSpan({ runId, worker });
|
|
179
262
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
180
263
|
const tx = queriesFor(transaction);
|
|
181
264
|
const current = yield* tx.snapshot(runId);
|
|
@@ -183,14 +266,48 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
183
266
|
message: "The orchestrator stopped during the attempt",
|
|
184
267
|
cause: new Error("attempt interrupted by orchestrator restart"),
|
|
185
268
|
});
|
|
186
|
-
const
|
|
269
|
+
const requested = yield* tx.nextStart(runId);
|
|
270
|
+
const decision = decideBeginAttempt(current, interruption, live, requested);
|
|
187
271
|
if (decision._tag === "RunClosed") {
|
|
188
272
|
yield* Effect.logInfo("Attempt start skipped", { runId, state: decision.state._tag });
|
|
189
|
-
return new RunClosed(
|
|
273
|
+
return new RunClosed();
|
|
190
274
|
}
|
|
191
275
|
if (decision._tag === "RunFailed") {
|
|
192
276
|
yield* applyFinish(tx, runId, decision);
|
|
193
|
-
return new AttemptRecoveryFailed(
|
|
277
|
+
return new AttemptRecoveryFailed();
|
|
278
|
+
}
|
|
279
|
+
if (decision._tag === "ReattachAttempt") {
|
|
280
|
+
const reattachedAt = yield* DateTime.now;
|
|
281
|
+
const attempt = new Attempt({
|
|
282
|
+
...decision.attempt,
|
|
283
|
+
reattachedAt: Option.some(reattachedAt),
|
|
284
|
+
});
|
|
285
|
+
yield* tx.recordReattach(attempt.id, reattachedAt);
|
|
286
|
+
yield* Effect.annotateCurrentSpan({ attemptId: attempt.id });
|
|
287
|
+
yield* Effect.logInfo("Attempt reattached after an orchestrator restart", {
|
|
288
|
+
runId,
|
|
289
|
+
attemptId: attempt.id,
|
|
290
|
+
number: attempt.number,
|
|
291
|
+
worker: attempt.worker,
|
|
292
|
+
});
|
|
293
|
+
if (attempt.worker !== worker) {
|
|
294
|
+
// The Run reserves a slot on the Attempt's own Worker (#177), so a lease on another
|
|
295
|
+
// Worker is a `StepExecutor` defect.
|
|
296
|
+
yield* Effect.logError("Reattached Attempt keeps its own Worker, not the reserved one", {
|
|
297
|
+
runId,
|
|
298
|
+
attemptId: attempt.id,
|
|
299
|
+
worker: attempt.worker,
|
|
300
|
+
reservedWorker: worker,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return new AttemptStarted({
|
|
304
|
+
execution: executionOf(current, {
|
|
305
|
+
attempt,
|
|
306
|
+
state: decision.state,
|
|
307
|
+
resume: decision.resume,
|
|
308
|
+
reattach: true,
|
|
309
|
+
}),
|
|
310
|
+
});
|
|
194
311
|
}
|
|
195
312
|
if (Option.isSome(decision.interrupted)) {
|
|
196
313
|
yield* applyFinish(tx, runId, decision.interrupted.value);
|
|
@@ -213,35 +330,68 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
213
330
|
parentModel: current.parentModel,
|
|
214
331
|
startedAt,
|
|
215
332
|
outcome: Option.none(),
|
|
333
|
+
endedAt: Option.none(),
|
|
334
|
+
salvage: Option.none(),
|
|
335
|
+
start: Option.some(decision.start),
|
|
336
|
+
reattachedAt: Option.none(),
|
|
216
337
|
});
|
|
217
338
|
yield* tx.addAttempt(attempt);
|
|
339
|
+
if (Option.isSome(requested))
|
|
340
|
+
yield* tx.setNextStart(runId, Option.none());
|
|
218
341
|
yield* Effect.logInfo("Attempt started", {
|
|
219
342
|
runId,
|
|
220
343
|
attemptId,
|
|
221
344
|
number: decision.number,
|
|
222
345
|
worker,
|
|
346
|
+
start: decision.start._tag,
|
|
347
|
+
resumesFrom: Option.map(decision.resume, ({ ref }) => ref),
|
|
223
348
|
});
|
|
224
349
|
return new AttemptStarted({
|
|
225
|
-
execution:
|
|
350
|
+
execution: executionOf(current, {
|
|
226
351
|
attempt,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
eventId: current.run.eventId,
|
|
231
|
-
loop: current.run.loop,
|
|
232
|
-
state: decision.state,
|
|
233
|
-
openedAt: current.run.openedAt,
|
|
234
|
-
}),
|
|
235
|
-
step: current.step,
|
|
236
|
-
repository: current.repository,
|
|
237
|
-
revision: current.revision,
|
|
238
|
-
task: current.task,
|
|
239
|
-
agent: current.agent,
|
|
240
|
-
parentModel: current.parentModel,
|
|
352
|
+
state: decision.state,
|
|
353
|
+
resume: decision.resume,
|
|
354
|
+
reattach: false,
|
|
241
355
|
}),
|
|
242
356
|
});
|
|
243
357
|
}));
|
|
244
358
|
}, persistence("begin attempt"));
|
|
359
|
+
const recordSalvage = Effect.fn("RunStore.recordSalvage")(function* (attemptId, salvage) {
|
|
360
|
+
yield* Effect.annotateCurrentSpan({ attemptId, salvage: salvage._tag });
|
|
361
|
+
yield* database.transaction((transaction) => queriesFor(transaction).recordSalvage(attemptId, salvage));
|
|
362
|
+
yield* Effect.logInfo("Attempt salvage recorded", Match.valueTags(salvage, {
|
|
363
|
+
Pushed: ({ ref, commit }) => ({ attemptId, ref, commit }),
|
|
364
|
+
Retained: ({ path, commit }) => ({ attemptId, path, commit }),
|
|
365
|
+
}));
|
|
366
|
+
}, persistence("record attempt salvage"));
|
|
367
|
+
const recordComments = Effect.fn("RunStore.recordComments")(function* (attemptId, comments) {
|
|
368
|
+
yield* Effect.annotateCurrentSpan({ attemptId, comments: comments.length });
|
|
369
|
+
yield* database.transaction((transaction) => queriesFor(transaction).recordComments(attemptId, comments));
|
|
370
|
+
yield* Effect.logInfo("Recorded the comments an Attempt posted before it ended without success", {
|
|
371
|
+
attemptId,
|
|
372
|
+
comments: comments.map(({ kind, url }) => `${kind} ${url.href}`),
|
|
373
|
+
});
|
|
374
|
+
}, persistence("record attempt comments"));
|
|
375
|
+
const attemptWindows = Effect.fn("RunStore.attemptWindows")(function* (attemptId) {
|
|
376
|
+
yield* Effect.annotateCurrentSpan({ attemptId });
|
|
377
|
+
const windows = yield* attemptWindowQueries(database).attemptWindows(attemptId);
|
|
378
|
+
yield* Effect.logDebug("Read the Attempt's window and the Attempts of the same Agent that overlap it on its Item", {
|
|
379
|
+
attemptId,
|
|
380
|
+
recorded: Option.isSome(windows.own),
|
|
381
|
+
ended: Option.exists(windows.own, ({ endedAt }) => Option.isSome(endedAt)),
|
|
382
|
+
overlapping: windows.overlapping.map((window) => window.attemptId),
|
|
383
|
+
});
|
|
384
|
+
return windows;
|
|
385
|
+
}, persistence("read attempt windows"));
|
|
386
|
+
const discardLeftovers = Effect.fn("RunStore.discardLeftovers")(function* (worker) {
|
|
387
|
+
yield* Effect.annotateCurrentSpan({ worker });
|
|
388
|
+
const discardedAt = yield* DateTime.now;
|
|
389
|
+
return yield* database.transaction((transaction) => leftoverQueries(transaction).discardLeftovers(worker, discardedAt));
|
|
390
|
+
}, persistence("discard attempt leftovers"));
|
|
391
|
+
const openAttempts = Effect.fn("RunStore.openAttempts")(function* (worker) {
|
|
392
|
+
yield* Effect.annotateCurrentSpan({ worker });
|
|
393
|
+
return yield* leftoverQueries(database).openAttempts(worker);
|
|
394
|
+
}, persistence("count open attempts"));
|
|
245
395
|
const finishAttempt = Effect.fn("RunStore.finishAttempt")(function* (attempt, result) {
|
|
246
396
|
yield* Effect.annotateCurrentSpan({ runId: attempt.runId, attemptId: attempt.id });
|
|
247
397
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -249,14 +399,52 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
249
399
|
const current = yield* tx.snapshot(attempt.runId);
|
|
250
400
|
const decision = decideFinishAttempt(current, attempt.id, result);
|
|
251
401
|
yield* applyFinish(tx, attempt.runId, decision);
|
|
402
|
+
const stop = operatorStop(decision);
|
|
403
|
+
if (Option.isSome(stop))
|
|
404
|
+
yield* suppressForOperator(tx, current, stop.value);
|
|
252
405
|
return decision;
|
|
253
406
|
}));
|
|
254
407
|
}, persistence("finish attempt"));
|
|
255
|
-
const
|
|
256
|
-
yield*
|
|
408
|
+
const resolveFailure = Effect.fn("RunStore.resolveFailure")(function* (runId, attemptId) {
|
|
409
|
+
yield* Effect.annotateCurrentSpan({ runId, attemptId });
|
|
410
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
411
|
+
const tx = queriesFor(transaction);
|
|
412
|
+
const current = yield* tx.snapshot(runId);
|
|
413
|
+
const latest = current.attempts.at(-1);
|
|
414
|
+
if (!RunState.operatorActions(current.run.state).canResolveFailure ||
|
|
415
|
+
latest?.id !== attemptId) {
|
|
416
|
+
yield* Effect.logInfo("Failure resolution rejected; Run or Attempt changed", {
|
|
417
|
+
runId,
|
|
418
|
+
attemptId,
|
|
419
|
+
state: current.run.state._tag,
|
|
420
|
+
latestAttemptId: latest?.id,
|
|
421
|
+
});
|
|
422
|
+
return yield* new RunOperationRejectedError({
|
|
423
|
+
runId,
|
|
424
|
+
operation: "resolveFailure",
|
|
425
|
+
state: current.run.state._tag,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (Option.contains(current.resolvedFailureAttemptId, attemptId)) {
|
|
429
|
+
yield* Effect.logInfo("Run failure already resolved", { runId, attemptId });
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
yield* tx.resolveFailure(runId, attemptId);
|
|
433
|
+
const lifted = yield* tx.loopItems.liftRun(runId);
|
|
434
|
+
yield* Effect.logInfo("Run failure resolved", {
|
|
435
|
+
runId,
|
|
436
|
+
attemptId,
|
|
437
|
+
liftedSuppression: lifted,
|
|
438
|
+
});
|
|
439
|
+
return lifted;
|
|
440
|
+
}));
|
|
441
|
+
}, persistence("resolve run failure"));
|
|
442
|
+
const approve = Effect.fn("RunStore.approve")(function* (runId, approval, source) {
|
|
443
|
+
yield* Effect.annotateCurrentSpan({ runId, eventId: approval.id, channel: source.channel });
|
|
444
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
257
445
|
const tx = queriesFor(transaction);
|
|
258
446
|
if (yield* tx.approvalExists(runId))
|
|
259
|
-
return;
|
|
447
|
+
return false;
|
|
260
448
|
const current = yield* tx.snapshot(runId);
|
|
261
449
|
if (current.run.state._tag !== "Waiting") {
|
|
262
450
|
return yield* new RunOperationRejectedError({
|
|
@@ -265,23 +453,40 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
265
453
|
state: current.run.state._tag,
|
|
266
454
|
});
|
|
267
455
|
}
|
|
268
|
-
yield* tx.recordApproval(runId, approval);
|
|
456
|
+
yield* tx.recordApproval(runId, approval, source);
|
|
269
457
|
yield* tx.updateState(runId, RunState.approve(current.run.state));
|
|
270
|
-
|
|
458
|
+
return true;
|
|
271
459
|
}));
|
|
272
460
|
}, persistence("approve run"));
|
|
273
|
-
const retry = Effect.fn("RunStore.retry")(function* (runId,
|
|
461
|
+
const retry = Effect.fn("RunStore.retry")(function* (runId, request = plainRetry) {
|
|
462
|
+
const { amendment, requestedBy } = request;
|
|
274
463
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
275
|
-
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
464
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
276
465
|
const tx = queriesFor(transaction);
|
|
277
466
|
const current = yield* tx.snapshot(runId);
|
|
278
|
-
const decision = retryRun(current
|
|
467
|
+
const decision = retryRun(current);
|
|
279
468
|
yield* Effect.logInfo("Operator retry decided", {
|
|
280
469
|
runId,
|
|
281
470
|
state: current.run.state._tag,
|
|
282
471
|
decision: decision._tag,
|
|
283
472
|
});
|
|
284
473
|
if (decision._tag === "Resume") {
|
|
474
|
+
// A later Event can have opened another Run of this Loop on the Item; reopening this one
|
|
475
|
+
// would run both.
|
|
476
|
+
const blocking = (yield* tx.blockingRun(current.run.loop, current.itemKey))[0];
|
|
477
|
+
if (blocking !== undefined) {
|
|
478
|
+
yield* Effect.logInfo("Operator retry rejected; another Run is open on the Item", {
|
|
479
|
+
runId,
|
|
480
|
+
openRunId: blocking.runId,
|
|
481
|
+
loop: current.run.loop,
|
|
482
|
+
itemKey: current.itemKey,
|
|
483
|
+
});
|
|
484
|
+
return yield* new RunRetryBlockedError({
|
|
485
|
+
runId,
|
|
486
|
+
openRunId: blocking.runId,
|
|
487
|
+
message: `Cannot retry run ${runId}: run ${blocking.runId} of loop "${current.run.loop}" is open on the same item. Cancel run ${blocking.runId}, or try again after it closes.`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
285
490
|
if (Option.isSome(amendment)) {
|
|
286
491
|
yield* tx.amend(runId, amendment.value);
|
|
287
492
|
yield* Effect.logInfo("Run effective task and Agent amended", {
|
|
@@ -289,12 +494,18 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
289
494
|
agent: amendment.value.agent.name,
|
|
290
495
|
});
|
|
291
496
|
}
|
|
497
|
+
// A retry lifts the suppression, so Events queue behind the running Run again.
|
|
498
|
+
if (yield* tx.loopItems.liftRun(runId)) {
|
|
499
|
+
yield* Effect.logInfo("Operator retry lifts the suppression of the Item", { runId });
|
|
500
|
+
}
|
|
501
|
+
yield* tx.setNextStart(runId, Option.some(Option.isSome(amendment)
|
|
502
|
+
? new StartedByEditAndRetry({ operator: requestedBy })
|
|
503
|
+
: new StartedByRetry({ operator: requestedBy })));
|
|
292
504
|
yield* tx.updateState(runId, decision.state);
|
|
293
|
-
|
|
294
|
-
return;
|
|
505
|
+
return true;
|
|
295
506
|
}
|
|
296
507
|
if (decision._tag === "ResumePending" && Option.isNone(amendment))
|
|
297
|
-
return;
|
|
508
|
+
return false;
|
|
298
509
|
return yield* new RunOperationRejectedError({
|
|
299
510
|
runId,
|
|
300
511
|
operation: "retry",
|
|
@@ -302,6 +513,83 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
302
513
|
});
|
|
303
514
|
}));
|
|
304
515
|
}, persistence("retry run"));
|
|
516
|
+
const runAgain = Effect.fn("RunStore.runAgain")(function* (runId, request) {
|
|
517
|
+
yield* Effect.annotateCurrentSpan({ runId, agent: request.context.agent.name });
|
|
518
|
+
const receivedAt = yield* DateTime.now;
|
|
519
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
520
|
+
const tx = queriesFor(transaction);
|
|
521
|
+
const current = yield* tx.snapshot(runId);
|
|
522
|
+
const { loop } = current.run;
|
|
523
|
+
const { itemKey } = current;
|
|
524
|
+
if (!RunState.operatorActions(current.run.state).canRunAgain) {
|
|
525
|
+
yield* Effect.logInfo("Run again rejected; only a succeeded or canceled Run runs again", {
|
|
526
|
+
runId,
|
|
527
|
+
state: current.run.state._tag,
|
|
528
|
+
});
|
|
529
|
+
return yield* new RunOperationRejectedError({
|
|
530
|
+
runId,
|
|
531
|
+
operation: "runAgain",
|
|
532
|
+
state: current.run.state._tag,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const blocking = (yield* tx.blockingRun(loop, itemKey))[0];
|
|
536
|
+
if (blocking !== undefined) {
|
|
537
|
+
yield* Effect.logInfo("Run again rejected; another Run is open on the Item", {
|
|
538
|
+
runId,
|
|
539
|
+
openRunId: blocking.runId,
|
|
540
|
+
loop,
|
|
541
|
+
itemKey,
|
|
542
|
+
});
|
|
543
|
+
return yield* new RunRetryBlockedError({
|
|
544
|
+
runId,
|
|
545
|
+
openRunId: blocking.runId,
|
|
546
|
+
message: `Run ${runId} cannot run again: run ${blocking.runId} of loop "${loop}" is open on the same item. Cancel run ${blocking.runId}, or try again after it closes.`,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const trigger = Option.getOrElse(yield* tx.reopenedEvent(current.event.id), () => ({
|
|
550
|
+
eventId: current.event.id,
|
|
551
|
+
dedupKey: current.event.dedupKey,
|
|
552
|
+
}));
|
|
553
|
+
const reopening = Option.match(yield* tx.newestReopening(trigger.eventId), {
|
|
554
|
+
onNone: () => 1,
|
|
555
|
+
onSome: (newest) => newest.reopening + 1,
|
|
556
|
+
});
|
|
557
|
+
const generation = Option.match(yield* tx.cycles.get(itemKey), {
|
|
558
|
+
onNone: () => CycleGeneration.make(0),
|
|
559
|
+
onSome: (budget) => budget.generation,
|
|
560
|
+
});
|
|
561
|
+
const again = runAgainSubmission(current, request, {
|
|
562
|
+
trigger: trigger.dedupKey,
|
|
563
|
+
reopening,
|
|
564
|
+
generation,
|
|
565
|
+
receivedAt,
|
|
566
|
+
});
|
|
567
|
+
// Not recorded as a sighting, so a later poll of the unchanged Item finds the new Run
|
|
568
|
+
// through the reopening.
|
|
569
|
+
const recorded = yield* tx.recordTriggerEvent(again, Option.some({ eventId: trigger.eventId, reopening }));
|
|
570
|
+
yield* tx.recordRunAgain(recorded.event.id, new RunAgain({ from: runId, requestedBy: request.requestedBy }));
|
|
571
|
+
const result = yield* admitRun(tx, recorded, yield* runIdOf(recorded));
|
|
572
|
+
yield* Effect.logInfo("Run again decided", Match.valueTags(result, {
|
|
573
|
+
Claimed: ({ runId: openedRunId }) => ({
|
|
574
|
+
runId,
|
|
575
|
+
eventId: recorded.event.id,
|
|
576
|
+
reopening,
|
|
577
|
+
decision: "OpenRun",
|
|
578
|
+
openedRunId,
|
|
579
|
+
requestedBy: Option.getOrElse(request.requestedBy, () => "none"),
|
|
580
|
+
}),
|
|
581
|
+
CycleBlocked: ({ ask }) => ({
|
|
582
|
+
runId,
|
|
583
|
+
eventId: recorded.event.id,
|
|
584
|
+
reopening,
|
|
585
|
+
decision: "CycleBlocked",
|
|
586
|
+
askId: ask.id,
|
|
587
|
+
requestedBy: Option.getOrElse(request.requestedBy, () => "none"),
|
|
588
|
+
}),
|
|
589
|
+
}));
|
|
590
|
+
return result;
|
|
591
|
+
}));
|
|
592
|
+
}, persistence("run again"));
|
|
305
593
|
const cancel = Effect.fn("RunStore.cancel")(function* (runId) {
|
|
306
594
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
307
595
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -315,12 +603,12 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
315
603
|
});
|
|
316
604
|
if (decision._tag === "AlreadyClosed")
|
|
317
605
|
return decision;
|
|
606
|
+
const canceledAt = yield* DateTime.now;
|
|
318
607
|
if (Option.isSome(decision.attempt)) {
|
|
319
|
-
|
|
608
|
+
const { id, outcome } = decision.attempt.value;
|
|
609
|
+
yield* tx.closeAttempt(id, outcome, canceledAt);
|
|
320
610
|
}
|
|
321
|
-
yield* tx.updateState(runId, decision.state);
|
|
322
|
-
yield* tx.requestInterrupt(runId);
|
|
323
|
-
yield* tx.requestWorkflowCommand(runId, "Continue");
|
|
611
|
+
yield* tx.updateState(runId, decision.state, canceledAt);
|
|
324
612
|
const canceled = {
|
|
325
613
|
_tag: "Canceled",
|
|
326
614
|
openAttempt: Option.map(decision.attempt, ({ id }) => id),
|
|
@@ -331,23 +619,34 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
331
619
|
const revoke = Effect.fn("RunStore.revoke")(function* (loop, itemKey) {
|
|
332
620
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
333
621
|
const tx = queriesFor(transaction);
|
|
622
|
+
yield* tx.loopItems.unmatched(loop, itemKey);
|
|
334
623
|
const storedRuns = yield* tx.revokeUnstarted(loop, itemKey);
|
|
335
624
|
for (const row of storedRuns) {
|
|
336
625
|
const current = yield* tx.snapshot(row.runId);
|
|
337
626
|
if (RunState.isUnstarted(current.run.state)) {
|
|
338
627
|
yield* tx.updateState(row.runId, RunState.revoke(current.run.state));
|
|
339
|
-
yield* tx.requestInterrupt(row.runId);
|
|
340
628
|
}
|
|
341
629
|
}
|
|
342
630
|
return storedRuns.map((row) => row.runId);
|
|
343
631
|
}));
|
|
344
632
|
}, persistence("revoke run"));
|
|
633
|
+
const revokeQueued = Effect.fn("RunStore.revokeQueued")(function* (queued) {
|
|
634
|
+
const { loop, itemKey, event } = queued.submission;
|
|
635
|
+
yield* Effect.annotateCurrentSpan({ loop, itemKey, eventId: event.id });
|
|
636
|
+
// Not recorded as a sighting, because the trigger can still submit this Event from a fetch
|
|
637
|
+
// made before the closing Run changed the Item.
|
|
638
|
+
return yield* queries.revokeQueued(loop, itemKey, queued.eventIds);
|
|
639
|
+
}, persistence("revoke queued events"));
|
|
345
640
|
const claimQueued = Effect.fn("RunStore.claimQueued")(function* (submission, runId) {
|
|
346
641
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
347
642
|
const tx = queriesFor(transaction);
|
|
348
643
|
const existing = yield* tx.runForEvent(submission.event.id);
|
|
349
644
|
if (existing[0] !== undefined) {
|
|
350
|
-
|
|
645
|
+
yield* Effect.logInfo("Queued Event already opened its Run", {
|
|
646
|
+
eventId: submission.event.id,
|
|
647
|
+
runId: existing[0].runId,
|
|
648
|
+
});
|
|
649
|
+
return Option.none();
|
|
351
650
|
}
|
|
352
651
|
if ((yield* tx.ignoredEvent(submission.event.id)).length > 0) {
|
|
353
652
|
return Option.none();
|
|
@@ -355,21 +654,14 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
355
654
|
if ((yield* tx.blockingRun(submission.loop, submission.itemKey)).length > 0) {
|
|
356
655
|
return Option.none();
|
|
357
656
|
}
|
|
657
|
+
if (Option.isSome(yield* tx.loopItems.suppression(submission.loop, submission.itemKey))) {
|
|
658
|
+
return Option.none();
|
|
659
|
+
}
|
|
358
660
|
if (!(yield* tx.cycles.isCurrentEvent(submission)))
|
|
359
661
|
return Option.none();
|
|
360
|
-
|
|
361
|
-
return claimed.result._tag === "Claimed"
|
|
362
|
-
? Option.some(new StoredSubmission({ submission, runId }))
|
|
363
|
-
: Option.none();
|
|
364
|
-
}));
|
|
365
|
-
}, persistence("claim queued Event"));
|
|
366
|
-
const recoverable = Effect.fn("RunStore.recoverable")(function* () {
|
|
367
|
-
return (yield* queries.recoverable()).map((row) => new RecoverableRun({
|
|
368
|
-
submission: submissionFromRow(row),
|
|
369
|
-
runId: row.runId,
|
|
370
|
-
state: row.state,
|
|
662
|
+
return (yield* openRun(tx, submission, runId)).openedRun;
|
|
371
663
|
}));
|
|
372
|
-
}, persistence("
|
|
664
|
+
}, persistence("claim queued event"));
|
|
373
665
|
const appendTranscript = Effect.fn("RunStore.appendTranscript")(function* (direction, envelope) {
|
|
374
666
|
const receivedAt = yield* DateTime.now;
|
|
375
667
|
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -380,65 +672,51 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
380
672
|
if (direction === "outbound" && envelope.payload._tag === "InterventionApplied") {
|
|
381
673
|
yield* tx.markIntervened(envelope.attemptId);
|
|
382
674
|
}
|
|
383
|
-
yield* tx.appendTranscript(direction, envelope, receivedAt)
|
|
675
|
+
if (!(yield* tx.appendTranscript(direction, envelope, receivedAt)))
|
|
676
|
+
yield* Effect.logDebug("Transcript envelope already recorded; keeping the first copy", {
|
|
677
|
+
attemptId: envelope.attemptId,
|
|
678
|
+
direction,
|
|
679
|
+
sequence: envelope.sequence,
|
|
680
|
+
});
|
|
384
681
|
}));
|
|
385
|
-
}, persistence("append
|
|
682
|
+
}, persistence("append attempt transcript"));
|
|
386
683
|
const transcript = Effect.fn("RunStore.transcript")(function* (query) {
|
|
387
684
|
return yield* queries.transcript(query);
|
|
388
|
-
}, persistence("read
|
|
389
|
-
const reconcileCompletion = Effect.fn("RunStore.reconcileCompletion")(function* (runId, result) {
|
|
390
|
-
yield* Effect.annotateCurrentSpan({ runId });
|
|
391
|
-
if (result._tag !== "Complete") {
|
|
392
|
-
yield* Effect.logDebug("Workflow reconciliation skipped", { runId, reason: "Suspended" });
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
396
|
-
const tx = queriesFor(transaction);
|
|
397
|
-
const current = yield* tx.snapshot(runId);
|
|
398
|
-
// Workflow defects and interruption do not become typed Attempt failures.
|
|
399
|
-
// Keep the projection available to the existing restart recovery path.
|
|
400
|
-
const completion = Exit.isSuccess(result.exit)
|
|
401
|
-
? Option.some(Result.succeed(result.exit.value))
|
|
402
|
-
: Option.map(Cause.findErrorOption(result.exit.cause), Result.fail);
|
|
403
|
-
if (Option.isNone(completion)) {
|
|
404
|
-
yield* Effect.logInfo("Workflow reconciliation skipped", {
|
|
405
|
-
runId,
|
|
406
|
-
reason: "NoTypedOutcome",
|
|
407
|
-
});
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
yield* applyFinish(tx, runId, reconcileAttempt(current, completion.value));
|
|
411
|
-
}));
|
|
412
|
-
}, persistence("reconcile workflow completion"));
|
|
685
|
+
}, persistence("read attempt transcript"));
|
|
413
686
|
return {
|
|
414
687
|
approve,
|
|
415
688
|
appendTranscript,
|
|
416
689
|
beginAttempt,
|
|
417
690
|
cancel,
|
|
418
691
|
claimQueued,
|
|
419
|
-
completeWorkflowCommand: (runId, command) => queries
|
|
420
|
-
.completeWorkflowCommand(runId, command)
|
|
421
|
-
.pipe(persistence("complete workflow command")),
|
|
422
692
|
finishAttempt,
|
|
423
|
-
nextQueued: (runId) => queries.nextQueued(runId).pipe(persistence("find queued
|
|
693
|
+
nextQueued: (runId) => queries.nextQueued(runId).pipe(persistence("find queued event")),
|
|
424
694
|
parkForApproval,
|
|
425
|
-
|
|
426
|
-
.
|
|
427
|
-
.pipe(persistence("list pending
|
|
695
|
+
pendingContinuations: queries
|
|
696
|
+
.pendingContinuations()
|
|
697
|
+
.pipe(persistence("list pending continuations")),
|
|
698
|
+
runIdsToDrive: queries.runIdsToDrive().pipe(persistence("list the runs to drive")),
|
|
428
699
|
recordAndClaim,
|
|
429
|
-
|
|
430
|
-
|
|
700
|
+
salvageCandidates: queries.salvageCandidates().pipe(persistence("list salvage candidates")),
|
|
701
|
+
salvageCandidate: (attemptId) => queries.salvageCandidate(attemptId).pipe(persistence("read a salvage candidate")),
|
|
702
|
+
recordSalvage,
|
|
703
|
+
recordComments,
|
|
704
|
+
attemptWindows,
|
|
705
|
+
discardLeftovers,
|
|
706
|
+
openAttempts,
|
|
707
|
+
submission: (runId) => queries.eventRouteByRun(runId).pipe(Effect.flatMap((routes) => routes[0] === undefined
|
|
708
|
+
? Effect.fail(new RunNotFoundError({ runId }))
|
|
709
|
+
: Effect.succeed(submissionFromRow(routes[0]))), persistence("read run submission")),
|
|
710
|
+
resolveFailure,
|
|
431
711
|
retry,
|
|
432
712
|
revoke,
|
|
713
|
+
revokeQueued,
|
|
714
|
+
runAgain,
|
|
433
715
|
snapshot: (runId) => queries.snapshot(runId).pipe(persistence("read run")),
|
|
434
716
|
transcript,
|
|
435
717
|
};
|
|
436
718
|
});
|
|
437
|
-
/**
|
|
438
|
-
* The durable Run projection store. Another backend implements this interface
|
|
439
|
-
* with its own `Layer.effect(RunStore, ...)`; methods speak domain vocabulary
|
|
440
|
-
* and fail only with `RunStoreError`.
|
|
441
|
-
*/
|
|
719
|
+
/** The durable Run store; its methods fail only with `RunStoreError`. */
|
|
442
720
|
export class RunStore extends Context.Service()("@usine/engine/internal/RunStore") {
|
|
443
721
|
static layer = Layer.effect(RunStore, makeRunStore);
|
|
444
722
|
}
|