@lsctech/polaris 0.5.8 → 0.5.10

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.
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runMedicRunHealthConsult = runMedicRunHealthConsult;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const index_js_1 = require("../run-health/index.js");
7
+ const chart_id_js_1 = require("./chart-id.js");
8
+ const treatment_packets_js_1 = require("./treatment-packets.js");
9
+ function appendTelemetry(telemetryFile, event) {
10
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
11
+ (0, node_fs_1.writeFileSync)(telemetryFile, JSON.stringify(event) + "\n", { flag: "a", encoding: "utf-8" });
12
+ }
13
+ function symptomNeedsTreatment(symptom) {
14
+ return symptom.severity === "critical" || symptom.severity === "high";
15
+ }
16
+ function deriveQcArtifactRefs(report) {
17
+ const refs = new Set([
18
+ ...report.evidence_refs,
19
+ ...report.symptoms.flatMap((s) => s.evidence_refs),
20
+ ]);
21
+ return Array.from(refs);
22
+ }
23
+ function deriveDiagnosis(report) {
24
+ const codes = report.symptoms.map((s) => s.code);
25
+ const unique = Array.from(new Set(codes));
26
+ return `Run-health report for ${report.run_id} recorded ${report.symptoms.length} symptom(s): ${unique.join(", ")}.`;
27
+ }
28
+ function deriveDecision(report) {
29
+ const treatable = report.symptoms.filter(symptomNeedsTreatment);
30
+ if (treatable.length === 0) {
31
+ return {
32
+ decision: "no-treatment-needed",
33
+ rationale: "No critical or high-severity symptoms recorded; observation and follow-up are sufficient.",
34
+ };
35
+ }
36
+ const plan = treatable.map((s) => `Round-1 treatment for ${s.id} (${s.code}): address ${s.message}`);
37
+ return {
38
+ decision: "treatment-required",
39
+ rationale: `${treatable.length} symptom(s) require treatment.`,
40
+ treatmentPlan: plan,
41
+ };
42
+ }
43
+ function writeChart(chart, runId, repoRoot) {
44
+ const chartsDir = (0, node_path_1.join)(repoRoot, "smartdocs", "medic", "charts");
45
+ (0, node_fs_1.mkdirSync)(chartsDir, { recursive: true });
46
+ const nextId = (0, chart_id_js_1.generateNextChartId)(chartsDir);
47
+ const chartId = nextId.full;
48
+ const now = new Date().toISOString();
49
+ const treatmentPlanText = chart.treatment_plan && chart.treatment_plan.length > 0
50
+ ? chart.treatment_plan.map((p) => `- ${p}`).join("\n")
51
+ : "No treatment required.";
52
+ const followUpText = chart.follow_up_conditions && chart.follow_up_conditions.length > 0
53
+ ? chart.follow_up_conditions.map((c) => `- ${c}`).join("\n")
54
+ : "None.";
55
+ const noTreatmentText = chart.no_treatment_rationale
56
+ ? chart.no_treatment_rationale
57
+ : "N/A";
58
+ const content = `---
59
+ chart_id: ${chartId}
60
+ cluster_id: ${chart.cluster_id}
61
+ route: src/run-health
62
+ status: active
63
+ related_charts: []
64
+ created: ${now}
65
+ updated: ${now}
66
+ ---
67
+
68
+ ## Problem
69
+
70
+ Run-health symptoms were recorded during ${runId}.
71
+
72
+ ## Symptoms
73
+
74
+ ${chart.symptoms.map((s) => `- **${s.code}** (${s.id}): ${s.message}`).join("\n")}
75
+
76
+ ## Root Cause
77
+
78
+ ${chart.diagnosis}
79
+
80
+ ## Affected Files
81
+
82
+ ${chart.evidence_refs.join("\n")}
83
+
84
+ ## Treatment
85
+
86
+ ${treatmentPlanText}
87
+
88
+ ## Validation
89
+
90
+ Treatment workers must pass their embedded validation commands.
91
+
92
+ ## Prevention
93
+
94
+ Review run-health symptoms and root causes to prevent recurrence.
95
+
96
+ ## When To Read This Chart
97
+
98
+ When similar symptoms appear in future runs.
99
+
100
+ ## Decision
101
+
102
+ **Decision:** ${chart.decision}
103
+
104
+ **No-treatment rationale:** ${noTreatmentText}
105
+
106
+ **Follow-up conditions:**
107
+ ${followUpText}
108
+ `;
109
+ const filePath = (0, node_path_1.join)(chartsDir, `${chartId}.md`);
110
+ (0, node_fs_1.writeFileSync)(filePath, content, "utf-8");
111
+ return (0, node_path_1.relative)(repoRoot, filePath);
112
+ }
113
+ function markInProgress(report, chartRef, repoRoot) {
114
+ (0, index_js_1.markMedicDecision)(report.run_id, { status: "in-progress", chartRefs: [chartRef] }, repoRoot);
115
+ }
116
+ function markResolved(report, chartRef, treatmentRefs, outcome, repoRoot) {
117
+ (0, index_js_1.markMedicDecision)(report.run_id, {
118
+ status: "resolved",
119
+ chartRefs: [chartRef],
120
+ treatmentPacketRefs: treatmentRefs,
121
+ resolvedAt: new Date().toISOString(),
122
+ resolutionNotes: `Terminal outcome: ${outcome}`,
123
+ }, repoRoot);
124
+ }
125
+ async function runMedicRunHealthConsult(input) {
126
+ const { packet, repoRoot, stateFile, telemetryFile, branch, validationCommands, maxConcurrentWorkers, dryRun, dispatchTreatmentWorkerFn, } = input;
127
+ const now = new Date().toISOString();
128
+ appendTelemetry(telemetryFile, {
129
+ event: "medic-run-health-consult-started",
130
+ run_id: packet.run_id,
131
+ cluster_id: packet.cluster_id,
132
+ dispatch_id: packet.dispatch_id,
133
+ timestamp: now,
134
+ });
135
+ const report = (0, index_js_1.readRunHealthReport)(packet.run_id, repoRoot);
136
+ if (!report) {
137
+ const error = `No run-health report found for run "${packet.run_id}"`;
138
+ appendTelemetry(telemetryFile, {
139
+ event: "medic-run-health-terminal",
140
+ run_id: packet.run_id,
141
+ cluster_id: packet.cluster_id,
142
+ dispatch_id: packet.dispatch_id,
143
+ outcome: "error",
144
+ error,
145
+ timestamp: new Date().toISOString(),
146
+ });
147
+ return {
148
+ run_id: packet.run_id,
149
+ cluster_id: packet.cluster_id,
150
+ dispatch_id: packet.dispatch_id,
151
+ status: "error",
152
+ chart_id: null,
153
+ decision: "terminal",
154
+ treatment_packet_refs: [],
155
+ terminal_outcome: "error",
156
+ error_message: error,
157
+ timestamp: new Date().toISOString(),
158
+ };
159
+ }
160
+ const decisionInfo = deriveDecision(report);
161
+ const chart = {
162
+ chart_id: "", // filled in after write
163
+ cluster_id: packet.cluster_id,
164
+ symptoms: report.symptoms.map((s) => ({ id: s.id, code: s.code, message: s.message })),
165
+ diagnosis: deriveDiagnosis(report),
166
+ evidence_refs: deriveQcArtifactRefs(report),
167
+ decision: decisionInfo.decision,
168
+ treatment_plan: decisionInfo.treatmentPlan,
169
+ no_treatment_rationale: decisionInfo.rationale,
170
+ follow_up_conditions: ["Re-check run-health report after next run."],
171
+ created_at: now,
172
+ };
173
+ const chartRef = writeChart(chart, packet.run_id, repoRoot);
174
+ chart.chart_id = chartRef.replace("smartdocs/medic/charts/", "").replace(".md", "");
175
+ appendTelemetry(telemetryFile, {
176
+ event: "medic-run-health-chart-created",
177
+ run_id: packet.run_id,
178
+ cluster_id: packet.cluster_id,
179
+ dispatch_id: packet.dispatch_id,
180
+ chart_id: chart.chart_id,
181
+ chart_ref: chartRef,
182
+ decision: chart.decision,
183
+ timestamp: new Date().toISOString(),
184
+ });
185
+ markInProgress(report, chart.chart_id, repoRoot);
186
+ if (chart.decision === "no-treatment-needed") {
187
+ const outcome = "no-treatment-needed";
188
+ markResolved(report, chart.chart_id, [], outcome, repoRoot);
189
+ appendTelemetry(telemetryFile, {
190
+ event: "medic-run-health-terminal",
191
+ run_id: packet.run_id,
192
+ cluster_id: packet.cluster_id,
193
+ dispatch_id: packet.dispatch_id,
194
+ outcome,
195
+ chart_id: chart.chart_id,
196
+ timestamp: new Date().toISOString(),
197
+ });
198
+ return {
199
+ run_id: packet.run_id,
200
+ cluster_id: packet.cluster_id,
201
+ dispatch_id: packet.dispatch_id,
202
+ status: "resolved",
203
+ chart_id: chart.chart_id,
204
+ decision: chart.decision,
205
+ treatment_packet_refs: [],
206
+ terminal_outcome: outcome,
207
+ timestamp: new Date().toISOString(),
208
+ };
209
+ }
210
+ if (!dispatchTreatmentWorkerFn) {
211
+ const error = "dispatchTreatmentWorkerFn is required when treatment is required";
212
+ appendTelemetry(telemetryFile, {
213
+ event: "medic-run-health-terminal",
214
+ run_id: packet.run_id,
215
+ cluster_id: packet.cluster_id,
216
+ dispatch_id: packet.dispatch_id,
217
+ outcome: "error",
218
+ error,
219
+ chart_id: chart.chart_id,
220
+ timestamp: new Date().toISOString(),
221
+ });
222
+ return {
223
+ run_id: packet.run_id,
224
+ cluster_id: packet.cluster_id,
225
+ dispatch_id: packet.dispatch_id,
226
+ status: "error",
227
+ chart_id: chart.chart_id,
228
+ decision: "terminal",
229
+ treatment_packet_refs: [],
230
+ terminal_outcome: "error",
231
+ error_message: error,
232
+ timestamp: new Date().toISOString(),
233
+ };
234
+ }
235
+ const maxRounds = Math.max(1, packet.policy_limits.max_treatment_rounds);
236
+ let round = 1;
237
+ let remainingSymptoms = report.symptoms.filter(symptomNeedsTreatment);
238
+ const allTreatmentRefs = [];
239
+ const terminalEvent = (outcome) => appendTelemetry(telemetryFile, {
240
+ event: "medic-run-health-terminal",
241
+ run_id: packet.run_id,
242
+ cluster_id: packet.cluster_id,
243
+ dispatch_id: packet.dispatch_id,
244
+ outcome,
245
+ chart_id: chart.chart_id,
246
+ rounds_completed: round,
247
+ timestamp: new Date().toISOString(),
248
+ });
249
+ while (round <= maxRounds && remainingSymptoms.length > 0) {
250
+ const syntheticReport = {
251
+ ...report,
252
+ symptoms: remainingSymptoms,
253
+ };
254
+ const treatmentPackets = (0, treatment_packets_js_1.buildTreatmentPackets)({
255
+ report: syntheticReport,
256
+ round,
257
+ repoRoot,
258
+ validationCommands: validationCommands ?? treatment_packets_js_1.DEFAULT_TREATMENT_VALIDATION_COMMANDS,
259
+ });
260
+ const treatmentRefs = treatmentPackets.map((t) => (0, node_path_1.join)(".polaris", "clusters", t.cluster_id, "medic", `${t.packet_id}.json`));
261
+ allTreatmentRefs.push(...treatmentRefs);
262
+ appendTelemetry(telemetryFile, {
263
+ event: "medic-run-health-treatment-packets-created",
264
+ run_id: packet.run_id,
265
+ cluster_id: packet.cluster_id,
266
+ dispatch_id: packet.dispatch_id,
267
+ round,
268
+ packet_count: treatmentPackets.length,
269
+ packet_refs: treatmentRefs,
270
+ timestamp: new Date().toISOString(),
271
+ });
272
+ const results = [];
273
+ try {
274
+ for (const treatment of treatmentPackets) {
275
+ if (dryRun) {
276
+ results.push({ packet_id: treatment.packet_id, status: "success" });
277
+ continue;
278
+ }
279
+ const result = await dispatchTreatmentWorkerFn({
280
+ treatment,
281
+ stateFile,
282
+ telemetryFile,
283
+ branch,
284
+ maxConcurrentWorkers,
285
+ });
286
+ results.push(result);
287
+ }
288
+ }
289
+ catch (err) {
290
+ const msg = err instanceof Error ? err.message : String(err);
291
+ appendTelemetry(telemetryFile, {
292
+ event: "medic-run-health-treatment-dispatch-error",
293
+ run_id: packet.run_id,
294
+ cluster_id: packet.cluster_id,
295
+ dispatch_id: packet.dispatch_id,
296
+ round,
297
+ error: msg,
298
+ timestamp: new Date().toISOString(),
299
+ });
300
+ const outcome = "dispatch-failure";
301
+ markResolved(report, chart.chart_id, allTreatmentRefs, outcome, repoRoot);
302
+ terminalEvent(outcome);
303
+ return {
304
+ run_id: packet.run_id,
305
+ cluster_id: packet.cluster_id,
306
+ dispatch_id: packet.dispatch_id,
307
+ status: "error",
308
+ chart_id: chart.chart_id,
309
+ decision: "terminal",
310
+ treatment_packet_refs: allTreatmentRefs,
311
+ terminal_outcome: outcome,
312
+ error_message: msg,
313
+ timestamp: new Date().toISOString(),
314
+ };
315
+ }
316
+ const failed = results.filter((r) => r.status === "failure");
317
+ const succeeded = results.filter((r) => r.status === "success");
318
+ appendTelemetry(telemetryFile, {
319
+ event: "medic-run-health-treatment-completed",
320
+ run_id: packet.run_id,
321
+ cluster_id: packet.cluster_id,
322
+ dispatch_id: packet.dispatch_id,
323
+ round,
324
+ success_count: succeeded.length,
325
+ failure_count: failed.length,
326
+ failed_packet_ids: failed.map((f) => f.packet_id),
327
+ timestamp: new Date().toISOString(),
328
+ });
329
+ if (failed.length === 0) {
330
+ const outcome = "treatment-success";
331
+ markResolved(report, chart.chart_id, allTreatmentRefs, outcome, repoRoot);
332
+ terminalEvent(outcome);
333
+ return {
334
+ run_id: packet.run_id,
335
+ cluster_id: packet.cluster_id,
336
+ dispatch_id: packet.dispatch_id,
337
+ status: "resolved",
338
+ chart_id: chart.chart_id,
339
+ decision: "terminal",
340
+ treatment_packet_refs: allTreatmentRefs,
341
+ terminal_outcome: outcome,
342
+ timestamp: new Date().toISOString(),
343
+ };
344
+ }
345
+ if (round < maxRounds) {
346
+ const failedIds = new Set(failed.map((f) => f.packet_id));
347
+ remainingSymptoms = remainingSymptoms.filter((s) => failedIds.has((0, treatment_packets_js_1.buildTreatmentPacketId)(report.run_id, round, s.id)));
348
+ round += 1;
349
+ }
350
+ else {
351
+ const outcome = "max-rounds";
352
+ markResolved(report, chart.chart_id, allTreatmentRefs, outcome, repoRoot);
353
+ terminalEvent(outcome);
354
+ return {
355
+ run_id: packet.run_id,
356
+ cluster_id: packet.cluster_id,
357
+ dispatch_id: packet.dispatch_id,
358
+ status: "resolved",
359
+ chart_id: chart.chart_id,
360
+ decision: "terminal",
361
+ treatment_packet_refs: allTreatmentRefs,
362
+ terminal_outcome: outcome,
363
+ timestamp: new Date().toISOString(),
364
+ };
365
+ }
366
+ }
367
+ const outcome = remainingSymptoms.length === 0 ? "treatment-success" : "max-rounds";
368
+ markResolved(report, chart.chart_id, allTreatmentRefs, outcome, repoRoot);
369
+ terminalEvent(outcome);
370
+ return {
371
+ run_id: packet.run_id,
372
+ cluster_id: packet.cluster_id,
373
+ dispatch_id: packet.dispatch_id,
374
+ status: "resolved",
375
+ chart_id: chart.chart_id,
376
+ decision: "terminal",
377
+ treatment_packet_refs: allTreatmentRefs,
378
+ terminal_outcome: outcome,
379
+ timestamp: new Date().toISOString(),
380
+ };
381
+ }
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TREATMENT_PROHIBITED_SCOPE = exports.DEFAULT_TREATMENT_VALIDATION_COMMANDS = void 0;
4
+ exports.getMedicDir = getMedicDir;
5
+ exports.buildTreatmentPacketId = buildTreatmentPacketId;
6
+ exports.buildTreatmentPackets = buildTreatmentPackets;
7
+ exports.compileTreatmentWorkerPacket = compileTreatmentWorkerPacket;
8
+ exports.writeTreatmentPacket = writeTreatmentPacket;
9
+ exports.parseTreatmentWorkerSummary = parseTreatmentWorkerSummary;
10
+ exports.dispatchTreatmentWorker = dispatchTreatmentWorker;
11
+ const node_fs_1 = require("node:fs");
12
+ const node_path_1 = require("node:path");
13
+ const node_crypto_1 = require("node:crypto");
14
+ const worker_packet_js_1 = require("../loop/worker-packet.js");
15
+ /** Default validation commands embedded in treatment packets. */
16
+ exports.DEFAULT_TREATMENT_VALIDATION_COMMANDS = [
17
+ "npm run build",
18
+ "npm test",
19
+ ];
20
+ /** Paths a treatment worker must never touch. */
21
+ exports.DEFAULT_TREATMENT_PROHIBITED_SCOPE = [
22
+ ".polaris/**",
23
+ ".taskchain_artifacts/**",
24
+ "**/telemetry.jsonl",
25
+ ];
26
+ /**
27
+ * Return the cluster-scoped Medic directory where treatment packets are persisted.
28
+ */
29
+ function getMedicDir(clusterId, repoRoot) {
30
+ return (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "medic");
31
+ }
32
+ /**
33
+ * Build a deterministic treatment packet id from run id, round, and symptom.
34
+ */
35
+ function buildTreatmentPacketId(runId, round, symptomId) {
36
+ const base = `${runId}-r${round}-${symptomId}`;
37
+ return base.replace(/[^a-zA-Z0-9_-]/g, "_");
38
+ }
39
+ /**
40
+ * Determine whether a symptom should receive a treatment packet.
41
+ */
42
+ function symptomNeedsTreatment(symptom) {
43
+ return symptom.severity === "critical" || symptom.severity === "high";
44
+ }
45
+ /**
46
+ * Derive an allowed file scope from symptom evidence refs.
47
+ * Falls back to the repository root when no concrete refs are present.
48
+ */
49
+ function scopeFromEvidenceRefs(refs) {
50
+ const fileRefs = refs.filter((ref) => ref.includes("/") || ref.includes("."));
51
+ return fileRefs.length > 0 ? fileRefs : ["."];
52
+ }
53
+ /**
54
+ * Build treatment packets for every symptom that needs treatment in the report.
55
+ */
56
+ function buildTreatmentPackets(input) {
57
+ const { report, round, repoRoot, validationCommands } = input;
58
+ const treated = report.symptoms.filter(symptomNeedsTreatment);
59
+ return treated.map((symptom) => {
60
+ const packetId = buildTreatmentPacketId(report.run_id, round, symptom.id);
61
+ const dispatchId = (0, node_crypto_1.randomUUID)();
62
+ const resultFile = (0, node_path_1.join)(getMedicDir(report.cluster_id, repoRoot), `${packetId}-result.json`);
63
+ return {
64
+ packet_id: packetId,
65
+ run_id: report.run_id,
66
+ cluster_id: report.cluster_id,
67
+ round,
68
+ source_symptom_ids: [symptom.id],
69
+ allowed_scope: scopeFromEvidenceRefs(symptom.evidence_refs),
70
+ prohibited_scope: exports.DEFAULT_TREATMENT_PROHIBITED_SCOPE,
71
+ validation_commands: validationCommands ?? exports.DEFAULT_TREATMENT_VALIDATION_COMMANDS,
72
+ root_cause_hint: `Run-health symptom ${symptom.code}: ${symptom.message}`,
73
+ dispatch_metadata: {
74
+ dispatch_id: dispatchId,
75
+ worker_id: `${report.run_id}:treatment:${packetId}:${Date.now()}`,
76
+ result_file: resultFile,
77
+ },
78
+ status: "pending",
79
+ };
80
+ });
81
+ }
82
+ /**
83
+ * Compile a Medic treatment packet into a normal Foreman WorkerPacket.
84
+ *
85
+ * The resulting packet uses `worker_role: "repair"` so it re-enters the same
86
+ * dispatch path as QC repair workers, with no special implementation behavior.
87
+ */
88
+ function compileTreatmentWorkerPacket(input) {
89
+ const { treatment, stateFile, telemetryFile, branch, maxConcurrentWorkers } = input;
90
+ return (0, worker_packet_js_1.compileRepairWorkerPacket)({
91
+ runId: treatment.run_id,
92
+ clusterId: treatment.cluster_id,
93
+ packetId: treatment.packet_id,
94
+ branch,
95
+ stateFile,
96
+ telemetryFile,
97
+ round: treatment.round,
98
+ allowedScope: treatment.allowed_scope,
99
+ prohibitedScope: treatment.prohibited_scope,
100
+ validationCommands: treatment.validation_commands,
101
+ rootCauseHint: treatment.root_cause_hint,
102
+ resultFile: treatment.dispatch_metadata.result_file,
103
+ maxConcurrentWorkers,
104
+ });
105
+ }
106
+ /**
107
+ * Persist a treatment packet to disk and return its repo-relative path.
108
+ */
109
+ function writeTreatmentPacket(input) {
110
+ const { treatment, repoRoot } = input;
111
+ const dir = getMedicDir(treatment.cluster_id, repoRoot);
112
+ const filePath = (0, node_path_1.join)(dir, `${treatment.packet_id}.json`);
113
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
114
+ (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(treatment, null, 2), "utf-8");
115
+ return filePath;
116
+ }
117
+ /**
118
+ * Parse a treatment worker summary from a dispatch result summary string.
119
+ */
120
+ function parseTreatmentWorkerSummary(summary) {
121
+ if (!summary)
122
+ return null;
123
+ try {
124
+ const parsed = JSON.parse(summary);
125
+ const status = String(parsed["status"] ?? "").toLowerCase();
126
+ const commit = typeof parsed["commit"] === "string" ? parsed["commit"] : undefined;
127
+ const error_message = typeof parsed["error_message"] === "string" ? parsed["error_message"] : undefined;
128
+ if (status === "done" || status === "success") {
129
+ return { status: "done", commit, error_message };
130
+ }
131
+ if (status === "blocked") {
132
+ return { status: "blocked", commit, error_message };
133
+ }
134
+ return { status: "failure", commit, error_message };
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ /**
141
+ * Dispatch a single treatment worker through the normal Foreman adapter and
142
+ * return a normalized treatment result.
143
+ */
144
+ async function dispatchTreatmentWorker(input) {
145
+ const { treatment, stateFile, telemetryFile, branch, repoRoot, dispatch, maxConcurrentWorkers } = input;
146
+ writeTreatmentPacket({ treatment, repoRoot });
147
+ const workerPacket = compileTreatmentWorkerPacket({
148
+ treatment,
149
+ stateFile,
150
+ telemetryFile,
151
+ branch,
152
+ maxConcurrentWorkers,
153
+ });
154
+ const result = await dispatch(workerPacket);
155
+ const summary = parseTreatmentWorkerSummary(result.summary);
156
+ if (result.exit_code === 0 && summary?.status === "done") {
157
+ return {
158
+ packet_id: treatment.packet_id,
159
+ status: "success",
160
+ commit_sha: summary.commit,
161
+ };
162
+ }
163
+ return {
164
+ packet_id: treatment.packet_id,
165
+ status: "failure",
166
+ error_message: summary?.error_message ??
167
+ (result.exit_code === 0 ? "treatment worker returned non-done status" : "dispatch failed"),
168
+ };
169
+ }