@h-rig/run-plugin 0.0.6-alpha.186
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/README.md +1 -0
- package/dist/src/plugin.d.ts +4 -0
- package/dist/src/plugin.js +4319 -0
- package/dist/src/read-model/inspect-command.d.ts +19 -0
- package/dist/src/read-model/inspect-command.js +341 -0
- package/dist/src/read-model/plugin.d.ts +4 -0
- package/dist/src/read-model/plugin.js +2550 -0
- package/dist/src/read-model/read-model-backend/diagnostics.d.ts +9 -0
- package/dist/src/read-model/read-model-backend/diagnostics.js +51 -0
- package/dist/src/read-model/read-model-backend/guard.d.ts +4 -0
- package/dist/src/read-model/read-model-backend/guard.js +25 -0
- package/dist/src/read-model/read-model-backend/inbox.d.ts +23 -0
- package/dist/src/read-model/read-model-backend/inbox.js +669 -0
- package/dist/src/read-model/read-model-backend/index.d.ts +8 -0
- package/dist/src/read-model/read-model-backend/index.js +1163 -0
- package/dist/src/read-model/read-model-backend/inspect.d.ts +26 -0
- package/dist/src/read-model/read-model-backend/inspect.js +770 -0
- package/dist/src/read-model/read-model-backend/projection.d.ts +39 -0
- package/dist/src/read-model/read-model-backend/projection.js +669 -0
- package/dist/src/read-model/read-model-backend/run-status.d.ts +27 -0
- package/dist/src/read-model/read-model-backend/run-status.js +237 -0
- package/dist/src/read-model/read-model-backend/stats.d.ts +12 -0
- package/dist/src/read-model/read-model-backend/stats.js +800 -0
- package/dist/src/read-model/read-model-service.d.ts +2 -0
- package/dist/src/read-model/read-model-service.js +1725 -0
- package/dist/src/read-model/reconcile.d.ts +23 -0
- package/dist/src/read-model/reconcile.js +306 -0
- package/dist/src/read-model/run-format.d.ts +23 -0
- package/dist/src/read-model/run-format.js +202 -0
- package/dist/src/read-model/runs-screen.d.ts +14 -0
- package/dist/src/read-model/runs-screen.js +217 -0
- package/dist/src/read-model/session-journal.d.ts +26 -0
- package/dist/src/read-model/session-journal.js +355 -0
- package/dist/src/read-model/stats-command.d.ts +16 -0
- package/dist/src/read-model/stats-command.js +88 -0
- package/dist/src/read-model/stats-format.d.ts +1 -0
- package/dist/src/read-model/stats-format.js +8 -0
- package/dist/src/worker/autohost.d.ts +11 -0
- package/dist/src/worker/autohost.js +858 -0
- package/dist/src/worker/constants.d.ts +3 -0
- package/dist/src/worker/constants.js +10 -0
- package/dist/src/worker/extension.d.ts +14 -0
- package/dist/src/worker/extension.js +881 -0
- package/dist/src/worker/host.d.ts +1 -0
- package/dist/src/worker/host.js +1 -0
- package/dist/src/worker/inbox-command.d.ts +23 -0
- package/dist/src/worker/inbox-command.js +163 -0
- package/dist/src/worker/index.d.ts +2 -0
- package/dist/src/worker/index.js +1767 -0
- package/dist/src/worker/local-run-changes.d.ts +3 -0
- package/dist/src/worker/local-run-changes.js +65 -0
- package/dist/src/worker/notifications.d.ts +1 -0
- package/dist/src/worker/notifications.js +27 -0
- package/dist/src/worker/notify-cap.d.ts +11 -0
- package/dist/src/worker/notify-cap.js +13 -0
- package/dist/src/worker/panel-plugin.d.ts +11 -0
- package/dist/src/worker/panel-plugin.js +37 -0
- package/dist/src/worker/plugin.d.ts +3 -0
- package/dist/src/worker/plugin.js +1761 -0
- package/dist/src/worker/run-control-service.d.ts +2 -0
- package/dist/src/worker/run-control-service.js +210 -0
- package/dist/src/worker/session-journal-writer.d.ts +4 -0
- package/dist/src/worker/session-journal-writer.js +184 -0
- package/dist/src/worker/stall.d.ts +21 -0
- package/dist/src/worker/stall.js +55 -0
- package/dist/src/worker/utils.d.ts +21 -0
- package/dist/src/worker/utils.js +29 -0
- package/dist/src/worker/workflow-journal-writer.d.ts +7 -0
- package/dist/src/worker/workflow-journal-writer.js +76 -0
- package/package.json +47 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
|
|
18
|
+
// packages/run-plugin/src/read-model/read-model-backend/run-status.ts
|
|
19
|
+
function isRuntimeActiveStatus(status) {
|
|
20
|
+
return RUNTIME_ACTIVE_STATUSES.has(normalizeRunStatusToken(status));
|
|
21
|
+
}
|
|
22
|
+
function normalizeRunStatusToken(status) {
|
|
23
|
+
return String(status ?? "").trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
24
|
+
}
|
|
25
|
+
function canonicalStatusToken(status) {
|
|
26
|
+
const normalized = normalizeRunStatusToken(status);
|
|
27
|
+
if (normalized === "waiting-input")
|
|
28
|
+
return "waiting-user-input";
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
function isNeedsAttention(run) {
|
|
32
|
+
return canonicalStatusToken(run.status) === "needs-attention" || run.pendingApprovals + run.pendingInputs > 0 || run.stallCount > 0;
|
|
33
|
+
}
|
|
34
|
+
var OPERATOR_INACTIVE_RUN_STATUSES, TERMINAL_RUN_STATUSES, RUNTIME_ACTIVE_STATUSES, ACTIVE_RUN_STATUSES, KNOWN_RUN_STATUS;
|
|
35
|
+
var init_run_status = __esm(() => {
|
|
36
|
+
OPERATOR_INACTIVE_RUN_STATUSES = new Set([
|
|
37
|
+
"completed",
|
|
38
|
+
"complete",
|
|
39
|
+
"done",
|
|
40
|
+
"success",
|
|
41
|
+
"succeeded",
|
|
42
|
+
"passed",
|
|
43
|
+
"failed",
|
|
44
|
+
"failure",
|
|
45
|
+
"error",
|
|
46
|
+
"errored",
|
|
47
|
+
"stopped",
|
|
48
|
+
"cancelled",
|
|
49
|
+
"canceled",
|
|
50
|
+
"aborted",
|
|
51
|
+
"abort",
|
|
52
|
+
"merged",
|
|
53
|
+
"closed",
|
|
54
|
+
"skipped",
|
|
55
|
+
"needs-attention",
|
|
56
|
+
"needs_attention"
|
|
57
|
+
]);
|
|
58
|
+
TERMINAL_RUN_STATUSES = ["completed", "failed", "stopped"];
|
|
59
|
+
RUNTIME_ACTIVE_STATUSES = new Set([
|
|
60
|
+
"created",
|
|
61
|
+
"preparing",
|
|
62
|
+
"running",
|
|
63
|
+
"validating",
|
|
64
|
+
"reviewing",
|
|
65
|
+
"closing-out"
|
|
66
|
+
]);
|
|
67
|
+
ACTIVE_RUN_STATUSES = [
|
|
68
|
+
"created",
|
|
69
|
+
"queued",
|
|
70
|
+
"preparing",
|
|
71
|
+
"running",
|
|
72
|
+
"waiting-approval",
|
|
73
|
+
"waiting-user-input",
|
|
74
|
+
"paused",
|
|
75
|
+
"validating",
|
|
76
|
+
"reviewing",
|
|
77
|
+
"closing-out",
|
|
78
|
+
"needs-attention"
|
|
79
|
+
];
|
|
80
|
+
KNOWN_RUN_STATUS = Object.fromEntries([...ACTIVE_RUN_STATUSES, ...TERMINAL_RUN_STATUSES].map((status) => [status, true]));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// packages/run-plugin/src/read-model/session-journal.ts
|
|
84
|
+
import { Schema } from "effect";
|
|
85
|
+
import {
|
|
86
|
+
CUSTOM_TYPE_FOR,
|
|
87
|
+
RIG_CONTROL_SENTINEL_END,
|
|
88
|
+
RIG_INBOX_RESOLUTION_SENTINEL,
|
|
89
|
+
RIG_PAUSE_SENTINEL,
|
|
90
|
+
RIG_RESUME_SENTINEL,
|
|
91
|
+
RIG_STOP_SENTINEL,
|
|
92
|
+
RIG_STOP_SENTINEL_END,
|
|
93
|
+
RunJournalEvent,
|
|
94
|
+
TYPE_FOR_CUSTOM
|
|
95
|
+
} from "@rig/contracts";
|
|
96
|
+
function isTerminalRunStatus(status) {
|
|
97
|
+
return TERMINAL_RUN_STATUSES2.includes(status);
|
|
98
|
+
}
|
|
99
|
+
function canTransitionRunStatus(from, to) {
|
|
100
|
+
if (from === null)
|
|
101
|
+
return true;
|
|
102
|
+
if (from === to)
|
|
103
|
+
return true;
|
|
104
|
+
return RUN_STATUS_TRANSITIONS[from].includes(to);
|
|
105
|
+
}
|
|
106
|
+
function reduceRunJournal(events, runId = null) {
|
|
107
|
+
let record = {};
|
|
108
|
+
let status = null;
|
|
109
|
+
const statusHistory = [];
|
|
110
|
+
const pendingApprovals = new Map;
|
|
111
|
+
const resolvedApprovals = [];
|
|
112
|
+
const pendingUserInputs = new Map;
|
|
113
|
+
const resolvedUserInputs = [];
|
|
114
|
+
const closeoutPhases = [];
|
|
115
|
+
let resolvedPipeline = null;
|
|
116
|
+
const stageOutcomes = [];
|
|
117
|
+
const anomalies = [];
|
|
118
|
+
let steeringCount = 0;
|
|
119
|
+
let stallCount = 0;
|
|
120
|
+
let lastSeq = 0;
|
|
121
|
+
let lastEventAt = null;
|
|
122
|
+
const projectedRunId = runId ?? events[0]?.runId ?? null;
|
|
123
|
+
for (const event of events) {
|
|
124
|
+
lastSeq = event.seq;
|
|
125
|
+
lastEventAt = event.at;
|
|
126
|
+
switch (event.type) {
|
|
127
|
+
case "status-changed": {
|
|
128
|
+
if (!canTransitionRunStatus(status, event.to)) {
|
|
129
|
+
anomalies.push({ seq: event.seq, kind: "illegal-transition", detail: `${status ?? "(none)"} -> ${event.to}` });
|
|
130
|
+
}
|
|
131
|
+
statusHistory.push({ seq: event.seq, at: event.at, from: status, to: event.to, reason: event.reason ?? null });
|
|
132
|
+
const wasTerminal = status !== null && isTerminalRunStatus(status);
|
|
133
|
+
status = event.to;
|
|
134
|
+
record = { ...record, updatedAt: event.at };
|
|
135
|
+
if (isTerminalRunStatus(event.to) && !record.completedAt)
|
|
136
|
+
record = { ...record, completedAt: event.at };
|
|
137
|
+
if (!isTerminalRunStatus(event.to) && wasTerminal)
|
|
138
|
+
record = { ...record, completedAt: null };
|
|
139
|
+
if (event.to === "running" && !record.startedAt)
|
|
140
|
+
record = { ...record, startedAt: event.at };
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "record-patch": {
|
|
144
|
+
const next = { ...record };
|
|
145
|
+
for (const [key, value] of Object.entries(event.patch)) {
|
|
146
|
+
if (value !== undefined)
|
|
147
|
+
next[key] = value;
|
|
148
|
+
}
|
|
149
|
+
next.updatedAt = event.patch.updatedAt ?? event.at;
|
|
150
|
+
record = next;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "approval-requested": {
|
|
154
|
+
pendingApprovals.set(event.requestId, {
|
|
155
|
+
requestId: event.requestId,
|
|
156
|
+
requestKind: event.requestKind,
|
|
157
|
+
actionId: event.actionId ?? null,
|
|
158
|
+
payload: event.payload,
|
|
159
|
+
requestedAt: event.at
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "approval-resolved": {
|
|
164
|
+
const pending = pendingApprovals.get(event.requestId);
|
|
165
|
+
if (!pending) {
|
|
166
|
+
const alreadyResolved = resolvedApprovals.some((entry) => entry.requestId === event.requestId);
|
|
167
|
+
anomalies.push({ seq: event.seq, kind: alreadyResolved ? "duplicate-resolution" : "unknown-request", detail: `approval ${event.requestId}` });
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
pendingApprovals.delete(event.requestId);
|
|
171
|
+
resolvedApprovals.push({ ...pending, decision: event.decision, note: event.note ?? null, actor: event.actor, resolvedAt: event.at });
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "input-requested": {
|
|
175
|
+
pendingUserInputs.set(event.requestId, {
|
|
176
|
+
requestId: event.requestId,
|
|
177
|
+
requestKind: "user-input",
|
|
178
|
+
actionId: null,
|
|
179
|
+
payload: event.payload,
|
|
180
|
+
requestedAt: event.at
|
|
181
|
+
});
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "input-resolved": {
|
|
185
|
+
const pending = pendingUserInputs.get(event.requestId);
|
|
186
|
+
if (!pending) {
|
|
187
|
+
const alreadyResolved = resolvedUserInputs.some((entry) => entry.requestId === event.requestId);
|
|
188
|
+
anomalies.push({ seq: event.seq, kind: alreadyResolved ? "duplicate-resolution" : "unknown-request", detail: `user-input ${event.requestId}` });
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
pendingUserInputs.delete(event.requestId);
|
|
192
|
+
resolvedUserInputs.push({ ...pending, answers: event.answers, actor: event.actor, resolvedAt: event.at });
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "steering":
|
|
196
|
+
steeringCount += 1;
|
|
197
|
+
break;
|
|
198
|
+
case "adopted":
|
|
199
|
+
record = { ...record, pid: event.pid, updatedAt: event.at };
|
|
200
|
+
break;
|
|
201
|
+
case "stall-detected":
|
|
202
|
+
stallCount += 1;
|
|
203
|
+
break;
|
|
204
|
+
case "closeout-phase":
|
|
205
|
+
closeoutPhases.push({ seq: event.seq, at: event.at, phase: event.phase, outcome: event.outcome, detail: event.detail ?? null });
|
|
206
|
+
break;
|
|
207
|
+
case "pipeline-resolved":
|
|
208
|
+
resolvedPipeline = event.pipeline;
|
|
209
|
+
break;
|
|
210
|
+
case "stage-outcome":
|
|
211
|
+
stageOutcomes.push({ seq: event.seq, at: event.at, outcome: event.outcome });
|
|
212
|
+
break;
|
|
213
|
+
case "timeline-entry":
|
|
214
|
+
case "log-entry":
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
runId: projectedRunId,
|
|
220
|
+
record,
|
|
221
|
+
status,
|
|
222
|
+
statusHistory,
|
|
223
|
+
pendingApprovals: [...pendingApprovals.values()],
|
|
224
|
+
resolvedApprovals,
|
|
225
|
+
pendingUserInputs: [...pendingUserInputs.values()],
|
|
226
|
+
resolvedUserInputs,
|
|
227
|
+
steeringCount,
|
|
228
|
+
stallCount,
|
|
229
|
+
closeoutPhases,
|
|
230
|
+
resolvedPipeline,
|
|
231
|
+
stageOutcomes,
|
|
232
|
+
lastSeq,
|
|
233
|
+
lastEventAt,
|
|
234
|
+
anomalies
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function isRunSessionCustomType(customType) {
|
|
238
|
+
return customType !== undefined && Object.hasOwn(TYPE_FOR_CUSTOM, customType);
|
|
239
|
+
}
|
|
240
|
+
function foldRunSessionEntries(entries, runId) {
|
|
241
|
+
const events = [];
|
|
242
|
+
entries.forEach((entry, index) => {
|
|
243
|
+
if (entry.type !== "custom" || !isRunSessionCustomType(entry.customType))
|
|
244
|
+
return;
|
|
245
|
+
const data = entry.data !== null && typeof entry.data === "object" ? entry.data : {};
|
|
246
|
+
const stamped = {
|
|
247
|
+
v: 1,
|
|
248
|
+
seq: index + 1,
|
|
249
|
+
at: typeof data.at === "string" ? data.at : new Date(0).toISOString(),
|
|
250
|
+
runId,
|
|
251
|
+
...data,
|
|
252
|
+
type: TYPE_FOR_CUSTOM[entry.customType]
|
|
253
|
+
};
|
|
254
|
+
try {
|
|
255
|
+
events.push(decodeRunJournalEvent(stamped));
|
|
256
|
+
} catch {}
|
|
257
|
+
});
|
|
258
|
+
return reduceRunJournal(events, runId);
|
|
259
|
+
}
|
|
260
|
+
function isRecord(value) {
|
|
261
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
262
|
+
}
|
|
263
|
+
function timelineEntryFromCustomEntry(entry) {
|
|
264
|
+
if (entry.customType !== CUSTOM_TYPE_FOR["timeline-entry"] || !isRecord(entry.data))
|
|
265
|
+
return null;
|
|
266
|
+
const payload = isRecord(entry.data.payload) ? entry.data.payload : entry.data;
|
|
267
|
+
const type = typeof payload.type === "string" ? payload.type : "timeline";
|
|
268
|
+
const stage = typeof payload.stage === "string" ? payload.stage : typeof payload.name === "string" ? payload.name : null;
|
|
269
|
+
const status = typeof payload.status === "string" ? payload.status : typeof payload.outcome === "string" ? payload.outcome : null;
|
|
270
|
+
const detail = typeof payload.detail === "string" ? payload.detail : typeof payload.message === "string" ? payload.message : null;
|
|
271
|
+
const at = typeof entry.data.at === "string" ? entry.data.at : typeof payload.at === "string" ? payload.at : null;
|
|
272
|
+
return { at, type, stage, status, detail };
|
|
273
|
+
}
|
|
274
|
+
function timelineEntriesFromCustomEntries(entries) {
|
|
275
|
+
return entries.flatMap((entry) => {
|
|
276
|
+
const timelineEntry = timelineEntryFromCustomEntry(entry);
|
|
277
|
+
return timelineEntry ? [timelineEntry] : [];
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
var decodeRunJournalEvent = (value) => Schema.decodeUnknownSync(RunJournalEvent)(value), RUN_STATUS_TRANSITIONS, TERMINAL_RUN_STATUSES2;
|
|
281
|
+
var init_session_journal = __esm(() => {
|
|
282
|
+
RUN_STATUS_TRANSITIONS = {
|
|
283
|
+
created: ["queued", "preparing", "running", "failed", "stopped"],
|
|
284
|
+
queued: ["preparing", "running", "failed", "stopped"],
|
|
285
|
+
preparing: ["queued", "running", "needs-attention", "failed", "stopped"],
|
|
286
|
+
running: [
|
|
287
|
+
"queued",
|
|
288
|
+
"waiting-approval",
|
|
289
|
+
"waiting-user-input",
|
|
290
|
+
"paused",
|
|
291
|
+
"validating",
|
|
292
|
+
"reviewing",
|
|
293
|
+
"closing-out",
|
|
294
|
+
"needs-attention",
|
|
295
|
+
"completed",
|
|
296
|
+
"failed",
|
|
297
|
+
"stopped"
|
|
298
|
+
],
|
|
299
|
+
"waiting-approval": ["running", "needs-attention", "failed", "stopped"],
|
|
300
|
+
"waiting-user-input": ["running", "needs-attention", "failed", "stopped"],
|
|
301
|
+
paused: ["running", "failed", "stopped"],
|
|
302
|
+
validating: ["running", "reviewing", "closing-out", "needs-attention", "completed", "failed", "stopped"],
|
|
303
|
+
reviewing: ["running", "validating", "closing-out", "needs-attention", "completed", "failed", "stopped"],
|
|
304
|
+
"closing-out": ["running", "needs-attention", "completed", "failed", "stopped"],
|
|
305
|
+
"needs-attention": ["queued", "preparing", "running", "closing-out", "completed", "failed", "stopped"],
|
|
306
|
+
completed: [],
|
|
307
|
+
failed: ["queued", "preparing", "running", "closing-out"],
|
|
308
|
+
stopped: ["queued", "preparing", "running", "closing-out"]
|
|
309
|
+
};
|
|
310
|
+
TERMINAL_RUN_STATUSES2 = ["completed", "failed", "stopped"];
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// packages/run-plugin/src/read-model/read-model-backend/diagnostics.ts
|
|
314
|
+
function normalizeString(value) {
|
|
315
|
+
if (typeof value !== "string")
|
|
316
|
+
return null;
|
|
317
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
318
|
+
return normalized.length > 0 ? normalized : null;
|
|
319
|
+
}
|
|
320
|
+
function isGenericRunFailure(value) {
|
|
321
|
+
const text = normalizeString(value);
|
|
322
|
+
return Boolean(text && /^Task run failed \([^)]*\)$/i.test(text));
|
|
323
|
+
}
|
|
324
|
+
function appendCandidate(candidates, value) {
|
|
325
|
+
const text = normalizeString(value);
|
|
326
|
+
if (text && !candidates.includes(text))
|
|
327
|
+
candidates.push(text);
|
|
328
|
+
}
|
|
329
|
+
function categorizeUsefulRunError(lines) {
|
|
330
|
+
const taskSourceFailure = lines.find((line) => /failed to update task source/i.test(line));
|
|
331
|
+
if (taskSourceFailure)
|
|
332
|
+
return `Task source update failed: ${taskSourceFailure}`;
|
|
333
|
+
const moduleFailure = lines.find((line) => /cannot find module/i.test(line));
|
|
334
|
+
if (moduleFailure)
|
|
335
|
+
return `Runtime module resolution failed: ${moduleFailure}`;
|
|
336
|
+
const providerFailure = lines.find((line) => /no api key found|unauthorized|authentication failed|invalid api key/i.test(line));
|
|
337
|
+
if (providerFailure)
|
|
338
|
+
return `Provider authentication failed: ${providerFailure}`;
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function summarizeRunError(projection) {
|
|
342
|
+
if (projection.status !== "failed")
|
|
343
|
+
return null;
|
|
344
|
+
const candidates = [];
|
|
345
|
+
for (const anomaly of projection.anomalies) {
|
|
346
|
+
appendCandidate(candidates, `Journal anomaly (${anomaly.kind}): ${anomaly.detail}`);
|
|
347
|
+
}
|
|
348
|
+
for (const phase of projection.closeoutPhases) {
|
|
349
|
+
if (phase.outcome === "failed")
|
|
350
|
+
appendCandidate(candidates, phase.detail);
|
|
351
|
+
}
|
|
352
|
+
for (const entry of projection.statusHistory) {
|
|
353
|
+
appendCandidate(candidates, entry.reason);
|
|
354
|
+
}
|
|
355
|
+
appendCandidate(candidates, projection.record.statusDetail);
|
|
356
|
+
appendCandidate(candidates, projection.record.errorText);
|
|
357
|
+
const nonGeneric = candidates.filter((candidate) => !isGenericRunFailure(candidate));
|
|
358
|
+
return categorizeUsefulRunError(nonGeneric) ?? nonGeneric.at(-1) ?? normalizeString(projection.record.errorText);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// packages/run-plugin/src/read-model/read-model-backend/projection.ts
|
|
362
|
+
var exports_projection = {};
|
|
363
|
+
__export(exports_projection, {
|
|
364
|
+
selectRunProjection: () => selectRunProjection,
|
|
365
|
+
runRecordsFromCollab: () => runRecordsFromCollab,
|
|
366
|
+
runRecordFromRegistryEntry: () => runRecordFromRegistryEntry,
|
|
367
|
+
resolveRunJoinTarget: () => resolveRunJoinTarget,
|
|
368
|
+
resolveJoinTarget: () => resolveJoinTarget,
|
|
369
|
+
readSessionRunEntries: () => readSessionRunEntries,
|
|
370
|
+
listRuns: () => listRuns,
|
|
371
|
+
listRunProjections: () => listRunProjections,
|
|
372
|
+
getRunProjection: () => getRunProjection,
|
|
373
|
+
getRun: () => getRun,
|
|
374
|
+
discoveryDiagnosticRunRecord: () => discoveryDiagnosticRunRecord
|
|
375
|
+
});
|
|
376
|
+
import { existsSync, readFileSync } from "fs";
|
|
377
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
378
|
+
import { RUN_DISCOVERY, RUN_REGISTRY_BACKBONE } from "@rig/contracts";
|
|
379
|
+
import { loadCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
380
|
+
import { defineCapability } from "@rig/core/capability";
|
|
381
|
+
function registryEntryFromCollab(collab) {
|
|
382
|
+
const record = collab;
|
|
383
|
+
return {
|
|
384
|
+
roomId: collab.sessionId,
|
|
385
|
+
title: collab.title,
|
|
386
|
+
status: record.registryStatus ?? (collab.stale ? "stale" : "running"),
|
|
387
|
+
startedAt: collab.startedAt ?? null,
|
|
388
|
+
heartbeatAt: collab.updatedAt ?? null,
|
|
389
|
+
sessionPath: collab.sessionPath ?? null,
|
|
390
|
+
cwd: collab.cwd ?? null,
|
|
391
|
+
joinLink: collab.joinLink ?? null,
|
|
392
|
+
webLink: collab.webLink ?? null,
|
|
393
|
+
relayUrl: collab.relayUrl ?? null,
|
|
394
|
+
stale: collab.stale,
|
|
395
|
+
repo: collab.selectedRepo ?? null,
|
|
396
|
+
...collab.pid === undefined ? {} : { pid: collab.pid },
|
|
397
|
+
projection: record.registryProjection ?? null
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function readSessionRunEntries(sessionPath) {
|
|
401
|
+
if (!sessionPath || !existsSync(sessionPath))
|
|
402
|
+
return [];
|
|
403
|
+
try {
|
|
404
|
+
return parseSessionRunEntries(readFileSync(sessionPath, "utf8"));
|
|
405
|
+
} catch {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function parseSessionRunEntries(raw) {
|
|
410
|
+
const entries = [];
|
|
411
|
+
for (const line of raw.split(`
|
|
412
|
+
`)) {
|
|
413
|
+
if (!line.trim())
|
|
414
|
+
continue;
|
|
415
|
+
try {
|
|
416
|
+
const parsed = JSON.parse(line);
|
|
417
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
418
|
+
continue;
|
|
419
|
+
entries.push({
|
|
420
|
+
type: "type" in parsed && typeof parsed.type === "string" ? parsed.type : "",
|
|
421
|
+
..."customType" in parsed && typeof parsed.customType === "string" ? { customType: parsed.customType } : {},
|
|
422
|
+
..."data" in parsed ? { data: parsed.data } : {}
|
|
423
|
+
});
|
|
424
|
+
} catch {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return entries;
|
|
429
|
+
}
|
|
430
|
+
function stringOrNull(value) {
|
|
431
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
432
|
+
}
|
|
433
|
+
function numberOrNull(value) {
|
|
434
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
435
|
+
}
|
|
436
|
+
function objectRecord(value) {
|
|
437
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
438
|
+
}
|
|
439
|
+
function payloadString(payload, keys) {
|
|
440
|
+
if (!payload || typeof payload !== "object")
|
|
441
|
+
return null;
|
|
442
|
+
const record = payload;
|
|
443
|
+
for (const key of keys) {
|
|
444
|
+
const value = record[key];
|
|
445
|
+
if (typeof value === "string" && value.trim())
|
|
446
|
+
return value.trim();
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
function payloadOptions(payload) {
|
|
451
|
+
if (!payload || typeof payload !== "object")
|
|
452
|
+
return;
|
|
453
|
+
const record = payload;
|
|
454
|
+
const options = Array.isArray(record.options) ? record.options : Array.isArray(record.choices) ? record.choices : null;
|
|
455
|
+
const values = options?.filter((value) => typeof value === "string" && value.trim().length > 0) ?? [];
|
|
456
|
+
return values.length > 0 ? values : undefined;
|
|
457
|
+
}
|
|
458
|
+
function inboxRequest(request, kind) {
|
|
459
|
+
const fallback = kind === "approval" ? "Approval requested" : "Input requested";
|
|
460
|
+
const body = payloadString(request.payload, ["body", "description", "detail", "details"]);
|
|
461
|
+
const options = payloadOptions(request.payload);
|
|
462
|
+
return {
|
|
463
|
+
requestId: request.requestId,
|
|
464
|
+
kind,
|
|
465
|
+
title: payloadString(request.payload, ["title", "message", "reason", "prompt", "summary"]) ?? fallback,
|
|
466
|
+
...body !== undefined ? { body } : {},
|
|
467
|
+
...options ? { options } : {},
|
|
468
|
+
requestedAt: request.requestedAt ?? null,
|
|
469
|
+
source: "run"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function inboxFromProjection(projection) {
|
|
473
|
+
return [
|
|
474
|
+
...projection.pendingApprovals.map((request) => inboxRequest(request, "approval")),
|
|
475
|
+
...projection.pendingUserInputs.map((request) => inboxRequest(request, "input"))
|
|
476
|
+
].sort((a, b) => (Date.parse(a.requestedAt ?? "") || 0) - (Date.parse(b.requestedAt ?? "") || 0));
|
|
477
|
+
}
|
|
478
|
+
function isProjectPath(projectRoot, cwd) {
|
|
479
|
+
const root = resolve(projectRoot);
|
|
480
|
+
const candidate = resolve(cwd);
|
|
481
|
+
const pathFromRoot = relative(root, candidate);
|
|
482
|
+
return pathFromRoot === "" || !pathFromRoot.startsWith("../") && pathFromRoot !== ".." && !isAbsolute(pathFromRoot);
|
|
483
|
+
}
|
|
484
|
+
function sourceFromEntry(projectRoot, projection, entry) {
|
|
485
|
+
const candidates = [
|
|
486
|
+
stringOrNull(projection.collabCwd),
|
|
487
|
+
stringOrNull(projection.cwd),
|
|
488
|
+
stringOrNull(entry.cwd),
|
|
489
|
+
stringOrNull(projection.worktreePath)
|
|
490
|
+
].filter((cwd) => cwd !== null);
|
|
491
|
+
return candidates.some((cwd) => isProjectPath(projectRoot, cwd)) ? "local" : "remote";
|
|
492
|
+
}
|
|
493
|
+
function pendingRequests(value, fallbackKind) {
|
|
494
|
+
if (!Array.isArray(value))
|
|
495
|
+
return [];
|
|
496
|
+
return value.flatMap((item) => {
|
|
497
|
+
const record = objectRecord(item);
|
|
498
|
+
const requestId = stringOrNull(record?.requestId) ?? stringOrNull(record?.id);
|
|
499
|
+
if (!record || !requestId)
|
|
500
|
+
return [];
|
|
501
|
+
return [{
|
|
502
|
+
requestId,
|
|
503
|
+
requestKind: stringOrNull(record.requestKind) ?? stringOrNull(record.kind) ?? fallbackKind,
|
|
504
|
+
actionId: stringOrNull(record.actionId),
|
|
505
|
+
payload: "payload" in record ? record.payload : record,
|
|
506
|
+
requestedAt: stringOrNull(record.requestedAt) ?? stringOrNull(record.at) ?? ""
|
|
507
|
+
}];
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function emptyFoldedProjection(runId, projection, toRunStatus) {
|
|
511
|
+
const projectionRecord = projection;
|
|
512
|
+
const nestedProjection = objectRecord(projectionRecord.projection);
|
|
513
|
+
const pendingApprovals = pendingRequests(nestedProjection?.pendingApprovals ?? projectionRecord.pendingApprovals, "approval");
|
|
514
|
+
const pendingUserInputs = pendingRequests(nestedProjection?.pendingUserInputs ?? nestedProjection?.pendingInputs ?? projectionRecord.pendingUserInputs ?? (Array.isArray(projectionRecord.pendingInputs) ? projectionRecord.pendingInputs : undefined), "user-input");
|
|
515
|
+
const nestedRecord = objectRecord(nestedProjection?.record);
|
|
516
|
+
const nestedFolded = nestedProjection;
|
|
517
|
+
return {
|
|
518
|
+
...EMPTY_PROJECTION,
|
|
519
|
+
...nestedFolded ?? {},
|
|
520
|
+
record: {
|
|
521
|
+
...nestedRecord ?? {},
|
|
522
|
+
runId,
|
|
523
|
+
...projection.taskId ? { taskId: projection.taskId } : {},
|
|
524
|
+
...projection.title ? { title: projection.title } : {},
|
|
525
|
+
...projection.startedAt ? { startedAt: projection.startedAt } : {},
|
|
526
|
+
...projection.updatedAt ? { updatedAt: projection.updatedAt } : {},
|
|
527
|
+
...projection.completedAt ? { completedAt: projection.completedAt } : {},
|
|
528
|
+
...projection.sessionPath ? { sessionPath: projection.sessionPath } : {},
|
|
529
|
+
...projection.worktreePath ? { worktreePath: projection.worktreePath } : {},
|
|
530
|
+
...projection.prUrl ? { prUrl: projection.prUrl } : {}
|
|
531
|
+
},
|
|
532
|
+
status: toRunStatus(projection.status) ?? toRunStatus(nestedProjection?.status),
|
|
533
|
+
pendingApprovals,
|
|
534
|
+
pendingUserInputs,
|
|
535
|
+
steeringCount: projection.steeringCount ?? numberOrNull(nestedProjection?.steeringCount) ?? 0,
|
|
536
|
+
stallCount: projection.stallCount ?? numberOrNull(nestedProjection?.stallCount) ?? 0,
|
|
537
|
+
lastEventAt: projection.updatedAt ?? stringOrNull(nestedProjection?.lastEventAt)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function foldedProjectionForRun(runId, projection, sessionPath, toRunStatus) {
|
|
541
|
+
const registryFolded = emptyFoldedProjection(runId, projection, toRunStatus);
|
|
542
|
+
const sessionEntries = readSessionRunEntries(sessionPath);
|
|
543
|
+
if (sessionEntries.length === 0)
|
|
544
|
+
return { projection: registryFolded, hasSessionEntries: false };
|
|
545
|
+
const sessionFolded = foldRunSessionEntries(sessionEntries, runId);
|
|
546
|
+
if (sessionFolded.lastSeq === 0)
|
|
547
|
+
return { projection: registryFolded, hasSessionEntries: false };
|
|
548
|
+
return {
|
|
549
|
+
projection: {
|
|
550
|
+
...registryFolded,
|
|
551
|
+
...sessionFolded,
|
|
552
|
+
record: {
|
|
553
|
+
...registryFolded.record,
|
|
554
|
+
...sessionFolded.record,
|
|
555
|
+
runId
|
|
556
|
+
},
|
|
557
|
+
status: sessionFolded.status ?? registryFolded.status,
|
|
558
|
+
lastEventAt: sessionFolded.lastEventAt ?? registryFolded.lastEventAt
|
|
559
|
+
},
|
|
560
|
+
hasSessionEntries: true
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function runRecordFromRegistryEntry(projectRoot, entry, toRunStatus) {
|
|
564
|
+
const projection = entry.projection && typeof entry.projection === "object" ? entry.projection : {};
|
|
565
|
+
const projectionRecord = projection;
|
|
566
|
+
const runId = stringOrNull(projection.runId) ?? entry.roomId;
|
|
567
|
+
const sessionPath = stringOrNull(projection.sessionPath) ?? stringOrNull(entry.sessionPath) ?? null;
|
|
568
|
+
const foldedResult = foldedProjectionForRun(runId, projection, sessionPath, toRunStatus);
|
|
569
|
+
const folded = foldedResult.projection;
|
|
570
|
+
const pushedStatus = folded.status ?? toRunStatus(projection.status) ?? toRunStatus(entry.status);
|
|
571
|
+
const status = entry.stale ? pushedStatus && isTerminalRunStatus(pushedStatus) ? pushedStatus : "stale" : pushedStatus ?? "running";
|
|
572
|
+
const collabCwd = stringOrNull(projection.collabCwd) ?? stringOrNull(projection.cwd) ?? stringOrNull(entry.cwd) ?? stringOrNull(projection.worktreePath);
|
|
573
|
+
const pendingApprovals = foldedResult.hasSessionEntries ? folded.pendingApprovals.length : numberOrNull(projectionRecord.pendingApprovals) ?? folded.pendingApprovals.length;
|
|
574
|
+
const pendingInputs = foldedResult.hasSessionEntries ? folded.pendingUserInputs.length : numberOrNull(projectionRecord.pendingInputs) ?? folded.pendingUserInputs.length;
|
|
575
|
+
return {
|
|
576
|
+
runId,
|
|
577
|
+
taskId: stringOrNull(projection.taskId),
|
|
578
|
+
title: stringOrNull(projection.title) ?? entry.title,
|
|
579
|
+
status,
|
|
580
|
+
source: sourceFromEntry(projectRoot, projection, entry),
|
|
581
|
+
live: !entry.stale,
|
|
582
|
+
stale: entry.stale,
|
|
583
|
+
startedAt: stringOrNull(projection.startedAt) ?? entry.startedAt ?? null,
|
|
584
|
+
updatedAt: stringOrNull(projection.updatedAt) ?? entry.heartbeatAt ?? null,
|
|
585
|
+
completedAt: stringOrNull(projection.completedAt),
|
|
586
|
+
joinLink: stringOrNull(projection.joinLink) ?? entry.joinLink ?? null,
|
|
587
|
+
webLink: stringOrNull(projection.webLink) ?? entry.webLink ?? null,
|
|
588
|
+
relayUrl: stringOrNull(projection.relayUrl) ?? entry.relayUrl ?? null,
|
|
589
|
+
sessionPath,
|
|
590
|
+
prUrl: stringOrNull(projection.prUrl),
|
|
591
|
+
worktreePath: stringOrNull(projection.worktreePath) ?? collabCwd,
|
|
592
|
+
pendingApprovals,
|
|
593
|
+
pendingInputs,
|
|
594
|
+
steeringCount: projection.steeringCount ?? folded.steeringCount ?? 0,
|
|
595
|
+
stallCount: projection.stallCount ?? folded.stallCount ?? 0,
|
|
596
|
+
errorSummary: stringOrNull(projection.errorSummary) ?? summarizeRunError(folded),
|
|
597
|
+
timeline: Array.isArray(projection.timeline) ? [...projection.timeline] : [...timelineEntriesFromCustomEntries([])],
|
|
598
|
+
inbox: inboxFromProjection(folded),
|
|
599
|
+
collabCwd: collabCwd ?? null,
|
|
600
|
+
projection: folded
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function runRecordsFromCollab(projectRoot, collabs, toRunStatus) {
|
|
604
|
+
return sortByRecency(collabs.map((collab) => runRecordFromRegistryEntry(projectRoot, registryEntryFromCollab(collab), toRunStatus)).filter((record) => record !== null));
|
|
605
|
+
}
|
|
606
|
+
function sortByRecency(records) {
|
|
607
|
+
return [...records].sort((a, b) => {
|
|
608
|
+
const at = Date.parse(b.updatedAt ?? b.startedAt ?? "") || 0;
|
|
609
|
+
const bt = Date.parse(a.updatedAt ?? a.startedAt ?? "") || 0;
|
|
610
|
+
return at - bt;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function discoveryDiagnosticRunRecord(projectRoot, error) {
|
|
614
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
615
|
+
const runId = DISCOVERY_DIAGNOSTIC_RUN_ID;
|
|
616
|
+
const projection = {
|
|
617
|
+
...EMPTY_PROJECTION,
|
|
618
|
+
record: { runId, title: "Registry discovery unavailable" },
|
|
619
|
+
status: "needs-attention",
|
|
620
|
+
stallCount: 1
|
|
621
|
+
};
|
|
622
|
+
return {
|
|
623
|
+
runId,
|
|
624
|
+
taskId: null,
|
|
625
|
+
title: "Registry discovery unavailable",
|
|
626
|
+
status: "needs-attention",
|
|
627
|
+
source: "remote",
|
|
628
|
+
live: false,
|
|
629
|
+
stale: true,
|
|
630
|
+
startedAt: null,
|
|
631
|
+
updatedAt: null,
|
|
632
|
+
completedAt: null,
|
|
633
|
+
joinLink: null,
|
|
634
|
+
webLink: null,
|
|
635
|
+
relayUrl: null,
|
|
636
|
+
sessionPath: null,
|
|
637
|
+
prUrl: null,
|
|
638
|
+
worktreePath: null,
|
|
639
|
+
pendingApprovals: 0,
|
|
640
|
+
pendingInputs: 0,
|
|
641
|
+
steeringCount: 0,
|
|
642
|
+
stallCount: 1,
|
|
643
|
+
errorSummary: `registry discovery unavailable: ${detail}`,
|
|
644
|
+
timeline: [],
|
|
645
|
+
inbox: [],
|
|
646
|
+
collabCwd: null,
|
|
647
|
+
projection
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
async function listRunProjections(projectRoot, filter = {}) {
|
|
651
|
+
try {
|
|
652
|
+
const discovery = await loadCapabilityForRoot(projectRoot, RunDiscoveryCap);
|
|
653
|
+
if (!discovery)
|
|
654
|
+
return [discoveryDiagnosticRunRecord(projectRoot, "run discovery capability unavailable")];
|
|
655
|
+
const backbone = await loadCapabilityForRoot(projectRoot, RunRegistryBackboneCap);
|
|
656
|
+
if (!backbone)
|
|
657
|
+
return [discoveryDiagnosticRunRecord(projectRoot, "run registry backbone capability unavailable")];
|
|
658
|
+
const collabs = await discovery.listActiveRunCollab(projectRoot, filter, { isRuntimeActiveStatus });
|
|
659
|
+
return runRecordsFromCollab(projectRoot, collabs, backbone.registryStatusToRunStatus);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
return [discoveryDiagnosticRunRecord(projectRoot, error)];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function selectRunProjection(runs, runId) {
|
|
665
|
+
const exactRun = runs.find((run) => run.runId === runId);
|
|
666
|
+
if (exactRun)
|
|
667
|
+
return exactRun;
|
|
668
|
+
const exactTask = runs.find((run) => run.taskId === runId);
|
|
669
|
+
if (exactTask)
|
|
670
|
+
return exactTask;
|
|
671
|
+
const prefixMatches = runs.filter((run) => run.runId.startsWith(runId));
|
|
672
|
+
if (prefixMatches.length === 1)
|
|
673
|
+
return prefixMatches[0] ?? null;
|
|
674
|
+
if (prefixMatches.length > 1) {
|
|
675
|
+
const matches = prefixMatches.map((run) => run.runId).join(", ");
|
|
676
|
+
throw new Error(`Ambiguous run id prefix "${runId}" matched ${prefixMatches.length} runs: ${matches}`);
|
|
677
|
+
}
|
|
678
|
+
return runs.find((run) => run.runId === DISCOVERY_DIAGNOSTIC_RUN_ID) ?? null;
|
|
679
|
+
}
|
|
680
|
+
async function getRunProjection(projectRoot, runId, filter = {}) {
|
|
681
|
+
const runs = await listRunProjections(projectRoot, filter);
|
|
682
|
+
return selectRunProjection(runs, runId);
|
|
683
|
+
}
|
|
684
|
+
async function resolveRunJoinTarget(projectRoot, runId, filter = {}) {
|
|
685
|
+
const run = await getRunProjection(projectRoot, runId, filter);
|
|
686
|
+
if (!run || !run.joinLink)
|
|
687
|
+
return null;
|
|
688
|
+
return { runId: run.runId, taskId: run.taskId, joinLink: run.joinLink, cwd: run.collabCwd ?? run.sessionPath, stale: run.stale };
|
|
689
|
+
}
|
|
690
|
+
var RunDiscoveryCap, RunRegistryBackboneCap, EMPTY_PROJECTION, DISCOVERY_DIAGNOSTIC_RUN_ID = "__registry_discovery_error__", listRuns, getRun, resolveJoinTarget;
|
|
691
|
+
var init_projection = __esm(() => {
|
|
692
|
+
init_session_journal();
|
|
693
|
+
init_run_status();
|
|
694
|
+
RunDiscoveryCap = defineCapability(RUN_DISCOVERY);
|
|
695
|
+
RunRegistryBackboneCap = defineCapability(RUN_REGISTRY_BACKBONE);
|
|
696
|
+
EMPTY_PROJECTION = foldRunSessionEntries([], "");
|
|
697
|
+
listRuns = listRunProjections;
|
|
698
|
+
getRun = getRunProjection;
|
|
699
|
+
resolveJoinTarget = resolveRunJoinTarget;
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// packages/run-plugin/src/read-model/read-model-backend/stats.ts
|
|
703
|
+
init_run_status();
|
|
704
|
+
init_run_status();
|
|
705
|
+
function parseTimestamp(value) {
|
|
706
|
+
if (!value)
|
|
707
|
+
return null;
|
|
708
|
+
const parsed = Date.parse(value);
|
|
709
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
710
|
+
}
|
|
711
|
+
function median(values) {
|
|
712
|
+
if (values.length === 0)
|
|
713
|
+
return null;
|
|
714
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
715
|
+
const middle = Math.floor(sorted.length / 2);
|
|
716
|
+
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
717
|
+
}
|
|
718
|
+
function rate(part, total) {
|
|
719
|
+
return total === 0 ? null : part / total;
|
|
720
|
+
}
|
|
721
|
+
function completedDuration(run) {
|
|
722
|
+
if (run.status !== "completed")
|
|
723
|
+
return null;
|
|
724
|
+
const startedAt = parseTimestamp(run.startedAt);
|
|
725
|
+
const completedAt = parseTimestamp(run.completedAt);
|
|
726
|
+
if (startedAt === null || completedAt === null || completedAt < startedAt)
|
|
727
|
+
return null;
|
|
728
|
+
return completedAt - startedAt;
|
|
729
|
+
}
|
|
730
|
+
async function computeStats(projectRootOrRuns, options = {}) {
|
|
731
|
+
const sinceMs = options.since ? options.since.getTime() : null;
|
|
732
|
+
const allRuns = typeof projectRootOrRuns === "string" ? await (options.listRuns ?? (await Promise.resolve().then(() => (init_projection(), exports_projection))).listRuns)(projectRootOrRuns) : projectRootOrRuns;
|
|
733
|
+
const runs = allRuns.filter((run) => {
|
|
734
|
+
if (sinceMs === null)
|
|
735
|
+
return true;
|
|
736
|
+
const startedAt = parseTimestamp(run.startedAt);
|
|
737
|
+
return startedAt !== null && startedAt >= sinceMs;
|
|
738
|
+
});
|
|
739
|
+
const statusCounts = {};
|
|
740
|
+
const completionDurations = [];
|
|
741
|
+
let completedRuns = 0;
|
|
742
|
+
let failedRuns = 0;
|
|
743
|
+
let needsAttentionRuns = 0;
|
|
744
|
+
let steeringTotal = 0;
|
|
745
|
+
let stallTotal = 0;
|
|
746
|
+
let approvalsPending = 0;
|
|
747
|
+
let approvalsApproved = 0;
|
|
748
|
+
let approvalsRejected = 0;
|
|
749
|
+
for (const run of runs) {
|
|
750
|
+
const status = run.status || "unknown";
|
|
751
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
752
|
+
if (status === "completed")
|
|
753
|
+
completedRuns += 1;
|
|
754
|
+
if (status === "failed")
|
|
755
|
+
failedRuns += 1;
|
|
756
|
+
if (isNeedsAttention(run))
|
|
757
|
+
needsAttentionRuns += 1;
|
|
758
|
+
const duration = completedDuration(run);
|
|
759
|
+
if (duration !== null)
|
|
760
|
+
completionDurations.push(duration);
|
|
761
|
+
steeringTotal += run.steeringCount;
|
|
762
|
+
stallTotal += run.stallCount;
|
|
763
|
+
approvalsPending += run.projection.pendingApprovals.length;
|
|
764
|
+
for (const approval of run.projection.resolvedApprovals) {
|
|
765
|
+
if (approval.decision === "approve")
|
|
766
|
+
approvalsApproved += 1;
|
|
767
|
+
if (approval.decision === "reject")
|
|
768
|
+
approvalsRejected += 1;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const approvalsRequested = approvalsPending + approvalsApproved + approvalsRejected;
|
|
772
|
+
const totalRuns = runs.length;
|
|
773
|
+
return {
|
|
774
|
+
since: options.since ? options.since.toISOString() : null,
|
|
775
|
+
totalRuns,
|
|
776
|
+
statusCounts,
|
|
777
|
+
completedRuns,
|
|
778
|
+
failedRuns,
|
|
779
|
+
needsAttentionRuns,
|
|
780
|
+
completionRate: rate(completedRuns, totalRuns),
|
|
781
|
+
failureRate: rate(failedRuns, totalRuns),
|
|
782
|
+
needsAttentionRate: rate(needsAttentionRuns, totalRuns),
|
|
783
|
+
medianCompletionMs: median(completionDurations),
|
|
784
|
+
steeringTotal,
|
|
785
|
+
steeringPerRun: totalRuns === 0 ? null : steeringTotal / totalRuns,
|
|
786
|
+
stallTotal,
|
|
787
|
+
approvalsRequested,
|
|
788
|
+
approvalsApproved,
|
|
789
|
+
approvalsRejected,
|
|
790
|
+
approvalsPending
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
export {
|
|
794
|
+
rate,
|
|
795
|
+
parseTimestamp,
|
|
796
|
+
median,
|
|
797
|
+
isNeedsAttention,
|
|
798
|
+
computeStats,
|
|
799
|
+
completedDuration
|
|
800
|
+
};
|