@themoltnet/agent-daemon 0.15.0 → 0.17.0

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 +5 -0
  2. package/dist/main.js +888 -136
  3. package/package.json +9 -7
package/dist/main.js CHANGED
@@ -9,10 +9,10 @@ import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";
9
9
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
10
10
  import crypto$1, { createHash } from "crypto";
11
11
  import { createHash as createHash$1 } from "node:crypto";
12
- import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import path, { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
13
  import { parseArgs, parseEnv, promisify } from "node:util";
14
14
  import { execFile, execFileSync } from "node:child_process";
15
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
15
+ import { accessSync, constants, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
16
16
  import { DatabaseSync } from "node:sqlite";
17
17
  import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
18
18
  import { pino, transport } from "pino";
@@ -836,8 +836,8 @@ Reset();
836
836
  *
837
837
  * Idempotent: registration is guarded by `Format.Has(...)`.
838
838
  */
839
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
840
- if (!Has("uuid")) Set$1("uuid", (v) => UUID_RE.test(v));
839
+ var UUID_RE$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
840
+ if (!Has("uuid")) Set$1("uuid", (v) => UUID_RE$1.test(v));
841
841
  if (!Has("date-time")) Set$1("date-time", (v) => !Number.isNaN(Date.parse(v)));
842
842
  //#endregion
843
843
  //#region ../../node_modules/.pnpm/typebox@1.2.8/node_modules/typebox/build/system/memory/metrics.mjs
@@ -4375,6 +4375,170 @@ function validateRubricWeights(rubric) {
4375
4375
  return null;
4376
4376
  }
4377
4377
  //#endregion
4378
+ //#region ../../libs/tasks/src/runtime-profiles.ts
4379
+ var RuntimeProfileName = String$1({
4380
+ minLength: 1,
4381
+ maxLength: 100,
4382
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,99}$"
4383
+ });
4384
+ var RuntimeProfileEnvName = String$1({
4385
+ minLength: 1,
4386
+ maxLength: 128,
4387
+ pattern: "^[A-Z_][A-Z0-9_]*$"
4388
+ });
4389
+ var RuntimeProfileToolName = String$1({
4390
+ minLength: 1,
4391
+ maxLength: 128,
4392
+ pattern: "^[a-zA-Z0-9._/-]+$"
4393
+ });
4394
+ var SandboxResumeCommandWhenSchema = _Object_({ workspaceMode: Optional(_Array_(Union([
4395
+ Literal("shared_mount"),
4396
+ Literal("dedicated_worktree"),
4397
+ Literal("scratch_mount")
4398
+ ]), {
4399
+ minItems: 1,
4400
+ maxItems: 3
4401
+ })) }, { additionalProperties: false });
4402
+ var RuntimeProfileSandboxResumeCommand = Union([String$1({
4403
+ minLength: 1,
4404
+ maxLength: 4096
4405
+ }), _Object_({
4406
+ run: String$1({
4407
+ minLength: 1,
4408
+ maxLength: 4096
4409
+ }),
4410
+ when: Optional(SandboxResumeCommandWhenSchema),
4411
+ retries: Optional(Integer({
4412
+ minimum: 0,
4413
+ maximum: 5
4414
+ })),
4415
+ retryBackoffMs: Optional(Integer({
4416
+ minimum: 0,
4417
+ maximum: 6e4
4418
+ }))
4419
+ }, { additionalProperties: false })]);
4420
+ var RuntimeProfileSandbox = _Object_({
4421
+ snapshot: Optional(_Object_({
4422
+ setupCommands: Optional(_Array_(String$1({
4423
+ minLength: 1,
4424
+ maxLength: 4096
4425
+ }), { maxItems: 20 })),
4426
+ allowedHosts: Optional(_Array_(String$1({
4427
+ minLength: 1,
4428
+ maxLength: 255
4429
+ }), { maxItems: 50 })),
4430
+ overlaySize: Optional(String$1({
4431
+ minLength: 2,
4432
+ maxLength: 16,
4433
+ pattern: "^[0-9]+[KMGTP]?$"
4434
+ }))
4435
+ }, { additionalProperties: false })),
4436
+ resumeCommands: Optional(_Array_(RuntimeProfileSandboxResumeCommand, { maxItems: 30 })),
4437
+ vfs: Optional(_Object_({
4438
+ shadow: Optional(_Array_(String$1({
4439
+ minLength: 1,
4440
+ maxLength: 255
4441
+ }), { maxItems: 100 })),
4442
+ shadowMode: Optional(Union([Literal("deny"), Literal("tmpfs")]))
4443
+ }, { additionalProperties: false })),
4444
+ env: Optional(Record(RuntimeProfileEnvName, String$1({ maxLength: 4096 }))),
4445
+ hostExec: Optional(_Object_({ autoApprove: Optional(Literal(false)) }, { additionalProperties: false })),
4446
+ resources: Optional(_Object_({
4447
+ memory: Optional(String$1({
4448
+ minLength: 2,
4449
+ maxLength: 16,
4450
+ pattern: "^[0-9]+[KMG]?$"
4451
+ })),
4452
+ cpus: Optional(Integer({
4453
+ minimum: 1,
4454
+ maximum: 32
4455
+ }))
4456
+ }, { additionalProperties: false }))
4457
+ }, {
4458
+ $id: "RuntimeProfileSandbox",
4459
+ additionalProperties: false
4460
+ });
4461
+ var RuntimeProfileContext = _Object_({
4462
+ slug: String$1({
4463
+ minLength: 1,
4464
+ maxLength: 64,
4465
+ pattern: "^[a-zA-Z0-9_-]+$"
4466
+ }),
4467
+ binding: Union([
4468
+ Literal("skill"),
4469
+ Literal("context_inline"),
4470
+ Literal("prompt_prefix"),
4471
+ Literal("user_inline")
4472
+ ]),
4473
+ content: String$1({
4474
+ minLength: 1,
4475
+ maxLength: 65536
4476
+ })
4477
+ }, {
4478
+ $id: "RuntimeProfileContext",
4479
+ additionalProperties: false
4480
+ });
4481
+ var RuntimeProfileRef = _Object_({ profileId: String$1({ format: "uuid" }) }, {
4482
+ $id: "RuntimeProfileRef",
4483
+ additionalProperties: false
4484
+ });
4485
+ var RuntimeProfileLeaseTtlSec = Integer({
4486
+ minimum: 1,
4487
+ maximum: 86400
4488
+ });
4489
+ var RuntimeProfileHeartbeatIntervalMs = Integer({
4490
+ minimum: 0,
4491
+ maximum: 36e5
4492
+ });
4493
+ var RuntimeProfileMaxBatchSize = Integer({
4494
+ minimum: 1,
4495
+ maximum: 1e3
4496
+ });
4497
+ _Object_({
4498
+ id: String$1({ format: "uuid" }),
4499
+ teamId: String$1({ format: "uuid" }),
4500
+ name: RuntimeProfileName,
4501
+ description: Union([String$1({ maxLength: 4096 }), Null()]),
4502
+ provider: String$1({
4503
+ minLength: 1,
4504
+ maxLength: 100
4505
+ }),
4506
+ model: String$1({
4507
+ minLength: 1,
4508
+ maxLength: 200
4509
+ }),
4510
+ runtimeKind: Literal("gondolin_pi"),
4511
+ sandbox: RuntimeProfileSandbox,
4512
+ sessionStorageMode: Literal("local"),
4513
+ workspaceStorageMode: Literal("local"),
4514
+ sessionTtlSec: Integer({
4515
+ minimum: 1,
4516
+ maximum: 86400
4517
+ }),
4518
+ workspaceTtlSec: Integer({
4519
+ minimum: 1,
4520
+ maximum: 86400
4521
+ }),
4522
+ leaseTtlSec: RuntimeProfileLeaseTtlSec,
4523
+ heartbeatIntervalMs: RuntimeProfileHeartbeatIntervalMs,
4524
+ maxBatchSize: RuntimeProfileMaxBatchSize,
4525
+ requiredEnv: _Array_(RuntimeProfileEnvName, { maxItems: 100 }),
4526
+ requiredTools: _Array_(RuntimeProfileToolName, { maxItems: 100 }),
4527
+ context: _Array_(RuntimeProfileContext, { maxItems: 5 }),
4528
+ revision: Integer({ minimum: 1 }),
4529
+ definitionCid: String$1({
4530
+ minLength: 1,
4531
+ maxLength: 100
4532
+ }),
4533
+ createdByAgentId: Union([String$1({ format: "uuid" }), Null()]),
4534
+ createdByHumanId: Union([String$1({ format: "uuid" }), Null()]),
4535
+ createdAt: String$1({ format: "date-time" }),
4536
+ updatedAt: String$1({ format: "date-time" })
4537
+ }, {
4538
+ $id: "RuntimeProfile",
4539
+ additionalProperties: false
4540
+ });
4541
+ //#endregion
4378
4542
  //#region ../../libs/tasks/src/success-criteria.ts
4379
4543
  /**
4380
4544
  * SuccessCriteria — proposer-stated acceptance criteria, evaluated in two
@@ -8967,6 +9131,7 @@ var TaskAttemptStatus = Union([
8967
9131
  Literal("completed"),
8968
9132
  Literal("failed"),
8969
9133
  Literal("cancelled"),
9134
+ Literal("aborted"),
8970
9135
  Literal("timed_out")
8971
9136
  ], { $id: "TaskAttemptStatus" });
8972
9137
  var ExecutorTrustLevel = Union([
@@ -8975,14 +9140,6 @@ var ExecutorTrustLevel = Union([
8975
9140
  Literal("releaseVerifiedTool"),
8976
9141
  Literal("sandboxAttested")
8977
9142
  ], { $id: "ExecutorTrustLevel" });
8978
- /** Identifies a (provider, model) daemon pair allowed to claim a task. */
8979
- var ExecutorRef = _Object_({
8980
- provider: String$1({ minLength: 1 }),
8981
- model: String$1({ minLength: 1 })
8982
- }, {
8983
- $id: "ExecutorRef",
8984
- additionalProperties: false
8985
- });
8986
9143
  var OutputKind = Union([Literal("artifact"), Literal("judgment")], { $id: "OutputKind" });
8987
9144
  var TaskMessageKind = Union([
8988
9145
  Literal("text_delta"),
@@ -9127,7 +9284,7 @@ _Object_({
9127
9284
  acceptedAttemptN: Union([Number$1(), Null()]),
9128
9285
  claimCondition: Union([Unsafe(Ref$2("ClaimCondition")), Null()]),
9129
9286
  requiredExecutorTrustLevel: ExecutorTrustLevel,
9130
- allowedExecutors: _Array_(ExecutorRef, { maxItems: 16 }),
9287
+ allowedProfiles: _Array_(RuntimeProfileRef, { maxItems: 16 }),
9131
9288
  status: TaskStatus,
9132
9289
  queuedAt: IsoTimestamp,
9133
9290
  completedAt: Union([IsoTimestamp, Null()]),
@@ -9223,15 +9380,23 @@ _Object_({
9223
9380
  //#region src/lib/help.ts
9224
9381
  var COMMON_REQUIRED_FLAGS = `\
9225
9382
  -a, --agent <name> MoltNet agent identity. Reads credentials
9226
- from <repo-root>/.moltnet/<name>/moltnet.json.
9383
+ from <repo-root>/.moltnet/<name>/moltnet.json.`;
9384
+ var COMMON_MODEL_FLAGS = `\
9227
9385
  -p, --provider <id> LLM provider id (e.g. anthropic, openai-codex).
9228
9386
  -m, --model <id> LLM model id for the provider (e.g.
9229
- claude-sonnet-4-5, gpt-5.3-codex).`;
9387
+ claude-sonnet-4-5, gpt-5.3-codex). Required
9388
+ unless --profile is set.`;
9230
9389
  var COMMON_OPTIONAL_FLAGS = `\
9231
9390
  --sandbox <path> Path to sandbox.json. Default: search up from
9232
9391
  the daemon's CWD until found. The directory
9233
9392
  containing sandbox.json is also used as the
9234
- VM mountPath.
9393
+ VM mountPath. Cannot be used with --profile.
9394
+ --profile <uuid|name> Remote runtime profile. When set, provider,
9395
+ model, and sandbox policy come from the
9396
+ profile; task listing/claiming is restricted
9397
+ to unrestricted tasks plus tasks allowing this
9398
+ profile. requiredEnv/requiredTools are checked
9399
+ before claiming. Name lookup is team-scoped.
9235
9400
  --lease-ttl-sec <n> Sliding liveness window. Silence longer than
9236
9401
  this ends the attempt with lease_expired.
9237
9402
  Default: 300.
@@ -9249,7 +9414,8 @@ var COMMON_OPTIONAL_FLAGS = `\
9249
9414
  --warm-session-ttl-sec <n> Retain resumable daemon slots (Pi sessions +
9250
9415
  reusable worktrees) in local daemon state for
9251
9416
  this many seconds after use. 0 = disable reuse.
9252
- Default: 1800.
9417
+ Default: 1800, or min(profile session/workspace
9418
+ TTL) when --profile is set.
9253
9419
  --debug Verbose logging: also log successful list/claim
9254
9420
  outcomes (candidate counts, claim attempts).`;
9255
9421
  var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
@@ -9272,9 +9438,10 @@ Run \`agent-daemon <command> --help\` for command-specific flags.
9272
9438
 
9273
9439
  Prerequisites (all subcommands):
9274
9440
  - <repo-root>/.moltnet/<agent>/moltnet.json — credentials (see --agent)
9275
- - sandbox.json — Gondolin snapshot config; resolved by searching up
9276
- from CWD, or pass --sandbox <path>. Its containing directory is the
9277
- VM mountPath.
9441
+ - sandbox.json or --profile — local sandbox config is resolved by
9442
+ searching up from CWD, or pass --sandbox <path>. With --profile, the
9443
+ remote runtime profile supplies provider/model/sandbox policy and CWD
9444
+ is used as the VM mountPath.
9278
9445
 
9279
9446
  Registered task types: ${knownTaskTypesList()}`;
9280
9447
  var POLL_HELP = `\
@@ -9287,6 +9454,7 @@ Required:
9287
9454
  --team <uuid> Team whose queue to serve. The daemon must be
9288
9455
  a member of this team (canAccessTeam permit).
9289
9456
  ${COMMON_REQUIRED_FLAGS}
9457
+ ${COMMON_MODEL_FLAGS}
9290
9458
 
9291
9459
  Optional:
9292
9460
  --task-types <csv> Whitelist of task types to claim. Default:
@@ -9317,6 +9485,7 @@ Required:
9317
9485
  -t, --task-id <uuid> Task to claim and execute. Must already be
9318
9486
  in 'queued' status.
9319
9487
  ${COMMON_REQUIRED_FLAGS}
9488
+ ${COMMON_MODEL_FLAGS}
9320
9489
 
9321
9490
  Optional:
9322
9491
  ${COMMON_OPTIONAL_FLAGS}
@@ -9342,6 +9511,7 @@ sleeps and retries forever).
9342
9511
  Required:
9343
9512
  --team <uuid> Team whose queue to drain.
9344
9513
  ${COMMON_REQUIRED_FLAGS}
9514
+ ${COMMON_MODEL_FLAGS}
9345
9515
 
9346
9516
  Optional:
9347
9517
  --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
@@ -24318,6 +24488,9 @@ var ApiTaskReporter = class {
24318
24488
  get cancelReason() {
24319
24489
  return this.observedCancelReason;
24320
24490
  }
24491
+ requestCancel(reason) {
24492
+ this.abortForCancel(reason);
24493
+ }
24321
24494
  async open(ctx) {
24322
24495
  if (this.heartbeatTimer) {
24323
24496
  clearInterval(this.heartbeatTimer);
@@ -24500,13 +24673,15 @@ var ApiTaskReporter = class {
24500
24673
  async sendHeartbeat() {
24501
24674
  const body = this.opts.leaseTtlSec ? { leaseTtlSec: this.opts.leaseTtlSec } : {};
24502
24675
  const response = await this.opts.tasks.heartbeat(this.taskId, this.attemptN, body);
24503
- if (response?.cancelled && !this.cancelController.signal.aborted) {
24504
- this.observedCancelReason = response.cancelReason ?? null;
24505
- this.cancelController.abort(/* @__PURE__ */ new Error(`Task cancelled by proposer${this.observedCancelReason ? `: ${this.observedCancelReason}` : ""}`));
24506
- if (this.heartbeatTimer) {
24507
- clearInterval(this.heartbeatTimer);
24508
- this.heartbeatTimer = null;
24509
- }
24676
+ if (response?.cancelled) this.abortForCancel(response.cancelReason ?? null);
24677
+ }
24678
+ abortForCancel(reason) {
24679
+ if (this.cancelController.signal.aborted) return;
24680
+ this.observedCancelReason = reason;
24681
+ this.cancelController.abort(/* @__PURE__ */ new Error(`Task cancelled${this.observedCancelReason ? `: ${this.observedCancelReason}` : ""}`));
24682
+ if (this.heartbeatTimer) {
24683
+ clearInterval(this.heartbeatTimer);
24684
+ this.heartbeatTimer = null;
24510
24685
  }
24511
24686
  }
24512
24687
  };
@@ -24519,6 +24694,7 @@ var AgentRuntime = class {
24519
24694
  currentTaskId: null
24520
24695
  };
24521
24696
  stopRequested = false;
24697
+ currentReporter = null;
24522
24698
  logger;
24523
24699
  constructor(opts) {
24524
24700
  this.opts = opts;
@@ -24547,6 +24723,7 @@ var AgentRuntime = class {
24547
24723
  });
24548
24724
  taskLogger.info({}, "agent-runtime.task_claimed");
24549
24725
  const reporter = this.opts.makeReporter(claimedTask);
24726
+ this.currentReporter = reporter;
24550
24727
  const taskCtx = Object.keys(claimedTask.traceHeaders).length ? propagation.extract(ROOT_CONTEXT, claimedTask.traceHeaders) : context.active();
24551
24728
  const taskStart = Date.now();
24552
24729
  let output;
@@ -24604,16 +24781,19 @@ var AgentRuntime = class {
24604
24781
  }
24605
24782
  this.status.tasksProcessed += 1;
24606
24783
  this.status.currentTaskId = null;
24784
+ this.currentReporter = null;
24607
24785
  }
24608
24786
  } finally {
24787
+ this.currentReporter = null;
24609
24788
  await this.opts.source.close();
24610
24789
  this.status.state = "stopped";
24611
24790
  }
24612
24791
  return outputs;
24613
24792
  }
24614
24793
  /** Request cooperative shutdown. Safe from signal handlers. */
24615
- stop() {
24794
+ stop(reason) {
24616
24795
  this.stopRequested = true;
24796
+ if (reason !== void 0) this.currentReporter?.requestCancel?.(reason);
24617
24797
  }
24618
24798
  };
24619
24799
  //#endregion
@@ -24625,8 +24805,11 @@ var ApiTaskSource = class {
24625
24805
  }
24626
24806
  async claim() {
24627
24807
  if (this.claimed) return null;
24628
- const { agent, taskId, leaseTtlSec } = this.opts;
24629
- const result = await agent.tasks.claim(taskId, leaseTtlSec ? { leaseTtlSec } : {});
24808
+ const { agent, taskId, leaseTtlSec, profileId } = this.opts;
24809
+ const result = await agent.tasks.claim(taskId, {
24810
+ ...leaseTtlSec ? { leaseTtlSec } : {},
24811
+ ...profileId ? { profileId } : {}
24812
+ });
24630
24813
  this.claimed = true;
24631
24814
  return {
24632
24815
  task: result.task,
@@ -26271,6 +26454,124 @@ var updateRenderedPack = (options) => (options.client ?? client).patch({
26271
26454
  }
26272
26455
  });
26273
26456
  /**
26457
+ * List runtime profiles for the active team context.
26458
+ */
26459
+ var listRuntimeProfiles = (options) => (options?.client ?? client).get({
26460
+ security: [
26461
+ {
26462
+ scheme: "bearer",
26463
+ type: "http"
26464
+ },
26465
+ {
26466
+ name: "X-Moltnet-Session-Token",
26467
+ type: "apiKey"
26468
+ },
26469
+ {
26470
+ in: "cookie",
26471
+ name: "ory_kratos_session",
26472
+ type: "apiKey"
26473
+ }
26474
+ ],
26475
+ url: "/runtime-profiles",
26476
+ ...options
26477
+ });
26478
+ /**
26479
+ * Create a runtime profile for the active team context.
26480
+ */
26481
+ var createRuntimeProfile = (options) => (options?.client ?? client).post({
26482
+ security: [
26483
+ {
26484
+ scheme: "bearer",
26485
+ type: "http"
26486
+ },
26487
+ {
26488
+ name: "X-Moltnet-Session-Token",
26489
+ type: "apiKey"
26490
+ },
26491
+ {
26492
+ in: "cookie",
26493
+ name: "ory_kratos_session",
26494
+ type: "apiKey"
26495
+ }
26496
+ ],
26497
+ url: "/runtime-profiles",
26498
+ ...options,
26499
+ headers: {
26500
+ "Content-Type": "application/json",
26501
+ ...options?.headers
26502
+ }
26503
+ });
26504
+ /**
26505
+ * Delete one runtime profile.
26506
+ */
26507
+ var deleteRuntimeProfile = (options) => (options.client ?? client).delete({
26508
+ security: [
26509
+ {
26510
+ scheme: "bearer",
26511
+ type: "http"
26512
+ },
26513
+ {
26514
+ name: "X-Moltnet-Session-Token",
26515
+ type: "apiKey"
26516
+ },
26517
+ {
26518
+ in: "cookie",
26519
+ name: "ory_kratos_session",
26520
+ type: "apiKey"
26521
+ }
26522
+ ],
26523
+ url: "/runtime-profiles/{profileId}",
26524
+ ...options
26525
+ });
26526
+ /**
26527
+ * Get one runtime profile.
26528
+ */
26529
+ var getRuntimeProfile = (options) => (options.client ?? client).get({
26530
+ security: [
26531
+ {
26532
+ scheme: "bearer",
26533
+ type: "http"
26534
+ },
26535
+ {
26536
+ name: "X-Moltnet-Session-Token",
26537
+ type: "apiKey"
26538
+ },
26539
+ {
26540
+ in: "cookie",
26541
+ name: "ory_kratos_session",
26542
+ type: "apiKey"
26543
+ }
26544
+ ],
26545
+ url: "/runtime-profiles/{profileId}",
26546
+ ...options
26547
+ });
26548
+ /**
26549
+ * Update one runtime profile.
26550
+ */
26551
+ var updateRuntimeProfile = (options) => (options.client ?? client).patch({
26552
+ security: [
26553
+ {
26554
+ scheme: "bearer",
26555
+ type: "http"
26556
+ },
26557
+ {
26558
+ name: "X-Moltnet-Session-Token",
26559
+ type: "apiKey"
26560
+ },
26561
+ {
26562
+ in: "cookie",
26563
+ name: "ory_kratos_session",
26564
+ type: "apiKey"
26565
+ }
26566
+ ],
26567
+ url: "/runtime-profiles/{profileId}",
26568
+ ...options,
26569
+ headers: {
26570
+ "Content-Type": "application/json",
26571
+ ...options.headers
26572
+ }
26573
+ });
26574
+ /**
26274
26575
  * List tasks for a team with optional filters.
26275
26576
  */
26276
26577
  var listTasks = (options) => (options.client ?? client).get({
@@ -26385,6 +26686,32 @@ var listTaskAttempts = (options) => (options.client ?? client).get({
26385
26686
  ...options
26386
26687
  });
26387
26688
  /**
26689
+ * Claimant intentionally abandons this attempt (e.g. daemon shutdown). The attempt becomes aborted and the task requeues for another claim (or fails when retries are exhausted). Does NOT cancel the task.
26690
+ */
26691
+ var abortTaskAttempt = (options) => (options.client ?? client).post({
26692
+ security: [
26693
+ {
26694
+ scheme: "bearer",
26695
+ type: "http"
26696
+ },
26697
+ {
26698
+ name: "X-Moltnet-Session-Token",
26699
+ type: "apiKey"
26700
+ },
26701
+ {
26702
+ in: "cookie",
26703
+ name: "ory_kratos_session",
26704
+ type: "apiKey"
26705
+ }
26706
+ ],
26707
+ url: "/tasks/{id}/attempts/{n}/abort",
26708
+ ...options,
26709
+ headers: {
26710
+ "Content-Type": "application/json",
26711
+ ...options.headers
26712
+ }
26713
+ });
26714
+ /**
26388
26715
  * Mark an attempt as completed with output.
26389
26716
  */
26390
26717
  var completeTask = (options) => (options.client ?? client).post({
@@ -28517,6 +28844,54 @@ function createRecoveryNamespace(context) {
28517
28844
  };
28518
28845
  }
28519
28846
  //#endregion
28847
+ //#region ../../libs/sdk/src/namespaces/runtime-profiles.ts
28848
+ function createRuntimeProfilesNamespace(context) {
28849
+ const { client, auth } = context;
28850
+ return {
28851
+ async list(options) {
28852
+ return unwrapResult(await listRuntimeProfiles({
28853
+ client,
28854
+ auth,
28855
+ headers: teamHeaders(options)
28856
+ }));
28857
+ },
28858
+ async create(body, options) {
28859
+ return unwrapResult(await createRuntimeProfile({
28860
+ client,
28861
+ auth,
28862
+ headers: teamHeaders(options),
28863
+ body
28864
+ }));
28865
+ },
28866
+ async get(profileId) {
28867
+ return unwrapResult(await getRuntimeProfile({
28868
+ client,
28869
+ auth,
28870
+ path: { profileId }
28871
+ }));
28872
+ },
28873
+ async update(profileId, body) {
28874
+ return unwrapResult(await updateRuntimeProfile({
28875
+ client,
28876
+ auth,
28877
+ path: { profileId },
28878
+ body
28879
+ }));
28880
+ },
28881
+ async delete(profileId) {
28882
+ const result = await deleteRuntimeProfile({
28883
+ client,
28884
+ auth,
28885
+ path: { profileId }
28886
+ });
28887
+ if (result.error) unwrapResult(result);
28888
+ }
28889
+ };
28890
+ }
28891
+ function teamHeaders(options) {
28892
+ return options?.teamId ? { "x-moltnet-team-id": options.teamId } : void 0;
28893
+ }
28894
+ //#endregion
28520
28895
  //#region ../../libs/sdk/src/namespaces/signing-requests.ts
28521
28896
  function createSigningRequestsNamespace(context) {
28522
28897
  const { client, auth } = context;
@@ -28637,6 +29012,17 @@ function createTasksNamespace(context) {
28637
29012
  body
28638
29013
  }));
28639
29014
  },
29015
+ async abortAttempt(id, n, body) {
29016
+ return unwrapResult(await abortTaskAttempt({
29017
+ client,
29018
+ auth,
29019
+ path: {
29020
+ id,
29021
+ n
29022
+ },
29023
+ body
29024
+ }));
29025
+ },
28640
29026
  async cancel(id, body) {
28641
29027
  return unwrapResult(await cancelTask({
28642
29028
  client,
@@ -28820,6 +29206,7 @@ function createAgent(options) {
28820
29206
  legreffier: createLegreffierNamespace(context),
28821
29207
  problems: createProblemsNamespace(context),
28822
29208
  teams: createTeamsNamespace(context),
29209
+ runtimeProfiles: createRuntimeProfilesNamespace(context),
28823
29210
  tasks: createTasksNamespace(context),
28824
29211
  client,
28825
29212
  getToken: () => tokenManager.getToken()
@@ -30908,7 +31295,6 @@ var PollingApiTaskSource = class {
30908
31295
  this.minBackoffMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
30909
31296
  this.maxBackoffMs = opts.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS;
30910
31297
  if (this.maxBackoffMs < this.minBackoffMs) throw new Error(`PollingApiTaskSource: maxPollIntervalMs (${this.maxBackoffMs}) must be >= pollIntervalMs (${this.minBackoffMs})`);
30911
- if (Boolean(opts.provider) !== Boolean(opts.model)) throw new Error("PollingApiTaskSource: provider and model must be set together");
30912
31298
  this.listLimit = opts.listLimit ?? DEFAULT_LIST_LIMIT;
30913
31299
  this.currentBackoffMs = this.minBackoffMs;
30914
31300
  this.logger = (opts.logger ?? pino({ name: "polling-api-source" })).child({ teamId: opts.teamId });
@@ -30951,10 +31337,7 @@ var PollingApiTaskSource = class {
30951
31337
  teamId: this.opts.teamId,
30952
31338
  status: "queued",
30953
31339
  ...taskTypes ? { taskTypes } : {},
30954
- ...this.opts.provider && this.opts.model ? {
30955
- provider: this.opts.provider,
30956
- model: this.opts.model
30957
- } : {},
31340
+ ...this.opts.profileId ? { profileId: this.opts.profileId } : {},
30958
31341
  ...cursor ? { cursor } : {},
30959
31342
  limit: this.listLimit
30960
31343
  });
@@ -30969,9 +31352,9 @@ var PollingApiTaskSource = class {
30969
31352
  if (this.opts.taskTypes && this.opts.taskTypes.length > 0 && !this.opts.taskTypes.includes(item.taskType)) continue;
30970
31353
  if (this.opts.diaryIds && this.opts.diaryIds.length > 0 && (item.diaryId === null || !this.opts.diaryIds.includes(item.diaryId))) continue;
30971
31354
  if (this.opts.slotRegistry && !await isContinuationClaimableByThisDaemon(item, this.opts.slotRegistry)) continue;
30972
- if (this.opts.provider && this.opts.model) {
30973
- const allowed = item.allowedExecutors ?? [];
30974
- if (allowed.length > 0 && !allowed.some((e) => e.provider === this.opts.provider && e.model === this.opts.model)) continue;
31355
+ if (this.opts.profileId) {
31356
+ const allowed = item.allowedProfiles ?? [];
31357
+ if (allowed.length > 0 && !allowed.some((p) => p.profileId === this.opts.profileId)) continue;
30975
31358
  }
30976
31359
  if (item.status !== "queued") continue;
30977
31360
  seen.add(item.id);
@@ -30994,7 +31377,10 @@ var PollingApiTaskSource = class {
30994
31377
  for (const task of candidates) {
30995
31378
  if (this.aborted()) return null;
30996
31379
  try {
30997
- const result = await this.opts.agent.tasks.claim(task.id, { leaseTtlSec: this.opts.leaseTtlSec });
31380
+ const result = await this.opts.agent.tasks.claim(task.id, {
31381
+ leaseTtlSec: this.opts.leaseTtlSec,
31382
+ ...this.opts.profileId ? { profileId: this.opts.profileId } : {}
31383
+ });
30998
31384
  if (this.opts.debug) this.logger.debug({
30999
31385
  taskId: result.task.id,
31000
31386
  taskType: result.task.taskType,
@@ -32128,6 +32514,63 @@ function pruneOldSnapshots(maxCached, currentDir) {
32128
32514
  });
32129
32515
  }
32130
32516
  //#endregion
32517
+ //#region ../../libs/pi-extension/src/abort-utils.ts
32518
+ function throwIfAborted(signal, label) {
32519
+ if (!signal?.aborted) return;
32520
+ throw abortError(label, signal);
32521
+ }
32522
+ function abortError(label, signal) {
32523
+ const reason = signal.reason;
32524
+ const suffix = reason instanceof Error ? reason.message : reason === void 0 ? "aborted" : String(reason);
32525
+ const err = /* @__PURE__ */ new Error(`${label} aborted: ${suffix}`);
32526
+ err.name = "AbortError";
32527
+ return err;
32528
+ }
32529
+ function cleanupLateResource(resourcePromise, opts) {
32530
+ resourcePromise.then(async (resource) => {
32531
+ try {
32532
+ await opts.cleanup(resource);
32533
+ } catch (err) {
32534
+ opts.onCleanupError?.(err);
32535
+ }
32536
+ }, () => {});
32537
+ }
32538
+ async function abortableResource(opts) {
32539
+ const { signal } = opts;
32540
+ if (!signal) return opts.promise;
32541
+ throwIfAborted(signal, opts.label);
32542
+ const resourcePromise = Promise.resolve(opts.promise);
32543
+ const abortPromise = new Promise((_, reject) => {
32544
+ const abort = () => {
32545
+ cleanupLateResource(resourcePromise, opts);
32546
+ reject(abortError(opts.label, signal));
32547
+ };
32548
+ signal.addEventListener("abort", abort, { once: true });
32549
+ resourcePromise.then(() => signal.removeEventListener("abort", abort), () => signal.removeEventListener("abort", abort));
32550
+ });
32551
+ return Promise.race([resourcePromise, abortPromise]);
32552
+ }
32553
+ async function delay(ms, signal, label) {
32554
+ if (!signal) {
32555
+ await new Promise((resolve) => {
32556
+ setTimeout(resolve, ms);
32557
+ });
32558
+ return;
32559
+ }
32560
+ throwIfAborted(signal, label);
32561
+ await new Promise((resolve, reject) => {
32562
+ const listener = () => {
32563
+ clearTimeout(timeout);
32564
+ reject(abortError(label, signal));
32565
+ };
32566
+ const timeout = setTimeout(() => {
32567
+ signal.removeEventListener("abort", listener);
32568
+ resolve();
32569
+ }, ms);
32570
+ signal.addEventListener("abort", listener, { once: true });
32571
+ });
32572
+ }
32573
+ //#endregion
32131
32574
  //#region ../../libs/pi-extension/src/vm-manager.ts
32132
32575
  /**
32133
32576
  * Memory-backed VFS mount used by the daemon to inject task-context
@@ -32244,23 +32687,33 @@ var BASE_ALLOWED_HOSTS = [
32244
32687
  * surface immediately rather than fall through to cryptic agent
32245
32688
  * errors later.
32246
32689
  */
32247
- async function vmRun(vm, label, command) {
32690
+ async function vmRun(vm, label, command, signal) {
32248
32691
  const wrapped = `set -eu\nset -o pipefail\n${command}`;
32692
+ throwIfAborted(signal, `resume step "${label}"`);
32249
32693
  const r = await vm.exec([
32250
32694
  "sh",
32251
32695
  "-c",
32252
32696
  wrapped
32253
- ]);
32697
+ ], { signal });
32254
32698
  if (r.exitCode !== 0) {
32255
32699
  const tail = [r.stderr, r.stdout].filter(Boolean).join("\n").slice(-800);
32256
32700
  throw new Error(`resume step "${label}" failed (exit ${r.exitCode}):\n${tail}`);
32257
32701
  }
32258
32702
  }
32703
+ function nonErrorMessage(err) {
32704
+ if (typeof err === "string") return err;
32705
+ try {
32706
+ return JSON.stringify(err) ?? "unknown error";
32707
+ } catch {
32708
+ return "unknown error";
32709
+ }
32710
+ }
32259
32711
  /**
32260
32712
  * Resume a VM from a checkpoint, inject credentials, configure egress +
32261
32713
  * TLS. Returns the managed VM handle.
32262
32714
  */
32263
32715
  async function resumeVm(config) {
32716
+ throwIfAborted(config.signal, "VM resume");
32264
32717
  const mainRepo = findMainWorktree();
32265
32718
  const agentDir = path.join(mainRepo, ".moltnet", config.agentName);
32266
32719
  const guestWorkspace = path.resolve(config.mountPath);
@@ -32304,24 +32757,33 @@ async function resumeVm(config) {
32304
32757
  };
32305
32758
  const resources = config.sandboxConfig?.resources;
32306
32759
  const workspaceMode = config.workspaceMode ?? "shared_mount";
32307
- const vm = await VmCheckpoint.load(config.checkpointPath).resume({
32308
- httpHooks,
32309
- env: vmEnv,
32310
- ...resources?.memory && { memory: resources.memory },
32311
- ...resources?.cpus && { cpus: resources.cpus },
32312
- vfs: { mounts: {
32313
- [guestWorkspace]: workspaceProvider,
32314
- [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
32315
- } }
32760
+ const vm = await abortableResource({
32761
+ promise: VmCheckpoint.load(config.checkpointPath).resume({
32762
+ httpHooks,
32763
+ env: vmEnv,
32764
+ ...resources?.memory && { memory: resources.memory },
32765
+ ...resources?.cpus && { cpus: resources.cpus },
32766
+ vfs: { mounts: {
32767
+ [guestWorkspace]: workspaceProvider,
32768
+ [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
32769
+ } }
32770
+ }),
32771
+ signal: config.signal,
32772
+ label: "VM resume",
32773
+ cleanup: (resumedVm) => resumedVm.close(),
32774
+ onCleanupError: (err) => {
32775
+ const message = err instanceof Error ? err.message : String(err);
32776
+ process.stderr.write(`[vm] aborted resume late vm.close() failed: ${message}\n`);
32777
+ }
32316
32778
  });
32317
32779
  try {
32318
- await vm.exec(`sh -c '
32780
+ await vmRun(vm, "TLS certificates", `
32319
32781
  cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
32320
32782
  update-ca-certificates 2>/dev/null
32321
32783
  cat /etc/gondolin/mitm/ca.crt >> /etc/ssl/certs/ca-certificates.crt
32322
- '`);
32323
- await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
32324
- await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
32784
+ `, config.signal);
32785
+ await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`, config.signal);
32786
+ await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`, config.signal);
32325
32787
  for (const [i, entry] of (config.sandboxConfig?.resumeCommands ?? []).entries()) {
32326
32788
  if (!shouldRunResumeCommand(entry, { workspaceMode })) continue;
32327
32789
  const { run, retries, backoffMs } = typeof entry === "string" ? {
@@ -32336,34 +32798,67 @@ async function resumeVm(config) {
32336
32798
  const label = `resumeCommands[${i}]`;
32337
32799
  let lastErr;
32338
32800
  for (let attempt = 0; attempt <= retries; attempt++) try {
32339
- await vmRun(vm, label, run);
32801
+ await vmRun(vm, label, run, config.signal);
32340
32802
  lastErr = void 0;
32341
32803
  break;
32342
32804
  } catch (err) {
32343
32805
  lastErr = err;
32344
32806
  if (attempt === retries) break;
32345
- await new Promise((resolve) => {
32346
- setTimeout(resolve, (attempt + 1) * backoffMs);
32347
- });
32807
+ await delay((attempt + 1) * backoffMs, config.signal, label);
32348
32808
  }
32349
- if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
32809
+ if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(nonErrorMessage(lastErr));
32350
32810
  }
32351
32811
  const vmSshDir = `${vmAgentDir}/ssh`;
32352
- await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
32353
- if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
32812
+ await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`, { signal: config.signal });
32813
+ if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, {
32814
+ mode: 384,
32815
+ signal: config.signal
32816
+ });
32354
32817
  const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
32355
- await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
32356
- await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
32818
+ await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, {
32819
+ mode: 384,
32820
+ signal: config.signal
32821
+ });
32822
+ await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, {
32823
+ mode: 384,
32824
+ signal: config.signal
32825
+ });
32357
32826
  if (creds.gitconfig) {
32358
32827
  const vmSigningKey = `${vmSshDir}/id_ed25519`;
32359
32828
  const vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
32360
- await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
32829
+ await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, {
32830
+ mode: 420,
32831
+ signal: config.signal
32832
+ });
32361
32833
  }
