@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.
@@ -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 existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
6
- import { dirname as dirname5, relative as relative2, resolve as resolve9 } from "path";
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 existsSync5, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
102
- import { dirname as dirname4, resolve as resolve7 } from "path";
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 resolve5 } from "path";
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(resolve5(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
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 existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
464
- import { resolve as resolve6 } from "path";
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 (!existsSync3(stateFile))
513
+ if (!existsSync4(stateFile))
496
514
  return {};
497
515
  try {
498
- const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
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
- mkdirSync3(resolve6(stateFile, ".."), { recursive: true });
531
- writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
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 resolve6(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
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 existsSync4 } from "fs";
990
+ import { existsSync as existsSync5 } from "fs";
973
991
  async function readTaskConfig(projectRoot) {
974
992
  const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
975
- if (!existsSync4(taskConfigPath)) {
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((resolve8) => {
993
- releaseTurn = resolve8;
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 = resolve7(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
1047
+ const issuesPath = resolve8(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
1030
1048
  const taskConfig = await readTaskConfig(projectRoot);
1031
- if (!existsSync5(issuesPath)) {
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 resolve8 } from "path";
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(resolve8(artifactRoot, "validation-summary.json"), null);
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 ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
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
- mkdirSync5(runDir, { recursive: true });
1345
- writeFileSync5(resolve9(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
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
- writeFileSync5(resolve9(runDir, "timeline.jsonl"), `${JSON.stringify({
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 = resolve9(cliProjectRoot, "packages/cli/bin/rig.ts");
1409
- if (!existsSync6(cliEntryPoint)) {
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 ? { RIG_GITHUB_TOKEN: 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((resolve10) => {
1519
- child.once("error", (error) => resolve10({ code: 1, signal: null, error }));
1520
- child.once("close", (code, signal) => resolve10({ 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 (existsSync6(resolve9(candidate, "packages/cli/bin/rig.ts"))) {
1621
- return resolve9(candidate);
1659
+ if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
1660
+ return resolve10(candidate);
1622
1661
  }
1623
1662
  }
1624
- if (existsSync6(resolve9(projectRoot, "packages/cli/bin/rig.ts"))) {
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 (existsSync6(resolve9(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
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 = resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
1658
- const existingLines = fileExists(timelinePath) ? readFileSync3(timelinePath, "utf8").trim() : "";
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
- writeFileSync5(timelinePath, existingLines.length > 0 ? `${existingLines}
1705
+ writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
1667
1706
  ${nextLine}
1668
1707
  ` : `${nextLine}
1669
1708
  `, "utf8");
1670
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
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(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
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 ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1735
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
1736
- let changed = false;
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
- const serverPid = run.serverPid;
1740
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
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 = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
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 ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
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 {
@@ -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 model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || "openai-codex/gpt-5.5";
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 ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
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 ? { RIG_GITHUB_TOKEN: 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 ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4305
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
4306
- let changed = false;
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
- const serverPid = run.serverPid;
4310
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
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 = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
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) {