@defrex/autobuild 0.11.0 → 0.13.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 +3 -1
- package/SPEC.md +335 -31
- package/docs/architecture.md +125 -7
- package/docs/configuration.md +142 -21
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +240 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +97 -7
- package/docs/remote-store-protocol.md +199 -144
- package/docs/remote-ticket-protocol.md +249 -19
- package/docs/setup.md +199 -16
- package/docs/ticket-dependencies.md +1 -1
- package/docs/ticket-migration.md +64 -0
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/build-control.ts +12 -1
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +364 -0
- package/packages/core/src/cli/context.ts +7 -1
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch.ts +336 -41
- package/packages/core/src/cli/harvest.ts +13 -3
- package/packages/core/src/cli/help.ts +51 -16
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +60 -16
- package/packages/core/src/cli/mcp.ts +9 -2
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/self-update.ts +15 -1
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +172 -35
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +197 -60
- package/packages/core/src/config/live.ts +3 -0
- package/packages/core/src/config/schema.ts +90 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +11 -2
- package/packages/core/src/events/payloads.ts +50 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/integration/harness.ts +37 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +145 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +11 -1
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +10 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +2 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/index.ts +12 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/ticket-source-backend.ts +17 -20
- package/packages/core/src/operator/tickets.ts +136 -28
- package/packages/core/src/plugin-sdk/index.ts +7 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +7 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +3 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +54 -10
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -3
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +96 -0
- package/packages/core/src/ports/tickets/remote.ts +61 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +6 -1
- package/packages/core/src/ports/workspace/create.ts +15 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +51 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +168 -41
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +31 -4
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +166 -104
- package/packages/core/src/processes/harvest.ts +34 -14
- package/packages/core/src/processes/phase-failure-budget.ts +2 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +35 -0
- package/packages/core/src/store/contract.ts +32 -1
- package/packages/core/src/store/local/store.ts +1 -1
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +25 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +1 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +232 -34
- package/skills/guide/references/operator-api.md +142 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +199 -144
- package/skills/guide/references/remote-ticket-protocol.md +253 -18
- package/skills/guide/references/setup.md +199 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/operate/SKILL.md +126 -36
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ensureExecutionProgress } from './execution-no-progress'
|
|
2
|
+
import { ticketIdentity } from '../ports/tickets/identity'
|
|
3
|
+
import { ticketLifecycle } from '../ports/tickets/lifecycle'
|
|
4
|
+
import { ensureDispatchBudget } from './dispatch-failure-budget'
|
|
1
5
|
import {
|
|
2
6
|
reduceTicketSourceHealth,
|
|
3
7
|
type TicketSourceDegradation,
|
|
@@ -61,6 +65,8 @@ import {
|
|
|
61
65
|
} from '../kernel/auto-merge-default'
|
|
62
66
|
import { reduceDispatchSettings } from '../kernel/dispatch-settings'
|
|
63
67
|
import { decideNextFromFacts } from '../kernel/engine'
|
|
68
|
+
import { composeBuildConfig } from '../config/live'
|
|
69
|
+
import { BUILD_EFFECTIVE_CONFIG_ARTIFACT, parseEffectiveBuildConfig } from './build-execution-state'
|
|
64
70
|
import {
|
|
65
71
|
DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
|
|
66
72
|
decideHarvestControl,
|
|
@@ -82,7 +88,7 @@ import type {
|
|
|
82
88
|
import { TicketSourceRateLimitError } from '../ports/types'
|
|
83
89
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
84
90
|
import type { ArtifactMeta, BuildDigest, BuildRecord, BuildStore, Clock } from '../store/types'
|
|
85
|
-
import {
|
|
91
|
+
import { preparePinnedAssets } from '../store/ticket-assets'
|
|
86
92
|
import { resolveRepoOrigin } from '../cli/repo-state'
|
|
87
93
|
import { specConformance } from '../spec-standard'
|
|
88
94
|
export { specConformance, type SpecConformance } from '../spec-standard'
|
|
@@ -147,16 +153,11 @@ export function readyCriteria(config: Config): {
|
|
|
147
153
|
* readiness is: the file tracker's grooming area IS the `triage/` directory,
|
|
148
154
|
* while a Linear team only has a "Triage" workflow state when the team's
|
|
149
155
|
* triage feature is enabled — Backlog is the state every Linear team has.
|
|
150
|
-
*
|
|
151
|
-
*
|
|
156
|
+
* Hosted projects default to lowercase triage; plugin sources use the neutral
|
|
157
|
+
* `Triage` fallback unless configured.
|
|
152
158
|
*/
|
|
153
159
|
export function defaultTriageState(config: Config): string {
|
|
154
|
-
return (
|
|
155
|
-
config.tickets.triageState ??
|
|
156
|
-
(config.tickets.source === 'linear' || config.tickets.source === 'hosted'
|
|
157
|
-
? 'Backlog'
|
|
158
|
-
: 'Triage')
|
|
159
|
-
)
|
|
160
|
+
return ticketLifecycle(config.tickets).triage
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
/**
|
|
@@ -546,6 +547,8 @@ export interface DispatcherDeps {
|
|
|
546
547
|
retiredWorkspaces?: readonly WorkspaceProvider[]
|
|
547
548
|
forge: Forge
|
|
548
549
|
config: Config
|
|
550
|
+
/** Configuration-only publication for incompatible pins; never launches a runner. */
|
|
551
|
+
repairBuildConfig?: (slug: string) => Promise<void>
|
|
549
552
|
/** Current process snapshot, captured once at the beginning of each tick.
|
|
550
553
|
* Omission preserves the cron/static-config API. */
|
|
551
554
|
getConfig?: () => Config
|
|
@@ -646,6 +649,38 @@ export function heldByRepositoryPause(state: BuildState, paused: boolean): boole
|
|
|
646
649
|
}
|
|
647
650
|
|
|
648
651
|
export class Dispatcher {
|
|
652
|
+
/** Read at each decision boundary: artifact revisions are independent of events. */
|
|
653
|
+
private async decideBuild(
|
|
654
|
+
slug: string,
|
|
655
|
+
state: Parameters<typeof decideNextFromFacts>[0],
|
|
656
|
+
log: Parameters<typeof decideNextFromFacts>[1],
|
|
657
|
+
report: TickReport,
|
|
658
|
+
): Promise<ReturnType<typeof decideNextFromFacts> | undefined> {
|
|
659
|
+
let revision: number | undefined
|
|
660
|
+
try {
|
|
661
|
+
let artifact = await this.store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
662
|
+
if (artifact === null) return decideNextFromFacts(state, log, this.deps.config)
|
|
663
|
+
revision = artifact.meta.revision
|
|
664
|
+
let pinned: Config
|
|
665
|
+
try {
|
|
666
|
+
pinned = parseEffectiveBuildConfig(artifact)
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (this.deps.repairBuildConfig === undefined) throw error
|
|
669
|
+
await this.deps.repairBuildConfig(slug)
|
|
670
|
+
artifact = await this.store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
671
|
+
if (artifact === null) throw new Error('configuration repair deposited no pipeline')
|
|
672
|
+
revision = artifact.meta.revision
|
|
673
|
+
pinned = parseEffectiveBuildConfig(artifact)
|
|
674
|
+
}
|
|
675
|
+
return decideNextFromFacts(state, log, composeBuildConfig(pinned, this.deps.config))
|
|
676
|
+
} catch (error) {
|
|
677
|
+
report.blockedDiagnostics.push(
|
|
678
|
+
`build ${slug} pipeline${revision === undefined ? '' : ` revision ${revision}`} unavailable: pipeline decisions withheld; next tick retries. Restore readable build-branch configuration or use a compatible dispatcher version. ${error instanceof Error ? error.message : String(error)}`,
|
|
679
|
+
)
|
|
680
|
+
return undefined
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
649
684
|
/** Memoized normalized origin of the served repository (one git call per
|
|
650
685
|
* dispatcher), recorded on created builds so ambient reads can accept a
|
|
651
686
|
* differently located checkout of the same repository by origin equality.
|
|
@@ -665,7 +700,15 @@ export class Dispatcher {
|
|
|
665
700
|
return this.repoOrigin
|
|
666
701
|
}
|
|
667
702
|
private readonly leaseTtlMs: number
|
|
668
|
-
private
|
|
703
|
+
private get doneState(): string {
|
|
704
|
+
return (
|
|
705
|
+
this.deps.opts?.doneState ??
|
|
706
|
+
(this.deps.config.tickets.source === 'hosted'
|
|
707
|
+
? ticketLifecycle(this.deps.config.tickets).done
|
|
708
|
+
: undefined) ??
|
|
709
|
+
'Done'
|
|
710
|
+
)
|
|
711
|
+
}
|
|
669
712
|
private readonly slugNamingTimeoutMs: number
|
|
670
713
|
private readonly maxHarvestRecoveryAttempts: number
|
|
671
714
|
private readonly provisionStaleMs: number
|
|
@@ -695,7 +738,6 @@ export class Dispatcher {
|
|
|
695
738
|
? deps.store
|
|
696
739
|
: new RepoViewStore(deps.store, { repo: deps.repo })
|
|
697
740
|
this.leaseTtlMs = deps.opts?.leaseTtlMs ?? 0
|
|
698
|
-
this.doneState = deps.opts?.doneState ?? 'Done'
|
|
699
741
|
this.slugNamingTimeoutMs = deps.opts?.slugNamingTimeoutMs ?? DEFAULT_SLUG_NAMING_TIMEOUT_MS
|
|
700
742
|
this.maxHarvestRecoveryAttempts =
|
|
701
743
|
deps.opts?.maxHarvestRecoveryAttempts ?? DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS
|
|
@@ -980,6 +1022,23 @@ export class Dispatcher {
|
|
|
980
1022
|
}
|
|
981
1023
|
}
|
|
982
1024
|
|
|
1025
|
+
private async activeTicketIdentities(refs: TicketRef[], listed: Ticket[]): Promise<Set<string>> {
|
|
1026
|
+
const key = (ref: TicketRef) => `${ref.source}:${ref.id}`
|
|
1027
|
+
const ids = new Set(refs.map(key))
|
|
1028
|
+
if (this.deps.tickets.name !== 'hosted' || listed.length === 0) return ids
|
|
1029
|
+
const listedIds = new Set(listed.map((ticket) => key(ticket.ref)))
|
|
1030
|
+
const identity = ticketIdentity(this.deps.tickets)
|
|
1031
|
+
try {
|
|
1032
|
+
for (const ref of refs) {
|
|
1033
|
+
if (ref.source === 'hosted' && !listedIds.has(key(ref))) ids.add(await identity(ref))
|
|
1034
|
+
}
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
await this.sourceFailure(error, 'ready-scan')
|
|
1037
|
+
throw error
|
|
1038
|
+
}
|
|
1039
|
+
return ids
|
|
1040
|
+
}
|
|
1041
|
+
|
|
983
1042
|
/** Observation-only ready listing for Store-backed frontends while intake is
|
|
984
1043
|
* disabled. It shares the active-ticket exclusion with dispatch but performs
|
|
985
1044
|
* no dependency fetch, claim, bounce, build, workspace, or launch side
|
|
@@ -991,20 +1050,21 @@ export class Dispatcher {
|
|
|
991
1050
|
}> {
|
|
992
1051
|
this.deps.config = this.deps.getConfig?.() ?? this.deps.config
|
|
993
1052
|
await this.store.refresh()
|
|
994
|
-
const activeTicketIds = new Set<string>()
|
|
995
|
-
for (const { record, digest } of await this.repoBuildDigests()) {
|
|
996
|
-
if (record.ticket !== undefined && digest.terminal === undefined) {
|
|
997
|
-
activeTicketIds.add(record.ticket.id)
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
1053
|
const report = emptyTickReport()
|
|
1001
1054
|
this.restoreScan(report)
|
|
1002
1055
|
const checkpoint = await this.scanCheckpoint('observation')
|
|
1003
1056
|
if (checkpoint.due) {
|
|
1004
1057
|
await this.store.refreshJournal()
|
|
1005
1058
|
const listing = await this.listReady()
|
|
1059
|
+
const refs: TicketRef[] = []
|
|
1060
|
+
for (const { record, digest } of await this.repoBuildDigests()) {
|
|
1061
|
+
if (record.ticket && digest.terminal === undefined) refs.push(record.ticket)
|
|
1062
|
+
}
|
|
1063
|
+
const activeTicketIds = await this.activeTicketIdentities(refs, listing.tickets)
|
|
1006
1064
|
await this.store.refreshJournal()
|
|
1007
|
-
report.queued = listing.tickets.filter(
|
|
1065
|
+
report.queued = listing.tickets.filter(
|
|
1066
|
+
(ticket) => !activeTicketIds.has(`${ticket.ref.source}:${ticket.ref.id}`),
|
|
1067
|
+
).length
|
|
1008
1068
|
report.invalidTickets = listing.diagnostics.length
|
|
1009
1069
|
report.ticketDiagnostics = [...listing.diagnostics]
|
|
1010
1070
|
report.dependencyBlocked = 0
|
|
@@ -1165,9 +1225,24 @@ export class Dispatcher {
|
|
|
1165
1225
|
}
|
|
1166
1226
|
}
|
|
1167
1227
|
|
|
1168
|
-
private async launch(
|
|
1228
|
+
private async launch(
|
|
1229
|
+
slug: string,
|
|
1230
|
+
launched: Set<string>,
|
|
1231
|
+
report: TickReport = emptyTickReport(),
|
|
1232
|
+
): Promise<LaunchRunnerResult> {
|
|
1169
1233
|
if (launched.has(slug)) return 'already-active'
|
|
1170
1234
|
launched.add(slug)
|
|
1235
|
+
if (
|
|
1236
|
+
!(await ensureExecutionProgress(
|
|
1237
|
+
this.store,
|
|
1238
|
+
this.deps.ids,
|
|
1239
|
+
slug,
|
|
1240
|
+
this.deps.config,
|
|
1241
|
+
(state, log) => this.decideBuild(slug, state, log, report),
|
|
1242
|
+
))
|
|
1243
|
+
) {
|
|
1244
|
+
return 'already-active'
|
|
1245
|
+
}
|
|
1171
1246
|
return this.deps.launchRunner(slug)
|
|
1172
1247
|
}
|
|
1173
1248
|
|
|
@@ -1508,6 +1583,10 @@ export class Dispatcher {
|
|
|
1508
1583
|
generation,
|
|
1509
1584
|
seed,
|
|
1510
1585
|
tail,
|
|
1586
|
+
}).catch(() => {
|
|
1587
|
+
// Detached work must never reject into the dispatcher process. Failure
|
|
1588
|
+
// recording can itself fail on a store outage; durable boundaries and
|
|
1589
|
+
// lease expiry let the next tick repair or retry the unfinished work.
|
|
1511
1590
|
})
|
|
1512
1591
|
return 'kicked'
|
|
1513
1592
|
}
|
|
@@ -1542,6 +1621,7 @@ export class Dispatcher {
|
|
|
1542
1621
|
const slug = entry.slug
|
|
1543
1622
|
let leaseClaimed = false
|
|
1544
1623
|
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
1624
|
+
let boundary: 'workspace' | 'read' | 'dispatch' | 'launch' = 'workspace'
|
|
1545
1625
|
const stage: EventPayload<'dispatch.failed'>['stage'] = 'workspace'
|
|
1546
1626
|
try {
|
|
1547
1627
|
if (entry.controller.signal.aborted) {
|
|
@@ -1606,12 +1686,19 @@ export class Dispatcher {
|
|
|
1606
1686
|
await store.releaseLease(slug, entry.instance)
|
|
1607
1687
|
leaseClaimed = false
|
|
1608
1688
|
if (entry.controller.signal.aborted) return
|
|
1689
|
+
boundary = 'read'
|
|
1609
1690
|
const record = await store.getBuild(slug)
|
|
1610
1691
|
if (record === null) return
|
|
1611
|
-
await (
|
|
1612
|
-
|
|
1613
|
-
|
|
1692
|
+
const queued = reduceBuild(await store.getEvents(slug)).status === 'queued'
|
|
1693
|
+
boundary = input.tail === 'dispatch' || queued ? 'dispatch' : 'launch'
|
|
1694
|
+
if (boundary === 'dispatch') await this.completeDispatchBoundaries(record, input.seed)
|
|
1695
|
+
else await this.deps.launchRunner(slug)
|
|
1614
1696
|
} catch (error) {
|
|
1697
|
+
// Dispatch boundaries own their failure facts and exhaustion repair.
|
|
1698
|
+
// A failed tail read/write leaves durable unfinished work for the next
|
|
1699
|
+
// tick; it is not another provider workspace failure. Replacement
|
|
1700
|
+
// runner launch errors retain the existing infrastructure-budget path.
|
|
1701
|
+
if (boundary === 'read' || boundary === 'dispatch') return
|
|
1615
1702
|
const events = await store.getEvents(slug)
|
|
1616
1703
|
if (workspaces.recovery !== undefined) {
|
|
1617
1704
|
const open = openWorkspace(events)
|
|
@@ -2069,7 +2156,7 @@ export class Dispatcher {
|
|
|
2069
2156
|
}
|
|
2070
2157
|
// The dispatcher never runs agents (§15.7): re-attach a build-runner,
|
|
2071
2158
|
// which executes the reconcile epilogue phase.
|
|
2072
|
-
await this.launch(record.slug, launched)
|
|
2159
|
+
await this.launch(record.slug, launched, report)
|
|
2073
2160
|
report.conflicted += 1
|
|
2074
2161
|
return
|
|
2075
2162
|
}
|
|
@@ -2103,12 +2190,12 @@ export class Dispatcher {
|
|
|
2103
2190
|
const latest = await store.buildFacts(record.slug)
|
|
2104
2191
|
const latestState = latest.state
|
|
2105
2192
|
const latestIntent = pendingAutoMerge(latestState)
|
|
2106
|
-
const decision =
|
|
2193
|
+
const decision = await this.decideBuild(record.slug, latest.state, latest.log, report)
|
|
2107
2194
|
if (
|
|
2108
2195
|
latestState.pr?.number === pr.number &&
|
|
2109
2196
|
latestIntent?.enabled === true &&
|
|
2110
2197
|
latestIntent.commandSeq === autoMerge.commandSeq &&
|
|
2111
|
-
decision
|
|
2198
|
+
decision?.kind === 'wait' &&
|
|
2112
2199
|
decision.reason === 'awaiting-pr'
|
|
2113
2200
|
) {
|
|
2114
2201
|
await forge.squashMerge(workspacePath, pr.number, result.headSha)
|
|
@@ -2485,6 +2572,16 @@ export class Dispatcher {
|
|
|
2485
2572
|
return sha
|
|
2486
2573
|
}
|
|
2487
2574
|
|
|
2575
|
+
private dispatchBudget(slug: string): Promise<boolean> {
|
|
2576
|
+
return ensureDispatchBudget(
|
|
2577
|
+
this.store,
|
|
2578
|
+
this.deps.ids,
|
|
2579
|
+
slug,
|
|
2580
|
+
this.deps.config.policy.maxDispatchAttempts,
|
|
2581
|
+
this.deps.workspaces.recovery !== undefined,
|
|
2582
|
+
)
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2488
2585
|
// ── b. Interrupted dispatch recovery ───────────────────────────────────────
|
|
2489
2586
|
|
|
2490
2587
|
/** The claim-time `build.created` payload. Assets are omitted when the ticket
|
|
@@ -2539,6 +2636,7 @@ export class Dispatcher {
|
|
|
2539
2636
|
): Promise<DispatchOutcome> {
|
|
2540
2637
|
const store = this.store
|
|
2541
2638
|
const { config } = this.deps
|
|
2639
|
+
if (!(await this.dispatchBudget(record.slug))) return 'parked'
|
|
2542
2640
|
let stage: EventPayload<'dispatch.failed'>['stage'] = 'create'
|
|
2543
2641
|
try {
|
|
2544
2642
|
let events = await store.getEvents(record.slug)
|
|
@@ -2553,7 +2651,14 @@ export class Dispatcher {
|
|
|
2553
2651
|
// can only sample the ticket's current assets (SPEC §6.3).
|
|
2554
2652
|
const assets =
|
|
2555
2653
|
seed?.assets ??
|
|
2556
|
-
(await
|
|
2654
|
+
(await preparePinnedAssets(this.deps.tickets, store, record.repo, record.ticket.id).catch(
|
|
2655
|
+
(error) => {
|
|
2656
|
+
// Preserve legacy recovery only without native assets. Native preparation
|
|
2657
|
+
// must never publish an empty pin after losing the source bytes.
|
|
2658
|
+
if (this.deps.tickets.assets || this.deps.tickets.name === 'hosted') throw error
|
|
2659
|
+
return []
|
|
2660
|
+
},
|
|
2661
|
+
))
|
|
2557
2662
|
await store.appendIfCurrent(record.slug, 0, {
|
|
2558
2663
|
actor: DISPATCHER,
|
|
2559
2664
|
type: 'build.created',
|
|
@@ -2628,8 +2733,6 @@ export class Dispatcher {
|
|
|
2628
2733
|
} satisfies EventWrite<'workspace.provisioned'>)
|
|
2629
2734
|
events = await store.getEvents(record.slug)
|
|
2630
2735
|
}
|
|
2631
|
-
|
|
2632
|
-
return await this.completeDispatchBoundaries(record, seed, report, launched)
|
|
2633
2736
|
} catch (error) {
|
|
2634
2737
|
const events = await store.getEvents(record.slug)
|
|
2635
2738
|
if (stage === 'workspace' && this.deps.workspaces.recovery !== undefined) {
|
|
@@ -2654,8 +2757,10 @@ export class Dispatcher {
|
|
|
2654
2757
|
},
|
|
2655
2758
|
})
|
|
2656
2759
|
report.dispatchFailed += 1
|
|
2760
|
+
await this.dispatchBudget(record.slug)
|
|
2657
2761
|
return 'failed'
|
|
2658
2762
|
}
|
|
2763
|
+
return this.completeDispatchBoundaries(record, seed, report, launched)
|
|
2659
2764
|
}
|
|
2660
2765
|
|
|
2661
2766
|
/** The post-workspace dispatch boundaries — spec, comment, launch — shared
|
|
@@ -2676,10 +2781,13 @@ export class Dispatcher {
|
|
|
2676
2781
|
): Promise<Exclude<DispatchOutcome, 'deferred'>> {
|
|
2677
2782
|
const store = this.store
|
|
2678
2783
|
|
|
2784
|
+
if (!(await this.dispatchBudget(record.slug))) return 'parked'
|
|
2679
2785
|
let stage: EventPayload<'dispatch.failed'>['stage'] = 'spec'
|
|
2680
2786
|
let authored = false
|
|
2681
2787
|
try {
|
|
2682
2788
|
const events = await store.getEvents(record.slug)
|
|
2789
|
+
const state = reduceBuild(events)
|
|
2790
|
+
if (state.status !== 'queued' || state.discardRequest !== undefined) return 'parked'
|
|
2683
2791
|
if (
|
|
2684
2792
|
!events.some((event) => event.type === 'spec.imported' || event.type === 'spec.authored')
|
|
2685
2793
|
) {
|
|
@@ -2762,7 +2870,7 @@ export class Dispatcher {
|
|
|
2762
2870
|
return 'parked'
|
|
2763
2871
|
}
|
|
2764
2872
|
if (launched === undefined) await this.deps.launchRunner(record.slug)
|
|
2765
|
-
else await this.launch(record.slug, launched)
|
|
2873
|
+
else await this.launch(record.slug, launched, report)
|
|
2766
2874
|
if (authored && report !== undefined) report.authored += 1
|
|
2767
2875
|
return 'completed'
|
|
2768
2876
|
} catch (error) {
|
|
@@ -2779,6 +2887,7 @@ export class Dispatcher {
|
|
|
2779
2887
|
},
|
|
2780
2888
|
})
|
|
2781
2889
|
if (report !== undefined) report.dispatchFailed += 1
|
|
2890
|
+
await this.dispatchBudget(record.slug)
|
|
2782
2891
|
return 'failed'
|
|
2783
2892
|
}
|
|
2784
2893
|
}
|
|
@@ -2810,9 +2919,11 @@ export class Dispatcher {
|
|
|
2810
2919
|
// Queued itself means runner attachment is still missing. Always enter
|
|
2811
2920
|
// the fact-driven helper: it skips every completed boundary and retries
|
|
2812
2921
|
// only the final launch when setup is already durable.
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2922
|
+
const outcome = await this.attemptDispatchCompletion(record, launched, report)
|
|
2923
|
+
if (outcome === 'completed') report.recovered += 1
|
|
2924
|
+
// A failed boundary owns this tick's attempt; startup and lease recovery
|
|
2925
|
+
// must not bypass the unfinished dispatch boundary later in the tick.
|
|
2926
|
+
if (outcome === 'failed') launched.add(record.slug)
|
|
2816
2927
|
}
|
|
2817
2928
|
}
|
|
2818
2929
|
|
|
@@ -2836,7 +2947,6 @@ export class Dispatcher {
|
|
|
2836
2947
|
opts: TickOpts = {},
|
|
2837
2948
|
): Promise<void> {
|
|
2838
2949
|
const store = this.store
|
|
2839
|
-
const { config } = this.deps
|
|
2840
2950
|
for (const record of await this.nonterminalBuilds()) {
|
|
2841
2951
|
if (launched.has(record.slug)) continue
|
|
2842
2952
|
// Budget gate: resume launches runners (transport-backed work).
|
|
@@ -2853,7 +2963,14 @@ export class Dispatcher {
|
|
|
2853
2963
|
continue
|
|
2854
2964
|
}
|
|
2855
2965
|
|
|
2856
|
-
|
|
2966
|
+
if (
|
|
2967
|
+
state.status === 'queued' &&
|
|
2968
|
+
!state.pendingCommands.some((command) => command.command === 'abort') &&
|
|
2969
|
+
!(await this.dispatchBudget(record.slug))
|
|
2970
|
+
)
|
|
2971
|
+
continue
|
|
2972
|
+
const decision = await this.decideBuild(record.slug, state, facts.log, report)
|
|
2973
|
+
if (decision === undefined) continue
|
|
2857
2974
|
if (decision.kind === 'wait') {
|
|
2858
2975
|
if (decision.reason === 'blocked' && state.openEscalations.length > 0) {
|
|
2859
2976
|
report.blockedDiagnostics.push(
|
|
@@ -2867,7 +2984,7 @@ export class Dispatcher {
|
|
|
2867
2984
|
// Disposable remote environments must pass through the stale-lease
|
|
2868
2985
|
// fencing sweep below; startup must never attach directly to an old VM.
|
|
2869
2986
|
if (this.deps.workspaces.recovery !== undefined) continue
|
|
2870
|
-
const result = await this.launch(record.slug, launched)
|
|
2987
|
+
const result = await this.launch(record.slug, launched, report)
|
|
2871
2988
|
if (result === 'scheduled') report.resumed += 1
|
|
2872
2989
|
}
|
|
2873
2990
|
}
|
|
@@ -2923,7 +3040,14 @@ export class Dispatcher {
|
|
|
2923
3040
|
) {
|
|
2924
3041
|
continue
|
|
2925
3042
|
}
|
|
2926
|
-
|
|
3043
|
+
if (
|
|
3044
|
+
state.status === 'queued' &&
|
|
3045
|
+
!state.pendingCommands.some((command) => command.command === 'abort') &&
|
|
3046
|
+
!(await this.dispatchBudget(record.slug))
|
|
3047
|
+
)
|
|
3048
|
+
continue
|
|
3049
|
+
const decision = await this.decideBuild(record.slug, state, facts.log, report)
|
|
3050
|
+
if (decision === undefined) continue
|
|
2927
3051
|
if (state.status === 'done' || state.status === 'aborted') continue
|
|
2928
3052
|
// Raw history, read at most once and only by a branch that acts on it.
|
|
2929
3053
|
let loaded: AbEvent[] | undefined
|
|
@@ -3024,7 +3148,7 @@ export class Dispatcher {
|
|
|
3024
3148
|
// boundaries and the launch; this tick performs neither.
|
|
3025
3149
|
continue
|
|
3026
3150
|
}
|
|
3027
|
-
const result = await this.launch(record.slug, launched)
|
|
3151
|
+
const result = await this.launch(record.slug, launched, report)
|
|
3028
3152
|
if (result === 'scheduled') report.swept += 1
|
|
3029
3153
|
}
|
|
3030
3154
|
}
|
|
@@ -3052,11 +3176,11 @@ export class Dispatcher {
|
|
|
3052
3176
|
// pending work are live, only waiting on a human. Capacity is per repo
|
|
3053
3177
|
// (§16.1) — another repo's builds never consume this repo's slots.
|
|
3054
3178
|
let active = 0
|
|
3055
|
-
const
|
|
3179
|
+
const activeRefs: TicketRef[] = []
|
|
3056
3180
|
for (const { record, digest } of await this.repoBuildDigests()) {
|
|
3057
3181
|
if (digest.terminal === undefined) {
|
|
3058
3182
|
active += 1
|
|
3059
|
-
if (record.ticket)
|
|
3183
|
+
if (record.ticket) activeRefs.push(record.ticket)
|
|
3060
3184
|
}
|
|
3061
3185
|
}
|
|
3062
3186
|
let capacity = config.capacity - active
|
|
@@ -3068,6 +3192,7 @@ export class Dispatcher {
|
|
|
3068
3192
|
await this.store.refreshJournal()
|
|
3069
3193
|
const journalBeforeReady = this.store.recordedJournal()
|
|
3070
3194
|
const listing = await this.listReady()
|
|
3195
|
+
const activeTicketIds = await this.activeTicketIdentities(activeRefs, listing.tickets)
|
|
3071
3196
|
await this.store.refreshJournal()
|
|
3072
3197
|
const journalAfterReady = this.store.recordedJournal()
|
|
3073
3198
|
if (reduceTicketSourceHealth(journalAfterReady)?.cause === 'ready-scan-failed') {
|
|
@@ -3079,7 +3204,9 @@ export class Dispatcher {
|
|
|
3079
3204
|
creation,
|
|
3080
3205
|
]),
|
|
3081
3206
|
)
|
|
3082
|
-
const ready = listing.tickets.filter(
|
|
3207
|
+
const ready = listing.tickets.filter(
|
|
3208
|
+
(ticket) => !activeTicketIds.has(`${ticket.ref.source}:${ticket.ref.id}`),
|
|
3209
|
+
)
|
|
3083
3210
|
report.invalidTickets += listing.diagnostics.length
|
|
3084
3211
|
report.ticketDiagnostics.push(...listing.diagnostics)
|
|
3085
3212
|
report.queued = ready.length
|
|
@@ -3176,7 +3303,7 @@ export class Dispatcher {
|
|
|
3176
3303
|
|
|
3177
3304
|
// Freeze the ticket's assets at the claim (SPEC §6.3): sampled before the
|
|
3178
3305
|
// spec-authoring interval so edits made during it are never pinned.
|
|
3179
|
-
const assets = await
|
|
3306
|
+
const assets = await preparePinnedAssets(tickets, store, this.deps.repo, ticket.ref.id)
|
|
3180
3307
|
|
|
3181
3308
|
// Quality gate (§6.3): import a conforming ticket body as the spec;
|
|
3182
3309
|
// author one for thin-but-groomed tickets; otherwise bounce.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { AbEvent } from '../events/catalog'
|
|
2
|
+
import { DISPATCHER } from '../events/envelope'
|
|
3
|
+
import type { EventType } from '../events/payloads'
|
|
4
|
+
import type { IdSource } from '../ids'
|
|
5
|
+
import type { Config } from '../config/schema'
|
|
6
|
+
import { type Decision, decideNextFromFacts } from '../kernel/engine'
|
|
7
|
+
import { defineReducer } from '../kernel/incremental'
|
|
8
|
+
import type { RepoViewStore } from './repo-view'
|
|
9
|
+
|
|
10
|
+
/** Every new event must explicitly choose whether it advances build work. */
|
|
11
|
+
export const executionProgress = {
|
|
12
|
+
'build.created': 'progress',
|
|
13
|
+
'build.completed': 'progress',
|
|
14
|
+
'runner.attached': 'bookkeeping',
|
|
15
|
+
'runner.setup-failed': 'failure',
|
|
16
|
+
'workspace.provisioned': 'bookkeeping',
|
|
17
|
+
'workspace.provision-started': 'bookkeeping',
|
|
18
|
+
'workspace.released': 'bookkeeping',
|
|
19
|
+
'execution.started': 'bookkeeping',
|
|
20
|
+
'execution.ended': 'bookkeeping',
|
|
21
|
+
'infrastructure.failed': 'failure',
|
|
22
|
+
'infrastructure.cleanup-attempted': 'bookkeeping',
|
|
23
|
+
'pr-summary.requested': 'progress',
|
|
24
|
+
'pr-summary.attempted': 'progress',
|
|
25
|
+
'pr-summary.result': 'progress',
|
|
26
|
+
'pr-summary.settled': 'progress',
|
|
27
|
+
'finalize.step-started': 'progress',
|
|
28
|
+
'publication.requested': 'progress',
|
|
29
|
+
'publication.lost': 'progress',
|
|
30
|
+
'abort.remote-branch-deleted': 'progress',
|
|
31
|
+
'abort.local-branch-deleted': 'progress',
|
|
32
|
+
'abort.ticket-returned': 'progress',
|
|
33
|
+
'dispatch.failed': 'failure',
|
|
34
|
+
'dispatch.comment-posted': 'bookkeeping',
|
|
35
|
+
'build.pause-requested': 'progress',
|
|
36
|
+
'build.resume-requested': 'progress',
|
|
37
|
+
'build.abort-requested': 'progress',
|
|
38
|
+
'build.discard-requested': 'progress',
|
|
39
|
+
'build.auto-merge-requested': 'progress',
|
|
40
|
+
'build.auto-merge-cancelled': 'progress',
|
|
41
|
+
'build.auto-merge-default-observed': 'bookkeeping',
|
|
42
|
+
'build.paused': 'progress',
|
|
43
|
+
'build.resumed': 'progress',
|
|
44
|
+
'build.aborted': 'progress',
|
|
45
|
+
'spec.imported': 'progress',
|
|
46
|
+
'spec.authored': 'progress',
|
|
47
|
+
'spec.revised': 'progress',
|
|
48
|
+
'session.started': 'progress',
|
|
49
|
+
'session.ended': 'progress',
|
|
50
|
+
'plan.started': 'progress',
|
|
51
|
+
'plan.completed': 'progress',
|
|
52
|
+
'plan-review.started': 'progress',
|
|
53
|
+
'plan-review.verdict': 'progress',
|
|
54
|
+
'implement.started': 'progress',
|
|
55
|
+
'implement.completed': 'progress',
|
|
56
|
+
'code-review.started': 'progress',
|
|
57
|
+
'code-review.verdict': 'progress',
|
|
58
|
+
'verify.reconcile-policy-captured': 'progress',
|
|
59
|
+
'verify.started': 'progress',
|
|
60
|
+
'verify.completed': 'progress',
|
|
61
|
+
'finalize.started': 'progress',
|
|
62
|
+
'finalize.completed': 'progress',
|
|
63
|
+
'finalize.step-completed': 'progress',
|
|
64
|
+
'pr-attachment.designated': 'progress',
|
|
65
|
+
'pr-attachment.hosted': 'progress',
|
|
66
|
+
'pr-attachment.reclaimed': 'progress',
|
|
67
|
+
'pr-attachment.reclaim-failed': 'progress',
|
|
68
|
+
'pr.auto-merge-enabled': 'progress',
|
|
69
|
+
'pr.auto-merge-disabled': 'progress',
|
|
70
|
+
'pr.merged': 'progress',
|
|
71
|
+
'pr.closed': 'progress',
|
|
72
|
+
'pr.conflicted': 'progress',
|
|
73
|
+
'reconcile.progress-checked': 'progress',
|
|
74
|
+
'reconcile.started': 'progress',
|
|
75
|
+
'reconcile.completed': 'progress',
|
|
76
|
+
'observation.recorded': 'progress',
|
|
77
|
+
'escalation.raised': 'progress',
|
|
78
|
+
'escalation.answered': 'progress',
|
|
79
|
+
'phase.failed': 'failure',
|
|
80
|
+
} satisfies Record<EventType, 'bookkeeping' | 'failure' | 'progress'>
|
|
81
|
+
|
|
82
|
+
interface Accumulator {
|
|
83
|
+
active: { instance: string; seq: number; eligible: boolean } | null
|
|
84
|
+
count: number
|
|
85
|
+
open: string[]
|
|
86
|
+
latest: { instance: string; seq: number } | null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const executionNoProgressReducer = defineReducer({
|
|
90
|
+
version: 1,
|
|
91
|
+
initial: (): Accumulator => ({ active: null, count: 0, open: [], latest: null }),
|
|
92
|
+
fold(acc, events: readonly AbEvent[]) {
|
|
93
|
+
for (const event of events) {
|
|
94
|
+
if (event.type === 'execution.started') {
|
|
95
|
+
// An abandoned start breaks the consecutive completed-execution streak.
|
|
96
|
+
if (acc.active !== null) acc.count = 0
|
|
97
|
+
acc.active = { instance: event.payload.instance, seq: event.seq, eligible: true }
|
|
98
|
+
} else if (event.type === 'execution.ended') {
|
|
99
|
+
if (acc.active?.instance !== event.payload.instance) continue
|
|
100
|
+
const clean = event.payload.outcome === 'completed' && event.payload.exitCode === 0
|
|
101
|
+
acc.count = clean && acc.active.eligible ? acc.count + 1 : 0
|
|
102
|
+
acc.latest = { instance: event.payload.instance, seq: event.seq }
|
|
103
|
+
acc.active = null
|
|
104
|
+
} else if (
|
|
105
|
+
event.type === 'escalation.raised' &&
|
|
106
|
+
event.payload.source === 'policy' &&
|
|
107
|
+
event.payload.policyCause === 'execution-no-progress'
|
|
108
|
+
) {
|
|
109
|
+
if (!acc.open.includes(event.payload.id)) acc.open.push(event.payload.id)
|
|
110
|
+
} else if (event.type === 'escalation.answered') {
|
|
111
|
+
if (!acc.open.includes(event.payload.id)) continue
|
|
112
|
+
acc.open = acc.open.filter((id) => id !== event.payload.id)
|
|
113
|
+
acc.count = 0
|
|
114
|
+
acc.active = null
|
|
115
|
+
} else if (executionProgress[event.type] !== 'bookkeeping') {
|
|
116
|
+
acc.count = 0
|
|
117
|
+
if (acc.active !== null) acc.active.eligible = false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
finish: (acc) => ({ ...acc, open: [...acc.open] }),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** Describe routing without copying commands or phase feedback into the question. */
|
|
125
|
+
function expectedAction(decision: Decision): string {
|
|
126
|
+
if (decision.kind === 'run-phase') return `${decision.kind} ${decision.phase}@${decision.round}`
|
|
127
|
+
if ('step' in decision)
|
|
128
|
+
return `${decision.kind} ${decision.step} (attempt ${'attempt' in decision ? decision.attempt : 1})`
|
|
129
|
+
if (decision.kind === 'raise-escalation') return `${decision.kind} ${decision.phase}`
|
|
130
|
+
return decision.kind
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Repair the third-completion/raise crash gap without provisioning execution four. */
|
|
134
|
+
export async function ensureExecutionProgress(
|
|
135
|
+
store: Pick<RepoViewStore, 'buildFacts' | 'appendIfCurrent' | 'refreshBuild'>,
|
|
136
|
+
ids: IdSource,
|
|
137
|
+
slug: string,
|
|
138
|
+
config: Config,
|
|
139
|
+
decide?: (
|
|
140
|
+
state: Parameters<typeof decideNextFromFacts>[0],
|
|
141
|
+
log: Parameters<typeof decideNextFromFacts>[1],
|
|
142
|
+
) => Promise<Decision | undefined>,
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
for (;;) {
|
|
145
|
+
await store.refreshBuild(slug)
|
|
146
|
+
const facts = await store.buildFacts(slug)
|
|
147
|
+
const decision = decide
|
|
148
|
+
? await decide(facts.state, facts.log)
|
|
149
|
+
: decideNextFromFacts(facts.state, facts.log, config)
|
|
150
|
+
if (decision === undefined) return false
|
|
151
|
+
// Commands must still be acknowledged even while this guard is parked.
|
|
152
|
+
if (decision.kind === 'acknowledge') return true
|
|
153
|
+
const streak = facts.executionNoProgress
|
|
154
|
+
if (streak.open.length > 0) return false
|
|
155
|
+
// Callers own wait routing, including publication-recovery launches. A
|
|
156
|
+
// legitimate wait is not dispatcher/runner disagreement.
|
|
157
|
+
if (decision.kind === 'wait') return true
|
|
158
|
+
if (streak.count < 3) return true
|
|
159
|
+
const raised = await store.appendIfCurrent(slug, facts.state.lastSeq, {
|
|
160
|
+
actor: DISPATCHER,
|
|
161
|
+
type: 'escalation.raised',
|
|
162
|
+
payload: {
|
|
163
|
+
id: ids('esc'),
|
|
164
|
+
phase: 'setup',
|
|
165
|
+
source: 'policy',
|
|
166
|
+
policyCause: 'execution-no-progress',
|
|
167
|
+
question: `${streak.count} consecutive executions ended normally without build progress. Dispatcher expected ${expectedAction(decision)}. Inspect the dispatcher/runner disagreement, then answer with guidance or retry.`,
|
|
168
|
+
refs:
|
|
169
|
+
streak.latest === null
|
|
170
|
+
? []
|
|
171
|
+
: [`execution:${streak.latest.instance}`, `seq:${streak.latest.seq}`],
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
if (raised !== null) return false
|
|
175
|
+
}
|
|
176
|
+
}
|