@usine/engine 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -36
- 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 +16 -9
- package/dist/database/local-sqlite.js.map +1 -1
- package/dist/database/schema.d.ts +581 -22
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +104 -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 +57 -24
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +144 -120
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/error.d.ts +36 -9
- package/dist/engine/error.d.ts.map +1 -1
- package/dist/engine/error.js +35 -7
- 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 +65 -9
- 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 +381 -42
- package/dist/store/codec.d.ts.map +1 -1
- package/dist/store/codec.js +59 -15
- 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 +345 -37
- package/dist/store/queries.d.ts.map +1 -1
- package/dist/store/queries.js +333 -160
- 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 +481 -190
- package/dist/store/store.js.map +1 -1
- package/dist/store/transcript.d.ts +9 -0
- package/dist/store/transcript.d.ts.map +1 -0
- package/dist/store/transcript.js +90 -0
- package/dist/store/transcript.js.map +1 -0
- package/dist/transcript.d.ts +41 -20
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +45 -25
- 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/20260912111855_operator_intervention/migration.sql +42 -0
- package/drizzle/20260912111855_operator_intervention/snapshot.json +1461 -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
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);
|
|
@@ -207,38 +324,74 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
207
324
|
worker,
|
|
208
325
|
number: decision.number,
|
|
209
326
|
brain: current.agent.brain,
|
|
327
|
+
task: current.task,
|
|
328
|
+
agent: current.agent.name,
|
|
329
|
+
operatorIntervened: false,
|
|
210
330
|
parentModel: current.parentModel,
|
|
211
331
|
startedAt,
|
|
212
332
|
outcome: Option.none(),
|
|
333
|
+
endedAt: Option.none(),
|
|
334
|
+
salvage: Option.none(),
|
|
335
|
+
start: Option.some(decision.start),
|
|
336
|
+
reattachedAt: Option.none(),
|
|
213
337
|
});
|
|
214
338
|
yield* tx.addAttempt(attempt);
|
|
339
|
+
if (Option.isSome(requested))
|
|
340
|
+
yield* tx.setNextStart(runId, Option.none());
|
|
215
341
|
yield* Effect.logInfo("Attempt started", {
|
|
216
342
|
runId,
|
|
217
343
|
attemptId,
|
|
218
344
|
number: decision.number,
|
|
219
345
|
worker,
|
|
346
|
+
start: decision.start._tag,
|
|
347
|
+
resumesFrom: Option.map(decision.resume, ({ ref }) => ref),
|
|
220
348
|
});
|
|
221
349
|
return new AttemptStarted({
|
|
222
|
-
execution:
|
|
350
|
+
execution: executionOf(current, {
|
|
223
351
|
attempt,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
eventId: current.run.eventId,
|
|
228
|
-
loop: current.run.loop,
|
|
229
|
-
state: decision.state,
|
|
230
|
-
openedAt: current.run.openedAt,
|
|
231
|
-
}),
|
|
232
|
-
step: current.step,
|
|
233
|
-
repository: current.repository,
|
|
234
|
-
revision: current.revision,
|
|
235
|
-
task: current.task,
|
|
236
|
-
agent: current.agent,
|
|
237
|
-
parentModel: current.parentModel,
|
|
352
|
+
state: decision.state,
|
|
353
|
+
resume: decision.resume,
|
|
354
|
+
reattach: false,
|
|
238
355
|
}),
|
|
239
356
|
});
|
|
240
357
|
}));
|
|
241
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"));
|
|
242
395
|
const finishAttempt = Effect.fn("RunStore.finishAttempt")(function* (attempt, result) {
|
|
243
396
|
yield* Effect.annotateCurrentSpan({ runId: attempt.runId, attemptId: attempt.id });
|
|
244
397
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -246,14 +399,52 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
246
399
|
const current = yield* tx.snapshot(attempt.runId);
|
|
247
400
|
const decision = decideFinishAttempt(current, attempt.id, result);
|
|
248
401
|
yield* applyFinish(tx, attempt.runId, decision);
|
|
402
|
+
const stop = operatorStop(decision);
|
|
403
|
+
if (Option.isSome(stop))
|
|
404
|
+
yield* suppressForOperator(tx, current, stop.value);
|
|
249
405
|
return decision;
|
|
250
406
|
}));
|
|
251
407
|
}, persistence("finish attempt"));
|
|
252
|
-
const
|
|
253
|
-
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* () {
|
|
254
445
|
const tx = queriesFor(transaction);
|
|
255
446
|
if (yield* tx.approvalExists(runId))
|
|
256
|
-
return;
|
|
447
|
+
return false;
|
|
257
448
|
const current = yield* tx.snapshot(runId);
|
|
258
449
|
if (current.run.state._tag !== "Waiting") {
|
|
259
450
|
return yield* new RunOperationRejectedError({
|
|
@@ -262,29 +453,59 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
262
453
|
state: current.run.state._tag,
|
|
263
454
|
});
|
|
264
455
|
}
|
|
265
|
-
yield* tx.recordApproval(runId, approval);
|
|
456
|
+
yield* tx.recordApproval(runId, approval, source);
|
|
266
457
|
yield* tx.updateState(runId, RunState.approve(current.run.state));
|
|
267
|
-
|
|
458
|
+
return true;
|
|
268
459
|
}));
|
|
269
460
|
}, persistence("approve run"));
|
|
270
|
-
const retry = Effect.fn("RunStore.retry")(function* (runId) {
|
|
461
|
+
const retry = Effect.fn("RunStore.retry")(function* (runId, request = plainRetry) {
|
|
462
|
+
const { amendment, requestedBy } = request;
|
|
271
463
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
272
|
-
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
464
|
+
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
273
465
|
const tx = queriesFor(transaction);
|
|
274
466
|
const current = yield* tx.snapshot(runId);
|
|
275
|
-
const decision = retryRun(current
|
|
467
|
+
const decision = retryRun(current);
|
|
276
468
|
yield* Effect.logInfo("Operator retry decided", {
|
|
277
469
|
runId,
|
|
278
470
|
state: current.run.state._tag,
|
|
279
471
|
decision: decision._tag,
|
|
280
472
|
});
|
|
281
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
|
+
}
|
|
490
|
+
if (Option.isSome(amendment)) {
|
|
491
|
+
yield* tx.amend(runId, amendment.value);
|
|
492
|
+
yield* Effect.logInfo("Run effective task and Agent amended", {
|
|
493
|
+
runId,
|
|
494
|
+
agent: amendment.value.agent.name,
|
|
495
|
+
});
|
|
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 })));
|
|
282
504
|
yield* tx.updateState(runId, decision.state);
|
|
283
|
-
|
|
284
|
-
return;
|
|
505
|
+
return true;
|
|
285
506
|
}
|
|
286
|
-
if (decision._tag === "ResumePending")
|
|
287
|
-
return;
|
|
507
|
+
if (decision._tag === "ResumePending" && Option.isNone(amendment))
|
|
508
|
+
return false;
|
|
288
509
|
return yield* new RunOperationRejectedError({
|
|
289
510
|
runId,
|
|
290
511
|
operation: "retry",
|
|
@@ -292,6 +513,83 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
292
513
|
});
|
|
293
514
|
}));
|
|
294
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"));
|
|
295
593
|
const cancel = Effect.fn("RunStore.cancel")(function* (runId) {
|
|
296
594
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
297
595
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -305,12 +603,12 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
305
603
|
});
|
|
306
604
|
if (decision._tag === "AlreadyClosed")
|
|
307
605
|
return decision;
|
|
606
|
+
const canceledAt = yield* DateTime.now;
|
|
308
607
|
if (Option.isSome(decision.attempt)) {
|
|
309
|
-
|
|
608
|
+
const { id, outcome } = decision.attempt.value;
|
|
609
|
+
yield* tx.closeAttempt(id, outcome, canceledAt);
|
|
310
610
|
}
|
|
311
|
-
yield* tx.updateState(runId, decision.state);
|
|
312
|
-
yield* tx.requestInterrupt(runId);
|
|
313
|
-
yield* tx.requestWorkflowCommand(runId, "Continue");
|
|
611
|
+
yield* tx.updateState(runId, decision.state, canceledAt);
|
|
314
612
|
const canceled = {
|
|
315
613
|
_tag: "Canceled",
|
|
316
614
|
openAttempt: Option.map(decision.attempt, ({ id }) => id),
|
|
@@ -321,23 +619,34 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
321
619
|
const revoke = Effect.fn("RunStore.revoke")(function* (loop, itemKey) {
|
|
322
620
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
323
621
|
const tx = queriesFor(transaction);
|
|
622
|
+
yield* tx.loopItems.unmatched(loop, itemKey);
|
|
324
623
|
const storedRuns = yield* tx.revokeUnstarted(loop, itemKey);
|
|
325
624
|
for (const row of storedRuns) {
|
|
326
625
|
const current = yield* tx.snapshot(row.runId);
|
|
327
626
|
if (RunState.isUnstarted(current.run.state)) {
|
|
328
627
|
yield* tx.updateState(row.runId, RunState.revoke(current.run.state));
|
|
329
|
-
yield* tx.requestInterrupt(row.runId);
|
|
330
628
|
}
|
|
331
629
|
}
|
|
332
630
|
return storedRuns.map((row) => row.runId);
|
|
333
631
|
}));
|
|
334
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"));
|
|
335
640
|
const claimQueued = Effect.fn("RunStore.claimQueued")(function* (submission, runId) {
|
|
336
641
|
return yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
337
642
|
const tx = queriesFor(transaction);
|
|
338
643
|
const existing = yield* tx.runForEvent(submission.event.id);
|
|
339
644
|
if (existing[0] !== undefined) {
|
|
340
|
-
|
|
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();
|
|
341
650
|
}
|
|
342
651
|
if ((yield* tx.ignoredEvent(submission.event.id)).length > 0) {
|
|
343
652
|
return Option.none();
|
|
@@ -345,21 +654,14 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
345
654
|
if ((yield* tx.blockingRun(submission.loop, submission.itemKey)).length > 0) {
|
|
346
655
|
return Option.none();
|
|
347
656
|
}
|
|
657
|
+
if (Option.isSome(yield* tx.loopItems.suppression(submission.loop, submission.itemKey))) {
|
|
658
|
+
return Option.none();
|
|
659
|
+
}
|
|
348
660
|
if (!(yield* tx.cycles.isCurrentEvent(submission)))
|
|
349
661
|
return Option.none();
|
|
350
|
-
|
|
351
|
-
return claimed.result._tag === "Claimed"
|
|
352
|
-
? Option.some(new StoredSubmission({ submission, runId }))
|
|
353
|
-
: Option.none();
|
|
662
|
+
return (yield* openRun(tx, submission, runId)).openedRun;
|
|
354
663
|
}));
|
|
355
|
-
}, persistence("claim queued
|
|
356
|
-
const recoverable = Effect.fn("RunStore.recoverable")(function* () {
|
|
357
|
-
return (yield* queries.recoverable()).map((row) => new RecoverableRun({
|
|
358
|
-
submission: submissionFromRow(row),
|
|
359
|
-
runId: row.runId,
|
|
360
|
-
state: row.state,
|
|
361
|
-
}));
|
|
362
|
-
}, persistence("list recoverable runs"));
|
|
664
|
+
}, persistence("claim queued event"));
|
|
363
665
|
const appendTranscript = Effect.fn("RunStore.appendTranscript")(function* (direction, envelope) {
|
|
364
666
|
const receivedAt = yield* DateTime.now;
|
|
365
667
|
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
@@ -367,65 +669,54 @@ const makeRunStore = Effect.gen(function* () {
|
|
|
367
669
|
if (direction === "outbound" && envelope.payload._tag === "Started") {
|
|
368
670
|
yield* tx.updateAttemptParentModel(envelope.attemptId, envelope.payload.parentModel);
|
|
369
671
|
}
|
|
370
|
-
|
|
672
|
+
if (direction === "outbound" && envelope.payload._tag === "InterventionApplied") {
|
|
673
|
+
yield* tx.markIntervened(envelope.attemptId);
|
|
674
|
+
}
|
|
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
|
+
});
|
|
371
681
|
}));
|
|
372
|
-
}, persistence("append
|
|
682
|
+
}, persistence("append attempt transcript"));
|
|
373
683
|
const transcript = Effect.fn("RunStore.transcript")(function* (query) {
|
|
374
684
|
return yield* queries.transcript(query);
|
|
375
|
-
}, persistence("read
|
|
376
|
-
const reconcileCompletion = Effect.fn("RunStore.reconcileCompletion")(function* (runId, result) {
|
|
377
|
-
yield* Effect.annotateCurrentSpan({ runId });
|
|
378
|
-
if (result._tag !== "Complete") {
|
|
379
|
-
yield* Effect.logDebug("Workflow reconciliation skipped", { runId, reason: "Suspended" });
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
yield* database.transaction((transaction) => Effect.gen(function* () {
|
|
383
|
-
const tx = queriesFor(transaction);
|
|
384
|
-
const current = yield* tx.snapshot(runId);
|
|
385
|
-
// Workflow defects and interruption do not become typed Attempt failures.
|
|
386
|
-
// Keep the projection available to the existing restart recovery path.
|
|
387
|
-
const completion = Exit.isSuccess(result.exit)
|
|
388
|
-
? Option.some(Result.succeed(result.exit.value))
|
|
389
|
-
: Option.map(Cause.findErrorOption(result.exit.cause), Result.fail);
|
|
390
|
-
if (Option.isNone(completion)) {
|
|
391
|
-
yield* Effect.logInfo("Workflow reconciliation skipped", {
|
|
392
|
-
runId,
|
|
393
|
-
reason: "NoTypedOutcome",
|
|
394
|
-
});
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
yield* applyFinish(tx, runId, reconcileAttempt(current, completion.value));
|
|
398
|
-
}));
|
|
399
|
-
}, persistence("reconcile workflow completion"));
|
|
685
|
+
}, persistence("read attempt transcript"));
|
|
400
686
|
return {
|
|
401
687
|
approve,
|
|
402
688
|
appendTranscript,
|
|
403
689
|
beginAttempt,
|
|
404
690
|
cancel,
|
|
405
691
|
claimQueued,
|
|
406
|
-
completeWorkflowCommand: (runId, command) => queries
|
|
407
|
-
.completeWorkflowCommand(runId, command)
|
|
408
|
-
.pipe(persistence("complete workflow command")),
|
|
409
692
|
finishAttempt,
|
|
410
|
-
nextQueued: (runId) => queries.nextQueued(runId).pipe(persistence("find queued
|
|
693
|
+
nextQueued: (runId) => queries.nextQueued(runId).pipe(persistence("find queued event")),
|
|
411
694
|
parkForApproval,
|
|
412
|
-
|
|
413
|
-
.
|
|
414
|
-
.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")),
|
|
415
699
|
recordAndClaim,
|
|
416
|
-
|
|
417
|
-
|
|
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,
|
|
418
711
|
retry,
|
|
419
712
|
revoke,
|
|
713
|
+
revokeQueued,
|
|
714
|
+
runAgain,
|
|
420
715
|
snapshot: (runId) => queries.snapshot(runId).pipe(persistence("read run")),
|
|
421
716
|
transcript,
|
|
422
717
|
};
|
|
423
718
|
});
|
|
424
|
-
/**
|
|
425
|
-
* The durable Run projection store. Another backend implements this interface
|
|
426
|
-
* with its own `Layer.effect(RunStore, ...)`; methods speak domain vocabulary
|
|
427
|
-
* and fail only with `RunStoreError`.
|
|
428
|
-
*/
|
|
719
|
+
/** The durable Run store; its methods fail only with `RunStoreError`. */
|
|
429
720
|
export class RunStore extends Context.Service()("@usine/engine/internal/RunStore") {
|
|
430
721
|
static layer = Layer.effect(RunStore, makeRunStore);
|
|
431
722
|
}
|