@themoltnet/agent-daemon 0.23.0 → 0.25.1

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.
Files changed (3) hide show
  1. package/README.md +7 -8
  2. package/dist/main.js +307 -26
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -179,14 +179,13 @@ This matters for evals in particular. `run_eval` tasks declare their intended
179
179
  workspace shape in `input.execution.workspace`: `none` becomes a
180
180
  `scratch_mount`, `shared_mount` uses the daemon mount, and
181
181
  `dedicated_worktree` uses an isolated checkout. Downstream
182
- `judge_eval_attempt` tasks only resolve against a still-live producer
183
- session/workspace slot. If the producer slot or its local session/workspace
184
- files cannot be resolved, the judge fails with `producer_context_missing`.
185
- When the judge does claim in time, it immediately forks the producer session and copies the
186
- producer workspace into judge-owned scratch state so the running judge no
187
- longer depends on the producer slot after claim time. Repo-specific
188
- `resumeCommands` that should not run in scratch mode must still be guarded
189
- with `when.workspaceMode`.
182
+ `judge_eval_attempt` tasks can hydrate the producer Pi session from durable
183
+ runtime-session storage when producer slot/workspace metadata is available but
184
+ the local session file is unavailable. Workspace copying still depends on
185
+ producer slot/workspace metadata; if the daemon cannot resolve the required
186
+ producer context, the judge fails with `producer_context_missing`.
187
+ Repo-specific `resumeCommands` that should not run in scratch mode must still
188
+ be guarded with `when.workspaceMode`.
190
189
 
191
190
  ### 1. Start the local stack
192
191
 
package/dist/main.js CHANGED
@@ -12,7 +12,7 @@ import { parseArgs, promisify } from "node:util";
12
12
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } from "@themoltnet/agent-runtime";
13
13
  import { createPiTaskExecutor, findMainWorktree } from "@themoltnet/pi-extension";
14
14
  import { execFile, execFileSync } from "node:child_process";
15
- import { accessSync, constants, existsSync, mkdirSync, readdirSync } from "node:fs";
15
+ import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync } from "node:fs";
16
16
  import { MoltNetError, connect } from "@themoltnet/sdk";
17
17
  import { once } from "node:events";
18
18
  import { pino, transport } from "pino";
@@ -22,6 +22,8 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
22
22
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
23
23
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
24
24
  import { AsyncLocalStorage } from "node:async_hooks";
25
+ import { mkdir } from "node:fs/promises";
26
+ import { pipeline } from "node:stream/promises";
25
27
  //#region ../../libs/observability/src/instrumentation.ts
