@defrex/autobuild 0.13.0 → 0.14.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 +1 -1
- package/SPEC.md +19 -1
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +96 -70
- package/docs/hosted-service.md +2 -2
- package/docs/operator-api.md +1 -1
- package/docs/remote-store-protocol.md +84 -2
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +11 -15
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/context.ts +16 -78
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +148 -79
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +71 -39
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/main.ts +3 -3
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket.ts +2 -2
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +1 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2429 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +137 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +2 -0
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/tickets.ts +2 -2
- package/packages/core/src/plugin-sdk/index.ts +13 -0
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +113 -173
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +34 -70
- package/packages/core/src/processes/harvest.ts +38 -15
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1134
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +251 -0
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +179 -54
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-assets.ts +4 -4
- package/packages/core/src/store/types.ts +39 -2
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +6 -1
- package/skills/guide/references/operator-api.md +1 -1
- package/skills/guide/references/remote-store-protocol.md +84 -2
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { readBuildDigests } from '../store/reduced-reads'
|
|
2
|
+
import { readBuildFacts, type CurrentBuildFacts } from '../store/reduced-reads'
|
|
1
3
|
import { ensureExecutionProgress } from './execution-no-progress'
|
|
2
4
|
import { ticketIdentity } from '../ports/tickets/identity'
|
|
3
5
|
import { ticketLifecycle } from '../ports/tickets/lifecycle'
|
|
4
6
|
import { ensureDispatchBudget } from './dispatch-failure-budget'
|
|
5
|
-
import {
|
|
6
|
-
reduceTicketSourceHealth,
|
|
7
|
-
type TicketSourceDegradation,
|
|
8
|
-
} from '../kernel/ticket-source-health'
|
|
7
|
+
import type { TicketSourceDegradation } from '../kernel/ticket-source-health'
|
|
9
8
|
/**
|
|
10
9
|
* The dispatcher process (SPEC §3.3, §12): watches the TicketSource for
|
|
11
10
|
* Ready tickets, claims, provisions a workspace, and launches build-runners
|
|
@@ -60,21 +59,18 @@ import {
|
|
|
60
59
|
} from '../kernel/auto-merge'
|
|
61
60
|
import {
|
|
62
61
|
autoMergeDefaultTarget,
|
|
63
|
-
|
|
62
|
+
pickAutoMergeDefault,
|
|
64
63
|
type AutoMergeDefaultFact,
|
|
65
64
|
} from '../kernel/auto-merge-default'
|
|
66
|
-
import { reduceDispatchSettings } from '../kernel/dispatch-settings'
|
|
67
65
|
import { decideNextFromFacts } from '../kernel/engine'
|
|
68
66
|
import { composeBuildConfig } from '../config/live'
|
|
69
67
|
import { BUILD_EFFECTIVE_CONFIG_ARTIFACT, parseEffectiveBuildConfig } from './build-execution-state'
|
|
70
68
|
import {
|
|
71
69
|
DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
|
|
72
70
|
decideHarvestControl,
|
|
73
|
-
|
|
74
|
-
reduceHarvest,
|
|
71
|
+
harvestCreationsFromStates,
|
|
75
72
|
} from '../kernel/harvest'
|
|
76
|
-
import {
|
|
77
|
-
import { reduceBuild, type BuildState } from '../kernel/reducer'
|
|
73
|
+
import type { BuildState } from '../kernel/reducer'
|
|
78
74
|
import { createOperatorSandboxService, type OperatorSandboxService } from '../operator/sandbox'
|
|
79
75
|
import type { ArtifactRef, TicketRef } from '../ontology'
|
|
80
76
|
import { BUILD_EXECUTION_LEASE_TTL_MS } from '../ports/workspace/build-execution'
|
|
@@ -94,19 +90,9 @@ import { specConformance } from '../spec-standard'
|
|
|
94
90
|
export { specConformance, type SpecConformance } from '../spec-standard'
|
|
95
91
|
import { recordInfrastructureFailure as appendInfrastructureFailure } from './infrastructure-failure-budget'
|
|
96
92
|
import { settleOpenExecution } from './execution-settlement'
|
|
97
|
-
import { openHarvestExecutions } from './harvest-execution-state'
|
|
98
93
|
import { isWorkDigest, RepoViewStore } from './repo-view'
|
|
99
|
-
import {
|
|
100
|
-
|
|
101
|
-
openBuildWorkspace,
|
|
102
|
-
provisionMarkerReducer,
|
|
103
|
-
recoveryCheckpointReducer,
|
|
104
|
-
type OpenBuildWorkspace,
|
|
105
|
-
type ProvisionMarker,
|
|
106
|
-
} from './dispatcher-selectors'
|
|
107
|
-
import type { RepositoryEvent } from '../events/repository'
|
|
108
|
-
import { sandboxStates } from './sandbox-state'
|
|
109
|
-
import { settlePublicationBeforeRelease } from './publication-loss'
|
|
94
|
+
import type { OpenBuildWorkspace, ProvisionMarker } from './dispatcher-selectors'
|
|
95
|
+
import { settlePublicationFactsBeforeRelease } from './publication-loss'
|
|
110
96
|
|
|
111
97
|
// ── Readiness resolution (SPEC §3.3) ─────────────────────────────────────────
|
|
112
98
|
|
|
@@ -611,14 +597,14 @@ export interface DispatcherDeps {
|
|
|
611
597
|
/** Latest `workspace.provisioned` not followed by a `workspace.released` —
|
|
612
598
|
* the reducer deliberately ignores workspace events (liveness is the
|
|
613
599
|
* dispatcher's concern), so the janitor scans the raw log. */
|
|
614
|
-
function openWorkspace(events:
|
|
615
|
-
return
|
|
600
|
+
function openWorkspace(events: CurrentBuildFacts): OpenBuildWorkspace | null {
|
|
601
|
+
return events.workspace
|
|
616
602
|
}
|
|
617
603
|
|
|
618
604
|
/** The build's base branch, from its own `build.created` fact (§15.3);
|
|
619
605
|
* falls back to the repo config for logs missing one. */
|
|
620
|
-
function baseBranchOf(events:
|
|
621
|
-
return
|
|
606
|
+
function baseBranchOf(events: CurrentBuildFacts, config: Config): string {
|
|
607
|
+
return events.baseBranch ?? config.baseBranch
|
|
622
608
|
}
|
|
623
609
|
|
|
624
610
|
function artifactRefOf(deposited: ArtifactMeta[]): ArtifactRef {
|
|
@@ -759,14 +745,7 @@ export class Dispatcher {
|
|
|
759
745
|
}
|
|
760
746
|
|
|
761
747
|
async tick(opts: TickOpts = {}): Promise<TickReport> {
|
|
762
|
-
|
|
763
|
-
return await this.runTick(opts)
|
|
764
|
-
} finally {
|
|
765
|
-
// Leave this tick's reduced state behind for the next process (a cache;
|
|
766
|
-
// a failure to write it is never a tick failure). Throttled in a
|
|
767
|
-
// resident dispatcher, always run by a cold single-tick process.
|
|
768
|
-
await this.store.persistSnapshots()
|
|
769
|
-
}
|
|
748
|
+
return this.runTick(opts)
|
|
770
749
|
}
|
|
771
750
|
|
|
772
751
|
private async runTick(opts: TickOpts): Promise<TickReport> {
|
|
@@ -791,9 +770,9 @@ export class Dispatcher {
|
|
|
791
770
|
// likewise samples before calling `tick`); the auto-merge default fact is
|
|
792
771
|
// the durable driver of this tick's fan-out below. Neither control
|
|
793
772
|
// pretends to a serialization the store does not offer.
|
|
794
|
-
const
|
|
795
|
-
const paused =
|
|
796
|
-
const autoMergeDefault =
|
|
773
|
+
const repoFacts = this.store.repositoryFacts()
|
|
774
|
+
const paused = repoFacts.settings.paused
|
|
775
|
+
const autoMergeDefault = pickAutoMergeDefault(repoFacts.autoMerge)
|
|
797
776
|
// The claim-time seed cites the newest fact whose enabled state matches
|
|
798
777
|
// the launch sample, not blindly the newest fact: the CLI samples
|
|
799
778
|
// `defaultAutoMerge` from its own earlier read, so an interleaved toggle
|
|
@@ -802,7 +781,7 @@ export class Dispatcher {
|
|
|
802
781
|
// so the fan-out reconciles the build on the next tick instead of the
|
|
803
782
|
// stale sample out-voting the durable default forever (f_b851c0e8).
|
|
804
783
|
const autoMergeSeedFact =
|
|
805
|
-
opts.defaultAutoMerge === true ?
|
|
784
|
+
opts.defaultAutoMerge === true ? pickAutoMergeDefault(repoFacts.autoMerge, true) : undefined
|
|
806
785
|
// Stage 0 — SETTLEMENT: observe foreign executions from durable facts plus
|
|
807
786
|
// provider liveness and settle their completion, lease, and publication.
|
|
808
787
|
// Runs before the janitor so its executionLeaseLive checks see freshly
|
|
@@ -916,7 +895,7 @@ export class Dispatcher {
|
|
|
916
895
|
const store = this.store
|
|
917
896
|
const { repo } = this.deps
|
|
918
897
|
const records = (await store.listBuilds()).filter((record) => record.repo === repo)
|
|
919
|
-
const digests = await store
|
|
898
|
+
const digests = await readBuildDigests(store, repo)
|
|
920
899
|
return records.map((record) => {
|
|
921
900
|
const digest = digests.get(record.slug)
|
|
922
901
|
if (digest === undefined) {
|
|
@@ -952,7 +931,7 @@ export class Dispatcher {
|
|
|
952
931
|
successfulOperation?: 'ready-scan' | 'dependency-check',
|
|
953
932
|
): Promise<void> {
|
|
954
933
|
await this.store.refreshJournal()
|
|
955
|
-
const current =
|
|
934
|
+
const current = this.store.repositoryFacts().health
|
|
956
935
|
if (next !== undefined && current?.cause === next.cause && current.source === next.source) {
|
|
957
936
|
if (
|
|
958
937
|
next.cause === 'rate-limited' &&
|
|
@@ -1082,12 +1061,7 @@ export class Dispatcher {
|
|
|
1082
1061
|
}
|
|
1083
1062
|
|
|
1084
1063
|
private latestScan() {
|
|
1085
|
-
return this.store
|
|
1086
|
-
.recordedJournal()
|
|
1087
|
-
.findLast(
|
|
1088
|
-
(event): event is Extract<RepositoryEvent, { type: 'dispatcher.ticket-scan-completed' }> =>
|
|
1089
|
-
event.type === 'dispatcher.ticket-scan-completed',
|
|
1090
|
-
)?.payload
|
|
1064
|
+
return this.store.repositoryFacts().scan
|
|
1091
1065
|
}
|
|
1092
1066
|
|
|
1093
1067
|
private restoreScan(report: TickReport): void {
|
|
@@ -1219,7 +1193,7 @@ export class Dispatcher {
|
|
|
1219
1193
|
return
|
|
1220
1194
|
}
|
|
1221
1195
|
// Bounded subset (AUT-489): the harvest reducer consumes durable types only.
|
|
1222
|
-
const state =
|
|
1196
|
+
const state = this.store.repositoryFacts().harvest
|
|
1223
1197
|
if (decideHarvestControl(state, this.maxHarvestRecoveryAttempts).kind !== 'park') {
|
|
1224
1198
|
start()
|
|
1225
1199
|
}
|
|
@@ -1305,13 +1279,13 @@ export class Dispatcher {
|
|
|
1305
1279
|
* repository lease. */
|
|
1306
1280
|
private async settleHarvestExecutions(opts: TickOpts = {}): Promise<void> {
|
|
1307
1281
|
const active = this.deps.activeHarvestExecutions?.() ?? new Set<string>()
|
|
1308
|
-
let
|
|
1282
|
+
let facts: ReturnType<RepoViewStore['repositoryFacts']>
|
|
1309
1283
|
try {
|
|
1310
1284
|
// The earlier stages awaited providers; take the journal delta so a
|
|
1311
1285
|
// foreign append (operator activity, harvest facts) made meanwhile is
|
|
1312
1286
|
// seen before any destructive decision below.
|
|
1313
1287
|
await this.store.refreshJournal()
|
|
1314
|
-
|
|
1288
|
+
facts = this.store.repositoryFacts()
|
|
1315
1289
|
} catch {
|
|
1316
1290
|
// The read itself failed (a transient store error — a missing record
|
|
1317
1291
|
// answers `[]` inside the helper, so it never reaches this arm):
|
|
@@ -1319,7 +1293,7 @@ export class Dispatcher {
|
|
|
1319
1293
|
// tick proceeds, and the next tick retries what remains.
|
|
1320
1294
|
return
|
|
1321
1295
|
}
|
|
1322
|
-
for (const execution of
|
|
1296
|
+
for (const execution of facts.openHarvest) {
|
|
1323
1297
|
if (active.has(execution.execution)) continue
|
|
1324
1298
|
// Budget gate: once spent, stop settling further harvest executions;
|
|
1325
1299
|
// the next tick retries what remains.
|
|
@@ -1408,13 +1382,13 @@ export class Dispatcher {
|
|
|
1408
1382
|
const capability = this.deps.workspaces.orchestratorSandbox
|
|
1409
1383
|
if (capability === undefined) return
|
|
1410
1384
|
const providerName = this.deps.workspaces.name
|
|
1411
|
-
let
|
|
1385
|
+
let facts: ReturnType<RepoViewStore['repositoryFacts']>
|
|
1412
1386
|
try {
|
|
1413
1387
|
// The earlier stages awaited providers; take the journal delta so a
|
|
1414
1388
|
// foreign append (operator activity, harvest facts) made meanwhile is
|
|
1415
1389
|
// seen before any destructive decision below.
|
|
1416
1390
|
await this.store.refreshJournal()
|
|
1417
|
-
|
|
1391
|
+
facts = this.store.repositoryFacts()
|
|
1418
1392
|
} catch {
|
|
1419
1393
|
// The read itself failed (a transient store error — a missing record
|
|
1420
1394
|
// answers `[]` inside the helper, so it never reaches this arm):
|
|
@@ -1423,7 +1397,7 @@ export class Dispatcher {
|
|
|
1423
1397
|
return
|
|
1424
1398
|
}
|
|
1425
1399
|
const idleMs = config.orchestrator.sandbox.idleMinutes * 60_000
|
|
1426
|
-
for (const environment of
|
|
1400
|
+
for (const environment of facts.sandboxes) {
|
|
1427
1401
|
if (environment.state !== 'live') continue
|
|
1428
1402
|
// Budget gate: once spent, stop settling further environments; the
|
|
1429
1403
|
// next tick retries what remains.
|
|
@@ -1505,8 +1479,8 @@ export class Dispatcher {
|
|
|
1505
1479
|
|
|
1506
1480
|
/** The latest `workspace.provision-started` not yet followed by a
|
|
1507
1481
|
* provisioned/released fact — an open provisioning marker. */
|
|
1508
|
-
private openProvisionMarker(events:
|
|
1509
|
-
return
|
|
1482
|
+
private openProvisionMarker(events: CurrentBuildFacts): ProvisionMarker | undefined {
|
|
1483
|
+
return events.provisionMarker
|
|
1510
1484
|
}
|
|
1511
1485
|
|
|
1512
1486
|
/** Liveness of the lease behind an open provision marker: `live` means a
|
|
@@ -1527,7 +1501,7 @@ export class Dispatcher {
|
|
|
1527
1501
|
* report progress without awaiting any of it. */
|
|
1528
1502
|
private async ensureProvisionContinuation(
|
|
1529
1503
|
record: BuildRecord,
|
|
1530
|
-
events:
|
|
1504
|
+
events: CurrentBuildFacts,
|
|
1531
1505
|
tail: 'dispatch' | 'launch',
|
|
1532
1506
|
seed?: {
|
|
1533
1507
|
ticket: Ticket
|
|
@@ -1543,12 +1517,7 @@ export class Dispatcher {
|
|
|
1543
1517
|
const marker = this.openProvisionMarker(events)
|
|
1544
1518
|
if (marker !== undefined) {
|
|
1545
1519
|
const liveness = await this.provisionMarkerLiveness(slug)
|
|
1546
|
-
const failedAttempt = events.
|
|
1547
|
-
(event) =>
|
|
1548
|
-
event.type === 'dispatch.failed' &&
|
|
1549
|
-
event.payload.stage === 'workspace' &&
|
|
1550
|
-
event.seq > marker.seq,
|
|
1551
|
-
)
|
|
1520
|
+
const failedAttempt = events.latestWorkspaceFailureSeq > marker.seq
|
|
1552
1521
|
if (liveness === 'live') return 'foreign-live'
|
|
1553
1522
|
if (
|
|
1554
1523
|
liveness === 'absent' &&
|
|
@@ -1563,7 +1532,7 @@ export class Dispatcher {
|
|
|
1563
1532
|
// Expired lease, failed attempt, or an old lease-less marker: adopt.
|
|
1564
1533
|
}
|
|
1565
1534
|
const branch = record.branch ?? `ab/${slug}`
|
|
1566
|
-
const generation = events.
|
|
1535
|
+
const generation = events.workspaceGenerations
|
|
1567
1536
|
await this.store.append(slug, {
|
|
1568
1537
|
actor: DISPATCHER,
|
|
1569
1538
|
type: 'workspace.provision-started',
|
|
@@ -1643,10 +1612,8 @@ export class Dispatcher {
|
|
|
1643
1612
|
// so the next supervisor adopts the marker immediately.
|
|
1644
1613
|
return
|
|
1645
1614
|
}
|
|
1646
|
-
const events = await store
|
|
1647
|
-
const priorGenerations = events.
|
|
1648
|
-
(event) => event.type === 'workspace.provisioned',
|
|
1649
|
-
).length
|
|
1615
|
+
const events = (await readBuildFacts(store, slug)).value
|
|
1616
|
+
const priorGenerations = events.workspaceGenerations
|
|
1650
1617
|
const handle = await workspaces.provision({
|
|
1651
1618
|
// Local providers provision FROM the physical checkout; remote
|
|
1652
1619
|
// providers ignore the value (their host seams are injected).
|
|
@@ -1689,7 +1656,7 @@ export class Dispatcher {
|
|
|
1689
1656
|
boundary = 'read'
|
|
1690
1657
|
const record = await store.getBuild(slug)
|
|
1691
1658
|
if (record === null) return
|
|
1692
|
-
const queued =
|
|
1659
|
+
const queued = (await readBuildFacts(store, slug)).value.state.status === 'queued'
|
|
1693
1660
|
boundary = input.tail === 'dispatch' || queued ? 'dispatch' : 'launch'
|
|
1694
1661
|
if (boundary === 'dispatch') await this.completeDispatchBoundaries(record, input.seed)
|
|
1695
1662
|
else await this.deps.launchRunner(slug)
|
|
@@ -1699,10 +1666,10 @@ export class Dispatcher {
|
|
|
1699
1666
|
// tick; it is not another provider workspace failure. Replacement
|
|
1700
1667
|
// runner launch errors retain the existing infrastructure-budget path.
|
|
1701
1668
|
if (boundary === 'read' || boundary === 'dispatch') return
|
|
1702
|
-
const events = await store
|
|
1669
|
+
const events = (await readBuildFacts(store, slug)).value
|
|
1703
1670
|
if (workspaces.recovery !== undefined) {
|
|
1704
1671
|
const open = openWorkspace(events)
|
|
1705
|
-
const latestRef =
|
|
1672
|
+
const latestRef = events.latestRelease
|
|
1706
1673
|
await this.recordInfrastructureFailure(slug, events, {
|
|
1707
1674
|
provider: workspaces.name,
|
|
1708
1675
|
workspaceRef:
|
|
@@ -1715,7 +1682,7 @@ export class Dispatcher {
|
|
|
1715
1682
|
cleanupPending: true,
|
|
1716
1683
|
})
|
|
1717
1684
|
}
|
|
1718
|
-
const attempt = events.
|
|
1685
|
+
const attempt = events.dispatchFailureCount + 1
|
|
1719
1686
|
const message = (error instanceof Error ? error.message : String(error)).trim()
|
|
1720
1687
|
await store.append(slug, {
|
|
1721
1688
|
actor: DISPATCHER,
|
|
@@ -1764,9 +1731,9 @@ export class Dispatcher {
|
|
|
1764
1731
|
for (const entry of entries) entry.controller.abort()
|
|
1765
1732
|
for (const entry of entries) {
|
|
1766
1733
|
try {
|
|
1767
|
-
const events = await this.store
|
|
1734
|
+
const events = (await readBuildFacts(this.store, entry.slug)).value
|
|
1768
1735
|
if (this.openProvisionMarker(events) !== undefined) {
|
|
1769
|
-
const attempt = events.
|
|
1736
|
+
const attempt = events.dispatchFailureCount + 1
|
|
1770
1737
|
await this.store.append(entry.slug, {
|
|
1771
1738
|
actor: DISPATCHER,
|
|
1772
1739
|
type: 'dispatch.failed',
|
|
@@ -1816,9 +1783,9 @@ export class Dispatcher {
|
|
|
1816
1783
|
const executionLeaseLive = this.hasLiveExecutionLease(record)
|
|
1817
1784
|
// Raw history, read at most once and only by a branch that acts on it:
|
|
1818
1785
|
// the facts above answer every quiet-path question without it.
|
|
1819
|
-
let loaded:
|
|
1820
|
-
const history = async (): Promise<
|
|
1821
|
-
loaded ??= await this.store
|
|
1786
|
+
let loaded: CurrentBuildFacts | undefined
|
|
1787
|
+
const history = async (): Promise<CurrentBuildFacts> => {
|
|
1788
|
+
loaded ??= (await readBuildFacts(this.store, record.slug)).value
|
|
1822
1789
|
return loaded
|
|
1823
1790
|
}
|
|
1824
1791
|
// Pipeline/ticket/workspace state is settled, but release-asset cleanup
|
|
@@ -1881,7 +1848,7 @@ export class Dispatcher {
|
|
|
1881
1848
|
}
|
|
1882
1849
|
}
|
|
1883
1850
|
|
|
1884
|
-
private forgeWorkspacePath(events:
|
|
1851
|
+
private forgeWorkspacePath(events: CurrentBuildFacts): string {
|
|
1885
1852
|
return this.forgeWorkspacePathOf(openWorkspace(events))
|
|
1886
1853
|
}
|
|
1887
1854
|
|
|
@@ -1905,7 +1872,7 @@ export class Dispatcher {
|
|
|
1905
1872
|
* repeating on later ticks. `build.completed` is deliberately last. */
|
|
1906
1873
|
private async cleanupAborted(
|
|
1907
1874
|
record: BuildRecord,
|
|
1908
|
-
events:
|
|
1875
|
+
events: CurrentBuildFacts,
|
|
1909
1876
|
report: TickReport,
|
|
1910
1877
|
): Promise<void> {
|
|
1911
1878
|
const store = this.store
|
|
@@ -1919,13 +1886,11 @@ export class Dispatcher {
|
|
|
1919
1886
|
// in the main checkout, and origin mode has no checkout at all: the
|
|
1920
1887
|
// local-branch git step is skipped, and the saga's later steps tolerate
|
|
1921
1888
|
// its absence.
|
|
1922
|
-
const hadLocalWorkspace = events.
|
|
1923
|
-
|
|
1924
|
-
)
|
|
1925
|
-
const has = (type: AbEvent['type']): boolean => events.some((event) => event.type === type)
|
|
1889
|
+
const hadLocalWorkspace = events.localWorkspace
|
|
1890
|
+
const has = (type: AbEvent['type']): boolean => events.seenTypes.includes(type)
|
|
1926
1891
|
const append = async <T extends EventWrite['type']>(write: EventWrite<T>): Promise<void> => {
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1892
|
+
const _event = await store.append(record.slug, write)
|
|
1893
|
+
Object.assign(events, (await readBuildFacts(store, record.slug)).value)
|
|
1929
1894
|
}
|
|
1930
1895
|
const fail = (operation: string, identity: string, error: unknown): never => {
|
|
1931
1896
|
const detail = error instanceof Error ? error.message : String(error)
|
|
@@ -1942,7 +1907,7 @@ export class Dispatcher {
|
|
|
1942
1907
|
}
|
|
1943
1908
|
}
|
|
1944
1909
|
|
|
1945
|
-
const reduced =
|
|
1910
|
+
const reduced = events.state
|
|
1946
1911
|
if (reduced.pr !== undefined && !has('pr.closed') && !has('pr.merged')) {
|
|
1947
1912
|
const closePr = forge.closePr
|
|
1948
1913
|
if (closePr === undefined) {
|
|
@@ -1972,7 +1937,8 @@ export class Dispatcher {
|
|
|
1972
1937
|
|
|
1973
1938
|
try {
|
|
1974
1939
|
const released = await this.releaseWorkspace(record.slug, events, 'abort')
|
|
1975
|
-
if (released !== undefined)
|
|
1940
|
+
if (released !== undefined)
|
|
1941
|
+
Object.assign(events, (await readBuildFacts(store, record.slug)).value)
|
|
1976
1942
|
} catch (error) {
|
|
1977
1943
|
fail('workspace release', `build ${record.slug}`, error)
|
|
1978
1944
|
}
|
|
@@ -2069,7 +2035,7 @@ export class Dispatcher {
|
|
|
2069
2035
|
* every partial cleanup is retried after a crash. */
|
|
2070
2036
|
private async cleanupDiscarded(
|
|
2071
2037
|
record: BuildRecord,
|
|
2072
|
-
events:
|
|
2038
|
+
events: CurrentBuildFacts,
|
|
2073
2039
|
report: TickReport,
|
|
2074
2040
|
): Promise<void> {
|
|
2075
2041
|
const store = this.store
|
|
@@ -2099,7 +2065,7 @@ export class Dispatcher {
|
|
|
2099
2065
|
record: BuildRecord,
|
|
2100
2066
|
// `workspace` is the open workspace fact; `history` loads the raw log, which
|
|
2101
2067
|
// only the branches that act on a PR transition need.
|
|
2102
|
-
source: { history: () => Promise<
|
|
2068
|
+
source: { history: () => Promise<CurrentBuildFacts>; workspace: OpenBuildWorkspace | null },
|
|
2103
2069
|
state: BuildState,
|
|
2104
2070
|
report: TickReport,
|
|
2105
2071
|
launched: Set<string>,
|
|
@@ -2139,12 +2105,12 @@ export class Dispatcher {
|
|
|
2139
2105
|
if (state.prState === 'conflicted') return
|
|
2140
2106
|
const events = await source.history()
|
|
2141
2107
|
const baseSha = await this.baseSha(baseBranchOf(events, this.deps.config))
|
|
2142
|
-
const
|
|
2108
|
+
const _conflicted = await store.append(record.slug, {
|
|
2143
2109
|
actor: DISPATCHER,
|
|
2144
2110
|
type: 'pr.conflicted',
|
|
2145
2111
|
payload: { baseSha },
|
|
2146
2112
|
} satisfies EventWrite<'pr.conflicted'>)
|
|
2147
|
-
events.
|
|
2113
|
+
Object.assign(events, (await readBuildFacts(this.store, record.slug)).value)
|
|
2148
2114
|
if (openWorkspace(events) === null && this.deps.workspaces.recovery !== undefined) {
|
|
2149
2115
|
// The reconcile workspace is provisioned by the tracked background
|
|
2150
2116
|
// continuation, whose tail re-attaches the build-runner; this tick
|
|
@@ -2270,12 +2236,9 @@ export class Dispatcher {
|
|
|
2270
2236
|
* is deliberately post-terminal and best-effort: a provider, timeout, or
|
|
2271
2237
|
* store failure cannot roll back build.completed/ticket/workspace work.
|
|
2272
2238
|
* Pending handles remain derivable and are retried on every later tick. */
|
|
2273
|
-
private async reclaimPrAttachments(slug: string, events:
|
|
2274
|
-
for (const hosted of
|
|
2275
|
-
const priorAttempts = events.
|
|
2276
|
-
(event) =>
|
|
2277
|
-
event.type === 'pr-attachment.reclaim-failed' && event.payload.hostedSeq === hosted.seq,
|
|
2278
|
-
).length
|
|
2239
|
+
private async reclaimPrAttachments(slug: string, events: CurrentBuildFacts): Promise<void> {
|
|
2240
|
+
for (const hosted of events.pendingAttachments) {
|
|
2241
|
+
const priorAttempts = events.reclaimCounts[String(hosted.seq)] ?? 0
|
|
2279
2242
|
try {
|
|
2280
2243
|
const capability = this.deps.forge.prAttachments
|
|
2281
2244
|
if (capability === undefined) {
|
|
@@ -2315,13 +2278,13 @@ export class Dispatcher {
|
|
|
2315
2278
|
}
|
|
2316
2279
|
}
|
|
2317
2280
|
|
|
2318
|
-
private recoveryCheckpoint(events:
|
|
2319
|
-
return
|
|
2281
|
+
private recoveryCheckpoint(events: CurrentBuildFacts): string | undefined {
|
|
2282
|
+
return events.recoveryCheckpoint
|
|
2320
2283
|
}
|
|
2321
2284
|
|
|
2322
2285
|
private async recordInfrastructureFailure(
|
|
2323
2286
|
slug: string,
|
|
2324
|
-
events:
|
|
2287
|
+
events: CurrentBuildFacts,
|
|
2325
2288
|
input: {
|
|
2326
2289
|
provider: string
|
|
2327
2290
|
workspaceRef: string
|
|
@@ -2330,8 +2293,8 @@ export class Dispatcher {
|
|
|
2330
2293
|
cleanupPending: boolean
|
|
2331
2294
|
},
|
|
2332
2295
|
): Promise<void> {
|
|
2333
|
-
const execution =
|
|
2334
|
-
const
|
|
2296
|
+
const execution = events.state.executions.at(-1)
|
|
2297
|
+
const _appended = await appendInfrastructureFailure(
|
|
2335
2298
|
{ store: this.store, ids: this.deps.ids },
|
|
2336
2299
|
{
|
|
2337
2300
|
slug,
|
|
@@ -2339,22 +2302,19 @@ export class Dispatcher {
|
|
|
2339
2302
|
maxAttempts: this.deps.config.policy.maxInfrastructureAttempts,
|
|
2340
2303
|
provider: input.provider,
|
|
2341
2304
|
workspaceRef: input.workspaceRef,
|
|
2342
|
-
instance:
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
: 'dispatcher-recovery',
|
|
2346
|
-
...(execution?.type === 'execution.started' && execution.payload.environmentId !== undefined
|
|
2347
|
-
? { environmentId: execution.payload.environmentId }
|
|
2305
|
+
instance: execution !== undefined ? execution.instance : 'dispatcher-recovery',
|
|
2306
|
+
...(execution !== undefined && execution.environmentId !== undefined
|
|
2307
|
+
? { environmentId: execution.environmentId }
|
|
2348
2308
|
: {}),
|
|
2349
|
-
...(execution
|
|
2350
|
-
? { sessionId: execution.
|
|
2309
|
+
...(execution !== undefined && execution.sessionId !== undefined
|
|
2310
|
+
? { sessionId: execution.sessionId }
|
|
2351
2311
|
: {}),
|
|
2352
2312
|
operation: input.operation,
|
|
2353
2313
|
error: input.error,
|
|
2354
2314
|
cleanupPending: input.cleanupPending,
|
|
2355
2315
|
},
|
|
2356
2316
|
)
|
|
2357
|
-
events.
|
|
2317
|
+
Object.assign(events, (await readBuildFacts(this.store, slug)).value)
|
|
2358
2318
|
}
|
|
2359
2319
|
|
|
2360
2320
|
/** The provider that owns a recorded workspace: the one named on its
|
|
@@ -2370,7 +2330,7 @@ export class Dispatcher {
|
|
|
2370
2330
|
* only after exact-name cleanup is durably acknowledged. */
|
|
2371
2331
|
private async reapStaleWorkspace(
|
|
2372
2332
|
slug: string,
|
|
2373
|
-
events:
|
|
2333
|
+
events: CurrentBuildFacts,
|
|
2374
2334
|
reason: 'pause' | 'blocked' | 'replacement',
|
|
2375
2335
|
): Promise<boolean> {
|
|
2376
2336
|
const open = openWorkspace(events)
|
|
@@ -2381,18 +2341,13 @@ export class Dispatcher {
|
|
|
2381
2341
|
// publication's fate BEFORE the destructive reap. The guard's append
|
|
2382
2342
|
// failure propagates — a stale workspace with an unrecorded pending
|
|
2383
2343
|
// request is never reaped (the next tick retries).
|
|
2384
|
-
events = await
|
|
2344
|
+
events = await settlePublicationFactsBeforeRelease(
|
|
2385
2345
|
{ store: this.store, settlePublication: this.settlePublication },
|
|
2386
2346
|
slug,
|
|
2387
2347
|
events,
|
|
2388
2348
|
reason,
|
|
2389
2349
|
)
|
|
2390
|
-
const attempt =
|
|
2391
|
-
events.filter(
|
|
2392
|
-
(event) =>
|
|
2393
|
-
event.type === 'infrastructure.cleanup-attempted' &&
|
|
2394
|
-
event.payload.workspaceRef === open.ref,
|
|
2395
|
-
).length + 1
|
|
2350
|
+
const attempt = (events.cleanupCounts[JSON.stringify(open.ref)] ?? 0) + 1
|
|
2396
2351
|
try {
|
|
2397
2352
|
const outcome = await recovery.reap({
|
|
2398
2353
|
provider: open.provider,
|
|
@@ -2401,7 +2356,7 @@ export class Dispatcher {
|
|
|
2401
2356
|
...(open.localPath !== undefined ? { localPath: open.localPath } : {}),
|
|
2402
2357
|
branch: open.branch,
|
|
2403
2358
|
})
|
|
2404
|
-
const
|
|
2359
|
+
const _cleaned = await this.store.append(slug, {
|
|
2405
2360
|
actor: DISPATCHER,
|
|
2406
2361
|
type: 'infrastructure.cleanup-attempted',
|
|
2407
2362
|
payload: {
|
|
@@ -2413,17 +2368,17 @@ export class Dispatcher {
|
|
|
2413
2368
|
snapshots: outcome.snapshots,
|
|
2414
2369
|
},
|
|
2415
2370
|
})
|
|
2416
|
-
events.
|
|
2417
|
-
const
|
|
2371
|
+
Object.assign(events, (await readBuildFacts(this.store, slug)).value)
|
|
2372
|
+
const _released = await this.store.append(slug, {
|
|
2418
2373
|
actor: DISPATCHER,
|
|
2419
2374
|
type: 'workspace.released',
|
|
2420
2375
|
payload: { ref: open.ref, reason },
|
|
2421
2376
|
})
|
|
2422
|
-
events.
|
|
2377
|
+
Object.assign(events, (await readBuildFacts(this.store, slug)).value)
|
|
2423
2378
|
return true
|
|
2424
2379
|
} catch (error) {
|
|
2425
2380
|
const message = error instanceof Error ? error.message : String(error)
|
|
2426
|
-
const
|
|
2381
|
+
const _failed = await this.store.append(slug, {
|
|
2427
2382
|
actor: DISPATCHER,
|
|
2428
2383
|
type: 'infrastructure.cleanup-attempted',
|
|
2429
2384
|
payload: {
|
|
@@ -2435,7 +2390,7 @@ export class Dispatcher {
|
|
|
2435
2390
|
error: message || 'cleanup failed without an error message',
|
|
2436
2391
|
},
|
|
2437
2392
|
})
|
|
2438
|
-
events.
|
|
2393
|
+
Object.assign(events, (await readBuildFacts(this.store, slug)).value)
|
|
2439
2394
|
await this.recordInfrastructureFailure(slug, events, {
|
|
2440
2395
|
provider: open.provider,
|
|
2441
2396
|
workspaceRef: open.ref,
|
|
@@ -2451,7 +2406,7 @@ export class Dispatcher {
|
|
|
2451
2406
|
* append `workspace.released`. Log-deduped, so re-runs are no-ops. */
|
|
2452
2407
|
private async releaseWorkspace(
|
|
2453
2408
|
slug: string,
|
|
2454
|
-
events:
|
|
2409
|
+
events: CurrentBuildFacts,
|
|
2455
2410
|
reason: 'completion' | 'abort' | 'pause' | 'blocked' | 'replacement' | 'discard' = 'completion',
|
|
2456
2411
|
): Promise<AbEvent | undefined> {
|
|
2457
2412
|
const open = openWorkspace(events)
|
|
@@ -2470,7 +2425,7 @@ export class Dispatcher {
|
|
|
2470
2425
|
// pending publication BEFORE any provider cleanup call, while the
|
|
2471
2426
|
// workspace still exists. The guard's append failure propagates — the
|
|
2472
2427
|
// release never proceeds past an unrecorded pending request.
|
|
2473
|
-
events = await
|
|
2428
|
+
events = await settlePublicationFactsBeforeRelease(
|
|
2474
2429
|
{ store: this.store, settlePublication: this.settlePublication },
|
|
2475
2430
|
slug,
|
|
2476
2431
|
events,
|
|
@@ -2478,15 +2433,10 @@ export class Dispatcher {
|
|
|
2478
2433
|
)
|
|
2479
2434
|
const recovery = owner.recovery
|
|
2480
2435
|
if (recovery !== undefined) {
|
|
2481
|
-
const attempt =
|
|
2482
|
-
events.filter(
|
|
2483
|
-
(event) =>
|
|
2484
|
-
event.type === 'infrastructure.cleanup-attempted' &&
|
|
2485
|
-
event.payload.workspaceRef === open.ref,
|
|
2486
|
-
).length + 1
|
|
2436
|
+
const attempt = (events.cleanupCounts[JSON.stringify(open.ref)] ?? 0) + 1
|
|
2487
2437
|
try {
|
|
2488
2438
|
const outcome = await recovery.reap(handle)
|
|
2489
|
-
const
|
|
2439
|
+
const _cleaned = await this.store.append(slug, {
|
|
2490
2440
|
actor: DISPATCHER,
|
|
2491
2441
|
type: 'infrastructure.cleanup-attempted',
|
|
2492
2442
|
payload: {
|
|
@@ -2498,10 +2448,10 @@ export class Dispatcher {
|
|
|
2498
2448
|
snapshots: outcome.snapshots,
|
|
2499
2449
|
},
|
|
2500
2450
|
})
|
|
2501
|
-
events.
|
|
2451
|
+
Object.assign(events, (await readBuildFacts(this.store, slug)).value)
|
|
2502
2452
|
} catch (error) {
|
|
2503
2453
|
const message = error instanceof Error ? error.message : String(error)
|
|
2504
|
-
const
|
|
2454
|
+
const _failed = await this.store.append(slug, {
|
|
2505
2455
|
actor: DISPATCHER,
|
|
2506
2456
|
type: 'infrastructure.cleanup-attempted',
|
|
2507
2457
|
payload: {
|
|
@@ -2513,7 +2463,7 @@ export class Dispatcher {
|
|
|
2513
2463
|
error: message || 'cleanup failed without an error message',
|
|
2514
2464
|
},
|
|
2515
2465
|
})
|
|
2516
|
-
events.
|
|
2466
|
+
Object.assign(events, (await readBuildFacts(this.store, slug)).value)
|
|
2517
2467
|
await this.recordInfrastructureFailure(slug, events, {
|
|
2518
2468
|
provider: open.provider,
|
|
2519
2469
|
workspaceRef: open.ref,
|
|
@@ -2639,9 +2589,9 @@ export class Dispatcher {
|
|
|
2639
2589
|
if (!(await this.dispatchBudget(record.slug))) return 'parked'
|
|
2640
2590
|
let stage: EventPayload<'dispatch.failed'>['stage'] = 'create'
|
|
2641
2591
|
try {
|
|
2642
|
-
let events = await store
|
|
2643
|
-
if (
|
|
2644
|
-
if (events.
|
|
2592
|
+
let events = (await readBuildFacts(store, record.slug)).value
|
|
2593
|
+
if (events.created === undefined) {
|
|
2594
|
+
if (events.state.lastSeq !== 0 || record.ticket === undefined) {
|
|
2645
2595
|
throw new Error(
|
|
2646
2596
|
'build record lacks the immutable ticket facts needed to reconstruct build.created',
|
|
2647
2597
|
)
|
|
@@ -2664,22 +2614,19 @@ export class Dispatcher {
|
|
|
2664
2614
|
type: 'build.created',
|
|
2665
2615
|
payload: this.buildCreatedPayload(record.ticket, record.repo, { ...seed, assets }),
|
|
2666
2616
|
})
|
|
2667
|
-
events = await store
|
|
2668
|
-
if (
|
|
2617
|
+
events = (await readBuildFacts(store, record.slug)).value
|
|
2618
|
+
if (events.created === undefined) {
|
|
2669
2619
|
throw new Error('build.created reconstruction lost its conditional append race')
|
|
2670
2620
|
}
|
|
2671
2621
|
}
|
|
2672
2622
|
|
|
2673
|
-
const created = events.
|
|
2623
|
+
const created = events.created
|
|
2674
2624
|
const autoMergeUser = seed?.autoMergeUser ?? created?.payload.autoMergeRequestedBy
|
|
2675
2625
|
// The claim's sampled default fact, for the request's fan-out provenance:
|
|
2676
2626
|
// the seed's own value first (this dispatch created the build), else the
|
|
2677
2627
|
// durable `build.created` fact a recovery pass resumes from.
|
|
2678
2628
|
const autoMergeDefaultSeq = seed?.autoMergeDefaultSeq ?? created?.payload.autoMergeDefaultSeq
|
|
2679
|
-
if (
|
|
2680
|
-
autoMergeUser !== undefined &&
|
|
2681
|
-
!events.some((event) => event.type === 'build.auto-merge-requested')
|
|
2682
|
-
) {
|
|
2629
|
+
if (autoMergeUser !== undefined && !events.autoMergeEverRequested) {
|
|
2683
2630
|
await store.append(record.slug, {
|
|
2684
2631
|
actor: humanActor(autoMergeUser),
|
|
2685
2632
|
type: 'build.auto-merge-requested',
|
|
@@ -2687,7 +2634,7 @@ export class Dispatcher {
|
|
|
2687
2634
|
...(autoMergeDefaultSeq !== undefined ? { defaultSeq: autoMergeDefaultSeq } : {}),
|
|
2688
2635
|
},
|
|
2689
2636
|
})
|
|
2690
|
-
events = await store
|
|
2637
|
+
events = (await readBuildFacts(store, record.slug)).value
|
|
2691
2638
|
}
|
|
2692
2639
|
|
|
2693
2640
|
stage = 'workspace'
|
|
@@ -2702,9 +2649,7 @@ export class Dispatcher {
|
|
|
2702
2649
|
launched.add(record.slug)
|
|
2703
2650
|
return 'deferred'
|
|
2704
2651
|
}
|
|
2705
|
-
const priorGenerations = events.
|
|
2706
|
-
(event) => event.type === 'workspace.provisioned',
|
|
2707
|
-
).length
|
|
2652
|
+
const priorGenerations = events.workspaceGenerations
|
|
2708
2653
|
const handle = await this.deps.workspaces.provision({
|
|
2709
2654
|
// Local providers provision FROM the physical checkout; remote
|
|
2710
2655
|
// providers ignore the value (their host seams are injected).
|
|
@@ -2731,10 +2676,10 @@ export class Dispatcher {
|
|
|
2731
2676
|
base: handle.base,
|
|
2732
2677
|
},
|
|
2733
2678
|
} satisfies EventWrite<'workspace.provisioned'>)
|
|
2734
|
-
events = await store
|
|
2679
|
+
events = (await readBuildFacts(store, record.slug)).value
|
|
2735
2680
|
}
|
|
2736
2681
|
} catch (error) {
|
|
2737
|
-
const events = await store
|
|
2682
|
+
const events = (await readBuildFacts(store, record.slug)).value
|
|
2738
2683
|
if (stage === 'workspace' && this.deps.workspaces.recovery !== undefined) {
|
|
2739
2684
|
const open = openWorkspace(events)
|
|
2740
2685
|
await this.recordInfrastructureFailure(record.slug, events, {
|
|
@@ -2745,7 +2690,7 @@ export class Dispatcher {
|
|
|
2745
2690
|
cleanupPending: true,
|
|
2746
2691
|
})
|
|
2747
2692
|
}
|
|
2748
|
-
const attempt = events.
|
|
2693
|
+
const attempt = events.dispatchFailureCount + 1
|
|
2749
2694
|
const message = (error instanceof Error ? error.message : String(error)).trim()
|
|
2750
2695
|
await store.append(record.slug, {
|
|
2751
2696
|
actor: DISPATCHER,
|
|
@@ -2785,12 +2730,10 @@ export class Dispatcher {
|
|
|
2785
2730
|
let stage: EventPayload<'dispatch.failed'>['stage'] = 'spec'
|
|
2786
2731
|
let authored = false
|
|
2787
2732
|
try {
|
|
2788
|
-
const events = await store
|
|
2789
|
-
const state =
|
|
2733
|
+
const events = (await readBuildFacts(store, record.slug)).value
|
|
2734
|
+
const state = events.state
|
|
2790
2735
|
if (state.status !== 'queued' || state.discardRequest !== undefined) return 'parked'
|
|
2791
|
-
if (
|
|
2792
|
-
!events.some((event) => event.type === 'spec.imported' || event.type === 'spec.authored')
|
|
2793
|
-
) {
|
|
2736
|
+
if (!events.specRecorded) {
|
|
2794
2737
|
let ticket: Ticket
|
|
2795
2738
|
if (seed?.ticket !== undefined) {
|
|
2796
2739
|
ticket = seed.ticket
|
|
@@ -2848,10 +2791,7 @@ export class Dispatcher {
|
|
|
2848
2791
|
}
|
|
2849
2792
|
|
|
2850
2793
|
stage = 'comment'
|
|
2851
|
-
if (
|
|
2852
|
-
record.ticket !== undefined &&
|
|
2853
|
-
!events.some((event) => event.type === 'dispatch.comment-posted')
|
|
2854
|
-
) {
|
|
2794
|
+
if (record.ticket !== undefined && !events.commentPosted) {
|
|
2855
2795
|
await this.deps.tickets.comment(record.ticket.id, `build ${record.slug} dispatched`)
|
|
2856
2796
|
await store.append(record.slug, {
|
|
2857
2797
|
actor: DISPATCHER,
|
|
@@ -2865,7 +2805,7 @@ export class Dispatcher {
|
|
|
2865
2805
|
// provider awaits. Re-read immediately before launch so a request made
|
|
2866
2806
|
// during those boundaries parks cleanly for the next janitor pass.
|
|
2867
2807
|
await store.refreshBuild(record.slug)
|
|
2868
|
-
const launchState =
|
|
2808
|
+
const launchState = (await readBuildFacts(store, record.slug)).value.state
|
|
2869
2809
|
if (launchState.status !== 'queued' || launchState.discardRequest !== undefined) {
|
|
2870
2810
|
return 'parked'
|
|
2871
2811
|
}
|
|
@@ -2874,8 +2814,8 @@ export class Dispatcher {
|
|
|
2874
2814
|
if (authored && report !== undefined) report.authored += 1
|
|
2875
2815
|
return 'completed'
|
|
2876
2816
|
} catch (error) {
|
|
2877
|
-
const events = await store
|
|
2878
|
-
const attempt = events.
|
|
2817
|
+
const events = (await readBuildFacts(store, record.slug)).value
|
|
2818
|
+
const attempt = events.dispatchFailureCount + 1
|
|
2879
2819
|
const message = (error instanceof Error ? error.message : String(error)).trim()
|
|
2880
2820
|
await store.append(record.slug, {
|
|
2881
2821
|
actor: DISPATCHER,
|
|
@@ -3050,9 +2990,9 @@ export class Dispatcher {
|
|
|
3050
2990
|
if (decision === undefined) continue
|
|
3051
2991
|
if (state.status === 'done' || state.status === 'aborted') continue
|
|
3052
2992
|
// Raw history, read at most once and only by a branch that acts on it.
|
|
3053
|
-
let loaded:
|
|
3054
|
-
const history = async (): Promise<
|
|
3055
|
-
loaded ??= await this.store
|
|
2993
|
+
let loaded: CurrentBuildFacts | undefined
|
|
2994
|
+
const history = async (): Promise<CurrentBuildFacts> => {
|
|
2995
|
+
loaded ??= (await readBuildFacts(this.store, record.slug)).value
|
|
3056
2996
|
return loaded
|
|
3057
2997
|
}
|
|
3058
2998
|
// Liveness gate (durable supervision): a recorded execution the provider
|
|
@@ -3136,7 +3076,7 @@ export class Dispatcher {
|
|
|
3136
3076
|
if (
|
|
3137
3077
|
facts.workspace === null &&
|
|
3138
3078
|
this.deps.workspaces.recovery !== undefined &&
|
|
3139
|
-
(await history()).
|
|
3079
|
+
(await history()).workspaceGenerations > 0
|
|
3140
3080
|
) {
|
|
3141
3081
|
const kicked = await this.ensureProvisionContinuation(record, await history(), 'launch')
|
|
3142
3082
|
if (kicked === 'kicked') {
|
|
@@ -3190,16 +3130,16 @@ export class Dispatcher {
|
|
|
3190
3130
|
// dispatcher is saturated. Sources that keep a claimed ticket in the
|
|
3191
3131
|
// ready state are deduped against the active builds embodying them.
|
|
3192
3132
|
await this.store.refreshJournal()
|
|
3193
|
-
const journalBeforeReady = this.store.
|
|
3133
|
+
const journalBeforeReady = this.store.repositoryFacts().harvest
|
|
3194
3134
|
const listing = await this.listReady()
|
|
3195
3135
|
const activeTicketIds = await this.activeTicketIdentities(activeRefs, listing.tickets)
|
|
3196
3136
|
await this.store.refreshJournal()
|
|
3197
|
-
const journalAfterReady = this.store.
|
|
3198
|
-
if (
|
|
3137
|
+
const journalAfterReady = this.store.repositoryFacts()
|
|
3138
|
+
if (journalAfterReady.health?.cause === 'ready-scan-failed') {
|
|
3199
3139
|
await this.sourceCondition(undefined, 'ready-scan')
|
|
3200
3140
|
}
|
|
3201
3141
|
const creationHolds = new Map(
|
|
3202
|
-
|
|
3142
|
+
harvestCreationsFromStates(journalBeforeReady, journalAfterReady.harvest).map((creation) => [
|
|
3203
3143
|
creation.creationKey.toLowerCase(),
|
|
3204
3144
|
creation,
|
|
3205
3145
|
]),
|