@runuai/host 0.9.70 → 0.9.72

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/src/index.ts CHANGED
@@ -19,7 +19,9 @@ import {
19
19
  } from "../lib/agent-cli";
20
20
  import {
21
21
  clearRefresh,
22
+ injectIntoContainer,
22
23
  reconcileTaskGitAuth,
24
+ requestAccessToken,
23
25
  } from "../lib/github-tokens";
24
26
  import {
25
27
  prepareTaskGithubGitCredential,
@@ -35,7 +37,10 @@ import {
35
37
  missingRequiredKeys,
36
38
  parseEnvSchema,
37
39
  } from "../lib/env-schema";
38
- import { appendTranscript as writeTranscript } from "../lib/transcript";
40
+ import {
41
+ appendTranscript as writeTranscript,
42
+ appendTranscriptViaEnvironment,
43
+ } from "../lib/transcript";
39
44
  import { buildTaskDiff } from "../lib/task-diff";
40
45
  import {
41
46
  deleteHostTask,
@@ -448,6 +453,37 @@ export const hostCommands: HostCommands = {
448
453
  runtimeQuarantinedForRecreate = true;
449
454
  }
450
455
  }
456
+ } else if (
457
+ existingTask?.statusMirror === "running" &&
458
+ existingTask.environmentProvider === "machine"
459
+ ) {
460
+ // ADR-121: a machine-backed task revalidates through its own
461
+ // provider — the compose fast path below speaks docker and would
462
+ // misread a machine row. Running machine → persisted success;
463
+ // anything else falls through to provisionTaskEnvironment, whose
464
+ // machine provider owns recovery semantics.
465
+ try {
466
+ const environment = await reconstructPersistedTaskEnvironment(
467
+ existingTask,
468
+ );
469
+ if (environment) {
470
+ const machineStatus = await environment.status();
471
+ if (machineStatus.state === "running") {
472
+ orchestrator.allowChannel(input.task.id);
473
+ return {
474
+ ok: true,
475
+ value: {
476
+ composeProject: existingTask.composeProject ?? "",
477
+ worktreePath: existingTask.worktreePath ?? "",
478
+ },
479
+ };
480
+ }
481
+ }
482
+ } catch (error) {
483
+ console.warn(
484
+ `[machine] task ${input.task.id}: running-row revalidation failed, continuing to provision: ${error instanceof Error ? error.message : String(error)}`,
485
+ );
486
+ }
451
487
  } else if (existingTask?.statusMirror === "running") {
452
488
  if (!existingTask.composeProject || !existingTask.worktreePath) {
453
489
  return {
@@ -813,16 +849,33 @@ export const hostCommands: HostCommands = {
813
849
  // and mark a running task as errored. The reconciler injects + schedules
814
850
  // (or emits a system note on failure) on its own; the agents come up
815
851
  // regardless and the token lands well before the first `gh` call.
816
- void reconcileTaskGitAuth(
817
- input.task.id,
818
- input.task.ownerUserId,
819
- ).catch((err) =>
820
- console.warn(
821
- `[github] task ${input.task.id}: post-start reconciliation failed: ${
822
- err instanceof Error ? err.message : String(err)
823
- }`,
824
- ),
825
- );
852
+ // ADR-121: machine-backed tasks take a direct environment injection —
853
+ // the compose reconciler would probe for an app container, find none,
854
+ // and silently skip. Reconnect re-injection for machines is a
855
+ // follow-up alongside the machine recovery pass.
856
+ if (getHostTask(input.task.id)?.environmentProvider === "machine") {
857
+ void machineTaskGithubAuth(
858
+ input.task.id,
859
+ input.task.ownerUserId,
860
+ ).catch((err) =>
861
+ console.warn(
862
+ `[github] machine task ${input.task.id}: auth injection failed: ${
863
+ err instanceof Error ? err.message : String(err)
864
+ }`,
865
+ ),
866
+ );
867
+ } else {
868
+ void reconcileTaskGitAuth(
869
+ input.task.id,
870
+ input.task.ownerUserId,
871
+ ).catch((err) =>
872
+ console.warn(
873
+ `[github] task ${input.task.id}: post-start reconciliation failed: ${
874
+ err instanceof Error ? err.message : String(err)
875
+ }`,
876
+ ),
877
+ );
878
+ }
826
879
  } else if (result.code === HostErrorCode.HostUnavailable) {
827
880
  // A daemon outage is retryable infrastructure state, not proof that
828
881
  // the task itself failed. Undo the optimistic local `starting` mirror
@@ -904,6 +957,17 @@ export const hostCommands: HostCommands = {
904
957
  }
905
958
  const result = await wrapAgent(ctx, "taskDown", async () => {
906
959
  if (environment === null) return agent.taskDown(input);
960
+ // ADR-121: an ordinary stop of a machine task must keep the machine's
961
+ // disk — the workspace lives THERE, not on a host worktree the way
962
+ // compose teardown preserves it. Stop is the resumable gesture
963
+ // (provision restarts a stopped machine); only orphan GC terminates.
964
+ if (
965
+ !orphanGc &&
966
+ environment.descriptor.locator.provider === "machine"
967
+ ) {
968
+ await environment.stop();
969
+ return taskDownResultForInput(input, { status: "stopped" });
970
+ }
907
971
  return taskDownResultForInput(input, await environment.teardown());
908
972
  });
909
973
  if (result.ok) {
@@ -1265,6 +1329,24 @@ export const hostCommands: HostCommands = {
1265
1329
  async appendTranscript(_ctx, taskId, author, text, targets) {
1266
1330
  // Per-message + high-frequency, so no logCommand (avoid log spam).
1267
1331
  try {
1332
+ // ADR-121: a machine-backed workspace lives in the machine's world —
1333
+ // the append must travel over the environment transport. Container
1334
+ // tasks keep the direct host-FS write (their workspace is a bind
1335
+ // mount and the sync path is cheaper than a docker exec per message).
1336
+ const task = getHostTask(taskId);
1337
+ if (task?.environmentProvider === "machine") {
1338
+ const environment = await reconstructHostTaskEnvironment(taskId);
1339
+ if (environment) {
1340
+ await appendTranscriptViaEnvironment(
1341
+ taskId,
1342
+ environment,
1343
+ author,
1344
+ text,
1345
+ targets,
1346
+ );
1347
+ return ok(undefined);
1348
+ }
1349
+ }
1268
1350
  writeTranscript(taskId, author, text, targets);
1269
1351
  return ok(undefined);
1270
1352
  } catch (err) {
@@ -1277,6 +1359,30 @@ function normalizeChannelSpec(input: ChannelEnsureInput): ChannelEnsureInput {
1277
1359
  return { ...input, workspacePath: "/workspace" };
1278
1360
  }
1279
1361
 
1362
+ /**
1363
+ * ADR-121: gh auth for a machine-backed task, injected over the environment
1364
+ * transport (token via exec stdin — same `gh auth login --with-token` +
1365
+ * `setup-git` gesture as containers, no compose probing). Best-effort like
1366
+ * the compose reconciler: absence of a GitHub connection is a quiet no-op.
1367
+ */
1368
+ async function machineTaskGithubAuth(
1369
+ taskId: string,
1370
+ userId: string,
1371
+ ): Promise<void> {
1372
+ const environment = await reconstructHostTaskEnvironment(taskId);
1373
+ if (!environment) return;
1374
+ const token = await requestAccessToken(userId);
1375
+ if (!token) return;
1376
+ // Machine work is not container work: no docker/apple runtime admission.
1377
+ await injectIntoContainer(
1378
+ taskId,
1379
+ token.accessToken,
1380
+ undefined,
1381
+ () => {},
1382
+ environment,
1383
+ );
1384
+ }
1385
+
1280
1386
  async function reconstructHostTaskEnvironment(
1281
1387
  taskId: string,
1282
1388
  ): Promise<TaskEnvironmentHandle<TaskDownResult> | null> {