32362
- if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
32363
- if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
32364
- if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
32365
- if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
32366
- await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
32834
+ if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, {
32835
+ mode: 384,
32836
+ signal: config.signal
32837
+ });
32838
+ if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, {
32839
+ mode: 420,
32840
+ signal: config.signal
32841
+ });
32842
+ if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, {
32843
+ mode: 420,
32844
+ signal: config.signal
32845
+ });
32846
+ if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, {
32847
+ mode: 384,
32848
+ signal: config.signal
32849
+ });
32850
+ await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet", { signal: config.signal });
32851
+ const gitCredHelperPath = `${vmSshDir}/git-credential-moltnet`;
32852
+ const credHelperScript = `#!/bin/sh
32853
+ echo "username=x-access-token"
32854
+ echo "password=$(moltnet github token --credentials ${vmSshDir}/moltnet.json)"
32855
+ `;
32856
+ await vm.fs.writeFile(gitCredHelperPath, credHelperScript, {
32857
+ mode: 493,
32858
+ signal: config.signal
32859
+ });
32860
+ await vmRun(vm, "git credential helper", `git config --global credential.helper ${gitCredHelperPath} && \
32861
+ git config --global url."https://github.com/".insteadOf "git@github.com:"`, config.signal);
32367
32862
  return {
32368
32863
  vm,
32369
32864
  credentials: creds,
@@ -33616,6 +34111,20 @@ async function executePiTask(claimedTask, reporter, opts) {
33616
34111
  retryable: false
33617
34112
  }
33618
34113
  });
