@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.3

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 (46) hide show
  1. package/docs/http-api.md +66 -0
  2. package/package.json +1 -1
  3. package/src/api/__tests__/index.test.ts +311 -3
  4. package/src/api/index.ts +94 -45
  5. package/src/config/__tests__/statuses.test.ts +41 -0
  6. package/src/config/statuses.ts +20 -1
  7. package/src/core/__tests__/config.test.ts +31 -1
  8. package/src/core/__tests__/job-concurrency.test.ts +60 -0
  9. package/src/core/__tests__/job-view.test.ts +859 -0
  10. package/src/core/__tests__/orchestrator.test.ts +242 -0
  11. package/src/core/__tests__/runner-liveness.test.ts +865 -0
  12. package/src/core/__tests__/single-derivation.test.ts +159 -0
  13. package/src/core/config.ts +19 -0
  14. package/src/core/job-concurrency.ts +6 -1
  15. package/src/core/job-view.ts +329 -0
  16. package/src/core/orchestrator.ts +78 -6
  17. package/src/core/runner-liveness.ts +276 -0
  18. package/src/core/status-writer.ts +24 -5
  19. package/src/ui/client/__tests__/job-adapter.test.ts +78 -0
  20. package/src/ui/client/adapters/job-adapter.ts +10 -0
  21. package/src/ui/client/types.ts +2 -0
  22. package/src/ui/components/JobTable.tsx +29 -12
  23. package/src/ui/components/__tests__/JobTable.test.tsx +54 -1
  24. package/src/ui/components/types.ts +2 -0
  25. package/src/ui/dist/assets/{index--RH3sAt3.js → index-L6cvsCAx.js} +25 -12
  26. package/src/ui/dist/assets/{index--RH3sAt3.js.map → index-L6cvsCAx.js.map} +1 -1
  27. package/src/ui/dist/index.html +1 -1
  28. package/src/ui/embedded-assets.js +6 -6
  29. package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
  30. package/src/ui/server/__tests__/job-control-endpoints.test.ts +57 -1
  31. package/src/ui/server/__tests__/job-endpoints.test.ts +115 -1
  32. package/src/ui/server/__tests__/sse-enhancer.test.ts +31 -0
  33. package/src/ui/server/config-bridge.ts +3 -4
  34. package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
  35. package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
  36. package/src/ui/server/endpoints/job-control-endpoints.ts +7 -68
  37. package/src/ui/server/endpoints/job-endpoints.ts +6 -0
  38. package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
  39. package/src/ui/state/__tests__/snapshot.test.ts +51 -0
  40. package/src/ui/state/__tests__/types.test.ts +98 -5
  41. package/src/ui/state/snapshot.ts +1 -1
  42. package/src/ui/state/transformers/__tests__/list-transformer.test.ts +43 -2
  43. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +208 -22
  44. package/src/ui/state/transformers/list-transformer.ts +7 -3
  45. package/src/ui/state/transformers/status-transformer.ts +36 -170
  46. package/src/ui/state/types.ts +9 -47