26
28
  /**
27
29
  * Register OTel auto-instrumentation for common Node.js modules.
@@ -4399,6 +4401,16 @@ var RuntimeProfileToolName = String$1({
4399
4401
  maxLength: 128,
4400
4402
  pattern: "^[a-zA-Z0-9._/-]+$"
4401
4403
  });
4404
+ var RuntimeProfileWorkspaceMode = Union([
4405
+ Literal("none"),
4406
+ Literal("shared_mount"),
4407
+ Literal("dedicated_worktree")
4408
+ ]);
4409
+ var RuntimeProfileAllowedWorkspaceModes = _Array_(RuntimeProfileWorkspaceMode, {
4410
+ minItems: 1,
4411
+ maxItems: 3,
4412
+ uniqueItems: true
4413
+ });
4402
4414
  var SandboxResumeCommandWhenSchema = _Object_({ workspaceMode: Optional(_Array_(Union([
4403
4415
  Literal("shared_mount"),
4404
4416
  Literal("dedicated_worktree"),
@@ -4502,6 +4514,14 @@ var RuntimeProfileMaxBatchSize = Integer({
4502
4514
  minimum: 1,
4503
4515
  maximum: 1e3
4504
4516
  });
4517
+ var RuntimeProfileMaxTurns = Integer({
4518
+ minimum: 0,
4519
+ maximum: 1e4
4520
+ });
4521
+ var RuntimeProfileMaxBashTimeouts = Integer({
4522
+ minimum: 0,
4523
+ maximum: 1e3
4524
+ });
4505
4525
  _Object_({
4506
4526
  id: String$1({ format: "uuid" }),
4507
4527
  teamId: String$1({ format: "uuid" }),
@@ -4519,6 +4539,8 @@ _Object_({
4519
4539
  sandbox: RuntimeProfileSandbox,
4520
4540
  sessionStorageMode: Literal("local"),
4521
4541
  workspaceStorageMode: Literal("local"),
4542
+ defaultWorkspaceMode: Union([RuntimeProfileWorkspaceMode, Null()]),
4543
+ allowedWorkspaceModes: RuntimeProfileAllowedWorkspaceModes,
4522
4544
  sessionTtlSec: Integer({
4523
4545
  minimum: 1,
4524
4546
  maximum: 86400
@@ -4530,6 +4552,8 @@ _Object_({
4530
4552
  leaseTtlSec: RuntimeProfileLeaseTtlSec,
4531
4553
  heartbeatIntervalMs: RuntimeProfileHeartbeatIntervalMs,
4532
4554
  maxBatchSize: RuntimeProfileMaxBatchSize,
4555
+ maxTurns: RuntimeProfileMaxTurns,
4556
+ maxBashTimeouts: RuntimeProfileMaxBashTimeouts,
4533
4557
  requiredEnv: _Array_(RuntimeProfileEnvName, { maxItems: 100 }),
4534
4558
  requiredTools: _Array_(RuntimeProfileToolName, { maxItems: 100 }),
4535
4559
  context: _Array_(RuntimeProfileContext, { maxItems: 5 }),
@@ -4547,6 +4571,64 @@ _Object_({
4547
4571
  additionalProperties: false
4548
4572
  });
4549
4573
  //#endregion
4574
+ //#region ../../libs/tasks/src/runtime-sessions.ts
4575
+ var RuntimeSessionKind = Union([
4576
+ Literal("root"),
4577
+ Literal("extend"),
4578
+ Literal("fork")
4579
+ ]);
4580
+ var RuntimeSessionCheckpointKind = Union([Literal("attempt_final")]);
4581
+ _Object_({
4582
+ id: String$1({ format: "uuid" }),
4583
+ teamId: String$1({ format: "uuid" }),
4584
+ taskId: String$1({ format: "uuid" }),
4585
+ attemptN: Integer({ minimum: 1 }),
4586
+ sourceSlotId: Union([String$1({ format: "uuid" }), Null()]),
4587
+ sourceRuntimeProfileId: Union([String$1({ format: "uuid" }), Null()]),
4588
+ sessionKind: RuntimeSessionKind,
4589
+ parentSessionId: Union([String$1({ format: "uuid" }), Null()]),
4590
+ contentType: String$1({
4591
+ minLength: 1,
4592
+ maxLength: 200
4593
+ }),
4594
+ contentEncoding: Union([String$1({
4595
+ minLength: 1,
4596
+ maxLength: 100
4597
+ }), Null()]),
4598
+ sizeBytes: Integer({ minimum: 0 }),
4599
+ sha256: String$1({
4600
+ minLength: 64,
4601
+ maxLength: 64
4602
+ }),
4603
+ storageClass: String$1({
4604
+ minLength: 1,
4605
+ maxLength: 100
4606
+ }),
4607
+ checkpointKind: RuntimeSessionCheckpointKind,
4608
+ uploadedAt: String$1({ format: "date-time" })
4609
+ }, { $id: "RuntimeSession" });
4610
+ _Object_({
4611
+ sourceSlotId: Optional(String$1({ format: "uuid" })),
4612
+ sourceRuntimeProfileId: Optional(String$1({ format: "uuid" })),
4613
+ sessionKind: RuntimeSessionKind,
4614
+ parentSessionId: Optional(String$1({ format: "uuid" }))
4615
+ }, {
4616
+ $id: "UploadRuntimeSessionQuery",
4617
+ additionalProperties: false
4618
+ });
4619
+ String$1({
4620
+ $id: "RuntimeSessionContent",
4621
+ description: "Runtime session content stream.",
4622
+ format: "binary"
4623
+ });
4624
+ _Object_({
4625
+ taskId: String$1({ format: "uuid" }),
4626
+ attemptN: Integer({ minimum: 1 })
4627
+ }, {
4628
+ $id: "RuntimeSessionAttemptParams",
4629
+ additionalProperties: false
4630
+ });
4631
+ //#endregion
4550
4632
  //#region ../../libs/tasks/src/runtime-slots.ts
4551
4633
  var RuntimeWorkspaceKind = Union([
4552
4634
  Literal("origin"),
@@ -9449,9 +9531,9 @@ function slugifySessionComponent(input) {
9449
9531
  }
9450
9532
  //#endregion
9451
9533
  //#region src/lib/task-execution-plan.ts
9452
- function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec) {
9534
+ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec, runtimeProfileWorkspacePolicy = {}) {
9453
9535
  const descriptor = deriveTaskSessionDescriptor(task);
9454
- const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy);
9536
+ const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
9455
9537
  const slotKey = warmSessionTtlSec > 0 ? descriptor.sessionKey : null;
9456
9538
  const workspaceScope = slotKey !== null ? descriptor.policy.workspaceScope : "attempt";
9457
9539
  const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
@@ -9501,14 +9583,40 @@ function resolveTaskWorktreeBranch(task, workspaceMode) {
9501
9583
  }
9502
9584
  return `task/${slugifyAsciiLower(task.taskType, 60) || "task"}-${task.id.slice(0, 8)}`;
9503
9585
  }
9504
- function resolveTaskWorkspaceMode(task, policy) {
9505
- if (!policy.acceptsInputWorkspaceOverride) return policy.workspaceMode;
9506
- switch (typeof task.input.execution?.workspace === "string" ? task.input.execution.workspace : null) {
9507
- case "none": return "scratch_mount";
9508
- case "shared_mount": return "shared_mount";
9509
- case "dedicated_worktree": return "dedicated_worktree";
9510
- default: return policy.workspaceMode;
9511
- }
9586
+ function resolveTaskWorkspaceMode(task, policy, runtimeProfileWorkspacePolicy) {
9587
+ const allowed = resolveAllowedWorkspaceModes(runtimeProfileWorkspacePolicy);
9588
+ const profileDefault = runtimeProfileWorkspacePolicy.defaultWorkspaceMode ?? null;
9589
+ const requestedWorkspace = policy.acceptsInputWorkspaceOverride && typeof task.input.execution?.workspace === "string" ? task.input.execution.workspace : null;
9590
+ if (isRuntimeProfileWorkspaceMode(requestedWorkspace)) {
9591
+ if (allowed.has(requestedWorkspace)) return toDaemonWorkspaceMode(requestedWorkspace);
9592
+ }
9593
+ if (profileDefault && allowed.has(profileDefault)) return toDaemonWorkspaceMode(profileDefault);
9594
+ if (allowed.has(policy.workspaceMode)) return policy.workspaceMode;
9595
+ return toDaemonWorkspaceMode(firstAllowedWorkspaceMode(allowed));
9596
+ }
9597
+ var ALL_WORKSPACE_MODES = [
9598
+ "none",
9599
+ "shared_mount",
9600
+ "dedicated_worktree"
9601
+ ];
9602
+ var WORKSPACE_MODE_FALLBACK_ORDER = [
9603
+ "none",
9604
+ "dedicated_worktree",
9605
+ "shared_mount"
9606
+ ];
9607
+ function resolveAllowedWorkspaceModes(policy) {
9608
+ const modes = policy.allowedWorkspaceModes && policy.allowedWorkspaceModes.length > 0 ? policy.allowedWorkspaceModes : ALL_WORKSPACE_MODES;
9609
+ return new Set(modes.filter((mode) => isRuntimeProfileWorkspaceMode(mode)));
9610
+ }
9611
+ function firstAllowedWorkspaceMode(allowed) {
9612
+ for (const mode of WORKSPACE_MODE_FALLBACK_ORDER) if (allowed.has(mode)) return mode;
9613
+ return "none";
9614
+ }
9615
+ function isRuntimeProfileWorkspaceMode(value) {
9616
+ return value === "none" || value === "shared_mount" || value === "dedicated_worktree";
9617
+ }
9618
+ function toDaemonWorkspaceMode(mode) {
9619
+ return mode === "none" ? "scratch_mount" : mode;
9512
9620
  }
9513
9621
  function resolveTaskWorkspaceId(task, executionPlan) {
9514
9622
  if (executionPlan.workspaceScope === "session" && executionPlan.sessionKey !== null) return `session-${encodeURIComponent(executionPlan.sessionKey)}`;
@@ -9524,12 +9632,14 @@ var ProducerContextResolutionError = class extends Error {
9524
9632
  };
9525
9633
  function createExecutionPlanCache(args) {
9526
9634
  const cache = /* @__PURE__ */ new Map();
