@h-rig/run-worker 0.0.6-alpha.155 → 0.0.6-alpha.156
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.js +10 -10
- package/dist/src/extension.js +10 -10
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +108 -33
- package/dist/src/notifications.js +1 -1
- package/dist/src/panel-plugin.d.ts +4 -5
- package/dist/src/panel-plugin.js +2 -2
- package/dist/src/plugin.d.ts +14 -0
- package/dist/src/plugin.js +851 -0
- package/dist/src/runs/control.d.ts +24 -0
- package/dist/src/runs/control.js +398 -0
- package/dist/src/runs/diagnostics.d.ts +10 -0
- package/dist/src/runs/diagnostics.js +53 -0
- package/dist/src/runs/guard.d.ts +4 -0
- package/dist/src/runs/guard.js +26 -0
- package/dist/src/runs/inbox.d.ts +44 -0
- package/dist/src/runs/inbox.js +499 -0
- package/dist/src/runs/index.d.ts +9 -0
- package/dist/src/runs/index.js +990 -0
- package/dist/src/runs/inspect.d.ts +27 -0
- package/dist/src/runs/inspect.js +459 -0
- package/dist/src/runs/projection.d.ts +24 -0
- package/dist/src/runs/projection.js +356 -0
- package/dist/src/runs/run-status.d.ts +20 -0
- package/dist/src/runs/run-status.js +181 -0
- package/dist/src/runs/stats.d.ts +13 -0
- package/dist/src/runs/stats.js +485 -0
- package/package.json +13 -8
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CollabFrame } from "@oh-my-pi/pi-coding-agent/collab/protocol";
|
|
2
|
+
import { type RunRecord } from "./projection";
|
|
3
|
+
export type RunControl = {
|
|
4
|
+
readonly kind: "steer";
|
|
5
|
+
readonly message: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: "stop";
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "pause";
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "resume";
|
|
13
|
+
};
|
|
14
|
+
export interface DeliverRemoteControlDeps {
|
|
15
|
+
readonly WebSocket?: typeof WebSocket;
|
|
16
|
+
}
|
|
17
|
+
export type RemoteControlTarget = Pick<RunRecord, "runId" | "joinLink">;
|
|
18
|
+
export declare function collabControlFrames(runId: string, control: RunControl): readonly CollabFrame[];
|
|
19
|
+
export declare function collabControlFrame(runId: string, control: RunControl): CollabFrame;
|
|
20
|
+
export declare function deliverRemoteRunControl(projectRoot: string, target: RemoteControlTarget | string, control: RunControl, deps?: DeliverRemoteControlDeps): Promise<void>;
|
|
21
|
+
export declare function deliverRunControl(projectRoot: string, target: RunRecord | string, control: RunControl, deps?: DeliverRemoteControlDeps & {
|
|
22
|
+
readonly getRun?: (projectRoot: string, runId: string) => Promise<RunRecord | null>;
|
|
23
|
+
readonly deliverRemote?: (projectRoot: string, target: RunRecord, control: RunControl) => Promise<void>;
|
|
24
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/run-worker/src/runs/control.ts
|
|
5
|
+
import { buildPauseSentinel, buildResumeSentinel, buildStopSentinel } from "@rig/contracts";
|
|
6
|
+
|
|
7
|
+
// packages/run-worker/src/runs/projection.ts
|
|
8
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
9
|
+
import { Duration, Effect, Option, Stream } from "effect";
|
|
10
|
+
import {
|
|
11
|
+
foldRunSessionEntries,
|
|
12
|
+
isTerminalRunStatus,
|
|
13
|
+
timelineEntriesFromCustomEntries
|
|
14
|
+
} from "@rig/contracts";
|
|
15
|
+
import { registrySnapshotStream } from "@rig/runtime/control-plane/discovery";
|
|
16
|
+
|
|
17
|
+
// packages/run-worker/src/runs/diagnostics.ts
|
|
18
|
+
function normalizeString(value) {
|
|
19
|
+
if (typeof value !== "string")
|
|
20
|
+
return null;
|
|
21
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
22
|
+
return normalized.length > 0 ? normalized : null;
|
|
23
|
+
}
|
|
24
|
+
function isGenericRunFailure(value) {
|
|
25
|
+
const text = normalizeString(value);
|
|
26
|
+
return Boolean(text && /^Task run failed \([^)]*\)$/i.test(text));
|
|
27
|
+
}
|
|
28
|
+
function appendCandidate(candidates, value) {
|
|
29
|
+
const text = normalizeString(value);
|
|
30
|
+
if (text && !candidates.includes(text))
|
|
31
|
+
candidates.push(text);
|
|
32
|
+
}
|
|
33
|
+
function categorizeUsefulRunError(lines) {
|
|
34
|
+
const taskSourceFailure = lines.find((line) => /failed to update task source/i.test(line));
|
|
35
|
+
if (taskSourceFailure)
|
|
36
|
+
return `Task source update failed: ${taskSourceFailure}`;
|
|
37
|
+
const moduleFailure = lines.find((line) => /cannot find module/i.test(line));
|
|
38
|
+
if (moduleFailure)
|
|
39
|
+
return `Runtime module resolution failed: ${moduleFailure}`;
|
|
40
|
+
const providerFailure = lines.find((line) => /no api key found|unauthorized|authentication failed|invalid api key/i.test(line));
|
|
41
|
+
if (providerFailure)
|
|
42
|
+
return `Provider authentication failed: ${providerFailure}`;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function summarizeRunError(projection) {
|
|
46
|
+
if (projection.status !== "failed")
|
|
47
|
+
return null;
|
|
48
|
+
const candidates = [];
|
|
49
|
+
for (const anomaly of projection.anomalies) {
|
|
50
|
+
appendCandidate(candidates, `Journal anomaly (${anomaly.kind}): ${anomaly.detail}`);
|
|
51
|
+
}
|
|
52
|
+
for (const phase of projection.closeoutPhases) {
|
|
53
|
+
if (phase.outcome === "failed")
|
|
54
|
+
appendCandidate(candidates, phase.detail);
|
|
55
|
+
}
|
|
56
|
+
for (const entry of projection.statusHistory) {
|
|
57
|
+
appendCandidate(candidates, entry.reason);
|
|
58
|
+
}
|
|
59
|
+
appendCandidate(candidates, projection.record.statusDetail);
|
|
60
|
+
appendCandidate(candidates, projection.record.errorText);
|
|
61
|
+
const nonGeneric = candidates.filter((candidate) => !isGenericRunFailure(candidate));
|
|
62
|
+
return categorizeUsefulRunError(nonGeneric) ?? nonGeneric.at(-1) ?? normalizeString(projection.record.errorText);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// packages/run-worker/src/runs/projection.ts
|
|
66
|
+
var EMPTY_PROJECTION = foldRunSessionEntries([], "");
|
|
67
|
+
var DISCOVERY_DIAGNOSTIC_RUN_ID = "__registry_discovery_error__";
|
|
68
|
+
function stringOrNull(value) {
|
|
69
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
70
|
+
}
|
|
71
|
+
function numberOrNull(value) {
|
|
72
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
73
|
+
}
|
|
74
|
+
function objectRecord(value) {
|
|
75
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
76
|
+
}
|
|
77
|
+
function registryStatusAsRunStatus(status) {
|
|
78
|
+
if (status === "waiting-input")
|
|
79
|
+
return "waiting-user-input";
|
|
80
|
+
if (status === "starting")
|
|
81
|
+
return "preparing";
|
|
82
|
+
return typeof status === "string" && [
|
|
83
|
+
"created",
|
|
84
|
+
"queued",
|
|
85
|
+
"preparing",
|
|
86
|
+
"running",
|
|
87
|
+
"waiting-approval",
|
|
88
|
+
"waiting-user-input",
|
|
89
|
+
"paused",
|
|
90
|
+
"validating",
|
|
91
|
+
"reviewing",
|
|
92
|
+
"closing-out",
|
|
93
|
+
"needs-attention",
|
|
94
|
+
"completed",
|
|
95
|
+
"failed",
|
|
96
|
+
"stopped"
|
|
97
|
+
].includes(status) ? status : null;
|
|
98
|
+
}
|
|
99
|
+
function payloadString(payload, keys) {
|
|
100
|
+
if (!payload || typeof payload !== "object")
|
|
101
|
+
return null;
|
|
102
|
+
const record = payload;
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
const value = record[key];
|
|
105
|
+
if (typeof value === "string" && value.trim())
|
|
106
|
+
return value.trim();
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function payloadOptions(payload) {
|
|
111
|
+
if (!payload || typeof payload !== "object")
|
|
112
|
+
return;
|
|
113
|
+
const record = payload;
|
|
114
|
+
const options = Array.isArray(record.options) ? record.options : Array.isArray(record.choices) ? record.choices : null;
|
|
115
|
+
const values = options?.filter((value) => typeof value === "string" && value.trim().length > 0) ?? [];
|
|
116
|
+
return values.length > 0 ? values : undefined;
|
|
117
|
+
}
|
|
118
|
+
function inboxRequest(request, kind) {
|
|
119
|
+
const fallback = kind === "approval" ? "Approval requested" : "Input requested";
|
|
120
|
+
return {
|
|
121
|
+
requestId: request.requestId,
|
|
122
|
+
kind,
|
|
123
|
+
title: payloadString(request.payload, ["title", "message", "reason", "prompt", "summary"]) ?? fallback,
|
|
124
|
+
body: payloadString(request.payload, ["body", "description", "detail", "details"]),
|
|
125
|
+
...payloadOptions(request.payload) ? { options: payloadOptions(request.payload) } : {},
|
|
126
|
+
requestedAt: request.requestedAt ?? null,
|
|
127
|
+
source: "run"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function inboxFromProjection(projection) {
|
|
131
|
+
return [
|
|
132
|
+
...projection.pendingApprovals.map((request) => inboxRequest(request, "approval")),
|
|
133
|
+
...projection.pendingUserInputs.map((request) => inboxRequest(request, "input"))
|
|
134
|
+
].sort((a, b) => (Date.parse(a.requestedAt ?? "") || 0) - (Date.parse(b.requestedAt ?? "") || 0));
|
|
135
|
+
}
|
|
136
|
+
function isProjectPath(projectRoot, cwd) {
|
|
137
|
+
const root = resolve(projectRoot);
|
|
138
|
+
const candidate = resolve(cwd);
|
|
139
|
+
const pathFromRoot = relative(root, candidate);
|
|
140
|
+
return pathFromRoot === "" || !pathFromRoot.startsWith("../") && pathFromRoot !== ".." && !isAbsolute(pathFromRoot);
|
|
141
|
+
}
|
|
142
|
+
function sourceFromEntry(projectRoot, projection, entry) {
|
|
143
|
+
const candidates = [
|
|
144
|
+
stringOrNull(projection.collabCwd),
|
|
145
|
+
stringOrNull(projection.cwd),
|
|
146
|
+
stringOrNull(entry.cwd),
|
|
147
|
+
stringOrNull(projection.worktreePath)
|
|
148
|
+
].filter((cwd) => cwd !== null);
|
|
149
|
+
return candidates.some((cwd) => isProjectPath(projectRoot, cwd)) ? "local" : "remote";
|
|
150
|
+
}
|
|
151
|
+
function pendingRequests(value, fallbackKind) {
|
|
152
|
+
if (!Array.isArray(value))
|
|
153
|
+
return [];
|
|
154
|
+
return value.flatMap((item) => {
|
|
155
|
+
const record = objectRecord(item);
|
|
156
|
+
const requestId = stringOrNull(record?.requestId) ?? stringOrNull(record?.id);
|
|
157
|
+
if (!record || !requestId)
|
|
158
|
+
return [];
|
|
159
|
+
return [{
|
|
160
|
+
requestId,
|
|
161
|
+
requestKind: stringOrNull(record.requestKind) ?? stringOrNull(record.kind) ?? fallbackKind,
|
|
162
|
+
actionId: stringOrNull(record.actionId),
|
|
163
|
+
payload: "payload" in record ? record.payload : record,
|
|
164
|
+
requestedAt: stringOrNull(record.requestedAt) ?? stringOrNull(record.at) ?? ""
|
|
165
|
+
}];
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function emptyFoldedProjection(runId, projection) {
|
|
169
|
+
const projectionRecord = projection;
|
|
170
|
+
const nestedProjection = objectRecord(projectionRecord.projection);
|
|
171
|
+
const pendingApprovals = pendingRequests(nestedProjection?.pendingApprovals ?? projectionRecord.pendingApprovals, "approval");
|
|
172
|
+
const pendingUserInputs = pendingRequests(nestedProjection?.pendingUserInputs ?? nestedProjection?.pendingInputs ?? projectionRecord.pendingUserInputs ?? (Array.isArray(projectionRecord.pendingInputs) ? projectionRecord.pendingInputs : undefined), "user-input");
|
|
173
|
+
const nestedRecord = objectRecord(nestedProjection?.record);
|
|
174
|
+
const nestedFolded = nestedProjection;
|
|
175
|
+
return {
|
|
176
|
+
...EMPTY_PROJECTION,
|
|
177
|
+
...nestedFolded ?? {},
|
|
178
|
+
record: {
|
|
179
|
+
...nestedRecord ?? {},
|
|
180
|
+
runId,
|
|
181
|
+
...projection.taskId ? { taskId: projection.taskId } : {},
|
|
182
|
+
...projection.title ? { title: projection.title } : {},
|
|
183
|
+
...projection.startedAt ? { startedAt: projection.startedAt } : {},
|
|
184
|
+
...projection.updatedAt ? { updatedAt: projection.updatedAt } : {},
|
|
185
|
+
...projection.completedAt ? { completedAt: projection.completedAt } : {},
|
|
186
|
+
...projection.sessionPath ? { sessionPath: projection.sessionPath } : {},
|
|
187
|
+
...projection.worktreePath ? { worktreePath: projection.worktreePath } : {},
|
|
188
|
+
...projection.prUrl ? { prUrl: projection.prUrl } : {}
|
|
189
|
+
},
|
|
190
|
+
status: registryStatusAsRunStatus(projection.status) ?? registryStatusAsRunStatus(nestedProjection?.status),
|
|
191
|
+
pendingApprovals,
|
|
192
|
+
pendingUserInputs,
|
|
193
|
+
steeringCount: projection.steeringCount ?? numberOrNull(nestedProjection?.steeringCount) ?? 0,
|
|
194
|
+
stallCount: projection.stallCount ?? numberOrNull(nestedProjection?.stallCount) ?? 0,
|
|
195
|
+
lastEventAt: projection.updatedAt ?? stringOrNull(nestedProjection?.lastEventAt)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function runRecordFromRegistryEntry(projectRoot, entry) {
|
|
199
|
+
const projection = entry.projection && typeof entry.projection === "object" ? entry.projection : {};
|
|
200
|
+
const runId = stringOrNull(projection.runId) ?? entry.roomId;
|
|
201
|
+
const folded = emptyFoldedProjection(runId, projection);
|
|
202
|
+
const pushedStatus = registryStatusAsRunStatus(projection.status) ?? folded.status ?? registryStatusAsRunStatus(entry.status);
|
|
203
|
+
const status = entry.stale ? pushedStatus && isTerminalRunStatus(pushedStatus) ? pushedStatus : "stale" : pushedStatus ?? "running";
|
|
204
|
+
const sessionPath = stringOrNull(projection.sessionPath) ?? stringOrNull(entry.sessionPath) ?? null;
|
|
205
|
+
const collabCwd = stringOrNull(projection.collabCwd) ?? stringOrNull(projection.cwd) ?? stringOrNull(entry.cwd) ?? stringOrNull(projection.worktreePath);
|
|
206
|
+
return {
|
|
207
|
+
runId,
|
|
208
|
+
taskId: stringOrNull(projection.taskId),
|
|
209
|
+
title: stringOrNull(projection.title) ?? entry.title,
|
|
210
|
+
status,
|
|
211
|
+
source: sourceFromEntry(projectRoot, projection, entry),
|
|
212
|
+
live: !entry.stale,
|
|
213
|
+
stale: entry.stale,
|
|
214
|
+
startedAt: stringOrNull(projection.startedAt) ?? entry.startedAt ?? null,
|
|
215
|
+
updatedAt: stringOrNull(projection.updatedAt) ?? entry.heartbeatAt ?? null,
|
|
216
|
+
completedAt: stringOrNull(projection.completedAt),
|
|
217
|
+
joinLink: stringOrNull(projection.joinLink) ?? entry.joinLink ?? null,
|
|
218
|
+
webLink: stringOrNull(projection.webLink) ?? entry.webLink ?? null,
|
|
219
|
+
relayUrl: stringOrNull(projection.relayUrl) ?? entry.relayUrl ?? null,
|
|
220
|
+
sessionPath,
|
|
221
|
+
prUrl: stringOrNull(projection.prUrl),
|
|
222
|
+
worktreePath: stringOrNull(projection.worktreePath) ?? collabCwd,
|
|
223
|
+
pendingApprovals: numberOrNull(projection.pendingApprovals) ?? folded.pendingApprovals.length,
|
|
224
|
+
pendingInputs: numberOrNull(projection.pendingInputs) ?? folded.pendingUserInputs.length,
|
|
225
|
+
steeringCount: projection.steeringCount ?? 0,
|
|
226
|
+
stallCount: projection.stallCount ?? 0,
|
|
227
|
+
errorSummary: stringOrNull(projection.errorSummary) ?? summarizeRunError(folded),
|
|
228
|
+
timeline: Array.isArray(projection.timeline) ? [...projection.timeline] : [...timelineEntriesFromCustomEntries([])],
|
|
229
|
+
inbox: inboxFromProjection(folded),
|
|
230
|
+
collabCwd: collabCwd ?? null,
|
|
231
|
+
projection: folded
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function runRecordsFromRegistrySnapshot(projectRoot, snapshot) {
|
|
235
|
+
return sortByRecency(snapshot.entries.map((entry) => runRecordFromRegistryEntry(projectRoot, entry)).filter((record) => record !== null));
|
|
236
|
+
}
|
|
237
|
+
function sortByRecency(records) {
|
|
238
|
+
return [...records].sort((a, b) => {
|
|
239
|
+
const at = Date.parse(b.updatedAt ?? b.startedAt ?? "") || 0;
|
|
240
|
+
const bt = Date.parse(a.updatedAt ?? a.startedAt ?? "") || 0;
|
|
241
|
+
return at - bt;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function discoveryDiagnosticRunRecord(projectRoot, error) {
|
|
245
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
246
|
+
const runId = DISCOVERY_DIAGNOSTIC_RUN_ID;
|
|
247
|
+
const projection = {
|
|
248
|
+
...EMPTY_PROJECTION,
|
|
249
|
+
record: { runId, title: "Registry discovery unavailable" },
|
|
250
|
+
status: "needs-attention",
|
|
251
|
+
stallCount: 1
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
runId,
|
|
255
|
+
taskId: null,
|
|
256
|
+
title: "Registry discovery unavailable",
|
|
257
|
+
status: "needs-attention",
|
|
258
|
+
source: "remote",
|
|
259
|
+
live: false,
|
|
260
|
+
stale: true,
|
|
261
|
+
startedAt: null,
|
|
262
|
+
updatedAt: null,
|
|
263
|
+
completedAt: null,
|
|
264
|
+
joinLink: null,
|
|
265
|
+
webLink: null,
|
|
266
|
+
relayUrl: null,
|
|
267
|
+
sessionPath: null,
|
|
268
|
+
prUrl: null,
|
|
269
|
+
worktreePath: null,
|
|
270
|
+
pendingApprovals: 0,
|
|
271
|
+
pendingInputs: 0,
|
|
272
|
+
steeringCount: 0,
|
|
273
|
+
stallCount: 1,
|
|
274
|
+
errorSummary: `registry discovery unavailable: ${detail}`,
|
|
275
|
+
timeline: [],
|
|
276
|
+
inbox: [],
|
|
277
|
+
collabCwd: null,
|
|
278
|
+
projection
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
async function listRunProjections(projectRoot, filter = {}) {
|
|
282
|
+
try {
|
|
283
|
+
const snapshot = await Effect.runPromise(registrySnapshotStream(projectRoot, filter).pipe(Stream.runHead, Effect.timeout(Duration.seconds(15)), Effect.map(Option.getOrNull)));
|
|
284
|
+
return snapshot ? runRecordsFromRegistrySnapshot(projectRoot, snapshot) : [discoveryDiagnosticRunRecord(projectRoot, "registry discovery stream ended without a snapshot")];
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return [discoveryDiagnosticRunRecord(projectRoot, error)];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function selectRunProjection(runs, runId) {
|
|
290
|
+
const exactRun = runs.find((run) => run.runId === runId);
|
|
291
|
+
if (exactRun)
|
|
292
|
+
return exactRun;
|
|
293
|
+
const exactTask = runs.find((run) => run.taskId === runId);
|
|
294
|
+
if (exactTask)
|
|
295
|
+
return exactTask;
|
|
296
|
+
const prefixMatches = runs.filter((run) => run.runId.startsWith(runId));
|
|
297
|
+
if (prefixMatches.length === 1)
|
|
298
|
+
return prefixMatches[0] ?? null;
|
|
299
|
+
if (prefixMatches.length > 1) {
|
|
300
|
+
const matches = prefixMatches.map((run) => run.runId).join(", ");
|
|
301
|
+
throw new Error(`Ambiguous run id prefix "${runId}" matched ${prefixMatches.length} runs: ${matches}`);
|
|
302
|
+
}
|
|
303
|
+
return runs.find((run) => run.runId === DISCOVERY_DIAGNOSTIC_RUN_ID) ?? null;
|
|
304
|
+
}
|
|
305
|
+
async function getRunProjection(projectRoot, runId, filter = {}) {
|
|
306
|
+
const runs = await listRunProjections(projectRoot, filter);
|
|
307
|
+
return selectRunProjection(runs, runId);
|
|
308
|
+
}
|
|
309
|
+
var getRun = getRunProjection;
|
|
310
|
+
|
|
311
|
+
// packages/run-worker/src/runs/control.ts
|
|
312
|
+
async function loadCollabControlDeps() {
|
|
313
|
+
const [{ importRoomKey, seal }, { COLLAB_PROTO, packEnvelope, parseCollabLink }] = await Promise.all([
|
|
314
|
+
import("@oh-my-pi/pi-coding-agent/collab/crypto"),
|
|
315
|
+
import("@oh-my-pi/pi-coding-agent/collab/protocol")
|
|
316
|
+
]);
|
|
317
|
+
return { importRoomKey, seal, COLLAB_PROTO, packEnvelope, parseCollabLink };
|
|
318
|
+
}
|
|
319
|
+
function collabControlFrames(runId, control) {
|
|
320
|
+
switch (control.kind) {
|
|
321
|
+
case "steer":
|
|
322
|
+
return [{ t: "prompt", text: control.message }];
|
|
323
|
+
case "resume":
|
|
324
|
+
return [{ t: "prompt", text: `${buildResumeSentinel(runId)}
|
|
325
|
+
Continue the run from where it paused.` }];
|
|
326
|
+
case "pause":
|
|
327
|
+
return [
|
|
328
|
+
{ t: "prompt", text: `${buildPauseSentinel(runId)}
|
|
329
|
+
Pause this run after the current interruption settles.` },
|
|
330
|
+
{ t: "abort" }
|
|
331
|
+
];
|
|
332
|
+
case "stop":
|
|
333
|
+
return [
|
|
334
|
+
{ t: "prompt", text: `${buildStopSentinel(runId, control.reason)}
|
|
335
|
+
Stop this run and do not continue after the interruption settles.` },
|
|
336
|
+
{ t: "abort" }
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function collabControlFrame(runId, control) {
|
|
341
|
+
return collabControlFrames(runId, control)[0];
|
|
342
|
+
}
|
|
343
|
+
async function sendSealedCollabFrames(joinLink, frames, deps = {}) {
|
|
344
|
+
const { importRoomKey, seal, COLLAB_PROTO, packEnvelope, parseCollabLink } = await loadCollabControlDeps();
|
|
345
|
+
const parsed = parseCollabLink(joinLink);
|
|
346
|
+
if ("error" in parsed)
|
|
347
|
+
throw new Error(parsed.error);
|
|
348
|
+
if (!parsed.writeToken)
|
|
349
|
+
throw new Error("Run collab link is read-only; cannot send control.");
|
|
350
|
+
const key = await importRoomKey(parsed.key);
|
|
351
|
+
const WebSocketCtor = deps.WebSocket ?? WebSocket;
|
|
352
|
+
const ws = new WebSocketCtor(`${parsed.wsUrl}?role=guest`);
|
|
353
|
+
await new Promise((resolveOpen, rejectOpen) => {
|
|
354
|
+
ws.addEventListener("open", () => resolveOpen(), { once: true });
|
|
355
|
+
ws.addEventListener("error", () => rejectOpen(new Error("collab websocket error")), { once: true });
|
|
356
|
+
});
|
|
357
|
+
const hello = {
|
|
358
|
+
t: "hello",
|
|
359
|
+
proto: COLLAB_PROTO,
|
|
360
|
+
name: "rig-operator",
|
|
361
|
+
writeToken: Buffer.from(parsed.writeToken).toString("base64url")
|
|
362
|
+
};
|
|
363
|
+
ws.send(packEnvelope(0, await seal(key, hello)));
|
|
364
|
+
for (const frame of frames)
|
|
365
|
+
ws.send(packEnvelope(0, await seal(key, frame)));
|
|
366
|
+
try {
|
|
367
|
+
ws.close();
|
|
368
|
+
} catch {}
|
|
369
|
+
}
|
|
370
|
+
async function deliverRemoteRunControl(projectRoot, target, control, deps = {}) {
|
|
371
|
+
const record = typeof target === "string" ? await getRun(projectRoot, target) : target;
|
|
372
|
+
const runId = typeof target === "string" ? target : target.runId;
|
|
373
|
+
if (!record)
|
|
374
|
+
throw new Error(`Run not found: ${runId}`);
|
|
375
|
+
if (!record.joinLink)
|
|
376
|
+
throw new Error(`Run ${record.runId} has no writable collab join link.`);
|
|
377
|
+
await sendSealedCollabFrames(record.joinLink, collabControlFrames(record.runId, control), deps);
|
|
378
|
+
}
|
|
379
|
+
async function deliverRunControl(projectRoot, target, control, deps = {}) {
|
|
380
|
+
const record = typeof target === "string" ? await (deps.getRun ?? getRun)(projectRoot, target) : target;
|
|
381
|
+
const runId = typeof target === "string" ? target : target.runId;
|
|
382
|
+
if (!record)
|
|
383
|
+
throw new Error(`Run not found: ${runId}`);
|
|
384
|
+
if (!record.joinLink)
|
|
385
|
+
throw new Error(`Run ${record.runId} has no writable collab join link; attach interactively to ${control.kind}.`);
|
|
386
|
+
try {
|
|
387
|
+
await (deps.deliverRemote ?? deliverRemoteRunControl)(projectRoot, record, control);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const detail = error instanceof Error ? error.message : `Attach to ${control.kind} interactively: rig run attach ${record.runId}`;
|
|
390
|
+
throw new Error(`Could not ${control.kind} run ${record.runId}. ${detail}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
export {
|
|
394
|
+
deliverRunControl,
|
|
395
|
+
deliverRemoteRunControl,
|
|
396
|
+
collabControlFrames,
|
|
397
|
+
collabControlFrame
|
|
398
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RunJournalProjection } from "@rig/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Extract the most useful human-facing failure diagnostic from the folded run journal.
|
|
4
|
+
*
|
|
5
|
+
* The live projection deliberately does not retain log/timeline lines, so this mirrors the
|
|
6
|
+
* deleted server-side categorization using only fields that survive folding: record fields,
|
|
7
|
+
* status reasons, closeout details, and reducer anomalies.
|
|
8
|
+
*/
|
|
9
|
+
export declare function summarizeRunError(projection: RunJournalProjection): string | null;
|
|
10
|
+
export declare const summarizeUsefulRunError: typeof summarizeRunError;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/run-worker/src/runs/diagnostics.ts
|
|
3
|
+
function normalizeString(value) {
|
|
4
|
+
if (typeof value !== "string")
|
|
5
|
+
return null;
|
|
6
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
7
|
+
return normalized.length > 0 ? normalized : null;
|
|
8
|
+
}
|
|
9
|
+
function isGenericRunFailure(value) {
|
|
10
|
+
const text = normalizeString(value);
|
|
11
|
+
return Boolean(text && /^Task run failed \([^)]*\)$/i.test(text));
|
|
12
|
+
}
|
|
13
|
+
function appendCandidate(candidates, value) {
|
|
14
|
+
const text = normalizeString(value);
|
|
15
|
+
if (text && !candidates.includes(text))
|
|
16
|
+
candidates.push(text);
|
|
17
|
+
}
|
|
18
|
+
function categorizeUsefulRunError(lines) {
|
|
19
|
+
const taskSourceFailure = lines.find((line) => /failed to update task source/i.test(line));
|
|
20
|
+
if (taskSourceFailure)
|
|
21
|
+
return `Task source update failed: ${taskSourceFailure}`;
|
|
22
|
+
const moduleFailure = lines.find((line) => /cannot find module/i.test(line));
|
|
23
|
+
if (moduleFailure)
|
|
24
|
+
return `Runtime module resolution failed: ${moduleFailure}`;
|
|
25
|
+
const providerFailure = lines.find((line) => /no api key found|unauthorized|authentication failed|invalid api key/i.test(line));
|
|
26
|
+
if (providerFailure)
|
|
27
|
+
return `Provider authentication failed: ${providerFailure}`;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function summarizeRunError(projection) {
|
|
31
|
+
if (projection.status !== "failed")
|
|
32
|
+
return null;
|
|
33
|
+
const candidates = [];
|
|
34
|
+
for (const anomaly of projection.anomalies) {
|
|
35
|
+
appendCandidate(candidates, `Journal anomaly (${anomaly.kind}): ${anomaly.detail}`);
|
|
36
|
+
}
|
|
37
|
+
for (const phase of projection.closeoutPhases) {
|
|
38
|
+
if (phase.outcome === "failed")
|
|
39
|
+
appendCandidate(candidates, phase.detail);
|
|
40
|
+
}
|
|
41
|
+
for (const entry of projection.statusHistory) {
|
|
42
|
+
appendCandidate(candidates, entry.reason);
|
|
43
|
+
}
|
|
44
|
+
appendCandidate(candidates, projection.record.statusDetail);
|
|
45
|
+
appendCandidate(candidates, projection.record.errorText);
|
|
46
|
+
const nonGeneric = candidates.filter((candidate) => !isGenericRunFailure(candidate));
|
|
47
|
+
return categorizeUsefulRunError(nonGeneric) ?? nonGeneric.at(-1) ?? normalizeString(projection.record.errorText);
|
|
48
|
+
}
|
|
49
|
+
var summarizeUsefulRunError = summarizeRunError;
|
|
50
|
+
export {
|
|
51
|
+
summarizeUsefulRunError,
|
|
52
|
+
summarizeRunError
|
|
53
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RunRecord } from "./projection";
|
|
2
|
+
export declare function runBlocksNewRunForTask(run: RunRecord): boolean;
|
|
3
|
+
export declare function activeRunByTaskId(listRuns: (projectRoot: string) => Promise<RunRecord[]>, projectRoot: string): Promise<Map<string, string>>;
|
|
4
|
+
export declare function assertNoActiveRunForTask(listRuns: (projectRoot: string) => Promise<RunRecord[]>, projectRoot: string, taskId: string): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/run-worker/src/runs/guard.ts
|
|
3
|
+
import { TERMINAL_RUN_STATUSES } from "@rig/contracts";
|
|
4
|
+
var TERMINAL = new Set(TERMINAL_RUN_STATUSES);
|
|
5
|
+
function runBlocksNewRunForTask(run) {
|
|
6
|
+
return run.live && !run.stale && !TERMINAL.has(run.status) && run.status !== "needs-attention" && run.status !== "needs_attention";
|
|
7
|
+
}
|
|
8
|
+
async function activeRunByTaskId(listRuns, projectRoot) {
|
|
9
|
+
const active = new Map;
|
|
10
|
+
for (const run of await listRuns(projectRoot)) {
|
|
11
|
+
if (run.taskId && runBlocksNewRunForTask(run) && !active.has(run.taskId))
|
|
12
|
+
active.set(run.taskId, run.runId);
|
|
13
|
+
}
|
|
14
|
+
return active;
|
|
15
|
+
}
|
|
16
|
+
async function assertNoActiveRunForTask(listRuns, projectRoot, taskId) {
|
|
17
|
+
const existing = (await activeRunByTaskId(listRuns, projectRoot)).get(taskId);
|
|
18
|
+
if (existing) {
|
|
19
|
+
throw new Error(`Task ${taskId} already has an active run: ${existing}. Attach instead: rig run attach ${existing} \u2014 or re-dispatch anyway with --force.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
runBlocksNewRunForTask,
|
|
24
|
+
assertNoActiveRunForTask,
|
|
25
|
+
activeRunByTaskId
|
|
26
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { deliverRunControl } from "./control";
|
|
2
|
+
import { type RunRecord, type UnifiedInboxRequest } from "./projection";
|
|
3
|
+
export type InboxKind = "approvals" | "inputs";
|
|
4
|
+
export interface InboxFilters {
|
|
5
|
+
readonly run?: string;
|
|
6
|
+
readonly task?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface InboxRecord {
|
|
9
|
+
readonly runId: string;
|
|
10
|
+
readonly taskId: string | null;
|
|
11
|
+
readonly requestId: string;
|
|
12
|
+
readonly status: "pending";
|
|
13
|
+
readonly prompt: string;
|
|
14
|
+
readonly requestedAt: string;
|
|
15
|
+
readonly payload: unknown;
|
|
16
|
+
readonly kind: "approval" | "input";
|
|
17
|
+
}
|
|
18
|
+
export type InboxDecision = {
|
|
19
|
+
readonly kind: "approval";
|
|
20
|
+
readonly decision: "approved" | "rejected" | "approve" | "reject";
|
|
21
|
+
readonly note?: string | null;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "input";
|
|
24
|
+
readonly answer: string | Record<string, string>;
|
|
25
|
+
};
|
|
26
|
+
export declare function recordsFromRun(run: RunRecord, kind: InboxKind): InboxRecord[];
|
|
27
|
+
export interface InboxDeps {
|
|
28
|
+
readonly listRuns?: (projectRoot: string) => Promise<readonly RunRecord[]>;
|
|
29
|
+
}
|
|
30
|
+
export interface ResolveInboxDeps {
|
|
31
|
+
readonly deliverRunControl?: typeof deliverRunControl;
|
|
32
|
+
}
|
|
33
|
+
export declare function listInboxRecords(context: {
|
|
34
|
+
readonly projectRoot: string;
|
|
35
|
+
} | string, kind: InboxKind, filters?: InboxFilters, deps?: InboxDeps): Promise<InboxRecord[]>;
|
|
36
|
+
export declare function listInbox(projectRoot: string, filters?: InboxFilters, deps?: InboxDeps): Promise<UnifiedInboxRequest[]>;
|
|
37
|
+
export declare function readPendingInboxCounts(context: {
|
|
38
|
+
readonly projectRoot: string;
|
|
39
|
+
} | string, deps?: InboxDeps): Promise<{
|
|
40
|
+
approvals: number;
|
|
41
|
+
inputs: number;
|
|
42
|
+
} | null>;
|
|
43
|
+
export declare const readInboxCounts: typeof readPendingInboxCounts;
|
|
44
|
+
export declare function resolveInboxRequest(projectRoot: string, runId: string, requestId: string, decision: InboxDecision, deps?: ResolveInboxDeps): Promise<void>;
|