@h-rig/run-worker 0.0.6-alpha.157 → 0.0.6-alpha.159
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/src/autohost.d.ts +8 -10
- package/dist/src/autohost.js +683 -95
- package/dist/src/constants.d.ts +0 -1
- package/dist/src/constants.js +0 -2
- package/dist/src/extension.js +683 -95
- package/dist/src/host-kernel.d.ts +22 -0
- package/dist/src/host-kernel.js +78 -0
- package/dist/src/host.d.ts +2 -0
- package/dist/src/host.js +419 -0
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +1913 -133
- package/dist/src/local-run-changes.d.ts +3 -0
- package/dist/src/local-run-changes.js +65 -0
- package/dist/src/notifications.js +13 -5
- package/dist/src/notify-cap.d.ts +11 -0
- package/dist/src/notify-cap.js +13 -0
- package/dist/src/panel-plugin.js +3 -7
- package/dist/src/plugin.d.ts +0 -11
- package/dist/src/plugin.js +1910 -101
- package/dist/src/{runs → read-model-backend}/control.d.ts +0 -1
- package/dist/src/{runs → read-model-backend}/control.js +361 -38
- package/dist/src/{runs → read-model-backend}/diagnostics.d.ts +0 -1
- package/dist/src/{runs → read-model-backend}/diagnostics.js +1 -3
- package/dist/src/{runs → read-model-backend}/guard.js +2 -3
- package/dist/src/{runs → read-model-backend}/inbox.js +366 -36
- package/dist/src/{runs → read-model-backend}/index.js +552 -223
- package/dist/src/{runs → read-model-backend}/inspect.d.ts +0 -1
- package/dist/src/{runs → read-model-backend}/inspect.js +350 -34
- package/dist/src/{runs → read-model-backend}/projection.d.ts +21 -7
- package/dist/src/{runs → read-model-backend}/projection.js +349 -31
- package/dist/src/{runs → read-model-backend}/run-status.d.ts +6 -3
- package/dist/src/{runs → read-model-backend}/run-status.js +53 -33
- package/dist/src/{runs → read-model-backend}/stats.d.ts +0 -1
- package/dist/src/{runs → read-model-backend}/stats.js +373 -58
- package/dist/src/read-model-service.d.ts +2 -0
- package/dist/src/read-model-service.js +1433 -0
- package/dist/src/session-journal.d.ts +60 -0
- package/dist/src/session-journal.js +471 -0
- package/dist/src/stall.d.ts +8 -3
- package/dist/src/stall.js +0 -1
- package/dist/src/utils.js +282 -3
- package/package.json +9 -12
- package/dist/src/journal.d.ts +0 -33
- package/dist/src/journal.js +0 -31
- /package/dist/src/{runs → read-model-backend}/guard.d.ts +0 -0
- /package/dist/src/{runs → read-model-backend}/inbox.d.ts +0 -0
- /package/dist/src/{runs → read-model-backend}/index.d.ts +0 -0
|
@@ -0,0 +1,1433 @@
|
|
|
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
|
+
var __require = import.meta.require;
|
|
18
|
+
|
|
19
|
+
// packages/run-worker/src/session-journal.ts
|
|
20
|
+
import { Schema } from "effect";
|
|
21
|
+
import {
|
|
22
|
+
CUSTOM_TYPE_FOR,
|
|
23
|
+
RIG_CONTROL_SENTINEL_END,
|
|
24
|
+
RIG_INBOX_RESOLUTION_SENTINEL,
|
|
25
|
+
RIG_PAUSE_SENTINEL,
|
|
26
|
+
RIG_RESUME_SENTINEL,
|
|
27
|
+
RIG_STOP_SENTINEL,
|
|
28
|
+
RIG_STOP_SENTINEL_END,
|
|
29
|
+
RIG_WORKFLOW_STATUS_CHANGED,
|
|
30
|
+
RunJournalEvent,
|
|
31
|
+
TYPE_FOR_CUSTOM
|
|
32
|
+
} from "@rig/contracts";
|
|
33
|
+
function isTerminalRunStatus(status) {
|
|
34
|
+
return TERMINAL_RUN_STATUSES.includes(status);
|
|
35
|
+
}
|
|
36
|
+
function canTransitionRunStatus(from, to) {
|
|
37
|
+
if (from === null)
|
|
38
|
+
return true;
|
|
39
|
+
if (from === to)
|
|
40
|
+
return true;
|
|
41
|
+
return RUN_STATUS_TRANSITIONS[from].includes(to);
|
|
42
|
+
}
|
|
43
|
+
function assertRunStatusTransition(from, to) {
|
|
44
|
+
if (!canTransitionRunStatus(from, to)) {
|
|
45
|
+
throw new Error(`Illegal run status transition: ${from ?? "(none)"} -> ${to}. ` + `Allowed from ${from ?? "(none)"}: ${from ? RUN_STATUS_TRANSITIONS[from].join(", ") : "(any)"}.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function reduceRunJournal(events, runId = null) {
|
|
49
|
+
let record = {};
|
|
50
|
+
let status = null;
|
|
51
|
+
const statusHistory = [];
|
|
52
|
+
const pendingApprovals = new Map;
|
|
53
|
+
const resolvedApprovals = [];
|
|
54
|
+
const pendingUserInputs = new Map;
|
|
55
|
+
const resolvedUserInputs = [];
|
|
56
|
+
const closeoutPhases = [];
|
|
57
|
+
let resolvedPipeline = null;
|
|
58
|
+
const stageOutcomes = [];
|
|
59
|
+
const anomalies = [];
|
|
60
|
+
let steeringCount = 0;
|
|
61
|
+
let stallCount = 0;
|
|
62
|
+
let lastSeq = 0;
|
|
63
|
+
let lastEventAt = null;
|
|
64
|
+
const projectedRunId = runId ?? events[0]?.runId ?? null;
|
|
65
|
+
for (const event of events) {
|
|
66
|
+
lastSeq = event.seq;
|
|
67
|
+
lastEventAt = event.at;
|
|
68
|
+
switch (event.type) {
|
|
69
|
+
case "status-changed": {
|
|
70
|
+
if (!canTransitionRunStatus(status, event.to)) {
|
|
71
|
+
anomalies.push({ seq: event.seq, kind: "illegal-transition", detail: `${status ?? "(none)"} -> ${event.to}` });
|
|
72
|
+
}
|
|
73
|
+
statusHistory.push({ seq: event.seq, at: event.at, from: status, to: event.to, reason: event.reason ?? null });
|
|
74
|
+
const wasTerminal = status !== null && isTerminalRunStatus(status);
|
|
75
|
+
status = event.to;
|
|
76
|
+
record = { ...record, updatedAt: event.at };
|
|
77
|
+
if (isTerminalRunStatus(event.to) && !record.completedAt)
|
|
78
|
+
record = { ...record, completedAt: event.at };
|
|
79
|
+
if (!isTerminalRunStatus(event.to) && wasTerminal)
|
|
80
|
+
record = { ...record, completedAt: null };
|
|
81
|
+
if (event.to === "running" && !record.startedAt)
|
|
82
|
+
record = { ...record, startedAt: event.at };
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "record-patch": {
|
|
86
|
+
const next = { ...record };
|
|
87
|
+
for (const [key, value] of Object.entries(event.patch)) {
|
|
88
|
+
if (value !== undefined)
|
|
89
|
+
next[key] = value;
|
|
90
|
+
}
|
|
91
|
+
next.updatedAt = event.patch.updatedAt ?? event.at;
|
|
92
|
+
record = next;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "approval-requested": {
|
|
96
|
+
pendingApprovals.set(event.requestId, {
|
|
97
|
+
requestId: event.requestId,
|
|
98
|
+
requestKind: event.requestKind,
|
|
99
|
+
actionId: event.actionId ?? null,
|
|
100
|
+
payload: event.payload,
|
|
101
|
+
requestedAt: event.at
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "approval-resolved": {
|
|
106
|
+
const pending = pendingApprovals.get(event.requestId);
|
|
107
|
+
if (!pending) {
|
|
108
|
+
const alreadyResolved = resolvedApprovals.some((entry) => entry.requestId === event.requestId);
|
|
109
|
+
anomalies.push({ seq: event.seq, kind: alreadyResolved ? "duplicate-resolution" : "unknown-request", detail: `approval ${event.requestId}` });
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
pendingApprovals.delete(event.requestId);
|
|
113
|
+
resolvedApprovals.push({ ...pending, decision: event.decision, note: event.note ?? null, actor: event.actor, resolvedAt: event.at });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "input-requested": {
|
|
117
|
+
pendingUserInputs.set(event.requestId, {
|
|
118
|
+
requestId: event.requestId,
|
|
119
|
+
requestKind: "user-input",
|
|
120
|
+
actionId: null,
|
|
121
|
+
payload: event.payload,
|
|
122
|
+
requestedAt: event.at
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "input-resolved": {
|
|
127
|
+
const pending = pendingUserInputs.get(event.requestId);
|
|
128
|
+
if (!pending) {
|
|
129
|
+
const alreadyResolved = resolvedUserInputs.some((entry) => entry.requestId === event.requestId);
|
|
130
|
+
anomalies.push({ seq: event.seq, kind: alreadyResolved ? "duplicate-resolution" : "unknown-request", detail: `user-input ${event.requestId}` });
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
pendingUserInputs.delete(event.requestId);
|
|
134
|
+
resolvedUserInputs.push({ ...pending, answers: event.answers, actor: event.actor, resolvedAt: event.at });
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "steering":
|
|
138
|
+
steeringCount += 1;
|
|
139
|
+
break;
|
|
140
|
+
case "adopted":
|
|
141
|
+
record = { ...record, pid: event.pid, updatedAt: event.at };
|
|
142
|
+
break;
|
|
143
|
+
case "stall-detected":
|
|
144
|
+
stallCount += 1;
|
|
145
|
+
break;
|
|
146
|
+
case "closeout-phase":
|
|
147
|
+
closeoutPhases.push({ seq: event.seq, at: event.at, phase: event.phase, outcome: event.outcome, detail: event.detail ?? null });
|
|
148
|
+
break;
|
|
149
|
+
case "pipeline-resolved":
|
|
150
|
+
resolvedPipeline = event.pipeline;
|
|
151
|
+
break;
|
|
152
|
+
case "stage-outcome":
|
|
153
|
+
stageOutcomes.push({ seq: event.seq, at: event.at, outcome: event.outcome });
|
|
154
|
+
break;
|
|
155
|
+
case "timeline-entry":
|
|
156
|
+
case "log-entry":
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
runId: projectedRunId,
|
|
162
|
+
record,
|
|
163
|
+
status,
|
|
164
|
+
statusHistory,
|
|
165
|
+
pendingApprovals: [...pendingApprovals.values()],
|
|
166
|
+
resolvedApprovals,
|
|
167
|
+
pendingUserInputs: [...pendingUserInputs.values()],
|
|
168
|
+
resolvedUserInputs,
|
|
169
|
+
steeringCount,
|
|
170
|
+
stallCount,
|
|
171
|
+
closeoutPhases,
|
|
172
|
+
resolvedPipeline,
|
|
173
|
+
stageOutcomes,
|
|
174
|
+
lastSeq,
|
|
175
|
+
lastEventAt,
|
|
176
|
+
anomalies
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function isRunSessionCustomType(customType) {
|
|
180
|
+
return customType !== undefined && Object.hasOwn(TYPE_FOR_CUSTOM, customType);
|
|
181
|
+
}
|
|
182
|
+
function foldRunSessionEntries(entries, runId) {
|
|
183
|
+
const events = [];
|
|
184
|
+
entries.forEach((entry, index) => {
|
|
185
|
+
if (entry.type !== "custom" || !isRunSessionCustomType(entry.customType))
|
|
186
|
+
return;
|
|
187
|
+
const data = entry.data !== null && typeof entry.data === "object" ? entry.data : {};
|
|
188
|
+
const stamped = {
|
|
189
|
+
v: 1,
|
|
190
|
+
seq: index + 1,
|
|
191
|
+
at: typeof data.at === "string" ? data.at : new Date(0).toISOString(),
|
|
192
|
+
runId,
|
|
193
|
+
...data,
|
|
194
|
+
type: TYPE_FOR_CUSTOM[entry.customType]
|
|
195
|
+
};
|
|
196
|
+
try {
|
|
197
|
+
events.push(decodeRunJournalEvent(stamped));
|
|
198
|
+
} catch {}
|
|
199
|
+
});
|
|
200
|
+
return reduceRunJournal(events, runId);
|
|
201
|
+
}
|
|
202
|
+
function isRecord(value) {
|
|
203
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
204
|
+
}
|
|
205
|
+
function timelineEntryFromCustomEntry(entry) {
|
|
206
|
+
if (entry.customType !== CUSTOM_TYPE_FOR["timeline-entry"] || !isRecord(entry.data))
|
|
207
|
+
return null;
|
|
208
|
+
const payload = isRecord(entry.data.payload) ? entry.data.payload : entry.data;
|
|
209
|
+
const type = typeof payload.type === "string" ? payload.type : "timeline";
|
|
210
|
+
const stage = typeof payload.stage === "string" ? payload.stage : typeof payload.name === "string" ? payload.name : null;
|
|
211
|
+
const status = typeof payload.status === "string" ? payload.status : typeof payload.outcome === "string" ? payload.outcome : null;
|
|
212
|
+
const detail = typeof payload.detail === "string" ? payload.detail : typeof payload.message === "string" ? payload.message : null;
|
|
213
|
+
const at = typeof entry.data.at === "string" ? entry.data.at : typeof payload.at === "string" ? payload.at : null;
|
|
214
|
+
return { at, type, stage, status, detail };
|
|
215
|
+
}
|
|
216
|
+
function timelineEntriesFromCustomEntries(entries) {
|
|
217
|
+
return entries.flatMap((entry) => {
|
|
218
|
+
const timelineEntry = timelineEntryFromCustomEntry(entry);
|
|
219
|
+
return timelineEntry ? [timelineEntry] : [];
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function asCustomEntries(entries) {
|
|
223
|
+
return entries.filter((entry) => entry.type === "custom");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
class RunSessionJournal {
|
|
227
|
+
sm;
|
|
228
|
+
runId;
|
|
229
|
+
constructor(sm, runId) {
|
|
230
|
+
this.sm = sm;
|
|
231
|
+
this.runId = runId;
|
|
232
|
+
}
|
|
233
|
+
#append(init) {
|
|
234
|
+
this.sm.appendCustomEntry(CUSTOM_TYPE_FOR[init.type], { ...init, at: new Date().toISOString() });
|
|
235
|
+
}
|
|
236
|
+
appendStatus(to, opts = {}) {
|
|
237
|
+
const from = foldRunSessionEntries(asCustomEntries(this.sm.getEntries()), this.runId).status;
|
|
238
|
+
if (!opts.force)
|
|
239
|
+
assertRunStatusTransition(from, to);
|
|
240
|
+
if (opts.errorText !== undefined)
|
|
241
|
+
this.#append({ type: "record-patch", patch: { errorText: opts.errorText } });
|
|
242
|
+
this.#append({
|
|
243
|
+
type: "status-changed",
|
|
244
|
+
from,
|
|
245
|
+
to,
|
|
246
|
+
...opts.reason !== undefined ? { reason: opts.reason } : {},
|
|
247
|
+
...opts.actor !== undefined ? { actor: opts.actor } : {}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
appendTimeline(payload) {
|
|
251
|
+
this.#append({ type: "timeline-entry", payload });
|
|
252
|
+
}
|
|
253
|
+
appendCloseoutPhase(input) {
|
|
254
|
+
this.#append({
|
|
255
|
+
type: "closeout-phase",
|
|
256
|
+
phase: input.phase,
|
|
257
|
+
outcome: input.outcome,
|
|
258
|
+
...input.detail !== undefined ? { detail: input.detail } : {}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
appendApprovalResolved(input) {
|
|
262
|
+
this.#append({
|
|
263
|
+
type: "approval-resolved",
|
|
264
|
+
requestId: input.requestId,
|
|
265
|
+
decision: input.decision,
|
|
266
|
+
actor: input.actor,
|
|
267
|
+
...input.note !== undefined ? { note: input.note } : {}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
appendInputResolved(input) {
|
|
271
|
+
this.#append({ type: "input-resolved", requestId: input.requestId, answers: input.answers, actor: input.actor });
|
|
272
|
+
}
|
|
273
|
+
appendStall(input) {
|
|
274
|
+
this.#append({ type: "stall-detected", detail: input.detail });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
var decodeRunJournalEvent, RUN_STATUS_TRANSITIONS, TERMINAL_RUN_STATUSES;
|
|
278
|
+
var init_session_journal = __esm(() => {
|
|
279
|
+
decodeRunJournalEvent = Schema.decodeUnknownSync(RunJournalEvent);
|
|
280
|
+
RUN_STATUS_TRANSITIONS = {
|
|
281
|
+
created: ["queued", "preparing", "running", "failed", "stopped"],
|
|
282
|
+
queued: ["preparing", "running", "failed", "stopped"],
|
|
283
|
+
preparing: ["queued", "running", "needs-attention", "failed", "stopped"],
|
|
284
|
+
running: [
|
|
285
|
+
"queued",
|
|
286
|
+
"waiting-approval",
|
|
287
|
+
"waiting-user-input",
|
|
288
|
+
"paused",
|
|
289
|
+
"validating",
|
|
290
|
+
"reviewing",
|
|
291
|
+
"closing-out",
|
|
292
|
+
"needs-attention",
|
|
293
|
+
"completed",
|
|
294
|
+
"failed",
|
|
295
|
+
"stopped"
|
|
296
|
+
],
|
|
297
|
+
"waiting-approval": ["running", "needs-attention", "failed", "stopped"],
|
|
298
|
+
"waiting-user-input": ["running", "needs-attention", "failed", "stopped"],
|
|
299
|
+
paused: ["running", "failed", "stopped"],
|
|
300
|
+
validating: ["running", "reviewing", "closing-out", "needs-attention", "completed", "failed", "stopped"],
|
|
301
|
+
reviewing: ["running", "validating", "closing-out", "needs-attention", "completed", "failed", "stopped"],
|
|
302
|
+
"closing-out": ["running", "needs-attention", "completed", "failed", "stopped"],
|
|
303
|
+
"needs-attention": ["queued", "preparing", "running", "closing-out", "completed", "failed", "stopped"],
|
|
304
|
+
completed: [],
|
|
305
|
+
failed: ["queued", "preparing", "running", "closing-out"],
|
|
306
|
+
stopped: ["queued", "preparing", "running", "closing-out"]
|
|
307
|
+
};
|
|
308
|
+
TERMINAL_RUN_STATUSES = ["completed", "failed", "stopped"];
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// packages/run-worker/src/read-model-backend/run-status.ts
|
|
312
|
+
import { OPERATOR_INACTIVE_RUN_STATUSES } from "@rig/contracts";
|
|
313
|
+
function isTerminalRunStatus2(status) {
|
|
314
|
+
return TERMINAL_RUN_STATUSES2.includes(status);
|
|
315
|
+
}
|
|
316
|
+
function isActiveRunStatus(status) {
|
|
317
|
+
return !isTerminalRunStatus2(status);
|
|
318
|
+
}
|
|
319
|
+
function normalizeRunStatusToken(status) {
|
|
320
|
+
return String(status ?? "").trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
321
|
+
}
|
|
322
|
+
function canonicalStatusToken(status) {
|
|
323
|
+
const normalized = normalizeRunStatusToken(status);
|
|
324
|
+
if (normalized === "waiting-input")
|
|
325
|
+
return "waiting-user-input";
|
|
326
|
+
return normalized;
|
|
327
|
+
}
|
|
328
|
+
function asRunStatus(status) {
|
|
329
|
+
return KNOWN_RUN_STATUS[status] ? status : null;
|
|
330
|
+
}
|
|
331
|
+
function statusColorRole(status) {
|
|
332
|
+
switch (canonicalStatusToken(status)) {
|
|
333
|
+
case "done":
|
|
334
|
+
case "completed":
|
|
335
|
+
case "ready":
|
|
336
|
+
case "healthy":
|
|
337
|
+
case "approved":
|
|
338
|
+
case "merged":
|
|
339
|
+
return "success";
|
|
340
|
+
case "needs-attention":
|
|
341
|
+
case "waiting-approval":
|
|
342
|
+
case "waiting-user-input":
|
|
343
|
+
case "blocked":
|
|
344
|
+
case "paused":
|
|
345
|
+
return "action-yellow";
|
|
346
|
+
case "running":
|
|
347
|
+
case "adopted":
|
|
348
|
+
case "preparing":
|
|
349
|
+
case "created":
|
|
350
|
+
case "queued":
|
|
351
|
+
case "starting":
|
|
352
|
+
case "pending":
|
|
353
|
+
case "in-progress":
|
|
354
|
+
case "active":
|
|
355
|
+
case "booting":
|
|
356
|
+
case "validating":
|
|
357
|
+
case "reviewing":
|
|
358
|
+
case "closing-out":
|
|
359
|
+
return "active-cyan";
|
|
360
|
+
case "failed":
|
|
361
|
+
case "error":
|
|
362
|
+
case "rejected":
|
|
363
|
+
return "failure";
|
|
364
|
+
case "stopped":
|
|
365
|
+
case "cancelled":
|
|
366
|
+
case "canceled":
|
|
367
|
+
case "stale":
|
|
368
|
+
return "muted";
|
|
369
|
+
default:
|
|
370
|
+
return "neutral";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function isNeedsAttention(run) {
|
|
374
|
+
return canonicalStatusToken(run.status) === "needs-attention" || run.pendingApprovals + run.pendingInputs > 0 || run.stallCount > 0;
|
|
375
|
+
}
|
|
376
|
+
function phaseForStatus(status, runStatus, needsAttention) {
|
|
377
|
+
if (runStatus && isTerminalRunStatus2(runStatus))
|
|
378
|
+
return runStatus === "completed" || runStatus === "failed" || runStatus === "stopped" ? runStatus : "stopped";
|
|
379
|
+
if (needsAttention)
|
|
380
|
+
return "needs-attention";
|
|
381
|
+
switch (status) {
|
|
382
|
+
case "created":
|
|
383
|
+
case "queued":
|
|
384
|
+
case "preparing":
|
|
385
|
+
case "starting":
|
|
386
|
+
case "booting":
|
|
387
|
+
return "starting";
|
|
388
|
+
case "waiting-approval":
|
|
389
|
+
case "waiting-user-input":
|
|
390
|
+
return "waiting";
|
|
391
|
+
case "paused":
|
|
392
|
+
return "paused";
|
|
393
|
+
case "running":
|
|
394
|
+
case "validating":
|
|
395
|
+
case "reviewing":
|
|
396
|
+
case "closing-out":
|
|
397
|
+
return "active";
|
|
398
|
+
default:
|
|
399
|
+
return runStatus && isActiveRunStatus(runStatus) ? "active" : "unknown";
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function classifyRun(run) {
|
|
403
|
+
const status = canonicalStatusToken(run.status) || (run.live && !run.stale ? "starting" : "unknown");
|
|
404
|
+
const runStatus = asRunStatus(status);
|
|
405
|
+
const isTerminal = runStatus ? isTerminalRunStatus2(runStatus) : false;
|
|
406
|
+
const isNeedsAttentionValue = isNeedsAttention(run);
|
|
407
|
+
return {
|
|
408
|
+
status,
|
|
409
|
+
phase: phaseForStatus(status, runStatus, isNeedsAttentionValue),
|
|
410
|
+
isActive: runStatus ? isActiveRunStatus(runStatus) : !isTerminal && status !== "unknown",
|
|
411
|
+
isTerminal,
|
|
412
|
+
isNeedsAttention: isNeedsAttentionValue
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function runStatusColorRole(run) {
|
|
416
|
+
const classification = classifyRun(run);
|
|
417
|
+
return classification.isNeedsAttention && !classification.isTerminal ? "action-yellow" : statusColorRole(classification.status);
|
|
418
|
+
}
|
|
419
|
+
function statusRank(run) {
|
|
420
|
+
const classification = classifyRun(run);
|
|
421
|
+
if (classification.isNeedsAttention)
|
|
422
|
+
return 0;
|
|
423
|
+
switch (classification.phase) {
|
|
424
|
+
case "needs-attention":
|
|
425
|
+
return 0;
|
|
426
|
+
case "active":
|
|
427
|
+
case "waiting":
|
|
428
|
+
case "paused":
|
|
429
|
+
return 1;
|
|
430
|
+
case "starting":
|
|
431
|
+
return 2;
|
|
432
|
+
case "completed":
|
|
433
|
+
return 3;
|
|
434
|
+
case "failed":
|
|
435
|
+
return 4;
|
|
436
|
+
case "stopped":
|
|
437
|
+
return 5;
|
|
438
|
+
case "unknown":
|
|
439
|
+
return 6;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
var TERMINAL_RUN_STATUSES2, ACTIVE_RUN_STATUSES, KNOWN_RUN_STATUS;
|
|
443
|
+
var init_run_status = __esm(() => {
|
|
444
|
+
TERMINAL_RUN_STATUSES2 = ["completed", "failed", "stopped"];
|
|
445
|
+
ACTIVE_RUN_STATUSES = [
|
|
446
|
+
"created",
|
|
447
|
+
"queued",
|
|
448
|
+
"preparing",
|
|
449
|
+
"running",
|
|
450
|
+
"waiting-approval",
|
|
451
|
+
"waiting-user-input",
|
|
452
|
+
"paused",
|
|
453
|
+
"validating",
|
|
454
|
+
"reviewing",
|
|
455
|
+
"closing-out",
|
|
456
|
+
"needs-attention"
|
|
457
|
+
];
|
|
458
|
+
KNOWN_RUN_STATUS = Object.fromEntries([...ACTIVE_RUN_STATUSES, ...TERMINAL_RUN_STATUSES2].map((status) => [status, true]));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// packages/run-worker/src/read-model-backend/diagnostics.ts
|
|
462
|
+
function normalizeString(value) {
|
|
463
|
+
if (typeof value !== "string")
|
|
464
|
+
return null;
|
|
465
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
466
|
+
return normalized.length > 0 ? normalized : null;
|
|
467
|
+
}
|
|
468
|
+
function isGenericRunFailure(value) {
|
|
469
|
+
const text = normalizeString(value);
|
|
470
|
+
return Boolean(text && /^Task run failed \([^)]*\)$/i.test(text));
|
|
471
|
+
}
|
|
472
|
+
function appendCandidate(candidates, value) {
|
|
473
|
+
const text = normalizeString(value);
|
|
474
|
+
if (text && !candidates.includes(text))
|
|
475
|
+
candidates.push(text);
|
|
476
|
+
}
|
|
477
|
+
function categorizeUsefulRunError(lines) {
|
|
478
|
+
const taskSourceFailure = lines.find((line) => /failed to update task source/i.test(line));
|
|
479
|
+
if (taskSourceFailure)
|
|
480
|
+
return `Task source update failed: ${taskSourceFailure}`;
|
|
481
|
+
const moduleFailure = lines.find((line) => /cannot find module/i.test(line));
|
|
482
|
+
if (moduleFailure)
|
|
483
|
+
return `Runtime module resolution failed: ${moduleFailure}`;
|
|
484
|
+
const providerFailure = lines.find((line) => /no api key found|unauthorized|authentication failed|invalid api key/i.test(line));
|
|
485
|
+
if (providerFailure)
|
|
486
|
+
return `Provider authentication failed: ${providerFailure}`;
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
function summarizeRunError(projection) {
|
|
490
|
+
if (projection.status !== "failed")
|
|
491
|
+
return null;
|
|
492
|
+
const candidates = [];
|
|
493
|
+
for (const anomaly of projection.anomalies) {
|
|
494
|
+
appendCandidate(candidates, `Journal anomaly (${anomaly.kind}): ${anomaly.detail}`);
|
|
495
|
+
}
|
|
496
|
+
for (const phase of projection.closeoutPhases) {
|
|
497
|
+
if (phase.outcome === "failed")
|
|
498
|
+
appendCandidate(candidates, phase.detail);
|
|
499
|
+
}
|
|
500
|
+
for (const entry of projection.statusHistory) {
|
|
501
|
+
appendCandidate(candidates, entry.reason);
|
|
502
|
+
}
|
|
503
|
+
appendCandidate(candidates, projection.record.statusDetail);
|
|
504
|
+
appendCandidate(candidates, projection.record.errorText);
|
|
505
|
+
const nonGeneric = candidates.filter((candidate) => !isGenericRunFailure(candidate));
|
|
506
|
+
return categorizeUsefulRunError(nonGeneric) ?? nonGeneric.at(-1) ?? normalizeString(projection.record.errorText);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// packages/run-worker/src/read-model-backend/projection.ts
|
|
510
|
+
var exports_projection = {};
|
|
511
|
+
__export(exports_projection, {
|
|
512
|
+
selectRunProjection: () => selectRunProjection,
|
|
513
|
+
runRecordsFromCollab: () => runRecordsFromCollab,
|
|
514
|
+
runRecordFromRegistryEntry: () => runRecordFromRegistryEntry,
|
|
515
|
+
resolveRunJoinTarget: () => resolveRunJoinTarget,
|
|
516
|
+
resolveJoinTarget: () => resolveJoinTarget,
|
|
517
|
+
readSessionRunEntries: () => readSessionRunEntries,
|
|
518
|
+
listRuns: () => listRuns,
|
|
519
|
+
listRunProjections: () => listRunProjections,
|
|
520
|
+
getRunProjection: () => getRunProjection,
|
|
521
|
+
getRun: () => getRun,
|
|
522
|
+
discoveryDiagnosticRunRecord: () => discoveryDiagnosticRunRecord
|
|
523
|
+
});
|
|
524
|
+
import { existsSync, readFileSync } from "fs";
|
|
525
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
526
|
+
import { RUN_DISCOVERY } from "@rig/contracts";
|
|
527
|
+
import { loadCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
528
|
+
import { defineCapability } from "@rig/core/capability";
|
|
529
|
+
function registryEntryFromCollab(collab) {
|
|
530
|
+
const record = collab;
|
|
531
|
+
return {
|
|
532
|
+
roomId: collab.sessionId,
|
|
533
|
+
title: collab.title,
|
|
534
|
+
status: record.registryStatus ?? (collab.stale ? "stale" : "running"),
|
|
535
|
+
startedAt: collab.startedAt ?? null,
|
|
536
|
+
heartbeatAt: collab.updatedAt ?? null,
|
|
537
|
+
sessionPath: collab.sessionPath ?? null,
|
|
538
|
+
cwd: collab.cwd ?? null,
|
|
539
|
+
joinLink: collab.joinLink ?? null,
|
|
540
|
+
webLink: collab.webLink ?? null,
|
|
541
|
+
relayUrl: collab.relayUrl ?? null,
|
|
542
|
+
stale: collab.stale,
|
|
543
|
+
repo: collab.selectedRepo ?? null,
|
|
544
|
+
...collab.pid === undefined ? {} : { pid: collab.pid },
|
|
545
|
+
projection: record.registryProjection ?? null
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function readSessionRunEntries(sessionPath) {
|
|
549
|
+
if (!sessionPath || !existsSync(sessionPath))
|
|
550
|
+
return [];
|
|
551
|
+
try {
|
|
552
|
+
return parseSessionRunEntries(readFileSync(sessionPath, "utf8"));
|
|
553
|
+
} catch {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function parseSessionRunEntries(raw) {
|
|
558
|
+
const entries = [];
|
|
559
|
+
for (const line of raw.split(`
|
|
560
|
+
`)) {
|
|
561
|
+
if (!line.trim())
|
|
562
|
+
continue;
|
|
563
|
+
try {
|
|
564
|
+
const parsed = JSON.parse(line);
|
|
565
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
566
|
+
continue;
|
|
567
|
+
entries.push({
|
|
568
|
+
type: "type" in parsed && typeof parsed.type === "string" ? parsed.type : "",
|
|
569
|
+
..."customType" in parsed && typeof parsed.customType === "string" ? { customType: parsed.customType } : {},
|
|
570
|
+
..."data" in parsed ? { data: parsed.data } : {}
|
|
571
|
+
});
|
|
572
|
+
} catch {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return entries;
|
|
577
|
+
}
|
|
578
|
+
function stringOrNull(value) {
|
|
579
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
580
|
+
}
|
|
581
|
+
function numberOrNull(value) {
|
|
582
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
583
|
+
}
|
|
584
|
+
function objectRecord(value) {
|
|
585
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
586
|
+
}
|
|
587
|
+
function registryStatusAsRunStatus(status) {
|
|
588
|
+
if (status === "waiting-input")
|
|
589
|
+
return "waiting-user-input";
|
|
590
|
+
if (status === "starting")
|
|
591
|
+
return "preparing";
|
|
592
|
+
return typeof status === "string" ? asRunStatus(status) : null;
|
|
593
|
+
}
|
|
594
|
+
function payloadString(payload, keys) {
|
|
595
|
+
if (!payload || typeof payload !== "object")
|
|
596
|
+
return null;
|
|
597
|
+
const record = payload;
|
|
598
|
+
for (const key of keys) {
|
|
599
|
+
const value = record[key];
|
|
600
|
+
if (typeof value === "string" && value.trim())
|
|
601
|
+
return value.trim();
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
function payloadOptions(payload) {
|
|
606
|
+
if (!payload || typeof payload !== "object")
|
|
607
|
+
return;
|
|
608
|
+
const record = payload;
|
|
609
|
+
const options = Array.isArray(record.options) ? record.options : Array.isArray(record.choices) ? record.choices : null;
|
|
610
|
+
const values = options?.filter((value) => typeof value === "string" && value.trim().length > 0) ?? [];
|
|
611
|
+
return values.length > 0 ? values : undefined;
|
|
612
|
+
}
|
|
613
|
+
function inboxRequest(request, kind) {
|
|
614
|
+
const fallback = kind === "approval" ? "Approval requested" : "Input requested";
|
|
615
|
+
const body = payloadString(request.payload, ["body", "description", "detail", "details"]);
|
|
616
|
+
const options = payloadOptions(request.payload);
|
|
617
|
+
return {
|
|
618
|
+
requestId: request.requestId,
|
|
619
|
+
kind,
|
|
620
|
+
title: payloadString(request.payload, ["title", "message", "reason", "prompt", "summary"]) ?? fallback,
|
|
621
|
+
...body !== undefined ? { body } : {},
|
|
622
|
+
...options ? { options } : {},
|
|
623
|
+
requestedAt: request.requestedAt ?? null,
|
|
624
|
+
source: "run"
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function inboxFromProjection(projection) {
|
|
628
|
+
return [
|
|
629
|
+
...projection.pendingApprovals.map((request) => inboxRequest(request, "approval")),
|
|
630
|
+
...projection.pendingUserInputs.map((request) => inboxRequest(request, "input"))
|
|
631
|
+
].sort((a, b) => (Date.parse(a.requestedAt ?? "") || 0) - (Date.parse(b.requestedAt ?? "") || 0));
|
|
632
|
+
}
|
|
633
|
+
function isProjectPath(projectRoot, cwd) {
|
|
634
|
+
const root = resolve(projectRoot);
|
|
635
|
+
const candidate = resolve(cwd);
|
|
636
|
+
const pathFromRoot = relative(root, candidate);
|
|
637
|
+
return pathFromRoot === "" || !pathFromRoot.startsWith("../") && pathFromRoot !== ".." && !isAbsolute(pathFromRoot);
|
|
638
|
+
}
|
|
639
|
+
function sourceFromEntry(projectRoot, projection, entry) {
|
|
640
|
+
const candidates = [
|
|
641
|
+
stringOrNull(projection.collabCwd),
|
|
642
|
+
stringOrNull(projection.cwd),
|
|
643
|
+
stringOrNull(entry.cwd),
|
|
644
|
+
stringOrNull(projection.worktreePath)
|
|
645
|
+
].filter((cwd) => cwd !== null);
|
|
646
|
+
return candidates.some((cwd) => isProjectPath(projectRoot, cwd)) ? "local" : "remote";
|
|
647
|
+
}
|
|
648
|
+
function pendingRequests(value, fallbackKind) {
|
|
649
|
+
if (!Array.isArray(value))
|
|
650
|
+
return [];
|
|
651
|
+
return value.flatMap((item) => {
|
|
652
|
+
const record = objectRecord(item);
|
|
653
|
+
const requestId = stringOrNull(record?.requestId) ?? stringOrNull(record?.id);
|
|
654
|
+
if (!record || !requestId)
|
|
655
|
+
return [];
|
|
656
|
+
return [{
|
|
657
|
+
requestId,
|
|
658
|
+
requestKind: stringOrNull(record.requestKind) ?? stringOrNull(record.kind) ?? fallbackKind,
|
|
659
|
+
actionId: stringOrNull(record.actionId),
|
|
660
|
+
payload: "payload" in record ? record.payload : record,
|
|
661
|
+
requestedAt: stringOrNull(record.requestedAt) ?? stringOrNull(record.at) ?? ""
|
|
662
|
+
}];
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function emptyFoldedProjection(runId, projection) {
|
|
666
|
+
const projectionRecord = projection;
|
|
667
|
+
const nestedProjection = objectRecord(projectionRecord.projection);
|
|
668
|
+
const pendingApprovals = pendingRequests(nestedProjection?.pendingApprovals ?? projectionRecord.pendingApprovals, "approval");
|
|
669
|
+
const pendingUserInputs = pendingRequests(nestedProjection?.pendingUserInputs ?? nestedProjection?.pendingInputs ?? projectionRecord.pendingUserInputs ?? (Array.isArray(projectionRecord.pendingInputs) ? projectionRecord.pendingInputs : undefined), "user-input");
|
|
670
|
+
const nestedRecord = objectRecord(nestedProjection?.record);
|
|
671
|
+
const nestedFolded = nestedProjection;
|
|
672
|
+
return {
|
|
673
|
+
...EMPTY_PROJECTION,
|
|
674
|
+
...nestedFolded ?? {},
|
|
675
|
+
record: {
|
|
676
|
+
...nestedRecord ?? {},
|
|
677
|
+
runId,
|
|
678
|
+
...projection.taskId ? { taskId: projection.taskId } : {},
|
|
679
|
+
...projection.title ? { title: projection.title } : {},
|
|
680
|
+
...projection.startedAt ? { startedAt: projection.startedAt } : {},
|
|
681
|
+
...projection.updatedAt ? { updatedAt: projection.updatedAt } : {},
|
|
682
|
+
...projection.completedAt ? { completedAt: projection.completedAt } : {},
|
|
683
|
+
...projection.sessionPath ? { sessionPath: projection.sessionPath } : {},
|
|
684
|
+
...projection.worktreePath ? { worktreePath: projection.worktreePath } : {},
|
|
685
|
+
...projection.prUrl ? { prUrl: projection.prUrl } : {}
|
|
686
|
+
},
|
|
687
|
+
status: registryStatusAsRunStatus(projection.status) ?? registryStatusAsRunStatus(nestedProjection?.status),
|
|
688
|
+
pendingApprovals,
|
|
689
|
+
pendingUserInputs,
|
|
690
|
+
steeringCount: projection.steeringCount ?? numberOrNull(nestedProjection?.steeringCount) ?? 0,
|
|
691
|
+
stallCount: projection.stallCount ?? numberOrNull(nestedProjection?.stallCount) ?? 0,
|
|
692
|
+
lastEventAt: projection.updatedAt ?? stringOrNull(nestedProjection?.lastEventAt)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function runRecordFromRegistryEntry(projectRoot, entry) {
|
|
696
|
+
const projection = entry.projection && typeof entry.projection === "object" ? entry.projection : {};
|
|
697
|
+
const runId = stringOrNull(projection.runId) ?? entry.roomId;
|
|
698
|
+
const folded = emptyFoldedProjection(runId, projection);
|
|
699
|
+
const pushedStatus = registryStatusAsRunStatus(projection.status) ?? folded.status ?? registryStatusAsRunStatus(entry.status);
|
|
700
|
+
const status = entry.stale ? pushedStatus && isTerminalRunStatus(pushedStatus) ? pushedStatus : "stale" : pushedStatus ?? "running";
|
|
701
|
+
const sessionPath = stringOrNull(projection.sessionPath) ?? stringOrNull(entry.sessionPath) ?? null;
|
|
702
|
+
const collabCwd = stringOrNull(projection.collabCwd) ?? stringOrNull(projection.cwd) ?? stringOrNull(entry.cwd) ?? stringOrNull(projection.worktreePath);
|
|
703
|
+
return {
|
|
704
|
+
runId,
|
|
705
|
+
taskId: stringOrNull(projection.taskId),
|
|
706
|
+
title: stringOrNull(projection.title) ?? entry.title,
|
|
707
|
+
status,
|
|
708
|
+
source: sourceFromEntry(projectRoot, projection, entry),
|
|
709
|
+
live: !entry.stale,
|
|
710
|
+
stale: entry.stale,
|
|
711
|
+
startedAt: stringOrNull(projection.startedAt) ?? entry.startedAt ?? null,
|
|
712
|
+
updatedAt: stringOrNull(projection.updatedAt) ?? entry.heartbeatAt ?? null,
|
|
713
|
+
completedAt: stringOrNull(projection.completedAt),
|
|
714
|
+
joinLink: stringOrNull(projection.joinLink) ?? entry.joinLink ?? null,
|
|
715
|
+
webLink: stringOrNull(projection.webLink) ?? entry.webLink ?? null,
|
|
716
|
+
relayUrl: stringOrNull(projection.relayUrl) ?? entry.relayUrl ?? null,
|
|
717
|
+
sessionPath,
|
|
718
|
+
prUrl: stringOrNull(projection.prUrl),
|
|
719
|
+
worktreePath: stringOrNull(projection.worktreePath) ?? collabCwd,
|
|
720
|
+
pendingApprovals: numberOrNull(projection.pendingApprovals) ?? folded.pendingApprovals.length,
|
|
721
|
+
pendingInputs: numberOrNull(projection.pendingInputs) ?? folded.pendingUserInputs.length,
|
|
722
|
+
steeringCount: projection.steeringCount ?? 0,
|
|
723
|
+
stallCount: projection.stallCount ?? 0,
|
|
724
|
+
errorSummary: stringOrNull(projection.errorSummary) ?? summarizeRunError(folded),
|
|
725
|
+
timeline: Array.isArray(projection.timeline) ? [...projection.timeline] : [...timelineEntriesFromCustomEntries([])],
|
|
726
|
+
inbox: inboxFromProjection(folded),
|
|
727
|
+
collabCwd: collabCwd ?? null,
|
|
728
|
+
projection: folded
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function runRecordsFromCollab(projectRoot, collabs) {
|
|
732
|
+
return sortByRecency(collabs.map((collab) => runRecordFromRegistryEntry(projectRoot, registryEntryFromCollab(collab))).filter((record) => record !== null));
|
|
733
|
+
}
|
|
734
|
+
function sortByRecency(records) {
|
|
735
|
+
return [...records].sort((a, b) => {
|
|
736
|
+
const at = Date.parse(b.updatedAt ?? b.startedAt ?? "") || 0;
|
|
737
|
+
const bt = Date.parse(a.updatedAt ?? a.startedAt ?? "") || 0;
|
|
738
|
+
return at - bt;
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function discoveryDiagnosticRunRecord(projectRoot, error) {
|
|
742
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
743
|
+
const runId = DISCOVERY_DIAGNOSTIC_RUN_ID;
|
|
744
|
+
const projection = {
|
|
745
|
+
...EMPTY_PROJECTION,
|
|
746
|
+
record: { runId, title: "Registry discovery unavailable" },
|
|
747
|
+
status: "needs-attention",
|
|
748
|
+
stallCount: 1
|
|
749
|
+
};
|
|
750
|
+
return {
|
|
751
|
+
runId,
|
|
752
|
+
taskId: null,
|
|
753
|
+
title: "Registry discovery unavailable",
|
|
754
|
+
status: "needs-attention",
|
|
755
|
+
source: "remote",
|
|
756
|
+
live: false,
|
|
757
|
+
stale: true,
|
|
758
|
+
startedAt: null,
|
|
759
|
+
updatedAt: null,
|
|
760
|
+
completedAt: null,
|
|
761
|
+
joinLink: null,
|
|
762
|
+
webLink: null,
|
|
763
|
+
relayUrl: null,
|
|
764
|
+
sessionPath: null,
|
|
765
|
+
prUrl: null,
|
|
766
|
+
worktreePath: null,
|
|
767
|
+
pendingApprovals: 0,
|
|
768
|
+
pendingInputs: 0,
|
|
769
|
+
steeringCount: 0,
|
|
770
|
+
stallCount: 1,
|
|
771
|
+
errorSummary: `registry discovery unavailable: ${detail}`,
|
|
772
|
+
timeline: [],
|
|
773
|
+
inbox: [],
|
|
774
|
+
collabCwd: null,
|
|
775
|
+
projection
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
async function listRunProjections(projectRoot, filter = {}) {
|
|
779
|
+
try {
|
|
780
|
+
const discovery = await loadCapabilityForRoot(projectRoot, RunDiscoveryCap);
|
|
781
|
+
if (!discovery)
|
|
782
|
+
return [discoveryDiagnosticRunRecord(projectRoot, "run discovery capability unavailable")];
|
|
783
|
+
const collabs = await discovery.listActiveRunCollab(projectRoot, filter);
|
|
784
|
+
return runRecordsFromCollab(projectRoot, collabs);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
return [discoveryDiagnosticRunRecord(projectRoot, error)];
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function selectRunProjection(runs, runId) {
|
|
790
|
+
const exactRun = runs.find((run) => run.runId === runId);
|
|
791
|
+
if (exactRun)
|
|
792
|
+
return exactRun;
|
|
793
|
+
const exactTask = runs.find((run) => run.taskId === runId);
|
|
794
|
+
if (exactTask)
|
|
795
|
+
return exactTask;
|
|
796
|
+
const prefixMatches = runs.filter((run) => run.runId.startsWith(runId));
|
|
797
|
+
if (prefixMatches.length === 1)
|
|
798
|
+
return prefixMatches[0] ?? null;
|
|
799
|
+
if (prefixMatches.length > 1) {
|
|
800
|
+
const matches = prefixMatches.map((run) => run.runId).join(", ");
|
|
801
|
+
throw new Error(`Ambiguous run id prefix "${runId}" matched ${prefixMatches.length} runs: ${matches}`);
|
|
802
|
+
}
|
|
803
|
+
return runs.find((run) => run.runId === DISCOVERY_DIAGNOSTIC_RUN_ID) ?? null;
|
|
804
|
+
}
|
|
805
|
+
async function getRunProjection(projectRoot, runId, filter = {}) {
|
|
806
|
+
const runs = await listRunProjections(projectRoot, filter);
|
|
807
|
+
return selectRunProjection(runs, runId);
|
|
808
|
+
}
|
|
809
|
+
async function resolveRunJoinTarget(projectRoot, runId, filter = {}) {
|
|
810
|
+
const run = await getRunProjection(projectRoot, runId, filter);
|
|
811
|
+
if (!run || !run.joinLink)
|
|
812
|
+
return null;
|
|
813
|
+
return { runId: run.runId, taskId: run.taskId, joinLink: run.joinLink, cwd: run.collabCwd ?? run.sessionPath, stale: run.stale };
|
|
814
|
+
}
|
|
815
|
+
var RunDiscoveryCap, EMPTY_PROJECTION, DISCOVERY_DIAGNOSTIC_RUN_ID = "__registry_discovery_error__", listRuns, getRun, resolveJoinTarget;
|
|
816
|
+
var init_projection = __esm(() => {
|
|
817
|
+
init_session_journal();
|
|
818
|
+
init_run_status();
|
|
819
|
+
RunDiscoveryCap = defineCapability(RUN_DISCOVERY);
|
|
820
|
+
EMPTY_PROJECTION = foldRunSessionEntries([], "");
|
|
821
|
+
listRuns = listRunProjections;
|
|
822
|
+
getRun = getRunProjection;
|
|
823
|
+
resolveJoinTarget = resolveRunJoinTarget;
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// packages/run-worker/src/read-model-service.ts
|
|
827
|
+
import { RUN_REGISTRY_BACKBONE } from "@rig/contracts";
|
|
828
|
+
import { loadCapabilityForRoot as loadCapabilityForRoot2 } from "@rig/core/capability-loaders";
|
|
829
|
+
import { defineCapability as defineCapability2 } from "@rig/core/capability";
|
|
830
|
+
import { resolveOwnerNamespaceKey, resolveRegistryBaseUrl } from "@rig/core/remote-config";
|
|
831
|
+
|
|
832
|
+
// packages/run-worker/src/read-model-backend/control.ts
|
|
833
|
+
init_projection();
|
|
834
|
+
import { RIG_CONTROL_SENTINEL_END as RIG_CONTROL_SENTINEL_END2, RIG_PAUSE_SENTINEL as RIG_PAUSE_SENTINEL2, RIG_RESUME_SENTINEL as RIG_RESUME_SENTINEL2, RIG_STOP_SENTINEL as RIG_STOP_SENTINEL2, RIG_STOP_SENTINEL_END as RIG_STOP_SENTINEL_END2 } from "@rig/contracts";
|
|
835
|
+
function buildPauseSentinel(runId) {
|
|
836
|
+
return `${RIG_PAUSE_SENTINEL2} runId=${runId} requestedBy=operator ${RIG_CONTROL_SENTINEL_END2}`;
|
|
837
|
+
}
|
|
838
|
+
function buildResumeSentinel(runId) {
|
|
839
|
+
return `${RIG_RESUME_SENTINEL2} runId=${runId} requestedBy=operator ${RIG_CONTROL_SENTINEL_END2}`;
|
|
840
|
+
}
|
|
841
|
+
function buildStopSentinel(runId, reason) {
|
|
842
|
+
return `${RIG_STOP_SENTINEL2} runId=${runId} reason=${reason} ${RIG_STOP_SENTINEL_END2}`;
|
|
843
|
+
}
|
|
844
|
+
async function loadCollabControlDeps() {
|
|
845
|
+
const [{ importRoomKey, seal }, { COLLAB_PROTO, packEnvelope, parseCollabLink }] = await Promise.all([
|
|
846
|
+
import("@oh-my-pi/pi-coding-agent/collab/crypto"),
|
|
847
|
+
import("@oh-my-pi/pi-coding-agent/collab/protocol")
|
|
848
|
+
]);
|
|
849
|
+
return { importRoomKey, seal, COLLAB_PROTO, packEnvelope, parseCollabLink };
|
|
850
|
+
}
|
|
851
|
+
function collabControlFrames(runId, control) {
|
|
852
|
+
switch (control.kind) {
|
|
853
|
+
case "steer":
|
|
854
|
+
return [{ t: "prompt", text: control.message }];
|
|
855
|
+
case "resume":
|
|
856
|
+
return [{ t: "prompt", text: `${buildResumeSentinel(runId)}
|
|
857
|
+
Continue the run from where it paused.` }];
|
|
858
|
+
case "pause":
|
|
859
|
+
return [
|
|
860
|
+
{ t: "prompt", text: `${buildPauseSentinel(runId)}
|
|
861
|
+
Pause this run after the current interruption settles.` },
|
|
862
|
+
{ t: "abort" }
|
|
863
|
+
];
|
|
864
|
+
case "stop":
|
|
865
|
+
return [
|
|
866
|
+
{ t: "prompt", text: `${buildStopSentinel(runId, control.reason)}
|
|
867
|
+
Stop this run and do not continue after the interruption settles.` },
|
|
868
|
+
{ t: "abort" }
|
|
869
|
+
];
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
async function sendSealedCollabFrames(joinLink, frames, deps = {}) {
|
|
873
|
+
const { importRoomKey, seal, COLLAB_PROTO, packEnvelope, parseCollabLink } = await loadCollabControlDeps();
|
|
874
|
+
const parsed = parseCollabLink(joinLink);
|
|
875
|
+
if ("error" in parsed)
|
|
876
|
+
throw new Error(parsed.error);
|
|
877
|
+
if (!parsed.writeToken)
|
|
878
|
+
throw new Error("Run collab link is read-only; cannot send control.");
|
|
879
|
+
const key = await importRoomKey(parsed.key);
|
|
880
|
+
const WebSocketCtor = deps.WebSocket ?? WebSocket;
|
|
881
|
+
const ws = new WebSocketCtor(`${parsed.wsUrl}?role=guest`);
|
|
882
|
+
await new Promise((resolveOpen, rejectOpen) => {
|
|
883
|
+
ws.addEventListener("open", () => resolveOpen(), { once: true });
|
|
884
|
+
ws.addEventListener("error", () => rejectOpen(new Error("collab websocket error")), { once: true });
|
|
885
|
+
});
|
|
886
|
+
const hello = {
|
|
887
|
+
t: "hello",
|
|
888
|
+
proto: COLLAB_PROTO,
|
|
889
|
+
name: "rig-operator",
|
|
890
|
+
writeToken: Buffer.from(parsed.writeToken).toString("base64url")
|
|
891
|
+
};
|
|
892
|
+
ws.send(packEnvelope(0, await seal(key, hello)));
|
|
893
|
+
for (const frame of frames)
|
|
894
|
+
ws.send(packEnvelope(0, await seal(key, frame)));
|
|
895
|
+
try {
|
|
896
|
+
ws.close();
|
|
897
|
+
} catch {}
|
|
898
|
+
}
|
|
899
|
+
async function deliverRemoteRunControl(projectRoot, target, control, deps = {}) {
|
|
900
|
+
const record = typeof target === "string" ? await getRun(projectRoot, target) : target;
|
|
901
|
+
const runId = typeof target === "string" ? target : target.runId;
|
|
902
|
+
if (!record)
|
|
903
|
+
throw new Error(`Run not found: ${runId}`);
|
|
904
|
+
if (!record.joinLink)
|
|
905
|
+
throw new Error(`Run ${record.runId} has no writable collab join link.`);
|
|
906
|
+
await sendSealedCollabFrames(record.joinLink, collabControlFrames(record.runId, control), deps);
|
|
907
|
+
}
|
|
908
|
+
async function deliverRunControl(projectRoot, target, control, deps = {}) {
|
|
909
|
+
const record = typeof target === "string" ? await (deps.getRun ?? getRun)(projectRoot, target) : target;
|
|
910
|
+
const runId = typeof target === "string" ? target : target.runId;
|
|
911
|
+
if (!record)
|
|
912
|
+
throw new Error(`Run not found: ${runId}`);
|
|
913
|
+
if (!record.joinLink)
|
|
914
|
+
throw new Error(`Run ${record.runId} has no writable collab join link; attach interactively to ${control.kind}.`);
|
|
915
|
+
try {
|
|
916
|
+
await (deps.deliverRemote ?? deliverRemoteRunControl)(projectRoot, record, control);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
const detail = error instanceof Error ? error.message : `Attach to ${control.kind} interactively: rig run attach ${record.runId}`;
|
|
919
|
+
throw new Error(`Could not ${control.kind} run ${record.runId}. ${detail}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// packages/run-worker/src/read-model-backend/guard.ts
|
|
923
|
+
var TERMINAL = new Set(["completed", "failed", "stopped"]);
|
|
924
|
+
// packages/run-worker/src/read-model-backend/inbox.ts
|
|
925
|
+
import { RIG_CONTROL_SENTINEL_END as RIG_CONTROL_SENTINEL_END3, RIG_INBOX_RESOLUTION_SENTINEL as RIG_INBOX_RESOLUTION_SENTINEL2 } from "@rig/contracts";
|
|
926
|
+
init_projection();
|
|
927
|
+
function buildInboxResolutionSentinel(runId, payload) {
|
|
928
|
+
return `${RIG_INBOX_RESOLUTION_SENTINEL2} runId=${runId} data=${encodeURIComponent(JSON.stringify(payload))} requestedBy=operator ${RIG_CONTROL_SENTINEL_END3}`;
|
|
929
|
+
}
|
|
930
|
+
function promptFromPayload(payload, fallback) {
|
|
931
|
+
if (payload && typeof payload === "object") {
|
|
932
|
+
const record = payload;
|
|
933
|
+
for (const key of ["title", "message", "reason", "prompt", "summary"]) {
|
|
934
|
+
const value = record[key];
|
|
935
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
936
|
+
return value.trim();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return fallback;
|
|
940
|
+
}
|
|
941
|
+
function recordsFromRun(run, kind) {
|
|
942
|
+
const pending = kind === "approvals" ? run.projection.pendingApprovals : run.projection.pendingUserInputs;
|
|
943
|
+
return pending.map((request) => ({
|
|
944
|
+
runId: run.runId,
|
|
945
|
+
taskId: run.taskId,
|
|
946
|
+
requestId: request.requestId,
|
|
947
|
+
status: "pending",
|
|
948
|
+
prompt: promptFromPayload(request.payload, kind === "approvals" ? "Approval requested" : "Input requested"),
|
|
949
|
+
requestedAt: request.requestedAt,
|
|
950
|
+
payload: request.payload,
|
|
951
|
+
kind: kind === "approvals" ? "approval" : "input"
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
async function listInboxRecords(context, kind, filters = {}, deps = {}) {
|
|
955
|
+
const projectRoot = typeof context === "string" ? context : context.projectRoot;
|
|
956
|
+
const runs = await (deps.listRuns ?? listRuns)(projectRoot);
|
|
957
|
+
const out = [];
|
|
958
|
+
for (const run of runs) {
|
|
959
|
+
if (filters.run && run.runId !== filters.run)
|
|
960
|
+
continue;
|
|
961
|
+
if (filters.task && run.taskId !== filters.task)
|
|
962
|
+
continue;
|
|
963
|
+
out.push(...recordsFromRun(run, kind));
|
|
964
|
+
}
|
|
965
|
+
return out;
|
|
966
|
+
}
|
|
967
|
+
function normalizedApprovalDecision(decision) {
|
|
968
|
+
return decision === "approved" || decision === "approve" ? "approve" : "reject";
|
|
969
|
+
}
|
|
970
|
+
function inputAnswers(answer) {
|
|
971
|
+
return typeof answer === "string" ? { answer } : answer;
|
|
972
|
+
}
|
|
973
|
+
function decisionText(runId, requestId, decision) {
|
|
974
|
+
if (decision.kind === "approval") {
|
|
975
|
+
const normalized = normalizedApprovalDecision(decision.decision);
|
|
976
|
+
const label = normalized === "approve" ? "approved" : "rejected";
|
|
977
|
+
return [
|
|
978
|
+
buildInboxResolutionSentinel(runId, { kind: "approval", requestId, decision: normalized, note: decision.note ?? null }),
|
|
979
|
+
`Rig inbox approval ${requestId} was ${label}.`,
|
|
980
|
+
decision.note ? `Operator note: ${decision.note}` : null,
|
|
981
|
+
"Continue using this approval decision."
|
|
982
|
+
].filter((line) => Boolean(line)).join(`
|
|
983
|
+
`);
|
|
984
|
+
}
|
|
985
|
+
const answers = inputAnswers(decision.answer);
|
|
986
|
+
return [
|
|
987
|
+
buildInboxResolutionSentinel(runId, { kind: "input", requestId, answers }),
|
|
988
|
+
`Rig inbox input ${requestId} was answered with:`,
|
|
989
|
+
JSON.stringify(answers),
|
|
990
|
+
"Continue using this input answer."
|
|
991
|
+
].join(`
|
|
992
|
+
`);
|
|
993
|
+
}
|
|
994
|
+
async function resolveInboxRequest(projectRoot, runId, requestId, decision, deps = {}) {
|
|
995
|
+
await (deps.deliverRunControl ?? deliverRunControl)(projectRoot, runId, { kind: "steer", message: decisionText(runId, requestId, decision) });
|
|
996
|
+
}
|
|
997
|
+
// packages/run-worker/src/read-model-backend/inspect.ts
|
|
998
|
+
init_projection();
|
|
999
|
+
import { RIG_RUN_LOG_ENTRY } from "@rig/contracts";
|
|
1000
|
+
function asRecord(value) {
|
|
1001
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1002
|
+
}
|
|
1003
|
+
function firstString(record, keys) {
|
|
1004
|
+
for (const key of keys) {
|
|
1005
|
+
const value = record[key];
|
|
1006
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
1007
|
+
return value;
|
|
1008
|
+
}
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
function stringifyLogPayload(value) {
|
|
1012
|
+
if (typeof value === "string")
|
|
1013
|
+
return value;
|
|
1014
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
1015
|
+
return String(value);
|
|
1016
|
+
const record = asRecord(value);
|
|
1017
|
+
if (!record)
|
|
1018
|
+
return null;
|
|
1019
|
+
const direct = firstString(record, ["line", "message", "text", "content", "output", "summary", "detail"]);
|
|
1020
|
+
if (direct)
|
|
1021
|
+
return direct;
|
|
1022
|
+
try {
|
|
1023
|
+
return JSON.stringify(record);
|
|
1024
|
+
} catch {
|
|
1025
|
+
return String(record);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function logLineFromValue(value) {
|
|
1029
|
+
const record = asRecord(value);
|
|
1030
|
+
if (!record)
|
|
1031
|
+
return stringifyLogPayload(value);
|
|
1032
|
+
const direct = firstString(record, ["line", "message", "text", "content", "output", "summary", "detail"]);
|
|
1033
|
+
if (direct)
|
|
1034
|
+
return direct;
|
|
1035
|
+
if ("payload" in record)
|
|
1036
|
+
return stringifyLogPayload(record.payload);
|
|
1037
|
+
if ("data" in record)
|
|
1038
|
+
return stringifyLogPayload(record.data);
|
|
1039
|
+
return stringifyLogPayload(record);
|
|
1040
|
+
}
|
|
1041
|
+
function logLineFromSessionEntry(entry) {
|
|
1042
|
+
if (entry.type !== "custom" || entry.customType !== RIG_RUN_LOG_ENTRY)
|
|
1043
|
+
return null;
|
|
1044
|
+
return logLineFromValue(entry.data);
|
|
1045
|
+
}
|
|
1046
|
+
function logLinesFromProjection(projection) {
|
|
1047
|
+
const projected = projection;
|
|
1048
|
+
const lines = [];
|
|
1049
|
+
for (const key of ["logs", "logEntries", "journalLogs", "runLogs"]) {
|
|
1050
|
+
const entries = projected[key];
|
|
1051
|
+
if (!Array.isArray(entries))
|
|
1052
|
+
continue;
|
|
1053
|
+
for (const entry of entries) {
|
|
1054
|
+
const line = logLineFromValue(entry);
|
|
1055
|
+
if (line !== null)
|
|
1056
|
+
lines.push(line);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return lines;
|
|
1060
|
+
}
|
|
1061
|
+
function extractRunLogs(run, deps = {}) {
|
|
1062
|
+
const projectedLines = logLinesFromProjection(run.projection);
|
|
1063
|
+
if (projectedLines.length > 0)
|
|
1064
|
+
return projectedLines;
|
|
1065
|
+
return (deps.readSessionRunEntries ?? readSessionRunEntries)(run.sessionPath).map(logLineFromSessionEntry).filter((line) => line !== null);
|
|
1066
|
+
}
|
|
1067
|
+
function summarizeRunFailures(run) {
|
|
1068
|
+
const failures = [];
|
|
1069
|
+
const useful = summarizeRunError(run.projection);
|
|
1070
|
+
if (useful)
|
|
1071
|
+
failures.push(`${run.runId}: ${useful}`);
|
|
1072
|
+
if (run.status === "failed" || run.projection.status === "failed")
|
|
1073
|
+
failures.push(`${run.runId}: run status failed`);
|
|
1074
|
+
for (const phase of run.projection.closeoutPhases) {
|
|
1075
|
+
if (phase.outcome === "failed")
|
|
1076
|
+
failures.push(`${run.runId}: closeout ${phase.phase} failed${phase.detail ? ` \u2014 ${phase.detail}` : ""}`);
|
|
1077
|
+
}
|
|
1078
|
+
for (const anomaly of run.projection.anomalies) {
|
|
1079
|
+
failures.push(`${run.runId}: anomaly ${anomaly.kind}${anomaly.detail ? ` \u2014 ${anomaly.detail}` : ""}`);
|
|
1080
|
+
}
|
|
1081
|
+
return failures;
|
|
1082
|
+
}
|
|
1083
|
+
function runInspectSummaryRows(runs, options = {}) {
|
|
1084
|
+
const attachable = runs.filter((run) => Boolean(run.joinLink && !run.stale)).length;
|
|
1085
|
+
const pendingInbox = options.pendingInbox ?? runs.reduce((total, run) => total + run.pendingApprovals + run.pendingInputs, 0);
|
|
1086
|
+
return [
|
|
1087
|
+
{ id: "title", label: "Inspect", currentValue: "runtime", heading: true, description: "session/runtime inspection rows from @rig/client" },
|
|
1088
|
+
{ id: "inspect:sessions", label: "RUNS", currentValue: String(runs.length), heading: true, description: `${attachable} attachable OMP collab links` },
|
|
1089
|
+
{ id: "inspect:cwd", label: "PROJECT ROOT", currentValue: options.projectRoot ?? "", heading: true, description: "selected target/project root used by Rig chrome" },
|
|
1090
|
+
{ id: "inspect:inbox", label: "INBOX", currentValue: String(pendingInbox), values: ["open"], description: "pending run gates; open Inbox to resolve" },
|
|
1091
|
+
{ id: "inspect:runs", label: "RUNS", currentValue: String(runs.length), values: ["open"], description: "open Runs to inspect individual run detail, logs, and attach controls" },
|
|
1092
|
+
{ id: "inspect:audit", label: "AUDIT", currentValue: "unavailable", heading: true, description: "CLI inspect audit has no cockpit equivalent; use rig inspect audit" }
|
|
1093
|
+
];
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// packages/run-worker/src/read-model-backend/index.ts
|
|
1097
|
+
init_run_status();
|
|
1098
|
+
|
|
1099
|
+
// packages/run-worker/src/read-model-backend/stats.ts
|
|
1100
|
+
init_run_status();
|
|
1101
|
+
init_run_status();
|
|
1102
|
+
function parseTimestamp(value) {
|
|
1103
|
+
if (!value)
|
|
1104
|
+
return null;
|
|
1105
|
+
const parsed = Date.parse(value);
|
|
1106
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1107
|
+
}
|
|
1108
|
+
function median(values) {
|
|
1109
|
+
if (values.length === 0)
|
|
1110
|
+
return null;
|
|
1111
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
1112
|
+
const middle = Math.floor(sorted.length / 2);
|
|
1113
|
+
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
1114
|
+
}
|
|
1115
|
+
function rate(part, total) {
|
|
1116
|
+
return total === 0 ? null : part / total;
|
|
1117
|
+
}
|
|
1118
|
+
function completedDuration(run) {
|
|
1119
|
+
if (run.status !== "completed")
|
|
1120
|
+
return null;
|
|
1121
|
+
const startedAt = parseTimestamp(run.startedAt);
|
|
1122
|
+
const completedAt = parseTimestamp(run.completedAt);
|
|
1123
|
+
if (startedAt === null || completedAt === null || completedAt < startedAt)
|
|
1124
|
+
return null;
|
|
1125
|
+
return completedAt - startedAt;
|
|
1126
|
+
}
|
|
1127
|
+
async function computeStats(projectRootOrRuns, options = {}) {
|
|
1128
|
+
const sinceMs = options.since ? options.since.getTime() : null;
|
|
1129
|
+
const allRuns = typeof projectRootOrRuns === "string" ? await (options.listRuns ?? (await Promise.resolve().then(() => (init_projection(), exports_projection))).listRuns)(projectRootOrRuns) : projectRootOrRuns;
|
|
1130
|
+
const runs = allRuns.filter((run) => {
|
|
1131
|
+
if (sinceMs === null)
|
|
1132
|
+
return true;
|
|
1133
|
+
const startedAt = parseTimestamp(run.startedAt);
|
|
1134
|
+
return startedAt !== null && startedAt >= sinceMs;
|
|
1135
|
+
});
|
|
1136
|
+
const statusCounts = {};
|
|
1137
|
+
const completionDurations = [];
|
|
1138
|
+
let completedRuns = 0;
|
|
1139
|
+
let failedRuns = 0;
|
|
1140
|
+
let needsAttentionRuns = 0;
|
|
1141
|
+
let steeringTotal = 0;
|
|
1142
|
+
let stallTotal = 0;
|
|
1143
|
+
let approvalsPending = 0;
|
|
1144
|
+
for (const run of runs) {
|
|
1145
|
+
const status = run.status || "unknown";
|
|
1146
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
1147
|
+
if (status === "completed")
|
|
1148
|
+
completedRuns += 1;
|
|
1149
|
+
if (status === "failed")
|
|
1150
|
+
failedRuns += 1;
|
|
1151
|
+
if (isNeedsAttention(run))
|
|
1152
|
+
needsAttentionRuns += 1;
|
|
1153
|
+
const duration = completedDuration(run);
|
|
1154
|
+
if (duration !== null)
|
|
1155
|
+
completionDurations.push(duration);
|
|
1156
|
+
steeringTotal += run.steeringCount;
|
|
1157
|
+
stallTotal += run.stallCount;
|
|
1158
|
+
approvalsPending += run.pendingApprovals;
|
|
1159
|
+
}
|
|
1160
|
+
const totalRuns = runs.length;
|
|
1161
|
+
return {
|
|
1162
|
+
since: options.since ? options.since.toISOString() : null,
|
|
1163
|
+
totalRuns,
|
|
1164
|
+
statusCounts,
|
|
1165
|
+
completedRuns,
|
|
1166
|
+
failedRuns,
|
|
1167
|
+
needsAttentionRuns,
|
|
1168
|
+
completionRate: rate(completedRuns, totalRuns),
|
|
1169
|
+
failureRate: rate(failedRuns, totalRuns),
|
|
1170
|
+
needsAttentionRate: rate(needsAttentionRuns, totalRuns),
|
|
1171
|
+
medianCompletionMs: median(completionDurations),
|
|
1172
|
+
steeringTotal,
|
|
1173
|
+
steeringPerRun: totalRuns === 0 ? null : steeringTotal / totalRuns,
|
|
1174
|
+
stallTotal,
|
|
1175
|
+
approvalsRequested: approvalsPending,
|
|
1176
|
+
approvalsApproved: 0,
|
|
1177
|
+
approvalsRejected: 0,
|
|
1178
|
+
approvalsPending
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// packages/run-worker/src/read-model-backend/index.ts
|
|
1183
|
+
init_projection();
|
|
1184
|
+
|
|
1185
|
+
// packages/run-worker/src/read-model-service.ts
|
|
1186
|
+
var RunRegistryBackboneCap = defineCapability2(RUN_REGISTRY_BACKBONE);
|
|
1187
|
+
function discoveryFilter(filter) {
|
|
1188
|
+
return filter ?? {};
|
|
1189
|
+
}
|
|
1190
|
+
function requestedKinds(kind) {
|
|
1191
|
+
if (kind === "approval")
|
|
1192
|
+
return ["approvals"];
|
|
1193
|
+
if (kind === "input")
|
|
1194
|
+
return ["inputs"];
|
|
1195
|
+
return ["approvals", "inputs"];
|
|
1196
|
+
}
|
|
1197
|
+
function sourceMatches(run, source) {
|
|
1198
|
+
return !source || source === "all" || run.source === source;
|
|
1199
|
+
}
|
|
1200
|
+
function failureSummaries(run) {
|
|
1201
|
+
return summarizeRunFailures(run).map((summary) => ({ kind: "run", summary }));
|
|
1202
|
+
}
|
|
1203
|
+
function inboxRecord(record) {
|
|
1204
|
+
return {
|
|
1205
|
+
runId: record.runId,
|
|
1206
|
+
taskId: record.taskId,
|
|
1207
|
+
requestId: record.requestId,
|
|
1208
|
+
kind: record.kind,
|
|
1209
|
+
status: record.status,
|
|
1210
|
+
title: record.prompt,
|
|
1211
|
+
prompt: record.prompt,
|
|
1212
|
+
requestedAt: record.requestedAt,
|
|
1213
|
+
payload: record.payload,
|
|
1214
|
+
source: "run"
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function inboxDecision(resolution) {
|
|
1218
|
+
if (resolution.kind === "approval") {
|
|
1219
|
+
return { kind: "approval", decision: resolution.decision, ...resolution.note === undefined ? {} : { note: resolution.note } };
|
|
1220
|
+
}
|
|
1221
|
+
return { kind: "input", answer: resolution.answers };
|
|
1222
|
+
}
|
|
1223
|
+
async function listedRuns(input) {
|
|
1224
|
+
const runs = await listRuns(input.projectRoot, discoveryFilter(input.discoveryFilter));
|
|
1225
|
+
return runs.filter((run) => {
|
|
1226
|
+
if (input.taskId && run.taskId !== input.taskId && run.runId !== input.taskId)
|
|
1227
|
+
return false;
|
|
1228
|
+
return sourceMatches(run, input.source);
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
async function selectedRun(input) {
|
|
1232
|
+
return getRun(input.projectRoot, input.id, discoveryFilter(input.discoveryFilter));
|
|
1233
|
+
}
|
|
1234
|
+
async function readDetails(input) {
|
|
1235
|
+
const run = await selectedRun(input);
|
|
1236
|
+
if (!run)
|
|
1237
|
+
return null;
|
|
1238
|
+
const pendingInbox = run.inbox;
|
|
1239
|
+
const inboxCounts = {
|
|
1240
|
+
approvals: run.pendingApprovals,
|
|
1241
|
+
inputs: run.pendingInputs,
|
|
1242
|
+
total: run.pendingApprovals + run.pendingInputs
|
|
1243
|
+
};
|
|
1244
|
+
const sessionEntries = run.sessionPath ? readSessionRunEntries(run.sessionPath) : undefined;
|
|
1245
|
+
return {
|
|
1246
|
+
run,
|
|
1247
|
+
projection: run.projection,
|
|
1248
|
+
pendingInbox,
|
|
1249
|
+
inboxCounts,
|
|
1250
|
+
sourceTask: null,
|
|
1251
|
+
...sessionEntries ? { sessionEntries } : {},
|
|
1252
|
+
failures: failureSummaries(run),
|
|
1253
|
+
logs: input.includeLogs ? extractRunLogs(run) : []
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
async function inboxRecords(input) {
|
|
1257
|
+
const out = [];
|
|
1258
|
+
for (const kind of requestedKinds(input.kind)) {
|
|
1259
|
+
const records = await listInboxRecords({ projectRoot: input.projectRoot }, kind, { ...input.runId ? { run: input.runId } : {}, ...input.taskId ? { task: input.taskId } : {} }, { listRuns: (projectRoot) => listedRuns({ projectRoot, discoveryFilter: input.discoveryFilter }) });
|
|
1260
|
+
out.push(...records.map(inboxRecord));
|
|
1261
|
+
}
|
|
1262
|
+
return out;
|
|
1263
|
+
}
|
|
1264
|
+
function classify(run) {
|
|
1265
|
+
const classification = classifyRun(run);
|
|
1266
|
+
return {
|
|
1267
|
+
...classification,
|
|
1268
|
+
role: runStatusColorRole(run),
|
|
1269
|
+
rank: statusRank(run)
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
async function controlTarget(input) {
|
|
1273
|
+
const run = await selectedRun({ projectRoot: input.projectRoot, id: input.runId, discoveryFilter: input.discoveryFilter });
|
|
1274
|
+
if (!run)
|
|
1275
|
+
return null;
|
|
1276
|
+
const canDeliver = Boolean(run.joinLink && !run.stale);
|
|
1277
|
+
return {
|
|
1278
|
+
runId: run.runId,
|
|
1279
|
+
taskId: run.taskId,
|
|
1280
|
+
sessionId: run.runId,
|
|
1281
|
+
sessionPath: run.sessionPath,
|
|
1282
|
+
joinLink: run.joinLink,
|
|
1283
|
+
webLink: run.webLink,
|
|
1284
|
+
relayUrl: run.relayUrl,
|
|
1285
|
+
collabCwd: run.collabCwd,
|
|
1286
|
+
live: run.live,
|
|
1287
|
+
stale: run.stale,
|
|
1288
|
+
canDeliver,
|
|
1289
|
+
reason: canDeliver ? null : run.stale ? "run is stale" : "run has no join link"
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
async function removeRegistryEntry(projectRoot, runId) {
|
|
1293
|
+
const namespaceKey = resolveOwnerNamespaceKey(projectRoot);
|
|
1294
|
+
if (!namespaceKey)
|
|
1295
|
+
return false;
|
|
1296
|
+
try {
|
|
1297
|
+
const registryBackbone = await loadCapabilityForRoot2(projectRoot, RunRegistryBackboneCap);
|
|
1298
|
+
if (!registryBackbone)
|
|
1299
|
+
return false;
|
|
1300
|
+
await registryBackbone.createRegistryClient({ baseUrl: resolveRegistryBaseUrl(projectRoot), namespaceKey }).removeRoom(runId);
|
|
1301
|
+
return true;
|
|
1302
|
+
} catch {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
var runReadModelService = {
|
|
1307
|
+
async listRuns(input) {
|
|
1308
|
+
return listedRuns(input);
|
|
1309
|
+
},
|
|
1310
|
+
async getRun(input) {
|
|
1311
|
+
return selectedRun({ projectRoot: input.projectRoot, id: input.selector.id, discoveryFilter: input.discoveryFilter });
|
|
1312
|
+
},
|
|
1313
|
+
async getRunProjection(input) {
|
|
1314
|
+
const run = await selectedRun({ projectRoot: input.projectRoot, id: input.runId, discoveryFilter: input.discoveryFilter });
|
|
1315
|
+
return run?.projection ?? null;
|
|
1316
|
+
},
|
|
1317
|
+
async getRunDetails(input) {
|
|
1318
|
+
return readDetails({
|
|
1319
|
+
projectRoot: input.projectRoot,
|
|
1320
|
+
id: input.selector.id,
|
|
1321
|
+
includeLogs: input.includeLogs,
|
|
1322
|
+
discoveryFilter: input.discoveryFilter
|
|
1323
|
+
});
|
|
1324
|
+
},
|
|
1325
|
+
async inspectRun(input) {
|
|
1326
|
+
const details = await readDetails({
|
|
1327
|
+
projectRoot: input.projectRoot,
|
|
1328
|
+
id: input.selector.id,
|
|
1329
|
+
includeLogs: input.includeLogs,
|
|
1330
|
+
discoveryFilter: input.discoveryFilter
|
|
1331
|
+
});
|
|
1332
|
+
if (!details)
|
|
1333
|
+
return null;
|
|
1334
|
+
return {
|
|
1335
|
+
details,
|
|
1336
|
+
classification: classify(details.run),
|
|
1337
|
+
rows: runInspectSummaryRows([details.run], { projectRoot: input.projectRoot, pendingInbox: details.inboxCounts.total })
|
|
1338
|
+
};
|
|
1339
|
+
},
|
|
1340
|
+
listInboxRecords: inboxRecords,
|
|
1341
|
+
async getInboxCounts(input) {
|
|
1342
|
+
const records = await inboxRecords(input);
|
|
1343
|
+
const approvals = records.filter((record) => record.kind === "approval").length;
|
|
1344
|
+
const inputs = records.filter((record) => record.kind === "input").length;
|
|
1345
|
+
return { approvals, inputs, total: approvals + inputs };
|
|
1346
|
+
},
|
|
1347
|
+
async resolveInboxRequest(input) {
|
|
1348
|
+
const target = await controlTarget({ projectRoot: input.projectRoot, runId: input.runId, discoveryFilter: input.discoveryFilter });
|
|
1349
|
+
if (!target) {
|
|
1350
|
+
return {
|
|
1351
|
+
runId: input.runId,
|
|
1352
|
+
requestId: input.resolution.requestId,
|
|
1353
|
+
kind: input.resolution.kind,
|
|
1354
|
+
status: "not-found",
|
|
1355
|
+
delivered: false,
|
|
1356
|
+
detail: "run not found"
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
if (!target.canDeliver) {
|
|
1360
|
+
return {
|
|
1361
|
+
runId: target.runId,
|
|
1362
|
+
requestId: input.resolution.requestId,
|
|
1363
|
+
kind: input.resolution.kind,
|
|
1364
|
+
status: "not-live",
|
|
1365
|
+
delivered: false,
|
|
1366
|
+
detail: target.reason ?? "run is not deliverable"
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
await resolveInboxRequest(input.projectRoot, target.runId, input.resolution.requestId, inboxDecision(input.resolution));
|
|
1370
|
+
return {
|
|
1371
|
+
runId: target.runId,
|
|
1372
|
+
requestId: input.resolution.requestId,
|
|
1373
|
+
kind: input.resolution.kind,
|
|
1374
|
+
status: "resolved",
|
|
1375
|
+
delivered: true
|
|
1376
|
+
};
|
|
1377
|
+
},
|
|
1378
|
+
async getStats(input) {
|
|
1379
|
+
const since = input.since ? new Date(input.since) : null;
|
|
1380
|
+
const runs = input.runs ?? await listedRuns({ projectRoot: input.projectRoot, discoveryFilter: input.discoveryFilter });
|
|
1381
|
+
return computeStats(runs, { since });
|
|
1382
|
+
},
|
|
1383
|
+
async getInspectRows(input) {
|
|
1384
|
+
const runs = input.runs ?? await listedRuns({ projectRoot: input.projectRoot, discoveryFilter: input.discoveryFilter });
|
|
1385
|
+
return runInspectSummaryRows(runs, { projectRoot: input.projectRoot, pendingInbox: input.pendingInbox ?? null });
|
|
1386
|
+
},
|
|
1387
|
+
async deliverControl(input) {
|
|
1388
|
+
const run = await selectedRun({ projectRoot: input.projectRoot, id: input.runId, discoveryFilter: input.discoveryFilter });
|
|
1389
|
+
if (!run) {
|
|
1390
|
+
return {
|
|
1391
|
+
runId: input.runId,
|
|
1392
|
+
kind: input.control.kind,
|
|
1393
|
+
status: "not-found",
|
|
1394
|
+
delivered: false,
|
|
1395
|
+
detail: "run not found"
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
if (!run.joinLink) {
|
|
1399
|
+
return {
|
|
1400
|
+
runId: run.runId,
|
|
1401
|
+
kind: input.control.kind,
|
|
1402
|
+
status: "not-live",
|
|
1403
|
+
delivered: false,
|
|
1404
|
+
detail: `Run ${run.runId} has no writable collab join link.`
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
try {
|
|
1408
|
+
await deliverRunControl(input.projectRoot, run, input.control);
|
|
1409
|
+
return {
|
|
1410
|
+
runId: run.runId,
|
|
1411
|
+
kind: input.control.kind,
|
|
1412
|
+
status: "delivered",
|
|
1413
|
+
delivered: true
|
|
1414
|
+
};
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
return {
|
|
1417
|
+
runId: run.runId,
|
|
1418
|
+
kind: input.control.kind,
|
|
1419
|
+
status: "unsupported",
|
|
1420
|
+
delivered: false,
|
|
1421
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
classifyRun: classify,
|
|
1426
|
+
resolveControlTarget: controlTarget,
|
|
1427
|
+
async removeRunRegistryEntry(input) {
|
|
1428
|
+
return { runId: input.runId, removed: await removeRegistryEntry(input.projectRoot, input.runId) };
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
export {
|
|
1432
|
+
runReadModelService
|
|
1433
|
+
};
|