9635
+ const runtimeSessionStore = args.runtimeSessionStore ?? createNullRuntimeSessionStore();
9527
9636
  return {
9528
9637
  async getOrCreate(claimedTask) {
9529
9638
  const key = buildClaimedTaskKey(claimedTask);
9530
9639
  const existing = cache.get(key);
9531
9640
  if (existing) return existing;
9532
- const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec), args.stateDirs, args.slotRegistry);
9641
+ const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec, args.workspacePolicy), args.stateDirs, args.slotRegistry, runtimeSessionStore);
9642
+ assertPlanAllowedByWorkspacePolicy(plan, args.workspacePolicy);
9533
9643
  cache.set(key, plan);
9534
9644
  return plan;
9535
9645
  },
@@ -9538,13 +9648,45 @@ function createExecutionPlanCache(args) {
9538
9648
  }
9539
9649
  };
9540
9650
  }
9651
+ function createNullRuntimeSessionStore() {
9652
+ return {
9653
+ async findRuntimeSessionByTaskAttempt() {
9654
+ return null;
9655
+ },
9656
+ async hydrateSession() {
9657
+ throw new ProducerContextResolutionError("Cannot hydrate runtime session: no runtime session store configured");
9658
+ },
9659
+ async uploadAttemptFinal() {}
9660
+ };
9661
+ }
9662
+ function assertPlanAllowedByWorkspacePolicy(plan, policy) {
9663
+ const allowed = new Set(policy?.allowedWorkspaceModes && policy.allowedWorkspaceModes.length > 0 ? policy.allowedWorkspaceModes : [
9664
+ "none",
9665
+ "shared_mount",
9666
+ "dedicated_worktree"
9667
+ ]);
9668
+ const effectiveMode = planToRuntimeProfileWorkspaceMode(plan);
9669
+ if (!allowed.has(effectiveMode)) throw new Error(`Runtime profile forbids final workspace mode "${effectiveMode}" for this task`);
9670
+ }
9671
+ function planToRuntimeProfileWorkspaceMode(plan) {
9672
+ if (plan.workspaceMode === "scratch_mount") return "none";
9673
+ if (plan.workspaceMode === "dedicated_worktree" && !plan.worktreeBranch) return "shared_mount";
9674
+ return plan.workspaceMode;
9675
+ }
9541
9676
  function buildClaimedTaskKey(task) {
9542
9677
  return `${task.task.id}:${task.attemptN}`;
9543
9678
  }
