@usine/engine 0.3.0 → 0.5.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/LICENSE +661 -21
- package/README.md +112 -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 +14 -8
- package/dist/cycle-budget/cycle-budget.d.ts.map +1 -1
- package/dist/cycle-budget/cycle-budget.js +51 -10
- package/dist/cycle-budget/cycle-budget.js.map +1 -1
- package/dist/cycle-budget/model.d.ts +6 -2
- package/dist/cycle-budget/model.d.ts.map +1 -1
- package/dist/cycle-budget/model.js +5 -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 +13 -10
- package/dist/database/local-sqlite.d.ts.map +1 -1
- package/dist/database/local-sqlite.js +22 -17
- package/dist/database/local-sqlite.js.map +1 -1
- package/dist/database/schema.d.ts +668 -36
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +123 -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 +79 -0
- package/dist/engine/checkout-recovery.d.ts.map +1 -0
- package/dist/engine/checkout-recovery.js +238 -0
- package/dist/engine/checkout-recovery.js.map +1 -0
- package/dist/engine/engine.d.ts +67 -26
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +136 -129
- 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 +83 -28
- package/dist/engine/lifecycle.d.ts.map +1 -1
- package/dist/engine/lifecycle.js +94 -45
- 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 +68 -0
- package/dist/engine/run-driver.d.ts.map +1 -0
- package/dist/engine/run-driver.js +448 -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 +43 -12
- package/dist/engine/submission.js.map +1 -1
- package/dist/engine/unconfirmed-stops.d.ts +27 -0
- package/dist/engine/unconfirmed-stops.d.ts.map +1 -0
- package/dist/engine/unconfirmed-stops.js +26 -0
- package/dist/engine/unconfirmed-stops.js.map +1 -0
- package/dist/item-projection/item-projection.d.ts +14 -4
- package/dist/item-projection/item-projection.d.ts.map +1 -1
- package/dist/item-projection/item-projection.js +60 -14
- 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 +25 -10
- package/dist/run-reader/run-reader.d.ts.map +1 -1
- package/dist/run-reader/run-reader.js +25 -7
- package/dist/run-reader/run-reader.js.map +1 -1
- package/dist/step-executor.d.ts +86 -13
- 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 +242 -43
- package/dist/store/codec.d.ts.map +1 -1
- package/dist/store/codec.js +61 -16
- 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 +385 -36
- package/dist/store/queries.d.ts.map +1 -1
- package/dist/store/queries.js +355 -107
- package/dist/store/queries.js.map +1 -1
- package/dist/store/store.d.ts +106 -54
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +497 -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/drizzle/20261024162954_cycle_ask_closures/migration.sql +10 -0
- package/drizzle/20261024162954_cycle_ask_closures/snapshot.json +1948 -0
- package/drizzle/20261027104206_attempt_out_of_memory/migration.sql +1 -0
- package/drizzle/20261027104206_attempt_out_of_memory/snapshot.json +1958 -0
- package/drizzle/20261027191944_failure_resolution/migration.sql +1 -0
- package/drizzle/20261027191944_failure_resolution/snapshot.json +1968 -0
- package/package.json +22 -12
- 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, FailureResolution, } 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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
});
|
|
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));
|
|
106
157
|
}
|
|
107
|
-
|
|
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,85 @@ 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
|
+
PartlyPushed: ({ ref, commit, path }) => ({ attemptId, ref, commit, path }),
|
|
365
|
+
Retained: ({ path, commit }) => ({ attemptId, path, commit }),
|
|
366
|
+
Rejected: ({ path, commit }) => ({ attemptId, path, commit }),
|
|
367
|
+
}));
|
|
368
|
+
}, persistence("record attempt salvage"));
|
|
369
|
+
const recordComments = Effect.fn("RunStore.recordComments")(function* (attemptId, comments) {
|
|
370
|
+
yield* Effect.annotateCurrentSpan({ attemptId, comments: comments.length });
|
|
371
|
+
yield* database.transaction((transaction) => queriesFor(transaction).recordComments(attemptId, comments));
|
|
372
|
+
yield* Effect.logInfo("Recorded the comments an Attempt posted before it ended without success", {
|
|
373
|
+
attemptId,
|
|
374
|
+
comments: comments.map(({ kind, url }) => `${kind} ${url.href}`),
|
|
375
|
+
});
|
|
376
|
+
}, persistence("record attempt comments"));
|
|
377
|
+
const recordOutOfMemory = Effect.fn("RunStore.recordOutOfMemory")(function* (attemptId, kills) {
|
|
378
|
+
yield* Effect.annotateCurrentSpan({
|
|
379
|
+
attemptId,
|
|
380
|
+
container: kills.container,
|
|
381
|
+
kills: kills.kills,
|
|
382
|
+
});
|
|
383
|
+
const attempt = yield* database.transaction((transaction) => queriesFor(transaction).recordOutOfMemory(attemptId, kills));
|
|
384
|
+
yield* Effect.logInfo("Recorded the processes the kernel killed in the Attempt's container", {
|
|
385
|
+
attemptId,
|
|
386
|
+
container: kills.container,
|
|
387
|
+
kills: kills.kills,
|
|
388
|
+
memoryMib: kills.memoryMib,
|
|
389
|
+
attemptKills: attempt.kills,
|
|
390
|
+
});
|
|
391
|
+
}, persistence("record attempt out-of-memory kills"));
|
|
392
|
+
const attemptWindows = Effect.fn("RunStore.attemptWindows")(function* (attemptId) {
|
|
393
|
+
yield* Effect.annotateCurrentSpan({ attemptId });
|
|
394
|
+
const windows = yield* attemptWindowQueries(database).attemptWindows(attemptId);
|
|
395
|
+
yield* Effect.logDebug("Read the Attempt's window and the Attempts of the same Agent that overlap it on its Item", {
|
|
396
|
+
attemptId,
|
|
397
|
+
recorded: Option.isSome(windows.own),
|
|
398
|
+
ended: Option.exists(windows.own, ({ endedAt }) => Option.isSome(endedAt)),
|
|
399
|
+
overlapping: windows.overlapping.map((window) => window.attemptId),
|
|
400
|
+
});
|
|
401
|
+
return windows;
|
|
402
|
+
}, persistence("read attempt windows"));
|
|
403
|
+
const discardLeftovers = Effect.fn("RunStore.discardLeftovers")(function* (worker) {
|
|
404
|
+
yield* Effect.annotateCurrentSpan({ worker });
|
|
405
|
+
const discardedAt = yield* DateTime.now;
|
|
406
|
+
return yield* database.transaction((transaction) => leftoverQueries(transaction).discardLeftovers(worker, discardedAt));
|
|
407
|
+
}, persistence("discard attempt leftovers"));
|
|
408
|
+
const openAttempts = Effect.fn("RunStore.openAttempts")(function* (worker) {
|
|
409
|
+
yield* Effect.annotateCurrentSpan({ worker });
|
|
410
|
+
return yield* leftoverQueries(database).openAttempts(worker);
|
|
411
|
+
}, persistence("count open attempts"));
|
|
245
412
|
const finishAttempt = Effect.fn("RunStore.finishAttempt")(function* (attempt, result) {
|
|
246
413
|
yield* Effect.annotateCurrentSpan({ runId: attempt.runId, attemptId: attempt.id });
|
|
247
414
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -249,14 +416,55 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
249
416
|
const current = yield* tx.snapshot(attempt.runId);
|
|
250
417
|
const decision = decideFinishAttempt(current, attempt.id, result);
|
|
251
418
|
yield* applyFinish(tx, attempt.runId, decision);
|
|
419
|
+
const stop = operatorStop(decision);
|
|
420
|
+
if (Option.isSome(stop))
|
|
421
|
+
yield* suppressForOperator(tx, current, stop.value);
|
|
252
422
|
return decision;
|
|
253
423
|
}));
|
|
254
424
|
}, persistence("finish attempt"));
|
|
255
|
-
const
|
|
256
|
-
yield*
|
|
425
|
+
const resolveFailure = Effect.fn("RunStore.resolveFailure")(function* (runId, attemptId, operator) {
|
|
426
|
+
yield* Effect.annotateCurrentSpan({ runId, attemptId });
|
|
427
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
428
|
+
const tx = queriesFor(transaction);
|
|
429
|
+
const current = yield* tx.snapshot(runId);
|
|
430
|
+
const latest = current.attempts.at(-1);
|
|
431
|
+
if (!RunState.operatorActions(current.run.state).canResolveFailure ||
|
|
432
|
+
latest?.id !== attemptId) {
|
|
433
|
+
yield* Effect.logInfo("Failure resolution rejected; Run or Attempt changed", {
|
|
434
|
+
runId,
|
|
435
|
+
attemptId,
|
|
436
|
+
state: current.run.state._tag,
|
|
437
|
+
latestAttemptId: latest?.id,
|
|
438
|
+
});
|
|
439
|
+
return yield* new RunOperationRejectedError({
|
|
440
|
+
runId,
|
|
441
|
+
operation: "resolveFailure",
|
|
442
|
+
state: current.run.state._tag,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (Option.contains(current.resolvedFailureAttemptId, attemptId)) {
|
|
446
|
+
yield* Effect.logInfo("Run failure already resolved", { runId, attemptId });
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const resolution = new FailureResolution({ resolvedAt: yield* DateTime.now, operator });
|
|
450
|
+
yield* tx.resolveFailure(runId, attemptId, resolution);
|
|
451
|
+
const lifted = yield* tx.loopItems.liftRun(runId);
|
|
452
|
+
yield* Effect.logInfo("Run failure resolved", {
|
|
453
|
+
runId,
|
|
454
|
+
attemptId,
|
|
455
|
+
liftedSuppression: lifted,
|
|
456
|
+
resolvedAt: DateTime.formatIso(resolution.resolvedAt),
|
|
457
|
+
operator: Option.getOrNull(operator),
|
|
458
|
+
});
|
|
459
|
+
return lifted;
|
|
460
|
+
}));
|
|
461
|
+
}, persistence("resolve run failure"));
|
|
462
|
+
const approve = Effect.fn("RunStore.approve")(function* (runId, approval, source) {
|
|
463
|
+
yield* Effect.annotateCurrentSpan({ runId, eventId: approval.id, channel: source.channel });
|
|
464
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
257
465
|
const tx = queriesFor(transaction);
|
|
258
466
|
if (yield* tx.approvalExists(runId))
|
|
259
|
-
return;
|
|
467
|
+
return false;
|
|
260
468
|
const current = yield* tx.snapshot(runId);
|
|
261
469
|
if (current.run.state._tag !== "Waiting") {
|
|
262
470
|
return yield* new RunOperationRejectedError({
|
|
@@ -265,23 +473,40 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
265
473
|
state: current.run.state._tag,
|
|
266
474
|
});
|
|
267
475
|
}
|
|
268
|
-
yield* tx.recordApproval(runId, approval);
|
|
476
|
+
yield* tx.recordApproval(runId, approval, source);
|
|
269
477
|
yield* tx.updateState(runId, RunState.approve(current.run.state));
|
|
270
|
-
|
|
478
|
+
return true;
|
|
271
479
|
}));
|
|
272
480
|
}, persistence("approve run"));
|
|
273
|
-
const retry = Effect.fn("RunStore.retry")(function* (runId,
|
|
481
|
+
const retry = Effect.fn("RunStore.retry")(function* (runId, request = plainRetry) {
|
|
482
|
+
const { amendment, requestedBy } = request;
|
|
274
483
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
275
|
-
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
484
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
276
485
|
const tx = queriesFor(transaction);
|
|
277
486
|
const current = yield* tx.snapshot(runId);
|
|
278
|
-
const decision = retryRun(current
|
|
487
|
+
const decision = retryRun(current);
|
|
279
488
|
yield* Effect.logInfo("Operator retry decided", {
|
|
280
489
|
runId,
|
|
281
490
|
state: current.run.state._tag,
|
|
282
491
|
decision: decision._tag,
|
|
283
492
|
});
|
|
284
493
|
if (decision._tag === "Resume") {
|
|
494
|
+
// A later Event can have opened another Run of this Loop on the Item; reopening this one
|
|
495
|
+
// would run both.
|
|
496
|
+
const blocking = (yield* tx.blockingRun(current.run.loop, current.itemKey))[0];
|
|
497
|
+
if (blocking !== undefined) {
|
|
498
|
+
yield* Effect.logInfo("Operator retry rejected; another Run is open on the Item", {
|
|
499
|
+
runId,
|
|
500
|
+
openRunId: blocking.runId,
|
|
501
|
+
loop: current.run.loop,
|
|
502
|
+
itemKey: current.itemKey,
|
|
503
|
+
});
|
|
504
|
+
return yield* new RunRetryBlockedError({
|
|
505
|
+
runId,
|
|
506
|
+
openRunId: blocking.runId,
|
|
507
|
+
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.`,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
285
510
|
if (Option.isSome(amendment)) {
|
|
286
511
|
yield* tx.amend(runId, amendment.value);
|
|
287
512
|
yield* Effect.logInfo("Run effective task and Agent amended", {
|
|
@@ -289,12 +514,18 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
289
514
|
agent: amendment.value.agent.name,
|
|
290
515
|
});
|
|
291
516
|
}
|
|
517
|
+
// A retry lifts the suppression, so Events queue behind the running Run again.
|
|
518
|
+
if (yield* tx.loopItems.liftRun(runId)) {
|
|
519
|
+
yield* Effect.logInfo("Operator retry lifts the suppression of the Item", { runId });
|
|
520
|
+
}
|
|
521
|
+
yield* tx.setNextStart(runId, Option.some(Option.isSome(amendment)
|
|
522
|
+
? new StartedByEditAndRetry({ operator: requestedBy })
|
|
523
|
+
: new StartedByRetry({ operator: requestedBy })));
|
|
292
524
|
yield* tx.updateState(runId, decision.state);
|
|
293
|
-
|
|
294
|
-
return;
|
|
525
|
+
return true;
|
|
295
526
|
}
|
|
296
527
|
if (decision._tag === "ResumePending" && Option.isNone(amendment))
|
|
297
|
-
return;
|
|
528
|
+
return false;
|
|
298
529
|
return yield* new RunOperationRejectedError({
|
|
299
530
|
runId,
|
|
300
531
|
operation: "retry",
|
|
@@ -302,6 +533,83 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
302
533
|
});
|
|
303
534
|
}));
|
|
304
535
|
}, persistence("retry run"));
|
|
536
|
+
const runAgain = Effect.fn("RunStore.runAgain")(function* (runId, request) {
|
|
537
|
+
yield* Effect.annotateCurrentSpan({ runId, agent: request.context.agent.name });
|
|
538
|
+
const receivedAt = yield* DateTime.now;
|
|
539
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
540
|
+
const tx = queriesFor(transaction);
|
|
541
|
+
const current = yield* tx.snapshot(runId);
|
|
542
|
+
const { loop } = current.run;
|
|
543
|
+
const { itemKey } = current;
|
|
544
|
+
if (!RunState.operatorActions(current.run.state).canRunAgain) {
|
|
545
|
+
yield* Effect.logInfo("Run again rejected; only a succeeded or canceled Run runs again", {
|
|
546
|
+
runId,
|
|
547
|
+
state: current.run.state._tag,
|
|
548
|
+
});
|
|
549
|
+
return yield* new RunOperationRejectedError({
|
|
550
|
+
runId,
|
|
551
|
+
operation: "runAgain",
|
|
552
|
+
state: current.run.state._tag,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const blocking = (yield* tx.blockingRun(loop, itemKey))[0];
|
|
556
|
+
if (blocking !== undefined) {
|
|
557
|
+
yield* Effect.logInfo("Run again rejected; another Run is open on the Item", {
|
|
558
|
+
runId,
|
|
559
|
+
openRunId: blocking.runId,
|
|
560
|
+
loop,
|
|
561
|
+
itemKey,
|
|
562
|
+
});
|
|
563
|
+
return yield* new RunRetryBlockedError({
|
|
564
|
+
runId,
|
|
565
|
+
openRunId: blocking.runId,
|
|
566
|
+
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.`,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const trigger = Option.getOrElse(yield* tx.reopenedEvent(current.event.id), () => ({
|
|
570
|
+
eventId: current.event.id,
|
|
571
|
+
dedupKey: current.event.dedupKey,
|
|
572
|
+
}));
|
|
573
|
+
const reopening = Option.match(yield* tx.newestReopening(trigger.eventId), {
|
|
574
|
+
onNone: () => 1,
|
|
575
|
+
onSome: (newest) => newest.reopening + 1,
|
|
576
|
+
});
|
|
577
|
+
const generation = Option.match(yield* tx.cycles.get(itemKey), {
|
|
578
|
+
onNone: () => CycleGeneration.make(0),
|
|
579
|
+
onSome: (budget) => budget.generation,
|
|
580
|
+
});
|
|
581
|
+
const again = runAgainSubmission(current, request, {
|
|
582
|
+
trigger: trigger.dedupKey,
|
|
583
|
+
reopening,
|
|
584
|
+
generation,
|
|
585
|
+
receivedAt,
|
|
586
|
+
});
|
|
587
|
+
// Not recorded as a sighting, so a later poll of the unchanged Item finds the new Run
|
|
588
|
+
// through the reopening.
|
|
589
|
+
const recorded = yield* tx.recordTriggerEvent(again, Option.some({ eventId: trigger.eventId, reopening }));
|
|
590
|
+
yield* tx.recordRunAgain(recorded.event.id, new RunAgain({ from: runId, requestedBy: request.requestedBy }));
|
|
591
|
+
const result = yield* admitRun(tx, recorded, yield* runIdOf(recorded));
|
|
592
|
+
yield* Effect.logInfo("Run again decided", Match.valueTags(result, {
|
|
593
|
+
Claimed: ({ runId: openedRunId }) => ({
|
|
594
|
+
runId,
|
|
595
|
+
eventId: recorded.event.id,
|
|
596
|
+
reopening,
|
|
597
|
+
decision: "OpenRun",
|
|
598
|
+
openedRunId,
|
|
599
|
+
requestedBy: Option.getOrElse(request.requestedBy, () => "none"),
|
|
600
|
+
}),
|
|
601
|
+
CycleBlocked: ({ ask }) => ({
|
|
602
|
+
runId,
|
|
603
|
+
eventId: recorded.event.id,
|
|
604
|
+
reopening,
|
|
605
|
+
decision: "CycleBlocked",
|
|
606
|
+
askId: ask.id,
|
|
607
|
+
requestedBy: Option.getOrElse(request.requestedBy, () => "none"),
|
|
608
|
+
}),
|
|
609
|
+
}));
|
|
610
|
+
return result;
|
|
611
|
+
}));
|
|
612
|
+
}, persistence("run again"));
|
|
305
613
|
const cancel = Effect.fn("RunStore.cancel")(function* (runId) {
|
|
306
614
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
307
615
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -315,15 +623,23 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
315
623
|
});
|
|
316
624
|
if (decision._tag === "AlreadyClosed")
|
|
317
625
|
return decision;
|
|
626
|
+
if (decision._tag === "CancelRejected")
|
|
627
|
+
return yield* new RunOperationRejectedError({
|
|
628
|
+
runId,
|
|
629
|
+
operation: "cancel",
|
|
630
|
+
state: current.run.state._tag,
|
|
631
|
+
});
|
|
632
|
+
const canceledAt = yield* DateTime.now;
|
|
318
633
|
if (Option.isSome(decision.attempt)) {
|
|
319
|
-
|
|
634
|
+
const { id, outcome } = decision.attempt.value;
|
|
635
|
+
yield* tx.closeAttempt(id, outcome, canceledAt);
|
|
320
636
|
}
|
|
321
|
-
yield* tx.updateState(runId, decision.state);
|
|
322
|
-
yield* tx.requestInterrupt(runId);
|
|
323
|
-
yield* tx.requestWorkflowCommand(runId, "Continue");
|
|
637
|
+
yield* tx.updateState(runId, decision.state, canceledAt);
|
|
324
638
|
const canceled = {
|
|
325
639
|
_tag: "Canceled",
|
|
326
640
|
openAttempt: Option.map(decision.attempt, ({ id }) => id),
|
|
641
|
+
loop: current.run.loop,
|
|
642
|
+
itemKey: current.itemKey,
|
|
327
643
|
};
|
|
328
644
|
return canceled;
|
|
329
645
|
}));
|
|
@@ -331,23 +647,34 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
331
647
|
const revoke = Effect.fn("RunStore.revoke")(function* (loop, itemKey) {
|
|
332
648
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
333
649
|
const tx = queriesFor(transaction);
|
|
650
|
+
yield* tx.loopItems.unmatched(loop, itemKey);
|
|
334
651
|
const storedRuns = yield* tx.revokeUnstarted(loop, itemKey);
|
|
335
652
|
for (const row of storedRuns) {
|
|
336
653
|
const current = yield* tx.snapshot(row.runId);
|
|
337
654
|
if (RunState.isUnstarted(current.run.state)) {
|
|
338
655
|
yield* tx.updateState(row.runId, RunState.revoke(current.run.state));
|
|
339
|
-
yield* tx.requestInterrupt(row.runId);
|
|
340
656
|
}
|
|
341
657
|
}
|
|
342
658
|
return storedRuns.map((row) => row.runId);
|
|
343
659
|
}));
|
|
344
660
|
}, persistence("revoke run"));
|
|
661
|
+
const revokeQueued = Effect.fn("RunStore.revokeQueued")(function* (queued) {
|
|
662
|
+
const { loop, itemKey, event } = queued.submission;
|
|
663
|
+
yield* Effect.annotateCurrentSpan({ loop, itemKey, eventId: event.id });
|
|
664
|
+
// Not recorded as a sighting, because the trigger can still submit this Event from a fetch
|
|
665
|
+
// made before the closing Run changed the Item.
|
|
666
|
+
return yield* queries.revokeQueued(loop, itemKey, queued.eventIds);
|
|
667
|
+
}, persistence("revoke queued events"));
|
|
345
668
|
const claimQueued = Effect.fn("RunStore.claimQueued")(function* (submission, runId) {
|
|
346
669
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
347
670
|
const tx = queriesFor(transaction);
|
|
348
671
|
const existing = yield* tx.runForEvent(submission.event.id);
|
|
349
672
|
if (existing[0] !== undefined) {
|
|
350
|
-
|
|
673
|
+
yield* Effect.logInfo("Queued Event already opened its Run", {
|
|
674
|
+
eventId: submission.event.id,
|
|
675
|
+
runId: existing[0].runId,
|
|
676
|
+
});
|
|
677
|
+
return Option.none();
|
|
351
678
|
}
|
|
352
679
|
if ((yield* tx.ignoredEvent(submission.event.id)).length > 0) {
|
|
353
680
|
return Option.none();
|
|
@@ -355,21 +682,14 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
355
682
|
if ((yield* tx.blockingRun(submission.loop, submission.itemKey)).length > 0) {
|
|
356
683
|
return Option.none();
|
|
357
684
|
}
|
|
685
|
+
if (Option.isSome(yield* tx.loopItems.suppression(submission.loop, submission.itemKey))) {
|
|
686
|
+
return Option.none();
|
|
687
|
+
}
|
|
358
688
|
if (!(yield* tx.cycles.isCurrentEvent(submission)))
|
|
359
689
|
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,
|
|
690
|
+
return (yield* openRun(tx, submission, runId)).openedRun;
|
|
371
691
|
}));
|
|
372
|
-
}, persistence("
|
|
692
|
+
}, persistence("claim queued event"));
|
|
373
693
|
const appendTranscript = Effect.fn("RunStore.appendTranscript")(function* (direction, envelope) {
|
|
374
694
|
const receivedAt = yield* DateTime.now;
|
|
375
695
|
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -380,65 +700,52 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
380
700
|
if (direction === "outbound" && envelope.payload._tag === "InterventionApplied") {
|
|
381
701
|
yield* tx.markIntervened(envelope.attemptId);
|
|
382
702
|
}
|
|
383
|
-
yield* tx.appendTranscript(direction, envelope, receivedAt)
|
|
703
|
+
if (!(yield* tx.appendTranscript(direction, envelope, receivedAt)))
|
|
704
|
+
yield* Effect.logDebug("Transcript envelope already recorded; keeping the first copy", {
|
|
705
|
+
attemptId: envelope.attemptId,
|
|
706
|
+
direction,
|
|
707
|
+
sequence: envelope.sequence,
|
|
708
|
+
});
|
|
384
709
|
}));
|
|
385
|
-
}, persistence("append
|
|
710
|
+
}, persistence("append attempt transcript"));
|
|
386
711
|
const transcript = Effect.fn("RunStore.transcript")(function* (query) {
|
|
387
712
|
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"));
|
|
713
|
+
}, persistence("read attempt transcript"));
|
|
413
714
|
return {
|
|
414
715
|
approve,
|
|
415
716
|
appendTranscript,
|
|
416
717
|
beginAttempt,
|
|
417
718
|
cancel,
|
|
418
719
|
claimQueued,
|
|
419
|
-
completeWorkflowCommand: (runId, command) => queries
|
|
420
|
-
.completeWorkflowCommand(runId, command)
|
|
421
|
-
.pipe(persistence("complete workflow command")),
|
|
422
720
|
finishAttempt,
|
|
423
|
-
nextQueued: (runId) => queries.nextQueued(runId).pipe(persistence("find queued
|
|
721
|
+
nextQueued: (runId) => queries.nextQueued(runId).pipe(persistence("find queued event")),
|
|
424
722
|
parkForApproval,
|
|
425
|
-
|
|
426
|
-
.
|
|
427
|
-
.pipe(persistence("list pending
|
|
723
|
+
pendingContinuations: queries
|
|
724
|
+
.pendingContinuations()
|
|
725
|
+
.pipe(persistence("list pending continuations")),
|
|
726
|
+
runIdsToDrive: queries.runIdsToDrive().pipe(persistence("list the runs to drive")),
|
|
428
727
|
recordAndClaim,
|
|
429
|
-
|
|
430
|
-
|
|
728
|
+
salvageCandidates: queries.salvageCandidates().pipe(persistence("list salvage candidates")),
|
|
729
|
+
salvageCandidate: (attemptId) => queries.salvageCandidate(attemptId).pipe(persistence("read a salvage candidate")),
|
|
730
|
+
recordSalvage,
|
|
731
|
+
recordComments,
|
|
732
|
+
recordOutOfMemory,
|
|
733
|
+
attemptWindows,
|
|
734
|
+
discardLeftovers,
|
|
735
|
+
openAttempts,
|
|
736
|
+
submission: (runId) => queries.eventRouteByRun(runId).pipe(Effect.flatMap((routes) => routes[0] === undefined
|
|
737
|
+
? Effect.fail(new RunNotFoundError({ runId }))
|
|
738
|
+
: Effect.succeed(submissionFromRow(routes[0]))), persistence("read run submission")),
|
|
739
|
+
resolveFailure,
|
|
431
740
|
retry,
|
|
432
741
|
revoke,
|
|
742
|
+
revokeQueued,
|
|
743
|
+
runAgain,
|
|
433
744
|
snapshot: (runId) => queries.snapshot(runId).pipe(persistence("read run")),
|
|
434
745
|
transcript,
|
|
435
746
|
};
|
|
436
747
|
});
|
|
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
|
-
*/
|
|
748
|
+
/** The durable Run store; its methods fail only with `RunStoreError`. */
|
|
442
749
|
export class RunStore extends Context.Service()("@usine/engine/internal/RunStore") {
|
|
443
750
|
static layer = Layer.effect(RunStore, makeRunStore);
|
|
444
751
|
}
|