@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.4
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/index.js +135 -36
- package/dist/src/server-helpers/http-router.js +338 -4
- package/dist/src/server-helpers/issue-analysis.js +4 -1
- package/dist/src/server-helpers/run-mutations.js +102 -72
- package/dist/src/server-helpers/ws-router.js +1 -1
- package/dist/src/server.js +135 -36
- package/package.json +4 -4
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// packages/server/src/server-helpers/run-mutations.ts
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { loadConfig } from "@rig/core/load-config";
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { dirname as dirname5, relative as relative2, resolve as
|
|
5
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6
|
+
import { dirname as dirname5, relative as relative2, resolve as resolve10 } from "path";
|
|
7
7
|
import {
|
|
8
8
|
listAuthorityRuns as listAuthorityRuns7,
|
|
9
9
|
readAuthorityRun as readAuthorityRun8,
|
|
@@ -98,8 +98,8 @@ function normalizeStatus(value) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// packages/server/src/server.ts
|
|
101
|
-
import { existsSync as
|
|
102
|
-
import { dirname as dirname4, resolve as
|
|
101
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
102
|
+
import { dirname as dirname4, resolve as resolve8 } from "path";
|
|
103
103
|
import {
|
|
104
104
|
listAuthorityArtifactRoots,
|
|
105
105
|
listAuthorityRuns as listAuthorityRuns6,
|
|
@@ -295,6 +295,24 @@ var snapshotCache = new Map;
|
|
|
295
295
|
var contextCache = new Map;
|
|
296
296
|
var taskListCache = new Map;
|
|
297
297
|
|
|
298
|
+
// packages/server/src/server-helpers/task-projection.ts
|
|
299
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
300
|
+
import { resolve as resolve5 } from "path";
|
|
301
|
+
function projectionPath(projectRoot) {
|
|
302
|
+
return resolve5(projectRoot, ".rig", "state", "task-projection.json");
|
|
303
|
+
}
|
|
304
|
+
function readTaskProjection(projectRoot) {
|
|
305
|
+
const file = projectionPath(projectRoot);
|
|
306
|
+
if (!existsSync3(file))
|
|
307
|
+
return null;
|
|
308
|
+
try {
|
|
309
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
310
|
+
return parsed && parsed.version === 1 && Array.isArray(parsed.tasks) ? parsed : null;
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
298
316
|
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
299
317
|
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
300
318
|
|
|
@@ -302,7 +320,7 @@ import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
|
302
320
|
import { RIG_WS_CHANNELS } from "@rig/contracts";
|
|
303
321
|
|
|
304
322
|
// packages/server/src/server-helpers/run-writers.ts
|
|
305
|
-
import { resolve as
|
|
323
|
+
import { resolve as resolve6 } from "path";
|
|
306
324
|
import {
|
|
307
325
|
appendJsonlRecord,
|
|
308
326
|
readAuthorityRun as readAuthorityRun3,
|
|
@@ -327,7 +345,7 @@ function patchRunRecord(projectRoot, runId, patch) {
|
|
|
327
345
|
...patch,
|
|
328
346
|
updatedAt: normalizeString(patch.updatedAt) ?? new Date().toISOString()
|
|
329
347
|
};
|
|
330
|
-
writeJsonFile2(
|
|
348
|
+
writeJsonFile2(resolve6(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
|
|
331
349
|
return next;
|
|
332
350
|
}
|
|
333
351
|
function buildRunStartPatch(startedAt) {
|
|
@@ -460,8 +478,8 @@ import {
|
|
|
460
478
|
|
|
461
479
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
462
480
|
import { randomBytes } from "crypto";
|
|
463
|
-
import { chmodSync, existsSync as
|
|
464
|
-
import { resolve as
|
|
481
|
+
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
482
|
+
import { resolve as resolve7 } from "path";
|
|
465
483
|
function cleanString(value) {
|
|
466
484
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
467
485
|
}
|
|
@@ -492,10 +510,10 @@ function parseApiSessions(value) {
|
|
|
492
510
|
});
|
|
493
511
|
}
|
|
494
512
|
function readStoredAuth(stateFile) {
|
|
495
|
-
if (!
|
|
513
|
+
if (!existsSync4(stateFile))
|
|
496
514
|
return {};
|
|
497
515
|
try {
|
|
498
|
-
const parsed = JSON.parse(
|
|
516
|
+
const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
|
|
499
517
|
return {
|
|
500
518
|
...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
|
|
501
519
|
login: cleanString(parsed.login),
|
|
@@ -527,15 +545,15 @@ function newApiSessionToken() {
|
|
|
527
545
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
528
546
|
}
|
|
529
547
|
function writeStoredAuth(stateFile, payload) {
|
|
530
|
-
|
|
531
|
-
|
|
548
|
+
mkdirSync4(resolve7(stateFile, ".."), { recursive: true });
|
|
549
|
+
writeFileSync4(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
532
550
|
`, { encoding: "utf8", mode: 384 });
|
|
533
551
|
try {
|
|
534
552
|
chmodSync(stateFile, 384);
|
|
535
553
|
} catch {}
|
|
536
554
|
}
|
|
537
555
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
538
|
-
return
|
|
556
|
+
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
539
557
|
}
|
|
540
558
|
function createGitHubAuthStore(projectRoot) {
|
|
541
559
|
const stateFile = resolveGitHubAuthStateFile(projectRoot);
|
|
@@ -969,10 +987,10 @@ var CLUSTERS = {
|
|
|
969
987
|
};
|
|
970
988
|
|
|
971
989
|
// packages/server/src/server-helpers/task-config.ts
|
|
972
|
-
import { existsSync as
|
|
990
|
+
import { existsSync as existsSync5 } from "fs";
|
|
973
991
|
async function readTaskConfig(projectRoot) {
|
|
974
992
|
const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
|
|
975
|
-
if (!
|
|
993
|
+
if (!existsSync5(taskConfigPath)) {
|
|
976
994
|
return {};
|
|
977
995
|
}
|
|
978
996
|
try {
|
|
@@ -989,8 +1007,8 @@ var serverPathEnvQueue = Promise.resolve();
|
|
|
989
1007
|
async function withServerPathEnv(projectRoot, fn) {
|
|
990
1008
|
const waitForTurn = serverPathEnvQueue;
|
|
991
1009
|
let releaseTurn;
|
|
992
|
-
serverPathEnvQueue = new Promise((
|
|
993
|
-
releaseTurn =
|
|
1010
|
+
serverPathEnvQueue = new Promise((resolve9) => {
|
|
1011
|
+
releaseTurn = resolve9;
|
|
994
1012
|
});
|
|
995
1013
|
await waitForTurn;
|
|
996
1014
|
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
@@ -1026,9 +1044,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
|
|
|
1026
1044
|
return withServerPathEnv(projectRoot, fn);
|
|
1027
1045
|
}
|
|
1028
1046
|
async function readWorkspaceTasks(projectRoot) {
|
|
1029
|
-
const issuesPath =
|
|
1047
|
+
const issuesPath = resolve8(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
|
|
1030
1048
|
const taskConfig = await readTaskConfig(projectRoot);
|
|
1031
|
-
if (!
|
|
1049
|
+
if (!existsSync6(issuesPath)) {
|
|
1032
1050
|
return [];
|
|
1033
1051
|
}
|
|
1034
1052
|
const latestById = new Map;
|
|
@@ -1074,7 +1092,7 @@ async function readWorkspaceTasks(projectRoot) {
|
|
|
1074
1092
|
if (false) {}
|
|
1075
1093
|
|
|
1076
1094
|
// packages/server/src/server-helpers/validation-failure.ts
|
|
1077
|
-
import { resolve as
|
|
1095
|
+
import { resolve as resolve9 } from "path";
|
|
1078
1096
|
import {
|
|
1079
1097
|
readJsonFile as readJsonFile4,
|
|
1080
1098
|
resolveTaskArtifactDirs
|
|
@@ -1088,7 +1106,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
1088
1106
|
continue;
|
|
1089
1107
|
}
|
|
1090
1108
|
seen.add(artifactRoot);
|
|
1091
|
-
const summary = readJsonFile4(
|
|
1109
|
+
const summary = readJsonFile4(resolve9(artifactRoot, "validation-summary.json"), null);
|
|
1092
1110
|
if (!summary || summary.status !== "fail") {
|
|
1093
1111
|
continue;
|
|
1094
1112
|
}
|
|
@@ -1303,11 +1321,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
1303
1321
|
return;
|
|
1304
1322
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
1305
1323
|
}
|
|
1324
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
1325
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
1326
|
+
if (fromReader)
|
|
1327
|
+
return fromReader;
|
|
1328
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
1329
|
+
if (projected)
|
|
1330
|
+
return projected;
|
|
1331
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
1332
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
1333
|
+
}
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1306
1336
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
1307
1337
|
if ("taskId" in input && input.taskId) {
|
|
1308
1338
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
1309
1339
|
}
|
|
1310
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
1340
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
1311
1341
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
1312
1342
|
const runDir = resolveAuthorityRunDir4(projectRoot, input.runId);
|
|
1313
1343
|
const runRecord = {
|
|
@@ -1341,11 +1371,11 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
|
|
|
1341
1371
|
initiatedBy: input.initiatedBy ?? null,
|
|
1342
1372
|
...sourceTask ? { sourceTask: sourceTaskContract(sourceTask) } : {}
|
|
1343
1373
|
};
|
|
1344
|
-
|
|
1345
|
-
|
|
1374
|
+
mkdirSync6(runDir, { recursive: true });
|
|
1375
|
+
writeFileSync6(resolve10(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
|
|
1346
1376
|
`, "utf8");
|
|
1347
1377
|
if ("initialPrompt" in input && input.initialPrompt && input.initialPrompt.trim().length > 0) {
|
|
1348
|
-
|
|
1378
|
+
writeFileSync6(resolve10(runDir, "timeline.jsonl"), `${JSON.stringify({
|
|
1349
1379
|
id: `message-${Date.now()}`,
|
|
1350
1380
|
type: "user_message",
|
|
1351
1381
|
text: input.initialPrompt,
|
|
@@ -1379,6 +1409,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
1379
1409
|
throw new Error(`Run not found: ${runId}`);
|
|
1380
1410
|
}
|
|
1381
1411
|
const startedAt = new Date().toISOString();
|
|
1412
|
+
const resumeMode = options?.resume === true;
|
|
1382
1413
|
state.runProcesses.set(runId, {
|
|
1383
1414
|
runId,
|
|
1384
1415
|
child: null,
|
|
@@ -1395,9 +1426,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
1395
1426
|
summary: run.title
|
|
1396
1427
|
});
|
|
1397
1428
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
1398
|
-
id: `log:${runId}:prepare`,
|
|
1399
|
-
title: "Rig task run starting",
|
|
1400
|
-
detail: run.taskId ?? run.title,
|
|
1429
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
1430
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
1431
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
1401
1432
|
tone: "info",
|
|
1402
1433
|
status: "preparing",
|
|
1403
1434
|
createdAt: startedAt
|
|
@@ -1405,8 +1436,8 @@ async function startLocalRun(state, runId, options) {
|
|
|
1405
1436
|
broadcastRunLogAppended(state, runId, readLatestRawRunLog(state.projectRoot, runId));
|
|
1406
1437
|
broadcastSnapshotInvalidation(state);
|
|
1407
1438
|
const cliProjectRoot = resolveLocalRunCliProjectRoot(state.projectRoot);
|
|
1408
|
-
const cliEntryPoint =
|
|
1409
|
-
if (!
|
|
1439
|
+
const cliEntryPoint = resolve10(cliProjectRoot, "packages/cli/bin/rig.ts");
|
|
1440
|
+
if (!existsSync7(cliEntryPoint)) {
|
|
1410
1441
|
const completedAt = new Date().toISOString();
|
|
1411
1442
|
const failureSummary = `Rig task-run entrypoint missing at ${relative2(state.projectRoot, cliEntryPoint)}`;
|
|
1412
1443
|
patchRunRecord(state.projectRoot, runId, {
|
|
@@ -1475,7 +1506,15 @@ async function startLocalRun(state, runId, options) {
|
|
|
1475
1506
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
1476
1507
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
1477
1508
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
1478
|
-
...bridgeGitHubToken ? {
|
|
1509
|
+
...bridgeGitHubToken ? {
|
|
1510
|
+
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
1511
|
+
GITHUB_TOKEN: bridgeGitHubToken,
|
|
1512
|
+
GH_TOKEN: bridgeGitHubToken
|
|
1513
|
+
} : {},
|
|
1514
|
+
...resumeMode ? {
|
|
1515
|
+
RIG_RUN_RESUME: "1",
|
|
1516
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
1517
|
+
} : {}
|
|
1479
1518
|
},
|
|
1480
1519
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1481
1520
|
});
|
|
@@ -1515,9 +1554,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
1515
1554
|
handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
|
|
1516
1555
|
});
|
|
1517
1556
|
try {
|
|
1518
|
-
const exit = await new Promise((
|
|
1519
|
-
child.once("error", (error) =>
|
|
1520
|
-
child.once("close", (code, signal) =>
|
|
1557
|
+
const exit = await new Promise((resolve11) => {
|
|
1558
|
+
child.once("error", (error) => resolve11({ code: 1, signal: null, error }));
|
|
1559
|
+
child.once("close", (code, signal) => resolve11({ code, signal }));
|
|
1521
1560
|
});
|
|
1522
1561
|
if (exit.error) {
|
|
1523
1562
|
throw new Error(`Failed to start task run: ${exit.error.message}`);
|
|
@@ -1617,17 +1656,17 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
|
|
|
1617
1656
|
process.env.PROJECT_RIG_ROOT?.trim()
|
|
1618
1657
|
].filter((value) => !!value);
|
|
1619
1658
|
for (const candidate of envCandidates) {
|
|
1620
|
-
if (
|
|
1621
|
-
return
|
|
1659
|
+
if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
|
|
1660
|
+
return resolve10(candidate);
|
|
1622
1661
|
}
|
|
1623
1662
|
}
|
|
1624
|
-
if (
|
|
1663
|
+
if (existsSync7(resolve10(projectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1625
1664
|
return projectRoot;
|
|
1626
1665
|
}
|
|
1627
1666
|
try {
|
|
1628
1667
|
const monorepoRoot = resolveMonorepoRoot6(projectRoot);
|
|
1629
1668
|
const outerProjectRoot = dirname5(dirname5(monorepoRoot));
|
|
1630
|
-
if (
|
|
1669
|
+
if (existsSync7(resolve10(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1631
1670
|
return outerProjectRoot;
|
|
1632
1671
|
}
|
|
1633
1672
|
} catch {}
|
|
@@ -1647,15 +1686,15 @@ async function resumeRunRecord(state, input) {
|
|
|
1647
1686
|
if (run.status === "completed") {
|
|
1648
1687
|
throw new Error("Completed runs cannot be resumed.");
|
|
1649
1688
|
}
|
|
1650
|
-
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
|
|
1689
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
1651
1690
|
}
|
|
1652
1691
|
function appendRunMessage(projectRoot, input) {
|
|
1653
1692
|
const run = readAuthorityRun8(projectRoot, input.runId);
|
|
1654
1693
|
if (!run) {
|
|
1655
1694
|
throw new Error(`Run not found: ${input.runId}`);
|
|
1656
1695
|
}
|
|
1657
|
-
const timelinePath =
|
|
1658
|
-
const existingLines = fileExists(timelinePath) ?
|
|
1696
|
+
const timelinePath = resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
|
|
1697
|
+
const existingLines = fileExists(timelinePath) ? readFileSync4(timelinePath, "utf8").trim() : "";
|
|
1659
1698
|
const nextLine = JSON.stringify({
|
|
1660
1699
|
id: input.messageId,
|
|
1661
1700
|
type: "user_message",
|
|
@@ -1663,11 +1702,11 @@ function appendRunMessage(projectRoot, input) {
|
|
|
1663
1702
|
attachments: input.attachments ?? [],
|
|
1664
1703
|
createdAt: input.createdAt
|
|
1665
1704
|
});
|
|
1666
|
-
|
|
1705
|
+
writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
|
|
1667
1706
|
${nextLine}
|
|
1668
1707
|
` : `${nextLine}
|
|
1669
1708
|
`, "utf8");
|
|
1670
|
-
writeJsonFile4(
|
|
1709
|
+
writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
|
|
1671
1710
|
...run,
|
|
1672
1711
|
updatedAt: input.createdAt
|
|
1673
1712
|
});
|
|
@@ -1693,7 +1732,7 @@ async function stopRunRecord(stateOrProjectRoot, input) {
|
|
|
1693
1732
|
completedAt: run.completedAt ?? input.createdAt,
|
|
1694
1733
|
updatedAt: input.createdAt
|
|
1695
1734
|
};
|
|
1696
|
-
writeJsonFile4(
|
|
1735
|
+
writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
|
|
1697
1736
|
if (run.status !== "completed" && run.taskId) {
|
|
1698
1737
|
const taskId = run.taskId;
|
|
1699
1738
|
(async () => {
|
|
@@ -1731,34 +1770,12 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
|
|
|
1731
1770
|
writeQueueState(projectRoot, next);
|
|
1732
1771
|
return next;
|
|
1733
1772
|
}
|
|
1734
|
-
var
|
|
1735
|
-
function
|
|
1736
|
-
|
|
1737
|
-
for (const run of runs) {
|
|
1773
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
1774
|
+
function collectResumableLocalRuns(state, runs) {
|
|
1775
|
+
return runs.filter((run) => {
|
|
1738
1776
|
const status = normalizeString(run.status)?.toLowerCase() ?? "";
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
|
|
1742
|
-
continue;
|
|
1743
|
-
}
|
|
1744
|
-
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
1745
|
-
patchRunRecord(state.projectRoot, run.runId, {
|
|
1746
|
-
status: "failed",
|
|
1747
|
-
completedAt: run.completedAt ?? nowIso,
|
|
1748
|
-
updatedAt: nowIso,
|
|
1749
|
-
errorText: detail
|
|
1750
|
-
});
|
|
1751
|
-
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
1752
|
-
id: `log:${run.runId}:stale-local-run`,
|
|
1753
|
-
title: "Run marked stale after server restart",
|
|
1754
|
-
detail,
|
|
1755
|
-
tone: "error",
|
|
1756
|
-
status: "failed",
|
|
1757
|
-
createdAt: nowIso
|
|
1758
|
-
});
|
|
1759
|
-
changed = true;
|
|
1760
|
-
}
|
|
1761
|
-
return changed;
|
|
1777
|
+
return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
|
|
1778
|
+
});
|
|
1762
1779
|
}
|
|
1763
1780
|
async function reconcileScheduler(state, reason) {
|
|
1764
1781
|
if (state.scheduler.reconciling) {
|
|
@@ -1773,7 +1790,20 @@ async function reconcileScheduler(state, reason) {
|
|
|
1773
1790
|
const queue = readQueueState(state.projectRoot);
|
|
1774
1791
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
1775
1792
|
let runs = listAuthorityRuns7(state.projectRoot);
|
|
1776
|
-
let changed =
|
|
1793
|
+
let changed = false;
|
|
1794
|
+
const resumableRuns = collectResumableLocalRuns(state, runs);
|
|
1795
|
+
for (const run of resumableRuns) {
|
|
1796
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
1797
|
+
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
1798
|
+
title: "Run auto-resume scheduled",
|
|
1799
|
+
detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
|
|
1800
|
+
tone: "info",
|
|
1801
|
+
status: "preparing",
|
|
1802
|
+
createdAt: new Date().toISOString()
|
|
1803
|
+
});
|
|
1804
|
+
await startLocalRun(state, run.runId, { resume: true });
|
|
1805
|
+
changed = true;
|
|
1806
|
+
}
|
|
1777
1807
|
if (changed) {
|
|
1778
1808
|
runs = listAuthorityRuns7(state.projectRoot);
|
|
1779
1809
|
}
|
|
@@ -403,7 +403,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
403
403
|
"needs-attention",
|
|
404
404
|
"stopped"
|
|
405
405
|
]);
|
|
406
|
-
var
|
|
406
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
407
407
|
|
|
408
408
|
// packages/server/src/server-helpers/http-router.ts
|
|
409
409
|
import {
|
package/dist/src/server.js
CHANGED
|
@@ -2463,7 +2463,10 @@ function createPiIssueAnalyzer(input = {}) {
|
|
|
2463
2463
|
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
2464
2464
|
return async ({ prompt }) => {
|
|
2465
2465
|
const args = ["--print", "--mode", "json", "--no-session"];
|
|
2466
|
-
const
|
|
2466
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
2467
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
2468
|
+
if (provider)
|
|
2469
|
+
args.push("--provider", provider);
|
|
2467
2470
|
if (model)
|
|
2468
2471
|
args.push("--model", model);
|
|
2469
2472
|
args.push(prompt);
|
|
@@ -3873,11 +3876,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
3873
3876
|
return;
|
|
3874
3877
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
3875
3878
|
}
|
|
3879
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
3880
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
3881
|
+
if (fromReader)
|
|
3882
|
+
return fromReader;
|
|
3883
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
3884
|
+
if (projected)
|
|
3885
|
+
return projected;
|
|
3886
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
3887
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
3888
|
+
}
|
|
3889
|
+
return null;
|
|
3890
|
+
}
|
|
3876
3891
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
3877
3892
|
if ("taskId" in input && input.taskId) {
|
|
3878
3893
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
3879
3894
|
}
|
|
3880
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
3895
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
3881
3896
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
3882
3897
|
const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
|
|
3883
3898
|
const runRecord = {
|
|
@@ -3949,6 +3964,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
3949
3964
|
throw new Error(`Run not found: ${runId}`);
|
|
3950
3965
|
}
|
|
3951
3966
|
const startedAt = new Date().toISOString();
|
|
3967
|
+
const resumeMode = options?.resume === true;
|
|
3952
3968
|
state.runProcesses.set(runId, {
|
|
3953
3969
|
runId,
|
|
3954
3970
|
child: null,
|
|
@@ -3965,9 +3981,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
3965
3981
|
summary: run.title
|
|
3966
3982
|
});
|
|
3967
3983
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
3968
|
-
id: `log:${runId}:prepare`,
|
|
3969
|
-
title: "Rig task run starting",
|
|
3970
|
-
detail: run.taskId ?? run.title,
|
|
3984
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
3985
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
3986
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
3971
3987
|
tone: "info",
|
|
3972
3988
|
status: "preparing",
|
|
3973
3989
|
createdAt: startedAt
|
|
@@ -4045,7 +4061,15 @@ async function startLocalRun(state, runId, options) {
|
|
|
4045
4061
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
4046
4062
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4047
4063
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4048
|
-
...bridgeGitHubToken ? {
|
|
4064
|
+
...bridgeGitHubToken ? {
|
|
4065
|
+
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
4066
|
+
GITHUB_TOKEN: bridgeGitHubToken,
|
|
4067
|
+
GH_TOKEN: bridgeGitHubToken
|
|
4068
|
+
} : {},
|
|
4069
|
+
...resumeMode ? {
|
|
4070
|
+
RIG_RUN_RESUME: "1",
|
|
4071
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
4072
|
+
} : {}
|
|
4049
4073
|
},
|
|
4050
4074
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4051
4075
|
});
|
|
@@ -4217,7 +4241,7 @@ async function resumeRunRecord(state, input) {
|
|
|
4217
4241
|
if (run.status === "completed") {
|
|
4218
4242
|
throw new Error("Completed runs cannot be resumed.");
|
|
4219
4243
|
}
|
|
4220
|
-
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
|
|
4244
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4221
4245
|
}
|
|
4222
4246
|
function appendRunMessage(projectRoot, input) {
|
|
4223
4247
|
const run = readAuthorityRun4(projectRoot, input.runId);
|
|
@@ -4301,34 +4325,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4301
4325
|
writeQueueState(projectRoot, next);
|
|
4302
4326
|
return next;
|
|
4303
4327
|
}
|
|
4304
|
-
var
|
|
4305
|
-
function
|
|
4306
|
-
|
|
4307
|
-
for (const run of runs) {
|
|
4328
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
4329
|
+
function collectResumableLocalRuns(state, runs) {
|
|
4330
|
+
return runs.filter((run) => {
|
|
4308
4331
|
const status = normalizeString(run.status)?.toLowerCase() ?? "";
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
|
|
4312
|
-
continue;
|
|
4313
|
-
}
|
|
4314
|
-
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
4315
|
-
patchRunRecord(state.projectRoot, run.runId, {
|
|
4316
|
-
status: "failed",
|
|
4317
|
-
completedAt: run.completedAt ?? nowIso,
|
|
4318
|
-
updatedAt: nowIso,
|
|
4319
|
-
errorText: detail
|
|
4320
|
-
});
|
|
4321
|
-
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4322
|
-
id: `log:${run.runId}:stale-local-run`,
|
|
4323
|
-
title: "Run marked stale after server restart",
|
|
4324
|
-
detail,
|
|
4325
|
-
tone: "error",
|
|
4326
|
-
status: "failed",
|
|
4327
|
-
createdAt: nowIso
|
|
4328
|
-
});
|
|
4329
|
-
changed = true;
|
|
4330
|
-
}
|
|
4331
|
-
return changed;
|
|
4332
|
+
return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
|
|
4333
|
+
});
|
|
4332
4334
|
}
|
|
4333
4335
|
async function reconcileScheduler(state, reason) {
|
|
4334
4336
|
if (state.scheduler.reconciling) {
|
|
@@ -4343,7 +4345,20 @@ async function reconcileScheduler(state, reason) {
|
|
|
4343
4345
|
const queue = readQueueState(state.projectRoot);
|
|
4344
4346
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4345
4347
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4346
|
-
let changed =
|
|
4348
|
+
let changed = false;
|
|
4349
|
+
const resumableRuns = collectResumableLocalRuns(state, runs);
|
|
4350
|
+
for (const run of resumableRuns) {
|
|
4351
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4352
|
+
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
4353
|
+
title: "Run auto-resume scheduled",
|
|
4354
|
+
detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
|
|
4355
|
+
tone: "info",
|
|
4356
|
+
status: "preparing",
|
|
4357
|
+
createdAt: new Date().toISOString()
|
|
4358
|
+
});
|
|
4359
|
+
await startLocalRun(state, run.runId, { resume: true });
|
|
4360
|
+
changed = true;
|
|
4361
|
+
}
|
|
4347
4362
|
if (changed) {
|
|
4348
4363
|
runs = listAuthorityRuns4(state.projectRoot);
|
|
4349
4364
|
}
|
|
@@ -5621,6 +5636,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
5621
5636
|
}
|
|
5622
5637
|
return filtered;
|
|
5623
5638
|
}
|
|
5639
|
+
function issueAnalysisTargetFor(source) {
|
|
5640
|
+
if (!source)
|
|
5641
|
+
return null;
|
|
5642
|
+
const candidate = source;
|
|
5643
|
+
if (typeof candidate.updateTask !== "function")
|
|
5644
|
+
return null;
|
|
5645
|
+
return {
|
|
5646
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
5647
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
5648
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
5649
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
5650
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
function uniqueStringList(value) {
|
|
5654
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
5655
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
5656
|
+
}
|
|
5657
|
+
function taskRecordId(task) {
|
|
5658
|
+
return String(task.id ?? "");
|
|
5659
|
+
}
|
|
5624
5660
|
function redactRemoteEndpoint(endpoint) {
|
|
5625
5661
|
const { token, ...rest } = endpoint;
|
|
5626
5662
|
return {
|
|
@@ -5963,6 +5999,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
5963
5999
|
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
5964
6000
|
});
|
|
5965
6001
|
}
|
|
6002
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6003
|
+
const body = await deps.readJsonBody(req);
|
|
6004
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
6005
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
6006
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
6007
|
+
return deps.badRequest("ids is required unless all=true");
|
|
6008
|
+
}
|
|
6009
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
6010
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
6011
|
+
const target = issueAnalysisTargetFor(source);
|
|
6012
|
+
if (!source || !target) {
|
|
6013
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
6014
|
+
}
|
|
6015
|
+
const allTasks = [...await source.list()];
|
|
6016
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
6017
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
6018
|
+
if (cached)
|
|
6019
|
+
return cached;
|
|
6020
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
6021
|
+
}))).filter((task) => Boolean(task));
|
|
6022
|
+
if (issues.length === 0) {
|
|
6023
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
6024
|
+
}
|
|
6025
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
6026
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
6027
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
6028
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
6029
|
+
const service = createIssueAnalysisService({
|
|
6030
|
+
analyzer: createPiIssueAnalyzer({
|
|
6031
|
+
...model ? { model } : {},
|
|
6032
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
6033
|
+
}),
|
|
6034
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
6035
|
+
});
|
|
6036
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
6037
|
+
let results;
|
|
6038
|
+
try {
|
|
6039
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
6040
|
+
} catch (error) {
|
|
6041
|
+
return deps.jsonResponse({
|
|
6042
|
+
ok: false,
|
|
6043
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6044
|
+
reason,
|
|
6045
|
+
ids: issues.map((issue) => issue.id)
|
|
6046
|
+
}, 502);
|
|
6047
|
+
}
|
|
6048
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
6049
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
6050
|
+
return;
|
|
6051
|
+
});
|
|
6052
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
6053
|
+
return deps.jsonResponse({
|
|
6054
|
+
ok: true,
|
|
6055
|
+
reason,
|
|
6056
|
+
analyzed: results.map((entry) => ({
|
|
6057
|
+
id: entry.issue.id,
|
|
6058
|
+
title: entry.issue.title ?? null,
|
|
6059
|
+
result: entry.result
|
|
6060
|
+
}))
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
5966
6063
|
if (url.pathname === "/api/server/status") {
|
|
5967
6064
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
5968
6065
|
const taskSource = await buildTaskSourceStatus(state, config);
|
|
@@ -6658,11 +6755,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6658
6755
|
const runId = normalizeString(body.runId);
|
|
6659
6756
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
6660
6757
|
const promptOverride = normalizeString(body.promptOverride);
|
|
6758
|
+
const restart = body.restart === true;
|
|
6661
6759
|
if (!runId) {
|
|
6662
6760
|
return deps.badRequest("runId is required");
|
|
6663
6761
|
}
|
|
6664
6762
|
try {
|
|
6665
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
6763
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
6666
6764
|
deps.broadcastSnapshotInvalidation(state);
|
|
6667
6765
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
6668
6766
|
} catch (error) {
|
|
@@ -12572,6 +12670,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
12572
12670
|
const server = Bun.serve({
|
|
12573
12671
|
hostname: options.host,
|
|
12574
12672
|
port: options.port,
|
|
12673
|
+
idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
|
|
12575
12674
|
fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
|
|
12576
12675
|
websocket: {
|
|
12577
12676
|
open(ws) {
|