@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
package/README.md
CHANGED
package/SPEC.md
CHANGED
|
@@ -457,7 +457,7 @@ builds id/slug, ticket ref, repo, branch, status (derived), created/update
|
|
|
457
457
|
lease + heartbeat (mutable liveness columns — §15.2.6, never events)
|
|
458
458
|
events build_id, seq, timestamp, actor, type, payload (JSON) — append-only
|
|
459
459
|
artifacts build_id, kind, revision, blobRef, metadata
|
|
460
|
-
repo_streams repo, created/updated, lease + heartbeat
|
|
460
|
+
repo_streams repo, created/updated, lease + heartbeat, bounded tick observation
|
|
461
461
|
repo_events repo, seq, timestamp, actor, type, payload (JSON) — append-only
|
|
462
462
|
repo_artifacts repo, kind, revision, blobRef, metadata
|
|
463
463
|
ticket_assets repo, ticket_id, kind, name, revision, layout, manifest (JSON), size,
|
|
@@ -1926,6 +1926,24 @@ head (or an explicit commit) to a deterministic operator branch and a pull
|
|
|
1926
1926
|
request against the base branch and records published/publish-failed journal
|
|
1927
1927
|
facts.
|
|
1928
1928
|
|
|
1929
|
+
Successful dispatcher ticks with no effects, counters, diagnostics, queue change,
|
|
1930
|
+
or attention transition append no tick boundary events. The first queue baseline,
|
|
1931
|
+
failed ticks, meaningful ticks, diagnostic clearing, failure recovery, and the
|
|
1932
|
+
first retry after a source cooldown remain recorded. Recorded starts may arrive
|
|
1933
|
+
with completion; optional `startedAt` preserves the actual start time, falling
|
|
1934
|
+
back to envelope time for historical journals.
|
|
1935
|
+
|
|
1936
|
+
Repository tick telemetry is bounded mutable operational metadata, like leases.
|
|
1937
|
+
`beginRepoTick` persists the open boundary before unsafe work; `finishRepoTick`
|
|
1938
|
+
atomically replaces it with actual completion time and outcome and appends any
|
|
1939
|
+
validated tick boundaries. Both are fenced by the unexpired repository lease,
|
|
1940
|
+
and neither renews it. Quiet completions advance telemetry without advancing the
|
|
1941
|
+
journal. Readers refresh telemetry independently of event cursors, correlate it
|
|
1942
|
+
by run, and merge with historical completions by actual time; heartbeat never
|
|
1943
|
+
stands in for tick completion. Telemetry survives lease release. Safe shutdown
|
|
1944
|
+
uses the matching run's open observation, historical journal boundaries when
|
|
1945
|
+
absent, and refuses forced termination when the observation cannot be read.
|
|
1946
|
+
|
|
1929
1947
|
The operator's job across many concurrent builds: see status at a glance,
|
|
1930
1948
|
act on a selected build, find blocked builds, answer escalations, and inspect
|
|
1931
1949
|
any build's trail. Each dashboard build row spends its header on identity and
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { readDispatcherRunFacts } from '../packages/core/src/store/reduced-reads'
|
|
2
3
|
/** Private, shipped child for the interactive `ab dispatch` kernel. It owns no
|
|
3
4
|
* terminal handles; the public command launches it with immutable options. */
|
|
4
5
|
import { abDispatch } from '../packages/core/src/cli/dispatch'
|
|
@@ -61,12 +62,9 @@ try {
|
|
|
61
62
|
const opened = openStoreForRepoState(state, { env: process.env })
|
|
62
63
|
try {
|
|
63
64
|
await opened.store.ensureRepo(state.repo)
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
(event) => event.type === 'dispatcher.run-stopped' && event.payload.run === options.run,
|
|
68
|
-
)
|
|
69
|
-
) {
|
|
65
|
+
const facts = (await readDispatcherRunFacts(opened.store, state.repo, options.run, false))
|
|
66
|
+
.value
|
|
67
|
+
if (!facts.stop) {
|
|
70
68
|
await opened.store.appendRepo(state.repo, {
|
|
71
69
|
actor: DISPATCHER,
|
|
72
70
|
type: 'dispatcher.run-stopped',
|
package/docs/architecture.md
CHANGED
|
@@ -145,9 +145,9 @@ by a later advance. Each reducer publishes a `version`; bump it whenever the
|
|
|
145
145
|
accumulator shape or the fold semantics change, because a cached accumulator is
|
|
146
146
|
only valid for the version that wrote it. `kernel/reducer-registry.ts` lists
|
|
147
147
|
every reducer with its version, and its snapshot test fails when an accumulator
|
|
148
|
-
shape changes without a bump.
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
shape changes without a bump. Current-state consumers use the typed persisted
|
|
149
|
+
read helpers in `store/reduced-reads.ts`; whole-array reducers remain pure
|
|
150
|
+
reference projections for tests and explicit historical output.
|
|
151
151
|
|
|
152
152
|
**Reducer snapshots.** The `BuildStore` port can keep, per scope (one build log
|
|
153
153
|
or the repository journal) and registered reducer, that reducer's accumulator as
|
|
@@ -173,72 +173,86 @@ reads every snapshot as missing and replays (SQLSTATE `42P01` is never an
|
|
|
173
173
|
error), and an older client never asks. Run `bun run postgres:migrate` once to
|
|
174
174
|
gain the table; until then the dispatcher and every other reader replay in full.
|
|
175
175
|
|
|
176
|
-
**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
the
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
176
|
+
**Named reducer values.** `BuildStore.getReducerValue(scope, {reducer, args?})`
|
|
177
|
+
returns `{version, cursor, value}` for each eligible entry in the executable
|
|
178
|
+
reducer registry. Eligible reducers consume build or repository events and finish
|
|
179
|
+
as plain JSON; raw journal-event lists, operator-session reducers, collections,
|
|
180
|
+
selector functions, and top-level optional results are explicitly excluded with
|
|
181
|
+
reasons. Factory arguments are validated and participate in a bounded digest
|
|
182
|
+
cache key. The persisted envelope also records the canonical scope, arguments,
|
|
183
|
+
name and version, and nested accumulator shapes are validated before restore.
|
|
184
|
+
The `value.*` namespace is reserved: public snapshot reads and writes reject it,
|
|
185
|
+
and the engine persists through private adapter hooks. Versioned cache keys
|
|
186
|
+
exclude checkpoints produced while the old namespace was publicly writable.
|
|
187
|
+
`tools/reducer-value-shapes.ts` regenerates those structural validators from the
|
|
188
|
+
declared accumulator types.
|
|
189
|
+
|
|
190
|
+
Memory, SQLite and Postgres use the same algorithm: capture the authoritative
|
|
191
|
+
high-water, load persisted state on every call, fold the indexed contiguous delta
|
|
192
|
+
through that boundary, persist, and return an independent JSON value. There is
|
|
193
|
+
no process-local accumulator cache. A foreign checkpoint newer than the initial
|
|
194
|
+
boundary causes a fresh high-water check. Normal writes are monotone; corrupt,
|
|
195
|
+
wrong-version or ahead-of-log rows are repaired only by comparing the exact row
|
|
196
|
+
observed, atomically with the log boundary. A losing writer still returns its
|
|
197
|
+
correct prefix. Missing or unreadable cache state replays; log failures propagate.
|
|
198
|
+
|
|
199
|
+
The remote adapter makes one value request; only the finished answer crosses the
|
|
200
|
+
wire. The service uses named effective-configuration references for hosted ticket
|
|
201
|
+
operations (including policy publication) and semantic build/harvest lifecycle
|
|
202
|
+
facts for delegated authentication and issuance. Lifecycle snapshots keep session,
|
|
203
|
+
execution and terminal evidence, including historical endings, without dispatcher
|
|
204
|
+
tick tails. Anchor membership follows the contiguous log prefix. The legacy
|
|
205
|
+
state-events route remains a deprecated compatibility endpoint. Public generic
|
|
206
|
+
snapshot routes remain compatible, but current consumer reads do not restore the
|
|
207
|
+
former dispatcher work bundles.
|
|
208
|
+
|
|
209
|
+
**Dispatcher repository view.** `RepoViewStore`
|
|
210
|
+
(`packages/core/src/processes/repo-view.ts`) coordinates repository metadata and
|
|
211
|
+
source promise chains. Its build and journal facts come from persisted reduced
|
|
212
|
+
values. Reads after an append or lost conditional append consult that boundary
|
|
213
|
+
again, so no process-local memo suppresses a required delta. The adapter owns
|
|
214
|
+
cache persistence and recovery; the view neither writes work bundles nor hydrates
|
|
215
|
+
whole logs for decisions. Discovery uses persisted build digests, including for
|
|
216
|
+
settled builds. Raw methods forward actual cursor event output.
|
|
217
|
+
|
|
218
|
+
JSON-safe companion projections preserve collection and optional-value contracts.
|
|
219
|
+
The build facts projection returns one prefix for engine routing; its log-index
|
|
220
|
+
rounds and finalize steps reconstruct Maps and Sets explicitly, while publication
|
|
221
|
+
selectors reconstruct from a JSON ledger. Repository facts retain configuration,
|
|
222
|
+
control, Harvest, and dispatcher run correlations. Lease metadata and clocks stay
|
|
223
|
+
outside event reducers: liveness combines durable run facts with the current
|
|
224
|
+
repository record and observation time, including an older lease holder hidden
|
|
225
|
+
by a newer run. The shared repository value keeps a bounded recent-run window, including the
|
|
226
|
+
latest started and stopped runs. Fresh invocations use those shared facts for
|
|
227
|
+
first-tick publication, liveness and supervision without creating a cold
|
|
228
|
+
parameterized cache. Older holder correlations can use `dispatcherRunFacts` for
|
|
229
|
+
the requested historical run. A never-started fresh invocation does not request
|
|
230
|
+
historical correlation. Stopped dispatcher history is not copied into every
|
|
231
|
+
current read.
|
|
232
|
+
|
|
233
|
+
**Operator and CLI reads.** Build listings and details, repository and Harvest
|
|
234
|
+
status, dispatcher decisions, operator API queries, MCP delegation and web
|
|
235
|
+
dashboard refreshes use the same typed persisted read helpers. Shared projection
|
|
236
|
+
functions render those facts; whole-array overloads remain reference oracles.
|
|
237
|
+
Operator passes are single-flight per repository, with requests arriving during a
|
|
238
|
+
pass queued for one trailing pass. Fresh readers use persisted cache rows too;
|
|
239
|
+
steady-state event-row cost depends on newly appended events at fixed build/run
|
|
240
|
+
cardinality, not on log length. Reduced collections may legitimately grow in JSON
|
|
241
|
+
size. Cache loss or corruption can replay history once to recover the same answer.
|
|
242
|
+
|
|
243
|
+
Watch and wait register from a compound persisted `buildStreamSeed` at cursor C.
|
|
244
|
+
Its build accumulator, deferral ledger and insertion-ordered historical PR
|
|
245
|
+
eligibility describe that same prefix. Ordinary polling, including the first
|
|
246
|
+
batch on an old build, reads only events after the cursor and advances that seed
|
|
247
|
+
event by event, preserving state at event N in each emitted record. Watch uses
|
|
248
|
+
historical eligibility for baseline suppression; wait retains its separate
|
|
249
|
+
current-attention registration policy. Only an explicit `--since` older than C
|
|
250
|
+
reconstructs a historical prefix once in `cli/historical-build-state.ts`.
|
|
251
|
+
Actual event listings, transcripts and cursor streams remain raw reads. The
|
|
252
|
+
interactive terminal frontend retains its independently established incremental
|
|
253
|
+
feed. `tools/persisted-reduced-reads.test.ts` scans production TypeScript calls
|
|
254
|
+
and maintains precise, reason-bearing exceptions for these boundaries and pure
|
|
255
|
+
reference projections; stale exceptions fail the check.
|
|
242
256
|
|
|
243
257
|
**Session Store authority.** `packages/core/src/cli/binary.ts` validates a complete build or
|
|
244
258
|
Harvest ambient tuple before opening its phase Store through
|
|
@@ -387,7 +401,19 @@ SIGTERM restore terminal modes immediately and begin one supervised drain;
|
|
|
387
401
|
persistent frontend and child handlers keep repeated signals graceful until the
|
|
388
402
|
kernel child is reaped, after which a non-SIGINT terminating signal is replayed
|
|
389
403
|
in the frontend. A parent-liveness watch remains the detached-kernel fallback.
|
|
390
|
-
|
|
404
|
+
The repository record carries constant-size, lease-holder-fenced tick telemetry:
|
|
405
|
+
`beginRepoTick` opens the unsafe boundary, and `finishRepoTick` atomically closes
|
|
406
|
+
it and appends selected tick facts. Successful unchanged ticks append nothing;
|
|
407
|
+
queue transitions, counters, diagnostics, attention resets/retries, and failures
|
|
408
|
+
retain their journal evidence. Buffered starts preserve actual `startedAt` for
|
|
409
|
+
attention cooldowns; old starts use their envelope time. Liveness reads refresh
|
|
410
|
+
the repository record even with an unchanged event cursor, merging matching-run
|
|
411
|
+
completion samples with journal ticks. Heartbeat renewal cannot invent progress.
|
|
412
|
+
The memory, SQLite, Postgres and remote adapters share this contract, including
|
|
413
|
+
no mutation after lease expiry and no partial publication on validation failure.
|
|
414
|
+
|
|
415
|
+
Timeout escalation is held while the matching repository tick observation is open
|
|
416
|
+
(with historical `tick-started` journal fallback when no observation exists),
|
|
391
417
|
because force-killing the ticket claim before build creation would not be
|
|
392
418
|
recoverable. After a build child claims a takeover lease, it reduces one
|
|
393
419
|
captured event prefix and appends transcriptless reclaimed `session.ended`
|
package/docs/hosted-service.md
CHANGED
|
@@ -17,8 +17,8 @@ package's `deployment/` directory and in `templates/hosted-service/`).
|
|
|
17
17
|
"build": "ab-hosted-service-build"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@defrex/autobuild-hosted-store-service": "0.
|
|
21
|
-
"@defrex/autobuild": "0.
|
|
20
|
+
"@defrex/autobuild-hosted-store-service": "0.14.0",
|
|
21
|
+
"@defrex/autobuild": "0.14.0",
|
|
22
22
|
"next": "16.1.6",
|
|
23
23
|
"react": "19.2.4",
|
|
24
24
|
"react-dom": "19.2.4"
|
package/docs/operator-api.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Operator API v1
|
|
2
2
|
|
|
3
3
|
The hosted service exposes the polling-oriented operator surface at
|
|
4
|
-
`/operator/v1/repos/{repo}`. It is versioned with remote store protocol **
|
|
4
|
+
`/operator/v1/repos/{repo}`. It is versioned with remote store protocol **4**.
|
|
5
5
|
Every request must send `X-Autobuild-Version`,
|
|
6
6
|
`X-Autobuild-Protocol-Version: 3`, and `Authorization: Bearer …`. Identity is
|
|
7
7
|
checked before authentication or resource lookup.
|
|
@@ -37,7 +37,7 @@ headers, or available backend.
|
|
|
37
37
|
`[store] url` (in that precedence order, before the local default); the shipped client appends the routes below.
|
|
38
38
|
- Every `/builds`, `/repos`, and `/sessions` request sends
|
|
39
39
|
`X-Autobuild-Version` with the exact client package version and
|
|
40
|
-
`X-Autobuild-Protocol-Version` with the remote protocol version (`
|
|
40
|
+
`X-Autobuild-Protocol-Version` with the remote protocol version (`4` in this
|
|
41
41
|
distribution). Before authentication,
|
|
42
42
|
body parsing, or resource lookup, the server validates the release-line window and known protocol identity.
|
|
43
43
|
Unsupported or missing values receive `409 {"kind":"conflict","error":"…"}`;
|
|
@@ -355,6 +355,74 @@ part of this protocol. Retention is enforced server-side inside the store at
|
|
|
355
355
|
deposit time (bounded pruning of dispatcher run/config artifact revisions),
|
|
356
356
|
not through any endpoint.
|
|
357
357
|
|
|
358
|
+
### Named reducer values
|
|
359
|
+
|
|
360
|
+
`POST /builds/{slug}/reducer-values` and
|
|
361
|
+
`POST /repos/{repo}/reducer-values` accept at most 4 KiB of JSON:
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
{"reducer":"dispatchStatus","args":{"run":"run-a"}}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
The response is `200 {"version":2,"cursor":123,"value":{...}}`. Only the
|
|
368
|
+
finished JSON answer is returned, with no accumulator or raw-log bundle. `cursor`
|
|
369
|
+
is the authoritative prefix included in the answer, not a guarantee to include
|
|
370
|
+
appends concurrent with the request. Every call checks durable progress and
|
|
371
|
+
persisted state; parallel instances may safely advance the same checkpoint.
|
|
372
|
+
Snapshots are disposable caches: deletion, corruption, wrong versions and
|
|
373
|
+
unreadable cache storage cause replay. Log read failures remain errors.
|
|
374
|
+
|
|
375
|
+
The executable catalog is `kernel/reducer-registry.ts`. All requests reject
|
|
376
|
+
unknown names, excluded reducers, wrong scopes and invalid/extra arguments with
|
|
377
|
+
`400 validation`. A missing resource is `404`, project and delegated authority
|
|
378
|
+
checks are the same as other reads, and unsupported methods remain unavailable.
|
|
379
|
+
The operation is read-only authority even though its HTTP verb is POST and its
|
|
380
|
+
internal cache may advance. It does not accept caller-supplied state. This is an
|
|
381
|
+
additive protocol route: historical snapshot and event handlers and the supported
|
|
382
|
+
client version window are unchanged.
|
|
383
|
+
|
|
384
|
+
| Scope | Reducers with no arguments |
|
|
385
|
+
| --- | --- |
|
|
386
|
+
| Build | `executionNoProgress`, `summaryRequests`, `build`, `buildDigest`, `currentPrAttachments`, `pendingPrAttachmentReclaims`, `openExecution`, `lastExecutionOutcome`, `infrastructureFailureReset`, `openWorkspace`, `setupStreak`, `openBuildWorkspace`, `currentDeferralObservation`, `dashboardFacts`, `pinnedAssets`, `buildDelegation` |
|
|
387
|
+
| Repository | `harvest`, `dispatchSettings`, `sandboxStates`, `openHarvestExecutions`, `autoMergeDefault`, `effectiveConfig`, `harvestDelegation` |
|
|
388
|
+
|
|
389
|
+
| Reducer | Scope | Required arguments |
|
|
390
|
+
| --- | --- | --- |
|
|
391
|
+
| `dispatchStatus`, `openTick` | Repository | `run`: nonempty string |
|
|
392
|
+
| `dispatchFailureBudget` | Build | `infrastructureOwnsWorkspace`: boolean |
|
|
393
|
+
| `phaseFailures` | Build | `phase`: valid phase name; `round`: nonnegative integer |
|
|
394
|
+
|
|
395
|
+
Arguments are canonicalized into the cache identity; variants never reuse one
|
|
396
|
+
another's accumulator. `effectiveConfig` returns an object with optional `run`
|
|
397
|
+
and `reference: {kind, rev}`. Absence is distinct from an unreadable referenced
|
|
398
|
+
artifact. Delegation values contain semantic lifecycle facts indexed by prefixed
|
|
399
|
+
identifiers, not authorization decisions; request time and grace expiry are
|
|
400
|
+
always evaluated against those facts on the service.
|
|
401
|
+
|
|
402
|
+
Each remaining registered reducer is explicitly excluded:
|
|
403
|
+
|
|
404
|
+
| Reducers | Reason |
|
|
405
|
+
| --- | --- |
|
|
406
|
+
| `journalView`, `repositoryStateEvents`, `dispatchFrontend` | Raw repository event lists |
|
|
407
|
+
| `session` | Operator-session log scope |
|
|
408
|
+
| `publicationState` | Selector functions in finished value |
|
|
409
|
+
| `logIndex` | Maps and Sets in finished value |
|
|
410
|
+
| `hostedPrAttachments` | Map as finished value |
|
|
411
|
+
| `summaryBoundary`, `ticketSourceHealth`, `frozenPrImageHost`, `publishedBranchHead`, `verifyDiffBase`, `recordedBaseSha`, `baseBranch`, `provisionMarker`, `recoveryCheckpoint` | Existing finished contract can return top-level `undefined`, which is not JSON |
|
|
412
|
+
|
|
413
|
+
`GET /repos/{repo}/state-events` is **deprecated**. Its response remains unchanged
|
|
414
|
+
for supported clients, including the current dispatcher run's event tail. It is
|
|
415
|
+
the compatibility exception to flat event-row reads; new callers should request
|
|
416
|
+
named values. The current product no longer loads or writes raw-tail snapshots.
|
|
417
|
+
|
|
418
|
+
The `value.*` snapshot namespace is reserved for the adapter's private value
|
|
419
|
+
checkpoint hooks. Public snapshot GET and PUT requests for this prefix return
|
|
420
|
+
`400 validation`, including requests made with delegated or administrator
|
|
421
|
+
credentials. Adapter snapshot APIs reject the same names. Ordinary snapshot
|
|
422
|
+
names retain their existing behavior. The value cache key format is versioned;
|
|
423
|
+
checkpoints from the former publicly writable format are ignored and rebuilt
|
|
424
|
+
from the log after upgrading.
|
|
425
|
+
|
|
358
426
|
### Reducer snapshots
|
|
359
427
|
|
|
360
428
|
A reducer snapshot is one registered reducer's serialized state as of a log
|
|
@@ -1136,7 +1204,7 @@ Four shipped behaviors do not add `BuildStore` routes:
|
|
|
1136
1204
|
store lifecycle; there is no close endpoint.
|
|
1137
1205
|
- `GET /health` is outside the `BuildStore` interface. It is always open and
|
|
1138
1206
|
returns exactly `200 {"ok":true,"autobuildVersion":"<package>",
|
|
1139
|
-
"protocolVersion":"
|
|
1207
|
+
"protocolVersion":"4", "supportedClientRange":{"minVersion":"0.9.0",
|
|
1140
1208
|
"maxVersionExclusive":"0.12.0"}}`. It reports HTTP-process availability and identity,
|
|
1141
1209
|
not a deeper backing-store transaction or migration check.
|
|
1142
1210
|
|
|
@@ -1213,3 +1281,17 @@ require a project credential and operate only on its own identity; admin and
|
|
|
1213
1281
|
internal operator credentials cannot impersonate self-service. All protocol
|
|
1214
1282
|
responses are no-store/no-referrer. Connection commands are unavailable in
|
|
1215
1283
|
build/harvest sessions.
|
|
1284
|
+
|
|
1285
|
+
### Repository tick telemetry (protocol 4)
|
|
1286
|
+
|
|
1287
|
+
`POST /repos/:repo/tick/begin` accepts `{holder}` and returns the actual start
|
|
1288
|
+
timestamp, or null when the unexpired repository lease does not match.
|
|
1289
|
+
`POST /repos/:repo/tick/finish` accepts `{holder, completion, events}`. Completion
|
|
1290
|
+
is `{outcome:"completed", counters}` or `{outcome:"failed", error}`; events is
|
|
1291
|
+
empty for a quiet success or a matching dispatcher start/terminal pair for that
|
|
1292
|
+
holder. The validated batch and finished telemetry commit atomically. The reply
|
|
1293
|
+
is `{observation, events}` with assigned envelopes, or null on fencing failure.
|
|
1294
|
+
These operations never renew the lease. Repository records optionally expose
|
|
1295
|
+
`tick: {run, open?: {startedAt}, finished?: {run, startedAt, completedAt, ...completion}}`.
|
|
1296
|
+
They retain the previous completion while a new tick is open and after lease
|
|
1297
|
+
release. Build and session credentials cannot mutate repository ticks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defrex/autobuild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Agent-driven software delivery from groomed ticket to merged PR",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
"docs"
|
|
115
115
|
],
|
|
116
116
|
"scripts": {
|
|
117
|
+
"ab": "bun bin/ab.ts",
|
|
117
118
|
"dev:cli": "bun --hot --no-clear-screen bin/ab-dev.ts",
|
|
118
119
|
"capture:dashboard": "bun tools/dashboard-capture.ts",
|
|
119
120
|
"capture:web-dashboard": "bun packages/web-dashboard-capture/src/web-dashboard-capture.tsx",
|
|
@@ -143,8 +144,8 @@
|
|
|
143
144
|
},
|
|
144
145
|
"devDependencies": {
|
|
145
146
|
"@biomejs/biome": "2.5.5",
|
|
146
|
-
"@defrex/autobuild-postgres-store": "0.
|
|
147
|
-
"@defrex/autobuild-vercel-sandbox": "0.
|
|
147
|
+
"@defrex/autobuild-postgres-store": "0.14.0",
|
|
148
|
+
"@defrex/autobuild-vercel-sandbox": "0.14.0",
|
|
148
149
|
"@resvg/resvg-js": "2.6.2",
|
|
149
150
|
"@types/bun": "^1.3.14",
|
|
150
151
|
"@types/node": "^24.0.0",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AbEvent } from '../events/catalog'
|
|
2
2
|
import type { RepositoryEvent } from '../events/repository'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
currentDeferralObservationReducer,
|
|
5
|
+
mergeAttentionFromFacts,
|
|
6
|
+
type DeferralLedger,
|
|
7
|
+
} from '../kernel/auto-merge'
|
|
4
8
|
import { reduceBuild, type BuildState } from '../kernel/reducer'
|
|
5
9
|
|
|
6
10
|
export interface MergeDecision {
|
|
@@ -17,22 +21,18 @@ export function mergeDecision(
|
|
|
17
21
|
now: Date,
|
|
18
22
|
state = reduceBuild(events),
|
|
19
23
|
): MergeDecision | null {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
pr: state.pr,
|
|
33
|
-
reason: deferral ?? 'Auto-merge is not armed',
|
|
34
|
-
observedAt: now.toISOString(),
|
|
35
|
-
}
|
|
24
|
+
return mergeDecisionFromFacts(state, currentDeferralObservationReducer.reduce(events), now)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function mergeDecisionFromFacts(
|
|
28
|
+
state: BuildState,
|
|
29
|
+
ledger: DeferralLedger,
|
|
30
|
+
now: Date,
|
|
31
|
+
): MergeDecision | null {
|
|
32
|
+
const decision = mergeAttentionFromFacts(state, ledger)
|
|
33
|
+
return decision
|
|
34
|
+
? { type: 'build.merge-decision', ...decision, observedAt: now.toISOString() }
|
|
35
|
+
: null
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function buildNeedsAttention(events: AbEvent[], state: BuildState): boolean {
|
|
@@ -60,150 +60,4 @@ export function repositoryNeedsAttention(event: RepositoryEvent): boolean {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
type Failed = Extract<RepositoryEvent, { type: 'dispatcher.tick-failed' }>
|
|
65
|
-
export type PersistenceObservation =
|
|
66
|
-
| {
|
|
67
|
-
type: 'dispatcher.ticket-source-persistent'
|
|
68
|
-
source: string
|
|
69
|
-
cause: string
|
|
70
|
-
message: string
|
|
71
|
-
resetAt?: string
|
|
72
|
-
episodeSeq: number
|
|
73
|
-
observedAt: string
|
|
74
|
-
dependencyLimited: boolean
|
|
75
|
-
}
|
|
76
|
-
| {
|
|
77
|
-
type: 'dispatcher.tick-failure-persistent'
|
|
78
|
-
run: string
|
|
79
|
-
firstFailure: string
|
|
80
|
-
error: string
|
|
81
|
-
episodeSeq: number
|
|
82
|
-
observedAt: string
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Incremental journal projection. Episode starts survive run anchors and
|
|
86
|
-
* repeated evidence can strengthen, but never postpone, an existing deadline. */
|
|
87
|
-
export class RepositoryAttention {
|
|
88
|
-
private sources = new Map<
|
|
89
|
-
string,
|
|
90
|
-
{
|
|
91
|
-
event: Degraded
|
|
92
|
-
deadline: number
|
|
93
|
-
dependencyLimited: boolean
|
|
94
|
-
requiresRetry: boolean
|
|
95
|
-
retryObserved: boolean
|
|
96
|
-
}
|
|
97
|
-
>()
|
|
98
|
-
private failure: Failed | undefined
|
|
99
|
-
private tickStarts = new Map<string, number>()
|
|
100
|
-
|
|
101
|
-
private sourceDefersTick(now: number): boolean {
|
|
102
|
-
return [...this.sources.values()].some(
|
|
103
|
-
(source) => now < source.deadline || (source.requiresRetry && !source.retryObserved),
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Crossing resetAt alone does not prove persistence: the dispatcher must
|
|
108
|
-
* finish a tick that could actually retry after the provider cooldown. */
|
|
109
|
-
private observeRetry(run: string): void {
|
|
110
|
-
const startedAt = this.tickStarts.get(run)
|
|
111
|
-
this.tickStarts.delete(run)
|
|
112
|
-
if (startedAt === undefined) return
|
|
113
|
-
for (const source of this.sources.values()) {
|
|
114
|
-
if (startedAt >= source.deadline) source.retryObserved = true
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
fold(event: RepositoryEvent): void {
|
|
118
|
-
switch (event.type) {
|
|
119
|
-
case 'dispatcher.ticket-source-degraded': {
|
|
120
|
-
const existing = this.sources.get(event.payload.source)
|
|
121
|
-
const deadline =
|
|
122
|
-
'resetAt' in event.payload
|
|
123
|
-
? Date.parse(event.payload.resetAt)
|
|
124
|
-
: Date.parse(event.ts) + 15 * 60_000
|
|
125
|
-
if (existing) {
|
|
126
|
-
existing.deadline = Math.min(existing.deadline, deadline)
|
|
127
|
-
existing.requiresRetry ||= 'resetAt' in event.payload
|
|
128
|
-
} else
|
|
129
|
-
this.sources.set(event.payload.source, {
|
|
130
|
-
event,
|
|
131
|
-
deadline,
|
|
132
|
-
dependencyLimited: false,
|
|
133
|
-
requiresRetry: 'resetAt' in event.payload,
|
|
134
|
-
retryObserved: false,
|
|
135
|
-
})
|
|
136
|
-
// Cooldown-driven failures are not an independent dispatcher outage.
|
|
137
|
-
if (this.sourceDefersTick(Date.parse(event.ts))) this.failure = undefined
|
|
138
|
-
break
|
|
139
|
-
}
|
|
140
|
-
case 'dispatcher.ticket-source-dependency-limited': {
|
|
141
|
-
const source = this.sources.get(event.payload.source)
|
|
142
|
-
if (source) source.dependencyLimited = true
|
|
143
|
-
break
|
|
144
|
-
}
|
|
145
|
-
case 'dispatcher.ticket-source-recovered':
|
|
146
|
-
this.sources.delete(event.payload.source)
|
|
147
|
-
break
|
|
148
|
-
case 'dispatcher.run-started':
|
|
149
|
-
this.failure = undefined
|
|
150
|
-
this.tickStarts.clear()
|
|
151
|
-
break
|
|
152
|
-
case 'dispatcher.run-stopped':
|
|
153
|
-
this.tickStarts.delete(event.payload.run)
|
|
154
|
-
if (this.failure?.payload.run === event.payload.run) this.failure = undefined
|
|
155
|
-
break
|
|
156
|
-
case 'dispatcher.tick-started':
|
|
157
|
-
this.tickStarts.set(event.payload.run, Date.parse(event.ts))
|
|
158
|
-
break
|
|
159
|
-
case 'dispatcher.tick-completed':
|
|
160
|
-
this.observeRetry(event.payload.run)
|
|
161
|
-
if (this.failure?.payload.run === event.payload.run) this.failure = undefined
|
|
162
|
-
break
|
|
163
|
-
case 'dispatcher.tick-failed':
|
|
164
|
-
this.observeRetry(event.payload.run)
|
|
165
|
-
if (this.sourceDefersTick(Date.parse(event.ts))) this.failure = undefined
|
|
166
|
-
else if (this.failure?.payload.run !== event.payload.run) this.failure = event
|
|
167
|
-
break
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
pending(now: Date): { identity: string; observation: PersistenceObservation }[] {
|
|
171
|
-
const results: { identity: string; observation: PersistenceObservation }[] = []
|
|
172
|
-
for (const {
|
|
173
|
-
event,
|
|
174
|
-
deadline,
|
|
175
|
-
dependencyLimited,
|
|
176
|
-
requiresRetry,
|
|
177
|
-
retryObserved,
|
|
178
|
-
} of this.sources.values()) {
|
|
179
|
-
if (now.getTime() < deadline || (requiresRetry && !retryObserved)) continue
|
|
180
|
-
results.push({
|
|
181
|
-
identity: `source:${event.seq}`,
|
|
182
|
-
observation: {
|
|
183
|
-
type: 'dispatcher.ticket-source-persistent',
|
|
184
|
-
...event.payload,
|
|
185
|
-
episodeSeq: event.seq,
|
|
186
|
-
observedAt: now.toISOString(),
|
|
187
|
-
dependencyLimited,
|
|
188
|
-
},
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
const event = this.failure
|
|
192
|
-
if (
|
|
193
|
-
event &&
|
|
194
|
-
!this.sourceDefersTick(now.getTime()) &&
|
|
195
|
-
now.getTime() >= Date.parse(event.ts) + 5 * 60_000
|
|
196
|
-
)
|
|
197
|
-
results.push({
|
|
198
|
-
identity: `tick:${event.seq}`,
|
|
199
|
-
observation: {
|
|
200
|
-
type: 'dispatcher.tick-failure-persistent',
|
|
201
|
-
...event.payload,
|
|
202
|
-
firstFailure: event.ts,
|
|
203
|
-
episodeSeq: event.seq,
|
|
204
|
-
observedAt: now.toISOString(),
|
|
205
|
-
},
|
|
206
|
-
})
|
|
207
|
-
return results
|
|
208
|
-
}
|
|
209
|
-
}
|
|
63
|
+
export { RepositoryAttention, type PersistenceObservation } from '../kernel/repository-attention'
|