@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,3 +1,4 @@
|
|
|
1
|
+
import type { RepositoryReadFacts } from '../store/reduced-reads'
|
|
1
2
|
/**
|
|
2
3
|
* Durable harvest execution state: the repository-journal projection that
|
|
3
4
|
* hosted-harvest supervision (AUT-305) reads instead of process memory. The
|
|
@@ -88,22 +89,27 @@ export interface HarvestExecutionClassificationInput {
|
|
|
88
89
|
* `held`; anything else is `idle`. The result feeds the same counters a
|
|
89
90
|
* locally run harvest reports. */
|
|
90
91
|
export function classifyHarvestOutcome(
|
|
91
|
-
events: readonly RepositoryEvent[],
|
|
92
|
+
events: readonly RepositoryEvent[] | RepositoryReadFacts,
|
|
92
93
|
input: HarvestExecutionClassificationInput,
|
|
93
94
|
): HarvestRunnerResult {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
95
|
+
const facts = !Array.isArray(events) ? (events as RepositoryReadFacts) : undefined
|
|
96
|
+
const raw = Array.isArray(events) ? events : []
|
|
97
|
+
const state = facts?.harvest ?? reduceHarvest(raw)
|
|
98
|
+
const started = raw.find(
|
|
96
99
|
(event) =>
|
|
97
100
|
event.type === 'harvest.execution.started' && event.seq === input.executionStartedSeq,
|
|
98
101
|
)
|
|
102
|
+
const execution =
|
|
103
|
+
facts?.harvestExecutionStarts[String(input.executionStartedSeq)] ??
|
|
104
|
+
(started?.type === 'harvest.execution.started' ? started.payload.execution : undefined)
|
|
99
105
|
const authorization =
|
|
100
|
-
|
|
101
|
-
?
|
|
106
|
+
execution === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: (facts?.harvestAuthorizations[JSON.stringify(execution)] ??
|
|
109
|
+
raw.find(
|
|
102
110
|
(event) =>
|
|
103
|
-
event.type === 'harvest.execution.authorized' &&
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
: undefined
|
|
111
|
+
event.type === 'harvest.execution.authorized' && event.payload.execution === execution,
|
|
112
|
+
))
|
|
107
113
|
const selected =
|
|
108
114
|
authorization?.type === 'harvest.execution.authorized' ? authorization.payload : undefined
|
|
109
115
|
const latestRun = selected
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readRepositoryFacts } from '../store/reduced-reads'
|
|
1
2
|
import { ticketIdentity } from '../ports/tickets/identity'
|
|
2
3
|
import { issueTicketGrant } from '../ports/tickets/grants'
|
|
3
4
|
import { delegateCredential } from '../store/remote/credentials'
|
|
@@ -22,7 +23,6 @@ import {
|
|
|
22
23
|
decideHarvestControl,
|
|
23
24
|
openHarvestRun,
|
|
24
25
|
proposalArtifactForRound,
|
|
25
|
-
reduceHarvest,
|
|
26
26
|
type HarvestRunState,
|
|
27
27
|
} from '../kernel/harvest'
|
|
28
28
|
import type { ArtifactRef, Feedback, Verdict } from '../ontology'
|
|
@@ -239,28 +239,23 @@ export class HarvestRunner {
|
|
|
239
239
|
// Bounded read (AUT-489): reduceHarvest and the verdict/failure filters
|
|
240
240
|
// below consume durable types only, so the subset is replay-equivalent.
|
|
241
241
|
if (this.deps.preparedRun) {
|
|
242
|
-
const events = await store
|
|
243
|
-
const authorization = events.
|
|
244
|
-
(event) =>
|
|
245
|
-
event.type === 'harvest.execution.authorized' &&
|
|
246
|
-
event.payload.execution === instance &&
|
|
247
|
-
event.payload.run === this.deps.preparedRun!.run,
|
|
248
|
-
)
|
|
242
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
243
|
+
const authorization = events.harvestAuthorizations[JSON.stringify(instance)]
|
|
249
244
|
if (authorization?.type !== 'harvest.execution.authorized')
|
|
250
245
|
throw new Error('prepared harvest execution unavailable')
|
|
251
|
-
run =
|
|
246
|
+
run = events.harvest.runs.find((run) => run.run === this.deps.preparedRun!.run)
|
|
252
247
|
initial = authorization.payload.launch
|
|
253
248
|
} else {
|
|
254
|
-
let events = await store
|
|
255
|
-
let state =
|
|
249
|
+
let events = (await readRepositoryFacts(store, repo)).value
|
|
250
|
+
let state = events.harvest
|
|
256
251
|
run = actionableHarvestRun(state)
|
|
257
252
|
|
|
258
253
|
// Settle durable operator control before scanning or resuming work. The
|
|
259
254
|
// selected run comes from the full journal, so a shadowed failure or
|
|
260
255
|
// exhaustion barrier is recovered/reported before any later open run.
|
|
261
256
|
await this.controlBoundary(run?.run)
|
|
262
|
-
events = await store
|
|
263
|
-
state =
|
|
257
|
+
events = (await readRepositoryFacts(store, repo)).value
|
|
258
|
+
state = events.harvest
|
|
264
259
|
run = openHarvestRun(state)
|
|
265
260
|
|
|
266
261
|
if (!run) {
|
|
@@ -274,7 +269,7 @@ export class HarvestRunner {
|
|
|
274
269
|
// Bounded read (AUT-489): the gate's journal input is consumed only
|
|
275
270
|
// through reduceHarvest, so the subset is replay-equivalent here too.
|
|
276
271
|
const [gateEvents, digests] = await Promise.all([
|
|
277
|
-
store.
|
|
272
|
+
readRepositoryFacts(store, repo).then((result) => result.value.harvest),
|
|
278
273
|
store.getRepoBuildDigests(repo),
|
|
279
274
|
])
|
|
280
275
|
const pressure = evaluateHarvestPressureFromDigests({
|
|
@@ -338,8 +333,8 @@ export class HarvestRunner {
|
|
|
338
333
|
}),
|
|
339
334
|
)
|
|
340
335
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
341
|
-
events = await store
|
|
342
|
-
run =
|
|
336
|
+
events = (await readRepositoryFacts(store, repo)).value
|
|
337
|
+
run = events.harvest.runs.find((candidate) => candidate.run === runId)
|
|
343
338
|
if (!run) throw new Error(`harvest.started did not reduce run "${runId}"`)
|
|
344
339
|
// A request racing the claim parks this exact run; its snapshot stays
|
|
345
340
|
// open and is resumed rather than scanned again.
|
|
@@ -503,14 +498,8 @@ export class HarvestRunner {
|
|
|
503
498
|
)
|
|
504
499
|
// Bounded read (AUT-489): only harvest.review.verdict facts are
|
|
505
500
|
// filtered here, and those are durable.
|
|
506
|
-
const event = (await this.deps.store
|
|
507
|
-
(
|
|
508
|
-
candidate,
|
|
509
|
-
): candidate is Extract<RepositoryEvent, { type: 'harvest.review.verdict' }> =>
|
|
510
|
-
candidate.type === 'harvest.review.verdict' &&
|
|
511
|
-
candidate.payload.run === run.run &&
|
|
512
|
-
candidate.payload.round === round,
|
|
513
|
-
)
|
|
501
|
+
const event = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value
|
|
502
|
+
.harvestVerdicts[JSON.stringify([run.run, round])]?.first
|
|
514
503
|
if (!event) throw new Error('review reducer/event mismatch')
|
|
515
504
|
await this.controlBoundary(run.run)
|
|
516
505
|
return verdictFromEvent(event)
|
|
@@ -572,15 +561,8 @@ export class HarvestRunner {
|
|
|
572
561
|
})
|
|
573
562
|
// Bounded read (AUT-489): only harvest.review.verdict facts are filtered
|
|
574
563
|
// here, and those are durable.
|
|
575
|
-
const events = await this.deps.store
|
|
576
|
-
const verdict = [
|
|
577
|
-
.reverse()
|
|
578
|
-
.find(
|
|
579
|
-
(event): event is Extract<RepositoryEvent, { type: 'harvest.review.verdict' }> =>
|
|
580
|
-
event.type === 'harvest.review.verdict' &&
|
|
581
|
-
event.payload.run === run.run &&
|
|
582
|
-
event.payload.round === round,
|
|
583
|
-
)
|
|
564
|
+
const events = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value
|
|
565
|
+
const verdict = events.harvestVerdicts[JSON.stringify([run.run, round])]?.latest
|
|
584
566
|
if (!verdict) throw new Error(`review@${round} terminal is missing`)
|
|
585
567
|
await this.completeStep(
|
|
586
568
|
run.run,
|
|
@@ -624,22 +606,16 @@ export class HarvestRunner {
|
|
|
624
606
|
await this.ensureLease()
|
|
625
607
|
// Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
|
|
626
608
|
// consume durable types only.
|
|
627
|
-
const events = await store
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
event.payload.step === spec.step &&
|
|
633
|
-
event.payload.round === spec.round,
|
|
634
|
-
)
|
|
635
|
-
const failures = matchingFailures.length
|
|
636
|
-
const latestFailure = matchingFailures.at(-1)
|
|
637
|
-
const current = reduceHarvest(events).runs.find((candidate) => candidate.run === spec.run)
|
|
609
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
610
|
+
const failureFacts = events.harvestFailures[JSON.stringify([spec.run, spec.step, spec.round])]
|
|
611
|
+
const failures = failureFacts?.count ?? 0
|
|
612
|
+
const latestFailure = failureFacts?.latest
|
|
613
|
+
const current = events.harvest.runs.find((candidate) => candidate.run === spec.run)
|
|
638
614
|
const resumedAfterTerminalFailure =
|
|
639
615
|
latestFailure?.payload.willRetry === false &&
|
|
640
616
|
current?.status === 'running' &&
|
|
641
617
|
current.failure === undefined &&
|
|
642
|
-
events.
|
|
618
|
+
events.latestHarvestResumedSeq > latestFailure.seq
|
|
643
619
|
if (failures >= this.maxSessionAttempts && !resumedAfterTerminalFailure) {
|
|
644
620
|
throw new SessionFailure(`${spec.step}@${spec.round} exhausted retries`)
|
|
645
621
|
}
|
|
@@ -884,17 +860,11 @@ export class HarvestRunner {
|
|
|
884
860
|
await this.ensureLease()
|
|
885
861
|
// Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
|
|
886
862
|
// consume durable types only.
|
|
887
|
-
const events = await store
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
event.payload.step === spec.step &&
|
|
893
|
-
event.payload.round === spec.round,
|
|
894
|
-
)
|
|
895
|
-
const failures = matchingFailures.length
|
|
896
|
-
const latestFailure = matchingFailures.at(-1)
|
|
897
|
-
const current = reduceHarvest(events).runs.find((candidate) => candidate.run === spec.run)
|
|
863
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
864
|
+
const failureFacts = events.harvestFailures[JSON.stringify([spec.run, spec.step, spec.round])]
|
|
865
|
+
const failures = failureFacts?.count ?? 0
|
|
866
|
+
const latestFailure = failureFacts?.latest
|
|
867
|
+
const current = events.harvest.runs.find((candidate) => candidate.run === spec.run)
|
|
898
868
|
// A stopping failure may already have consumed the ordinary retry budget.
|
|
899
869
|
// harvest.resumed deliberately clears that reduced stop and grants one real
|
|
900
870
|
// re-entry without deleting history or resetting attempt numbers. The next
|
|
@@ -903,7 +873,7 @@ export class HarvestRunner {
|
|
|
903
873
|
latestFailure?.payload.willRetry === false &&
|
|
904
874
|
current?.status === 'running' &&
|
|
905
875
|
current.failure === undefined &&
|
|
906
|
-
events.
|
|
876
|
+
events.latestHarvestResumedSeq > latestFailure.seq
|
|
907
877
|
if (failures >= this.maxSessionAttempts && !resumedAfterTerminalFailure) {
|
|
908
878
|
throw new SessionFailure(`${spec.step}@${spec.round} exhausted retries`)
|
|
909
879
|
}
|
|
@@ -1366,8 +1336,8 @@ export class HarvestRunner {
|
|
|
1366
1336
|
await this.ensureLease()
|
|
1367
1337
|
// Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
|
|
1368
1338
|
// consume durable types only.
|
|
1369
|
-
const events = await this.deps.store
|
|
1370
|
-
const state =
|
|
1339
|
+
const events = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value
|
|
1340
|
+
const state = events.harvest
|
|
1371
1341
|
const current = state.runs.find((candidate) => candidate.run === run)
|
|
1372
1342
|
if (current?.status !== 'running') return
|
|
1373
1343
|
const open = [...current.steps]
|
|
@@ -1375,13 +1345,7 @@ export class HarvestRunner {
|
|
|
1375
1345
|
.find((occurrence) => occurrence.completedSeq === undefined)
|
|
1376
1346
|
const step = open?.step ?? 'file'
|
|
1377
1347
|
const round = open?.round
|
|
1378
|
-
const prior = events.
|
|
1379
|
-
(event) =>
|
|
1380
|
-
event.type === 'harvest.failed' &&
|
|
1381
|
-
event.payload.run === run &&
|
|
1382
|
-
event.payload.step === step &&
|
|
1383
|
-
event.payload.round === round,
|
|
1384
|
-
).length
|
|
1348
|
+
const prior = events.harvestFailures[JSON.stringify([run, step, round])]?.count ?? 0
|
|
1385
1349
|
const attempt = prior + 1
|
|
1386
1350
|
const willRetry = attempt < this.maxSessionAttempts
|
|
1387
1351
|
const message = errorMessage(error)
|
|
@@ -1421,7 +1385,7 @@ export class HarvestRunner {
|
|
|
1421
1385
|
if (this.deps.preparedRun) return
|
|
1422
1386
|
await this.ensureLease()
|
|
1423
1387
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1424
|
-
const state =
|
|
1388
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1425
1389
|
const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
|
|
1426
1390
|
if (decision.kind !== 'exhaust-recovery' || decision.run !== run) return
|
|
1427
1391
|
await this.finalizeRecoveryExhaustion(decision.run, decision.attempts, decision.limit)
|
|
@@ -1447,7 +1411,7 @@ export class HarvestRunner {
|
|
|
1447
1411
|
|
|
1448
1412
|
await this.ensureLease()
|
|
1449
1413
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1450
|
-
const state =
|
|
1414
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1451
1415
|
const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
|
|
1452
1416
|
if (
|
|
1453
1417
|
decision.kind !== 'exhaust-recovery' ||
|
|
@@ -1484,7 +1448,7 @@ export class HarvestRunner {
|
|
|
1484
1448
|
while (true) {
|
|
1485
1449
|
await this.ensureLease()
|
|
1486
1450
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1487
|
-
const state =
|
|
1451
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1488
1452
|
const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
|
|
1489
1453
|
if (decision.kind === 'proceed') return
|
|
1490
1454
|
if (this.deps.preparedRun) throw new HarvestParkedSignal(run)
|
|
@@ -1529,7 +1493,7 @@ export class HarvestRunner {
|
|
|
1529
1493
|
|
|
1530
1494
|
private async refreshRun(run: string): Promise<HarvestRunState> {
|
|
1531
1495
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1532
|
-
const state =
|
|
1496
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1533
1497
|
const found = state.runs.find((candidate) => candidate.run === run)
|
|
1534
1498
|
if (!found) throw new Error(`unknown harvest run "${run}"`)
|
|
1535
1499
|
return found
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readBuildDigests } from '../store/reduced-reads'
|
|
2
|
+
import { readRepositoryFacts, readBuildFacts } from '../store/reduced-reads'
|
|
1
3
|
/** Deterministic halves of observation harvest: scan, dedup, validation, and
|
|
2
4
|
* proposal rendering. Agent judgment is deliberately absent from this file. */
|
|
3
5
|
import type { AbEvent } from '../events/catalog'
|
|
@@ -135,10 +137,12 @@ export function evaluateHarvestPressure(
|
|
|
135
137
|
* so latest-wins carries exactly the drift fact the scan collects. */
|
|
136
138
|
export function evaluateHarvestPressureFromDigests(input: {
|
|
137
139
|
digests: Map<string, BuildDigest>
|
|
138
|
-
harvestEvents: RepositoryEvent[]
|
|
140
|
+
harvestEvents: RepositoryEvent[] | HarvestState
|
|
139
141
|
policy: { harvestThreshold: number; harvestMaxDrift: number }
|
|
140
142
|
}): HarvestPressure {
|
|
141
|
-
const claimed = claimedOccurrenceKeys(
|
|
143
|
+
const claimed = claimedOccurrenceKeys(
|
|
144
|
+
Array.isArray(input.harvestEvents) ? reduceHarvest(input.harvestEvents) : input.harvestEvents,
|
|
145
|
+
)
|
|
142
146
|
const unclaimed: HarvestPressureFacts['unclaimed'] = []
|
|
143
147
|
const merges: HarvestPressureFacts['merges'] = []
|
|
144
148
|
for (const digest of input.digests.values()) {
|
|
@@ -160,11 +164,11 @@ export function evaluateHarvestPressureFromDigests(input: {
|
|
|
160
164
|
export async function evaluateHarvestPressureFromStore(input: {
|
|
161
165
|
store: BuildStore
|
|
162
166
|
repo: string
|
|
163
|
-
harvestEvents: RepositoryEvent[]
|
|
167
|
+
harvestEvents: RepositoryEvent[] | HarvestState
|
|
164
168
|
policy: { harvestThreshold: number; harvestMaxDrift: number }
|
|
165
169
|
}): Promise<HarvestPressure> {
|
|
166
170
|
await input.store.ensureRepo(input.repo)
|
|
167
|
-
const digests = await input.store
|
|
171
|
+
const digests = await readBuildDigests(input.store, input.repo)
|
|
168
172
|
return evaluateHarvestPressureFromDigests({
|
|
169
173
|
digests,
|
|
170
174
|
harvestEvents: input.harvestEvents,
|
|
@@ -182,9 +186,11 @@ export function collectUnclaimedObservations(input: {
|
|
|
182
186
|
repo: string
|
|
183
187
|
records: BuildRecord[]
|
|
184
188
|
eventsByBuild: Map<string, AbEvent[]>
|
|
185
|
-
harvestEvents: RepositoryEvent[]
|
|
189
|
+
harvestEvents: RepositoryEvent[] | HarvestState
|
|
186
190
|
}): HarvestScanResult {
|
|
187
|
-
const state =
|
|
191
|
+
const state = Array.isArray(input.harvestEvents)
|
|
192
|
+
? reduceHarvest(input.harvestEvents)
|
|
193
|
+
: input.harvestEvents
|
|
188
194
|
const claimed = claimedOccurrenceKeys(state)
|
|
189
195
|
const observations: HarvestObservation[] = []
|
|
190
196
|
const merges: HarvestMerge[] = []
|
|
@@ -235,9 +241,11 @@ export function collectUnclaimedObservations(input: {
|
|
|
235
241
|
* are not assumed globally unique. */
|
|
236
242
|
export function unclaimedObservationCount(input: {
|
|
237
243
|
digests: Map<string, BuildDigest>
|
|
238
|
-
harvestEvents: RepositoryEvent[]
|
|
244
|
+
harvestEvents: RepositoryEvent[] | HarvestState
|
|
239
245
|
}): number {
|
|
240
|
-
const claimed = claimedOccurrenceKeys(
|
|
246
|
+
const claimed = claimedOccurrenceKeys(
|
|
247
|
+
Array.isArray(input.harvestEvents) ? reduceHarvest(input.harvestEvents) : input.harvestEvents,
|
|
248
|
+
)
|
|
241
249
|
let count = 0
|
|
242
250
|
for (const digest of input.digests.values()) {
|
|
243
251
|
for (const observation of digest.observations) {
|
|
@@ -279,7 +287,7 @@ export async function sampleUnclaimedObservationCount(
|
|
|
279
287
|
): Promise<number> {
|
|
280
288
|
const [digests, harvestEvents] = await Promise.all([
|
|
281
289
|
store.getRepoBuildDigests(repo),
|
|
282
|
-
|
|
290
|
+
readRepositoryFacts(store, repo).then((result) => result.value.harvest),
|
|
283
291
|
])
|
|
284
292
|
return unclaimedObservationCount({ digests, harvestEvents })
|
|
285
293
|
}
|
|
@@ -292,16 +300,31 @@ export async function scanUnclaimedObservations(
|
|
|
292
300
|
repo: string,
|
|
293
301
|
): Promise<HarvestScanResult> {
|
|
294
302
|
await store.ensureRepo(repo)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const harvestEvents = await store.getRepoStateEvents(repo)
|
|
303
|
+
const state = (await readRepositoryFacts(store, repo)).value.harvest
|
|
304
|
+
const claimed = claimedOccurrenceKeys(state)
|
|
298
305
|
const records = await store.listBuilds()
|
|
299
|
-
const
|
|
306
|
+
const observations: HarvestObservation[] = []
|
|
307
|
+
const merges: HarvestMerge[] = []
|
|
300
308
|
for (const record of records) {
|
|
301
309
|
if (record.repo !== repo) continue
|
|
302
|
-
|
|
310
|
+
const facts = (await readBuildFacts(store, record.slug)).value
|
|
311
|
+
for (const ts of facts.mergeTimes) merges.push({ build: record.slug, ts })
|
|
312
|
+
for (const observation of facts.observationFacts) {
|
|
313
|
+
const occurrence = { build: record.slug, seq: observation.seq }
|
|
314
|
+
if (claimed.has(occurrenceKey(occurrence))) continue
|
|
315
|
+
observations.push({
|
|
316
|
+
occurrence,
|
|
317
|
+
...observation.payload,
|
|
318
|
+
ts: observation.ts,
|
|
319
|
+
...(record.ticket ? { ticket: structuredClone(record.ticket) } : {}),
|
|
320
|
+
})
|
|
321
|
+
}
|
|
303
322
|
}
|
|
304
|
-
|
|
323
|
+
observations.sort(
|
|
324
|
+
(a, b) =>
|
|
325
|
+
a.occurrence.build.localeCompare(b.occurrence.build) || a.occurrence.seq - b.occurrence.seq,
|
|
326
|
+
)
|
|
327
|
+
return { state, observations, merges }
|
|
305
328
|
}
|
|
306
329
|
|
|
307
330
|
/** Distinct previously filed/joined proposal tickets, reconciled through the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readBuildFacts, type CurrentBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
import type { AbEvent } from '../events/catalog'
|
|
2
3
|
import { DISPATCHER } from '../events/envelope'
|
|
3
4
|
import type { EventPayload } from '../events/payloads'
|
|
@@ -10,7 +11,7 @@ export type InfrastructureCause = EventPayload<'infrastructure.failed'>['cause']
|
|
|
10
11
|
|
|
11
12
|
export interface InfrastructureFailureInput {
|
|
12
13
|
slug: string
|
|
13
|
-
events: readonly AbEvent[]
|
|
14
|
+
events: readonly AbEvent[] | CurrentBuildFacts
|
|
14
15
|
maxAttempts: number
|
|
15
16
|
provider: string
|
|
16
17
|
workspaceRef: string
|
|
@@ -23,7 +24,7 @@ export interface InfrastructureFailureInput {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface InfrastructureFailureDependencies {
|
|
26
|
-
store: Pick<BuildStore, 'append' | '
|
|
27
|
+
store: Pick<BuildStore, 'append' | 'getReducerValue'>
|
|
27
28
|
ids: IdSource
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -104,10 +105,14 @@ export async function recordInfrastructureFailure(
|
|
|
104
105
|
deps: InfrastructureFailureDependencies,
|
|
105
106
|
input: InfrastructureFailureInput,
|
|
106
107
|
): Promise<AbEvent[]> {
|
|
107
|
-
const lastReset =
|
|
108
|
-
|
|
109
|
-
input.events
|
|
110
|
-
|
|
108
|
+
const lastReset = Array.isArray(input.events)
|
|
109
|
+
? infrastructureFailureResetSeq(input.events)
|
|
110
|
+
: (input.events as CurrentBuildFacts).infrastructureReset
|
|
111
|
+
const attempt = Array.isArray(input.events)
|
|
112
|
+
? input.events.filter(
|
|
113
|
+
(event) => event.type === 'infrastructure.failed' && event.seq > lastReset,
|
|
114
|
+
).length + 1
|
|
115
|
+
: (input.events as CurrentBuildFacts).infrastructureCount + 1
|
|
111
116
|
const message = normalizeInfrastructureError(input.error)
|
|
112
117
|
const failure = await deps.store.append(input.slug, {
|
|
113
118
|
actor: DISPATCHER,
|
|
@@ -130,15 +135,8 @@ export async function recordInfrastructureFailure(
|
|
|
130
135
|
|
|
131
136
|
if (attempt < input.maxAttempts) return appended
|
|
132
137
|
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
!currentEvents.some(
|
|
136
|
-
(event) =>
|
|
137
|
-
event.seq > lastReset &&
|
|
138
|
-
event.type === 'escalation.raised' &&
|
|
139
|
-
event.payload.policyCause === 'infrastructure-failure-limit',
|
|
140
|
-
)
|
|
141
|
-
) {
|
|
138
|
+
const current = (await readBuildFacts(deps.store, input.slug)).value
|
|
139
|
+
if (current.infrastructureRaisedSeq <= lastReset) {
|
|
142
140
|
appended.push(
|
|
143
141
|
await deps.store.append(input.slug, {
|
|
144
142
|
actor: DISPATCHER,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
/**
|
|
2
3
|
* The dispatcher tick's orchestrator step (AUT-342): resume suspended turns,
|
|
3
4
|
* reap crashed ones, and wake idle sessions from build attention events.
|
|
@@ -28,7 +29,6 @@ import { buildRegistry, type OperatorToolRegistry } from '../operator/registry'
|
|
|
28
29
|
import type { OperatorSandboxService } from '../operator/sandbox'
|
|
29
30
|
import { ticketBackendFromSource } from '../operator/ticket-source-backend'
|
|
30
31
|
import { reduceSession, type SessionTurnTrigger } from '../store/session-reducer'
|
|
31
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
32
32
|
import type { AbEvent } from '../events/catalog'
|
|
33
33
|
import type { BuildStore, Clock } from '../store/types'
|
|
34
34
|
import type { RepoViewStore } from './repo-view'
|
|
@@ -315,7 +315,7 @@ export async function runOrchestratorTickStep(
|
|
|
315
315
|
event,
|
|
316
316
|
buildState: (options.view !== undefined
|
|
317
317
|
? await options.view.buildState(newest.build)
|
|
318
|
-
:
|
|
318
|
+
: (await readBuildFacts(store, newest.build)).value.state) as unknown as Record<
|
|
319
319
|
string,
|
|
320
320
|
unknown
|
|
321
321
|
>,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { summaryBoundary } from '../kernel/pr-summary'
|
|
2
1
|
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
3
2
|
/**
|
|
4
3
|
* Last-chance publication settlement at the workspace-release boundary
|
|
@@ -12,11 +11,6 @@ import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
|
12
11
|
*/
|
|
13
12
|
import { DISPATCHER } from '../events/envelope'
|
|
14
13
|
import type { AbEvent } from '../events/catalog'
|
|
15
|
-
import {
|
|
16
|
-
latestUncompletedPublicationRequest,
|
|
17
|
-
publicationLostRecorded,
|
|
18
|
-
publicationRequestCompleted,
|
|
19
|
-
} from './publication-state'
|
|
20
14
|
import type { BuildStore } from '../store/types'
|
|
21
15
|
|
|
22
16
|
export interface PublicationLossDeps {
|
|
@@ -35,36 +29,56 @@ export interface PublicationLossDeps {
|
|
|
35
29
|
export async function settlePublicationBeforeRelease(
|
|
36
30
|
deps: PublicationLossDeps,
|
|
37
31
|
slug: string,
|
|
38
|
-
|
|
32
|
+
_events: AbEvent[],
|
|
39
33
|
reason: string,
|
|
40
34
|
): Promise<AbEvent[]> {
|
|
41
|
-
const
|
|
35
|
+
const { readBuildFacts } = await import('../store/reduced-reads')
|
|
36
|
+
await settlePublicationFactsBeforeRelease(
|
|
37
|
+
deps,
|
|
38
|
+
slug,
|
|
39
|
+
(await readBuildFacts(deps.store, slug)).value,
|
|
40
|
+
reason,
|
|
41
|
+
)
|
|
42
|
+
// Legacy callers explicitly requested the raw event output.
|
|
43
|
+
return deps.store.getEvents(slug)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Current-state release guard; the returned facts follow every settlement write. */
|
|
47
|
+
export async function settlePublicationFactsBeforeRelease(
|
|
48
|
+
deps: PublicationLossDeps,
|
|
49
|
+
slug: string,
|
|
50
|
+
facts: import('../store/reduced-reads').CurrentBuildFacts,
|
|
51
|
+
reason: string,
|
|
52
|
+
) {
|
|
53
|
+
const { readBuildFacts } = await import('../store/reduced-reads')
|
|
54
|
+
const request = facts.publication.latestUncompletedRequest()
|
|
42
55
|
if (request === undefined) {
|
|
43
|
-
const workspace = events.findLast((event) => event.type === 'workspace.provisioned')
|
|
44
56
|
if (
|
|
45
|
-
workspace
|
|
46
|
-
isRemoteWorkspace(workspace
|
|
47
|
-
summaryBoundary
|
|
57
|
+
facts.workspace &&
|
|
58
|
+
isRemoteWorkspace(facts.workspace) &&
|
|
59
|
+
facts.summaryBoundary !== undefined
|
|
48
60
|
) {
|
|
49
61
|
try {
|
|
50
62
|
await deps.settlePublication?.(slug)
|
|
51
63
|
} catch {
|
|
52
64
|
/* Summary is best effort. */
|
|
53
65
|
}
|
|
54
|
-
return deps.store
|
|
66
|
+
return (await readBuildFacts(deps.store, slug)).value
|
|
55
67
|
}
|
|
56
|
-
return
|
|
68
|
+
return facts
|
|
57
69
|
}
|
|
58
70
|
try {
|
|
59
71
|
await deps.settlePublication?.(slug)
|
|
60
72
|
} catch {
|
|
61
|
-
|
|
62
|
-
// either way the loss record below is what makes the release safe.
|
|
73
|
+
/* Record loss before release. */
|
|
63
74
|
}
|
|
64
|
-
const refreshed = await deps.store
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
const refreshed = (await readBuildFacts(deps.store, slug)).value
|
|
76
|
+
if (
|
|
77
|
+
refreshed.publication.requestCompleted(request) ||
|
|
78
|
+
refreshed.publication.lostRecorded(request)
|
|
79
|
+
)
|
|
80
|
+
return refreshed
|
|
81
|
+
await deps.store.append(slug, {
|
|
68
82
|
actor: DISPATCHER,
|
|
69
83
|
type: 'publication.lost',
|
|
70
84
|
payload: {
|
|
@@ -75,5 +89,5 @@ export async function settlePublicationBeforeRelease(
|
|
|
75
89
|
reason,
|
|
76
90
|
},
|
|
77
91
|
})
|
|
78
|
-
return
|
|
92
|
+
return (await readBuildFacts(deps.store, slug)).value
|
|
79
93
|
}
|