9544
- async function resolveWarmSlot(slotRegistry, teamId, sourceTaskId, sourceAttemptN, stateDirs) {
9679
+ async function resolveWarmSlot(slotRegistry, runtimeSessionStore, teamId, sourceTaskId, sourceAttemptN, stateDirs) {
9545
9680
  const producerContext = await slotRegistry.findLatestSlotByTaskAttempt(teamId, sourceTaskId, sourceAttemptN);
9546
9681
  if (!producerContext) return { kind: "missing" };
9547
- const sourceSessionPath = resolveProducerSessionPath(producerContext);
9682
+ const localSessionPath = resolveProducerSessionPath(producerContext);
9683
+ const remoteSession = localSessionPath ? null : await runtimeSessionStore.findRuntimeSessionByTaskAttempt(teamId, sourceTaskId, sourceAttemptN);
9684
+ const sourceSessionPath = localSessionPath ? localSessionPath : remoteSession ? await runtimeSessionStore.hydrateSession({
9685
+ attemptN: sourceAttemptN,
9686
+ destinationDir: `${stateDirs.piSessionsDir}/remote-${sourceTaskId}-attempt-${sourceAttemptN}`,
9687
+ taskId: sourceTaskId,
9688
+ teamId
9689
+ }) : null;
9548
9690
  if (!sourceSessionPath) return { kind: "no-session-path" };
9549
9691
  return {
9550
9692
  kind: "found",
@@ -9553,11 +9695,11 @@ async function resolveWarmSlot(slotRegistry, teamId, sourceTaskId, sourceAttempt
9553
9695
  workspacePath: resolveProducerWorkspaceCopySource(producerContext, stateDirs)
9554
9696
  };
9555
9697
  }
9556
- async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slotRegistry) {
9698
+ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slotRegistry, runtimeSessionStore) {
9557
9699
  if (claimedTask.task.taskType === "freeform") {
9558
9700
  const continueFrom = claimedTask.task.input.continueFrom;
9559
9701
  if (!continueFrom) return basePlan;
9560
- const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
9702
+ const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
9561
9703
  if (resolution.kind === "missing") throw new ProducerContextResolutionError(`Continuation source task ${continueFrom.taskId} attempt ${continueFrom.attemptN} has no live runtime slot on this daemon — claim affinity filter should have prevented this claim`);
9562
9704
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Continuation source attempt ${continueFrom.taskId}/${continueFrom.attemptN} has no persisted Pi session path`);
9563
9705
  const sessionDir = `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
@@ -9594,7 +9736,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
9594
9736
  const targetTaskId = typeof claimedTask.task.input.targetTaskId === "string" ? claimedTask.task.input.targetTaskId : null;
9595
9737
  const targetAttemptN = typeof claimedTask.task.input.targetAttemptN === "number" ? claimedTask.task.input.targetAttemptN : null;
9596
9738
  if (!targetTaskId || !targetAttemptN) throw new ProducerContextResolutionError("judge_eval_attempt is missing targetTaskId/targetAttemptN");
9597
- const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, targetTaskId, targetAttemptN, stateDirs);
9739
+ const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, targetTaskId, targetAttemptN, stateDirs);
9598
9740
  if (resolution.kind === "missing") throw new ProducerContextResolutionError(`No live producer runtime slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
9599
9741
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
9600
9742
  return {
@@ -9798,6 +9940,8 @@ function parseCommonOptions(args, options = {}) {
9798
9940
  leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
9799
9941
  heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
9800
9942
  maxBatchSize: options.runtimeDefaults?.maxBatchSize ?? DEFAULTS.maxBatchSize,
9943
+ maxTurns: options.runtimeDefaults?.maxTurns ?? DEFAULTS.maxTurns,
9944
+ maxBashTimeouts: options.runtimeDefaults?.maxBashTimeouts ?? DEFAULTS.maxBashTimeouts,
9801
9945
  warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
9802
9946
  };
9803
9947
  if (!args.agent) throw new MissingRequiredOptionError("agent");
@@ -9808,8 +9952,8 @@ function parseCommonOptions(args, options = {}) {
9808
9952
  heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
9809
9953
  maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
9810
9954
  flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
9811
- maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
9812
- maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
9955
+ maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", runtimeDefaults.maxTurns),
9956
+ maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", runtimeDefaults.maxBashTimeouts),
9813
9957
  warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", runtimeDefaults.warmSessionTtlSec),
9814
9958
  debug: args.debug === true
9815
9959
  };
@@ -9941,8 +10085,12 @@ async function resolveRuntimeProfile(options) {
9941
10085
  leaseTtlSec: profile.leaseTtlSec,
9942
10086
  heartbeatIntervalMs: profile.heartbeatIntervalMs,
9943
10087
  maxBatchSize: profile.maxBatchSize,
10088
+ maxTurns: profile.maxTurns,
10089
+ maxBashTimeouts: profile.maxBashTimeouts,
9944
10090
  sessionTtlSec: profile.sessionTtlSec,
9945
10091
  workspaceTtlSec: profile.workspaceTtlSec,
10092
+ defaultWorkspaceMode: profile.defaultWorkspaceMode ?? null,
10093
+ allowedWorkspaceModes: profile.allowedWorkspaceModes,
9946
10094
  requiredEnv: profile.requiredEnv,
9947
10095
  requiredTools: profile.requiredTools,
9948
10096
  sandboxConfig: profile.sandbox,
@@ -9998,6 +10146,70 @@ function isExecutable(path) {
9998
10146
  }
9999
10147
  }
10000
10148
  //#endregion
10149
+ //#region src/lib/runtime-sessions.ts
10150
+ function resolveRuntimeSessionKind(claimedTask) {
10151
+ const continueFrom = resolveContinueFrom(claimedTask);
10152
+ if (!continueFrom) return "root";
10153
+ return continueFrom.mode === "fork" ? "fork" : "extend";
10154
+ }
10155
+ async function resolveParentRuntimeSession(runtimeSessionStore, claimedTask) {
10156
+ const continueFrom = resolveContinueFrom(claimedTask);
10157
+ if (!continueFrom) return null;
10158
+ return runtimeSessionStore.findRuntimeSessionByTaskAttempt(claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN);
10159
+ }
10160
+ function applyRuntimeSessionUploadFailure(output, err) {
10161
+ if (output.status !== "completed") return output;
10162
+ return {
10163
+ ...output,
10164
+ contentSignature: void 0,
10165
+ error: {
10166
+ code: "runtime_session_upload_failed",
10167
+ message: "Task completed, but durable runtime session checkpoint upload failed: " + (err instanceof Error ? err.message : String(err)),
10168
+ retryable: true
10169
+ },
10170
+ output: null,
10171
+ outputCid: null,
10172
+ status: "failed"
10173
+ };
10174
+ }
10175
+ function createApiRuntimeSessionStore(args) {
10176
+ const { agent } = args;
10177
+ return {
10178
+ async findRuntimeSessionByTaskAttempt(teamId, taskId, attemptN) {
10179
+ return agent.runtimeSessions.getForAttempt({
10180
+ attemptN,
10181
+ taskId
10182
+ }, { teamId });
10183
+ },
10184
+ async hydrateSession(input) {
10185
+ const downloaded = await agent.runtimeSessions.download({
10186
+ attemptN: input.attemptN,
10187
+ taskId: input.taskId
10188
+ }, { teamId: input.teamId });
10189
+ await mkdir(input.destinationDir, { recursive: true });
10190
+ const sessionPath = join(input.destinationDir, `remote-${input.taskId}-attempt-${input.attemptN}.jsonl`);
10191
+ await pipeline(downloaded, createWriteStream(sessionPath));
10192
+ return sessionPath;
10193
+ },
10194
+ async uploadAttemptFinal(input) {
10195
+ const sessionPath = resolveLatestPiSessionPath(input.sessionDir);
10196
+ if (!sessionPath) throw new Error(`Cannot upload runtime session for ${input.taskId}/${input.attemptN}: no local session file in ${input.sessionDir}`);
10197
+ await agent.runtimeSessions.upload({
10198
+ attemptN: input.attemptN,
10199
+ taskId: input.taskId
10200
+ }, createReadStream(sessionPath), {
10201
+ parentSessionId: input.parentSessionId ?? void 0,
10202
+ sessionKind: input.sessionKind,
10203
+ sourceRuntimeProfileId: input.sourceRuntimeProfileId ?? void 0,
10204
+ sourceSlotId: input.sourceSlotId ?? void 0
10205
+ }, { teamId: input.teamId });
10206
+ }
10207
+ };
10208
+ }
10209
+ function resolveContinueFrom(claimedTask) {
10210
+ return claimedTask.task.input.continueFrom;
10211
+ }
10212
+ //#endregion
10001
10213
  //#region src/lib/runtime-slots.ts
10002
10214
  function createApiRuntimeSlotStore(args) {
10003
10215
  const { agent } = args;
@@ -10039,7 +10251,11 @@ function createApiRuntimeSlotStore(args) {
10039
10251
  }, { teamId });
10040
10252
  if (!resolved) return null;
10041
10253
  return {
10042
- slot: { expiresAtMs: resolved.slot.expiresAtMs },
10254
+ slot: {
10255
+ expiresAtMs: resolved.slot.expiresAtMs,
10256
+ id: resolved.slot.id,
10257
+ runtimeProfileId: resolved.slot.runtimeProfileId
10258
+ },
10043
10259
  session: resolved.slot.sessionDir ? {
10044
10260
  sessionDir: resolved.slot.sessionDir,
10045
10261
  sessionPath: resolved.slot.sessionPath
@@ -10194,12 +10410,15 @@ async function runPolling(opts) {
10194
10410
  });
10195
10411
  for (const profile of profiles) validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10196
10412
  const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
10413
+ const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
10197
10414
  const runtimes = /* @__PURE__ */ new Map();
10198
10415
  for (const profile of profiles) {
10199
10416
  const common = parseCommonOptions(values, { runtimeDefaults: {
10200
10417
  leaseTtlSec: profile.leaseTtlSec,
10201
10418
  heartbeatIntervalMs: profile.heartbeatIntervalMs,
10202
10419
  maxBatchSize: profile.maxBatchSize,
10420
+ maxTurns: profile.maxTurns,
10421
+ maxBashTimeouts: profile.maxBashTimeouts,
10203
10422
  warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10204
10423
  } });
10205
10424
  const sandbox = {
@@ -10217,7 +10436,12 @@ async function runPolling(opts) {
10217
10436
  stateDirs,
10218
10437
  slotIdentity,
10219
10438
  warmSessionTtlSec: common.warmSessionTtlSec,
10220
- slotRegistry
10439
+ workspacePolicy: {
10440
+ defaultWorkspaceMode: profile.defaultWorkspaceMode,
10441
+ allowedWorkspaceModes: profile.allowedWorkspaceModes
10442
+ },
10443
+ slotRegistry,
10444
+ runtimeSessionStore
10221
10445
  });
10222
10446
  runtimes.set(profile.id, {
10223
10447
  common,
@@ -10293,9 +10517,13 @@ async function runPolling(opts) {
10293
10517
  sandbox: runtime.sandbox.path,
10294
10518
  leaseTtlSec: runtime.common.leaseTtlSec,
10295
10519
  heartbeatIntervalMs: runtime.common.heartbeatIntervalMs,
10520
+ maxTurns: runtime.common.maxTurns,
10521
+ maxBashTimeouts: runtime.common.maxBashTimeouts,
10296
10522
  warmSessionTtlSec: runtime.common.warmSessionTtlSec,
10297
10523
  profileSessionTtlSec: profile.sessionTtlSec,
10298
- profileWorkspaceTtlSec: profile.workspaceTtlSec
10524
+ profileWorkspaceTtlSec: profile.workspaceTtlSec,
10525
+ defaultWorkspaceMode: profile.defaultWorkspaceMode,
10526
+ allowedWorkspaceModes: profile.allowedWorkspaceModes
10299
10527
  };
10300
10528
  }),
10301
10529
  piAgentDir: piAgentDir.path,
@@ -10322,7 +10550,8 @@ async function runPolling(opts) {
10322
10550
  stopWhenEmpty: opts.stopWhenEmpty,
10323
10551
  debug: baseCommon.debug,
10324
10552
  logger: rootLogger,
10325
- slotRegistry
10553
+ slotRegistry,
10554
+ sessionRegistry: runtimeSessionStore
10326
10555
  }),
10327
10556
  makeReporter: (claimedTask) => {
10328
10557
  const selected = runtimeForClaimedTask(runtimes, claimedTask);
@@ -10337,7 +10566,28 @@ async function runPolling(opts) {
10337
10566
  onTaskFinished: async (output, claimedTask) => {
10338
10567
  const selected = runtimeForClaimedTask(runtimes, claimedTask);
10339
10568
  const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
10340
- return finalizeTask(ctx.agent, output, {
10569
+ let terminalOutput = output;
10570
+ if (resolved?.session?.sessionDir) try {
10571
+ const parentSession = await resolveParentRuntimeSession(runtimeSessionStore, claimedTask);
10572
+ await runtimeSessionStore.uploadAttemptFinal({
10573
+ attemptN: claimedTask.attemptN,
10574
+ parentSessionId: parentSession?.id ?? null,
10575
+ sessionDir: resolved.session.sessionDir,
10576
+ sessionKind: resolveRuntimeSessionKind(claimedTask),
10577
+ sourceRuntimeProfileId: resolved.slot.runtimeProfileId,
10578
+ sourceSlotId: resolved.slot.id,
10579
+ taskId: claimedTask.task.id,
10580
+ teamId: claimedTask.task.teamId
10581
+ });
10582
+ } catch (err) {
10583
+ rootLogger.error({
10584
+ err,
10585
+ taskId: claimedTask.task.id,
10586
+ attemptN: claimedTask.attemptN
10587
+ }, "agent-daemon.runtime_session_upload_failed");
10588
+ terminalOutput = applyRuntimeSessionUploadFailure(output, err);
10589
+ }
10590
+ return finalizeTask(ctx.agent, terminalOutput, {
10341
10591
  task: claimedTask.task,
10342
10592
  slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
10343
10593
  writeCorrelationAnchors: makePrBodyAnchorWriter({
@@ -10589,6 +10839,8 @@ async function runOnce(argv) {
10589
10839
  leaseTtlSec: profile.leaseTtlSec,
10590
10840
  heartbeatIntervalMs: profile.heartbeatIntervalMs,
10591
10841
  maxBatchSize: profile.maxBatchSize,
10842
+ maxTurns: profile.maxTurns,
10843
+ maxBashTimeouts: profile.maxBashTimeouts,
10592
10844
  warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10593
10845
  } });
10594
10846
  const sandbox = {
@@ -10600,6 +10852,7 @@ async function runOnce(argv) {
10600
10852
  activatePiCodingAgentDir(piAgentDir.path);
10601
10853
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
10602
10854
  const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
10855
+ const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
10603
10856
  const slotIdentity = {
10604
10857
  agentName: opts.agent,
10605
10858
  runtimeProfileId: profile.id
@@ -10608,7 +10861,12 @@ async function runOnce(argv) {
10608
10861
  stateDirs,
10609
10862
  slotIdentity,
10610
10863
  warmSessionTtlSec: opts.warmSessionTtlSec,
10611
- slotRegistry
10864
+ workspacePolicy: {
10865
+ defaultWorkspaceMode: profile.defaultWorkspaceMode,
10866
+ allowedWorkspaceModes: profile.allowedWorkspaceModes
10867
+ },
10868
+ slotRegistry,
10869
+ runtimeSessionStore
10612
10870
  });
10613
10871
  const otelShutdown = await initWorkerOtel({
10614
10872
  serviceName: "moltnet.agent-daemon.once",
@@ -10639,6 +10897,8 @@ async function runOnce(argv) {
10639
10897
  taskId,
10640
10898
  leaseTtlSec: opts.leaseTtlSec,
10641
10899
  heartbeatIntervalMs: opts.heartbeatIntervalMs,
10900
+ maxTurns: opts.maxTurns,
10901
+ maxBashTimeouts: opts.maxBashTimeouts,
10642
10902
  warmSessionTtlSec: opts.warmSessionTtlSec,
10643
10903
  profileId: profile.id,
10644
10904
  profileSessionTtlSec: profile.sessionTtlSec,
@@ -10766,7 +11026,28 @@ async function runOnce(argv) {
10766
11026
  }),
10767
11027
  onTaskFinished: async (output, claimedTask) => {
10768
11028
  const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
10769
- return finalizeTask(ctx.agent, output, {
11029
+ let terminalOutput = output;
11030
+ if (resolved?.session?.sessionDir) try {
11031
+ const parentSession = await resolveParentRuntimeSession(runtimeSessionStore, claimedTask);
11032
+ await runtimeSessionStore.uploadAttemptFinal({
11033
+ attemptN: claimedTask.attemptN,
11034
+ parentSessionId: parentSession?.id ?? null,
11035
+ sessionDir: resolved.session.sessionDir,
11036
+ sessionKind: resolveRuntimeSessionKind(claimedTask),
11037
+ sourceRuntimeProfileId: resolved.slot.runtimeProfileId,
11038
+ sourceSlotId: resolved.slot.id,
11039
+ taskId: claimedTask.task.id,
11040
+ teamId: claimedTask.task.teamId
11041
+ });
11042
+ } catch (err) {
11043
+ rootLogger.error({
11044
+ err,
11045
+ taskId: claimedTask.task.id,
11046
+ attemptN: claimedTask.attemptN
11047
+ }, "agent-daemon.runtime_session_upload_failed");
11048
+ terminalOutput = applyRuntimeSessionUploadFailure(output, err);
11049
+ }
11050
+ return finalizeTask(ctx.agent, terminalOutput, {
10770
11051
  task: claimedTask.task,
10771
11052
  slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
10772
11053
  writeCorrelationAnchors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.23.0",
3
+ "version": "0.25.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
@@ -45,9 +45,9 @@
45
45
  "@opentelemetry/semantic-conventions": "^1.39.0",
46
46
  "pino": "^10.3.1",
47
47
  "pino-pretty": "^13.1.3",
48
- "@themoltnet/pi-extension": "0.27.0",
49
- "@themoltnet/sdk": "0.112.0",
50
- "@themoltnet/agent-runtime": "0.29.0"
48
+ "@themoltnet/pi-extension": "0.27.2",
49
+ "@themoltnet/sdk": "0.113.1",
50
+ "@themoltnet/agent-runtime": "0.30.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "tsx": "^4.7.0",
@@ -55,9 +55,9 @@
55
55
  "vite": "^8.0.0",
56
56
  "vitest": "^3.0.0",
57
57
  "@moltnet/bootstrap": "0.1.0",
58
+ "@moltnet/crypto-service": "0.1.0",
58
59
  "@moltnet/observability": "0.1.0",
59
- "@moltnet/tasks": "0.1.0",
60
- "@moltnet/crypto-service": "0.1.0"
60
+ "@moltnet/tasks": "0.1.0"
61
61
  },
62
62
  "nx": {
63
63
  "tags": [