@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.1
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/AGENTS.md +30 -0
- package/README.md +16 -2
- package/dist/agents.js +11 -0
- package/dist/cli-adapter.js +8 -0
- package/dist/component.js +65 -0
- package/dist/config-schema.js +265 -0
- package/dist/execution/activity.js +78 -0
- package/dist/execution/content.js +184 -0
- package/dist/execution/json-sections.js +231 -0
- package/dist/execution/presentation.js +79 -0
- package/dist/execution/preview.js +13 -0
- package/dist/execution/process.js +125 -0
- package/dist/execution/projection.js +327 -0
- package/dist/execution/query.js +18 -0
- package/dist/execution/routes.js +189 -0
- package/dist/execution/schema.js +322 -0
- package/dist/execution/service.js +1993 -0
- package/dist/features.js +22 -0
- package/dist/http.js +65 -0
- package/dist/performance/analysis.js +330 -0
- package/dist/performance/catalog.js +391 -0
- package/dist/performance/dashboard.js +483 -0
- package/dist/performance/error.js +15 -0
- package/dist/performance/index.js +6 -0
- package/dist/performance/inflight.js +43 -0
- package/dist/performance/projection.js +138 -0
- package/dist/performance/prompt/base.txt +38 -0
- package/dist/performance/prompt/builder.js +21 -0
- package/dist/performance/routes/performance-route.js +439 -0
- package/dist/performance/schema.js +628 -0
- package/dist/performance/timeline.js +98 -0
- package/dist/performance/trace-detail.js +91 -0
- package/dist/presentation/service.js +270 -0
- package/dist/presentation/tools/workbench-view.js +65 -0
- package/dist/presentation/tools.js +10 -0
- package/dist/project/directories.js +363 -0
- package/dist/project/git-health.js +498 -0
- package/dist/project/ide.js +66 -0
- package/dist/project/migration.js +22 -0
- package/dist/project/routes/directories.js +89 -0
- package/dist/project/routes/git.js +30 -0
- package/dist/project/routes/task-defaults.js +42 -0
- package/dist/project/session-health.js +9 -0
- package/dist/project/startup.js +28 -0
- package/dist/project/task-defaults.js +68 -0
- package/dist/project/tools/session-control.js +664 -0
- package/dist/project/tools/session-control.txt +60 -0
- package/dist/project/tools/worktree-archive.js +76 -0
- package/dist/project/tools/worktree-enter.js +175 -0
- package/dist/project/tools/worktree-leave.js +119 -0
- package/dist/project/tools/worktree-list.js +52 -0
- package/dist/project/tools.js +26 -0
- package/dist/project/vcs-branch-watcher.js +76 -0
- package/dist/project/vcs.js +114 -0
- package/dist/project/worktree-janitor.js +140 -0
- package/dist/project/worktrees.js +98 -0
- package/dist/push/bridge.js +171 -0
- package/dist/push/index.js +4 -0
- package/dist/push/routes/push.js +224 -0
- package/dist/push/service.js +93 -0
- package/dist/push/store.js +121 -0
- package/dist/push/types.js +58 -0
- package/dist/review/git.js +226 -0
- package/dist/review/routes.js +45 -0
- package/dist/review/schema.js +32 -0
- package/dist/review/state.js +79 -0
- package/dist/stats/aggregator.js +252 -0
- package/dist/stats/canonical.js +89 -0
- package/dist/stats/cli/stats.js +302 -0
- package/dist/stats/engine.js +176 -0
- package/dist/stats/index.js +5 -0
- package/dist/stats/rollup.js +460 -0
- package/dist/stats/routes/stats.js +94 -0
- package/dist/stats/routes/usage.js +83 -0
- package/dist/stats/storage.js +86 -0
- package/dist/stats/types.js +267 -0
- package/dist/worker.js +6 -0
- package/package.json +232 -4
- package/src/agents.ts +15 -0
- package/src/cli-adapter.ts +11 -0
- package/src/component.ts +73 -0
- package/src/config-schema.ts +289 -0
- package/src/execution/activity.ts +83 -0
- package/src/execution/content.ts +189 -0
- package/src/execution/json-sections.ts +230 -0
- package/src/execution/presentation.ts +91 -0
- package/src/execution/preview.ts +12 -0
- package/src/execution/process.ts +118 -0
- package/src/execution/projection.ts +330 -0
- package/src/execution/query.ts +19 -0
- package/src/execution/routes.ts +324 -0
- package/src/execution/schema.ts +336 -0
- package/src/execution/service.ts +2149 -0
- package/src/features.ts +22 -0
- package/src/http.ts +75 -0
- package/src/performance/analysis.ts +356 -0
- package/src/performance/catalog.ts +902 -0
- package/src/performance/dashboard.ts +521 -0
- package/src/performance/error.ts +32 -0
- package/src/performance/index.ts +6 -0
- package/src/performance/inflight.ts +46 -0
- package/src/performance/projection.ts +143 -0
- package/src/performance/prompt/base.txt +38 -0
- package/src/performance/prompt/builder.ts +24 -0
- package/src/performance/routes/performance-route.ts +541 -0
- package/src/performance/schema.ts +695 -0
- package/src/performance/timeline.ts +100 -0
- package/src/performance/trace-detail.ts +97 -0
- package/src/presentation/service.ts +303 -0
- package/src/presentation/tools/workbench-view.ts +75 -0
- package/src/presentation/tools.ts +11 -0
- package/src/project/directories.ts +391 -0
- package/src/project/git-health.ts +577 -0
- package/src/project/ide.ts +80 -0
- package/src/project/migration.ts +24 -0
- package/src/project/routes/directories.ts +124 -0
- package/src/project/routes/git.ts +42 -0
- package/src/project/routes/task-defaults.ts +50 -0
- package/src/project/session-health.ts +10 -0
- package/src/project/startup.ts +28 -0
- package/src/project/task-defaults.ts +70 -0
- package/src/project/tools/session-control.ts +725 -0
- package/src/project/tools/session-control.txt +60 -0
- package/src/project/tools/worktree-archive.ts +92 -0
- package/src/project/tools/worktree-enter.ts +227 -0
- package/src/project/tools/worktree-leave.ts +140 -0
- package/src/project/tools/worktree-list.ts +68 -0
- package/src/project/tools.ts +29 -0
- package/src/project/vcs-branch-watcher.ts +81 -0
- package/src/project/vcs.ts +124 -0
- package/src/project/worktree-janitor.ts +153 -0
- package/src/project/worktrees.ts +110 -0
- package/src/push/bridge.ts +182 -0
- package/src/push/index.ts +4 -0
- package/src/push/routes/push.ts +267 -0
- package/src/push/service.ts +108 -0
- package/src/push/store.ts +120 -0
- package/src/push/types.ts +67 -0
- package/src/review/git.ts +221 -0
- package/src/review/routes.ts +65 -0
- package/src/review/schema.ts +34 -0
- package/src/review/state.ts +77 -0
- package/src/stats/aggregator.ts +277 -0
- package/src/stats/canonical.ts +93 -0
- package/src/stats/cli/stats.ts +348 -0
- package/src/stats/engine.ts +197 -0
- package/src/stats/index.ts +5 -0
- package/src/stats/rollup.ts +522 -0
- package/src/stats/routes/stats.ts +107 -0
- package/src/stats/routes/usage.ts +103 -0
- package/src/stats/storage.ts +78 -0
- package/src/stats/types.ts +351 -0
- package/src/worker.ts +7 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { Server } from "@ericsanchezok/synergy-server/server/server"
|
|
2
|
+
import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context"
|
|
3
|
+
import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context"
|
|
4
|
+
import { Scope } from "@ericsanchezok/synergy-harness/scope"
|
|
5
|
+
import { Session } from "@ericsanchezok/synergy-harness/session"
|
|
6
|
+
import { Storage } from "@ericsanchezok/synergy-harness/storage/storage"
|
|
7
|
+
import { applyProjectionPatch, projectionPatch } from "@ericsanchezok/synergy-util/projection-patch"
|
|
8
|
+
import { ExecutionService } from "./service"
|
|
9
|
+
import { ExecutionSchema } from "./schema"
|
|
10
|
+
import { z } from "zod"
|
|
11
|
+
|
|
12
|
+
export namespace ExecutionProjection {
|
|
13
|
+
const checkpoints = RuntimeContext.state(
|
|
14
|
+
() =>
|
|
15
|
+
new Map<
|
|
16
|
+
string,
|
|
17
|
+
{ sessionID: string; scopeID: string } & (
|
|
18
|
+
{ summary: ExecutionSchema.Summary } | { projection: ExecutionSchema.Projection }
|
|
19
|
+
)
|
|
20
|
+
>(),
|
|
21
|
+
)
|
|
22
|
+
export function checkpoint(sessionID: string, token: string) {
|
|
23
|
+
const value = checkpoints().get(token)
|
|
24
|
+
if (
|
|
25
|
+
!value ||
|
|
26
|
+
!("summary" in value) ||
|
|
27
|
+
value.sessionID !== sessionID ||
|
|
28
|
+
value.scopeID !== ScopeContext.current.scope.id
|
|
29
|
+
)
|
|
30
|
+
throw new Storage.NotFoundError({ message: "Execution checkpoint expired" })
|
|
31
|
+
return value.summary
|
|
32
|
+
}
|
|
33
|
+
export function projectionCheckpoint(sessionID: string, token: string) {
|
|
34
|
+
const value = checkpoints().get(token)
|
|
35
|
+
if (
|
|
36
|
+
!value ||
|
|
37
|
+
!("projection" in value) ||
|
|
38
|
+
value.sessionID !== sessionID ||
|
|
39
|
+
value.scopeID !== ScopeContext.current.scope.id
|
|
40
|
+
)
|
|
41
|
+
throw new Storage.NotFoundError({ message: "Execution checkpoint expired" })
|
|
42
|
+
return structuredClone(value.projection)
|
|
43
|
+
}
|
|
44
|
+
const Demand = z.enum(["root", "summary", "trajectory"])
|
|
45
|
+
async function openDemand(...[interest, send, signal]: Parameters<Server.ViewProjectionContribution["open"]>) {
|
|
46
|
+
const demands = z.array(Demand).min(1).parse(interest.demands)
|
|
47
|
+
const scope = await Scope.resolve({ scopeID: interest.scopeID })
|
|
48
|
+
const session = await Session.get(interest.sessionID)
|
|
49
|
+
if (session.scope.id !== scope.id)
|
|
50
|
+
throw new Storage.NotFoundError({ message: "Execution belongs to another Scope" })
|
|
51
|
+
const controller = new AbortController()
|
|
52
|
+
const abort = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal
|
|
53
|
+
abort.throwIfAborted()
|
|
54
|
+
const trajectory = demands.includes("trajectory")
|
|
55
|
+
const summary = trajectory || demands.includes("summary")
|
|
56
|
+
const releases: Array<() => void> = []
|
|
57
|
+
if (summary) releases.push(ExecutionService.subscribe(session.id, { trajectory }))
|
|
58
|
+
let disposed = false
|
|
59
|
+
let frameRevision = 0
|
|
60
|
+
let token: string | undefined
|
|
61
|
+
let previous: unknown
|
|
62
|
+
let current: ExecutionSchema.Projection = {
|
|
63
|
+
state: "root",
|
|
64
|
+
sessionID: session.id,
|
|
65
|
+
revision: 0,
|
|
66
|
+
previousRevision: 0,
|
|
67
|
+
contexts: [],
|
|
68
|
+
contextTotal: 0,
|
|
69
|
+
}
|
|
70
|
+
const store = checkpoints()
|
|
71
|
+
const dispose = () => {
|
|
72
|
+
if (disposed) return
|
|
73
|
+
disposed = true
|
|
74
|
+
abort.removeEventListener("abort", dispose)
|
|
75
|
+
controller.abort()
|
|
76
|
+
for (const release of releases.splice(0)) release()
|
|
77
|
+
if (token) store.delete(token)
|
|
78
|
+
previous = undefined
|
|
79
|
+
}
|
|
80
|
+
abort.addEventListener("abort", dispose, { once: true })
|
|
81
|
+
const publish = (value: ExecutionSchema.Projection) => {
|
|
82
|
+
if (disposed) return
|
|
83
|
+
value = ExecutionSchema.Projection.parse(value)
|
|
84
|
+
current = value
|
|
85
|
+
const patches = projectionPatch(previous, value)
|
|
86
|
+
if (!patches.length) return
|
|
87
|
+
const properties = {
|
|
88
|
+
kind: "execution",
|
|
89
|
+
sessionID: session.id,
|
|
90
|
+
generation: interest.generation,
|
|
91
|
+
revision: frameRevision + 1,
|
|
92
|
+
previousRevision: frameRevision,
|
|
93
|
+
patches,
|
|
94
|
+
snapshot: undefined as string | undefined,
|
|
95
|
+
}
|
|
96
|
+
const frame = { scopeID: scope.id, payload: { type: "server.view.patch", properties } }
|
|
97
|
+
if (Buffer.byteLength(JSON.stringify(frame)) > 64 * 1024) {
|
|
98
|
+
if (token) store.delete(token)
|
|
99
|
+
token = crypto.randomUUID()
|
|
100
|
+
store.set(token, { scopeID: scope.id, sessionID: session.id, projection: structuredClone(value) })
|
|
101
|
+
properties.patches = []
|
|
102
|
+
properties.snapshot = token
|
|
103
|
+
}
|
|
104
|
+
previous = structuredClone(value)
|
|
105
|
+
frameRevision++
|
|
106
|
+
send(frame)
|
|
107
|
+
}
|
|
108
|
+
const scoped = <T>(fn: () => Promise<T>) => ScopeContext.provide({ scope, fn })
|
|
109
|
+
try {
|
|
110
|
+
const release = await scoped(() =>
|
|
111
|
+
ExecutionService.observeRoot(
|
|
112
|
+
session.id,
|
|
113
|
+
(root) => {
|
|
114
|
+
const selected = root.contexts.filter((context) => !interest.runID || context.runID === interest.runID)
|
|
115
|
+
const visible = new Set(interest.nodeIDs)
|
|
116
|
+
const contexts = selected.filter((context, index) => index < 30 || visible.has(context.nodeID))
|
|
117
|
+
publish({
|
|
118
|
+
...current,
|
|
119
|
+
revision: root.revision,
|
|
120
|
+
previousRevision: current.revision,
|
|
121
|
+
contexts,
|
|
122
|
+
contextTotal: selected.length,
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
abort,
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
if (disposed) release()
|
|
129
|
+
else releases.push(release)
|
|
130
|
+
if (summary && !disposed) {
|
|
131
|
+
const releaseProgress = await scoped(() =>
|
|
132
|
+
ExecutionService.observePreparation(session.id, (progress) => {
|
|
133
|
+
if (current.state === "preparing") publish({ ...current, progress })
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
if (disposed) {
|
|
137
|
+
releaseProgress()
|
|
138
|
+
return { project() {}, dispose }
|
|
139
|
+
}
|
|
140
|
+
releases.push(releaseProgress)
|
|
141
|
+
publish({ ...current, state: "preparing", progress: { stage: "metadata", prepared: 0 } })
|
|
142
|
+
void scoped(async () => {
|
|
143
|
+
const value = await ExecutionService.summary(session.id, undefined, abort)
|
|
144
|
+
const rounds = interest.runID ? [await ExecutionService.summary(session.id, interest.runID, abort)] : []
|
|
145
|
+
if (current.state === "ready" && current.summary.revision > value.revision) return
|
|
146
|
+
publish({
|
|
147
|
+
...current,
|
|
148
|
+
state: "ready",
|
|
149
|
+
summary: value,
|
|
150
|
+
detailPreviousRevision: value.revision,
|
|
151
|
+
roundSummaries: rounds,
|
|
152
|
+
upserts: [],
|
|
153
|
+
processUpserts: [],
|
|
154
|
+
processRemoved: [],
|
|
155
|
+
removed: [],
|
|
156
|
+
})
|
|
157
|
+
await ExecutionService.verifyJournal(session.id, abort)
|
|
158
|
+
abort.throwIfAborted()
|
|
159
|
+
const verified = await ExecutionService.summary(session.id, undefined, abort)
|
|
160
|
+
const verifiedRounds = interest.runID
|
|
161
|
+
? [await ExecutionService.summary(session.id, interest.runID, abort)]
|
|
162
|
+
: []
|
|
163
|
+
if (current.state === "ready" && current.summary.revision > verified.revision) return
|
|
164
|
+
publish({
|
|
165
|
+
...current,
|
|
166
|
+
state: "ready",
|
|
167
|
+
summary: verified,
|
|
168
|
+
roundSummaries: verifiedRounds,
|
|
169
|
+
detailPreviousRevision: current.state === "ready" ? current.summary.revision : verified.revision,
|
|
170
|
+
upserts: [],
|
|
171
|
+
processUpserts: [],
|
|
172
|
+
processRemoved: [],
|
|
173
|
+
removed: [],
|
|
174
|
+
})
|
|
175
|
+
}).catch((error) => {
|
|
176
|
+
if (!disposed)
|
|
177
|
+
publish({
|
|
178
|
+
...current,
|
|
179
|
+
state: "failed",
|
|
180
|
+
error: {
|
|
181
|
+
kind: error instanceof Error ? error.name : "Error",
|
|
182
|
+
message: "Unable to prepare execution summary",
|
|
183
|
+
retryable: true,
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
dispose()
|
|
190
|
+
throw error
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
project(event: Parameters<Awaited<ReturnType<Server.ViewProjectionContribution["open"]>>["project"]>[0]) {
|
|
194
|
+
if (event.scopeID !== scope.id) return
|
|
195
|
+
if (
|
|
196
|
+
event.payload.type === "session.deleted" &&
|
|
197
|
+
(event.payload.properties.info as Session.Info)?.id === session.id
|
|
198
|
+
) {
|
|
199
|
+
dispose()
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
if (!summary || event.payload.type !== "execution.updated" || event.payload.properties.sessionID !== session.id)
|
|
203
|
+
return
|
|
204
|
+
const value = ExecutionSchema.Updated.properties.parse(event.payload.properties)
|
|
205
|
+
if (current.state === "ready" && value.summary.revision <= current.summary.revision) return
|
|
206
|
+
const visible = interest.nodeIDs && new Set(interest.nodeIDs)
|
|
207
|
+
publish({
|
|
208
|
+
...current,
|
|
209
|
+
state: "ready",
|
|
210
|
+
summary: value.summary,
|
|
211
|
+
detailPreviousRevision: value.previousRevision ?? value.summary.revision,
|
|
212
|
+
roundSummaries: value.roundSummaries.filter((round) => !interest.runID || round.runID === interest.runID),
|
|
213
|
+
upserts: trajectory ? value.upserts.filter((node) => !visible || visible.has(node.id)) : [],
|
|
214
|
+
processUpserts: trajectory ? (value.processUpserts ?? []) : [],
|
|
215
|
+
processRemoved: trajectory ? (value.processRemoved ?? []) : [],
|
|
216
|
+
removed: trajectory ? value.removed : [],
|
|
217
|
+
})
|
|
218
|
+
},
|
|
219
|
+
dispose,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export function baseline(summary: ExecutionSchema.Summary) {
|
|
223
|
+
return {
|
|
224
|
+
sessionID: summary.sessionID,
|
|
225
|
+
revision: summary.revision,
|
|
226
|
+
summary,
|
|
227
|
+
roundSummaries: [],
|
|
228
|
+
contextUpserts: [],
|
|
229
|
+
upserts: [],
|
|
230
|
+
processUpserts: [],
|
|
231
|
+
processRemoved: [],
|
|
232
|
+
removed: [],
|
|
233
|
+
previousRevision: summary.revision,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export function contribution(): Server.ViewProjectionContribution {
|
|
237
|
+
return {
|
|
238
|
+
kind: "execution",
|
|
239
|
+
eventType: "execution.updated",
|
|
240
|
+
legacy: ExecutionService.legacyClient,
|
|
241
|
+
async open(interest, send, signal) {
|
|
242
|
+
if (interest.demands) return openDemand(interest, send, signal)
|
|
243
|
+
signal?.throwIfAborted()
|
|
244
|
+
const scope = await Scope.resolve({ scopeID: interest.scopeID })
|
|
245
|
+
const session = await Session.get(interest.sessionID)
|
|
246
|
+
if (session.scope.id !== scope.id)
|
|
247
|
+
throw new Storage.NotFoundError({ message: "Execution belongs to another Scope" })
|
|
248
|
+
signal?.throwIfAborted()
|
|
249
|
+
const release = ExecutionService.subscribe(session.id)
|
|
250
|
+
let previous: unknown,
|
|
251
|
+
revision = 0,
|
|
252
|
+
token: string | undefined,
|
|
253
|
+
disposed = false
|
|
254
|
+
const store = checkpoints()
|
|
255
|
+
const dispose = () => {
|
|
256
|
+
if (disposed) return
|
|
257
|
+
disposed = true
|
|
258
|
+
signal?.removeEventListener("abort", dispose)
|
|
259
|
+
release()
|
|
260
|
+
if (token) store.delete(token)
|
|
261
|
+
previous = undefined
|
|
262
|
+
}
|
|
263
|
+
signal?.addEventListener("abort", dispose, { once: true })
|
|
264
|
+
const publish = (value: Record<string, unknown>) => {
|
|
265
|
+
if (disposed) return
|
|
266
|
+
const patches = projectionPatch(previous, value)
|
|
267
|
+
if (!patches.length) return
|
|
268
|
+
const properties = {
|
|
269
|
+
kind: "execution",
|
|
270
|
+
sessionID: session.id,
|
|
271
|
+
generation: interest.generation,
|
|
272
|
+
revision: revision + 1,
|
|
273
|
+
previousRevision: revision,
|
|
274
|
+
patches,
|
|
275
|
+
snapshot: undefined as string | undefined,
|
|
276
|
+
}
|
|
277
|
+
const frame = { scopeID: scope.id, payload: { type: "server.view.patch", properties } }
|
|
278
|
+
if (Buffer.byteLength(JSON.stringify(frame)) > 64 * 1024) {
|
|
279
|
+
const summary = value.summary as ExecutionSchema.Summary
|
|
280
|
+
if (token) store.delete(token)
|
|
281
|
+
token = crypto.randomUUID()
|
|
282
|
+
const snapshot = structuredClone(summary)
|
|
283
|
+
store.set(token, { scopeID: scope.id, sessionID: session.id, summary: snapshot })
|
|
284
|
+
previous = baseline(snapshot)
|
|
285
|
+
properties.patches = []
|
|
286
|
+
properties.snapshot = token
|
|
287
|
+
} else previous = applyProjectionPatch(previous, patches)
|
|
288
|
+
revision++
|
|
289
|
+
send(frame)
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const summary = await ScopeContext.provide({
|
|
293
|
+
scope,
|
|
294
|
+
fn: () => ExecutionService.summary(session.id, undefined, signal),
|
|
295
|
+
})
|
|
296
|
+
publish(baseline(summary))
|
|
297
|
+
} catch (error) {
|
|
298
|
+
dispose()
|
|
299
|
+
throw error
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
project(event) {
|
|
303
|
+
if (
|
|
304
|
+
event.scopeID !== scope.id ||
|
|
305
|
+
event.payload.type !== "execution.updated" ||
|
|
306
|
+
event.payload.properties.sessionID !== session.id
|
|
307
|
+
)
|
|
308
|
+
return
|
|
309
|
+
const value = event.payload.properties
|
|
310
|
+
const priorRevision = (previous as { revision?: number } | undefined)?.revision ?? -1
|
|
311
|
+
if (Number(value.revision) <= priorRevision) return
|
|
312
|
+
const visible = interest.nodeIDs && new Set(interest.nodeIDs)
|
|
313
|
+
const selected = {
|
|
314
|
+
...value,
|
|
315
|
+
roundSummaries: (value.roundSummaries as ExecutionSchema.Summary[]).filter(
|
|
316
|
+
(summary) => !interest.runID || summary.runID === interest.runID,
|
|
317
|
+
),
|
|
318
|
+
contextUpserts: ((value.contextUpserts ?? []) as ExecutionSchema.ContextSnapshot[]).filter(
|
|
319
|
+
(context) => !visible || visible.has(context.nodeID),
|
|
320
|
+
),
|
|
321
|
+
upserts: (value.upserts as Array<{ id: string }>).filter((node) => !visible || visible.has(node.id)),
|
|
322
|
+
}
|
|
323
|
+
publish(selected)
|
|
324
|
+
},
|
|
325
|
+
dispose,
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
export class ExecutionQueryError extends RangeError {}
|
|
4
|
+
|
|
5
|
+
export function queryValue<T extends z.ZodType>(schema: T, value: unknown): z.infer<T> {
|
|
6
|
+
const parsed = schema.safeParse(value)
|
|
7
|
+
if (!parsed.success) throw new ExecutionQueryError("Invalid execution query")
|
|
8
|
+
return parsed.data
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function queryCursor<T extends z.ZodType>(schema: T, cursor: string): z.infer<T> {
|
|
12
|
+
let value: unknown
|
|
13
|
+
try {
|
|
14
|
+
value = JSON.parse(Buffer.from(cursor, "base64url").toString())
|
|
15
|
+
} catch {
|
|
16
|
+
throw new ExecutionQueryError("Invalid execution cursor")
|
|
17
|
+
}
|
|
18
|
+
return queryValue(schema, value)
|
|
19
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { RolloutEvidence } from "@ericsanchezok/synergy-harness/rollout"
|
|
2
|
+
import { Log } from "@ericsanchezok/synergy-harness/util/log"
|
|
3
|
+
import { ExecutionQueryError } from "./query"
|
|
4
|
+
import { Hono } from "hono"
|
|
5
|
+
import { describeRoute, resolver, validator } from "hono-openapi"
|
|
6
|
+
import { z } from "zod"
|
|
7
|
+
import { Identifier } from "@ericsanchezok/synergy-harness/id/id"
|
|
8
|
+
import { Storage } from "@ericsanchezok/synergy-harness/storage/storage"
|
|
9
|
+
import { ExecutionSchema } from "./schema"
|
|
10
|
+
import { ExecutionService } from "./service"
|
|
11
|
+
import { ExecutionProjection } from "./projection"
|
|
12
|
+
import { ExecutionContent } from "./content"
|
|
13
|
+
import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context"
|
|
14
|
+
import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context"
|
|
15
|
+
import { Scope } from "@ericsanchezok/synergy-harness/scope"
|
|
16
|
+
|
|
17
|
+
const Params = z.object({ sessionID: Identifier.schema("session") })
|
|
18
|
+
const ContextParams = Params.extend({ callID: z.string().min(1).max(4096) })
|
|
19
|
+
const NodeParams = Params.extend({ nodeID: z.string().min(1).max(4096) })
|
|
20
|
+
const ContentQuery = ExecutionContent.Query.extend({
|
|
21
|
+
offset: z.coerce.number().int().nonnegative().safe().default(0),
|
|
22
|
+
limit: z.coerce.number().int().min(1).max(65_536).default(65_536),
|
|
23
|
+
})
|
|
24
|
+
async function download(source: ExecutionContent.Source, field: string, signal: AbortSignal) {
|
|
25
|
+
const runtime = RuntimeContext.current()
|
|
26
|
+
const scope = ScopeContext.current.scope
|
|
27
|
+
const workspace = ScopeContext.current.workspace
|
|
28
|
+
const iterator = source.stream()[Symbol.asyncIterator]()
|
|
29
|
+
const release = Scope.registerArchiveGuard((id) => {
|
|
30
|
+
if (id === scope.id) throw new Error("Execution content is being read")
|
|
31
|
+
})
|
|
32
|
+
const bind = <T>(fn: () => T | Promise<T>) => runtime.bind(() => ScopeContext.provide({ scope, workspace, fn }))
|
|
33
|
+
let closed = false
|
|
34
|
+
let controller: ReadableStreamDefaultController<Uint8Array> | undefined
|
|
35
|
+
const finish = () => {
|
|
36
|
+
if (closed) return
|
|
37
|
+
closed = true
|
|
38
|
+
release()
|
|
39
|
+
signal.removeEventListener("abort", abort)
|
|
40
|
+
}
|
|
41
|
+
const abort = () => {
|
|
42
|
+
if (closed) return
|
|
43
|
+
controller?.error(signal.reason)
|
|
44
|
+
finish()
|
|
45
|
+
void bind(() => iterator.return?.())().catch(() => {})
|
|
46
|
+
}
|
|
47
|
+
signal.addEventListener("abort", abort, { once: true })
|
|
48
|
+
const body = new ReadableStream<Uint8Array>({
|
|
49
|
+
start(value) {
|
|
50
|
+
controller = value
|
|
51
|
+
if (signal.aborted) abort()
|
|
52
|
+
},
|
|
53
|
+
pull: bind(async () => {
|
|
54
|
+
if (closed) return
|
|
55
|
+
try {
|
|
56
|
+
const item = await iterator.next()
|
|
57
|
+
if (closed) return
|
|
58
|
+
if (item.done) {
|
|
59
|
+
finish()
|
|
60
|
+
controller!.close()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
controller!.enqueue(item.value)
|
|
64
|
+
} catch (error) {
|
|
65
|
+
finish()
|
|
66
|
+
controller!.error(error)
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
cancel: bind(async () => {
|
|
70
|
+
finish()
|
|
71
|
+
await iterator.return?.()
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
return new Response(body, {
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "application/octet-stream",
|
|
77
|
+
"Content-Disposition": `attachment; filename="execution-${field.replace(/[^a-zA-Z0-9_-]/g, "_")}.${source.mediaType.includes("json") ? "json" : "txt"}"`,
|
|
78
|
+
"Content-Length": String(source.bytes),
|
|
79
|
+
"Cache-Control": "no-store",
|
|
80
|
+
"X-Execution-Content-Version": source.contentVersion,
|
|
81
|
+
"X-Execution-Content-Status": source.status,
|
|
82
|
+
...(source.sha256 ? { "X-Execution-Content-Sha256": source.sha256 } : {}),
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
const metadata = (operationId: string, summary: string, schema: z.ZodType) =>
|
|
87
|
+
describeRoute({
|
|
88
|
+
operationId,
|
|
89
|
+
summary,
|
|
90
|
+
responses: {
|
|
91
|
+
200: { description: summary, content: { "application/json": { schema: resolver(schema) } } },
|
|
92
|
+
400: { description: "Invalid execution query" },
|
|
93
|
+
500: { description: "Execution computation failed" },
|
|
94
|
+
404: { description: "Execution evidence was not found in the selected Scope and session" },
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
async function result<T>(body: () => Promise<T>) {
|
|
98
|
+
try {
|
|
99
|
+
return { status: 200 as const, value: await body() }
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof Storage.NotFoundError) return { status: 404 as const, value: { message: error.message } }
|
|
102
|
+
if (error instanceof ExecutionQueryError || error instanceof RolloutEvidence.QueryError)
|
|
103
|
+
return { status: 400 as const, value: { message: "Invalid execution query" } }
|
|
104
|
+
if (!(error instanceof Error && error.name === "AbortError"))
|
|
105
|
+
Log.create({ service: "execution" }).error("execution read failed", {
|
|
106
|
+
kind: error instanceof Error ? error.name : "unknown",
|
|
107
|
+
issues:
|
|
108
|
+
error instanceof z.ZodError
|
|
109
|
+
? error.issues.map((issue) => ({ code: issue.code, path: issue.path }))
|
|
110
|
+
: undefined,
|
|
111
|
+
})
|
|
112
|
+
throw error
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export const ExecutionRoute = () =>
|
|
116
|
+
new Hono()
|
|
117
|
+
.get(
|
|
118
|
+
"/:sessionID/execution/projection",
|
|
119
|
+
metadata("session.executionProjection", "Read a captured execution projection", ExecutionSchema.Projection),
|
|
120
|
+
validator("param", Params),
|
|
121
|
+
validator("query", z.object({ token: z.string().uuid() })),
|
|
122
|
+
async (c) => {
|
|
123
|
+
const response = await result(() =>
|
|
124
|
+
Promise.resolve(
|
|
125
|
+
ExecutionProjection.projectionCheckpoint(c.req.valid("param").sessionID, c.req.valid("query").token),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
return c.json(response.value, response.status)
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
.get(
|
|
132
|
+
"/:sessionID/execution/summary",
|
|
133
|
+
metadata("session.executionSummary", "Read a compact session execution summary", ExecutionSchema.Summary),
|
|
134
|
+
validator("param", Params),
|
|
135
|
+
validator(
|
|
136
|
+
"query",
|
|
137
|
+
ExecutionSchema.Query.pick({ runID: true }).extend({ projection: z.string().uuid().optional() }),
|
|
138
|
+
),
|
|
139
|
+
async (c) => {
|
|
140
|
+
const response = await result(() =>
|
|
141
|
+
c.req.valid("query").projection
|
|
142
|
+
? Promise.resolve(
|
|
143
|
+
ExecutionProjection.checkpoint(c.req.valid("param").sessionID, c.req.valid("query").projection!),
|
|
144
|
+
)
|
|
145
|
+
: ExecutionService.summary(c.req.valid("param").sessionID, c.req.valid("query").runID, c.req.raw.signal),
|
|
146
|
+
)
|
|
147
|
+
return c.json(response.value, response.status)
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
.get(
|
|
151
|
+
"/:sessionID/execution/context/history",
|
|
152
|
+
metadata("session.executionContextHistory", "Read context request history", ExecutionSchema.ContextHistory),
|
|
153
|
+
validator("param", Params),
|
|
154
|
+
validator("query", ExecutionSchema.ContextQuery),
|
|
155
|
+
async (c) => {
|
|
156
|
+
const response = await result(() =>
|
|
157
|
+
ExecutionService.contextHistory(c.req.valid("param").sessionID, c.req.valid("query"), c.req.raw.signal),
|
|
158
|
+
)
|
|
159
|
+
return c.json(response.value, response.status)
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
.get(
|
|
163
|
+
"/:sessionID/execution/context/snapshots/:callID",
|
|
164
|
+
metadata(
|
|
165
|
+
"session.executionContextSnapshot",
|
|
166
|
+
"Read one context request snapshot",
|
|
167
|
+
ExecutionSchema.ContextSnapshot,
|
|
168
|
+
),
|
|
169
|
+
validator("param", ContextParams),
|
|
170
|
+
validator("query", ExecutionSchema.ContextQuery.pick({ runID: true })),
|
|
171
|
+
async (c) => {
|
|
172
|
+
const { sessionID, callID } = c.req.valid("param")
|
|
173
|
+
const response = await result(() =>
|
|
174
|
+
ExecutionService.contextSnapshot(sessionID, callID, c.req.valid("query").runID, c.req.raw.signal),
|
|
175
|
+
)
|
|
176
|
+
return c.json(response.value, response.status)
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
.get(
|
|
180
|
+
"/:sessionID/execution/context/snapshots/:callID/items",
|
|
181
|
+
metadata("session.executionContextItems", "Read a bounded context source index", ExecutionSchema.ContextItems),
|
|
182
|
+
validator("param", ContextParams),
|
|
183
|
+
validator("query", ExecutionSchema.ContextItemsQuery),
|
|
184
|
+
async (c) => {
|
|
185
|
+
const { sessionID, callID } = c.req.valid("param")
|
|
186
|
+
const response = await result(() =>
|
|
187
|
+
ExecutionService.contextItems(sessionID, callID, c.req.valid("query"), c.req.raw.signal),
|
|
188
|
+
)
|
|
189
|
+
return c.json(response.value, response.status)
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
.get(
|
|
193
|
+
"/:sessionID/execution/trajectory",
|
|
194
|
+
metadata("session.executionTrajectory", "Page through session execution records", ExecutionSchema.Page),
|
|
195
|
+
validator("param", Params),
|
|
196
|
+
validator("query", ExecutionSchema.Query),
|
|
197
|
+
async (c) => {
|
|
198
|
+
const response = await result(() =>
|
|
199
|
+
ExecutionService.trajectory(c.req.valid("param").sessionID, c.req.valid("query"), c.req.raw.signal),
|
|
200
|
+
)
|
|
201
|
+
return c.json(response.value, response.status)
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
.get(
|
|
205
|
+
"/:sessionID/execution/nodes/:nodeID",
|
|
206
|
+
metadata("session.executionNode", "Inspect one session execution record", ExecutionSchema.Detail),
|
|
207
|
+
validator("param", NodeParams),
|
|
208
|
+
validator("query", ExecutionSchema.Query.pick({ runID: true })),
|
|
209
|
+
async (c) => {
|
|
210
|
+
const { sessionID, nodeID } = c.req.valid("param")
|
|
211
|
+
const response = await result(() =>
|
|
212
|
+
ExecutionService.node(sessionID, nodeID, c.req.valid("query").runID, c.req.raw.signal),
|
|
213
|
+
)
|
|
214
|
+
return c.json(response.value, response.status)
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
.get(
|
|
218
|
+
"/:sessionID/execution/nodes/:nodeID/content",
|
|
219
|
+
metadata("session.executionContent", "Read a bounded execution content page", ExecutionSchema.Content),
|
|
220
|
+
validator("param", NodeParams),
|
|
221
|
+
validator("query", ContentQuery),
|
|
222
|
+
async (c) => {
|
|
223
|
+
const { sessionID, nodeID } = c.req.valid("param")
|
|
224
|
+
const { field, offset, limit, runID, version } = c.req.valid("query")
|
|
225
|
+
const response = await result(() =>
|
|
226
|
+
ExecutionService.content(sessionID, nodeID, field, offset, limit, runID, version, c.req.raw.signal),
|
|
227
|
+
)
|
|
228
|
+
return c.json(response.value, response.status)
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
.get(
|
|
232
|
+
"/:sessionID/execution/nodes/:nodeID/content/sections",
|
|
233
|
+
metadata(
|
|
234
|
+
"session.executionContentSections",
|
|
235
|
+
"Read structured execution content sections",
|
|
236
|
+
ExecutionContent.Sections,
|
|
237
|
+
),
|
|
238
|
+
validator("param", NodeParams),
|
|
239
|
+
validator("query", ExecutionContent.PageQuery),
|
|
240
|
+
async (c) => {
|
|
241
|
+
const { sessionID, nodeID } = c.req.valid("param")
|
|
242
|
+
const input = c.req.valid("query")
|
|
243
|
+
const response = await result(async () =>
|
|
244
|
+
ExecutionContent.sections(
|
|
245
|
+
await ExecutionService.contentSource(
|
|
246
|
+
sessionID,
|
|
247
|
+
nodeID,
|
|
248
|
+
input.field,
|
|
249
|
+
input.runID,
|
|
250
|
+
input.version,
|
|
251
|
+
c.req.raw.signal,
|
|
252
|
+
),
|
|
253
|
+
input,
|
|
254
|
+
c.req.raw.signal,
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
return c.json(response.value, response.status)
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
.get(
|
|
261
|
+
"/:sessionID/execution/nodes/:nodeID/content/search",
|
|
262
|
+
metadata(
|
|
263
|
+
"session.executionContentSearch",
|
|
264
|
+
"Search an entire version of execution content",
|
|
265
|
+
ExecutionContent.Search,
|
|
266
|
+
),
|
|
267
|
+
validator("param", NodeParams),
|
|
268
|
+
validator("query", ExecutionContent.SearchQuery),
|
|
269
|
+
async (c) => {
|
|
270
|
+
const { sessionID, nodeID } = c.req.valid("param")
|
|
271
|
+
const input = c.req.valid("query")
|
|
272
|
+
const response = await result(async () =>
|
|
273
|
+
ExecutionContent.search(
|
|
274
|
+
await ExecutionService.contentSource(
|
|
275
|
+
sessionID,
|
|
276
|
+
nodeID,
|
|
277
|
+
input.field,
|
|
278
|
+
input.runID,
|
|
279
|
+
input.version,
|
|
280
|
+
c.req.raw.signal,
|
|
281
|
+
),
|
|
282
|
+
input,
|
|
283
|
+
c.req.raw.signal,
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
return c.json(response.value, response.status)
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
.get(
|
|
290
|
+
"/:sessionID/execution/nodes/:nodeID/content/download",
|
|
291
|
+
describeRoute({
|
|
292
|
+
operationId: "session.executionContentDownload",
|
|
293
|
+
summary: "Download complete execution content at a fixed version",
|
|
294
|
+
responses: {
|
|
295
|
+
200: {
|
|
296
|
+
description: "Verified execution content",
|
|
297
|
+
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } },
|
|
298
|
+
},
|
|
299
|
+
400: { description: "Invalid execution content query or version" },
|
|
300
|
+
404: { description: "Execution content was not found" },
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
validator("param", NodeParams),
|
|
304
|
+
validator("query", ExecutionContent.Query),
|
|
305
|
+
async (c) => {
|
|
306
|
+
const { sessionID, nodeID } = c.req.valid("param")
|
|
307
|
+
const input = c.req.valid("query")
|
|
308
|
+
const response = await result(async () =>
|
|
309
|
+
download(
|
|
310
|
+
await ExecutionService.contentSource(
|
|
311
|
+
sessionID,
|
|
312
|
+
nodeID,
|
|
313
|
+
input.field,
|
|
314
|
+
input.runID,
|
|
315
|
+
input.version,
|
|
316
|
+
c.req.raw.signal,
|
|
317
|
+
),
|
|
318
|
+
input.field,
|
|
319
|
+
c.req.raw.signal,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
return response.status === 200 ? response.value : c.json(response.value, response.status)
|
|
323
|
+
},
|
|
324
|
+
)
|