@defrex/autobuild 0.8.0 → 0.10.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 +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* content-addressed.
|
|
12
12
|
*/
|
|
13
13
|
import { Database } from 'bun:sqlite'
|
|
14
|
-
import { and, asc, desc, eq, gt, inArray, sql } from 'drizzle-orm'
|
|
14
|
+
import { and, asc, desc, eq, gt, gte, inArray, or, sql } from 'drizzle-orm'
|
|
15
15
|
import { drizzle, type BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite'
|
|
16
16
|
import { mkdirSync } from 'node:fs'
|
|
17
17
|
import { access, copyFile, mkdtemp, rm } from 'node:fs/promises'
|
|
@@ -40,7 +40,14 @@ import {
|
|
|
40
40
|
type SessionEventWrite,
|
|
41
41
|
} from '../../events/sessions'
|
|
42
42
|
import { createBuildScopedStore } from '../build-scope'
|
|
43
|
+
import type { ReducerSnapshot, SnapshotScope } from '../snapshots'
|
|
44
|
+
import { validateCreatedEvent } from '../new-build'
|
|
43
45
|
import { createSessionScopedStore } from '../session-handle'
|
|
46
|
+
import { DIGEST_EVENT_TYPES, type DigestEventRow, reduceBuildDigest } from '../digest'
|
|
47
|
+
import {
|
|
48
|
+
REPOSITORY_STATE_EVENT_TYPES,
|
|
49
|
+
readRepoStateEventsWithAnchorRecheck,
|
|
50
|
+
} from '../repo-state-events'
|
|
44
51
|
import {
|
|
45
52
|
DEFAULT_ARTIFACT_RETENTION_MAX_REVISIONS,
|
|
46
53
|
isRetentionManagedKind,
|
|
@@ -49,7 +56,7 @@ import {
|
|
|
49
56
|
import { pollingSubscribe } from '../subscribe'
|
|
50
57
|
import { StreamLocks } from '../streams/lock'
|
|
51
58
|
import { assembleUIMessageDocument } from '../streams/assemble'
|
|
52
|
-
import { readEventsWithWait, readStreamWithWait } from '../streams/wait'
|
|
59
|
+
import { EVENT_WAIT_POLL_MS, readEventsWithWait, readStreamWithWait } from '../streams/wait'
|
|
53
60
|
import {
|
|
54
61
|
serializedBatchSize,
|
|
55
62
|
STREAM_BATCH_MAX_BYTES,
|
|
@@ -76,6 +83,7 @@ import {
|
|
|
76
83
|
type ArtifactMeta,
|
|
77
84
|
type BlobStore,
|
|
78
85
|
type BuildRecord,
|
|
86
|
+
type BuildDigest,
|
|
79
87
|
type BuildScopedStore,
|
|
80
88
|
type BuildStore,
|
|
81
89
|
type Clock,
|
|
@@ -91,6 +99,19 @@ import {
|
|
|
91
99
|
type SubscribeOptions,
|
|
92
100
|
type Unsubscribe,
|
|
93
101
|
} from '../types'
|
|
102
|
+
import {
|
|
103
|
+
DEFAULT_TICKET_ASSET_LIMITS,
|
|
104
|
+
loadTicketAsset,
|
|
105
|
+
storeTicketAssetBlobs,
|
|
106
|
+
resolvePinnedAsset,
|
|
107
|
+
summarizeTicketAsset,
|
|
108
|
+
validateTicketAssetInput,
|
|
109
|
+
type TicketAsset,
|
|
110
|
+
type TicketAssetInput,
|
|
111
|
+
type TicketAssetLimits,
|
|
112
|
+
type TicketAssetMeta,
|
|
113
|
+
type TicketAssetSummary,
|
|
114
|
+
} from '../ticket-assets'
|
|
94
115
|
import { DirBlobStore } from './blobs'
|
|
95
116
|
import {
|
|
96
117
|
artifacts,
|
|
@@ -102,6 +123,7 @@ import {
|
|
|
102
123
|
sessionArtifacts,
|
|
103
124
|
sessionEvents,
|
|
104
125
|
sessions,
|
|
126
|
+
ticketAssets,
|
|
105
127
|
streamChunks,
|
|
106
128
|
streams,
|
|
107
129
|
} from './schema'
|
|
@@ -111,7 +133,21 @@ import {
|
|
|
111
133
|
* the drizzle schema is the source of truth; this is its inlined form so
|
|
112
134
|
* opening a store never needs a migration step.
|
|
113
135
|
*/
|
|
114
|
-
|
|
136
|
+
// Exported for the schema-parity test, which pins this DDL against the
|
|
137
|
+
// drizzle schema; schema.ts stays the source of truth, this is its inlined form.
|
|
138
|
+
export const BOOTSTRAP_DDL = [
|
|
139
|
+
// Reducer snapshots: additive and idempotent, so databases that predate the
|
|
140
|
+
// table gain it on open.
|
|
141
|
+
`CREATE TABLE IF NOT EXISTS reducer_snapshots (
|
|
142
|
+
scope_kind TEXT NOT NULL,
|
|
143
|
+
scope_key TEXT NOT NULL,
|
|
144
|
+
reducer TEXT NOT NULL,
|
|
145
|
+
version INTEGER NOT NULL,
|
|
146
|
+
cursor INTEGER NOT NULL,
|
|
147
|
+
state TEXT NOT NULL,
|
|
148
|
+
updated_at TEXT NOT NULL,
|
|
149
|
+
PRIMARY KEY (scope_kind, scope_key, reducer)
|
|
150
|
+
)`,
|
|
115
151
|
`CREATE TABLE IF NOT EXISTS builds (
|
|
116
152
|
slug TEXT PRIMARY KEY,
|
|
117
153
|
repo TEXT NOT NULL,
|
|
@@ -134,6 +170,10 @@ const BOOTSTRAP_DDL = [
|
|
|
134
170
|
payload TEXT NOT NULL,
|
|
135
171
|
PRIMARY KEY (build, seq)
|
|
136
172
|
)`,
|
|
173
|
+
// The build-digest scan (AUT-487): type-leading so the digest query's cost
|
|
174
|
+
// grows with the observation/terminal events themselves, not with total
|
|
175
|
+
// history. Idempotent at open, so pre-existing databases gain it too.
|
|
176
|
+
`CREATE INDEX IF NOT EXISTS events_type_build_seq ON events (type, build, seq)`,
|
|
137
177
|
`CREATE TABLE IF NOT EXISTS artifacts (
|
|
138
178
|
build TEXT NOT NULL,
|
|
139
179
|
kind TEXT NOT NULL,
|
|
@@ -170,6 +210,19 @@ const BOOTSTRAP_DDL = [
|
|
|
170
210
|
created_at TEXT NOT NULL,
|
|
171
211
|
PRIMARY KEY (repo, kind, revision)
|
|
172
212
|
)`,
|
|
213
|
+
`CREATE TABLE IF NOT EXISTS ticket_assets (
|
|
214
|
+
repo TEXT NOT NULL,
|
|
215
|
+
ticket_id TEXT NOT NULL,
|
|
216
|
+
kind TEXT NOT NULL,
|
|
217
|
+
name TEXT NOT NULL,
|
|
218
|
+
revision INTEGER NOT NULL,
|
|
219
|
+
layout TEXT NOT NULL,
|
|
220
|
+
manifest TEXT NOT NULL,
|
|
221
|
+
size INTEGER NOT NULL,
|
|
222
|
+
removed INTEGER NOT NULL,
|
|
223
|
+
created_at TEXT NOT NULL,
|
|
224
|
+
PRIMARY KEY (repo, ticket_id, kind, name, revision)
|
|
225
|
+
)`,
|
|
173
226
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
174
227
|
id TEXT PRIMARY KEY,
|
|
175
228
|
repo TEXT NOT NULL,
|
|
@@ -248,6 +301,15 @@ export interface SqliteBuildStoreOptions {
|
|
|
248
301
|
retention?: { maxRevisions?: number }
|
|
249
302
|
}
|
|
250
303
|
|
|
304
|
+
function ticketAssetKey(repo: string, ticketId: string, kind: string, name: string) {
|
|
305
|
+
return and(
|
|
306
|
+
eq(ticketAssets.repo, repo),
|
|
307
|
+
eq(ticketAssets.ticketId, ticketId),
|
|
308
|
+
eq(ticketAssets.kind, kind),
|
|
309
|
+
eq(ticketAssets.name, name),
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
251
313
|
export class SqliteBuildStore implements BuildStore {
|
|
252
314
|
private readonly sqlite: Database
|
|
253
315
|
private readonly db: BunSQLiteDatabase
|
|
@@ -399,6 +461,7 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
399
461
|
}
|
|
400
462
|
|
|
401
463
|
async createBuild(input: NewBuildInput): Promise<BuildRecord> {
|
|
464
|
+
const created = validateCreatedEvent(input)
|
|
402
465
|
const ts = this.now()
|
|
403
466
|
return this.writeTx(() => {
|
|
404
467
|
if (this.buildRow(input.slug)) {
|
|
@@ -416,6 +479,7 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
416
479
|
updatedAt: ts,
|
|
417
480
|
})
|
|
418
481
|
.run()
|
|
482
|
+
if (created !== undefined) this.appendInTx(input.slug, created)
|
|
419
483
|
return this.toRecord(this.requireBuild(input.slug))
|
|
420
484
|
})
|
|
421
485
|
}
|
|
@@ -430,6 +494,59 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
430
494
|
return rows.map((row) => this.toRecord(row))
|
|
431
495
|
}
|
|
432
496
|
|
|
497
|
+
async getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
|
|
498
|
+
// From the builds side (AUT-487): the type filter lives in the ON clause
|
|
499
|
+
// so a build whose log holds none of the digest-relevant event
|
|
500
|
+
// types still yields its row — a join that starts from `events` would
|
|
501
|
+
// silently drop such builds and break the operation's completeness
|
|
502
|
+
// contract. Only the digest-relevant types are fetched, then one shared
|
|
503
|
+
// derivation
|
|
504
|
+
// per build, so the answer cannot drift from `reduceBuild`.
|
|
505
|
+
const rows = this.sqlite
|
|
506
|
+
.query(
|
|
507
|
+
`SELECT b.slug AS slug, e.seq AS seq, e.ts AS ts, e.type AS type,
|
|
508
|
+
CASE WHEN e.type = 'pr-attachment.reclaimed'
|
|
509
|
+
THEN json_extract(e.payload, '$.hostedSeq') END AS hostedSeq,
|
|
510
|
+
CASE WHEN e.type IN ('execution.started', 'execution.ended')
|
|
511
|
+
THEN json_extract(e.payload, '$.instance') END AS instance
|
|
512
|
+
FROM builds b
|
|
513
|
+
LEFT JOIN events e
|
|
514
|
+
ON e.build = b.slug AND e.type IN (${DIGEST_EVENT_TYPES.map(() => '?').join(', ')})
|
|
515
|
+
WHERE b.repo = ?
|
|
516
|
+
ORDER BY b.slug, e.seq`,
|
|
517
|
+
)
|
|
518
|
+
.all(...DIGEST_EVENT_TYPES, repo) as {
|
|
519
|
+
slug: string
|
|
520
|
+
seq: number | null
|
|
521
|
+
ts: string | null
|
|
522
|
+
type: string | null
|
|
523
|
+
hostedSeq: number | null
|
|
524
|
+
instance: string | null
|
|
525
|
+
}[]
|
|
526
|
+
const eventsByBuild = new Map<string, DigestEventRow[]>()
|
|
527
|
+
for (const row of rows) {
|
|
528
|
+
if (row.seq === null || row.ts === null || row.type === null) continue
|
|
529
|
+
const events = eventsByBuild.get(row.slug) ?? []
|
|
530
|
+
events.push({
|
|
531
|
+
type: row.type as AbEvent['type'],
|
|
532
|
+
seq: row.seq,
|
|
533
|
+
ts: row.ts,
|
|
534
|
+
...(row.hostedSeq !== null ? { hostedSeq: row.hostedSeq } : {}),
|
|
535
|
+
...(row.instance !== null ? { instance: row.instance } : {}),
|
|
536
|
+
})
|
|
537
|
+
eventsByBuild.set(row.slug, events)
|
|
538
|
+
}
|
|
539
|
+
const digests = new Map<string, BuildDigest>()
|
|
540
|
+
for (const row of rows) {
|
|
541
|
+
if (digests.has(row.slug)) continue
|
|
542
|
+
digests.set(row.slug, {
|
|
543
|
+
slug: row.slug,
|
|
544
|
+
...reduceBuildDigest(eventsByBuild.get(row.slug) ?? []),
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
return digests
|
|
548
|
+
}
|
|
549
|
+
|
|
433
550
|
/** Current build-stream tail inside an open transaction. */
|
|
434
551
|
private currentSeqInTx(slug: string): number {
|
|
435
552
|
this.requireBuild(slug)
|
|
@@ -664,6 +781,10 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
664
781
|
},
|
|
665
782
|
waitSeconds: opts?.waitSeconds,
|
|
666
783
|
signal: opts?.signal,
|
|
784
|
+
// Hosted budget: held build-event reads poll at most once per second
|
|
785
|
+
// (AUT-394) — not the 25 ms stream default, which is presentation
|
|
786
|
+
// content's cadence.
|
|
787
|
+
pollMs: EVENT_WAIT_POLL_MS,
|
|
667
788
|
})
|
|
668
789
|
}
|
|
669
790
|
|
|
@@ -935,9 +1056,163 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
935
1056
|
},
|
|
936
1057
|
waitSeconds: opts?.waitSeconds,
|
|
937
1058
|
signal: opts?.signal,
|
|
1059
|
+
// Hosted budget: held repository-event reads poll at most once per
|
|
1060
|
+
// second (AUT-394) — not the 25 ms stream default, which is
|
|
1061
|
+
// presentation content's cadence.
|
|
1062
|
+
pollMs: EVENT_WAIT_POLL_MS,
|
|
938
1063
|
})
|
|
939
1064
|
}
|
|
940
1065
|
|
|
1066
|
+
async getRepoHighWater(repo: string): Promise<number> {
|
|
1067
|
+
this.requireRepo(repo)
|
|
1068
|
+
const row = this.db
|
|
1069
|
+
.select({ seq: sql<number | null>`max(${repoEvents.seq})` })
|
|
1070
|
+
.from(repoEvents)
|
|
1071
|
+
.where(eq(repoEvents.repo, repo))
|
|
1072
|
+
.get()
|
|
1073
|
+
return row?.seq ?? 0
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** The scope's log tail, or `undefined` for a scope that does not exist. */
|
|
1077
|
+
private snapshotTail(scope: SnapshotScope): number | undefined {
|
|
1078
|
+
if (scope.kind === 'build') {
|
|
1079
|
+
const row = this.sqlite
|
|
1080
|
+
.query(
|
|
1081
|
+
'SELECT (SELECT COALESCE(MAX(seq), 0) FROM events WHERE build = ?1) AS tail FROM builds WHERE slug = ?1',
|
|
1082
|
+
)
|
|
1083
|
+
.get(scope.slug) as { tail: number } | null
|
|
1084
|
+
return row?.tail
|
|
1085
|
+
}
|
|
1086
|
+
const row = this.sqlite
|
|
1087
|
+
.query(
|
|
1088
|
+
'SELECT (SELECT COALESCE(MAX(seq), 0) FROM repo_events WHERE repo = ?1) AS tail FROM repo_streams WHERE repo = ?1',
|
|
1089
|
+
)
|
|
1090
|
+
.get(scope.repo) as { tail: number } | null
|
|
1091
|
+
return row?.tail
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
async getReducerSnapshot(
|
|
1095
|
+
scope: SnapshotScope,
|
|
1096
|
+
reducer: string,
|
|
1097
|
+
version: number,
|
|
1098
|
+
): Promise<ReducerSnapshot | null> {
|
|
1099
|
+
const tail = this.snapshotTail(scope)
|
|
1100
|
+
if (tail === undefined) return null
|
|
1101
|
+
const row = this.sqlite
|
|
1102
|
+
.query(
|
|
1103
|
+
'SELECT version, cursor, state FROM reducer_snapshots WHERE scope_kind = ?1 AND scope_key = ?2 AND reducer = ?3',
|
|
1104
|
+
)
|
|
1105
|
+
.get(scope.kind, scope.kind === 'build' ? scope.slug : scope.repo, reducer) as {
|
|
1106
|
+
version: number
|
|
1107
|
+
cursor: number
|
|
1108
|
+
state: string
|
|
1109
|
+
} | null
|
|
1110
|
+
if (row === null || row.version !== version || row.cursor > tail) return null
|
|
1111
|
+
try {
|
|
1112
|
+
return { version: row.version, cursor: row.cursor, state: JSON.parse(row.state) }
|
|
1113
|
+
} catch {
|
|
1114
|
+
return null
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async putReducerSnapshot(
|
|
1119
|
+
scope: SnapshotScope,
|
|
1120
|
+
reducer: string,
|
|
1121
|
+
snapshot: ReducerSnapshot,
|
|
1122
|
+
): Promise<boolean> {
|
|
1123
|
+
const tail = this.snapshotTail(scope)
|
|
1124
|
+
if (tail === undefined || snapshot.cursor > tail) return false
|
|
1125
|
+
// One statement: the stored cursor never decreases, whatever interleaves.
|
|
1126
|
+
const result = this.sqlite
|
|
1127
|
+
.query(
|
|
1128
|
+
`INSERT INTO reducer_snapshots (scope_kind, scope_key, reducer, version, cursor, state, updated_at)
|
|
1129
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
|
1130
|
+
ON CONFLICT (scope_kind, scope_key, reducer) DO UPDATE SET
|
|
1131
|
+
version = excluded.version, cursor = excluded.cursor,
|
|
1132
|
+
state = excluded.state, updated_at = excluded.updated_at
|
|
1133
|
+
WHERE excluded.cursor > reducer_snapshots.cursor
|
|
1134
|
+
OR (excluded.cursor = reducer_snapshots.cursor AND excluded.version > reducer_snapshots.version)`,
|
|
1135
|
+
)
|
|
1136
|
+
.run(
|
|
1137
|
+
scope.kind,
|
|
1138
|
+
scope.kind === 'build' ? scope.slug : scope.repo,
|
|
1139
|
+
reducer,
|
|
1140
|
+
snapshot.version,
|
|
1141
|
+
snapshot.cursor,
|
|
1142
|
+
JSON.stringify(snapshot.state ?? null),
|
|
1143
|
+
this.clock().toISOString(),
|
|
1144
|
+
)
|
|
1145
|
+
return result.changes > 0
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async getRepoStateEvents(repo: string): Promise<RepositoryEvent[]> {
|
|
1149
|
+
this.requireRepo(repo)
|
|
1150
|
+
// Two queries, mirroring the oracle's derivation: the latest
|
|
1151
|
+
// run-started anchor, then one select of durable types plus the tail
|
|
1152
|
+
// from that anchor. No new index: local journals are small and the
|
|
1153
|
+
// existing (repo, seq) PK index keeps the anchor scan acceptable.
|
|
1154
|
+
//
|
|
1155
|
+
// The two drizzle reads are synchronous, so there is no in-process await
|
|
1156
|
+
// point between them — but a concurrent writer from another process can
|
|
1157
|
+
// still land between the two statements in real time (AUT-551): a
|
|
1158
|
+
// first-ever `dispatcher.run-started` appended between them would be
|
|
1159
|
+
// invisible to the probe, absent from the durable-only selection, yet
|
|
1160
|
+
// present in the journal by the time this method returns. The shared
|
|
1161
|
+
// helper below closes that window with a one-shot anchor re-check on the
|
|
1162
|
+
// durable-only outcome: a durable-only result is returned only when the
|
|
1163
|
+
// journal was anchor-free as of the final probe. When the probe *does*
|
|
1164
|
+
// find an anchor, the anchored select's `gte(seq, anchor)` arm is
|
|
1165
|
+
// monotone-inclusive, so any later run-started is picked up by that same
|
|
1166
|
+
// select; an append after the final re-probe is an ordinary post-read
|
|
1167
|
+
// concurrent append, not this race.
|
|
1168
|
+
return readRepoStateEventsWithAnchorRecheck(
|
|
1169
|
+
async () => {
|
|
1170
|
+
// This probe is a LIMIT 1 select, not an aggregate: it returns zero or
|
|
1171
|
+
// one rows, so row absence is the deliberate empty-journal signal — no
|
|
1172
|
+
// MAX() NULL semantics involved (contrast the Postgres adapter's
|
|
1173
|
+
// MAX(seq) probe, which always returns one row and must test the NULL
|
|
1174
|
+
// explicitly). The `anchor === undefined` branch in the select below is
|
|
1175
|
+
// the explicit durable-only path; a null anchor must never reach the
|
|
1176
|
+
// gte(repoEvents.seq, anchor) arm (in either dialect the comparison is
|
|
1177
|
+
// never true, so the durable-only outcome would then survive only by
|
|
1178
|
+
// accident), and the no-anchor case must not degenerate into
|
|
1179
|
+
// gte(repoEvents.seq, 0), which would select the whole journal.
|
|
1180
|
+
const anchorRows = this.db
|
|
1181
|
+
.select({ seq: repoEvents.seq })
|
|
1182
|
+
.from(repoEvents)
|
|
1183
|
+
.where(and(eq(repoEvents.repo, repo), eq(repoEvents.type, 'dispatcher.run-started')))
|
|
1184
|
+
.orderBy(desc(repoEvents.seq))
|
|
1185
|
+
.limit(1)
|
|
1186
|
+
.all()
|
|
1187
|
+
return anchorRows[0]?.seq
|
|
1188
|
+
},
|
|
1189
|
+
async (anchor) => {
|
|
1190
|
+
const durable = inArray(repoEvents.type, [...REPOSITORY_STATE_EVENT_TYPES])
|
|
1191
|
+
const rows = this.db
|
|
1192
|
+
.select()
|
|
1193
|
+
.from(repoEvents)
|
|
1194
|
+
.where(
|
|
1195
|
+
anchor === undefined
|
|
1196
|
+
? and(eq(repoEvents.repo, repo), durable)
|
|
1197
|
+
: and(eq(repoEvents.repo, repo), or(gte(repoEvents.seq, anchor), durable)),
|
|
1198
|
+
)
|
|
1199
|
+
.orderBy(asc(repoEvents.seq))
|
|
1200
|
+
.all()
|
|
1201
|
+
return rows.map(
|
|
1202
|
+
(row) =>
|
|
1203
|
+
({
|
|
1204
|
+
repo: row.repo,
|
|
1205
|
+
seq: row.seq,
|
|
1206
|
+
ts: row.ts,
|
|
1207
|
+
actor: row.actor,
|
|
1208
|
+
type: row.type,
|
|
1209
|
+
payload: row.payload,
|
|
1210
|
+
}) as RepositoryEvent,
|
|
1211
|
+
)
|
|
1212
|
+
},
|
|
1213
|
+
)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
941
1216
|
async putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
|
|
942
1217
|
const prepared = await this.prepareArtifact(artifact)
|
|
943
1218
|
return this.writeTx(() => this.depositRepoInTx(repo, prepared))
|
|
@@ -994,6 +1269,144 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
994
1269
|
.map((row) => this.toRepoMeta(row))
|
|
995
1270
|
}
|
|
996
1271
|
|
|
1272
|
+
async ticketAssetLimits(_repo: string): Promise<TicketAssetLimits> {
|
|
1273
|
+
return { ...DEFAULT_TICKET_ASSET_LIMITS }
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private toTicketAssetMeta(row: typeof ticketAssets.$inferSelect): TicketAssetMeta {
|
|
1277
|
+
return {
|
|
1278
|
+
repo: row.repo,
|
|
1279
|
+
ticketId: row.ticketId,
|
|
1280
|
+
kind: row.kind,
|
|
1281
|
+
name: row.name,
|
|
1282
|
+
revision: row.revision,
|
|
1283
|
+
layout: row.layout,
|
|
1284
|
+
size: row.size,
|
|
1285
|
+
entries: row.manifest,
|
|
1286
|
+
createdAt: row.createdAt,
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async putTicketAsset(
|
|
1291
|
+
repo: string,
|
|
1292
|
+
ticketId: string,
|
|
1293
|
+
asset: TicketAssetInput,
|
|
1294
|
+
): Promise<TicketAssetMeta> {
|
|
1295
|
+
const size = validateTicketAssetInput(asset)
|
|
1296
|
+
const entries = await storeTicketAssetBlobs(this.blobs, asset.entries)
|
|
1297
|
+
return this.writeTx(() => {
|
|
1298
|
+
const latest = this.db
|
|
1299
|
+
.select({ revision: ticketAssets.revision })
|
|
1300
|
+
.from(ticketAssets)
|
|
1301
|
+
.where(ticketAssetKey(repo, ticketId, asset.kind, asset.name))
|
|
1302
|
+
.orderBy(desc(ticketAssets.revision))
|
|
1303
|
+
.limit(1)
|
|
1304
|
+
.get()
|
|
1305
|
+
const row: typeof ticketAssets.$inferInsert = {
|
|
1306
|
+
repo,
|
|
1307
|
+
ticketId,
|
|
1308
|
+
kind: asset.kind,
|
|
1309
|
+
name: asset.name,
|
|
1310
|
+
revision: (latest?.revision ?? -1) + 1,
|
|
1311
|
+
layout: asset.layout,
|
|
1312
|
+
manifest: entries,
|
|
1313
|
+
size,
|
|
1314
|
+
removed: 0,
|
|
1315
|
+
createdAt: this.clock().toISOString(),
|
|
1316
|
+
}
|
|
1317
|
+
this.db.insert(ticketAssets).values(row).run()
|
|
1318
|
+
return this.toTicketAssetMeta(row as typeof ticketAssets.$inferSelect)
|
|
1319
|
+
})
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
async getTicketAsset(
|
|
1323
|
+
repo: string,
|
|
1324
|
+
ticketId: string,
|
|
1325
|
+
kind: string,
|
|
1326
|
+
name: string,
|
|
1327
|
+
rev?: number,
|
|
1328
|
+
): Promise<TicketAsset | null> {
|
|
1329
|
+
const scoped = ticketAssetKey(repo, ticketId, kind, name)
|
|
1330
|
+
const row =
|
|
1331
|
+
rev === undefined
|
|
1332
|
+
? this.db
|
|
1333
|
+
.select()
|
|
1334
|
+
.from(ticketAssets)
|
|
1335
|
+
.where(scoped)
|
|
1336
|
+
.orderBy(desc(ticketAssets.revision))
|
|
1337
|
+
.limit(1)
|
|
1338
|
+
.get()
|
|
1339
|
+
: this.db
|
|
1340
|
+
.select()
|
|
1341
|
+
.from(ticketAssets)
|
|
1342
|
+
.where(and(scoped, eq(ticketAssets.revision, rev)))
|
|
1343
|
+
.get()
|
|
1344
|
+
if (!row || row.removed) return null
|
|
1345
|
+
return loadTicketAsset(this.blobs, this.toTicketAssetMeta(row))
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async getPinnedTicketAsset(
|
|
1349
|
+
slug: string,
|
|
1350
|
+
kind: string,
|
|
1351
|
+
name: string,
|
|
1352
|
+
rev?: number,
|
|
1353
|
+
): Promise<TicketAsset | null> {
|
|
1354
|
+
return resolvePinnedAsset(this, slug, kind, name, rev)
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
async listTicketAssets(
|
|
1358
|
+
repo: string,
|
|
1359
|
+
ticketId: string,
|
|
1360
|
+
opts?: { revisions?: boolean },
|
|
1361
|
+
): Promise<TicketAssetSummary[]> {
|
|
1362
|
+
const rows = this.db
|
|
1363
|
+
.select()
|
|
1364
|
+
.from(ticketAssets)
|
|
1365
|
+
.where(and(eq(ticketAssets.repo, repo), eq(ticketAssets.ticketId, ticketId)))
|
|
1366
|
+
.orderBy(asc(ticketAssets.kind), asc(ticketAssets.name), asc(ticketAssets.revision))
|
|
1367
|
+
.all()
|
|
1368
|
+
if (opts?.revisions) {
|
|
1369
|
+
return rows
|
|
1370
|
+
.filter((row) => !row.removed)
|
|
1371
|
+
.map((row) => summarizeTicketAsset(this.toTicketAssetMeta(row)))
|
|
1372
|
+
}
|
|
1373
|
+
const latest = new Map<string, typeof ticketAssets.$inferSelect>()
|
|
1374
|
+
for (const row of rows) latest.set(JSON.stringify([row.kind, row.name]), row)
|
|
1375
|
+
return [...latest.values()]
|
|
1376
|
+
.filter((row) => !row.removed)
|
|
1377
|
+
.map((row) => summarizeTicketAsset(this.toTicketAssetMeta(row)))
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
async removeTicketAsset(
|
|
1381
|
+
repo: string,
|
|
1382
|
+
ticketId: string,
|
|
1383
|
+
kind: string,
|
|
1384
|
+
name: string,
|
|
1385
|
+
): Promise<TicketAssetMeta | null> {
|
|
1386
|
+
return this.writeTx(() => {
|
|
1387
|
+
const latest = this.db
|
|
1388
|
+
.select()
|
|
1389
|
+
.from(ticketAssets)
|
|
1390
|
+
.where(ticketAssetKey(repo, ticketId, kind, name))
|
|
1391
|
+
.orderBy(desc(ticketAssets.revision))
|
|
1392
|
+
.limit(1)
|
|
1393
|
+
.get()
|
|
1394
|
+
if (!latest || latest.removed) return null
|
|
1395
|
+
this.db
|
|
1396
|
+
.insert(ticketAssets)
|
|
1397
|
+
.values({
|
|
1398
|
+
...latest,
|
|
1399
|
+
revision: latest.revision + 1,
|
|
1400
|
+
manifest: [],
|
|
1401
|
+
size: 0,
|
|
1402
|
+
removed: 1,
|
|
1403
|
+
createdAt: this.clock().toISOString(),
|
|
1404
|
+
})
|
|
1405
|
+
.run()
|
|
1406
|
+
return this.toTicketAssetMeta(latest)
|
|
1407
|
+
})
|
|
1408
|
+
}
|
|
1409
|
+
|
|
997
1410
|
async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
|
|
998
1411
|
return this.writeTx(() => {
|
|
999
1412
|
const row = this.requireRepo(repo)
|
|
@@ -1189,6 +1602,26 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1189
1602
|
return this.writeTx(() => this.appendSessionInTx(id, validated) as SessionEventEnvelope<T>)
|
|
1190
1603
|
}
|
|
1191
1604
|
|
|
1605
|
+
async appendSessionEventIfCurrent<T extends SessionEventType>(
|
|
1606
|
+
id: string,
|
|
1607
|
+
expectedSeq: number,
|
|
1608
|
+
event: SessionEventWrite<T>,
|
|
1609
|
+
): Promise<SessionEventEnvelope<T> | null> {
|
|
1610
|
+
validateExpectedSeq(expectedSeq)
|
|
1611
|
+
const validated = validateSessionEventWrite(event)
|
|
1612
|
+
return this.writeTx(() => {
|
|
1613
|
+
// BEGIN IMMEDIATE serializes this comparison and append across every
|
|
1614
|
+
// SQLite connection/process using the same store file.
|
|
1615
|
+
const row = this.db
|
|
1616
|
+
.select({ count: sql<number>`count(*)` })
|
|
1617
|
+
.from(sessionEvents)
|
|
1618
|
+
.where(eq(sessionEvents.session, id))
|
|
1619
|
+
.get()
|
|
1620
|
+
if ((row?.count ?? 0) !== expectedSeq) return null
|
|
1621
|
+
return this.appendSessionInTx(id, validated) as SessionEventEnvelope<T>
|
|
1622
|
+
})
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1192
1625
|
async getSessionEvents(
|
|
1193
1626
|
id: string,
|
|
1194
1627
|
sinceSeq = 0,
|
|
@@ -1214,7 +1647,15 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1214
1647
|
}) as SessionEvent,
|
|
1215
1648
|
)
|
|
1216
1649
|
}
|
|
1217
|
-
return readEventsWithWait({
|
|
1650
|
+
return readEventsWithWait({
|
|
1651
|
+
read,
|
|
1652
|
+
waitSeconds: opts?.waitSeconds,
|
|
1653
|
+
signal: opts?.signal,
|
|
1654
|
+
// Hosted budget: held session-event reads poll at most once per second
|
|
1655
|
+
// (AUT-394), matching the build and repository event reads — not the
|
|
1656
|
+
// 25 ms stream default, which is presentation content's cadence.
|
|
1657
|
+
pollMs: EVENT_WAIT_POLL_MS,
|
|
1658
|
+
})
|
|
1218
1659
|
}
|
|
1219
1660
|
|
|
1220
1661
|
/** Runs inside an open transaction — see `depositInTx` for `prune`. */
|