@defrex/autobuild 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -0,0 +1,205 @@
|
|
|
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 { defineReducer, type IncrementalReducer } from '../kernel/incremental'
|
|
51
|
+
import {
|
|
52
|
+
dispatcherSettingEventPayloadSchemas,
|
|
53
|
+
dispatcherStatusEventPayloadSchemas,
|
|
54
|
+
harvestEventPayloadSchemas,
|
|
55
|
+
orchestratorSandboxEventPayloadSchemas,
|
|
56
|
+
} from '../events/repository'
|
|
57
|
+
|
|
58
|
+
/** The anchor fact a bounded read's tail hangs from. */
|
|
59
|
+
const RUN_STARTED = 'dispatcher.run-started'
|
|
60
|
+
|
|
61
|
+
/** Durable, slow-growing repository event types: every fact a state reducer
|
|
62
|
+
* may consult at full replay depth. Derived from the catalog groups so a new
|
|
63
|
+
* harvest or sandbox event type joins automatically; the three dispatcher
|
|
64
|
+
* setting types are enumerated because they share one schema group with the
|
|
65
|
+
* run-correlated `dispatcher.config-reloaded`. */
|
|
66
|
+
export const REPOSITORY_STATE_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
67
|
+
...Object.keys(harvestEventPayloadSchemas),
|
|
68
|
+
...Object.keys(orchestratorSandboxEventPayloadSchemas),
|
|
69
|
+
...Object.keys(dispatcherSettingEventPayloadSchemas).filter(
|
|
70
|
+
(type) => type !== 'dispatcher.config-reloaded',
|
|
71
|
+
),
|
|
72
|
+
] as RepositoryEventType[]
|
|
73
|
+
|
|
74
|
+
/** Run-correlated repository event types: facts whose only state consumers
|
|
75
|
+
* reduce them for the latest dispatcher run, so the tail from the latest
|
|
76
|
+
* `dispatcher.run-started` contains everything those consumers keep. */
|
|
77
|
+
export const REPOSITORY_RUN_SCOPED_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
78
|
+
...Object.keys(dispatcherStatusEventPayloadSchemas),
|
|
79
|
+
'dispatcher.config-reloaded',
|
|
80
|
+
] as RepositoryEventType[]
|
|
81
|
+
|
|
82
|
+
const STATE_TYPES = new Set<string>(REPOSITORY_STATE_EVENT_TYPES)
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The race-closed read shape every SQL adapter delegates `getRepoStateEvents`
|
|
86
|
+
* to (AUT-551). Both adapters issue the anchor probe and the selection as two
|
|
87
|
+
* separate statements, so a concurrent append of the journal's first
|
|
88
|
+
* `dispatcher.run-started` between them is invisible to the probe, absent
|
|
89
|
+
* from the durable-only selection, yet present in the journal by the time the
|
|
90
|
+
* method returns — a stateless reader can transiently throw
|
|
91
|
+
* `effective-config-unavailable` where a moment-later full replay would
|
|
92
|
+
* succeed.
|
|
93
|
+
*
|
|
94
|
+
* The fix is a **one-shot anchor re-check on the durable-only outcome**:
|
|
95
|
+
* after a selection that took the durable-only branch (the probe saw no
|
|
96
|
+
* anchor), re-probe the anchor once; if an anchor now exists, re-run the
|
|
97
|
+
* anchored selection against it. The invariant this achieves: a durable-only
|
|
98
|
+
* result is returned only when the journal was anchor-free as of the final
|
|
99
|
+
* probe — so a run-started appended between the original probe and the
|
|
100
|
+
* selection can never be missed while present. When the first probe *did*
|
|
101
|
+
* find an anchor, no re-check is needed: the anchored select's
|
|
102
|
+
* `seq >= anchor` arm is monotone-inclusive, so any later run-started (higher
|
|
103
|
+
* seq) is picked up by that same select. A run-started appended after the
|
|
104
|
+
* final re-probe is an ordinary post-read concurrent append — outside this
|
|
105
|
+
* read's snapshot, exactly as if it had landed after the method returned —
|
|
106
|
+
* and is not this race; that bound is why the re-check is one-shot and never
|
|
107
|
+
* a loop.
|
|
108
|
+
*
|
|
109
|
+
* Non-concurrent behavior is byte-identical: the same two statements run in
|
|
110
|
+
* the same order; only the durable-only outcome may trigger one extra
|
|
111
|
+
* probe+select pair.
|
|
112
|
+
*/
|
|
113
|
+
export async function readRepoStateEventsWithAnchorRecheck(
|
|
114
|
+
probeAnchor: () => Promise<number | undefined>,
|
|
115
|
+
select: (anchor: number | undefined) => Promise<RepositoryEvent[]>,
|
|
116
|
+
): Promise<RepositoryEvent[]> {
|
|
117
|
+
const anchor = await probeAnchor()
|
|
118
|
+
const events = await select(anchor)
|
|
119
|
+
if (anchor !== undefined) return events
|
|
120
|
+
const rechecked = await probeAnchor()
|
|
121
|
+
return rechecked === undefined ? events : select(rechecked)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Carried state for the bounded read: the latest run-started seq (the last
|
|
125
|
+
* in array order, as the whole-array pass overwrites it) and every event seen,
|
|
126
|
+
* in array order. Nothing is pruned: a later run-started may carry a lower seq
|
|
127
|
+
* than an earlier one and restore events below the earlier anchor, so only
|
|
128
|
+
* `finish` can apply the filter exactly. Pruning below a monotone anchor is
|
|
129
|
+
* valid only for seq-ordered input and belongs with the cached-state
|
|
130
|
+
* follow-up. */
|
|
131
|
+
export interface RepositoryStateEventsAcc {
|
|
132
|
+
anchor?: number
|
|
133
|
+
retained: RepositoryEvent[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Bump when `RepositoryStateEventsAcc` or its fold changes. */
|
|
137
|
+
export const REPOSITORY_STATE_EVENTS_REDUCER_VERSION = 1
|
|
138
|
+
|
|
139
|
+
const keeps = (event: RepositoryEvent, anchor: number | undefined): boolean =>
|
|
140
|
+
STATE_TYPES.has(event.type) || (anchor !== undefined && event.seq >= anchor)
|
|
141
|
+
|
|
142
|
+
export const repositoryStateEventsReducer: IncrementalReducer<
|
|
143
|
+
RepositoryStateEventsAcc,
|
|
144
|
+
RepositoryEvent,
|
|
145
|
+
RepositoryEvent[]
|
|
146
|
+
> = defineReducer<RepositoryStateEventsAcc, RepositoryEvent, RepositoryEvent[]>({
|
|
147
|
+
version: REPOSITORY_STATE_EVENTS_REDUCER_VERSION,
|
|
148
|
+
initial: () => ({ retained: [] }),
|
|
149
|
+
fold(acc, events) {
|
|
150
|
+
for (const event of events) {
|
|
151
|
+
if (event.type === RUN_STARTED) acc.anchor = event.seq
|
|
152
|
+
acc.retained.push(event)
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
finish: (acc) => acc.retained.filter((event) => keeps(event, acc.anchor)),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
/** Carried state for the pruned journal view: the latest run-started seq and
|
|
159
|
+
* the subset the bounded read would return as of the events folded so far. Unlike
|
|
160
|
+
* `RepositoryStateEventsAcc` it prunes on every `dispatcher.run-started`
|
|
161
|
+
* (everything before the new anchor that is not durable leaves the tail), which
|
|
162
|
+
* is exact for seq-ordered input — the only input a real journal produces — and
|
|
163
|
+
* keeps the accumulator bounded, so it is the one a snapshot stores. */
|
|
164
|
+
export interface JournalViewAcc {
|
|
165
|
+
anchor?: number
|
|
166
|
+
retained: RepositoryEvent[]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Bump when `JournalViewAcc` or its fold changes. */
|
|
170
|
+
export const JOURNAL_VIEW_REDUCER_VERSION = 1
|
|
171
|
+
|
|
172
|
+
/** Fold seq-ordered events into a journal view in place. */
|
|
173
|
+
export function foldJournalView(acc: JournalViewAcc, events: readonly RepositoryEvent[]): void {
|
|
174
|
+
for (const event of events) {
|
|
175
|
+
if (event.type === RUN_STARTED) {
|
|
176
|
+
acc.anchor = event.seq
|
|
177
|
+
acc.retained = acc.retained.filter((held) => STATE_TYPES.has(held.type))
|
|
178
|
+
}
|
|
179
|
+
if (STATE_TYPES.has(event.type) || acc.anchor !== undefined) acc.retained.push(event)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const journalViewReducer: IncrementalReducer<
|
|
184
|
+
JournalViewAcc,
|
|
185
|
+
RepositoryEvent,
|
|
186
|
+
RepositoryEvent[]
|
|
187
|
+
> = defineReducer<JournalViewAcc, RepositoryEvent, RepositoryEvent[]>({
|
|
188
|
+
version: JOURNAL_VIEW_REDUCER_VERSION,
|
|
189
|
+
initial: () => ({ retained: [] }),
|
|
190
|
+
fold: foldJournalView,
|
|
191
|
+
finish: (acc) => [...acc.retained],
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/** The normative subset derivation every adapter implements and the contract
|
|
195
|
+
* tests use as the oracle: durable types across the whole journal, plus —
|
|
196
|
+
* only when the journal has one — every event from the latest
|
|
197
|
+
* `dispatcher.run-started` onward, ordered by seq. A journal with no
|
|
198
|
+
* run-started has **no tail at all** (durable types only); the anchor is
|
|
199
|
+
* never defaulted to 0, which would make a `seq >= anchor` clause select the
|
|
200
|
+
* whole journal and defeat the bound. */
|
|
201
|
+
export function projectRepositoryStateEvents(
|
|
202
|
+
events: readonly RepositoryEvent[],
|
|
203
|
+
): RepositoryEvent[] {
|
|
204
|
+
return repositoryStateEventsReducer.reduce(events)
|
|
205
|
+
}
|
|
@@ -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)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReducerSnapshot, SnapshotScope } from './snapshots'
|
|
1
2
|
import type { EventType } from '../events/payloads'
|
|
2
3
|
import type { EventWrite } from '../events/catalog'
|
|
3
4
|
import type { RepositoryEventType, RepositoryEventWrite } from '../events/repository'
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
Artifact,
|
|
14
15
|
ArtifactInput,
|
|
15
16
|
ArtifactMeta,
|
|
17
|
+
BuildDigest,
|
|
16
18
|
BuildRecord,
|
|
17
19
|
BuildStore,
|
|
18
20
|
NewBuildInput,
|
|
@@ -26,6 +28,13 @@ import type {
|
|
|
26
28
|
SubscribeOptions,
|
|
27
29
|
Unsubscribe,
|
|
28
30
|
} from './types'
|
|
31
|
+
import type {
|
|
32
|
+
TicketAsset,
|
|
33
|
+
TicketAssetInput,
|
|
34
|
+
TicketAssetLimits,
|
|
35
|
+
TicketAssetMeta,
|
|
36
|
+
TicketAssetSummary,
|
|
37
|
+
} from './ticket-assets'
|
|
29
38
|
import type {
|
|
30
39
|
StreamChunk,
|
|
31
40
|
StreamOutcome,
|
|
@@ -160,6 +169,41 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
160
169
|
getRepoEvents(repo: string, _sinceSeq?: number): Promise<RepositoryEvent[]> {
|
|
161
170
|
return Promise.reject(new SessionScopeError(scope, 'getRepoEvents', repo))
|
|
162
171
|
},
|
|
172
|
+
getRepoStateEvents(repo: string): Promise<RepositoryEvent[]> {
|
|
173
|
+
return Promise.reject(new SessionScopeError(scope, 'getRepoStateEvents', repo))
|
|
174
|
+
},
|
|
175
|
+
getRepoHighWater(repo: string): Promise<number> {
|
|
176
|
+
return Promise.reject(new SessionScopeError(scope, 'getRepoHighWater', repo))
|
|
177
|
+
},
|
|
178
|
+
getReducerSnapshot(
|
|
179
|
+
target: SnapshotScope,
|
|
180
|
+
_reducer: string,
|
|
181
|
+
_version: number,
|
|
182
|
+
): Promise<ReducerSnapshot | null> {
|
|
183
|
+
return Promise.reject(
|
|
184
|
+
new SessionScopeError(
|
|
185
|
+
scope,
|
|
186
|
+
'getReducerSnapshot',
|
|
187
|
+
target.kind === 'build' ? target.slug : target.repo,
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
},
|
|
191
|
+
putReducerSnapshot(
|
|
192
|
+
target: SnapshotScope,
|
|
193
|
+
_reducer: string,
|
|
194
|
+
_snapshot: ReducerSnapshot,
|
|
195
|
+
): Promise<boolean> {
|
|
196
|
+
return Promise.reject(
|
|
197
|
+
new SessionScopeError(
|
|
198
|
+
scope,
|
|
199
|
+
'putReducerSnapshot',
|
|
200
|
+
target.kind === 'build' ? target.slug : target.repo,
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
},
|
|
204
|
+
getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
|
|
205
|
+
return Promise.reject(new SessionScopeError(scope, 'getRepoBuildDigests', repo))
|
|
206
|
+
},
|
|
163
207
|
putRepoArtifact(repo: string, _artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
|
|
164
208
|
return Promise.reject(new SessionScopeError(scope, 'putRepoArtifact', repo))
|
|
165
209
|
},
|
|
@@ -173,6 +217,48 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
173
217
|
listRepoArtifacts(repo: string, _kind?: string): Promise<RepositoryArtifactMeta[]> {
|
|
174
218
|
return Promise.reject(new SessionScopeError(scope, 'listRepoArtifacts', repo))
|
|
175
219
|
},
|
|
220
|
+
ticketAssetLimits(repo: string): Promise<TicketAssetLimits> {
|
|
221
|
+
return Promise.reject(new SessionScopeError(scope, 'ticketAssetLimits', repo))
|
|
222
|
+
},
|
|
223
|
+
putTicketAsset(
|
|
224
|
+
repo: string,
|
|
225
|
+
_ticketId: string,
|
|
226
|
+
_asset: TicketAssetInput,
|
|
227
|
+
): Promise<TicketAssetMeta> {
|
|
228
|
+
return Promise.reject(new SessionScopeError(scope, 'putTicketAsset', repo))
|
|
229
|
+
},
|
|
230
|
+
getTicketAsset(
|
|
231
|
+
repo: string,
|
|
232
|
+
_ticketId: string,
|
|
233
|
+
_kind: string,
|
|
234
|
+
_name: string,
|
|
235
|
+
_rev?: number,
|
|
236
|
+
): Promise<TicketAsset | null> {
|
|
237
|
+
return Promise.reject(new SessionScopeError(scope, 'getTicketAsset', repo))
|
|
238
|
+
},
|
|
239
|
+
getPinnedTicketAsset(
|
|
240
|
+
slug: string,
|
|
241
|
+
_kind: string,
|
|
242
|
+
_name: string,
|
|
243
|
+
_rev?: number,
|
|
244
|
+
): Promise<TicketAsset | null> {
|
|
245
|
+
return Promise.reject(new SessionScopeError(scope, 'getPinnedTicketAsset', slug))
|
|
246
|
+
},
|
|
247
|
+
listTicketAssets(
|
|
248
|
+
repo: string,
|
|
249
|
+
_ticketId: string,
|
|
250
|
+
_opts?: { revisions?: boolean },
|
|
251
|
+
): Promise<TicketAssetSummary[]> {
|
|
252
|
+
return Promise.reject(new SessionScopeError(scope, 'listTicketAssets', repo))
|
|
253
|
+
},
|
|
254
|
+
removeTicketAsset(
|
|
255
|
+
repo: string,
|
|
256
|
+
_ticketId: string,
|
|
257
|
+
_kind: string,
|
|
258
|
+
_name: string,
|
|
259
|
+
): Promise<TicketAssetMeta | null> {
|
|
260
|
+
return Promise.reject(new SessionScopeError(scope, 'removeTicketAsset', repo))
|
|
261
|
+
},
|
|
176
262
|
claimRepoLease(repo: string, _holder: string, _ttlMs: number): Promise<boolean> {
|
|
177
263
|
return Promise.reject(new SessionScopeError(scope, 'claimRepoLease', repo))
|
|
178
264
|
},
|
|
@@ -199,6 +285,14 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
199
285
|
own('appendSessionEvent', id)
|
|
200
286
|
return store.appendSessionEvent(id, event)
|
|
201
287
|
},
|
|
288
|
+
async appendSessionEventIfCurrent<T extends SessionEventType>(
|
|
289
|
+
id: string,
|
|
290
|
+
expectedSeq: number,
|
|
291
|
+
event: SessionEventWrite<T>,
|
|
292
|
+
): Promise<SessionEventEnvelope<T> | null> {
|
|
293
|
+
own('appendSessionEventIfCurrent', id)
|
|
294
|
+
return store.appendSessionEventIfCurrent(id, expectedSeq, event)
|
|
295
|
+
},
|
|
202
296
|
async getSessionEvents(
|
|
203
297
|
id: string,
|
|
204
298
|
sinceSeq?: number,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* history; unknown facts cannot exist.
|
|
7
7
|
*/
|
|
8
8
|
import type { SessionEvent } from '../events/sessions'
|
|
9
|
+
import { defineReducer, type IncrementalReducer } from '../kernel/incremental'
|
|
9
10
|
|
|
10
11
|
export type SessionStatus = 'idle' | 'running' | 'suspended' | 'awaiting-approval' | 'archived'
|
|
11
12
|
export type SessionSuspensionCause = 'budget' | 'approval'
|
|
@@ -13,7 +14,7 @@ export type TurnOutcomeState = 'open' | 'suspended' | 'completed' | 'failed'
|
|
|
13
14
|
|
|
14
15
|
export type SessionTurnTrigger =
|
|
15
16
|
| { kind: 'message'; messageSeq: number }
|
|
16
|
-
| { kind: 'wake'; build
|
|
17
|
+
| { kind: 'wake'; build?: string; journal?: true; seq: number; type: string }
|
|
17
18
|
|
|
18
19
|
export interface SessionTurn {
|
|
19
20
|
turn: string
|
|
@@ -21,8 +22,11 @@ export interface SessionTurn {
|
|
|
21
22
|
startedSeq: number
|
|
22
23
|
trigger: SessionTurnTrigger
|
|
23
24
|
state: TurnOutcomeState
|
|
24
|
-
usage?: { inputTokens: number; outputTokens: number; turns?: number }
|
|
25
|
+
usage?: { inputTokens: number; outputTokens: number; steps: number; turns?: number }
|
|
25
26
|
error?: string
|
|
27
|
+
/** The typed failure class of a failed turn (provider-unavailable,
|
|
28
|
+
* exhausted, credentials, configuration, or internal). */
|
|
29
|
+
kind?: 'provider-unavailable' | 'exhausted' | 'credentials' | 'configuration' | 'internal'
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export interface SessionState {
|
|
@@ -43,6 +47,10 @@ export interface SessionState {
|
|
|
43
47
|
wakeGlobs: string[]
|
|
44
48
|
/** Per build, the highest build event seq a wake trigger has consumed. */
|
|
45
49
|
wakeCursors: Record<string, number>
|
|
50
|
+
/** The highest repository-journal seq a wake trigger has consumed (0 when
|
|
51
|
+
* no journal wake has been consumed). The journal has an independent seq
|
|
52
|
+
* space with no build slug, so it gets its own cursor. */
|
|
53
|
+
journalWakeCursor: number
|
|
46
54
|
/** Every turn in start order, with its stream id and outcome. */
|
|
47
55
|
turns: SessionTurn[]
|
|
48
56
|
}
|
|
@@ -55,16 +63,28 @@ interface TurnEntry extends SessionTurn {
|
|
|
55
63
|
const isTerminal = (state: SessionTurn['state']): boolean =>
|
|
56
64
|
state === 'completed' || state === 'failed'
|
|
57
65
|
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
/** The carried accumulator: the reduced fields plus the per-turn suspension
|
|
67
|
+
* cause the public state drops. Once `archived`, the fold ignores every later
|
|
68
|
+
* event, so an advance after archive stays frozen. */
|
|
69
|
+
export interface SessionAcc {
|
|
70
|
+
turns: TurnEntry[]
|
|
71
|
+
archived: boolean
|
|
72
|
+
pendingApproval: SessionState['pendingApproval']
|
|
73
|
+
wakeGlobs: string[]
|
|
74
|
+
wakeCursors: Record<string, number>
|
|
75
|
+
journalWakeCursor: number
|
|
76
|
+
}
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
/** Bump when `SessionAcc`'s shape or fold semantics change. */
|
|
79
|
+
export const SESSION_REDUCER_VERSION = 1
|
|
80
|
+
|
|
81
|
+
function foldSession(acc: SessionAcc, events: readonly SessionEvent[]): void {
|
|
82
|
+
const turns = acc.turns
|
|
83
|
+
let archived = acc.archived
|
|
84
|
+
let pendingApproval = acc.pendingApproval
|
|
85
|
+
let wakeGlobs = acc.wakeGlobs
|
|
86
|
+
const wakeCursors = acc.wakeCursors
|
|
87
|
+
let journalWakeCursor = acc.journalWakeCursor
|
|
68
88
|
|
|
69
89
|
for (const event of events) {
|
|
70
90
|
if (archived) break
|
|
@@ -85,8 +105,15 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
85
105
|
}
|
|
86
106
|
turns.push(entry)
|
|
87
107
|
if (event.payload.trigger.kind === 'wake') {
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
if (event.payload.trigger.journal === true) {
|
|
109
|
+
journalWakeCursor = Math.max(journalWakeCursor, event.payload.trigger.seq)
|
|
110
|
+
} else {
|
|
111
|
+
// The catalog's `.check()` guarantees `build` when `journal` is
|
|
112
|
+
// absent — a stored trigger can never be a journal wake without
|
|
113
|
+
// the flag or a build wake without the slug.
|
|
114
|
+
const build = event.payload.trigger.build!
|
|
115
|
+
wakeCursors[build] = Math.max(wakeCursors[build] ?? 0, event.payload.trigger.seq)
|
|
116
|
+
}
|
|
90
117
|
}
|
|
91
118
|
break
|
|
92
119
|
}
|
|
@@ -118,11 +145,13 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
118
145
|
pendingApproval.toolCallId === event.payload.toolCallId
|
|
119
146
|
) {
|
|
120
147
|
pendingApproval = undefined
|
|
121
|
-
// The answer
|
|
122
|
-
//
|
|
123
|
-
// the
|
|
124
|
-
|
|
125
|
-
|
|
148
|
+
// The answer only clears the pending approval — `turn.resumed` is
|
|
149
|
+
// the sole resume fact, recorded by the runner when it actually
|
|
150
|
+
// picks the turn up. Flipping the turn state here (the earlier
|
|
151
|
+
// behavior) made a session look `running` while no runner existed:
|
|
152
|
+
// an answered approval whose resuming invocation died would be
|
|
153
|
+
// unrecoverable. The status sequence after an answer is therefore
|
|
154
|
+
// awaiting-approval → suspended(approval) → running.
|
|
126
155
|
}
|
|
127
156
|
break
|
|
128
157
|
}
|
|
@@ -139,6 +168,7 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
139
168
|
if (entry && !isTerminal(entry.state)) {
|
|
140
169
|
entry.state = 'failed'
|
|
141
170
|
entry.error = event.payload.error
|
|
171
|
+
entry.kind = event.payload.kind
|
|
142
172
|
}
|
|
143
173
|
break
|
|
144
174
|
}
|
|
@@ -148,7 +178,15 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
148
178
|
}
|
|
149
179
|
}
|
|
150
180
|
|
|
151
|
-
|
|
181
|
+
acc.archived = archived
|
|
182
|
+
acc.pendingApproval = pendingApproval
|
|
183
|
+
acc.wakeGlobs = wakeGlobs
|
|
184
|
+
acc.journalWakeCursor = journalWakeCursor
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function finishSession(acc: SessionAcc): SessionState {
|
|
188
|
+
const { turns, archived, pendingApproval, wakeGlobs, wakeCursors, journalWakeCursor } = acc
|
|
189
|
+
const openEntry = turns.findLast((turn) => !isTerminal(turn.state))
|
|
152
190
|
// Status precedence: archived > awaiting-approval > suspended > running >
|
|
153
191
|
// idle. An unanswered approval outranks its turn's suspension; the answer
|
|
154
192
|
// that clears it hands status back to the turn.
|
|
@@ -180,6 +218,28 @@ export function reduceSession(events: SessionEvent[]): SessionState {
|
|
|
180
218
|
...(pendingApproval !== undefined ? { pendingApproval } : {}),
|
|
181
219
|
wakeGlobs,
|
|
182
220
|
wakeCursors,
|
|
221
|
+
journalWakeCursor,
|
|
183
222
|
turns: turns.map(({ suspensionCause: _suspensionCause, ...turn }) => turn),
|
|
184
223
|
}
|
|
185
224
|
}
|
|
225
|
+
|
|
226
|
+
export const sessionReducer: IncrementalReducer<SessionAcc, SessionEvent, SessionState> =
|
|
227
|
+
defineReducer({
|
|
228
|
+
version: SESSION_REDUCER_VERSION,
|
|
229
|
+
initial: (): SessionAcc => ({
|
|
230
|
+
turns: [],
|
|
231
|
+
archived: false,
|
|
232
|
+
pendingApproval: undefined,
|
|
233
|
+
wakeGlobs: [],
|
|
234
|
+
wakeCursors: {},
|
|
235
|
+
journalWakeCursor: 0,
|
|
236
|
+
}),
|
|
237
|
+
fold: foldSession,
|
|
238
|
+
finish: finishSession,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
/** Reduce a session's events into its derived state. `session.archived` is
|
|
242
|
+
* terminal: every fact after it is ignored — the archived state is frozen. */
|
|
243
|
+
export function reduceSession(events: SessionEvent[]): SessionState {
|
|
244
|
+
return sessionReducer.reduce(events)
|
|
245
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reducer snapshots: a cache beside the event log, never an authority.
|
|
3
|
+
*
|
|
4
|
+
* For one scope (a build log or a repository journal) and one registered
|
|
5
|
+
* reducer, the store may keep that reducer's serialized accumulator as of a
|
|
6
|
+
* cursor, so a process with no hot memory loads it, reads only the events
|
|
7
|
+
* newer than the cursor, and advances — instead of replaying the whole log.
|
|
8
|
+
*
|
|
9
|
+
* The log always regenerates a snapshot. Every consumer treats a missing,
|
|
10
|
+
* version-mismatched, ahead-of-log, malformed or unreadable snapshot as a miss
|
|
11
|
+
* and replays; deleting every snapshot changes no observable output.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
import type { IncrementalReducer } from '../kernel/incremental'
|
|
15
|
+
import type { BuildStore } from './types'
|
|
16
|
+
|
|
17
|
+
export type SnapshotScope = { kind: 'build'; slug: string } | { kind: 'repo'; repo: string }
|
|
18
|
+
|
|
19
|
+
export interface ReducerSnapshot {
|
|
20
|
+
/** The reducer version that wrote `state`; a reader on another version
|
|
21
|
+
* ignores the snapshot. */
|
|
22
|
+
version: number
|
|
23
|
+
/** The greatest event seq folded into `state`. */
|
|
24
|
+
cursor: number
|
|
25
|
+
/** The reducer's JSON accumulator as of `cursor`. */
|
|
26
|
+
state: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const reducerSnapshotWireSchema = z.object({
|
|
30
|
+
version: z.number().int().min(1),
|
|
31
|
+
cursor: z.number().int().min(0),
|
|
32
|
+
state: z.unknown(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** The registry name a snapshot is keyed by: a short identifier, safe in a URL
|
|
36
|
+
* path segment and a SQL key. */
|
|
37
|
+
export const SNAPSHOT_REDUCER_NAME = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/
|
|
38
|
+
|
|
39
|
+
export function snapshotScopeKey(scope: SnapshotScope): string {
|
|
40
|
+
return scope.kind === 'build' ? `build:${scope.slug}` : `repo:${scope.repo}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Whether a put replaces the stored row. The stored cursor never decreases:
|
|
44
|
+
* a higher cursor wins whatever the versions, and an upgraded reducer replaces
|
|
45
|
+
* an older shape at an unchanged cursor. */
|
|
46
|
+
export function snapshotSupersedes(
|
|
47
|
+
next: Pick<ReducerSnapshot, 'version' | 'cursor'>,
|
|
48
|
+
stored: Pick<ReducerSnapshot, 'version' | 'cursor'> | undefined,
|
|
49
|
+
): boolean {
|
|
50
|
+
if (stored === undefined) return true
|
|
51
|
+
if (next.cursor !== stored.cursor) return next.cursor > stored.cursor
|
|
52
|
+
return next.version > stored.version
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type SnapshotStore = Pick<BuildStore, 'getReducerSnapshot' | 'putReducerSnapshot'>
|
|
56
|
+
|
|
57
|
+
/** Read a snapshot, degrading every failure to a miss. */
|
|
58
|
+
export async function loadReducerSnapshot(
|
|
59
|
+
store: Partial<SnapshotStore>,
|
|
60
|
+
scope: SnapshotScope,
|
|
61
|
+
name: string,
|
|
62
|
+
reducer: Pick<IncrementalReducer<unknown, unknown, unknown>, 'version'>,
|
|
63
|
+
): Promise<ReducerSnapshot | null> {
|
|
64
|
+
if (store.getReducerSnapshot === undefined) return null
|
|
65
|
+
try {
|
|
66
|
+
const found = await store.getReducerSnapshot(scope, name, reducer.version)
|
|
67
|
+
if (found === null || found.version !== reducer.version) return null
|
|
68
|
+
if (!Number.isInteger(found.cursor) || found.cursor < 0) return null
|
|
69
|
+
return found
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Write a snapshot best-effort; a failing store never reaches the caller. */
|
|
76
|
+
export async function persistReducerSnapshot(
|
|
77
|
+
store: Partial<SnapshotStore>,
|
|
78
|
+
scope: SnapshotScope,
|
|
79
|
+
name: string,
|
|
80
|
+
snapshot: ReducerSnapshot,
|
|
81
|
+
): Promise<boolean> {
|
|
82
|
+
if (store.putReducerSnapshot === undefined) return false
|
|
83
|
+
try {
|
|
84
|
+
return await store.putReducerSnapshot(scope, name, snapshot)
|
|
85
|
+
} catch {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -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.
|