@defrex/autobuild 0.9.0 → 0.10.1
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 +97 -113
- package/SPEC.md +17 -4
- package/docs/architecture.md +109 -0
- package/docs/configuration.md +26 -7
- package/docs/remote-store-protocol.md +35 -0
- package/docs/website.md +4 -0
- package/package.json +6 -4
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +85 -147
- package/packages/core/src/cli/dashboard/poll.ts +7 -2
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +80 -37
- package/packages/core/src/cli/help.ts +15 -1
- package/packages/core/src/cli/init-validation.ts +18 -0
- package/packages/core/src/cli/init.ts +3 -2
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +1 -1
- package/packages/core/src/cli/ticket.ts +146 -1
- package/packages/core/src/cli/wait.ts +10 -2
- package/packages/core/src/cli/watch.ts +10 -2
- package/packages/core/src/config/live.ts +1 -0
- package/packages/core/src/config/pipeline-source.ts +17 -5
- package/packages/core/src/config/schema.ts +21 -0
- package/packages/core/src/integration/harness.ts +113 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +43 -9
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +216 -74
- package/packages/core/src/operator/query.ts +68 -28
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/orchestrator/prompt.ts +3 -1
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +32 -1
- package/packages/core/src/processes/build-execution-state.ts +31 -15
- package/packages/core/src/processes/build-runner.ts +184 -90
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +270 -176
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +44 -3
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +26 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +10 -1
- package/packages/core/src/store/build-scope.ts +26 -0
- package/packages/core/src/store/contract.ts +337 -0
- package/packages/core/src/store/digest.ts +71 -18
- package/packages/core/src/store/local/schema.ts +17 -0
- package/packages/core/src/store/local/store.ts +111 -4
- package/packages/core/src/store/memory.ts +68 -1
- package/packages/core/src/store/phase-session.ts +23 -0
- package/packages/core/src/store/remote/client.ts +50 -1
- package/packages/core/src/store/remote/index.ts +2 -0
- package/packages/core/src/store/remote/protocol.ts +15 -0
- package/packages/core/src/store/repo-state-events.ts +72 -7
- package/packages/core/src/store/session-handle.ts +30 -0
- package/packages/core/src/store/session-reducer.ts +52 -11
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/ticket-assets.ts +41 -8
- package/packages/core/src/store/types.ts +38 -0
- package/skills/guide/SKILL.md +38 -21
- package/skills/guide/references/remote-store-protocol.md +35 -0
- package/skills/operate/SKILL.md +146 -19
- package/skills/tickets/SKILL.md +57 -5
|
@@ -18,58 +18,80 @@ import type {
|
|
|
18
18
|
BuildExecutionIdentity,
|
|
19
19
|
ExecutionObservation,
|
|
20
20
|
} from '../ports/workspace/build-execution'
|
|
21
|
+
import { defineReducer } from '../kernel/incremental'
|
|
21
22
|
import type { BuildStore } from '../store/types'
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
* `execution.ended`, or null when no execution is open. */
|
|
25
|
-
export function openExecution(events: AbEvent[]): {
|
|
24
|
+
export interface OpenExecution {
|
|
26
25
|
instance: string
|
|
27
26
|
workspaceRef: string
|
|
28
27
|
provider: string
|
|
29
28
|
environmentId?: string
|
|
30
29
|
sessionId?: string
|
|
31
30
|
commandId?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const OPEN_EXECUTION_REDUCER_VERSION = 1
|
|
34
|
+
|
|
35
|
+
/** The latest `execution.started` not followed by an instance-matched
|
|
36
|
+
* `execution.ended`, or null when no execution is open. */
|
|
37
|
+
export const openExecutionReducer = defineReducer<
|
|
38
|
+
{ open: OpenExecution | null },
|
|
39
|
+
AbEvent,
|
|
40
|
+
OpenExecution | null
|
|
41
|
+
>({
|
|
42
|
+
version: OPEN_EXECUTION_REDUCER_VERSION,
|
|
43
|
+
initial: () => ({ open: null }),
|
|
44
|
+
fold(acc, events) {
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
if (event.type === 'execution.started') {
|
|
47
|
+
const payload = event.payload
|
|
48
|
+
acc.open = {
|
|
49
|
+
instance: payload.instance,
|
|
50
|
+
workspaceRef: payload.workspaceRef,
|
|
51
|
+
provider: payload.provider,
|
|
52
|
+
...(payload.environmentId !== undefined ? { environmentId: payload.environmentId } : {}),
|
|
53
|
+
...(payload.sessionId !== undefined ? { sessionId: payload.sessionId } : {}),
|
|
54
|
+
...(payload.commandId !== undefined ? { commandId: payload.commandId } : {}),
|
|
55
|
+
}
|
|
56
|
+
} else if (event.type === 'execution.ended' && acc.open !== null) {
|
|
57
|
+
if (event.payload.instance === acc.open.instance) acc.open = null
|
|
51
58
|
}
|
|
52
|
-
} else if (event.type === 'execution.ended' && open !== null) {
|
|
53
|
-
if (event.payload.instance === open.instance) open = null
|
|
54
59
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
60
|
+
},
|
|
61
|
+
finish: (acc) => (acc.open === null ? null : { ...acc.open }),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export function openExecution(events: AbEvent[]): OpenExecution | null {
|
|
65
|
+
return openExecutionReducer.reduce(events)
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
export type ExecutionOutcome = 'open' | 'completed' | 'stopped' | 'lost' | 'none'
|
|
69
|
+
|
|
70
|
+
export const LAST_EXECUTION_OUTCOME_REDUCER_VERSION = 1
|
|
71
|
+
|
|
59
72
|
/** The lifecycle state of the latest recorded execution: `open` (no matching
|
|
60
73
|
* end yet), `completed`/`stopped`/`lost` (its recorded end), or `none`. */
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
export const lastExecutionOutcomeReducer = defineReducer<
|
|
75
|
+
{ state: ExecutionOutcome },
|
|
76
|
+
AbEvent,
|
|
77
|
+
ExecutionOutcome
|
|
78
|
+
>({
|
|
79
|
+
version: LAST_EXECUTION_OUTCOME_REDUCER_VERSION,
|
|
80
|
+
initial: () => ({ state: 'none' }),
|
|
81
|
+
fold(acc, events) {
|
|
82
|
+
for (const event of events) {
|
|
83
|
+
if (event.type === 'execution.started') {
|
|
84
|
+
acc.state = 'open'
|
|
85
|
+
} else if (event.type === 'execution.ended' && acc.state === 'open') {
|
|
86
|
+
acc.state = event.payload.outcome
|
|
87
|
+
}
|
|
70
88
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
89
|
+
},
|
|
90
|
+
finish: (acc) => acc.state,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export function lastExecutionOutcome(events: AbEvent[]): ExecutionOutcome {
|
|
94
|
+
return lastExecutionOutcomeReducer.reduce(events)
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
function observationIdentity(open: {
|
|
@@ -124,12 +146,23 @@ async function releaseSettledLease(
|
|
|
124
146
|
* Observation errors are contained and read as `running` (conservative: never
|
|
125
147
|
* reap on an unknown); the next tick retries.
|
|
126
148
|
*/
|
|
127
|
-
export
|
|
149
|
+
export function settleExecution(
|
|
128
150
|
deps: ExecutionSettlementDeps,
|
|
129
151
|
slug: string,
|
|
130
152
|
events: AbEvent[],
|
|
131
153
|
): Promise<ExecutionSettlement> {
|
|
132
|
-
|
|
154
|
+
return settleOpenExecution(deps, slug, openExecution(events))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `settleExecution` over an already-derived open execution, for callers that
|
|
158
|
+
* hold the projection (a reducer snapshot advanced to the log tail) and so
|
|
159
|
+
* never need the event array. A `running` result or an observation error
|
|
160
|
+
* touches no event rows. */
|
|
161
|
+
export async function settleOpenExecution(
|
|
162
|
+
deps: ExecutionSettlementDeps,
|
|
163
|
+
slug: string,
|
|
164
|
+
open: OpenExecution | null,
|
|
165
|
+
): Promise<ExecutionSettlement> {
|
|
133
166
|
if (open === null) return 'settled'
|
|
134
167
|
if (open.commandId === undefined || deps.execution.observe === undefined) return 'running'
|
|
135
168
|
let observation: ExecutionObservation
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* supervisor died.
|
|
9
9
|
*/
|
|
10
10
|
import type { RepositoryEvent } from '../events/repository'
|
|
11
|
+
import { defineReducer } from '../kernel/incremental'
|
|
11
12
|
import {
|
|
12
13
|
DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
|
|
13
14
|
actionableHarvestRun,
|
|
@@ -16,42 +17,53 @@ import {
|
|
|
16
17
|
} from '../kernel/harvest'
|
|
17
18
|
import type { HarvestRunnerResult } from './harvest-runner'
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
* a matching `harvest.execution.released`, in journal order. */
|
|
21
|
-
export function openHarvestExecutions(events: readonly RepositoryEvent[]): Array<{
|
|
20
|
+
export interface OpenHarvestExecution {
|
|
22
21
|
execution: string
|
|
23
22
|
provider: string
|
|
24
23
|
environmentId: string
|
|
25
24
|
sessionId?: string
|
|
26
25
|
commandId?: string
|
|
27
26
|
seq: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const OPEN_HARVEST_EXECUTIONS_REDUCER_VERSION = 1
|
|
30
|
+
|
|
31
|
+
/** Open harvest executions: every `harvest.execution.started` not followed by
|
|
32
|
+
* a matching `harvest.execution.released`, in journal order. The accumulator is
|
|
33
|
+
* an ordered array: a re-started execution keeps its original position, as it
|
|
34
|
+
* did in the `Map` this replaces. */
|
|
35
|
+
export const openHarvestExecutionsReducer = defineReducer<
|
|
36
|
+
{ open: OpenHarvestExecution[] },
|
|
37
|
+
RepositoryEvent,
|
|
38
|
+
OpenHarvestExecution[]
|
|
39
|
+
>({
|
|
40
|
+
version: OPEN_HARVEST_EXECUTIONS_REDUCER_VERSION,
|
|
41
|
+
initial: () => ({ open: [] }),
|
|
42
|
+
fold(acc, events) {
|
|
43
|
+
for (const event of events) {
|
|
44
|
+
if (event.type === 'harvest.execution.started') {
|
|
45
|
+
const entry: OpenHarvestExecution = {
|
|
46
|
+
execution: event.payload.execution,
|
|
47
|
+
provider: event.payload.provider,
|
|
48
|
+
environmentId: event.payload.environmentId,
|
|
49
|
+
...(event.payload.sessionId !== undefined ? { sessionId: event.payload.sessionId } : {}),
|
|
50
|
+
...(event.payload.commandId !== undefined ? { commandId: event.payload.commandId } : {}),
|
|
51
|
+
seq: event.seq,
|
|
52
|
+
}
|
|
53
|
+
const index = acc.open.findIndex((item) => item.execution === entry.execution)
|
|
54
|
+
if (index === -1) acc.open.push(entry)
|
|
55
|
+
else acc.open[index] = entry
|
|
56
|
+
} else if (event.type === 'harvest.execution.released') {
|
|
57
|
+
const index = acc.open.findIndex((item) => item.execution === event.payload.execution)
|
|
58
|
+
if (index !== -1) acc.open.splice(index, 1)
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
|
-
}
|
|
54
|
-
|
|
61
|
+
},
|
|
62
|
+
finish: (acc) => acc.open.map((item) => ({ ...item })),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export function openHarvestExecutions(events: readonly RepositoryEvent[]): OpenHarvestExecution[] {
|
|
66
|
+
return openHarvestExecutionsReducer.reduce(events)
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
export interface HarvestExecutionClassificationInput {
|
|
@@ -2,6 +2,7 @@ import type { AbEvent } from '../events/catalog'
|
|
|
2
2
|
import { DISPATCHER } from '../events/envelope'
|
|
3
3
|
import type { EventPayload } from '../events/payloads'
|
|
4
4
|
import type { IdSource } from '../ids'
|
|
5
|
+
import { defineReducer } from '../kernel/incremental'
|
|
5
6
|
import type { BuildStore } from '../store/types'
|
|
6
7
|
|
|
7
8
|
export type InfrastructureOperation = EventPayload<'infrastructure.failed'>['operation']
|
|
@@ -26,27 +27,39 @@ export interface InfrastructureFailureDependencies {
|
|
|
26
27
|
ids: IdSource
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export const INFRASTRUCTURE_FAILURE_RESET_REDUCER_VERSION = 1
|
|
31
|
+
|
|
29
32
|
/** Infrastructure retries are re-armed only by a confirmed execution boundary
|
|
30
33
|
* or a retry answer to this policy's own previously-raised exhaustion escalation. */
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
export const infrastructureFailureResetReducer = defineReducer<
|
|
35
|
+
{ escalations: string[]; resetSeq: number },
|
|
36
|
+
AbEvent,
|
|
37
|
+
number
|
|
38
|
+
>({
|
|
39
|
+
version: INFRASTRUCTURE_FAILURE_RESET_REDUCER_VERSION,
|
|
40
|
+
initial: () => ({ escalations: [], resetSeq: 0 }),
|
|
41
|
+
fold(acc, events) {
|
|
42
|
+
for (const event of events) {
|
|
43
|
+
if (
|
|
44
|
+
event.type === 'escalation.raised' &&
|
|
45
|
+
event.payload.policyCause === 'infrastructure-failure-limit'
|
|
46
|
+
) {
|
|
47
|
+
if (!acc.escalations.includes(event.payload.id)) acc.escalations.push(event.payload.id)
|
|
48
|
+
} else if (
|
|
49
|
+
event.type === 'execution.ended' ||
|
|
50
|
+
(event.type === 'escalation.answered' &&
|
|
51
|
+
event.payload.resolution === 'retry' &&
|
|
52
|
+
acc.escalations.includes(event.payload.id))
|
|
53
|
+
) {
|
|
54
|
+
acc.resetSeq = event.seq
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
|
-
}
|
|
49
|
-
|
|
57
|
+
},
|
|
58
|
+
finish: (acc) => acc.resetSeq,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
export function infrastructureFailureResetSeq(events: readonly AbEvent[]): number {
|
|
62
|
+
return infrastructureFailureResetReducer.reduce(events)
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
export function normalizeInfrastructureError(error: unknown): string {
|
|
@@ -31,6 +31,7 @@ import { reduceSession, type SessionTurnTrigger } from '../store/session-reducer
|
|
|
31
31
|
import { reduceBuild } from '../kernel/reducer'
|
|
32
32
|
import type { AbEvent } from '../events/catalog'
|
|
33
33
|
import type { BuildStore, Clock } from '../store/types'
|
|
34
|
+
import type { RepoViewStore } from './repo-view'
|
|
34
35
|
import type { IdSource } from '../ids'
|
|
35
36
|
import type { LanguageModel } from 'ai'
|
|
36
37
|
import {
|
|
@@ -69,6 +70,10 @@ export interface OrchestratorTickOptions {
|
|
|
69
70
|
* Less than ORCHESTRATOR_MIN_TURN_SECONDS skips the whole step
|
|
70
71
|
* (suspension state is durable — skipping is safe). */
|
|
71
72
|
remainingBudgetSeconds?: number
|
|
73
|
+
/** The dispatcher's repository view, when the tick runs under one: the wake
|
|
74
|
+
* pass reads its full build state through it (no replay of history the view
|
|
75
|
+
* already folded) and releases consumed window history through it. */
|
|
76
|
+
view?: Pick<RepoViewStore, 'buildState' | 'releaseWindowsBelow'>
|
|
72
77
|
log?: (message: string) => void
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -133,6 +138,9 @@ export async function runOrchestratorTickStep(
|
|
|
133
138
|
const sessions = await store.listSessions(repo)
|
|
134
139
|
|
|
135
140
|
// ── Resume pass ─────────────────────────────────────────────────────────
|
|
141
|
+
// The lowest cursors only cover sessions visited; a budget break leaves later
|
|
142
|
+
// sessions (possibly with lower cursors) uncounted, so it skips the trim.
|
|
143
|
+
let visitedAll = true
|
|
136
144
|
for (const record of sessions) {
|
|
137
145
|
const events = await store.getSessionEvents(record.id)
|
|
138
146
|
const state = reduceSession(events)
|
|
@@ -202,9 +210,28 @@ export async function runOrchestratorTickStep(
|
|
|
202
210
|
|
|
203
211
|
// ── Wake pass ───────────────────────────────────────────────────────────
|
|
204
212
|
const builds = (await store.listBuilds()).filter((candidate) => candidate.repo === repo)
|
|
213
|
+
/** The lowest wake cursor across every session that scans, per source: the
|
|
214
|
+
* view may drop window history at or below it once the pass ends. */
|
|
215
|
+
const lowestBuildCursor = new Map<string, number>()
|
|
216
|
+
let lowestJournalCursor: number | undefined
|
|
217
|
+
let scanners = 0
|
|
205
218
|
for (const record of sessions) {
|
|
206
219
|
const events = await store.getSessionEvents(record.id)
|
|
207
220
|
const state = reduceSession(events)
|
|
221
|
+
if (state.wakeGlobs.length > 0) {
|
|
222
|
+
scanners += 1
|
|
223
|
+
for (const build of builds) {
|
|
224
|
+
const cursor = state.wakeCursors[build.slug] ?? 0
|
|
225
|
+
lowestBuildCursor.set(
|
|
226
|
+
build.slug,
|
|
227
|
+
Math.min(lowestBuildCursor.get(build.slug) ?? cursor, cursor),
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
lowestJournalCursor = Math.min(
|
|
231
|
+
lowestJournalCursor ?? state.journalWakeCursor,
|
|
232
|
+
state.journalWakeCursor,
|
|
233
|
+
)
|
|
234
|
+
}
|
|
208
235
|
if (state.status !== 'idle') continue
|
|
209
236
|
if (state.wakeGlobs.length === 0) continue
|
|
210
237
|
let filters: RegExp[]
|
|
@@ -226,7 +253,7 @@ export async function runOrchestratorTickStep(
|
|
|
226
253
|
| undefined
|
|
227
254
|
for (const build of builds) {
|
|
228
255
|
const cursor = state.wakeCursors[build.slug] ?? 0
|
|
229
|
-
const buildEvents = await store.getEvents(build.slug)
|
|
256
|
+
const buildEvents = await store.getEvents(build.slug, cursor)
|
|
230
257
|
for (const event of buildEvents) {
|
|
231
258
|
if (event.seq <= cursor) continue
|
|
232
259
|
if (!filters.some((regex) => regex.test(event.type))) continue
|
|
@@ -262,7 +289,10 @@ export async function runOrchestratorTickStep(
|
|
|
262
289
|
// by the step's shared deadline; past the floor the wake defers to a
|
|
263
290
|
// later tick.
|
|
264
291
|
const remaining = remainingNow() - ORCHESTRATOR_MIN_TURN_SECONDS
|
|
265
|
-
if (remaining < ORCHESTRATOR_MIN_TURN_SECONDS)
|
|
292
|
+
if (remaining < ORCHESTRATOR_MIN_TURN_SECONDS) {
|
|
293
|
+
visitedAll = false
|
|
294
|
+
break
|
|
295
|
+
}
|
|
266
296
|
|
|
267
297
|
// A journal wake delivers the frozen event record only — the turn's
|
|
268
298
|
// operator registry already exposes bounded repository reads, so no
|
|
@@ -283,7 +313,9 @@ export async function runOrchestratorTickStep(
|
|
|
283
313
|
? { event }
|
|
284
314
|
: {
|
|
285
315
|
event,
|
|
286
|
-
buildState:
|
|
316
|
+
buildState: (options.view !== undefined
|
|
317
|
+
? await options.view.buildState(newest.build)
|
|
318
|
+
: reduceBuild(await store.getEvents(newest.build))) as unknown as Record<
|
|
287
319
|
string,
|
|
288
320
|
unknown
|
|
289
321
|
>,
|
|
@@ -301,6 +333,15 @@ export async function runOrchestratorTickStep(
|
|
|
301
333
|
}
|
|
302
334
|
}
|
|
303
335
|
|
|
336
|
+
if (options.view !== undefined && scanners > 0 && visitedAll) {
|
|
337
|
+
for (const [slug, cursor] of lowestBuildCursor) {
|
|
338
|
+
if (cursor > 0) options.view.releaseWindowsBelow({ build: slug }, cursor)
|
|
339
|
+
}
|
|
340
|
+
if (lowestJournalCursor !== undefined && lowestJournalCursor > 0) {
|
|
341
|
+
options.view.releaseWindowsBelow('journal', lowestJournalCursor)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
304
345
|
options.log?.(
|
|
305
346
|
`orchestrator step: resumed=${report.resumed} reaped=${report.reaped} woken=${report.woken}`,
|
|
306
347
|
)
|
|
@@ -1,32 +1,170 @@
|
|
|
1
1
|
import type { AbEvent } from '../events/catalog'
|
|
2
|
+
import { defineReducer, type IncrementalReducer } from '../kernel/incremental'
|
|
2
3
|
|
|
3
4
|
export type PublicationRequest = Extract<AbEvent, { type: 'publication.requested' }>
|
|
4
5
|
|
|
6
|
+
/** One entry of the order-preserving publication ledger: the fields the
|
|
7
|
+
* publication selectors read from each relevant event, in array order. */
|
|
8
|
+
type LedgerItem =
|
|
9
|
+
| { t: 'request'; seq: number; event: PublicationRequest }
|
|
10
|
+
| { t: 'lost'; seq: number; request: number }
|
|
11
|
+
| { t: 'provisioned'; seq: number; ref: string }
|
|
12
|
+
| { t: 'released'; seq: number; ref?: string | null }
|
|
13
|
+
| { t: 'implement'; seq: number; round: number; base: string; head: string }
|
|
14
|
+
| { t: 'reconcile'; seq: number; mergeCommit: string }
|
|
15
|
+
| { t: 'finalize'; seq: number; headSha: string }
|
|
16
|
+
| { t: 'step'; seq: number; step: string; ok: boolean; headSha?: string }
|
|
17
|
+
/** A maximal run of consecutive irrelevant events. Only its max seq
|
|
18
|
+
* matters: the workspace scan's `seq > request.seq` break test. */
|
|
19
|
+
| { t: 'barrier'; seq: number }
|
|
20
|
+
|
|
21
|
+
/** The publication family's accumulator: the whole event sequence in array
|
|
22
|
+
* order, relevant events compacted and each run of irrelevant ones collapsed
|
|
23
|
+
* to a barrier. Filtering to relevant types alone would be wrong, because
|
|
24
|
+
* `publicationRequestSettled` stops its workspace scan at the first event (of
|
|
25
|
+
* any type) whose seq exceeds the request's. */
|
|
26
|
+
export interface PublicationLedger {
|
|
27
|
+
ledger: LedgerItem[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Bump when `PublicationLedger` or its fold changes. */
|
|
31
|
+
export const PUBLICATION_STATE_REDUCER_VERSION = 1
|
|
32
|
+
|
|
33
|
+
function compact(event: AbEvent): LedgerItem | undefined {
|
|
34
|
+
switch (event.type) {
|
|
35
|
+
case 'publication.requested':
|
|
36
|
+
return { t: 'request', seq: event.seq, event }
|
|
37
|
+
case 'publication.lost':
|
|
38
|
+
return { t: 'lost', seq: event.seq, request: event.payload.request }
|
|
39
|
+
case 'workspace.provisioned':
|
|
40
|
+
return { t: 'provisioned', seq: event.seq, ref: event.payload.ref }
|
|
41
|
+
case 'workspace.released': {
|
|
42
|
+
const payload = event.payload as { ref?: string }
|
|
43
|
+
return 'ref' in payload
|
|
44
|
+
? { t: 'released', seq: event.seq, ref: payload.ref ?? null }
|
|
45
|
+
: { t: 'released', seq: event.seq }
|
|
46
|
+
}
|
|
47
|
+
case 'implement.completed':
|
|
48
|
+
return {
|
|
49
|
+
t: 'implement',
|
|
50
|
+
seq: event.seq,
|
|
51
|
+
round: event.payload.round,
|
|
52
|
+
base: event.payload.commits.base,
|
|
53
|
+
head: event.payload.commits.head,
|
|
54
|
+
}
|
|
55
|
+
case 'reconcile.completed':
|
|
56
|
+
return { t: 'reconcile', seq: event.seq, mergeCommit: event.payload.mergeCommit }
|
|
57
|
+
case 'finalize.completed':
|
|
58
|
+
return { t: 'finalize', seq: event.seq, headSha: event.payload.pr.headSha }
|
|
59
|
+
case 'finalize.step-completed': {
|
|
60
|
+
const item: LedgerItem = {
|
|
61
|
+
t: 'step',
|
|
62
|
+
seq: event.seq,
|
|
63
|
+
step: event.payload.step,
|
|
64
|
+
ok: event.payload.ok,
|
|
65
|
+
}
|
|
66
|
+
const headSha = (event.payload as { headSha?: string }).headSha
|
|
67
|
+
if (headSha !== undefined) item.headSha = headSha
|
|
68
|
+
return item
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function itemCompletes(item: LedgerItem, request: PublicationRequest): boolean {
|
|
76
|
+
if (item.seq <= request.seq) return false
|
|
77
|
+
const payload = request.payload
|
|
78
|
+
if (payload.operation === 'implement')
|
|
79
|
+
return (
|
|
80
|
+
item.t === 'implement' &&
|
|
81
|
+
item.round === payload.round &&
|
|
82
|
+
item.base === payload.base &&
|
|
83
|
+
item.head === payload.sha
|
|
84
|
+
)
|
|
85
|
+
if (payload.operation === 'reconcile')
|
|
86
|
+
return item.t === 'reconcile' && item.mergeCommit === payload.sha
|
|
87
|
+
if (payload.operation === 'finalize') return item.t === 'finalize' && item.headSha === payload.sha
|
|
88
|
+
return (
|
|
89
|
+
item.t === 'step' && item.step === payload.step && (!item.ok || item.headSha === payload.sha)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The publication selectors, derived from one ledger. */
|
|
94
|
+
export interface PublicationView {
|
|
95
|
+
requestCompleted(request: PublicationRequest): boolean
|
|
96
|
+
requestSettled(request: PublicationRequest): boolean
|
|
97
|
+
latestUncompletedRequest(): PublicationRequest | undefined
|
|
98
|
+
lostRecorded(request: PublicationRequest): boolean
|
|
99
|
+
abandonedPending(): boolean
|
|
100
|
+
pending(): boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function viewOf(ledger: readonly LedgerItem[]): PublicationView {
|
|
104
|
+
const requestCompleted = (request: PublicationRequest): boolean =>
|
|
105
|
+
ledger.some((item) => itemCompletes(item, request))
|
|
106
|
+
|
|
107
|
+
const requestSettled = (request: PublicationRequest): boolean => {
|
|
108
|
+
let workspaceRef: string | undefined
|
|
109
|
+
for (const item of ledger) {
|
|
110
|
+
if (item.seq > request.seq) break
|
|
111
|
+
if (item.t === 'provisioned') workspaceRef = item.ref
|
|
112
|
+
else if (item.t === 'released') workspaceRef = undefined
|
|
113
|
+
}
|
|
114
|
+
return (
|
|
115
|
+
requestCompleted(request) ||
|
|
116
|
+
ledger.some(
|
|
117
|
+
(item) =>
|
|
118
|
+
item.seq > request.seq &&
|
|
119
|
+
item.t === 'released' &&
|
|
120
|
+
(item.ref === undefined || workspaceRef === undefined || item.ref === workspaceRef),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const latestUncompletedRequest = (): PublicationRequest | undefined => {
|
|
126
|
+
const item = ledger.findLast((entry) => entry.t === 'request' && !requestCompleted(entry.event))
|
|
127
|
+
return item?.t === 'request' ? item.event : undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
requestCompleted,
|
|
132
|
+
requestSettled,
|
|
133
|
+
latestUncompletedRequest,
|
|
134
|
+
lostRecorded: (request) =>
|
|
135
|
+
ledger.some((item) => item.t === 'lost' && item.request === request.seq),
|
|
136
|
+
abandonedPending() {
|
|
137
|
+
const request = latestUncompletedRequest()
|
|
138
|
+
return request !== undefined && requestSettled(request)
|
|
139
|
+
},
|
|
140
|
+
pending: () => ledger.some((item) => item.t === 'request' && !requestSettled(item.event)),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const publicationStateReducer: IncrementalReducer<
|
|
145
|
+
PublicationLedger,
|
|
146
|
+
AbEvent,
|
|
147
|
+
PublicationView
|
|
148
|
+
> = defineReducer<PublicationLedger, AbEvent, PublicationView>({
|
|
149
|
+
version: PUBLICATION_STATE_REDUCER_VERSION,
|
|
150
|
+
initial: () => ({ ledger: [] }),
|
|
151
|
+
fold(acc, events) {
|
|
152
|
+
for (const event of events) {
|
|
153
|
+
const item = compact(event)
|
|
154
|
+
const last = acc.ledger[acc.ledger.length - 1]
|
|
155
|
+
if (item !== undefined) acc.ledger.push(item)
|
|
156
|
+
else if (last?.t === 'barrier') last.seq = Math.max(last.seq, event.seq)
|
|
157
|
+
else acc.ledger.push({ t: 'barrier', seq: event.seq })
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
finish: (acc) => viewOf(acc.ledger),
|
|
161
|
+
})
|
|
162
|
+
|
|
5
163
|
export function publicationRequestCompleted(
|
|
6
164
|
events: readonly AbEvent[],
|
|
7
165
|
request: PublicationRequest,
|
|
8
166
|
): boolean {
|
|
9
|
-
return events.
|
|
10
|
-
if (event.seq <= request.seq) return false
|
|
11
|
-
if (request.payload.operation === 'implement')
|
|
12
|
-
return (
|
|
13
|
-
event.type === 'implement.completed' &&
|
|
14
|
-
event.payload.round === request.payload.round &&
|
|
15
|
-
event.payload.commits.base === request.payload.base &&
|
|
16
|
-
event.payload.commits.head === request.payload.sha
|
|
17
|
-
)
|
|
18
|
-
if (request.payload.operation === 'reconcile')
|
|
19
|
-
return (
|
|
20
|
-
event.type === 'reconcile.completed' && event.payload.mergeCommit === request.payload.sha
|
|
21
|
-
)
|
|
22
|
-
if (request.payload.operation === 'finalize')
|
|
23
|
-
return event.type === 'finalize.completed' && event.payload.pr.headSha === request.payload.sha
|
|
24
|
-
return (
|
|
25
|
-
event.type === 'finalize.step-completed' &&
|
|
26
|
-
event.payload.step === request.payload.step &&
|
|
27
|
-
(!event.payload.ok || event.payload.headSha === request.payload.sha)
|
|
28
|
-
)
|
|
29
|
-
})
|
|
167
|
+
return publicationStateReducer.reduce(events).requestCompleted(request)
|
|
30
168
|
}
|
|
31
169
|
|
|
32
170
|
/** A request is no longer publishable after its workspace is released. The
|
|
@@ -36,23 +174,7 @@ export function publicationRequestSettled(
|
|
|
36
174
|
events: readonly AbEvent[],
|
|
37
175
|
request: PublicationRequest,
|
|
38
176
|
): boolean {
|
|
39
|
-
|
|
40
|
-
for (const event of events) {
|
|
41
|
-
if (event.seq > request.seq) break
|
|
42
|
-
if (event.type === 'workspace.provisioned') workspaceRef = event.payload.ref
|
|
43
|
-
else if (event.type === 'workspace.released') workspaceRef = undefined
|
|
44
|
-
}
|
|
45
|
-
return (
|
|
46
|
-
publicationRequestCompleted(events, request) ||
|
|
47
|
-
events.some(
|
|
48
|
-
(event) =>
|
|
49
|
-
event.seq > request.seq &&
|
|
50
|
-
event.type === 'workspace.released' &&
|
|
51
|
-
(!('ref' in event.payload) ||
|
|
52
|
-
workspaceRef === undefined ||
|
|
53
|
-
event.payload.ref === workspaceRef),
|
|
54
|
-
)
|
|
55
|
-
)
|
|
177
|
+
return publicationStateReducer.reduce(events).requestSettled(request)
|
|
56
178
|
}
|
|
57
179
|
|
|
58
180
|
/** The latest `publication.requested` with no settling completion fact — the
|
|
@@ -61,11 +183,7 @@ export function publicationRequestSettled(
|
|
|
61
183
|
export function latestUncompletedPublicationRequest(
|
|
62
184
|
events: readonly AbEvent[],
|
|
63
185
|
): PublicationRequest | undefined {
|
|
64
|
-
|
|
65
|
-
(event) =>
|
|
66
|
-
event.type === 'publication.requested' && !publicationRequestCompleted(events, event),
|
|
67
|
-
)
|
|
68
|
-
return request?.type === 'publication.requested' ? request : undefined
|
|
186
|
+
return publicationStateReducer.reduce(events).latestUncompletedRequest()
|
|
69
187
|
}
|
|
70
188
|
|
|
71
189
|
/** Whether the durable loss record (AUT-328) has already been appended for
|
|
@@ -75,19 +193,13 @@ export function publicationLostRecorded(
|
|
|
75
193
|
events: readonly AbEvent[],
|
|
76
194
|
request: PublicationRequest,
|
|
77
195
|
): boolean {
|
|
78
|
-
return events.
|
|
79
|
-
(event) => event.type === 'publication.lost' && event.payload.request === request.seq,
|
|
80
|
-
)
|
|
196
|
+
return publicationStateReducer.reduce(events).lostRecorded(request)
|
|
81
197
|
}
|
|
82
198
|
|
|
83
199
|
export function abandonedPublicationPending(events: readonly AbEvent[]): boolean {
|
|
84
|
-
|
|
85
|
-
return request !== undefined && publicationRequestSettled(events, request)
|
|
200
|
+
return publicationStateReducer.reduce(events).abandonedPending()
|
|
86
201
|
}
|
|
87
202
|
|
|
88
203
|
export function publicationPending(events: readonly AbEvent[]): boolean {
|
|
89
|
-
return events.
|
|
90
|
-
(request) =>
|
|
91
|
-
request.type === 'publication.requested' && !publicationRequestSettled(events, request),
|
|
92
|
-
)
|
|
204
|
+
return publicationStateReducer.reduce(events).pending()
|
|
93
205
|
}
|