34114
+ const makeCancelledOutput = (message) => ({
34115
+ taskId: task.id,
34116
+ attemptN,
34117
+ status: "cancelled",
34118
+ output: null,
34119
+ outputCid: null,
34120
+ usage: finalUsage,
34121
+ durationMs: Date.now() - startTime,
34122
+ error: {
34123
+ code: "task_cancelled",
34124
+ message,
34125
+ retryable: false
34126
+ }
34127
+ });
33619
34128
  let onTurnEvent;
33620
34129
  if (opts.makeOnTurnEvent) try {
33621
34130
  onTurnEvent = opts.makeOnTurnEvent(claimedTask);
@@ -33678,10 +34187,15 @@ async function executePiTask(claimedTask, reporter, opts) {
33678
34187
  mountPath,
33679
34188
  workspaceMode: workspace.mode,
33680
34189
  extraAllowedHosts: opts.extraAllowedHosts,
33681
- sandboxConfig
34190
+ sandboxConfig,
34191
+ signal: reporter.cancelSignal
33682
34192
  });
33683
34193
  } catch (err) {
33684
34194
  const message = err instanceof Error ? err.message : String(err);
34195
+ if (reporter.cancelSignal.aborted) {
34196
+ await emitError("vm_resume", message, { cancelled: true });
34197
+ return makeCancelledOutput(reporter.cancelReason ?? "Task cancelled during VM resume.");
34198
+ }
33685
34199
  await emitError("vm_resume", message);
33686
34200
  return makeFailedOutput("vm_resume_failed", message);
33687
34201
  }
