@usine/orchestrator 0.2.1 → 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/LICENSE +661 -21
- package/README.md +174 -52
- package/dist/board/items.d.ts +22 -3
- package/dist/board/items.d.ts.map +1 -1
- package/dist/board/items.js +44 -4
- package/dist/board/items.js.map +1 -1
- package/dist/board/projection.d.ts +8 -1
- package/dist/board/projection.d.ts.map +1 -1
- package/dist/board/projection.js +63 -14
- package/dist/board/projection.js.map +1 -1
- package/dist/board/tasks.d.ts +15 -3
- package/dist/board/tasks.d.ts.map +1 -1
- package/dist/board/tasks.js +28 -9
- package/dist/board/tasks.js.map +1 -1
- package/dist/cloudflare.d.ts +32 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +68 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/credentials.d.ts +5 -6
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +22 -15
- package/dist/credentials.js.map +1 -1
- package/dist/current-instance/changes.d.ts +87 -0
- package/dist/current-instance/changes.d.ts.map +1 -0
- package/dist/current-instance/changes.js +141 -0
- package/dist/current-instance/changes.js.map +1 -0
- package/dist/current-instance/current-instance.d.ts +89 -0
- package/dist/current-instance/current-instance.d.ts.map +1 -0
- package/dist/current-instance/current-instance.js +163 -0
- package/dist/current-instance/current-instance.js.map +1 -0
- package/dist/exe-dev.d.ts +6 -2
- package/dist/exe-dev.d.ts.map +1 -1
- package/dist/exe-dev.js +10 -9
- package/dist/exe-dev.js.map +1 -1
- package/dist/http-client.d.ts +8 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +8 -0
- package/dist/http-client.js.map +1 -0
- package/dist/idle-triggers.d.ts +1 -1
- package/dist/idle-triggers.d.ts.map +1 -1
- package/dist/idle-triggers.js +1 -1
- package/dist/idle-triggers.js.map +1 -1
- package/dist/label-reconciler.d.ts +5 -2
- package/dist/label-reconciler.d.ts.map +1 -1
- package/dist/label-reconciler.js.map +1 -1
- package/dist/lease-order.d.ts +60 -0
- package/dist/lease-order.d.ts.map +1 -0
- package/dist/lease-order.js +76 -0
- package/dist/lease-order.js.map +1 -0
- package/dist/local.d.ts +4 -18
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +9 -45
- package/dist/local.js.map +1 -1
- package/dist/loop-matches.d.ts +28 -0
- package/dist/loop-matches.d.ts.map +1 -0
- package/dist/loop-matches.js +74 -0
- package/dist/loop-matches.js.map +1 -0
- package/dist/operator/attempt.d.ts +76 -10
- package/dist/operator/attempt.d.ts.map +1 -1
- package/dist/operator/attempt.js +69 -11
- package/dist/operator/attempt.js.map +1 -1
- package/dist/operator/board.d.ts +48 -16
- package/dist/operator/board.d.ts.map +1 -1
- package/dist/operator/board.js +48 -17
- package/dist/operator/board.js.map +1 -1
- package/dist/operator/configuration.d.ts +314 -0
- package/dist/operator/configuration.d.ts.map +1 -0
- package/dist/operator/configuration.js +490 -0
- package/dist/operator/configuration.js.map +1 -0
- package/dist/operator/desk.d.ts +206 -0
- package/dist/operator/desk.d.ts.map +1 -0
- package/dist/operator/desk.js +525 -0
- package/dist/operator/desk.js.map +1 -0
- package/dist/operator/github-changes.d.ts +88 -0
- package/dist/operator/github-changes.d.ts.map +1 -0
- package/dist/operator/github-changes.js +266 -0
- package/dist/operator/github-changes.js.map +1 -0
- package/dist/operator/http.d.ts +135 -105
- package/dist/operator/http.d.ts.map +1 -1
- package/dist/operator/http.js +155 -30
- package/dist/operator/http.js.map +1 -1
- package/dist/operator/machines.d.ts +71 -0
- package/dist/operator/machines.d.ts.map +1 -0
- package/dist/operator/machines.js +103 -0
- package/dist/operator/machines.js.map +1 -0
- package/dist/operator/need.d.ts +58 -11
- package/dist/operator/need.d.ts.map +1 -1
- package/dist/operator/need.js +61 -9
- package/dist/operator/need.js.map +1 -1
- package/dist/operator/operator.d.ts +135 -29
- package/dist/operator/operator.d.ts.map +1 -1
- package/dist/operator/operator.js +370 -63
- package/dist/operator/operator.js.map +1 -1
- package/dist/operator/overview.d.ts +7 -7
- package/dist/operator/overview.d.ts.map +1 -1
- package/dist/operator/overview.js +5 -5
- package/dist/operator/overview.js.map +1 -1
- package/dist/operator/projection.d.ts +56 -5
- package/dist/operator/projection.d.ts.map +1 -1
- package/dist/operator/projection.js +108 -9
- package/dist/operator/projection.js.map +1 -1
- package/dist/operator/run.d.ts +101 -6
- package/dist/operator/run.d.ts.map +1 -1
- package/dist/operator/run.js +100 -7
- package/dist/operator/run.js.map +1 -1
- package/dist/operator/statistics/cache.d.ts +7 -0
- package/dist/operator/statistics/cache.d.ts.map +1 -0
- package/dist/operator/statistics/cache.js +40 -0
- package/dist/operator/statistics/cache.js.map +1 -0
- package/dist/operator/statistics/runs.d.ts +36 -0
- package/dist/operator/statistics/runs.d.ts.map +1 -0
- package/dist/operator/statistics/runs.js +284 -0
- package/dist/operator/statistics/runs.js.map +1 -0
- package/dist/operator/statistics/statistics.d.ts +20 -0
- package/dist/operator/statistics/statistics.d.ts.map +1 -0
- package/dist/operator/statistics/statistics.js +138 -0
- package/dist/operator/statistics/statistics.js.map +1 -0
- package/dist/operator/statistics/tasks.d.ts +30 -0
- package/dist/operator/statistics/tasks.d.ts.map +1 -0
- package/dist/operator/statistics/tasks.js +244 -0
- package/dist/operator/statistics/tasks.js.map +1 -0
- package/dist/operator/statistics/view.d.ts +302 -0
- package/dist/operator/statistics/view.d.ts.map +1 -0
- package/dist/operator/statistics/view.js +243 -0
- package/dist/operator/statistics/view.js.map +1 -0
- package/dist/operator/statistics/window.d.ts +24 -0
- package/dist/operator/statistics/window.d.ts.map +1 -0
- package/dist/operator/statistics/window.js +47 -0
- package/dist/operator/statistics/window.js.map +1 -0
- package/dist/operator/task.d.ts +94 -0
- package/dist/operator/task.d.ts.map +1 -0
- package/dist/operator/task.js +204 -0
- package/dist/operator/task.js.map +1 -0
- package/dist/operator/transcript.d.ts +12 -12
- package/dist/operator/transcript.d.ts.map +1 -1
- package/dist/operator/transcript.js +11 -9
- package/dist/operator/transcript.js.map +1 -1
- package/dist/operator/usage.d.ts +103 -0
- package/dist/operator/usage.d.ts.map +1 -0
- package/dist/operator/usage.js +234 -0
- package/dist/operator/usage.js.map +1 -0
- package/dist/orchestrator.d.ts +8 -5
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +4 -2
- package/dist/orchestrator.js.map +1 -1
- package/dist/polling/closed-item-asks.d.ts +8 -0
- package/dist/polling/closed-item-asks.d.ts.map +1 -0
- package/dist/polling/closed-item-asks.js +28 -0
- package/dist/polling/closed-item-asks.js.map +1 -0
- package/dist/polling/cycle.d.ts +28 -10
- package/dist/polling/cycle.d.ts.map +1 -1
- package/dist/polling/cycle.js +20 -6
- package/dist/polling/cycle.js.map +1 -1
- package/dist/polling/evaluation-failures.d.ts +42 -0
- package/dist/polling/evaluation-failures.d.ts.map +1 -0
- package/dist/polling/evaluation-failures.js +103 -0
- package/dist/polling/evaluation-failures.js.map +1 -0
- package/dist/polling/hints.d.ts +2 -2
- package/dist/polling/hints.d.ts.map +1 -1
- package/dist/polling/hints.js +2 -2
- package/dist/polling/hints.js.map +1 -1
- package/dist/polling/loop-evaluation.d.ts +21 -11
- package/dist/polling/loop-evaluation.d.ts.map +1 -1
- package/dist/polling/loop-evaluation.js +70 -27
- package/dist/polling/loop-evaluation.js.map +1 -1
- package/dist/polling/open-issues.d.ts +23 -0
- package/dist/polling/open-issues.d.ts.map +1 -0
- package/dist/polling/open-issues.js +77 -0
- package/dist/polling/open-issues.js.map +1 -0
- package/dist/polling/open-pull-requests.d.ts +28 -0
- package/dist/polling/open-pull-requests.d.ts.map +1 -0
- package/dist/polling/open-pull-requests.js +131 -0
- package/dist/polling/open-pull-requests.js.map +1 -0
- package/dist/polling/polling.d.ts +20 -11
- package/dist/polling/polling.d.ts.map +1 -1
- package/dist/polling/polling.js +215 -52
- package/dist/polling/polling.js.map +1 -1
- package/dist/polling/repository-sync.d.ts +21 -14
- package/dist/polling/repository-sync.d.ts.map +1 -1
- package/dist/polling/repository-sync.js +35 -28
- package/dist/polling/repository-sync.js.map +1 -1
- package/dist/polling-triggers.d.ts +1 -1
- package/dist/polling-triggers.d.ts.map +1 -1
- package/dist/polling-triggers.js +1 -1
- package/dist/polling-triggers.js.map +1 -1
- package/dist/reloads.d.ts +14 -0
- package/dist/reloads.d.ts.map +1 -0
- package/dist/reloads.js +7 -0
- package/dist/reloads.js.map +1 -0
- package/dist/runtime.d.ts +24 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +58 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schedule-triggers.d.ts +14 -10
- package/dist/schedule-triggers.d.ts.map +1 -1
- package/dist/schedule-triggers.js +36 -31
- package/dist/schedule-triggers.js.map +1 -1
- package/dist/scheduler.d.ts +45 -5
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +587 -44
- package/dist/scheduler.js.map +1 -1
- package/dist/ssh.d.ts +103 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +144 -0
- package/dist/ssh.js.map +1 -0
- package/dist/trigger-condition.d.ts +12 -0
- package/dist/trigger-condition.d.ts.map +1 -0
- package/dist/trigger-condition.js +88 -0
- package/dist/trigger-condition.js.map +1 -0
- package/dist/trigger-submission.d.ts +16 -13
- package/dist/trigger-submission.d.ts.map +1 -1
- package/dist/trigger-submission.js +19 -14
- package/dist/trigger-submission.js.map +1 -1
- package/dist/worker-engine.d.ts +13 -0
- package/dist/worker-engine.d.ts.map +1 -0
- package/dist/worker-engine.js +56 -0
- package/dist/worker-engine.js.map +1 -0
- package/dist/worker-status.d.ts +140 -0
- package/dist/worker-status.d.ts.map +1 -0
- package/dist/worker-status.js +76 -0
- package/dist/worker-status.js.map +1 -0
- package/package.json +47 -22
|
@@ -1,35 +1,71 @@
|
|
|
1
1
|
import { transcriptEvent } from "./projection.js";
|
|
2
2
|
import { TranscriptSessionsQuery } from "@usine/engine/transcript";
|
|
3
3
|
import { GitHubLabelName } from "@usine/config/label";
|
|
4
|
-
import { resolveParentModel } from "@usine/core/agent";
|
|
5
|
-
import { AttemptContext } from "@usine/core/attempt";
|
|
4
|
+
import { Agent, resolveParentModel } from "@usine/core/agent";
|
|
5
|
+
import { AttemptContext, AttemptId } from "@usine/core/attempt";
|
|
6
6
|
import { Intervention } from "@usine/core/attempt/channel";
|
|
7
7
|
import { Event, DedupKey, EventId } from "@usine/core/event";
|
|
8
8
|
import { CycleBudgetAskId } from "@usine/core/cycle-budget";
|
|
9
|
+
import { BoardMoveRecord, BoardMoves } from "@usine/engine/board-moves";
|
|
9
10
|
import { CycleBudgets } from "@usine/engine/cycle-budget";
|
|
10
11
|
import { LoopName } from "@usine/core/loop";
|
|
11
12
|
import { RunId } from "@usine/core/run";
|
|
12
|
-
import {
|
|
13
|
+
import { ApprovalSource } from "@usine/core/run/approval";
|
|
14
|
+
import * as RunState from "@usine/core/run/state";
|
|
15
|
+
import { canonicalRepositoryName } from "@usine/core/repository";
|
|
16
|
+
import { RetryRequest, RunAgainRequest, RunEngine, RunOperationRejectedError, UnconfirmedStops, } from "@usine/engine/engine";
|
|
13
17
|
import { ItemProjectionReader } from "@usine/engine/item-projection";
|
|
14
18
|
import { LoopControl } from "@usine/engine/loop-control";
|
|
15
|
-
import { RunReader } from "@usine/engine/run-reader";
|
|
19
|
+
import { RunReader, } from "@usine/engine/run-reader";
|
|
16
20
|
import { RepositoryLabelProjectionReader } from "@usine/engine/repository-label-projection";
|
|
17
21
|
import { GitHubItemWriter } from "@usine/github/item-writer";
|
|
18
22
|
import { ChangedIssue, ChangedPullRequest } from "@usine/github/polling";
|
|
19
|
-
import { Context, DateTime, Effect, Exit, Layer, Option, Schema, SynchronizedRef } from "effect";
|
|
23
|
+
import { Array as EffectArray, Context, DateTime, Effect, Exit, Layer, Match, Option, Schema, SynchronizedRef, } from "effect";
|
|
24
|
+
import { runNeedsAttention, settledItemState } from "../board/items.js";
|
|
20
25
|
import { labelMutationForBoardMove, projectBoard } from "../board/projection.js";
|
|
26
|
+
import { LoopMatches } from "../loop-matches.js";
|
|
21
27
|
import { GitHubItemKey, PollingTriggers } from "../polling-triggers.js";
|
|
22
28
|
import { ScheduleTriggers } from "../schedule-triggers.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
29
|
+
import { cycleLimitOf } from "../trigger-submission.js";
|
|
30
|
+
import { ApprovalNeed, ConfigFileNeed, CycleBudgetNeed, BoardProjectionNeed, PollFailureNeed, PreflightFindingNeed, RunFailureNeed, ScheduleFailureNeed, UnconfiguredWorkerNeed, UnconfirmedStopNeed, } from "./need.js";
|
|
31
|
+
import { BoardItemNotFoundError as BoardItemNotFound, BoardLaneNotFoundError as BoardLaneNotFound, BoardLaneReadOnlyError as BoardLaneReadOnly, BoardProjectNotFoundError as BoardProjectNotFound, BoardTaskView, BoardView, LabelView, ReadOnlyBoardLaneView, WritableBoardLaneView, } from "./board.js";
|
|
25
32
|
import { ColdTranscriptRequest, } from "./transcript.js";
|
|
26
33
|
import { LoopView, ProjectView, ReadyOverview } from "./overview.js";
|
|
27
|
-
import { RunDetailView, EditAndRetry, SessionIntervention, RosterAgentNotFoundError, } from "./run.js";
|
|
28
|
-
import { readTranscript, runView } from "./projection.js";
|
|
29
|
-
|
|
34
|
+
import { RunDetailView, EditAndRetry, LoopNotFoundError, SessionIntervention, RosterAgentNotFoundError, UnconfirmedStopView, } from "./run.js";
|
|
35
|
+
import { readRunningEvents, readTranscript, readUnrecordedEnds, runDuration, runView, workerAccesses, } from "./projection.js";
|
|
36
|
+
import { RunUsageReader, taskUsage } from "./usage.js";
|
|
37
|
+
import { RunGitHubChanges, } from "./github-changes.js";
|
|
38
|
+
import { TaskReader } from "./task.js";
|
|
39
|
+
import { OperatorDesk } from "./desk.js";
|
|
40
|
+
import { OperatorStatistics } from "./statistics/statistics.js";
|
|
41
|
+
import { machineView } from "./machines.js";
|
|
42
|
+
import { WorkerStatus } from "../worker-status.js";
|
|
43
|
+
/** The number of closed Runs the overview keeps next to the open ones. */
|
|
30
44
|
const OVERVIEW_CLOSED_RUNS = 100;
|
|
31
|
-
/**
|
|
45
|
+
/** The number of newest Items a Board shows. */
|
|
32
46
|
const OVERVIEW_BOARD_ITEMS = 500;
|
|
47
|
+
const sameAgent = Schema.toEquivalence(Agent);
|
|
48
|
+
/**
|
|
49
|
+
* Returns `overview` with the usage of every card that contains a `changed` Run recomputed from
|
|
50
|
+
* `usage`.
|
|
51
|
+
*/
|
|
52
|
+
const withRunUsage = (overview, changed, usage) => new ReadyOverview({
|
|
53
|
+
...overview,
|
|
54
|
+
projects: overview.projects.map((project) => new ProjectView({
|
|
55
|
+
...project,
|
|
56
|
+
board: new BoardView({
|
|
57
|
+
...project.board,
|
|
58
|
+
lanes: project.board.lanes.map((lane) => {
|
|
59
|
+
const items = lane.items.map((task) => task.runs.some(({ id }) => changed.has(id))
|
|
60
|
+
? new BoardTaskView({ ...task, usage: taskUsage(task.runs, usage) })
|
|
61
|
+
: task);
|
|
62
|
+
return lane._tag === "WritableBoardLane"
|
|
63
|
+
? new WritableBoardLaneView({ ...lane, items })
|
|
64
|
+
: new ReadOnlyBoardLaneView({ ...lane, items });
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
})),
|
|
68
|
+
});
|
|
33
69
|
const makeOperatorObservation = Effect.gen(function* () {
|
|
34
70
|
const budgets = yield* CycleBudgets;
|
|
35
71
|
const reader = yield* RunReader;
|
|
@@ -38,44 +74,90 @@ const makeOperatorObservation = Effect.gen(function* () {
|
|
|
38
74
|
const repositoryLabels = yield* RepositoryLabelProjectionReader;
|
|
39
75
|
const polling = yield* PollingTriggers;
|
|
40
76
|
const schedules = yield* ScheduleTriggers;
|
|
77
|
+
const loopMatches = yield* LoopMatches;
|
|
41
78
|
const cache = yield* SynchronizedRef.make(Option.none());
|
|
42
|
-
const
|
|
43
|
-
|
|
79
|
+
const usageReader = yield* RunUsageReader;
|
|
80
|
+
const stops = yield* UnconfirmedStops;
|
|
81
|
+
const workerStatus = yield* WorkerStatus;
|
|
82
|
+
const projectOverview = Effect.fn("Operator.projectOverview")(function* (instance, pollFailures, preflightFindings, scheduleFailures, revision, unconfirmedStops) {
|
|
83
|
+
const [snapshots, states, cycleAsks, workerAttempts] = yield* Effect.all([
|
|
44
84
|
reader.list({ closedLimit: OVERVIEW_CLOSED_RUNS }),
|
|
45
85
|
controls.states,
|
|
46
86
|
budgets.asks(),
|
|
87
|
+
reader.attemptsByWorker(),
|
|
47
88
|
], { concurrency: "unbounded" });
|
|
48
89
|
const paused = new Set(states.filter((state) => state.paused).map((state) => state.loop));
|
|
49
|
-
const
|
|
90
|
+
const access = workerAccesses(instance.config.workers);
|
|
91
|
+
const views = snapshots.map((snapshot) => runView(snapshot, paused, access));
|
|
50
92
|
const runsByRepository = new Map();
|
|
51
93
|
for (const view of views) {
|
|
52
94
|
const projectRuns = runsByRepository.get(view.repository) ?? [];
|
|
53
95
|
projectRuns.push(view);
|
|
54
96
|
runsByRepository.set(view.repository, projectRuns);
|
|
55
97
|
}
|
|
56
|
-
const
|
|
98
|
+
const projected = yield* Effect.forEach(instance.config.repos, (repository) => Effect.gen(function* () {
|
|
57
99
|
const { name } = repository;
|
|
58
100
|
const projectFindings = preflightFindings.filter(({ repository: findingRepository }) => findingRepository === name);
|
|
59
101
|
const [boardItems, labelProjection] = yield* Effect.all([
|
|
60
102
|
itemReader.list(name, { limit: OVERVIEW_BOARD_ITEMS }),
|
|
61
103
|
repositoryLabels.get(name),
|
|
62
104
|
], { concurrency: "unbounded" });
|
|
63
|
-
const history = yield*
|
|
105
|
+
const [history, matches] = yield* Effect.all([
|
|
106
|
+
reader.forItems(boardItems.map(({ itemKey }) => itemKey)),
|
|
107
|
+
loopMatches.matching(instance, repository, boardItems),
|
|
108
|
+
], { concurrency: "unbounded" });
|
|
109
|
+
const itemLoops = new Map();
|
|
110
|
+
for (const { loop, item } of matches) {
|
|
111
|
+
itemLoops.set(item.itemKey, [...(itemLoops.get(item.itemKey) ?? []), loop.name]);
|
|
112
|
+
}
|
|
64
113
|
const merged = new Map((runsByRepository.get(name) ?? []).map((run) => [run.id, run]));
|
|
65
114
|
for (const saved of history) {
|
|
66
|
-
const view = runView(saved, paused);
|
|
115
|
+
const view = runView(saved, paused, access);
|
|
67
116
|
merged.set(view.id, view);
|
|
68
117
|
}
|
|
69
118
|
const projectRuns = [...merged.values()].sort((a, b) => b.openedAt.localeCompare(a.openedAt) || b.id.localeCompare(a.id));
|
|
119
|
+
// The reader keeps ended Attempts' reports, so only running Attempts are read again. A
|
|
120
|
+
// running Run's Event gives its card's lane while its Item has no lane label.
|
|
121
|
+
const [usages, runEvents] = yield* Effect.all([
|
|
122
|
+
Effect.forEach(history, (summary) => usageReader
|
|
123
|
+
.read(summary)
|
|
124
|
+
.pipe(Effect.map((usage) => [summary.run.id, usage])), { concurrency: 4 }),
|
|
125
|
+
readRunningEvents(reader, history),
|
|
126
|
+
], { concurrency: "unbounded" });
|
|
127
|
+
const runUsage = new Map(usages);
|
|
128
|
+
yield* Effect.logDebug("Read the usage of the Board's Runs", {
|
|
129
|
+
repository: name,
|
|
130
|
+
runs: runUsage.size,
|
|
131
|
+
});
|
|
70
132
|
const catalog = Option.match(labelProjection, {
|
|
71
133
|
onNone: () => [],
|
|
72
134
|
onSome: ({ labels }) => labels,
|
|
73
135
|
});
|
|
74
|
-
const board = yield* projectBoard(repository, catalog.map(({ name }) => name), boardItems, projectRuns, cycleAsks.map(({ itemKey }) => itemKey));
|
|
136
|
+
const board = yield* projectBoard(repository, catalog.map(({ name }) => name), boardItems, projectRuns, cycleAsks.map(({ itemKey }) => itemKey), itemLoops, runUsage, runEvents);
|
|
137
|
+
const working = history.filter(({ attempts }) => attempts.some(({ outcome }) => Option.isNone(outcome)));
|
|
138
|
+
return { name, projectFindings, projectRuns, board, catalog, runUsage, working };
|
|
139
|
+
}), { concurrency: 4 });
|
|
140
|
+
const allRuns = new Map(views.map((run) => [run.id, run]));
|
|
141
|
+
for (const { projectRuns } of projected)
|
|
142
|
+
for (const run of projectRuns)
|
|
143
|
+
allRuns.set(run.id, run);
|
|
144
|
+
// A Run of a repository that usine.yaml no longer lists has no Project to mark.
|
|
145
|
+
const managed = new Set(instance.config.repos.map(({ name }) => canonicalRepositoryName(name)));
|
|
146
|
+
const managedRuns = [];
|
|
147
|
+
for (const run of allRuns.values()) {
|
|
148
|
+
if (managed.has(canonicalRepositoryName(run.repository)))
|
|
149
|
+
managedRuns.push(run);
|
|
150
|
+
else if (run.state === "Waiting" || run.canResolveFailure)
|
|
151
|
+
yield* Effect.logDebug("Run's repository is not in usine.yaml; left out of Needs attention", { runId: run.id, repository: run.repository, state: run.state });
|
|
152
|
+
}
|
|
153
|
+
const failedItems = new Set(managedRuns.flatMap((run) => run.state !== "Waiting" && run.canResolveFailure ? [run.itemKey] : []));
|
|
154
|
+
// The Items of failed Runs that are closed, merged, or gone, with their state.
|
|
155
|
+
const settledItems = new Map(EffectArray.getSomes(yield* Effect.forEach(failedItems, (key) => settledItemState(key).pipe(Effect.provideService(ItemProjectionReader, itemReader), Effect.map(Option.map((state) => [key, state]))), { concurrency: 4 })));
|
|
156
|
+
const projects = projected.map(({ name, projectFindings, projectRuns, board, catalog }) => {
|
|
75
157
|
const status = cycleAsks.some((ask) => ask.repository === name) ||
|
|
76
158
|
board.failures.length > 0 ||
|
|
77
159
|
projectFindings.length > 0 ||
|
|
78
|
-
projectRuns.some((run) => run.
|
|
160
|
+
projectRuns.some((run) => runNeedsAttention(run, settledItems.has(run.itemKey)))
|
|
79
161
|
? "needs-you"
|
|
80
162
|
: projectRuns.some((run) => run.canCancel)
|
|
81
163
|
? "active"
|
|
@@ -92,7 +174,11 @@ const makeOperatorObservation = Effect.gen(function* () {
|
|
|
92
174
|
})),
|
|
93
175
|
preflightFindings: projectFindings,
|
|
94
176
|
});
|
|
95
|
-
})
|
|
177
|
+
});
|
|
178
|
+
const usage = {
|
|
179
|
+
runs: new Map(projected.flatMap(({ runUsage }) => [...runUsage])),
|
|
180
|
+
working: projected.flatMap(({ working }) => working),
|
|
181
|
+
};
|
|
96
182
|
const loops = instance.loops.map((loop) => new LoopView({
|
|
97
183
|
name: loop.name,
|
|
98
184
|
trigger: loop.trigger.on,
|
|
@@ -115,23 +201,31 @@ const makeOperatorObservation = Effect.gen(function* () {
|
|
|
115
201
|
}),
|
|
116
202
|
}));
|
|
117
203
|
}
|
|
118
|
-
const
|
|
119
|
-
for (const
|
|
120
|
-
for (const run of project.runs)
|
|
121
|
-
allRuns.set(run.id, run);
|
|
122
|
-
for (const run of allRuns.values()) {
|
|
204
|
+
const cardedItems = new Map(projects.flatMap(({ board }) => board.lanes.flatMap(({ items }) => items.flatMap((task) => task.items.map((item) => [item.itemKey, item])))));
|
|
205
|
+
for (const run of managedRuns) {
|
|
123
206
|
if (run.state === "Waiting") {
|
|
124
207
|
needsYou.push(new ApprovalNeed({
|
|
125
208
|
id: `approval:${run.id}`,
|
|
126
209
|
repository: run.repository,
|
|
127
210
|
runId: run.id,
|
|
128
211
|
loop: run.loop,
|
|
212
|
+
item: cardedItems.get(run.itemKey) ?? null,
|
|
129
213
|
}));
|
|
130
214
|
}
|
|
131
|
-
else if (run.
|
|
215
|
+
else if (run.canResolveFailure) {
|
|
132
216
|
const attempt = run.attempts.at(-1);
|
|
133
217
|
if (attempt?.outcome?._tag !== "Failed") {
|
|
134
|
-
return yield* Effect.die(new Error(`Failed
|
|
218
|
+
return yield* Effect.die(new Error(`Failed run ${run.id} has no failed attempt`));
|
|
219
|
+
}
|
|
220
|
+
const settled = settledItems.get(run.itemKey);
|
|
221
|
+
if (settled !== undefined) {
|
|
222
|
+
yield* Effect.logDebug("Failed Run's Item is closed; left out of Needs attention and the Project status", {
|
|
223
|
+
runId: run.id,
|
|
224
|
+
itemKey: run.itemKey,
|
|
225
|
+
itemState: settled,
|
|
226
|
+
failure: attempt.outcome.failure._tag,
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
135
229
|
}
|
|
136
230
|
needsYou.push(new RunFailureNeed({
|
|
137
231
|
id: `run-failure:${run.id}`,
|
|
@@ -168,16 +262,78 @@ const makeOperatorObservation = Effect.gen(function* () {
|
|
|
168
262
|
failure,
|
|
169
263
|
}));
|
|
170
264
|
});
|
|
171
|
-
const
|
|
265
|
+
for (const { file, error } of instance.invalidAgents) {
|
|
266
|
+
needsYou.push(new ConfigFileNeed({ id: `config-file:${file}`, kind: "agent", error }));
|
|
267
|
+
}
|
|
268
|
+
for (const { file, error } of instance.quarantinedLoops) {
|
|
269
|
+
needsYou.push(new ConfigFileNeed({ id: `config-file:${file}`, kind: "loop", error }));
|
|
270
|
+
}
|
|
271
|
+
const configuredWorkers = new Set(instance.config.workers.map(({ name }) => name));
|
|
272
|
+
for (const { worker, attempts, running } of workerAttempts) {
|
|
273
|
+
if (configuredWorkers.has(worker))
|
|
274
|
+
continue;
|
|
275
|
+
needsYou.push(new UnconfiguredWorkerNeed({
|
|
276
|
+
id: `unconfigured-worker:${worker}`,
|
|
277
|
+
worker,
|
|
278
|
+
attempts,
|
|
279
|
+
running,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
for (const { attemptId, runId, worker } of unconfirmedStops) {
|
|
283
|
+
const run = allRuns.get(runId) ?? runView(yield* reader.inspect(runId), paused, access);
|
|
284
|
+
const attempt = run.attempts.find(({ id }) => id === attemptId);
|
|
285
|
+
if (attempt === undefined) {
|
|
286
|
+
yield* Effect.logWarning("The Run has no Attempt with the unconfirmed stop; no need shown", {
|
|
287
|
+
runId,
|
|
288
|
+
attemptId,
|
|
289
|
+
});
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
needsYou.push(new UnconfirmedStopNeed({
|
|
293
|
+
id: `unconfirmed-stop:${attemptId}`,
|
|
294
|
+
repository: run.repository,
|
|
295
|
+
runId,
|
|
296
|
+
loop: run.loop,
|
|
297
|
+
attemptNumber: attempt.number,
|
|
298
|
+
worker,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
const overview = new ReadyOverview({ projects, loops, needsYou });
|
|
172
302
|
yield* Effect.logDebug("Refreshed operator overview", {
|
|
173
303
|
revision,
|
|
174
304
|
runs: views.length,
|
|
175
305
|
needsYou: needsYou.length,
|
|
306
|
+
runsAtWork: usage.working.length,
|
|
307
|
+
});
|
|
308
|
+
return { overview, usage };
|
|
309
|
+
});
|
|
310
|
+
// Each session keeps its highest report, so a Run's usage changes only with more tokens or
|
|
311
|
+
// another session.
|
|
312
|
+
const refreshWorkingUsage = Effect.fn("Operator.refreshWorkingUsage")(function* (cached) {
|
|
313
|
+
if (cached.usage.working.length === 0)
|
|
314
|
+
return cached;
|
|
315
|
+
const read = yield* Effect.forEach(cached.usage.working, (summary) => usageReader.read(summary).pipe(Effect.map((usage) => [summary.run.id, usage])), { concurrency: 4 });
|
|
316
|
+
const changed = new Set(read.flatMap(([runId, usage]) => {
|
|
317
|
+
const before = cached.usage.runs.get(runId);
|
|
318
|
+
return before?.tokens === usage.tokens && before.sessions.length === usage.sessions.length
|
|
319
|
+
? []
|
|
320
|
+
: [runId];
|
|
321
|
+
}));
|
|
322
|
+
if (changed.size === 0)
|
|
323
|
+
return cached;
|
|
324
|
+
const runs = new Map([...cached.usage.runs, ...read]);
|
|
325
|
+
yield* Effect.logDebug("Refreshed the usage of Runs at work on the cached overview", {
|
|
326
|
+
runsAtWork: read.length,
|
|
327
|
+
changed: changed.size,
|
|
176
328
|
});
|
|
177
|
-
return
|
|
329
|
+
return {
|
|
330
|
+
...cached,
|
|
331
|
+
overview: withRunUsage(cached.overview, changed, runs),
|
|
332
|
+
usage: { ...cached.usage, runs },
|
|
333
|
+
};
|
|
178
334
|
});
|
|
179
335
|
const overview = Effect.fn("Operator.overview")((instance) => SynchronizedRef.modifyEffect(cache, (cached) => Effect.gen(function* () {
|
|
180
|
-
const [revision, latestPoll, latestSchedule] = yield* Effect.all([reader.revision(), polling.latest, schedules.latest], { concurrency: "unbounded" });
|
|
336
|
+
const [revision, latestPoll, latestSchedule, unconfirmedStops] = yield* Effect.all([reader.revision(), polling.latest, schedules.latest, stops.list], { concurrency: "unbounded" });
|
|
181
337
|
const pollFailures = Option.match(latestPoll, {
|
|
182
338
|
onNone: () => [],
|
|
183
339
|
onSome: ({ failures }) => failures,
|
|
@@ -190,52 +346,119 @@ const makeOperatorObservation = Effect.gen(function* () {
|
|
|
190
346
|
onNone: () => [],
|
|
191
347
|
onSome: ({ failures }) => failures,
|
|
192
348
|
});
|
|
193
|
-
// Without the invalidation singleton staleness
|
|
194
|
-
//
|
|
349
|
+
// Without the invalidation singleton, staleness can't be detected, so the cache is
|
|
350
|
+
// bypassed.
|
|
195
351
|
const token = Option.map(revision, (value) => JSON.stringify([
|
|
196
352
|
value,
|
|
197
|
-
instance.config.repos.map(({ name, lanes }) => [name, lanes]),
|
|
198
|
-
instance.
|
|
353
|
+
instance.config.repos.map(({ name, tags, lanes }) => [name, tags, lanes]),
|
|
354
|
+
[...workerAccesses(instance.config.workers)],
|
|
355
|
+
instance.loops.map(({ name, agent, trigger, selector }) => [
|
|
356
|
+
name,
|
|
357
|
+
agent,
|
|
358
|
+
trigger,
|
|
359
|
+
selector,
|
|
360
|
+
]),
|
|
361
|
+
[...instance.invalidAgents, ...instance.quarantinedLoops].map(({ file, error }) => [
|
|
362
|
+
file,
|
|
363
|
+
error.stage,
|
|
364
|
+
error.message,
|
|
365
|
+
]),
|
|
199
366
|
pollFailures,
|
|
200
367
|
preflightFindings,
|
|
201
368
|
scheduleFailures,
|
|
369
|
+
unconfirmedStops.map(({ attemptId }) => attemptId),
|
|
202
370
|
]));
|
|
203
371
|
if (Option.isNone(token)) {
|
|
204
372
|
yield* Effect.logWarning("Projection revision singleton is missing; the overview cache is bypassed");
|
|
205
|
-
const projected = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision);
|
|
373
|
+
const { overview: projected } = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision, unconfirmedStops);
|
|
206
374
|
return [projected, Option.none()];
|
|
207
375
|
}
|
|
208
376
|
if (Option.isSome(cached) && cached.value.token === token.value) {
|
|
209
377
|
yield* Effect.logDebug("Reusing unchanged operator overview", {
|
|
210
378
|
revision,
|
|
211
379
|
});
|
|
212
|
-
|
|
380
|
+
const current = yield* refreshWorkingUsage(cached.value);
|
|
381
|
+
return [current.overview, Option.some(current)];
|
|
213
382
|
}
|
|
214
|
-
const projected = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision);
|
|
215
|
-
return [projected, Option.some({ token: token.value,
|
|
383
|
+
const projected = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision, unconfirmedStops);
|
|
384
|
+
return [projected.overview, Option.some({ token: token.value, ...projected })];
|
|
216
385
|
})));
|
|
217
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Returns the unconfirmed stop on the Loop and Item of the open Run of `snapshot`, which keeps
|
|
388
|
+
* the Run from starting an Attempt, or null.
|
|
389
|
+
*/
|
|
390
|
+
const blockingStop = Effect.fn("Operator.blockingStop")(function* (snapshot, unconfirmedStops) {
|
|
391
|
+
if (!RunState.isOpen(snapshot.run.state))
|
|
392
|
+
return null;
|
|
393
|
+
const stop = unconfirmedStops.find(({ loop, itemKey }) => loop === snapshot.run.loop && itemKey === snapshot.itemKey);
|
|
394
|
+
if (stop === undefined)
|
|
395
|
+
return null;
|
|
396
|
+
const fields = {
|
|
397
|
+
runId: snapshot.run.id,
|
|
398
|
+
unstoppedRunId: stop.runId,
|
|
399
|
+
attemptId: stop.attemptId,
|
|
400
|
+
};
|
|
401
|
+
yield* Effect.annotateCurrentSpan(fields);
|
|
402
|
+
const attempts = stop.runId === snapshot.run.id
|
|
403
|
+
? snapshot.attempts
|
|
404
|
+
: (yield* reader.inspect(stop.runId)).attempts;
|
|
405
|
+
const attempt = attempts.find(({ id }) => id === stop.attemptId);
|
|
406
|
+
if (attempt === undefined) {
|
|
407
|
+
yield* Effect.logWarning("The Run with the unconfirmed stop has no such Attempt; the Run view shows no wait", fields);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return new UnconfirmedStopView({
|
|
411
|
+
runId: stop.runId,
|
|
412
|
+
attemptNumber: attempt.number,
|
|
413
|
+
worker: stop.worker,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
const detail = Effect.fn("Operator.detail")(function* (instance, runId, request = new ColdTranscriptRequest()) {
|
|
218
417
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
219
|
-
const [snapshot, states] = yield* Effect.all([reader.inspect(runId), controls.states], { concurrency: "unbounded" });
|
|
418
|
+
const [snapshot, states, unconfirmedStops, inFlight] = yield* Effect.all([reader.inspect(runId), controls.states, stops.list, workerStatus.machines], { concurrency: "unbounded" });
|
|
419
|
+
const openAttempt = snapshot.attempts.find((attempt) => Option.isNone(attempt.outcome));
|
|
420
|
+
// The machines are oldest first, so the last is the newest Attempt's.
|
|
421
|
+
const machine = [...inFlight]
|
|
422
|
+
.reverse()
|
|
423
|
+
.find((candidate) => candidate.state !== "deleting" &&
|
|
424
|
+
(Option.contains(candidate.runId, runId) ||
|
|
425
|
+
(openAttempt !== undefined && Option.contains(candidate.attemptId, openAttempt.id))));
|
|
220
426
|
const paused = new Set(states.filter((state) => state.paused).map((state) => state.loop));
|
|
221
|
-
const [transcript, sessionState] = yield* Effect.all([
|
|
427
|
+
const [transcript, sessionState, usage, unrecordedEnds, settled, unconfirmedStop] = yield* Effect.all([
|
|
222
428
|
readTranscript(reader, snapshot, request),
|
|
223
429
|
Effect.forEach(snapshot.attempts.filter((attempt) => Option.isNone(attempt.outcome)), (attempt) => reader.transcript(new TranscriptSessionsQuery({ attemptId: attempt.id })), { concurrency: "unbounded" }),
|
|
430
|
+
usageReader.read(snapshot),
|
|
431
|
+
readUnrecordedEnds(reader, snapshot),
|
|
432
|
+
settledItemState(snapshot.itemKey).pipe(Effect.provideService(ItemProjectionReader, itemReader)),
|
|
433
|
+
blockingStop(snapshot, unconfirmedStops),
|
|
224
434
|
], { concurrency: "unbounded" });
|
|
435
|
+
const currentAgent = instance.agents.find(({ name }) => name === snapshot.agent.name);
|
|
225
436
|
return new RunDetailView({
|
|
226
|
-
run: runView(snapshot, paused),
|
|
437
|
+
run: runView(snapshot, paused, workerAccesses(instance.config.workers)),
|
|
438
|
+
agentDefinitionChanged: currentAgent !== undefined && !sameAgent(snapshot.agent, currentAgent.agent),
|
|
227
439
|
eventDedupKey: snapshot.event.dedupKey,
|
|
228
440
|
eventPayload: snapshot.event.payload,
|
|
229
441
|
transcript: transcript.events,
|
|
230
442
|
sessionState: sessionState.flat().map(transcriptEvent),
|
|
231
443
|
earlierTranscript: transcript.earlier,
|
|
444
|
+
worker: snapshot.attempts.at(-1)?.worker ?? null,
|
|
445
|
+
settledItemState: Option.getOrNull(settled),
|
|
446
|
+
duration: runDuration(snapshot.attempts, unrecordedEnds),
|
|
447
|
+
usage,
|
|
448
|
+
unconfirmedStop,
|
|
449
|
+
machine: machine === undefined ? null : machineView(machine),
|
|
232
450
|
});
|
|
233
451
|
});
|
|
234
|
-
|
|
452
|
+
const tasks = yield* TaskReader;
|
|
453
|
+
const { desk } = yield* OperatorDesk;
|
|
454
|
+
const { statistics, runStatistics } = yield* OperatorStatistics;
|
|
455
|
+
return { detail, overview, task: tasks.read, desk, statistics, runStatistics };
|
|
235
456
|
});
|
|
236
|
-
/** The read-only operator surface
|
|
457
|
+
/** The read-only operator surface, which needs no trigger execution. */
|
|
237
458
|
export class OperatorObservation extends Context.Service()("@usine/orchestrator/OperatorObservation") {
|
|
238
|
-
static layer = Layer.effect(OperatorObservation, makeOperatorObservation)
|
|
459
|
+
static layer = Layer.effect(OperatorObservation, makeOperatorObservation).pipe(Layer.provide(Layer.mergeAll(TaskReader.layer, OperatorDesk.layer, LoopMatches.layer, RunUsageReader.layer,
|
|
460
|
+
// Shares the overview's usage reader, so both read an ended Attempt's reports once.
|
|
461
|
+
OperatorStatistics.layer.pipe(Layer.provide(RunUsageReader.layer)))));
|
|
239
462
|
}
|
|
240
463
|
const makeOperator = Effect.gen(function* () {
|
|
241
464
|
const budgets = yield* CycleBudgets;
|
|
@@ -245,8 +468,11 @@ const makeOperator = Effect.gen(function* () {
|
|
|
245
468
|
const items = yield* ItemProjectionReader;
|
|
246
469
|
const repositoryLabels = yield* RepositoryLabelProjectionReader;
|
|
247
470
|
const github = yield* GitHubItemWriter;
|
|
471
|
+
const reader = yield* RunReader;
|
|
472
|
+
const boardMoves = yield* BoardMoves;
|
|
248
473
|
const polling = yield* PollingTriggers;
|
|
249
|
-
const
|
|
474
|
+
const githubChanges = yield* RunGitHubChanges;
|
|
475
|
+
const approve = Effect.fn("Operator.approve")(function* (instance, runId) {
|
|
250
476
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
251
477
|
const current = yield* runs.inspect(runId);
|
|
252
478
|
if (current.run.state._tag !== "Waiting") {
|
|
@@ -267,12 +493,16 @@ const makeOperator = Effect.gen(function* () {
|
|
|
267
493
|
dedupKey,
|
|
268
494
|
payload: { runId, decision: "approved" },
|
|
269
495
|
receivedAt,
|
|
270
|
-
}));
|
|
496
|
+
}), new ApprovalSource({ channel: "ui", approver: instance.config.operator }));
|
|
271
497
|
yield* Effect.logInfo("Operator approved Run", { runId });
|
|
272
498
|
});
|
|
273
|
-
const
|
|
499
|
+
const resolveFailure = Effect.fn("Operator.resolveFailure")(function* (instance, runId, attemptId) {
|
|
500
|
+
yield* Effect.annotateCurrentSpan({ runId, attemptId });
|
|
501
|
+
yield* runs.resolveFailure(runId, attemptId, instance.config.operator);
|
|
502
|
+
});
|
|
503
|
+
const retry = Effect.fn("Operator.retry")(function* (instance, runId) {
|
|
274
504
|
yield* Effect.annotateCurrentSpan({ runId });
|
|
275
|
-
yield* runs.retry(runId);
|
|
505
|
+
yield* runs.retry(runId, new RetryRequest({ requestedBy: instance.config.operator, amendment: Option.none() }));
|
|
276
506
|
yield* Effect.logInfo("Operator retried Run", { runId });
|
|
277
507
|
});
|
|
278
508
|
const editAndRetry = Effect.fn("Operator.editAndRetry")(function* (instance, runId, edit) {
|
|
@@ -281,16 +511,63 @@ const makeOperator = Effect.gen(function* () {
|
|
|
281
511
|
if (file === undefined)
|
|
282
512
|
return yield* new RosterAgentNotFoundError({
|
|
283
513
|
agent: edit.agent,
|
|
284
|
-
message: `Agent ${edit.agent} is not in the roster`,
|
|
514
|
+
message: `Agent "${edit.agent}" is not in the roster`,
|
|
285
515
|
});
|
|
286
516
|
const agent = file.agent;
|
|
287
|
-
yield* runs.retry(runId,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
517
|
+
yield* runs.retry(runId, new RetryRequest({
|
|
518
|
+
requestedBy: instance.config.operator,
|
|
519
|
+
amendment: Option.some(new AttemptContext({
|
|
520
|
+
task: edit.task,
|
|
521
|
+
agent,
|
|
522
|
+
parentModel: resolveParentModel(agent, instance.config.settings.parentModel),
|
|
523
|
+
})),
|
|
524
|
+
}));
|
|
292
525
|
yield* Effect.logInfo("Operator amended and retried Run", { runId, agent: agent.name });
|
|
293
526
|
});
|
|
527
|
+
const runAgain = Effect.fn("Operator.runAgain")(function* (instance, runId, edit) {
|
|
528
|
+
yield* Effect.annotateCurrentSpan({ runId, agent: edit.agent });
|
|
529
|
+
const current = yield* runs.inspect(runId);
|
|
530
|
+
const loop = instance.loops.find(({ name }) => name === current.run.loop);
|
|
531
|
+
if (loop === undefined) {
|
|
532
|
+
yield* Effect.logInfo("Run again rejected; the Run's Loop is not loaded", {
|
|
533
|
+
runId,
|
|
534
|
+
loop: current.run.loop,
|
|
535
|
+
});
|
|
536
|
+
return yield* new LoopNotFoundError({
|
|
537
|
+
loop: current.run.loop,
|
|
538
|
+
message: `Run ${runId} cannot run again: loop "${current.run.loop}" is not loaded.`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
const file = instance.agents.find(({ name }) => name === edit.agent);
|
|
542
|
+
if (file === undefined) {
|
|
543
|
+
yield* Effect.logInfo("Run again rejected; the Agent is not in the roster", {
|
|
544
|
+
runId,
|
|
545
|
+
agent: edit.agent,
|
|
546
|
+
});
|
|
547
|
+
return yield* new RosterAgentNotFoundError({
|
|
548
|
+
agent: edit.agent,
|
|
549
|
+
message: `Agent "${edit.agent}" is not in the roster`,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const agent = file.agent;
|
|
553
|
+
const result = yield* runs.runAgain(runId, new RunAgainRequest({
|
|
554
|
+
step: loop.step,
|
|
555
|
+
approval: loop.approval,
|
|
556
|
+
cycleLimit: cycleLimitOf(loop),
|
|
557
|
+
permissions: loop.permissions,
|
|
558
|
+
context: new AttemptContext({
|
|
559
|
+
task: edit.task,
|
|
560
|
+
agent,
|
|
561
|
+
parentModel: resolveParentModel(agent, instance.config.settings.parentModel),
|
|
562
|
+
}),
|
|
563
|
+
requestedBy: instance.config.operator,
|
|
564
|
+
}));
|
|
565
|
+
yield* Effect.logInfo("Operator ran a Run again", Match.valueTags(result, {
|
|
566
|
+
Claimed: ({ runId: openedRunId }) => ({ runId, agent: agent.name, openedRunId }),
|
|
567
|
+
CycleBlocked: ({ ask }) => ({ runId, agent: agent.name, askId: ask.id }),
|
|
568
|
+
}));
|
|
569
|
+
return result;
|
|
570
|
+
});
|
|
294
571
|
const intervene = Effect.fn("Operator.intervene")(function* (instance, runId, input) {
|
|
295
572
|
yield* Effect.annotateCurrentSpan({
|
|
296
573
|
runId,
|
|
@@ -330,10 +607,16 @@ const makeOperator = Effect.gen(function* () {
|
|
|
330
607
|
items.list(move.repository, { limit: OVERVIEW_BOARD_ITEMS }),
|
|
331
608
|
repositoryLabels.get(move.repository),
|
|
332
609
|
], { concurrency: "unbounded" });
|
|
610
|
+
// Place each card as the Board does, so the move records the lane that the card left.
|
|
611
|
+
const summaries = yield* reader.forItems(storedItems.map(({ itemKey }) => itemKey));
|
|
612
|
+
const runEvents = yield* readRunningEvents(reader, summaries);
|
|
613
|
+
const access = workerAccesses(instance.config.workers);
|
|
333
614
|
const board = yield* projectBoard(repository, Option.match(labelProjection, {
|
|
334
615
|
onNone: () => [],
|
|
335
616
|
onSome: ({ labels }) => labels.map(({ name }) => name),
|
|
336
|
-
}), storedItems,
|
|
617
|
+
}), storedItems,
|
|
618
|
+
// Placement reads no pause state, so these views leave every Loop unpaused.
|
|
619
|
+
summaries.map((summary) => runView(summary, new Set(), access)), [], new Map(), new Map(), runEvents);
|
|
337
620
|
const source = board.lanes.find(({ items: laneItems }) => laneItems.some(({ items }) => items.some(({ number }) => number === move.item)));
|
|
338
621
|
const movedItem = storedItems.find(({ number }) => number === move.item);
|
|
339
622
|
if (source === undefined || movedItem === undefined) {
|
|
@@ -347,7 +630,18 @@ const makeOperator = Effect.gen(function* () {
|
|
|
347
630
|
return yield* new BoardLaneReadOnly({ repository: move.repository, lane: move.toLane });
|
|
348
631
|
}
|
|
349
632
|
const mutation = labelMutationForBoardMove(board, target);
|
|
350
|
-
const
|
|
633
|
+
const movedAt = yield* DateTime.now;
|
|
634
|
+
// Recorded before the write, so the Run reader never attributes the swap to an Agent.
|
|
635
|
+
yield* boardMoves.record(new BoardMoveRecord({
|
|
636
|
+
repository: move.repository,
|
|
637
|
+
itemKey: movedItem.itemKey,
|
|
638
|
+
at: movedAt,
|
|
639
|
+
fromLane: source.name,
|
|
640
|
+
toLane: target.name,
|
|
641
|
+
added: mutation.add,
|
|
642
|
+
removed: mutation.remove,
|
|
643
|
+
}));
|
|
644
|
+
const changed = { number: move.item, updatedAt: movedAt };
|
|
351
645
|
const item = movedItem.source === "pr" ? new ChangedPullRequest(changed) : new ChangedIssue(changed);
|
|
352
646
|
const applied = yield* github.updateLabels(move.repository, move.item, mutation).pipe(
|
|
353
647
|
// A failed or interrupted request may have applied part of the label
|
|
@@ -380,23 +674,36 @@ const makeOperator = Effect.gen(function* () {
|
|
|
380
674
|
...observation,
|
|
381
675
|
approve,
|
|
382
676
|
cancel,
|
|
677
|
+
discardLeftovers: (worker) => runs.discardLeftovers(worker).pipe(Effect.asVoid),
|
|
383
678
|
moveBoardItem,
|
|
679
|
+
githubChanges: githubChanges.read,
|
|
384
680
|
pause,
|
|
385
681
|
retry,
|
|
682
|
+
resolveFailure,
|
|
386
683
|
editAndRetry,
|
|
684
|
+
runAgain,
|
|
387
685
|
intervene,
|
|
388
686
|
unpause,
|
|
389
687
|
resetCycleBudget,
|
|
390
688
|
};
|
|
391
689
|
});
|
|
392
|
-
/**
|
|
690
|
+
/**
|
|
691
|
+
* The operator's factory view and actions for approving, retrying, canceling, and directing work.
|
|
692
|
+
*/
|
|
393
693
|
export class Operator extends Context.Service()("@usine/orchestrator/Operator") {
|
|
394
|
-
static layer = Layer.effect(Operator, makeOperator).pipe(Layer.provide(OperatorObservation.layer));
|
|
694
|
+
static layer = Layer.effect(Operator, makeOperator).pipe(Layer.provide(Layer.merge(OperatorObservation.layer, RunGitHubChanges.layer)));
|
|
395
695
|
}
|
|
396
696
|
export { ProjectStatus, ProjectView, LoopView, ReadyOverview, NeedsSetupOverview, FactoryOverview, } from "./overview.js";
|
|
397
|
-
export {
|
|
398
|
-
export {
|
|
399
|
-
export {
|
|
400
|
-
export {
|
|
697
|
+
export { LabelAddedView, LabelRemovedView, CommentedView, ReviewedView, BoardMovedView, GitHubChangeView, AttemptGitHubChangesView, RunGitHubChangesView, } from "./github-changes.js";
|
|
698
|
+
export { ApprovalView, RunStateTag, RunView, RunDurationView, RunDetailView, UnconfirmedStopView, EditAndRetry, LoopNotFoundError, SessionIntervention, RosterAgentNotFoundError, } from "./run.js";
|
|
699
|
+
export { UsageCostView, SessionUsageView, PoolUsageView, RunUsageView, TaskPoolUsageView, TaskUsageView, } from "./usage.js";
|
|
700
|
+
export { InfraFailureView, DefectFailureView, AgentFailureView, BlockedFailureView, QuestionFailureView, RunawayFailureView, ChildSessionBudgetFailureView, FailureView, SucceededAttemptView, FailedAttemptView, CanceledAttemptView, AttemptOutcomeView, LocalWorkerAccess, SshWorkerAccess, ProvisionedWorkerAccess, SandboxWorkerAccess, WorkerAccess, AttemptView, } from "./attempt.js";
|
|
701
|
+
export { MachineView, ProvisioningPauseView, HetznerProvisionerView, BoatProvisionerView, ProvisionerView, } from "./machines.js";
|
|
702
|
+
export { BoardLaneId, BoardLaneOrigin, BoardItemLink, BoardPullRequestView, BoardTaskView, WritableBoardLaneView, ReadOnlyBoardLaneView, BoardLaneView, BoardProjectionError, BoardView, LabelView, BoardMove, BoardProjectNotFoundError, BoardItemNotFoundError, BoardLaneNotFoundError, BoardLaneReadOnlyError, } from "./board.js";
|
|
703
|
+
export { TaskItemView, TaskLaneView, TaskCommentView, TaskView, } from "./task.js";
|
|
704
|
+
export { ApprovalNeed, CycleBudgetNeed, RunFailureNeed, PollFailureNeed, PreflightFindingNeed, BoardProjectionNeed, ScheduleFailureNeed, ConfigFileNeed, UnconfiguredWorkerNeed, UnconfirmedStopNeed, NeedView, } from "./need.js";
|
|
705
|
+
export { LoopSelectorView, LoopFileView, QuarantinedLoopView, AgentFileView, InvalidAgentView, GatewayView, WorkerSlotsView, PassedCheckView, DegradedCheckView, DeferredCheckView, FailedCheckView, PreflightCheckView, ContainerView, HarnessPreflightView, WorkerConfigurationView, RemovedWorkerView, LabelStateView, LabelLaneSourceView, StateLaneSourceView, LaneSourceView, LaneConfigurationView, RepositoryConfigurationView, SettingsView, ConfigurationView, OperatorConfiguration, } from "./configuration.js";
|
|
706
|
+
export { WorkerSilentView, ProviderRetryingView, ConnectivityWarningView, RunningAttemptView, SuppressedForOperatorView, ClosedUnchangedView, RunFailedView, LoopPausedView, WaitingForStopView, CycleBudgetUsedUpView, AwaitingApprovalView, QueuedForWorkerView, AwaitingPollView, StallReasonView, StalledItemView, ClosedRunView, WorkerView, RepositoryPollView, DeskView, } from "./desk.js";
|
|
707
|
+
export { StatisticsWindow, StatisticsBucket, TaskDoneView, TaskPeriodView, LoopShareView, LoopRunTimeView, TaskTimeView, TaskCostView, TaskTokensView, UntimedTasksView, StatisticsView, DurationSummaryView, OutcomeCountsView, PoolTokensView, CostSummaryView, UsageSummaryView, LoopStatisticsView, AgentStatisticsView, BrainStatisticsView, ThroughputPeriodView, WorkerQueueWaitView, AttemptCountView, WorkerUtilizationView, NeedsAttentionPeriodView, NeedsAttentionView, RunStatisticsView, } from "./statistics/view.js";
|
|
401
708
|
export { FailedTranscriptView, TranscriptPayload, TranscriptEventView, TranscriptPageCursor, ColdTranscriptRequest, TailTranscriptRequest, EarlierTranscriptRequest, TranscriptRequest, TranscriptAttemptNotFoundError, } from "./transcript.js";
|
|
402
709
|
//# sourceMappingURL=operator.js.map
|