tickmarkr 1.84.0 → 1.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code.d.ts +1 -0
- package/dist/adapters/claude-code.js +57 -1
- package/dist/adapters/fake.js +9 -0
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +21 -0
- package/dist/cli/commands/status.js +160 -30
- package/dist/compile/collateral.d.ts +86 -2
- package/dist/compile/collateral.js +294 -3
- package/dist/config/config.d.ts +62 -0
- package/dist/config/config.js +157 -2
- package/dist/drivers/herdr.d.ts +20 -3
- package/dist/drivers/herdr.js +288 -105
- package/dist/gates/baseline.d.ts +1 -0
- package/dist/gates/baseline.js +91 -13
- package/dist/gates/review.d.ts +7 -0
- package/dist/gates/review.js +99 -6
- package/dist/gates/run-gates.d.ts +9 -0
- package/dist/gates/run-gates.js +285 -41
- package/dist/run/daemon.d.ts +48 -2
- package/dist/run/daemon.js +1417 -315
- package/dist/run/journal.d.ts +56 -3
- package/dist/run/journal.js +275 -1
- package/dist/run/stall.d.ts +35 -1
- package/dist/run/stall.js +118 -8
- package/dist/tui/cockpit/components.d.ts +2 -0
- package/dist/tui/cockpit/derive.d.ts +29 -2
- package/dist/tui/cockpit/derive.js +219 -23
- package/dist/tui/cockpit/run-cockpit.js +128 -27
- package/package.json +1 -1
|
@@ -3,7 +3,14 @@ import type { RunViewId } from "./views.js";
|
|
|
3
3
|
export declare const SPARKLINE_BUCKET_WINDOW = 12;
|
|
4
4
|
export declare function selectSparklineBucketWidthMs(elapsedMs: number): number;
|
|
5
5
|
type MetricSample = number | null;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* What the run is, in one word. `parked` is first-class beside the rest because
|
|
8
|
+
* a run holding an unresolved `human` task owes the operator a verb — it is not
|
|
9
|
+
* done, and calling it done is the render OBS-252 caught. The task-level word
|
|
10
|
+
* for the same fact stays `human` (TaskState): that is the event the journal
|
|
11
|
+
* writes, and `parked` is what the fold of it means for the run.
|
|
12
|
+
*/
|
|
13
|
+
export type RunStatus = "done" | "failed" | "interrupted" | "parked" | "running";
|
|
7
14
|
export type RunCockpitData = {
|
|
8
15
|
readonly binaryVersion: string;
|
|
9
16
|
readonly runId: string;
|
|
@@ -80,12 +87,30 @@ export type TaskRow = {
|
|
|
80
87
|
readonly attempts?: number;
|
|
81
88
|
readonly actor?: string;
|
|
82
89
|
readonly lastEventTime?: string;
|
|
90
|
+
/** Complete UTC instant paired with `lastEventTime`; additive for existing consumers. */
|
|
91
|
+
readonly lastEventTimestamp?: string;
|
|
83
92
|
readonly title?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The park the task is standing in right now, as the daemon recorded it
|
|
95
|
+
* (`task-human` data.kind). A resolved park has no kind at all: the badge
|
|
96
|
+
* retires with the state it belonged to, so a merged task never wears the
|
|
97
|
+
* `gate-fail` it was released from an hour earlier (OBS-256).
|
|
98
|
+
*/
|
|
99
|
+
readonly parkKind?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Recorded merged, and only then. A task the journal carried to `done` has
|
|
102
|
+
* finished its work; a task the journal recorded a `merge` for has landed.
|
|
103
|
+
* The two are different rows, so a consumer drawing a check-mark has the fact
|
|
104
|
+
* that earns one rather than having to read `done` as if it were the same.
|
|
105
|
+
*/
|
|
106
|
+
readonly merged?: true;
|
|
84
107
|
};
|
|
85
108
|
/** One recorded gate result. `details` is the record's own text, verbatim. */
|
|
86
109
|
export type GateRow = {
|
|
87
110
|
readonly id: string;
|
|
88
111
|
readonly time: string;
|
|
112
|
+
/** Complete UTC instant paired with `time`; additive for existing consumers. */
|
|
113
|
+
readonly timestamp?: string;
|
|
89
114
|
readonly state: ComponentState;
|
|
90
115
|
readonly gate?: string;
|
|
91
116
|
readonly taskId?: string;
|
|
@@ -99,8 +124,10 @@ export type FleetRow = {
|
|
|
99
124
|
readonly model: string;
|
|
100
125
|
readonly dispatches?: number;
|
|
101
126
|
readonly lastEventTime?: string;
|
|
127
|
+
/** Complete UTC instant paired with `lastEventTime`; additive for existing consumers. */
|
|
128
|
+
readonly lastEventTimestamp?: string;
|
|
102
129
|
};
|
|
103
|
-
export type TaskState = "done" | "failed" | "human" | "pending" | "running" | "interrupted";
|
|
130
|
+
export type TaskState = "completed" | "done" | "failed" | "human" | "pending" | "running" | "interrupted";
|
|
104
131
|
/**
|
|
105
132
|
* The source identities the named view's rows carry, in draw order. Selection
|
|
106
133
|
* is repaired against these, so it follows a row across a refresh instead of
|
|
@@ -116,6 +116,75 @@ function daemonPid(events) {
|
|
|
116
116
|
}
|
|
117
117
|
return undefined;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* The state an event writes onto the task it names — the whole list of them, so
|
|
121
|
+
* "did anything move this task" is asked in one place. `task-dispatch` and
|
|
122
|
+
* `merge` carry more than a state and are folded by hand below; they are named
|
|
123
|
+
* here because they move one all the same.
|
|
124
|
+
*
|
|
125
|
+
* A Map, because an event name is whatever the journal wrote: an object's own
|
|
126
|
+
* lookup answers `constructor` and `toString` with something inherited and
|
|
127
|
+
* truthy, so a line naming one would masquerade as a state transition — writing
|
|
128
|
+
* a function where a TaskState belongs and releasing a park the run declared.
|
|
129
|
+
* A Map answers only for the keys stated here.
|
|
130
|
+
*/
|
|
131
|
+
const TASK_STATE_BY_EVENT = new Map([
|
|
132
|
+
["task-dispatch", "running"],
|
|
133
|
+
["merge", "done"],
|
|
134
|
+
["task-done", "done"],
|
|
135
|
+
["task-failed", "failed"],
|
|
136
|
+
["task-human", "human"],
|
|
137
|
+
["task-approved", "pending"],
|
|
138
|
+
]);
|
|
139
|
+
/** Only task identities the summary actually records can enter its fold. */
|
|
140
|
+
function summaryTaskIds(value) {
|
|
141
|
+
return Array.isArray(value)
|
|
142
|
+
? value.filter((id) => typeof id === "string")
|
|
143
|
+
: [];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The parks the latest `run-end` says the daemon stopped holding, that nothing
|
|
147
|
+
* since resolved. The summary is the daemon's own closing statement, so a run
|
|
148
|
+
* whose end names a task in `human` owes the operator a verb whether or not a
|
|
149
|
+
* `task-human` line happens to precede it — that dependence is what let a run
|
|
150
|
+
* end read `done` over a parked task (OBS-252).
|
|
151
|
+
*
|
|
152
|
+
* "Since" is measured from the summary forward, and only forward. The summary
|
|
153
|
+
* describes the moment the daemon exited, so a state written *before* it —
|
|
154
|
+
* the dispatch it parked, the completion of a round it then held for review —
|
|
155
|
+
* is history the summary already accounts for and cannot answer it. Weighing
|
|
156
|
+
* the whole-journal fold against it let a line above the summary retire a park
|
|
157
|
+
* the summary itself declares: `run-start · task-dispatch T1 · run-end
|
|
158
|
+
* human:[T1]` read `interrupted`, and the same journal with a `task-done T1`
|
|
159
|
+
* read `done` — OBS-252 again, one line lower.
|
|
160
|
+
*
|
|
161
|
+
* So a name is resolved only when a later event moved that task: approved,
|
|
162
|
+
* redispatched, finished, landed. Nothing later — including a task the record
|
|
163
|
+
* never mentions at all — leaves the park standing, because an uncorroborated
|
|
164
|
+
* park is still the run's own claim that it is waiting and withholding `done`
|
|
165
|
+
* on a claim the surface cannot check is the fail-closed reading. It stays a
|
|
166
|
+
* reading of the run and of the tasks it names: a summary is a journal event,
|
|
167
|
+
* so its explicit task outcomes are facts the board must count and draw even
|
|
168
|
+
* when the more detailed task event is absent.
|
|
169
|
+
*/
|
|
170
|
+
function unresolvedSummaryParks(events) {
|
|
171
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
172
|
+
const event = events[index];
|
|
173
|
+
if (event.event !== "run-end")
|
|
174
|
+
continue;
|
|
175
|
+
const parked = Array.isArray(event.data.human) ? event.data.human : [];
|
|
176
|
+
const released = new Map();
|
|
177
|
+
for (const later of events.slice(index + 1)) {
|
|
178
|
+
if (later.taskId === undefined)
|
|
179
|
+
continue;
|
|
180
|
+
if (!TASK_STATE_BY_EVENT.has(later.event))
|
|
181
|
+
continue;
|
|
182
|
+
released.set(later.taskId, later.event !== "task-human");
|
|
183
|
+
}
|
|
184
|
+
return parked.filter((id) => typeof id === "string" && released.get(id) !== true);
|
|
185
|
+
}
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
119
188
|
function deriveTasks(events, interrupted) {
|
|
120
189
|
const tasks = new Map();
|
|
121
190
|
const task = (taskId, index, ts) => {
|
|
@@ -126,6 +195,8 @@ function deriveTasks(events, interrupted) {
|
|
|
126
195
|
id: taskId,
|
|
127
196
|
dispatches: [],
|
|
128
197
|
state: undefined,
|
|
198
|
+
parkKind: undefined,
|
|
199
|
+
merged: false,
|
|
129
200
|
lastIndex: index,
|
|
130
201
|
lastTs: ts,
|
|
131
202
|
phase: "pending",
|
|
@@ -134,6 +205,30 @@ function deriveTasks(events, interrupted) {
|
|
|
134
205
|
return created;
|
|
135
206
|
};
|
|
136
207
|
for (const [index, event] of events.entries()) {
|
|
208
|
+
if (event.event === "run-end") {
|
|
209
|
+
// The closing summary is a recorded fold, not commentary. Apply all
|
|
210
|
+
// task outcomes it owns before later resume events get their ordinary
|
|
211
|
+
// chance to supersede them. Failure comes last so a malformed overlap
|
|
212
|
+
// fails closed instead of repainting a failed task done.
|
|
213
|
+
const summaries = [
|
|
214
|
+
[event.data.done, "done"],
|
|
215
|
+
[event.data.human, "human"],
|
|
216
|
+
[event.data.failed, "failed"],
|
|
217
|
+
];
|
|
218
|
+
for (const [ids, state] of summaries) {
|
|
219
|
+
for (const taskId of summaryTaskIds(ids)) {
|
|
220
|
+
const fact = task(taskId, index, event.ts);
|
|
221
|
+
fact.state = state;
|
|
222
|
+
// A summary does not invent a park kind; it preserves one only when
|
|
223
|
+
// it confirms that the same park still stands.
|
|
224
|
+
if (state !== "human")
|
|
225
|
+
fact.parkKind = undefined;
|
|
226
|
+
fact.lastIndex = index;
|
|
227
|
+
fact.lastTs = event.ts;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
137
232
|
if (!event.taskId)
|
|
138
233
|
continue;
|
|
139
234
|
if (event.event === "task-dispatch") {
|
|
@@ -148,11 +243,25 @@ function deriveTasks(events, interrupted) {
|
|
|
148
243
|
assignment: assignmentFrom(event.data.assignment),
|
|
149
244
|
});
|
|
150
245
|
fact.state = "running";
|
|
246
|
+
fact.parkKind = undefined;
|
|
151
247
|
fact.phase = "worker";
|
|
152
248
|
fact.lastIndex = index;
|
|
153
249
|
fact.lastTs = event.ts;
|
|
154
250
|
continue;
|
|
155
251
|
}
|
|
252
|
+
if (event.event === "merge") {
|
|
253
|
+
const fact = task(event.taskId, index, event.ts);
|
|
254
|
+
fact.merged = true;
|
|
255
|
+
// A merge is a landing, and it is a state the journal recorded: the task
|
|
256
|
+
// is done, and whatever park it was released from is retired by the merge
|
|
257
|
+
// itself rather than by whichever event happened to precede it. Nothing
|
|
258
|
+
// here depends on a `task-done` line arriving first (OBS-256).
|
|
259
|
+
fact.state = "done";
|
|
260
|
+
fact.parkKind = undefined;
|
|
261
|
+
fact.lastIndex = index;
|
|
262
|
+
fact.lastTs = event.ts;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
156
265
|
if (event.event === "phase-start") {
|
|
157
266
|
const fact = task(event.taskId, index, event.ts);
|
|
158
267
|
fact.phase = typeof event.data.phase === "string" ? event.data.phase : fact.phase;
|
|
@@ -160,15 +269,19 @@ function deriveTasks(events, interrupted) {
|
|
|
160
269
|
fact.lastTs = event.ts;
|
|
161
270
|
continue;
|
|
162
271
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"task-approved": "pending",
|
|
168
|
-
}[event.event];
|
|
272
|
+
// `task-dispatch` and `merge` returned above; the rest write their state and
|
|
273
|
+
// nothing else. One list, read from one place, so a new state-writing event
|
|
274
|
+
// cannot move the fold without also moving what resolves a park.
|
|
275
|
+
const state = TASK_STATE_BY_EVENT.get(event.event);
|
|
169
276
|
if (state) {
|
|
170
277
|
const fact = task(event.taskId, index, event.ts);
|
|
171
278
|
fact.state = state;
|
|
279
|
+
// The park kind travels with the park: `task-human` records it, and the
|
|
280
|
+
// next recorded state — approved, done, failed — retires it with the park
|
|
281
|
+
// it named. A resolved state must not leave its badge behind (OBS-256).
|
|
282
|
+
fact.parkKind = state === "human" && typeof event.data.kind === "string"
|
|
283
|
+
? event.data.kind
|
|
284
|
+
: undefined;
|
|
172
285
|
fact.lastIndex = index;
|
|
173
286
|
fact.lastTs = event.ts;
|
|
174
287
|
}
|
|
@@ -250,14 +363,42 @@ function recordsFailure(event) {
|
|
|
250
363
|
|| event.data.pass === false
|
|
251
364
|
|| event.data.ok === false
|
|
252
365
|
|| (typeof event.data.exitCode === "number" && event.data.exitCode !== 0)
|
|
253
|
-
|| (event.event === "run-end"
|
|
366
|
+
|| (event.event === "run-end"
|
|
367
|
+
&& (event.data.tipVerify === "failed"
|
|
368
|
+
|| summaryTaskIds(event.data.failed).length > 0));
|
|
254
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* The outcome an event records as a success. `merge` is here and `task-done` is
|
|
372
|
+
* not: a merge records the landing, and a completion records only that the
|
|
373
|
+
* worker stopped working — the gates that judge it and the merge that lands it
|
|
374
|
+
* come after. A `run-end` counts only when its tip verification reads passed:
|
|
375
|
+
* an absent verdict is nothing verified, and a check-mark on nothing verified
|
|
376
|
+
* is the run claiming a pass tickmarkr never corroborated — the same
|
|
377
|
+
* fail-closed reading the status strip already draws (OBS-244), now on the row.
|
|
378
|
+
*/
|
|
255
379
|
function recordsSuccess(event) {
|
|
256
|
-
return event.event === "
|
|
257
|
-
|| event.event === "merge"
|
|
380
|
+
return event.event === "merge"
|
|
258
381
|
|| event.data.pass === true
|
|
259
382
|
|| event.data.ok === true
|
|
260
|
-
|| (event.event === "run-end" && event.data.tipVerify
|
|
383
|
+
|| (event.event === "run-end" && event.data.tipVerify === "passed");
|
|
384
|
+
}
|
|
385
|
+
/** The newest run-end speaks for the run; spotlight is context, not outcome. */
|
|
386
|
+
function runPresentation(runStatus, spotlight) {
|
|
387
|
+
switch (runStatus) {
|
|
388
|
+
case "failed":
|
|
389
|
+
return { state: "fail", word: "failed" };
|
|
390
|
+
case "parked":
|
|
391
|
+
return { state: "warn", word: "parked" };
|
|
392
|
+
case "interrupted":
|
|
393
|
+
return { state: "warn", word: "interrupted" };
|
|
394
|
+
case "running":
|
|
395
|
+
return { state: "neutral", word: "running" };
|
|
396
|
+
case "done":
|
|
397
|
+
// A task named beside the run still earns a check only from its merge.
|
|
398
|
+
return spotlight === undefined || spotlight.merged
|
|
399
|
+
? { state: "pass", word: "pass" }
|
|
400
|
+
: { state: "neutral", word: "done" };
|
|
401
|
+
}
|
|
261
402
|
}
|
|
262
403
|
/**
|
|
263
404
|
* A row depicts the event it names: the event's own recorded outcome decides
|
|
@@ -268,11 +409,25 @@ function recordsSuccess(event) {
|
|
|
268
409
|
* Every state carries its word, so meaning never rides on hue alone.
|
|
269
410
|
*/
|
|
270
411
|
function eventPresentation(event, task) {
|
|
271
|
-
if (event.event === "escalation"
|
|
412
|
+
if (event.event === "escalation")
|
|
272
413
|
return { state: "warn", word: "warn" };
|
|
273
|
-
|
|
414
|
+
// A park is a state, not a severity: the row says what the engagement is
|
|
415
|
+
// waiting for, which is the operator.
|
|
416
|
+
if (event.event === "task-human")
|
|
417
|
+
return { state: "warn", word: "parked" };
|
|
274
418
|
if (recordsFailure(event))
|
|
275
419
|
return { state: "fail", word: "fail" };
|
|
420
|
+
if (event.event === "run-end"
|
|
421
|
+
&& summaryTaskIds(event.data.human).length > 0) {
|
|
422
|
+
return { state: "warn", word: "parked" };
|
|
423
|
+
}
|
|
424
|
+
// Work finished is not work landed. `task-done` records the first, so its row
|
|
425
|
+
// says `done` and carries no check: the check-mark belongs to the `merge` that
|
|
426
|
+
// records the landing, and until the journal writes one there is nothing to
|
|
427
|
+
// put it on (OBS-256). This is the same reading `taskPresentation` gives the
|
|
428
|
+
// task itself, stated here for the event because a row depicts its own event.
|
|
429
|
+
if (event.event === "task-done")
|
|
430
|
+
return { state: "neutral", word: "done" };
|
|
276
431
|
if (recordsSuccess(event))
|
|
277
432
|
return { state: "pass", word: "pass" };
|
|
278
433
|
if (task?.state === "interrupted") {
|
|
@@ -282,19 +437,29 @@ function eventPresentation(event, task) {
|
|
|
282
437
|
}
|
|
283
438
|
function historyRow(event, tasks, spotlight, newest, runStatus) {
|
|
284
439
|
const task = event.taskId === undefined ? undefined : tasks.get(event.taskId);
|
|
285
|
-
|
|
440
|
+
// Spotlight is context only for the taskless closing summary. A task-owned
|
|
441
|
+
// newest event keeps its own identity and outcome: T2's dispatch or failure
|
|
442
|
+
// cannot become a pass for merged T1 merely because T1 wins the spotlight.
|
|
443
|
+
const summary = newest && event.event === "run-end"
|
|
444
|
+
? runPresentation(runStatus, spotlight)
|
|
445
|
+
: undefined;
|
|
446
|
+
const { state, word } = summary ?? eventPresentation(event, task);
|
|
447
|
+
const displayedTask = newest
|
|
448
|
+
? event.taskId === undefined ? spotlight : task
|
|
449
|
+
: task;
|
|
286
450
|
// The spotlight entry must keep the task, its attempt and its acting adapter
|
|
287
451
|
// inside the narrowest band's width budget — that budget cannot pay for the
|
|
288
452
|
// separators and the event name, so the state word and the run's status
|
|
289
453
|
// (what the newest event recorded) ride after the adapter instead.
|
|
290
|
-
const text = newest &&
|
|
291
|
-
? `${
|
|
454
|
+
const text = newest && displayedTask !== undefined
|
|
455
|
+
? `${displayedTask.id} attempt ${taskAttempt(displayedTask)} ${taskActor(displayedTask)} ${word}${word === runStatus ? "" : ` ${runStatus}`}`
|
|
292
456
|
: task !== undefined
|
|
293
457
|
? `${task.id} ${word} · ${event.event}`
|
|
294
458
|
: `${word} · ${event.event}`;
|
|
295
459
|
return {
|
|
296
460
|
id: `event:${event.line}`,
|
|
297
461
|
time: eventTime(event.ts),
|
|
462
|
+
timestamp: event.ts,
|
|
298
463
|
state,
|
|
299
464
|
text,
|
|
300
465
|
};
|
|
@@ -332,14 +497,28 @@ function taskRows(events, tasks, graph) {
|
|
|
332
497
|
// zero when a run resumes, so a task dispatched 0,1,0 was tried three times
|
|
333
498
|
// and reporting its last label would say one.
|
|
334
499
|
const attempts = fact?.dispatches.length ?? 0;
|
|
500
|
+
// `task-done` records completion; only `merge` records landing. The fixed
|
|
501
|
+
// Tasks-view consumer maps `done` to a check, so the derived row must keep
|
|
502
|
+
// an unmerged completion in the neutral `completed` state and reserve
|
|
503
|
+
// `done` for a fact whose merge the journal actually carries.
|
|
504
|
+
const state = fact?.state === "done" && fact.merged !== true
|
|
505
|
+
? "completed"
|
|
506
|
+
: fact?.state;
|
|
335
507
|
return {
|
|
336
508
|
id: `task:${id}`,
|
|
337
509
|
taskId: id,
|
|
338
|
-
...(
|
|
510
|
+
...(state === undefined ? {} : { state }),
|
|
339
511
|
...(attempts === 0 ? {} : { attempts }),
|
|
340
512
|
...(assignment === undefined ? {} : { actor: channelKey(assignment) }),
|
|
341
513
|
...(lastTs === undefined ? {} : { lastEventTime: eventTime(lastTs) }),
|
|
514
|
+
...(lastTs === undefined ? {} : { lastEventTimestamp: lastTs }),
|
|
342
515
|
...(title === undefined ? {} : { title }),
|
|
516
|
+
// Only while the park stands: the fold cleared it the moment the journal
|
|
517
|
+
// recorded what the park resolved to.
|
|
518
|
+
...(fact?.parkKind === undefined ? {} : { parkKind: fact.parkKind }),
|
|
519
|
+
// Landing is its own fact. `done` is work finished; only a recorded merge
|
|
520
|
+
// is work landed, and only that earns a check-mark.
|
|
521
|
+
...(fact?.merged === true ? { merged: true } : {}),
|
|
343
522
|
};
|
|
344
523
|
});
|
|
345
524
|
}
|
|
@@ -356,6 +535,7 @@ function gateRows(events) {
|
|
|
356
535
|
return [{
|
|
357
536
|
id: `gate:${event.line}`,
|
|
358
537
|
time: eventTime(event.ts),
|
|
538
|
+
timestamp: event.ts,
|
|
359
539
|
state: pass === true ? "pass" : pass === false ? "fail" : "neutral",
|
|
360
540
|
...(typeof event.data.gate === "string" ? { gate: event.data.gate } : {}),
|
|
361
541
|
...(event.taskId === undefined ? {} : { taskId: event.taskId }),
|
|
@@ -421,6 +601,7 @@ function fleetRows(events) {
|
|
|
421
601
|
model: channel.model,
|
|
422
602
|
...(channel.dispatches === 0 ? {} : { dispatches: channel.dispatches }),
|
|
423
603
|
lastEventTime: eventTime(channel.lastTs),
|
|
604
|
+
lastEventTimestamp: channel.lastTs,
|
|
424
605
|
}));
|
|
425
606
|
}
|
|
426
607
|
/**
|
|
@@ -535,15 +716,30 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
|
535
716
|
const tipPassed = tipVerificationPassed(events);
|
|
536
717
|
const spotlight = spotlightTask(tasks);
|
|
537
718
|
const samples = bucketedMetricSamples(events);
|
|
719
|
+
// A task the journal carried somewhere other than done — approved and awaiting
|
|
720
|
+
// the resume that redispatches it, most of all. `done` is a claim about all of
|
|
721
|
+
// them, so one outstanding task retires it.
|
|
722
|
+
const outstanding = taskFacts.some((task) => task.state !== undefined && task.state !== "done");
|
|
723
|
+
// Parked outranks every reading but failure: a run holding an unresolved
|
|
724
|
+
// `human` task owes the operator a verb, and the daemon's exit after a park is
|
|
725
|
+
// the park's own consequence — not an interruption, and never done. A run that
|
|
726
|
+
// also failed a task owes a fix first, so failure still leads (OBS-252).
|
|
727
|
+
//
|
|
728
|
+
// The word is owed on two readings, not one: the parks the fold holds, and
|
|
729
|
+
// the parks the latest run-end names and nothing since resolved. The second
|
|
730
|
+
// is what keeps the reading off the luck of a `task-human` line preceding the
|
|
731
|
+
// run-end — the run's own closing statement is enough.
|
|
538
732
|
const runStatus = failed > 0
|
|
539
733
|
? "failed"
|
|
540
|
-
:
|
|
541
|
-
? "
|
|
542
|
-
: lifecycle === "
|
|
543
|
-
? "
|
|
544
|
-
:
|
|
545
|
-
? "interrupted"
|
|
546
|
-
:
|
|
734
|
+
: human > 0 || unresolvedSummaryParks(events).length > 0
|
|
735
|
+
? "parked"
|
|
736
|
+
: lifecycle === "superseded" || hasInterruptedTask
|
|
737
|
+
? "interrupted"
|
|
738
|
+
: lifecycle === "completed"
|
|
739
|
+
? outstanding ? "interrupted" : "done"
|
|
740
|
+
: interrupted
|
|
741
|
+
? "interrupted"
|
|
742
|
+
: "running";
|
|
547
743
|
return {
|
|
548
744
|
binaryVersion,
|
|
549
745
|
runId: source.fileName.replace(/(?:\.interrupted)?\.journal\.jsonl$/, ""),
|