@@ -1,9 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
+ import type { JobView } from "../../../core/job-view";
3
4
  import type {
4
5
  APIJob,
5
6
  AcquireResult,
6
7
  CanonicalJob,
8
+ CanonicalJobStatus,
7
9
  ChangeTrackerState,
8
10
  ComposeSnapshotOptions,
9
11
  FilterOptions,
@@ -44,7 +46,7 @@ describe("ui/state types", () => {
44
46
  jobId: "job-1",
45
47
  name: "Job 1",
46
48
  title: "Job 1",
47
- status: "pending",
49
+ status: "failed",
48
50
  progress: 0,
49
51
  createdAt: null,
50
52
  updatedAt: null,
@@ -56,15 +58,15 @@ describe("ui/state types", () => {
56
58
  const grouped: GroupedJobs = {
57
59
  running: [],
58
60
  waiting: [],
59
- error: [],
60
- pending: [job],
61
+ failed: [],
62
+ pending: [],
61
63
  complete: [],
62
64
  };
63
65
  const filter: FilterOptions = { status: "pending", location: "current" };
64
66
  const apiJob: APIJob = {
65
67
  jobId: "job-1",
66
68
  title: "Job 1",
67
- status: "pending",
69
+ status: "failed",
68
70
  progress: 0,
69
71
  createdAt: null,
70
72
  updatedAt: null,
@@ -96,9 +98,100 @@ describe("ui/state types", () => {
96
98
  expect(snapshotDeps.readJob).toBeUndefined();
97
99
  expect(schema.fileName).toBe("seed.json");
98
100
  expect(result.response).toBeInstanceOf(Response);
99
- expect(grouped.pending[0]).toBe(job);
101
+ expect(job.status).toBe("failed");
102
+ expect(grouped.failed).toEqual([]);
100
103
  expect(filter.location).toBe("current");
101
104
  expect(apiJob.costsSummary.totalCost).toBe(0);
105
+ expect(apiJob.status).toBe("failed");
102
106
  expect(stats.transformationRate).toBe(0);
103
107
  });
108
+
109
+ it("rejects status:\"error\" as a CanonicalJobStatus at the type level", () => {
110
+ // @ts-expect-error — "error" is not assignable to CanonicalJobStatus
111
+ const rejected: CanonicalJobStatus = "error";
112
+ expect(rejected).toBe("error");
113
+ });
114
+
115
+ it("rejects status:\"error\" on a CanonicalJob literal", () => {
116
+ const job: CanonicalJob = {
117
+ id: "job-1",
118
+ jobId: "job-1",
119
+ name: "Job 1",
120
+ title: "Job 1",
121
+ // @ts-expect-error — "error" is not assignable to CanonicalJobStatus
122
+ status: "error",
123
+ progress: 0,
124
+ createdAt: null,
125
+ updatedAt: null,
126
+ location: "current",
127
+ tasks: {},
128
+ files: {},
129
+ costs: {},
130
+ };
131
+ expect(job.jobId).toBe("job-1");
132
+ });
133
+
134
+ it("rejects status:\"error\" on an APIJob literal", () => {
135
+ const apiJob: APIJob = {
136
+ jobId: "job-1",
137
+ title: "Job 1",
138
+ // @ts-expect-error — "error" is not assignable to CanonicalJobStatus
139
+ status: "error",
140
+ progress: 0,
141
+ createdAt: null,
142
+ updatedAt: null,
143
+ location: "current",
144
+ tasks: {},
145
+ costsSummary: {
146
+ totalTokens: 0,
147
+ totalInputTokens: 0,
148
+ totalOutputTokens: 0,
149
+ totalCost: 0,
150
+ totalInputCost: 0,
151
+ totalOutputCost: 0,
152
+ },
153
+ };
154
+ expect(apiJob.jobId).toBe("job-1");
155
+ });
156
+
157
+ it("preserves readable:false and error: { code, message } on a CanonicalJob", () => {
158
+ const job: CanonicalJob = {
159
+ id: "job-1",
160
+ jobId: "job-1",
161
+ name: "Job 1",
162
+ title: "Job 1",
163
+ status: "failed",
164
+ progress: 0,
165
+ createdAt: null,
166
+ updatedAt: null,
167
+ location: "current",
168
+ tasks: {},
169
+ files: {},
170
+ costs: {},
171
+ readable: false,
172
+ error: { code: "E_FOO", message: "broken", path: "/tmp/job-1" },
173
+ };
174
+
175
+ expect(job.readable).toBe(false);
176
+ expect(job.error).toEqual({ code: "E_FOO", message: "broken", path: "/tmp/job-1" });
177
+ });
178
+
179
+ it("CanonicalJob is structurally identical to JobView", () => {
180
+ const job: CanonicalJob = {
181
+ id: "job-1",
182
+ jobId: "job-1",
183
+ name: "Job 1",
184
+ title: "Job 1",
185
+ status: "failed",
186
+ progress: 0,
187
+ createdAt: null,
188
+ updatedAt: null,
189
+ location: "current",
190
+ tasks: {},
191
+ files: {},
192
+ costs: {},
193
+ };
194
+ const view: JobView = job;
195
+ expect(view.id).toBe("job-1");
196
+ });
104
197
  });
@@ -10,7 +10,7 @@ import type {
10
10
  StateSnapshot,
11
11
  } from "./types";
12
12
 
13
- const SNAPSHOT_STATUS_ORDER = ["error", "running", "complete", "pending"] as const;
13
+ const SNAPSHOT_STATUS_ORDER = ["failed", "running", "complete", "pending"] as const;
14
14
 
15
15
  function nowIso(now: (() => Date) | undefined): string {
16
16
  return (now ?? (() => new Date()))().toISOString();
@@ -34,6 +34,7 @@ describe("list-transformer", () => {
34
34
  it("returns status priorities and sorts valid jobs only", () => {
35
35
  expect(getStatusPriority("running")).toBe(4);
36
36
  expect(getStatusPriority("waiting")).toBe(4);
37
+ expect(getStatusPriority("failed")).toBe(3);
37
38
  expect(getStatusPriority("unknown")).toBe(0);
38
39
 
39
40
  const jobs = sortJobs([
@@ -57,17 +58,22 @@ describe("list-transformer", () => {
57
58
  const jobs = [
58
59
  makeJob({ id: "job-1", title: "Alpha", status: "running", progress: 50 }),
59
60
  makeJob({ id: "job-2", jobId: "job-2", title: "Beta", status: "complete", progress: 100, location: "complete" }),
61
+ makeJob({ id: "job-3", jobId: "job-3", title: "Gamma", status: "failed", progress: 25 }),
60
62
  ];
61
63
 
62
- expect(groupJobsByStatus(jobs)).toMatchObject({
64
+ const grouped = groupJobsByStatus(jobs);
65
+ expect(grouped).toMatchObject({
63
66
  running: [jobs[0]],
64
67
  complete: [jobs[1]],
68
+ failed: [jobs[2]],
65
69
  waiting: [],
66
70
  });
67
- expect(getJobListStats(jobs).averageProgress).toBe(75);
71
+ expect(grouped).not.toHaveProperty("error");
72
+ expect(getJobListStats(jobs).averageProgress).toBe(58);
68
73
  expect(filterJobs(jobs, "alp")).toEqual([jobs[0]]);
69
74
  expect(filterJobs(jobs, "JOB-2")).toEqual([jobs[1]]);
70
75
  expect(filterJobs(jobs, "", { status: "complete", location: "complete" })).toEqual([jobs[1]]);
76
+ expect(filterJobs(jobs, "", { status: "failed" })).toEqual([jobs[2]]);
71
77
  });
72
78
 
73
79
  it("always includes zeroed costs in API output and computes aggregation stats", () => {
@@ -103,4 +109,39 @@ describe("list-transformer", () => {
103
109
  gate,
104
110
  });
105
111
  });
112
+
113
+ it("keeps an unreadable placeholder with null createdAt through aggregate and transform", () => {
114
+ const error = {
115
+ code: "INVALID_JSON",
116
+ message: "Repair or delete /path/to/tasks-status.json for job \"job-x\" to recover.",
117
+ path: "/path/to/tasks-status.json",
118
+ };
119
+ const placeholder = makeJob({
120
+ id: "job-x",
121
+ jobId: "job-x",
122
+ status: "failed",
123
+ createdAt: null,
124
+ readable: false,
125
+ error,
126
+ });
127
+
128
+ const aggregated = aggregateAndSortJobs([placeholder], []);
129
+ expect(aggregated).toHaveLength(1);
130
+ expect(aggregated[0]?.id).toBe("job-x");
131
+
132
+ const apiJob = transformJobListForAPI(aggregated)[0];
133
+ expect(apiJob?.jobId).toBe("job-x");
134
+ expect(apiJob?.readable).toBe(false);
135
+ expect(apiJob?.error).toEqual(error);
136
+ });
137
+
138
+ it("does not add readable or error to a normal job in API output", () => {
139
+ const normal = makeJob();
140
+
141
+ expect(aggregateAndSortJobs([normal], [])).toHaveLength(1);
142
+
143
+ const apiJob = transformJobListForAPI([normal])[0];
144
+ expect(apiJob).not.toHaveProperty("readable");
145
+ expect(apiJob).not.toHaveProperty("error");
146
+ });
106
147
  });
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
+ import { normalizeJobView } from "../../../../core/job-view";
3
4
  import {
5
+ buildUnreadableJob,
4
6
  computeJobStatus,
5
7
  getTransformationStats,
6
8
  transformJobStatus,
@@ -48,11 +50,11 @@ describe("status-transformer", () => {
48
50
  expect(job?.id).toBe("job-1");
49
51
  expect(job?.jobId).toBe("job-1");
50
52
  expect(job?.name).toBe("Title");
51
- expect(warn).toHaveBeenCalledOnce();
53
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("job id mismatch"));
52
54
  warn.mockRestore();
53
55
  });
54
56
 
55
- it("computes progress from tasks alone, ignoring any previously persisted value", () => {
57
+ it("computeJobStatus returns running when half the tasks are done", () => {
56
58
  const result = computeJobStatus({
57
59
  a: { state: "done" },
58
60
  b: { state: "done" },
@@ -63,7 +65,7 @@ describe("status-transformer", () => {
63
65
  expect(result).toEqual({ status: "running", progress: 50 });
64
66
  });
65
67
 
66
- it("counts skipped tasks as completed progress units", () => {
68
+ it("computeJobStatus returns pending when some tasks are pending and none running", () => {
67
69
  const result = computeJobStatus({
68
70
  a: { state: "done" },
69
71
  b: { state: "skipped" },
@@ -77,8 +79,8 @@ describe("status-transformer", () => {
77
79
  expect(computeJobStatus([])).toEqual({ status: "pending", progress: 0 });
78
80
  });
79
81
 
80
- it("transformJobStatus derives progress from tasks, not record.progress", () => {
81
- const job = transformJobStatus(
82
+ it("transformJobStatus reads finite record.progress before deriving from tasks", () => {
83
+ const withProgress = transformJobStatus(
82
84
  {
83
85
  title: "Test",
84
86
  progress: 100,
@@ -91,8 +93,44 @@ describe("status-transformer", () => {
91
93
  "current",
92
94
  );
93
95
 
94
- expect(job?.progress).toBe(50);
95
- expect(job?.status).toBe("running");
96
+ expect(withProgress?.progress).toBe(100);
97
+ expect(withProgress?.status).toBe("running");
98
+
99
+ const withoutProgress = transformJobStatus(
100
+ {
101
+ title: "Test",
102
+ tasks: {
103
+ a: { state: "done" },
104
+ },
105
+ },
106
+ "job-2",
107
+ "current",
108
+ );
109
+
110
+ expect(withoutProgress?.progress).toBe(100);
111
+ expect(withoutProgress?.status).toBe("complete");
112
+ });
113
+
114
+ it("transformJobStatus aggregates task tokenUsage costs through the canonical view", () => {
115
+ const job = transformJobStatus(
116
+ {
117
+ title: "Costed",
118
+ state: "done",
119
+ tasks: {
120
+ a: { state: "done", tokenUsage: [["openai:gpt-4o", 12, 6, 0.25]] },
121
+ b: { state: "done", tokenUsage: [["anthropic:claude", 3, 9, 0.5]] },
122
+ },
123
+ },
124
+ "job-cost",
125
+ "complete",
126
+ );
127
+
128
+ expect(job?.costs).toEqual({
129
+ totalCost: 0.75,
130
+ totalTokens: 30,
131
+ totalInputTokens: 15,
132
+ totalOutputTokens: 15,
133
+ });
96
134
  });
97
135
 
98
136
  it("surfaces waiting snapshots and gate metadata", () => {
@@ -138,7 +176,7 @@ describe("status-transformer", () => {
138
176
  "current",
139
177
  );
140
178
 
141
- expect(job?.status).toBe("error");
179
+ expect(job?.status).toBe("failed");
142
180
  });
143
181
 
144
182
  it("maps numeric restartCount on tasks", () => {
@@ -183,23 +221,15 @@ describe("status-transformer", () => {
183
221
  expect(tasks.alpha?.lastRetryError).toBeNull();
184
222
  });
185
223
 
186
- it("filters failed reads and computes transformation stats", () => {
187
- const transformed = transformMultipleJobs([
224
+ it("computes transformation stats for valid reads", () => {
225
+ const readResults = [
188
226
  { ok: true, data: { tasks: { a: { state: "done" } } }, jobId: "job-1", location: "current" },
189
- { ok: false, jobId: "job-2", location: "current" },
190
- ]);
227
+ ];
228
+ const transformed = transformMultipleJobs(readResults);
191
229
 
192
230
  expect(transformed).toHaveLength(1);
193
- expect(
194
- getTransformationStats(
195
- [
196
- { ok: true, data: { tasks: { a: { state: "done" } } }, jobId: "job-1", location: "current" },
197
- { ok: false, jobId: "job-2", location: "current" },
198
- ],
199
- transformed,
200
- ),
201
- ).toEqual({
202
- totalRead: 2,
231
+ expect(getTransformationStats(readResults, transformed)).toEqual({
232
+ totalRead: 1,
203
233
  successfulReads: 1,
204
234
  successfulTransforms: 1,
205
235
  failedTransforms: 0,
@@ -207,4 +237,160 @@ describe("status-transformer", () => {
207
237
  statusDistribution: { complete: 1 },
208
238
  });
209
239
  });
240
+
241
+ it("computes transformation stats without counting unreadable placeholders as successful transforms", () => {
242
+ const readResults = [
243
+ { ok: true, data: { tasks: { a: { state: "done" } } }, jobId: "job-1", location: "current" },
244
+ { ok: false, code: "INVALID_JSON", jobId: "job-2", location: "current" },
245
+ { ok: false, code: "JOB_NOT_FOUND", jobId: "job-3", location: "current" },
246
+ ];
247
+ const transformed = transformMultipleJobs(readResults);
248
+
249
+ expect(transformed).toHaveLength(2);
250
+ expect(getTransformationStats(readResults, transformed)).toEqual({
251
+ totalRead: 3,
252
+ successfulReads: 1,
253
+ successfulTransforms: 1,
254
+ failedTransforms: 0,
255
+ transformationRate: 1,
256
+ statusDistribution: { complete: 1, failed: 1 },
257
+ });
258
+ });
259
+
260
+ it("buildUnreadableJob maps corrupt reads to an error placeholder with recovery guidance", () => {
261
+ const placeholder = buildUnreadableJob({
262
+ ok: false,
263
+ code: "INVALID_JSON",
264
+ path: "/jobs/job-2/tasks-status.json",
265
+ jobId: "job-2",
266
+ location: "current",
267
+ });
268
+
269
+ expect(placeholder.readable).toBe(false);
270
+ expect(placeholder.status).toBe("failed");
271
+ expect(placeholder.error?.code).toBe("INVALID_JSON");
272
+ expect(placeholder.error?.path).toBe("/jobs/job-2/tasks-status.json");
273
+ const message = placeholder.error?.message.toLowerCase() ?? "";
274
+ expect(message).toContain("repair");
275
+ expect(message).toContain("delete");
276
+ expect(message).toContain("job-2");
277
+ expect(message).toContain("/jobs/job-2/tasks-status.json");
278
+ });
279
+
280
+ it("buildUnreadableJob uses the same fallback code in the message and structured error", () => {
281
+ const placeholder = buildUnreadableJob({
282
+ ok: false,
283
+ message: "EACCES",
284
+ jobId: "job-2",
285
+ location: "current",
286
+ });
287
+
288
+ expect(placeholder.error?.code).toBe("UNKNOWN_READ_ERROR");
289
+ expect(placeholder.error?.message).toContain("(UNKNOWN_READ_ERROR)");
290
+ expect(placeholder.error?.message).toContain("EACCES");
291
+ });
292
+
293
+ it("transformMultipleJobs maps corrupt reads to placeholders, skips missing reads, and leaves valid results readable", () => {
294
+ const transformed = transformMultipleJobs([
295
+ {
296
+ ok: true,
297
+ data: { tasks: { a: { state: "done" } } },
298
+ jobId: "job-1",
299
+ location: "current",
300
+ },
301
+ {
302
+ ok: false,
303
+ code: "INVALID_JSON",
304
+ path: "/jobs/job-2/tasks-status.json",
305
+ jobId: "job-2",
306
+ location: "current",
307
+ },
308
+ {
309
+ ok: false,
310
+ code: "JOB_NOT_FOUND",
311
+ jobId: "job-3",
312
+ location: "current",
313
+ },
314
+ ]);
315
+
316
+ expect(transformed).toHaveLength(2);
317
+ const [valid, unreadable] = transformed;
318
+
319
+ expect(valid?.status).toBe("complete");
320
+ expect(valid?.readable).toBeUndefined();
321
+ expect(valid?.error).toBeUndefined();
322
+
323
+ expect(unreadable?.readable).toBe(false);
324
+ expect(unreadable?.status).toBe("failed");
325
+ expect(unreadable?.error?.code).toBe("INVALID_JSON");
326
+ expect(unreadable?.error?.path).toBe("/jobs/job-2/tasks-status.json");
327
+ });
328
+
329
+ it("coerces parseable-but-tasks-missing snapshots to readable jobs", () => {
330
+ const transformed = transformMultipleJobs([
331
+ { ok: true, data: { jobId: "job-3" }, jobId: "job-3", location: "current" },
332
+ ]);
333
+
334
+ expect(transformed).toHaveLength(1);
335
+ const job = transformed[0];
336
+ expect(job?.readable).not.toBe(false);
337
+ expect(job?.error).toBeUndefined();
338
+ expect(job?.status).toBe("pending");
339
+ expect(job?.tasks).toEqual({});
340
+ });
341
+
342
+ it("transformJobStatus output equals normalizeJobView for object inputs", () => {
343
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
344
+ const raw = {
345
+ jobId: "job-1",
346
+ title: "Title",
347
+ state: "failed",
348
+ tasks: { alpha: { state: "done" } },
349
+ gate: null,
350
+ error: { name: "GateRejected", message: "gate rejected" },
351
+ };
352
+
353
+ const wrapped = transformJobStatus(raw, "job-1", "current");
354
+ const direct = normalizeJobView(raw, "job-1", "current");
355
+
356
+ expect(wrapped).not.toBeNull();
357
+ expect(wrapped).toEqual(direct);
358
+ warn.mockRestore();
359
+ });
360
+
361
+ it("transformJobStatus returns null for non-object raw input", () => {
362
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
363
+ expect(transformJobStatus(null, "job-1", "current")).toBeNull();
364
+ expect(transformJobStatus("not an object", "job-1", "current")).toBeNull();
365
+ expect(transformJobStatus([1, 2, 3], "job-1", "current")).toBeNull();
366
+ expect(warn).not.toHaveBeenCalled();
367
+ warn.mockRestore();
368
+ });
369
+
370
+ it("buildUnreadableJob returns status:'failed' and survives isValidJob", () => {
371
+ const placeholder = buildUnreadableJob({
372
+ ok: false,
373
+ code: "INVALID_JSON",
374
+ path: "/jobs/job-2/tasks-status.json",
375
+ jobId: "job-2",
376
+ location: "current",
377
+ });
378
+
379
+ expect(placeholder.status).toBe("failed");
380
+ expect(placeholder.readable).toBe(false);
381
+ expect(placeholder.id).toBe("job-2");
382
+ expect(placeholder.jobId).toBe("job-2");
383
+ expect(placeholder.error?.code).toBe("INVALID_JSON");
384
+ expect(placeholder.error?.path).toBe("/jobs/job-2/tasks-status.json");
385
+
386
+ expect(Boolean(placeholder.id && placeholder.status)).toBe(true);
387
+ });
388
+
389
+ it("transformTasks uses zero-based fallback ids for unnamed array entries", () => {
390
+ const unnamed = transformTasks([{}, {}]);
391
+ expect(Object.keys(unnamed)).toEqual(["task-0", "task-1"]);
392
+
393
+ const mixed = transformTasks([{ name: "alpha" }, {}]);
394
+ expect(Object.keys(mixed)).toEqual(["alpha", "task-1"]);
395
+ });
210
396
  });
@@ -12,7 +12,7 @@ import type {
12
12
  const STATUS_PRIORITY: Record<string, number> = {
13
13
  running: 4,
14
14
  waiting: 4,
15
- error: 3,
15
+ failed: 3,
16
16
  pending: 2,
17
17
  complete: 1,
18
18
  };
@@ -27,6 +27,7 @@ const ZERO_COSTS: CostsSummary = {
27
27
  };
28
28
 
29
29
  function isValidJob(job: CanonicalJob): boolean {
30
+ if (job.readable === false) return Boolean(job.id && job.status);
30
31
  return Boolean(job.id && job.status && job.createdAt);
31
32
  }
32
33
 
@@ -56,7 +57,7 @@ export function sortJobs(jobs: CanonicalJob[]): CanonicalJob[] {
56
57
  .sort((left, right) => {
57
58
  const priority = getStatusPriority(right.status) - getStatusPriority(left.status);
58
59
  if (priority !== 0) return priority;
59
- const created = left.createdAt!.localeCompare(right.createdAt!);
60
+ const created = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
60
61
  if (created !== 0) return created;
61
62
  return left.id.localeCompare(right.id);
62
63
  });
@@ -82,7 +83,7 @@ export function groupJobsByStatus(jobs: CanonicalJob[]): GroupedJobs {
82
83
  const grouped: GroupedJobs = {
83
84
  running: [],
84
85
  waiting: [],
85
- error: [],
86
+ failed: [],
86
87
  pending: [],
87
88
  complete: [],
88
89
  };
@@ -158,6 +159,9 @@ export function transformJobListForAPI(
158
159
  apiJob.pipelineConfig = job.pipelineConfig;
159
160
  }
160
161
 
162
+ if (job.readable !== undefined) apiJob.readable = job.readable;
163
+ if (job.error !== undefined) apiJob.error = job.error;
164
+
161
165
  return apiJob;
162
166
  });
163
167
  }