@synth-deploy/server 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/agent/envoy-client.d.ts +65 -15
  2. package/dist/agent/envoy-client.d.ts.map +1 -1
  3. package/dist/agent/envoy-client.js +58 -8
  4. package/dist/agent/envoy-client.js.map +1 -1
  5. package/dist/agent/stale-deployment-detector.js +1 -1
  6. package/dist/agent/stale-deployment-detector.js.map +1 -1
  7. package/dist/agent/synth-agent.d.ts +7 -5
  8. package/dist/agent/synth-agent.d.ts.map +1 -1
  9. package/dist/agent/synth-agent.js +59 -50
  10. package/dist/agent/synth-agent.js.map +1 -1
  11. package/dist/alert-webhooks/alert-parsers.d.ts +21 -0
  12. package/dist/alert-webhooks/alert-parsers.d.ts.map +1 -0
  13. package/dist/alert-webhooks/alert-parsers.js +184 -0
  14. package/dist/alert-webhooks/alert-parsers.js.map +1 -0
  15. package/dist/api/agent.d.ts +0 -6
  16. package/dist/api/agent.d.ts.map +1 -1
  17. package/dist/api/agent.js +6 -459
  18. package/dist/api/agent.js.map +1 -1
  19. package/dist/api/alert-webhooks.d.ts +13 -0
  20. package/dist/api/alert-webhooks.d.ts.map +1 -0
  21. package/dist/api/alert-webhooks.js +279 -0
  22. package/dist/api/alert-webhooks.js.map +1 -0
  23. package/dist/api/envoy-reports.js +2 -2
  24. package/dist/api/envoy-reports.js.map +1 -1
  25. package/dist/api/envoys.js +1 -1
  26. package/dist/api/envoys.js.map +1 -1
  27. package/dist/api/fleet.d.ts.map +1 -1
  28. package/dist/api/fleet.js +14 -15
  29. package/dist/api/fleet.js.map +1 -1
  30. package/dist/api/graph.js +3 -3
  31. package/dist/api/graph.js.map +1 -1
  32. package/dist/api/operations.d.ts +7 -0
  33. package/dist/api/operations.d.ts.map +1 -0
  34. package/dist/api/operations.js +1900 -0
  35. package/dist/api/operations.js.map +1 -0
  36. package/dist/api/partitions.js +1 -1
  37. package/dist/api/partitions.js.map +1 -1
  38. package/dist/api/schemas.d.ts +434 -133
  39. package/dist/api/schemas.d.ts.map +1 -1
  40. package/dist/api/schemas.js +53 -25
  41. package/dist/api/schemas.js.map +1 -1
  42. package/dist/api/system.d.ts.map +1 -1
  43. package/dist/api/system.js +22 -21
  44. package/dist/api/system.js.map +1 -1
  45. package/dist/artifact-analyzer.js +2 -2
  46. package/dist/artifact-analyzer.js.map +1 -1
  47. package/dist/fleet/fleet-executor.js +3 -3
  48. package/dist/fleet/fleet-executor.js.map +1 -1
  49. package/dist/graph/graph-executor.d.ts.map +1 -1
  50. package/dist/graph/graph-executor.js +18 -4
  51. package/dist/graph/graph-executor.js.map +1 -1
  52. package/dist/index.js +89 -61
  53. package/dist/index.js.map +1 -1
  54. package/dist/mcp/resources.js +3 -3
  55. package/dist/mcp/resources.js.map +1 -1
  56. package/dist/mcp/tools.d.ts.map +1 -1
  57. package/dist/mcp/tools.js +2 -9
  58. package/dist/mcp/tools.js.map +1 -1
  59. package/dist/middleware/auth.js +1 -1
  60. package/dist/middleware/auth.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/agent/envoy-client.ts +111 -19
  63. package/src/agent/stale-deployment-detector.ts +1 -1
  64. package/src/agent/synth-agent.ts +76 -56
  65. package/src/alert-webhooks/alert-parsers.ts +291 -0
  66. package/src/api/agent.ts +9 -528
  67. package/src/api/alert-webhooks.ts +354 -0
  68. package/src/api/envoy-reports.ts +2 -2
  69. package/src/api/envoys.ts +1 -1
  70. package/src/api/fleet.ts +14 -15
  71. package/src/api/graph.ts +3 -3
  72. package/src/api/operations.ts +2260 -0
  73. package/src/api/partitions.ts +1 -1
  74. package/src/api/schemas.ts +59 -27
  75. package/src/api/system.ts +23 -21
  76. package/src/artifact-analyzer.ts +2 -2
  77. package/src/fleet/fleet-executor.ts +3 -3
  78. package/src/graph/graph-executor.ts +18 -4
  79. package/src/index.ts +91 -61
  80. package/src/mcp/resources.ts +3 -3
  81. package/src/mcp/tools.ts +5 -9
  82. package/src/middleware/auth.ts +1 -1
  83. package/tests/agent-mode.test.ts +5 -376
  84. package/tests/api-handlers.test.ts +27 -27
  85. package/tests/composite-operations.test.ts +557 -0
  86. package/tests/decision-diary.test.ts +62 -63
  87. package/tests/diary-reader.test.ts +14 -18
  88. package/tests/mcp-tools.test.ts +1 -1
  89. package/tests/orchestration.test.ts +34 -30
  90. package/tests/partition-isolation.test.ts +4 -9
  91. package/tests/rbac-enforcement.test.ts +8 -8
  92. package/tests/ui-journey.test.ts +9 -9
  93. package/dist/api/deployments.d.ts +0 -11
  94. package/dist/api/deployments.d.ts.map +0 -1
  95. package/dist/api/deployments.js +0 -1098
  96. package/dist/api/deployments.js.map +0 -1
  97. package/src/api/deployments.ts +0 -1347
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Alert Webhook API routes — channel management + alert receipt endpoint.
3
+ *
4
+ * External monitoring systems (Prometheus AlertManager, PagerDuty, Datadog,
5
+ * Grafana) POST alerts here. Synth parses them, creates operations, and
6
+ * dispatches planning through the normal operation flow.
7
+ */
8
+
9
+ import crypto from "node:crypto";
10
+ import type { FastifyInstance } from "fastify";
11
+ import type {
12
+ IDeploymentStore,
13
+ IEnvironmentStore,
14
+ IPartitionStore,
15
+ ITelemetryStore,
16
+ DebriefWriter,
17
+ AlertWebhookSource,
18
+ } from "@synth-deploy/core";
19
+ import type { PersistentAlertWebhookStore } from "@synth-deploy/core";
20
+ import { requirePermission } from "../middleware/permissions.js";
21
+ import { parseAlerts, interpolateIntent } from "../alert-webhooks/alert-parsers.js";
22
+ import { EnvoyClient } from "../agent/envoy-client.js";
23
+ import type { EnvoyRegistry } from "../agent/envoy-registry.js";
24
+
25
+ const VALID_SOURCES: AlertWebhookSource[] = ["prometheus", "pagerduty", "datadog", "grafana", "generic"];
26
+ const VALID_OP_TYPES = ["maintain", "deploy", "query", "investigate"] as const;
27
+
28
+ export function registerAlertWebhookRoutes(
29
+ app: FastifyInstance,
30
+ store: PersistentAlertWebhookStore,
31
+ deployments: IDeploymentStore,
32
+ debrief: DebriefWriter,
33
+ environments: IEnvironmentStore,
34
+ partitions: IPartitionStore,
35
+ telemetry: ITelemetryStore,
36
+ envoyRegistry?: EnvoyRegistry,
37
+ ): void {
38
+
39
+ // -----------------------------------------------------------------------
40
+ // Channel management — JWT-protected CRUD
41
+ // -----------------------------------------------------------------------
42
+
43
+ app.get(
44
+ "/api/alert-webhooks",
45
+ { preHandler: [requirePermission("settings.manage")] },
46
+ async () => {
47
+ const channels = store.list().map((ch) => ({
48
+ ...ch,
49
+ // Never expose authToken in list responses
50
+ authToken: undefined,
51
+ }));
52
+ return { channels };
53
+ },
54
+ );
55
+
56
+ app.get<{ Params: { id: string } }>(
57
+ "/api/alert-webhooks/:id",
58
+ { preHandler: [requirePermission("settings.manage")] },
59
+ async (request, reply) => {
60
+ const channel = store.get(request.params.id);
61
+ if (!channel) {
62
+ return reply.status(404).send({ error: "Alert webhook channel not found" });
63
+ }
64
+ return { channel: { ...channel, authToken: undefined } };
65
+ },
66
+ );
67
+
68
+ app.post(
69
+ "/api/alert-webhooks",
70
+ { preHandler: [requirePermission("settings.manage")] },
71
+ async (request, reply) => {
72
+ const body = request.body as Record<string, unknown>;
73
+ const name = body.name as string;
74
+ const source = body.source as string;
75
+ const defaultOperationType = (body.defaultOperationType as string) ?? "maintain";
76
+ const defaultIntent = body.defaultIntent as string | undefined;
77
+ const environmentId = body.environmentId as string | undefined;
78
+ const partitionId = body.partitionId as string | undefined;
79
+ const envoyId = body.envoyId as string | undefined;
80
+
81
+ if (!name) {
82
+ return reply.status(400).send({ error: "name is required" });
83
+ }
84
+ if (!source || !VALID_SOURCES.includes(source as AlertWebhookSource)) {
85
+ return reply.status(400).send({ error: `source must be one of: ${VALID_SOURCES.join(", ")}` });
86
+ }
87
+ if (!VALID_OP_TYPES.includes(defaultOperationType as typeof VALID_OP_TYPES[number])) {
88
+ return reply.status(400).send({ error: `defaultOperationType must be one of: ${VALID_OP_TYPES.join(", ")}` });
89
+ }
90
+
91
+ const authToken = crypto.randomUUID();
92
+ const channel = store.create({
93
+ name,
94
+ source: source as AlertWebhookSource,
95
+ enabled: true,
96
+ authToken,
97
+ defaultOperationType: defaultOperationType as "maintain" | "deploy" | "query" | "investigate",
98
+ defaultIntent,
99
+ environmentId,
100
+ partitionId,
101
+ envoyId,
102
+ });
103
+
104
+ const actor = (request.user as { email?: string })?.email ?? "anonymous";
105
+ telemetry.record({
106
+ actor,
107
+ action: "alert-webhook.created",
108
+ target: { type: "alert-webhook", id: channel.id },
109
+ details: { source, name },
110
+ });
111
+
112
+ // Return the full channel including authToken (only on creation)
113
+ return reply.status(201).send({
114
+ channel,
115
+ webhookUrl: `/api/alert-webhooks/receive/${channel.id}`,
116
+ });
117
+ },
118
+ );
119
+
120
+ app.put<{ Params: { id: string } }>(
121
+ "/api/alert-webhooks/:id",
122
+ { preHandler: [requirePermission("settings.manage")] },
123
+ async (request, reply) => {
124
+ const existing = store.get(request.params.id);
125
+ if (!existing) {
126
+ return reply.status(404).send({ error: "Alert webhook channel not found" });
127
+ }
128
+
129
+ const body = request.body as Record<string, unknown>;
130
+ const updates: Record<string, unknown> = {};
131
+
132
+ if (body.name !== undefined) updates.name = body.name;
133
+ if (body.source !== undefined) {
134
+ if (!VALID_SOURCES.includes(body.source as AlertWebhookSource)) {
135
+ return reply.status(400).send({ error: `source must be one of: ${VALID_SOURCES.join(", ")}` });
136
+ }
137
+ updates.source = body.source;
138
+ }
139
+ if (body.enabled !== undefined) updates.enabled = body.enabled;
140
+ if (body.defaultOperationType !== undefined) {
141
+ if (!VALID_OP_TYPES.includes(body.defaultOperationType as typeof VALID_OP_TYPES[number])) {
142
+ return reply.status(400).send({ error: `defaultOperationType must be one of: ${VALID_OP_TYPES.join(", ")}` });
143
+ }
144
+ updates.defaultOperationType = body.defaultOperationType;
145
+ }
146
+ if (body.defaultIntent !== undefined) updates.defaultIntent = body.defaultIntent;
147
+ if (body.environmentId !== undefined) updates.environmentId = body.environmentId;
148
+ if (body.partitionId !== undefined) updates.partitionId = body.partitionId;
149
+ if (body.envoyId !== undefined) updates.envoyId = body.envoyId;
150
+
151
+ const updated = store.update(request.params.id, updates);
152
+ return { channel: { ...updated, authToken: undefined } };
153
+ },
154
+ );
155
+
156
+ app.delete<{ Params: { id: string } }>(
157
+ "/api/alert-webhooks/:id",
158
+ { preHandler: [requirePermission("settings.manage")] },
159
+ async (request, reply) => {
160
+ const deleted = store.delete(request.params.id);
161
+ if (!deleted) {
162
+ return reply.status(404).send({ error: "Alert webhook channel not found" });
163
+ }
164
+ return { deleted: true };
165
+ },
166
+ );
167
+
168
+ // -----------------------------------------------------------------------
169
+ // Alert receipt — token-authenticated, JWT-exempt
170
+ // -----------------------------------------------------------------------
171
+
172
+ app.post<{ Params: { channelId: string } }>(
173
+ "/api/alert-webhooks/receive/:channelId",
174
+ async (request, reply) => {
175
+ const channel = store.get(request.params.channelId);
176
+ if (!channel) {
177
+ return reply.status(404).send({ error: "Webhook channel not found" });
178
+ }
179
+
180
+ if (!channel.enabled) {
181
+ return reply.status(403).send({ error: "Webhook channel is disabled" });
182
+ }
183
+
184
+ // Validate auth token from query parameter or header
185
+ const queryToken = (request.query as Record<string, string>).token;
186
+ const headerToken = request.headers["x-webhook-token"] as string | undefined;
187
+ const authHeader = request.headers.authorization;
188
+ const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
189
+ const token = queryToken || headerToken || bearerToken;
190
+
191
+ if (!token || token !== channel.authToken) {
192
+ return reply.status(401).send({ error: "Invalid or missing webhook token" });
193
+ }
194
+
195
+ // Parse the alert payload
196
+ const alerts = parseAlerts(channel.source, request.body);
197
+
198
+ if (alerts.length === 0) {
199
+ return reply.status(200).send({
200
+ received: true,
201
+ operationsCreated: 0,
202
+ reason: "No firing alerts in payload (resolved alerts are ignored)",
203
+ });
204
+ }
205
+
206
+ const createdOps: string[] = [];
207
+
208
+ for (const alert of alerts) {
209
+ // Deduplication: check for active operations from this channel with the same alert name
210
+ const allOps = deployments.list();
211
+ const activeExisting = allOps.find(
212
+ (op) =>
213
+ op.lineage === `alert-webhook:${channel.id}` &&
214
+ op.intent?.includes(alert.name) &&
215
+ ["pending", "planning", "awaiting_approval", "approved", "running"].includes(op.status),
216
+ );
217
+
218
+ if (activeExisting) {
219
+ debrief.record({
220
+ partitionId: channel.partitionId ?? null,
221
+ operationId: activeExisting.id,
222
+ agent: "server",
223
+ decisionType: "alert-webhook-suppressed",
224
+ decision: `Alert "${alert.name}" suppressed — operation ${activeExisting.id} is already in progress (${activeExisting.status})`,
225
+ reasoning: `Deduplication: an operation for this alert from webhook channel "${channel.name}" is already active.`,
226
+ context: { channelId: channel.id, alertName: alert.name, activeOpId: activeExisting.id },
227
+ });
228
+ continue;
229
+ }
230
+
231
+ // Build intent from template or alert summary
232
+ const intent = channel.defaultIntent
233
+ ? interpolateIntent(channel.defaultIntent, alert)
234
+ : `[${alert.severity.toUpperCase()}] ${alert.name}: ${alert.summary}`;
235
+
236
+ // Resolve environment/partition variables
237
+ const environment = channel.environmentId ? environments.get(channel.environmentId) : undefined;
238
+ const partition = channel.partitionId ? partitions.get(channel.partitionId) : undefined;
239
+ const envVars = environment?.variables ?? {};
240
+ const partitionVars = partition?.variables ?? {};
241
+ const resolved: Record<string, string> = { ...partitionVars, ...envVars };
242
+
243
+ const operationInput = channel.defaultOperationType === "deploy"
244
+ ? { type: "deploy" as const, artifactId: "" }
245
+ : channel.defaultOperationType === "investigate"
246
+ ? { type: "investigate" as const, intent }
247
+ : channel.defaultOperationType === "query"
248
+ ? { type: "query" as const, intent }
249
+ : { type: "maintain" as const, intent };
250
+
251
+ const operation = {
252
+ id: crypto.randomUUID(),
253
+ input: operationInput,
254
+ intent,
255
+ lineage: `alert-webhook:${channel.id}`,
256
+ triggeredBy: "webhook" as const,
257
+ environmentId: channel.environmentId,
258
+ partitionId: channel.partitionId,
259
+ envoyId: channel.envoyId,
260
+ version: "",
261
+ status: "pending" as const,
262
+ variables: resolved,
263
+ debriefEntryIds: [] as string[],
264
+ createdAt: new Date(),
265
+ };
266
+
267
+ deployments.save(operation);
268
+ createdOps.push(operation.id);
269
+
270
+ // Record the alert receipt in debrief
271
+ debrief.record({
272
+ partitionId: channel.partitionId ?? null,
273
+ operationId: operation.id,
274
+ agent: "server",
275
+ decisionType: "alert-webhook-received",
276
+ decision: `External alert received from ${channel.source}: "${alert.name}" (${alert.severity})`,
277
+ reasoning: `Webhook channel "${channel.name}" received a ${alert.severity} alert. Intent: ${intent}`,
278
+ context: {
279
+ channelId: channel.id,
280
+ channelName: channel.name,
281
+ source: channel.source,
282
+ alertName: alert.name,
283
+ alertSeverity: alert.severity,
284
+ alertLabels: alert.labels,
285
+ rawPayload: alert.rawPayload,
286
+ },
287
+ });
288
+
289
+ telemetry.record({
290
+ actor: `webhook:${channel.name}`,
291
+ action: "alert-webhook.fired",
292
+ target: { type: "deployment" as const, id: operation.id },
293
+ details: { channelId: channel.id, alertName: alert.name, severity: alert.severity },
294
+ });
295
+
296
+ // Dispatch planning to an envoy
297
+ if (envoyRegistry) {
298
+ const targetEnvoy = channel.envoyId
299
+ ? envoyRegistry.get(channel.envoyId)
300
+ : environment
301
+ ? envoyRegistry.findForEnvironment(environment.name)
302
+ : envoyRegistry.list()[0];
303
+
304
+ if (targetEnvoy) {
305
+ const planningClient = new EnvoyClient(targetEnvoy.url);
306
+ const environmentForPlanning = environment
307
+ ? { id: environment.id, name: environment.name, variables: environment.variables }
308
+ : { id: `direct:${targetEnvoy.id}`, name: targetEnvoy.name, variables: {} };
309
+
310
+ planningClient.requestPlan({
311
+ operationId: operation.id,
312
+ operationType: channel.defaultOperationType as "deploy" | "query" | "investigate" | "maintain",
313
+ intent,
314
+ environment: environmentForPlanning,
315
+ partition: partition
316
+ ? { id: partition.id, name: partition.name, variables: partition.variables }
317
+ : undefined,
318
+ version: "",
319
+ resolvedVariables: resolved,
320
+ }).then((result) => {
321
+ const dep = deployments.get(operation.id);
322
+ if (!dep || dep.status !== "pending") return;
323
+
324
+ dep.plan = result.plan;
325
+ dep.rollbackPlan = result.rollbackPlan;
326
+ dep.envoyId = targetEnvoy.id;
327
+
328
+ if (result.blocked) {
329
+ dep.status = "failed" as typeof dep.status;
330
+ dep.failureReason = result.blockReason ?? "Plan blocked";
331
+ deployments.save(dep);
332
+ } else {
333
+ dep.status = "awaiting_approval" as typeof dep.status;
334
+ deployments.save(dep);
335
+ }
336
+ }).catch((err) => {
337
+ const dep = deployments.get(operation.id);
338
+ if (!dep || dep.status !== "pending") return;
339
+ dep.status = "failed" as typeof dep.status;
340
+ dep.failureReason = err instanceof Error ? err.message : "Planning failed";
341
+ deployments.save(dep);
342
+ });
343
+ }
344
+ }
345
+ }
346
+
347
+ return reply.status(201).send({
348
+ received: true,
349
+ operationsCreated: createdOps.length,
350
+ operationIds: createdOps,
351
+ });
352
+ },
353
+ );
354
+ }
@@ -90,7 +90,7 @@ export function registerEnvoyReportRoutes(
90
90
  if (!deployment || deployment.partitionId !== entry.partitionId) {
91
91
  debrief.record({
92
92
  partitionId: entry.partitionId,
93
- deploymentId: entry.deploymentId,
93
+ operationId: entry.deploymentId,
94
94
  agent: "server",
95
95
  decisionType: "system",
96
96
  decision: "Rejected Envoy report: partition boundary violation",
@@ -114,7 +114,7 @@ export function registerEnvoyReportRoutes(
114
114
  for (const entry of report.debriefEntries) {
115
115
  debrief.record({
116
116
  partitionId: entry.partitionId,
117
- deploymentId: entry.deploymentId,
117
+ operationId: entry.deploymentId,
118
118
  agent: entry.agent as "server" | "envoy",
119
119
  decisionType: entry.decisionType as DecisionType,
120
120
  decision: entry.decision,
package/src/api/envoys.ts CHANGED
@@ -202,7 +202,7 @@ export function registerEnvoyRoutes(
202
202
  const observations: { id: string; timestamp: string; text: string }[] = [];
203
203
 
204
204
  for (const d of envoyDeployments) {
205
- const entries = debrief.getByDeployment(d.id);
205
+ const entries = debrief.getByOperation(d.id);
206
206
  for (const e of entries) {
207
207
  if (e.decisionType === "environment-scan") {
208
208
  observations.push({ id: e.id, timestamp: e.timestamp.toISOString(), text: e.reasoning || e.decision });
package/src/api/fleet.ts CHANGED
@@ -97,7 +97,7 @@ export function registerFleetRoutes(
97
97
 
98
98
  debrief.record({
99
99
  partitionId: null,
100
- deploymentId: fleetDeployment.id,
100
+ operationId: fleetDeployment.id,
101
101
  agent: "server",
102
102
  decisionType: "system",
103
103
  decision: `Fleet deployment created for ${targetEnvoys.length} envoys with ${rolloutConfig.strategy} strategy`,
@@ -143,8 +143,7 @@ export function registerFleetRoutes(
143
143
 
144
144
  const deployment: Deployment = {
145
145
  id: crypto.randomUUID(),
146
- artifactId: fleet.artifactId,
147
- artifactVersionId: fleet.artifactVersionId,
146
+ input: { type: "deploy" as const, artifactId: fleet.artifactId, ...(fleet.artifactVersionId ? { artifactVersionId: fleet.artifactVersionId } : {}) },
148
147
  envoyId: targetEnvoyId,
149
148
  environmentId: fleet.environmentId,
150
149
  version: "",
@@ -162,7 +161,7 @@ export function registerFleetRoutes(
162
161
 
163
162
  debrief.record({
164
163
  partitionId: null,
165
- deploymentId: fleet.id,
164
+ operationId: fleet.id,
166
165
  agent: "server",
167
166
  decisionType: "system",
168
167
  decision: `Representative plan created for envoy ${targetEnvoyId}`,
@@ -240,7 +239,7 @@ export function registerFleetRoutes(
240
239
 
241
240
  debrief.record({
242
241
  partitionId: null,
243
- deploymentId: fleet.id,
242
+ operationId: fleet.id,
244
243
  agent: "server",
245
244
  decisionType: "system",
246
245
  decision: `Fleet deployment approved by ${actor}, starting fleet validation`,
@@ -262,7 +261,7 @@ export function registerFleetRoutes(
262
261
 
263
262
  debrief.record({
264
263
  partitionId: null,
265
- deploymentId: fleet.id,
264
+ operationId: fleet.id,
266
265
  agent: "server",
267
266
  decisionType: "system",
268
267
  decision: `Fleet validation complete: ${validationResult.validated}/${validationResult.total} envoys passed`,
@@ -278,7 +277,7 @@ export function registerFleetRoutes(
278
277
 
279
278
  debrief.record({
280
279
  partitionId: null,
281
- deploymentId: fleet.id,
280
+ operationId: fleet.id,
282
281
  agent: "server",
283
282
  decisionType: "deployment-failure",
284
283
  decision: "Fleet validation failed unexpectedly",
@@ -328,7 +327,7 @@ export function registerFleetRoutes(
328
327
 
329
328
  debrief.record({
330
329
  partitionId: null,
331
- deploymentId: fleet.id,
330
+ operationId: fleet.id,
332
331
  agent: "server",
333
332
  decisionType: "system",
334
333
  decision: `Fleet rollout started by ${actor}`,
@@ -359,7 +358,7 @@ export function registerFleetRoutes(
359
358
  if (event.type === "envoy-failed") {
360
359
  debrief.record({
361
360
  partitionId: null,
362
- deploymentId: fleet.id,
361
+ operationId: fleet.id,
363
362
  agent: "server",
364
363
  decisionType: "deployment-failure",
365
364
  decision: `Envoy ${event.envoyName ?? event.envoyId} failed during fleet rollout`,
@@ -369,7 +368,7 @@ export function registerFleetRoutes(
369
368
  } else if (event.type === "fleet-completed") {
370
369
  debrief.record({
371
370
  partitionId: null,
372
- deploymentId: fleet.id,
371
+ operationId: fleet.id,
373
372
  agent: "server",
374
373
  decisionType: "deployment-completion",
375
374
  decision: `Fleet rollout completed: ${event.progress.succeeded}/${event.progress.totalEnvoys} succeeded`,
@@ -379,7 +378,7 @@ export function registerFleetRoutes(
379
378
  } else if (event.type === "fleet-failed") {
380
379
  debrief.record({
381
380
  partitionId: null,
382
- deploymentId: fleet.id,
381
+ operationId: fleet.id,
383
382
  agent: "server",
384
383
  decisionType: "deployment-failure",
385
384
  decision: `Fleet rollout halted: failure threshold reached`,
@@ -394,7 +393,7 @@ export function registerFleetRoutes(
394
393
  const durationSec = Math.round(durationMs / 1000);
395
394
  debrief.record({
396
395
  partitionId: null,
397
- deploymentId: fleet.id,
396
+ operationId: fleet.id,
398
397
  agent: "server",
399
398
  decisionType: "deployment-completion",
400
399
  decision: `Fleet deployment ${fleet.status}: ${fleet.progress.succeeded}/${fleet.progress.totalEnvoys} envoys succeeded, ${fleet.progress.failed} failed`,
@@ -417,7 +416,7 @@ export function registerFleetRoutes(
417
416
 
418
417
  debrief.record({
419
418
  partitionId: null,
420
- deploymentId: fleet.id,
419
+ operationId: fleet.id,
421
420
  agent: "server",
422
421
  decisionType: "deployment-failure",
423
422
  decision: `Fleet rollout failed with unexpected error after ${durationSec}s`,
@@ -456,7 +455,7 @@ export function registerFleetRoutes(
456
455
 
457
456
  debrief.record({
458
457
  partitionId: null,
459
- deploymentId: fleet.id,
458
+ operationId: fleet.id,
460
459
  agent: "server",
461
460
  decisionType: "system",
462
461
  decision: `Fleet rollout paused by ${actor}`,
@@ -495,7 +494,7 @@ export function registerFleetRoutes(
495
494
 
496
495
  debrief.record({
497
496
  partitionId: null,
498
- deploymentId: fleet.id,
497
+ operationId: fleet.id,
499
498
  agent: "server",
500
499
  decisionType: "system",
501
500
  decision: `Fleet rollout resumed by ${actor}`,
package/src/api/graph.ts CHANGED
@@ -83,7 +83,7 @@ export function registerGraphRoutes(
83
83
  // Record debrief entry for graph creation with inference reasoning
84
84
  debrief.record({
85
85
  partitionId: graph.partitionId ?? null,
86
- deploymentId: null,
86
+ operationId: null,
87
87
  agent: "server",
88
88
  decisionType: "plan-generation",
89
89
  decision: `Created deployment graph "${graph.name}" with ${graph.nodes.length} nodes and ${graph.edges.length} edges`,
@@ -166,7 +166,7 @@ export function registerGraphRoutes(
166
166
  // Record debrief entry for user corrections
167
167
  debrief.record({
168
168
  partitionId: graph.partitionId ?? null,
169
- deploymentId: null,
169
+ operationId: null,
170
170
  agent: "server",
171
171
  decisionType: "plan-modification",
172
172
  decision: `User corrected deployment graph "${graph.name}"`,
@@ -308,7 +308,7 @@ export function registerGraphRoutes(
308
308
 
309
309
  debrief.record({
310
310
  partitionId: graph.partitionId ?? null,
311
- deploymentId: null,
311
+ operationId: null,
312
312
  agent: "server",
313
313
  decisionType:
314
314
  finalFailedCount > 0 ? "deployment-failure" : "deployment-completion",