@defrex/autobuild 0.8.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.
Files changed (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,780 @@
1
+ /**
2
+ * RemoteBuildStore (SPEC §7.2 adapter 2): the client half of the remote
3
+ * store — a full BuildStore over the wire protocol in protocol.ts. The
4
+ * contract suite runs against it unchanged; in particular:
5
+ *
6
+ * - D6: a 422 from the server rehydrates as `EventValidationError` with the
7
+ * server's message — validation feedback survives the wire.
8
+ * - D8: 401/403 surface as `AuthError`; 404 as an `Error` matching the
9
+ * local adapters' `unknown build "…"` message shape.
10
+ *
11
+ * The client takes a *fixed* base URL for its lifetime; continuity across
12
+ * server restarts lives in the backing store (§7.4), not in the client.
13
+ * `subscribe` is the shared polling implementation over `getEvents(since)`
14
+ * (§7.2); `close()` is a no-op — the server owns the backing store.
15
+ */
16
+ import {
17
+ EventValidationError,
18
+ type AbEvent,
19
+ type EventEnvelope,
20
+ type EventWrite,
21
+ } from '../../events/catalog'
22
+ import type { EventType } from '../../events/payloads'
23
+ import type {
24
+ RepositoryEvent,
25
+ RepositoryEventEnvelope,
26
+ RepositoryEventType,
27
+ RepositoryEventWrite,
28
+ } from '../../events/repository'
29
+ import type {
30
+ SessionEvent,
31
+ SessionEventEnvelope,
32
+ SessionEventType,
33
+ SessionEventWrite,
34
+ } from '../../events/sessions'
35
+ import { createBuildScopedStore } from '../build-scope'
36
+ import { createSessionScopedStore } from '../session-handle'
37
+ import { pollingSubscribe } from '../subscribe'
38
+ import type {
39
+ StreamChunk,
40
+ StreamOutcome,
41
+ StreamPart,
42
+ StreamRead,
43
+ StreamRecord,
44
+ StreamScope,
45
+ } from '../streams/types'
46
+ import { StreamBatchTooLargeError, StreamClosedError } from '../streams/types'
47
+ import {
48
+ toBytes,
49
+ type Artifact,
50
+ type ArtifactInput,
51
+ type ArtifactMeta,
52
+ type BuildRecord,
53
+ type BuildScopedStore,
54
+ type BuildStore,
55
+ type NewBuildInput,
56
+ type NewSessionInput,
57
+ type RepositoryArtifact,
58
+ type RepositoryArtifactMeta,
59
+ type RepositoryRecord,
60
+ type SessionArtifact,
61
+ type SessionArtifactMeta,
62
+ type SessionRecord,
63
+ type SessionScopedStore,
64
+ type SubscribeOptions,
65
+ type Unsubscribe,
66
+ } from '../types'
67
+ import {
68
+ artifactGetResponseSchema,
69
+ artifactMetaListSchema,
70
+ artifactMetaWireSchema,
71
+ buildRecordListSchema,
72
+ buildRecordWireSchema,
73
+ conditionalEventResponseSchema,
74
+ decodeBase64,
75
+ depositsResponseSchema,
76
+ encodeBase64,
77
+ errorBodySchema,
78
+ eventEnvelopeWireSchema,
79
+ eventListSchema,
80
+ repositoryEventEnvelopeWireSchema,
81
+ repositoryEventListSchema,
82
+ okResponseSchema,
83
+ placeholderRev,
84
+ repoDepositsResponseSchema,
85
+ repositoryArtifactGetResponseSchema,
86
+ repositoryArtifactMetaListSchema,
87
+ repositoryArtifactMetaWireSchema,
88
+ repositoryRecordWireSchema,
89
+ sessionArtifactGetResponseSchema,
90
+ sessionArtifactMetaListSchema,
91
+ sessionArtifactMetaWireSchema,
92
+ sessionDepositsResponseSchema,
93
+ sessionEventEnvelopeWireSchema,
94
+ sessionEventListSchema,
95
+ sessionRecordListSchema,
96
+ sessionRecordWireSchema,
97
+ streamChunkWireSchema,
98
+ streamReadWireSchema,
99
+ streamRecordListSchema,
100
+ streamRecordWireSchema,
101
+ } from './protocol'
102
+ import {
103
+ AUTOBUILD_VERSION,
104
+ AUTOBUILD_VERSION_HEADER,
105
+ REMOTE_STORE_PROTOCOL_VERSION,
106
+ REMOTE_STORE_PROTOCOL_VERSION_HEADER,
107
+ } from './version'
108
+
109
+ /** A scoped-token rejection (D8): 401 (missing/expired) or 403 (wrong build). */
110
+ export class AuthError extends Error {
111
+ constructor(message: string) {
112
+ super(message)
113
+ this.name = 'AuthError'
114
+ }
115
+ }
116
+
117
+ export interface RemoteStoreIdentity {
118
+ autobuildVersion: string
119
+ protocolVersion: string
120
+ }
121
+
122
+ /** The bounded-wait window the client requests on event reads and watch
123
+ * polls (AUT-334): matches the hosted service's documented ceiling. A server
124
+ * with a smaller ceiling clamps server-side; one that ignores the parameter
125
+ * answers immediately, which only raises the request rate. */
126
+ export const REMOTE_EVENT_WAIT_SECONDS = 25
127
+
128
+ export interface RemoteBuildStoreOptions {
129
+ /** Base URL of a store server (e.g. `http://127.0.0.1:4711`); fixed for life. */
130
+ url: string
131
+ /** Scoped bearer token (D8); omit against an open (no-secret) server. */
132
+ token?: string
133
+ /** Injectable network seam; defaults to global fetch. */
134
+ fetchFn?: typeof fetch
135
+ /** Test seam for exercising package/protocol skew diagnostics. */
136
+ identity?: Partial<RemoteStoreIdentity>
137
+ }
138
+
139
+ export class RemoteBuildStore implements BuildStore {
140
+ private readonly base: string
141
+ private readonly token: string | undefined
142
+ private readonly fetchFn: typeof fetch
143
+ private readonly identity: RemoteStoreIdentity
144
+
145
+ constructor(opts: RemoteBuildStoreOptions) {
146
+ this.base = opts.url.replace(/\/+$/, '')
147
+ this.token = opts.token
148
+ this.fetchFn = opts.fetchFn ?? fetch
149
+ this.identity = {
150
+ autobuildVersion: opts.identity?.autobuildVersion ?? AUTOBUILD_VERSION,
151
+ protocolVersion: opts.identity?.protocolVersion ?? REMOTE_STORE_PROTOCOL_VERSION,
152
+ }
153
+ }
154
+
155
+ scopeBuild(slug: string): BuildScopedStore {
156
+ return createBuildScopedStore(this, slug)
157
+ }
158
+
159
+ scopeSession(id: string): SessionScopedStore {
160
+ return createSessionScopedStore(this, id)
161
+ }
162
+
163
+ private buildPath(slug: string): string {
164
+ return `/builds/${encodeURIComponent(slug)}`
165
+ }
166
+
167
+ private repoPath(repo: string): string {
168
+ return `/repos/${encodeURIComponent(repo)}`
169
+ }
170
+
171
+ private sessionPath(id: string): string {
172
+ return `/sessions/${encodeURIComponent(id)}`
173
+ }
174
+
175
+ private async raw(
176
+ method: 'GET' | 'POST',
177
+ path: string,
178
+ body?: unknown,
179
+ signal?: AbortSignal,
180
+ ): Promise<Response> {
181
+ const headers: Record<string, string> = {
182
+ [AUTOBUILD_VERSION_HEADER]: this.identity.autobuildVersion,
183
+ [REMOTE_STORE_PROTOCOL_VERSION_HEADER]: this.identity.protocolVersion,
184
+ }
185
+ if (this.token !== undefined) headers.authorization = `Bearer ${this.token}`
186
+ if (body !== undefined) headers['content-type'] = 'application/json'
187
+ return this.fetchFn(`${this.base}${path}`, {
188
+ method,
189
+ headers,
190
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
191
+ ...(signal !== undefined ? { signal } : {}),
192
+ })
193
+ }
194
+
195
+ private async toError(response: Response): Promise<Error> {
196
+ let body: { error: string; kind: string } | undefined
197
+ try {
198
+ body = errorBodySchema.parse(await response.json())
199
+ } catch {
200
+ // Not a protocol error body — fall back to the status line.
201
+ }
202
+ const message = body?.error ?? `store server responded ${response.status}`
203
+ if (response.status === 401 || response.status === 403) {
204
+ return new AuthError(message)
205
+ }
206
+ // D6: validation feedback crosses the wire as the same error type with
207
+ // the server's message intact.
208
+ if (response.status === 422) return new EventValidationError(message)
209
+ // Typed stream rejections rehydrate by the server's message shape: the
210
+ // ceiling names its bytes, the closed-append names its stream (a 409 is
211
+ // otherwise the generic already-exists conflict).
212
+ if (response.status === 413 && message.startsWith('stream batch of ')) {
213
+ return new StreamBatchTooLargeError(undefined, undefined, message)
214
+ }
215
+ if (response.status === 409 && /^stream ".+" is closed$/.test(message)) {
216
+ return new StreamClosedError(message)
217
+ }
218
+ // 404 carries the local adapters' message shape: `unknown build "slug"`.
219
+ return new Error(message)
220
+ }
221
+
222
+ private async requestJson<T>(
223
+ method: 'GET' | 'POST',
224
+ path: string,
225
+ schema: { parse: (data: unknown) => T },
226
+ body?: unknown,
227
+ signal?: AbortSignal,
228
+ ): Promise<T> {
229
+ const response = await this.raw(method, path, body, signal)
230
+ if (!response.ok) throw await this.toError(response)
231
+ return schema.parse(await response.json())
232
+ }
233
+
234
+ async createBuild(input: NewBuildInput): Promise<BuildRecord> {
235
+ return this.requestJson('POST', '/builds', buildRecordWireSchema, input)
236
+ }
237
+
238
+ async getBuild(slug: string): Promise<BuildRecord | null> {
239
+ const response = await this.raw('GET', this.buildPath(slug))
240
+ if (response.status === 404) return null
241
+ if (!response.ok) throw await this.toError(response)
242
+ return buildRecordWireSchema.parse(await response.json())
243
+ }
244
+
245
+ async listBuilds(): Promise<BuildRecord[]> {
246
+ return this.requestJson('GET', '/builds', buildRecordListSchema)
247
+ }
248
+
249
+ async append<T extends EventType>(slug: string, event: EventWrite<T>): Promise<EventEnvelope<T>> {
250
+ const envelope = await this.requestJson(
251
+ 'POST',
252
+ `${this.buildPath(slug)}/events`,
253
+ eventEnvelopeWireSchema,
254
+ { actor: event.actor, type: event.type, payload: event.payload },
255
+ )
256
+ return envelope as unknown as EventEnvelope<T>
257
+ }
258
+
259
+ async appendIfCurrent<T extends EventType>(
260
+ slug: string,
261
+ expectedSeq: number,
262
+ event: EventWrite<T>,
263
+ ): Promise<EventEnvelope<T> | null> {
264
+ const envelope = await this.requestJson(
265
+ 'POST',
266
+ `${this.buildPath(slug)}/events/conditional`,
267
+ conditionalEventResponseSchema,
268
+ {
269
+ expectedSeq,
270
+ event: { actor: event.actor, type: event.type, payload: event.payload },
271
+ },
272
+ )
273
+ return envelope as EventEnvelope<T> | null
274
+ }
275
+
276
+ /**
277
+ * The wire form of atomic deposits (D6 — see protocol.ts): `makeEvent`
278
+ * runs client-side against sentinel metas whose `revision` is the negative
279
+ * placeholder `-(index+1)`; the resulting payload (carrying the sentinel
280
+ * `{kind, rev}` refs) ships to the server, which substitutes the real
281
+ * revisions inside the backing store's atomic `appendWithArtifacts`.
282
+ * Payloads may embed the deposited refs as `{kind, rev}` objects — the
283
+ * system's only usage pattern; computing over the revision values is
284
+ * unsupported by design.
285
+ */
286
+ async appendWithArtifacts<T extends EventType>(
287
+ slug: string,
288
+ artifacts: ArtifactInput[],
289
+ makeEvent: (deposited: ArtifactMeta[]) => EventWrite<T>,
290
+ ): Promise<{ event: EventEnvelope<T>; artifacts: ArtifactMeta[] }> {
291
+ const sentinels: ArtifactMeta[] = artifacts.map((artifact, index) => ({
292
+ build: slug,
293
+ kind: artifact.kind,
294
+ revision: placeholderRev(index),
295
+ blobRef: '',
296
+ metadata: structuredClone(artifact.metadata ?? {}),
297
+ createdAt: '',
298
+ }))
299
+ const write = makeEvent(sentinels)
300
+ const result = await this.requestJson(
301
+ 'POST',
302
+ `${this.buildPath(slug)}/deposits`,
303
+ depositsResponseSchema,
304
+ {
305
+ artifacts: artifacts.map((artifact) => ({
306
+ kind: artifact.kind,
307
+ contentBase64: encodeBase64(toBytes(artifact.content)),
308
+ ...(artifact.metadata !== undefined ? { metadata: artifact.metadata } : {}),
309
+ })),
310
+ event: { actor: write.actor, type: write.type, payload: write.payload },
311
+ },
312
+ )
313
+ return {
314
+ event: result.event as unknown as EventEnvelope<T>,
315
+ artifacts: result.artifacts,
316
+ }
317
+ }
318
+
319
+ /** The BuildStore abort contract (AUT-380): a held read whose caller's
320
+ * signal fires resolves with the empty result — the client-side mirror of
321
+ * the server's `withDisconnect`, which discards the aborted response
322
+ * anyway — instead of rejecting with fetch's AbortError. Failures that are
323
+ * not the caller's own abort (the store being unreachable, say) still
324
+ * reject. */
325
+ private async heldRequest<T>(
326
+ signal: AbortSignal | undefined,
327
+ empty: () => T,
328
+ read: () => Promise<T>,
329
+ ): Promise<T> {
330
+ try {
331
+ return await read()
332
+ } catch (error) {
333
+ if (signal?.aborted) return empty()
334
+ throw error
335
+ }
336
+ }
337
+
338
+ async getEvents(
339
+ slug: string,
340
+ sinceSeq = 0,
341
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
342
+ ): Promise<AbEvent[]> {
343
+ const params = new URLSearchParams({ since: String(sinceSeq) })
344
+ if (opts?.waitSeconds !== undefined) params.set('wait', String(opts.waitSeconds))
345
+ return this.heldRequest(
346
+ opts?.signal,
347
+ (): AbEvent[] => [],
348
+ () =>
349
+ this.requestJson(
350
+ 'GET',
351
+ `${this.buildPath(slug)}/events?${params}`,
352
+ eventListSchema,
353
+ undefined,
354
+ opts?.signal,
355
+ ) as Promise<AbEvent[]>,
356
+ )
357
+ }
358
+
359
+ async putArtifact(slug: string, artifact: ArtifactInput): Promise<ArtifactMeta> {
360
+ return this.requestJson('POST', `${this.buildPath(slug)}/artifacts`, artifactMetaWireSchema, {
361
+ kind: artifact.kind,
362
+ contentBase64: encodeBase64(toBytes(artifact.content)),
363
+ ...(artifact.metadata !== undefined ? { metadata: artifact.metadata } : {}),
364
+ })
365
+ }
366
+
367
+ async getArtifact(slug: string, kind: string, rev?: number): Promise<Artifact | null> {
368
+ const params = new URLSearchParams({ kind })
369
+ if (rev !== undefined) params.set('rev', String(rev))
370
+ const result = await this.requestJson(
371
+ 'GET',
372
+ `${this.buildPath(slug)}/artifacts?${params}`,
373
+ artifactGetResponseSchema,
374
+ )
375
+ if (result === null) return null
376
+ return { meta: result.meta, content: decodeBase64(result.contentBase64) }
377
+ }
378
+
379
+ async listArtifacts(slug: string, kind?: string): Promise<ArtifactMeta[]> {
380
+ const query = kind !== undefined ? `?kind=${encodeURIComponent(kind)}` : ''
381
+ return this.requestJson(
382
+ 'GET',
383
+ `${this.buildPath(slug)}/artifact-list${query}`,
384
+ artifactMetaListSchema,
385
+ )
386
+ }
387
+
388
+ async claimLease(slug: string, holder: string, ttlMs: number): Promise<boolean> {
389
+ const result = await this.requestJson(
390
+ 'POST',
391
+ `${this.buildPath(slug)}/lease/claim`,
392
+ okResponseSchema,
393
+ { holder, ttlMs },
394
+ )
395
+ return result.ok
396
+ }
397
+
398
+ async heartbeat(slug: string, holder: string): Promise<boolean> {
399
+ const result = await this.requestJson(
400
+ 'POST',
401
+ `${this.buildPath(slug)}/lease/heartbeat`,
402
+ okResponseSchema,
403
+ { holder },
404
+ )
405
+ return result.ok
406
+ }
407
+
408
+ async releaseLease(slug: string, holder: string): Promise<void> {
409
+ await this.requestJson('POST', `${this.buildPath(slug)}/lease/release`, okResponseSchema, {
410
+ holder,
411
+ })
412
+ }
413
+
414
+ async ensureRepo(repo: string): Promise<RepositoryRecord> {
415
+ return this.requestJson('POST', '/repos', repositoryRecordWireSchema, { repo })
416
+ }
417
+
418
+ async getRepo(repo: string): Promise<RepositoryRecord | null> {
419
+ const response = await this.raw('GET', this.repoPath(repo))
420
+ if (response.status === 404) return null
421
+ if (!response.ok) throw await this.toError(response)
422
+ return repositoryRecordWireSchema.parse(await response.json())
423
+ }
424
+
425
+ async appendRepo<T extends RepositoryEventType>(
426
+ repo: string,
427
+ event: RepositoryEventWrite<T>,
428
+ ): Promise<RepositoryEventEnvelope<T>> {
429
+ const envelope = await this.requestJson(
430
+ 'POST',
431
+ `${this.repoPath(repo)}/events`,
432
+ repositoryEventEnvelopeWireSchema,
433
+ { actor: event.actor, type: event.type, payload: event.payload },
434
+ )
435
+ return envelope as unknown as RepositoryEventEnvelope<T>
436
+ }
437
+
438
+ async appendRepoWithArtifacts<T extends RepositoryEventType>(
439
+ repo: string,
440
+ artifacts: ArtifactInput[],
441
+ makeEvent: (deposited: RepositoryArtifactMeta[]) => RepositoryEventWrite<T>,
442
+ ): Promise<{
443
+ event: RepositoryEventEnvelope<T>
444
+ artifacts: RepositoryArtifactMeta[]
445
+ }> {
446
+ const sentinels: RepositoryArtifactMeta[] = artifacts.map((artifact, index) => ({
447
+ repo,
448
+ kind: artifact.kind,
449
+ revision: placeholderRev(index),
450
+ blobRef: '',
451
+ metadata: structuredClone(artifact.metadata ?? {}),
452
+ createdAt: '',
453
+ }))
454
+ const write = makeEvent(sentinels)
455
+ const result = await this.requestJson(
456
+ 'POST',
457
+ `${this.repoPath(repo)}/deposits`,
458
+ repoDepositsResponseSchema,
459
+ {
460
+ artifacts: artifacts.map((artifact) => ({
461
+ kind: artifact.kind,
462
+ contentBase64: encodeBase64(toBytes(artifact.content)),
463
+ ...(artifact.metadata !== undefined ? { metadata: artifact.metadata } : {}),
464
+ })),
465
+ event: { actor: write.actor, type: write.type, payload: write.payload },
466
+ },
467
+ )
468
+ return {
469
+ event: result.event as unknown as RepositoryEventEnvelope<T>,
470
+ artifacts: result.artifacts,
471
+ }
472
+ }
473
+
474
+ async getRepoEvents(
475
+ repo: string,
476
+ sinceSeq = 0,
477
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
478
+ ): Promise<RepositoryEvent[]> {
479
+ const params = new URLSearchParams({ since: String(sinceSeq) })
480
+ if (opts?.waitSeconds !== undefined) params.set('wait', String(opts.waitSeconds))
481
+ return this.heldRequest(
482
+ opts?.signal,
483
+ (): RepositoryEvent[] => [],
484
+ () =>
485
+ this.requestJson(
486
+ 'GET',
487
+ `${this.repoPath(repo)}/events?${params}`,
488
+ repositoryEventListSchema,
489
+ undefined,
490
+ opts?.signal,
491
+ ) as Promise<RepositoryEvent[]>,
492
+ )
493
+ }
494
+
495
+ async putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
496
+ return this.requestJson(
497
+ 'POST',
498
+ `${this.repoPath(repo)}/artifacts`,
499
+ repositoryArtifactMetaWireSchema,
500
+ {
501
+ kind: artifact.kind,
502
+ contentBase64: encodeBase64(toBytes(artifact.content)),
503
+ ...(artifact.metadata !== undefined ? { metadata: artifact.metadata } : {}),
504
+ },
505
+ )
506
+ }
507
+
508
+ async getRepoArtifact(
509
+ repo: string,
510
+ kind: string,
511
+ rev?: number,
512
+ ): Promise<RepositoryArtifact | null> {
513
+ const params = new URLSearchParams({ kind })
514
+ if (rev !== undefined) params.set('rev', String(rev))
515
+ const result = await this.requestJson(
516
+ 'GET',
517
+ `${this.repoPath(repo)}/artifacts?${params}`,
518
+ repositoryArtifactGetResponseSchema,
519
+ )
520
+ return result === null
521
+ ? null
522
+ : { meta: result.meta, content: decodeBase64(result.contentBase64) }
523
+ }
524
+
525
+ async listRepoArtifacts(repo: string, kind?: string): Promise<RepositoryArtifactMeta[]> {
526
+ const query = kind !== undefined ? `?kind=${encodeURIComponent(kind)}` : ''
527
+ return this.requestJson(
528
+ 'GET',
529
+ `${this.repoPath(repo)}/artifact-list${query}`,
530
+ repositoryArtifactMetaListSchema,
531
+ )
532
+ }
533
+
534
+ async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
535
+ const result = await this.requestJson(
536
+ 'POST',
537
+ `${this.repoPath(repo)}/lease/claim`,
538
+ okResponseSchema,
539
+ { holder, ttlMs },
540
+ )
541
+ return result.ok
542
+ }
543
+
544
+ async heartbeatRepo(repo: string, holder: string): Promise<boolean> {
545
+ const result = await this.requestJson(
546
+ 'POST',
547
+ `${this.repoPath(repo)}/lease/heartbeat`,
548
+ okResponseSchema,
549
+ { holder },
550
+ )
551
+ return result.ok
552
+ }
553
+
554
+ async releaseRepoLease(repo: string, holder: string): Promise<void> {
555
+ await this.requestJson('POST', `${this.repoPath(repo)}/lease/release`, okResponseSchema, {
556
+ holder,
557
+ })
558
+ }
559
+
560
+ // ── Operator sessions (SPEC §7.1.1) ──────────────────────────────────
561
+ // Mirrors the repository-journal family: collection routes under
562
+ // /repos/{repo}/sessions and addressed routes under /sessions/{id}.
563
+
564
+ async createSession(input: NewSessionInput): Promise<SessionRecord> {
565
+ return this.requestJson(
566
+ 'POST',
567
+ `${this.repoPath(input.repo)}/sessions`,
568
+ sessionRecordWireSchema,
569
+ input,
570
+ )
571
+ }
572
+
573
+ async getSession(id: string): Promise<SessionRecord | null> {
574
+ const response = await this.raw('GET', this.sessionPath(id))
575
+ if (response.status === 404) return null
576
+ if (!response.ok) throw await this.toError(response)
577
+ const body: unknown = await response.json()
578
+ return body === null ? null : sessionRecordWireSchema.parse(body)
579
+ }
580
+
581
+ async listSessions(repo: string): Promise<SessionRecord[]> {
582
+ return this.requestJson('GET', `${this.repoPath(repo)}/sessions`, sessionRecordListSchema)
583
+ }
584
+
585
+ async appendSessionEvent<T extends SessionEventType>(
586
+ id: string,
587
+ event: SessionEventWrite<T>,
588
+ ): Promise<SessionEventEnvelope<T>> {
589
+ const envelope = await this.requestJson(
590
+ 'POST',
591
+ `${this.sessionPath(id)}/events`,
592
+ sessionEventEnvelopeWireSchema,
593
+ { actor: event.actor, type: event.type, payload: event.payload },
594
+ )
595
+ return envelope as unknown as SessionEventEnvelope<T>
596
+ }
597
+
598
+ async getSessionEvents(
599
+ id: string,
600
+ sinceSeq = 0,
601
+ opts?: { waitSeconds?: number; signal?: AbortSignal },
602
+ ): Promise<SessionEvent[]> {
603
+ const params = new URLSearchParams({ since: String(sinceSeq) })
604
+ if (opts?.waitSeconds !== undefined) params.set('wait', String(opts.waitSeconds))
605
+ return this.heldRequest(
606
+ opts?.signal,
607
+ (): SessionEvent[] => [],
608
+ () =>
609
+ this.requestJson(
610
+ 'GET',
611
+ `${this.sessionPath(id)}/events?${params}`,
612
+ sessionEventListSchema,
613
+ undefined,
614
+ opts?.signal,
615
+ ) as Promise<SessionEvent[]>,
616
+ )
617
+ }
618
+
619
+ async appendSessionWithArtifacts<T extends SessionEventType>(
620
+ id: string,
621
+ artifacts: ArtifactInput[],
622
+ makeEvent: (deposited: SessionArtifactMeta[]) => SessionEventWrite<T>,
623
+ ): Promise<{ event: SessionEventEnvelope<T>; artifacts: SessionArtifactMeta[] }> {
624
+ const sentinels: SessionArtifactMeta[] = artifacts.map((artifact, index) => ({
625
+ session: id,
626
+ kind: artifact.kind,
627
+ revision: placeholderRev(index),
628
+ blobRef: '',
629
+ metadata: structuredClone(artifact.metadata ?? {}),
630
+ createdAt: '',
631
+ }))
632
+ const write = makeEvent(sentinels)
633
+ const result = await this.requestJson(
634
+ 'POST',
635
+ `${this.sessionPath(id)}/deposits`,
636
+ sessionDepositsResponseSchema,
637
+ {
638
+ artifacts: artifacts.map((artifact) => ({
639
+ kind: artifact.kind,
640
+ contentBase64: encodeBase64(toBytes(artifact.content)),
641
+ ...(artifact.metadata !== undefined ? { metadata: artifact.metadata } : {}),
642
+ })),
643
+ event: { actor: write.actor, type: write.type, payload: write.payload },
644
+ },
645
+ )
646
+ return {
647
+ event: result.event as unknown as SessionEventEnvelope<T>,
648
+ artifacts: result.artifacts,
649
+ }
650
+ }
651
+
652
+ async putSessionArtifact(id: string, artifact: ArtifactInput): Promise<SessionArtifactMeta> {
653
+ return this.requestJson(
654
+ 'POST',
655
+ `${this.sessionPath(id)}/artifacts`,
656
+ sessionArtifactMetaWireSchema,
657
+ {
658
+ kind: artifact.kind,
659
+ contentBase64: encodeBase64(toBytes(artifact.content)),
660
+ ...(artifact.metadata !== undefined ? { metadata: artifact.metadata } : {}),
661
+ },
662
+ )
663
+ }
664
+
665
+ async getSessionArtifact(
666
+ id: string,
667
+ kind: string,
668
+ rev?: number,
669
+ ): Promise<SessionArtifact | null> {
670
+ const params = new URLSearchParams({ kind })
671
+ if (rev !== undefined) params.set('rev', String(rev))
672
+ const result = await this.requestJson(
673
+ 'GET',
674
+ `${this.sessionPath(id)}/artifacts?${params}`,
675
+ sessionArtifactGetResponseSchema,
676
+ )
677
+ return result === null
678
+ ? null
679
+ : { meta: result.meta, content: decodeBase64(result.contentBase64) }
680
+ }
681
+
682
+ async listSessionArtifacts(id: string, kind?: string): Promise<SessionArtifactMeta[]> {
683
+ const query = kind !== undefined ? `?kind=${encodeURIComponent(kind)}` : ''
684
+ return this.requestJson(
685
+ 'GET',
686
+ `${this.sessionPath(id)}/artifact-list${query}`,
687
+ sessionArtifactMetaListSchema,
688
+ )
689
+ }
690
+
691
+ subscribe(slug: string, opts: SubscribeOptions, onEvent: (event: AbEvent) => void): Unsubscribe {
692
+ // Default to the bounded-wait window (AUT-334): a plain `subscribe` on a
693
+ // quiet stream costs one held request per wait window, not one request
694
+ // per `pollMs`. Callers may pass their own `waitSeconds` (or 0 to force
695
+ // the immediate interval loop); a server that ignores the parameter
696
+ // answers immediately and the cadence degrades to the old request rate.
697
+ const { waitSeconds = REMOTE_EVENT_WAIT_SECONDS, ...rest } = opts
698
+ return pollingSubscribe(
699
+ (since, pollOpts) => this.getEvents(slug, since, pollOpts),
700
+ { ...rest, waitSeconds: waitSeconds > 0 ? waitSeconds : undefined },
701
+ onEvent,
702
+ )
703
+ }
704
+
705
+ // ── Streams (SPEC §7.6) ──────────────────────────────────────────────────
706
+ // Create and list are scoped, so they use the family routes. The four
707
+ // addressed operations know only the stream id — a store-assigned id is
708
+ // globally unique — so they use the protocol's top-level `/streams/{id}`
709
+ // routes, where the server resolves the stream's own scope and
710
+ // authorizes against it.
711
+
712
+ private streamFamilyPath(scope: StreamScope): string {
713
+ return scope.kind === 'build'
714
+ ? `${this.buildPath(scope.build)}/streams`
715
+ : scope.kind === 'repo'
716
+ ? `${this.repoPath(scope.repo)}/streams`
717
+ : `${this.sessionPath(scope.session)}/streams`
718
+ }
719
+
720
+ private streamPath(streamId: string, suffix = ''): string {
721
+ return `/streams/${encodeURIComponent(streamId)}${suffix}`
722
+ }
723
+
724
+ async createStream(scope: StreamScope, label: string): Promise<StreamRecord> {
725
+ return this.requestJson('POST', this.streamFamilyPath(scope), streamRecordWireSchema, {
726
+ label,
727
+ }) as Promise<StreamRecord>
728
+ }
729
+
730
+ async appendStreamParts(streamId: string, parts: StreamPart[]): Promise<StreamChunk> {
731
+ return this.requestJson('POST', this.streamPath(streamId, '/chunks'), streamChunkWireSchema, {
732
+ parts,
733
+ }) as Promise<StreamChunk>
734
+ }
735
+
736
+ async readStream(
737
+ streamId: string,
738
+ opts?: { since?: number; waitSeconds?: number; signal?: AbortSignal },
739
+ ): Promise<StreamRead> {
740
+ const params = new URLSearchParams({ since: String(opts?.since ?? 0) })
741
+ if (opts?.waitSeconds !== undefined) params.set('wait', String(opts.waitSeconds))
742
+ // A closed stream never waits, so an aborted hold was necessarily on an
743
+ // open stream — the empty read is `open` with no chunks.
744
+ return this.heldRequest(
745
+ opts?.signal,
746
+ (): StreamRead => ({ chunks: [], status: 'open' }),
747
+ () =>
748
+ this.requestJson(
749
+ 'GET',
750
+ `${this.streamPath(streamId, '/chunks')}?${params}`,
751
+ streamReadWireSchema,
752
+ undefined,
753
+ opts?.signal,
754
+ ) as Promise<StreamRead>,
755
+ )
756
+ }
757
+
758
+ async closeStream(streamId: string, outcome: StreamOutcome): Promise<StreamRecord> {
759
+ return this.requestJson('POST', this.streamPath(streamId, '/close'), streamRecordWireSchema, {
760
+ outcome,
761
+ }) as Promise<StreamRecord>
762
+ }
763
+
764
+ async getStream(streamId: string): Promise<StreamRecord | null> {
765
+ const response = await this.raw('GET', this.streamPath(streamId))
766
+ if (response.status === 404) return null
767
+ if (!response.ok) throw await this.toError(response)
768
+ return streamRecordWireSchema.parse(await response.json()) as StreamRecord
769
+ }
770
+
771
+ async listStreams(scope: StreamScope): Promise<StreamRecord[]> {
772
+ return this.requestJson('GET', this.streamFamilyPath(scope), streamRecordListSchema) as Promise<
773
+ StreamRecord[]
774
+ >
775
+ }
776
+
777
+ async close(): Promise<void> {
778
+ // No-op: the server owns the backing store's lifecycle.
779
+ }
780
+ }