@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.
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- 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
|
+
}
|