@@ -34269,7 +34783,9 @@ function loadConfig() {
34269
34783
  return {
34270
34784
  agentDaemonStateDatabaseUrl: process.env["MOLTNET_AGENT_DAEMON_STATE_DATABASE_URL"] ?? "",
34271
34785
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
34272
- logLevel: process.env["LOG_LEVEL"] ?? ""
34786
+ logLevel: process.env["LOG_LEVEL"] ?? "",
34787
+ profilePrerequisiteEnv: process.env,
34788
+ profilePrerequisitePath: process.env.PATH ?? ""
34273
34789
  };
34274
34790
  }
34275
34791
  //#endregion
@@ -34697,13 +35213,27 @@ function createRootLogger(options) {
34697
35213
  target: "pino-pretty",
34698
35214
  options: { colorize: true }
34699
35215
  }) : null;
35216
+ let transportClosed = prettyTransport === null;
35217
+ let shutdownStarted = false;
35218
+ prettyTransport?.on("close", () => {
35219
+ transportClosed = true;
35220
+ });
35221
+ prettyTransport?.on("error", (err) => {
35222
+ transportClosed = true;
35223
+ process.stderr.write(`[pino] transport error: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35224
+ });
34700
35225
  const logger = pino(options, prettyTransport ?? void 0);
34701
35226
  const shutdown = async () => {
34702
- if (!prettyTransport) return;
34703
- logger.flush();
35227
+ if (!prettyTransport || shutdownStarted || transportClosed) return;
35228
+ shutdownStarted = true;
35229
+ try {
35230
+ logger.flush();
35231
+ } catch (err) {
35232
+ process.stderr.write(`[pino] logger flush failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35233
+ }
34704
35234
  try {
34705
35235
  prettyTransport.end();
34706
- await once(prettyTransport, "close");
35236
+ if (!transportClosed) await once(prettyTransport, "close");
34707
35237
  } catch (err) {
34708
35238
  process.stderr.write(`[pino] transport teardown failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
34709
35239
  }
@@ -34731,22 +35261,29 @@ var MissingRequiredOptionError = class extends Error {
34731
35261
  this.name = "MissingRequiredOptionError";
34732
35262
  }
34733
35263
  };
34734
- function parseCommonOptions(args) {
35264
+ function parseCommonOptions(args, options = {}) {
35265
+ const requireProviderModel = options.requireProviderModel ?? true;
35266
+ const runtimeDefaults = {
35267
+ leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
35268
+ heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
35269
+ maxBatchSize: options.runtimeDefaults?.maxBatchSize ?? DEFAULTS.maxBatchSize,
35270
+ warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
35271
+ };
34735
35272
  if (!args.agent) throw new MissingRequiredOptionError("agent");
34736
- if (!args.provider) throw new MissingRequiredOptionError("provider");
34737
- if (!args.model) throw new MissingRequiredOptionError("model");
35273
+ if (requireProviderModel && !args.provider) throw new MissingRequiredOptionError("provider");
35274
+ if (requireProviderModel && !args.model) throw new MissingRequiredOptionError("model");
34738
35275
  if (!/^[a-zA-Z0-9_-]+$/.test(args.agent)) throw new Error(`Invalid --agent "${args.agent}": must match /^[a-zA-Z0-9_-]+$/`);
34739
35276
  return {
34740
35277
  agent: args.agent,
34741
- provider: args.provider,
34742
- model: args.model,
34743
- leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", DEFAULTS.leaseTtlSec),
34744
- heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", DEFAULTS.heartbeatIntervalMs),
34745
- maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", DEFAULTS.maxBatchSize),
35278
+ ...args.provider ? { provider: args.provider } : {},
35279
+ ...args.model ? { model: args.model } : {},
35280
+ leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", runtimeDefaults.leaseTtlSec),
35281
+ heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
35282
+ maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
34746
35283
  flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
34747
35284
  maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
34748
35285
  maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
34749
- warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", DEFAULTS.warmSessionTtlSec),
35286
+ warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", runtimeDefaults.warmSessionTtlSec),
34750
35287
  debug: args.debug === true
34751
35288
  };
34752
35289
  }
@@ -34837,6 +35374,71 @@ async function initWorkerOtel(options) {
34837
35374
  };
34838
35375
  }
34839
35376
  //#endregion
35377
+ //#region src/lib/runtime-profile.ts
35378
+ var RuntimeProfilePrerequisiteError = class extends Error {
35379
+ constructor(profileName, missingEnv, missingTools) {
35380
+ const parts = [missingEnv.length > 0 ? `missing env: ${missingEnv.join(", ")}` : null, missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : null].filter(Boolean);
35381
+ super(`Runtime profile "${profileName}" prerequisites are not satisfied: ${parts.join("; ")}`);
35382
+ this.profileName = profileName;
35383
+ this.missingEnv = missingEnv;
35384
+ this.missingTools = missingTools;
35385
+ this.name = "RuntimeProfilePrerequisiteError";
35386
+ }
35387
+ };
35388
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35389
+ async function resolveRuntimeProfile(options) {
35390
+ const profile = UUID_RE.test(options.profile) ? await options.agent.runtimeProfiles.get(options.profile) : await resolveProfileByName(options);
35391
+ if (options.teamId && profile.teamId !== options.teamId) throw new Error(`Runtime profile "${options.profile}" belongs to team ${profile.teamId}, not ${options.teamId}.`);
35392
+ return {
35393
+ id: profile.id,
35394
+ name: profile.name,
35395
+ teamId: profile.teamId,
35396
+ provider: profile.provider,
35397
+ model: profile.model,
35398
+ leaseTtlSec: profile.leaseTtlSec,
35399
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35400
+ maxBatchSize: profile.maxBatchSize,
35401
+ sessionTtlSec: profile.sessionTtlSec,
35402
+ workspaceTtlSec: profile.workspaceTtlSec,
35403
+ requiredEnv: profile.requiredEnv,
35404
+ requiredTools: profile.requiredTools,
35405
+ sandboxConfig: profile.sandbox,
35406
+ mountPath: resolve(options.cwd),
35407
+ source: `runtime-profile:${profile.id}`
35408
+ };
35409
+ }
35410
+ function validateRuntimeProfilePrerequisites(profile, env, pathValue) {
35411
+ const missingEnv = profile.requiredEnv.filter((name) => !env[name]);
35412
+ const missingTools = profile.requiredTools.filter((tool) => !isExecutableOnPath(tool, pathValue));
35413
+ if (missingEnv.length > 0 || missingTools.length > 0) throw new RuntimeProfilePrerequisiteError(profile.name, missingEnv, missingTools);
35414
+ }
35415
+ function resolveProfileWarmSessionTtlSec(profile) {
35416
+ return Math.min(profile.sessionTtlSec, profile.workspaceTtlSec);
35417
+ }
35418
+ async function resolveProfileByName(options) {
35419
+ if (!options.teamId) throw new Error(`Runtime profile name "${options.profile}" requires --team. Use a profile UUID when running without a team-scoped list.`);
35420
+ const matches = (await options.agent.runtimeProfiles.list({ teamId: options.teamId })).items.filter((item) => item.name === options.profile);
35421
+ if (matches.length === 0) throw new Error(`Runtime profile "${options.profile}" was not found in team ${options.teamId}.`);
35422
+ if (matches.length > 1) throw new Error(`Runtime profile name "${options.profile}" is ambiguous in team ${options.teamId}. Use the profile UUID instead.`);
35423
+ return matches[0];
35424
+ }
35425
+ function isExecutableOnPath(tool, pathValue) {
35426
+ if (tool.includes("/")) return isExecutable(isAbsolute(tool) ? tool : resolve(process.cwd(), tool));
35427
+ for (const dir of (pathValue ?? "").split(delimiter)) {
35428
+ if (!dir) continue;
35429
+ if (isExecutable(resolve(dir, tool))) return true;
35430
+ }
35431
+ return false;
35432
+ }
35433
+ function isExecutable(path) {
35434
+ try {
35435
+ accessSync(path, constants.X_OK);
35436
+ return true;
35437
+ } catch {
35438
+ return false;
35439
+ }
35440
+ }
35441
+ //#endregion
34840
35442
  //#region src/lib/sandbox.ts
34841
35443
  function resolveSandbox(startDir, explicitPath) {
34842
35444
  const path = explicitPath ? isAbsolute(explicitPath) ? explicitPath : resolve(startDir, explicitPath) : findUp(startDir, "sandbox.json");
@@ -34865,6 +35467,38 @@ function findUp(startDir, filename) {
34865
35467
  }
34866
35468
  }
34867
35469
  //#endregion
35470
+ //#region src/lib/shutdown-signal.ts
35471
+ function installShutdownSignalHandlers(opts) {
35472
+ const proc = opts.proc ?? process;
35473
+ let drainingSignal = null;
35474
+ const onSignal = (signal) => {
35475
+ if (drainingSignal) {
35476
+ proc.stderr.write(`[agent-daemon] ${signal} received while already draining from ${drainingSignal}; waiting for cleanup.\n`);
35477
+ proc.exitCode = signalExitCode(signal);
35478
+ return;
35479
+ }
35480
+ drainingSignal = signal;
35481
+ proc.exitCode = signalExitCode(signal);
35482
+ try {
35483
+ opts.logDrain(signal);
35484
+ } catch (err) {
35485
+ proc.stderr.write(`[agent-daemon] failed to log ${signal}: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35486
+ }
35487
+ opts.drain(signal);
35488
+ };
35489
+ const handleSigint = () => onSignal("SIGINT");
35490
+ const handleSigterm = () => onSignal("SIGTERM");
35491
+ proc.on("SIGINT", handleSigint);
35492
+ proc.on("SIGTERM", handleSigterm);
35493
+ return { dispose: () => {
35494
+ proc.off("SIGINT", handleSigint);
35495
+ proc.off("SIGTERM", handleSigterm);
35496
+ } };
35497
+ }
35498
+ function signalExitCode(signal) {
35499
+ return signal === "SIGINT" ? 130 : 143;
35500
+ }
35501
+ //#endregion
34868
35502
  //#region src/lib/state-dir.ts
34869
35503
  function ensureDaemonStateDirs(mountPath) {
34870
35504
  const rootDir = join(mountPath, ".moltnet", "d");
@@ -34914,7 +35548,8 @@ async function runPolling(opts) {
34914
35548
  "poll-interval-ms": { type: "string" },
34915
35549
  "max-poll-interval-ms": { type: "string" },
34916
35550
  "list-limit": { type: "string" },
34917
- sandbox: { type: "string" }
35551
+ sandbox: { type: "string" },
35552
+ profile: { type: "string" }
34918
35553
  }
34919
35554
  });
