@h-rig/server 0.0.6-alpha.3 → 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 CHANGED
@@ -4383,11 +4383,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
4383
4383
  return;
4384
4384
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
4385
4385
  }
4386
+ async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
4387
+ const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4388
+ if (fromReader)
4389
+ return fromReader;
4390
+ const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
4391
+ if (projected)
4392
+ return projected;
4393
+ if (readTasks !== readWorkspaceTasks) {
4394
+ return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
4395
+ }
4396
+ return null;
4397
+ }
4386
4398
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
4387
4399
  if ("taskId" in input && input.taskId) {
4388
4400
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
4389
4401
  }
4390
- const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
4402
+ const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
4391
4403
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
4392
4404
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
4393
4405
  const runRecord = {
@@ -4459,6 +4471,7 @@ async function startLocalRun(state, runId, options) {
4459
4471
  throw new Error(`Run not found: ${runId}`);
4460
4472
  }
4461
4473
  const startedAt = new Date().toISOString();
4474
+ const resumeMode = options?.resume === true;
4462
4475
  state.runProcesses.set(runId, {
4463
4476
  runId,
4464
4477
  child: null,
@@ -4475,9 +4488,9 @@ async function startLocalRun(state, runId, options) {
4475
4488
  summary: run.title
4476
4489
  });
4477
4490
  appendRunLogEntry(state.projectRoot, runId, {
4478
- id: `log:${runId}:prepare`,
4479
- title: "Rig task run starting",
4480
- detail: run.taskId ?? run.title,
4491
+ id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
4492
+ title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
4493
+ detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
4481
4494
  tone: "info",
4482
4495
  status: "preparing",
4483
4496
  createdAt: startedAt
@@ -4559,6 +4572,10 @@ async function startLocalRun(state, runId, options) {
4559
4572
  RIG_GITHUB_TOKEN: bridgeGitHubToken,
4560
4573
  GITHUB_TOKEN: bridgeGitHubToken,
4561
4574
  GH_TOKEN: bridgeGitHubToken
4575
+ } : {},
4576
+ ...resumeMode ? {
4577
+ RIG_RUN_RESUME: "1",
4578
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4562
4579
  } : {}
4563
4580
  },
4564
4581
  stdio: ["ignore", "pipe", "pipe"]
@@ -4731,7 +4748,7 @@ async function resumeRunRecord(state, input) {
4731
4748
  if (run.status === "completed") {
4732
4749
  throw new Error("Completed runs cannot be resumed.");
4733
4750
  }
4734
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
4751
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4735
4752
  }
4736
4753
  function appendRunMessage(projectRoot, input) {
4737
4754
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4815,34 +4832,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4815
4832
  writeQueueState(projectRoot, next);
4816
4833
  return next;
4817
4834
  }
4818
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4819
- function reconcileOrphanedLocalRuns(state, runs, nowIso2) {
4820
- let changed = false;
4821
- for (const run of runs) {
4835
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
4836
+ function collectResumableLocalRuns(state, runs) {
4837
+ return runs.filter((run) => {
4822
4838
  const status = normalizeString(run.status)?.toLowerCase() ?? "";
4823
- const serverPid = run.serverPid;
4824
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4825
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4826
- continue;
4827
- }
4828
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4829
- patchRunRecord(state.projectRoot, run.runId, {
4830
- status: "failed",
4831
- completedAt: run.completedAt ?? nowIso2,
4832
- updatedAt: nowIso2,
4833
- errorText: detail
4834
- });
4835
- appendRunLogEntry(state.projectRoot, run.runId, {
4836
- id: `log:${run.runId}:stale-local-run`,
4837
- title: "Run marked stale after server restart",
4838
- detail,
4839
- tone: "error",
4840
- status: "failed",
4841
- createdAt: nowIso2
4842
- });
4843
- changed = true;
4844
- }
4845
- return changed;
4839
+ return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
4840
+ });
4846
4841
  }
4847
4842
  async function reconcileScheduler(state, reason) {
4848
4843
  if (state.scheduler.reconciling) {
@@ -4857,7 +4852,20 @@ async function reconcileScheduler(state, reason) {
4857
4852
  const queue = readQueueState(state.projectRoot);
4858
4853
  const tasks = await state.snapshotService.getWorkspaceTasks();
4859
4854
  let runs = listAuthorityRuns4(state.projectRoot);
4860
- let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
4855
+ let changed = false;
4856
+ const resumableRuns = collectResumableLocalRuns(state, runs);
4857
+ for (const run of resumableRuns) {
4858
+ appendRunLogEntry(state.projectRoot, run.runId, {
4859
+ id: `log:${run.runId}:auto-resume:${Date.now()}`,
4860
+ title: "Run auto-resume scheduled",
4861
+ detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
4862
+ tone: "info",
4863
+ status: "preparing",
4864
+ createdAt: new Date().toISOString()
4865
+ });
4866
+ await startLocalRun(state, run.runId, { resume: true });
4867
+ changed = true;
4868
+ }
4861
4869
  if (changed) {
4862
4870
  runs = listAuthorityRuns4(state.projectRoot);
4863
4871
  }
@@ -7254,11 +7262,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
7254
7262
  const runId = normalizeString(body.runId);
7255
7263
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
7256
7264
  const promptOverride = normalizeString(body.promptOverride);
7265
+ const restart = body.restart === true;
7257
7266
  if (!runId) {
7258
7267
  return deps.badRequest("runId is required");
7259
7268
  }
7260
7269
  try {
7261
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
7270
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
7262
7271
  deps.broadcastSnapshotInvalidation(state);
7263
7272
  return deps.jsonResponse({ ok: true, runId, createdAt });
7264
7273
  } catch (error) {
@@ -1167,7 +1167,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
1167
1167
  "needs-attention",
1168
1168
  "stopped"
1169
1169
  ]);
1170
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1170
+ var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
1171
1171
 
1172
1172
  // packages/server/src/server-helpers/ws-router.ts
1173
1173
  import {
@@ -3545,11 +3545,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
3545
3545
  const runId = normalizeString(body.runId);
3546
3546
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
3547
3547
  const promptOverride = normalizeString(body.promptOverride);
3548
+ const restart = body.restart === true;
3548
3549
  if (!runId) {
3549
3550
  return deps.badRequest("runId is required");
3550
3551
  }
3551
3552
  try {
3552
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
3553
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
3553
3554
  deps.broadcastSnapshotInvalidation(state);
3554
3555
  return deps.jsonResponse({ ok: true, runId, createdAt });
3555
3556
  } catch (error) {
@@ -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, {
@@ -1479,6 +1510,10 @@ async function startLocalRun(state, runId, options) {
1479
1510
  RIG_GITHUB_TOKEN: bridgeGitHubToken,
1480
1511
  GITHUB_TOKEN: bridgeGitHubToken,
1481
1512
  GH_TOKEN: bridgeGitHubToken
1513
+ } : {},
1514
+ ...resumeMode ? {
1515
+ RIG_RUN_RESUME: "1",
1516
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
1482
1517
  } : {}
1483
1518
  },
1484
1519
  stdio: ["ignore", "pipe", "pipe"]
@@ -1519,9 +1554,9 @@ async function startLocalRun(state, runId, options) {
1519
1554
  handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
1520
1555
  });
1521
1556
  try {
1522
- const exit = await new Promise((resolve10) => {
1523
- child.once("error", (error) => resolve10({ code: 1, signal: null, error }));
1524
- 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 }));
1525
1560
  });
1526
1561
  if (exit.error) {
1527
1562
  throw new Error(`Failed to start task run: ${exit.error.message}`);
@@ -1621,17 +1656,17 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
1621
1656
  process.env.PROJECT_RIG_ROOT?.trim()
1622
1657
  ].filter((value) => !!value);
1623
1658
  for (const candidate of envCandidates) {
1624
- if (existsSync6(resolve9(candidate, "packages/cli/bin/rig.ts"))) {
1625
- return resolve9(candidate);
1659
+ if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
1660
+ return resolve10(candidate);
1626
1661
  }
1627
1662
  }
1628
- if (existsSync6(resolve9(projectRoot, "packages/cli/bin/rig.ts"))) {
1663
+ if (existsSync7(resolve10(projectRoot, "packages/cli/bin/rig.ts"))) {
1629
1664
  return projectRoot;
1630
1665
  }
1631
1666
  try {
1632
1667
  const monorepoRoot = resolveMonorepoRoot6(projectRoot);
1633
1668
  const outerProjectRoot = dirname5(dirname5(monorepoRoot));
1634
- if (existsSync6(resolve9(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
1669
+ if (existsSync7(resolve10(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
1635
1670
  return outerProjectRoot;
1636
1671
  }
1637
1672
  } catch {}
@@ -1651,15 +1686,15 @@ async function resumeRunRecord(state, input) {
1651
1686
  if (run.status === "completed") {
1652
1687
  throw new Error("Completed runs cannot be resumed.");
1653
1688
  }
1654
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
1689
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
1655
1690
  }
1656
1691
  function appendRunMessage(projectRoot, input) {
1657
1692
  const run = readAuthorityRun8(projectRoot, input.runId);
1658
1693
  if (!run) {
1659
1694
  throw new Error(`Run not found: ${input.runId}`);
1660
1695
  }
1661
- const timelinePath = resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
1662
- 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() : "";
1663
1698
  const nextLine = JSON.stringify({
1664
1699
  id: input.messageId,
1665
1700
  type: "user_message",
@@ -1667,11 +1702,11 @@ function appendRunMessage(projectRoot, input) {
1667
1702
  attachments: input.attachments ?? [],
1668
1703
  createdAt: input.createdAt
1669
1704
  });
1670
- writeFileSync5(timelinePath, existingLines.length > 0 ? `${existingLines}
1705
+ writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
1671
1706
  ${nextLine}
1672
1707
  ` : `${nextLine}
1673
1708
  `, "utf8");
1674
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
1709
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
1675
1710
  ...run,
1676
1711
  updatedAt: input.createdAt
1677
1712
  });
@@ -1697,7 +1732,7 @@ async function stopRunRecord(stateOrProjectRoot, input) {
1697
1732
  completedAt: run.completedAt ?? input.createdAt,
1698
1733
  updatedAt: input.createdAt
1699
1734
  };
1700
- writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
1735
+ writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
1701
1736
  if (run.status !== "completed" && run.taskId) {
1702
1737
  const taskId = run.taskId;
1703
1738
  (async () => {
@@ -1735,34 +1770,12 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
1735
1770
  writeQueueState(projectRoot, next);
1736
1771
  return next;
1737
1772
  }
1738
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
1739
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
1740
- let changed = false;
1741
- 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) => {
1742
1776
  const status = normalizeString(run.status)?.toLowerCase() ?? "";
1743
- const serverPid = run.serverPid;
1744
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
1745
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
1746
- continue;
1747
- }
1748
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
1749
- patchRunRecord(state.projectRoot, run.runId, {
1750
- status: "failed",
1751
- completedAt: run.completedAt ?? nowIso,
1752
- updatedAt: nowIso,
1753
- errorText: detail
1754
- });
1755
- appendRunLogEntry(state.projectRoot, run.runId, {
1756
- id: `log:${run.runId}:stale-local-run`,
1757
- title: "Run marked stale after server restart",
1758
- detail,
1759
- tone: "error",
1760
- status: "failed",
1761
- createdAt: nowIso
1762
- });
1763
- changed = true;
1764
- }
1765
- return changed;
1777
+ return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
1778
+ });
1766
1779
  }
1767
1780
  async function reconcileScheduler(state, reason) {
1768
1781
  if (state.scheduler.reconciling) {
@@ -1777,7 +1790,20 @@ async function reconcileScheduler(state, reason) {
1777
1790
  const queue = readQueueState(state.projectRoot);
1778
1791
  const tasks = await state.snapshotService.getWorkspaceTasks();
1779
1792
  let runs = listAuthorityRuns7(state.projectRoot);
1780
- 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
+ }
1781
1807
  if (changed) {
1782
1808
  runs = listAuthorityRuns7(state.projectRoot);
1783
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 {
@@ -3876,11 +3876,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
3876
3876
  return;
3877
3877
  throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
3878
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
+ }
3879
3891
  async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
3880
3892
  if ("taskId" in input && input.taskId) {
3881
3893
  assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
3882
3894
  }
3883
- 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;
3884
3896
  const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
3885
3897
  const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
3886
3898
  const runRecord = {
@@ -3952,6 +3964,7 @@ async function startLocalRun(state, runId, options) {
3952
3964
  throw new Error(`Run not found: ${runId}`);
3953
3965
  }
3954
3966
  const startedAt = new Date().toISOString();
3967
+ const resumeMode = options?.resume === true;
3955
3968
  state.runProcesses.set(runId, {
3956
3969
  runId,
3957
3970
  child: null,
@@ -3968,9 +3981,9 @@ async function startLocalRun(state, runId, options) {
3968
3981
  summary: run.title
3969
3982
  });
3970
3983
  appendRunLogEntry(state.projectRoot, runId, {
3971
- id: `log:${runId}:prepare`,
3972
- title: "Rig task run starting",
3973
- 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,
3974
3987
  tone: "info",
3975
3988
  status: "preparing",
3976
3989
  createdAt: startedAt
@@ -4052,6 +4065,10 @@ async function startLocalRun(state, runId, options) {
4052
4065
  RIG_GITHUB_TOKEN: bridgeGitHubToken,
4053
4066
  GITHUB_TOKEN: bridgeGitHubToken,
4054
4067
  GH_TOKEN: bridgeGitHubToken
4068
+ } : {},
4069
+ ...resumeMode ? {
4070
+ RIG_RUN_RESUME: "1",
4071
+ RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
4055
4072
  } : {}
4056
4073
  },
4057
4074
  stdio: ["ignore", "pipe", "pipe"]
@@ -4224,7 +4241,7 @@ async function resumeRunRecord(state, input) {
4224
4241
  if (run.status === "completed") {
4225
4242
  throw new Error("Completed runs cannot be resumed.");
4226
4243
  }
4227
- await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
4244
+ await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
4228
4245
  }
4229
4246
  function appendRunMessage(projectRoot, input) {
4230
4247
  const run = readAuthorityRun4(projectRoot, input.runId);
@@ -4308,34 +4325,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
4308
4325
  writeQueueState(projectRoot, next);
4309
4326
  return next;
4310
4327
  }
4311
- var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
4312
- function reconcileOrphanedLocalRuns(state, runs, nowIso) {
4313
- let changed = false;
4314
- 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) => {
4315
4331
  const status = normalizeString(run.status)?.toLowerCase() ?? "";
4316
- const serverPid = run.serverPid;
4317
- const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
4318
- if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
4319
- continue;
4320
- }
4321
- const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
4322
- patchRunRecord(state.projectRoot, run.runId, {
4323
- status: "failed",
4324
- completedAt: run.completedAt ?? nowIso,
4325
- updatedAt: nowIso,
4326
- errorText: detail
4327
- });
4328
- appendRunLogEntry(state.projectRoot, run.runId, {
4329
- id: `log:${run.runId}:stale-local-run`,
4330
- title: "Run marked stale after server restart",
4331
- detail,
4332
- tone: "error",
4333
- status: "failed",
4334
- createdAt: nowIso
4335
- });
4336
- changed = true;
4337
- }
4338
- return changed;
4332
+ return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
4333
+ });
4339
4334
  }
4340
4335
  async function reconcileScheduler(state, reason) {
4341
4336
  if (state.scheduler.reconciling) {
@@ -4350,7 +4345,20 @@ async function reconcileScheduler(state, reason) {
4350
4345
  const queue = readQueueState(state.projectRoot);
4351
4346
  const tasks = await state.snapshotService.getWorkspaceTasks();
4352
4347
  let runs = listAuthorityRuns4(state.projectRoot);
4353
- 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
+ }
4354
4362
  if (changed) {
4355
4363
  runs = listAuthorityRuns4(state.projectRoot);
4356
4364
  }
@@ -6747,11 +6755,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
6747
6755
  const runId = normalizeString(body.runId);
6748
6756
  const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
6749
6757
  const promptOverride = normalizeString(body.promptOverride);
6758
+ const restart = body.restart === true;
6750
6759
  if (!runId) {
6751
6760
  return deps.badRequest("runId is required");
6752
6761
  }
6753
6762
  try {
6754
- await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
6763
+ await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
6755
6764
  deps.broadcastSnapshotInvalidation(state);
6756
6765
  return deps.jsonResponse({ ok: true, runId, createdAt });
6757
6766
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h-rig/server",
3
- "version": "0.0.6-alpha.3",
3
+ "version": "0.0.6-alpha.4",
4
4
  "type": "module",
5
5
  "description": "Rig package",
6
6
  "license": "UNLICENSED",
@@ -25,9 +25,9 @@
25
25
  "rig-server": "./dist/src/server.js"
26
26
  },
27
27
  "dependencies": {
28
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.3",
29
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.3",
30
- "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.3",
28
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.4",
29
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.4",
30
+ "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.4",
31
31
  "effect": "4.0.0-beta.78"
32
32
  }
33
33
  }