@defrex/autobuild 0.8.0 → 0.9.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 +313 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +57 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +151 -25
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +169 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +10 -0
- package/package.json +43 -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/model.ts +57 -7
- package/packages/core/src/cli/dashboard/poll.ts +69 -10
- 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.ts +320 -46
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +47 -8
- 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 +248 -222
- 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/status.ts +149 -27
- 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 +307 -7
- package/packages/core/src/cli/wait.ts +147 -39
- package/packages/core/src/cli/watch.ts +128 -166
- package/packages/core/src/config/live.ts +22 -0
- package/packages/core/src/config/pipeline-source.ts +376 -0
- package/packages/core/src/config/schema.ts +230 -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 +2 -1
- package/packages/core/src/kernel/auto-merge-default.ts +123 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/reducer.ts +40 -1
- 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 +208 -19
- 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/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/types.ts +17 -2
- 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 +37 -0
- package/packages/core/src/processes/build-runner.ts +54 -3
- package/packages/core/src/processes/dispatcher.ts +247 -41
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/orchestrator-tick.ts +319 -0
- package/packages/core/src/processes/sandbox-state.ts +10 -0
- package/packages/core/src/store/adapter.ts +32 -0
- package/packages/core/src/store/build-scope.ts +64 -0
- package/packages/core/src/store/contract.ts +868 -0
- package/packages/core/src/store/digest.ts +49 -0
- package/packages/core/src/store/local/schema.ts +29 -2
- package/packages/core/src/store/local/store.ts +338 -4
- package/packages/core/src/store/memory.ts +179 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +71 -0
- package/packages/core/src/store/remote/client.ts +260 -1
- package/packages/core/src/store/remote/index.ts +12 -2
- package/packages/core/src/store/remote/protocol.ts +100 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +140 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +64 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
- 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 +425 -0
- package/packages/core/src/store/types.ts +122 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +126 -33
- 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 +169 -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/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
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// Client half + shared wire vocabulary for the remote BuildStore (SPEC §7.2).
|
|
2
|
+
// The HTTP server half ships in `@defrex/autobuild-hosted-store-service`;
|
|
3
|
+
// this barrel is what a local CLI and the hosted package both build against.
|
|
1
4
|
export { AuthError, RemoteBuildStore } from './client'
|
|
2
5
|
export type { RemoteBuildStoreOptions, RemoteStoreIdentity } from './client'
|
|
3
|
-
export { createStoreServer, startStoreServer } from './server'
|
|
4
|
-
export type { StartStoreServerOptions, StoreServer, StoreServerOptions } from './server'
|
|
5
6
|
export { mintToken, tokenResource, verifyToken } from './token'
|
|
6
7
|
export type { OperatorTokenScope, TokenScope } from './token'
|
|
7
8
|
export {
|
|
@@ -10,3 +11,12 @@ export {
|
|
|
10
11
|
REMOTE_STORE_PROTOCOL_VERSION,
|
|
11
12
|
REMOTE_STORE_PROTOCOL_VERSION_HEADER,
|
|
12
13
|
} from './version'
|
|
14
|
+
// The whole wire protocol: schemas, error vocabulary, and the placeholder-ref
|
|
15
|
+
// deposit convention. The protocol servers (hosted package) compose these;
|
|
16
|
+
// clients parse them for feedback.
|
|
17
|
+
export * from './protocol'
|
|
18
|
+
// Event vocabulary the wire envelopes and the server's D6 mapping rely on.
|
|
19
|
+
export { EventValidationError } from '../../events/catalog'
|
|
20
|
+
export type { Via } from '../../events/envelope'
|
|
21
|
+
export type { SessionEvent, SessionEventWrite } from '../../events/sessions'
|
|
22
|
+
export type { RepositoryEvent, RepositoryEventWrite } from '../../events/repository'
|
|
@@ -40,6 +40,18 @@ export const errorBodySchema = z.object({
|
|
|
40
40
|
})
|
|
41
41
|
export type ErrorBody = z.infer<typeof errorBodySchema>
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Deliberately loose: the ontology is enforced by `validateEventWrite` in
|
|
45
|
+
* the *backing store* (§8), so its precise EventValidationError message —
|
|
46
|
+
* the agent feedback D6 exists for — survives the wire instead of being
|
|
47
|
+
* shadowed by a duplicate wire-schema error.
|
|
48
|
+
*/
|
|
49
|
+
export const eventWriteWireSchema = z.object({
|
|
50
|
+
actor: z.unknown(),
|
|
51
|
+
type: z.string().min(1),
|
|
52
|
+
payload: z.unknown(),
|
|
53
|
+
})
|
|
54
|
+
|
|
43
55
|
// ── Builds ───────────────────────────────────────────────────────────────────
|
|
44
56
|
|
|
45
57
|
export const newBuildBodySchema = z.object({
|
|
@@ -51,6 +63,9 @@ export const newBuildBodySchema = z.object({
|
|
|
51
63
|
repoOrigin: z.string().optional(),
|
|
52
64
|
ticket: ticketRefSchema.optional(),
|
|
53
65
|
branch: z.string().min(1).optional(),
|
|
66
|
+
// Additive optional initial `build.created` event, appended atomically with
|
|
67
|
+
// the record. Loose like every event wire schema: the backing store validates.
|
|
68
|
+
created: eventWriteWireSchema.optional(),
|
|
54
69
|
})
|
|
55
70
|
|
|
56
71
|
export const buildRecordWireSchema = z.object({
|
|
@@ -68,18 +83,6 @@ export const buildRecordListSchema = z.array(buildRecordWireSchema)
|
|
|
68
83
|
|
|
69
84
|
// ── Events ───────────────────────────────────────────────────────────────────
|
|
70
85
|
|
|
71
|
-
/**
|
|
72
|
-
* Deliberately loose: the ontology is enforced by `validateEventWrite` in
|
|
73
|
-
* the *backing store* (§8), so its precise EventValidationError message —
|
|
74
|
-
* the agent feedback D6 exists for — survives the wire instead of being
|
|
75
|
-
* shadowed by a duplicate wire-schema error.
|
|
76
|
-
*/
|
|
77
|
-
export const eventWriteWireSchema = z.object({
|
|
78
|
-
actor: z.unknown(),
|
|
79
|
-
type: z.string().min(1),
|
|
80
|
-
payload: z.unknown(),
|
|
81
|
-
})
|
|
82
|
-
|
|
83
86
|
export const eventEnvelopeWireSchema = z.object({
|
|
84
87
|
build: z.string(),
|
|
85
88
|
seq: z.number().int().positive(),
|
|
@@ -138,6 +141,68 @@ export const repoDepositsResponseSchema = z.object({
|
|
|
138
141
|
artifacts: repositoryArtifactMetaListSchema,
|
|
139
142
|
})
|
|
140
143
|
|
|
144
|
+
// ── Ticket assets (SPEC §7.1) ────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export const ticketAssetLayoutSchema = z.enum(['file', 'tree'])
|
|
147
|
+
/** An entry on the put request: `{type:'file',path,contentBase64}` or `{type:'dir',path}`. */
|
|
148
|
+
export const ticketAssetEntryWireInputSchema = z.discriminatedUnion('type', [
|
|
149
|
+
z.object({ type: z.literal('file'), path: z.string(), contentBase64: z.string() }),
|
|
150
|
+
z.object({ type: z.literal('dir'), path: z.string() }),
|
|
151
|
+
])
|
|
152
|
+
export const putTicketAssetBodySchema = z.object({
|
|
153
|
+
kind: z.string(),
|
|
154
|
+
name: z.string(),
|
|
155
|
+
layout: ticketAssetLayoutSchema,
|
|
156
|
+
entries: z.array(ticketAssetEntryWireInputSchema),
|
|
157
|
+
})
|
|
158
|
+
/** A manifest entry: `dir` entries carry no size or blobRef. */
|
|
159
|
+
export const ticketAssetEntryWireSchema = z.discriminatedUnion('type', [
|
|
160
|
+
z.object({
|
|
161
|
+
type: z.literal('file'),
|
|
162
|
+
path: z.string(),
|
|
163
|
+
size: z.number().int().nonnegative(),
|
|
164
|
+
blobRef: z.string(),
|
|
165
|
+
}),
|
|
166
|
+
z.object({ type: z.literal('dir'), path: z.string() }),
|
|
167
|
+
])
|
|
168
|
+
export const ticketAssetMetaWireSchema = z.object({
|
|
169
|
+
repo: z.string(),
|
|
170
|
+
ticketId: z.string(),
|
|
171
|
+
kind: z.string(),
|
|
172
|
+
name: z.string(),
|
|
173
|
+
revision: z.number().int().nonnegative(),
|
|
174
|
+
layout: ticketAssetLayoutSchema,
|
|
175
|
+
size: z.number().int().nonnegative(),
|
|
176
|
+
entries: z.array(ticketAssetEntryWireSchema),
|
|
177
|
+
createdAt: z.string(),
|
|
178
|
+
})
|
|
179
|
+
export const ticketAssetSummaryWireSchema = z.object({
|
|
180
|
+
repo: z.string(),
|
|
181
|
+
ticketId: z.string(),
|
|
182
|
+
kind: z.string(),
|
|
183
|
+
name: z.string(),
|
|
184
|
+
revision: z.number().int().nonnegative(),
|
|
185
|
+
layout: ticketAssetLayoutSchema,
|
|
186
|
+
size: z.number().int().nonnegative(),
|
|
187
|
+
fileCount: z.number().int().nonnegative(),
|
|
188
|
+
dirCount: z.number().int().nonnegative(),
|
|
189
|
+
createdAt: z.string(),
|
|
190
|
+
})
|
|
191
|
+
export const ticketAssetSummaryListSchema = z.array(ticketAssetSummaryWireSchema)
|
|
192
|
+
/** `GET …/assets` answers the manifest only (no bytes); null when absent or removed. */
|
|
193
|
+
export const ticketAssetManifestResponseSchema = ticketAssetMetaWireSchema.nullable()
|
|
194
|
+
/** `GET …/assets/file` answers one file's bytes; null when absent. */
|
|
195
|
+
export const ticketAssetFileResponseSchema = z.object({ contentBase64: z.string() }).nullable()
|
|
196
|
+
export const removeTicketAssetBodySchema = z.object({ kind: z.string(), name: z.string() })
|
|
197
|
+
export const removeTicketAssetResponseSchema = ticketAssetMetaWireSchema.nullable()
|
|
198
|
+
export const ticketAssetLimitsWireSchema = z.object({
|
|
199
|
+
maxBytes: z.number().int().nonnegative(),
|
|
200
|
+
maxEntries: z.number().int().positive(),
|
|
201
|
+
/** Largest request or response body the deployment can carry. Absent: no ceiling. */
|
|
202
|
+
maxRequestBytes: z.number().int().positive().optional(),
|
|
203
|
+
})
|
|
204
|
+
export type TicketAssetLimitsWire = z.infer<typeof ticketAssetLimitsWireSchema>
|
|
205
|
+
|
|
141
206
|
// ── Operator sessions (SPEC §7.1.1) ─────────────────────────────────────────
|
|
142
207
|
|
|
143
208
|
export const newSessionBodySchema = z.object({
|
|
@@ -166,6 +231,12 @@ export const sessionEventEnvelopeWireSchema = z.object({
|
|
|
166
231
|
})
|
|
167
232
|
export const sessionEventListSchema = z.array(sessionEventEnvelopeWireSchema)
|
|
168
233
|
|
|
234
|
+
export const conditionalSessionEventBodySchema = z.object({
|
|
235
|
+
expectedSeq: z.number().int().nonnegative(),
|
|
236
|
+
event: eventWriteWireSchema,
|
|
237
|
+
})
|
|
238
|
+
export const conditionalSessionEventResponseSchema = sessionEventEnvelopeWireSchema.nullable()
|
|
239
|
+
|
|
169
240
|
export const sessionArtifactMetaWireSchema = z.object({
|
|
170
241
|
session: z.string(),
|
|
171
242
|
kind: z.string(),
|
|
@@ -187,6 +258,23 @@ export const sessionDepositsResponseSchema = z.object({
|
|
|
187
258
|
artifacts: sessionArtifactMetaListSchema,
|
|
188
259
|
})
|
|
189
260
|
|
|
261
|
+
// ── Build digests (repo-scoped batch read, AUT-487) ────────────────────────
|
|
262
|
+
// The wire form of `BuildDigest` (store/types.ts). `terminal` is omitted when
|
|
263
|
+
// the build's log carries no terminal fact, per the protocol's omission
|
|
264
|
+
// convention for optional members.
|
|
265
|
+
export const buildDigestWireSchema = z.object({
|
|
266
|
+
slug: z.string().min(1),
|
|
267
|
+
terminal: z.enum(['done', 'aborted']).optional(),
|
|
268
|
+
observations: z.array(
|
|
269
|
+
z.object({
|
|
270
|
+
seq: z.number().int().positive(),
|
|
271
|
+
ts: z.string().min(1),
|
|
272
|
+
}),
|
|
273
|
+
),
|
|
274
|
+
merged: z.string().min(1).optional(),
|
|
275
|
+
})
|
|
276
|
+
export const buildDigestListSchema = z.array(buildDigestWireSchema)
|
|
277
|
+
|
|
190
278
|
// ── Artifacts ────────────────────────────────────────────────────────────────
|
|
191
279
|
|
|
192
280
|
export const artifactMetaWireSchema = z.object({
|
|
@@ -3,8 +3,12 @@ import manifest from '../../../../../package.json'
|
|
|
3
3
|
/** Release-synchronized identity sent by every remote-store client. */
|
|
4
4
|
export const AUTOBUILD_VERSION = manifest.version
|
|
5
5
|
|
|
6
|
-
/** Wire compatibility version for the remote BuildStore protocol.
|
|
7
|
-
|
|
6
|
+
/** Wire compatibility version for the remote BuildStore protocol.
|
|
7
|
+
* Version 3 (AUT-521): the build-digest wire shape gains per-occurrence
|
|
8
|
+
* observation timestamps and the `merged` fact — a breaking shape change,
|
|
9
|
+
* so stale peers are rejected at handshake instead of failing mid-request
|
|
10
|
+
* at schema parse. */
|
|
11
|
+
export const REMOTE_STORE_PROTOCOL_VERSION = '3'
|
|
8
12
|
|
|
9
13
|
export const AUTOBUILD_VERSION_HEADER = 'x-autobuild-version'
|
|
10
14
|
export const REMOTE_STORE_PROTOCOL_VERSION_HEADER = 'x-autobuild-protocol-version'
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounded repository-journal read (AUT-489): a pure subset derivation
|
|
3
|
+
* that stateless full-state readers can fetch instead of the whole journal
|
|
4
|
+
* without changing what their reducers report.
|
|
5
|
+
*
|
|
6
|
+
* The hosted dispatcher appends ~4 journal events per invocation (run-started,
|
|
7
|
+
* tick-started, tick-completed/tick-yielded, run-stopped) whether or not it
|
|
8
|
+
* did anything, so the journal grows forever with wall-clock time while the
|
|
9
|
+
* durable signal stays small. Every repository-state consumer reduces the
|
|
10
|
+
* journal through one of four pure reducers whose event-type dependencies
|
|
11
|
+
* partition the catalog:
|
|
12
|
+
*
|
|
13
|
+
* - `reduceDispatchSettings` — only the three dispatcher setting types
|
|
14
|
+
* (latest-wins by seq).
|
|
15
|
+
* - `reduceDispatchStatus(events, latestRun)` — only run-correlated
|
|
16
|
+
* dispatcher facts, and it skips every event whose `payload.run` is not the
|
|
17
|
+
* latest run. A run's facts are appended under its own run id, which is
|
|
18
|
+
* minted at `dispatcher.run-started`, so all facts the reducer keeps live at
|
|
19
|
+
* or after the latest run-started.
|
|
20
|
+
* - `reduceHarvest` (and everything built on it) — only `harvest.*` events
|
|
21
|
+
* plus the three setting types, which it explicitly ignores.
|
|
22
|
+
* - `sandboxStates` — only `orchestrator.sandbox.*` events.
|
|
23
|
+
*
|
|
24
|
+
* So the bounded subset is: **every event of a durable (slow-growing) type —
|
|
25
|
+
* all `harvest.*`, all `orchestrator.sandbox.*`, and the three dispatcher
|
|
26
|
+
* setting types — plus, when the journal contains at least one
|
|
27
|
+
* `dispatcher.run-started`, the tail from that latest run-started to the end
|
|
28
|
+
* of the journal.** For a hosted repository with N no-op invocations this
|
|
29
|
+
* subset's size is a constant (one invocation's ~4 facts plus the durable
|
|
30
|
+
* signal), independent of N.
|
|
31
|
+
*
|
|
32
|
+
* INVARIANT FOR AUTHORS OF NEW REPOSITORY EVENT TYPES: a new type must either
|
|
33
|
+
* (a) join `REPOSITORY_STATE_EVENT_TYPES` — it is durable, slow-growing state
|
|
34
|
+
* some reducer depends on at full replay depth — or (b) join
|
|
35
|
+
* `REPOSITORY_RUN_SCOPED_EVENT_TYPES` — it is run-correlated and its only
|
|
36
|
+
* consumers reduce it for the latest run, so the tail from the latest
|
|
37
|
+
* `dispatcher.run-started` covers it. `tests/store/repo-state-events.test.ts`
|
|
38
|
+
* asserts the two sets exactly partition `REPOSITORY_EVENT_TYPES`; adding a
|
|
39
|
+
* type to neither fails the build until the author makes the call. A type in
|
|
40
|
+
* neither set would be silently dropped from bounded reads.
|
|
41
|
+
*
|
|
42
|
+
* Reducing this subset with the state readers yields exactly what a full
|
|
43
|
+
* replay yields: `reduceDispatchSettings`, `reduceHarvest`, `sandboxStates`
|
|
44
|
+
* see identical event lists, and `reduceDispatchStatus` sees every event it
|
|
45
|
+
* keeps (the rest are skipped by the `payload.run !== latestRun` guard). The
|
|
46
|
+
* journal itself stays append-only; no event is deleted, rewritten, or
|
|
47
|
+
* re-ordered, and a replay from the start is untouched.
|
|
48
|
+
*/
|
|
49
|
+
import type { RepositoryEvent, RepositoryEventType } from '../events/repository'
|
|
50
|
+
import {
|
|
51
|
+
dispatcherSettingEventPayloadSchemas,
|
|
52
|
+
dispatcherStatusEventPayloadSchemas,
|
|
53
|
+
harvestEventPayloadSchemas,
|
|
54
|
+
orchestratorSandboxEventPayloadSchemas,
|
|
55
|
+
} from '../events/repository'
|
|
56
|
+
|
|
57
|
+
/** The anchor fact a bounded read's tail hangs from. */
|
|
58
|
+
const RUN_STARTED = 'dispatcher.run-started'
|
|
59
|
+
|
|
60
|
+
/** Durable, slow-growing repository event types: every fact a state reducer
|
|
61
|
+
* may consult at full replay depth. Derived from the catalog groups so a new
|
|
62
|
+
* harvest or sandbox event type joins automatically; the three dispatcher
|
|
63
|
+
* setting types are enumerated because they share one schema group with the
|
|
64
|
+
* run-correlated `dispatcher.config-reloaded`. */
|
|
65
|
+
export const REPOSITORY_STATE_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
66
|
+
...Object.keys(harvestEventPayloadSchemas),
|
|
67
|
+
...Object.keys(orchestratorSandboxEventPayloadSchemas),
|
|
68
|
+
...Object.keys(dispatcherSettingEventPayloadSchemas).filter(
|
|
69
|
+
(type) => type !== 'dispatcher.config-reloaded',
|
|
70
|
+
),
|
|
71
|
+
] as RepositoryEventType[]
|
|
72
|
+
|
|
73
|
+
/** Run-correlated repository event types: facts whose only state consumers
|
|
74
|
+
* reduce them for the latest dispatcher run, so the tail from the latest
|
|
75
|
+
* `dispatcher.run-started` contains everything those consumers keep. */
|
|
76
|
+
export const REPOSITORY_RUN_SCOPED_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
77
|
+
...Object.keys(dispatcherStatusEventPayloadSchemas),
|
|
78
|
+
'dispatcher.config-reloaded',
|
|
79
|
+
] as RepositoryEventType[]
|
|
80
|
+
|
|
81
|
+
const STATE_TYPES = new Set<string>(REPOSITORY_STATE_EVENT_TYPES)
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The race-closed read shape every SQL adapter delegates `getRepoStateEvents`
|
|
85
|
+
* to (AUT-551). Both adapters issue the anchor probe and the selection as two
|
|
86
|
+
* separate statements, so a concurrent append of the journal's first
|
|
87
|
+
* `dispatcher.run-started` between them is invisible to the probe, absent
|
|
88
|
+
* from the durable-only selection, yet present in the journal by the time the
|
|
89
|
+
* method returns — a stateless reader can transiently throw
|
|
90
|
+
* `effective-config-unavailable` where a moment-later full replay would
|
|
91
|
+
* succeed.
|
|
92
|
+
*
|
|
93
|
+
* The fix is a **one-shot anchor re-check on the durable-only outcome**:
|
|
94
|
+
* after a selection that took the durable-only branch (the probe saw no
|
|
95
|
+
* anchor), re-probe the anchor once; if an anchor now exists, re-run the
|
|
96
|
+
* anchored selection against it. The invariant this achieves: a durable-only
|
|
97
|
+
* result is returned only when the journal was anchor-free as of the final
|
|
98
|
+
* probe — so a run-started appended between the original probe and the
|
|
99
|
+
* selection can never be missed while present. When the first probe *did*
|
|
100
|
+
* find an anchor, no re-check is needed: the anchored select's
|
|
101
|
+
* `seq >= anchor` arm is monotone-inclusive, so any later run-started (higher
|
|
102
|
+
* seq) is picked up by that same select. A run-started appended after the
|
|
103
|
+
* final re-probe is an ordinary post-read concurrent append — outside this
|
|
104
|
+
* read's snapshot, exactly as if it had landed after the method returned —
|
|
105
|
+
* and is not this race; that bound is why the re-check is one-shot and never
|
|
106
|
+
* a loop.
|
|
107
|
+
*
|
|
108
|
+
* Non-concurrent behavior is byte-identical: the same two statements run in
|
|
109
|
+
* the same order; only the durable-only outcome may trigger one extra
|
|
110
|
+
* probe+select pair.
|
|
111
|
+
*/
|
|
112
|
+
export async function readRepoStateEventsWithAnchorRecheck(
|
|
113
|
+
probeAnchor: () => Promise<number | undefined>,
|
|
114
|
+
select: (anchor: number | undefined) => Promise<RepositoryEvent[]>,
|
|
115
|
+
): Promise<RepositoryEvent[]> {
|
|
116
|
+
const anchor = await probeAnchor()
|
|
117
|
+
const events = await select(anchor)
|
|
118
|
+
if (anchor !== undefined) return events
|
|
119
|
+
const rechecked = await probeAnchor()
|
|
120
|
+
return rechecked === undefined ? events : select(rechecked)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The normative subset derivation every adapter implements and the contract
|
|
124
|
+
* tests use as the oracle: durable types across the whole journal, plus —
|
|
125
|
+
* only when the journal has one — every event from the latest
|
|
126
|
+
* `dispatcher.run-started` onward, ordered by seq. A journal with no
|
|
127
|
+
* run-started has **no tail at all** (durable types only); the anchor is
|
|
128
|
+
* never defaulted to 0, which would make a `seq >= anchor` clause select the
|
|
129
|
+
* whole journal and defeat the bound. */
|
|
130
|
+
export function projectRepositoryStateEvents(
|
|
131
|
+
events: readonly RepositoryEvent[],
|
|
132
|
+
): RepositoryEvent[] {
|
|
133
|
+
let anchor: number | undefined
|
|
134
|
+
for (const event of events) {
|
|
135
|
+
if (event.type === RUN_STARTED) anchor = event.seq
|
|
136
|
+
}
|
|
137
|
+
return events.filter(
|
|
138
|
+
(event) => STATE_TYPES.has(event.type) || (anchor !== undefined && event.seq >= anchor),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -38,6 +38,13 @@
|
|
|
38
38
|
*/
|
|
39
39
|
export const OPERATOR_NOTES_REPO_KIND = 'operator-notes'
|
|
40
40
|
|
|
41
|
+
/** The repository artifact carrying the schema-validated composed Config one
|
|
42
|
+
* dispatch run used. The embedded orchestrator's hosted wiring resolves its
|
|
43
|
+
* per-repository configuration from the latest revision of this kind
|
|
44
|
+
* (AUT-342). Defined here — the store must not import from the CLI — and
|
|
45
|
+
* re-exported by `cli/dispatch.ts`, which deposits it. */
|
|
46
|
+
export const DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT = 'dispatcher-effective-config'
|
|
47
|
+
|
|
41
48
|
/**
|
|
42
49
|
* Repository-scoped artifact kinds subject to retention. Values mirror the
|
|
43
50
|
* kind constants in `cli/dispatch.ts` (DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT)
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
Artifact,
|
|
14
14
|
ArtifactInput,
|
|
15
15
|
ArtifactMeta,
|
|
16
|
+
BuildDigest,
|
|
16
17
|
BuildRecord,
|
|
17
18
|
BuildStore,
|
|
18
19
|
NewBuildInput,
|
|
@@ -26,6 +27,13 @@ import type {
|
|
|
26
27
|
SubscribeOptions,
|
|
27
28
|
Unsubscribe,
|
|
28
29
|
} from './types'
|
|
30
|
+
import type {
|
|
31
|
+
TicketAsset,
|
|
32
|
+
TicketAssetInput,
|
|
33
|
+
TicketAssetLimits,
|
|
34
|
+
TicketAssetMeta,
|
|
35
|
+
TicketAssetSummary,
|
|
36
|
+
} from './ticket-assets'
|
|
29
37
|
import type {
|
|
30
38
|
StreamChunk,
|
|
31
39
|
StreamOutcome,
|
|
@@ -160,6 +168,12 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
160
168
|
getRepoEvents(repo: string, _sinceSeq?: number): Promise<RepositoryEvent[]> {
|
|
161
169
|
return Promise.reject(new SessionScopeError(scope, 'getRepoEvents', repo))
|
|
162
170
|
},
|
|
171
|
+
getRepoStateEvents(repo: string): Promise<RepositoryEvent[]> {
|
|
172
|
+
return Promise.reject(new SessionScopeError(scope, 'getRepoStateEvents', repo))
|
|
173
|
+
},
|
|
174
|
+
getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
|
|
175
|
+
return Promise.reject(new SessionScopeError(scope, 'getRepoBuildDigests', repo))
|
|
176
|
+
},
|
|
163
177
|
putRepoArtifact(repo: string, _artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
|
|
164
178
|
return Promise.reject(new SessionScopeError(scope, 'putRepoArtifact', repo))
|
|
165
179
|
},
|
|
@@ -173,6 +187,48 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
173
187
|
listRepoArtifacts(repo: string, _kind?: string): Promise<RepositoryArtifactMeta[]> {
|
|
174
188
|
return Promise.reject(new SessionScopeError(scope, 'listRepoArtifacts', repo))
|
|
175
189
|
},
|
|
190
|
+
ticketAssetLimits(repo: string): Promise<TicketAssetLimits> {
|
|
191
|
+
return Promise.reject(new SessionScopeError(scope, 'ticketAssetLimits', repo))
|
|
192
|
+
},
|
|
193
|
+
putTicketAsset(
|
|
194
|
+
repo: string,
|
|
195
|
+
_ticketId: string,
|
|
196
|
+
_asset: TicketAssetInput,
|
|
197
|
+
): Promise<TicketAssetMeta> {
|
|
198
|
+
return Promise.reject(new SessionScopeError(scope, 'putTicketAsset', repo))
|
|
199
|
+
},
|
|
200
|
+
getTicketAsset(
|
|
201
|
+
repo: string,
|
|
202
|
+
_ticketId: string,
|
|
203
|
+
_kind: string,
|
|
204
|
+
_name: string,
|
|
205
|
+
_rev?: number,
|
|
206
|
+
): Promise<TicketAsset | null> {
|
|
207
|
+
return Promise.reject(new SessionScopeError(scope, 'getTicketAsset', repo))
|
|
208
|
+
},
|
|
209
|
+
getPinnedTicketAsset(
|
|
210
|
+
slug: string,
|
|
211
|
+
_kind: string,
|
|
212
|
+
_name: string,
|
|
213
|
+
_rev?: number,
|
|
214
|
+
): Promise<TicketAsset | null> {
|
|
215
|
+
return Promise.reject(new SessionScopeError(scope, 'getPinnedTicketAsset', slug))
|
|
216
|
+
},
|
|
217
|
+
listTicketAssets(
|
|
218
|
+
repo: string,
|
|
219
|
+
_ticketId: string,
|
|
220
|
+
_opts?: { revisions?: boolean },
|
|
221
|
+
): Promise<TicketAssetSummary[]> {
|
|
222
|
+
return Promise.reject(new SessionScopeError(scope, 'listTicketAssets', repo))
|
|
223
|
+
},
|
|
224
|
+
removeTicketAsset(
|
|
225
|
+
repo: string,
|
|
226
|
+
_ticketId: string,
|
|
227
|
+
_kind: string,
|
|
228
|
+
_name: string,
|
|
229
|
+
): Promise<TicketAssetMeta | null> {
|
|
230
|
+
return Promise.reject(new SessionScopeError(scope, 'removeTicketAsset', repo))
|
|
231
|
+
},
|
|
176
232
|
claimRepoLease(repo: string, _holder: string, _ttlMs: number): Promise<boolean> {
|
|
177
233
|
return Promise.reject(new SessionScopeError(scope, 'claimRepoLease', repo))
|
|
178
234
|
},
|
|
@@ -199,6 +255,14 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
199
255
|
own('appendSessionEvent', id)
|
|
200
256
|
return store.appendSessionEvent(id, event)
|
|
201
257
|
},
|
|
258
|
+
async appendSessionEventIfCurrent<T extends SessionEventType>(
|
|
259
|
+
id: string,
|
|
260
|
+
expectedSeq: number,
|
|
261
|
+
event: SessionEventWrite<T>,
|
|
262
|
+
): Promise<SessionEventEnvelope<T> | null> {
|
|
263
|
+
own('appendSessionEventIfCurrent', id)
|
|
264
|
+
return store.appendSessionEventIfCurrent(id, expectedSeq, event)
|
|
265
|
+
},
|
|
202
266
|
async getSessionEvents(
|
|
203
267
|
id: string,
|
|
204
268
|
sinceSeq?: number,
|
|
@@ -13,7 +13,7 @@ export type TurnOutcomeState = 'open' | 'suspended' | 'completed' | 'failed'
|
|
|
13
13
|
|
|
14
14
|
export type SessionTurnTrigger =
|
|
15
15
|
| { kind: 'message'; messageSeq: number }
|
|
16
|
-
| { kind: 'wake'; build
|
|
16
|
+
| { kind: 'wake'; build?: string; journal?: true; seq: number; type: string }
|
|
17
17
|
|
|
18
18
|
export interface SessionTurn {
|
|
19
19
|
turn: string
|
|
@@ -21,8 +21,11 @@ export interface SessionTurn {
|
|
|
21
21
|
startedSeq: number
|
|
22
22
|
trigger: SessionTurnTrigger
|
|
23
23
|
state: TurnOutcomeState
|
|
24
|
-
usage?: { inputTokens: number; outputTokens: number; turns?: number }
|
|
24
|
+
usage?: { inputTokens: number; outputTokens: number; steps: number; turns?: number }
|
|
25
25
|
error?: string
|
|
26
|
+
/** The typed failure class of a failed turn (provider-unavailable,
|
|
27
|
+
* exhausted, credentials, configuration, or internal). */
|
|
28
|
+
kind?: 'provider-unavailable' | 'exhausted' | 'credentials' | 'configuration' | 'internal'
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export interface SessionState {
|
|
@@ -43,6 +46,10 @@ export interface SessionState {
|
|
|
43
46
|
wakeGlobs: string[]
|
|
44
47
|
/** Per build, the highest build event seq a wake trigger has consumed. */
|
|
45
48
|
wakeCursors: Record<string, number>
|
|
49
|
+
/** The highest repository-journal seq a wake trigger has consumed (0 when
|
|
50
|
+
* no journal wake has been consumed). The journal has an independent seq
|
|
51
|
+
* space with no build slug, so it gets its own cursor. */
|
|
52
|
+
journalWakeCursor: number
|
|
46
53
|
/** Every turn in start order, with its stream id and outcome. */
|
|
47
54
|
turns: SessionTurn[]
|
|
48
55
|
}
|
|
@@ -63,6 +70,7 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
63
70
|
let pendingApproval: SessionState['pendingApproval']
|
|
64
71
|
let wakeGlobs: string[] = []
|
|
65
72
|
const wakeCursors: Record<string, number> = {}
|
|
73
|
+
let journalWakeCursor = 0
|
|
66
74
|
|
|
67
75
|
const open = (): TurnEntry | undefined => turns.findLast((turn) => !isTerminal(turn.state))
|
|
68
76
|
|
|
@@ -85,8 +93,15 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
85
93
|
}
|
|
86
94
|
turns.push(entry)
|
|
87
95
|
if (event.payload.trigger.kind === 'wake') {
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
if (event.payload.trigger.journal === true) {
|
|
97
|
+
journalWakeCursor = Math.max(journalWakeCursor, event.payload.trigger.seq)
|
|
98
|
+
} else {
|
|
99
|
+
// The catalog's `.check()` guarantees `build` when `journal` is
|
|
100
|
+
// absent — a stored trigger can never be a journal wake without
|
|
101
|
+
// the flag or a build wake without the slug.
|
|
102
|
+
const build = event.payload.trigger.build!
|
|
103
|
+
wakeCursors[build] = Math.max(wakeCursors[build] ?? 0, event.payload.trigger.seq)
|
|
104
|
+
}
|
|
90
105
|
}
|
|
91
106
|
break
|
|
92
107
|
}
|
|
@@ -118,11 +133,13 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
118
133
|
pendingApproval.toolCallId === event.payload.toolCallId
|
|
119
134
|
) {
|
|
120
135
|
pendingApproval = undefined
|
|
121
|
-
// The answer
|
|
122
|
-
//
|
|
123
|
-
// the
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
// The answer only clears the pending approval — `turn.resumed` is
|
|
137
|
+
// the sole resume fact, recorded by the runner when it actually
|
|
138
|
+
// picks the turn up. Flipping the turn state here (the earlier
|
|
139
|
+
// behavior) made a session look `running` while no runner existed:
|
|
140
|
+
// an answered approval whose resuming invocation died would be
|
|
141
|
+
// unrecoverable. The status sequence after an answer is therefore
|
|
142
|
+
// awaiting-approval → suspended(approval) → running.
|
|
126
143
|
}
|
|
127
144
|
break
|
|
128
145
|
}
|
|
@@ -139,6 +156,7 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
139
156
|
if (entry && !isTerminal(entry.state)) {
|
|
140
157
|
entry.state = 'failed'
|
|
141
158
|
entry.error = event.payload.error
|
|
159
|
+
entry.kind = event.payload.kind
|
|
142
160
|
}
|
|
143
161
|
break
|
|
144
162
|
}
|
|
@@ -180,6 +198,7 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
180
198
|
...(pendingApproval !== undefined ? { pendingApproval } : {}),
|
|
181
199
|
wakeGlobs,
|
|
182
200
|
wakeCursors,
|
|
201
|
+
journalWakeCursor,
|
|
183
202
|
turns: turns.map(({ suspensionCause: _suspensionCause, ...turn }) => turn),
|
|
184
203
|
}
|
|
185
204
|
}
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
* never reorder), retries re-queue at the front under the same discipline,
|
|
24
24
|
* and `close` waits out any in-flight drain before closing the stream.
|
|
25
25
|
*/
|
|
26
|
-
import type { SessionStreamSink } from '../../ports/types'
|
|
27
26
|
import { STREAM_PART_MAX_BYTES, truncationPart } from '../../ports/runner/stream-parts'
|
|
28
27
|
import type { JsonRecord } from '../../ports/runner/json-record'
|
|
29
28
|
import {
|
|
@@ -52,6 +51,22 @@ export interface SessionStreamSinkOptions {
|
|
|
52
51
|
schedule?: (flush: () => void, ms: number) => () => void
|
|
53
52
|
}
|
|
54
53
|
|
|
54
|
+
/** The sink's concrete surface. Structurally a superset of the
|
|
55
|
+
* `SessionStreamSink` port (ports/types.ts): `open` keeps its port-declared
|
|
56
|
+
* shape for the build-runner's `openSessionStreamLabel` caller, while the
|
|
57
|
+
* orchestrator turn runner additionally uses `resume` (bind to an
|
|
58
|
+
* already-open stream — a resumed invocation must append to the turn's
|
|
59
|
+
* existing stream, never create a second one) and `flush` (await the drain,
|
|
60
|
+
* so a step-boundary checkpoint is durable before the next step starts). */
|
|
61
|
+
export type SessionStreamWriter = {
|
|
62
|
+
open(label: string): Promise<string>
|
|
63
|
+
append(parts: StreamPart[]): void
|
|
64
|
+
close(outcome: StreamOutcome): Promise<void>
|
|
65
|
+
} & {
|
|
66
|
+
resume(streamId: string): Promise<string>
|
|
67
|
+
flush(): Promise<void>
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
/** Consecutive failed flushes before the writer stops streaming a session. */
|
|
56
71
|
const MAX_FLUSH_FAILURES = 3
|
|
57
72
|
|
|
@@ -131,8 +146,10 @@ function boundPart(part: StreamPart, maxBytes: number): StreamPart[] {
|
|
|
131
146
|
/**
|
|
132
147
|
* Create the bracket-scoped sink. `open` creates the store stream and
|
|
133
148
|
* returns its id; appends buffer until then and flush on the cadence.
|
|
149
|
+
* `resume` binds to an already-open stream instead of creating one;
|
|
150
|
+
* `flush` awaits the drain without closing.
|
|
134
151
|
*/
|
|
135
|
-
export function createSessionStreamSink(options: SessionStreamSinkOptions):
|
|
152
|
+
export function createSessionStreamSink(options: SessionStreamSinkOptions): SessionStreamWriter {
|
|
136
153
|
const { store, scope } = options
|
|
137
154
|
const flushMs = options.flushMs ?? 250
|
|
138
155
|
const maxPartBytes = STREAM_PART_MAX_BYTES
|
|
@@ -288,6 +305,29 @@ export function createSessionStreamSink(options: SessionStreamSinkOptions): Sess
|
|
|
288
305
|
}
|
|
289
306
|
},
|
|
290
307
|
|
|
308
|
+
/** Bind to an already-open stream — the resume path. Never calls
|
|
309
|
+
* `createStream`, so a fresh invocation appends to the turn's existing
|
|
310
|
+
* stream instead of creating a second one nothing reads. Appends made
|
|
311
|
+
* before the bind stay buffered and flush right after. */
|
|
312
|
+
async resume(boundStreamId) {
|
|
313
|
+
if (streamId !== undefined) {
|
|
314
|
+
throw new Error(`session stream sink already bound to "${streamId}"`)
|
|
315
|
+
}
|
|
316
|
+
streamId = boundStreamId
|
|
317
|
+
ensureTimer()
|
|
318
|
+
await requestFlush()
|
|
319
|
+
return streamId
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/** Await the drain of everything appended so far (joining the in-flight
|
|
323
|
+
* drain under the single-flight rule), so a caller can make a checkpoint
|
|
324
|
+
* durable without closing. Best-effort like `append`: a defunct writer
|
|
325
|
+
* resolves without throwing, and the diagnostic hook already fired. */
|
|
326
|
+
async flush() {
|
|
327
|
+
if (streamId === undefined) return
|
|
328
|
+
await requestFlush()
|
|
329
|
+
},
|
|
330
|
+
|
|
291
331
|
append(parts) {
|
|
292
332
|
// Never throws: after defunct/close or before a successful open,
|
|
293
333
|
// parts are dropped or buffered without surfacing an error.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* synchronous (SQLite) and asynchronous (PostgreSQL) backends is worth more
|
|
11
11
|
* than wake-on-write precision. The loop serves two cadences: held *stream*
|
|
12
12
|
* reads are presentation content and poll at the ~25 ms `STREAM_WAIT_POLL_MS`;
|
|
13
|
-
* held *event* reads (build, repository, session — AUT-334/381/383) take the
|
|
13
|
+
* held *event* reads (build, repository, session — AUT-334/381/383/394) take the
|
|
14
14
|
* one-second `EVENT_WAIT_POLL_MS` budget when the adapter passes it per call
|
|
15
15
|
* (see `EVENT_WAIT_POLL_MS` for which adapters do). Closed streams never wait.
|
|
16
16
|
*
|
|
@@ -28,15 +28,14 @@ export const STREAM_WAIT_POLL_MS = 25
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* The one-second held-event poll budget (the hosted per-query budget,
|
|
31
|
-
* AUT-334/381/383): held event reads — build, repository, and session —
|
|
31
|
+
* AUT-334/381/383/394): held event reads — build, repository, and session —
|
|
32
32
|
* re-query at most once per second, so an append is observed at the next
|
|
33
33
|
* poll, typically within about one second. Held *stream* reads are excluded:
|
|
34
34
|
* they are presentation content and stay at the ~25 ms `STREAM_WAIT_POLL_MS`
|
|
35
35
|
* cadence. This deliberately diverges from `STREAM_WAIT_POLL_MS`; adapters
|
|
36
36
|
* pass it as `pollMs` on their event-family calls to `readEventsWithWait` —
|
|
37
|
-
* the PostgreSQL and
|
|
38
|
-
*
|
|
39
|
-
* its held event reads fall back to the stream default until it does.
|
|
37
|
+
* the PostgreSQL, memory, and SQLite stores do on all three families
|
|
38
|
+
* (AUT-334/381/383/394).
|
|
40
39
|
*
|
|
41
40
|
* The PostgreSQL adapter treats the bound as nominal, not hard: a worst-case
|
|
42
41
|
* ≤1 s guarantee would need Postgres LISTEN/NOTIFY wake-on-append — one
|