34920
35555
  if (!values.team) {
@@ -34934,7 +35569,7 @@ async function runPolling(opts) {
34934
35569
  const diaryIds = parseCsv(values["diary-ids"]);
34935
35570
  let common;
34936
35571
  try {
34937
- common = parseCommonOptions(values);
35572
+ common = parseCommonOptions(values, { requireProviderModel: !values.profile });
34938
35573
  } catch (err) {
34939
35574
  if (err instanceof MissingRequiredOptionError) {
34940
35575
  console.error(`${err.message}\n`);
@@ -34946,15 +35581,45 @@ async function runPolling(opts) {
34946
35581
  const pollIntervalMs = optionalPositiveInt(values["poll-interval-ms"], "poll-interval-ms", 2e3);
34947
35582
  const maxPollIntervalMs = optionalPositiveInt(values["max-poll-interval-ms"], "max-poll-interval-ms", 3e4);
34948
35583
  const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
35584
+ if (values.profile && values.sandbox) {
35585
+ console.error(`[${opts.modeLabel}] Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.`);
35586
+ return 1;
35587
+ }
34949
35588
  if (taskTypes.length === 0) console.error(`[${opts.modeLabel}] --task-types is empty — daemon will accept any registered type. Pass an explicit list to limit scope (e.g. --task-types fulfill_brief).`);
34950
- const sandbox = resolveSandbox(process.cwd(), values.sandbox);
34951
35589
  const cfg = loadConfig();
35590
+ const ctx = await resolveAgentContext(common.agent);
35591
+ const profile = values.profile ? await resolveRuntimeProfile({
35592
+ agent: ctx.agent,
35593
+ profile: values.profile,
35594
+ teamId,
35595
+ cwd: process.cwd()
35596
+ }) : null;
35597
+ if (profile) {
35598
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
35599
+ common = parseCommonOptions(values, {
35600
+ requireProviderModel: false,
35601
+ runtimeDefaults: {
35602
+ leaseTtlSec: profile.leaseTtlSec,
35603
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35604
+ maxBatchSize: profile.maxBatchSize,
35605
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
35606
+ }
35607
+ });
35608
+ }
35609
+ const provider = profile?.provider ?? common.provider;
35610
+ const model = profile?.model ?? common.model;
35611
+ if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
35612
+ const sandbox = profile ? {
35613
+ config: profile.sandboxConfig,
35614
+ rootDir: profile.mountPath,
35615
+ path: profile.source
35616
+ } : resolveSandbox(process.cwd(), values.sandbox);
34952
35617
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
34953
35618
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
34954
35619
  const slotIdentity = {
34955
35620
  agentName: common.agent,
34956
- provider: common.provider,
34957
- model: common.model
35621
+ provider,
35622
+ model
34958
35623
  };
34959
35624
  const mainRepo = findMainWorktree();
34960
35625
  const executionPlans = createExecutionPlanCache({
@@ -34963,7 +35628,6 @@ async function runPolling(opts) {
34963
35628
  warmSessionTtlSec: common.warmSessionTtlSec,
34964
35629
  slotRegistry
34965
35630
  });
34966
- const ctx = await resolveAgentContext(common.agent);
34967
35631
  const otelShutdown = await initWorkerOtel({
34968
35632
  serviceName: opts.serviceName,
34969
35633
  agentDir: ctx.agentDir,
@@ -34971,8 +35635,9 @@ async function runPolling(opts) {
34971
35635
  resourceAttributes: {
34972
35636
  "moltnet.team.id": teamId,
34973
35637
  "moltnet.agent.name": common.agent,
34974
- "moltnet.llm.provider": common.provider,
34975
- "moltnet.llm.model": common.model
35638
+ "moltnet.llm.provider": provider,
35639
+ "moltnet.llm.model": model,
35640
+ ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
34976
35641
  }
34977
35642
  });
34978
35643
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -34983,48 +35648,82 @@ async function runPolling(opts) {
34983
35648
  mode: opts.modeLabel,
34984
35649
  agent: common.agent,
34985
35650
  teamId,
34986
- provider: common.provider,
34987
- model: common.model
35651
+ provider,
35652
+ model,
35653
+ ...profile ? {
35654
+ daemonProfileId: profile.id,
35655
+ daemonProfileName: profile.name
35656
+ } : {}
34988
35657
  });
34989
35658
  const abort = new AbortController();
34990
35659
  let runtime = null;
34991
- const onSignal = (sig) => {
34992
- rootLogger.warn({ signal: sig }, "agent-daemon.draining");
34993
- abort.abort();
34994
- runtime?.stop();
34995
- };
34996
- process.on("SIGINT", () => onSignal("SIGINT"));
34997
- process.on("SIGTERM", () => onSignal("SIGTERM"));
35660
+ let active = null;
35661
+ const signalHandlers = installShutdownSignalHandlers({
35662
+ logDrain: (signal) => {
35663
+ rootLogger.warn({ signal }, "agent-daemon.draining");
35664
+ },
35665
+ drain: (signal) => {
35666
+ abort.abort();
35667
+ runtime?.stop(`agent-daemon received ${signal}`);
35668
+ if (active === null) return;
35669
+ const { taskId, attemptN } = active;
35670
+ ctx.agent.tasks.abortAttempt(taskId, attemptN, { reason: `runner_${signal.toLowerCase()}` }).catch((err) => {
35671
+ try {
35672
+ rootLogger.warn({
35673
+ err: err instanceof Error ? err.message : String(err),
35674
+ taskId,
35675
+ attemptN
35676
+ }, "agent-daemon.abort_on_signal_failed");
35677
+ } catch {}
35678
+ });
35679
+ }
35680
+ });
34998
35681
  rootLogger.info({
34999
35682
  sandbox: sandbox.path,
35000
35683
  taskTypes: taskTypes.length > 0 ? taskTypes : ["*"],
35001
35684
  diaryIds: diaryIds.length > 0 ? diaryIds : ["*"],
35002
35685
  leaseTtlSec: common.leaseTtlSec,
35003
35686
  heartbeatIntervalMs: common.heartbeatIntervalMs,
35687
+ warmSessionTtlSec: common.warmSessionTtlSec,
35004
35688
  pollIntervalMs,
35005
- maxPollIntervalMs
35689
+ maxPollIntervalMs,
35690
+ ...profile ? {
35691
+ profileId: profile.id,
35692
+ profileSessionTtlSec: profile.sessionTtlSec,
35693
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
35694
+ } : {}
35006
35695
  }, "agent-daemon.starting");
35007
35696
  const outputs = [];
35008
35697
  try {
35009
- const executeTask = createPiTaskExecutor({
35698
+ const rawExecuteTask = createPiTaskExecutor({
35010
35699
  agentName: common.agent,
35011
35700
  mountPath: sandbox.rootDir,
35012
- provider: common.provider,
35013
- model: common.model,
35701
+ provider,
35702
+ model,
35014
35703
  sandboxConfig: sandbox.config,
35015
35704
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
35016
35705
  makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
35017
35706
  maxTurns: common.maxTurns,
35018
35707
  maxBashTimeouts: common.maxBashTimeouts
35019
35708
  });
35709
+ const executeTask = async (claimedTask, reporter) => {
35710
+ active = {
35711
+ taskId: claimedTask.task.id,
35712
+ attemptN: claimedTask.attemptN
35713
+ };
35714
+ try {
35715
+ return await rawExecuteTask(claimedTask, reporter);
35716
+ } finally {
35717
+ active = null;
35718
+ }
35719
+ };
35020
35720
  runtime = new AgentRuntime({
35021
35721
  logger: rootLogger,
35022
35722
  source: new PollingApiTaskSource({
35023
35723
  agent: ctx.agent,
35024
35724
  teamId,
35025
35725
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
35026
- provider: common.provider.toLowerCase(),
35027
- model: common.model.toLowerCase(),
35726
+ ...profile ? { profileId: profile.id } : {},
35028
35727
  diaryIds: diaryIds.length > 0 ? diaryIds : void 0,
35029
35728
  leaseTtlSec: common.leaseTtlSec,
35030
35729
  listLimit,
@@ -35171,6 +35870,7 @@ async function runPolling(opts) {
35171
35870
  rootLogger.info({ processed: drained.length }, "agent-daemon.drained");
35172
35871
  return drained.some((o) => o.status !== "completed") ? 1 : 0;
35173
35872
  } finally {
35873
+ signalHandlers.dispose();
35174
35874
  await slotRegistry.close();
35175
35875
  await otelShutdown();
35176
35876
  await shutdownLogger();
@@ -35215,7 +35915,8 @@ async function runOnce(argv) {
35215
35915
  type: "string",
35216
35916
  short: "t"
35217
35917
  },
35218
- sandbox: { type: "string" }
35918
+ sandbox: { type: "string" },
35919
+ profile: { type: "string" }
35219
35920
  }
35220
35921
  });
35221
35922
  if (!values["task-id"]) {
@@ -35226,7 +35927,7 @@ async function runOnce(argv) {
35226
35927
  const taskId = values["task-id"];
35227
35928
  let opts;
35228
35929
  try {
35229
- opts = parseCommonOptions(values);
35930
+ opts = parseCommonOptions(values, { requireProviderModel: !values.profile });
35230
35931
  } catch (err) {
35231
35932
  if (err instanceof MissingRequiredOptionError) {
35232
35933
  console.error(`${err.message}\n`);
@@ -35235,14 +35936,43 @@ async function runOnce(argv) {
35235
35936
  }
35236
35937
  throw err;
35237
35938
  }
35238
- const sandbox = resolveSandbox(process.cwd(), values.sandbox);
35939
+ if (values.profile && values.sandbox) {
35940
+ console.error("Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.");
35941
+ return 1;
35942
+ }
35239
35943
  const cfg = loadConfig();
35944
+ const ctx = await resolveAgentContext(opts.agent);
35945
+ const profile = values.profile ? await resolveRuntimeProfile({
35946
+ agent: ctx.agent,
35947
+ profile: values.profile,
35948
+ cwd: process.cwd()
35949
+ }) : null;
35950
+ if (profile) {
35951
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
35952
+ opts = parseCommonOptions(values, {
35953
+ requireProviderModel: false,
35954
+ runtimeDefaults: {
35955
+ leaseTtlSec: profile.leaseTtlSec,
35956
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35957
+ maxBatchSize: profile.maxBatchSize,
35958
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
35959
+ }
35960
+ });
35961
+ }
35962
+ const provider = profile?.provider ?? opts.provider;
35963
+ const model = profile?.model ?? opts.model;
35964
+ if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
35965
+ const sandbox = profile ? {
35966
+ config: profile.sandboxConfig,
35967
+ rootDir: profile.mountPath,
35968
+ path: profile.source
35969
+ } : resolveSandbox(process.cwd(), values.sandbox);
35240
35970
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
35241
35971
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
35242
35972
  const slotIdentity = {
35243
35973
  agentName: opts.agent,
35244
- provider: opts.provider,
35245
- model: opts.model
35974
+ provider,
35975
+ model
35246
35976
  };
35247
35977
  const mainRepo = findMainWorktree();
35248
35978
  const executionPlans = createExecutionPlanCache({
@@ -35251,7 +35981,6 @@ async function runOnce(argv) {
35251
35981
  warmSessionTtlSec: opts.warmSessionTtlSec,
35252
35982
  slotRegistry
35253
35983
  });
35254
- const ctx = await resolveAgentContext(opts.agent);
35255
35984
  const otelShutdown = await initWorkerOtel({
35256
35985
  serviceName: "moltnet.agent-daemon.once",
35257
35986
  agentDir: ctx.agentDir,
@@ -35259,8 +35988,9 @@ async function runOnce(argv) {
35259
35988
  resourceAttributes: {
35260
35989
  "moltnet.task.id": taskId,
35261
35990
  "moltnet.agent.name": opts.agent,
35262
- "moltnet.llm.provider": opts.provider,
35263
- "moltnet.llm.model": opts.model
35991
+ "moltnet.llm.provider": provider,
35992
+ "moltnet.llm.model": model,
35993
+ ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
35264
35994
  }
35265
35995
  });
35266
35996
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -35270,39 +36000,57 @@ async function runOnce(argv) {
35270
36000
  const rootLogger = logger.child({
35271
36001
  mode: "once",
35272
36002
  agent: opts.agent,
35273
- provider: opts.provider,
35274
- model: opts.model
36003
+ provider,
36004
+ model,
36005
+ ...profile ? {
36006
+ daemonProfileId: profile.id,
36007
+ daemonProfileName: profile.name
36008
+ } : {}
35275
36009
  });
35276
36010
  rootLogger.info({
35277
36011
  sandbox: sandbox.path,
35278
- taskId
36012
+ taskId,
36013
+ leaseTtlSec: opts.leaseTtlSec,
36014
+ heartbeatIntervalMs: opts.heartbeatIntervalMs,
36015
+ warmSessionTtlSec: opts.warmSessionTtlSec,
36016
+ ...profile ? {
36017
+ profileId: profile.id,
36018
+ profileSessionTtlSec: profile.sessionTtlSec,
36019
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
36020
+ } : {}
35279
36021
  }, "agent-daemon.starting");
35280
36022
  let runtime = null;
35281
- const onSignal = (sig) => {
35282
- rootLogger.warn({
35283
- signal: sig,
35284
- taskId
35285
- }, "agent-daemon.draining");
35286
- runtime?.stop();
35287
- ctx.agent.tasks.cancel(taskId, { reason: `runner_${sig.toLowerCase()}` }).catch((err) => {
36023
+ let activeAttemptN = null;
36024
+ const signalHandlers = installShutdownSignalHandlers({
36025
+ logDrain: (signal) => {
35288
36026
  rootLogger.warn({
35289
- err: err instanceof Error ? err.message : String(err),
36027
+ signal,
35290
36028
  taskId
35291
- }, "agent-daemon.cancel_on_signal_failed");
35292
- });
35293
- };
35294
- process.on("SIGINT", () => {
35295
- onSignal("SIGINT");
35296
- });
35297
- process.on("SIGTERM", () => {
35298
- onSignal("SIGTERM");
36029
+ }, "agent-daemon.draining");
36030
+ },
36031
+ drain: (signal) => {
36032
+ runtime?.stop(`agent-daemon received ${signal}`);
36033
+ if (activeAttemptN === null) return;
36034
+ const attemptN = activeAttemptN;
36035
+ ctx.agent.tasks.abortAttempt(taskId, attemptN, { reason: `runner_${signal.toLowerCase()}` }).catch((err) => {
36036
+ try {
36037
+ rootLogger.warn({
36038
+ err: err instanceof Error ? err.message : String(err),
36039
+ taskId,
36040
+ attemptN
36041
+ }, "agent-daemon.abort_on_signal_failed");
36042
+ } catch (logErr) {
36043
+ process.stderr.write(`[agent-daemon] failed to log abort error: ` + (logErr instanceof Error ? logErr.message : String(logErr)) + "\n");
36044
+ }
36045
+ });
36046
+ }
35299
36047
  });
35300
36048
  try {
35301
36049
  const rawExecuteTask = createPiTaskExecutor({
35302
36050
  agentName: opts.agent,
35303
36051
  mountPath: sandbox.rootDir,
35304
- provider: opts.provider,
35305
- model: opts.model,
36052
+ provider,
36053
+ model,
35306
36054
  sandboxConfig: sandbox.config,
35307
36055
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
35308
36056
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
@@ -35363,9 +36111,11 @@ async function runOnce(argv) {
35363
36111
  lastAttemptN: claimedTask.attemptN,
35364
36112
  ttlSec: opts.warmSessionTtlSec
35365
36113
  });
36114
+ activeAttemptN = claimedTask.attemptN;
35366
36115
  try {
35367
36116
  return await rawExecuteTask(claimedTask, reporter);
35368
36117
  } finally {
36118
+ activeAttemptN = null;
35369
36119
  executionPlans.delete(claimedTask);
35370
36120
  if (executionPlan.slotKey) await slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, opts.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
35371
36121
  }
@@ -35379,7 +36129,8 @@ async function runOnce(argv) {
35379
36129
  source: new ApiTaskSource({
35380
36130
  agent: ctx.agent,
35381
36131
  taskId,
35382
- leaseTtlSec: opts.leaseTtlSec
36132
+ leaseTtlSec: opts.leaseTtlSec,
36133
+ ...profile ? { profileId: profile.id } : {}
35383
36134
  }),
35384
36135
  makeReporter: () => new ApiTaskReporter({
35385
36136
  tasks: ctx.agent.tasks,
@@ -35408,6 +36159,7 @@ async function runOnce(argv) {
35408
36159
  console.log(JSON.stringify(output, null, 2));
35409
36160
  return output.status === "completed" ? 0 : 1;
35410
36161
  } finally {
36162
+ signalHandlers.dispose();
35411
36163
  await slotRegistry.close();
35412
36164
  await otelShutdown();
35413
36165
  await shutdownLogger();